[
  {
    "path": ".cirrus.yml",
    "content": "# unittests using the 'latest' runtime python-dependencies\ntask:\n  container:\n    image: $ELECTRUM_IMAGE\n    cpu: 1\n    memory: 2G\n  matrix:\n    - name: \"unittests: py$ELECTRUM_PYTHON_VERSION\"\n      env:\n        ELECTRUM_IMAGE: python:$ELECTRUM_PYTHON_VERSION\n      matrix:\n       - env:\n           ELECTRUM_PYTHON_VERSION: 3.10\n       - env:\n           ELECTRUM_PYTHON_VERSION: 3.11\n       - env:\n           ELECTRUM_PYTHON_VERSION: 3.12\n       - env:\n           ELECTRUM_PYTHON_VERSION: 3.13\n       - env:\n           ELECTRUM_PYTHON_VERSION: 3.14\n       - name: \"unittests: py3.14, debug-mode\"\n         env:\n           ELECTRUM_PYTHON_VERSION: 3.14\n           # enable additional checks:\n           PYTHONASYNCIODEBUG: \"1\"\n           PYTHONDEVMODE: \"1\"\n  pip_cache:\n    folder: ~/.cache/pip\n    fingerprint_script: echo $ELECTRUM_IMAGE && cat $ELECTRUM_REQUIREMENTS_CI && cat $ELECTRUM_REQUIREMENTS\n  tag_script:\n    - git tag\n  libsecp_build_cache:\n    folder: contrib/_saved_secp256k1_build\n    fingerprint_script: sha256sum ./contrib/make_libsecp256k1.sh\n    populate_script:\n      - apt-get update\n      - apt-get -y install automake libtool\n      - ./contrib/make_libsecp256k1.sh\n      - mkdir contrib/_saved_secp256k1_build\n      - cp electrum/libsecp256k1.so.* contrib/_saved_secp256k1_build/\n  install_script:\n    - apt-get update\n    # qml test reqs:\n    - apt-get -y install libgl1 libegl1 libxkbcommon0 libdbus-1-3\n    - pip install -r $ELECTRUM_REQUIREMENTS_CI\n    # electrum itself:\n    - export ELECTRUM_ECC_DONT_COMPILE=1\n    - pip install \".[tests,qml_gui]\"\n  version_script:\n    - python3 --version\n    - pip freeze --all\n  pytest_script:\n    - >\n      coverage run --source=electrum \\\n               \"--omit=electrum/gui/*,electrum/plugins/*,electrum/scripts/*\" \\\n               -m pytest tests -v\n    - coverage report\n  coveralls_script:\n    - if [ ! -z \"$COVERALLS_REPO_TOKEN\" ] && [ \"$ELECTRUM_PYTHON_VERSION\" = \"3.10\" ] ; then coveralls ; fi\n  env:\n    LD_LIBRARY_PATH: contrib/_saved_secp256k1_build/\n    ELECTRUM_REQUIREMENTS_CI: contrib/requirements/requirements-ci.txt\n    ELECTRUM_REQUIREMENTS: contrib/requirements/requirements.txt\n    # following CI_* env vars are set up for coveralls\n    CI_NAME: \"CirrusCI\"\n    CI_BUILD_NUMBER: $CIRRUS_BUILD_ID\n    CI_JOB_ID: $CIRRUS_TASK_ID\n    CI_BUILD_URL: \"https://cirrus-ci.com/task/$CIRRUS_TASK_ID\"\n    CI_BRANCH: $CIRRUS_BRANCH\n    CI_PULL_REQUEST: $CIRRUS_PR\n    # in addition, COVERALLS_REPO_TOKEN is set as an \"override\" in https://cirrus-ci.com/settings/...\n  depends_on:\n    - \"linter: Flake8 Mandatory\"\n\n# unittests using the ~same frozen dependencies that are used in the released binaries\n# note: not using pinned pyqt here, due to \"qml_gui\" extra\ntask:\n  container:\n    image: $ELECTRUM_IMAGE\n    cpu: 1\n    memory: 2G\n  name: \"unittests: py3.10, frozen-deps\"\n  pip_cache:\n    folder: ~/.cache/pip\n    fingerprint_script: echo $ELECTRUM_IMAGE && cat contrib/requirements/requirements*.txt && cat contrib/deterministic-build/requirements*.txt\n  tag_script:\n    - git tag\n  libsecp_build_cache:\n    folder: contrib/_saved_secp256k1_build\n    fingerprint_script: sha256sum ./contrib/make_libsecp256k1.sh\n    populate_script:\n      - apt-get update\n      - apt-get -y install automake libtool\n      - ./contrib/make_libsecp256k1.sh\n      - mkdir contrib/_saved_secp256k1_build\n      - cp electrum/libsecp256k1.so.* contrib/_saved_secp256k1_build/\n  install_script:\n    - apt-get update\n    # qml test reqs:\n    - apt-get -y install libgl1 libegl1 libxkbcommon0 libdbus-1-3\n    - pip install -r contrib/deterministic-build/requirements-build-base.txt\n    - pip install -r contrib/requirements/requirements-ci.txt\n    # electrum itself:\n    - export ELECTRUM_ECC_DONT_COMPILE=1\n    - pip install -r contrib/deterministic-build/requirements.txt -r contrib/deterministic-build/requirements-binaries.txt\n    - pip install \".[tests,qml_gui]\"\n  version_script:\n    - python3 --version\n    - pip freeze --all\n  pytest_script:\n    - pytest tests -v\n  env:\n    ELECTRUM_IMAGE: python:3.10\n    LD_LIBRARY_PATH: contrib/_saved_secp256k1_build/\n  depends_on:\n    - \"linter: Flake8 Mandatory\"\n\ntask:\n  name: \"locale: upload to crowdin\"\n  container:\n    image: $ELECTRUM_IMAGE\n    cpu: 1\n    memory: 1G\n  pip_cache:\n    folder: ~/.cache/pip\n    fingerprint_script: echo Locale && echo $ELECTRUM_IMAGE && cat $ELECTRUM_REQUIREMENTS_CI\n  install_script:\n    - apt-get update\n    - apt-get -y install gettext qt6-l10n-tools\n    - pip install -r $ELECTRUM_REQUIREMENTS_CI\n    - pip install requests\n  submodules_script:\n    - git submodule update --init\n  locale_script:\n    - contrib/locale/push_locale.py\n  env:\n    ELECTRUM_IMAGE: python:3.10\n    ELECTRUM_REQUIREMENTS_CI: contrib/requirements/requirements-ci.txt\n    # in addition, crowdin_api_key is set as an \"override\" in https://cirrus-ci.com/settings/...\n    #   - api key is for crowdin account: \"SomberNight_CI_BOT\"\n    #   - see https://crowdin.com/settings#api-key\n  depends_on:\n    - \"unittests: py3.10\"\n  only_if: $CIRRUS_BRANCH == 'master'\n\ntask:\n  name: \"Regtest functional tests\"\n  compute_engine_instance:\n    image_project: cirrus-images\n    image: family/docker-builder\n    platform: linux\n    cpu: 1\n    memory: 1G\n  pip_cache:\n    folder: ~/.cache/pip\n    fingerprint_script: echo Regtest && echo docker_builder && cat $ELECTRUM_REQUIREMENTS\n  bitcoind_cache:\n    folder: /tmp/bitcoind\n    populate_script: mkdir -p /tmp/bitcoind\n  install_script:\n    - apt-get update\n    - apt-get -y install curl jq bc\n    - python3 -m pip install --user --upgrade pip\n    # install electrum\n    - export ELECTRUM_ECC_DONT_COMPILE=1  # we build manually to make caching it easier\n    - python3 -m pip install .[tests] --ignore-installed  # ignore installed system installed attrs\n    # install e-x some commits after 1.18.0 tag\n    - python3 -m pip install git+https://github.com/spesmilo/electrumx.git@0b260d4345242cc41e316e97d7de10ae472fd172\n    - \"BITCOIND_VERSION=$(curl https://bitcoincore.org/en/download/ | grep -E -i --only-matching 'Latest version: [0-9\\\\.]+' | grep -E --only-matching '[0-9\\\\.]+')\"\n    - BITCOIND_FILENAME=bitcoin-$BITCOIND_VERSION-x86_64-linux-gnu.tar.gz\n    - BITCOIND_PATH=/tmp/bitcoind/$BITCOIND_FILENAME\n    - BITCOIND_URL=https://bitcoincore.org/bin/bitcoin-core-$BITCOIND_VERSION/$BITCOIND_FILENAME\n    - tar -xaf $BITCOIND_PATH || (rm -f /tmp/bitcoind/* && curl --output $BITCOIND_PATH $BITCOIND_URL && tar -xaf $BITCOIND_PATH)\n    - cp -a bitcoin-$BITCOIND_VERSION/* /usr/\n  libsecp_build_cache:\n    folder: contrib/_saved_secp256k1_build\n    fingerprint_script: sha256sum ./contrib/make_libsecp256k1.sh\n    populate_script:\n      - apt-get -y install automake libtool\n      - ./contrib/make_libsecp256k1.sh\n      - mkdir contrib/_saved_secp256k1_build\n      - cp electrum/libsecp256k1.so.* contrib/_saved_secp256k1_build/\n  bitcoind_service_background_script:\n    - tests/regtest/run_bitcoind.sh\n  electrumx_service_background_script:\n    - tests/regtest/run_electrumx.sh\n  # if any test fails, the test will get aborted (--failfast) and the wallet directories will be\n  # available for download in the Cirrus UI\n  regtest_script:\n    - sleep 10s\n    - python3 -m unittest tests/regtest.py --failfast || TEST_EXIT_CODE=$?\n    - tar -czf test_wallets.tar.gz /tmp/alice /tmp/bob /tmp/carol || true\n    - exit ${TEST_EXIT_CODE:-0}\n  on_failure:\n    wallet_artifacts:\n      path: \"test_wallets.tar.gz\"\n  env:\n    LD_LIBRARY_PATH: contrib/_saved_secp256k1_build/\n    ELECTRUM_REQUIREMENTS: contrib/requirements/requirements.txt\n    PIP_BREAK_SYSTEM_PACKAGES: 1\n    # ElectrumX exits with an error without this:\n    ALLOW_ROOT: 1\n  depends_on:\n    - \"linter: Flake8 Mandatory\"\n\ntask:\n  container:\n    image: $ELECTRUM_IMAGE\n    cpu: 1\n    memory: 1G\n  pip_cache:\n    folder: ~/.cache/pip\n    fingerprint_script: echo Flake8 && echo $ELECTRUM_IMAGE && cat $ELECTRUM_REQUIREMENTS\n  install_script:\n    - pip install \"flake8==7.3.0\" \"flake8-bugbear==25.10.21\"\n  flake8_script:\n    - flake8 . --count --select=\"$ELECTRUM_LINTERS\" --ignore=\"$ELECTRUM_LINTERS_IGNORE\" --show-source --statistics --exclude \"*_pb2.py,electrum/_vendor/\"\n  env:\n    ELECTRUM_IMAGE: python:3.10\n    ELECTRUM_REQUIREMENTS: contrib/requirements/requirements.txt\n  matrix:\n    - name: \"linter: Flake8 Mandatory\"\n      env:\n        # list of error codes:\n        # - https://flake8.pycqa.org/en/latest/user/error-codes.html\n        # - https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes\n        # - https://github.com/PyCQA/flake8-bugbear/tree/8c0e7eb04217494d48d0ab093bf5b31db0921989#list-of-warnings\n        ELECTRUM_LINTERS: E9,E101,E129,E273,E274,E703,E71,E722,F5,F6,F7,F8,W191,W29,B,B909\n        ELECTRUM_LINTERS_IGNORE: B007,B009,B010,B036,B042,F541,F841\n    - name: \"linter: Flake8 Non-Mandatory\"\n      env:\n        ELECTRUM_LINTERS: E,F,W,C90,B\n        ELECTRUM_LINTERS_IGNORE: \"\"\n      allow_failures: true\n\ntask:\n  name: \"linter: ban unicode\"\n  container:\n    image: python:3.10\n    cpu: 1\n    memory: 1G\n  main_script:\n    - contrib/ban_unicode.py\n\n# Cron jobs configured in https://cirrus-ci.com/settings/...\n# - job \"nightly\" on branch \"master\" at \"0 30 2 * * ?\"  (every day at 02:30Z)\ntask:\n  name: \"build: Windows\"\n  matrix:\n    - trigger_type: manual\n      only_if: $CIRRUS_CRON == \"\"\n    - trigger_type: automatic\n      only_if: $CIRRUS_CRON == \"nightly\"\n  container:\n    dockerfile: contrib/build-wine/Dockerfile\n    cpu: 1\n    memory: 3G\n  pip_cache:\n    folders:\n      - contrib/build-wine/.cache/win*/wine_pip_cache\n    fingerprint_script:\n      - echo $CIRRUS_TASK_NAME\n      - git ls-files -s contrib/deterministic-build/*.txt\n      - git ls-files -s contrib/build-wine/\n  build2_cache:\n    folders:\n      - contrib/build-wine/.cache/win*/build\n    fingerprint_script:\n      - echo $CIRRUS_TASK_NAME\n      - cat contrib/make_libsecp256k1.sh | sha256sum\n      - cat contrib/make_libusb.sh | sha256sum\n      - cat contrib/make_zbar.sh | sha256sum\n      - git ls-files -s contrib/build-wine/\n  build_script:\n    - cd contrib/build-wine\n    - ./make_win.sh\n  binaries_artifacts:\n    path: \"contrib/build-wine/dist/*\"\n  env:\n    CIRRUS_WORKING_DIR: /opt/wine64/drive_c/electrum\n    CIRRUS_DOCKER_CONTEXT: contrib/build-wine\n  depends_on:\n    - \"unittests: py3.10\"\n\ntask:\n  name: \"build: Android (QML $APK_ARCH)\"\n  matrix:\n    - trigger_type: manual\n      only_if: $CIRRUS_CRON == \"\"\n    - trigger_type: automatic\n      only_if: $CIRRUS_CRON == \"nightly\"\n  timeout_in: 90m\n  container:\n    dockerfile: contrib/android/Dockerfile\n    cpu: 8\n    memory: 24G\n  env:\n    APK_ARCH: arm64-v8a\n  packages_tld_folder_cache:\n    folder: packages\n    fingerprint_script:\n      - echo $CIRRUS_TASK_NAME && cat contrib/deterministic-build/requirements.txt && cat contrib/make_packages.sh\n      - git ls-files -s contrib/android/\n  p4a_cache:\n    folders:\n      - \".buildozer/android/platform/build-$APK_ARCH/packages\"\n      - \".buildozer/android/platform/build-$APK_ARCH/build\"\n    fingerprint_script:\n      # note: should *at least* depend on Dockerfile and p4a_recipes/, but contrib/android/ is simplest\n      - git ls-files -s contrib/android/\n      - echo \"qml $APK_ARCH\"\n  build_script:\n    - ./contrib/android/make_apk.sh qml \"$APK_ARCH\" debug\n  binaries_artifacts:\n    path: \"dist/*\"\n  depends_on:\n    - \"unittests: py3.10\"\n\n## mac build disabled, as Cirrus CI no longer supports Intel-based mac builds\n#task:\n#  name: \"build: macOS\"\n#  macos_instance:\n#    image: catalina-xcode-11.3.1\n#  env:\n#    TARGET_OS: macOS\n#  pip_cache:\n#    folder: ~/Library/Caches/pip\n#    fingerprint_script:\n#      - echo $CIRRUS_TASK_NAME\n#      - git ls-files -s contrib/deterministic-build/*.txt\n#      - git ls-files -s contrib/osx/\n#  build2_cache:\n#    folder: contrib/osx/.cache\n#    fingerprint_script:\n#      - echo $CIRRUS_TASK_NAME\n#      - cat contrib/make_libsecp256k1.sh | shasum -a 256\n#      - cat contrib/make_libusb.sh | shasum -a 256\n#      - cat contrib/make_zbar.sh | shasum -a 256\n#      - git ls-files -s contrib/osx/\n#  install_script:\n#    - git fetch --all --tags\n#  build_script:\n#    - ./contrib/osx/make_osx.sh\n#  sum_script:\n#    - ls -lah dist\n#    - shasum -a 256 dist/*.dmg\n#  binaries_artifacts:\n#    path: \"dist/*\"\n\ntask:\n  name: \"build: AppImage\"\n  matrix:\n    - trigger_type: manual\n      only_if: $CIRRUS_CRON == \"\"\n    - trigger_type: automatic\n      only_if: $CIRRUS_CRON == \"nightly\"\n  compute_engine_instance:\n    image_project: cirrus-images\n    image: family/docker-builder\n    platform: linux\n    cpu: 2\n    memory: 2G\n  pip_cache:\n    folder: contrib/build-linux/appimage/.cache/pip_cache\n    fingerprint_script:\n      - echo $CIRRUS_TASK_NAME\n      - git ls-files -s contrib/deterministic-build/*.txt\n      - git ls-files -s contrib/build-linux/appimage/\n  build2_cache:\n    folder: contrib/build-linux/appimage/.cache/appimage\n    fingerprint_script:\n      - echo $CIRRUS_TASK_NAME\n      - cat contrib/make_libsecp256k1.sh | sha256sum\n      - git ls-files -s contrib/build-linux/appimage/\n  build_script:\n    - ./contrib/build-linux/appimage/build.sh\n  binaries_artifacts:\n    path: \"dist/*\"\n  depends_on:\n    - \"unittests: py3.10\"\n\ntask:\n  container:\n    dockerfile: contrib/build-linux/sdist/Dockerfile\n    cpu: 1\n    memory: 1G\n  pip_cache:\n    folder: ~/.cache/pip\n    fingerprint_script:\n      - echo $CIRRUS_TASK_NAME\n      - git ls-files -s contrib/deterministic-build/*.txt\n      - git ls-files -s contrib/build-linux/sdist/\n  build_script:\n    - ./contrib/build-linux/sdist/make_sdist.sh\n  binaries_artifacts:\n    path: \"dist/*\"\n  matrix:\n    - name: \"build: tarball\"\n    - name: \"build: source-only tarball\"\n      env:\n        OMIT_UNCLEAN_FILES: 1\n  depends_on:\n    - \"unittests: py3.10\"\n\ntask:\n  name: \"check submodules\"\n  container:\n    image: python:3.10\n    cpu: 1\n    memory: 1G\n  fetch_script:\n    - git fetch --all --tags\n  check_script:\n    - ./contrib/deterministic-build/check_submodules.sh\n  only_if: $CIRRUS_TAG != ''\n"
  },
  {
    "path": ".editorconfig",
    "content": "# see https://EditorConfig.org\n\nroot = true\n\n[*]\nindent_style = space\ntrim_trailing_whitespace = true\nend_of_line = lf\ncharset = utf-8\n\n[*.py]\nindent_size = 4\ninsert_final_newline = true\n\n[*.sh]\nindent_size = 4\ninsert_final_newline = true\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform end-of-line normalization (to LF)\n*        text=auto\n\n# These Windows files should have CRLF line endings in checkout\n*.bat    text eol=crlf\n*.ps1    text eol=crlf\n\n# Never perform LF normalization on these files\n*.ico    binary\n*.jar    binary\n*.png    binary\n*.zip    binary\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/01_issue.yml",
    "content": "name: Issue\ndescription: Submit a new issue.\n#labels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## Read this first!\n\n        * This issue tracker is for bug reports and development, not general questions.\n        * There is no private support! Scammers will reply to you here and tell you to go to their external website or contact them privately in email/telegram/whatsapp/etc.\n          They will try to **steal** your coins!\n          Be extremely suspicious of anyone offering help in a non-public way. Scammers will want to chat with you in private as then other people will not get a chance to point out the scam.\n        * Do not post issues about non-**Bitcoin** versions of Electrum.\n\n        ----\n  #- type: checkboxes\n  #  attributes:\n  #    label: Is there an existing issue for this already?\n  #    #description: Please search to see if this issue is already being tracked.\n  #    options:\n  #    - label: I have searched the existing issues\n  #      required: true\n  - type: textarea\n    id: main-text-body\n    attributes:\n      label: Description\n      #description: Tell us what went wrong\n      placeholder: Please search existing issues for duplicates.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n#  - name: Electrum Security Policy\n#    url: https://github.com/spesmilo/electrum/blob/master/SECURITY.md\n#    about: View security policy\n  - name: Community Forum\n    url: https://bitcointalk.org/index.php?board=98.0\n    about: Ask non-development-related questions here\n"
  },
  {
    "path": ".gitignore",
    "content": ".git/\n####-*.patch\n**/*.pyc\n*.swp\nbuild/\ndist/\n*.egg/\nElectrum.egg-info/\n.devlocaltmp/\n*_trial_temp\npackages\nenv/\n/.venv*/\n.buildozer\n.buildozer_*/\nbin/\n.idea\n.mypy_cache\n.vscode\nelectrum_data\n.DS_Store\ncontrib/trigger_website\ncontrib/trigger_binaries\n\n# tests/tox\n.tox/\n.cache/\n.coverage\n.pytest_cache\n\n# build workspaces\ncontrib/build-wine/tmp/\ncontrib/build-wine/build/\ncontrib/build-wine/.cache/\ncontrib/build-wine/dist/\ncontrib/build-wine/signed/\ncontrib/build-linux/appimage/build/\ncontrib/build-linux/appimage/.cache/\ncontrib/osx/.cache/\ncontrib/osx/build-venv/\ncontrib/android/android_debug.keystore\ncontrib/android/.cache/\ncontrib/secp256k1/\ncontrib/zbar/\ncontrib/libusb/\ncontrib/.venv_make_packages/\n\n# shared objects\nelectrum/*.so\nelectrum/*.so.*\nelectrum/*.dll\nelectrum/*.dylib\ncontrib/osx/*.dylib\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"electrum/locale\"]\n    path = electrum/locale\n    url = https://github.com/spesmilo/electrum-locale\n    ignore = dirty\n[submodule \"electrum/plugins/keepkey/keepkeylib\"]\n    path = electrum/plugins/keepkey/keepkeylib\n    url = https://github.com/spesmilo/electrum-keepkeylib.git\n"
  },
  {
    "path": "AUTHORS",
    "content": "ThomasV - Creator and maintainer.\nAnimazing / Tachikoma - Styled the new GUI. Mac version.\nAzelphur - GUI stuff.\nCoblee - Alternate coin support and py2app support.\nDeafboy - Ubuntu packages.\nSoren Stoutner - Debian packages and some Qt GUI layout.\nEagleTM - Bugfixes.\nErebusBat - Mac distribution.\nGenjix - Porting pro-mode functionality to lite-gui and worked on server\nSlush - Work on the server. Designed the original Stratum spec.\nJulian Toash (Tuxavant) - Various fixes to the client.\nrdymac - Website and translations.\nkyuupichan - Miscellaneous.\n"
  },
  {
    "path": "LICENCE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2011-2024 The Electrum developers\nCopyright (c) 2011-2024 Thomas Voegtlin\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENCE RELEASE-NOTES AUTHORS\ninclude README.md\ninclude electrum.desktop\ninclude *.py\ninclude run_electrum\ninclude org.electrum.electrum.metainfo.xml\nrecursive-include packages *.py\nrecursive-include packages cacert.pem\n\ninclude contrib/requirements/requirements*.txt\ninclude contrib/deterministic-build/requirements*.txt\ninclude contrib/*.sh\n\ngraft electrum\ngraft tests\ngraft contrib/udev\n\nexclude electrum/*.so\nexclude electrum/*.so.0\nexclude electrum/*.dll\nexclude electrum/*.dylib\n\nglobal-exclude __pycache__\nglobal-exclude *.py[co~]\nglobal-exclude *.py.orig\nglobal-exclude *.py.rej\nglobal-exclude .git\n\n# We include both source (.po) and compiled (.mo) locale files (if present).\n# When building the \"sourceonly\" tar.gz, the build script explicitly deletes the compiled files.\n# exclude electrum/locale/locale/*/LC_MESSAGES/electrum.mo\n"
  },
  {
    "path": "README.md",
    "content": "# Electrum - Lightweight Bitcoin client\n\n```\nLicence: MIT Licence\nAuthor: Thomas Voegtlin\nLanguage: Python (>= 3.10)\nHomepage: https://electrum.org/\n```\n\n[![Build Status](https://api.cirrus-ci.com/github/spesmilo/electrum.svg?branch=master)](https://cirrus-ci.com/github/spesmilo/electrum)\n[![Test coverage statistics](https://coveralls.io/repos/github/spesmilo/electrum/badge.svg?branch=master)](https://coveralls.io/github/spesmilo/electrum?branch=master)\n[![Help translate Electrum online](https://d322cqt584bo4o.cloudfront.net/electrum/localized.svg)](https://crowdin.com/project/electrum)\n\n\n## Getting started\n\n_(If you've come here looking to simply run Electrum,\n[you may download it here](https://electrum.org/#download).)_\n\nElectrum itself is pure Python, and so are most of the required dependencies,\nbut not everything. The following sections describe how to run from source, but here\nis a TL;DR:\n\n```\n$ sudo apt-get install libsecp256k1-dev\n$ ELECTRUM_ECC_DONT_COMPILE=1 python3 -m pip install --user \".[gui,crypto]\"\n```\n\n### Not pure-python dependencies\n\n#### Qt GUI\n\nIf you want to use the Qt interface, install the Qt dependencies:\n```\n$ sudo apt-get install python3-pyqt6\n```\n\n#### libsecp256k1\n\nFor elliptic curve operations,\n[libsecp256k1](https://github.com/bitcoin-core/secp256k1)\nis a required dependency.\n\nIf you \"pip install\" Electrum, by default libsecp will get compiled locally,\nas part of the `electrum-ecc` dependency. This can be opted-out of,\nby setting the `ELECTRUM_ECC_DONT_COMPILE=1` environment variable.\nFor the compilation to work, besides a C compiler, you need at least:\n```\n$ sudo apt-get install automake libtool\n```\nIf you opt out of the compilation, you need to provide libsecp in another way, e.g.:\n```\n$ sudo apt-get install libsecp256k1-dev\n```\n\n#### cryptography\n\nDue to the need for fast symmetric ciphers,\n[cryptography](https://github.com/pyca/cryptography) is required.\nInstall from your package manager (or from pip):\n```\n$ sudo apt-get install python3-cryptography\n```\n\n#### hardware-wallet support\n\nIf you would like hardware wallet support,\n[see this](https://github.com/spesmilo/electrum-docs/blob/master/hardware-linux.rst).\n\n\n### Running from tar.gz\n\nIf you downloaded the official package (tar.gz), you can run\nElectrum from its root directory without installing it on your\nsystem; all the pure python dependencies are included in the 'packages'\ndirectory. To run Electrum from its root directory, just do:\n```\n$ ./run_electrum\n```\n\nYou can also install Electrum on your system, by running this command:\n```\n$ sudo apt-get install python3-setuptools python3-pip\n$ python3 -m pip install --user .\n```\n\nThis will download and install the Python dependencies used by\nElectrum instead of using the 'packages' directory.\nIt will also place an executable named `electrum` in `~/.local/bin`,\nso make sure that is on your `PATH` variable.\n\n\n### Development version (git clone)\n\n_(For OS-specific instructions, see [here for Windows](contrib/build-wine/README_windows.md),\nand [for macOS](contrib/osx/README_macos.md))_\n\nCheck out the code from GitHub:\n```\n$ git clone https://github.com/spesmilo/electrum.git\n$ cd electrum\n$ git submodule update --init\n```\n\nRun install (this should install dependencies):\n```\n$ python3 -m pip install --user -e .\n```\n\nCreate translations (optional):\n```\n$ sudo apt-get install gettext\n$ ./contrib/locale/build_locale.sh electrum/locale/locale electrum/locale/locale\n```\n\nFinally, to start Electrum:\n```\n$ ./run_electrum\n```\n\n### Run tests\n\nRun unit tests with `pytest`:\n```\n$ pytest tests -v\n```\n(can be parallelized with `-n auto` option, using [`pytest-xdist`](https://github.com/pytest-dev/pytest-xdist) plugin)\n\nTo run a single file, specify it directly like this:\n```\n$ pytest tests/test_bitcoin.py -v\n```\n\n## Creating Binaries\n\n- [Linux (tarball)](contrib/build-linux/sdist/README.md)\n- [Linux (AppImage)](contrib/build-linux/appimage/README.md)\n- [macOS](contrib/osx/README.md)\n- [Windows](contrib/build-wine/README.md)\n- [Android](contrib/android/Readme.md)\n\n\n## Contributing\n\nAny help testing the software, reporting or fixing bugs, reviewing pull requests\nand recent changes, writing tests, or helping with outstanding issues is very welcome.\nImplementing new features, or improving/refactoring the codebase, is of course\nalso welcome, but to avoid wasted effort, especially for larger changes,\nwe encourage discussing these on the issue tracker or IRC first.\n\nBesides [GitHub](https://github.com/spesmilo/electrum),\nmost communication about Electrum development happens on IRC, in the\n`#electrum` channel on Libera Chat. The easiest way to participate on IRC is\nwith the web client, [web.libera.chat](https://web.libera.chat/#electrum).\n\nPlease improve translations on [Crowdin](https://crowdin.com/project/electrum).\n"
  },
  {
    "path": "RELEASE-NOTES",
    "content": "# Release 4.7.1 (Feb 26, 2026)\n * Qt GUI (desktop):\n   - new: changelog website accessible from \"Help\" toolbar menu (#10433)\n   - new: show translation completion percentage in language names (#10479)\n   - new: allow changing font size in console (#10494)\n   - changed: validate Electrum server address input with UI feedback (#10441)\n   - changed: stop showing anchor icon for lightning channels with anchor outputs (3979d70)\n   - fix: broken addresses tab for imported watch-only wallets (#10436)\n * QML GUI & Android:\n   - new: show translation completion percentage in language names (#10479)\n   - changed: validate Electrum server address input with UI feedback (#10441)\n   - fix: handle Java import error causing startup crash on Android 7 and 8 devices (#10484)\n * Onchain / Wallet:\n   - fix: improved fee estimation for replacement transactions (#10453)\n * Database:\n   - fix: handle upgrade failure for users with pending Lightning HTLCs (#10489)\n * Lightning:\n   - changed: send channel_update alongside node_announcement gossip messages (#10475)\n   - fix: improved safety when revealing preimage on-chain (#10442)\n   - fix: don't attempt to fetch gossip from Tor peers without a proxy enabled (#10448)\n   - fix: handle peer sending back our own channel_update (#10493)\n * Dependencies:\n   - changed: bump electrum-ecc (and libsecp256k1) from 0.7.0 to 0.7.1 (#10495)\n * Builds/binaries:\n   - Android:\n     - changed: bump docker base image to Debian 13 (#10452)\n * Contrib:\n   - changed: translation: stop sorting source strings (6c1e085)\n   - changed: freeze_packages.py: use stdlib \"venv\" instead of 3rd party virtualenv (4f7b6e8)\n   - fix: add_cosigner.py: compatibility with Python 3.13 (b495ee7)\n * Plugins:\n   - new: hook 'qt_utxo_menu' for Qt GUI UTXO list (cfe2a57)\n * Hardware wallets:\n   - new: support Ledger Nano Gen 5 (#10457)\n * Translations: Call for Proofreaders and Translators.\n  - Localisation of the UI has always been a community effort.\n    Recently we found several examples of vandalism and malicious behaviour\n    among the translated strings, including multiple bitcoin addresses\n    injected into UI strings. One user sent funds to one such address\n    and hence lost money. (see spesmilo/electrum-locale#46)\n  - We added some automated safeguards to try and prevent this in the future,\n    including basic regexes and an LLM proofreader. We also made the ongoing\n    git diffs for updating the frozen translations much smaller to make it\n    realistic to ~review. (see spesmilo/electrum-locale#47, #49, #51)\n  - However, the best solution would be per-language human review.\n    If we had 1-3 proofreaders per language on Crowdin, we could restrict\n    the set of translated strings that gets included in the binaries to the\n    \"proofreader-approved\" strings. We ask interested people to start contributing\n    and apply to be proofreaders. To get proofreader permissions, send us an email\n    or come to irc, with your crowdin username and some proof of work (such as\n    activity on crowdin in our or another project, contributions to open-source,\n    having an established identity on github/bitcointalk/stackoverflow/..., etc\n    -- just prove being a human and being well-intentioned).\n    (see https://github.com/spesmilo/electrum-locale/issues/47#issuecomment-3914866337)\n\n# Release 4.7.0 (Jan 22, 2026)\n * Qt GUI (desktop):\n   - new: \"Submarine Payments\": support reverse swaps to external address (#10303)\n          Allows doing onchain payments from the wallet's lightning balance.\n   - changed: flag console usage in crash reports (#10219)\n   - changed: add \"Tools\" text to the tools button for increased visibility (#10277)\n   - changed: improved UI feedback for send change to lightning function (#10247)\n   - fix: improve Network Tab behavior when switching connection mode (#10280)\n   - fix: re-add fiat values to csv/json history export (#10209)\n   - fix: not proposing tx batching in some cases (#10204)\n * QML GUI & Android:\n   - new: allow manual editing of fee/feerate (#10371)\n          This also allows sending sub-1 sat/b transactions on Android.\n   - new: support biometric authentication (#10340)\n          Allows using the Android system lockscreen (e.g. fingerprints)\n          to unlock the wallet and authorize payments.\n          The previous optional built-in PIN code authentication is removed.\n   - changed: make UI compatible with edge-to-edge layout (#10178)\n   - changed: fee histogram colors: extend color palette to cover sub-1 s/b (#10307)\n   - changed: enforce the usage of a single password for all wallet files (#10345)\n   - changed: allow tap-to-focus in the qr code scanner (#10385)\n   - fix: allow opening passwordless wallets (#10423)\n   - fix: also protect address private keys from screenshots (#10426)\n * Lightning:\n   - new: support LNURL-withdraw/LUD-3 (#9993)\n          Allows scanning QR codes to receive funds on lightning (e.g. ATMs, vouchers).\n   - changed: refactor handling of incoming htlcs (#10230)\n   - changed: collect htlcs failed back to us before re-splitting (#10274)\n   - fix: allow spending channel reserve if anchor channels are closed but not redeemed (2d17252)\n   - fix: logic bug in liquidity hint calculation (#10305)\n   - fix: race resulting in \"Not enough balance\" error when doing concurrent payments (#10325)\n   - fix: self payments (and rebalance function) (#10271)\n   - fix: gossip exchange with Core Lightning nodes (#10347)\n   - fix: only wait for pending htlcs to get removed if peer is connected (1845143)\n * Electrum protocol:\n   - new: add support for Electrum Protocol version 1.6 (#10295)\n          See https://electrum-protocol.readthedocs.io/en/latest/protocol-changes.html#version-1-6\n          Min required version is still 1.4.\n   - changed: prevent connecting to server with different genesis hash (#10281)\n   - changed: add warmup budget before batching server rpc calls for faster startup (#10281)\n   - changed: optimistically guess scripthash status on new blocks to reduce network traffic\n              and improve privacy (#10290)\n   - fix: flush network buffer before disconnecting from server (6423323)\n * Onchain / Wallet:\n   - changed: non-SPV verified transactions now considered unconfirmed (#10216)\n   - changed: always enforce dnssec validation for Openalias (#10349)\n * Submarine swaps:\n   - new: cli commands to get swap statistics for swapserver operators (#10198):\n          'swapserver_get_history' and 'swapserver_get_summary'\n * CLI/RPC:\n   - new: add 'export_lightning_preimage' command (#10242)\n   - changed: return lightning preimage from 'check_hold_invoice' command (#10242)\n   - changed: 'add_peer' now blocks until the connection is established (#10283)\n   - changed: 'version_info' now shows OpenSSL version (828fc56)\n   - fix: print warnings to stderr so output is still valid json (7bfe2dd)\n   - fix: imply enabled proxy when starting with proxy cli option (#10326)\n * Plugins:\n   - changed: plugins can now use existing cli command names without colliding with builtin commands (9c4c7f0)\n   - changed: Timelock Recovery: check if locking address is ours or script (#10272)\n   - removed: payserver plugin, now an external plugin, moved to spesmilo/electrum-payserver (d36b753)\n * Hardware wallets:\n   - Coldcard: fix: compatibility with ckcc-protocol v1.5.0 (2172dad)\n * Contrib:\n   - new: add README to scripts/ directory (5a14a58)\n * Dependencies:\n   - changed: bump min required electrum-aionostr to 0.1.0 (e188102)\n * Builds/binaries:\n   - Android:\n     - new: support 16kb page size (#10148)\n     - changed: bump Android target SDK version to 35 (#10178)\n     - changed: bump OpenSSL from 1.1.1w to 3.0.18 (#10332)\n     - changed: switch from cryptography to pycryptodomex (#10332)\n     - changed: bump python version from 3.10.18 to 3.11.14 (#10388)\n   - AppImage:\n     - changed: migrate AppImage build to use modern/maintained appimagetool (#10019)\n\n\n# Release 4.6.2 (Aug 25, 2025)\n * General:\n   - changed: minrelayfee clamps from [1, 50] to [0.1, 50] sat/vbyte (#10096)\n   - new: add support for \"mutinynet\" signet test network (#10134)\n   - new: network: don't request same tx from server that we just broadcast to it (#10111)\n   - new: logging: add config.LOGS_MAX_TOTAL_SIZE_BYTES: to limit size on disk (#10159)\n * QML GUI (Android):\n   - fix: cannot open keystore-encryption-only wallets (#10171)\n   - fix: wizard: restoring from seed broken if already opened a wallet (#10117)\n   - fix: handle invoice validation errors on save (#10122)\n   - fix: sweep: handle network errors gracefully (#10108)\n   - fix: sweep: handle unexpected script_types (#10145)\n * Qt GUI (desktop):\n   - fix: wizard: hardware device: handle missing xpub (#10109)\n   - fix: wizard: enable-keystore for bip39 seeds and hw devices (#10123)\n * Lightning:\n   - fix: slow down peers sending too much gossip, and other rate-limits (#10153)\n * Submarine swaps: several bug fixes and improved reliability.\n * CLI/RPC:\n   - changed: onchain_history: add back from_height/to_height params (#10119)\n   - changed: reverse_swap: new mandatory parameter 'prepayment' (#10165)\n   - new: get_submarine_swap_providers: added command to fetch swap providers (#10158)\n * Plugins:\n   - Nostr Cosigner: fix: don't allow saving tx without txid (#10128)\n\n\n# Release 4.6.1 (Aug 5, 2025)\n * QML GUI (Android):\n   - fix: QR scanner crashes due to null/orphaned View in hierarchy (#10071)\n   - fix: creating a tx with a pre-segwit watchonly wallet (#10042)\n * CLI/RPC:\n   - fix several bugs related to new hold_invoice APIs. This required\n     minor breaking changes in the new APIs. (#10059, #10082)\n   - add max_cltv, max_fee_msat parameters to lnpay command (#10067)\n * Hardware wallets:\n   - bitbox02: bump required and bundled library to 7.0.0 (#10040)\n     This should add support for the new BitBox02 \"Nova\" devices.\n * General:\n   - rework crash reporter (#10052)\n     - show additional confirmation popup on clicking \"Send\"\n     - remove the \"Never\" button and the corresponding config option.\n       The crash reporter is now always shown on uncaught exceptions.\n       This unifies some code paths: the crash-reporter-disabled case\n       was untested and buggy.\n     - don't show reporter multiple times for the \"same\" exception\n   - new: network: parallelize block-header-chunks downloads (#10033)\n * Lightning:\n   - wallet: don't spend reserve utxo to create new reserve utxo (#10091)\n * various UI fixes (#10060, #10062, #10081, ...)\n\n\n# Release 4.6.0 (July 16, 2025)\n * A 'Terms of Use' screen was added to the install wizard. While the\n   licence remains unchanged, we ask users to agree with the fact that\n   we are not a custodial service or a money transmitter. The Terms of\n   Use screen also makes it clear that all issues are to be resolved\n   in public, and that there is no user support via private channels.\n * Nostr support: (using new dependency: electrum-aionostr)\n   Electrum now uses Nostr in the context of submarine swaps,\n   and in several plugins. Electrum will not connect to Nostr\n   by default, only if required.\n * Submarine swaps over Nostr: The Electrum client will connect to\n   Nostr in order to discover submarine swap providers, and to perform\n   related RPCs. This means that:\n    - Anyone can become a swap provider (you need to run an Electrum\n      daemon with the 'swapserver' plugin). Submarine swap providers\n      advertise their fees and their liquidity on Nostr.\n      See https://electrum.readthedocs.io/en/latest/swapserver.html\n      for set-up documentation.\n    - Submarine swap providers do not need to provide an HTTP\n      endpoint, since RPCs are performed via Nostr. They also do not\n      need to have public lightning channels.\n    - Because a decentralized service needs to be trustless, the\n      option to perform zero-confirmation swaps has been removed from\n      Electrum.\n    Note that Electrum connections to Nostr relays are only initiated\n    when the user uses the swap service, and the nostr public key used\n    by the client is ephemeral. In contrast, swap providers use a\n    persisted identity.\n * Third-party plugins:\n   - Electrum supports the installation of plugins distributed by\n     third-parties as ZIP files. While it has long been possible to\n     install third-party plugins when running Electrum from python\n     sources, the same is now possible when using desktop binaries\n     (Windows, MacOS, Linux). Third-party plugins are installed as ZIP\n     files in the user's electrum data directory.\n   - In order to prevent plugin installation by malware, third-party\n     plugins can only be enabled if the user enters a plugin\n     authorization password (distinct from the wallet password).\n     Setting up that plugin authorization password requires\n     administrator permissions on the local machine; a\n     password-derived public key must be written in the system.\n * Lightning:\n   - Anchor channels (#9264): Newly created channels use\n       anchor commitments by default. Since sweeping outputs from anchor\n       channels may require external UTXOs, lightning can no longer be\n       enabled in wallets that do not have a software keystore (hardware\n       wallets, watching-only wallets).  Existing wallets that are in that\n       situation cannot create new channels.\n   - wallets with anchor channels must always have utxos available (#9536)\n   - support added for onion messages (only CLI for now) (#9039)\n   - lots of fixes and improvements (#8857, #8547, #9700, #9083, ...)\n * Qt Desktop GUI:\n   - migrate from Qt5 to Qt6 (#9189)\n   - new: screenshot \"protection\" on Windows (#9898). Inspired by Windows\n     Recall, by default screenshots will contain black rectangles in\n     place of the Electrum windows, to try to avoid leaking secret keys.\n     This is opt-out using a config variable.\n   - exposed option to connect to only a single server (--oneserver)\n   - Wallet file encryption:\n     - Non-multisig hardware wallet files can now be encrypted with\n       either using the hardware device or (new) a password. (#5561)\n     - The option to have a password-protected wallet without file\n       encryption has been removed from the Qt GUI. It is still possible\n       to create such a wallet using the command line.\n   - Wallet unlocking:\n     - Wallets can be unlocked in the Qt GUI. When a password-protected\n       wallet is unlocked, its password is kept in memory, and signing\n       transactions will not require to enter the password. The unlocked\n       state is rendered by the 'open lock' icon in the status bar.\n     - If a wallet needs to sweep anchor channel outputs using extra\n       UTXOs, the operations will be performed without requiring the\n       user password if the wallet is unlocked. If the wallet is locked,\n       the status bar will show a 'password required' button.\n   - Transaction batching: When creating a new payment, if the\n     output can be added to an existing mempool transaction, the 'New\n     transaction' window will show a drop-down menu, proposing a list of\n     transactions that can be batched with the current payment. This\n     replaces the previous 'batch' option checkbox, and gives more\n     control to the user.\n   - Keystore enabling/disabling (Qt):\n     - It is now possible to add a seed\n       to an existing watching-only wallet, or to a keystore within a\n       multisig wallet. Similarly, it is possible to pair a watching-only\n       keystore with a hardware device. These operations are performed\n       from the 'Wallet Information' dialog.\n   - Lightning address contacts:\n     - It is now possible to create contacts with (lnurl type) lightning\n       addresses as payment identifier.\n   - show warnings on wallet close if there are sensitive pending operations,\n     e.g. when in the middle of doing a swap (#9715)\n   - some performance improvements for large wallets (#9958, #9967, #9968)\n   - qr-reader: macos: add runtime requesting of camera permission (#9955)\n * Accounting rules: In order to properly handle on-chain transactions\n   created by lightning channel force closures, we consider that funds\n   successfully redeemed from a script with several possible\n   recipients have never left the final recipient's wallet. This\n   avoids having to write balance changes that are cancelled\n   later. The corresponding addresses are rendered in the GUI as\n   'accounting addresses' (in orange).\n * New plugins:\n   - Nostr Wallet Connect: This plugin allows remote control of\n     Electrum lightning wallets via Nostr NIP-47. (#9675)\n   - Nostr Cosigner: This plugin facilitates the exchange of\n     PSBTs between cosigners of a multisig wallet. It replaces the\n     former 'Cosigner pool' plugin. Instead of relying on a central\n     server, it uses Nostr to send/receive PSBTs. (#9261)\n   - Timelock Recovery: A timelock based inheritance scheme.\n     See timelockrecovery.com (#9589)\n * CLI:\n   - The command line help has been improved; parameters are\n     documented in the same docstring as the command they belong to.\n   - If the --wallet parameter passed to a command is a simple filename,\n     it is now interpreted as relative to the users wallets directory,\n     rather than to the current working directory.\n   - Plugins may add extra commands to the CLI. Plugin commands must\n     be prefixed with the plugin's internal name.\n   - Support for hold invoices.\n   - new commands:\n     - listconfig, helpconfig, unsetconfig\n     - onchain_capital_gains (was previously a field of onchain_history)\n     - {add,settle,cancel,check}_hold_invoice\n     - send_onion_message, get_blinded_path_via\n     - wait_for_sync\n * General:\n   - Mitigate against dust attacks; Add option to avoid spending from\n     used addresses. (#9636)\n   - Restrict process memory access on Linux. (#9749)\n   - locale: syntax-check i18n translations at runtime. Malformed translation\n     strings are now less likely to cause errors: instead we fallback to the\n     original English string (#10011)\n   - fix: would sometimes hang on startup if system clock jumped backwards (#9802)\n * QML GUI (Android):\n   - \"Sweep key\" feature ported to mobile\n   - Estimate amount when Max is checked\n   - exposed option to connect to only a single server (--oneserver)\n * Android:\n   - replace QR code scanning library to make scanning fun again (#9983)\n   - properly ask for (notification) OS permission access. (#9682)\n   - add option to prevent the app touching the screen brightness (#9321)\n * Electrum protocol: add padding and some noise to messages (#9875)\n * Hardware wallets:\n   - Coldcard: add feature to upload multisig wallet configuration to Coldcard via USB.\n   - KeepKey: we now vendor our fork of keepkeylib,\n       instead of using the unmaintained upstream as an external dependency (#9650)\n   - Ledger:\n     - rm support for \"HW.1\" and \"Nano\" (non-S) devices (#9652)\n     - rm dependency: btchip-python (#9370)\n * Builds/binaries:\n   - new minimum OS requirements:\n     - Windows: x86_64, Windows 10 (1809)\n         note: 32-bit Windows is no longer supported.\n     - macOS: 11 \"Big Sur\"\n     - Linux AppImage: x86_64, glibc 2.31 (\"debian 11\"-equivalent)\n * Dependencies:\n   - the minimum required python version was increased: 3.8->3.10 (#9418)\n   - new first-party dep: electrum-aionostr\n     - forked the seemingly unmaintained davestgermain/aionostr library\n   - new first-party dep: electrum-ecc\n     - split out our existing libsecp256k1 python bindings into\n       this separate package\n\n\n# Release 4.5.8 (Oct 23, 2024)\n * Qt Desktop GUI:\n   - fix: regression: bump_fee and dscancel dialogs erroring (#9273)\n\n\n# Release 4.5.7 (Oct 21, 2024)\n * General:\n   - new: add new historical exchange rate providers: Bitfinex and Bitstamp\n   - fix: wizard regression: 2fa wallet setup erroring (#9253)\n   - fix: python 3.13 compat: could not connect to some self-signed electrum\n     servers with weird TLS certs. As workaround, set pre-3.13 behaviour (#9258)\n * Lightning:\n   - fix: send update_fee right away after channel_reestablish (3a465593)\n     This fixes a race that can result in a force-closure if we try sending\n     a payment very soon after reestablishing the channel.\n * Qt Desktop GUI:\n   - fix: show fee warnings also in the transaction dialog (c4fe2796)\n\n\n# Release 4.5.6 (Oct 16, 2024)\n * General:\n   - new: add support for testnet4 (#9197)\n   - fix: wizard: allow passphrase for some '2fa' seeds (#9088)\n   - fix: trustedcoin wallet wizard continuation if file has keystore-only encryption (#9237)\n   - fix: trustedcoin: sanitize error messages coming from 2fa server\n   - fix: new wizard did not set keystore password if storage was not encrypted (#9147)\n   - changed: set stricter UNIX permissions for log files (fa8595b1)\n * QML GUI (Android):\n   - new: show seed passphrase in WalletDetails (#9204)\n   - new: set max screen brightness when displaying QR codes (79c08536)\n   - fix: crash due to ConcurrentModificationException (450b9a0)\n   - fix: issue deactivating PIN when no wallet loaded (#8366)\n   - fix: only allow Channel Backup import on Lightning-enabled wallets (8d9bcda)\n * Qt Desktop GUI:\n   - fix: scanning multi (privkeys, addresses) from QR (4dc64e4)\n * Hardware wallets:\n   - ColdCard: new: export multisig wallet to coldcard over USB (#7682)\n   - Trezor:\n     - new: add support for new device \"Safe 5\" (#9171)\n     - update: fix compat with and bump pinned library to 0.13.9 (#9141)\n   - Ledger:\n     - new: add support for new device \"Flex\" (#9179)\n     - update: bump pinned library to 0.3.0, raise max lib to <0.4 (719292f8)\n   - Jade: update: bump library to 1.0.31 (9a84bb32)\n * CLI/RPC:\n   - changed: require wallet password for lnpay and similar commands (#9236)\n     (This is in addition to the wallet needing to be loaded,\n     and requiring read access to the config file)\n * Builds/binaries:\n   - changed: include unit tests in tarballs (#9207)\n   - android:\n     - changed: set target_sdk_version to 34 (2917fde5)\n     - update: bump python version (3.8->3.10) (08127a60)\n     - work towards F-Droid inclusion:\n       - reproducible apks: strip file path prefix from .pyc files (6ebdbf04)\n       - add fastlane metadata for f-droid (#9211)\n       - change versionCode calculation (#9221)\n       - build.gradle: set android.dependenciesInfo.includeInApk=false (af18df10)\n       - contrib/release_www.sh: put android versionCode in \"version\" file (#9233)\n\n\n# Release 4.5.5 (May 30, 2024)\n * General:\n   - fix: timeout error shadowed by aiorpcx cancellation bug (#8954)\n   - changed: Fiat exchange rates: do not overwrite the locally saved historical\n     data. Instead, merge old and new data (a2fb70d6). This also ~fixes the\n     CoinGecko historical API by only asking for the last 365 days.\n   - update: support latest revision of SLIP-39 mnemonic spec (to restore) (#9059)\n * Lightning:\n   - new: unify max fee bounds for payments, make it configurable (#9041)\n   - changed: trampoline fees: instead of hardcoded list, use\n     exponential search, capped by configurable budget (#9033)\n   - fix: opening new channels with peer that has .onion address (#9002)\n * Dependencies:\n   - remove bitstring (#9020)\n * QML GUI (Android):\n   - new: add tx options to ConfirmTxDialog, RbfBumpFeeDialog (#8909)\n   - various UI fixes (#9018, 472a65eb)\n * Qt Desktop GUI:\n   - fix: save notes whenever modified (#8951)\n   - fix: offline 2fa wallet creation failing in some cases (#9037)\n   - various UI fixes (#8962, #8874, #9012, 1047200a, #9058)\n * Hardware wallets:\n   - Bitbox02: fix: call pairing dialog when necessary (#8971)\n   - Jade: update: bump library to 1.0.29 (#9007)\n * Binaries:\n   - new: add AppArmor profiles for tarball and AppImage (#9003)\n\n\n# Release 4.5.4 (March 14, 2024)\n * General:\n    - fix: failing WalletDB upgrade(58) in 4.5.3 (#8913), for wallets with\n      partial txs saved into the history as local txs\n * Lightning:\n   - changed: use longer final_cltv_delta for client-normal-swap, to\n     give more time for user to come back online while doing the swap (#8940)\n   - changed: create trampoline onions even when directly paying\n     a trampoline forwarder node (777c2ffb)\n * Hardware wallets:\n   - Trezor:\n     - fix: allow adding SLIP-19 ownership proofs to complete inputs (#8910)\n * Plugins:\n   - fix: a race in swapserver when handling server-normal-swaps (#8825)\n\n\n# Release 4.5.3 (February 23, 2024)\n * General:\n   - changed: label tx sizes as \"vbytes\", and feerates as \"sat/vbyte\" (#8864)\n   - fix: wizard regression not able to use HWW as cosigner for new wallets (643fbec)\n   - fix: onchain invoice paid detection broken if jsonpatch enabled (#8842)\n   - fix: program not starting because of bad \"proxy\" config value (#8837)\n   - fix: wizard: don't log sensitive values: replace blacklist with whitelist (638fdf11)\n * Qt Desktop GUI:\n   - new: basic \"add server as bookmark\" functionality (#8865)\n   - fix: potential race condition in wizard page construction (c78a90a)\n   - fix: don't use lightning invoice when user specifies MAX amount (#8900)\n   - various UI fixes (#8874, 2882c4b, #8889, 66af6e6)\n * QML GUI (Android):\n   - fix potential concurrency issue loading wallet (#8355)\n   - fix: wizard: fails to restore from 2fa seed: KeyError: 'x1' (#8861)\n   - various UI fixes (50a53aa, 0a6b2d5, #8782, 6738e1e, c0b8927, 016e500, #8898)\n * Hardware wallets:\n   - Trezor:\n     - new: support SLIP-19 ownership proofs, for trezor-based Standard_Wallets (#8871)\n     - fix: regression in sign_transaction for trezor one for multisig (#8813)\n * CLI/RPC:\n   - changed: nicer error messages and error-passing (#8888)\n * Lightning:\n   - fix: timing issue in lnpeer.reestablish_channel, for replaying unacked updates (79d88dcb)\n\n\n# Release 4.5.2 (January 20, 2024)\n * Qt Desktop GUI:\n   - fix crash during startup/wizard-open (#8833)\n\n\n# Release 4.5.1 (January 19, 2024)\n * Lightning:\n   - fix: MPP regression when using gossip that made paying small invoices fail (95c55c542)\n   - fix: better handle dataloss (#8814)\n     - allow manually requesting force-close in WE_ARE_TOXIC state\n     - fix some timing issues\n * General:\n   - localization: never translate CLI/RPC (0e5a1380)\n   - localization: simplify how default language is chosen (0e5a1380)\n * QML GUI (Android):\n   - bump min required android version from android 5.0 to 6.0 (#8761)\n     (older versions have not been working in practice since at least 4.4.0)\n   - properly refresh history if addresses are deleted from imported wallets (#8782)\n   - fix crash when LNURLp is scanned/pasted (#8822)\n   - fix crash for new wallets having cosigner using hww #8808)\n   - fix crash in finalizer when txid is undefined (#8807)\n   - various UI fixes (291f0ce, 3d9996a, ec81f00)\n * Qt Desktop GUI:\n   - also support unfinished wallets when opened through File>Open (#8809)\n   - fix handler for OpenFileEventFilter (6a28ef5)\n\n\n# Release 4.5.0 (January 12, 2024)\n * General:\n   - remove SSL options from config (012ce1c)\n   - make number of logfiles to keep configurable (5e8b14f)\n   - refactored SimpleConfig and added ConfigVars (#8454)\n   - incremental writes of wallet file (#8493)\n   - add warnings and prompt users when signing txs with non-default sighashes (#8687)\n   - refactored bip21/bolt11/lnurl/etc-handling into PaymentIdentifiers (#8462)\n   - add option to merge duplicate outputs (#8474)\n   - fix: consider bip21 URIs as invalid if they contain unknown req-* param (#8781)\n * Lightning:\n   - fix BOLT-04 \"MUST set `short_channel_id` to the `short_channel_id` used by the incoming onion\" (ca93af2)\n   - add support for hold invoices (1acf426)\n   - add support for bundled payments (c4eb7d8)\n   - various MPP improvements (#7987, ..)\n   - support large channels (40f2087)\n   - new flow for normal submarine swaps (fd10ae3)\n     - the client now uses hold invoices, just like the server\n     - the client waits until HTLCs are received before going on-chain\n     - the user may cancel the swaps during that waiting time\n   - don't create invoice with duplicate route hints (a3997f8)\n   - don't set channel OPEN before channel_ready has been both sent and received (#8641)\n   - if trampoline is enabled, do not add non-trampoline nodes to invoices (120faa4)\n * QML GUI (Android):\n   - port to Qt6 (#8545)\n   - fix regression for lnurl-pay (#8585)\n   - fix invoice amount bounds check (#8582)\n   - fix places where text was rendered off-screen for certain translations (#8611)\n   - fix lnworker undefined when node alias requested (#8635)\n   - fix BIP39 cosigner script type must be same as primary (8cd95f1)\n   - fix: never use current fiat exchange rate for old historical amounts (#8788)\n   - better handle android back-gesture (#8464)\n   - new: show private key in address details (016b5eb)\n   - new: show tx inputs in TxDetails and other dialogs (#8772)\n   - new: label sync plugin toggle (b6863b4)\n   - fix: properly suggest paying BOLT11 invoice onchain if insufficient balance (0a80460)\n   - new: message sign & verify (e5e1e46)\n   - new: allow never expiring payment requests (#8631)\n   - new: add coins/UTXOs to addresses list, add filters (cf91d2e)\n   - new: delete addresses from imported wallet (#8675)\n   - new: add support for lightning address and openalias (03dd38b)\n   - new: add setting to allow screenshots everywhere (0dae1733)\n   - simplify welcome page for first-start network settings (#8737)\n   - various UI fixes (b846eab, #8634, 9ed5f7b, 941f425, b20a4b9, af61b9d, 0fb47c8, 2995bc8, ..)\n * Qt Desktop GUI:\n   - port wizard to new implementation\n   - fix fiat balance sorting in address list window (#8469, #8478)\n   - remove thousands separator when copying numbers to clipboard (#8479)\n   - new: option to use extra trampoline for legacy payments (b2053c6)\n   - new: send change to lightning option for on-chain payments (649ce97)\n   - new: notes tab for saving text in the (encrypted) wallet file (d691aa07)\n   - simplify welcome page for first-start network settings (#8737)\n   - various UI fixes (#8587, #6526, ..)\n * Hardware wallets:\n   - Trezor: allow multiple change outputs (#3920)\n   - Trezor: support external pre-signed inputs (#8324)\n   - Bitbox02: update to 6.2.0 (#8459)\n * Plugins:\n   - new: swapserver plugin (#8489)\n * Builds/binaries:\n   - update bundled zbar, for security fixes (#8805)\n\n\n# Release 4.4.6 (August 18, 2023) (security update)\n * Lightning:\n   - security fix: multiple lightning-related security issues have\n     been fixed. See disclosures:\n     - https://github.com/spesmilo/electrum/security/advisories/GHSA-9gpc-prj9-89x7\n     - https://github.com/spesmilo/electrum/security/advisories/GHSA-8r85-vp7r-hjxf\n   - fix: cannot sweep from channel after local-force-close, if using\n     imported channel backup (#8536). Fixing this required adding a\n     new field (local_payment_pubkey) to the channel backup\n     import/export format and bumping its version number\n     (v0->v1). Both v0 and v1 can be imported, and we only export v1\n     backups. When you force close a channel, the GUI will prompt you\n     to save a backup. In that case, you must export the backup using\n     the updated Electrum, and not rely on a backup made with an older\n     release of Electrum.  Note that if you request a force close from\n     the remote node or co-op close, you do not need to save a channel\n     backup.\n   - fix: we would sometimes attempt sending MPP even if not supported\n     by the invoice (2cf6173c)\n * QML GUI:\n   - fix lnurl-pay when config.BTC_AMOUNTS_ADD_THOUSANDS_SEP is True\n     (5b4df759)\n * Hardware wallets:\n   - Trezor: support longer than 9 character PIN codes (#8526)\n   - Jade: support more custom-built DIY Jade devices (#8546)\n * Builds/binaries:\n   - include AppStream metainfo.xml in tarballs (#8501)\n * fix: exceptions in some callbacks got lost and not logged (3e6580b9)\n\n\n# Release 4.4.5 (June 20, 2023)\n * Hardware wallets:\n   - jade: fix regression in sign_transaction (#8463)\n * Lightning:\n   - fix \"rebalance_channels\" function (#8468)\n * enforce that we run with python asserts enabled,\n   regardless of platform (d1c88108)\n\n\n# Release 4.4.4 (May 31, 2023)\n * QML GUI:\n   - fix creating multisig wallets involving BIP39 seeds (#8432)\n   - fix \"cannot scroll to open a lightning channel\" (#8446)\n   - wizard: \"confirm seed\" screen to normalize whitespaces (#8442)\n   - fix assert on address details screen (#8420)\n * Qt GUI:\n   - better handle some expected errors in SwapDialog (#8430)\n * libsecp256k1: bump bundled version to 0.3.2 (10574bb1)\n\n\n# Release 4.4.3 (May 11, 2023)\n * Intentionally break multisig wallets that have heterogeneous master\n   keys. Versions 4.4.0 to 4.4.2 of Electrum for Android did not check\n   that master keys used the same script type. This may have resulted\n   in the creation of multisig wallets that cannot be spent from\n   with any existing version of Electrum. It is not sure whether any\n   users are affected by this; if there are any, we will publish\n   instructions on how to spend those coins (#8417, #8418).\n * Qt GUI:\n   - handle expected errors in DSCancelDialog (#8390)\n   - persist addresses tab toolbar \"show/hide\" state (b40a608b)\n * QML GUI:\n   - implement bip39 account detection (0e0c7980)\n   - add share toolbutton for outputs in TxDetails (#8410)\n * Hardware wallets:\n   - Ledger:\n     - fix old bitcoin app support (<2.1): \"no sig for ...\" (#8365)\n     - bump req ledger-bitcoin (0.2.0+), adapt to API change (30204991)\n * Lightning:\n   - limit max feature bit we accept to 10_000 (#8403)\n   - do not disconnect on \"warning\" messages (6fade55d)\n * fix wallet.get_tx_parents for chain of unconf txs (#8391)\n * locale: translate more strings when using \"default\" lang (a0c43573)\n * wallet: persist frozen state of addresses to disk right away (#8389)\n\n\n# Release 4.4.2 (May 4, 2023)\n * Qt GUI:\n   - fix undefined var check in swap_dialog (#8341)\n   - really fix \"recursion depth exceeded\" for utxo privacy analysis (#8315)\n * QML GUI:\n   - fix signing txs for 2fa wallets (#8368)\n   - fix for wallets with encrypted-keystore but unencrypted-storage (#8374)\n   - properly delete wizard components after use (#8357)\n   - avoid entering loadWallet if daemon is already busy loading (#8355)\n   - no auto capitalization on import and master key text fields (5600375d)\n   - remove Qt virtual keyboard and add Seedkeyboard for seed entry (#8371, #8352)\n   - add runtime toggling of android SECURE_FLAG, to allow screenshots (#8351)\n   - restrict cases where server is shown \"lagging\" (53d61c01)\n * fix hardened char \"h\" vs \"'\" needed for some hw wallets (#8364, 499f5153)\n * fix digitalbitbox(1) support (22b8c4e3)\n * fix wrong type for \"history_rates\" config option (#8367)\n * fix issues with wallet.get_tx_parents (a1bfea61, 56fa8325)\n\n\n# Release 4.4.1 (April 27, 2023)\n * Qt GUI:\n   - fix sweeping (#8340)\n   - fix send tab input_qr_from_camera (#8342)\n   - fix crash reporter showing if send fails on typical errors (#8312)\n   - bumpfee: disallow targeting an abs fee. only allow feerate (#8318)\n * QML GUI:\n   - fix offline-signing or co-signing pre-segwit txs (#8319)\n   - add option to show onchain address in ReceiveDetailsDialog (#8331)\n   - fix strings unique to QML did not get localized/translated (#8323)\n   - allow paying bip21 uri onchain that has both onchain and bolt11\n     if we cannot pay on LN (#8334, 312e50e9)\n   - virtual keyboard: make buttons somewhat larger (75e65c5c)\n   - fix(?) Android crash with some OS-accessibility settings (#8344)\n   - fix channelopener.connectStr qr scan popping under (#8335)\n   - fix restoring from old mpk (watchonly for \"old\" seeds) (#8356)\n * libsecp256k1: add runtime support for 0.3.x, bump bundled to 0.3.1\n * forbid paying to \"http:\" lnurls (enforce https or .onion) (1b5c7d46)\n * fix wallet.bump_fee \"decrease payment\" erroring on too high target\n   fee rate (#8316)\n * fix performance regressions in tx logic (ee521545, 910832c1)\n * fix \"recursion depth exceeded\" for utxo privacy analysis (#8315)\n\n\n# Release 4.4.0 (April 18, 2023)\n\n * New Android app, using QML instead of Kivy\n   - Using Qt 5.15.7, PyQt 5.15.9\n   - This release still on python3.8\n   - Feature parity with Kivy\n   - Android Back button used throughout, for cancel/close/back\n   - Note: two topbar menus; tap wallet name for wallet menu, tap\n     network orb for application menu\n   - Note: long-press Receive/Send for list of payment requests/invoices\n * Qt GUI improvements\n   - New onchain transaction creation flow, with configurable preview\n   - Various options have been moved to toolbars, where their effect\n     can be more directly observed.\n * Privacy features:\n    - lightning: support for option scid_alias.\n    - Qt GUI: UTXO privacy analysis: this dialog displays all the\n      wallet transactions that are either parent of a UTXO, or can be\n      related to it through address reuse (Note that in the case of\n      address reuse, it does not display children transactions.)\n    - Coins tab: New menu that lets users easily spend a selection\n      of UTXOs into a new channel, or into a submarine swap (Qt GUI).\n * Internal:\n    - Lightning invoices are regenerated every time routing hints are\n      deprecated due to liquidity changes.\n    - Script descriptors are used internally to sign transactions.\n\n\n# Release 4.3.4 - Copyright is Dubious (January 26, 2023)\n * Lightning:\n   - make sending trampoline payments more reliable (5251e7f8)\n   - use different trampoline feature bits than eclair (#8141)\n * invoice-handling: fix get_request_by_addr incorrectly mapping\n   addresses to request ids when an address was reused (#8113)\n * fix a deadlock in wallet.py (52e2da3a)\n * CLI: detect if daemon is already running (c7e2125f)\n * add an AppStream metainfo.xml file for Linux packagers (#8149)\n * payserver plugin:\n   -replaced vendored qrcode lib\n   -added tabs for on-chain and lightning invoices\n   -revamped html and javascript\n\n\n# Release 4.3.3 - (January 3, 2023)\n * Lightning:\n   - fix handling failed HTLCs in gossip-based routing (#7995)\n   - fix LN cooperative-chan-close to witness v1 addr (#8012)\n * PSBTs:\n   - never put ypub/zpub in psbts, only plain xpubs (#8036)\n   - for witness v0 txins, put both UTXO and WIT_UTXO in psbt (#8039)\n * Hardware wallets:\n   - Trezor: optimize signing speed by not serializing tx (#8058)\n   - Ledger:\n     - modify plugin to support new bitcoin app v2.1.0 (#8041),\n     - added a deprecation warning when using Ledger HW.1 devices.\n       Ledger itself stopped supporting HW.1 some years ago, and it is\n       becoming a maintenance burden for us to keep supporting it.\n       Please migrate away from these devices. Support will be removed\n       in a future release.\n * Binaries:\n   - tighten build system to only use source pkgs in more places\n     (#7999, #8000)\n   - Windows:\n     - use debian makensis instead of upstream windows exe (#8057)\n     - stop using debian sid, build missing dep instead (98d29cba)\n   - AppImage: fix failing to run on certain systems (#8011)\n * commands:\n   - getinfo() to show if running in testnet mode (#8044)\n   - add a \"convert_currency\" command (for fiat FX rate) (#8091)\n * Qt wizard: fix QR code not shown during 2fa wallet creation (#8071)\n * rework Tor-socks-proxy detection to reduce Tor-log-spam (#7317)\n * Android: add setting to enable debug logs (#7409)\n * fix payserver (merchant) js for electrum 4.3 invoice api (0fc90e07)\n * bip21: more robust handling of URIs that include a \"lightning\" key\n   (ac1d53f0, 2fd762c3, #8047)\n\n\n# Release 4.3.2 - (September 26, 2022)\n * When creating new requests, reuse addresses of expired requests\n   (fixes #7927).\n * Index requests by ID instead of receiving address. This affects the\n   following commands: get_request, get_invoice, list_requests,\n   list_invoices, delete_request, delete_invoice\n * Trampoline routing: remember routes that have failed. Try other\n   routes instead of systematically raising tampoline fees.\n * Fix sweep to_local output from channel backup (#7959)\n * Harden build script for macOS binary: avoid using\n   precompiled wheels from PyPI for most packages (#7918)\n * The Windows/AppImage/Android binaries are now built on debian using\n   the snapshot.debian.org archive instead of ubuntu. This should help\n   with historical reproducibility. (#7926)\n\n# Release 4.3.1 - (August 17, 2022)\n * build: we now also distribute a \"source-only\"\n   Linux-packager-friendly tarball (d0de44a7, #7594), in addition\n   to the current \"normal\" tarball. The \"source-only\" tarball excludes\n   compiled locale files, generated protobuf files, and does not\n   vendor our runtime python dependencies (the packages/ folder).\n * fix os.chmod when running in tmpfs on Linux (#7681)\n * (Qt GUI) some improvements for high-DPI monitors (38881129)\n * bring kivy request dialog more in-line with Qt (#7929)\n * rm support of \"legacy\" (without static_remotekey) LN channels.\n   Opening these channels were never supported in a release version,\n   only during development prior to the first lightning-capable\n   release. Wallets with such channels will have to close them.\n   (1f403d1c, 7b8e257e)\n * Qt: fix duplication of some OS notifications on onchain txs (#7943)\n * fix multiple recent regressions:\n    - handle NotEnoughFunds when trying to pay LN invoice (#7920)\n    - handle NotEnoughFunds when trying to open LN channel (#7921)\n    - labels of payment requests were not propagated to\n      history/addresses (#7919)\n    - better default labels of outgoing txs (#7942)\n    - kivy: dust-valued requests could not be created for LN (#7928)\n    - when closing LN channels, future (timelocked) txs were not\n      shown in history (#7930)\n    - kivy: fix deleting \"local\" tx from history (#7933)\n    - kivy: fix paying amountless LN invoice (#7935)\n    - Qt: better handle unparseable URIs (#7941)\n\n\n# Release 4.3.0 - (August 5, 2022)\n\n * This version introduces a set of UI modifications that simplify the\n   use of Lightning. The idea is to abstract payments from the payment\n   layer, and to suggest solutions when a lightning payment is hindered\n   by liquidity issues.\n    - Invoice unification: on-chain and lightning invoices have been\n      merged into a unique type of invoice, and the GUI has a single\n      'create request' button. Unified invoices contain both a\n      lightning invoice and an onchain fallback address.\n    - The receive tab of the GUI can display, for each payment\n      request, a lightning invoice, a BIP21 URI, or an onchain\n      address. If the request is paid off-chain, the associated\n      on-chain address will be recycled in subsequent requests.\n    - The receive tab displays whether a payment can be received using\n      Lightning, given the current channel liquidity. If a payment\n      cannot be received, but may be received after a channel\n      rebalance or a submarine swap, the GUI will propose such an\n      operation.\n    - Similarly, if channels do not have enough liquidity to pay a\n      lightning invoice, the GUI will suggest available alternatives:\n      rebalance existing channels, open a new channel, perform a\n      submarine swap, or pay to the provided onchain fallback address.\n    - A single balance is shown in the GUI. A pie chart reflects how\n      that balance is distributed (on-chain, lightning, unconfirmed,\n      frozen, etc).\n    - The semantics of the wallet balance has been modified: only\n      incoming transactions are considered in the 'unconfirmed' part\n      of the balance. Indeed, if an outgoing transaction does not get\n      mined, that is not going to decrease the wallet balance. Thus,\n      change outputs of outgoing transactions are not subtracted from\n      the confirmed balance. (Before this change, the arithmetic\n      values of both incoming and outgoing transactions were added to\n      the unconfirmed balance, and could potentially cancel\n      each other.)\n\n * In addition, the following new features are worth noting:\n    - support for the Blockstream Jade hardware wallet (#7633)\n    - support for LNURL-pay (LUD-06) (#7839)\n    - updated trampoline feature bit in invoices (#7801)\n    - the claim transactions of reverse swaps are not broadcast until\n      the parent transaction is confirmed. This can be overridden by\n      manually broadcasting the local transaction.\n    - the fee of submarine swap transactions can be bumped (#7724)\n    - better error handling for trampoline payments, which should\n      improve payment success rate (#7844)\n    - channel backups are removed automatically when the corresponding\n      channel is redeemed (#7513)\n\n\n# Release 4.2.2 - (May 27, 2022)\n * Lightning:\n   - watching onchain outputs: significant perf. improvements (#7781)\n   - enforce relative order of some msgs during chan reestablishment,\n     lack of which can lead to unwanted force-closures (#7830)\n   - fix: in case of a force-close containing incoming HTLCs, we were\n     redeeming all HTLCs that we know the preimage for. This might\n     publish the preimage of an incomplete MPP. (1a5ef554, e74e9d8e)\n * Hardware wallets:\n   - smarter pairing during sign_transaction (238619f1)\n   - keepkey: fix pairing with device using a workaround (#7779)\n * fix AppImage failing to run on certain systems (#7784)\n * fix \"Automated BIP39 recovery\" not scanning change paths (#7804)\n * bypass network proxy for localhost electrum server (#3126)\n * security fix: remove support of \"file://\" URIs from BIP70 payment\n   requests, which could be used to trigger \"open()\" on arbitrary files\n   (see https://github.com/spesmilo/electrum/security/advisories/GHSA-4fh4-hx35-r355)\n\n\n# Release 4.2.1 - (March 26, 2022)\n * Binaries:\n   - Windows: we are dropping support for Windows 7. (#7728)\n     Version 4.2.0 already unintentionally broke compatibility with\n     Win7 and there is no easy way to restore and maintain support.\n     Existing users can keep using version 4.1.5 for now, but should\n     consider upgrading or changing their OS.\n     Win8.1 still works but only Win10 is regularly tested.\n   - bump bundled Python version (win, mac, appimage) to 3.9.11,\n     (android) to 3.8.13 (1bb7ef92, #7721)\n     (note these include a fix to an openssl DOS-vector CVE-2022-0778)\n   - windows: bump pyinstaller to 4.10 and wine to 7.0 (#7721)\n * Kivy GUI:\n   - fix \"Child Pays For Parent\" not working on Android (#7723)\n   - revert to defaulting the UI language to English (25fee6a6)\n * Qt GUI:\n   - macOS: fix opening \"Preferences\" segfaulting for some (#7725)\n   - more resilient startup: better error-handling and fallback (#7447)\n * Library:\n   - fix LN error/warning message-handling, and fix regression that\n     errors during channel-open were not properly shown in GUI (a92dede4)\n   - during LN chan open, do not backup wallet automatically (#7733)\n   - Imported wallets: fix delete_address rm-ing too many txs (#7587)\n   - fix potential deadlock in wallet.py (d3476b6b)\n * Hardware wallets:\n   - ledger: add progress indicator to sign_transaction (#7516)\n * fix the \"--portable\" flag for AppImage, and for pip installs (#7732)\n\n\n# Release 4.2.0 - (March 16, 2022)\n * The minimum python version was increased to 3.8 (#7661)\n * Lightning:\n   - redesigned MPP splitting algorithm (#7202)\n   - trampoline: implement multi-trampoline MPP (#7623)\n   - implement option_shutdown_anysegwit, and allow dust limits\n     below 546 sat (#7542)\n   - implement option_channel_type (#7636)\n   - implement modern closing negotiation (#7586, #7680)\n * improve support for \"lightning:\" URIs on all platforms (#7301)\n * Qt GUI:\n   - add setting \"show amounts with msat precision\" (5891e039)\n   - add setting \"add thousand separators to bitcoin amounts\" (#7427)\n * CLI/RPC:\n   - implement Unix sockets and make them the default (#7545, #7566)\n   - add \"bumpfee\" command (#7438)\n * Kivy GUI:\n   - show network setup on first start before wallet creation (#7464)\n   - add \"Child Pays For Parent\" option (#7487)\n   - improved locale handling (22bb52d5, 7cb11ced, 4293d6ec)\n * Hardware wallets:\n   - trezor: bump trezorlib to 0.13 (#7590)\n   - bitbox02: bump bitbox02 to 6.0, support send-to-taproot (#7693)\n   - ledger: support \"Ledger Nano S Plus\" (#7692)\n * Library:\n   - added support for sighash types beside \"ALL\" (#7453)\n   - signmessage: also accept Trezor-type sigs for segwit addrs (#7668)\n   - network: make request timeout configurable (#7696)\n   - paytomany (onchain txout batching) now allows multiple max(\"!\")\n     amounts with specified weights (#7492)\n * Binary builds\n   - AppImage: changed base image from ubuntu 16.04 to 18.04 (5d0aa63a)\n * migrated from Travis CI to Cirrus CI (#7431)\n * Lots of other minor bugfixes and usability improvements.\n\n\n# Release 4.1.5 - (July 19, 2021)\n * Builds/binaries:\n   - macOS: the .dmg binary should now be reproducible\n * Kivy/Android: fix paying bip70 invoices (regression) (90579ccf)\n * fix: payment requests not saved if process is killed (6a049d99)\n * Lightning: improve payment success when using trampoline (3a7f5373)\n * add support for signet test network (#7282)\n * Qt GUI:\n   - allow restoring from SLIP39 seeds (#6917)\n   - rework QR code scanning on Windows and macOS (#7365)\n   - support smaller window sizes, decrease minimums (#7385)\n * GUIs: add \"funded or unused\" filter option to Addresses tab (#5823)\n\n\n# Release 4.1.4 - (June 17, 2021)\n * Kivy/Android: fix a regression where a non-LN wallet\n   could not open the settings (c49d6995)\n * CLI/RPC: fix \"close_wallet\" command (#7348)\n\n\n# Release 4.1.3 - (June 16, 2021)\n * Builds/binaries:\n   - Android: the binaries (APKs) should now be reproducible (#7263)\n   - AppImage: fix some startup issues by including libxcb deps (#7198)\n * Lightning:\n   - smarter LN pathfinding (if trampoline is disabled):\n     - estimate liquidity in channels using previous attempts (#7152)\n     - consider inflight HTLCs and try to route around them (#7292)\n   - bugfix: add more safety checks to avoid \"batch RBF\" feature\n     merging LN funding txs (#7298)\n   - remove HTLC value upper limit of ~42 mBTC (#7328)\n   - Kivy GUI: implement freezing LN channels (11bb39ee)\n * imported wallets: when enabling the \"Use change addresses\" option,\n   change will now be sent to a random unused imported address. (#7330)\n   As before, by default, change is sent back to the \"from address\".\n * seed generation: make sure newly created electrum seeds don't have\n   correct bip39 checksum by chance (#6001)\n * other minor fixes\n\n\n# Release 4.1.2 - (April 8, 2021)\n * Qt GUI:\n    - fix some crashes when exiting (#6889)\n    - make sure pressing Ctrl-C always quits (c41cd4ae)\n * Kivy GUI (Android):\n    - fix bug with scrollbar, again (#7155)\n    - 2fa wallets: fix making transactions (#7190)\n    - implement freezing addresses (#7178)\n * Android: use more modern application launcher/icon (#7187)\n\n\n# Release 4.1.1 - (April 2, 2021)\n * fix Qt crash with the swap dialog\n * fix Kivy bug with scrollbar (#7155)\n * fix localization issues (#7158 #4621)\n * fix python crash with swaps (#7160)\n * other minor fixes\n\n\n# Release 4.1.0 - Kangaroo (March 30, 2021)\n\nThis version is our second major release with support for the\nLightning Network. While our initial Lightning release was mostly\nabout implementing the protocol, this release brings features that are\nspecifically aimed at keeping Electrum lightweight and trustless,\nwhile avoiding single points of failure. Most of the features listed\nbelow are user-visible.\n * The wallet creation wizard no longer asks for a seed type, and\n   creates segwit wallets with bech32 addresses. Older seed types can\n   still be created with the command line.\n * Paid invoices (both incoming and outgoing) are automatically\n   removed from the send/receive lists of the GUI (one confirmation is\n   needed for onchain invoices). Once removed from the list, invoice\n   details can still be accessed from the transaction history. In Qt,\n   invoice lists have been renamed to 'Sending queue' and 'Receiving\n   queue'.\n * Lightning:\n    - recoverable channels (see below)\n    - trampoline payments (see below)\n    - support multi-part-payment\n    - support upfront-shutdown-script\n * Recoverable channels (option):\n   - Recovery data is added to the channel funding transaction using\n     an OP_RETURN. This makes it possible to recover a static backup\n     of the channel from the wallet seed. Please note that static\n     backups only allow users to request a force-close of the channel\n     with the remote node, so that funds not locked in HTLCs can be\n     recovered. This assumes that the remote node is still online, did\n     not lose its data, and accepts to force close the channel.\n   - This option is only available for standard wallets with an\n     Electrum seed. It is not available for hardware wallets, because\n     it requires a deterministic derivation of the nodeID. It is also\n     not available in watching-only wallets, for the same reason. If a\n     wallet can have recoverable channels but has an old nodeID, users\n     who want to use that feature need to close all their existing\n     channels, and to restore their wallet from seed.\n   - Channel recovery data uses 20 bytes (16 bytes of the remote\n     NodeID plus 4 magic bytes) and is encrypted so that only the\n     wallet that owns it can decrypt it. However, blockchain analysis\n     will be able to tell that the transaction was probably created by\n     Electrum.\n   - If the 'use recoverable channels' option is enabled, other nodes\n     cannot open a channel to Electrum.\n   - If a channel is force-closed, the information in the on-chain\n     backup is not sufficient to retrieve the funds in the to_local\n     output, in case the wallet is lost in a boating accident before\n     expiration of the CSV delay. For that reason, an additional\n     backup is presented to the user if they force-close a channel.\n * Trampoline routing (option): Trampoline is a solution that allows\n   light clients to delegate path-finding on the Lightning Network, so\n   that they do not have to download the entire network\n   graph. Trampoline routing was originally proposed by Bastien\n   Teinturier and is used in the Phoenix wallet. Here is how\n   Trampoline works in Electrum:\n   - Trampoline is enabled by default, in order to prevent unwanted\n     download of the network gossip. If trampoline is disabled, the\n     gossip will be downloaded, regardless of the existence of\n     channels.\n   - Because there is no discovery mechanism for trampoline nodes, the\n     list of available trampolines is hardcoded in the client (it will\n     remain so until support for trampoline routing is announced in\n     gossip). 3 trampoline nodes are currently available on mainnet:\n     ACINQ, Electrum and Hodlister.\n   - If Trampoline is enabled:\n      - payments use trampoline routing.\n      - gossip is disabled.\n      - the wallet can only open channels with trampoline nodes.\n      - pre-existing channels with non-trampoline nodes are frozen for\n        sending.\n   - There are two types of trampoline payments: legacy and trampoline\n     end-to-end. Legacy payments are possible with any receiver, but\n     they offer less privacy than end-to-end trampoline\n     payments. Electrum decides whether to perform legacy or\n     end-to-end based on the features in the invoice:\n       - OPTION_TRAMPOLINE_ROUTING_OPT (bit 25) for Electrum\n       - OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR (bit 51) for Eclair/Phoenix\n   - When performing a legacy payment, Electrum will add a second\n     trampoline node to the route in order to protect the privacy of\n     the payer and payee. It will fall back to a single trampoline if\n     the two-trampoline strategy has failed for all trampolines.\n     (Note: two-trampoline payments are currently not possible if the\n     first trampoline is the ACINQ node, and is disabled for that\n     node.)\n   - Similar to Phoenix, the fee and CLTV delay are found by\n     trial-and-error. If there is a second trampoline in the route, we\n     use the same fee/CLTV for both. This trial-and-error is\n     temporary; the final specification should add fee information in\n     the failure messages, so that we will be able to better fine-tune\n     trampoline fees.\n * Qt: The increase fee dialog now has advanced options, and offers\n   the choice between different RBF strategies.\n * Watchtowers: The 'use_local_watchtower' feature is deprecated, and\n   it has been removed from the Qt GUI. The 'use_remote_watchtower'\n   setting has been renamed to 'use_watchtower'.\n * Password unification (Android only): When the Android app is\n   started, the entered password is checked against all wallets in\n   the directory. If the test passes:\n    - all wallets are encrypted\n    - new wallets will use the unified password\n    - password updates are performed on all wallets\n   Whether the password is unified can be seen in the GUI: In the\n   'Settings' dialog, the description for the password setting is\n   'Change password for this wallet' if the password is not unified,\n   and becomes 'Change password' if password is unified.\n * Submarine swaps are now available on kivy/android.\n * Android PIN reset: If the password is unified, the PIN can be reset\n   by providing the password.\n * Android: on-chain fees have been removed from the settings\n   dialog. Instead, the fee slider is shown to the user every time an\n   on-chain transaction will be performed (sending a payment, opening\n   a channel, initiating a submarine swap)\n * BIP-0350: use bech32m for witness version 1+ addresses (4315fa43).\n   We have supported sending to any witness version since Electrum\n   3.0, using BIP-0173 (bech32) addresses. BIP-0350 makes a breaking\n   change in address encoding, and recommends using a new encoding\n   (bech32m) for sending to witness version 1 and later.\n * Block explorer: allow setting a custom URL in Qt GUI (#6965)\n\n\n# Release 4.0.9 - (Dec 18, 2020)\n * fixes a regression introduced in 4.0.8, that prevents from\n   paying BIP70 invoices (#6859)\n * reflect frozen channels and disconnected peers in the displayed\n   'can send/can receive' amounts.\n\n# Release 4.0.8 - (Dec 17, 2020)\n * fix decoding BIP21 URIs with uppercase schema (d40bedb2)\n * psbt: put full derivation paths into PSBT by default (c8155129)\n * invoices: allow address-reuse (#6609, #6852)\n * A few other minor bugfixes.\n\n# Release 4.0.7 - (Dec 9, 2020)\n * kivy: fix open channel with 'max' amount\n * kivy: fix regression introduced in last release (a9fc440)\n * other minor GUI fixes\n * Dependencies: as part of adapting to new dnspython (#6828),\n   - python-ecdsa is no longer needed at all,\n   - cryptography is now required (min 2.6), the user can no\n     longer choose between cryptography and pycryptodomex\n\n# Release 4.0.6 - (Dec 4, 2020)\n * Fix 'Max' button issue for submarine swaps button (#6770)\n * Fix 'Max' button in kivy (#6169)\n * Various fixes for Kivy/Android install wizard\n * More robust account keypath for BitBox02 (#6766)\n\n# Release 4.0.5 - (Nov 18, 2020)\n * Fix .dmg binary hanging on recently released macOS 11 Big Sur (#6461)\n * Lightning:\n   - bugfix: during LN channel opening, if the client crashed at the\n     wrong moment, the channel might not get fully persisted to disk,\n     and would need manual console-tinkering to recover (#6656)\n   - Lightning is enabled by default. Electrum will not connect to\n     the Lightning Network until the user opens a channel. (#6639)\n   - smarter node recommendation (to open channels with) (#6705)\n * user interface: some minor changes that aim to improve usability\n * Ledger:\n   - fix enumerating devices with new bitcoin app (1.5.1) (b78cbcff)\n   - fix compat with HW.1 (200f547a)\n * A few other minor bugfixes.\n\n# Release 4.0.4 - (Oct 15, 2020)\n * PSBT: fix regression in 4.0.3 where UTXO data was not included in\n   QR codes (#6600)\n * new feature: \"Cancel tx\" (#6641). The Qt/kivy GUI allows cancelling\n   an unconfirmed RBF tx by double-spending its inputs to self.\n * Windows binary:\n   - fix some issues with QR scanning by building zbar ourselves (#6593)\n   - when using setup exe, also install a debug binary (#6603)\n * Ledger: fix \"The derivation path is unusual\" warnings (#6512)\n   (needs Bitcoin app 1.4.8+ installed on device)\n * A few other minor bugfixes and usability improvements.\n\n# Release 4.0.3 - (Sep 11, 2020)\n * PSBT: restore compatibility with Bitcoin Core following CVE-2020-14199:\n   we now allow a PSBT input to have both UTXO and WITNESS_UTXO (#6429).\n   (PSBTs created since 4.0.1 already contained UTXO for segwit inputs)\n * Hardware wallets:\n   - bitbox02: better multisig UX: implement get_soft_device_id (#6386)\n   - coldcard: fix \"show address\" for multisig (#6517)\n   - all: run all device communication on a dedicated thread (#6561).\n     This should resolve some threading issues.\n * new feature: \"Automated BIP39 recovery\" (#6219, #6155)\n   When restoring from a BIP39 seed, add option to scan many known\n   derivation paths for history, and show them to user to choose from.\n * show derivation path of keystores in Qt GUI Wallet>Information (#4700)\n * fix \"signtransaction\" RPC command (#6502)\n * Dependencies: pyaes is no longer needed (#6563)\n * The tar.gz source dist now bundles make_libsecp256k1.sh, to help\n   users getting libsecp256k1 (#6323).\n * A few other minor bugfixes and usability improvements.\n\n# Release 4.0.2 - (July 8, 2020)\n - rm old corrupted non-bip70 invoices (#6345)\n - other minor fixes\n\n# Release 4.0.1 - (July 3, 2020)\n * Lightning Network support (experimental)\n   - Our implementation of Lightning relies on Electrum servers to\n     query channel states. Since servers can lie about the state of a\n     channel, users should either use a server that they trust, or\n     setup a private watchtower (see below). A watchtower is also\n     recommended for lightning wallets that remain offline for\n     extended periods of time (the default CSV 'to_self_delay' is 1\n     week). Please note that Electrum Personal Server (EPS) cannot be\n     used with lightning wallets, because channels funding addresses\n     are arbitrary.\n   - Lightning funds cannot be restored from seed. Instead, users need\n     to create static backups of their channels. Static backups cannot\n     be used to perform lightning transactions, they can only be used\n     to trigger a remote-force-close of a channel.\n   - Lightning-enabled wallet files must not be copied. Instead, a\n     backup of the wallet can be created from the Qt menu, and it will\n     contain static backups of all its channels. Backups can also be\n     exported for each channel (e.g. via QR code), and imported in\n     another wallet. Since backups are encrypted with a key derived\n     from the wallet's xpub, they can only be imported into another\n     instance of the same wallet, or a watch-only version of it. The\n     force-close is not triggered automatically when the backup is\n     imported; imported backups can live inside a wallet file.\n   - Lightning can be enabled in the GUI (Wallet>Information) or from\n     the CLI (init_lightning). Lightning is currently restricted to HD\n     p2wpkh wallets (including watch-only and hardware wallets). The\n     Qt GUI, CLI/RPC, and the kivy GUI (Android) all have LN support,\n     with feature-richness in that order.\n   - LN protocol details: dataloss_protect and static_remotekey are\n     required; varonion and payment_secret are implemented, MPP not yet.\n     Channels are not announced ('private'), forwarding is disabled.\n     We do not serve gossip queries, only consume them.\n   - Submarine swaps: the GUI integrates a service that offers\n     atomically exchanging on-chain and lightning bitcoins for a fee.\n     Electrum Technologies runs a central server for this, powered by\n     the Boltz backend.\n   - Watchtowers: Electrum can run a local watchtower (GUI setting),\n     or it can connect to a remote watchtower. A watchtower contains\n     pre-signed transactions and does not need your private keys. A\n     local watchtower will watch your channels whenever an Electrum\n     instance is running, without needing access to your wallet file.\n     An Electrum daemon can be configured to be used as a remote\n     watchtower by setting 'watchtower_address', 'watchtower_user' and\n     'watchtower_password'.\n * Partially Signed Bitcoin Transactions (PSBT, BIP-174) are supported\n   (#5721). The previous Electrum partial transaction format is no\n   longer supported, i.e. this is an incompatible change. Users should\n   make sure that all instances of Electrum they use to co-sign or\n   offline sign, are updated together.\n * Hardware wallets: several fixes in general; notable changes:\n   - The BitBox02 is now supported (#5993)\n   - Multisig support for Coldcard (#5440)\n   - Compatibility with latest Trezor fw (#6064, #6198, #5692)\n * Dependencies (see README for install instructions):\n   - libsecp256k1 is now required (previously optional). python-ecdsa\n     remains a dependency but it is now only used for DNSSEC.\n   - Added: either one of pycryptodomex or cryptography is now required,\n     mainly due to LN (previously pycryptodomex was optional, for fast AES)\n   - Removed: jsonrpclib-pelix, the JSON-RPC library used for CLI/daemon\n * Qt GUI: several changes, notably:\n   - Separation between output selection and transaction finalization.\n   - Coin selection moved to the Coins tab, and it affects all txns,\n     e.g. RBF fee-bumping, LN channel opens, submarine swaps.\n   - Editable tx preview dialog that allows e.g. changing the locktime,\n     toggling RBF, and manual coinjoins.\n * HTTP PayServer: The configuration of a bitcoin-accepting website\n   using Electrum has been simplified and requires fewer steps (see\n   documentation). The Payserver supports BIP70 and Lightning payments.\n * Android:\n   - We now build two APKs, one for ARMv7 and one for ARMv8\n   - The kivy GUI now supports importing BIP39 seeds\n   - Each wallet on kivy now can have a separate generic password,\n     using which the wallet files are encrypted. An optional PIN,\n     shared among all wallets, can be added to get prompted for spends.\n * The API of several CLI/RPC commands have changed, and several new\n   commands have been introduced (mainly for LN).\n * Distributables:\n   - The .tar.gz source dist is now built reproducibly.\n     Relatedly, we no longer distribute a .zip sdist.\n   - The MacOS binary now conforms to macOS 10.15; it is notarized\n     by Apple. This required bumping the min macOS version to 10.13.\n     Startup times should now be faster on 10.15. (#6128, #6225)\n * Transactions:\n   - we now grind low R for ECDSA signatures to match bitcoind (#5820)\n * Lots and lots of other minor bugfixes and improvements.\n\n\n# Release 3.3.8 - (July 11, 2019)\n\n * fix some bugs with recent bump fee (RBF) improvements (#5483, #5502)\n * fix #5491: watch-only wallets could not bump fee in some cases\n * appimage: URLs could not be opened on some desktop environments (#5425)\n * faster tx signing for segwit inputs for really large txns (#5494)\n * A few other minor bugfixes and usability improvements.\n\n\n# Release 3.3.7 - (July 3, 2019)\n\n * The AppImage Linux x86_64 binary and the Windows setup.exe\n   (so now all Windows binaries) are now built reproducibly.\n * Bump fee (RBF) improvements:\n   Implemented a new fee-bump strategy that can add new inputs,\n   so now any tx can be fee-bumped (d0a4366). The old strategy\n   was to decrease the value of outputs (starting with change).\n   We will now try the new strategy first, and only use the old\n   as a fallback (needed e.g. when spending \"Max\").\n * CoinChooser improvements:\n   - more likely to construct txs without change (when possible)\n   - less likely to construct txs with really small change (e864fa5)\n   - will now only spend negative effective value coins when\n     beneficial for privacy (cb69aa8)\n * fix long-standing bug that broke wallets with >65k addresses (#5366)\n * Windows binaries: we now build the PyInstaller boot loader ourselves,\n   as this seems to reduce anti-virus false positives (1d0f679)\n * Android: (fix) BIP70 payment requests could not be paid (#5376)\n * Android: allow copy-pasting partial transactions from/to clipboard\n * Fix a performance regression for large wallets (c6a54f0)\n * Qt: fix some high DPI issues related to text fields (37809be)\n * Trezor:\n   - allow bypassing \"too old firmware\" error (#5391)\n   - use only the Bridge to scan devices if it is available (#5420)\n * hw wallets: (known issue) on Win10-1903, some hw devices\n   (that also have U2F functionality) can only be detected with\n   Administrator privileges. (see #5420 and #5437)\n   A workaround is to run as Admin, or for Trezor to install the Bridge.\n * Several other minor bugfixes and usability improvements.\n\n\n# Release 3.3.6 - (May 16, 2019)\n\n * qt: fix crash during 2FA wallet creation (#5334)\n * fix synchronizer not to keep resubscribing to addresses of\n   already closed wallets (e415c0d9)\n * fix removing addresses/keys from imported wallets (#4481)\n * kivy: fix crash when aborting 2FA wallet creation (#5333)\n * kivy: fix rare crash when changing exchange rate settings (#5329)\n * A few other minor bugfixes and usability improvements.\n\n\n# Release 3.3.5 - (May 9, 2019)\n\n * The logging system has been overhauled (#5296).\n   Logs can now also optionally be written to disk, disabled by default.\n * Fix a bug in synchronizer (#5122) where client could get stuck.\n   Also, show the progress of history sync in the GUI. (#5319)\n * fix Revealer in Windows and MacOS binaries (#5027)\n * fiat rate providers:\n   - added CoinGecko.com and CoinCap.io\n   - BitcoinAverage now only provides historical exchange rates for\n     paying customers. Changed default provider to CoinGecko.com (#5188)\n * hardware wallets:\n   - Ledger: Nano X is now recognized (#5140)\n   - KeepKey:\n     - device was not getting detected using Windows binary (#5165)\n     - support firmware 6.0.0+ (#5205)\n   - Trezor: implemented \"seedless\" mode (#5118)\n * Coin Control in Qt: implemented freezing individual UTXOs\n   in addition to freezing addresses (#5152)\n * TrustedCoin (2FA wallets):\n   - better error messages (#5184)\n   - longer signing timeout (#5221)\n * Kivy:\n   - fix bug with local transactions (#5156)\n   - allow selecting fiat rate providers without historical data (#5162)\n * fix CPFP: the fees already paid by the parent were not included in\n   the calculation, so it always overestimated (#5244)\n * Testnet: there is now a warning when the client is started in\n   testnet mode as there were a number of reports of users getting\n   scammed through social engineering (#5295)\n * CoinChooser: performance of creating transactions has been improved\n   significantly for large wallets. (d56917f4)\n * Importing/sweeping WIF keys: stricter checks (#4638, #5290)\n * Electrum protocol: the client's \"user agent\" has been changed from\n   \"3.3.5\" to \"electrum/3.3.5\". Other libraries connecting to servers\n   can consider not \"spoofing\" to be Electrum. (#5246)\n * Several other minor bugfixes and usability improvements.\n\n\n# Release 3.3.4 - (February 13, 2019)\n\n * AppImage: we now also distribute self-contained binaries for x86_64\n   Linux in the form of an AppImage (#5042). The Python interpreter,\n   PyQt5, libsecp256k1, PyCryptodomex, zbar, hidapi/libusb (including\n   hardware wallet libraries) are all bundled. Note that users of\n   hw wallets still need to set udev rules themselves.\n * hw wallets: fix a regression during transaction signing that prompts\n   the user too many times for confirmations (commit 2729909)\n * transactions now set nVersion to 2, to mimic Bitcoin Core\n * fix Qt bug that made all hw wallets unusable on Windows 8.1 (#4960)\n * fix bugs in wallet creation wizard that resulted in corrupted\n   wallets being created in rare cases (#5082, #5057)\n * fix compatibility with Qt 5.12 (#5109)\n\n\n# Release 3.3.3 - (January 25, 2019)\n\n * Do not expose users to server error messages (#4968)\n * Notify users of new releases. Release announcements must be signed,\n   and they are verified byElectrum using a hardcoded Bitcoin address.\n * Hardware wallet fixes (#4991, #4993, #5006)\n * Display only QR code in QRcode Window\n * Fixed code signing on MacOS\n * Randomise locktime of transactions\n\n\n# Release 3.3.2 - (December 21, 2018)\n\n * Fix Qt history export bug\n * Improve network timeouts\n * Prepend server transaction_broadcast error messages with\n   explanatory message. Render error messages as plain text.\n\n\n# Release 3.3.1 - (December 20, 2018)\n\n * Qt: Fix invoices tab crash (#4941)\n * Android: Minor GUI improvements\n\n\n# Release 3.3.0 - Hodler's Edition (December 19, 2018)\n\n * The network layer has been rewritten using asyncio and aiorpcx.\n   In addition to easier maintenance, this makes the client\n   more robust against misbehaving servers.\n * The minimum python version was increased to 3.6\n * The blockchain headers and fork handling logic has been generalized.\n   Clients by default now follow chain based on most work, not length.\n * New wallet creation defaults to native segwit (bech32).\n * Segwit 2FA: TrustedCoin now supports native segwit p2wsh\n   two-factor wallets.\n * RBF batching (opt-in): If the wallet has an unconfirmed RBF\n   transaction, new payments will be added to that transaction,\n   instead of creating new transactions.\n * MacOS: support QR code scanner in binaries.\n * Android APK:\n   - build using Google NDK instead of Crystax NDK\n   - target API 28\n   - do not use external storage (previously for block headers)\n * hardware wallets:\n   - Coldcard now supports spending from p2wpkh-p2sh,\n     fixed p2pkh signing for fw 1.1.0\n   - Archos Safe-T mini: fix #4726 signing issue\n   - KeepKey: full segwit support\n   - Trezor: refactoring and compat with python-trezor 0.11\n   - Digital BitBox: support firmware v5.0.0\n * fix bitcoin URI handling when app already running (#4796)\n * Qt listings rewritten:\n   the History tab now uses QAbstractItemModel, the other tabs use\n   QStandardItemModel. Performance should be better for large wallets.\n * Several other minor bugfixes and usability improvements.\n\n\n# Release 3.2.4 - (December 30, 2018)\n\n * backport anti-phishing measures from master\n\n\n# Release 3.2.3 - (September 3, 2018)\n\n * hardware wallet: the Safe-T mini from Archos is now supported.\n * hardware wallet: the Coldcard from Coinkite is now supported.\n * BIP39 seeds: if a seed extension (aka passphrase) contained\n   multiple consecutive whitespaces or leading/trailing whitespaces\n   then the derived addresses were not following spec. This has been\n   fixed, and affected should move their coins. The wizard will show a\n   warning in this case. (#4566)\n * Revealer: the PRNG used has been changed (#4649)\n * fix Linux distributables: 'typing' was not bundled, needed for python 3.4\n * fix #4626: fix spending from segwit multisig wallets involving a Trezor\n   cosigner when using a custom derivation path\n * fix #4491: on Android, if user had set \"uBTC\" as base unit, app crashed\n * fix #4497: on Android, paying bip70 invoices from cold start did not work\n * Several other minor bugfixes and usability improvements.\n\n\n# Release 3.2.2 - (July 2nd, 2018)\n\n * Fix DNS resolution on Windows\n * Fix websocket bug in daemon\n\n\n# Release 3.2.1 - (July 1st, 2018)\n\n * fix Windows binaries: due to build process changes, the locale files\n   were not included; the language could not be changed from English\n * fix Linux distributables: wordlists were not included (#4475)\n\n\n# Release 3.2.0 - Satoshi's Vision (June 30, 2018)\n\n * If present, libsecp256k1 is used to speed up elliptic curve\n   operations. The library is bundled in the Windows, MacOS, and\n   Android binaries. On Linux, it needs to be installed separately.\n * Two-factor authentication is available on Android. Note that this\n   will only provide additional security if one time passwords are\n   generated on a separate device.\n * Semi-automated crash reporting is implemented for Android.\n * Transactions that are dropped from the mempool are kept in the\n   wallet as 'local', and can be rebroadcast. Previously these\n   transactions were deleted from the wallet.\n * The scriptSig and witness part of transaction inputs are no longer\n   parsed, unless actually needed. The wallet will no longer display\n   'from' addresses corresponding to transaction inputs, except for\n   its own inputs.\n * The partial transaction format has been incompatibly changed. This\n   was needed as for partial transactions the scriptSig/witness has to\n   be parsed, but for signed transactions we did not want to do the\n   parsing.  Users should make sure that all instances of Electrum\n   they use to co-sign or offline sign, are updated together.\n * Signing of partial transactions created with online imported\n   addresses wallets now supports significantly more\n   setups. Previously only online p2pkh address + offline WIF was\n   supported.  Now the following setups are all supported:\n   - online {p2pkh, p2wpkh-p2sh, p2wpkh} address + offline WIF,\n   - online {p2pkh, p2wpkh-p2sh, p2wpkh} address + offline seed/xprv,\n   - online {p2sh, p2wsh-p2sh, p2wsh}-multisig address + offline seeds/xprvs\n     (potentially distributed among several different machines)\n   Note that for the online address + offline HD secret case, you need\n   the offline wallet to recognize the address (i.e. within gap\n   limit).  Having an xpub on the online machine is still the\n   recommended setup, as this allows the online machine to generate\n   new addresses on demand.\n * Segwit multisig for bip39 and hardware wallets is now enabled.\n   (both p2wsh-p2sh and native p2wsh)\n * Ledger: offline signing for segwit inputs (#3302) This has already\n   worked for Trezor and Digital Bitbox. Offline segwit signing can be\n   combined with online imported addresses wallets.\n * Added Revealer plugin. ( https://revealer.cc ) Revealer is a seed\n   phrase back-up solution. It allows you to create a cold, analog,\n   multi-factor backup of your wallet seeds, or of any arbitrary\n   secret. The Revealer utilizes a transparent plastic visual one time\n   pad.\n * Fractional fee rates: the Qt GUI now displays fee rates with 0.1\n   sat/byte precision, and also allows this same resolution in the\n   Send tab.\n * Hardware wallets: a \"show address\" button is now displayed in the\n   Receive tab of the Qt GUI. (#4316)\n * Trezor One: implemented advanced/matrix recovery (#4329)\n * Qt/Kivy: added \"sat\" as optional base unit.\n * Kivy GUI: significant performance improvements when displaying\n   history and address list of large wallets; and transaction dialog\n   of large transactions.\n * Windows: use dnspython to resolve dns instead of socket.getaddrinfo\n   (#4422)\n * Importing minikeys: use uncompressed pubkey instead of compressed\n   (#4384)\n * SPV proofs: check inner nodes not to be valid transactions (#4436)\n * Qt GUI: there is now an optional \"dark\" theme (#4461)\n * Several other minor bugfixes and usability improvements.\n\n\n# Release 3.1.3 - (April 16, 2018)\n\n * Qt GUI: seed word auto-complete during restore\n * Android: fix some crashes\n * performance improvements (wallet, and Qt GUI)\n * hardware wallets: show debug message during device scan\n * Digital Bitbox: enabled BIP84 (p2wpkh) wallet creation\n * add regtest support (via --regtest flag)\n * other minor bugfixes and usability improvements\n\n# Release 3.1.2 - (March 28, 2018)\n\n * Kivy/android: request PIN on startup\n * Improve OSX build process\n * Fix various bugs with hardware wallets\n * Other minor bugfixes\n\n# Release 3.1.1 - (March 12, 2018)\n\n * fix #4031: Trezor T support\n * partial fix #4060: proxy and hardware wallet can't be used together\n * fix #4039: can't set address labels\n * fix crash related to coinbase transactions\n * MacOS: use internal graphics card\n * fix openalias related crashes\n * speed-up capital gains calculations\n * hw wallet encryption: re-prompt for passphrase if incorrect\n * other minor fixes.\n\n\n\n# Release 3.1.0 - (March 5, 2018)\n\n * Memory-pool based fee estimation. Dynamic fees can target a desired\n   depth in the memory pool. This feature is optional, and ETA-based\n   estimates from Bitcoin Core are still available. Note that miners\n   could exploit this feature, if they conspired and filled the memory\n   pool with expensive transactions that never get mined. However,\n   since the Electrum client already trusts an Electrum server with\n   fee estimates, activating this feature does not introduce any new\n   vulnerability. In addition, the client uses a hard threshold to\n   protect itself from servers sending excessive fee estimates. In\n   practice, ETA-based estimates have resulted in sticky fees, and\n   caused many users to overpay for transactions. Advanced users tend\n   to visit (and trust) websites that display memory-pool data in\n   order to set their fees.\n * Capital gains: For each outgoing transaction, the difference\n   between the acquisition and liquidation prices of outgoing coins is\n   displayed in the wallet history. By default, historical exchange\n   rates are used to compute acquisition and liquidation prices. These\n   values can also be entered manually, in order to match the actual\n   price realized by the user. The order of liquidation of coins is\n   the natural order defined by the blockchain; this results in\n   capital gain values that are invariant to changes in the set of\n   addresses that are in the wallet. Any other ordering strategy (such\n   as FIFO, LIFO) would result in capital gain values that depend on\n   the presence of other addresses in the wallet.\n * Local transactions: Transactions can be saved in the wallet without\n   being broadcast. The inputs of local transactions are considered as\n   spent, and their change outputs can be re-used in subsequent\n   transactions. This can be combined with cold storage, in order to\n   create several transactions before broadcasting them. Outgoing\n   transactions that have been removed from the memory pool are also\n   saved in the wallet, and can be broadcast again.\n * Checkpoints: The initial download of a headers file was replaced\n   with hardcoded checkpoints. The wallet uses one checkpoint per\n   retargeting period. The headers for a retargeting period are\n   downloaded only if transactions need to be verified in this period.\n * The 'privacy' and 'priority' coin selection policies have been\n   merged into one. Previously, the 'privacy' policy has been unusable\n   because it was was not prioritizing confirmed coins. The new policy\n   is similar to 'privacy', except that it de-prioritizes addresses\n   that have unconfirmed coins.\n * The 'Send' tab of the Qt GUI displays how transaction fees are\n   computed from transaction size.\n * The wallet history can be filtered by time interval.\n * Replace-by-fee is enabled by default. Note that this might cause\n   some issues with wallets that do not display RBF transactions until\n   they are confirmed.\n * Watching-only wallets and hardware wallets can be encrypted.\n * Semi-automated crash reporting\n * The SSL checkbox option was removed from the GUI.\n * The Trezor T hardware wallet is now supported.\n * BIP84: native segwit p2wpkh scripts for bip39 seeds and hardware\n   wallets can now be created when specifying a BIP84 derivation\n   path. This is usable with Trezor and Ledger.\n * Windows: the binaries now include ZBar, and QR code scanning should work.\n * The Wallet Import Format (WIF) for private keys that was extended in 3.0\n   is changed. Keys in the previous format can be imported, compatibility\n   is maintained. Newly exported keys will be serialized as\n   \"script_type:original_wif_format_key\".\n * BIP32 master keys for testnet once again have different version bytes than\n   on mainnet. For the mainnet prefixes {x,y,Y,z,Z}|{pub,prv}, the\n   corresponding testnet prefixes are   {t,u,U,v,V}|{pub,prv}.\n   More details and exact version bytes are specified at:\n   https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst\n   Note that due to this change, testnet wallet files created with previous\n   versions of Electrum must be considered broken, and they need to be\n   recreated from seed words.\n * A new version of the Electrum protocol is required by the client\n   (version 1.2). Servers using older versions of the protocol will\n   not be displayed in the GUI.\n\n\n# Release 3.0.6 :\n  * Fix transaction parsing bug #3788\n\n# Release 3.0.5 : (Security update)\n\nThis is a follow-up to the 3.0.4 release, which did not completely fix\nissue #3374. Users should upgrade to 3.0.5.\n\n * The JSONRPC interface is password protected\n * JSONRPC commands are disabled if the GUI is running, except 'ping',\n   which is used to determine if a GUI is already running\n\n\n# Release 3.0.4 : (Security update)\n\n * Fix a vulnerability caused by Cross-Origin Resource Sharing (CORS)\n   in the JSONRPC interface. Previous versions of Electrum are\n   vulnerable to port scanning and deanonimization attacks from\n   malicious websites. Wallets that are not password-protected are\n   vulnerable to theft.\n * Bundle QR scanner with Android app\n * Minor bug fixes\n\n# Release 3.0.3\n  * Qt GUI: sweeping now uses the Send tab, allowing fees to be set\n  * Windows: if using the installer binary, there is now a separate shortcut\n    for \"Electrum Testnet\"\n  * Digital Bitbox: added support for p2sh-segwit\n  * OS notifications for incoming transactions\n  * better transaction size estimation:\n    - fees for segwit txns were somewhat underestimated (#3347)\n    - some multisig txns were underestimated\n    - handle uncompressed pubkeys\n  * fix #3321: testnet for Windows binaries\n  * fix #3264: Ledger/dbb signing on some platforms\n  * fix #3407: KeepKey sending to p2sh output\n  * other minor fixes and usability improvements\n\n# Release 3.0.2\n  * Android: replace requests tab with address tab, with access to\n    private keys\n  * sweeping minikeys: search for both compressed and uncompressed\n    pubkeys\n  * fix wizard crash when attempting to reset Google Authenticator\n  * fix #3248: fix Ledger+segwit signing\n  * fix #3262: fix SSL payment request signing\n  * other minor fixes.\n\n# Release 3.0.1\n  * minor bug and usability fixes\n\n# Release 3.0 - Uncanny Valley (November 1st, 2017)\n\n  * The project was migrated to Python3 and Qt5. Python2 is no longer\n    supported. If you cloned the source repository, you will need to\n    run \"python3 setup.py install\" in order to install the new\n    dependencies.\n\n  * Segwit support:\n\n    - Native segwit scripts are supported using a new type of\n      seed. The version number for segwit seeds is 0x100. The install\n      wizard will not create segwit seeds by default; users must\n      opt-in with the segwit option.\n\n    - Native segwit scripts are represented using bech32 addresses,\n      following BIP173. Please note that BIP173 is still in draft\n      status, and that other wallets/websites may not support\n      it. Thus, you should keep a non-segwit wallet in order to be\n      able to receive bitcoins during the transition period. If BIP173\n      ends up being rejected or substantially modified, your wallet\n      may have to be restored from seed. This will not affect funds\n      sent to bech32 addresses, and it will not affect the capacity of\n      Electrum to spend these funds.\n\n    - Segwit scripts embedded in p2sh are supported with hardware\n      wallets or bip39 seeds. To create a segwit-in-p2sh wallet,\n      trezor/ledger users will need to enter a BIP49 derivation path.\n\n    - The BIP32 master keys of segwit wallets are serialized using new\n      version numbers. The new version numbers encode the script type,\n      and they result in the following prefixes:\n\n         * xpub/xprv : p2pkh or p2sh\n         * ypub/yprv : p2wpkh-in-p2sh\n         * Ypub/Yprv : p2wsh-in-p2sh\n         * zpub/zprv : p2wpkh\n         * Zpub/Zprv : p2wsh\n\n      These values are identical for mainnet and testnet; tpub/tprv\n      prefixes are no longer used in testnet wallets.\n\n    - The Wallet Import Format (WIF) is similarly extended for segwit\n      scripts. After a base58-encoded key is decoded to binary, its\n      first byte encodes the script type:\n\n         * 128 + 0: p2pkh\n         * 128 + 1: p2wpkh\n         * 128 + 2: p2wpkh-in-p2sh\n         * 128 + 5: p2sh\n         * 128 + 6: p2wsh\n         * 128 + 7: p2wsh-in-p2sh\n\n      The distinction between p2sh and p2pkh in private key means that\n      it is not possible to import a p2sh private key and associate it\n      to a p2pkh address.\n\n  * A new version of the Electrum protocol is required by the client\n    (version 1.1). Servers using older versions of the protocol will\n    not be displayed in the GUI.\n\n  * By default, transactions are time-locked to the height of the\n    current block. Other values of locktime may be passed using the\n    command line.\n\n\n# Release 2.9.4 (security update)\n  * Backport security fixes from 3.0.5 after vulnerability was\n    discovered in JSONRPC interface.\n\n# Release 2.9.3\n  * fix configuration file issue #2719\n  * fix ledger signing of non-RBF transactions\n  * disable 'spend confirmed only' option by default\n\n# Release 2.9.2\n  * force headers download if headers file is corrupted\n  * add websocket to windows builds\n\n# Release 2.9.1\n  * fix initial headers download\n  * validate contacts on import\n  * command-line option for locktime\n\n# Release 2.9 - Independence (July 27th, 2017)\n  * Multiple Chain Validation: Electrum will download and validate\n    block headers sent by servers that may follow different branches\n    of a fork in the Bitcoin blockchain. Instead of a linear sequence,\n    block headers are organized in a tree structure. Branching points\n    are located efficiently using binary search. The purpose of MCV is\n    to detect and handle blockchain forks that are invisible to the\n    classical SPV model.\n  * The desired branch of a blockchain fork can be selected using the\n    network dialog. Branches are identified by the hash and height of\n    the diverging block. Coin splitting is possible using RBF\n    transaction (a tutorial will be added).\n  * Multibit support: If the user enters a BIP39 seed (or uses a\n    hardware wallet), the full derivation path is configurable in the\n    install wizard.\n  * Option to send only confirmed coins\n  * Qt GUI:\n    - Network dialog uses tabs and gets updated by network events.\n    - The gui tabs use icons\n  * Kivy GUI:\n    - separation between network dialog and wallet settings dialog.\n    - option for manual server entry\n    - proxy configuration\n  * Daemon: The wallet password can be passed as parameter to the\n    JSONRPC API.\n  * Various other bugfixes and improvements.\n\n\n# Release 2.8.3\n  * Fix crash on reading older wallet formats.\n  * TrustedCoin: remove pay-per-tx option\n\n# Release 2.8.2\n  * show paid invoices in history tab\n  * improve CPFP dialog\n  * fixes for trezor, keepkey\n  * other minor bugfixes\n\n# Release 2.8.1\n  * fix Digital Bitbox plugin\n  * fix daemon jsonrpc\n  * fix trustedcoin wallet creation\n  * other minor bugfixes\n\n# Release 2.8.0 (March 9, 2017)\n  * Wallet file encryption using ECIES: A keypair is derived from the\n    wallet password. Once the wallet is decrypted, only the public key\n    is retained in memory, in order to save the encrypted file.\n  * The daemon requires wallets to be explicitly loaded before\n    commands can use them. Wallets can be loaded using: 'electrum\n    daemon load_wallet [-w path]'. This command will require a\n    password if the wallet is encrypted.\n  * Invoices and contacts are stored in the wallet file and are no\n    longer shared between wallets. Previously created invoices and\n    contacts files may be imported from the menu.\n  * Fees improvements:\n    - Dynamic fees are enabled by default.\n    - Child Pays For Parent (CPFP) dialog in the GUI.\n    - RBF is automatically proposed for low fee transactions.\n  * Support for Segregated Witness (testnet only).\n  * Support for Digital Bitbox hardware wallet.\n  * The GUI shows a blue icon when connected using a proxy.\n\n# Release 2.7.18\n  * enforce https on exchange rate APIs\n  * use hardcoded list of exchanges\n  * move 'Freeze' menu to Coins (utxo) tab\n  * various bugfixes\n\n# Release 2.7.17\n  * fix a few minor regressions in the Qt GUI\n\n# Release 2.7.16\n  * add Testnet support (fix #541)\n  * allow daemon to be launched in the foreground (fix #1873)\n  * Qt: use separate tabs for addresses and UTXOs\n  * Qt: update fee slider with a network callback\n  * Ledger: new ui and mobile 2fa validation (neocogent)\n\n# Release 2.7.15\n  * Use fee slider for both static and dynamic fees.\n  * Add fee slider to RBF dialog (fix #2083).\n  * Simplify fee preferences.\n  * Critical: Fix password update issue (#2097). This bug prevents\n    password updates in multisig and 2FA wallets. It may also cause\n    wallet corruption if the wallet contains several master private\n    keys (such as 2FA wallets that have been restored from\n    seed). Affected wallets will need to be restored again.\n\n# Release 2.7.14\n  * Merge exchange_rate plugin with main code\n  * Faster synchronization and transaction creation\n  * Fix bugs #2096, #2016\n\n# Release 2.7.13\n  * fix message signing with imported keys\n  * add size to transaction details window\n  * move plot plugin to main code\n  * minor bugfixes\n\n# Release 2.7.12\n  various bugfixes\n\n# Release 2.7.11\n  * fix offline signing (issue #195)\n  * fix android crashes caused by threads\n\n# Release 2.7.10\n  * various fixes for hardware wallets\n  * improve fee bumping\n  * separate sign and broadcast buttons in Qt tx dialog\n  * allow spaces in private keys\n\n# Release 2.7.9\n  * Fix a bug with the ordering of pubkeys in recent multisig wallets.\n    Affected wallets will regenerate their public keys when opened for\n    the first time. This bug does not affect address generation.\n  * Fix hardware wallet issues #1975, #1976\n\n# Release 2.7.8\n  * Fix a bug with fee bumping\n  * Fix crash when parsing request (issue #1969)\n\n# Release 2.7.7\n  * Fix utf8 encoding bug with old wallet seeds (issue #1967)\n  * Fix delete request from menu (issue #1968)\n\n# Release 2.7.6\n * Fixes a critical bug with imported private keys (issue #1966). Keys\n   imported in Electrum 2.7.x were not encrypted, even if the wallet\n   had a password. If you imported private keys using Electrum 2.7.x,\n   you will need to import those keys again. If you imported keys in\n   2.6 and converted with 2.7.x, you don't need to do anything, but\n   you still need to upgrade in order to be able to spend.\n * Wizard: Hide seed options in a popup dialog.\n\n# Release 2.7.5\n * Add number of confirmations to request status. (issue #1757)\n * In the GUI, refer to passphrase as 'seed extension'.\n * Fix bug with utf8 encoded passphrases.\n * Kivy wizard: add a dialog for seed options.\n * Kivy wizard: add current word to suggestions, because some users\n   don't see the space key.\n\n# Release 2.7.4\n * Fix private key import in wizard\n * Fix Ledger display (issue #1961)\n * Fix old watching-only wallets (issue #1959)\n * Fix Android compatibility (issue #1947)\n\n# Release 2.7.3\n * fix Trezor and Keepkey support in Windows builds\n * fix sweep private key dialog\n * minor fixes: #1958, #1959\n\n# Release 2.7.2\n * fix bug in password update (issue #1954)\n * fix fee slider (issue #1953)\n\n# Release 2.7.1\n * fix wizard crash with old seeds\n * fix issue #1948: fee slider\n\n# Release 2.7.0 (Oct 2 2016)\n\n * The wallet file format has been upgraded. This upgrade is not\n   backward compatible, which means that a wallet upgraded to the 2.7\n   format will not be readable by earlier versions of\n   Electrum. Multiple accounts inside the same wallet are not\n   supported in the new format; the Qt GUI will propose to split any\n   wallet that has several accounts. Make sure that you have saved\n   your seed phrase before you upgrade Electrum.\n * This version introduces a separation between wallets types and\n   keystores types. 'Wallet type' defines the type of Bitcoin contract\n   used in the wallet, while 'keystore type' refers to the method used\n   to store private keys. Therefore, so-called 'hardware wallets' will\n   be referred to as 'hardware keystores'.\n * Hardware keystores:\n   - The Ledger Nano S is supported.\n   - Hardware keystores can be used as cosigners in multi-signature\n     wallets.\n   - Multiple hardware cosigners can be used in the same multisig\n     wallet. One icon per keystore is displayed in the satus bar. Each\n     connected device will co-sign the transaction.\n * Replace-By-Fee: RBF transactions are supported in both Qt and\n   Android. A warning is displayed in the history for transactions\n   that are replaceable, have unconfirmed parents, or that have very\n   low fees.\n * Dynamic fees: Dynamic fees are enabled by default. A slider allows\n   the user to select the expected confirmation time of their\n   transaction. The expected confirmation times of incoming\n   transactions is also displayed in the history.\n * The install wizards of Qt and Kivy have been unified.\n * Qt GUI (Desktop):\n   - A fee slider is visible in the in send tab\n   - The Address tab is hidden by default, can be shown with Ctrl-A\n   - UTXOs are displayed in the Address tab\n * Kivy GUI (Android):\n   - The GUI displays the complete transaction history.\n   - Multisig wallets are supported.\n   - Wallets can be created and deleted in the GUI.\n * Seed phrases can be extended with a user-chosen passphrase. The\n   length of seed phrases is standardized to 12 words, using 132 bits\n   of entropy (including 2FA seeds). In the wizard, the type of the\n   seed is displayed in the seed input dialog.\n * TrustedCoin users can request a reset of their Google Authenticator\n   account, if they still have their seed.\n\n\n# Release 2.6.4 (bugfixes)\n * fix coinchooser bug (#1703)\n * fix daemon JSONRPC (#1731)\n * fix command-line broadcast (#1728)\n * QT: add colors to labels\n\n# Release 2.6.3 (bugfixes)\n * fix command line parsing of transactions\n * fix signtransaction --privkey (#1715)\n\n# Release 2.6.2 (bugfixes)\n * fix Trustedcoin restore from seed (bug #1704)\n * small improvements to kivy GUI\n\n# Release 2.6.1 (bugfixes)\n * fix broadcast command (bug #1688)\n * fix tx dialog (bug #1690)\n * kivy: support old-type seed phrases in wizard\n\n# Release 2.6\n * The source code is relicensed under the MIT Licence\n * First official release of the Kivy GUI, with android APK\n * The old 'android' and 'gtk' GUIs are deprecated\n * Separation between plugins and GUIs\n * The command line uses jsonrpc to communicate with the daemon\n * New command: 'notify <address> <url>'\n * Alternative coin selection policy, designed to help preserve user\n   privacy. Enable it by setting the Coin Selection preference to\n   Privacy.\n * The install wizard has been rewritten and improved\n * Support minikeys as used in Casascius coins for private key import\n   and sweeping\n * Much improved support for TREZOR and KeepKey devices:\n   - full device information display\n   - initialize a new or wiped device in 4 ways:\n     1) device generates a new wallet\n     2) you enter a seed\n     3) you enter a BIP39 mnemonic to generate the seed\n     4) you enter a master private key\n   - KeepKey secure seed recovery (KeepKey only)\n   - change / set / disable PIN\n   - set homescreen (TREZOR only)\n   - set a session timeout.  Once a session has timed out, further use\n     of the device requires your PIN and passhphrase to be re-entered\n   - enable / disable passphrases\n   - device wipe\n   - multiple device support\n\n# Release 2.5.4\n * increase MIN_RELAY_TX_FEE to avoid dust transactions\n\n# Release 2.5.3 (bugfixes)\n * installwizard: do not allow direct copy-paste of the seed\n * installwizard: fix bug #1531 (starting offline)\n\n# Release 2.5.2 (bugfixes)\n * fix bug #1513 (client tries to broadcast transaction while not connected)\n * fix synchronization bug (#1520)\n * fix command line bug (#1494)\n * fixes for exchange rate plugin\n\n# Release 2.5.1 (bugfixes)\n * signatures in transactions were still using the old class\n * make sure that setup.py uses python2\n * fix wizard crash with trustedcoin plugin\n * fix socket infinite loop\n * fix history bug #1479\n\n# Release 2.5\n * Low-S values are used in signatures (BIP 62).\n * The Kivy GUI has been merged into master.\n * The Qt GUI supports multiple windows in the same process. When a\n   new Electrum instance is started, it checks for an already running\n   Electrum process, and connects to it.\n * The network layer uses select(), so all server communication is\n   handled by a single thread. Moreover, the synchronizer, verifier,\n   and exchange rate plugin now run as separate jobs within the\n   networking thread instead of as their own threads.\n * Plugins are revamped, particularly the exchange rate plugin.\n\n# Release 2.4.4\n * Fix bug with TrustedCoin plugin\n\n# Release 2.4.3\n * Support for KeepKey hardware wallet\n * Simplified Chinese wordlist\n * Minor bugfixes and GUI tweaks\n\n# Release 2.4.2\n * Command line can read arguments from stdin (pipe)\n * Speedup fee computation for large transactions\n * Various bugfixes\n\n# Release 2.4.1\n * Use ssl.PROTOCOL_TLSv1\n * Fix DNSSEC issues with ECDSA signatures\n * Replace TLSLite dependency with minimal RSA implementation\n * Dynamic Fees: using estimatefee value returned by server\n * Various GUI improvements\n\n# Release 2.4\n * Payment to DNS names storing a Bitcoin addresses (OpenAlias) is\n   supported directly, without activating a plugin. The verification\n   uses DNSSEC.\n * The DNSSEC verification code was rewritten. The previous code,\n   which was part of the OpenAlias plugin, is vulnerable and should\n   not be trusted (Electrum 2.0 to 2.3).\n * Payment requests can be signed using Bitcoin addresses stored\n   in DNS (OpenAlias). The identity of the requestor is verified using\n   DNSSEC.\n * Payment requests signed with OpenAlias keys can be shared as\n   bitcoin: URIs, if they are simple (a single address-type\n   output). The BIP21 URI scheme is extended with 'name', 'sig',\n   'time', 'exp'.\n * Arbitrary m-of-n multisig wallets are supported (n<=15).\n * Multisig transactions can be signed with TREZOR. When you create\n   the multisig wallet, just enter the xpub of your existing TREZOR\n   wallet.\n * Transaction fees set manually in the GUI are retained, including\n   when the user uses the '!' shortcut.\n * New 'email' plugin, that enables sending and receiving payment\n   requests by email.\n * The daemon supports Websocket notifications of payments.\n\n# Release 2.3.3\n * fix proxy settings (issue #1309)\n * improvements to the transaction dialog:\n    - request password after showing transaction\n    - show change addresses in yellow color\n\n# Release 2.3.2\n * minor bugfixes\n * updated ledger plugin\n * sort inputs/outputs lexicographically (BIP-LI01)\n\n# Release 2.3.1\n * patch a bug with payment requests\n\n# Release 2.3\n * Improved logic for the network layer.\n * More efficient coin selection. Spend oldest coins first, and\n   minimize the number of transaction inputs.\n * Plugins are loaded independently of the GUI. As a result, Openalias,\n   TrustedCoin and TREZOR wallets can be used with the command\n   line. Example: 'electrum payto <openalias> <amount>'\n * The command line has been refactored:\n  - Arguments are parsed with argparse.\n  - The inline help includes a description of options.\n  - Some commands have been renamed. Notably, 'mktx' and 'payto' have\n    been merged into a single command, with a --broadcast option.\n   Type 'electrum --help' for a complete overview.\n * The command line accepts the '!' syntax to send the maximum\n   amount available. It can be combined with the '--from' option.\n   Example: 'payto <destination> ! --from <from_address>'\n * The command line also accepts a '?' shortcut for private keys\n   arguments, that triggers a prompt.\n * Payment requests can be managed with the command line, using the\n   following commands: 'addrequest', 'rmrequest', 'listrequests'.\n   Payment requests can be signed with a SSL certificate, and published\n   as bip70 files in a public web directory. To see the relevant\n   configuration variables, type 'electrum addrequest --help'\n * Commands can be called with jsonrpc, using the 'jsonrpc' gui. The\n   jsonrpc interface may be called by php.\n\n# Release 2.2\n * Show amounts (thousands separators and decimal point)\n   according to locale in GUI\n * Show unmatured coins in balance\n * Fix exchange rates plugin\n * Network layer: refactoring and fixes\n\n# Release 2.1.1\n * patch a bug that prevents new wallet creation.\n * fix connection issue on osx binaries\n\n# Release 2.1\n * Faster startup, thanks to the following optimizations:\n   1. Transaction input/outputs are cached in the wallet file\n   2. Fast X509 certificate parser, not using pyasn1 anymore.\n   3. The Label Sync plugin only requests modified labels.\n * The 'Invoices' and 'Send' tabs have been merged.\n * Contacts are stored in a separate file, shared between wallets.\n * A Search Box is available in the GUI (Ctrl-S)\n * Payment requests have an expiration date and can be exported to\n   BIP70 files.\n * file: scheme support in BIP72 URIs: \"bitcoin:?r=file:///...\"\n * Own addresses are shown in green in the Transaction dialog.\n * Address History dialog.\n * The OpenAlias plugin was improved.\n * Various bug fixes and GUI improvements.\n * A new LabelSync backend is being used an import of the old\n   database was made but since the release came later it's\n   recommended that you do a full push when you upgrade.\n\n# Release 2.0.4 - Minor GUI improvements\n * The password dialog will ask for password again if the user enters\n   a wrong password\n * The Master Public Key dialog displays which keys belong to the\n   wallet, and which are cosigners\n * The transaction dialog will ask to save unsaved transaction\n   received from cosigner pool, when user clicks on 'Close'\n * The multisig restore dialog accepts xprv keys.\n * The network daemon must be started explicitly before using commands\n   that require a connection\n   Example:\n     electrum daemon start\n     electrum getaddressunspent <addr>\n     electrum daemon status\n     electrum daemon stop\n   If a daemon is running, the GUI will use it.\n\n# Release 2.0.3 - bugfixes and minor GUI improvements\n * Do not use daemon threads (fix #960)\n * Add a zoom button to receive tab\n * Add exchange rate conversion to receive tab\n * Use Tor's default port number in default proxy config\n\n# Release 2.0.2 - bugfixes\n * Fix transaction sweep (#1066)\n * Fix thread timing bug (#1054)\n\n# Release 2.0.1 - bugfixes\n * Fix critical bug in TREZOR address derivation: passphrases were not\n   NFKD normalized. TREZOR users who created a wallet protected by a\n   passphrase containing utf-8 characters with diacritics are\n   affected. These users will have to open their wallet with version\n   2.0 and to move their funds to a new wallet.\n * Use a file socket for the daemon (fixes network dialog issues)\n * Fix crash caused by QR scanner icon when zbar not installed.\n * Fix CosignerPool plugin\n * Label Sync plugin: Fix label sharing between multisig wallets\n\n\n# Release 2.0\n\n * Before you upgrade, make sure you have saved your wallet seed on\n   paper.\n\n * Documentation is now hosted on a wiki: http://electrum.orain.org\n\n * New seed derivation method (not compatible with BIP39). The seed\n   phrase includes a version number, that refers to the wallet\n   structure. The version number also serves as a checksum, and it\n   will prevent the import of seeds from incompatible wallets. Old\n   Electrum seeds are still supported.\n\n * New address derivation (BIP32). Standard wallets are single account\n   and use a gap limit of 20.\n\n * Support for Multisig wallets using parallel BIP32 derivations and\n   P2SH addresses (\"2 of 2\", \"2 of 3\").\n\n * Compact serialization format for unsigned or partially signed\n   transactions, that includes the BIP32 master public key and\n   derivation needed to sign inputs. Serialized transactions can be\n   sent to cosigners or to cold storage using QR codes (using Andreas\n   Schildbach's base 43 idea).\n\n * Support for BIP70 payment requests:\n   - Verification of the chain of signatures uses tlslite.\n   - In the GUI, payment requests are shown in the 'Invoices' tab.\n\n * Support for hardware wallets: TREZOR (SatoshiLabs) and Btchip (Ledger).\n\n * Two-factor authentication service by TrustedCoin. This service uses\n   \"2 of 3\" multisig wallets and Google Authenticator. Note that\n   wallets protected by this service can be deterministically restored\n   from seed, without Trustedcoin's server.\n\n * Cosigner Pool plugin: encrypted communication channel for multisig\n   wallets, to send and receive partially signed transactions.\n\n * Audio Modem plugin: send and receive transactions by sound.\n\n * OpenAlias plugin: send bitcoins to aliases verified using DNSSEC.\n\n * New 'Receive' tab in the GUI:\n   - create and manage payment requests, with QR Codes\n   - the former 'Receive' tab was renamed to 'Addresses'\n   - the former Point of Sale plugin is replaced by a resizable\n     window that pops up if you click on the QR code\n\n * The 'Send' tab in the Qt GUI supports transactions with multiple\n   outputs, and raw hexadecimal scripts.\n\n * The GUI can connect to the Electrum daemon: \"electrum -d\" will\n   start the daemon if it is not already running, and the GUI will\n   connect to it. The daemon can serve several clients. It times out\n   if no client uses if for more than 5 minutes.\n\n * The install wizard can be used to import addresses or private\n   keys. A watching-only wallet is created by entering a list of\n   addresses in the wizard dialog.\n\n * New file format: Wallets files are saved as JSON. Note that new\n   wallet files cannot be read by older versions of Electrum. Old\n   wallet files will be converted to the new format; this operation\n   may take some time, because public keys will be derived for each\n   address of your wallet.\n\n * The client accepts servers with a CA-signed SSL certificate.\n\n * ECIES encrypt/decrypt methods, available in the GUI and using\n   the command line:\n      encrypt <pubkey> <message>\n      decrypt <pubkey> <message>\n\n * The Android GUI has received various updates and it is much more\n   stable. Another script was added to Android, called Authenticator,\n   that works completely offline: it reads an unsigned transaction\n   shown as QR code, signs it and shows the result as a QR code.\n\n\n# Release 1.9.8\n\n* Electrum servers were upgraded to version 0.9. The new server stores\n  a Patrica tree of all UTXOs, an idea proposed by Alan Reiner in the\n  bitcointalk forum. This property allows the client to directly\n  request the balance of any address. The new commands are:\n     1. getaddressbalance <address>\n     2. getaddressunspent <address>\n     3. getutxoaddress <txid> <pos>\n\n* Command-line commands that require a connection to the network spawn\n  a daemon, that remains connected and handles subsequent\n  commands. The daemon terminates itself if it remains unused for more\n  than one minute. The purpose of this is to make scripting more\n  efficient. For example, a bash script using many electrum commands\n  will open only one connection.\n\n# Release 1.9.7\n* Fix for offline signing\n* Various bugfixes\n* GUI usability improvements\n* Coinbase Buyback plugin\n\n# Release 1.9.6\n* During wallet creation, do not write seed to disk until it is encrypted.\n* Confirmation dialog if the transaction fee is higher than 1mBTC.\n* bugfixes\n\n# Release 1.9.5\n\n* Coin control: select addresses to send from\n* Put addresses that have been used in a minimized section (Qt GUI)\n* Allow non ascii chars in passwords\n\n\n# Release 1.9.4\nbugfixes: offline transactions\n\n# Release 1.9.3\nbugfixes: connection problems, transactions staying unverified\n\n# Release 1.9.2\n* fix a syntax error\n\n# Release 1.9.1\n* fix regression with --offline mode\n* fix regression with --portable mode: use a dedicated directory\n\n# Release 1.9\n\n* The client connects to multiple servers in order to retrieve block headers and find the longest chain\n* SSL certificate validation (to prevent MITM)\n* Deterministic signatures (RFC 6979)\n* Menu to create/restore/open wallets\n* Create transactions with multiple outputs from CSV (comma separated values)\n* New text gui: stdio\n* Plugins are no longer tied to the qt GUI, they can reach all GUIs\n* Proxy bugs have been fixed\n\n\n# Release 1.8.1\n\n* Notification option when receiving new transactions\n* Confirm dialogue before sending large amounts\n* Alternative datafile location for non-windows systems\n* Fix offline wallet creation\n* Remove enforced tx fee\n* Tray icon improvements\n* Various bugfixes\n\n\n# Release 1.8\n\n* Menubar in classic gui\n* Updated the QR Code plugin to enable offline/online wallets to transmit unsigned/signed transactions via QR code.\n* Fixed bug where never-confirmed transactions prevented further spending\n\n\n# Release 1.7.4\n\n* Increase default fee\n* fix create and restore in command line\n* fix verify message in the gui\n\n\n# Release 1.7.3:\n\n* Classic GUI can display amounts in mBTC\n* Account selector in the classic GUI\n* Changed the way the portable flag uses without supplying a -w argument\n* Classic GUI asks users to enter their seed on wallet creation\n\n\n# Release 1.7.2:\n\n* Transactions that are in the same block are displayed in chronological order in the history.\n* The client computes transaction priority and rejects zero-fee transactions that need a fee.\n* The default fee was lowered to 200 uBTC per kb.\n* Due to an internal format change, your history may be pruned when\n  you open your wallet for the first time after upgrading to 1.7.2. If\n  this is the case, please visit a full server to restore your full\n  history. You will only need to do that once.\n\n\n# Release 1.7.1:  bugfixes.\n\n\n# Release 1.7\n\n* The Classic GUI can be extended with plugins. Developers who want to\nadd new features or third-party services to Electrum are invited to\nwrite plugins. Some previously existing and non-essential features of\nElectrum (point-of-sale mode, qrcode scanner) were removed from the\ncore and are now available as plugins.\n\n* The wallet waits for 2 confirmations before creating new\naddresses. This makes recovery from seed more robust. Note that it\nmight create unwanted gaps if you use Electrum 1.7 together with older\nversions of Electrum.\n\n* An interactive Python console replaces the 'Wall' tab. The provided\npython environment gives users access to the wallet and gui. Most\nelectrum commands are available as python function in the\nconsole. Custom scripts an be loaded with a \"run(filename)\"\ncommand. Tab-completions are available.\n\n* The location of the Electrum folder in Windows changed from\nLOCALAPPDATA to APPDATA. Discussion on this topic can be found here:\nhttps://bitcointalk.org/index.php?topic=144575.0\n\n* Private keys can be exported from within the classic GUI:\n  For a single address, use the address menu (right-click).\n  To export the keys of your entire wallet, use the settings dialog (import/export tab).\n\n* It is possible to create, sign and redeem multisig transaction using the\ncommand line interface.  This is made possible by the following new commands:\n    dumpprivkey, listunspent, createmultisig, createrawtransaction, decoderawtransaction, signrawtransaction\nThe syntax of these commands is similar to their bitcoind counterpart.\nFor an example, see Gavin's tutorial: https://gist.github.com/gavinandresen/3966071\n\n* Offline wallets now work in a way similar to Armory:\n  1. user creates an unsigned transaction using the online (watching-only) wallet.\n  2. unsigned transaction is copied to the offline computer, and signed by the offline wallet.\n  3. signed transaction is copied to the online computer, broadcasted by the online client.\n  4. All these steps can be done via the command line interface or the classic GUI.\n\n* Many command line commands have been renamed in order to make the syntax consistent with bitcoind.\n\n# Release 1.6.2\n\n== Classic GUI\n* Added new version notification\n\n# Release 1.6.1 (11-01-2013)\n\n== Core\n* It is now possible to restore a wallet from MPK (this will create a watching-only wallet)\n* A switch button allows to easily switch between Lite and Classic GUI.\n\n== Classic GUI\n* Seed and MPK help dialogs were rewritten\n* Point of Sale: requested amounts can be expressed in other currencies and are converted to bitcoin.\n\n== Lite GUI\n* The receiving button was removed in favor of a menu item to keep it consistent with the history toggle.\n\n# Release 1.6.0 (07-01-2013)\n\n== Core\n* (Feature) Add support for importing, signing and verifiying compressed keys\n* (Feature) Auto reconnect to random server on disconnect\n* (Feature) Ultimate fallback to HTTP port 80 if TCP doesn't work on any server\n* (Bug) Under rare circumstances changing password with incorrect password could damage wallet\n\n== Lite GUI\n* (Chore) Use blockchain.info for exchange rate data\n* (Feature) added currency conversion for BRL, CNY, RUB\n* (Feature) Saraha theme\n* (Feature) csv import/export for transactions including labels\n\n== Classic GUI\n* (Chore) pruning servers now called \"p\", full servers \"f\" to avoid confusion with terms\n* (Feature) Debits in history shown in red\n* (Feature) csv import/export for transactions including labels\n\n# Release 1.5.8 (02-01-2013)\n\n== Core\n* (Bug) Fix pending address balance on received coins for pruning servers\n* (Bug) Fix history command line option to show output again (regression by SPV)\n* (Chore) Add timeout to blockchain headers file download by HTTP\n* (Feature) new option: -L, --language: default language used in GUI.\n\n== Lite GUI\n* (Bug) Sending to auto-completed contacts works again\n* (Chore) Added version number to title bar\n\n== Classic GUI\n* (Feature) Language selector in options.\n\n# Release 1.5.7 (18-12-2012)\n\n== Core\n* The blockchain headers file is no longer included in the packages, it is downloaded on startup.\n* New command line option: -P or --portable, for portable wallets. With this flag, all preferences are saved to the wallet file, and the blockchain headers file is in the same directory as the wallet\n\n== Lite GUI\n* (Feature) Added the ability to export your transactions to a CSV file.\n* (Feature) Added a label dialog after sending a transaction.\n* (Feature) Reworked receiving addresses; instead of a random selection from one of your receiving addresses a new widget will show listing unused addresses.\n* (Chore)   Removed server selection. With all the new server options a simple menu item does not suffice anymore.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nTo report security issues, send an email to the addresses listed below.\n(Not for support. Support requests will be *ignored*.)\n\nPlease send any report to *all* emails listed here.\n\nThe following GPG keys may be used to communicate sensitive information.\n\n\n| Name        | Email                                  | GPG fingerprint                                   |\n|-------------|----------------------------------------|---------------------------------------------------|\n| ThomasV     | thomasv [AT] electrum [DOT] org        | 6694 D8DE 7BE8 EE56 31BE D950 2BD5 824B 7F94 70E6 |\n| SomberNight | somber.night [AT] protonmail [DOT] com | 4AD6 4339 DFA0 5E20 B3F6 AD51 E7B7 48CD AF5E 5ED9 |\n\n\n#### Where to find GPG keys\n\nYou can import a key by running the following command with that\nindividual’s fingerprint: `gpg --recv-keys \"<fingerprint>\"`\n\nThese public keys can also be found in the Electrum git repository,\nin the top-level `pubkeys` folder.\n"
  },
  {
    "path": "contrib/add_cosigner",
    "content": "#!/usr/bin/python3\n#\n# This script is part of the workflow for BUILDERs to reproduce and sign the\n# release binaries. (for builders who do not have sftp access to \"electrum-downloads-airlock\")\n#\n# env vars:\n# - SSHUSER\n#\n#\n# - BUILDER builds all binaries and checks they match the official releases\n#   (using release.sh, and perhaps some manual steps)\n# - BUILDER creates a PR against https://github.com/spesmilo/electrum-signatures/\n#   to add their sigs for a given release, which then gets merged\n# - SFTPUSER runs `$ SSHUSER=$SFTPUSER electrum/contrib/add_cosigner $BUILDER`\n# - SFTPUSER runs `$ electrum/contrib/make_download $WWW_DIR`\n# -     $ (cd $WWW_DIR; git commit -a -m \"add_cosigner\"; git push)\n# - SFTPUSER runs `$ electrum-web/publish.sh $SFTPUSER`\n# - (for the website to be updated, both ThomasV and SomberNight needs to run publish.sh)\n\nimport re\nimport os\nimport sys\nimport importlib\nimport importlib.util\nimport subprocess\n\n\nif len(sys.argv) < 2:\n    print(f\"usage: {os.path.basename(__file__)} <cosigner>\", file=sys.stderr)\n    sys.exit(1)\n\n# cd to project root\nos.chdir(os.path.dirname(os.path.dirname(__file__)))\n\n# load version.py; needlessly complicated alternative to \"imp.load_source\":\nversion_spec = importlib.util.spec_from_file_location('version', 'electrum/version.py')\nversion_module = importlib.util.module_from_spec(version_spec)\nversion_spec.loader.exec_module(version_module)\n\nELECTRUM_VERSION = version_module.ELECTRUM_VERSION\nprint(\"version\", ELECTRUM_VERSION)\n\n# GPG name of cosigner\ncosigner = sys.argv[1]\n\nversion = version_win = version_mac = version_android = ELECTRUM_VERSION\n\nfiles = {\n    \"tgz\": f\"Electrum-{version}.tar.gz\",\n    \"tgz_srconly\": f\"Electrum-sourceonly-{version}.tar.gz\",\n    \"appimage\": f\"electrum-{version}-x86_64.AppImage\",\n    \"mac\": f\"electrum-{version_mac}.dmg\",\n    \"win\": f\"electrum-{version_win}.exe\",\n    \"win_setup\": f\"electrum-{version_win}-setup.exe\",\n    \"win_portable\": f\"electrum-{version_win}-portable.exe\",\n    \"apk_arm64\": f\"Electrum-{version_android}-arm64-v8a-release.apk\",\n    \"apk_armeabi\": f\"Electrum-{version_android}-armeabi-v7a-release.apk\",\n    \"apk_x86_64\": f\"Electrum-{version_android}-x86_64-release.apk\",\n}\n\n\nfor shortname, filename in files.items():\n    path = f\"dist/{filename}\"\n    link = f\"https://download.electrum.org/{version}/{filename}\"\n    if not os.path.exists(path):\n        os.system(f\"wget -q {link} -O {path}\")\n    if not os.path.getsize(path):\n        raise Exception(path)\n    sig_name = f\"{filename}.{cosigner}.asc\"\n    sig_url = f\"https://raw.githubusercontent.com/spesmilo/electrum-signatures/master/{version}/{filename}/{sig_name}\"\n    sig_path = f\"dist/{sig_name}\"\n    os.system(f\"wget -nc {sig_url} -O {sig_path}\")\n    if os.system(f\"gpg --verify {sig_path} {path}\") != 0:\n        raise Exception(sig_name)\n\nprint(\"Calling upload.sh now... This might take some time.\")\nsubprocess.check_output([\"./contrib/upload.sh\", ])\n"
  },
  {
    "path": "contrib/android/Dockerfile",
    "content": "# based on https://github.com/kivy/python-for-android/blob/master/Dockerfile\n\nFROM debian:trixie@sha256:a3b5f4f0286249a124bfe9845b3aec0f88de32ff31dd8d7e1b945f9f98d116b0\n\nENV DEBIAN_FRONTEND=noninteractive\n\nENV ANDROID_HOME=\"/opt/android\"\n\n# need ca-certificates before using snapshot packages\nRUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \\\n    ca-certificates\n\n# pin the distro packages.\nCOPY contrib/android/apt.sources.list /etc/apt/sources.list\nCOPY contrib/android/apt.preferences /etc/apt/preferences.d/snapshot\n\n# configure locale\nRUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends --allow-downgrades \\\n    locales && \\\n    locale-gen en_US.UTF-8\nENV LANG=\"en_US.UTF-8\" \\\n    LANGUAGE=\"en_US.UTF-8\" \\\n    LC_ALL=\"en_US.UTF-8\"\n\nRUN apt -y update -qq \\\n    && apt -y install -qq --no-install-recommends --allow-downgrades \\\n    curl \\\n    wget \\\n    unzip \\\n    ca-certificates \\\n    python3 \\\n    && apt -y autoremove\n\n\nENV ANDROID_NDK_HOME=\"${ANDROID_HOME}/android-ndk\"\n#ENV ANDROID_NDK_VERSION=\"23b\"\n#ENV ANDROID_NDK_HASH=\"c6e97f9c8cfe5b7be0a9e6c15af8e7a179475b7ded23e2d1c1fa0945d6fb4382\"\n#ENV ANDROID_NDK_VERSION=\"27d\"\n#ENV ANDROID_NDK_HASH=\"601246087a682d1944e1e16dd85bc6e49560fe8b6d61255be2829178c8ed15d9\"\nENV ANDROID_NDK_VERSION=\"23d-canary\"\nENV ANDROID_NDK_HASH=\"6944ffc20ab018ff4ef6a403048d0a99d50a0630c3eae690c8f803c452f46f3e\"\nENV ANDROID_NDK_HOME_V=\"${ANDROID_NDK_HOME}-r${ANDROID_NDK_VERSION}\"\n\n# get the latest version from https://developer.android.com/ndk/downloads/index.html\nENV ANDROID_NDK_ARCHIVE=\"android-ndk-r${ANDROID_NDK_VERSION}-linux.zip\"\nENV ANDROID_NDK_DL_URL=\"https://dl.google.com/android/repository/${ANDROID_NDK_ARCHIVE}\"\n\n# below disabled in favor of CI build download\n\n# download and install Android NDK\n#RUN curl --location --progress-bar \\\n#        \"${ANDROID_NDK_DL_URL}\" \\\n#        --output \"${ANDROID_NDK_ARCHIVE}\" \\\n#    && echo \"${ANDROID_NDK_HASH} ${ANDROID_NDK_ARCHIVE}\" | sha256sum -c - \\\n#    && mkdir --parents \"${ANDROID_NDK_HOME_V}\" \\\n#    && unzip -q \"${ANDROID_NDK_ARCHIVE}\" -d \"${ANDROID_HOME}\" \\\n#    && ln -sfn \"${ANDROID_NDK_HOME_V}\" \"${ANDROID_NDK_HOME}\" \\\n#    && rm -rf \"${ANDROID_NDK_ARCHIVE}\"\n\n# temporary build using NDK from CI\nENV CI_REV=\"12186248\"\nENV CI_NDK_FILE=\"android-ndk-${CI_REV}-linux-x86_64.zip\"\nCOPY contrib/android/dl-ndk-ci.sh /tmp/\nRUN /tmp/dl-ndk-ci.sh https://ci.android.com/builds/submitted/${CI_REV}/linux/latest/${CI_NDK_FILE} \\\n    && echo \"${ANDROID_NDK_HASH} android-ndk-ci-linux-x86_64.zip\" | sha256sum -c - \\\n    && mkdir --parents \"${ANDROID_NDK_HOME_V}\" \\\n    && unzip -q \"android-ndk-ci-linux-x86_64.zip\" -d \"${ANDROID_HOME}\" \\\n    && ln -sfn \"${ANDROID_NDK_HOME_V}\" \"${ANDROID_NDK_HOME}\" \\\n    && rm -rf \"android-ndk-ci-linux-x86_64.zip\"\n\nENV ANDROID_SDK_HOME=\"${ANDROID_HOME}/android-sdk\"\n\n# get the latest version from https://developer.android.com/studio/index.html\nENV ANDROID_SDK_TOOLS_VERSION=\"9477386\"\nENV ANDROID_SDK_HASH=\"bd1aa17c7ef10066949c88dc6c9c8d536be27f992a1f3b5a584f9bd2ba5646a0\"\nENV ANDROID_SDK_TOOLS_ARCHIVE=\"commandlinetools-linux-${ANDROID_SDK_TOOLS_VERSION}_latest.zip\"\nENV ANDROID_SDK_TOOLS_DL_URL=\"https://dl.google.com/android/repository/${ANDROID_SDK_TOOLS_ARCHIVE}\"\nENV ANDROID_SDK_MANAGER=\"${ANDROID_SDK_HOME}/cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_SDK_HOME}\"\n\n# download and install Android SDK\nRUN curl --location --progress-bar \\\n        \"${ANDROID_SDK_TOOLS_DL_URL}\" \\\n        --output \"${ANDROID_SDK_TOOLS_ARCHIVE}\" \\\n    && echo \"${ANDROID_SDK_HASH} ${ANDROID_SDK_TOOLS_ARCHIVE}\" | sha256sum -c - \\\n    && mkdir --parents \"${ANDROID_SDK_HOME}\" \\\n    && unzip -q \"${ANDROID_SDK_TOOLS_ARCHIVE}\" -d \"${ANDROID_SDK_HOME}\" \\\n    && rm -rf \"${ANDROID_SDK_TOOLS_ARCHIVE}\"\n\n# update Android SDK, install Android API, Build Tools...\nRUN mkdir --parents \"${ANDROID_SDK_HOME}/.android/\" \\\n    && echo '### User Sources for Android SDK Manager' \\\n        > \"${ANDROID_SDK_HOME}/.android/repositories.cfg\"\n\n# download Java-17 (debian 13 only packages Java-21 and Java-25)\n# - we download the amd64 binaries from debian 12 repos\n# - we should try to upgrade to Java-21...\n#   - the main blocker seems to be having to update Gradle (to a version compatible with Java-21)\n#     - make_barcode_scanner.sh: markusfisch/{zxing-cpp, ...} pins old Gradle\nENV JAVA_JRE_DL_URL=\"https://snapshot.debian.org/archive/debian/20260130T143028Z/pool/main/o/openjdk-17/openjdk-17-jre-headless_17.0.18+8-1~deb12u1_amd64.deb\"\nENV JAVA_JRE_ARCHIVE=\"openjdk-17-jre-headless.deb\"\nENV JAVA_JRE_HASH=\"5bc36cbb4e383dbea4168d57b5fd9b42375ec8837dd62a1d56677632c3c960e0\"\nENV JAVA_JDK_DL_URL=\"https://snapshot.debian.org/archive/debian/20260130T143028Z/pool/main/o/openjdk-17/openjdk-17-jdk-headless_17.0.18+8-1~deb12u1_amd64.deb\"\nENV JAVA_JDK_ARCHIVE=\"openjdk-17-jdk-headless.deb\"\nENV JAVA_JDK_HASH=\"8841044caa66860a71039342fe3c02b7853b61c518e05970e501faa215b1788a\"\nRUN apt -y update -qq \\\n    && apt -y install -qq --no-install-recommends \\\n        ca-certificates-java \\\n        java-common \\\n        libcups2 \\\n        libfontconfig1 \\\n        liblcms2-2 \\\n        libjpeg62-turbo \\\n        libnss3 \\\n        libasound2 \\\n        libfreetype6 \\\n        libharfbuzz0b \\\n        libpcsclite1 \\\n    && apt -y autoremove \\\n    && cd /opt \\\n    && curl --location --progress-bar \"${JAVA_JRE_DL_URL}\" --output \"${JAVA_JRE_ARCHIVE}\" \\\n    && echo \"${JAVA_JRE_HASH} ${JAVA_JRE_ARCHIVE}\" | sha256sum -c - \\\n    && dpkg -i \"${JAVA_JRE_ARCHIVE}\" \\\n    && rm \"${JAVA_JRE_ARCHIVE}\" \\\n    && curl --location --progress-bar \"${JAVA_JDK_DL_URL}\" --output \"${JAVA_JDK_ARCHIVE}\" \\\n    && echo \"${JAVA_JDK_HASH} ${JAVA_JDK_ARCHIVE}\" | sha256sum -c - \\\n    && dpkg -i \"${JAVA_JDK_ARCHIVE}\" \\\n    && rm \"${JAVA_JDK_ARCHIVE}\"\n\n# accept Android licenses (JDK necessary!)\nRUN yes | ${ANDROID_SDK_MANAGER} --licenses > /dev/null\n\n\nENV ANDROID_SDK_BUILD_TOOLS_MAJOR_V=\"31\"\nENV ANDROID_SDK_BUILD_TOOLS_VERSION=\"31.0.0\"\n\n# download platforms, API, build tools\nRUN ${ANDROID_SDK_MANAGER} \"platforms;android-${ANDROID_SDK_BUILD_TOOLS_MAJOR_V}\" > /dev/null && \\\n    ${ANDROID_SDK_MANAGER} \"build-tools;${ANDROID_SDK_BUILD_TOOLS_VERSION}\" > /dev/null && \\\n    ${ANDROID_SDK_MANAGER} \"extras;android;m2repository\" > /dev/null && \\\n    chmod +x \"${ANDROID_SDK_HOME}/cmdline-tools/bin/avdmanager\"\n\n# download ANT\nENV APACHE_ANT_VERSION=\"1.10.13\"\nENV APACHE_ANT_HASH=\"776be4a5704158f00ef3f23c0327546e38159389bc8f39abbfe114913f88bab1\"\nENV APACHE_ANT_ARCHIVE=\"apache-ant-${APACHE_ANT_VERSION}-bin.tar.gz\"\nENV APACHE_ANT_DL_URL=\"https://archive.apache.org/dist/ant/binaries/${APACHE_ANT_ARCHIVE}\"\nENV APACHE_ANT_HOME=\"${ANDROID_HOME}/apache-ant\"\nENV APACHE_ANT_HOME_V=\"${APACHE_ANT_HOME}-${APACHE_ANT_VERSION}\"\n\nRUN curl --location --progress-bar \\\n        \"${APACHE_ANT_DL_URL}\" \\\n        --output \"${APACHE_ANT_ARCHIVE}\" \\\n    && echo \"${APACHE_ANT_HASH} ${APACHE_ANT_ARCHIVE}\" | sha256sum -c - \\\n    && tar -xf \"${APACHE_ANT_ARCHIVE}\" -C \"${ANDROID_HOME}\" \\\n    && ln -sfn \"${APACHE_ANT_HOME_V}\" \"${APACHE_ANT_HOME}\" \\\n    && rm -rf \"${APACHE_ANT_ARCHIVE}\"\n\n\n# install system/build dependencies\n# https://github.com/kivy/buildozer/blob/master/docs/source/installation.rst#android-on-ubuntu-2004-64bit\nRUN apt -y update -q \\\n    && apt -y install -q --no-install-recommends --allow-downgrades \\\n        wget \\\n        lbzip2 \\\n        patch \\\n        sudo \\\n        git \\\n        zip \\\n        unzip \\\n        rsync \\\n        build-essential \\\n        ccache \\\n        autoconf \\\n        autopoint \\\n        libtool \\\n        pkg-config \\\n        zlib1g-dev \\\n        libncurses-dev \\\n        cmake \\\n        libffi-dev \\\n        libssl-dev \\\n        automake \\\n        gettext \\\n        libltdl-dev \\\n    && apt -y autoremove \\\n    && apt -y clean\n\n# cross compile deps for Qt6\nRUN apt -y update -qq \\\n    && apt -y install -qq --no-install-recommends --allow-downgrades \\\n        libopengl-dev \\\n        libegl-dev \\\n        dos2unix \\\n    && apt -y autoremove \\\n    && apt -y clean\n\n\n# create new user to avoid using root; but with sudo access and no password for convenience.\nARG UID=1000\nRUN if [ \"$UID\" != \"0\" ] ; then useradd --uid $UID --create-home --shell /bin/bash \"user\" ; fi\nRUN usermod -append --groups sudo $(id -nu $UID || echo \"user\")\nRUN echo \"%sudo ALL=(ALL) NOPASSWD: ALL\" >> /etc/sudoers\nRUN HOME_DIR=$(getent passwd $UID | cut -d: -f6)\nENV WORK_DIR=\"${HOME_DIR}/wspace\" \\\n    PATH=\"${HOME_DIR}/.local/bin:${PATH}\"\nWORKDIR ${WORK_DIR}\nRUN chown --recursive ${UID} ${WORK_DIR} ${ANDROID_SDK_HOME}\nRUN chown ${UID} /opt\nUSER ${UID}\n\n# build cpython. FIXME we can't use the python3 from apt, as it is too new o.O\n# - p4a and buildozer require cython<3 (see https://github.com/kivy/python-for-android/issues/2919)\n#   but the last such version, cython 0.29.37, can only be built by up to python 3.12\nENV VENV_PYTHON_VERSION=\"3.12.12\"\nENV VENV_PY_VER_MAJOR=\"3.12\"\nENV VENV_PYTHON_HASH=\"487c908ddf4097a1b9ba859f25fe46d22ccaabfb335880faac305ac62bffb79b\"\nRUN mkdir --parents \"/opt/cpython/download\" && cd \"/opt/cpython/download\" \\\n    && wget \"https://www.python.org/ftp/python/${VENV_PYTHON_VERSION}/Python-${VENV_PYTHON_VERSION}.tgz\" \\\n    && echo \"${VENV_PYTHON_HASH} Python-${VENV_PYTHON_VERSION}.tgz\" | sha256sum -c - \\\n    && tar xf \"Python-${VENV_PYTHON_VERSION}.tgz\" -C \"/opt/cpython/download\" \\\n    && cd \"Python-${VENV_PYTHON_VERSION}\" \\\n    && mkdir \"/opt/cpython/install\" \\\n    && ./configure \\\n        --prefix=\"/opt/cpython/install\" \\\n        -q \\\n    && make \"-j$(nproc)\" -s \\\n    && make -s altinstall \\\n    && ln -s \"/opt/cpython/install/bin/python${VENV_PY_VER_MAJOR}\" \"/opt/cpython/install/bin/python3\"\nRUN \"/opt/cpython/install/bin/python3\" -m ensurepip\n\n# venv, VIRTUAL_ENV is used by buildozer to indicate a venv environment\nENV VIRTUAL_ENV=/opt/venv\nRUN \"/opt/cpython/install/bin/python3\" -m venv ${VIRTUAL_ENV}\nENV PATH=\"${VIRTUAL_ENV}/bin:${PATH}\"\n\nCOPY contrib/deterministic-build/requirements-build-base.txt /opt/deterministic-build/\nCOPY contrib/deterministic-build/requirements-build-android.txt /opt/deterministic-build/\nRUN /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies \\\n    -r /opt/deterministic-build/requirements-build-base.txt\nRUN /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: \\\n    -r /opt/deterministic-build/requirements-build-android.txt\n\n# install buildozer\nENV BUILDOZER_CHECKOUT_COMMIT=\"4403ecf445f10b5fbf7c74f4621bf2b922ad35b5\"\n# ^ from branch electrum_20240930 (note: careful with force-pushing! see #8162)\nRUN cd /opt \\\n    && git clone https://github.com/spesmilo/buildozer \\\n    && cd buildozer \\\n    && git checkout \"${BUILDOZER_CHECKOUT_COMMIT}^{commit}\" \\\n    && /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies -e .\n\n# install python-for-android\nENV P4A_CHECKOUT_COMMIT=\"a01269f7799587ad74ee40e0b642d917b8db7d4e\"\n# ^ from branch electrum_20251211 (note: careful with force-pushing! see #8162)\nRUN cd /opt \\\n    && git clone https://github.com/spesmilo/python-for-android \\\n    && cd python-for-android \\\n    && git checkout \"${P4A_CHECKOUT_COMMIT}^{commit}\" \\\n    && /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies -e .\n\n# build env vars\nENV USE_SDK_WRAPPER=1\nENV GRADLE_OPTS=\"-Xmx1536M -Dorg.gradle.jvmargs='-Xmx1536M'\"\n#ENV P4A_FULL_DEBUG=1\n"
  },
  {
    "path": "contrib/android/Makefile",
    "content": "SHELL := /bin/bash\nPYTHON = python3\n\n# for reproducible builds\nexport LC_ALL             := C\nexport TZ                 := UTC\nifndef ELEC_APK_USE_CURRENT_TIME\n    export SOURCE_DATE_EPOCH  := $(shell git log -1 --pretty=%ct)\nelse\n    # p4a sets \"private_version\" based on SOURCE_DATE_EPOCH. \"private_version\" gets compiled into the apk,\n    # and is used at runtime to decide whether the already extracted project files in the app's datadir need updating.\n    # So, \"private_version\" needs to be reproducible, but it would be useful during development if it changed\n    # between subsequent builds (otherwise the new code won't be unpacked and used at runtime!).\n    # For this reason, for development purposes, we set SOURCE_DATE_EPOCH here to the current time.\n    # see https://github.com/kivy/python-for-android/blob/e8686e2104a553f05959cdaf7dd26867671fc8e6/pythonforandroid/bootstraps/common/build/build.py#L575-L587\n    export SOURCE_DATE_EPOCH  := $(shell date +%s)\nendif\nexport PYTHONHASHSEED     := $(SOURCE_DATE_EPOCH)\nexport BUILD_DATE         := $(shell LC_ALL=C TZ=UTC date +'%b %e %Y' -d @$(SOURCE_DATE_EPOCH))\nexport BUILD_TIME         := $(shell LC_ALL=C TZ=UTC date +'%H:%M:%S' -d @$(SOURCE_DATE_EPOCH))\n\n\n.PHONY: apk clean\n\nprepare:\n\t# running pre build setup\n\t# copy electrum to main.py\n\t@cp buildozer_$(ELEC_APK_GUI).spec ../../buildozer.spec\n\t@cp ../../run_electrum ../../main.py\napk:\n\t@make prepare\n\t@-cd ../..; buildozer android debug\n\t@make clean\nrelease:\n\t@make prepare\n\t@-cd ../..; buildozer android release\n\t@make clean\nclean:\n\t# Cleaning up\n\t# rename main.py to electrum\n\t@-rm ../../main.py\n\t# remove buildozer.spec\n\t@-rm ../../buildozer.spec\n"
  },
  {
    "path": "contrib/android/Readme.md",
    "content": "# Qml GUI\n\nThe Qml GUI is used with Electrum on Android devices, since Electrum 4.4.\nTo generate an APK file, follow these instructions.\n\n(note: older versions of Electrum for Android used the \"kivy\" GUI)\n\n## Android binary with Docker\n\n✓ _These binaries should be reproducible, meaning you should be able to generate\n   binaries that match the official releases._\n\n- _Minimum supported target system (i.e. what end-users need): Android 6.0 (API 23)_\n\nThis assumes an Ubuntu (x86_64) host, but it should not be too hard to adapt to another\nsimilar system.\n\n1. Install Docker\n\n    See [`contrib/docker_notes.md`](../docker_notes.md).\n\n    (worth reading even if you already have docker)\n\n2. Build binaries\n\n    The build script takes a few arguments. To see syntax, run it without providing any:\n    ```\n    $ ./build.sh\n    ```\n    For development, consider e.g. `$ ./build.sh qml arm64-v8a debug`\n\n    If you want reproducibility, try instead e.g.:\n    ```\n    $ ELECBUILD_COMMIT=HEAD ./build.sh qml all release-unsigned\n    ```\n\n3. The generated binary is in `./dist`.\n\n\n## Verifying reproducibility and comparing against official binary\n\nEvery user can verify that the official binary was created from the source code in this\nrepository.\n\n1. Build your own binary as described above.\n   Make sure you don't build in `debug` mode,\n   instead use either of `release` or `release-unsigned`.\n   If you build in `release` mode, the apk will be signed, which requires a keystore\n   that you need to create manually (see source of `make_apk.sh` for an example).\n2. Note that the binaries are not going to be byte-for-byte identical, as the official\n   release is signed by a keystore that only the project maintainers have.\n   You can use the `apkdiff.py` python script (written by the Signal developers) to compare\n   the two binaries.\n    ```\n    $ python3 contrib/android/apkdiff.py Electrum_apk_that_you_built.apk Electrum_apk_official_release.apk\n    ```\n   This should output `APKs match!`.\n\n\n## FAQ\n\n### I changed something but I don't see any differences on the phone. What did I do wrong?\nYou probably need to clear the cache: `rm -rf .buildozer/android/platform/build-*/{build,dists}`\n\n\n### How do I deploy on connected phone for quick testing?\nAssuming `adb` is installed:\n```\n$ adb -d install -r dist/Electrum-*-arm64-v8a-debug.apk\n$ adb shell monkey -p org.electrum.electrum 1\n```\nNote `adb install` can take a `--user {userId}` option to install the app for a specific profile.\nWithout that, the default is to install to *all* profiles.\n\n\n### How do I get an interactive shell inside docker?\n```\n$ docker run -it --rm \\\n    -v $PWD:/home/user/wspace/electrum \\\n    -v $PWD/.buildozer/.gradle:/home/user/.gradle \\\n    --workdir /home/user/wspace/electrum \\\n    electrum-android-builder-img\n```\n\n\n### How do I get more verbose logs for the build?\nSee `log_level` in `buildozer.spec`\n\n\n### How can I see logs at runtime?\nThis should work OK for most scenarios:\n```\nadb logcat | grep python\n```\nBetter `grep` but fragile because of `cut`:\n```\nadb logcat | grep -F \"`adb shell ps | grep org.electrum.electrum | cut -c14-19`\"\n```\n\n\n### The Qml GUI can be run directly on Linux Desktop. How?\nInstall requirements:\n```\npython3 -m pip install \".[qml_gui]\"\n```\n\nRun electrum with the `-g` switch: `electrum -g qml`\n\nNotes:\n\n- pyqt ~6.4 would work best, as the gui has not yet been adapted to styling changes in 6.5\n- However, pyqt6 as distributed on PyPI does not include a required module (PyQt6.QtQml) until 6.5\n- Installing these deps from your OS package manager should also work,\n  except many don't distribute pyqt6 yet.\n  For pyqt5 on debian-based distros, this used to look like this:\n  ```\n  sudo apt-get install python3-pyqt5 python3-pyqt5.qtquick python3-pyqt5.qtmultimedia\n  sudo apt-get install python3-pil\n  sudo apt-get install qml-module-qtquick-controls2 qml-module-qtquick-layouts \\\n      qml-module-qtquick-window2 qml-module-qtmultimedia \\\n      libqt5multimedia5-plugins qml-module-qt-labs-folderlistmodel\n  ```\n\n\n### debug vs release build\nIf you just follow the instructions above, you will build the apk\nin debug mode. The most notable difference is that the apk will be\nsigned using a debug keystore. If you are planning to upload\nwhat you build to e.g. the Play Store, you should create your own\nkeystore, back it up safely, and run `./build.sh` in `release` mode.\n\nSee e.g. [kivy wiki](https://github.com/kivy/kivy/wiki/Creating-a-Release-APK)\nand [android dev docs](https://developer.android.com/studio/build/building-cmdline#sign_cmdline).\n\n### Access datadir on Android from desktop (e.g. to copy wallet file)\nNote that this only works for debug builds! Otherwise the security model\nof Android does not let you access the internal storage of an app without root.\n(See [this](https://stackoverflow.com/q/9017073))\nTo pull a file:\n```\n$ adb shell\nadb$ run-as org.electrum.electrum ls /data/data/org.electrum.electrum/files/data\nadb$ exit\n$ adb exec-out run-as org.electrum.electrum cat /data/data/org.electrum.electrum/files/data/wallets/my_wallet > my_wallet\n```\nTo push a file:\n```\n$ adb push ~/wspace/tmp/my_wallet /data/local/tmp\n$ adb shell\nadb$ ls -la /data/local/tmp\nadb$ run-as org.electrum.testnet.electrum cp /data/local/tmp/my_wallet /data/data/org.electrum.testnet.electrum/files/data/testnet/wallets/\nadb$ run-as org.electrum.testnet.electrum chmod -R 700 /data/data/org.electrum.testnet.electrum/files/data/testnet/wallets\nadb$ run-as org.electrum.testnet.electrum chmod -R u-x,u+X /data/data/org.electrum.testnet.electrum/files/data/testnet/wallets\nadb$ rm /data/local/tmp/my_wallet\n```\n\nOr use Android Studio: \"Device File Explorer\", which can download/upload data directly from device (via adb).\n\n#### Device with multiple user profiles\n\nThere are further complications if using an Android device\n[with multiple user profiles](https://source.android.com/docs/devices/admin/multi-user-testing)\n(typical for GrapheneOS/etc).\n\nRun `$ adb shell pm list users` to get a list of all existing users, and take note of the user ids.\n\nInstead of `/data/data/{app.path}`, private app data is stored at `/data/user/{userId}/{app.path}`.\n\nFurther, instead of `adb$ run-as org.electrum.electrum`,\nyou need `adb$ run-as org.electrum.electrum --user {userId}`.\n\n### How to investigate diff between binaries if reproducibility fails?\n```\ncd dist/\nunzip Electrum-*.apk1 -d apk1\nmkdir apk1/assets/private_mp3/\ntar -xzvf apk1/assets/private.tar --directory apk1/assets/private_mp3/\nmkdir apk1/lib/_libpybundle/\ntar -xzvf apk1/lib/*/libpybundle.so --directory apk1/lib/_libpybundle/\n\nunzip Electrum-*.apk2 -d apk2\nmkdir apk2/assets/private_mp3/\ntar -xzvf apk2/assets/private.tar --directory apk2/assets/private_mp3/\nmkdir apk2/lib/_libpybundle/\ntar -xzvf apk2/lib/*/libpybundle.so --directory apk2/lib/_libpybundle/\n\nsudo chown --recursive \"$(id -u -n)\":\"$(id -u -n)\" apk1/ apk2/\nchmod -R +Xr  apk1/ apk2/\n\nunzip apk1/lib/_libpybundle/_python_bundle/stdlib.zip -d apk1/lib/_libpybundle/_python_bundle/stdlib\nunzip apk2/lib/_libpybundle/_python_bundle/stdlib.zip -d apk2/lib/_libpybundle/_python_bundle/stdlib\n\nsudo chown --recursive \"$(id -u -n)\":\"$(id -u -n)\" apk1/ apk2/\nchmod -R +Xr  apk1/ apk2/\n$(cd apk1; find -type f -exec sha256sum '{}' \\; > ./../sha256sum1)\n$(cd apk2; find -type f -exec sha256sum '{}' \\; > ./../sha256sum2)\ndiff sha256sum1 sha256sum2 > d\ncat d\n```\n\n### How to install apks built by the CI on my phone?\n\nThe CI (Cirrus) builds apks on most git commits.\nSee e.g. [here](https://github.com/spesmilo/electrum/runs/9272252577).\nThe task name should start with \"Android build\".\nClick \"View more details on Cirrus CI\" to get to cirrus' website, and search for \"Artifacts\".\nThe apk is built in `debug` mode, and is signed using an ephemeral RSA key.\n\nFor tech demo purposes, you can directly install this apk on your phone.\nHowever, if you already have electrum installed on your phone, Android's TOFU signing model\nwill not let you upgrade that to the CI apk due to mismatching signing keys. As the CI key\nis ephemeral, it is not even possible to upgrade from an older CI apk to a newer CI apk.\n\nHowever, it is possible to resign the apk manually with one's own key, using\ne.g. [`apksigner`](https://developer.android.com/studio/command-line/apksigner),\nmutating the apk in place, after which it should be possible to upgrade:\n```\napksigner sign --ks ~/wspace/electrum/contrib/android/android_debug.keystore Electrum-*-arm64-v8a-debug.apk\n```\n"
  },
  {
    "path": "contrib/android/apkdiff.py",
    "content": "#! /usr/bin/env python3\n# from https://github.com/signalapp/Signal-Android/blob/2029ea378f249a70983c1fc3d55b9a63588bc06c/reproducible-builds/apkdiff/apkdiff.py\n\nimport sys\nfrom zipfile import ZipFile\n\n\n# FIXME it is possible to hide data in the apk signing block - and then the application\n#       can introspect itself at runtime and access that, even execute it as code... :/\n#       see https://source.android.com/docs/security/features/apksigning/v2#apk-signing-block\n#           https://android.izzysoft.de/articles/named/iod-scan-apkchecks\n#           https://github.com/obfusk/sigblock-code-poc\n#       I think if the app did this kind of introspection, that should be caught by code review,\n#       but still, note that with this current diff script it is possible to smuggle data in the apk.\nclass ApkDiff:\n    IGNORE_FILES = [\"META-INF/MANIFEST.MF\", \"META-INF/CERT.RSA\", \"META-INF/CERT.SF\"]\n\n    def compare(self, sourceApk, destinationApk) -> bool:\n        sourceZip      = ZipFile(sourceApk, 'r')\n        destinationZip = ZipFile(destinationApk, 'r')\n\n        if self.compareManifests(sourceZip, destinationZip) and self.compareEntries(sourceZip, destinationZip):\n            print(\"APKs match!\")\n            return True\n        else:\n            print(\"APKs don't match!\")\n            return False\n\n    def compareManifests(self, sourceZip, destinationZip):\n        sourceEntrySortedList      = sorted(sourceZip.namelist())\n        destinationEntrySortedList = sorted(destinationZip.namelist())\n\n        for ignoreFile in self.IGNORE_FILES:\n            while ignoreFile in sourceEntrySortedList: sourceEntrySortedList.remove(ignoreFile)\n            while ignoreFile in destinationEntrySortedList: destinationEntrySortedList.remove(ignoreFile)\n\n        if len(sourceEntrySortedList) != len(destinationEntrySortedList):\n            print(\"Manifest lengths differ!\")\n\n        for (sourceEntryName, destinationEntryName) in zip(sourceEntrySortedList, destinationEntrySortedList):\n            if sourceEntryName != destinationEntryName:\n                print(\"Sorted manifests don't match, %s vs %s\" % (sourceEntryName, destinationEntryName))\n                return False\n\n        return True\n\n    def compareEntries(self, sourceZip, destinationZip):\n        sourceInfoList      = list(filter(lambda sourceInfo: sourceInfo.filename not in self.IGNORE_FILES, sourceZip.infolist()))\n        destinationInfoList = list(filter(lambda destinationInfo: destinationInfo.filename not in self.IGNORE_FILES, destinationZip.infolist()))\n\n        if len(sourceInfoList) != len(destinationInfoList):\n            print(\"APK info lists of different length!\")\n            return False\n\n        for sourceEntryInfo in sourceInfoList:\n            for destinationEntryInfo in list(destinationInfoList):\n                if sourceEntryInfo.filename == destinationEntryInfo.filename:\n                    sourceEntry      = sourceZip.open(sourceEntryInfo, 'r')\n                    destinationEntry = destinationZip.open(destinationEntryInfo, 'r')\n\n                    if not self.compareFiles(sourceEntry, destinationEntry):\n                        print(\"APK entry %s does not match %s!\" % (sourceEntryInfo.filename, destinationEntryInfo.filename))\n                        return False\n\n                    destinationInfoList.remove(destinationEntryInfo)\n                    break\n\n        return True\n\n    def compareFiles(self, sourceFile, destinationFile):\n        sourceChunk      = sourceFile.read(1024)\n        destinationChunk = destinationFile.read(1024)\n\n        while sourceChunk != b\"\" or destinationChunk != b\"\":\n            if sourceChunk != destinationChunk:\n                return False\n\n            sourceChunk      = sourceFile.read(1024)\n            destinationChunk = destinationFile.read(1024)\n\n        return True\n\nif __name__ == '__main__':\n    if len(sys.argv) != 3:\n        print(\"Usage: apkdiff <pathToFirstApk> <pathToSecondApk>\")\n        sys.exit(1)\n\n    match = ApkDiff().compare(sys.argv[1], sys.argv[2])\n    if match:\n        sys.exit(0)\n    else:\n        sys.exit(1)\n"
  },
  {
    "path": "contrib/android/apt.preferences",
    "content": "Package: *\nPin: origin \"snapshot.debian.org\"\nPin-Priority: 1001\n"
  },
  {
    "path": "contrib/android/apt.sources.list",
    "content": "deb https://snapshot.debian.org/archive/debian/20260129T082333Z/ trixie main\ndeb-src https://snapshot.debian.org/archive/debian/20260129T082333Z/ trixie main\n"
  },
  {
    "path": "contrib/android/bitcoin_intent.xml",
    "content": "<intent-filter >\n  <action android:name=\"android.intent.action.VIEW\" />\n  <action android:name=\"android.nfc.action.NDEF_DISCOVERED\"/>\n  <category android:name=\"android.intent.category.DEFAULT\" />\n  <category android:name=\"android.intent.category.BROWSABLE\" />\n  <data android:scheme=\"bitcoin\" />\n  <data android:scheme=\"lightning\" />\n</intent-filter>\n"
  },
  {
    "path": "contrib/android/build.sh",
    "content": "#!/bin/bash\n#\n# env vars:\n# - ELECBUILD_NOCACHE: if set, forces rebuild of docker image\n# - ELECBUILD_COMMIT: if set, do a fresh clone and git checkout\n\nset -e\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/../..\"\nPROJECT_ROOT_OR_FRESHCLONE_ROOT=\"$PROJECT_ROOT\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\nCONTRIB_ANDROID=\"$CONTRIB/android\"\nDISTDIR=\"$PROJECT_ROOT/dist\"\nBUILD_UID=$(/usr/bin/stat -c %u \"$PROJECT_ROOT\")\n\n. \"$CONTRIB\"/build_tools_util.sh\n\n# check arguments\nif [[ -n \"$3\" \\\n\t  && ( \"$1\" == \"qml\" ) \\\n\t  && ( \"$2\" == \"all\"  || \"$2\" == \"armeabi-v7a\" || \"$2\" == \"arm64-v8a\" || \"$2\" == \"x86\" || \"$2\" == \"x86_64\" ) \\\n\t  && ( \"$3\" == \"debug\"  || \"$3\" == \"release\" || \"$3\" == \"release-unsigned\" ) ]] ; then\n    info \"arguments $1 $2 $3\"\nelse\n    fail \"usage: build.sh <qml|...> <arm64-v8a|armeabi-v7a|x86|x86_64|all> <debug|release|release-unsigned>\"\n    exit 1\nfi\n\n# create symlink\nrm -f ${PROJECT_ROOT}/.buildozer\nmkdir -p \"${PROJECT_ROOT}/.buildozer_$1\"\nln -s \".buildozer_$1\" ${PROJECT_ROOT}/.buildozer\n\nDOCKER_BUILD_FLAGS=\"\"\nif [ ! -z \"$ELECBUILD_NOCACHE\" ] ; then\n    info \"ELECBUILD_NOCACHE is set. forcing rebuild of docker image.\"\n    DOCKER_BUILD_FLAGS=\"--pull --no-cache\"\nfi\n\nif [ -z \"$ELECBUILD_COMMIT\" ] ; then  # local dev build\n    DOCKER_BUILD_FLAGS=\"$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID\"\nfi\n\ninfo \"building docker image.\"\ndocker build \\\n    $DOCKER_BUILD_FLAGS \\\n    -t electrum-android-builder-img \\\n    --file \"$CONTRIB_ANDROID/Dockerfile\" \\\n    \"$PROJECT_ROOT\"\n\n# maybe do fresh clone\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then\n    info \"ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout.\"\n    FRESH_CLONE=${FRESH_CLONE:-\"/tmp/electrum_build/android/fresh_clone/electrum\"}\n    rm -rf \"$FRESH_CLONE\" 2>/dev/null || ( info \"we need sudo to rm prev FRESH_CLONE.\" && sudo rm -rf \"$FRESH_CLONE\" )\n    umask 0022\n    git clone \"$PROJECT_ROOT\" \"$FRESH_CLONE\"\n    cd \"$FRESH_CLONE\"\n    git checkout \"$ELECBUILD_COMMIT\"\n    PROJECT_ROOT_OR_FRESHCLONE_ROOT=\"$FRESH_CLONE\"\nelse\n    info \"not doing fresh clone.\"\nfi\n\nDOCKER_RUN_FLAGS=\"\"\nif [[ \"$3\" == \"release\" ]] ; then\n    info \"'release' mode selected. mounting ~/.keystore inside container.\"\n    DOCKER_RUN_FLAGS=\"-v $HOME/.keystore:/home/user/.keystore\"\nfi\nif sh -c \": >/dev/tty\" >/dev/null 2>/dev/null; then\n    info \"/dev/tty is available and usable\"\n    DOCKER_RUN_FLAGS=\"$DOCKER_RUN_FLAGS -it\"\nfi\n\ninfo \"building binary...\"\nmkdir --parents \"$PROJECT_ROOT_OR_FRESHCLONE_ROOT\"/.buildozer/.gradle\n# check uid and maybe chown. see #8261\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then  # fresh clone (reproducible build)\n    if [ $(id -u) != \"1000\" ] || [ $(id -g) != \"1000\" ] ; then\n        info \"need to chown -R FRESH_CLONE dir. prompting for sudo.\"\n        sudo chown -R 1000:1000 \"$FRESH_CLONE\"\n    fi\nfi\ndocker run --rm \\\n    --name electrum-android-builder-cont \\\n    -v \"$PROJECT_ROOT_OR_FRESHCLONE_ROOT\":/home/user/wspace/electrum \\\n    -v \"$PROJECT_ROOT_OR_FRESHCLONE_ROOT\"/.buildozer/.gradle:/home/user/.gradle \\\n    $DOCKER_RUN_FLAGS \\\n    --workdir /home/user/wspace/electrum \\\n    electrum-android-builder-img \\\n    ./contrib/android/make_apk.sh \"$@\"\n\n# make sure resulting binary location is independent of fresh_clone\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then\n    mkdir --parents \"$DISTDIR/\"\n    cp -f \"$FRESH_CLONE/dist\"/* \"$DISTDIR/\"\nfi\n"
  },
  {
    "path": "contrib/android/buildozer_qml.spec",
    "content": "[app]\n\n# (str) Title of your application\ntitle = Electrum\n\n# (str) Package name\npackage.name = Electrum\n\n# (str) Package domain (needed for android/ios packaging)\npackage.domain = org.electrum\n\n# (str) Source code where the main.py live\nsource.dir = .\n\n# (list) Source files to include (let empty to include all the files)\nsource.include_exts = py,png,jpg,qml,qmltypes,ttf,txt,gif,pem,mo,json,csv,so,svg\n\n# (list) Source files to exclude (let empty to not exclude anything)\nsource.exclude_exts = spec\n\n# (list) List of directory to exclude (let empty to not exclude anything)\nsource.exclude_dirs =\n    bin,\n    build,\n    dist,\n    contrib,\n    env,\n    tests,\n    fastlane,\n    electrum/www,\n    electrum/scripts,\n    electrum/utils,\n    electrum/gui/qt,\n    electrum/plugins/audio_modem,\n    electrum/plugins/bitbox02,\n    electrum/plugins/coldcard,\n    electrum/plugins/digitalbitbox,\n    electrum/plugins/jade,\n    electrum/plugins/keepkey,\n    electrum/plugins/ledger,\n    electrum/plugins/nwc,\n    electrum/plugins/payserver,\n    electrum/plugins/revealer,\n    electrum/plugins/safe_t,\n    electrum/plugins/swapserver,\n    electrum/plugins/timelock_recovery,\n    electrum/plugins/trezor,\n    electrum/plugins/watchtower,\n    packages/qdarkstyle,\n    packages/qtpy,\n    packages/bin,\n    packages/share,\n    packages/pkg_resources,\n    packages/setuptools\n\n# (list) List of exclusions using pattern matching\nsource.exclude_patterns = Makefile,setup*,\n    # not reproducible:\n    packages/aiohttp-*.dist-info/*,\n    packages/frozenlist-*.dist-info/*\n\n# (str) Application versioning (method 1)\nversion.regex = ELECTRUM_VERSION = '(.*)'\nversion.filename = %(source.dir)s/electrum/version.py\n\n# (str) Application versioning (method 2)\n#version = 1.9.8\n\n# (list) Application requirements\n# note: versions and hashes are pinned in ./p4a_recipes/*\nrequirements =\n    hostpython3,\n    python3,\n    android,\n    openssl,\n    plyer,\n    libffi,\n    libsecp256k1,\n    pycryptodomex,\n    pyqt6sip,\n    pyqt6,\n    libzbar\n\n# (str) Presplash of the application\npresplash.filename = %(source.dir)s/electrum/gui/icons/electrum_presplash.png\n\n# (str) Icon of the application\nicon.filename = %(source.dir)s/electrum/gui/icons/android_electrum_icon_legacy.png\nicon.adaptive_foreground.filename = %(source.dir)s/electrum/gui/icons/android_electrum_icon_foreground.png\nicon.adaptive_background.filename = %(source.dir)s/electrum/gui/icons/android_electrum_icon_background.png\n\n# (str) Supported orientation (one of landscape, portrait or all)\norientation = portrait\n\n# (bool) Indicate if the application should be fullscreen or not\nfullscreen = False\n\n\n#\n# Android specific\n#\n\n# (list) Permissions\nandroid.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE, POST_NOTIFICATIONS, USE_BIOMETRIC\n\n# (int) Android API to use  (compileSdkVersion)\n# note: when changing, Dockerfile also needs to be changed to install corresponding build tools\nandroid.api = 31\n\n# (int) Android targetSdkVersion\nandroid.target_sdk_version = 35\n\n# (int) Minimum API required. You will need to set the android.ndk_api to be as low as this value.\nandroid.minapi = 23\n\n# (str) Android NDK version to use\nandroid.ndk = 23b\n\n# (int) Android NDK API to use (optional). This is the minimum API your app will support.\nandroid.ndk_api = 23\n\n# (bool) Use --private data storage (True) or --dir public storage (False)\n#android.private_storage = True\n\n# (str) Android NDK directory (if empty, it will be automatically downloaded.)\nandroid.ndk_path = /opt/android/android-ndk\n\n# (str) Android SDK directory (if empty, it will be automatically downloaded.)\nandroid.sdk_path = /opt/android/android-sdk\n\n# (str) ANT directory (if empty, it will be automatically downloaded.)\nandroid.ant_path = /opt/android/apache-ant\n\n# (bool) If True, then skip trying to update the Android sdk\n# This can be useful to avoid excess Internet downloads or save time\n# when an update is due and you just want to test/build your package\n# note(ghost43): probably needed for reproducibility. versions pinned in Dockerfile.\nandroid.skip_update = True\n\n# (bool) If True, then automatically accept SDK license\n# agreements. This is intended for automation only. If set to False,\n# the default, you will be shown the license when first running\n# buildozer.\nandroid.accept_sdk_license = True\n\n# (str) Android entry point, default is ok for Kivy-based app\n#android.entrypoint = org.renpy.android.PythonActivity\n\n# (list) List of Java .jar files to add to the libs so that pyjnius can access\n# their classes. Don't add jars that you do not need, since extra jars can slow\n# down the build process. Allows wildcards matching, for example:\n# OUYA-ODK/libs/*.jar\n#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar\n#android.add_jars = lib/android/zbar.jar\n\nandroid.add_jars = .buildozer/android/platform/*/build/libs_collections/Electrum/jar/*.jar\n\n\nandroid.add_aars =\n    contrib/android/.cache/aars/BarcodeScannerView.aar,\n    contrib/android/.cache/aars/CameraView.aar,\n    contrib/android/.cache/aars/zxing-cpp.aar\n\n\n# (list) List of Java files to add to the android project (can be java or a\n# directory containing the files)\nandroid.add_src = electrum/gui/qml/java_classes/\n\n# kotlin-stdlib is required for zxing-cpp (BarcodeScannerView)\nandroid.gradle_dependencies =\n    com.android.support:support-compat:28.0.0,\n    org.jetbrains.kotlin:kotlin-stdlib:1.8.22\n\nandroid.add_activities = org.electrum.qr.SimpleScannerActivity, org.electrum.biometry.BiometricActivity\n\n# (list) Put these files or directories in the apk res directory.\n# The option may be used in three ways, the value may contain one or zero ':'\n# Some examples:\n# 1) A file to add to resources, legal resource names contain ['a-z','0-9','_']\n# android.add_resources = my_icons/all-inclusive.png:drawable/all_inclusive.png\n# 2) A directory, here  'legal_icons' must contain resources of one kind\n# android.add_resources = legal_icons:drawable\n# 3) A directory, here 'legal_resources' must contain one or more directories,\n# each of a resource kind:  drawable, xml, etc...\n# android.add_resources = legal_resources\nandroid.add_resources = electrum/gui/qml/android_res/layout:layout\n\n# (str) python-for-android branch to use, if not master, useful to try\n# not yet merged features.\n#android.branch = master\n\n# (str) OUYA Console category. Should be one of GAME or APP\n# If you leave this blank, OUYA support will not be enabled\n#android.ouya.category = GAME\n\n# (str) Filename of OUYA Console icon. It must be a 732x412 png image.\n#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png\n\n# (str) XML file to include as an intent filters in <activity> tag\nandroid.manifest.intent_filters = contrib/android/bitcoin_intent.xml\n\n# (str) launchMode to set for the main activity\nandroid.manifest.launch_mode = singleTask\n\n# (list) Android additional libraries to copy into libs/armeabi\n#android.add_libs_armeabi = lib/android/*.so\n\n# (bool) Indicate whether the screen should stay on\n# Don't forget to add the WAKE_LOCK permission if you set this to True\n#android.wakelock = False\n\n# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64\n# note: can be overwritten by APP_ANDROID_ARCH env var\n#android.arch = armeabi-v7a\n\n# (int) overrides automatic versionCode computation (used in build.gradle)\n# this is not the same as app version and should only be edited if you know what you're doing\n# android.numeric_version = 1\n\n# (list) Android application meta-data to set (key=value format)\n#android.meta_data =\n\n# (list) Android library project to add (will be added in the\n# project.properties automatically.)\n#android.library_references =\n\nandroid.whitelist = lib-dynload/_csv.so\n\n# (bool) enables Android auto backup feature (Android API >=23)\nandroid.allow_backup = False\n\n# (str) The format used to package the app for release mode (aab or apk or aar).\nandroid.release_artifact = apk\n\n# (str) The format used to package the app for debug mode (apk or aar).\nandroid.debug_artifact = apk\n\n#\n# Python for android (p4a) specific\n#\n\n# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)\np4a.source_dir = /opt/python-for-android\n\n# (str) The directory in which python-for-android should look for your own build recipes (if any)\np4a.local_recipes = %(source.dir)s/contrib/android/p4a_recipes/\n\n# (str) Filename to the hook for p4a\n#p4a.hook =\n\n# (str) Bootstrap to use for android builds\np4a.bootstrap = qt6\n\n# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)\n#p4a.port =\n\n\n#\n# iOS specific\n#\n\n# (str) Name of the certificate to use for signing the debug version\n# Get a list of available identities: buildozer ios list_identities\n#ios.codesign.debug = \"iPhone Developer: <lastname> <firstname> (<hexstring>)\"\n\n# (str) Name of the certificate to use for signing the release version\n#ios.codesign.release = %(ios.codesign.debug)s\n\n\n\n[buildozer]\n\n# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))\nlog_level = 2\n\n# (str) Path to build output (i.e. .apk, .ipa) storage\nbin_dir = ./dist\n\n\n# -----------------------------------------------------------------------------\n# List as sections\n#\n# You can define all the \"list\" as [section:key].\n# Each line will be considered as a option to the list.\n# Let's take [app] / source.exclude_patterns.\n# Instead of doing:\n#\n#     [app]\n#     source.exclude_patterns = license,data/audio/*.wav,data/images/original/*\n#\n# This can be translated into:\n#\n#     [app:source.exclude_patterns]\n#     license\n#     data/audio/*.wav\n#     data/images/original/*\n#\n\n# -----------------------------------------------------------------------------\n# Profiles\n#\n# You can extend section / key with a profile\n# For example, you want to deploy a demo version of your application without\n# HD content. You could first change the title to add \"(demo)\" in the name\n# and extend the excluded directories to remove the HD content.\n#\n#     [app@demo]\n#     title = My Application (demo)\n#\n#     [app:source.exclude_patterns@demo]\n#     images/hd/*\n#\n# Then, invoke the command line with the \"demo\" profile:\n#\n#     buildozer --profile demo android debug\n"
  },
  {
    "path": "contrib/android/dl-ndk-ci.sh",
    "content": "#!/bin/sh\nif [ -z \"$1\" ]; then\n  echo \"missing url\"\n  exit 1\nfi\necho $1\n\ncurl $1 | grep \"var JSVariables\" | python3 -c \"import sys; line=sys.stdin.read(); line=line[line.find('{'):-2]; import json; j=json.loads(line); print(j['artifactUrl'])\" | wget -i - -O android-ndk-ci-linux-x86_64.zip\n"
  },
  {
    "path": "contrib/android/get_apk_versioncode.py",
    "content": "#!/usr/bin/python3\n\nimport importlib.util\nimport os\nimport sys\n\nARCH_DICT = {\n    \"x86_64\": \"4\",\n    \"arm64-v8a\": \"3\",\n    \"armeabi-v7a\": \"2\",\n    \"x86\": \"1\",\n    \"null\": \"0\",\n}\n\n\ndef get_electrum_version() -> str:\n    project_root = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))\n    version_file_path = os.path.join(project_root, \"electrum\", \"version.py\")\n    # load version.py; needlessly complicated alternative to \"imp.load_source\":\n    version_spec = importlib.util.spec_from_file_location('version', version_file_path)\n    version_module = version = importlib.util.module_from_spec(version_spec)\n    version_spec.loader.exec_module(version_module)\n    return version.ELECTRUM_VERSION\n\n\ndef get_android_versioncode(*, arch_name: str) -> int:\n    version_code = 0\n    # add ELECTRUM_VERSION\n    app_version = get_electrum_version()\n    # if alpha/beta, and not stable: strip out alpha/beta part from last component.\n    # NOTE: we REUSE the version_code int between alphas/betas and the final stable.\n    #       This is not allowed on Google Play or F-Droid.\n    #       This means we MUST NOT upload alphas/betas there.\n    if any(c in app_version for c in (\"a\", \"b\")):\n        c_pos = app_version.find(\"a\")\n        if c_pos == -1:\n            c_pos = app_version.find(\"b\")\n        app_version = app_version[:c_pos]\n    # now the app_version str must contain exactly three dot-delimited components\n    app_version_components = app_version.split('.')\n    assert len(app_version_components) == 3, f\"version str expected to have 3 components, but got {app_version!r}\"\n    # convert to int\n    for i in app_version_components:\n        version_code *= 100\n        version_code += int(i)\n    # add arch\n    arch_code = ARCH_DICT[arch_name]\n    assert len(arch_code) == 1\n    version_code *= 10\n    version_code += int(arch_code)\n    # compensate for legacy scheme\n    # note: up until version 4.5.5, we used a different scheme for version_code.\n    #       4_______________4_05_05_00\n    #       ^ android arch, ^ app_version (4.5.5.0)\n    # This offset ensures that all new-scheme version codes are larger than the old-scheme version codes.\n    offset_due_to_legacy_scheme = 45_000_000\n    version_code += offset_due_to_legacy_scheme\n    return version_code\n\n\nif __name__ == '__main__':\n    try:\n        android_arch = sys.argv[1]\n    except Exception:\n        print(f\"usage: {os.path.basename(__file__)} <android_arch>\", file=sys.stderr)\n        sys.exit(1)\n    if android_arch not in ARCH_DICT:\n        print(f\"usage: {os.path.basename(__file__)} <android_arch>\", file=sys.stderr)\n        print(f\"error: unknown {android_arch=}\", file=sys.stderr)\n        print(f\"       should be one of: {list(ARCH_DICT.keys())}\", file=sys.stderr)\n        sys.exit(1)\n    version_code = get_android_versioncode(arch_name=android_arch)\n    assert isinstance(version_code, int), f\"{version_code=!r} must be an int.\"\n    print(version_code, file=sys.stdout)\n"
  },
  {
    "path": "contrib/android/make_apk.sh",
    "content": "#!/bin/bash\n\nset -e\n\nCONTRIB_ANDROID=\"$(dirname \"$(readlink -e \"$0\")\")\"\nCONTRIB=\"$CONTRIB_ANDROID\"/..\nPROJECT_ROOT=\"$CONTRIB\"/..\nPACKAGES=\"$PROJECT_ROOT\"/packages/\n\n. \"$CONTRIB\"/build_tools_util.sh\n\ngit -C \"$PROJECT_ROOT\" rev-parse 2>/dev/null || fail \"Building outside a git clone is not supported.\"\n\n\n# arguments have been checked in build.sh\nexport ELEC_APK_GUI=$1\n\nif [ ! -d \"$PACKAGES\" ]; then\n    \"$CONTRIB\"/make_packages.sh || fail \"make_packages failed\"\nfi\n\n# update locale\ninfo \"preparing electrum-locale.\"\n(\n    \"$CONTRIB/locale/build_cleanlocale.sh\"\n    # we want the binary to have only compiled (.mo) locale files; not source (.po) files\n    rm -r \"$PROJECT_ROOT/electrum/locale/locale\"/*/electrum.po\n)\n\npushd \"$CONTRIB_ANDROID\"\n\ninfo \"apk building phase starts.\"\n\n# Uncomment and change below to set a custom android package id,\n# e.g. to allow simultaneous mainnet and testnet installs of the apk.\n# defaults:\n#   export APP_PACKAGE_NAME=Electrum\n#   export APP_PACKAGE_DOMAIN=org.electrum\n# FIXME: changing \"APP_PACKAGE_NAME\" seems to require a clean rebuild of \".buildozer/\",\n#        to avoid that, maybe change \"APP_PACKAGE_DOMAIN\" instead.\n# So, in particular, to build a testnet apk, simply uncomment:\n#export APP_PACKAGE_DOMAIN=org.electrum.testnet\n\nif [ $CI ]; then\n    # override log level specified in buildozer.spec to \"debug\":\n    export BUILDOZER_LOG_LEVEL=2\nfi\n\nif [[ \"$3\" == \"release\" ]] ; then\n    # do release build, and sign the APKs.\n    TARGET=\"release\"\n    export P4A_RELEASE_KEYSTORE_PASSWD=\"$4\"\n    export P4A_RELEASE_KEYALIAS_PASSWD=\"$4\"\n    export P4A_RELEASE_KEYSTORE=~/.keystore\n    export P4A_RELEASE_KEYALIAS=electrum\n    if [ -z \"$P4A_RELEASE_KEYSTORE_PASSWD\" ] || [ -z \"$P4A_RELEASE_KEYALIAS_PASSWD\" ]; then\n        echo \"p4a password not defined\"\n        exit 1\n    fi\nelif [[ \"$3\" == \"release-unsigned\" ]] ; then\n    # do release build, but do not sign the APKs.\n    TARGET=\"release\"\nelif [[ \"$3\" == \"debug\" ]] ; then\n    # do debug build.\n    TARGET=\"apk\"\n    export P4A_DEBUG_KEYSTORE=\"$CONTRIB_ANDROID\"/android_debug.keystore\n    export P4A_DEBUG_KEYSTORE_PASSWD=unsafepassword\n    export P4A_DEBUG_KEYALIAS_PASSWD=unsafepassword\n    export P4A_DEBUG_KEYALIAS=electrum\n    # create keystore if needed\n    if [ ! -f \"$P4A_DEBUG_KEYSTORE\" ]; then\n        keytool -genkey -v -keystore \"$CONTRIB_ANDROID\"/android_debug.keystore \\\n            -alias \"$P4A_DEBUG_KEYALIAS\" -keyalg RSA -keysize 2048 -validity 10000 \\\n            -dname \"CN=mqttserver.ibm.com, OU=ID, O=IBM, L=Hursley, S=Hants, C=GB\" \\\n            -storepass \"$P4A_DEBUG_KEYSTORE_PASSWD\" \\\n            -keypass \"$P4A_DEBUG_KEYALIAS_PASSWD\"\n    fi\n    export ELEC_APK_USE_CURRENT_TIME=1\nelse\n    fail \"unknown build type\"\nfi\n\n\nif [[ \"$2\" == \"all\" ]] ; then\n    # build all apks\n    # FIXME failures are not propagated out: we should fail the script if any arch build fails\n    export APP_ANDROID_ARCHS=armeabi-v7a\n    export APP_ANDROID_NUMERIC_VERSION=$(\"$CONTRIB_ANDROID\"/get_apk_versioncode.py \"$APP_ANDROID_ARCHS\")\n    \"$CONTRIB_ANDROID\"/make_barcode_scanner.sh \"$APP_ANDROID_ARCHS\" || fail \"make_barcode_scanner.sh failed\"\n    make $TARGET\n\n    export APP_ANDROID_ARCHS=arm64-v8a\n    export APP_ANDROID_NUMERIC_VERSION=$(\"$CONTRIB_ANDROID\"/get_apk_versioncode.py \"$APP_ANDROID_ARCHS\")\n    \"$CONTRIB_ANDROID\"/make_barcode_scanner.sh \"$APP_ANDROID_ARCHS\" || fail \"make_barcode_scanner.sh failed\"\n    make $TARGET\n\n    export APP_ANDROID_ARCHS=x86_64\n    export APP_ANDROID_NUMERIC_VERSION=$(\"$CONTRIB_ANDROID\"/get_apk_versioncode.py \"$APP_ANDROID_ARCHS\")\n    \"$CONTRIB_ANDROID\"/make_barcode_scanner.sh \"$APP_ANDROID_ARCHS\" || fail \"make_barcode_scanner.sh failed\"\n    make $TARGET\nelse\n    export APP_ANDROID_ARCHS=$2\n    export APP_ANDROID_NUMERIC_VERSION=$(\"$CONTRIB_ANDROID\"/get_apk_versioncode.py \"$APP_ANDROID_ARCHS\")\n    \"$CONTRIB_ANDROID\"/make_barcode_scanner.sh \"$APP_ANDROID_ARCHS\" || fail \"make_barcode_scanner.sh failed\"\n    make $TARGET\nfi\n\npopd\n\n\ninfo \"done.\"\nls -la \"$PROJECT_ROOT/dist\"\nsha256sum \"$PROJECT_ROOT/dist\"/*\n"
  },
  {
    "path": "contrib/android/make_barcode_scanner.sh",
    "content": "#!/bin/bash\n\n# script to clone and build https://github.com/markusfisch/BarcodeScannerView and its dependencies,\n# https://github.com/markusfisch/CameraView/ and https://github.com/markusfisch/zxing-cpp\n# which are being used as barcode scanner in the Android app.\n\n# To bump the version of BarcodeScannerView, get the newest version tag from the github repo,\n# then get the required dependencies from\n# https://github.com/markusfisch/BarcodeScannerView/blob/**VERSION_TAG**/barcodescannerview/build.gradle\n# then update the commit hashes below. Also update kotlin-stdlib in buildozer_qml.spec to the\n# \"kotlin-version\" specified in the used zxing-cpp commit:\n# https://github.com/markusfisch/zxing-cpp/blob/master/wrappers/aar/build.gradle\n\n\nBARCODE_SCANNER_VIEW_COMMIT_HASH=\"0bdb69269c252bb6daef2f871b76403c8b051945\"  # 1.6.5\nBARCODE_SCANNER_VIEW_REPO=\"https://github.com/markusfisch/BarcodeScannerView.git\"\n\nCAMERA_VIEW_COMMIT_HASH=\"745597d05bc6abfdb3637a09a8ecaf30fdce7b6e\"  # 1.10.0\nCAMERA_VIEW_REPO=\"https://github.com/markusfisch/CameraView.git\"\n\nZXING_CPP_COMMIT_HASH=\"79f5adc6250e90de0bd635eb9181c5f8a18affda\"  # v2.3.0.4 using kotlin-stdlib 1.8.22\nZXING_CPP_REPO=\"https://github.com/markusfisch/zxing-cpp.git\"\n\n\n########################################################################################################\nset -e\n\nCONTRIB_ANDROID=\"$(dirname \"$(readlink -e \"$0\")\")\"\nCONTRIB=\"$CONTRIB_ANDROID\"/..\nCACHEDIR=\"$CONTRIB_ANDROID/.cache\"\nBUILDDIR=\"$CACHEDIR/builds\"\n\n. \"$CONTRIB\"/build_tools_util.sh\n\n# target architecture passed as argument by`make_apk.sh`\nTARGET_ARCH=\"$1\"\n\n# check if TARGET_ARCH is set and supported\nif [[ \"$TARGET_ARCH\" != \"armeabi-v7a\" \\\n        && \"$TARGET_ARCH\" != \"arm64-v8a\" \\\n        && \"$TARGET_ARCH\" != \"x86_64\" ]]; then\n    fail \"make_barcode_scanner.sh invalid target architecture argument: $TARGET_ARCH\"\nfi\n\ninfo \"Building BarcodeScannerView and deps for architecture: $TARGET_ARCH\"\n\n# check if directories exist, create them if not\nif [ ! -d \"$CACHEDIR/aars\" ]; then\n    mkdir -p \"$CACHEDIR/aars\"\nfi\n\nif [ ! -d \"$BUILDDIR\" ]; then\n    mkdir -p \"$BUILDDIR\"\nfi\n\n\n####### zxing-cpp ########\n\n# check if zxing-cpp aar is already in cachedir, else build it\nZXING_CPP_BUILD_ID=\"$TARGET_ARCH-$ZXING_CPP_COMMIT_HASH\"\nif [ -f \"$CACHEDIR/aars/zxing-cpp-$ZXING_CPP_BUILD_ID.aar\" ]; then\n    info \"zxing-cpp for $ZXING_CPP_BUILD_ID already exists in cache, skipping build.\"\n    cp \"$CACHEDIR/aars/zxing-cpp-$ZXING_CPP_BUILD_ID.aar\" \"$CACHEDIR/aars/zxing-cpp.aar\"\nelse\n    info \"Building zxing-cpp for $ZXING_CPP_BUILD_ID...\"\n    ZXING_CPP_DIR=\"$BUILDDIR/zxing-cpp\"\n    clone_or_update_repo \"$ZXING_CPP_REPO\" \"$ZXING_CPP_COMMIT_HASH\" \"$ZXING_CPP_DIR\"\n    cd \"$ZXING_CPP_DIR/wrappers/aar\"\n    chmod +x gradlew\n\n    # Set local.properties to use SDK of docker container\n    echo \"sdk.dir=${ANDROID_SDK_HOME}\" > local.properties\n    # gradlew will install a specific NDK version required by zxing-cpp\n    ./gradlew :zxingcpp:assembleRelease -Pandroid.injected.build.abi=\"$TARGET_ARCH\"\n\n    # Copy the built AAR to cache directory\n    ZXING_AAR_SOURCE=\"$ZXING_CPP_DIR/wrappers/aar/zxingcpp/build/outputs/aar/zxingcpp-release.aar\"\n    ZXING_AAR_DEST_GENERIC=\"$CACHEDIR/aars/zxing-cpp.aar\"\n    ZXING_AAR_DEST_SPECIFIC=\"$CACHEDIR/aars/zxing-cpp-$ZXING_CPP_BUILD_ID.aar\"\n    if [ ! -f \"$ZXING_AAR_SOURCE\" ]; then\n        fail \"zxing-cpp AAR not found at $ZXING_AAR_SOURCE, build failed?\"\n    fi\n    cp \"$ZXING_AAR_SOURCE\" \"$ZXING_AAR_DEST_GENERIC\"\n    # keeping an arch specific copy allows to skip the build later if it already exists\n    cp \"$ZXING_AAR_SOURCE\" \"$ZXING_AAR_DEST_SPECIFIC\"\n    info \"zxing-cpp AAR copied to $ZXING_AAR_DEST_GENERIC\"\nfi\n\n########### CameraView ###########\n\nCAMERA_VIEW_BUILD_ID=\"$CAMERA_VIEW_COMMIT_HASH\"\nif [ -f \"$CACHEDIR/aars/CameraView-$CAMERA_VIEW_BUILD_ID.aar\" ]; then\n    info \"CameraView AAR already exists in cache, skipping build.\"\n    cp \"$CACHEDIR/aars/CameraView-$CAMERA_VIEW_BUILD_ID.aar\" \"$CACHEDIR/aars/CameraView.aar\"\nelse\n    info \"Building CameraView...\"\n    CAMERA_VIEW_DIR=\"$BUILDDIR/CameraView\"\n    clone_or_update_repo \"$CAMERA_VIEW_REPO\" \"$CAMERA_VIEW_COMMIT_HASH\" \"$CAMERA_VIEW_DIR\"\n    cd \"$CAMERA_VIEW_DIR\"\n    chmod +x gradlew\n\n    echo \"sdk.dir=${ANDROID_SDK_HOME}\" > local.properties\n    ./gradlew :cameraview:assembleRelease\n\n    CAMERA_AAR_SOURCE=\"$CAMERA_VIEW_DIR/cameraview/build/outputs/aar/cameraview-release.aar\"\n    CAMERA_AAR_DEST_GENERIC=\"$CACHEDIR/aars/CameraView.aar\"\n    CAMERA_AAR_DEST_SPECIFIC=\"$CACHEDIR/aars/CameraView-$CAMERA_VIEW_BUILD_ID.aar\"\n    if [ ! -f \"$CAMERA_AAR_SOURCE\" ]; then\n        fail \"CameraView AAR not found at $CAMERA_AAR_SOURCE\"\n    fi\n    cp \"$CAMERA_AAR_SOURCE\" \"$CAMERA_AAR_DEST_GENERIC\"\n    cp \"$CAMERA_AAR_SOURCE\" \"$CAMERA_AAR_DEST_SPECIFIC\"\n    info \"CameraView AAR copied to $CAMERA_AAR_DEST_GENERIC\"\nfi\n\n########### BarcodeScannerView ###########\n\nBARCODE_SCANNER_VIEW_BUILD_ID=\"$BARCODE_SCANNER_VIEW_COMMIT_HASH\"\nif [ -f \"$CACHEDIR/aars/BarcodeScannerView-$BARCODE_SCANNER_VIEW_BUILD_ID.aar\" ]; then\n    info \"BarcodeScannerView AAR already exists in cache, skipping build.\"\n    cp \"$CACHEDIR/aars/BarcodeScannerView-$BARCODE_SCANNER_VIEW_BUILD_ID.aar\" \"$CACHEDIR/aars/BarcodeScannerView.aar\"\nelse\n    info \"Building BarcodeScannerView...\"\n    BARCODE_SCANNER_VIEW_DIR=\"$BUILDDIR/BarcodeScannerView\"\n    clone_or_update_repo \"$BARCODE_SCANNER_VIEW_REPO\" \"$BARCODE_SCANNER_VIEW_COMMIT_HASH\" \"$BARCODE_SCANNER_VIEW_DIR\"\n    cd \"$BARCODE_SCANNER_VIEW_DIR\"\n    chmod +x gradlew\n\n    echo \"sdk.dir=${ANDROID_SDK_HOME}\" > local.properties\n    ./gradlew :barcodescannerview:assembleRelease\n\n    BARCODE_AAR_SOURCE=\"$BARCODE_SCANNER_VIEW_DIR/barcodescannerview/build/outputs/aar/barcodescannerview-release.aar\"\n    BARCODE_AAR_DEST_GENERIC=\"$CACHEDIR/aars/BarcodeScannerView.aar\"\n    BARCODE_AAR_DEST_SPECIFIC=\"$CACHEDIR/aars/BarcodeScannerView-$BARCODE_SCANNER_VIEW_BUILD_ID.aar\"\n    if [ ! -f \"$BARCODE_AAR_SOURCE\" ]; then\n        fail \"BarcodeScannerView AAR not found at $BARCODE_AAR_SOURCE\"\n    fi\n    cp \"$BARCODE_AAR_SOURCE\" \"$BARCODE_AAR_DEST_GENERIC\"\n    cp \"$BARCODE_AAR_SOURCE\" \"$BARCODE_AAR_DEST_SPECIFIC\"\n    info \"BarcodeScannerView AAR copied to $BARCODE_AAR_DEST_GENERIC\"\nfi\n\n\ninfo \"All barcode scanner libraries built successfully for $TARGET_ARCH\"\n"
  },
  {
    "path": "contrib/android/p4a_recipes/README.md",
    "content": "python-for-android local recipes\n--------------------------------\n\nThese folders are recipes (build scripts) for most of our direct and transitive\ndependencies for the Android app. python-for-android has recipes built-in for\nmany packages but it also allows users to specify their \"local\" recipes.\nLocal recipes have precedence over the built-in recipes.\n\nThe local recipes we have here are mostly just used to pin down specific\nversions and hashes for reproducibility. The hashes are updated manually.\n"
  },
  {
    "path": "contrib/android/p4a_recipes/cffi/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.cffi import CffiRecipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert CffiRecipe._version == \"1.15.1\"\nassert CffiRecipe.depends == ['setuptools', 'pycparser', 'libffi', 'python3']\nassert CffiRecipe.python_depends == []\n\n\nclass CffiRecipePinned(util.InheritedRecipeMixin, CffiRecipe):\n    version = \"1.17.1\"\n    sha512sum = \"907129891d56351ca5cb885aae62334ad432321826d6eddfaa32195b4c7b7689a80333e6d14d0aab479a646aba148b9852c0815b80344dfffa4f183a5e74372c\"\n\n\nrecipe = CffiRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/cryptography/__init__.py",
    "content": "from pythonforandroid.recipes.cryptography import CryptographyRecipe\n\n\nassert CryptographyRecipe._version == \"2.8\"\nassert CryptographyRecipe.depends == ['openssl', 'six', 'setuptools', 'cffi', 'python3']\nassert CryptographyRecipe.python_depends == []\n\n\nclass CryptographyRecipePinned(CryptographyRecipe):\n    sha512sum = \"000816a5513691bfbb01c5c65d96fb3567a5ff25300da4b485e716b6d4dc789aec05ed0fe65df9c5e3e60127aa9110f04e646407db5b512f88882b0659f7123f\"\n\n\nrecipe = CryptographyRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/hostpython3/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.hostpython3 import HostPython3Recipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert HostPython3Recipe.depends == []\nassert HostPython3Recipe.python_depends == []\n\n\nclass HostPython3RecipePinned(util.InheritedRecipeMixin, HostPython3Recipe):\n    # PYTHON_VERSION=    # < line here so that I can grep the codebase and teleport here\n    version = \"3.11.14\"\n    sha512sum = \"41fb3ae22ce4ac0e8bb6b9ae8db88a810af1001d944e3f1abc9e86824ae4be31347e3e3a70425ab12271c6b7eeef552f00164ef23cfffa2551c3c9d1fe5ab91f\"\n\n\nrecipe = HostPython3RecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/libffi/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.libffi import LibffiRecipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert LibffiRecipe._version == \"v3.4.2\"\nassert LibffiRecipe.depends == []\nassert LibffiRecipe.python_depends == []\n\n\nclass LibffiRecipePinned(util.InheritedRecipeMixin, LibffiRecipe):\n    version = \"v3.4.8\"\n    sha512sum = \"064a43ddae005f3d0fa56db4da6071fae93aaae87a755b84888c0cb9c8fa2fe9bb452b3d9a382fab64c442c19d98a20ba15b8be92eba7bf3773815b31fb7824c\"\n\n\nrecipe = LibffiRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/libiconv/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.libiconv import LibIconvRecipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert LibIconvRecipe._version == \"1.16\"\nassert LibIconvRecipe.depends == []\nassert LibIconvRecipe.python_depends == []\n\n\nclass LibIconvRecipePinned(util.InheritedRecipeMixin, LibIconvRecipe):\n    version = \"1.18\"\n    sha512sum = \"a55eb3b7b785a78ab8918db8af541c9e11deb5ff4f89d54483287711ed797d87848ce0eafffa7ce26d9a7adb4b5a9891cb484f94bd4f51d3ce97a6a47b4c719a\"\n\n\nrecipe = LibIconvRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/libsecp256k1/__init__.py",
    "content": "from pythonforandroid.recipes.libsecp256k1 import LibSecp256k1Recipe\n\n\nassert LibSecp256k1Recipe.depends == []\nassert LibSecp256k1Recipe.python_depends == []\n\n\nclass LibSecp256k1RecipePinned(LibSecp256k1Recipe):\n    version = \"1a53f4961f337b4d166c25fce72ef0dc88806618\"\n    url = \"https://github.com/bitcoin-core/secp256k1/archive/{version}.zip\"\n    sha512sum = \"4072e45517bc1bb416250bc8e4fa4ed94f83b4eebbe25a70925fd7cc9759df3edbce64ab0116519c335f82353f6a029cde92018ed7116f2f85c8092a9adeb532\"\n\n\nrecipe = LibSecp256k1RecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/libzbar/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.libzbar import LibZBarRecipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert LibZBarRecipe.depends == ['libiconv']\nassert LibZBarRecipe.python_depends == []\n\n\nclass LibZBarRecipePinned(util.InheritedRecipeMixin, LibZBarRecipe):\n    version = \"bb05ec54eec57f8397cb13fb9161372a281a1219\"\n    url = \"https://github.com/mchehab/zbar/archive/{version}.zip\"\n    sha512sum = \"186312ef0a50404efef79a5fbed34534569fab2873a6bb6d2e3d8ea64fa461c5537ca4fb0e659670d72b021e514f8fd4651b1e85954bf987015d8eb2e6f68375\"\n    patches = []  # werror.patch not needed for modern zbar\n\n\nrecipe = LibZBarRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/openssl/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.openssl import OpenSSLRecipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert OpenSSLRecipe._version == \"3.0.18\"\nassert OpenSSLRecipe.depends == []\nassert OpenSSLRecipe.python_depends == []\n\n\nclass OpenSSLRecipePinned(util.InheritedRecipeMixin, OpenSSLRecipe):\n    version = \"3.0.18\"\n    sha512sum = \"6bdd16f33b83ae2a12777230c4ff00d0595bbc00253ac8c3ac31e1375e818fc74d7f491bd2e507ff33cab9f0498cfb28fa8690f75a98663568d40901523cdf3c\"\n\n\nrecipe = OpenSSLRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/packaging/__init__.py",
    "content": "from pythonforandroid.recipes.packaging import PackagingRecipe\n\n\nassert PackagingRecipe._version == \"21.3\"\nassert PackagingRecipe.depends == [\"setuptools\", \"pyparsing\", \"python3\"]\nassert PackagingRecipe.python_depends == []\n\n\nclass PackagingRecipePinned(PackagingRecipe):\n    #version = \"21.3\"\n    # note: 21.3 is the last version to use setup.py, so newer versions don't work. see comment for PyparsingRecipePinned\n    sha512sum = \"2e3aa276a4229ac7dc0654d586799473ced9761a83aa4159660d37ae1a2a8f30e987248dd0e260e2834106b589f259a57ce9936eef0dcc3c430a99ac6b663e05\"\n\n\nrecipe = PackagingRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/ply/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.ply import PlyRecipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert PlyRecipe._version == \"3.11\"\nassert PlyRecipe.depends == ['packaging', 'python3']\nassert PlyRecipe.python_depends == []\n\n\nclass PlyRecipePinned(util.InheritedRecipeMixin, PlyRecipe):\n    sha512sum = \"37e39a4f930874933223be58a3da7f259e155b75135f1edd47069b3b40e5e96af883ebf1c8a1bbd32f914a9e92cfc12e29fec05cf61b518f46c1d37421b20008\"\n\n\nrecipe = PlyRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/plyer/__init__.py",
    "content": "from pythonforandroid.recipe import PythonRecipe\n\n\nassert PythonRecipe.depends == ['python3']\nassert PythonRecipe.python_depends == []\n\n\nclass PlyerRecipePinned(PythonRecipe):\n    version = \"5262087c85b2c82c69e702fe944069f1d8465fdf\"\n    url = \"git+https://github.com/SomberNight/plyer\"\n    depends = [\"setuptools\"]\n\n\nrecipe = PlyerRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/pycparser/__init__.py",
    "content": "from pythonforandroid.recipes.pycparser import PycparserRecipe\n\n\nassert PycparserRecipe._version == \"2.14\"\nassert PycparserRecipe.depends == ['setuptools', 'python3']\nassert PycparserRecipe.python_depends == []\n\n\nclass PycparserRecipePinned(PycparserRecipe):\n    version = \"2.22\"\n    sha512sum = \"c9a81c78d87162f71281a32a076b279f4f7f2e17253fe14c89c6db5f9b3554a6563ff700c385549a8b51ef8832f99f7bb4ac07f22754c7c475dd91feeb0cf87f\"\n\n\nrecipe = PycparserRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/pycryptodomex/__init__.py",
    "content": "from pythonforandroid.recipe import PythonRecipe\n\n\nassert PythonRecipe.depends == ['python3']\nassert PythonRecipe.python_depends == []\n\n\nclass PycryptodomexRecipe(PythonRecipe):\n    version = \"3.23.0\"\n    sha512sum = \"951cebaad2e19b9f9d04fe85c73ab1ff8b515069c1e0e8e3cd6845ec9ccd5ef3e5737259e0934ed4a6536e289dee6aabac58e1c822a5a6393e86b482c60afc89\"\n    url = \"https://github.com/Legrandin/pycryptodome/archive/v{version}x.tar.gz\"\n    depends = [\"setuptools\", \"cffi\"]\n\n\nrecipe = PycryptodomexRecipe()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/pyjnius/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.pyjnius import PyjniusRecipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert PyjniusRecipe._version == \"1.5.0\"\nassert PyjniusRecipe.depends == [('genericndkbuild', 'sdl2', 'qt6'), 'six', 'python3']\nassert PyjniusRecipe.python_depends == []\n\n\nclass PyjniusRecipePinned(util.InheritedRecipeMixin, PyjniusRecipe):\n    version = \"1.6.1\"\n    sha512sum = \"deb5ac566479111c6f4c6adb895821b263d72bf88414fb093bdfd5ad5d0b7aea56b53d5ef0967e28db360f4fb6fb1c2264123f15c747884799df55848191c424\"\n\n\nrecipe = PyjniusRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/pyparsing/__init__.py",
    "content": "from pythonforandroid.recipes.pyparsing import PyparsingRecipe\n\n\nassert PyparsingRecipe._version == \"3.0.7\"\nassert PyparsingRecipe.depends == [\"setuptools\", \"python3\"]\nassert PyparsingRecipe.python_depends == []\n\n\nclass PyparsingRecipePinned(PyparsingRecipe):\n    #version = \"3.0.7\"\n    # note: 3.0.7 is the last version to use setup.py, so newer versions don't work,\n    #       as p4a runs \"$ python3 setup.py install\". This is only going become a larger problem, needs fix upstream.\n    #       see https://github.com/kivy/python-for-android/blob/be3de2e28e5a52d5f8949f3969f8a3b7f9eb3cba/pythonforandroid/recipe.py#L983\n    #       - but maybe upstream p4a already has a workaround?\n    #         see \"PyProjectRecipe\" from https://github.com/kivy/python-for-android/pull/3007\n    sha512sum = \"1e692f4cdaa6b6e8ca2729d0a3e2ba16d978f1957c538b6de3a4220ec7d996bdbe87c41c43abab851fffa3b0498a05841373e435602917b8c095042e273badb5\"\n\n\nrecipe = PyparsingRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/pyqt6/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.pyqt6 import PyQt6Recipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert PyQt6Recipe._version == \"6.4.2\"\nassert PyQt6Recipe.depends == ['qt6', 'pyjnius', 'setuptools', 'pyqt6sip', 'hostpython3', 'pyqt_builder']\nassert PyQt6Recipe.python_depends == []\n\n\nclass PyQt6RecipePinned(util.InheritedRecipeMixin, PyQt6Recipe):\n    sha512sum = \"51e5f0d028ee7984876da1653cb135d61e2c402f18b939a92477888cc7c86d3bc2889477403dee6b3d9f66519ee3236d344323493b4c2c2e658e1637b10e53bf\"\n\n\nrecipe = PyQt6RecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/pyqt6sip/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.pyqt6sip import PyQt6SipRecipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert PyQt6SipRecipe._version == \"13.5.1\"\nassert PyQt6SipRecipe.depends == ['setuptools', 'python3']\nassert PyQt6SipRecipe.python_depends == []\n\n\nclass PyQt6SipRecipePinned(util.InheritedRecipeMixin, PyQt6SipRecipe):\n    sha512sum = \"1e4170d167a326afe6df86e4a35e209299548054981cb2e5d56da234ef9db4d8594bcb05b6be363c3bc6252776ae9de63d589a3d9f33fba8250d39cdb5e9061a\"\n\n\nrecipe = PyQt6SipRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/pyqt_builder/__init__.py",
    "content": "from pythonforandroid.recipes.pyqt_builder import PyQtBuilderRecipe\n\n\nassert PyQtBuilderRecipe._version == \"1.15.1\"\nassert PyQtBuilderRecipe.depends == [\"sip\", \"packaging\", \"python3\"]\nassert PyQtBuilderRecipe.python_depends == []\n\n\nclass PyQtBuilderRecipePinned(PyQtBuilderRecipe):\n    sha512sum = \"61ee73b6bb922c04739da60025ab50d35d345d2e298943305fcbd3926cda31d732cc5e5b0dbfc39f5eb85c0f0b091b8c3f5fee00dcc240d7849c5c4191c1368a\"\n\n\nrecipe = PyQtBuilderRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/python3/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.python3 import Python3Recipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert Python3Recipe.depends == ['hostpython3', 'sqlite3', 'openssl', 'libffi']\nassert Python3Recipe.python_depends == []\n\n\nclass Python3RecipePinned(util.InheritedRecipeMixin, Python3Recipe):\n    # PYTHON_VERSION=    # < line here so that I can grep the codebase and teleport here\n    version = \"3.11.14\"\n    sha512sum = \"41fb3ae22ce4ac0e8bb6b9ae8db88a810af1001d944e3f1abc9e86824ae4be31347e3e3a70425ab12271c6b7eeef552f00164ef23cfffa2551c3c9d1fe5ab91f\"\n\n\nrecipe = Python3RecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/qt6/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.qt6 import Qt6Recipe\n\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\nassert Qt6Recipe._version == \"6.4.3\"\n# assert Qt6Recipe._version == \"6.5.3\"\nassert Qt6Recipe.depends == ['python3', 'hostqt6']\nassert Qt6Recipe.python_depends == []\n\nclass Qt6RecipePinned(util.InheritedRecipeMixin, Qt6Recipe):\n    sha512sum = \"0bdbe8b9a43390c98cf19e851ec5394bc78438d227cf9d0d7a3748aee9a32a7f14fc46f52d4fa283819f21413567080aee7225c566af5278557f5e1992674da3\"\n    # sha512sum = \"ca8ea3b81c121886636988275f7fa8ae6d19f7be02669e63ab19b4285b611057a41279db9532c25ae87baa3904b010e1db68b899cd0eda17a5a8d3d87098b4d5\"\n\n\nrecipe = Qt6RecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/setuptools/__init__.py",
    "content": "from pythonforandroid.recipes.setuptools import SetuptoolsRecipe\n\n\nassert SetuptoolsRecipe._version == \"51.3.3\"\nassert SetuptoolsRecipe.depends == ['python3']\nassert SetuptoolsRecipe.python_depends == []\n\n\nclass SetuptoolsRecipePinned(SetuptoolsRecipe):\n    sha512sum = \"5a3572466a68c6f650111448ce3343f64c62044650bb8635edbff97e2bc7b216b8bbe3b4e3bccf34e6887f3bedc911b27ca5f9a515201cae49cf44fbacf03345\"\n\n\nrecipe = SetuptoolsRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/sip/__init__.py",
    "content": "from pythonforandroid.recipes.sip import SipRecipe\n\n\nassert SipRecipe._version == \"6.7.9\"\nassert SipRecipe.depends == [\"setuptools\", \"packaging\", \"tomli\", \"ply\", \"python3\"], SipRecipe.depends\nassert SipRecipe.python_depends == []\n\n\nclass SipRecipePinned(SipRecipe):\n    sha512sum = \"bb9d0d0d92002b6fd33f7e8ebe8cd62456dacc16b5734b73760b1ba14fb9b1f2b9b6640b40196c6cf5f345e1afde48bdef39675c4d3480041771325d4cf3c233\"\n\n\nrecipe = SipRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/six/__init__.py",
    "content": "from pythonforandroid.recipes.six import SixRecipe\n\n\nassert SixRecipe._version == \"1.15.0\"\nassert SixRecipe.depends == ['setuptools', 'python3']\nassert SixRecipe.python_depends == []\n\n\nclass SixRecipePinned(SixRecipe):\n    version = \"1.17.0\"\n    sha512sum = \"fcfa58b03877ac3ac00a4f85b5fea4fecb2a010244451aa95013637a0aa21529f3dcfe25c0a07c72da46da1fa12bc0c16b6c641c40c6ab2133e5b5cbb5a71e4b\"\n\n\nrecipe = SixRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/sqlite3/__init__.py",
    "content": "import os\n\nfrom pythonforandroid.recipes.sqlite3 import Sqlite3Recipe\nfrom pythonforandroid.util import load_source\n\nutil = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))\n\n\nassert Sqlite3Recipe._version == \"3.35.5\"\nassert Sqlite3Recipe.depends == []\nassert Sqlite3Recipe.python_depends == []\n\n\nclass Sqlite3RecipePinned(util.InheritedRecipeMixin, Sqlite3Recipe):\n    version = \"3.50.0\"\n    url = 'https://www.sqlite.org/2025/sqlite-amalgamation-3500000.zip'\n    sha512sum = \"0fd87f2b8140300ce165600f6708aafef19041a181e9f00ed14f7aeaa3c06805c8c54c53751a9ce74d4d666f018ca6f48e3f5b5c874ccb9e1424a528c92326f0\"\n\n\nrecipe = Sqlite3RecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/toml/__init__.py",
    "content": "from pythonforandroid.recipes.toml import TomlRecipe\n\n\nassert TomlRecipe._version == \"0.10.2\"\nassert TomlRecipe.depends == [\"setuptools\", \"python3\"]\nassert TomlRecipe.python_depends == []\n\n\nclass TomlRecipePinned(TomlRecipe):\n    sha512sum = \"ede2c8fed610a3827dba828f6e7ab7a8dbd5745e8ef7c0cd955219afdc83b9caea714deee09e853627f05ad1c525dc60426a6e9e16f58758aa028cb4d3db4b39\"\n\n\nrecipe = TomlRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/tomli/__init__.py",
    "content": "from pythonforandroid.recipes.tomli import TomliRecipe\n\n\nassert TomliRecipe._version == \"2.0.1\"\nassert TomliRecipe.depends == [\"setuptools\", \"python3\"]\nassert TomliRecipe.python_depends == []\n\n\nclass TomliRecipePinned(TomliRecipe):\n    #version = \"2.0.1\"\n    # note: can't be easily updated as base recipe has version number hardcoded in custom \"patch\"-like setup.py\n    sha512sum = \"fd410039e255e2b3359e999d69a5a2d38b9b89b77e8557f734f2621dfbd5e1207e13aecc11589197ec22594c022f07f41b4cfe486a3a719281a595c95fd19ecf\"\n\n\nrecipe = TomliRecipePinned()\n"
  },
  {
    "path": "contrib/android/p4a_recipes/util.py",
    "content": "import os\n\n\nclass InheritedRecipeMixin:\n\n    def get_recipe_dir(self):\n        \"\"\"This is used to replace pythonforandroid.recipe.Recipe.get_recipe_dir.\n        If one of our local recipes inherits from a built-in p4a recipe, this override\n        ensures that potential patches and other local files used by the recipe will\n        be looked for in the built-in recipe's folder.\n        \"\"\"\n        return os.path.join(self.ctx.root_dir, 'recipes', self.name)\n"
  },
  {
    "path": "contrib/apparmor/README.md",
    "content": "# Electrum AppArmor Profiles\nAppArmor is a Mandatory Access Control (MAC) system which confines programs to a limited set of resources.\nAppArmor confinement is provided via profiles loaded into the kernel.\n\n## Installation\n\nCopy the AppArmor profile from `contrib/apparmor/apparmor.d/` to `/etc/apparmor.d/`:\n```\nsudo cp -R -L contrib/apparmor/apparmor.d/* /etc/apparmor.d\n```\nReload the AppArmor profiles to apply the changes:\n```\nsudo systemctl reload apparmor\n```\nVerify that the profile is loaded:\n```\nsudo apparmor_status\n```\nLook for the entry corresponding to `electrum`\n\n## Usage \nAfter installing the AppArmor profile, electrum will be restricted to the permissions specified in the profile.\n\n## Compatibility\nThe help tab may not function as expected as browser permissions can be tricky (Tarball Binaries)\n\nThese AppArmor profiles have been tested on the following operating systems:\n```\nDebian 12\nUbuntu 23.10\nKali Linux 6.6\n```\n"
  },
  {
    "path": "contrib/apparmor/apparmor.d/abstractions/electrum",
    "content": "include <abstractions/base>\ninclude <abstractions/fonts>\ninclude <abstractions/user-tmp>\ninclude <abstractions/X>\ninclude <abstractions/wayland>\ninclude <abstractions/mesa>\ninclude <abstractions/dri-enumerate>\ninclude <abstractions/nameservice>\ninclude <abstractions/openssl>\ninclude <abstractions/vulkan>\ninclude <abstractions/python>\ninclude if exists <abstractions/evince>\ninclude if exists <abstractions/xdg-open> \ninclude if exists <abstractions/ubuntu-browsers>\ninclude if exists <abstractions/snap_browsers>\n  \n  owner @{PROC}/@{pid}/{mounts,fd/} r,\n\n  /{usr/,}sbin/ldconfig ix,\n  /{usr/,}bin/{file,dash,dirname,uname} rix,\n  /{usr/,}bin/@{multiarch}-gcc-8 ix,\n  /{usr/,}bin/@{multiarch}-ld.bfd ix,\n  /etc/mime.types r,\n  @{system_share_dirs}/{mime,icons}/{**,} r,\n  /dev/bus/usb/ r,\n  /dev/bus/usb/** rw,\n  @{sys}/class/ r,\n  @{sys}/bus/ r,\n  /etc/udev/udev.conf r,\n  /etc/magic r,\n  @{sys}/devices/pci*/**/usb*/**{busnum,devnum,descriptors,speed,bConfigurationValue} r,\n  /dev/ r,\n  /{var/,}run/udev/data/* r,\n  @{sys}/bus/usb/devices/ r,\n  /{usr/,}/bin/uname rix,\n  owner @{user_share_dirs}/mime/** r,\n  \n  /{,run/}user/**/dconf/* rw,\n  /{var/,}lib/dbus/** r,\n  /etc/apt/apt.conf.d/ r,\n  /etc/machine-id r,\n  /{usr/,}bin/xdg-open ix,\n  /{usr/,}bin/evince ix,\n"
  },
  {
    "path": "contrib/apparmor/apparmor.d/electrum.appimage",
    "content": "# Credits : Mikhail Morfikov\nabi <abi/3.0>,\n\ninclude <tunables/global>\n\n@{exec_path} = /{usr/,}bin/fusermount{,3}\nprofile fusermount @{exec_path} {\n  include <abstractions/base>\n  include <abstractions/nameservice> \n\n  # To mount anything:\n  #  fusermount: mount failed: Operation not permitted\n  capability sys_admin,\n\n  # For jmtpfs\n  capability dac_read_search,\n\n  @{exec_path} mr,\n\n  # Where to mount ISO files\n  owner @{HOME}/*/ rw,\n  owner @{HOME}/*/*/ rw,\n  owner @{HOME}/.cache/**/ rw,\n\n  # Be able to mount ISO images\n  mount fstype={fuse,fuse.*}, \n  unmount fstype={fuse,fuse.*},\n\n  /etc/fuse.conf r,\n\n  /dev/fuse rw,\n\n  @{PROC}/@{pid}/mounts r,\n\n  include if exists <local/fusermount>\n}\n"
  },
  {
    "path": "contrib/apparmor/apparmor.d/usr.local.bin.electrum",
    "content": "#Credits: Anton Nesterov\nabi <abi/3.0>,\n\ninclude <tunables/global>\n\n@{electrum_exec_path} = /{usr/,usr/local/,*/*/.local/,}bin/electrum\n\nprofile electrum @{electrum_exec_path} {\n  include <abstractions/electrum>\n\n  @{electrum_exec_path} mr,\n  owner @{HOME}/.electrum/{**,} rw,\n  owner @{HOME}/.local/{**,} mrw,\n\n}\n"
  },
  {
    "path": "contrib/ban_unicode.py",
    "content": "#!/usr/bin/env python3\n#\n# Copyright (C) 2025 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n#\n# This script scans the whole codebase for unicode characters and\n# errors if it finds any, unless the character is specifically whitelisted below.\n# The motivation is to protect against homoglyph attacks, invisible unicode characters,\n# bidirectional and other control characters, and other malicious unicode usage.\n# Given that we mostly expect to use ASCII characters in the source code,\n# the most robust and generic fix seems to be to just ban all unicode usage.\n\nimport os.path\nimport subprocess\nimport sys\n\nproject_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))\nos.chdir(project_root)\n\nEXCLUDE_PATH_PREFIX = {\n    \"electrum/wordlist/\",\n    \"fastlane/\",\n    \"tests/\",\n}\nEXCLUDE_EXTENSIONS = {\n    \".jpg\", \".jpeg\", \".png\", \".ttf\", \".otf\", \".pdn\", \".icns\", \".ico\", \".gif\",\n}\nUNICODE_WHITELIST = {\n    \"💬\", \"🗯\", \"⚠\", chr(0xfe0f), \"✓\", \"▷\", \"▽\", \"…\", \"•\", \"█\", \"™\", \"≈\",\n    \"á\", \"é\", \"’\",\n    \"│\", \"─\", \"└\", \"├\", \"📋\",\n}\n\nexit_code = 0\n\nbfiles = subprocess.check_output([\"git\", \"ls-files\"])\nbfiles = bfiles.decode(\"utf-8\")\nfor file_path in bfiles.splitlines():\n    if os.path.isdir(file_path):\n        continue\n    if any(file_path.startswith(pattern) for pattern in EXCLUDE_PATH_PREFIX):\n        continue\n    _fname, ext = os.path.splitext(file_path)\n    if ext in EXCLUDE_EXTENSIONS:\n        continue\n    # open file\n    try:\n        with open(file_path, \"r\", encoding=\"utf-8\") as f:\n            for line_no, line in enumerate(f.read().splitlines()):\n                for char in line:\n                    if ord(char)>0x7f and char not in UNICODE_WHITELIST:\n                        print(f\"{file_path}:{line_no}. {line=}. hex={hex(ord(char))}. {char=}\")\n                        exit_code = 1\n    except UnicodeDecodeError as e:\n        raise Exception(f\"cannot parse file {file_path=}\") from e\n\nsys.exit(exit_code)\n"
  },
  {
    "path": "contrib/build-linux/appimage/.dockerignore",
    "content": "build/\n.cache/\n"
  },
  {
    "path": "contrib/build-linux/appimage/Dockerfile",
    "content": "# Note: we deliberately use an old Debian stable as base image.\n# from https://docs.appimage.org/introduction/concepts.html :\n# \"[AppImages] should be built on the oldest possible system, allowing them to run on newer system[s]\"\n\nFROM debian:bullseye@sha256:cf48c31af360e1c0a0aedd33aae4d928b68c2cdf093f1612650eb1ff434d1c34\n\nENV LC_ALL=C.UTF-8 LANG=C.UTF-8\nENV DEBIAN_FRONTEND=noninteractive\n\n# need ca-certificates before using snapshot packages\nRUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \\\n    ca-certificates\n\n# pin the distro packages\nCOPY apt.sources.list /etc/apt/sources.list\nCOPY apt.preferences /etc/apt/preferences.d/snapshot\n\nRUN apt-get update -q && \\\n    apt-get install -qy --allow-downgrades \\\n        sudo \\\n        git \\\n        wget \\\n        python3 \\\n        make \\\n        autotools-dev \\\n        autoconf \\\n        libtool \\\n        autopoint \\\n        pkg-config \\\n        xz-utils \\\n        libssl-dev \\\n        libssl1.1 \\\n        openssl \\\n        zlib1g-dev \\\n        libffi-dev \\\n        libncurses5-dev \\\n        libncurses5 \\\n        libtinfo-dev \\\n        libtinfo5 \\\n        libsqlite3-dev \\\n        libusb-1.0-0-dev \\\n        libudev-dev \\\n        libudev1 \\\n        gettext \\\n        libdbus-1-3 \\\n        xutils-dev \\\n        libxkbcommon0 \\\n        libxkbcommon-x11-0 \\\n        libxcb1-dev \\\n        libxcb-xinerama0 \\\n        libxcb-randr0 \\\n        libxcb-render0 \\\n        libxcb-shm0 \\\n        libxcb-shape0 \\\n        libxcb-sync1 \\\n        libxcb-xfixes0 \\\n        libxcb-xkb1 \\\n        libxcb-icccm4 \\\n        libxcb-image0 \\\n        libxcb-keysyms1 \\\n        libxcb-util1 \\\n        libxcb-render-util0 \\\n        libxcb-cursor0 \\\n        libx11-xcb1 \\\n        libc6-dev \\\n        libc6 \\\n        libc-dev-bin \\\n        libv4l-dev \\\n        libjpeg62-turbo-dev \\\n        libx11-dev \\\n        desktop-file-utils \\\n        && \\\n    rm -rf /var/lib/apt/lists/* && \\\n    apt-get autoremove -y && \\\n    apt-get clean\n\n# create new user to avoid using root; but with sudo access and no password for convenience.\nARG UID=1000\nRUN if [ \"$UID\" != \"0\" ] ; then useradd --uid $UID --create-home --shell /bin/bash \"user\" ; fi\nRUN usermod -append --groups sudo $(id -nu $UID || echo \"user\")\nRUN echo \"%sudo ALL=(ALL) NOPASSWD: ALL\" >> /etc/sudoers\nRUN HOME_DIR=$(getent passwd $UID | cut -d: -f6)\nENV WORK_DIR=\"${HOME_DIR}/wspace\" \\\n    PATH=\"${HOME_DIR}/.local/bin:${PATH}\"\nWORKDIR ${WORK_DIR}\nRUN chown --recursive ${UID} ${WORK_DIR}\nUSER ${UID}\n"
  },
  {
    "path": "contrib/build-linux/appimage/README.md",
    "content": "AppImage binary for Electrum\n============================\n\n✓ _This binary should be reproducible, meaning you should be able to generate\n   binaries that match the official releases._\n\n- _Minimum supported target system (i.e. what end-users need): x86_64, glibc 2.31_\n\nThis assumes an Ubuntu host, but it should not be too hard to adapt to another\nsimilar system. The host architecture should be x86_64 (amd64).\n\nWe currently only build a single AppImage, for x86_64 architecture.\nHelp to adapt these scripts to build for (some flavor of) ARM would be welcome,\nsee [issue #5159](https://github.com/spesmilo/electrum/issues/5159).\n\n\n1. Install Docker\n\n    See [`contrib/docker_notes.md`](../../docker_notes.md).\n\n    (worth reading even if you already have docker)\n\n2. Build binary\n\n    ```\n    $ ./build.sh\n    ```\n    If you want reproducibility, try instead e.g.:\n    ```\n    $ ELECBUILD_COMMIT=HEAD ./build.sh\n    ```\n\n3. The generated binary is in `./dist`.\n\n\n## FAQ\n\n### How can I see what is included in the AppImage?\nExecute the binary as follows: `./electrum*.AppImage --appimage-extract`\n\n### How to investigate diff between binaries if reproducibility fails?\n```\ncd dist/\n./electrum-*-x86_64.AppImage1 --appimage-extract\nmv squashfs-root/ squashfs-root1/\n./electrum-*-x86_64.AppImage2 --appimage-extract\nmv squashfs-root/ squashfs-root2/\n$(cd squashfs-root1; find -type f -exec sha256sum '{}' \\; > ./../sha256sum1)\n$(cd squashfs-root2; find -type f -exec sha256sum '{}' \\; > ./../sha256sum2)\ndiff sha256sum1 sha256sum2 > d\ncat d\n```\n\nFor file metadata, e.g. timestamps:\n```\nrsync -n -a -i --delete squashfs-root1/ squashfs-root2/\n```\n\nUseful binary comparison tools:\n- vbindiff\n- diffoscope\n"
  },
  {
    "path": "contrib/build-linux/appimage/apprun.sh",
    "content": "#!/bin/bash\n\nset -e\n\nAPPDIR=\"$(dirname \"$(readlink -e \"$0\")\")\"\n\nexport LD_LIBRARY_PATH=\"${APPDIR}/usr/lib/:${APPDIR}/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH+:$LD_LIBRARY_PATH}\"\nexport PATH=\"${APPDIR}/usr/bin:${PATH}\"\nexport LDFLAGS=\"-L${APPDIR}/usr/lib/x86_64-linux-gnu -L${APPDIR}/usr/lib\"\n\nexec \"${APPDIR}/usr/bin/python3\" -s \"${APPDIR}/usr/bin/electrum\" \"$@\"\n"
  },
  {
    "path": "contrib/build-linux/appimage/apt.preferences",
    "content": "Package: *\nPin: origin \"snapshot.debian.org\"\nPin-Priority: 1001\n"
  },
  {
    "path": "contrib/build-linux/appimage/apt.sources.list",
    "content": "deb https://snapshot.debian.org/archive/debian/20250530T143637Z/ bullseye main\ndeb-src https://snapshot.debian.org/archive/debian/20250530T143637Z/ bullseye main\n"
  },
  {
    "path": "contrib/build-linux/appimage/build.sh",
    "content": "#!/bin/bash\n#\n# env vars:\n# - ELECBUILD_NOCACHE: if set, forces rebuild of docker image\n# - ELECBUILD_COMMIT: if set, do a fresh clone and git checkout\n\nset -e\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/../../..\"\nPROJECT_ROOT_OR_FRESHCLONE_ROOT=\"$PROJECT_ROOT\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\nCONTRIB_APPIMAGE=\"$CONTRIB/build-linux/appimage\"\nDISTDIR=\"$PROJECT_ROOT/dist\"\nBUILD_UID=$(/usr/bin/stat -c %u \"$PROJECT_ROOT\")\n\n. \"$CONTRIB\"/build_tools_util.sh\n\n\nDOCKER_BUILD_FLAGS=\"\"\nif [ ! -z \"$ELECBUILD_NOCACHE\" ] ; then\n    info \"ELECBUILD_NOCACHE is set. forcing rebuild of docker image.\"\n    DOCKER_BUILD_FLAGS=\"--pull --no-cache\"\nfi\n\nif [ -z \"$ELECBUILD_COMMIT\" ] ; then  # local dev build\n    DOCKER_BUILD_FLAGS=\"$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID\"\nfi\n\ninfo \"building docker image.\"\ndocker build \\\n    $DOCKER_BUILD_FLAGS \\\n    -t electrum-appimage-builder-img \\\n    \"$CONTRIB_APPIMAGE\"\n\n# maybe do fresh clone\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then\n    info \"ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout.\"\n    FRESH_CLONE=\"/tmp/electrum_build/appimage/fresh_clone/electrum\"\n    rm -rf \"$FRESH_CLONE\" 2>/dev/null || ( info \"we need sudo to rm prev FRESH_CLONE.\" && sudo rm -rf \"$FRESH_CLONE\" )\n    umask 0022\n    git clone \"$PROJECT_ROOT\" \"$FRESH_CLONE\"\n    cd \"$FRESH_CLONE\"\n    git checkout \"$ELECBUILD_COMMIT\"\n    PROJECT_ROOT_OR_FRESHCLONE_ROOT=\"$FRESH_CLONE\"\nelse\n    info \"not doing fresh clone.\"\nfi\n\n# build the type2-runtime binary, this build step uses a separate docker container\n# defined in the type2-runtime repo (patched with type2-runtime-reproducible-build.patch)\n\"$PROJECT_ROOT_OR_FRESHCLONE_ROOT/contrib/build-linux/appimage/make_type2_runtime.sh\" || fail \"Error building type2-runtime.\"\n\nDOCKER_RUN_FLAGS=\"\"\nif sh -c \": >/dev/tty\" >/dev/null 2>/dev/null; then\n    info \"/dev/tty is available and usable\"\n    DOCKER_RUN_FLAGS=\"-it\"\nfi\n\ninfo \"building binary...\"\n# check uid and maybe chown. see #8261\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then  # fresh clone (reproducible build)\n    if [ $(id -u) != \"1000\" ] || [ $(id -g) != \"1000\" ] ; then\n        info \"need to chown -R FRESH_CLONE dir. prompting for sudo.\"\n        sudo chown -R 1000:1000 \"$FRESH_CLONE\"\n    fi\nfi\ndocker run $DOCKER_RUN_FLAGS \\\n    --name electrum-appimage-builder-cont \\\n    -v \"$PROJECT_ROOT_OR_FRESHCLONE_ROOT\":/opt/electrum \\\n    --rm \\\n    --workdir /opt/electrum/contrib/build-linux/appimage \\\n    electrum-appimage-builder-img \\\n    ./make_appimage.sh\n\n# make sure resulting binary location is independent of fresh_clone\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then\n    mkdir --parents \"$DISTDIR/\"\n    cp -f \"$FRESH_CLONE/dist\"/* \"$DISTDIR/\"\nfi\n"
  },
  {
    "path": "contrib/build-linux/appimage/make_appimage.sh",
    "content": "#!/bin/bash\n\nset -e\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/../../..\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\nCONTRIB_APPIMAGE=\"$CONTRIB/build-linux/appimage\"\nDISTDIR=\"$PROJECT_ROOT/dist\"\nBUILDDIR=\"$CONTRIB_APPIMAGE/build/appimage\"\nAPPDIR=\"$BUILDDIR/electrum.AppDir\"\nCACHEDIR=\"$CONTRIB_APPIMAGE/.cache/appimage\"\nTYPE2_RUNTIME_REPO_DIR=\"$CACHEDIR/type2-runtime\"\nexport DLL_TARGET_DIR=\"$CACHEDIR/dlls\"\nPIP_CACHE_DIR=\"$CONTRIB_APPIMAGE/.cache/pip_cache\"\n\n. \"$CONTRIB\"/build_tools_util.sh\n\ngit -C \"$PROJECT_ROOT\" rev-parse 2>/dev/null || fail \"Building outside a git clone is not supported.\"\n\nexport GCC_STRIP_BINARIES=\"1\"\n\n# pinned versions\nPYTHON_VERSION=3.12.11\nPY_VER_MAJOR=\"3.12\"  # as it appears in fs paths\nPKG2APPIMAGE_COMMIT=\"a9c85b7e61a3a883f4a35c41c5decb5af88b6b5d\"\n\nVERSION=$(git describe --tags --dirty --always)\nAPPIMAGE=\"$DISTDIR/electrum-$VERSION-x86_64.AppImage\"\n\nrm -rf \"$BUILDDIR\"\nmkdir -p \"$APPDIR\" \"$CACHEDIR\" \"$PIP_CACHE_DIR\" \"$DISTDIR\" \"$DLL_TARGET_DIR\"\n\n# potential leftover from setuptools that might make pip put garbage in binary\nrm -rf \"$PROJECT_ROOT/build\"\n\n\ninfo \"downloading some dependencies.\"\ndownload_if_not_exist \"$CACHEDIR/functions.sh\" \"https://raw.githubusercontent.com/AppImage/pkg2appimage/$PKG2APPIMAGE_COMMIT/functions.sh\"\nverify_hash \"$CACHEDIR/functions.sh\" \"8f67711a28635b07ce539a9b083b8c12d5488c00003d6d726c7b134e553220ed\"\n\ndownload_if_not_exist \"$CACHEDIR/appimagetool\" \"https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage\"\nverify_hash \"$CACHEDIR/appimagetool\" \"46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1\"\n# note: desktop-file-utils in the docker image is needed to run desktop-file-validate for appimagetool <= 1.9.0, so it can be removed once\n# appimagetool tags a new release (see https://github.com/AppImage/appimagetool/pull/47)\n\ndownload_if_not_exist \"$CACHEDIR/Python-$PYTHON_VERSION.tar.xz\" \"https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tar.xz\"\nverify_hash \"$CACHEDIR/Python-$PYTHON_VERSION.tar.xz\" \"c30bb24b7f1e9a19b11b55a546434f74e739bb4c271a3e3a80ff4380d49f7adb\"\n\n\n\ninfo \"building python.\"\ntar xf \"$CACHEDIR/Python-$PYTHON_VERSION.tar.xz\" -C \"$CACHEDIR\"\n(\n    if [ -f \"$CACHEDIR/Python-$PYTHON_VERSION/python\" ]; then\n        info \"python already built, skipping\"\n        exit 0\n    fi\n    cd \"$CACHEDIR/Python-$PYTHON_VERSION\"\n    LC_ALL=C export BUILD_DATE=$(date -u -d \"@$SOURCE_DATE_EPOCH\" \"+%b %d %Y\")\n    LC_ALL=C export BUILD_TIME=$(date -u -d \"@$SOURCE_DATE_EPOCH\" \"+%H:%M:%S\")\n    # Patches taken from Ubuntu http://archive.ubuntu.com/ubuntu/pool/main/p/python3.11/python3.11_3.11.6-3.debian.tar.xz\n    patch -p1 < \"$CONTRIB_APPIMAGE/patches/python-3.11-reproducible-buildinfo.diff\"\n    ./configure \\\n        --cache-file=\"$CACHEDIR/python.config.cache\" \\\n        --prefix=\"$APPDIR/usr\" \\\n        --enable-ipv6 \\\n        --enable-shared \\\n        -q\n    make \"-j$CPU_COUNT\" -s || fail \"Could not build Python\"\n)\ninfo \"installing python.\"\n(\n    cd \"$CACHEDIR/Python-$PYTHON_VERSION\"\n    make -s install > /dev/null || fail \"Could not install Python\"\n    # When building in docker on macOS, python builds with .exe extension because the\n    # case insensitive file system of macOS leaks into docker. This causes the build\n    # to result in a different output on macOS compared to Linux. We simply patch\n    # sysconfigdata to remove the extension.\n    # Some more info: https://bugs.python.org/issue27631\n    sed -i -e 's/\\.exe//g' \"${APPDIR}/usr/lib/python${PY_VER_MAJOR}\"/_sysconfigdata*\n)\n\n\nif ls \"$DLL_TARGET_DIR\"/libsecp256k1.so.* 1> /dev/null 2>&1; then\n    info \"libsecp256k1 already built, skipping\"\nelse\n    \"$CONTRIB\"/make_libsecp256k1.sh || fail \"Could not build libsecp\"\nfi\ncp -f \"$DLL_TARGET_DIR\"/libsecp256k1.so.* \"$APPDIR/usr/lib/\" || fail \"Could not copy libsecp to its destination\"\n\n\nif [ -f \"$DLL_TARGET_DIR/libzbar.so.0\" ]; then\n    info \"libzbar already built, skipping\"\nelse\n    # note: could instead just use the libzbar0 pkg from debian/apt, but that is too old and missing fixes for CVE-2023-40889\n    \"$CONTRIB\"/make_zbar.sh || fail \"Could not build zbar\"\nfi\ncp -f \"$DLL_TARGET_DIR/libzbar.so.0\" \"$APPDIR/usr/lib/\" || fail \"Could not copy libzbar to its destination\"\n\n\nappdir_python() {\n    env \\\n        PYTHONNOUSERSITE=1 \\\n        LD_LIBRARY_PATH=\"$APPDIR/usr/lib:$APPDIR/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH+:$LD_LIBRARY_PATH}\" \\\n        \"$APPDIR/usr/bin/python${PY_VER_MAJOR}\" \"$@\"\n}\n\npython='appdir_python'\n\n\ninfo \"installing pip.\"\n\"$python\" -m ensurepip\n\nbreak_legacy_easy_install\n\n\ninfo \"preparing electrum-locale.\"\n(\n    \"$CONTRIB/locale/build_cleanlocale.sh\"\n    # we want the binary to have only compiled (.mo) locale files; not source (.po) files\n    rm -r \"$PROJECT_ROOT/electrum/locale/locale\"/*/electrum.po\n)\n\n\ninfo \"Installing build dependencies.\"\n# note: re pip installing from PyPI,\n#       we prefer compiling C extensions ourselves, instead of using binary wheels,\n#       hence \"--no-binary :all:\" flags. However, we specifically allow\n#       - PyQt6, as it's harder to build from source\n#       - cryptography, as it's harder to build from source\n#       - the whole of \"requirements-build-base.txt\", which includes pip and friends, as it also includes \"wheel\",\n#         and I am not quite sure how to break the circular dependence there (I guess we could introduce\n#         \"requirements-build-base-base.txt\" with just wheel in it...)\n\"$python\" -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \\\n    --cache-dir \"$PIP_CACHE_DIR\" -r \"$CONTRIB/deterministic-build/requirements-build-base.txt\"\n\"$python\" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \\\n    --cache-dir \"$PIP_CACHE_DIR\" -r \"$CONTRIB/deterministic-build/requirements-build-appimage.txt\"\n\n\n# opt out of compiling C extensions\nexport YARL_NO_EXTENSIONS=1\nexport FROZENLIST_NO_EXTENSIONS=1\n\nexport ELECTRUM_ECC_DONT_COMPILE=1\n\ninfo \"installing electrum and its dependencies.\"\n\"$python\" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \\\n    --cache-dir \"$PIP_CACHE_DIR\" -r \"$CONTRIB/deterministic-build/requirements.txt\"\n\"$python\" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --only-binary PyQt6,PyQt6-Qt6,cryptography --no-warn-script-location \\\n    --cache-dir \"$PIP_CACHE_DIR\" -r \"$CONTRIB/deterministic-build/requirements-binaries.txt\"\n\"$python\" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \\\n    --cache-dir \"$PIP_CACHE_DIR\" -r \"$CONTRIB/deterministic-build/requirements-hw.txt\"\n\n\"$python\" -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \\\n    --cache-dir \"$PIP_CACHE_DIR\" \"$PROJECT_ROOT\"\n\n# was only needed during build time, not runtime\n\"$python\" -m pip uninstall -y Cython\n\n\ninfo \"desktop integration.\"\ncp \"$PROJECT_ROOT/electrum.desktop\" \"$APPDIR/electrum.desktop\"\ncp \"$PROJECT_ROOT/electrum/gui/icons/electrum.png\" \"$APPDIR/electrum.png\"\n\n\n# add launcher\ncp \"$CONTRIB_APPIMAGE/apprun.sh\" \"$APPDIR/AppRun\"\n\ninfo \"finalizing AppDir.\"\n(\n    export PKG2AICOMMIT=\"$PKG2APPIMAGE_COMMIT\"\n    . \"$CACHEDIR/functions.sh\"\n\n    cd \"$APPDIR\"\n    # copy system dependencies\n    copy_deps; copy_deps; copy_deps\n    move_lib\n\n    # apply global appimage blacklist to exclude stuff\n    # move usr/include out of the way to preserve usr/include/python${PY_VER_MAJOR}.\n    mv usr/include usr/include.tmp\n    delete_blacklisted\n    mv usr/include.tmp usr/include\n)\n\ninfo \"Copying additional libraries\"\n(\n    # On some systems it can cause problems to use the system libusb (on AppImage excludelist)\n    cp -f /usr/lib/x86_64-linux-gnu/libusb-1.0.so \"$APPDIR/usr/lib/libusb-1.0.so\" || fail \"Could not copy libusb\"\n    # some distros lack libxkbcommon-x11\n    cp -f /usr/lib/x86_64-linux-gnu/libxkbcommon-x11.so.0 \"$APPDIR\"/usr/lib/x86_64-linux-gnu || fail \"Could not copy libxkbcommon-x11\"\n    # some distros lack some libxcb libraries (see https://github.com/Electron-Cash/Electron-Cash/issues/2196)\n    cp -f /usr/lib/x86_64-linux-gnu/libxcb-* \"$APPDIR\"/usr/lib/x86_64-linux-gnu || fail \"Could not copy libxcb\"\n)\n\ninfo \"stripping binaries from debug symbols.\"\n# \"-R .note.gnu.build-id\" also strips the build id\n# \"-R .comment\" also strips the GCC version information\nstrip_binaries()\n{\n    chmod u+w -R \"$APPDIR\"\n    {\n        printf '%s\\0' \"$APPDIR/usr/bin/python${PY_VER_MAJOR}\"\n        find \"$APPDIR\" -type f -regex '.*\\.so\\(\\.[0-9.]+\\)?$' -print0\n    } | xargs -0 --no-run-if-empty --verbose strip -R .note.gnu.build-id -R .comment\n}\nstrip_binaries\n\nremove_emptydirs()\n{\n    find \"$APPDIR\" -type d -empty -print0 | xargs -0 --no-run-if-empty rmdir -vp --ignore-fail-on-non-empty\n}\nremove_emptydirs\n\n\ninfo \"removing some unneeded stuff to decrease binary size.\"\nrm -rf \"$APPDIR\"/usr/{share,include}\nPYDIR=\"$APPDIR/usr/lib/python${PY_VER_MAJOR}\"\nrm -rf \"$PYDIR\"/{test,ensurepip,lib2to3,idlelib,turtledemo}\nrm -rf \"$PYDIR\"/{ctypes,sqlite3,tkinter,unittest}/test\nrm -rf \"$PYDIR\"/distutils/{command,tests}\nrm -rf \"$PYDIR\"/config-3.*-x86_64-linux-gnu\nrm -rf \"$PYDIR\"/site-packages/{opt,pip,setuptools,wheel}\nrm -rf \"$PYDIR\"/site-packages/Cryptodome/SelfTest\nrm -rf \"$PYDIR\"/site-packages/{psutil,qrcode,websocket}/tests\n# rm lots of unused parts of Qt/PyQt. (assuming PyQt 6 layout)\nfor component in connectivity declarative help location multimedia quickcontrols2 serialport webengine websockets xmlpatterns ; do\n    rm -rf \"$PYDIR\"/site-packages/PyQt6/Qt6/translations/qt${component}_*\n    rm -rf \"$PYDIR\"/site-packages/PyQt6/Qt6/resources/qt${component}_*\ndone\nrm -rf \"$PYDIR\"/site-packages/PyQt6/Qt6/{qml,libexec}\nrm -rf \"$PYDIR\"/site-packages/PyQt6/{pyrcc*.so,pylupdate*.so,uic}\nrm -rf \"$PYDIR\"/site-packages/PyQt6/Qt6/plugins/{bearer,gamepads,geometryloaders,geoservices,playlistformats,position,renderplugins,sceneparsers,sensors,sqldrivers,texttospeech,webview}\nfor component in Bluetooth Concurrent Designer Help Location NetworkAuth Nfc Positioning PositioningQuick Qml Quick Sensors SerialPort Sql Test Web Xml Labs ShaderTools SpatialAudio ; do\n    rm -rf \"$PYDIR\"/site-packages/PyQt6/Qt6/lib/libQt6${component}*\n    rm -rf \"$PYDIR\"/site-packages/PyQt6/Qt${component}*\n    rm -rf \"$PYDIR\"/site-packages/PyQt6/bindings/Qt${component}*\ndone\nfor component in Qml Quick ; do\n    rm -rf \"$PYDIR\"/site-packages/PyQt6/Qt6/lib/libQt6*${component}.so*\ndone\nrm -rf \"$PYDIR\"/site-packages/PyQt6/Qt.so\n\n# these are deleted as they were not deterministic; and are not needed anyway\nfind \"$APPDIR\" -path '*/__pycache__*' -delete\n# although note that *.dist-info might be needed by certain packages...\n# e.g. slip10 uses importlib that needs it\nfor f in \"$PYDIR\"/site-packages/slip10-*.dist-info; do mv \"$f\" \"$(echo \"$f\" | sed s/\\.dist-info/\\.dist-info2/)\"; done\nrm -rf \"$PYDIR\"/site-packages/*.dist-info/\nrm -rf \"$PYDIR\"/site-packages/*.egg-info/\nfor f in \"$PYDIR\"/site-packages/slip10-*.dist-info2; do mv \"$f\" \"$(echo \"$f\" | sed s/\\.dist-info2/\\.dist-info/)\"; done\n\n\nfind -exec touch -h -d '2000-11-11T11:11:11+00:00' {} +\n\n\ninfo \"creating the AppImage.\"\n(\n    cd \"$BUILDDIR\"\n    cp \"$CACHEDIR/appimagetool\" \"$CACHEDIR/appimagetool_copy\"\n    # zero out \"appimage\" magic bytes, as on some systems they confuse the linker\n    sed -i 's|AI\\x02|\\x00\\x00\\x00|' \"$CACHEDIR/appimagetool_copy\"\n    chmod +x \"$CACHEDIR/appimagetool_copy\"\n    \"$CACHEDIR/appimagetool_copy\" --appimage-extract\n    # We build a small wrapper for mksquashfs that removes the -mkfs-time option\n    # as it conflicts with SOURCE_DATE_EPOCH.\n    mv \"$BUILDDIR/squashfs-root/usr/bin/mksquashfs\" \"$BUILDDIR/squashfs-root/usr/bin/mksquashfs_orig\"\n    cat > \"$BUILDDIR/squashfs-root/usr/bin/mksquashfs\" << EOF\n#!/bin/sh\nargs=\\$(echo \"\\$@\" | sed -e 's/-mkfs-time 0//')\n\"$BUILDDIR/squashfs-root/usr/bin/mksquashfs_orig\" \\$args\nEOF\n    chmod +x \"$BUILDDIR/squashfs-root/usr/bin/mksquashfs\"\n    env VERSION=\"$VERSION\" ARCH=x86_64 ./squashfs-root/AppRun --runtime-file \"$TYPE2_RUNTIME_REPO_DIR/runtime-x86_64\" --no-appstream --verbose \"$APPDIR\" \"$APPIMAGE\"\n)\n\n\ninfo \"done.\"\nls -la \"$DISTDIR\"\nsha256sum \"$DISTDIR\"/*\n"
  },
  {
    "path": "contrib/build-linux/appimage/make_type2_runtime.sh",
    "content": "#!/bin/bash\n\nset -e\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/../../..\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\nCONTRIB_APPIMAGE=\"$CONTRIB/build-linux/appimage\"\n\n# when bumping the runtime commit also check if the `type2-runtime-reproducible-build.patch` still works\nTYPE2_RUNTIME_COMMIT=\"5e7217b7cfeecee1491c2d251e355c3cf8ba6e4d\"\nTYPE2_RUNTIME_REPO=\"https://github.com/AppImage/type2-runtime.git\"\n\n. \"$CONTRIB\"/build_tools_util.sh\n\n\nTYPE2_RUNTIME_REPO_DIR=\"$PROJECT_ROOT/contrib/build-linux/appimage/.cache/appimage/type2-runtime\"\nif [ -f \"$TYPE2_RUNTIME_REPO_DIR/runtime-x86_64\" ]; then\n    info \"type2-runtime already built, skipping\"\n    exit 0\nfi\nclone_or_update_repo \"$TYPE2_RUNTIME_REPO\" \"$TYPE2_RUNTIME_COMMIT\" \"$TYPE2_RUNTIME_REPO_DIR\"\n\n# Apply patch to make runtime build reproducible\ninfo \"Applying type2-runtime patch...\"\ncd \"$TYPE2_RUNTIME_REPO_DIR\"\ngit apply \"$CONTRIB_APPIMAGE/patches/type2-runtime-reproducible-build.patch\" || fail \"Failed to apply runtime repo patch\"\n\ninfo \"building type2-runtime in build container...\"\ncd \"$TYPE2_RUNTIME_REPO_DIR/scripts/docker\"\nenv ARCH=x86_64 ./build-with-docker.sh\nmv \"./runtime-x86_64\" \"$TYPE2_RUNTIME_REPO_DIR/\"\n\n# clean up the empty created 'out' dir to prevent permission issues\nrm -rf \"$TYPE2_RUNTIME_REPO_DIR/out\"\n\ninfo \"runtime build successful: $(sha256sum \"$TYPE2_RUNTIME_REPO_DIR/runtime-x86_64\")\"\n"
  },
  {
    "path": "contrib/build-linux/appimage/patches/python-3.11-reproducible-buildinfo.diff",
    "content": "Description: Build reproduceable date and time into build info\n Build information is encoded into getbuildinfo.o at build time.\n Use the date and time from the debian changelog, to make this reproduceable.\n\nForwarded: no\n\n--- a/Makefile.pre.in\n+++ b/Makefile.pre.in\n@@ -1248,6 +1248,8 @@\n \t      -DGITVERSION=\"\\\"`LC_ALL=C $(GITVERSION)`\\\"\" \\\n \t      -DGITTAG=\"\\\"`LC_ALL=C $(GITTAG)`\\\"\" \\\n \t      -DGITBRANCH=\"\\\"`LC_ALL=C $(GITBRANCH)`\\\"\" \\\n+\t      $(if $(BUILD_DATE),-DDATE='\"$(BUILD_DATE)\"') \\\n+\t      $(if $(BUILD_TIME),-DTIME='\"$(BUILD_TIME)\"') \\\n \t      -o $@ $(srcdir)/Modules/getbuildinfo.c\n\n Modules/getpath.o: $(srcdir)/Modules/getpath.c Python/frozen_modules/getpath.h Makefile $(PYTHON_HEADERS)\n"
  },
  {
    "path": "contrib/build-linux/appimage/patches/type2-runtime-reproducible-build.patch",
    "content": "From 0c54d91dd1d33235ae97566600e692edfb613642 Mon Sep 17 00:00:00 2001\nFrom: f321x <f@f321x.com>\nDate: Thu, 10 Jul 2025 17:45:20 +0200\nSubject: [PATCH] make docker build reproducible\n\nattempts to make the docker build more reproducible by:\n* pinning the docker image (alpine:3.21) to a hash\n* version pinning the apk packages in the dockerfile\n* setting TZ, LC_ALL and SOURCE_DATE_EPOCH in the container\n* only building single threaded (make -j1)\n* use a fixed build directory in `build-runtime.sh` instead of mktemp\n* prevent linker from adding build id (-Wl,--build-id=none)\n* replace absolute build paths in debug info with relative paths\n  (-fdebug-prefix-map=$(PWD)=.)\n* replace absolute paths in all compiler output with relative paths\n  (-ffile-prefix-map=$(PWD)=.)\n* stop adding gnu-debuglink to runtime binary\n---\n scripts/build-runtime.sh               | 18 +++++++++++----\n scripts/common/install-dependencies.sh |  2 +-\n scripts/docker/Dockerfile              | 32 ++++++++++++++++++++++----\n src/runtime/Makefile                   |  2 +-\n 4 files changed, 42 insertions(+), 12 deletions(-)\n\ndiff --git a/scripts/build-runtime.sh b/scripts/build-runtime.sh\nindex 3ce3b91..e11f082 100755\n--- a/scripts/build-runtime.sh\n+++ b/scripts/build-runtime.sh\n@@ -8,8 +8,10 @@ set -euo pipefail\n out_dir=\"$(readlink -f \"$(pwd)\")\"/out\n mkdir -p \"$out_dir\"\n\n-# we create a temporary build directory\n-build_dir=\"$(mktemp -d -t type2-runtime-build-XXXXXX)\"\n+# we create a temporary build directory with a fixed name for reproducibility\n+build_dir=\"$(readlink -f \"$(pwd)\")\"/build-runtime-temp\n+rm -rf \"$build_dir\"\n+mkdir -p \"$build_dir\"\n\n # since the plain ol' Makefile doesn't support out-of-source builds at all, we need to copy all the files\n cp -R src \"$build_dir\"/\n@@ -17,13 +19,14 @@ cp -R src \"$build_dir\"/\n pushd \"$build_dir\"\n\n pushd src/runtime/\n-make -j\"$(nproc)\" runtime\n+make -j1 runtime\n\n file runtime\n\n objcopy --only-keep-debug runtime runtime.debug\n\n-strip --strip-debug --strip-unneeded runtime\n+# strip --strip-debug --strip-unneeded runtime\n+strip --strip-all runtime\n\n ls -lh runtime runtime.debug\n\n@@ -50,7 +53,7 @@ fi\n mv runtime runtime-\"$architecture\"\n mv runtime.debug runtime-\"$architecture\".debug\n\n-objcopy --add-gnu-debuglink runtime-\"$architecture\".debug runtime-\"$architecture\"\n+# objcopy --add-gnu-debuglink runtime-\"$architecture\".debug runtime-\"$architecture\"\n\n # \"classic\" magic bytes which cannot be embedded with compiler magic, always do AFTER strip\n # needs to be done after calls to objcopy, strip etc.\n@@ -61,3 +64,8 @@ cp runtime-\"$architecture\" \"$out_dir\"/\n cp runtime-\"$architecture\".debug \"$out_dir\"/\n\n ls -al \"$out_dir\"\n+\n+# cleanup\n+popd  # return to build_dir\n+popd  # return to original working directory\n+rm -rf \"$build_dir\"\ndiff --git a/scripts/common/install-dependencies.sh b/scripts/common/install-dependencies.sh\nindex 0e21cdb..5237079 100755\n--- a/scripts/common/install-dependencies.sh\n+++ b/scripts/common/install-dependencies.sh\n@@ -39,7 +39,7 @@ tar xf 0.5.2.tar.gz\n pushd squashfuse-*/\n ./autogen.sh\n ./configure LDFLAGS=\"-static\"\n-make -j\"$(nproc)\"\n+make -j1\n make install\n /usr/bin/install -c -m 644 ./*.h '/usr/local/include/squashfuse'\n popd\ndiff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile\nindex 07b6533..fba9c6e 100644\n--- a/scripts/docker/Dockerfile\n+++ b/scripts/docker/Dockerfile\n@@ -1,13 +1,35 @@\n-FROM alpine:3.21\n+FROM alpine:3.21@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c\n\n # includes dependencies from https://git.alpinelinux.org/aports/tree/main/fuse3/APKBUILD\n RUN apk add --no-cache \\\n-    bash alpine-sdk util-linux strace file autoconf automake libtool xz \\\n-    eudev-dev gettext-dev linux-headers meson \\\n-    zstd-dev zstd-static zlib-dev zlib-static clang musl-dev mimalloc-dev\n+    bash=5.2.37-r0 \\\n+    alpine-sdk=1.1-r0 \\\n+    util-linux=2.40.4-r1 \\\n+    strace=6.12-r0 \\\n+    file=5.46-r2 \\\n+    autoconf=2.72-r0 \\\n+    automake=1.17-r0 \\\n+    libtool=2.4.7-r3 \\\n+    xz=5.6.3-r1 \\\n+    eudev-dev=3.2.14-r5 \\\n+    gettext-dev=0.22.5-r0 \\\n+    linux-headers=6.6-r1 \\\n+    meson=1.6.1-r0 \\\n+    zstd-dev=1.5.6-r2 \\\n+    zstd-static=1.5.6-r2 \\\n+    zlib-dev=1.3.1-r2 \\\n+    zlib-static=1.3.1-r2 \\\n+    clang19=19.1.4-r0 \\\n+    musl-dev=1.2.5-r9 \\\n+    mimalloc2-dev=2.1.7-r0\n\n COPY scripts/common/install-dependencies.sh /tmp/scripts/common/install-dependencies.sh\n COPY patches/ /tmp/patches/\n\n+# Set environment variables for reproducible build\n+ENV SOURCE_DATE_EPOCH=1640995200\n+ENV TZ=UTC\n+ENV LC_ALL=C\n+\n WORKDIR /tmp\n-RUN bash scripts/common/install-dependencies.sh\n+RUN bash scripts/common/install-dependencies.sh\n\\ No newline at end of file\ndiff --git a/src/runtime/Makefile b/src/runtime/Makefile\nindex 9fd4165..3a3cbaa 100644\n--- a/src/runtime/Makefile\n+++ b/src/runtime/Makefile\n@@ -1,6 +1,6 @@\n GIT_COMMIT := $(shell cat version)\n CC            = clang\n-CFLAGS        = -std=gnu99 -Os -D_FILE_OFFSET_BITS=64 -DGIT_COMMIT=\\\"$(GIT_COMMIT)\\\" -T data_sections.ld -ffunction-sections -fdata-sections -Wl,--gc-sections -static -Wall -Werror -static-pie\n+CFLAGS        = -std=gnu99 -Os -D_FILE_OFFSET_BITS=64 -DGIT_COMMIT=\\\"$(GIT_COMMIT)\\\" -T data_sections.ld -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--build-id=none -static -Wall -Werror -static-pie -fdebug-prefix-map=$(PWD)=. -ffile-prefix-map=$(PWD)=.\n LIBS          = -lsquashfuse -lsquashfuse_ll -lzstd -lz -lfuse3 -lmimalloc\n\n all: runtime\n--\n2.50.0\n"
  },
  {
    "path": "contrib/build-linux/sdist/.dockerignore",
    "content": ""
  },
  {
    "path": "contrib/build-linux/sdist/Dockerfile",
    "content": "FROM debian:bookworm@sha256:b877a1a3fdf02469440f1768cf69c9771338a875b7add5e80c45b756c92ac20a\n\nENV LC_ALL=C.UTF-8 LANG=C.UTF-8\nENV DEBIAN_FRONTEND=noninteractive\n\nRUN apt-get update -q && \\\n    apt-get install -qy \\\n        git \\\n        gettext \\\n        python3 \\\n        python3-pip \\\n        python3-setuptools \\\n        python3-venv \\\n        && \\\n    rm -rf /var/lib/apt/lists/* && \\\n    apt-get autoremove -y && \\\n    apt-get clean\n\n# create new user to avoid using root; but with sudo access and no password for convenience.\nARG UID=1000\nRUN if [ \"$UID\" != \"0\" ] ; then useradd --uid $UID --create-home --shell /bin/bash \"user\" ; fi\nRUN usermod -append --groups sudo $(id -nu $UID || echo \"user\")\nRUN echo \"%sudo ALL=(ALL) NOPASSWD: ALL\" >> /etc/sudoers\nRUN HOME_DIR=$(getent passwd $UID | cut -d: -f6)\nENV WORK_DIR=\"${HOME_DIR}/wspace\" \\\n    PATH=\"${HOME_DIR}/.local/bin:${PATH}\"\nWORKDIR ${WORK_DIR}\nRUN chown --recursive ${UID} ${WORK_DIR}\nUSER ${UID}\n"
  },
  {
    "path": "contrib/build-linux/sdist/README.md",
    "content": "# Source tarballs\n\n✓ _These tarballs should be reproducible, meaning you should be able to generate\n   distributables that match the official releases._\n\nThis assumes an Ubuntu (x86_64) host, but it should not be too hard to adapt to another\nsimilar system.\n\nWe distribute two tarballs, a \"normal\" one (the default, recommended for users),\nand a strictly source-only one (for Linux distro packagers).\nThe normal tarball, in addition to including everything from\nthe source-only one, also includes:\n- compiled (`.mo`) locale files (in addition to source `.po` locale files)\n- compiled (`_pb2.py`) protobuf files (in addition to source `.proto` files)\n- the `packages/` folder containing source-only pure-python runtime dependencies\n\n\n## Build steps\n\n1. Install Docker\n\n    See [`contrib/docker_notes.md`](../../docker_notes.md).\n\n    (worth reading even if you already have docker)\n\n2. Build tarball\n\n    (set envvar `OMIT_UNCLEAN_FILES=1` to build the \"source-only\" tarball)\n    ```\n    $ ./build.sh\n    ```\n    If you want reproducibility, try instead e.g.:\n    ```\n    $ ELECBUILD_COMMIT=HEAD ELECBUILD_NOCACHE=1 ./build.sh\n    $ ELECBUILD_COMMIT=HEAD ELECBUILD_NOCACHE=1 OMIT_UNCLEAN_FILES=1 ./build.sh\n    ```\n\n3. The generated distributables are in `./dist`.\n\n\n## Differences between the `sourceonly` vs \"normal\" tar.gz\n\nThese scripts can either build a source-only or a \"normal\" tarball.\nThe official release process builds both.\n\nThe source-only tarball is aimed at Linux distro packagers.\nUsers wanting to run from source should typically use the normal tarball.\n\nThe differences are as follows:\n- the normal tarball bundles all the pure-python dependencies of Electrum.\n  These are placed into the `packages/` folder, and they are automatically\n  found and used at runtime.\n- the normal tarball includes compiled (.mo) locale files, the source-only tarball does not.\n  Both tarballs contain (.po) source locale files. If you are packaging for a Linux distro,\n  you probably want to compile the .mo locale files yourself (see `contrib/locale/build_locale.sh`).\n- the normal tarball includes generated `*_pb2.py` files. These are created\n  using `protobuf-compiler` from `.proto` files (see `contrib/generate_payreqpb2.sh`)\n"
  },
  {
    "path": "contrib/build-linux/sdist/build.sh",
    "content": "#!/bin/bash\n#\n# env vars:\n# - ELECBUILD_NOCACHE: if set, forces rebuild of docker image\n# - ELECBUILD_COMMIT: if set, do a fresh clone and git checkout\n\nset -e\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/../../..\"\nPROJECT_ROOT_OR_FRESHCLONE_ROOT=\"$PROJECT_ROOT\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\nCONTRIB_SDIST=\"$CONTRIB/build-linux/sdist\"\nDISTDIR=\"$PROJECT_ROOT/dist\"\nBUILD_UID=$(/usr/bin/stat -c %u \"$PROJECT_ROOT\")\n\n. \"$CONTRIB\"/build_tools_util.sh\n\n\nDOCKER_BUILD_FLAGS=\"\"\nif [ ! -z \"$ELECBUILD_NOCACHE\" ] ; then\n    info \"ELECBUILD_NOCACHE is set. forcing rebuild of docker image.\"\n    DOCKER_BUILD_FLAGS=\"--pull --no-cache\"\nfi\n\nif [ -z \"$ELECBUILD_COMMIT\" ] ; then  # local dev build\n    DOCKER_BUILD_FLAGS=\"$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID\"\nfi\n\ninfo \"building docker image.\"\ndocker build \\\n    $DOCKER_BUILD_FLAGS \\\n    -t electrum-sdist-builder-img \\\n    \"$CONTRIB_SDIST\"\n\n# maybe do fresh clone\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then\n    info \"ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout.\"\n    FRESH_CLONE=\"/tmp/electrum_build/sdist/fresh_clone/electrum\"\n    rm -rf \"$FRESH_CLONE\" 2>/dev/null || ( info \"we need sudo to rm prev FRESH_CLONE.\" && sudo rm -rf \"$FRESH_CLONE\" )\n    umask 0022\n    git clone \"$PROJECT_ROOT\" \"$FRESH_CLONE\"\n    cd \"$FRESH_CLONE\"\n    git checkout \"$ELECBUILD_COMMIT\"\n    PROJECT_ROOT_OR_FRESHCLONE_ROOT=\"$FRESH_CLONE\"\nelse\n    info \"not doing fresh clone.\"\nfi\n\nDOCKER_RUN_FLAGS=\"\"\nif sh -c \": >/dev/tty\" >/dev/null 2>/dev/null; then\n    info \"/dev/tty is available and usable\"\n    DOCKER_RUN_FLAGS=\"-it\"\nfi\n\ninfo \"building binary...\"\n# check uid and maybe chown. see #8261\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then  # fresh clone (reproducible build)\n    if [ $(id -u) != \"1000\" ] || [ $(id -g) != \"1000\" ] ; then\n        info \"need to chown -R FRESH_CLONE dir. prompting for sudo.\"\n        sudo chown -R 1000:1000 \"$FRESH_CLONE\"\n    fi\nfi\ndocker run $DOCKER_RUN_FLAGS \\\n    --name electrum-sdist-builder-cont \\\n    -v \"$PROJECT_ROOT_OR_FRESHCLONE_ROOT\":/opt/electrum \\\n    --rm \\\n    --workdir /opt/electrum/contrib/build-linux/sdist \\\n    --env OMIT_UNCLEAN_FILES \\\n    electrum-sdist-builder-img \\\n    ./make_sdist.sh\n\n# make sure resulting binary location is independent of fresh_clone\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then\n    mkdir --parents \"$DISTDIR/\"\n    cp -f \"$FRESH_CLONE/dist\"/* \"$DISTDIR/\"\nfi\n"
  },
  {
    "path": "contrib/build-linux/sdist/make_sdist.sh",
    "content": "#!/bin/bash\n\nset -e\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/../../..\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\nCONTRIB_SDIST=\"$CONTRIB/build-linux/sdist\"\nDISTDIR=\"$PROJECT_ROOT/dist\"\nBUILDDIR=\"$CONTRIB_SDIST/build\"\n\n. \"$CONTRIB\"/build_tools_util.sh\n\ngit -C \"$PROJECT_ROOT\" rev-parse 2>/dev/null || fail \"Building outside a git clone is not supported.\"\n\nrm -rf \"$BUILDDIR\"\nmkdir -p \"$BUILDDIR\" \"$DISTDIR\"\n\npython3 --version || fail \"python interpreter not found\"\n\nbreak_legacy_easy_install\n\nrm -rf \"$PROJECT_ROOT/packages/\"\nif ([ \"$OMIT_UNCLEAN_FILES\" != 1 ]); then\n    \"$CONTRIB\"/make_packages.sh || fail \"make_packages failed\"\nfi\n\ninfo \"preparing electrum-locale.\"\n(\n    \"$CONTRIB/locale/build_cleanlocale.sh\"\n    # By default, include both source (.po) and compiled (.mo) locale files in the source dist.\n    # Set option OMIT_UNCLEAN_FILES=1 to exclude the compiled locale files\n    # see https://askubuntu.com/a/144139 (also see MANIFEST.in)\n    if ([ \"$OMIT_UNCLEAN_FILES\" = 1 ]); then\n        rm -r \"$PROJECT_ROOT/electrum/locale/locale\"/*/LC_MESSAGES/electrum.mo\n    fi\n)\n\nif ([ \"$OMIT_UNCLEAN_FILES\" = 1 ]); then\n    # FIXME side-effecting repo... though in practice, this script probably runs in fresh_clone\n    rm -f \"$PROJECT_ROOT/electrum/paymentrequest_pb2.py\"\nfi\n\n(\n    cd \"$PROJECT_ROOT\"\n\n    find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} +\n\n    # note: .zip sdists would not be reproducible due to https://bugs.python.org/issue40963\n    if ([ \"$OMIT_UNCLEAN_FILES\" = 1 ]); then\n        PY_DISTDIR=\"$BUILDDIR/dist1/_sourceonly\" # The DISTDIR variable of this script is only used to find where the output is *finally* placed.\n    else\n        PY_DISTDIR=\"$BUILDDIR/dist1\"\n    fi\n    # build initial tar.gz\n    python3 setup.py --quiet sdist --format=gztar --dist-dir=\"$PY_DISTDIR\"\n\n    VERSION=$(\"$CONTRIB\"/print_electrum_version.py)\n    if ([ \"$OMIT_UNCLEAN_FILES\" = 1 ]); then\n        FINAL_DISTNAME=\"Electrum-sourceonly-$VERSION.tar.gz\"\n    else\n        FINAL_DISTNAME=\"Electrum-$VERSION.tar.gz\"\n    fi\n    if ([ \"$OMIT_UNCLEAN_FILES\" = 1 ]); then\n        mv \"$PY_DISTDIR/Electrum-$VERSION.tar.gz\" \"$PY_DISTDIR/../$FINAL_DISTNAME\"\n        rmdir \"$PY_DISTDIR\"\n    fi\n\n    # the initial tar.gz is not reproducible, see https://github.com/pypa/setuptools/issues/2133\n    # so we untar, fix timestamps, and then re-tar\n    mkdir -p \"$BUILDDIR/dist2\"\n    cd \"$BUILDDIR/dist2\"\n    tar -xzf \"$BUILDDIR/dist1/$FINAL_DISTNAME\"\n    find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} +\n    GZIP=-n tar --sort=name -czf \"$FINAL_DISTNAME\" \"Electrum-$VERSION/\"\n    mv \"$FINAL_DISTNAME\" \"$DISTDIR/$FINAL_DISTNAME\"\n)\n\n\ninfo \"done.\"\nls -la \"$DISTDIR\"\nsha256sum \"$DISTDIR\"/*\n"
  },
  {
    "path": "contrib/build-wine/.dockerignore",
    "content": "tmp/\nbuild/\n.cache/\ndist/\nsigned/\n"
  },
  {
    "path": "contrib/build-wine/Dockerfile",
    "content": "FROM debian:trixie@sha256:13f29b6806e531c3ff3b565bb6eed73f2132506c8c9d41bb996065ca20fb27f2\n\n# need ca-certificates before using snapshot packages\nRUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \\\n    ca-certificates\n\n# pin the distro packages.\nCOPY apt.sources.list /etc/apt/sources.list\nCOPY apt.preferences /etc/apt/preferences.d/snapshot\n\nENV LC_ALL=C.UTF-8 LANG=C.UTF-8\nENV DEBIAN_FRONTEND=noninteractive\n\nRUN dpkg --add-architecture i386 && \\\n    apt-get update -q && \\\n    apt-get install -qy --allow-downgrades \\\n        lsb-release \\\n        wget \\\n        gnupg2 \\\n        dirmngr \\\n        python3 \\\n        git \\\n        p7zip-full \\\n        make \\\n        cmake \\\n        pkgconf \\\n        mingw-w64 \\\n        mingw-w64-tools \\\n        autotools-dev \\\n        autoconf \\\n        autopoint \\\n        libtool \\\n        gettext \\\n        sudo \\\n        nsis \\\n        && \\\n    rm -rf /var/lib/apt/lists/* && \\\n    apt-get autoremove -y && \\\n    apt-get clean\n\nRUN DEBIAN_CODENAME=$(lsb_release --codename --short) && \\\n    WINEVERSION=\"11.0.0.0~${DEBIAN_CODENAME}-1\" && \\\n    wget -nc https://dl.winehq.org/wine-builds/winehq.key && \\\n        echo \"d965d646defe94b3dfba6d5b4406900ac6c81065428bf9d9303ad7a72ee8d1b8 winehq.key\" | sha256sum -c - && \\\n        cat winehq.key | gpg --dearmor -o /etc/apt/keyrings/winehq.gpg && \\\n        echo deb [signed-by=/etc/apt/keyrings/winehq.gpg] https://dl.winehq.org/wine-builds/debian/ ${DEBIAN_CODENAME} main >> /etc/apt/sources.list.d/winehq.list && \\\n        rm winehq.key && \\\n    apt-get update -q && \\\n    apt-get install -qy --allow-downgrades \\\n        wine-stable-amd64:amd64=${WINEVERSION} \\\n        wine-stable-i386:i386=${WINEVERSION} \\\n        wine-stable:amd64=${WINEVERSION} \\\n        winehq-stable:amd64=${WINEVERSION} \\\n        && \\\n    rm -rf /var/lib/apt/lists/* && \\\n    apt-get autoremove -y && \\\n    apt-get clean\n\n# create new user to avoid using root; but with sudo access and no password for convenience.\nARG UID=1000\nRUN if [ \"$UID\" != \"0\" ] ; then useradd --uid $UID --create-home --shell /bin/bash \"user\" ; fi\nRUN usermod -append --groups sudo $(id -nu $UID || echo \"user\")\nRUN echo \"%sudo ALL=(ALL) NOPASSWD: ALL\" >> /etc/sudoers\nRUN HOME_DIR=$(getent passwd $UID | cut -d: -f6)\nENV WORK_DIR=\"${HOME_DIR}/wspace\" \\\n    PATH=\"${HOME_DIR}/.local/bin:${PATH}\"\nWORKDIR ${WORK_DIR}\nRUN chown --recursive ${UID} ${WORK_DIR}\nRUN chown ${UID} /opt\nUSER ${UID}\n\nRUN mkdir --parents \"/opt/wine64/drive_c/electrum\"\n"
  },
  {
    "path": "contrib/build-wine/README.md",
    "content": "# Windows binaries\n\n✓ _These binaries should be reproducible, meaning you should be able to generate\n   binaries that match the official releases._\n\n- _Minimum supported target system (i.e. what end-users need): x86_64, Windows 10 (1809)_\n\nThis assumes an Ubuntu (x86_64) host, but it should not be too hard to adapt to another\nsimilar system.\n\n1. Install Docker\n\n    See [`contrib/docker_notes.md`](../docker_notes.md).\n\n    (worth reading even if you already have docker)\n\n    Note: older versions of Docker might not work well\n    (see [#6971](https://github.com/spesmilo/electrum/issues/6971)).\n    If having problems, try to upgrade to at least `docker 20.10`.\n\n2. Build Windows binaries\n\n    ```\n    $ ./build.sh\n    ```\n    If you want reproducibility, try instead e.g.:\n    ```\n    $ ELECBUILD_COMMIT=HEAD ./build.sh\n    ```\n\n3. The generated binaries are in `./contrib/build-wine/dist`.\n\n\n\n## Code Signing\n\nElectrum Windows builds are signed with a Microsoft Authenticode™ code signing\ncertificate in addition to the GPG-based signatures.\n\nThe advantage of using Authenticode is that Electrum users won't receive a\nWindows SmartScreen warning when starting it.\n\nThe release signing procedure involves a signer (the holder of the\ncertificate/key) and one or multiple trusted verifiers:\n\n\n| Signer                                                    | Verifier                             |\n|-----------------------------------------------------------|--------------------------------------|\n| Build .exe files using `make_win.sh`                      |                                      |\n| Sign .exe with `./sign.sh`                                |                                      |\n| Upload signed files to download server                    |                                      |\n|                                                           | Build .exe files using `make_win.sh` |\n|                                                           | Compare files using `unsign.sh`      |\n|                                                           | Sign .exe file using `gpg -b`        |\n\n| Signer and verifiers:                                                                            |\n|--------------------------------------------------------------------------------------------------|\n| Upload signatures to 'electrum-signatures' repo, as `$version/$filename.$builder.asc`            |\n\n\n\n## Verify Integrity of signed binary\n\nEvery user can verify that the official binary was created from the source code in this\nrepository. To do so, the Authenticode signature needs to be stripped since the signature\nis not reproducible.\n\nThis procedure removes the differences between the signed and unsigned binary:\n\n1. Remove the signature from the signed binary using osslsigncode or signtool.\n2. Set the COFF image checksum for the signed binary to 0x0. This is necessary\n   because pyinstaller doesn't generate a checksum.\n3. Append null bytes to the _unsigned_ binary until the byte count is a multiple\n   of 8.\n\nThe script `unsign.sh` performs these steps.\n\n## FAQ\n\n### How to investigate diff between binaries if reproducibility fails?\n`pyi-archive_viewer` is needed, for that run `$ pip install pyinstaller`.\nAs a first pass overview, run:\n```\npyi-archive_viewer -l electrum-*.exe1 > f1\npyi-archive_viewer -l electrum-*.exe2 > f2\ndiff f1 f2 > d\ncat d\n```\nThen investigate manually:\n```\n$ pyi-archive_viewer electrum-*.exe1\n? help\n```\n"
  },
  {
    "path": "contrib/build-wine/README_windows.md",
    "content": "# Running Electrum from source on Windows (development version)\n\n## Prerequisites\n\n- [python3](https://www.python.org/)\n- [git](https://gitforwindows.org/)\n\n## Main steps\n\n### 1. Check out the code from GitHub:\n```\n> git clone https://github.com/spesmilo/electrum.git\n> cd electrum\n> git submodule update --init\n```\n\nRun install (this should install most dependencies):\n```\n> python3 -m pip install --user -e \".[gui,crypto]\"\n```\n\n### 2. Install `libsecp256k1`\n\n[comment]: # (technically the dll should be put into site-packages/electrum_ecc/,\nbut putting it into electrum/ also works because of the `os.add_dll_directory` call in\nelectrum/__init__.py)\n\n[libsecp256k1](https://github.com/bitcoin-core/secp256k1) is a required dependency.\nThis is a C library, which you need to compile yourself.\nElectrum needs a dll, named `libsecp256k1-0.dll` (or newer `libsecp256k1-*.dll`),\nplaced into the inner `electrum/` folder.\n\nFor Unix-like systems, the (`contrib/make_libsecp256k1.sh`) script does this for you,\nhowever it does not work on Windows.\nIf you have access to a Linux machine (e.g. VM) or perhaps even using\nWSL (Windows Subsystem for Linux), you can cross-compile from there to Windows,\nand build this dll:\n```\n$ GCC_TRIPLET_HOST=\"x86_64-w64-mingw32\" ./contrib/make_libsecp256k1.sh\n```\n\nAlternatively, MSYS2 and MinGW-w64 can be used directly on Windows, as follows.\n\n- download and install [MSYS2](https://www.msys2.org/)\n- run MSYS2\n- inside the MSYS2 shell:\n  ```\n  $ pacman -Syu\n  $ pacman -S --needed git base-devel mingw-w64-x86_64-toolchain mingw-w64-x86_64-autotools\n  $ export PATH=\"$PATH:/mingw64/bin\"\n  ```\n  `cd` into the git clone, e.g. `C:\\wspace\\electrum` (auto-mounted at `/c/wspace/electrum`)\n  ```\n  $ cd /c/wspace/electrum\n  $ GCC_TRIPLET_HOST=\"x86_64-w64-mingw32\" ./contrib/make_libsecp256k1.sh\n  ```\n\n(note: this is a bit cumbersome, see [issue #5976](https://github.com/spesmilo/electrum/issues/5976)\nfor discussion)\n\n### 3. Run electrum:\n\n```\n> python3 ./run_electrum\n```\n\n"
  },
  {
    "path": "contrib/build-wine/apt.preferences",
    "content": "Package: *\nPin: origin \"snapshot.debian.org\"\nPin-Priority: 1001\n"
  },
  {
    "path": "contrib/build-wine/apt.sources.list",
    "content": "deb https://snapshot.debian.org/archive/debian/20260227T144551Z/ trixie main\ndeb-src https://snapshot.debian.org/archive/debian/20260227T144551Z/ trixie main\n"
  },
  {
    "path": "contrib/build-wine/build-electrum-git.sh",
    "content": "#!/bin/bash\n\nNAME_ROOT=electrum\nPROJECT_ROOT=\"$WINEPREFIX/drive_c/electrum\"\n\nexport PYTHONDONTWRITEBYTECODE=1  # don't create __pycache__/ folders with .pyc files\n\n\n# Let's begin!\nset -e\n\n. \"$CONTRIB\"/build_tools_util.sh\n\npushd \"$PROJECT_ROOT\"\n\nVERSION=$(git describe --tags --dirty --always)\ninfo \"Last commit: $VERSION\"\n\ninfo \"preparing electrum-locale.\"\n(\n    \"$CONTRIB/locale/build_cleanlocale.sh\"\n    # we want the binary to have only compiled (.mo) locale files; not source (.po) files\n    rm -r \"$PROJECT_ROOT/electrum/locale/locale\"/*/electrum.po\n)\n\nfind -exec touch -h -d '2000-11-11T11:11:11+00:00' {} +\npopd\n\n\n# opt out of compiling C extensions\nexport AIOHTTP_NO_EXTENSIONS=1\nexport YARL_NO_EXTENSIONS=1\nexport MULTIDICT_NO_EXTENSIONS=1\nexport FROZENLIST_NO_EXTENSIONS=1\nexport PROPCACHE_NO_EXTENSIONS=1\nexport ELECTRUM_ECC_DONT_COMPILE=1\n\ninfo \"Installing requirements...\"\n$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \\\n    --cache-dir \"$WINE_PIP_CACHE_DIR\" -r \"$CONTRIB\"/deterministic-build/requirements.txt\ninfo \"Installing dependencies specific to binaries...\"\n# TODO tighten \"--no-binary :all:\" (but we don't have a C compiler...)\n$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \\\n    --no-binary :all: --only-binary cffi,cryptography,PyQt6,PyQt6-Qt6,PyQt6-sip \\\n    --cache-dir \"$WINE_PIP_CACHE_DIR\" -r \"$CONTRIB\"/deterministic-build/requirements-binaries.txt\ninfo \"Installing hardware wallet requirements...\"\n$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \\\n    --no-binary :all: --only-binary cffi,cryptography,hidapi \\\n    --cache-dir \"$WINE_PIP_CACHE_DIR\" -r \"$CONTRIB\"/deterministic-build/requirements-hw.txt\n\npushd \"$PROJECT_ROOT\"\n# see https://github.com/pypa/pip/issues/2195 -- pip makes a copy of the entire directory\ninfo \"Pip installing Electrum. This might take a long time if the project folder is large.\"\n$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location .\n# pyinstaller needs to be able to \"import electrum_ecc\", for which we need libsecp256k1:\n# (or could try \"pip install -e\" instead)\ncp electrum/libsecp256k1-*.dll \"$WINEPREFIX/drive_c/python3/Lib/site-packages/electrum_ecc/\"\npopd\n\n\nrm -rf dist/\n\n# build standalone and portable versions\ninfo \"Running pyinstaller...\"\nELECTRUM_CMDLINE_NAME=\"$NAME_ROOT-$VERSION\" wine \"$WINE_PYHOME/scripts/pyinstaller.exe\" --noconfirm --clean pyinstaller.spec\n\n# set timestamps in dist, in order to make the installer reproducible\npushd dist\nfind -exec touch -h -d '2000-11-11T11:11:11+00:00' {} +\npopd\n\ninfo \"building NSIS installer\"\n# $VERSION could be passed to the electrum.nsi script, but this would require some rewriting in the script itself.\nmakensis -DPRODUCT_VERSION=$VERSION electrum.nsi\n\ncd dist\nmv electrum-setup.exe $NAME_ROOT-$VERSION-setup.exe\ncd ..\n\ninfo \"Padding binaries to 8-byte boundaries, and fixing COFF image checksum in PE header\"\n# note: 8-byte boundary padding is what osslsigncode uses:\n#       https://github.com/mtrojnar/osslsigncode/blob/6c8ec4427a0f27c145973450def818e35d4436f6/osslsigncode.c#L3047\n(\n    cd dist\n    for binary_file in ./*.exe; do\n        info \">> fixing $binary_file...\"\n        # code based on https://github.com/erocarrera/pefile/blob/bbf28920a71248ed5c656c81e119779c131d9bd4/pefile.py#L5877\n        python3 <<EOF\npe_file = \"$binary_file\"\nwith open(pe_file, \"rb\") as f:\n    binary = bytearray(f.read())\npe_offset = int.from_bytes(binary[0x3c:0x3c+4], byteorder=\"little\")\nchecksum_offset = pe_offset + 88\nchecksum = 0\n\n# Pad data to 8-byte boundary.\nremainder = len(binary) % 8\nbinary += bytes(8 - remainder)\n\nfor i in range(len(binary) // 4):\n    if i == checksum_offset // 4:  # Skip the checksum field\n        continue\n    dword = int.from_bytes(binary[i*4:i*4+4], byteorder=\"little\")\n    checksum = (checksum & 0xffffffff) + dword + (checksum >> 32)\n    if checksum > 2 ** 32:\n        checksum = (checksum & 0xffffffff) + (checksum >> 32)\n\nchecksum = (checksum & 0xffff) + (checksum >> 16)\nchecksum = (checksum) + (checksum >> 16)\nchecksum = checksum & 0xffff\nchecksum += len(binary)\n\n# Set the checksum\nbinary[checksum_offset : checksum_offset + 4] = int.to_bytes(checksum, byteorder=\"little\", length=4)\n\nwith open(pe_file, \"wb\") as f:\n    f.write(binary)\nEOF\n    done\n)\n\nsha256sum dist/electrum*.exe\n"
  },
  {
    "path": "contrib/build-wine/build.sh",
    "content": "#!/bin/bash\n#\n# env vars:\n# - ELECBUILD_NOCACHE: if set, forces rebuild of docker image\n# - ELECBUILD_COMMIT: if set, do a fresh clone and git checkout\n\nset -e\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/../..\"\nPROJECT_ROOT_OR_FRESHCLONE_ROOT=\"$PROJECT_ROOT\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\nCONTRIB_WINE=\"$CONTRIB/build-wine\"\nBUILD_UID=$(/usr/bin/stat -c %u \"$PROJECT_ROOT\")\n\n. \"$CONTRIB\"/build_tools_util.sh\n\ninfo \"Clearing $CONTRIB_WINE/dist...\"\nrm -rf \"$CONTRIB_WINE\"/dist/*\n\n\nDOCKER_BUILD_FLAGS=\"\"\nif [ ! -z \"$ELECBUILD_NOCACHE\" ] ; then\n    info \"ELECBUILD_NOCACHE is set. forcing rebuild of docker image.\"\n    DOCKER_BUILD_FLAGS=\"--pull --no-cache\"\nfi\n\nif [ -z \"$ELECBUILD_COMMIT\" ] ; then  # local dev build\n    DOCKER_BUILD_FLAGS=\"$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID\"\nfi\n\ninfo \"building docker image.\"\ndocker build \\\n    $DOCKER_BUILD_FLAGS \\\n    -t electrum-wine-builder-img \\\n    \"$CONTRIB_WINE\"\n\n# maybe do fresh clone\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then\n    info \"ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout.\"\n    FRESH_CLONE=\"/tmp/electrum_build/windows/fresh_clone/electrum\"\n    rm -rf \"$FRESH_CLONE\" 2>/dev/null || ( info \"we need sudo to rm prev FRESH_CLONE.\" && sudo rm -rf \"$FRESH_CLONE\" )\n    umask 0022\n    git clone \"$PROJECT_ROOT\" \"$FRESH_CLONE\"\n    cd \"$FRESH_CLONE\"\n    git checkout \"$ELECBUILD_COMMIT\"\n    PROJECT_ROOT_OR_FRESHCLONE_ROOT=\"$FRESH_CLONE\"\nelse\n    info \"not doing fresh clone.\"\nfi\n\nDOCKER_RUN_FLAGS=\"\"\nif sh -c \": >/dev/tty\" >/dev/null 2>/dev/null; then\n    info \"/dev/tty is available and usable\"\n    DOCKER_RUN_FLAGS=\"-it\"\nfi\n\ninfo \"building binary...\"\n# check uid and maybe chown. see #8261\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then  # fresh clone (reproducible build)\n    if [ $(id -u) != \"1000\" ] || [ $(id -g) != \"1000\" ] ; then\n        info \"need to chown -R FRESH_CLONE dir. prompting for sudo.\"\n        sudo chown -R 1000:1000 \"$FRESH_CLONE\"\n    fi\nfi\ndocker run $DOCKER_RUN_FLAGS \\\n    --name electrum-wine-builder-cont \\\n    -v \"$PROJECT_ROOT_OR_FRESHCLONE_ROOT\":/opt/wine64/drive_c/electrum \\\n    --rm \\\n    --workdir /opt/wine64/drive_c/electrum/contrib/build-wine \\\n    electrum-wine-builder-img \\\n    ./make_win.sh\n\n# make sure resulting binary location is independent of fresh_clone\nif [ ! -z \"$ELECBUILD_COMMIT\" ] ; then\n    mkdir --parents \"$PROJECT_ROOT/contrib/build-wine/dist/\"\n    cp -f \"$FRESH_CLONE/contrib/build-wine/dist\"/*.exe \"$PROJECT_ROOT/contrib/build-wine/dist/\"\nfi\n"
  },
  {
    "path": "contrib/build-wine/electrum.nsi",
    "content": ";--------------------------------\n;Include Modern UI\n  !include \"TextFunc.nsh\" ;Needed for the $GetSize function. I know, doesn't sound logical, it isn't.\n  !include \"MUI2.nsh\"\n\n;--------------------------------\n;Variables\n\n  !define PRODUCT_NAME \"Electrum\"\n  !define PRODUCT_WEB_SITE \"https://github.com/spesmilo/electrum\"\n  !define PRODUCT_PUBLISHER \"Electrum Technologies GmbH\"\n  !define PRODUCT_UNINST_KEY \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${PRODUCT_NAME}\"\n\n;--------------------------------\n;General\n\n  ;Name and file\n  Name \"${PRODUCT_NAME}\"\n  OutFile \"dist/electrum-setup.exe\"\n\n  ;Default installation folder\n  InstallDir \"$PROGRAMFILES64\\${PRODUCT_NAME}\"\n\n  ;Get installation folder from registry if available\n  InstallDirRegKey HKCU \"Software\\${PRODUCT_NAME}\" \"\"\n\n  ;Request application privileges for Windows Vista\n  RequestExecutionLevel admin\n\n  ;Specifies whether or not the installer will perform a CRC on itself before allowing an install\n  CRCCheck on\n\n  ;Sets whether or not the details of the install are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them.\n  ShowInstDetails show\n\n  ;Sets whether or not the details of the uninstall  are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them.\n  ShowUninstDetails show\n\n  ;Sets the colors to use for the install info screen (the default is 00FF00 000000. Use the form RRGGBB (in hexadecimal, as in HTML, only minus the leading '#', since # can be used for comments). Note that if \"/windows\" is specified as the only parameter, the default windows colors will be used.\n  InstallColors /windows\n\n  ;This command sets the compression algorithm used to compress files/data in the installer. (http://nsis.sourceforge.net/Reference/SetCompressor)\n  SetCompressor /SOLID lzma\n\n  ;Sets the dictionary size in megabytes (MB) used by the LZMA compressor (default is 8 MB).\n  SetCompressorDictSize 64\n\n  ;Sets the text that is shown (by default it is 'Nullsoft Install System vX.XX') in the bottom of the install window. Setting this to an empty string (\"\") uses the default; to set the string to blank, use \" \" (a space).\n  BrandingText \"${PRODUCT_NAME} Installer v${PRODUCT_VERSION}\"\n\n  ;Sets what the titlebars of the installer will display. By default, it is 'Name Setup', where Name is specified with the Name command. You can, however, override it with 'MyApp Installer' or whatever. If you specify an empty string (\"\"), the default will be used (you can however specify \" \" to achieve a blank string)\n  Caption \"${PRODUCT_NAME}\"\n\n  ;Adds the Product Version on top of the Version Tab in the Properties of the file.\n  VIProductVersion 1.0.0.0\n\n  ;VIAddVersionKey - Adds a field in the Version Tab of the File Properties. This can either be a field provided by the system or a user defined field.\n  VIAddVersionKey ProductName \"${PRODUCT_NAME} Installer\"\n  VIAddVersionKey Comments \"The installer for ${PRODUCT_NAME}\"\n  VIAddVersionKey CompanyName \"${PRODUCT_NAME}\"\n  VIAddVersionKey LegalCopyright \"2013-2018 ${PRODUCT_PUBLISHER}\"\n  VIAddVersionKey FileDescription \"${PRODUCT_NAME} Installer\"\n  VIAddVersionKey FileVersion ${PRODUCT_VERSION}\n  VIAddVersionKey ProductVersion ${PRODUCT_VERSION}\n  VIAddVersionKey InternalName \"${PRODUCT_NAME} Installer\"\n  VIAddVersionKey LegalTrademarks \"${PRODUCT_NAME} is a trademark of ${PRODUCT_PUBLISHER}\"\n  VIAddVersionKey OriginalFilename \"${PRODUCT_NAME}.exe\"\n\n;--------------------------------\n;Interface Settings\n\n  !define MUI_ABORTWARNING\n  !define MUI_ABORTWARNING_TEXT \"Are you sure you wish to abort the installation of ${PRODUCT_NAME}?\"\n\n  !define MUI_ICON \"..\\..\\electrum\\gui\\icons\\electrum.ico\"\n\n;--------------------------------\n;Pages\n\n  !insertmacro MUI_PAGE_DIRECTORY\n  !insertmacro MUI_PAGE_INSTFILES\n  !insertmacro MUI_UNPAGE_CONFIRM\n  !insertmacro MUI_UNPAGE_INSTFILES\n\n;--------------------------------\n;Languages\n\n  !insertmacro MUI_LANGUAGE \"English\"\n\n;--------------------------------\n;Functions\n\n!macro CreateEnsureNotRunning prefix operation\n\nFunction ${prefix}EnsureNotRunning\n  ; pop the directory to check from the stack into $R0\n  Pop $R0\n  ; if the dir at $R0 doesn't exist, jump to nodir\n  IfFileExists \"$R0\" 0 nodir\n    ; Find all .exe files in the directory, $1 is the handle, $2 is the filename\n    FindFirst $1 $2 \"$R0\\*.exe\"\n    IfErrors noexe 0\n\n    checkloop:\n    ; Skip checking the uninstaller if we are the uninstaller to avoid locking the uninstaller itself\n    !if \"${prefix}\" == \"un.\"\n        StrCmp $2 \"Uninstall.exe\" skipfile 0\n    !endif\n\n    ; Check if we can append to the .exe file. If we can't that means it is still running.\n    retryopen:\n    FileOpen $0 \"$R0\\$2\" a\n    IfErrors 0 closeexe\n      MessageBox MB_RETRYCANCEL \"Can not ${operation} because $2 is still running. Close it and retry.\" /SD IDCANCEL IDRETRY retryopen\n      FindClose $1\n      Abort\n    closeexe:\n    FileClose $0\n\n    skipfile:\n    ; Find next .exe file\n    FindNext $1 $2\n    IfErrors done 0\n    Goto checkloop\n\n    done:\n    FindClose $1\n\n  noexe:\n  nodir:\nFunctionEnd\n\n!macroend\n\n; The function has to be created twice, once for the installer and once for the uninstaller\n!insertmacro CreateEnsureNotRunning \"\" \"install\"\n!insertmacro CreateEnsureNotRunning \"un.\" \"uninstall\"\n\n;--------------------------------\n;Installer Sections\n\n;Check if we have Administrator rights\nFunction .onInit\n\tUserInfo::GetAccountType\n\tpop $0\n\t${If} $0 != \"admin\" ;Require admin rights on NT4+\n\t\tMessageBox mb_iconstop \"Administrator rights required!\"\n\t\tSetErrorLevel 740 ;ERROR_ELEVATION_REQUIRED\n\t\tQuit\n\t${EndIf}\n\n  ; Check if already installed and ensure the process is not running if it is\n  ReadRegStr $R0 HKCU \"Software\\${PRODUCT_NAME}\" \"\"\n  IfErrors noinstdir 0\n    Push $R0\n    Call EnsureNotRunning\n  noinstdir:\n  ClearErrors\nFunctionEnd\n\nSection\n  SetOutPath $INSTDIR\n\n  ;Uninstall previous version files\n  RMDir /r \"$INSTDIR\\*.*\"\n  Delete \"$DESKTOP\\${PRODUCT_NAME}.lnk\"\n  Delete \"$SMPROGRAMS\\${PRODUCT_NAME}\\*.*\"\n\n  ;Files to pack into the installer\n  File /r \"dist\\electrum\\*.*\"\n  File \"..\\..\\electrum\\gui\\icons\\electrum.ico\"\n\n  ;Store installation folder\n  WriteRegStr HKCU \"Software\\${PRODUCT_NAME}\" \"\" $INSTDIR\n\n  ;Create uninstaller\n  DetailPrint \"Creating uninstaller...\"\n  WriteUninstaller \"$INSTDIR\\Uninstall.exe\"\n\n  ;Create desktop shortcut\n  DetailPrint \"Creating desktop shortcut...\"\n  CreateShortCut \"$DESKTOP\\${PRODUCT_NAME}.lnk\" \"$INSTDIR\\electrum-${PRODUCT_VERSION}.exe\" \"\"\n\n  ;Create start-menu items\n  DetailPrint \"Creating start-menu items...\"\n  CreateDirectory \"$SMPROGRAMS\\${PRODUCT_NAME}\"\n  CreateShortCut \"$SMPROGRAMS\\${PRODUCT_NAME}\\Uninstall.lnk\" \"$INSTDIR\\Uninstall.exe\" \"\" \"$INSTDIR\\Uninstall.exe\" 0\n  CreateShortCut \"$SMPROGRAMS\\${PRODUCT_NAME}\\${PRODUCT_NAME}.lnk\" \"$INSTDIR\\electrum-${PRODUCT_VERSION}.exe\" \"\" \"$INSTDIR\\electrum-${PRODUCT_VERSION}.exe\" 0\n  CreateShortCut \"$SMPROGRAMS\\${PRODUCT_NAME}\\${PRODUCT_NAME} Testnet.lnk\" \"$INSTDIR\\electrum-${PRODUCT_VERSION}.exe\" \"--testnet\" \"$INSTDIR\\electrum-${PRODUCT_VERSION}.exe\" 0\n\n\n  ;Links bitcoin: and lightning: URIs to Electrum\n  WriteRegStr HKCU \"Software\\Classes\\bitcoin\" \"\" \"URL:bitcoin Protocol\"\n  WriteRegStr HKCU \"Software\\Classes\\bitcoin\" \"URL Protocol\" \"\"\n  WriteRegStr HKCU \"Software\\Classes\\bitcoin\" \"DefaultIcon\" \"$\\\"$INSTDIR\\electrum.ico, 0$\\\"\"\n  WriteRegStr HKCU \"Software\\Classes\\bitcoin\\shell\\open\\command\" \"\" \"$\\\"$INSTDIR\\electrum-${PRODUCT_VERSION}.exe$\\\" $\\\"%1$\\\"\"\n  WriteRegStr HKCU \"Software\\Classes\\lightning\" \"\" \"URL:lightning Protocol\"\n  WriteRegStr HKCU \"Software\\Classes\\lightning\" \"URL Protocol\" \"\"\n  WriteRegStr HKCU \"Software\\Classes\\lightning\" \"DefaultIcon\" \"$\\\"$INSTDIR\\electrum.ico, 0$\\\"\"\n  WriteRegStr HKCU \"Software\\Classes\\lightning\\shell\\open\\command\" \"\" \"$\\\"$INSTDIR\\electrum-${PRODUCT_VERSION}.exe$\\\" $\\\"%1$\\\"\"\n\n  ;Adds an uninstaller possibility to Windows Uninstall or change a program section\n  WriteRegStr HKCU \"${PRODUCT_UNINST_KEY}\" \"DisplayName\" \"$(^Name)\"\n  WriteRegStr HKCU \"${PRODUCT_UNINST_KEY}\" \"UninstallString\" \"$INSTDIR\\Uninstall.exe\"\n  WriteRegStr HKCU \"${PRODUCT_UNINST_KEY}\" \"DisplayVersion\" \"${PRODUCT_VERSION}\"\n  WriteRegStr HKCU \"${PRODUCT_UNINST_KEY}\" \"URLInfoAbout\" \"${PRODUCT_WEB_SITE}\"\n  WriteRegStr HKCU \"${PRODUCT_UNINST_KEY}\" \"Publisher\" \"${PRODUCT_PUBLISHER}\"\n  WriteRegStr HKCU \"${PRODUCT_UNINST_KEY}\" \"DisplayIcon\" \"$INSTDIR\\electrum.ico\"\n\n  ;Fixes Windows broken size estimates\n  ${GetSize} \"$INSTDIR\" \"/S=0K\" $0 $1 $2\n  IntFmt $0 \"0x%08X\" $0\n  WriteRegDWORD HKCU \"${PRODUCT_UNINST_KEY}\" \"EstimatedSize\" \"$0\"\nSectionEnd\n\n;--------------------------------\n;Descriptions\n\n;--------------------------------\n;Uninstaller Section\n\nSection \"Uninstall\"\n  RMDir /r \"$INSTDIR\\*.*\"\n\n  RMDir \"$INSTDIR\"\n\n  Delete \"$DESKTOP\\${PRODUCT_NAME}.lnk\"\n  Delete \"$SMPROGRAMS\\${PRODUCT_NAME}\\*.*\"\n  RMDir  \"$SMPROGRAMS\\${PRODUCT_NAME}\"\n\n  DeleteRegKey HKCU \"Software\\Classes\\bitcoin\"\n  DeleteRegKey HKCU \"Software\\${PRODUCT_NAME}\"\n  DeleteRegKey HKCU \"${PRODUCT_UNINST_KEY}\"\nSectionEnd\n\nFunction UN.onInit\n  ; Ensure the process is not running in the uninstallation directory\n  Push $INSTDIR\n  Call un.EnsureNotRunning\nFunctionEnd\n"
  },
  {
    "path": "contrib/build-wine/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: User-ID:\tSteve Dower (Python Release Signing) <steve.dower@microsoft.com>\nComment: Created:\t2015-04-06 02:32\nComment: Type:\t4096-bit RSA\nComment: Usage:\tSigning, Encryption, Certifying User-IDs\nComment: Fingerprint:\t7ED10B6531D7C8E1BC296021FC624643487034E5\n\n\nmQINBFUh1AUBEACdUPt6PwJVO23zGZqgtgBeA9JsO22dk3CMzrwPJdUmMd6mcRWa\nvl4BoAba66fuC17GvOgGXimKI+iaw5Vt9QI3uSjUjFSfc24J8T7NB/yAr/0zEcex\nraHD2dxT/JpE/iY0yWHxRlitvwGSw1Qlq3NnY8tDI1DJEJD+gBuCktvVvu1FfQTw\n6bd+aEq0c4sWJHAOnKLuLH0pNFOznnynAFGPGBBsm/YwYc5BP2JVvka775LUjA+W\n1h2Sgg3FAUPIm64pc4Pq6mUo6Tulw72xsWMpCL1/5atXNPXT6rJUOB8euTcNMr4l\n1O6GKSsiLeLAuvq4bmhOKtLzjWzXnY1gDVoOfdgpD6o4ZHk4xiVsdVE8hCa/ylz8\n1ZwRW2gGo2jP8t3hKciR2i+Qs+6lPNZpeFIxa6Uo9ER1IBgCHHapIR/UdcOFyoS0\nMNn7Ui7DLQNM4gI/G17eG9tfvjW2dl4SgFSYWMq/OtXnPDUBGqFUWsn8adOL2PFL\nB7kM5ZRTPc5SnY9hoSGa5E20rJZIXcpy1aygRz/xUjoKwNzAySSEyyIorUxZ8KaH\nEEBQSsqwe04MXIENqnDozH0/cvP4JXEDSl8EkzMSCWSoavQSIYD5pQppyFQpGHqa\n5CuOA25Ja+sgp2xqahtr3fEqZUknPQSoYlnJbaHnzsGSlRAVWMsklsZibQARAQAB\ntEBTdGV2ZSBEb3dlciAoUHl0aG9uIFJlbGVhc2UgU2lnbmluZykgPHN0ZXZlLmRv\nd2VyQG1pY3Jvc29mdC5jb20+iQEcBBABCAAGBQJYsBphAAoJEEhSKohZ29goZggI\nALKlgyoecD5v3ulh1eoctRqtCOxkAoENEfPt3l5x6N8Wq89yHzf10T1rVioEXOHh\nDi1m37DDoQmRJD0sOYQymq10xDGRYAJjyOf3X0pvRkZ+F7T0U4dSV3DasLIHcN26\nkRwv1yCYsf0QvhgT6EJZKyUNHtV9qrb9u3A1Zp6epC/EyT8zMZj+21GzTUrnbnug\n3Ak9p7+APCZS4Ahh9ZHFuD38MZ7+OwrUd6ot+6cbb1nnQLSAGQOHSp6EP6ktrnsK\nzts0L+tzHurxtJgUkR01imJuSFfYpLoZa/L7qXNyEpEUTC/SWzRWD9y2QkM7DLzX\ncaReVAyJr9rix1lDQbEFIquJAhwEEAEIAAYFAlW2TwMACgkQKeBHm5nIo5fahg/+\nIQSSE/yH8Cf82PYI7IGqDVNwRw2o7dq8iscB+fhFHfFFhXANwUUFpzPeDMrMrdmq\nBke7Vg1D3bIFocXYOiNwf2J7f4mBO6OL0VAvDX02Vyh/C2ZSc15uZyU6CWFQMCG8\nJOSmgQFs3kMHkL4qtut1Y5reoYesmteIe06UVyRw8yT1R1BkxP2whZ97qwsvUUE9\ncVD08wCvH486efw7EswIzYGa1KcZXji0MvjXfksVtkEQQbxMMI7SVXo0345ZReww\nbuioGL5gvvAPObgU43skORanFHFxiHEKmqgHBHXK/LKqaFUFMKcb4iFTNs2XKrhE\nXsEi5EMI1AFsJzjcXRqT50Wi2cZhXeRc70uF6gzqrdWvowa2oOPiO6zGDiTqZCW1\nAArk/QBzGtPjVh+nKEdHwnvpK9913UAkAN682h8QkoVPYXOvIKDYZRBr5EfpUyQt\ny2r9MYewz0YN4zlGP1PFS9FxncdSZiZJqQVif0CkOp1tdSxLynHcujQgATZNtgcu\nX9JwUwPp60MurgOcIZiW3nZw/z/5vzBBadSa9/TIFSJAFNBlqeKdIGQuik0UH+Cz\nRRtSFb38F7jMPwr0QUSktuntQ0HWuvNqj4N8DFm45/n5rN190eRotrVDXZmjGein\nqWPITuICslGIKAp+Q6y3t7JA71MIbeu/ZY6ZcftOka6JAhwEEAEIAAYFAlZRWicA\nCgkQxiNM8COVzQq5bRAAktnXceO3GCivMt9yR1Qr0Ov4A4Q+CJSIL45efLFmS30k\ncbkHHtaq+0FZNh2ZaMartC16MUja4a2OUejg53VBhaSVkQrVk/6M/HA6/o6CvIhb\nFW/5C+nRWBd5gfvwsWvjrtC3cKZco4wg+yYclkDbSH+2EPDZOKIHpBy46YTz9WQ1\n8SJ51WVkNUNiZqRBA6Ny5GFoyd6EpWZYEPelmzNemv3zOrQdVzLV24/mLejcLL2t\nKmI6ngX4XViXUCRUU3MH8/V+V2YTQGcTM/6HGaHpN0LTqknf6zEto9q9FiRTaiU2\nkzExhBq8Qf+cVqwm+1kMt0FGOgpT47VBWMeUWq62gQ3h5NfAs4DfriLgNURlTC1d\nJYAEquFhB/8oBQD1h/d9CjQyk88iib2pJInRBDsK2FcfQBap9iaeBFYoBWTzMQJx\ng+RuWK1wIm2n0oqa5urBYZtRHE5RIdDP8ZLogrBOFkfXGJxlRBQD1Gab77qohdp0\nSnErGw4Ne3gJH/SNhK+zzHkHERIrRZCR95zdYkKfZ2jyOPzSuABVRigEQVQPCDn0\nhbv3cblTCeJYwG2mfRdmfyqSMALKIgXe9yvJ2kl8QgaVOsJjNfQzIKeoHFPIm5Uw\n3YB6jgDFc5uzEaH7WSz74A7KhGYjC7huw2TugosHbWxphJKddwxfK1WujYaAeJyJ\nAhwEEAEIAAYFAlf2sPIACgkQfb+tds3soNuXEQ//XkWYHmJsKyeDZC8MFU+/vsVq\ndhnFs6UXZkvf7MoNFkuMDL+zgVoMpFHftTdyBqNAoEnndakk212jK8YWF8g4kQXI\na9uMRqJLM4mqCl9yco/twJ9z9EMA+JLSXYK0ZbTkLdutSDZEDKgpHbmekx2C1OsW\nlRLs9PahF5PAZQs0N+m+LJBnw6bEHOSTv4OE5uVUf9nvdes3OARvkGSEGURNmUaF\nchxWtZ/SF1q9Jfj0K/xgs9Gt855oueveRXLIGpjiEVoKH/drsgyKFMJVrpZDDgS4\nGVXG8bq3GTFiMAs7BPPd9bjI+jgvqttgItZcYsW/IQK1BIoG6Fere4cPvu+IshCc\nkm9T8nOK98tZuov8hLbND9mW2d7LChJI1r/HbzbKIl0k6OigdFMrJlun2zmtDxT9\nTp3uxOYSaW2YggcpNUjI28tv6AwoA8okVY93LWjO5kdZGkbliRnf/eJy7NJYn0LO\nogsvMUJClRAGnZTHLEr32Whq0MImlXa43kr6oPJT5dwXXyw5ELstEQztczCd1PYB\nkbQHUpD5j3PwgNVOinCnbd4pc/qVtYSqpg2g6TJi1XiJ1638jhn2k+i8wop/dyet\niN8lGR76twYGex9AavEAUpVR9r6qfpp4KBibEhdvL6o2O03RQu17GcRzXSAYzmUi\n5U5jZ3dBz5MYUjgUZM+JAhwEEAEKAAYFAllTh9gACgkQXLNh5VL7DRAk7Q//X8eU\nhwEvl/d9Sv2kBNCZFjAW3QmZp2L/sxhScJZXrOFzKUdmjap9Xlul1qr6/Wif7YLK\nbOdNUI7KziEBn+9SEd90XauoVkzU2F0Jn9ILGQfUHAIpocRTKuCwBrncaBozHQwD\nO3Dk33AhZ6lqTv/AVLRKHQXwigGTBJxK4cCEZ+VwK9tKk6BrQB48Rm7pg9HF5ey5\nJGPRWgUnn1v0IJN5ysZ5m9ChYbqF8VwvMw0txmgKgvdDKpXbF/S59Bp4TH/7Dr2D\nkAeNTcuzTFBaFE+siMgksZIYKZ1VkVoiN2qQA7ZaA5LQbUom0WdrKZGefFfPt9ES\nA4wyL3OfxRsmWmd/5Fxrwm1VbzgPoMd1Dc5ExlyqnecdGzDui2bmltNqRJd9ytRq\n6YUGYzXp4qQkWO61CoC3mkm2M8Ex7DGbUtXhdg0zoa08w9lXuOtHVhY7XlLWjO1U\np8cp4DVxsN/wOXtyH1pcleGo4aEsgyU/DH57prFLGz7Egp2JhRDHnZmlonWp74G1\nVLfqkOqZlqTU4mPA827C8qPCx6cMsRvFS7OEiDBswkFWBKjkUCw4rLC1tBMBCxJW\ntZlc+Y0LNyOryJ3h6EJmRIHO57oLen345e1WOi4ROOC/wQMErFk7B3P41Lqmrwb8\nHGuKn3ca+Aw70hVrZ+7Q3RRFTLlOS/vv107Fqu6JAjkEEwEIACMFAlUh1AUCGwMH\nCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRD8YkZDSHA05RfdD/97wPXnoe7e\nipP7UXQ942z1buV6pTGv0Lea2aHn20o2BBjHp97YXroF/e/8W6h+Y+Fq8hWoXdYJ\ndC9DVgzJhvbXAIG8VrF6/IDGQ62r4ff/AIyQY+kiCOCCVhjwuqOTjVYw2pYRUcI3\nUwXVPeptDSXcIZkHCLtEUnS5YMTdkPuZrAmucCCnfcJtevXbHD2yJYP4vwfXMbal\nsNBDKJi6uYAFc4yv+/DyS13rfXJvu2pYGvtRd+fs7mBETvUTubhI440pIss6TX6M\nlxWexX6Ty8vI5HCQT281H4zqdbe5GdzGmIx1EiYx1sJbgSBNqCh5sRJY5/BXzVJ3\ndfM/Mv5QYY4ulO/qUNFdC8f1cZm0euOo3maB4jY+Sjaff7t0WIz0GufO4dHARwJg\n3s0LO9Wf5+z/fbWOMcfvvcfaHNbhaKWk16kslc/g7NYvMfOuleM06YGyGPz//a9c\nbaX53OiMupNvLlhyPO5NfGppvRn5xAElcAw1RLhHJcgvTtIs/zVVfHPaK41u8A9c\nXKnmIUC39K4BGvOpPzEvCdQ2ZbAqzQLmZ1UICr15w1Nfs6uoERJbnuq+JgOPOcOk\nezAWELi5LdZTElnpJpZPTDQ03+3GvxD4R9sR+l5RT8Ul7kF+3PPPzekfQzF+Nisr\nBhPFb2lPt3Hw32FgTTIuXCMRTKEBb/6z77kCDQRVIdQFARAAtmnsZ9A8ovJIJ9Rl\nWeIylEhHRyQifqzgc/r50uDZVPBjewOA462LjH3+F6zFGEkU+q2aqSe0A0SJPF/W\nhj6MNYXLoibxi5D4mGkoIao9ExnXt4LXAc6ogQpY6vFQBJU5Nr8XCefQbm0loa/o\ny5uK8JHLWCZ2jAossnVpzDwNeN27+B8h5+OifnWhQCTun1xz5EJiyc0yoBmf46zf\nmU4CMUBsPvrXcLmw4J3wp35qmrHg1tNyPhd7VBlikMrgtrWX9IaPZ40dnrGG/WjO\nFYB3CKxGb0pTCj7GC4ubxo2upeWZqHLmdIVc7Nzsfp8EcwJbTj+jZ2Zfq6F8y+je\nsbgh8CaxYn4hEs23aPYRq5H4/buVmZhUw3/AAL9ZmyX6AtAQ0HktVtQe7ykP7DLs\nEpeLG+vPJFY363QeDsLHwOoxnZSfGziVlB4N/KqIkixNWcFTG8GSE1zKcdJVNoW+\n3MB3+FtMZWUJhH0FyKg5qLaJCtC7Yo5gsddU+QCqTn6gcZBnMX5j4LaAmW4hh1RX\nffwwsbfviK5uhXQCeUnbUaokieetDx4s6Kay6t9ahTRr0r/Z3VWzvr+xATxNWZzi\nxTdezCGOB2ycZ0vq4bKXBuN8CAyOy5X1hf7Rc1BiAVQCILHJDtz0Ak/Hax6DAa2A\nHnx9YlugHQf000KroLEY+GaxqYEAEQEAAYkCHwQYAQgACQUCVSHUBQIbDAAKCRD8\nYkZDSHA05RtyEACdOEmGolL1xG6I+lDVdot6oBZqC9e021aLWqCUpWJFDp0m0aTm\nCfmOI1gTaFjScxhq1W0GPUoJKUZhk3tlVfdSCtUckI+xuWKEfqJYtvUtTXpK4jDe\naZBovJ3KNpJRIynbr1566zCSQJhHiCGWmE/M5KN3gPsORbCBQXEkONSVsslf1Wm6\n6hU6uqSWUaceD+4fl5LClbck1DPWchAP7+uLKPEOtORyH6KRTgKl73zYo7xU1K4Q\nMN/1aMjobPkqNvvkXnUNwO7QMz18Nx+WqPc4ksJgW1O1aPQ2qL/ARY5jatZ6BBd7\niytfz7d6JOh0FOIlmhBqbWd7fEGrLsSA+EjBGBwW5BnIMmxP1xhjhwrcI18y8kAK\n5UzdW2hbbAlc2rlsuxEc+xOYh8kGcc+mZ1j/aMn4gALsTbSO/0T+YJhfODNnL1dC\nj7oPbJGmmG6pb/o7P4azBUVC9lHOuV3XlAPjSmJylnNsV7+PxwPlXlvKgh4S4C4Z\nPUc/iPetsxXR2djccOoNxVU4CqJBqYKgul/pUphXkh7QfEKyH+42UETbVhstdBVU\nazJ6SeUnv9ClVDGsCEhfEZfNOnOoDzJGxDfESoAw7ih91vIhTyHHsK83p2HLDMLP\nptLzx/0AFBfo6MWGGpd2RSnMWNbvh59wiThlDeI+Das3ln5nsAo67dMYdA==\n=fjOq\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "contrib/build-wine/make_win.sh",
    "content": "#!/bin/bash\n\nset -e\n\nhere=\"$(dirname \"$(readlink -e \"$0\")\")\"\ntest -n \"$here\" -a -d \"$here\" || exit\n\nif [ -z \"$WIN_ARCH\" ] ; then\n    export WIN_ARCH=\"win64\"  # default\nfi\nif [ \"$WIN_ARCH\" = \"win32\" ] ; then\n    export GCC_TRIPLET_HOST=\"i686-w64-mingw32\"\nelif [ \"$WIN_ARCH\" = \"win64\" ] ; then\n    export GCC_TRIPLET_HOST=\"x86_64-w64-mingw32\"\nelse\n    echo \"unexpected WIN_ARCH: $WIN_ARCH\"\n    exit 1\nfi\n\nexport BUILD_TYPE=\"wine\"\nexport GCC_TRIPLET_BUILD=\"x86_64-pc-linux-gnu\"\nexport GCC_STRIP_BINARIES=\"1\"\n\nexport CONTRIB=\"$here/..\"\nexport PROJECT_ROOT=\"$CONTRIB/..\"\nexport CACHEDIR=\"$here/.cache/$WIN_ARCH/build\"\nexport PIP_CACHE_DIR=\"$here/.cache/$WIN_ARCH/wine_pip_cache\"\nexport WINE_PIP_CACHE_DIR=\"c:/electrum/contrib/build-wine/.cache/$WIN_ARCH/wine_pip_cache\"\nexport DLL_TARGET_DIR=\"$CACHEDIR/dlls\"\n\nexport WINEPREFIX=\"/opt/wine64\"\nexport WINEDEBUG=-all\nexport WINE_PYHOME=\"c:/python3\"\nexport WINE_PYTHON=\"wine $WINE_PYHOME/python.exe -B\"\n\n. \"$CONTRIB\"/build_tools_util.sh\n\ngit -C \"$PROJECT_ROOT\" rev-parse 2>/dev/null || fail \"Building outside a git clone is not supported.\"\n\ninfo \"Clearing $here/build and $here/dist...\"\nrm \"$here\"/build/* -rf\nrm \"$here\"/dist/* -rf\n\nmkdir -p \"$CACHEDIR\" \"$DLL_TARGET_DIR\" \"$PIP_CACHE_DIR\"\n\nif ls \"$DLL_TARGET_DIR\"/libsecp256k1-*.dll 1> /dev/null 2>&1; then\n    info \"libsecp256k1 already built, skipping\"\nelse\n    \"$CONTRIB\"/make_libsecp256k1.sh || fail \"Could not build libsecp\"\nfi\n\nif [ -f \"$DLL_TARGET_DIR/libzbar-0.dll\" ]; then\n    info \"libzbar already built, skipping\"\nelse\n    (\n        # iconv is needed for zbar. see https://github.com/mchehab/zbar/blob/a549566ea11eb03622bd4458a1728ffe3f589163/README-windows.md\n        # (previously were using win-iconv, but changed to GNU libiconv due to compilation errors with modern gcc)\n        LIBICONV_VER=\"1.18\"\n        download_if_not_exist \"$CACHEDIR/libiconv-${LIBICONV_VER}.tar.gz\" \"https://ftp.gnu.org/pub/gnu/libiconv/libiconv-${LIBICONV_VER}.tar.gz\"\n        verify_hash \"$CACHEDIR/libiconv-${LIBICONV_VER}.tar.gz\" \"3b08f5f4f9b4eb82f151a7040bfd6fe6c6fb922efe4b1659c66ea933276965e8\"\n        tar xf \"$CACHEDIR/libiconv-${LIBICONV_VER}.tar.gz\" -C \"$CACHEDIR\"\n        # ref https://github.com/msys2/MINGW-packages/blob/7f68e9f2488737bbe03888ade094eaee8021d1c5/mingw-w64-libiconv/PKGBUILD\n        info \"Building libiconv...\"\n        cd \"$CACHEDIR/libiconv-${LIBICONV_VER}\"\n        # Patches taken from msys2/MINGW-packages\n        patch -p1 < \"$here/patches/libiconv-fix-pointer-buf.patch\"\n        ./configure \\\n            $AUTOCONF_FLAGS \\\n            --prefix=\"/usr/${GCC_TRIPLET_HOST}\" \\\n            --disable-static \\\n            --enable-shared \\\n            --enable-extra-encodings \\\n            --enable-relocatable \\\n            --disable-rpath \\\n            --enable-silent-rules \\\n            --enable-nls\n        CC=\"${GCC_TRIPLET_HOST}-gcc\" make \"-j$CPU_COUNT\" || fail \"Could not build libiconv\"\n        cp -fpv \"libcharset/lib/.libs/libcharset-1.dll\" \"$DLL_TARGET_DIR/\" || fail \"Could not copy the libcharset binary to DLL_TARGET_DIR\"\n        cp -fpv \"lib/.libs/libiconv-2.dll\" \"$DLL_TARGET_DIR/\" || fail \"Could not copy the libiconv binary to DLL_TARGET_DIR\"\n        # FIXME avoid using sudo\n        sudo make install  || fail \"Could not install libiconv\"\n        # workaround to delete files owned by root, created by \"make install\":\n        make clean\n    )\n    \"$CONTRIB\"/make_zbar.sh || fail \"Could not build zbar\"\nfi\n\nif [ -f \"$DLL_TARGET_DIR/libusb-1.0.dll\" ]; then\n    info \"libusb already built, skipping\"\nelse\n    \"$CONTRIB\"/make_libusb.sh || fail \"Could not build libusb\"\nfi\n\n\"$here/prepare-wine.sh\" || fail \"prepare-wine failed\"\n\ninfo \"Resetting modification time in C:\\Python...\"\n# (Because of some bugs in pyinstaller)\npushd /opt/wine64/drive_c/python*\nfind -exec touch -h -d '2000-11-11T11:11:11+00:00' {} +\npopd\nls -l /opt/wine64/drive_c/python*\n\n\"$here/build-electrum-git.sh\" || fail \"build-electrum-git failed\"\n\ninfo \"Done.\"\n"
  },
  {
    "path": "contrib/build-wine/patches/libiconv-fix-pointer-buf.patch",
    "content": "--- a/lib/iconv.c\t2018-05-03 23:18:55.997221700 -0400\n+++ b/lib/iconv.c\t2018-05-03 23:26:47.611682700 -0400\n@@ -170,12 +170,12 @@ static const struct stringpool2_t string\n #include \"aliases2.h\"\n #undef S\n };\n #define stringpool2 ((const char *) &stringpool2_contents)\n static const struct alias sysdep_aliases[] = {\n-#define S(tag,name,encoding_index) { (int)(long)&((struct stringpool2_t *)0)->stringpool_##tag, encoding_index },\n+#define S(tag,name,encoding_index) { (int)(intptr_t)&((struct stringpool2_t *)0)->stringpool_##tag, encoding_index },\n #include \"aliases2.h\"\n #undef S\n };\n #ifdef __GNUC__\n __inline\n #else\n--- a/lib/genaliases.c\t2023-01-14 00:00:00.000000000 +0000\n+++ b/lib/genaliases.c\t2023-01-14 10:18:00.000000000 +0000\n@@ -50,7 +50,7 @@\n       putc(c, out2);\n     }\n   }\n-  fprintf(out2,\"\\\")' tmp.h | sed -e 's|^.*\\\\(stringpool_str[0-9]*\\\\).*$|  (int)(long)\\\\&((struct stringpool_t *)0)->\\\\1,|'\\n\");\n+  fprintf(out2,\"\\\")' tmp.h | sed -e 's|^.*\\\\(stringpool_str[0-9]*\\\\).*$|  (int)(intptr_t)\\\\&((struct stringpool_t *)0)->\\\\1,|'\\n\");\n   for (; n > 0; names++, n--)\n     emit_alias(out1, *names, c_name);\n }\n--- a/lib/genaliases2.c\t2023-01-14 00:00:00.000000000 +0000\n+++ b/lib/genaliases2.c\t2023-01-14 10:18:00.000000000 +0000\n@@ -44,6 +44,6 @@\n static void emit_encoding (FILE* out1, FILE* out2, const char* tag, const char* const* names, size_t n, const char* c_name)\n {\n-  fprintf(out2,\"  (int)(long)&((struct stringpool2_t *)0)->stringpool_%s_%u,\\n\",tag,counter);\n+  fprintf(out2,\"  (int)(intptr_t)&((struct stringpool2_t *)0)->stringpool_%s_%u,\\n\",tag,counter);\n   for (; n > 0; names++, n--)\n     emit_alias(out1, tag, *names, c_name);\n }\n"
  },
  {
    "path": "contrib/build-wine/prepare-wine.sh",
    "content": "#!/bin/bash\n\nPYINSTALLER_REPO=\"https://github.com/pyinstaller/pyinstaller.git\"\nPYINSTALLER_COMMIT=\"306d4d92580fea7be7ff2c89ba112cdc6f73fac1\"\n# ^ tag \"v6.13.0\"\n\nPYTHON_VERSION=3.12.10\n\n\n# Let's begin!\nset -e\n\nhere=\"$(dirname \"$(readlink -e \"$0\")\")\"\n\n. \"$CONTRIB\"/build_tools_util.sh\n\ninfo \"Booting wine.\"\nwine 'wineboot'\n\n\ncd \"$CACHEDIR\"\nmkdir -p $WINEPREFIX/drive_c/tmp\n\ninfo \"Installing Python.\"\n# note: you might need \"sudo apt-get install dirmngr\" for the following\n# keys from https://www.python.org/downloads/#pubkeys\nKEYRING_PYTHON_DEV=\"keyring-electrum-build-python-dev.gpg\"\ngpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --import \"$here\"/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc\nif [ \"$WIN_ARCH\" = \"win32\" ] ; then\n    PYARCH=\"win32\"\nelif [ \"$WIN_ARCH\" = \"win64\" ] ; then\n    PYARCH=\"amd64\"\nelse\n    fail \"unexpected WIN_ARCH: $WIN_ARCH\"\nfi\nPYTHON_DOWNLOADS=\"$CACHEDIR/python$PYTHON_VERSION\"\nmkdir -p \"$PYTHON_DOWNLOADS\"\nfor msifile in core dev exe lib pip; do\n    echo \"Installing $msifile...\"\n    download_if_not_exist \"$PYTHON_DOWNLOADS/${msifile}.msi\" \"https://www.python.org/ftp/python/$PYTHON_VERSION/$PYARCH/${msifile}.msi\"\n    download_if_not_exist \"$PYTHON_DOWNLOADS/${msifile}.msi.asc\" \"https://www.python.org/ftp/python/$PYTHON_VERSION/$PYARCH/${msifile}.msi.asc\"\n    verify_signature \"$PYTHON_DOWNLOADS/${msifile}.msi.asc\" $KEYRING_PYTHON_DEV || fail \"invalid sig for ${msifile}.msi\"\n    wine msiexec /i \"$PYTHON_DOWNLOADS/${msifile}.msi\" /qb TARGETDIR=$WINE_PYHOME || fail \"wine msiexec failed for ${msifile}.msi\"\ndone\n\nbreak_legacy_easy_install\n\ninfo \"Installing build dependencies.\"\n$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \\\n    --cache-dir \"$WINE_PIP_CACHE_DIR\" -r \"$CONTRIB\"/deterministic-build/requirements-build-base.txt\n$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \\\n    --cache-dir \"$WINE_PIP_CACHE_DIR\" -r \"$CONTRIB\"/deterministic-build/requirements-build-wine.txt\n\n\n# copy already built DLLs\ncp \"$DLL_TARGET_DIR\"/*.dll \"$WINEPREFIX/drive_c/electrum/electrum/\" || fail \"Could not copy DLLs to destination\"\n\n\ninfo \"Building PyInstaller.\"\n# we build our own PyInstaller boot loader as the default one has high\n# anti-virus false positives\n(\n    if [ \"$WIN_ARCH\" = \"win32\" ] ; then\n        PYINST_ARCH=\"32bit\"\n    elif [ \"$WIN_ARCH\" = \"win64\" ] ; then\n        PYINST_ARCH=\"64bit\"\n    else\n        fail \"unexpected WIN_ARCH: $WIN_ARCH\"\n    fi\n    if [ -f \"$CACHEDIR/pyinstaller/PyInstaller/bootloader/Windows-$PYINST_ARCH-intel/runw.exe\" ]; then\n        info \"pyinstaller already built, skipping\"\n        exit 0\n    fi\n    cd \"$WINEPREFIX/drive_c/electrum\"\n    ELECTRUM_COMMIT_HASH=$(git rev-parse HEAD)\n    cd \"$CACHEDIR\"\n    rm -rf pyinstaller\n    mkdir pyinstaller\n    cd pyinstaller\n    # Shallow clone\n    git init\n    git remote add origin $PYINSTALLER_REPO\n    git fetch --depth 1 origin $PYINSTALLER_COMMIT\n    git checkout -b pinned \"${PYINSTALLER_COMMIT}^{commit}\"\n    rm -fv PyInstaller/bootloader/Windows-*/run*.exe || true\n    # add reproducible randomness. this ensures we build a different bootloader for each commit.\n    # if we built the same one for all releases, that might also get anti-virus false positives\n    echo \"const char *electrum_tag = \\\"tagged by Electrum@$ELECTRUM_COMMIT_HASH\\\";\" >> ./bootloader/src/pyi_main.c\n    pushd bootloader\n    # cross-compile to Windows using host python\n    python3 ./waf all CC=\"${GCC_TRIPLET_HOST}-gcc\" \\\n                      CFLAGS=\"-static\"\n    popd\n    # sanity check bootloader is there:\n    [[ -e \"PyInstaller/bootloader/Windows-$PYINST_ARCH-intel/runw.exe\" ]] || fail \"Could not find runw.exe in target dir!\"\n)\ninfo \"Installing PyInstaller.\"\n$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location ./pyinstaller\n\ninfo \"Wine is configured.\"\n"
  },
  {
    "path": "contrib/build-wine/pyinstaller.spec",
    "content": "# -*- mode: python -*-\nimport sys\nimport os\nfrom typing import TYPE_CHECKING\n\nfrom PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs, copy_metadata\n\nif TYPE_CHECKING:\n    from PyInstaller.building.build_main import Analysis, PYZ, EXE, COLLECT\n\n\nPYPKG=\"electrum\"\nMAIN_SCRIPT=\"run_electrum\"\nPROJECT_ROOT = \"C:/electrum\"\nICONS_FILE=f\"{PROJECT_ROOT}/{PYPKG}/gui/icons/electrum.ico\"\n\ncmdline_name = os.environ.get(\"ELECTRUM_CMDLINE_NAME\")\nif not cmdline_name:\n    raise Exception('no name')\n\n\n# see https://github.com/pyinstaller/pyinstaller/issues/2005\nhiddenimports = []\nhiddenimports += collect_submodules('pkg_resources')  # workaround for https://github.com/pypa/setuptools/issues/1963\nhiddenimports += collect_submodules(f\"{PYPKG}.plugins\")\n\n\nbinaries = []\n# Workaround for \"Retro Look\":\nbinaries += [b for b in collect_dynamic_libs('PyQt6') if 'qwindowsvista' in b[0]]\n# add libsecp256k1, libusb, etc:\nbinaries += [(f\"{PROJECT_ROOT}/{PYPKG}/*.dll\", '.')]\n\n\ndatas = [\n    (f\"{PROJECT_ROOT}/{PYPKG}/*.json\", PYPKG),\n    (f\"{PROJECT_ROOT}/{PYPKG}/lnwire/*.csv\", f\"{PYPKG}/lnwire\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/wordlist/english.txt\", f\"{PYPKG}/wordlist\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/wordlist/slip39.txt\", f\"{PYPKG}/wordlist\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/chains\", f\"{PYPKG}/chains\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/locale\", f\"{PYPKG}/locale\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/plugins\", f\"{PYPKG}/plugins\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/gui/icons\", f\"{PYPKG}/gui/icons\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/gui/fonts\", f\"{PYPKG}/gui/fonts\"),\n]\ndatas += collect_data_files(f\"{PYPKG}.plugins\")\ndatas += collect_data_files('trezorlib')  # TODO is this needed? and same question for other hww libs\ndatas += collect_data_files('safetlib')\ndatas += collect_data_files('ckcc')\ndatas += collect_data_files('bitbox02')\n\n# some deps rely on importlib metadata\ndatas += copy_metadata('slip10')  # from trezor->slip10\n\n# Exclude parts of Qt that we never use. Reduces binary size by tens of MBs. see #4815\nexcludes = [\n    \"PyQt6.QtBluetooth\",\n    \"PyQt6.QtDesigner\",\n    \"PyQt6.QtNfc\",\n    \"PyQt6.QtPositioning\",\n    \"PyQt6.QtQml\",\n    \"PyQt6.QtQuick\",\n    \"PyQt6.QtQuick3D\",\n    \"PyQt6.QtQuickWidgets\",\n    \"PyQt6.QtRemoteObjects\",\n    \"PyQt6.QtSensors\",\n    \"PyQt6.QtSerialPort\",\n    \"PyQt6.QtSpatialAudio\",\n    \"PyQt6.QtSql\",\n    \"PyQt6.QtTest\",\n    \"PyQt6.QtTextToSpeech\",\n    \"PyQt6.QtWebChannel\",\n    \"PyQt6.QtWebSockets\",\n    \"PyQt6.QtXml\",\n    # \"PyQt6.QtNetwork\",  # needed by QtMultimedia. kinda weird but ok.\n]\n\n# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports\na = Analysis([f\"{PROJECT_ROOT}/{MAIN_SCRIPT}\",\n              f\"{PROJECT_ROOT}/{PYPKG}/gui/qt/main_window.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/gui/qt/qrreader/qtmultimedia/camera_dialog.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/gui/text.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/util.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/wallet.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/simple_config.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/bitcoin.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/dnssec.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/commands.py\",\n              ],\n             binaries=binaries,\n             datas=datas,\n             hiddenimports=hiddenimports,\n             hookspath=[],\n             excludes=excludes,\n             )\n\n\n# http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal\nfor d in a.datas:\n    if 'pyconfig' in d[0]:\n        a.datas.remove(d)\n        break\n\n\n# hotfix for #3171 (pre-Win10 binaries)\na.binaries = [x for x in a.binaries if not x[1].lower().startswith(r'c:\\windows')]\n\npyz = PYZ(a.pure)\n\n\n#####\n# \"standalone\" exe with all dependencies packed into it\n\nexe_standalone = EXE(\n    pyz,\n    a.scripts,\n    a.binaries,\n    a.datas,\n    name=os.path.join(\"build\", \"pyi.win32\", PYPKG, f\"{cmdline_name}.exe\"),\n    debug=False,\n    strip=None,\n    upx=False,\n    icon=ICONS_FILE,\n    console=False)\n    # console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used\n\nexe_portable = EXE(\n    pyz,\n    a.scripts,\n    a.binaries,\n    a.datas + [('is_portable', 'README.md', 'DATA')],\n    name=os.path.join(\"build\", \"pyi.win32\", PYPKG, f\"{cmdline_name}-portable.exe\"),\n    debug=False,\n    strip=None,\n    upx=False,\n    icon=ICONS_FILE,\n    console=False)\n\n#####\n# exe and separate files that NSIS uses to build installer \"setup\" exe\n\nexe_inside_setup_noconsole = EXE(\n    pyz,\n    a.scripts,\n    exclude_binaries=True,\n    name=os.path.join(\"build\", \"pyi.win32\", PYPKG, f\"{cmdline_name}.exe\"),\n    debug=False,\n    strip=None,\n    upx=False,\n    icon=ICONS_FILE,\n    console=False)\n\nexe_inside_setup_console = EXE(\n    pyz,\n    a.scripts,\n    exclude_binaries=True,\n    name=os.path.join(\"build\", \"pyi.win32\", PYPKG, f\"{cmdline_name}-debug.exe\"),\n    debug=False,\n    strip=None,\n    upx=False,\n    icon=ICONS_FILE,\n    console=True)\n\ncoll = COLLECT(\n    exe_inside_setup_noconsole,\n    exe_inside_setup_console,\n    a.binaries,\n    a.zipfiles,\n    a.datas,\n    strip=None,\n    upx=True,\n    debug=False,\n    icon=ICONS_FILE,\n    console=False,\n    name=os.path.join('dist', PYPKG))\n"
  },
  {
    "path": "contrib/build-wine/sign.sh",
    "content": "#!/bin/bash\n\nset -e\n\nhere=\"$(dirname \"$0\")\"\nif [ -z \"$WIN_SIGNING_PASSWORD\" ]; then\n    echo \"password missing\"\n    exit 1\nfi\n\ntest -n \"$here\" -a -d \"$here\" || exit\ncd $here\n\nCERT_FILE=${CERT_FILE:-~/codesigning/cert.pem}\nKEY_FILE=${KEY_FILE:-~/codesigning/key.pem}\nif [[ ! -f \"$CERT_FILE\" ]]; then\n    ls \"$CERT_FILE\"\n    echo \"Make sure that $CERT_FILE and $KEY_FILE exist\"\nfi\n\nif ! which osslsigncode > /dev/null 2>&1; then\n    echo \"Please install osslsigncode\"\nfi\n\nrm -rf signed\nmkdir -p signed >/dev/null 2>&1\n\ncd dist\necho \"Found $(ls *.exe | wc -w) files to sign.\"\n\nfor f in $(ls *.exe); do\n    echo \"Signing $f...\"\n    osslsigncode sign \\\n        -pass \"$WIN_SIGNING_PASSWORD\" \\\n        -h sha256 \\\n        -certs \"$CERT_FILE\" \\\n        -key \"$KEY_FILE\" \\\n        -n \"Electrum\" \\\n        -i \"https://electrum.org/\" \\\n        -t \"http://timestamp.digicert.com/\" \\\n        -in \"$f\" \\\n        -out \"../signed/$f\"\n    ls \"../signed/$f\" -lah\ndone\n"
  },
  {
    "path": "contrib/build-wine/unsign.sh",
    "content": "#!/bin/bash\n\n# exit if command fails\nset -e\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/../..\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\nhere=\"$(dirname \"$0\")\"\ntest -n \"$here\" -a -d \"$here\" || exit\ncd \"$here\"\n\nif ! which osslsigncode > /dev/null 2>&1; then\n    echo \"Please install osslsigncode\"\n    exit 1\nfi\n\nrm -rf signed/stripped\nmkdir -p signed >/dev/null 2>&1\nmkdir -p signed/stripped >/dev/null 2>&1\n\nversion=$(\"$CONTRIB\"/print_electrum_version.py)\n\necho \"Found $(ls dist/*.exe | wc -w) files to verify.\"\n\nfor mine in dist/*.exe; do\n    echo \"---------------\"\n    f=\"$(basename \"$mine\")\"\n    if test -f \"signed/$f\"; then\n        echo \"Found file at signed/$f\"\n    else\n        echo \"Downloading https://download.electrum.org/$version/$f\"\n        wget -q \"https://download.electrum.org/$version/$f\" -O \"signed/$f\"\n    fi\n    out=\"signed/stripped/$f\"\n    # Remove PE signature from signed binary\n    osslsigncode remove-signature -in \"signed/$f\" -out \"$out\" > /dev/null 2>&1\n    chmod +x \"$out\"\n    if cmp -s \"$out\" \"$mine\"; then\n        echo \"Success: $f\"\n        #gpg --sign --armor --detach signed/$f\n    else\n        echo \"Failure: $f\"\n        exit 1\n    fi\ndone\n\nexit 0\n"
  },
  {
    "path": "contrib/build_tools_util.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\n# Set a fixed umask as this leaks into docker containers\numask 0022\n\nRED='\\033[0;31m'\nBLUE='\\033[0;34m'\nYELLOW='\\033[0;33m'\nNC='\\033[0m' # No Color\nfunction info {\n    printf \"\\r💬 ${BLUE}INFO:${NC}  ${1}\\n\"\n}\nfunction fail {\n    printf \"\\r🗯 ${RED}ERROR:${NC} ${1}\\n\"\n    exit 1\n}\nfunction warn {\n    printf \"\\r⚠️  ${YELLOW}WARNING:${NC}  ${1}\\n\"\n}\n\n\n# based on https://superuser.com/questions/497940/script-to-verify-a-signature-with-gpg\nfunction verify_signature() {\n    local file=$1 keyring=$2 out=\n    if out=$(gpg --no-default-keyring --keyring \"$keyring\" --status-fd 1 --verify \"$file\" 2>/dev/null) &&\n        echo \"$out\" | grep -qs \"^\\[GNUPG:\\] VALIDSIG \"; then\n        return 0\n    else\n        echo \"$out\" >&2\n        exit 1\n    fi\n}\n\nfunction verify_hash() {\n    local file=$1 expected_hash=$2\n    actual_hash=$(sha256sum \"$file\" | awk '{print $1}')\n    if [ \"$actual_hash\" == \"$expected_hash\" ]; then\n        return 0\n    else\n        echo \"$file $actual_hash (unexpected hash)\" >&2\n        rm \"$file\"\n        exit 1\n    fi\n}\n\nfunction download_if_not_exist() {\n    local file_name=$1 url=$2\n    if [ ! -e \"$file_name\" ] ; then\n        wget -O \"$file_name\" \"$url\"\n    fi\n}\n\n# Function to clone or update a git repository to a specific commit\nclone_or_update_repo() {\n    local repo_url=$1\n    local commit_hash=$2\n    local repo_dir=$3\n\n    if [ -z \"$repo_url\" ] || [ -z \"$commit_hash\" ] || [ -z \"$repo_dir\" ]; then\n        fail \"clone_or_update_repo: invalid arguments: repo_url='$repo_url', commit_hash='$commit_hash', repo_dir='$repo_dir'\"\n    fi\n\n    if [ -d \"$repo_dir\" ]; then\n        info \"Repository $repo_url exists in $repo_dir, updating...\"\n        git -C \"$repo_dir\" clean -ffxd >/dev/null 2>&1 || fail \"Failed to clean repository $repo_dir\"\n        git -C \"$repo_dir\" fetch --all >/dev/null 2>&1 || fail \"Failed to fetch from repository\"\n        git -C \"$repo_dir\" reset --hard \"$commit_hash^{commit}\" >/dev/null 2>&1 || fail \"Failed to reset to commit $commit_hash\"\n    else\n        info \"Cloning repository: $repo_url to $repo_dir\"\n        git clone \"$repo_url\" \"$repo_dir\" >/dev/null 2>&1 || fail \"Failed to clone repository $repo_url\"\n        git -C \"$repo_dir\" checkout \"$commit_hash^{commit}\" >/dev/null 2>&1 || fail \"Failed to checkout commit $commit_hash\"\n    fi\n}\n\napply_patch() {\n    local patch=$1\n    local path=$2\n\n    if [ -z \"$patch\" ] || [ -z \"$path\" ]; then\n        fail \"apply_patch: invalid arguments: patch='$patch', path='$path'\"\n    fi\n\n    if [ -d \"$path\" ]; then\n        info \"Patching: $patch\"\n        cd \"$path\"\n        patch -p1 <\"$patch\"\n        cd -\n    else\n        fail \"apply_patch: path='$path' not found\"\n    fi\n}\n\n# https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/templates/header.sh\nfunction retry() {\n    local result=0\n    local count=1\n    while [ $count -le 3 ]; do\n        [ $result -ne 0 ] && {\n            echo -e \"\\nThe command \\\"$@\\\" failed. Retrying, $count of 3.\\n\" >&2\n        }\n        ! { \"$@\"; result=$?; }\n        [ $result -eq 0 ] && break\n        count=$(($count + 1))\n        sleep 1\n    done\n\n    [ $count -gt 3 ] && {\n        echo -e \"\\nThe command \\\"$@\\\" failed 3 times.\\n\" >&2\n    }\n\n    return $result\n}\n\nfunction gcc_with_triplet()\n{\n    TRIPLET=\"$1\"\n    CMD=\"$2\"\n    shift 2\n    if [ -n \"$TRIPLET\" ] ; then\n        \"$TRIPLET-$CMD\" \"$@\"\n    else\n        \"$CMD\" \"$@\"\n    fi\n}\n\nfunction gcc_host()\n{\n    gcc_with_triplet \"$GCC_TRIPLET_HOST\" \"$@\"\n}\n\nfunction gcc_build()\n{\n    gcc_with_triplet \"$GCC_TRIPLET_BUILD\" \"$@\"\n}\n\nfunction host_strip()\n{\n    if [ \"$GCC_STRIP_BINARIES\" -ne \"0\" ] ; then\n        case \"$BUILD_TYPE\" in\n            linux|wine)\n                gcc_host strip \"$@\"\n                ;;\n            darwin)\n                # TODO: Strip on macOS?\n                ;;\n        esac\n    fi\n}\n\n# on MacOS, there is no realpath by default\nif ! [ -x \"$(command -v realpath)\" ]; then\n    function realpath() {\n        [[ $1 = /* ]] && echo \"$1\" || echo \"$PWD/${1#./}\"\n    }\nfi\n\n\nexport SOURCE_DATE_EPOCH=1530212462\nexport ZERO_AR_DATE=1 # for macOS\nexport PYTHONHASHSEED=22\n# Set the build type, overridden by wine build\nexport BUILD_TYPE=\"${BUILD_TYPE:-$(uname | tr '[:upper:]' '[:lower:]')}\"\n# Add host / build flags if the triplets are set\nif [ -n \"$GCC_TRIPLET_HOST\" ] ; then\n    export AUTOCONF_FLAGS=\"$AUTOCONF_FLAGS --host=$GCC_TRIPLET_HOST\"\nfi\nif [ -n \"$GCC_TRIPLET_BUILD\" ] ; then\n    export AUTOCONF_FLAGS=\"$AUTOCONF_FLAGS --build=$GCC_TRIPLET_BUILD\"\nfi\n\nexport GCC_STRIP_BINARIES=\"${GCC_STRIP_BINARIES:-0}\"\n\nif [ -n \"$CIRRUS_CPU\" ] ; then\n    # special-case for CI. see https://github.com/cirruslabs/cirrus-ci-docs/issues/1115\n    export CPU_COUNT=\"$CIRRUS_CPU\"\nelse\n    export CPU_COUNT=\"$(nproc 2> /dev/null || sysctl -n hw.ncpu)\"\nfi\ninfo \"Found $CPU_COUNT CPUs, which we might use for building.\"\n\n\nfunction break_legacy_easy_install() {\n    # We don't want setuptools sneakily installing dependencies, invisible to pip.\n    # This ensures that if setuptools calls distutils which then calls easy_install,\n    # easy_install will not download packages over the network.\n    # see https://pip.pypa.io/en/stable/reference/pip_install/#controlling-setup-requires\n    # see https://github.com/pypa/setuptools/issues/1916#issuecomment-743350566\n    info \"Intentionally breaking legacy easy_install.\"\n    DISTUTILS_CFG=\"${HOME}/.pydistutils.cfg\"\n    DISTUTILS_CFG_BAK=\"${HOME}/.pydistutils.cfg.orig\"\n    # If we are not inside docker, we might be overwriting a config file on the user's system...\n    if [ -e \"$DISTUTILS_CFG\" ] && [ ! -e \"$DISTUTILS_CFG_BAK\" ]; then\n        warn \"Overwriting python distutils config file at '$DISTUTILS_CFG'. A copy will be saved at '$DISTUTILS_CFG_BAK'.\"\n        mv \"$DISTUTILS_CFG\" \"$DISTUTILS_CFG_BAK\"\n    fi\n    cat <<EOF > \"$DISTUTILS_CFG\"\n[easy_install]\nindex_url = ''\nfind_links = ''\nEOF\n}\n\n"
  },
  {
    "path": "contrib/deterministic-build/README.md",
    "content": "# Notes\n\nThe frozen dependency lists in this folder are *generated* files.\n\n- Starting from `contrib/requirements/requirements*.txt`,\n- we use the `contrib/freeze_packages.sh` script,\n- to generate `contrib/deterministic-build/requirements*.txt`.\n\nThe source files list direct dependencies with loose version requirements,\nwhile the output files list all transitive dependencies with exact version+hash pins.\n\nThe build scripts only use these hash pinned requirement files.\n"
  },
  {
    "path": "contrib/deterministic-build/check_submodules.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/../..\"\nLOCALE=\"$PROJECT_ROOT/electrum/locale/\"\n\ncd \"$PROJECT_ROOT\"\n\ngit submodule init\ngit submodule update\n\nfunction get_git_mtime {\n    if [ $# -eq 1 ]; then\n        git log --pretty=%at -n1 -- $1\n    else\n        git log --pretty=%ar -n1 -- $2\n    fi\n}\n\nfail=0\n\n\nif [ $(date +%s -d \"2 weeks ago\") -gt $(get_git_mtime \"$LOCALE\") ]; then\n    echo \"Last update from electrum-locale is older than 2 weeks.\"\\\n         \"Please update it to incorporate the latest translations from crowdin.\"\n    fail=1\nfi\n\nexit ${fail}\n"
  },
  {
    "path": "contrib/deterministic-build/find_restricted_dependencies.py",
    "content": "#!/usr/bin/env python3\nimport sys\n\ntry:\n    import requests\nexcept ImportError as e:\n    sys.exit(f\"Error: {str(e)}. Try 'python3 -m pip install <module-name>'\")\n\ndef is_dependency_edge_blacklisted(*, parent_pkg: str, dep: str) -> bool:\n    \"\"\"Sometimes a package declares a hard dependency\n    for some niche functionality that we really do not care about.\n    \"\"\"\n    dep = dep.lower()\n    parent_pkg = parent_pkg.lower()\n    return (parent_pkg, dep) in {\n        (\"qrcode\", \"colorama\"),  # only needed for using qrcode-CLI on Windows.\n        (\"click\",  \"colorama\"),  # 'click' is a CLI tool, and it only needs colorama on Windows.\n                                 # In fact, we should blacklist 'click' itself, but that should be done elsewhere.\n    }\n\n\ndef check_restriction(*, dep: str, restricted: str, parent_pkg: str):\n    # See: https://www.python.org/dev/peps/pep-0496/\n    # Hopefully we don't need to parse the whole microlanguage\n    if is_dependency_edge_blacklisted(dep=dep, parent_pkg=parent_pkg):\n        return False\n    if \"extra\" in restricted and \"[\" not in dep:\n        return False\n    for marker in [\"os_name\", \"platform_release\", \"sys_platform\", \"platform_system\"]:\n        if marker in restricted:\n            return True\n    return False\n\n\ndef main():\n    for p in sys.stdin.read().split():\n        p = p.strip()\n        if not p:\n            continue\n        assert \"==\" in p, \"This script expects a list of packages with pinned version, e.g. package==1.2.3, not {}\".format(p)\n        p, v = p.rsplit(\"==\", 1)\n        try:\n            data = requests.get(\"https://pypi.org/pypi/{}/{}/json\".format(p, v)).json()[\"info\"]\n        except ValueError:\n            raise Exception(\"Package could not be found: {}=={}\".format(p, v))\n        try:\n            for r in data[\"requires_dist\"]:  # type: str\n                if \";\" not in r:\n                    continue\n                # example value for \"r\" at this point: \"pefile (>=2017.8.1) ; sys_platform == \\\"win32\\\"\"\n                dep, restricted = r.split(\";\", 1)\n                dep = dep.strip()\n                restricted = restricted.strip()\n                dep_basename = dep.split(\" \")[0]\n                if check_restriction(dep=dep, restricted=restricted, parent_pkg=p):\n                    print(dep_basename, sep=\" \")\n                    print(\"Installing {} from {} although it is only needed for {}\".format(dep, p, restricted), file=sys.stderr)\n        except TypeError:\n            # Has no dependencies at all\n            continue\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "contrib/deterministic-build/requirements-binaries-mac.txt",
    "content": "cffi==1.17.1 \\\n    --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \\\n    --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \\\n    --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \\\n    --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \\\n    --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \\\n    --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \\\n    --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \\\n    --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \\\n    --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \\\n    --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \\\n    --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \\\n    --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \\\n    --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \\\n    --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \\\n    --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \\\n    --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \\\n    --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \\\n    --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \\\n    --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \\\n    --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \\\n    --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \\\n    --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \\\n    --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \\\n    --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \\\n    --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \\\n    --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \\\n    --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \\\n    --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \\\n    --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \\\n    --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \\\n    --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \\\n    --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \\\n    --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \\\n    --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \\\n    --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \\\n    --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \\\n    --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \\\n    --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \\\n    --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \\\n    --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \\\n    --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \\\n    --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \\\n    --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \\\n    --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \\\n    --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \\\n    --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \\\n    --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \\\n    --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \\\n    --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \\\n    --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \\\n    --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \\\n    --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \\\n    --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \\\n    --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \\\n    --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \\\n    --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \\\n    --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \\\n    --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \\\n    --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \\\n    --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \\\n    --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \\\n    --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \\\n    --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \\\n    --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \\\n    --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \\\n    --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \\\n    --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b\ncryptography==45.0.3 \\\n    --hash=sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc \\\n    --hash=sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972 \\\n    --hash=sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b \\\n    --hash=sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4 \\\n    --hash=sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56 \\\n    --hash=sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716 \\\n    --hash=sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710 \\\n    --hash=sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8 \\\n    --hash=sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8 \\\n    --hash=sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782 \\\n    --hash=sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578 \\\n    --hash=sha256:71320fbefd05454ef2d457c481ba9a5b0e540f3753354fff6f780927c25d19b0 \\\n    --hash=sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71 \\\n    --hash=sha256:92d5f428c1a0439b2040435a1d6bc1b26ebf0af88b093c3628913dd464d13fa1 \\\n    --hash=sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490 \\\n    --hash=sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497 \\\n    --hash=sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca \\\n    --hash=sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc \\\n    --hash=sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19 \\\n    --hash=sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b \\\n    --hash=sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9 \\\n    --hash=sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57 \\\n    --hash=sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1 \\\n    --hash=sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06 \\\n    --hash=sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942 \\\n    --hash=sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab \\\n    --hash=sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342 \\\n    --hash=sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b \\\n    --hash=sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2 \\\n    --hash=sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c \\\n    --hash=sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899 \\\n    --hash=sha256:ec64ee375b5aaa354b2b273c921144a660a511f9df8785e6d1c942967106438e \\\n    --hash=sha256:ed43d396f42028c1f47b5fec012e9e12631266e3825e95c00e3cf94d472dac49 \\\n    --hash=sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7 \\\n    --hash=sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65 \\\n    --hash=sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f \\\n    --hash=sha256:fed5aaca1750e46db870874c9c273cd5182a9e9deb16f06f7bdffdb5c2bde4b9\npip==25.1.1 \\\n    --hash=sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af \\\n    --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077\npycparser==2.22 \\\n    --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \\\n    --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc\nPyQt6==6.6.1 \\\n    --hash=sha256:03a656d5dc5ac31b6a9ad200f7f4f7ef49fa00ad7ce7a991b9bb691617141d12 \\\n    --hash=sha256:5aa0e833cb5a79b93813f8181d9f145517dd5a46f4374544bcd1e93a8beec537 \\\n    --hash=sha256:6b43878d0bbbcf8b7de165d305ec0cb87113c8930c92de748a11c473a6db5085 \\\n    --hash=sha256:9f158aa29d205142c56f0f35d07784b8df0be28378d20a97bcda8bd64ffd0379\nPyQt6-Qt6==6.6.2 \\\n    --hash=sha256:5a41fe9d53b9e29e9ec5c23f3c5949dba160f90ca313ee8b96b8ffe6a5059387 \\\n    --hash=sha256:7ef446d3ffc678a8586ff6dc9f0d27caf4dff05dea02c353540d2f614386faf9 \\\n    --hash=sha256:8d7f674a4ec43ca00191e14945ca4129acbe37a2172ed9d08214ad58b170bc11 \\\n    --hash=sha256:b8363d88623342a72ac17da9127dc12f259bb3148796ea029762aa2d499778d9\nPyQt6-sip==13.10.2 \\\n    --hash=sha256:061d4a2eb60a603d8be7db6c7f27eb29d9cea97a09aa4533edc1662091ce4f03 \\\n    --hash=sha256:07f77e89d93747dda71b60c3490b00d754451729fbcbcec840e42084bf061655 \\\n    --hash=sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277 \\\n    --hash=sha256:1a6c2f168773af9e6c7ef5e52907f16297d4efd346e4c958eda54ea9135be18e \\\n    --hash=sha256:37af463dcce39285e686d49523d376994d8a2508b9acccb7616c4b117c9c4ed7 \\\n    --hash=sha256:38b5823dca93377f8a4efac3cbfaa1d20229aa5b640c31cf6ebbe5c586333808 \\\n    --hash=sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b \\\n    --hash=sha256:45ac06f0380b7aa4fcffd89f9e8c00d1b575dc700c603446a9774fda2dcfc0de \\\n    --hash=sha256:464ad156bf526500ce6bd05cac7a82280af6309974d816739b4a9a627156fafe \\\n    --hash=sha256:4ccf197f8fa410e076936bee28ad9abadb450931d5be5625446fd20e0d8b27a6 \\\n    --hash=sha256:4ffa71ddff6ef031d52cd4f88b8bba08b3516313c023c7e5825cf4a0ba598712 \\\n    --hash=sha256:5506b9a795098df3b023cc7d0a37f93d3224a9c040c43804d4bc06e0b2b742b0 \\\n    --hash=sha256:8132ec1cbbecc69d23dcff23916ec07218f1a9bbbc243bf6f1df967117ce303e \\\n    --hash=sha256:83e6a56d3e715f748557460600ec342cbd77af89ec89c4f2a68b185fa14ea46c \\\n    --hash=sha256:8b5d06a0eac36038fa8734657d99b5fe92263ae7a0cd0a67be6acfe220a063e1 \\\n    --hash=sha256:9c67ed66e21b11e04ffabe0d93bc21df22e0a5d7e2e10ebc8c1d77d2f5042991 \\\n    --hash=sha256:ad376a6078da37b049fdf9d6637d71b52727e65c4496a80b753ddc8d27526aca \\\n    --hash=sha256:b1d3cc9015a1bd8c8d3e86a009591e897d4d46b0c514aede7d2970a2208749cd \\\n    --hash=sha256:c7b34a495b92790c70eae690d9e816b53d3b625b45eeed6ae2c0fe24075a237e \\\n    --hash=sha256:c80cc059d772c632f5319632f183e7578cd0976b9498682833035b18a3483e92 \\\n    --hash=sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244 \\\n    --hash=sha256:ddd578a8d975bfb5fef83751829bf09a97a1355fa1de098e4fb4d1b74ee872fc \\\n    --hash=sha256:e455a181d45a28ee8d18d42243d4f470d269e6ccdee60f2546e6e71218e05bb4 \\\n    --hash=sha256:e907394795e61f1174134465c889177f584336a98d7a10beade2437bf5942244\nsetuptools==80.9.0 \\\n    --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \\\n    --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c\nwheel==0.45.1 \\\n    --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \\\n    --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"
  },
  {
    "path": "contrib/deterministic-build/requirements-binaries.txt",
    "content": "cffi==1.17.1 \\\n    --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \\\n    --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \\\n    --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \\\n    --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \\\n    --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \\\n    --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \\\n    --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \\\n    --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \\\n    --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \\\n    --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \\\n    --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \\\n    --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \\\n    --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \\\n    --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \\\n    --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \\\n    --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \\\n    --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \\\n    --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \\\n    --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \\\n    --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \\\n    --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \\\n    --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \\\n    --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \\\n    --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \\\n    --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \\\n    --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \\\n    --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \\\n    --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \\\n    --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \\\n    --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \\\n    --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \\\n    --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \\\n    --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \\\n    --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \\\n    --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \\\n    --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \\\n    --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \\\n    --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \\\n    --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \\\n    --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \\\n    --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \\\n    --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \\\n    --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \\\n    --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \\\n    --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \\\n    --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \\\n    --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \\\n    --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \\\n    --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \\\n    --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \\\n    --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \\\n    --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \\\n    --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \\\n    --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \\\n    --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \\\n    --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \\\n    --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \\\n    --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \\\n    --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \\\n    --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \\\n    --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \\\n    --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \\\n    --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \\\n    --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \\\n    --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \\\n    --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \\\n    --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b\ncryptography==45.0.3 \\\n    --hash=sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc \\\n    --hash=sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972 \\\n    --hash=sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b \\\n    --hash=sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4 \\\n    --hash=sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56 \\\n    --hash=sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716 \\\n    --hash=sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710 \\\n    --hash=sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8 \\\n    --hash=sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8 \\\n    --hash=sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782 \\\n    --hash=sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578 \\\n    --hash=sha256:71320fbefd05454ef2d457c481ba9a5b0e540f3753354fff6f780927c25d19b0 \\\n    --hash=sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71 \\\n    --hash=sha256:92d5f428c1a0439b2040435a1d6bc1b26ebf0af88b093c3628913dd464d13fa1 \\\n    --hash=sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490 \\\n    --hash=sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497 \\\n    --hash=sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca \\\n    --hash=sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc \\\n    --hash=sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19 \\\n    --hash=sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b \\\n    --hash=sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9 \\\n    --hash=sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57 \\\n    --hash=sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1 \\\n    --hash=sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06 \\\n    --hash=sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942 \\\n    --hash=sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab \\\n    --hash=sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342 \\\n    --hash=sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b \\\n    --hash=sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2 \\\n    --hash=sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c \\\n    --hash=sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899 \\\n    --hash=sha256:ec64ee375b5aaa354b2b273c921144a660a511f9df8785e6d1c942967106438e \\\n    --hash=sha256:ed43d396f42028c1f47b5fec012e9e12631266e3825e95c00e3cf94d472dac49 \\\n    --hash=sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7 \\\n    --hash=sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65 \\\n    --hash=sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f \\\n    --hash=sha256:fed5aaca1750e46db870874c9c273cd5182a9e9deb16f06f7bdffdb5c2bde4b9\npip==25.1.1 \\\n    --hash=sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af \\\n    --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077\npycparser==2.22 \\\n    --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \\\n    --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc\nPyQt6==6.9.0 \\\n    --hash=sha256:0c8b7251608e05b479cfe731f95857e853067459f7cbbcfe90f89de1bcf04280 \\\n    --hash=sha256:1cbc5a282454cf19691be09eadbde019783f1ae0523e269b211b0173b67373f6 \\\n    --hash=sha256:5344240747e81bde1a4e0e98d4e6e2d96ad56a985d8f36b69cd529c1ca9ff760 \\\n    --hash=sha256:6a8ff8e3cd18311bb7d937f7d741e787040ae7ff47ce751c28a94c5cddc1b4e6 \\\n    --hash=sha256:d36482000f0cd7ce84a35863766f88a5e671233d5f1024656b600cd8915b3752 \\\n    --hash=sha256:e344868228c71fc89a0edeb325497df4ff731a89cfa5fe57a9a4e9baecc9512b\nPyQt6-Qt6==6.9.0 \\\n    --hash=sha256:1188f118d1c570d27fba39707e3d8a48525f979816e73de0da55b9e6fa9ad0a1 \\\n    --hash=sha256:6d3875119dec6bf5f799facea362aa0ad39bb23aa9654112faa92477abccb5ff \\\n    --hash=sha256:9c0e603c934e4f130c110190fbf2c482ff1221a58317266570678bc02db6b152 \\\n    --hash=sha256:b1c4e4a78f0f22fbf88556e3d07c99e5ce93032feae5c1e575958d914612e0f9 \\\n    --hash=sha256:c825a6f5a9875ef04ef6681eda16aa3a9e9ad71847aa78dfafcf388c8007aa0a \\\n    --hash=sha256:cf840e8ae20a0704e0343810cf0e485552db28bf09ea976e58ec0e9b7bb27fcd\nPyQt6-sip==13.10.2 \\\n    --hash=sha256:061d4a2eb60a603d8be7db6c7f27eb29d9cea97a09aa4533edc1662091ce4f03 \\\n    --hash=sha256:07f77e89d93747dda71b60c3490b00d754451729fbcbcec840e42084bf061655 \\\n    --hash=sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277 \\\n    --hash=sha256:1a6c2f168773af9e6c7ef5e52907f16297d4efd346e4c958eda54ea9135be18e \\\n    --hash=sha256:37af463dcce39285e686d49523d376994d8a2508b9acccb7616c4b117c9c4ed7 \\\n    --hash=sha256:38b5823dca93377f8a4efac3cbfaa1d20229aa5b640c31cf6ebbe5c586333808 \\\n    --hash=sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b \\\n    --hash=sha256:45ac06f0380b7aa4fcffd89f9e8c00d1b575dc700c603446a9774fda2dcfc0de \\\n    --hash=sha256:464ad156bf526500ce6bd05cac7a82280af6309974d816739b4a9a627156fafe \\\n    --hash=sha256:4ccf197f8fa410e076936bee28ad9abadb450931d5be5625446fd20e0d8b27a6 \\\n    --hash=sha256:4ffa71ddff6ef031d52cd4f88b8bba08b3516313c023c7e5825cf4a0ba598712 \\\n    --hash=sha256:5506b9a795098df3b023cc7d0a37f93d3224a9c040c43804d4bc06e0b2b742b0 \\\n    --hash=sha256:8132ec1cbbecc69d23dcff23916ec07218f1a9bbbc243bf6f1df967117ce303e \\\n    --hash=sha256:83e6a56d3e715f748557460600ec342cbd77af89ec89c4f2a68b185fa14ea46c \\\n    --hash=sha256:8b5d06a0eac36038fa8734657d99b5fe92263ae7a0cd0a67be6acfe220a063e1 \\\n    --hash=sha256:9c67ed66e21b11e04ffabe0d93bc21df22e0a5d7e2e10ebc8c1d77d2f5042991 \\\n    --hash=sha256:ad376a6078da37b049fdf9d6637d71b52727e65c4496a80b753ddc8d27526aca \\\n    --hash=sha256:b1d3cc9015a1bd8c8d3e86a009591e897d4d46b0c514aede7d2970a2208749cd \\\n    --hash=sha256:c7b34a495b92790c70eae690d9e816b53d3b625b45eeed6ae2c0fe24075a237e \\\n    --hash=sha256:c80cc059d772c632f5319632f183e7578cd0976b9498682833035b18a3483e92 \\\n    --hash=sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244 \\\n    --hash=sha256:ddd578a8d975bfb5fef83751829bf09a97a1355fa1de098e4fb4d1b74ee872fc \\\n    --hash=sha256:e455a181d45a28ee8d18d42243d4f470d269e6ccdee60f2546e6e71218e05bb4 \\\n    --hash=sha256:e907394795e61f1174134465c889177f584336a98d7a10beade2437bf5942244\nsetuptools==80.9.0 \\\n    --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \\\n    --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c\nwheel==0.45.1 \\\n    --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \\\n    --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"
  },
  {
    "path": "contrib/deterministic-build/requirements-build-android.txt",
    "content": "appdirs==1.4.4 \\\n    --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41\ncolorama==0.4.5 \\\n    --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4\nCython==0.29.37 \\\n    --hash=sha256:f813d4a6dd94adee5d4ff266191d1d95bf6d4164a4facc535422c021b2504cfb\nJinja2==3.1.6 \\\n    --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d\nMarkupSafe==3.0.2 \\\n    --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0\npep517==0.13.1 \\\n    --hash=sha256:1b2fa2ffd3938bb4beffe5d6146cbcb2bda996a5a4da9f31abffd8b24e07b317\npexpect==4.9.0 \\\n    --hash=sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f\npip==25.1.1 \\\n    --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077\nptyprocess==0.7.0 \\\n    --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220\nsetuptools==80.9.0 \\\n    --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c\nsh==2.2.2 \\\n    --hash=sha256:653227a7c41a284ec5302173fbc044ee817c7bad5e6e4d8d55741b9aeb9eb65b\ntoml==0.10.2 \\\n    --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f\ntomli==2.2.1 \\\n    --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff\ntyping-extensions==4.13.2 \\\n    --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef\nwheel==0.45.1 \\\n    --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"
  },
  {
    "path": "contrib/deterministic-build/requirements-build-appimage.txt",
    "content": "Cython==3.1.1 \\\n    --hash=sha256:505ccd413669d5132a53834d792c707974248088c4f60c497deb1b416e366397\npip==25.1.1 \\\n    --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077\nsetuptools==80.9.0 \\\n    --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c\nwheel==0.45.1 \\\n    --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"
  },
  {
    "path": "contrib/deterministic-build/requirements-build-base.txt",
    "content": "expandvars==1.0.0 \\\n    --hash=sha256:f04070b8260264185f81142cd85e5df9ceef7229e836c5844302c4ccfa00c30d \\\n    --hash=sha256:ff1690eceb90bbdeefd1e4b15f4d217f22a3e66f776c2cb060635d2dde4a7689\nflit-core==3.12.0 \\\n    --hash=sha256:18f63100d6f94385c6ed57a72073443e1a71a4acb4339491615d0f16d6ff01b2 \\\n    --hash=sha256:e7a0304069ea895172e3c7bb703292e992c5d1555dd1233ab7b5621b5b69e62c\npackaging==25.0 \\\n    --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \\\n    --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f\npip==25.1.1 \\\n    --hash=sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af \\\n    --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077\npoetry-core==2.1.3 \\\n    --hash=sha256:0522a015477ed622c89aad56a477a57813cace0c8e7ff2a2906b7ef4a2e296a4 \\\n    --hash=sha256:2c704f05016698a54ca1d327f46ce2426d72eaca6ff614132c8477c292266771\nsetuptools==80.9.0 \\\n    --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \\\n    --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c\nsetuptools-scm==8.3.1 \\\n    --hash=sha256:332ca0d43791b818b841213e76b1971b7711a960761c5bea5fc5cdb5196fbce3 \\\n    --hash=sha256:3d555e92b75dacd037d32bafdf94f97af51ea29ae8c7b234cf94b7a5bd242a63\ntomli==2.0.2 \\\n    --hash=sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38 \\\n    --hash=sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed\nwheel==0.45.1 \\\n    --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \\\n    --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"
  },
  {
    "path": "contrib/deterministic-build/requirements-build-mac.txt",
    "content": "altgraph==0.17.4 \\\n    --hash=sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406\nCython==3.1.1 \\\n    --hash=sha256:505ccd413669d5132a53834d792c707974248088c4f60c497deb1b416e366397\nmacholib==1.16.3 \\\n    --hash=sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30\npackaging==25.0 \\\n    --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f\npip==25.1.1 \\\n    --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077\npyinstaller-hooks-contrib==2025.4 \\\n    --hash=sha256:5ce1afd1997b03e70f546207031cfdf2782030aabacc102190677059e2856446\nsetuptools==80.9.0 \\\n    --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c\nwheel==0.45.1 \\\n    --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"
  },
  {
    "path": "contrib/deterministic-build/requirements-build-wine.txt",
    "content": "altgraph==0.17.4 \\\n    --hash=sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406\npackaging==25.0 \\\n    --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f\npefile==2023.2.7 \\\n    --hash=sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc\npip==25.1.1 \\\n    --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077\npyinstaller-hooks-contrib==2025.4 \\\n    --hash=sha256:5ce1afd1997b03e70f546207031cfdf2782030aabacc102190677059e2856446\npywin32-ctypes==0.2.3 \\\n    --hash=sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755\nsetuptools==80.9.0 \\\n    --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c\nwheel==0.45.1 \\\n    --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"
  },
  {
    "path": "contrib/deterministic-build/requirements-hw.txt",
    "content": "base58==2.1.1 \\\n    --hash=sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2 \\\n    --hash=sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c\nbitbox02==7.0.0 \\\n    --hash=sha256:27d5105eb15a553719fa9d3e68921c864b00c861b3a644044d9ac68426f18447 \\\n    --hash=sha256:4b5b8422b94390b09962a4a93f4a9861429c093eb0f0b6c2d7661bbc1dd0e242\ncbor2==5.6.5 \\\n    --hash=sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468 \\\n    --hash=sha256:34cf5ab0dc310c3d0196caa6ae062dc09f6c242e2544bea01691fe60c0230596 \\\n    --hash=sha256:37096663a5a1c46a776aea44906cbe5fa3952f29f50f349179c00525d321c862 \\\n    --hash=sha256:38886c41bebcd7dca57739439455bce759f1e4c551b511f618b8e9c1295b431b \\\n    --hash=sha256:3d1a18b3a58dcd9b40ab55c726160d4a6b74868f2a35b71f9e726268b46dc6a2 \\\n    --hash=sha256:4586a4f65546243096e56a3f18f29d60752ee9204722377021b3119a03ed99ff \\\n    --hash=sha256:47261f54a024839ec649b950013c4de5b5f521afe592a2688eebbe22430df1dc \\\n    --hash=sha256:54c72a3207bb2d4480c2c39dad12d7971ce0853a99e3f9b8d559ce6eac84f66f \\\n    --hash=sha256:559dcf0d897260a9e95e7b43556a62253e84550b77147a1ad4d2c389a2a30192 \\\n    --hash=sha256:5b856fda4c50c5bc73ed3664e64211fa4f015970ed7a15a4d6361bd48462feaf \\\n    --hash=sha256:5ce13a27ef8fddf643fc17a753fe34aa72b251d03c23da6a560c005dc171085b \\\n    --hash=sha256:5cff06464b8f4ca6eb9abcba67bda8f8334a058abc01005c8e616728c387ad32 \\\n    --hash=sha256:61ceb77e6aa25c11c814d4fe8ec9e3bac0094a1f5bd8a2a8c95694596ea01e08 \\\n    --hash=sha256:66dd25dd919cddb0b36f97f9ccfa51947882f064729e65e6bef17c28535dc459 \\\n    --hash=sha256:6797b824b26a30794f2b169c0575301ca9b74ae99064e71d16e6ba0c9057de51 \\\n    --hash=sha256:6e14a1bf6269d25e02ef1d4008e0ce8880aa271d7c6b4c329dba48645764f60e \\\n    --hash=sha256:73b9647eed1493097db6aad61e03d8f1252080ee041a1755de18000dd2c05f37 \\\n    --hash=sha256:7488aec919f8408f9987a3a32760bd385d8628b23a35477917aa3923ff6ad45f \\\n    --hash=sha256:7f6d69f38f7d788b04c09ef2b06747536624b452b3c8b371ab78ad43b0296fab \\\n    --hash=sha256:824f202b556fc204e2e9a67d6d6d624e150fbd791278ccfee24e68caec578afd \\\n    --hash=sha256:863e0983989d56d5071270790e7ed8ddbda88c9e5288efdb759aba2efee670bc \\\n    --hash=sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33 \\\n    --hash=sha256:8f747b7a9aaa58881a0c5b4cd4a9b8fb27eca984ed261a769b61de1f6b5bd1e6 \\\n    --hash=sha256:90bfa36944caccec963e6ab7e01e64e31cc6664535dc06e6295ee3937c999cbb \\\n    --hash=sha256:93676af02bd9a0b4a62c17c5b20f8e9c37b5019b1a24db70a2ee6cb770423568 \\\n    --hash=sha256:94885903105eec66d7efb55f4ce9884fdc5a4d51f3bd75b6fedc68c5c251511b \\\n    --hash=sha256:97a7e409b864fecf68b2ace8978eb5df1738799a333ec3ea2b9597bfcdd6d7d2 \\\n    --hash=sha256:a34ee99e86b17444ecbe96d54d909dd1a20e2da9f814ae91b8b71cf1ee2a95e4 \\\n    --hash=sha256:a3ac50485cf67dfaab170a3e7b527630e93cb0a6af8cdaa403054215dff93adf \\\n    --hash=sha256:a83b76367d1c3e69facbcb8cdf65ed6948678e72f433137b41d27458aa2a40cb \\\n    --hash=sha256:a88f029522aec5425fc2f941b3df90da7688b6756bd3f0472ab886d21208acbd \\\n    --hash=sha256:a8947c102cac79d049eadbd5e2ffb8189952890df7cbc3ee262bbc2f95b011a9 \\\n    --hash=sha256:ae2b49226224e92851c333b91d83292ec62eba53a19c68a79890ce35f1230d70 \\\n    --hash=sha256:b682820677ee1dbba45f7da11898d2720f92e06be36acec290867d5ebf3d7e09 \\\n    --hash=sha256:b9d15b638539b68aa5d5eacc56099b4543a38b2d2c896055dccf7e83d24b7955 \\\n    --hash=sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2 \\\n    --hash=sha256:e25c2aebc9db99af7190e2261168cdde8ed3d639ca06868e4f477cf3a228a8e9 \\\n    --hash=sha256:f0d0a9c5aabd48ecb17acf56004a7542a0b8d8212be52f3102b8218284bd881e \\\n    --hash=sha256:f2764804ffb6553283fc4afb10a280715905a4cea4d6dc7c90d3e89c4a93bc8d \\\n    --hash=sha256:f4c7dbcdc59ea7f5a745d3e30ee5e6b6ff5ce7ac244aa3de6786391b10027bb3 \\\n    --hash=sha256:f91e6d74fa6917df31f8757fdd0e154203b0dd0609ec53eb957016a2b474896a \\\n    --hash=sha256:fa61a02995f3a996c03884cf1a0b5733f88cbfd7fa0e34944bf678d4227ee712 \\\n    --hash=sha256:fde21ac1cf29336a31615a2c469a9cb03cf0add3ae480672d4d38cda467d07fc \\\n    --hash=sha256:fe11c2eb518c882cfbeed456e7a552e544893c17db66fe5d3230dbeaca6b615c\ncertifi==2025.4.26 \\\n    --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \\\n    --hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3\ncffi==1.17.1 \\\n    --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \\\n    --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \\\n    --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \\\n    --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \\\n    --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \\\n    --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \\\n    --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \\\n    --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \\\n    --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \\\n    --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \\\n    --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \\\n    --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \\\n    --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \\\n    --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \\\n    --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \\\n    --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \\\n    --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \\\n    --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \\\n    --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \\\n    --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \\\n    --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \\\n    --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \\\n    --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \\\n    --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \\\n    --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \\\n    --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \\\n    --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \\\n    --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \\\n    --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \\\n    --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \\\n    --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \\\n    --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \\\n    --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \\\n    --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \\\n    --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \\\n    --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \\\n    --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \\\n    --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \\\n    --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \\\n    --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \\\n    --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \\\n    --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \\\n    --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \\\n    --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \\\n    --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \\\n    --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \\\n    --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \\\n    --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \\\n    --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \\\n    --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \\\n    --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \\\n    --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \\\n    --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \\\n    --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \\\n    --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \\\n    --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \\\n    --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \\\n    --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \\\n    --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \\\n    --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \\\n    --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \\\n    --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \\\n    --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \\\n    --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \\\n    --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \\\n    --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \\\n    --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b\ncharset-normalizer==3.4.2 \\\n    --hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \\\n    --hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \\\n    --hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \\\n    --hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \\\n    --hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \\\n    --hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \\\n    --hash=sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d \\\n    --hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \\\n    --hash=sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184 \\\n    --hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \\\n    --hash=sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b \\\n    --hash=sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64 \\\n    --hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \\\n    --hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \\\n    --hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \\\n    --hash=sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344 \\\n    --hash=sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58 \\\n    --hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \\\n    --hash=sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471 \\\n    --hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \\\n    --hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \\\n    --hash=sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836 \\\n    --hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \\\n    --hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \\\n    --hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \\\n    --hash=sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1 \\\n    --hash=sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01 \\\n    --hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \\\n    --hash=sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58 \\\n    --hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \\\n    --hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \\\n    --hash=sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2 \\\n    --hash=sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a \\\n    --hash=sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597 \\\n    --hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \\\n    --hash=sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5 \\\n    --hash=sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb \\\n    --hash=sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f \\\n    --hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \\\n    --hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \\\n    --hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \\\n    --hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \\\n    --hash=sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7 \\\n    --hash=sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7 \\\n    --hash=sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455 \\\n    --hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \\\n    --hash=sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4 \\\n    --hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \\\n    --hash=sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3 \\\n    --hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \\\n    --hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \\\n    --hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \\\n    --hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \\\n    --hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \\\n    --hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \\\n    --hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \\\n    --hash=sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12 \\\n    --hash=sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa \\\n    --hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \\\n    --hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \\\n    --hash=sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f \\\n    --hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \\\n    --hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \\\n    --hash=sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5 \\\n    --hash=sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02 \\\n    --hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \\\n    --hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \\\n    --hash=sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e \\\n    --hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \\\n    --hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \\\n    --hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \\\n    --hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \\\n    --hash=sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681 \\\n    --hash=sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba \\\n    --hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \\\n    --hash=sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a \\\n    --hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \\\n    --hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \\\n    --hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \\\n    --hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \\\n    --hash=sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027 \\\n    --hash=sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7 \\\n    --hash=sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518 \\\n    --hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \\\n    --hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \\\n    --hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \\\n    --hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \\\n    --hash=sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da \\\n    --hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \\\n    --hash=sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f \\\n    --hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \\\n    --hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f\nckcc-protocol==1.4.0 \\\n    --hash=sha256:c5fcc4705b4b78ec515b39549642570a660142407fa684c278cb0aea8122defa \\\n    --hash=sha256:cd93d4d3e3308ea4580aa6be5b4613a8266fd96b0cc1af51e7168def27bbece5\nclick==8.1.8 \\\n    --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \\\n    --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a\nconstruct==2.10.70 \\\n    --hash=sha256:4d2472f9684731e58cc9c56c463be63baa1447d674e0d66aeb5627b22f512c29 \\\n    --hash=sha256:c80be81ef595a1a821ec69dc16099550ed22197615f4320b57cc9ce2a672cb30\nconstruct-classes==0.1.2 \\\n    --hash=sha256:72ac1abbae5bddb4918688713f991f5a7fb6c9b593646a82f4bf3ac53de7eeb5 \\\n    --hash=sha256:e82437261790758bda41e45fb3d5622b54cfbf044ceb14774af68346faf5e08e\ncryptography==45.0.3 \\\n    --hash=sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc \\\n    --hash=sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972 \\\n    --hash=sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b \\\n    --hash=sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4 \\\n    --hash=sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56 \\\n    --hash=sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716 \\\n    --hash=sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710 \\\n    --hash=sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8 \\\n    --hash=sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8 \\\n    --hash=sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782 \\\n    --hash=sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578 \\\n    --hash=sha256:71320fbefd05454ef2d457c481ba9a5b0e540f3753354fff6f780927c25d19b0 \\\n    --hash=sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71 \\\n    --hash=sha256:92d5f428c1a0439b2040435a1d6bc1b26ebf0af88b093c3628913dd464d13fa1 \\\n    --hash=sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490 \\\n    --hash=sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497 \\\n    --hash=sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca \\\n    --hash=sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc \\\n    --hash=sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19 \\\n    --hash=sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b \\\n    --hash=sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9 \\\n    --hash=sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57 \\\n    --hash=sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1 \\\n    --hash=sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06 \\\n    --hash=sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942 \\\n    --hash=sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab \\\n    --hash=sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342 \\\n    --hash=sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b \\\n    --hash=sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2 \\\n    --hash=sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c \\\n    --hash=sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899 \\\n    --hash=sha256:ec64ee375b5aaa354b2b273c921144a660a511f9df8785e6d1c942967106438e \\\n    --hash=sha256:ed43d396f42028c1f47b5fec012e9e12631266e3825e95c00e3cf94d472dac49 \\\n    --hash=sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7 \\\n    --hash=sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65 \\\n    --hash=sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f \\\n    --hash=sha256:fed5aaca1750e46db870874c9c273cd5182a9e9deb16f06f7bdffdb5c2bde4b9\necdsa==0.19.1 \\\n    --hash=sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 \\\n    --hash=sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61\nhidapi==0.14.0.post4 \\\n    --hash=sha256:01747e681d138ec614321ef6f069e5be3743fa210112e529a34d3e99635e4ac0 \\\n    --hash=sha256:04357092b39631d8034b17fd111c5583be2790ad7979ac1983173344d28824e7 \\\n    --hash=sha256:0d51f8102a2441ce22e080576f8f370d25cb3962161818a89f236b0401840f18 \\\n    --hash=sha256:10a01af155c51a8089fe44e627af2fbd323cfbef7bd55a86837d971aef6088b0 \\\n    --hash=sha256:129d684c2760fafee9014ce63a58d8e2699cdf00cd1a11bb3d706d4715f5ff96 \\\n    --hash=sha256:1304fdeb694f581c46e7b0d6aebc6adfa66219177f04cacddbec0bd906bd5b7c \\\n    --hash=sha256:142374bb39c8973c6d04a2b8b767d64891741d05b09364b32531d9389c3a15bb \\\n    --hash=sha256:1487312ad50cf2c08a5ea786167b3229afd6478c4b26974157c3845a84e91231 \\\n    --hash=sha256:1591e98c0e6db4cc1e34e96295b4ea68eaf37d365d664570441388869e8e3618 \\\n    --hash=sha256:1807ff8abe3c5dcfa9d8acd71b1ab9f0aeb69cdbb039ddcbb150ed9fbbfd1ba7 \\\n    --hash=sha256:1bee0f731874d78367a3bf131cb0325578bc9fee0678ed00c4ca3ded45d11c20 \\\n    --hash=sha256:20a466e4cf2230687d21f55ffffb1a2384a2262fc343e507dd01d1ab981f7573 \\\n    --hash=sha256:21627bb8a0e2023da1dfb7cb7b970c30d6a86e6498721f1123d018b2f64b426f \\\n    --hash=sha256:21ebd1420db116733536fae227f1cb30ad74bded5090269cdda4facfa73a8867 \\\n    --hash=sha256:293b207e737df4577d27661c5135e7c16976f706d3739d7a53a169dde1aaebaa \\\n    --hash=sha256:2acadb4f1ae569c4f73ddb461af8733e8f5efcb290c3d0ef1b0671ba793b0ae3 \\\n    --hash=sha256:2d1c102f754b2085b270e7c29cb8a148ffb05e10325c373d05ac16e2cbce131c \\\n    --hash=sha256:3253d198b193065d633cde3f9a59dabeeb1608ece26f0f319a151e8c7775d7ae \\\n    --hash=sha256:348e68e3a2145a6ec6bebce13ffdf3e5883d8c720752c365027f16e16764def6 \\\n    --hash=sha256:380a74e743afe7a0241e0efce73ce9697f41d4e2e0a030be5458a44f9119427a \\\n    --hash=sha256:4169893fe5e368777fce7575a8bdedc1861f13d8fb9fda6b05e8155dde6eb7f1 \\\n    --hash=sha256:41d532d5a358a63db4d7fc1e57ea107150445c90167b39ba6f8fb84597396a48 \\\n    --hash=sha256:48fce253e526d17b663fbf9989c71c7ef7653ced5f4be65f1437c313fb3dbdf6 \\\n    --hash=sha256:4939faf6382d1c89462e72aa08636bbfe97ecb5464a34b14997e0ca3e1f92906 \\\n    --hash=sha256:4f04de00e40db2efc0bcdd047c160274ba7ccd861100fd87c295dd63cb932f2f \\\n    --hash=sha256:56d7538a4e156041bb80f07f47c327f8944e39da469b010041ce44e324d0657c \\\n    --hash=sha256:58a0a0c029886de8b301ce1ee2e7fd6914ae1ca49feb37cc9930c26baa683427 \\\n    --hash=sha256:5a5af70dad759b45536a9946d8232ef7d90859845d3554c93bea3e790250df75 \\\n    --hash=sha256:5c14c54cbfd45553cd3e6a23014f8e8f2d12c41cd2783e84c2cb774976d4648f \\\n    --hash=sha256:60115947607b8b0a719420726a541bad68728ece38b20654e81fef77c9e0bd2f \\\n    --hash=sha256:6270677da02e86b56b81afd5f6f313736b8315b493f3c8a431da285e3a3c5de9 \\\n    --hash=sha256:6439fc9686518d0336fac8c5e370093279f53c997540065fce131c97567118d8 \\\n    --hash=sha256:68d7e9ba5c48e50f322057b9f88d799c105b5d46c966981aa8e5047b6091541f \\\n    --hash=sha256:6b424ec16068d58d13fb67c7fb728824a3888f8f7fb6ffa3c82d5a54d8b74b7f \\\n    --hash=sha256:6e08884ee9e1e3963701c1cdf22edd17c7ff708728f163efc396964460b3f9b4 \\\n    --hash=sha256:6eaff1d120c47e1a121eada8dc85eac007d1ed81f3db7fc0da5b6ed17d8edefb \\\n    --hash=sha256:6f96ae777e906f0a9d6f75e873313145dfec2b774f558bfcae8ba34f09792460 \\\n    --hash=sha256:707b1ebf5cb051b020e94b039e603351bf2e6620b48fc970228e0dd5d3a91fca \\\n    --hash=sha256:74ae8ce339655b2568d74e49c8ef644d34a445dd0a9b4b89d1bf09447b83f5af \\\n    --hash=sha256:7d099c259aadcab2bc3f4fb5a1db579ec886c2cade7533016f62778235150746 \\\n    --hash=sha256:80fa94668d21b12daf62b034f647d71236470a8ba9a7580e220c47e9c119d932 \\\n    --hash=sha256:87218eeba366c871adcc273407aacbabab781d6a964919712d5583eded5ca50f \\\n    --hash=sha256:884fa003d899113e14908bd3b519c60b48fc3cec0410264dcbdad1c4a8fc2e8d \\\n    --hash=sha256:8a2d466b995f8ff387d68c052d3b74ee981a4ddc4f1a99f32f2dc7022273dc11 \\\n    --hash=sha256:8d924bd002a1c17ca51905b3b7b3d580e80ec211a9b8fe4667b73db0ff9e9b54 \\\n    --hash=sha256:8de94caca7f2616e41466c0ccdf7a96f567914e9e85e89e0b607018777fc0755 \\\n    --hash=sha256:8e20d0a1298a4bd342d7d927d928f1a5a29e5fc9dbf9a79e95dc6e2d386d5070 \\\n    --hash=sha256:949f437f517e81bc567429f41fb1e67349046eb43e52d47b2852b5847de452ee \\\n    --hash=sha256:97192b7756dd854cb2ebc8a1862ffa009cdc203e0399777764462cae3c459d58 \\\n    --hash=sha256:9e4b462fc1f2b160442618448132aebadb71c06b6eb7654eae4385c490100a67 \\\n    --hash=sha256:9f14ac2737fd6f58d88d2e6bf8ebd03aac7b486c14d3f570b7b1d0013d61b726 \\\n    --hash=sha256:a18af6ebd751eea7ddfb093ddf7d0371b05ba0f9a2f8593c7255a34e6bd753ff \\\n    --hash=sha256:a28de4a03fc276614518d8d0997d8152d0edaf8ca9166522316ef1c455e8bc29 \\\n    --hash=sha256:a2c4c3b3d77b759a4a118aa8428da1daf21c01b49915f44d7a3f198bcee4aa7b \\\n    --hash=sha256:a90cfdd29c10425cd4e4cff34adb12d25048561fc946f3562679e45721060a1c \\\n    --hash=sha256:ac3e6e794a0fd6ee4634bf1beea1c3c91ab6faf8b16f3f672a42541f9c5ea41f \\\n    --hash=sha256:b6b9c4dbf7d7e2635ff129ce6ea82174865c073b75888b8b97dda5a3d9a70493 \\\n    --hash=sha256:bca568a2b7d0d454c7921d70b1cc44f427eb6f95961b6d7b3b9b4532d0de74ef \\\n    --hash=sha256:c45a493dffdfe614a9943a8c7f0df617254f836f1242478f7780fbeafb18a131 \\\n    --hash=sha256:c8f722864a03c1d243a9538f0872e233d07fc3fe1d945c66c0cb632060d6d009 \\\n    --hash=sha256:cb1a2b5da0dcfab6837281342d1785cc373484bd3f27bd06fd2211d88075a7bd \\\n    --hash=sha256:d8ab5ba9fce95e342335ef48640221a46600c1afb66847432fad9823d40a2022 \\\n    --hash=sha256:da700db947562f8c0ac530215b74b5a27e4c669916ec99cfb5accd14ba08562c \\\n    --hash=sha256:da777638f5ecf9ef6c979f6c793417f54104d56ac99a48312d6f7e47858c2dd8 \\\n    --hash=sha256:e11d475429a1bc943ceac4ad8da4be63b240e00da5e10863fc3cbd9a35fdb51c \\\n    --hash=sha256:e1f6409854c0a8ed4d1fdbe88d5ee4baf6f19996d1561f76889a132cb083574d \\\n    --hash=sha256:e70eab52781e58e819730d99e3c825e92c15ec2138b6902ed078c8cd73317ce0 \\\n    --hash=sha256:e749b79d9cafc1e9fd9d397d8039377c928ca10a36847fda6407169513802f68 \\\n    --hash=sha256:e9af3c9191b7a4dade9152454001622519f4ecfa674b78929b739cfbf4b35d51 \\\n    --hash=sha256:f0cc21e82e95cb92ef951df8eb8acf5626ac8fa14ab5292abdab1b2349970445 \\\n    --hash=sha256:f27c74deda0282a97dd0f006fd79d6d08fdb16c7a3ba156d52fce85e48515b0a \\\n    --hash=sha256:f3ce310d366335e1ac9416d8e4a27d6eef2ae896fbee0135484d39d001711bea \\\n    --hash=sha256:f67e60eaa287e0fa35223f2d1f9afda81dd7312c7ba07e08fbdaf1af8a923530 \\\n    --hash=sha256:f787b76288450f60250895597dabb080894f0ea09ad5df0433412fee42452435 \\\n    --hash=sha256:fa66391be8acb358b381c30f32be5880d591a3358e531d980832d593dfe83d5a \\\n    --hash=sha256:fbd2835ff193d0261e0de375fea006cb7cb18a30ae1657af48a43e381f6a0995 \\\n    --hash=sha256:fedb9c3be6a2376de436d13fcb37a686a9b6bc988585bcc4f5ec61cad925e794 \\\n    --hash=sha256:ff021ed0962f2d5d67405ae53c85f6cb3ab8c5af3dff7db8c74672f79f7a39d1 \\\n    --hash=sha256:ff67139fbaa91eed55e7e916bdc1ccdaf8c909a80a9c480011caa65c4ba82a97\nidna==3.10 \\\n    --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \\\n    --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3\nledger-bitcoin==0.4.0 \\\n    --hash=sha256:2242452e78cf4b57c8b8d3509e831860fd4851b0a1bfab95f2f5e3f47d4d1500 \\\n    --hash=sha256:a33e78710671ec21e1003d0483406e955b48866ccf515fd8e7d8d81f4e1c1cf9\nledgercomm==1.2.1 \\\n    --hash=sha256:015cfc05f16b8c59f8cc1d9fc0b8935923f1fcc3806d33eeb6b0e055b44f5a91 \\\n    --hash=sha256:8ffef5703355b8ec7b73bca325f70288f4d0dafcb299c09833de9c197fb6dd34\nlibusb1==3.3.1 \\\n    --hash=sha256:0ef69825173ce74af34444754c081cc324233edc6acc405658b3ad784833e076 \\\n    --hash=sha256:3951d360f2daf0e0eacf839e15d2d1d2f4f5e7830231eb3188eeffef2dd17bad \\\n    --hash=sha256:6e21b772d80d6487fbb55d3d2141218536db302da82f1983754e96c72781c102 \\\n    --hash=sha256:808c9362299dcee01651aa87e71e9d681ccedb27fc4dbd70aaf14e245fb855f1\nmnemonic==0.21 \\\n    --hash=sha256:1fe496356820984f45559b1540c80ff10de448368929b9c60a2b55744cc88acf \\\n    --hash=sha256:72dc9de16ec5ef47287237b9b6943da11647a03fe7cf1f139fc3d7c4a7439288\nnoiseprotocol==0.3.1 \\\n    --hash=sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111 \\\n    --hash=sha256:b092a871b60f6a8f07f17950dc9f7098c8fe7d715b049bd4c24ee3752b90d645\npackaging==25.0 \\\n    --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \\\n    --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f\npip==25.1.1 \\\n    --hash=sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af \\\n    --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077\nprotobuf==3.20.3 \\\n    --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \\\n    --hash=sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c \\\n    --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2 \\\n    --hash=sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b \\\n    --hash=sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050 \\\n    --hash=sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9 \\\n    --hash=sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7 \\\n    --hash=sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454 \\\n    --hash=sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480 \\\n    --hash=sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469 \\\n    --hash=sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c \\\n    --hash=sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e \\\n    --hash=sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db \\\n    --hash=sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905 \\\n    --hash=sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b \\\n    --hash=sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86 \\\n    --hash=sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4 \\\n    --hash=sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402 \\\n    --hash=sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7 \\\n    --hash=sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4 \\\n    --hash=sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99 \\\n    --hash=sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee\npyaes==1.6.1 \\\n    --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f\npycparser==2.22 \\\n    --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \\\n    --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc\npyserial==3.5 \\\n    --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \\\n    --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0\nrequests==2.32.3 \\\n    --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \\\n    --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6\nsafet==0.1.5 \\\n    --hash=sha256:a7fd4b68bb1bc6185298af665c8e8e00e2bb2bcbddbb22844ead929b845c635e \\\n    --hash=sha256:f966a23243312f64d14c7dfe02e8f13f6eeba4c3f51341f2c11ae57831f07de3\nsemver==3.0.4 \\\n    --hash=sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746 \\\n    --hash=sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602\nsetuptools==80.9.0 \\\n    --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \\\n    --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c\nshamir-mnemonic==0.3.0 \\\n    --hash=sha256:188c6b5bd00d5e756e12e2b186c3cb7c98ff7ff44df608d4c1d2077f6b6e730f \\\n    --hash=sha256:bc04886a1ddfe2a64d8a3ec51abf0f664d98d5b557cc7e78a8ad2d10a1d87438\nsix==1.17.0 \\\n    --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \\\n    --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81\nslip10==1.0.1 \\\n    --hash=sha256:02b350ae557b591791428b17551f95d7ac57e9211f37debdc814c90b4a123a54 \\\n    --hash=sha256:4aa764369db0a261e468160ec1afeeb2b22d26392dd118c49b9daa91f642947b\ntrezor==0.13.10 \\\n    --hash=sha256:7a0b6ae4628dd0c31a5ceb51258918d9bbdd3ad851388837225826b228ee504f \\\n    --hash=sha256:7c85dc2c47998765c84d309fc753d2b116c943d447289157895488899c95706d\ntyping-extensions==4.13.2 \\\n    --hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \\\n    --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef\nurllib3==1.26.20 \\\n    --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \\\n    --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32\nwheel==0.45.1 \\\n    --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \\\n    --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"
  },
  {
    "path": "contrib/deterministic-build/requirements.txt",
    "content": "aiohappyeyeballs==2.6.1 \\\n    --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558\naiohttp==3.12.9 \\\n    --hash=sha256:2c9914c8914ff40b68c6e4ed5da33e88d4e8f368fddd03ceb0eb3175905ca782\naiohttp-socks==0.10.1 \\\n    --hash=sha256:49f2e1f8051f2885719beb1b77e312b5a27c3e4b60f0b045a388f194d995e068\naiorpcX==0.25.0 \\\n    --hash=sha256:940fa250ea5e9fd372d4c6acdc20dcb603bd1960ca324759d29864a4aaf64570\naiosignal==1.3.2 \\\n    --hash=sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54\nasync-timeout==5.0.1 \\\n    --hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3\nattrs==22.2.0 \\\n    --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99\ncertifi==2025.4.26 \\\n    --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6\ndnspython==2.4.2 \\\n    --hash=sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984\nelectrum-aionostr==0.1.0 \\\n    --hash=sha256:3774f8e8312388272e10851a869c9f4d3d4a54d8d564851c36e2dc40297bec84\nelectrum-ecc==0.0.7 \\\n    --hash=sha256:ed4134e1dbff0fd83022764c6acc97a02cde3512927d7c41f4d48b9a06e91fb2\nfrozenlist==1.6.0 \\\n    --hash=sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68\nidna==3.10 \\\n    --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9\njsonpatch==1.33 \\\n    --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c\njsonpointer==3.0.0 \\\n    --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef\nmultidict==6.4.4 \\\n    --hash=sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8\npackaging==25.0 \\\n    --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f\npip==25.1.1 \\\n    --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077\npropcache==0.3.1 \\\n    --hash=sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf\nprotobuf==3.20.3 \\\n    --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2\npython-socks==2.7.1 \\\n    --hash=sha256:f1a0bb603830fe81e332442eada96757b8f8dec02bd22d1d6f5c99a79704c550\nQDarkStyle==3.2.3 \\\n    --hash=sha256:0c0b7f74a6e92121008992b369bab60468157db1c02cd30d64a5e9a3b402f1ae\nqrcode==8.2 \\\n    --hash=sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c\nQtPy==2.4.3 \\\n    --hash=sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb\nsetuptools==80.9.0 \\\n    --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c\ntyping-extensions==4.13.2 \\\n    --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef\nwheel==0.45.1 \\\n    --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729\nyarl==1.20.0 \\\n    --hash=sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"
  },
  {
    "path": "contrib/docker_notes.md",
    "content": "# Using the build scripts\n\nMost of our build scripts are docker-based.\n(All, except the macOS build, which is a separate beast and always has to be special-cased\nat the cost of significant maintenance burden...)\n\nTypically, the build flow is:\n\n- build a docker image, based on debian\n  - the apt sources mirror used is `snapshot.debian.org`\n    - (except for the source tarball build, which is simple enough not to need this)\n    - this helps with historical reproducibility\n    - note that `snapshot.debian.org` is often slow and sometimes keeps timing out :/\n      (see #8496)\n      - a potential alternative would be `snapshot.notset.fr`, but that mirror is missing\n        e.g. `binary-i386`, which is needed for the wine/windows build.\n    - if you are just trying to build for yourself and don't need reproducibility,\n      you can just switch back to the default debian apt sources mirror.\n  - docker caches the build (locally), and so this step only needs to be rerun\n    if we update the Dockerfile. This caching happens automatically and by default.\n    - you can disable the caching by setting envvar `ELECBUILD_NOCACHE=1`. See below.\n- create a docker container from the image, and build the final binary inside the container\n\n\n## Notes about using Docker\n\n- To install Docker:\n\n    This assumes an Ubuntu (x86_64) host, but it should not be too hard to adapt to another similar system.\n\n    ```\n    $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -\n    $ sudo add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\"\n    $ sudo apt-get update\n    $ sudo apt-get install -y docker-ce\n    ```\n\n- To communicate with the docker daemon, the build scripts either need to be called via sudo,\n  or the unix user on the host system (e.g. the user you run as) needs to be\n  part of the `docker` group. i.e.:\n  ```\n  $ sudo usermod -aG docker ${USER}\n  ```\n  (and then reboot or similar for it to take effect)\n\n\n## Environment variables\n\n- `ELECBUILD_COMMIT`\n\n    When unset or empty, we build directly from the local git clone. These builds\n    are *not* reproducible.\n\n    When non-empty, it should be set to a git ref. We will create a fresh git clone\n    checked out at that reference in `/tmp/electrum_build/`, and build there.\n\n- `ELECBUILD_NOCACHE=1`\n\n    A non-empty value forces a rebuild of the docker image.\n\n    Before we started using `snapshot.debian.org` for apt sources,\n    setting this was necessary to properly test historical reproducibility.\n    (we were version-pinning packages installed using `apt`, but it was not realistic to\n     version-pin all transitive dependencies, and sometimes an update of those resulted in\n     changes to our binary builds)\n\n    I think setting this is no longer necessary for building reproducibly.\n\n"
  },
  {
    "path": "contrib/freeze_containers_distro.sh",
    "content": "#!/bin/sh\n\n# Run this after a new release to update pin for build container distro packages\n\nset -e\n\nDEBIAN_SNAPSHOT_BASE=\"https://snapshot.debian.org/archive/debian/\"\nDEBIAN_APPIMAGE_DISTRO=\"bullseye\"  # should match build-linux/appimage Dockerfile base\nDEBIAN_WINE_DISTRO=\"trixie\"    # should match build-wine Dockerfile base\nDEBIAN_ANDROID_DISTRO=\"trixie\" # should match android Dockerfile base\n\ncontrib=\"$(dirname \"$0\")\"\n\n\nif [ ! -x /bin/wget ]; then\n    echo \"no wget\"\n    exit 1\nfi\n\nDEBIAN_SNAPSHOT_LATEST=$(wget -O- \"${DEBIAN_SNAPSHOT_BASE}$(date +\"?year=%Y&month=%m\")\" 2>/dev/null | grep \"^<a href=\\\"20\" | tail -1 | sed -e 's#[^\"]*\"\\(.\\{17,17\\}\\).*#\\1#')\n\nif [ \"${DEBIAN_SNAPSHOT_LATEST}x\" = \"x\" ]; then\n    echo \"could not find timestamp for debian packages\"\n    exit 1\nfi\n\nDEBIAN_SNAPSHOT=${DEBIAN_SNAPSHOT_BASE}${DEBIAN_SNAPSHOT_LATEST}\n\necho \"Checking if URL valid..\"\nwget -O /dev/null ${DEBIAN_SNAPSHOT} 2>/dev/null\n\necho \"Valid!\"\n\n# build-linux\necho \"deb ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main\" > \"$contrib/build-linux/appimage/apt.sources.list\"\necho \"deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main\" >> \"$contrib/build-linux/appimage/apt.sources.list\"\n\n# build-wine\necho \"deb ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main\" > \"$contrib/build-wine/apt.sources.list\"\necho \"deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main\" >> \"$contrib/build-wine/apt.sources.list\"\n\n# android\necho \"deb ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main\" > \"$contrib/android/apt.sources.list\"\necho \"deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main\" >> \"$contrib/android/apt.sources.list\"\n\necho \"updated APT sources to ${DEBIAN_SNAPSHOT}\"\n"
  },
  {
    "path": "contrib/freeze_packages.sh",
    "content": "#!/bin/bash\n# Run this after a new release to update dependencies\n\nset -e\n\nvenv_dir=~/.electrum-venv\ncontrib=\"$(dirname \"$0\")\"\n\n# note: we should not use a higher version of python than what the binaries bundle\nif [[ ! \"$SYSTEM_PYTHON\" ]] ; then\n    SYSTEM_PYTHON=$(which python3.10) || printf \"\"\nelse\n    SYSTEM_PYTHON=$(which \"$SYSTEM_PYTHON\") || printf \"\"\nfi\nif [[ ! \"$SYSTEM_PYTHON\" ]] ; then\n    echo \"Please specify which python to use in \\$SYSTEM_PYTHON\" && exit 1\nfi\n\n\"${SYSTEM_PYTHON}\" -m hashin -h > /dev/null 2>&1 || { \"${SYSTEM_PYTHON}\" -m pip install hashin; }\n\nfor suffix in '' '-hw' '-binaries' '-binaries-mac' '-build-wine' '-build-mac' '-build-base' '-build-appimage' '-build-android'; do\n    reqfile=\"requirements${suffix}.txt\"\n\n    rm -rf \"$venv_dir\"\n    \"${SYSTEM_PYTHON}\" -m venv \"$venv_dir\"\n\n    source \"$venv_dir/bin/activate\"\n\n    echo \"Installing dependencies... (${reqfile})\"\n\n    # We pin all python packaging tools (pip and friends). Some of our dependencies might\n    # pull some of them in (e.g. protobuf->setuptools), and all transitive dependencies\n    # must be pinned, so we might as well pin all packaging tools. This however means\n    # that we should explicitly install them now, so that we pin latest versions if possible.\n    python -m pip install --upgrade pip setuptools wheel\n\n    python -m pip install -r \"$contrib/requirements/${reqfile}\" --upgrade\n\n    echo \"OK.\"\n\n    requirements=$(pip freeze --all)\n\n    restricted=$(echo $requirements | ${SYSTEM_PYTHON} \"$contrib/deterministic-build/find_restricted_dependencies.py\")\n    if [ ! -z \"$restricted\" ]; then\n        python -m pip install $restricted\n        requirements=$(pip freeze --all)\n    fi\n\n    echo \"Generating package hashes... (${reqfile})\"\n    rm -f \"$contrib/deterministic-build/${reqfile}\"\n    touch \"$contrib/deterministic-build/${reqfile}\"\n\n    # restrict ourselves to source-only packages.\n    # TODO expand this to all reqfiles...\n    HASHIN_FLAGS=\"\"\n    if [[\n        \"${suffix}\" == \"\" ||\n        \"${suffix}\" == \"-build-wine\" ||\n        \"${suffix}\" == \"-build-mac\" ||\n        \"${suffix}\" == \"-build-appimage\" ||\n        \"${suffix}\" == \"-build-android\" ||\n        \"0\" == \"1\"\n        ]] ;\n    then\n        HASHIN_FLAGS=\"--python-version source\"\n    fi\n\n    echo -e \"\\r  Hashing requirements for $reqfile...\"\n    ${SYSTEM_PYTHON} -m hashin $HASHIN_FLAGS -r \"$contrib/deterministic-build/${reqfile}\" $requirements\n\n    echo \"OK.\"\ndone\n\necho \"Done. Updated requirements\"\n"
  },
  {
    "path": "contrib/generate_payreqpb2.sh",
    "content": "#!/bin/bash\n# Generates the file paymentrequest_pb2.py\n\nset -e\n\nCONTRIB=\"$(dirname \"$(readlink -e \"$0\")\")\"\nEL=\"$CONTRIB\"/../electrum\n\nif ! which protoc > /dev/null 2>&1; then\n    echo \"Please install 'protoc'\"\n    echo \"If you're on Debian, try 'sudo apt install protobuf-compiler'?\"\n    exit 1\nfi\n\nprotoc --proto_path=\"$EL\" --python_out=\"$EL\" \"$EL\"/paymentrequest.proto\n"
  },
  {
    "path": "contrib/locale/build_cleanlocale.sh",
    "content": "#!/bin/bash\n\nset -e\n\nCONTRIB_LOCALE=\"$(dirname \"$(realpath \"$0\" 2> /dev/null || grealpath \"$0\")\")\"\nCONTRIB=\"$CONTRIB_LOCALE\"/..\nPROJECT_ROOT=\"$CONTRIB\"/..\n\ncd \"$PROJECT_ROOT\"\ngit submodule update --init\n\nLOCALE=\"$PROJECT_ROOT/electrum/locale/\"\ncd \"$LOCALE\"\ngit clean -ffxd\ngit reset --hard\nrm -rf llm_proofreader\n\"$CONTRIB_LOCALE/build_locale.sh\" \"$LOCALE/locale\" \"$LOCALE/locale\"\n"
  },
  {
    "path": "contrib/locale/build_locale.sh",
    "content": "#!/bin/bash\n#\n# This script converts human-readable (.po) locale files to compiled (.mo) locale files.\n\nset -e\n\nCONTRIB_LOCALE=\"$(dirname \"$(realpath \"$0\" 2> /dev/null || grealpath \"$0\")\")\"\n\n\nif [[ ! -d \"$1\" || -z \"$2\" ]]; then\n    echo \"usage: $0 locale_source_dir locale_dest_dir\"\n    echo \"       The dirs can match, to build in place.\"\n    # ^ note: these are the paths to the \"inner\" locale/ dir\n    exit 1\nfi\n\n# convert $1 and $2 to abs paths\nSRC_DIR=\"$(realpath \"$1\" 2> /dev/null || grealpath \"$1\")\"\nDST_DIR=\"$(realpath \"$2\" 2> /dev/null || grealpath \"$2\")\"\n\nif ! which msgfmt > /dev/null 2>&1; then\n    echo \"Please install gettext\"\n    exit 1\nfi\n\ncd \"$SRC_DIR\"\nmkdir -p \"$DST_DIR\"\n\nfor i in *; do\n    dir=\"$DST_DIR/$i/LC_MESSAGES\"\n    mkdir -p \"$dir\"\n    (msgfmt --output-file=\"$dir/electrum.mo\" \"$i/electrum.po\" || true)\ndone\n\necho \"running stats.py\"\n\"$CONTRIB_LOCALE/stats.py\"\n"
  },
  {
    "path": "contrib/locale/push_locale.py",
    "content": "#!/usr/bin/env python3\n#\n# This script extracts \"raw\" strings from the codebase,\n# and uploads them to crowdin, for the community to translate them.\n#\n# Dependencies:\n# $ sudo apt-get install python3-requests gettext qt6-l10n-tools\n\nimport glob\nimport os\nimport subprocess\nimport sys\n\ntry:\n    import requests\nexcept ImportError as e:\n    sys.exit(f\"Error: {str(e)}. Try 'python3 -m pip install --user <module-name>'\")\n\n# set cwd\nproject_root = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))\nos.chdir(project_root)\n\nlocale_dir = os.path.join(project_root, \"electrum\", \"locale\")\nif not os.path.exists(os.path.join(locale_dir, \"locale\")):\n    raise Exception(f\"missing git submodule for locale? {locale_dir}\")\n\n# check dependencies are available\ntry:\n    subprocess.check_output([\"xgettext\", \"--version\"])\n    subprocess.check_output([\"msgcat\", \"--version\"])\nexcept (subprocess.CalledProcessError, OSError) as e2:\n    raise Exception(\"missing gettext. Maybe try 'apt install gettext'\")\n\nQT_LUPDATE=\"lupdate\"\nQT_LCONVERT=\"lconvert\"\ntry:\n    subprocess.check_output([QT_LUPDATE, \"-version\"])\n    subprocess.check_output([QT_LCONVERT, \"-h\"])\nexcept (subprocess.CalledProcessError, OSError) as e1:\n    QT_LUPDATE=\"/usr/lib/qt6/bin/lupdate\"  # workaround qt5/qt6 confusion on ubuntu 22.04\n    QT_LCONVERT=\"/usr/lib/qt6/bin/lconvert\"\n    try:\n        subprocess.check_output([QT_LUPDATE, \"-version\"])\n        subprocess.check_output([QT_LCONVERT, \"-h\"])\n    except (subprocess.CalledProcessError, OSError) as e2:\n        raise Exception(\"missing Qt lupdate/convert tools. Maybe try 'apt install qt6-l10n-tools'\")\n\n# create build dir\nbuild_dir = os.path.join(locale_dir, \"build\")\nif not os.path.exists(build_dir):\n    os.mkdir(build_dir)\n\n# add .py files\nfiles_list = glob.glob(\"electrum/**/*.py\", recursive=True)\nfiles_list = sorted(files_list)  # makes output deterministic across CI runs\nwith open(f\"{build_dir}/app.fil\", \"w\", encoding=\"utf-8\") as f:\n    for item in files_list:\n        f.write(item + \"\\n\")\nprint(\"Found {} .py files to translate\".format(len(files_list)))\n\n# Generate fresh translation template\nprint('Generating template...')\n# note: do not use xgettext option \"--sort-output\", as that makes human translators have to context-switch all the time\ncmd = [\"xgettext\", \"--from-code\", \"UTF-8\", \"--language\", \"Python\", \"--no-wrap\", \"-f\", f\"{build_dir}/app.fil\", f\"--output={build_dir}/messages_gettext.pot\"]\nsubprocess.check_output(cmd)\n\n# add QML translations\nfiles_list = glob.glob(\"electrum/gui/qml/**/*.qml\", recursive=True)\nfiles_list = sorted(files_list)  # makes output deterministic across CI runs\nwith open(f\"{build_dir}/qml.lst\", \"w\", encoding=\"utf-8\") as f:\n    for item in files_list:\n        f.write(item + \"\\n\")\nprint(\"Found {} QML files to translate\".format(len(files_list)))\n\n# note: lupdate writes relative paths into its output .ts file, relative to the .ts file itself :/\ncmd = [QT_LUPDATE, f\"@{build_dir}/qml.lst\",\"-ts\", f\"{build_dir}/qml.ts\"]\nprint('Collecting strings')\nsubprocess.check_output(cmd)\n\ncmd = [QT_LCONVERT, \"-of\", \"po\", \"-o\", f\"{build_dir}/messages_qml.pot\", f\"{build_dir}/qml.ts\"]\nprint('Convert to gettext')\nsubprocess.check_output(cmd)\n\nprint(\"Fixing some paths in messages_qml.pot\")\n#  sed from \" ../../gui/qml/\"\n#      to   \" electrum/gui/qml/\"\ncmd = [\"sed\", \"-i\", r\"s/ ..\\/..\\/gui\\/qml\\// electrum\\/gui\\/qml\\//g\", f\"{build_dir}/messages_qml.pot\"]\nsubprocess.check_output(cmd)\n\ncmd = [\"msgcat\", \"-u\", \"-o\", f\"{build_dir}/messages.pot\", f\"{build_dir}/messages_gettext.pot\", f\"{build_dir}/messages_qml.pot\"]\nprint('Generate template')\nsubprocess.check_output(cmd)\n\n# Add a custom PO header entry to messages.pot. This header survives crowdin,\n# and will still be in the translated .po files, and will get compiled into the final .mo files.\ncnt_src_strings = 0\nwith open(f\"{build_dir}/messages.pot\", \"r\", encoding=\"utf-8\") as f:\n    for line in f.readlines():\n        if line.startswith('msgid '):\n            cnt_src_strings += 1\nwith open(f\"{build_dir}/messages_customheader.pot\", \"w\", encoding=\"utf-8\") as f:\n    f.write('''msgid \"\"\\n''')\n    f.write('''msgstr \"\"\\n''')\n    f.write(f'''\"X-Electrum-SourceStringCount: {cnt_src_strings}\"\\n''')\ncmd = [\"msgcat\", \"-u\", \"-o\", f\"{build_dir}/messages.pot\", f\"{build_dir}/messages.pot\", f\"{build_dir}/messages_customheader.pot\"]\nprint('Add custom header to template')\nsubprocess.check_output(cmd)\n\n# prepare uploading to crowdin\nos.chdir(os.path.join(project_root, \"electrum\"))\n\ncrowdin_api_key = None\nfilename = os.path.expanduser('~/.crowdin_api_key')\nif os.path.exists(filename):\n    with open(filename) as f:\n        crowdin_api_key = f.read().strip()\nif \"crowdin_api_key\" in os.environ:\n    crowdin_api_key = os.environ[\"crowdin_api_key\"]\nif not crowdin_api_key:\n    print('Missing crowdin_api_key. Cannot push.')\n    sys.exit(1)\nprint('Found crowdin_api_key. Will push updated source-strings to crowdin.')\n\ncrowdin_project_id = 20482  # for \"Electrum\" project on crowdin\nlocale_file_name = os.path.join(build_dir, \"messages.pot\")\ncrowdin_file_name = \"messages.pot\"\ncrowdin_file_id = 68  # for \"/electrum-client/messages.pot\"\nglobal_headers = {\"Authorization\": \"Bearer {}\".format(crowdin_api_key)}\n\n# client.storages.add_storage(f)\n# https://support.crowdin.com/developer/api/v2/?q=api#tag/Storage/operation/api.storages.post\nprint(f\"Uploading to temp storage...\")\nurl = f'https://api.crowdin.com/api/v2/storages'\nwith open(locale_file_name, 'rb') as f:\n    headers = {**global_headers, **{\"Crowdin-API-FileName\": crowdin_file_name}}\n    response = requests.request(\"POST\", url, data=f, headers=headers)\n    response.raise_for_status()\n    print(\"\", \"storages.add_storage:\", \"-\" * 20, response.text, \"-\" * 20, sep=\"\\n\")\n    storage_id = response.json()[\"data\"][\"id\"]\n\n# client.source_files.update_file(projectId=crowdin_project_id, storageId=storage_id, fileId=crowdin_file_id)\n# https://support.crowdin.com/developer/api/v2/?q=api#tag/Source-Files/operation/api.projects.files.put\nprint(f\"Copying from temp storage and updating file in perm storage...\")\nurl = f'https://api.crowdin.com/api/v2/projects/{crowdin_project_id}/files/{crowdin_file_id}'\nheaders = {**global_headers, **{\"content-type\": \"application/json\"}}\nresponse = requests.request(\"PUT\", url, json={\"storageId\": storage_id}, headers=headers)\nresponse.raise_for_status()\nprint(\"\", \"source_files.update_file:\", \"-\" * 20, response.text, \"-\" * 20, sep=\"\\n\")\n\n# client.translations.build_crowdin_project_translation(projectId=crowdin_project_id)\n# https://support.crowdin.com/developer/api/v2/?q=api#tag/Translations/operation/api.projects.translations.builds.post\nprint(f\"Rebuilding translations...\")\nurl = f'https://api.crowdin.com/api/v2/projects/{crowdin_project_id}/translations/builds'\nheaders = {**global_headers, **{\"content-type\": \"application/json\"}}\njson_data = {\n    #\"exportApprovedOnly\": True,  # only include translated-strings approved by users with \"Proofreader\" permission\n}  # note: these settings MUST be verified by electrum-locale/update.py again, at download-time.\nresponse = requests.request(\"POST\", url, json=json_data, headers=headers)\nresponse.raise_for_status()\nprint(\"\", \"translations.build_crowdin_project_translation:\", \"-\" * 20, response.text, \"-\" * 20, sep=\"\\n\")\n"
  },
  {
    "path": "contrib/locale/stats.py",
    "content": "#!/usr/bin/env python3\n#\n# Copyright (C) 2026 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n#\n#\n# This generates a 'stats.json' file containing some statistics about translation completeness.\n\nimport gettext\nimport glob\nimport json\nimport os\n\nPROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))\nLOCALE_DIR = os.path.join(PROJECT_ROOT, \"electrum\", \"locale\", \"locale\")\n\n\nif __name__ == '__main__':\n    catalog_size = {}  # type: dict[str, int]\n    source_string_count = None\n    # - calc stats\n    files_list = glob.glob(f\"{LOCALE_DIR}/*/LC_MESSAGES/*.mo\")\n    for fname in files_list:\n        lang_code = os.path.basename(os.path.dirname(os.path.dirname(fname)))\n        try:\n            t = gettext.translation('electrum', LOCALE_DIR, languages=[lang_code])\n        except OSError as e:\n            raise Exception(f\"cannot find or parse .mo file matching {fname!r}\") from e\n        # calc catalog size of translated strings\n        catalog_size[lang_code] = len(t._catalog)\n        # same SourceStringCount header should be present in all .mo files:\n        t_info = t.info()\n        try:\n            ss_cnt = int(t_info[\"x-electrum-sourcestringcount\"])\n        except Exception as e:\n            raise Exception(\n                f\"missing or malformed 'x-electrum-sourcestringcount' header, for {lang_code!r}.\\n\"\n                f\"found {t_info}\"\n            ) from e\n        if source_string_count is None:\n            source_string_count = ss_cnt\n        elif source_string_count != ss_cnt:\n            raise Exception(\n                f\"inconsistent 'x-electrum-sourcestringcount' headers! \"\n                f\"prev_cnt={source_string_count}, new_cnt={ss_cnt} (for lang={lang_code})\")\n    # - convert to json data. example:\n    #     {\n    #         \"source_string_count\": 9999,\n    #         \"translations\": {\n    #             \"de_DE\": {\n    #                 \"string_count\": 400,\n    #             },\n    #             ...\n    #         }\n    #     }\n    json_data = {\n        \"source_string_count\": source_string_count,\n        \"translations\": {},\n    }\n    for lang_code in catalog_size:\n        json_data[\"translations\"][lang_code] = {}\n        json_data[\"translations\"][lang_code][\"string_count\"] = catalog_size[lang_code]\n    # - write json to disk\n    with open(f\"{LOCALE_DIR}/stats.json\", \"w\", encoding=\"utf-8\") as f:\n        json_str = json.dumps(\n            json_data,\n            indent=4,\n            sort_keys=True\n        )\n        f.write(json_str)\n    print(f\"done. created file '{LOCALE_DIR}/stats.json'\")\n"
  },
  {
    "path": "contrib/make_download",
    "content": "#!/usr/bin/python3\nimport re\nimport os\nimport sys\nimport importlib\nfrom collections import defaultdict\n\n\nif len(sys.argv) < 2:\n    print(f\"ERROR. usage: {os.path.basename(__file__)} <WWW_DIR>\", file=sys.stderr)\n    sys.exit(1)\n\n# cd to project root\nos.chdir(os.path.dirname(os.path.dirname(__file__)))\n\n# load version.py; needlessly complicated alternative to \"imp.load_source\":\nversion_spec = importlib.util.spec_from_file_location('version', 'electrum/version.py')\nversion_module = importlib.util.module_from_spec(version_spec)\nversion_spec.loader.exec_module(version_module)\n\nELECTRUM_VERSION = version_module.ELECTRUM_VERSION\nprint(f\"version: {ELECTRUM_VERSION}\", file=sys.stderr)\n\ndirname = sys.argv[1]\n\nprint(f\"directory: {dirname}\", file=sys.stderr)\n\ndownload_page = os.path.join(dirname, \"panel-download.html\")\ndownload_template = download_page + \".template\"\n\nwith open(download_template) as f:\n    download_page_str = f.read()\n\nversion = version_win = version_mac = version_android = ELECTRUM_VERSION\ndownload_page_str = download_page_str.replace(\"##VERSION##\", version)\ndownload_page_str = download_page_str.replace(\"##VERSION_WIN##\", version_win)\ndownload_page_str = download_page_str.replace(\"##VERSION_MAC##\", version_mac)\ndownload_page_str = download_page_str.replace(\"##VERSION_ANDROID##\", version_android)\ndownload_page_str = download_page_str.replace(\"##VERSION_APK##\", version_android)\n\n# note: all dist files need to be listed here that we expect sigs for,\n#       even if they are not linked to from the website\nfiles = {\n    \"tgz\": f\"Electrum-{version}.tar.gz\",\n    \"tgz_srconly\": f\"Electrum-sourceonly-{version}.tar.gz\",\n    \"appimage\": f\"electrum-{version}-x86_64.AppImage\",\n    \"mac\": f\"electrum-{version_mac}.dmg\",\n    \"win\": f\"electrum-{version_win}.exe\",\n    \"win_setup\": f\"electrum-{version_win}-setup.exe\",\n    \"win_portable\": f\"electrum-{version_win}-portable.exe\",\n    \"apk_arm64\": f\"Electrum-{version_android}-arm64-v8a-release.apk\",\n    \"apk_armeabi\": f\"Electrum-{version_android}-armeabi-v7a-release.apk\",\n    \"apk_x86_64\": f\"Electrum-{version_android}-x86_64-release.apk\",\n}\n\n# default signers\nsigners = ['ThomasV', 'sombernight_releasekey']\n\n# detect extra signers\nlist_dir = sorted(os.listdir('dist'))\ndetected_sigs = defaultdict(set)\nfor f in list_dir:\n    if f.endswith('.asc'):\n        parts = f.split('.')\n        signer = parts[-2]\n        filename = '.'.join(parts[0:-2])\n        detected_sigs[signer].add(filename)\nfor k, v in detected_sigs.items():\n    if v == set(files.values()):\n        if k not in signers:\n            signers.append(k)\n\nprint(f\"signers: {signers}\", file=sys.stderr)\n\nfriendly_nick = lambda x: 'SomberNight' if x=='sombernight_releasekey' else x\nsigners_list = ', '.join(\"<a href=\\\"https://raw.githubusercontent.com/spesmilo/electrum/master/pubkeys/%s.asc\\\">%s</a>\"%(x, friendly_nick(x)) for x in signers)\ndownload_page_str = download_page_str.replace(\"##signers_list##\", signers_list)\n\nfor k, filename in files.items():\n    path = \"dist/%s\"%filename\n    assert filename in list_dir\n    link = \"https://download.electrum.org/%s/%s\"%(version, filename)\n    download_page_str = download_page_str.replace(\"##link_%s##\" % k, link)\n    download_page_str = download_page_str.replace(\"##sigs_%s##\" % k, link + '.asc')\n\n\n# download page has been constructed from template; now insert it into index.html\nindex_html_path = os.path.join(dirname, \"index.html\")\nwith open(f\"{index_html_path}.template\") as f:\n    index_html_str = f.read()\n\nindex_html_str = index_html_str.replace(\"##DOWNLOAD_PAGE##\", download_page_str)\n\nwith open(index_html_path, 'w') as f:\n    f.write(index_html_str)\n"
  },
  {
    "path": "contrib/make_libsecp256k1.sh",
    "content": "#!/bin/bash\n\n# This script was tested on Linux and MacOS hosts, where it can be used\n# to build native libsecp256k1 binaries.\n#\n# It can also be used to cross-compile to Windows:\n# $ sudo apt-get install mingw-w64\n# For a Windows x86 (32-bit) target, run:\n# $ GCC_TRIPLET_HOST=\"i686-w64-mingw32\" ./contrib/make_libsecp256k1.sh\n# Or for a Windows x86_64 (64-bit) target, run:\n# $ GCC_TRIPLET_HOST=\"x86_64-w64-mingw32\" ./contrib/make_libsecp256k1.sh\n#\n# To cross-compile to Linux x86:\n# sudo apt-get install gcc-multilib g++-multilib\n# $ AUTOCONF_FLAGS=\"--host=i686-linux-gnu CFLAGS=-m32 CXXFLAGS=-m32 LDFLAGS=-m32\" ./contrib/make_libsecp256k1.sh\n\nLIBSECP_VERSION=\"1a53f4961f337b4d166c25fce72ef0dc88806618\"\n# ^ tag \"v0.7.1\"\n# note: this version is duplicated in contrib/android/p4a_recipes/libsecp256k1/__init__.py\n#       (and also in electrum-ecc, for the \"secp256k1\" git submodule)\n\nset -e\n\n. \"$(dirname \"$0\")/build_tools_util.sh\" || (echo \"Could not source build_tools_util.sh\" && exit 1)\n\nhere=\"$(dirname \"$(realpath \"$0\" 2> /dev/null || grealpath \"$0\")\")\"\nCONTRIB=\"$here\"\nPROJECT_ROOT=\"$CONTRIB/..\"\n\npkgname=\"secp256k1\"\ninfo \"Building $pkgname...\"\n\n(\n    cd \"$CONTRIB\"\n    if [ ! -d secp256k1 ]; then\n        git clone https://github.com/bitcoin-core/secp256k1.git\n    fi\n    cd secp256k1\n    if ! $(git cat-file -e ${LIBSECP_VERSION}) ; then\n        info \"Could not find requested version $LIBSECP_VERSION in local clone; fetching...\"\n        git fetch --all\n    fi\n    git reset --hard\n    git clean -dfxq\n    git checkout \"${LIBSECP_VERSION}^{commit}\"\n\n    if ! [ -x configure ] ; then\n        echo \"LDFLAGS = -no-undefined\" >> Makefile.am\n        ./autogen.sh || fail \"Could not run autogen for $pkgname. Please make sure you have automake and libtool installed, and try again.\"\n    fi\n    if ! [ -r config.status ] ; then\n        ./configure \\\n            $AUTOCONF_FLAGS \\\n            --prefix=\"$here/$pkgname/dist\" \\\n            --enable-module-recovery \\\n            --enable-module-extrakeys \\\n            --enable-module-schnorrsig \\\n            --enable-experimental \\\n            --enable-module-ecdh \\\n            --disable-benchmark \\\n            --disable-tests \\\n            --disable-exhaustive-tests \\\n            --disable-static \\\n            --enable-shared || fail \"Could not configure $pkgname. Please make sure you have a C compiler installed and try again.\"\n    fi\n    make \"-j$CPU_COUNT\" || fail \"Could not build $pkgname\"\n    make install || fail \"Could not install $pkgname\"\n    . \"$here/$pkgname/dist/lib/libsecp256k1.la\"\n    host_strip \"$here/$pkgname/dist/lib/$dlname\"\n    if [ -n \"$DLL_TARGET_DIR\" ] ; then\n        cp -fpv \"$here/$pkgname/dist/lib/$dlname\" \"$DLL_TARGET_DIR/\" || fail \"Could not copy the $pkgname binary to DLL_TARGET_DIR\"\n    else\n        cp -fpv \"$here/$pkgname/dist/lib/$dlname\" \"$PROJECT_ROOT/electrum\" || fail \"Could not copy the $pkgname binary to its destination\"\n        info \"$dlname has been placed in the 'electrum' folder.\"\n    fi\n)\n"
  },
  {
    "path": "contrib/make_libusb.sh",
    "content": "#!/bin/bash\n\nLIBUSB_VERSION=\"d52e355daa09f17ce64819122cb067b8a2ee0d4b\"\n# ^ tag v1.0.27\n\nset -e\n\n. \"$(dirname \"$0\")/build_tools_util.sh\" || (echo \"Could not source build_tools_util.sh\" && exit 1)\n\nhere=\"$(dirname \"$(realpath \"$0\" 2> /dev/null || grealpath \"$0\")\")\"\nCONTRIB=\"$here\"\nPROJECT_ROOT=\"$CONTRIB/..\"\n\npkgname=\"libusb\"\ninfo \"Building $pkgname...\"\n\n(\n    cd \"$CONTRIB\"\n    if [ ! -d libusb ]; then\n        git clone https://github.com/libusb/libusb.git\n    fi\n    cd libusb\n    if ! $(git cat-file -e ${LIBUSB_VERSION}) ; then\n        info \"Could not find requested version $LIBUSB_VERSION in local clone; fetching...\"\n        git fetch --all\n    fi\n    git reset --hard\n    git clean -dfxq\n    git checkout \"${LIBUSB_VERSION}^{commit}\"\n\n    if [ \"$BUILD_TYPE\" = \"wine\" ] ; then\n        echo \"libusb_1_0_la_LDFLAGS += -Wc,-static\" >> libusb/Makefile.am\n    fi\n    ./bootstrap.sh || fail \"Could not bootstrap libusb\"\n    if ! [ -r config.status ] ; then\n        if [ \"$BUILD_TYPE\" = \"wine\" ] ; then\n            # windows target\n            LDFLAGS=\"-Wl,--no-insert-timestamp\"\n        elif [ $(uname) == \"Darwin\" ]; then\n            # macos target\n            LDFLAGS=\"-Wl -lm\"\n        else\n            # linux target\n            LDFLAGS=\"\"\n        fi\n        LDFLAGS=\"$LDFLAGS\" ./configure \\\n            $AUTOCONF_FLAGS \\\n            || fail \"Could not configure $pkgname. Please make sure you have a C compiler installed and try again.\"\n    fi\n    make \"-j$CPU_COUNT\" || fail \"Could not build $pkgname\"\n    make install || warn \"Could not install $pkgname\"\n    . \"$here/$pkgname/libusb/.libs/libusb-1.0.la\"\n    host_strip \"$here/$pkgname/libusb/.libs/$dlname\"\n    TARGET_NAME=\"$dlname\"\n    if [ $(uname) == \"Darwin\" ]; then  # on mac, dlname is \"libusb-1.0.0.dylib\"\n        TARGET_NAME=\"libusb-1.0.dylib\"\n    fi\n    cp -fpv \"$here/$pkgname/libusb/.libs/$dlname\" \"$PROJECT_ROOT/electrum/$TARGET_NAME\" || fail \"Could not copy the $pkgname binary to its destination\"\n    info \"$TARGET_NAME has been placed in the inner 'electrum' folder.\"\n    if [ -n \"$DLL_TARGET_DIR\" ] ; then\n        cp -fpv \"$here/$pkgname/libusb/.libs/$dlname\" \"$DLL_TARGET_DIR/$TARGET_NAME\" || fail \"Could not copy the $pkgname binary to DLL_TARGET_DIR\"\n    fi\n)\n"
  },
  {
    "path": "contrib/make_packages.sh",
    "content": "#!/bin/bash\n# This script installs our pure python dependencies into the 'packages' folder.\n\nset -e\n\nCONTRIB=\"$(dirname \"$(readlink -e \"$0\")\")\"\nPROJECT_ROOT=\"$CONTRIB\"/..\nPACKAGES=\"$PROJECT_ROOT\"/packages/\n\ntest -n \"$CONTRIB\" -a -d \"$CONTRIB\" || exit\ncd \"$CONTRIB\"\n\nif [ -d \"$PACKAGES\" ]; then\n    rm -r \"$PACKAGES\"\nfi\n\n# create virtualenv\n# note: venv path needs to be deterministic as some produced files will contain it\nvenv_dir=\"$CONTRIB/.venv_make_packages/\"\nrm -rf \"$venv_dir\"\npython3 -m venv \"$venv_dir\"\nsource \"$venv_dir\"/bin/activate\n\n# installing pinned build-time requirements, such as pip/wheel/setuptools\npython3 -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \\\n    -r \"$CONTRIB\"/deterministic-build/requirements-build-base.txt\n\n# opt out of compiling C extensions\nexport AIOHTTP_NO_EXTENSIONS=1\nexport YARL_NO_EXTENSIONS=1\nexport MULTIDICT_NO_EXTENSIONS=1\nexport FROZENLIST_NO_EXTENSIONS=1\nexport PROPCACHE_NO_EXTENSIONS=1\n\nexport ELECTRUM_ECC_DONT_COMPILE=1\n\n# see https://github.com/python-websockets/websockets/blob/e6d0ea1d6b13a979924329d02fb82f79d82c7236/setup.py#L22\nexport BUILD_EXTENSION=\"no\"\n\n\n# if we end up having to compile something, at least give reproducibility a fighting chance\nexport LC_ALL=C\nexport TZ=UTC\nexport SOURCE_DATE_EPOCH=\"$(git log -1 --pretty=%ct 2>/dev/null || printf 1530212462)\"\nexport PYTHONHASHSEED=\"$SOURCE_DATE_EPOCH\"\nexport BUILD_DATE=\"$(LC_ALL=C TZ=UTC date +'%b %e %Y' -d @$SOURCE_DATE_EPOCH)\"\nexport BUILD_TIME=\"$(LC_ALL=C TZ=UTC date +'%H:%M:%S' -d @$SOURCE_DATE_EPOCH)\"\n\n# FIXME aiohttp will compile some .so files using distutils\n#       (until https://github.com/aio-libs/aiohttp/pull/4079 gets released),\n#       which are not reproducible unless using at least python 3.9\n#       (as it needs https://github.com/python/cpython/commit/0d30ae1a03102de07758650af9243fd31211325a).\n#       Hence \"aiohttp-*.dist-info/\" is not reproducible either.\n#       All this means that downstream users of this script, such as the sdist build\n#       and the android apk build need to make sure these files get excluded.\n# note: --no-build-isolation is needed so that pip uses the locally available setuptools and wheel,\n#       instead of downloading the latest ones\npython3 -m pip install --no-build-isolation --no-compile --no-dependencies --no-binary :all: \\\n    -r \"$CONTRIB\"/deterministic-build/requirements.txt -t \"$PACKAGES\"\n\necho \"Pure-python dependencies have been placed into $PACKAGES\"\n"
  },
  {
    "path": "contrib/make_plugin",
    "content": "#!/usr/bin/python3\nimport os\nimport sys\nimport hashlib\nimport json\nimport zipfile\nimport zipimport\n\n# todo: use version number\n\nif len(sys.argv) != 2:\n    print(f\"usage: {os.path.basename(__file__)} <plugin_directory>\", file=sys.stderr)\n    sys.exit(1)\n\n\nsource_dir = sys.argv[1]  # where the plugin source code is\nif source_dir.endswith('/'):\n    source_dir = source_dir[:-1]\n\nplugin_name = os.path.basename(source_dir)\ndest_dir = os.getcwd()\nzip_path = os.path.join(dest_dir, plugin_name + '.zip')\n\n# remove old zipfile\nif os.path.exists(zip_path):\n    os.unlink(zip_path)\n# create zipfile\nprint('creating', zip_path)\nwith zipfile.ZipFile(zip_path, 'w') as zip_object:\n    for folder_name, sub_folders, file_names in os.walk(source_dir):\n        for filename in file_names:\n            file_path = os.path.join(folder_name, filename)\n            dest_path = os.path.join(plugin_name, os.path.relpath(folder_name, source_dir), os.path.basename(file_path))\n            zip_object.write(file_path, dest_path)\n            print('added', dest_path)\n\n# read version\ntry:\n    with open(os.path.join(source_dir, 'manifest.json'), 'r') as f:\n        manifest = json.load(f)\n        version = manifest.get('version')\nexcept FileNotFoundError:\n    raise Exception(f\"plugin doesn't contain manifest.json\")\n\nif version:\n    versioned_plugin_name = plugin_name + '-' + version + '.zip'\n    zip_path_with_version = os.path.join(dest_dir, versioned_plugin_name)\n    # rename zip file\n    os.rename(zip_path, zip_path_with_version)\n    print(f'Created {zip_path_with_version}')\nelse:\n    print(f'Created {zip_path}')\n"
  },
  {
    "path": "contrib/make_zbar.sh",
    "content": "#!/bin/bash\n\n# This script can be used on Linux hosts to build native libzbar binaries.\n# sudo apt-get install pkg-config libx11-dev libx11-6 libv4l-dev libxv-dev libxext-dev libjpeg-dev\n#\n# It can also be used to cross-compile to Windows:\n# $ sudo apt-get install mingw-w64 mingw-w64-tools win-iconv-mingw-w64-dev\n# For a Windows x86 (32-bit) target, run:\n# $ GCC_TRIPLET_HOST=\"i686-w64-mingw32\" BUILD_TYPE=\"wine\" ./contrib/make_zbar.sh\n# Or for a Windows x86_64 (64-bit) target, run:\n# $ GCC_TRIPLET_HOST=\"x86_64-w64-mingw32\" BUILD_TYPE=\"wine\" ./contrib/make_zbar.sh\n\nZBAR_VERSION=\"bb05ec54eec57f8397cb13fb9161372a281a1219\"\n# ^ tag 0.23.93\n\nset -e\n\n. \"$(dirname \"$0\")/build_tools_util.sh\" || (echo \"Could not source build_tools_util.sh\" && exit 1)\n\nhere=\"$(dirname \"$(realpath \"$0\" 2> /dev/null || grealpath \"$0\")\")\"\nCONTRIB=\"$here\"\nPROJECT_ROOT=\"$CONTRIB/..\"\n\npkgname=\"zbar\"\ninfo \"Building $pkgname...\"\n\n(\n    cd \"$CONTRIB\"\n    if [ ! -d zbar ]; then\n        git clone https://github.com/mchehab/zbar.git\n    fi\n    cd zbar\n    if ! $(git cat-file -e ${ZBAR_VERSION}) ; then\n        info \"Could not find requested version $ZBAR_VERSION in local clone; fetching...\"\n        git fetch --all\n    fi\n    git reset --hard\n    git clean -dfxq\n    git checkout \"${ZBAR_VERSION}^{commit}\"\n\n    if [ \"$BUILD_TYPE\" = \"wine\" ] ; then\n        echo \"libzbar_la_LDFLAGS += -Wc,-static\" >> zbar/Makefile.am\n        echo \"LDFLAGS += -Wc,-static\" >> Makefile.am\n    fi\n    if ! [ -x configure ] ; then\n        autoreconf -vfi || fail \"Could not run autoreconf for $pkgname. Please make sure you have automake and libtool installed, and try again.\"\n    fi\n    if ! [ -r config.status ] ; then\n        if [ \"$BUILD_TYPE\" = \"wine\" ] ; then\n            # windows target\n            AUTOCONF_FLAGS=\"$AUTOCONF_FLAGS \\\n                --with-x=no \\\n                --enable-video=yes \\\n                --with-jpeg=no \\\n                --with-directshow=yes \\\n                --disable-dependency-tracking\"\n        elif [ $(uname) == \"Darwin\" ]; then\n            # macos target\n            AUTOCONF_FLAGS=\"$AUTOCONF_FLAGS \\\n                --with-x=no \\\n                --enable-video=no \\\n                --with-jpeg=no\"\n        else\n            # linux target\n            AUTOCONF_FLAGS=\"$AUTOCONF_FLAGS \\\n                --with-x=yes \\\n                --enable-video=yes \\\n                --with-jpeg=yes\"\n        fi\n        ./configure \\\n            $AUTOCONF_FLAGS \\\n            --prefix=\"$here/$pkgname/dist\" \\\n            --enable-pthread=no \\\n            --enable-doc=no \\\n            --with-python=no \\\n            --with-gtk=no \\\n            --with-qt=no \\\n            --with-java=no \\\n            --with-imagemagick=no \\\n            --with-dbus=no \\\n            --enable-codes=qrcode \\\n            --disable-static \\\n            --enable-shared || fail \"Could not configure $pkgname. Please make sure you have a C compiler installed and try again.\"\n    fi\n    make \"-j$CPU_COUNT\" || fail \"Could not build $pkgname\"\n    make install || fail \"Could not install $pkgname\"\n    . \"$here/$pkgname/dist/lib/libzbar.la\"\n    host_strip \"$here/$pkgname/dist/lib/$dlname\"\n    cp -fpv \"$here/$pkgname/dist/lib/$dlname\" \"$PROJECT_ROOT/electrum\" || fail \"Could not copy the $pkgname binary to its destination\"\n    info \"$dlname has been placed in the inner 'electrum' folder.\"\n    if [ -n \"$DLL_TARGET_DIR\" ] ; then\n        cp -fpv \"$here/$pkgname/dist/lib/$dlname\" \"$DLL_TARGET_DIR/\" || fail \"Could not copy the $pkgname binary to DLL_TARGET_DIR\"\n    fi\n)\n"
  },
  {
    "path": "contrib/osx/README.md",
    "content": "Building macOS binaries\n=======================\n\n✓ _This binary should be reproducible, meaning you should be able to generate\n   binaries that match the official releases._\n\n- _Minimum supported target system (i.e. what end-users need): macOS 11_\n\nThis guide explains how to build Electrum binaries for macOS systems.\n\n\n## Building the binary\n\nThis needs to be done on a system running macOS or OS X.\n\nThe script is only tested on Intel-based (x86_64) Macs, and the binary built\ntargets `x86_64` currently.\n\nNotes about compatibility with different macOS versions:\n- In general the binary is not guaranteed to run on an older version of macOS\n  than what the build machine has. This is due to bundling the compiled Python into\n  the [PyInstaller binary](https://github.com/pyinstaller/pyinstaller/issues/1191).\n- The [bundled version of Qt](https://github.com/spesmilo/electrum/issues/3685) also\n  imposes a minimum supported macOS version.\n- If you want to build binaries that conform to the macOS \"Gatekeeper\", so as to\n  minimise the warnings users get, the binaries need to be codesigned with a\n  certificate issued by Apple, and starting with macOS 10.15 (targets) the binaries also\n  need to be notarized by Apple's central server. To be able to build\n  binaries that Apple will notarize (due to the requirements on the binaries themselves,\n  e.g. hardened runtime) the build machine needs at least macOS 10.14.\n  See [#6128](https://github.com/spesmilo/electrum/issues/6128).\n  - There are two tools that can be used to notarize a binary, both part of Xcode:\n    the old `altool` and the newer `notarytool`. `altool`\n    [was deprecated](https://developer.apple.com/news/?id=y5mjxqmn) by Apple.\n    `notarytool` requires Xcode 13+, and that in turn requires macOS 11.3+.\n\nWe currently build the release binaries on macOS 11.7.10, and these seem to run on\n11 or newer.\n\n\n#### Notes about reproducibility\n\n- We recommend creating a VM with a macOS guest, e.g. using VirtualBox,\n  and building there.\n- The guest should run macOS 11.7.10 (that specific version).\n- The unix username should be `vagrant`, and `electrum` should be cloned directly\n  to the user's home dir: `/Users/vagrant/electrum`.\n- Builders need to use the same version of Xcode; and note that\n  full Xcode and Xcode commandline tools differ!\n  We use the Xcode CLI tools as installed by brew. (version 13.2)\n\n  Sanity checks:\n    ```\n    $ sw_vers\n    ProductName:\tmacOS\n    ProductVersion:\t11.7.10\n    BuildVersion:\t20G1427\n    $ xcode-select -p\n    /Library/Developer/CommandLineTools\n    $ xcrun --show-sdk-path\n    /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk\n    $ pkgutil --pkg-info=com.apple.pkg.CLTools_Executables\n    package-id: com.apple.pkg.CLTools_Executables\n    version: 13.2.0.0.1.1638488800\n    volume: /\n    location: /\n    install-time: XXXXXXXXXX\n    groups: com.apple.FindSystemFiles.pkg-group\n    $ gcc --version\n    Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/4.2.1\n    Apple clang version 13.0.0 (clang-1300.0.29.30)\n    Target: x86_64-apple-darwin20.6.0\n    Thread model: posix\n    InstalledDir: /Library/Developer/CommandLineTools/usr/bin\n    ```\n- Installing extraneous brew packages can result in build differences.\n  For example, pyinstaller seems to pick up and bundle brew-installed `libffi`.\n  So having a dedicated \"electrum binary builder macOS VM\" is recommended.\n- Make sure that you are building from a fresh clone of electrum\n  (or run e.g. `git clean -ffxd` to rm all local changes).\n\n\n#### 1. Install brew\n\nInstall [`brew`](https://brew.sh/).\n\nLet brew install the Xcode CLI tools.\n\n\n#### 2. Build Electrum\n\n    cd electrum\n    ./contrib/osx/make_osx.sh\n\nThis creates both a folder named Electrum.app and the .dmg file (both unsigned).\n\n##### 2.1. For release binaries, here be dragons\n\nIf you want the binaries codesigned for macOS and notarised by Apple's central server,\nalso run the `sign_osx.sh` script:\n\n    CODESIGN_CERT=\"Developer ID Application: Electrum Technologies GmbH (L6P37P7P56)\" \\\n    APPLE_TEAM_ID=\"L6P37P7P56\" \\\n    APPLE_ID_USER=\"me@email.com\" \\\n    APPLE_ID_PASSWORD=\"1234\" \\\n    ./contrib/osx/sign_osx.sh\n\n(note: `APPLE_ID_PASSWORD` is an app-specific password, *not* the account password)\n\n\n## Verifying reproducibility and comparing against official binary\n\nEvery user can verify that the official binary was created from the source code in this\nrepository.\n\n1. Build your own binary as described above.\n2. Use the provided `compare_dmg` script to compare the binary you built with\n   the official release binary.\n    ```\n    $ ./contrib/osx/compare_dmg dist/electrum-*.dmg electrum_dmg_official_release.dmg\n    ```\n   The `compare_dmg` script is mostly only needed as the official release binary is\n   codesigned and notarized. Otherwise, the built `.app` bundles should be byte-identical.\n   (Note that we are using `hdutil` to create the `.dmg`, and its output is not\n   deterministic, but we cannot compare the `.dmg` files directly anyway as they contain\n   codesigned files)\n\n\n## FAQ\n\n### What is macOS \"codesigning\" and \"notarization\"?\n\nCodesigning is the macOS OS-native signing of executables/shared-libs,\nthat needs to be done using an ~x509-like certificate that chains back to Apple's root CA.\nOnce a developer certificate is obtained from Apple, it can be used to codesign locally\non a dev machine.\n\nNotarization is a further step usually done after, which entails uploading a distributable\nover the network to the Apple mothership central server, which runs some arbitrary checks on it,\nand if it finds the file ok, the central server gives the dev a notarization staple.\nThis staple can then be optionally \"attached\" to the distributable, mutating it, which we do.\n(If the staple is not attached, enduser machines request it from the mothership at runtime.)\n\nBoth these steps should be done during the build process.\n\n### What is \"codesigned\" and/or \"notarized\", re the official release?\n\n- `make_osx.sh` builds a `.app`, which is unsigned/unnotarized\n  - at this point, this `.app` is ~\"byte-for-byte\" reproducible\n    - this is the sanity-check hash printed at the end of `make_osx.sh`\n  - `make_osx.sh` creates a `.dmg` from the `.app`\n    - this `.dmg` is not used for the official release at all, but used as the basis of\n      testing reproducibility using the `compare_dmg` script\n- `sign_osx.sh` codesigns the `.app` (mutating it)\n- `sign_osx.sh` -> `notarize_app.sh` notarizes the `.app` (mutating it)\n- `sign_osx.sh` creates a `.dmg` from the `.app`\n- `sign_osx.sh` codesigns the `.dmg` (mutating it)\n  - this `.dmg` becomes the official release distributable\n\nThat is, the official release `.dmg` is codesigned but NOT notarized.\nIt contains a `.app`, which is codesigned AND notarized.\n\n### How to check if a file is codesigned?\n\nBoth the `.dmg` and the contained `.app` are codesigned:\n```\n$ codesign --verify --deep --strict --verbose=2 $HOME/Desktop/electrum-4.5.8.dmg && echo \"signed\"\n/Users/vagrant/Desktop/electrum-4.5.8.dmg: valid on disk\n/Users/vagrant/Desktop/electrum-4.5.8.dmg: satisfies its Designated Requirement\nsigned\n```\n```\n$ codesign --verify --deep --strict --verbose=1 $HOME/Desktop/Electrum-4.5.8.app && echo \"signed\"\n/Users/vagrant/Desktop/Electrum-4.5.8.app: valid on disk\n/Users/vagrant/Desktop/Electrum-4.5.8.app: satisfies its Designated Requirement\nsigned\n```\n\nAlso see `$ codesign -dvvv $HOME/Desktop/electrum-4.5.8.dmg`\n\n### How to check if a file is notarized?\n\nThe outer `.dmg` is NOT notarized, but the inner `.app` is notarized:\n```\n$ spctl -a -vvv -t install $HOME/Desktop/electrum-4.5.8.dmg\n/Users/vagrant/Desktop/electrum-4.5.8.dmg: rejected\nsource=Unnotarized Developer ID\norigin=Developer ID Application: Electrum Technologies GmbH (L6P37P7P56)\n```\n```\n$ spctl -a -vvv -t install $HOME/Desktop/Electrum-4.5.8.app\n/Users/vagrant/Desktop/Electrum-4.5.8.app: accepted\nsource=Notarized Developer ID\norigin=Developer ID Application: Electrum Technologies GmbH (L6P37P7P56)\n```\n\n### How to simulate the signing procedure?\n\nIt is possible to run `sign_osx.sh` using a self-signed certificate to test the\nsigning procedure without using a production certificate.\n\nNote that the notarization process will be skipped as it is not possible to notarize\nan executable with Apple using a self-signed certificate.\n\n#### To generate a self-signed certificate, inside your **MacOS VM**:\n1. Open the `Keychain Access` application.\n2. In the menubar go to `Keychain Access` > `Certificate Assistant` > `Create a Certificate...`\n3. Set a name (e.g. `signing_dummy`)\n4. Change `Certificate Type` to *'Code Signing'*\n5. Click `Create` and `Continue`.\n\nYou now have a self-signed certificate `signing_dummy` added to your `login` keychain.\n\n#### To sign the executables with the self-signed certificate:\n\nAssuming you have the two unsigned outputs of `make_osx.sh` inside `~/electrum/dist`\n(e.g. `Electrum.app` and `electrum-4.5.4-1368-gc8db684cc-unsigned.dmg`).\n\nIn `~/electrum` run:\n\n`$ CODESIGN_CERT=\"signing_dummy\" ./contrib/osx/sign_osx.sh`\n\nAfter `sign_osx.sh` finished, you will have a new `*.dmg` inside `electrum/dist`\n(without the `-unsigned` postfix) which is signed with your certificate.\n\n#### To compare the unsigned executable with the self-signed executable:\n\nRunning `compare_dmg` with `IS_NOTARIZED=false` should succeed:\n\n`$ IS_NOTARIZED=false ./electrum/contrib/osx/compare_dmg <unsigned executable> <self-signed executable>`"
  },
  {
    "path": "contrib/osx/README_macos.md",
    "content": "# Running Electrum from source on macOS (development version)\n\n## Prerequisites\n\n- [brew](https://brew.sh/)\n- python3\n- git\n\n## Main steps\n\n### 1. Check out the code from GitHub:\n```\n$ git clone https://github.com/spesmilo/electrum.git\n$ cd electrum\n$ git submodule update --init\n```\n\n### 2. Prepare for compiling libsecp256k1\n\nTo be able to build the `electrum-ecc` package from source\n(which is pulled in when installing Electrum in the next step),\nyou need:\n```\n$ brew install autoconf automake libtool coreutils\n```\n\n### 3. Install Electrum\n\nRun install (this should install the dependencies):\n```\n$ python3 -m pip install --user -e \".[gui,crypto]\"\n```\n\n### 4. Run electrum:\n```\n$ ./run_electrum\n```\n"
  },
  {
    "path": "contrib/osx/apply_sigs.sh",
    "content": "#!/bin/sh\n# Copyright (c) 2014-2019 The Bitcoin Core developers\n# Distributed under the MIT software license, see the accompanying\n# file COPYING or http://www.opensource.org/licenses/mit-license.php.\n#\n# This script is based on https://github.com/bitcoin/bitcoin/blob/194b9b8792d9b0798fdb570b79fa51f1d1f5ebaf/contrib/macdeploy/detached-sig-apply.sh\n\nexport LC_ALL=C\nset -e\n\nif [ $(uname) != \"Darwin\" ]; then\n    echo \"This script needs to be run on macOS.\"\n    exit 1\nfi\n\nCP=gcp\n\nUNSIGNED=\"$1\"\nSIGNATURE=\"$2\"\nARCH=x86_64\nOUTDIR=\"/tmp/electrum_compare_dmg/signed_app\"\n\nif [ -z \"$UNSIGNED\" ]; then\n    echo \"usage: $0 <unsigned app> <path to mac_extracted_sigs.tar.gz>\"\n    exit 1\nfi\n\nif [ -z \"$SIGNATURE\" ]; then\n    echo \"usage: $0 <unsigned app> <path to mac_extracted_sigs.tar.gz>\"\n    exit 1\nfi\n\nrm -rf ${OUTDIR} && mkdir -p ${OUTDIR}\n${CP} -rf ${UNSIGNED} ${OUTDIR}\ntar xf \"${SIGNATURE}\" -C ${OUTDIR}\n\nfind ${OUTDIR} -name \"*.sign\" | while read i; do\n    SIZE=$(gstat -c %s \"${i}\")\n    TARGET_FILE=\"$(echo \"${i}\" | sed 's/\\.sign$//')\"\n\n    if [ -z ${QUIET} ]; then\n        echo \"Allocating space for the signature of size ${SIZE} in ${TARGET_FILE}\"\n    fi\n    codesign_allocate -i \"${TARGET_FILE}\" -a ${ARCH} ${SIZE} -o \"${i}.tmp\"\n\n    OFFSET=$(pagestuff \"${i}.tmp\" -p | tail -2 | grep offset | sed 's/[^0-9]*//g')\n    if [ -z ${QUIET} ]; then\n        echo \"Attaching signature at offset ${OFFSET}\"\n    fi\n\n    dd if=\"$i\" of=\"${i}.tmp\" bs=1 seek=${OFFSET} count=${SIZE} 2>/dev/null\n    mv \"${i}.tmp\" \"${TARGET_FILE}\"\n    rm \"${i}\"\n    if [ -z ${QUIET} ]; then\n        echo \"Success.\"\n    fi\ndone\necho \"Done. .app with sigs applied is at: ${OUTDIR}\"\n"
  },
  {
    "path": "contrib/osx/cdrkit-deterministic.patch",
    "content": "--- cdrkit-1.1.11.old/genisoimage/tree.c\t2008-10-21 19:57:47.000000000 -0400\n+++ cdrkit-1.1.11/genisoimage/tree.c\t2013-12-06 00:23:18.489622668 -0500\n@@ -1139,8 +1139,9 @@\n scan_directory_tree(struct directory *this_dir, char *path,\n \t\t\t\t\t\t  struct directory_entry *de)\n {\n-\tDIR\t\t*current_dir;\n+        int             current_file;\n \tchar\t\twhole_path[PATH_MAX];\n+        struct dirent  **d_list;\n \tstruct dirent\t*d_entry;\n \tstruct directory *parent;\n \tint\t\tdflag;\n@@ -1164,7 +1165,8 @@\n \tthis_dir->dir_flags |= DIR_WAS_SCANNED;\n\n \terrno = 0;\t/* Paranoia */\n-\tcurrent_dir = opendir(path);\n+\t//current_dir = opendir(path);\n+        current_file = scandir(path, &d_list, NULL, alphasort);\n \td_entry = NULL;\n\n \t/*\n@@ -1173,12 +1175,12 @@\n \t */\n \told_path = path;\n\n-\tif (current_dir) {\n+\tif (current_file >= 0) {\n \t\terrno = 0;\n-\t\td_entry = readdir(current_dir);\n+\t\td_entry = d_list[0];\n \t}\n\n-\tif (!current_dir || !d_entry) {\n+\tif (current_file < 0 || !d_entry) {\n \t\tint\tret = 1;\n\n #ifdef\tUSE_LIBSCHILY\n@@ -1191,8 +1193,8 @@\n \t\t\tde->isorec.flags[0] &= ~ISO_DIRECTORY;\n \t\t\tret = 0;\n \t\t}\n-\t\tif (current_dir)\n-\t\t\tclosedir(current_dir);\n+\t\tif(d_list)\n+\t\t\tfree(d_list);\n \t\treturn (ret);\n \t}\n #ifdef\tABORT_DEEP_ISO_ONLY\n@@ -1208,7 +1210,7 @@\n \t\t\terrmsgno(EX_BAD, \"use Rock Ridge extensions via -R or -r,\\n\");\n \t\t\terrmsgno(EX_BAD, \"or allow deep ISO9660 directory nesting via -D.\\n\");\n \t\t}\n-\t\tclosedir(current_dir);\n+\t\tfree(d_list);\n \t\treturn (1);\n \t}\n #endif\n@@ -1250,13 +1252,13 @@\n \t\t * The first time through, skip this, since we already asked\n \t\t * for the first entry when we opened the directory.\n \t\t */\n-\t\tif (dflag)\n-\t\t\td_entry = readdir(current_dir);\n+\t\tif (dflag && current_file >= 0)\n+\t\t\td_entry = d_list[current_file];\n \t\tdflag++;\n\n-\t\tif (!d_entry)\n+\t\tif (current_file < 0)\n \t\t\tbreak;\n-\n+                current_file--;\n \t\t/* OK, got a valid entry */\n\n \t\t/* If we do not want all files, then pitch the backups. */\n@@ -1348,7 +1350,7 @@\n \t\tinsert_file_entry(this_dir, whole_path, d_entry->d_name);\n #endif\t/* APPLE_HYB */\n \t}\n-\tclosedir(current_dir);\n+\tfree(d_list);\n\n #ifdef APPLE_HYB\n \t/*"
  },
  {
    "path": "contrib/osx/compare_dmg",
    "content": "#!/usr/bin/env bash\nset -e\n\nif [ $(uname) != \"Darwin\" ]; then\n    echo \"This script needs to be run on macOS.\"\n    exit 1\nfi\n\nUNSIGNED_DMG=\"$1\"\nRELEASE_DMG=\"$2\"\nCONTRIB_OSX=\"$(dirname \"$(grealpath \"$0\")\")\"\nPROJECT_ROOT=\"$CONTRIB_OSX/../..\"\nWORKSPACE=\"/tmp/electrum_compare_dmg\"\nWS_VOL1=\"$WORKSPACE/vol1\"\nWS_VOL2=\"$WORKSPACE/vol2\"\n\nif [ -z \"$UNSIGNED_DMG\" ]; then\n    echo \"usage: $0 <unsigned dmg> <release dmg>\"\n    exit 1\nfi\n\nif [ -z \"$RELEASE_DMG\" ]; then\n    echo \"usage: $0 <unsigned dmg> <release dmg>\"\n    exit 1\nfi\n\nUNSIGNED_DMG=$(grealpath \"$UNSIGNED_DMG\")\nRELEASE_DMG=$(grealpath \"$RELEASE_DMG\")\n\ncd \"$PROJECT_ROOT\"\nrm -rf \"$WORKSPACE\"\nmkdir -p \"$WORKSPACE\" \"$WS_VOL1\" \"$WS_VOL2\"\n\nDMG_UNSIGNED_UNPACKED=\"$WORKSPACE/dmg1\"\nDMG_RELEASE_UNPACKED=\"$WORKSPACE/dmg2\"\n\nhdiutil attach -mountroot \"$WS_VOL1\" \"$UNSIGNED_DMG\"\ncp -r \"$WS_VOL1\"/Electrum \"$DMG_UNSIGNED_UNPACKED\"\nhdiutil detach \"$WS_VOL1\"/Electrum\n\nhdiutil attach -mountroot \"$WS_VOL2\" \"$RELEASE_DMG\"\ncp -r \"$WS_VOL2\"/Electrum \"$DMG_RELEASE_UNPACKED\"\nhdiutil detach \"$WS_VOL2\"/Electrum\n\n# copy signatures from RELEASE_DMG to UNSIGNED_DMG\necho \"Extracting signatures from release app...\"\nQUIET=\"1\" \"$CONTRIB_OSX/extract_sigs.sh\" \"$DMG_RELEASE_UNPACKED\"/Electrum.app\necho \"Applying extracted signatures to unsigned app...\"\nQUIET=\"1\" \"$CONTRIB_OSX/apply_sigs.sh\" \"$DMG_UNSIGNED_UNPACKED\"/Electrum.app mac_extracted_sigs.tar.gz\n\nrm mac_extracted_sigs.tar.gz\nrm -rf \"$DMG_UNSIGNED_UNPACKED\"\n\nset -x\ndiff=$(diff -qr \"$WORKSPACE/signed_app\" \"$DMG_RELEASE_UNPACKED\") || diff=\"diff errored\"\nset +x\necho $diff\nif [ \"$diff\" ]; then\n    echo \"DMGs do *not* match.\"\n    echo \"failure\"\n    exit 1\nelse\n    echo \"DMGs match.\"\n    echo \"success\"\n    exit 0\nfi\n"
  },
  {
    "path": "contrib/osx/entitlements.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <!-- These are required for binaries built by PyInstaller -->\n    <!-- see pyinstaller/pyinstaller#4629 -->\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n\n    <!-- These are required for USB HID access (hw wallets). -->\n    <!-- see https://github.com/Electron-Cash/Electron-Cash/commit/5abec73eee0cdeb725e3c5a989621ec4ccfb92a0 -->\n    <key>com.apple.security.cs.allow-dyld-environment-variables</key>\n    <true/>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n\n    <!-- Camera access, to read QR codes -->\n    <key>com.apple.security.device.camera</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "contrib/osx/extract_sigs.sh",
    "content": "#!/bin/sh\n# Copyright (c) 2014-2019 The Bitcoin Core developers\n# Distributed under the MIT software license, see the accompanying\n# file COPYING or http://www.opensource.org/licenses/mit-license.php.\n#\n# This script is based on https://github.com/bitcoin/bitcoin/blob/194b9b8792d9b0798fdb570b79fa51f1d1f5ebaf/contrib/macdeploy/detached-sig-create.sh\n\nexport LC_ALL=C\nset -e\n\nif [ $(uname) != \"Darwin\" ]; then\n    echo \"This script needs to be run on macOS.\"\n    exit 1\nfi\n\nTEMPDIR=\"/tmp/electrum_compare_dmg/sigs.temp\"\nOUT=mac_extracted_sigs.tar.gz\nOUTROOT=.\n\nif [ -z \"$1\" ]; then\n    echo \"usage: $0 <path to .app>\"\n    exit 1\nfi\nBUNDLE=\"$1\"\nBUNDLE_BASENAME=$(basename \"$BUNDLE\")\n\nrm -rf ${TEMPDIR}\nmkdir -p ${TEMPDIR}\n\nMAYBE_SIGNED_FILES=$(\n    find \"$BUNDLE/Contents/MacOS/\" -type f;\n    find \"$BUNDLE/Contents/Frameworks/\" -type f;\n    find \"$BUNDLE/Contents/Resources/\" -type f\n)\n\necho \"${MAYBE_SIGNED_FILES}\" | while read i; do\n    # skip files where pagestuff errors; these probably do not need signing:\n    pagestuff \"$i\" -p 1>/dev/null 2>/dev/null || continue\n    TARGETFILE=\"${BUNDLE_BASENAME}/$(echo \"${i}\" | sed \"s|.*${BUNDLE}/||\")\"\n    SIZE=$(pagestuff \"$i\" -p | tail -2 | grep size | sed 's/[^0-9]*//g')\n    OFFSET=$(pagestuff \"$i\" -p | tail -2 | grep offset | sed 's/[^0-9]*//g')\n    SIGNFILE=\"${TEMPDIR}/${OUTROOT}/${TARGETFILE}.sign\"\n    DIRNAME=\"$(dirname \"${SIGNFILE}\")\"\n    mkdir -p \"${DIRNAME}\"\n    if [ -z ${QUIET} ]; then\n        echo \"Adding detached signature for: ${TARGETFILE}. Size: ${SIZE}. Offset: ${OFFSET}\"\n    fi\n    dd if=\"$i\" of=\"${SIGNFILE}\" bs=1 skip=${OFFSET} count=${SIZE} 2>/dev/null\ndone\n\n# note: \"$BUNDLE/Contents/CodeResources\" is the \"notarization staple id\"\nFILES_TO_COPY=$(cat << EOF\n$BUNDLE/Contents/_CodeSignature/CodeResources\n$([ \"${IS_NOTARIZED:-true}\" != \"false\" ] && echo \"$BUNDLE/Contents/CodeResources\")\nEOF\n)\n\necho \"${FILES_TO_COPY}\" | while read i; do\n    TARGETFILE=\"${BUNDLE_BASENAME}/$(echo \"${i}\" | sed \"s|.*${BUNDLE}/||\")\"\n    RESOURCE=\"${TEMPDIR}/${OUTROOT}/${TARGETFILE}\"\n    DIRNAME=\"$(dirname \"${RESOURCE}\")\"\n    mkdir -p \"${DIRNAME}\"\n    if [ -z ${QUIET} ]; then\n        echo \"Adding resource for: \\\"${TARGETFILE}\\\"\"\n    fi\n    cp \"${i}\" \"${RESOURCE}\"\ndone\n\ntar -C \"${TEMPDIR}\" -czf \"${OUT}\" .\nrm -rf \"${TEMPDIR}\"\necho \"Created ${OUT}\"\n"
  },
  {
    "path": "contrib/osx/make_osx.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\n# Parameterize\nPYTHON_VERSION=3.12.10\nPY_VER_MAJOR=\"3.12\"  # as it appears in fs paths\nPACKAGE=Electrum\nGIT_REPO=https://github.com/spesmilo/electrum\n\nexport GCC_STRIP_BINARIES=\"1\"\nexport PYTHONDONTWRITEBYTECODE=1  # don't create __pycache__/ folders with .pyc files\n\n\n. \"$(dirname \"$0\")/../build_tools_util.sh\"\n\n\nCONTRIB_OSX=\"$(dirname \"$(realpath \"$0\")\")\"\nCONTRIB=\"$CONTRIB_OSX/..\"\nPROJECT_ROOT=\"$CONTRIB/..\"\nCACHEDIR=\"$CONTRIB_OSX/.cache\"\nexport DLL_TARGET_DIR=\"$CACHEDIR/dlls\"\nPIP_CACHE_DIR=\"$CACHEDIR/pip_cache\"\n\nmkdir -p \"$CACHEDIR\" \"$DLL_TARGET_DIR\" \"$PIP_CACHE_DIR\"\n\ncd \"$PROJECT_ROOT\"\n\ngit -C \"$PROJECT_ROOT\" rev-parse 2>/dev/null || fail \"Building outside a git clone is not supported.\"\n\n\nwhich brew > /dev/null 2>&1 || fail \"Please install brew from https://brew.sh/ to continue\"\nwhich xcodebuild > /dev/null 2>&1 || fail \"Please install xcode command line tools to continue\"\n\n\ninfo \"Installing Python $PYTHON_VERSION\"\nPKG_FILE=\"python-${PYTHON_VERSION}-macos11.pkg\"\nif [ ! -f \"$CACHEDIR/$PKG_FILE\" ]; then\n    curl -o \"$CACHEDIR/$PKG_FILE\" \"https://www.python.org/ftp/python/${PYTHON_VERSION}/$PKG_FILE\"\nfi\necho \"8373e58da4ea146b3eb1c1f9834f19a319440b6b679b06050b1f9ee3237aa8e4  $CACHEDIR/$PKG_FILE\" | shasum -a 256 -c \\\n    || fail \"python pkg checksum mismatched\"\nsudo installer -pkg \"$CACHEDIR/$PKG_FILE\" -target / \\\n    || fail \"failed to install python\"\n\n# sanity check \"python3\" has the version we just installed.\nFOUND_PY_VERSION=$(python3 -c 'import sys; print(\".\".join(map(str, sys.version_info[:3])))')\nif [[ \"$FOUND_PY_VERSION\" != \"$PYTHON_VERSION\" ]]; then\n    fail \"python version mismatch: $FOUND_PY_VERSION != $PYTHON_VERSION\"\nfi\n\nbreak_legacy_easy_install\n\n# create a fresh virtualenv\n# This helps to avoid older versions of pip-installed dependencies interfering with the build.\nVENV_DIR=\"$CONTRIB_OSX/build-venv\"\nrm -rf \"$VENV_DIR\"\npython3 -m venv \"$VENV_DIR\"\nsource \"$VENV_DIR/bin/activate\"\n\n# don't add debug info to compiled C files (e.g. when pip calls setuptools/wheel calls gcc)\n# see https://github.com/pypa/pip/issues/6505#issuecomment-526613584\n# note: this does not seem sufficient when cython is involved (although it is on linux, just not on mac... weird.)\n#       see additional \"strip\" pass on built files later in the file.\nexport CFLAGS=\"-g0\"\n\n# Do not build universal binaries. The default on macos 11+ and xcode 12+ is \"-arch arm64 -arch x86_64\"\n# but with that e.g. \"hid.cpython-310-darwin.so\" is not reproducible as built by clang.\nexport ARCHFLAGS=\"-arch x86_64\"\n\ninfo \"Installing build dependencies\"\n# note: re pip installing from PyPI,\n#       we prefer compiling C extensions ourselves, instead of using binary wheels,\n#       hence \"--no-binary :all:\" flags. However, we specifically allow\n#       - PyQt6, as it's harder to build from source\n#       - cryptography, as it's harder to build from source\n#       - the whole of \"requirements-build-base.txt\", which includes pip and friends, as it also includes \"wheel\",\n#         and I am not quite sure how to break the circular dependence there (I guess we could introduce\n#         \"requirements-build-base-base.txt\" with just wheel in it...)\npython3 -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \\\n    --cache-dir \"$PIP_CACHE_DIR\" -Ir ./contrib/deterministic-build/requirements-build-base.txt \\\n    || fail \"Could not install build dependencies (base)\"\npython3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \\\n    --cache-dir \"$PIP_CACHE_DIR\" -Ir ./contrib/deterministic-build/requirements-build-mac.txt \\\n    || fail \"Could not install build dependencies (mac)\"\n\ninfo \"Installing some build-time deps for compilation...\"\nbrew install autoconf automake libtool gettext coreutils pkgconfig\n\ninfo \"Building PyInstaller.\"\nPYINSTALLER_REPO=\"https://github.com/pyinstaller/pyinstaller.git\"\nPYINSTALLER_COMMIT=\"306d4d92580fea7be7ff2c89ba112cdc6f73fac1\"\n# ^ tag \"v6.13.0\"\n(\n    if [ -f \"$CACHEDIR/pyinstaller/PyInstaller/bootloader/Darwin-64bit/runw\" ]; then\n        info \"pyinstaller already built, skipping\"\n        exit 0\n    fi\n    cd \"$PROJECT_ROOT\"\n    ELECTRUM_COMMIT_HASH=$(git rev-parse HEAD)\n    cd \"$CACHEDIR\"\n    rm -rf pyinstaller\n    mkdir pyinstaller\n    cd pyinstaller\n    # Shallow clone\n    git init\n    git remote add origin $PYINSTALLER_REPO\n    git fetch --depth 1 origin $PYINSTALLER_COMMIT\n    git checkout -b pinned \"${PYINSTALLER_COMMIT}^{commit}\"\n    rm -fv PyInstaller/bootloader/Darwin-*/run* || true\n    # add reproducible randomness. this ensures we build a different bootloader for each commit.\n    # if we built the same one for all releases, that might also get anti-virus false positives\n    echo \"const char *electrum_tag = \\\"tagged by Electrum@$ELECTRUM_COMMIT_HASH\\\";\" >> ./bootloader/src/pyi_main.c\n    pushd bootloader\n    # compile bootloader\n    python3 ./waf all CFLAGS=\"-static\"\n    popd\n    # sanity check bootloader is there:\n    [[ -e \"PyInstaller/bootloader/Darwin-64bit/runw\" ]] || fail \"Could not find runw in target dir!\"\n)\ninfo \"Installing PyInstaller.\"\npython3 -m pip install --no-build-isolation --no-dependencies \\\n    --cache-dir \"$PIP_CACHE_DIR\" --no-warn-script-location \"$CACHEDIR/pyinstaller\"\n\ninfo \"Using these versions for building $PACKAGE:\"\nsw_vers\npython3 --version\necho -n \"Pyinstaller \"\npyinstaller --version\n\nrm -rf ./dist\n\ninfo \"resetting git submodules.\"\n# note: --force is less critical in other build scripts, but as the mac build is not doing a fresh clone,\n#       it is very useful here for reproducibility\ngit submodule update --init --force\n\ninfo \"preparing electrum-locale.\"\n(\n    if ! which msgfmt > /dev/null 2>&1; then\n        brew install gettext\n        brew link --force gettext\n    fi\n    \"$CONTRIB/locale/build_cleanlocale.sh\"\n    # we want the binary to have only compiled (.mo) locale files; not source (.po) files\n    rm -r \"$PROJECT_ROOT/electrum/locale/locale\"/*/electrum.po\n)\n\n\nif ls \"$DLL_TARGET_DIR\"/libsecp256k1.*.dylib 1> /dev/null 2>&1; then\n    info \"libsecp256k1 already built, skipping\"\nelse\n    info \"Building libsecp256k1 dylib...\"\n    \"$CONTRIB\"/make_libsecp256k1.sh || fail \"Could not build libsecp\"\nfi\ncp -f \"$DLL_TARGET_DIR\"/libsecp256k1.*.dylib \"$PROJECT_ROOT/electrum\" || fail \"Could not copy libsecp256k1 dylib\"\n\nif [ ! -f \"$DLL_TARGET_DIR/libzbar.0.dylib\" ]; then\n    info \"Building ZBar dylib...\"\n    \"$CONTRIB\"/make_zbar.sh || fail \"Could not build ZBar dylib\"\nelse\n    info \"Skipping ZBar build: reusing already built dylib.\"\nfi\ncp -f \"$DLL_TARGET_DIR/libzbar.0.dylib\" \"$PROJECT_ROOT/electrum/\" || fail \"Could not copy ZBar dylib\"\n\nif [ ! -f \"$DLL_TARGET_DIR/libusb-1.0.dylib\" ]; then\n    info \"Building libusb dylib...\"\n    \"$CONTRIB\"/make_libusb.sh || fail \"Could not build libusb dylib\"\nelse\n    info \"Skipping libusb build: reusing already built dylib.\"\nfi\ncp -f \"$DLL_TARGET_DIR/libusb-1.0.dylib\" \"$PROJECT_ROOT/electrum/\" || fail \"Could not copy libusb dylib\"\n\n\n# opt out of compiling C extensions\nexport YARL_NO_EXTENSIONS=1\nexport PROPCACHE_NO_EXTENSIONS=1\n\nexport ELECTRUM_ECC_DONT_COMPILE=1\n\ninfo \"Installing requirements...\"\npython3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: \\\n    --cache-dir \"$PIP_CACHE_DIR\" --no-warn-script-location \\\n    -Ir ./contrib/deterministic-build/requirements.txt \\\n    || fail \"Could not install requirements\"\n\ninfo \"Installing hardware wallet requirements...\"\npython3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: --only-binary cryptography \\\n    --cache-dir \"$PIP_CACHE_DIR\" --no-warn-script-location \\\n    -Ir ./contrib/deterministic-build/requirements-hw.txt \\\n    || fail \"Could not install hardware wallet requirements\"\n\ninfo \"Installing dependencies specific to binaries...\"\npython3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: --only-binary PyQt6,PyQt6-Qt6,cryptography \\\n    --cache-dir \"$PIP_CACHE_DIR\" --no-warn-script-location \\\n    -Ir ./contrib/deterministic-build/requirements-binaries-mac.txt \\\n    || fail \"Could not install dependencies specific to binaries\"\n\ninfo \"Building $PACKAGE...\"\npython3 -m pip install --no-build-isolation --no-dependencies \\\n    --cache-dir \"$PIP_CACHE_DIR\" --no-warn-script-location . > /dev/null || fail \"Could not build $PACKAGE\"\n# pyinstaller needs to be able to \"import electrum_ecc\", for which we need libsecp256k1:\n# (or could try \"pip install -e\" instead)\ncp \"$DLL_TARGET_DIR\"/libsecp256k1.*.dylib \"$VENV_DIR/lib/python$PY_VER_MAJOR/site-packages/electrum_ecc/\"\n\n# strip debug symbols of some compiled libs\n# - hidapi (hid.cpython-39-darwin.so) in particular is not reproducible without this\nfind \"$VENV_DIR/lib/python$PY_VER_MAJOR/site-packages/\" -type f -name '*.so' -print0 \\\n    | xargs -0 -t strip -x\n\ninfo \"Faking timestamps...\"\nfind . -exec touch -t '200101220000' {} + || true\n\n# note: no --dirty, as we have dirtied electrum/locale/ ourselves.\nVERSION=$(git describe --tags --always)\n\ninfo \"Building binary\"\nELECTRUM_VERSION=$VERSION pyinstaller --noconfirm --clean contrib/osx/pyinstaller.spec || fail \"Could not build binary\"\n\ninfo \"Finished building unsigned dist/${PACKAGE}.app. This hash should be reproducible:\"\nfind \"dist/${PACKAGE}.app\" -type f -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256\n\ninfo \"Creating unsigned .DMG\"\nhdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION-unsigned.dmg || fail \"Could not create .DMG\"\n\ninfo \"App was built successfully but was not code signed. Users may get security warnings from macOS.\"\ninfo \"Now you also need to run sign_osx.sh to codesign/notarize the binary.\"\n"
  },
  {
    "path": "contrib/osx/notarize_app.sh",
    "content": "#!/usr/bin/env bash\n# from https://github.com/metabrainz/picard/blob/e1354632d2db305b7a7624282701d34d73afa225/scripts/package/macos-notarize-app.sh\n\nset -e\n\nif [ -z \"$1\" ]; then\n    echo \"Specify app bundle as first parameter\"\n    exit 1\nfi\n\nif [ -z \"$APPLE_ID_USER\" ] || [ -z \"$APPLE_ID_PASSWORD\" ] || [ -z \"$APPLE_TEAM_ID\" ]; then\n    echo \"You need to set your Apple ID credentials with \\$APPLE_ID_USER and \\$APPLE_ID_PASSWORD.\"\n    exit 1\nfi\n\nAPP_BUNDLE=$(basename \"$1\")\nAPP_BUNDLE_DIR=$(dirname \"$1\")\n\ncd \"$APP_BUNDLE_DIR\" || exit 1\n\n# Package app for submission\necho \"Generating ZIP archive ${APP_BUNDLE}.zip...\"\nditto -c -k --rsrc --keepParent \"$APP_BUNDLE\" \"${APP_BUNDLE}.zip\"\n\n# Submit for notarization\necho \"Submitting $APP_BUNDLE for notarization...\"\nRESULT=$(xcrun notarytool submit \\\n    --team-id \"$APPLE_TEAM_ID\" \\\n    --apple-id \"$APPLE_ID_USER\" \\\n    --password \"$APPLE_ID_PASSWORD\" \\\n    --output-format plist \\\n    --wait \\\n    --timeout 10m \\\n    \"${APP_BUNDLE}.zip\"\n)\n\nif [ $? -ne 0 ]; then\n    echo \"Submitting $APP_BUNDLE failed:\"\n    echo \"$RESULT\"\n    exit 1\nfi\n\nSTATUS=$(echo \"$RESULT\" | xpath -e \\\n  \"//key[normalize-space(text()) = 'status']/following-sibling::string[1]/text()\" 2> /dev/null)\n\nif [ \"$STATUS\" = \"Accepted\" ]; then\n    echo \"Notarization of $APP_BUNDLE succeeded!\"\nelse\n    echo \"Notarization of $APP_BUNDLE failed:\"\n    echo \"$RESULT\"\n    exit 1\nfi\n\n# Staple the notary ticket\nxcrun stapler staple \"$APP_BUNDLE\"\n\n# rm zip\nrm \"${APP_BUNDLE}.zip\"\n"
  },
  {
    "path": "contrib/osx/package.sh",
    "content": "#!/usr/bin/env bash\n\nset -ex\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/../..\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\n. \"$CONTRIB\"/build_tools_util.sh\n\n# note: GCC 10.1 will need an extra option, see https://github.com/bitcoin/bitcoin/pull/19553\n\ncdrkit_version=1.1.11\ncdrkit_download_path=http://distro.ibiblio.org/fatdog/source/600/c\ncdrkit_file_name=cdrkit-${cdrkit_version}.tar.bz2\ncdrkit_sha256_hash=b50d64c214a65b1a79afe3a964c691931a4233e2ba605d793eb85d0ac3652564\ncdrkit_patches=cdrkit-deterministic.patch\ngenisoimage=genisoimage-$cdrkit_version\n\nlibdmg_url=https://github.com/theuni/libdmg-hfsplus\n\n\nexport LD_PRELOAD=$(locate libfaketime.so.1)\nexport FAKETIME=\"2000-01-22 00:00:00\"\nexport PATH=$PATH:~/bin\n\n\nif [ -z \"$1\" ]; then\n    echo \"Usage: $0 Electrum.app\"\n    exit -127\nfi\n\nmkdir -p ~/bin\n\nif ! which ${genisoimage} > /dev/null 2>&1; then\n    mkdir -p /tmp/electrum-macos\n    cd /tmp/electrum-macos\n    info \"Downloading cdrkit $cdrkit_version\"\n    wget -nc ${cdrkit_download_path}/${cdrkit_file_name}\n    tar xvf ${cdrkit_file_name}\n\n    info \"Patching genisoimage\"\n    cd cdrkit-${cdrkit_version}\n    patch -p1 <$CONTRIB/osx/cdrkit-deterministic.patch\n\n    info \"Building genisoimage\"\n    cmake . -Wno-dev\n    make genisoimage\n    cp genisoimage/genisoimage ~/bin/${genisoimage}\nfi\n\nif ! which dmg > /dev/null 2>&1; then\n    mkdir -p /tmp/electrum-macos\n    cd /tmp/electrum-macos\n    info \"Downloading libdmg\"\n    LD_PRELOAD= git clone ${libdmg_url}\n    cd libdmg-hfsplus\n    info \"Building libdmg\"\n    cmake .\n    make\n    cp dmg/dmg ~/bin\nfi\n\n${genisoimage} -version || fail \"Unable to install genisoimage\"\ndmg - || fail \"Unable to install libdmg\"\n\nplist=$1/Contents/Info.plist\ntest -f \"$plist\" || fail \"Info.plist not found\"\nVERSION=$(grep -1 ShortVersionString $plist | tail -1 | gawk 'match($0, /<string>(.*)<\\/string>/, a) {print a[1]}')\necho $VERSION\n\nrm -rf /tmp/electrum-macos/image > /dev/null 2>&1\nmkdir /tmp/electrum-macos/image/\ncp -r $1 /tmp/electrum-macos/image/\n\nbuild_dir=$(dirname \"$1\")\ntest -n \"$build_dir\" -a -d \"$build_dir\" || exit\ncd $build_dir\n\n${genisoimage} \\\n    -no-cache-inodes \\\n    -D \\\n    -l \\\n    -probe \\\n    -V \"Electrum\" \\\n    -no-pad \\\n    -r \\\n    -dir-mode 0755 \\\n    -apple \\\n    -o Electrum_uncompressed.dmg \\\n    /tmp/electrum-macos/image || fail \"Unable to create uncompressed dmg\"\n\ndmg dmg Electrum_uncompressed.dmg electrum-$VERSION.dmg || fail \"Unable to create compressed dmg\"\nrm Electrum_uncompressed.dmg\n\necho \"Done.\"\nsha256sum electrum-$VERSION.dmg\n"
  },
  {
    "path": "contrib/osx/pyinstaller.spec",
    "content": "# -*- mode: python -*-\nimport sys\nimport os\nfrom typing import TYPE_CHECKING\n\nfrom PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs, copy_metadata\n\nif TYPE_CHECKING:\n    from PyInstaller.building.build_main import Analysis, PYZ, EXE, BUNDLE\n\n\nPACKAGE_NAME='Electrum.app'\nPYPKG='electrum'\nMAIN_SCRIPT='run_electrum'\nPROJECT_ROOT = os.path.abspath(\".\")\nICONS_FILE=f\"{PROJECT_ROOT}/{PYPKG}/gui/icons/electrum.icns\"\n\n\nVERSION = os.environ.get(\"ELECTRUM_VERSION\")\nif not VERSION:\n    raise Exception('no version')\n\nblock_cipher = None\n\n# see https://github.com/pyinstaller/pyinstaller/issues/2005\nhiddenimports = []\nhiddenimports += collect_submodules('pkg_resources')  # workaround for https://github.com/pypa/setuptools/issues/1963\nhiddenimports += collect_submodules(f\"{PYPKG}.plugins\")\n\n\nbinaries = []\n# Workaround for \"Retro Look\":\nbinaries += [b for b in collect_dynamic_libs('PyQt6') if 'macstyle' in b[0]]\n# add libsecp256k1, libusb, etc:\nbinaries += [(f\"{PROJECT_ROOT}/{PYPKG}/*.dylib\", \".\")]\n\n\ndatas = [\n    (f\"{PROJECT_ROOT}/{PYPKG}/*.json\", PYPKG),\n    (f\"{PROJECT_ROOT}/{PYPKG}/lnwire/*.csv\", f\"{PYPKG}/lnwire\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/wordlist/english.txt\", f\"{PYPKG}/wordlist\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/wordlist/slip39.txt\", f\"{PYPKG}/wordlist\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/chains\", f\"{PYPKG}/chains\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/locale\", f\"{PYPKG}/locale\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/plugins\", f\"{PYPKG}/plugins\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/gui/icons\", f\"{PYPKG}/gui/icons\"),\n    (f\"{PROJECT_ROOT}/{PYPKG}/gui/fonts\", f\"{PYPKG}/gui/fonts\"),\n]\ndatas += collect_data_files(f\"{PYPKG}.plugins\")\ndatas += collect_data_files('trezorlib')  # TODO is this needed? and same question for other hww libs\ndatas += collect_data_files('safetlib')\ndatas += collect_data_files('ckcc')\ndatas += collect_data_files('bitbox02')\n\n# some deps rely on importlib metadata\ndatas += copy_metadata('slip10')  # from trezor->slip10\n\n# Exclude parts of Qt that we never use. Reduces binary size by tens of MBs. see #4815\nexcludes = [\n    \"PyQt6.QtBluetooth\",\n    \"PyQt6.QtDesigner\",\n    \"PyQt6.QtNfc\",\n    \"PyQt6.QtPositioning\",\n    \"PyQt6.QtQml\",\n    \"PyQt6.QtQuick\",\n    \"PyQt6.QtQuick3D\",\n    \"PyQt6.QtQuickWidgets\",\n    \"PyQt6.QtRemoteObjects\",\n    \"PyQt6.QtSensors\",\n    \"PyQt6.QtSerialPort\",\n    \"PyQt6.QtSpatialAudio\",\n    \"PyQt6.QtSql\",\n    \"PyQt6.QtTest\",\n    \"PyQt6.QtTextToSpeech\",\n    \"PyQt6.QtWebChannel\",\n    \"PyQt6.QtWebSockets\",\n    \"PyQt6.QtXml\",\n    # \"PyQt6.QtNetwork\",  # needed by QtMultimedia. kinda weird but ok.\n]\n\n# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports\na = Analysis([f\"{PROJECT_ROOT}/{MAIN_SCRIPT}\",\n              f\"{PROJECT_ROOT}/{PYPKG}/gui/qt/main_window.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/gui/qt/qrreader/qtmultimedia/camera_dialog.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/gui/text.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/util.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/wallet.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/simple_config.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/bitcoin.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/dnssec.py\",\n              f\"{PROJECT_ROOT}/{PYPKG}/commands.py\",\n              ],\n             binaries=binaries,\n             datas=datas,\n             hiddenimports=hiddenimports,\n             hookspath=[],\n             excludes=excludes,\n             )\n\n\n# http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal\nfor d in a.datas:\n    if 'pyconfig' in d[0]:\n        a.datas.remove(d)\n        break\n\n\npyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)\n\nexe = EXE(\n    pyz,\n    a.scripts,\n    exclude_binaries=True,\n    name=MAIN_SCRIPT,\n    debug=False,\n    strip=False,\n    upx=True,\n    icon=ICONS_FILE,\n    console=False,\n    target_arch='x86_64',  # TODO investigate building 'universal2'\n)\n\napp = BUNDLE(\n    exe,\n    a.binaries,\n    a.zipfiles,\n    a.datas,\n    version=VERSION,\n    name=PACKAGE_NAME,\n    icon=ICONS_FILE,\n    bundle_identifier=None,\n    info_plist={\n        'NSHighResolutionCapable': 'True',\n        'NSSupportsAutomaticGraphicsSwitching': 'True',\n        'CFBundleURLTypes':\n            [{\n                'CFBundleURLName': 'bitcoin',\n                'CFBundleURLSchemes': ['bitcoin', 'lightning', ],\n            }],\n        'LSMinimumSystemVersion': '11',\n        'NSCameraUsageDescription': 'Electrum would like to access the camera to scan for QR codes',\n    },\n)\n"
  },
  {
    "path": "contrib/osx/sign_osx.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nsecurity -v unlock-keychain login.keychain\n\n\nPACKAGE=Electrum\n\n\n. \"$(dirname \"$0\")/../build_tools_util.sh\"\n\n\nCONTRIB_OSX=\"$(dirname \"$(realpath \"$0\")\")\"\nCONTRIB=\"$CONTRIB_OSX/..\"\nPROJECT_ROOT=\"$CONTRIB/..\"\nCACHEDIR=\"$CONTRIB_OSX/.cache\"\n\n\ncd \"$PROJECT_ROOT\"\n\n\n# Code Signing: See https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html\nif [ -n \"$CODESIGN_CERT\" ]; then\n    # Test the identity is valid for signing by doing this hack. There is no other way to do this.\n    cp -f /bin/ls ./CODESIGN_TEST\n    set +e\n    codesign -s \"$CODESIGN_CERT\" --dryrun -f ./CODESIGN_TEST > /dev/null 2>&1\n    res=$?\n    set -e\n    rm -f ./CODESIGN_TEST\n    if ((res)); then\n        fail \"Code signing identity \\\"$CODESIGN_CERT\\\" appears to be invalid.\"\n    fi\n    unset res\n    info \"Code signing enabled using identity \\\"$CODESIGN_CERT\\\"\"\nelse\n    fail \"Code signing DISABLED. Specify a valid macOS Developer identity installed on the system to enable signing.\"\nfi\n\n\nfunction DoCodeSignMaybe { # ARGS: infoName fileOrDirName\n    infoName=\"$1\"\n    file=\"$2\"\n    deep=\"\"\n    if [ -z \"$CODESIGN_CERT\" ]; then\n        # no cert -> we won't codesign\n        return\n    fi\n    if [ -d \"$file\" ]; then\n        deep=\"--deep\"\n    fi\n    if [ -z \"$infoName\" ] || [ -z \"$file\" ] || [ ! -e \"$file\" ]; then\n        fail \"Argument error to internal function DoCodeSignMaybe()\"\n    fi\n    hardened_arg=\"--entitlements=${CONTRIB_OSX}/entitlements.plist -o runtime\"\n\n    info \"Code signing ${infoName}...\"\n    codesign -f -v $deep -s \"$CODESIGN_CERT\" $hardened_arg \"$file\" || fail \"Could not code sign ${infoName}\"\n}\n\n# note: no --dirty, as we have dirtied electrum/locale/ ourselves.\nVERSION=$(git describe --tags --always)\n\nDoCodeSignMaybe \"app bundle\" \"dist/${PACKAGE}.app\"\n\nif [ ! -z \"$CODESIGN_CERT\" ]; then\n    if [ ! -z \"$APPLE_ID_USER\" ]; then\n        info \"Notarizing .app with Apple's central server...\"\n        \"${CONTRIB_OSX}/notarize_app.sh\" \"dist/${PACKAGE}.app\" || fail \"Could not notarize binary.\"\n    else\n        warn \"AppleID details not set! Skipping Apple notarization.\"\n    fi\nfi\n\ninfo \"Creating .DMG\"\nhdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail \"Could not create .DMG\"\n\nDoCodeSignMaybe \".DMG\" \"dist/electrum-${VERSION}.dmg\"\n"
  },
  {
    "path": "contrib/print_electrum_version.py",
    "content": "#!/usr/bin/python3\n# For usage in shell, to get the version of electrum, without needing electrum installed.\n# usage: ./print_electrum_version.py [<attr_name>]\n#\n# For example:\n# $ VERSION=$(\"$CONTRIB\"/print_electrum_version.py)\n# instead of\n# $ VERSION=$(python3 -c \"import electrum; print(electrum.version.ELECTRUM_VERSION)\")\n\nimport importlib.util\nimport os\nimport sys\n\n\nif __name__ == '__main__':\n    if len(sys.argv) >= 2:\n        attr_name = sys.argv[1]\n    else:\n        attr_name = \"ELECTRUM_VERSION\"\n\n    project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))\n    version_file_path = os.path.join(project_root, \"electrum\", \"version.py\")\n\n    # load version.py; needlessly complicated alternative to \"imp.load_source\":\n    version_spec = importlib.util.spec_from_file_location('version', version_file_path)\n    version_module = version = importlib.util.module_from_spec(version_spec)\n    version_spec.loader.exec_module(version_module)\n\n    attr_val = getattr(version, attr_name)\n    print(attr_val, file=sys.stdout)\n\n"
  },
  {
    "path": "contrib/release.sh",
    "content": "#!/bin/bash\n#\n# This script is used for stage 1 of the release process. It operates exclusively on the airlock.\n# This script, for the RELEASEMANAGER (RM):\n# - builds and uploads all binaries to airlock,\n# - assumes all keys are available, and signs everything\n# This script, for other builders:\n# - builds all reproducible binaries,\n# - downloads binaries built by the release manager (from airlock if SFTPUSER, else from website),\n#   compares and signs them,\n# - and then uploads sigs (if SFTPUSER), else they can be submitted as PR to spesmilo/electrum-signatures\n# Note: the .dmg should be built separately beforehand and copied into dist/\n#       (as it is built on a separate machine)\n#\n#\n# env vars:\n# - ELECBUILD_NOCACHE: if set, forces rebuild of docker images\n#\n# \"uploadserver\" is set in /etc/hosts\n#\n# Note: steps before doing a new release:\n# - update locale:\n#     1. cd /opt/electrum-locale && ./update.py && git push\n#     2. cd to the submodule dir, and git pull\n#     3. cd .. && git push\n# - update RELEASE-NOTES and version.py\n# - $ git tag -s \"$VERSION\" -m \"$VERSION\"\n# - $ git push \"$REMOTE_ORIGIN\" tag \"$VERSION\"\n#\n# -----\n# Then, typical release flow:\n# - RM runs release.sh\n# - Another SFTPUSER BUILDER runs `$ ./release.sh`\n# - now airlock contains new binaries and two sigs for each\n# - deploy.sh will verify sigs and move binaries across airlock\n# - new binaries are now publicly available on uploadserver, but not linked from website yet\n# - other BUILDERS can now also try to reproduce binaries and open PRs with sigs against spesmilo/electrum-signatures\n#   - these PRs can get merged as they come\n#   - run add_cosigner\n# - after some time, RM can run release_www.sh to create and commit website-update\n#   - then run WWW_DIR/publish.sh to update website\n# - at least two people need to run WWW_DIR/publish.sh\n#\n\nset -e\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/..\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\n\ncd \"$PROJECT_ROOT\"\n\n. \"$CONTRIB\"/build_tools_util.sh\n\n# rm -rf dist/*\n# rm -f .buildozer\n\nGPGUSER=$1\nif [ -z \"$GPGUSER\" ]; then\n    fail \"usage: $0 gpg_username\"\nfi\n\nRELEASEMANAGER=\"\"\nif [ \"$GPGUSER\" == \"ThomasV\" ]; then\n    PUBKEY=\"--local-user 6694D8DE7BE8EE5631BED9502BD5824B7F9470E6\"\n    export SSHUSER=thomasv\n    RELEASEMANAGER=1\nelif [ \"$GPGUSER\" == \"sombernight_releasekey\" ]; then\n    PUBKEY=\"--local-user 0EEDCFD5CAFB459067349B23CA9EEEC43DF911DC\"\n    export SSHUSER=sombernight\nelse\n    warn \"unexpected GPGUSER=$GPGUSER\"\n    PUBKEY=\"\"\n    export SSHUSER=\"\"\nfi\n\n\nif [ ! -z \"$RELEASEMANAGER\" ] ; then\n    echo -n \"Code signing passphrase:\"\n    read -s password\n    # tests password against keystore\n    keytool -list -storepass $password\n    # the same password is used for windows signing\n    export WIN_SIGNING_PASSWORD=$password\nfi\n\n\nVERSION=$(\"$CONTRIB\"/print_electrum_version.py)\ninfo \"VERSION: $VERSION\"\nREV=$(git describe --tags)\ninfo \"REV: $REV\"\nCOMMIT=$(git rev-parse HEAD)\n\nexport ELECBUILD_COMMIT=\"${COMMIT}^{commit}\"\n\n\ngit_status=$(git status --porcelain)\nif [ ! -z \"$git_status\" ]; then\n    echo \"$git_status\"\n    fail \"git repo not clean, aborting\"\nfi\n\nset -x\n\n# create tarball\ntarball=\"Electrum-$VERSION.tar.gz\"\nif test -f \"dist/$tarball\"; then\n    info \"file exists: $tarball\"\nelse\n    ./contrib/build-linux/sdist/build.sh\nfi\n\n# create source-only tarball\nsrctarball=\"Electrum-sourceonly-$VERSION.tar.gz\"\nif test -f \"dist/$srctarball\"; then\n    info \"file exists: $srctarball\"\nelse\n    OMIT_UNCLEAN_FILES=1 ./contrib/build-linux/sdist/build.sh\nfi\n\n# appimage\nappimage=\"electrum-$REV-x86_64.AppImage\"\nif test -f \"dist/$appimage\"; then\n    info \"file exists: $appimage\"\nelse\n    ./contrib/build-linux/appimage/build.sh\nfi\n\n\n# windows\nwin1=\"electrum-$REV.exe\"\nwin2=\"electrum-$REV-portable.exe\"\nwin3=\"electrum-$REV-setup.exe\"\nif test -f \"dist/$win1\"; then\n    info \"file exists: $win1\"\nelse\n    pushd .\n    if test -f \"contrib/build-wine/dist/$win1\"; then\n        info \"unsigned file exists: $win1\"\n    else\n        ./contrib/build-wine/build.sh\n    fi\n    cd contrib/build-wine/\n    if [ ! -z \"$RELEASEMANAGER\" ] ; then\n        ./sign.sh\n        cp ./signed/*.exe \"$PROJECT_ROOT/dist/\"\n    else\n        cp ./dist/*.exe \"$PROJECT_ROOT/dist/\"\n    fi\n    popd\nfi\n\n# android\napk1=\"Electrum-$VERSION-armeabi-v7a-release.apk\"\napk2=\"Electrum-$VERSION-arm64-v8a-release.apk\"\napk3=\"Electrum-$VERSION-x86_64-release.apk\"\nfor arch in armeabi-v7a arm64-v8a x86_64\ndo\n    apk=\"Electrum-$VERSION-$arch-release.apk\"\n    apk_unsigned=\"Electrum-$VERSION-$arch-release-unsigned.apk\"\n    if test -f \"dist/$apk\"; then\n        info \"file exists: $apk\"\n    else\n        info \"file does not exists: $apk\"\n        if [ ! -z \"$RELEASEMANAGER\" ] ; then\n            ./contrib/android/build.sh qml $arch release $password\n        else\n            ./contrib/android/build.sh qml $arch release-unsigned\n            mv \"dist/$apk_unsigned\" \"dist/$apk\"\n        fi\n    fi\ndone\n\n# the macos binary is built on a separate machine.\n# the file that needs to be copied over is the codesigned release binary (regardless of builder role)\ndmg=\"electrum-$VERSION.dmg\"\nif ! test -f \"dist/$dmg\"; then\n    if [ ! -z \"$RELEASEMANAGER\" ] ; then  # RM\n        fail \"dmg is missing, aborting. Please build and codesign the dmg on a mac and copy it over.\"\n    else  # other builders\n        fail \"dmg is missing, aborting. Please build the unsigned dmg on a mac, compare it with file built by RM, and if matches, copy RM's dmg.\"\n    fi\nfi\n\n# now that we have all binaries, if we are the RM, sign them.\nif [ ! -z \"$RELEASEMANAGER\" ] ; then\n    if test -f \"dist/$dmg.asc\"; then\n        info \"packages are already signed\"\n    else\n        info \"signing packages\"\n        ./contrib/sign_packages \"$GPGUSER\"\n    fi\nfi\n\ninfo \"build complete\"\nsha256sum dist/*.tar.gz\nsha256sum dist/*.AppImage\nsha256sum contrib/build-wine/dist/*.exe\n\necho -n \"proceed (y/n)? \"\nread answer\n\nif [ \"$answer\" != \"y\" ]; then\n    echo \"exit\"\n    exit 1\nfi\n\n\nif [ -z \"$RELEASEMANAGER\" ] ; then\n    # people OTHER THAN release manager.\n    # download binaries built by RM\n    rm -rf \"$PROJECT_ROOT/dist/releasemanager\"\n    mkdir --parent \"$PROJECT_ROOT/dist/releasemanager\"\n    cd \"$PROJECT_ROOT/dist/releasemanager\"\n\n    if [ -z \"$SSHUSER\" ]; then\n        info \"No SFTP access, downloading binaries from website\"\n        BASE_URL=\"https://download.electrum.org/$VERSION\"\n        FILES_TO_DOWNLOAD=(\n            \"$tarball\"\n            \"$srctarball\"\n            \"$appimage\"\n            \"$win1\"\n            \"$win2\"\n            \"$win3\"\n            \"$apk1\"\n            \"$apk2\"\n            \"$apk3\"\n            \"$dmg\"\n        )\n\n        for filename in \"${FILES_TO_DOWNLOAD[@]}\"; do\n            if [ ! -f \"$filename\" ]; then\n                info \"Downloading $filename...\"\n                wget -q \"$BASE_URL/$filename\" -O \"$filename\" || fail \"Failed to download $filename\"\n            else\n                info \"File already exists: $filename\"\n            fi\n        done\n    else\n        # TODO check somehow that RM had finished uploading\n        sftp -oBatchMode=no -b - \"$SSHUSER@uploadserver\" <<-EOF\n           cd electrum-downloads-airlock\n           cd \"$VERSION\"\n           mget *\n           bye\nEOF\n    fi\n\n    # check we have each binary\n    test -f \"$tarball\"    || fail \"tarball not found among sftp downloads\"\n    test -f \"$srctarball\" || fail \"srctarball not found among sftp downloads\"\n    test -f \"$appimage\"   || fail \"appimage not found among sftp downloads\"\n    test -f \"$win1\"       || fail \"win1 not found among sftp downloads\"\n    test -f \"$win2\"       || fail \"win2 not found among sftp downloads\"\n    test -f \"$win3\"       || fail \"win3 not found among sftp downloads\"\n    test -f \"$apk1\"       || fail \"apk1 not found among sftp downloads\"\n    test -f \"$apk2\"       || fail \"apk2 not found among sftp downloads\"\n    test -f \"$apk3\"       || fail \"apk3 not found among sftp downloads\"\n    test -f \"$dmg\"        || fail \"dmg not found among sftp downloads\"\n    test -f \"$PROJECT_ROOT/dist/$tarball\"    || fail \"tarball not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$srctarball\" || fail \"srctarball not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$appimage\"   || fail \"appimage not found among built files\"\n    test -f \"$CONTRIB/build-wine/dist/$win1\" || fail \"win1 not found among built files\"\n    test -f \"$CONTRIB/build-wine/dist/$win2\" || fail \"win2 not found among built files\"\n    test -f \"$CONTRIB/build-wine/dist/$win3\" || fail \"win3 not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$apk1\"       || fail \"apk1 not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$apk2\"       || fail \"apk2 not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$apk3\"       || fail \"apk3 not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$dmg\"        || fail \"dmg not found among built files\"\n    # compare downloaded binaries against ones we built\n    cmp --silent \"$tarball\"    \"$PROJECT_ROOT/dist/$tarball\"    || fail \"files are different. tarball.\"\n    cmp --silent \"$srctarball\" \"$PROJECT_ROOT/dist/$srctarball\" || fail \"files are different. srctarball.\"\n    cmp --silent \"$appimage\"   \"$PROJECT_ROOT/dist/$appimage\"   || fail \"files are different. appimage.\"\n    rm -rf \"$CONTRIB/build-wine/signed/\" && mkdir --parents \"$CONTRIB/build-wine/signed/\"\n    cp -f \"$win1\" \"$win2\" \"$win3\" \"$CONTRIB/build-wine/signed/\"\n    \"$CONTRIB/build-wine/unsign.sh\" || fail \"files are different. windows.\"\n    \"$CONTRIB/android/apkdiff.py\" \"$apk1\" \"$PROJECT_ROOT/dist/$apk1\" || fail \"files are different. android.\"\n    \"$CONTRIB/android/apkdiff.py\" \"$apk2\" \"$PROJECT_ROOT/dist/$apk2\" || fail \"files are different. android.\"\n    \"$CONTRIB/android/apkdiff.py\" \"$apk3\" \"$PROJECT_ROOT/dist/$apk3\" || fail \"files are different. android.\"\n    cmp --silent \"$dmg\" \"$PROJECT_ROOT/dist/$dmg\" || fail \"files are different. macos.\"\n    # all files matched. sign them.\n    rm -rf \"$PROJECT_ROOT/dist/sigs/\"\n    mkdir --parents \"$PROJECT_ROOT/dist/sigs/\"\n    for fname in \"$tarball\" \"$srctarball\" \"$appimage\" \"$win1\" \"$win2\" \"$win3\" \"$apk1\" \"$apk2\" \"$apk3\" \"$dmg\" ; do\n        signame=\"$fname.$GPGUSER.asc\"\n        gpg --sign --armor --detach $PUBKEY --output \"$PROJECT_ROOT/dist/sigs/$signame\" \"$fname\"\n    done\n\n    if [ -z \"$SSHUSER\" ]; then\n        info \"Signing successfully, now open a pull request with your signatures to spesmilo/electrum-signatures\"\n        exit 0\n    else\n        # upload sigs\n        ELECBUILD_UPLOADFROM=\"$PROJECT_ROOT/dist/sigs/\" \"$CONTRIB/upload.sh\"\n    fi\n\nelse\n    # ONLY release manager\n\n    cd \"$PROJECT_ROOT\"\n\n    # check we have each binary\n    test -f \"$PROJECT_ROOT/dist/$tarball\"    || fail \"tarball not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$srctarball\" || fail \"srctarball not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$appimage\"   || fail \"appimage not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$win1\"       || fail \"win1 not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$win2\"       || fail \"win2 not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$win3\"       || fail \"win3 not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$apk1\"       || fail \"apk1 not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$apk2\"       || fail \"apk2 not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$apk3\"       || fail \"apk3 not found among built files\"\n    test -f \"$PROJECT_ROOT/dist/$dmg\"        || fail \"dmg not found among built files\"\n\n    if [ \"$REV\" != \"$VERSION\" ]; then\n        fail \"versions differ, not uploading\"\n    fi\n\n    # upload the files\n    ./contrib/upload.sh\n\nfi\n\nset +x\n\ninfo \"release.sh finished successfully.\"\ninfo \"After two people ran release.sh, the binaries will be publicly available on uploadserver.\"\ninfo \"Then, we wait for additional signers, and run add_cosigner for them.\"\ninfo \"Finally, release_www.sh needs to be run, for the website to be updated.\"\n"
  },
  {
    "path": "contrib/release_www.sh",
    "content": "#!/bin/bash\n#\n# env vars:\n# - WWW_DIR: path to \"electrum-web\" git clone\n# - for signing the version announcement file:\n#   - ELECTRUM_SIGNING_ADDRESS (required)\n#   - ELECTRUM_SIGNING_WALLET (required)\n#\n\nset -e\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/..\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\n\ncd \"$PROJECT_ROOT\"\n\n. \"$CONTRIB\"/build_tools_util.sh\n\n\necho -n \"Remember to run add_cosigner to add any additional sigs.  Continue (y/n)? \"\nread answer\nif [ \"$answer\" != \"y\" ]; then\n    echo \"exit\"\n    exit 1\nfi\n\n\nif [ -z \"$WWW_DIR\" ] ; then\n    WWW_DIR=/opt/electrum-web\nfi\n\nif [ -z \"$ELECTRUM_SIGNING_WALLET\" ] || [ -z \"$ELECTRUM_SIGNING_ADDRESS\" ]; then\n    echo \"You need to set env vars ELECTRUM_SIGNING_WALLET and ELECTRUM_SIGNING_ADDRESS!\"\n    exit 1\nfi\n\nVERSION=$(\"$CONTRIB\"/print_electrum_version.py)\ninfo \"VERSION: $VERSION\"\n\nANDROID_VERSIONCODE_NULLARCH=$(\"$CONTRIB\"/android/get_apk_versioncode.py \"null\")\n# ^ note: should parse as an integer in the final json\ninfo \"ANDROID_VERSIONCODE_NULLARCH: $ANDROID_VERSIONCODE_NULLARCH\"\n\nset -x\n\ninfo \"updating www repo\"\n./contrib/make_download \"$WWW_DIR\"\ninfo \"signing the version announcement file\"\nsig=$(./run_electrum -o signmessage \"$ELECTRUM_SIGNING_ADDRESS\" \"$VERSION\" -w \"$ELECTRUM_SIGNING_WALLET\")\n# note: the contents of \"extradata\" are currently not signed. We could add another field, extradata_sigs,\n#       containing signature(s) for \"extradata\". extradata, being json, would have to be canonically\n#       serialized before signing.\ncat <<EOF > \"$WWW_DIR\"/version\n{\n    \"version\": \"$VERSION\",\n    \"signatures\": {\"$ELECTRUM_SIGNING_ADDRESS\": \"$sig\"},\n    \"extradata\": {\n        \"android_versioncode_nullarch\": $ANDROID_VERSIONCODE_NULLARCH\n    }\n}\nEOF\n\n# push changes to website repo\npushd \"$WWW_DIR\"\ngit diff\ngit commit -a -m \"version $VERSION\"\ngit push\npopd\n\n\ninfo \"release_www.sh finished successfully.\"\ninfo \"now you should run WWW_DIR/publish.sh to sign the website commit and upload signature\"\n"
  },
  {
    "path": "contrib/requirements/requirements-binaries-mac.txt",
    "content": "# Qt 6.8 would require macOS 12+,  6.7 still supports macOS 11\n# Qt 6.7 has issue \"No QtMultimedia backends found.\" (i.e. camera does not work)\n# PyQt6-Qt6==6.6.3 segfaults with \"illegal hardware instruction\"\nPyQt6<6.7\nPyQt6-Qt6<6.7,!=6.6.3\n\ncryptography>=2.6\n"
  },
  {
    "path": "contrib/requirements/requirements-binaries.txt",
    "content": "PyQt6\n\n# we need at least cryptography>=2.1 for electrum.crypto,\n# and at least cryptography>=2.6 for dnspython[DNSSEC]\ncryptography>=2.6\n"
  },
  {
    "path": "contrib/requirements/requirements-build-android.txt",
    "content": "pip\nsetuptools\nwheel\n\n# needed by buildozer:\npexpect\nsh\n# some p4a recipes don't work with cython 3+\ncython<3.0\n\n# needed by python-for-android:\nappdirs\n# colorama upper bound to avoid needing hatchling\ncolorama>=0.3.3,<0.4.6\njinja2\nsh>=1.10\npep517\ntoml\n\n# needed for the Qt/QML Android GUI:\n# TODO double-check this\ntyping-extensions\n"
  },
  {
    "path": "contrib/requirements/requirements-build-appimage.txt",
    "content": "pip\nsetuptools\nwheel\n\n# Note: hidapi requires Cython at build-time (not needed at runtime).\n# For reproducible builds, the version of Cython must be pinned down.\n# The pinned Cython must be installed before hidapi is built;\n# otherwise when installing hidapi, pip just downloads the latest Cython.\n# see https://github.com/spesmilo/electrum/issues/5859\nCython>=0.27"
  },
  {
    "path": "contrib/requirements/requirements-build-base.txt",
    "content": "# This file contains build-time dependencies needed to build other higher level build-time dependencies\n# and runtime dependencies.\n# For reproducibility, some build-time deps, most notably \"wheel\", need to be pinned. (see #7640)\n# By default, when doing e.g. \"pip install\", pip downloads the latest version of wheel (and setuptools, etc),\n# regardless whether a sufficiently recent version of wheel is already installed locally...\n# The only way I have found to avoid this, is to use the \"--no-build-isolation\" flag,\n# in which case it becomes our responsibility to install *all* build time deps...\n\npip\nsetuptools\nwheel\n\n# importlib_metadata also needs:\n# https://github.com/python/importlib_metadata/blob/1e2381fe101fd70742a0171e51c1be82aedf519b/pyproject.toml#L2\nsetuptools_scm[toml]>=3.4.1\n# from https://github.com/pypa/setuptools-scm/commit/c766df10c18c3c5a6b5741e9f372e193412c0f69 :\n# (but also to avoid the binary wheels introduced in tomli 2.2)\ntomli<=2.0.2\n\n# dnspython also needs:\n# https://github.com/rthalley/dnspython/blob/1a7c14fb6c200be02ef5c2f3bb9fd84b85004459/pyproject.toml#L64\npoetry-core\n\n# typing-extensions also needs:\n# https://github.com/python/typing/blob/a2371460d184c96aab7a69acc47fd059f875e3b4/typing_extensions/pyproject.toml#L3\nflit_core>=3.4,<4\n\n# aio-libs/frozenlist and aio-libs/propcache needs:\n# https://github.com/aio-libs/frozenlist/blob/c28f32d6816ca0fa56a5876e84831c46084bb85d/pyproject.toml#L6\nexpandvars\n"
  },
  {
    "path": "contrib/requirements/requirements-build-mac.txt",
    "content": "pip\nsetuptools\nwheel\n\n# needed by pyinstaller:\n# fixme: ugly to have to duplicate this here from upstream\nmacholib>=1.8\naltgraph\npyinstaller-hooks-contrib>=2025.2\npackaging>=22.0\n\n# Note: hidapi requires Cython at build-time (not needed at runtime).\n# For reproducible builds, the version of Cython must be pinned down.\n# The pinned Cython must be installed before hidapi is built;\n# otherwise when installing hidapi, pip just downloads the latest Cython.\n# see https://github.com/spesmilo/electrum/issues/5859\nCython>=0.27\n"
  },
  {
    "path": "contrib/requirements/requirements-build-wine.txt",
    "content": "pip\nsetuptools\nwheel\n\n# needed by pyinstaller:\n# fixme: ugly to have to duplicate this here from upstream\npefile>=2022.5.30,!=2024.8.26\naltgraph\npywin32-ctypes>=0.2.1\npyinstaller-hooks-contrib>=2025.2\npackaging>=22.0\n"
  },
  {
    "path": "contrib/requirements/requirements-ci.txt",
    "content": "pytest\ncoverage\ncoveralls\n"
  },
  {
    "path": "contrib/requirements/requirements-hw.txt",
    "content": "hidapi\n\n# device plugin: trezor\ntrezor[hidapi]>=0.13.0,<0.14\n\n# device plugin: safe_t\nsafet>=0.1.5\n\n# device plugin: keepkey\necdsa>=0.9\nprotobuf>=3.20\nmnemonic>=0.8\nhidapi>=0.7.99.post15\nlibusb1>=1.6\n\n# device plugin: ledger\nledger-bitcoin>=0.2.0,<1.0\nhidapi\n\n# device plugin: coldcard\nckcc-protocol>=0.7.7\n\n# device plugin: bitbox02\nbitbox02>=7.0.0\n\n# device plugin: jade\ncbor2>=5.4.6,<6.0.0\npyserial>=3.5.0,<4.0.0\n\n# prefer older urllib3 to avoid needing hatchling\n# (pulled in via trezor -> requests -> urllib3)\nurllib3<2\n"
  },
  {
    "path": "contrib/requirements/requirements.txt",
    "content": "qrcode\nprotobuf>=3.20\nqdarkstyle>=3.2\naiorpcx>=0.25.0,<0.26\naiohttp>=3.11.0,<4.0.0\naiohttp_socks>=0.9.2\ncertifi\njsonpatch\nelectrum_ecc>=0.0.4,<0.1\nelectrum_aionostr>=0.1.0,<0.2\n\n# - upper limit to avoid needing hatchling at build-time :/\n#   (however newer versions should work at runtime)\nattrs>=20.1.0,<23\n\n# Note that we also need the dnspython[DNSSEC] extra which pulls in cryptography,\n# but as that is not pure-python it cannot be listed in this file!\n# - upper limit to avoid needing hatchling at build-time :/\n#   (however newer versions should work at runtime)\ndnspython>=2.2,<2.5\n"
  },
  {
    "path": "contrib/sign_packages",
    "content": "#!/usr/bin/env python3\nimport os, sys\n\nif __name__ == '__main__':\n    username = sys.argv[1]\n    os.chdir(\"dist\")\n    for fname in os.listdir('.'):\n        if fname.endswith('asc'):\n            continue\n        sig_name = fname + '.' + username + '.asc'\n        os.system(f\"gpg --sign --armor --detach --output {sig_name} {fname}\")\n    os.chdir(\"..\")\n"
  },
  {
    "path": "contrib/trigger_deploy.sh",
    "content": "#!/bin/bash\n# Triggers deploy.sh to maybe update the website or move binaries.\n# uploadserver needs to be defined in /etc/hosts\n\nSSHUSER=$1\nTRIGGERVERSION=$2\nif [ -z \"$SSHUSER\" ] || [ -z \"$TRIGGERVERSION\" ]; then\n    echo \"usage: $0 SSHUSER TRIGGERVERSION\"\n    echo \"e.g. $0 thomasv 3.0.0\"\n    echo \"e.g. $0 thomasv website\"\n    exit 1\nfi\nset -ex\ncd \"$(dirname \"$0\")\"\n\nif [ \"$TRIGGERVERSION\" == \"website\" ]; then\n    rm -f trigger_website\n    touch trigger_website\n    echo \"uploading file: trigger_website...\"\n    sftp -oBatchMode=no -b - \"$SSHUSER@uploadserver\" << !\n       cd electrum-downloads-airlock\n       mput trigger_website\n       bye\n!\nelse\n    rm -f trigger_binaries\n    printf \"$TRIGGERVERSION\" > trigger_binaries\n    echo \"uploading file: trigger_binaries...\"\n    sftp -oBatchMode=no -b - \"$SSHUSER@uploadserver\" << !\n       cd electrum-downloads-airlock\n       mput trigger_binaries\n       bye\n!\nfi\n\n"
  },
  {
    "path": "contrib/udev/20-hw1.rules",
    "content": "# HW.1, Nano\nSUBSYSTEMS==\"usb\", ATTRS{idVendor}==\"2581\", ATTRS{idProduct}==\"1b7c|2b7c|3b7c|4b7c\", TAG+=\"uaccess\", TAG+=\"udev-acl\"\n\n# Blue, NanoS, Aramis, HW.2, Nano X, NanoSP, Stax, Ledger Test,\nSUBSYSTEMS==\"usb\", ATTRS{idVendor}==\"2c97\", TAG+=\"uaccess\", TAG+=\"udev-acl\"\n\n# Same, but with hidraw-based library (instead of libusb)\nKERNEL==\"hidraw*\", ATTRS{idVendor}==\"2c97\", MODE=\"0666\"\n"
  },
  {
    "path": "contrib/udev/51-coinkite.rules",
    "content": "# Linux udev support file.\n#\n# This is a example udev file for HIDAPI devices which changes the permissions\n# to 0666 (world readable/writable) for a specific device on Linux systems.\n#\n# - Copy this file into /etc/udev/rules.d and unplug and re-plug your Coldcard.\n# - Udev does not have to be restarted.\n#\n\n# probably not needed:\nSUBSYSTEMS==\"usb\", ATTRS{idVendor}==\"d13e\", ATTRS{idProduct}==\"cc10\", GROUP=\"plugdev\", MODE=\"0666\"\n\n# required:\n# from <https://github.com/signal11/hidapi/blob/master/udev/99-hid.rules>\nKERNEL==\"hidraw*\", ATTRS{idVendor}==\"d13e\", ATTRS{idProduct}==\"cc10\", GROUP=\"plugdev\", MODE=\"0666\"\n\n"
  },
  {
    "path": "contrib/udev/51-hid-digitalbitbox.rules",
    "content": "SUBSYSTEM==\"usb\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"dbb%n\", ATTRS{idVendor}==\"03eb\", ATTRS{idProduct}==\"2402\"\n"
  },
  {
    "path": "contrib/udev/51-safe-t.rules",
    "content": "# Put this file into /usr/lib/udev/rules.d or /etc/udev/rules.d\n\n# Archos Safe-T mini\nSUBSYSTEM==\"usb\", ATTR{idVendor}==\"0e79\", ATTR{idProduct}==\"6000\", MODE=\"0660\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"safe-tr%n\"\nKERNEL==\"hidraw*\", ATTRS{idVendor}==\"0e79\", ATTRS{idProduct}==\"6000\",  MODE=\"0660\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\"\n\n# Archos Safe-T mini Bootloader\nSUBSYSTEM==\"usb\", ATTR{idVendor}==\"0e79\", ATTR{idProduct}==\"6001\", MODE=\"0660\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"safe-t%n\"\nKERNEL==\"hidraw*\", ATTRS{idVendor}==\"0e79\", ATTRS{idProduct}==\"6001\",  MODE=\"0660\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\"\n\n"
  },
  {
    "path": "contrib/udev/51-trezor.rules",
    "content": "# Trezor: The Original Hardware Wallet\n# https://trezor.io/\n#\n# Put this file into /etc/udev/rules.d\n#\n# If you are creating a distribution package,\n# put this into /usr/lib/udev/rules.d or /lib/udev/rules.d\n# depending on your distribution\n\n# Trezor\nSUBSYSTEM==\"usb\", ATTR{idVendor}==\"534c\", ATTR{idProduct}==\"0001\", MODE=\"0660\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"trezor%n\"\nKERNEL==\"hidraw*\", ATTRS{idVendor}==\"534c\", ATTRS{idProduct}==\"0001\", MODE=\"0660\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\"\n\n# Trezor v2\nSUBSYSTEM==\"usb\", ATTR{idVendor}==\"1209\", ATTR{idProduct}==\"53c0\", MODE=\"0660\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"trezor%n\"\nSUBSYSTEM==\"usb\", ATTR{idVendor}==\"1209\", ATTR{idProduct}==\"53c1\", MODE=\"0660\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"trezor%n\"\nKERNEL==\"hidraw*\", ATTRS{idVendor}==\"1209\", ATTRS{idProduct}==\"53c1\", MODE=\"0660\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\"\n"
  },
  {
    "path": "contrib/udev/51-usb-keepkey.rules",
    "content": "# KeepKey: Your Private Bitcoin Vault\n# http://www.keepkey.com/\n# Put this file into /usr/lib/udev/rules.d or /etc/udev/rules.d\n\n# KeepKey HID Firmware/Bootloader\nSUBSYSTEM==\"usb\", ATTR{idVendor}==\"2b24\", ATTR{idProduct}==\"0001\", MODE=\"0666\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"keepkey%n\"\nKERNEL==\"hidraw*\", ATTRS{idVendor}==\"2b24\", ATTRS{idProduct}==\"0001\",  MODE=\"0666\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\"\n\n# KeepKey WebUSB Firmware/Bootloader\nSUBSYSTEM==\"usb\", ATTR{idVendor}==\"2b24\", ATTR{idProduct}==\"0002\", MODE=\"0666\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"keepkey%n\"\nKERNEL==\"hidraw*\", ATTRS{idVendor}==\"2b24\", ATTRS{idProduct}==\"0002\",  MODE=\"0666\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\"\n"
  },
  {
    "path": "contrib/udev/52-hid-digitalbitbox.rules",
    "content": "KERNEL==\"hidraw*\", SUBSYSTEM==\"hidraw\", ATTRS{idVendor}==\"03eb\", ATTRS{idProduct}==\"2402\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"dbbf%n\"\n"
  },
  {
    "path": "contrib/udev/53-hid-bitbox02.rules",
    "content": "SUBSYSTEM==\"usb\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"bitbox02_%n\", ATTRS{idVendor}==\"03eb\", ATTRS{idProduct}==\"2403\"\n"
  },
  {
    "path": "contrib/udev/54-hid-bitbox02.rules",
    "content": "KERNEL==\"hidraw*\", SUBSYSTEM==\"hidraw\", ATTRS{idVendor}==\"03eb\", ATTRS{idProduct}==\"2403\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"bitbox02-%n\"\n"
  },
  {
    "path": "contrib/udev/55-usb-jade.rules",
    "content": "KERNEL==\"ttyUSB*\", SUBSYSTEMS==\"usb\", ATTRS{idVendor}==\"10c4\", ATTRS{idProduct}==\"ea60\", MODE=\"0660\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"jade%n\"\nKERNEL==\"ttyACM*\", SUBSYSTEMS==\"usb\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d4\", MODE=\"0660\", GROUP=\"plugdev\", TAG+=\"uaccess\", TAG+=\"udev-acl\", SYMLINK+=\"jade%n\"\n"
  },
  {
    "path": "contrib/udev/README.md",
    "content": "# udev rules\n\nThis directory contains all of the udev rules for the supported devices\nas retrieved from vendor websites and repositories.\nThese are necessary for the devices to be usable on Linux environments.\n\n - `20-hw1.rules` (Ledger): https://github.com/LedgerHQ/udev-rules/blob/master/20-hw1.rules\n - `51-coinkite.rules` (Coldcard): https://github.com/Coldcard/ckcc-protocol/blob/master/51-coinkite.rules\n - `51-hid-digitalbitbox.rules`, `52-hid-digitalbitbox.rules` (Digital Bitbox): https://github.com/digitalbitbox/bitbox-wallet-app/blob/master/frontends/qt/resources/deb-afterinstall.sh\n - `53-hid-bitbox02.rules`, `54-hid-bitbox02.rules` (BitBox02): https://github.com/digitalbitbox/bitbox-wallet-app/blob/master/frontends/qt/resources/deb-afterinstall.sh\n - `51-trezor.rules` (Trezor): https://github.com/trezor/trezor-common/blob/master/udev/51-trezor.rules\n - `51-usb-keepkey.rules` (Keepkey): https://github.com/keepkey/udev-rules/blob/master/51-usb-keepkey.rules\n - `51-safe-t.rules` (Archos): https://github.com/archos-safe-t/safe-t-common/blob/master/udev/51-safe-t.rules\n - `55-usb-jade.rules` (Blockstream Jade): https://github.com/Blockstream/Jade\n\n# Usage\n\nApply these rules by copying them to `/etc/udev/rules.d/` and notifying `udevadm`.\nYour user will need to be added to the `plugdev` group, which needs to be created if it does not already exist.\n\n```\n$ sudo groupadd plugdev\n$ sudo usermod -aG plugdev $(whoami)\n$ sudo cp contrib/udev/*.rules /etc/udev/rules.d/\n$ sudo udevadm control --reload-rules && sudo udevadm trigger\n```\n"
  },
  {
    "path": "contrib/upload.sh",
    "content": "#!/bin/bash\n# uploadserver is set in /etc/hosts\n#\n# env vars:\n# - ELECBUILD_UPLOADFROM\n# - SSHUSER\n\nset -ex\n\nPROJECT_ROOT=\"$(dirname \"$(readlink -e \"$0\")\")/..\"\nCONTRIB=\"$PROJECT_ROOT/contrib\"\n\nif [ -z \"$SSHUSER\" ]; then\n    SSHUSER=thomasv\nfi\n\ncd \"$PROJECT_ROOT\"\n\nVERSION=$(\"$CONTRIB\"/print_electrum_version.py)\necho \"$VERSION\"\n\nif [ -z \"$ELECBUILD_UPLOADFROM\" ]; then\n    cd \"$PROJECT_ROOT/dist\"\nelse\n    cd \"$ELECBUILD_UPLOADFROM\"\nfi\n\n\n# do not fail sftp if directory exists\n# see https://stackoverflow.com/questions/51437924/bash-shell-sftp-check-if-directory-exists-before-creating\n\nsftp -oBatchMode=no -b - \"$SSHUSER@uploadserver\" << !\n   cd electrum-downloads-airlock\n   -mkdir \"$VERSION\"\n   -chmod 777 \"$VERSION\"\n   cd \"$VERSION\"\n   -mput *\n   -chmod 444 *  # this prevents future re-uploads of same file\n   bye\n!\n\n\"$CONTRIB/trigger_deploy.sh\" \"$SSHUSER\" \"$VERSION\"\n"
  },
  {
    "path": "electrum/__init__.py",
    "content": "import sys\nimport os\n\n# these are ~duplicated from run_electrum:\nis_bundle = getattr(sys, 'frozen', False)\nis_local = not is_bundle and os.path.exists(os.path.join(os.path.dirname(os.path.dirname(__file__)), \"electrum.desktop\"))\n\n# when running from source, on Windows, also search for DLLs in inner 'electrum' folder\nif is_local and os.name == 'nt':  # fixme: duplicated between main script and __init__.py :(\n    os.add_dll_directory(os.path.dirname(__file__))\n\n\nclass GuiImportError(ImportError):\n    pass\n\n\nfrom .version import ELECTRUM_VERSION\nfrom .util import format_satoshis\nfrom .wallet import Wallet\nfrom .storage import WalletStorage\nfrom .coinchooser import COIN_CHOOSERS\nfrom .network import Network, pick_random_server\nfrom .interface import Interface\nfrom .simple_config import SimpleConfig\nfrom . import bitcoin\nfrom . import transaction\nfrom . import daemon\nfrom .transaction import Transaction\nfrom .plugin import BasePlugin\nfrom .commands import Commands, known_commands\nfrom .logging import get_logger\n\n\n__version__ = ELECTRUM_VERSION\n\n_logger = get_logger(__name__)\n\n\n# Ensure that asserts are enabled. For sanity and paranoia, we require this.\n# Code *should not rely* on asserts being enabled. In particular, safety and security checks should\n# always explicitly raise exceptions. However, this rule is mistakenly broken occasionally...\ntry:\n    assert False  # noqa: B011\nexcept AssertionError:\n    pass\nelse:\n    raise ImportError(\"Running with asserts disabled. Refusing to continue. Exiting...\")\n\n\n# Check that os.urandom works\nimport zlib\nlength = len(zlib.compress(os.urandom(1000)))\nif length <= 900:\n    raise ImportError(\"Broken PRNG. Refusing to continue. Exiting...\")\n\n"
  },
  {
    "path": "electrum/_vendor/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/_vendor/distutils/LICENSE",
    "content": "A. HISTORY OF THE SOFTWARE\n==========================\n\nPython was created in the early 1990s by Guido van Rossum at Stichting\nMathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands\nas a successor of a language called ABC.  Guido remains Python's\nprincipal author, although it includes many contributions from others.\n\nIn 1995, Guido continued his work on Python at the Corporation for\nNational Research Initiatives (CNRI, see http://www.cnri.reston.va.us)\nin Reston, Virginia where he released several versions of the\nsoftware.\n\nIn May 2000, Guido and the Python core development team moved to\nBeOpen.com to form the BeOpen PythonLabs team.  In October of the same\nyear, the PythonLabs team moved to Digital Creations, which became\nZope Corporation.  In 2001, the Python Software Foundation (PSF, see\nhttps://www.python.org/psf/) was formed, a non-profit organization\ncreated specifically to own Python-related Intellectual Property.\nZope Corporation was a sponsoring member of the PSF.\n\nAll Python releases are Open Source (see http://www.opensource.org for\nthe Open Source Definition).  Historically, most, but not all, Python\nreleases have also been GPL-compatible; the table below summarizes\nthe various releases.\n\n    Release         Derived     Year        Owner       GPL-\n                    from                                compatible? (1)\n\n    0.9.0 thru 1.2              1991-1995   CWI         yes\n    1.3 thru 1.5.2  1.2         1995-1999   CNRI        yes\n    1.6             1.5.2       2000        CNRI        no\n    2.0             1.6         2000        BeOpen.com  no\n    1.6.1           1.6         2001        CNRI        yes (2)\n    2.1             2.0+1.6.1   2001        PSF         no\n    2.0.1           2.0+1.6.1   2001        PSF         yes\n    2.1.1           2.1+2.0.1   2001        PSF         yes\n    2.1.2           2.1.1       2002        PSF         yes\n    2.1.3           2.1.2       2002        PSF         yes\n    2.2 and above   2.1.1       2001-now    PSF         yes\n\nFootnotes:\n\n(1) GPL-compatible doesn't mean that we're distributing Python under\n    the GPL.  All Python licenses, unlike the GPL, let you distribute\n    a modified version without making your changes open source.  The\n    GPL-compatible licenses make it possible to combine Python with\n    other software that is released under the GPL; the others don't.\n\n(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,\n    because its license has a choice of law clause.  According to\n    CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1\n    is \"not incompatible\" with the GPL.\n\nThanks to the many outside volunteers who have worked under Guido's\ndirection to make these releases possible.\n\n\nB. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON\n===============================================================\n\nPython software and documentation are licensed under the\nPython Software Foundation License Version 2.\n\nStarting with Python 3.8.6, examples, recipes, and other code in\nthe documentation are dual licensed under the PSF License Version 2\nand the Zero-Clause BSD license.\n\nSome software incorporated into Python is under different licenses.\nThe licenses are listed with code falling under that license.\n\n\nPYTHON SOFTWARE FOUNDATION LICENSE VERSION 2\n--------------------------------------------\n\n1. This LICENSE AGREEMENT is between the Python Software Foundation\n(\"PSF\"), and the Individual or Organization (\"Licensee\") accessing and\notherwise using this software (\"Python\") in source or binary form and\nits associated documentation.\n\n2. Subject to the terms and conditions of this License Agreement, PSF hereby\ngrants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,\nanalyze, test, perform and/or display publicly, prepare derivative works,\ndistribute, and otherwise use Python alone or in any derivative version,\nprovided, however, that PSF's License Agreement and PSF's notice of copyright,\ni.e., \"Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,\n2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022 Python Software Foundation;\nAll Rights Reserved\" are retained in Python alone or in any derivative version\nprepared by Licensee.\n\n3. In the event Licensee prepares a derivative work that is based on\nor incorporates Python or any part thereof, and wants to make\nthe derivative work available to others as provided herein, then\nLicensee hereby agrees to include in any such work a brief summary of\nthe changes made to Python.\n\n4. PSF is making Python available to Licensee on an \"AS IS\"\nbasis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR\nIMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND\nDISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS\nFOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT\nINFRINGE ANY THIRD PARTY RIGHTS.\n\n5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON\nFOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS\nA RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,\nOR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.\n\n6. This License Agreement will automatically terminate upon a material\nbreach of its terms and conditions.\n\n7. Nothing in this License Agreement shall be deemed to create any\nrelationship of agency, partnership, or joint venture between PSF and\nLicensee.  This License Agreement does not grant permission to use PSF\ntrademarks or trade name in a trademark sense to endorse or promote\nproducts or services of Licensee, or any third party.\n\n8. By copying, installing or otherwise using Python, Licensee\nagrees to be bound by the terms and conditions of this License\nAgreement.\n\n\nBEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0\n-------------------------------------------\n\nBEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1\n\n1. This LICENSE AGREEMENT is between BeOpen.com (\"BeOpen\"), having an\noffice at 160 Saratoga Avenue, Santa Clara, CA 95051, and the\nIndividual or Organization (\"Licensee\") accessing and otherwise using\nthis software in source or binary form and its associated\ndocumentation (\"the Software\").\n\n2. Subject to the terms and conditions of this BeOpen Python License\nAgreement, BeOpen hereby grants Licensee a non-exclusive,\nroyalty-free, world-wide license to reproduce, analyze, test, perform\nand/or display publicly, prepare derivative works, distribute, and\notherwise use the Software alone or in any derivative version,\nprovided, however, that the BeOpen Python License is retained in the\nSoftware, alone or in any derivative version prepared by Licensee.\n\n3. BeOpen is making the Software available to Licensee on an \"AS IS\"\nbasis.  BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR\nIMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND\nDISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS\nFOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT\nINFRINGE ANY THIRD PARTY RIGHTS.\n\n4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE\nSOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS\nAS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY\nDERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.\n\n5. This License Agreement will automatically terminate upon a material\nbreach of its terms and conditions.\n\n6. This License Agreement shall be governed by and interpreted in all\nrespects by the law of the State of California, excluding conflict of\nlaw provisions.  Nothing in this License Agreement shall be deemed to\ncreate any relationship of agency, partnership, or joint venture\nbetween BeOpen and Licensee.  This License Agreement does not grant\npermission to use BeOpen trademarks or trade names in a trademark\nsense to endorse or promote products or services of Licensee, or any\nthird party.  As an exception, the \"BeOpen Python\" logos available at\nhttp://www.pythonlabs.com/logos.html may be used according to the\npermissions granted on that web page.\n\n7. By copying, installing or otherwise using the software, Licensee\nagrees to be bound by the terms and conditions of this License\nAgreement.\n\n\nCNRI LICENSE AGREEMENT FOR PYTHON 1.6.1\n---------------------------------------\n\n1. This LICENSE AGREEMENT is between the Corporation for National\nResearch Initiatives, having an office at 1895 Preston White Drive,\nReston, VA 20191 (\"CNRI\"), and the Individual or Organization\n(\"Licensee\") accessing and otherwise using Python 1.6.1 software in\nsource or binary form and its associated documentation.\n\n2. Subject to the terms and conditions of this License Agreement, CNRI\nhereby grants Licensee a nonexclusive, royalty-free, world-wide\nlicense to reproduce, analyze, test, perform and/or display publicly,\nprepare derivative works, distribute, and otherwise use Python 1.6.1\nalone or in any derivative version, provided, however, that CNRI's\nLicense Agreement and CNRI's notice of copyright, i.e., \"Copyright (c)\n1995-2001 Corporation for National Research Initiatives; All Rights\nReserved\" are retained in Python 1.6.1 alone or in any derivative\nversion prepared by Licensee.  Alternately, in lieu of CNRI's License\nAgreement, Licensee may substitute the following text (omitting the\nquotes): \"Python 1.6.1 is made available subject to the terms and\nconditions in CNRI's License Agreement.  This Agreement together with\nPython 1.6.1 may be located on the internet using the following\nunique, persistent identifier (known as a handle): 1895.22/1013.  This\nAgreement may also be obtained from a proxy server on the internet\nusing the following URL: http://hdl.handle.net/1895.22/1013\".\n\n3. In the event Licensee prepares a derivative work that is based on\nor incorporates Python 1.6.1 or any part thereof, and wants to make\nthe derivative work available to others as provided herein, then\nLicensee hereby agrees to include in any such work a brief summary of\nthe changes made to Python 1.6.1.\n\n4. CNRI is making Python 1.6.1 available to Licensee on an \"AS IS\"\nbasis.  CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR\nIMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND\nDISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS\nFOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT\nINFRINGE ANY THIRD PARTY RIGHTS.\n\n5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON\n1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS\nA RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,\nOR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.\n\n6. This License Agreement will automatically terminate upon a material\nbreach of its terms and conditions.\n\n7. This License Agreement shall be governed by the federal\nintellectual property law of the United States, including without\nlimitation the federal copyright law, and, to the extent such\nU.S. federal law does not apply, by the law of the Commonwealth of\nVirginia, excluding Virginia's conflict of law provisions.\nNotwithstanding the foregoing, with regard to derivative works based\non Python 1.6.1 that incorporate non-separable material that was\npreviously distributed under the GNU General Public License (GPL), the\nlaw of the Commonwealth of Virginia shall govern this License\nAgreement only as to issues arising under or with respect to\nParagraphs 4, 5, and 7 of this License Agreement.  Nothing in this\nLicense Agreement shall be deemed to create any relationship of\nagency, partnership, or joint venture between CNRI and Licensee.  This\nLicense Agreement does not grant permission to use CNRI trademarks or\ntrade name in a trademark sense to endorse or promote products or\nservices of Licensee, or any third party.\n\n8. By clicking on the \"ACCEPT\" button where indicated, or by copying,\ninstalling or otherwise using Python 1.6.1, Licensee agrees to be\nbound by the terms and conditions of this License Agreement.\n\n        ACCEPT\n\n\nCWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2\n--------------------------------------------------\n\nCopyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,\nThe Netherlands.  All rights reserved.\n\nPermission to use, copy, modify, and distribute this software and its\ndocumentation for any purpose and without fee is hereby granted,\nprovided that the above copyright notice appear in all copies and that\nboth that copyright notice and this permission notice appear in\nsupporting documentation, and that the name of Stichting Mathematisch\nCentrum or CWI not be used in advertising or publicity pertaining to\ndistribution of the software without specific, written prior\npermission.\n\nSTICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO\nTHIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE\nFOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\nOF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION\n----------------------------------------------------------------------\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n"
  },
  {
    "path": "electrum/_vendor/distutils/__init__.py",
    "content": "\"\"\"(part of) distutils, taken from the cpython standard library\n\nat commit https://github.com/python/cpython/tree/9d38120e335357a3b294277fd5eff0a10e46e043/Lib/distutils\n\"\"\"\n"
  },
  {
    "path": "electrum/_vendor/distutils/version.py",
    "content": "#\n# distutils/version.py\n#\n# Implements multiple version numbering conventions for the\n# Python Module Distribution Utilities.\n#\n# $Id$\n#\n\n\"\"\"Provides classes to represent module version numbers (one class for\neach style of version numbering).  There are currently two such classes\nimplemented: StrictVersion and LooseVersion.\n\nEvery version number class implements the following interface:\n  * the 'parse' method takes a string and parses it to some internal\n    representation; if the string is an invalid version number,\n    'parse' raises a ValueError exception\n  * the class constructor takes an optional string argument which,\n    if supplied, is passed to 'parse'\n  * __str__ reconstructs the string that was passed to 'parse' (or\n    an equivalent string -- ie. one that will generate an equivalent\n    version number instance)\n  * __repr__ generates Python code to recreate the version number instance\n  * _cmp compares the current instance with either another instance\n    of the same class or a string (which will be parsed to an instance\n    of the same class, thus must follow the same rules)\n\"\"\"\n\nimport re\n\nclass Version:\n    \"\"\"Abstract base class for version numbering classes.  Just provides\n    constructor (__init__) and reproducer (__repr__), because those\n    seem to be the same for all version numbering classes; and route\n    rich comparisons to _cmp.\n    \"\"\"\n\n    def __init__ (self, vstring=None):\n        if vstring:\n            self.parse(vstring)\n\n    def __repr__ (self):\n        return \"%s ('%s')\" % (self.__class__.__name__, str(self))\n\n    def __eq__(self, other):\n        c = self._cmp(other)\n        if c is NotImplemented:\n            return c\n        return c == 0\n\n    def __lt__(self, other):\n        c = self._cmp(other)\n        if c is NotImplemented:\n            return c\n        return c < 0\n\n    def __le__(self, other):\n        c = self._cmp(other)\n        if c is NotImplemented:\n            return c\n        return c <= 0\n\n    def __gt__(self, other):\n        c = self._cmp(other)\n        if c is NotImplemented:\n            return c\n        return c > 0\n\n    def __ge__(self, other):\n        c = self._cmp(other)\n        if c is NotImplemented:\n            return c\n        return c >= 0\n\n\n# Interface for version-number classes -- must be implemented\n# by the following classes (the concrete ones -- Version should\n# be treated as an abstract class).\n#    __init__ (string) - create and take same action as 'parse'\n#                        (string parameter is optional)\n#    parse (string)    - convert a string representation to whatever\n#                        internal representation is appropriate for\n#                        this style of version numbering\n#    __str__ (self)    - convert back to a string; should be very similar\n#                        (if not identical to) the string supplied to parse\n#    __repr__ (self)   - generate Python code to recreate\n#                        the instance\n#    _cmp (self, other) - compare two version numbers ('other' may\n#                        be an unparsed version string, or another\n#                        instance of your version class)\n\n\nclass StrictVersion (Version):\n\n    \"\"\"Version numbering for anal retentives and software idealists.\n    Implements the standard interface for version number classes as\n    described above.  A version number consists of two or three\n    dot-separated numeric components, with an optional \"pre-release\" tag\n    on the end.  The pre-release tag consists of the letter 'a' or 'b'\n    followed by a number.  If the numeric components of two version\n    numbers are equal, then one with a pre-release tag will always\n    be deemed earlier (lesser) than one without.\n\n    The following are valid version numbers (shown in the order that\n    would be obtained by sorting according to the supplied cmp function):\n\n        0.4       0.4.0  (these two are equivalent)\n        0.4.1\n        0.5a1\n        0.5b3\n        0.5\n        0.9.6\n        1.0\n        1.0.4a3\n        1.0.4b1\n        1.0.4\n\n    The following are examples of invalid version numbers:\n\n        1\n        2.7.2.2\n        1.3.a4\n        1.3pl1\n        1.3c4\n\n    The rationale for this version numbering system will be explained\n    in the distutils documentation.\n    \"\"\"\n\n    version_re = re.compile(r'^(\\d+) \\. (\\d+) (\\. (\\d+))? ([ab](\\d+))?$',\n                            re.VERBOSE | re.ASCII)\n\n\n    def parse (self, vstring):\n        match = self.version_re.match(vstring)\n        if not match:\n            raise ValueError(\"invalid version number '%s'\" % vstring)\n\n        (major, minor, patch, prerelease, prerelease_num) = \\\n            match.group(1, 2, 4, 5, 6)\n\n        if patch:\n            self.version = tuple(map(int, [major, minor, patch]))\n        else:\n            self.version = tuple(map(int, [major, minor])) + (0,)\n\n        if prerelease:\n            self.prerelease = (prerelease[0], int(prerelease_num))\n        else:\n            self.prerelease = None\n\n\n    def __str__ (self):\n\n        if self.version[2] == 0:\n            vstring = '.'.join(map(str, self.version[0:2]))\n        else:\n            vstring = '.'.join(map(str, self.version))\n\n        if self.prerelease:\n            vstring = vstring + self.prerelease[0] + str(self.prerelease[1])\n\n        return vstring\n\n\n    def _cmp (self, other):\n        if isinstance(other, str):\n            other = StrictVersion(other)\n        elif not isinstance(other, StrictVersion):\n            return NotImplemented\n\n        if self.version != other.version:\n            # numeric versions don't match\n            # prerelease stuff doesn't matter\n            if self.version < other.version:\n                return -1\n            else:\n                return 1\n\n        # have to compare prerelease\n        # case 1: neither has prerelease; they're equal\n        # case 2: self has prerelease, other doesn't; other is greater\n        # case 3: self doesn't have prerelease, other does: self is greater\n        # case 4: both have prerelease: must compare them!\n\n        if (not self.prerelease and not other.prerelease):\n            return 0\n        elif (self.prerelease and not other.prerelease):\n            return -1\n        elif (not self.prerelease and other.prerelease):\n            return 1\n        elif (self.prerelease and other.prerelease):\n            if self.prerelease == other.prerelease:\n                return 0\n            elif self.prerelease < other.prerelease:\n                return -1\n            else:\n                return 1\n        else:\n            assert False, \"never get here\"\n\n# end class StrictVersion\n\n\n# The rules according to Greg Stein:\n# 1) a version number has 1 or more numbers separated by a period or by\n#    sequences of letters. If only periods, then these are compared\n#    left-to-right to determine an ordering.\n# 2) sequences of letters are part of the tuple for comparison and are\n#    compared lexicographically\n# 3) recognize the numeric components may have leading zeroes\n#\n# The LooseVersion class below implements these rules: a version number\n# string is split up into a tuple of integer and string components, and\n# comparison is a simple tuple comparison.  This means that version\n# numbers behave in a predictable and obvious way, but a way that might\n# not necessarily be how people *want* version numbers to behave.  There\n# wouldn't be a problem if people could stick to purely numeric version\n# numbers: just split on period and compare the numbers as tuples.\n# However, people insist on putting letters into their version numbers;\n# the most common purpose seems to be:\n#   - indicating a \"pre-release\" version\n#     ('alpha', 'beta', 'a', 'b', 'pre', 'p')\n#   - indicating a post-release patch ('p', 'pl', 'patch')\n# but of course this can't cover all version number schemes, and there's\n# no way to know what a programmer means without asking him.\n#\n# The problem is what to do with letters (and other non-numeric\n# characters) in a version number.  The current implementation does the\n# obvious and predictable thing: keep them as strings and compare\n# lexically within a tuple comparison.  This has the desired effect if\n# an appended letter sequence implies something \"post-release\":\n# eg. \"0.99\" < \"0.99pl14\" < \"1.0\", and \"5.001\" < \"5.001m\" < \"5.002\".\n#\n# However, if letters in a version number imply a pre-release version,\n# the \"obvious\" thing isn't correct.  Eg. you would expect that\n# \"1.5.1\" < \"1.5.2a2\" < \"1.5.2\", but under the tuple/lexical comparison\n# implemented here, this just isn't so.\n#\n# Two possible solutions come to mind.  The first is to tie the\n# comparison algorithm to a particular set of semantic rules, as has\n# been done in the StrictVersion class above.  This works great as long\n# as everyone can go along with bondage and discipline.  Hopefully a\n# (large) subset of Python module programmers will agree that the\n# particular flavour of bondage and discipline provided by StrictVersion\n# provides enough benefit to be worth using, and will submit their\n# version numbering scheme to its domination.  The free-thinking\n# anarchists in the lot will never give in, though, and something needs\n# to be done to accommodate them.\n#\n# Perhaps a \"moderately strict\" version class could be implemented that\n# lets almost anything slide (syntactically), and makes some heuristic\n# assumptions about non-digits in version number strings.  This could\n# sink into special-case-hell, though; if I was as talented and\n# idiosyncratic as Larry Wall, I'd go ahead and implement a class that\n# somehow knows that \"1.2.1\" < \"1.2.2a2\" < \"1.2.2\" < \"1.2.2pl3\", and is\n# just as happy dealing with things like \"2g6\" and \"1.13++\".  I don't\n# think I'm smart enough to do it right though.\n#\n# In any case, I've coded the test suite for this module (see\n# ../test/test_version.py) specifically to fail on things like comparing\n# \"1.2a2\" and \"1.2\".  That's not because the *code* is doing anything\n# wrong, it's because the simple, obvious design doesn't match my\n# complicated, hairy expectations for real-world version numbers.  It\n# would be a snap to fix the test suite to say, \"Yep, LooseVersion does\n# the Right Thing\" (ie. the code matches the conception).  But I'd rather\n# have a conception that matches common notions about version numbers.\n\nclass LooseVersion (Version):\n\n    \"\"\"Version numbering for anarchists and software realists.\n    Implements the standard interface for version number classes as\n    described above.  A version number consists of a series of numbers,\n    separated by either periods or strings of letters.  When comparing\n    version numbers, the numeric components will be compared\n    numerically, and the alphabetic components lexically.  The following\n    are all valid version numbers, in no particular order:\n\n        1.5.1\n        1.5.2b2\n        161\n        3.10a\n        8.02\n        3.4j\n        1996.07.12\n        3.2.pl0\n        3.1.1.6\n        2g6\n        11g\n        0.960923\n        2.2beta29\n        1.13++\n        5.5.kw\n        2.0b1pl0\n\n    In fact, there is no such thing as an invalid version number under\n    this scheme; the rules for comparison are simple and predictable,\n    but may not always give the results you want (for some definition\n    of \"want\").\n    \"\"\"\n\n    component_re = re.compile(r'(\\d+ | [a-z]+ | \\.)', re.VERBOSE)\n\n    def __init__ (self, vstring=None):\n        if vstring:\n            self.parse(vstring)\n\n\n    def parse (self, vstring):\n        # I've given up on thinking I can reconstruct the version string\n        # from the parsed tuple -- so I just store the string here for\n        # use by __str__\n        self.vstring = vstring\n        components = [x for x in self.component_re.split(vstring)\n                              if x and x != '.']\n        for i, obj in enumerate(components):\n            try:\n                components[i] = int(obj)\n            except ValueError:\n                pass\n\n        self.version = components\n\n\n    def __str__ (self):\n        return self.vstring\n\n\n    def __repr__ (self):\n        return \"LooseVersion ('%s')\" % str(self)\n\n\n    def _cmp (self, other):\n        if isinstance(other, str):\n            other = LooseVersion(other)\n        elif not isinstance(other, LooseVersion):\n            return NotImplemented\n\n        if self.version == other.version:\n            return 0\n        if self.version < other.version:\n            return -1\n        if self.version > other.version:\n            return 1\n\n\n# end class LooseVersion\n"
  },
  {
    "path": "electrum/_vendor/pyperclip/LICENSE.txt",
    "content": "Copyright (c) 2014, Al Sweigart\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the {organization} nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "electrum/_vendor/pyperclip/README.md",
    "content": "\nThis is a stripped-down copy of the 3rd-party `pyperclip` package.\n\nIt is used by the \"text\" GUI.\n\nAt revision https://github.com/asweigart/pyperclip/blob/781603ea491eefce3b58f4f203bf748dbf9ff003/src/pyperclip/__init__.py\n(version 1.8.2)\n\nModifications:\n- excluded most files\n- added support for pyqt6\n"
  },
  {
    "path": "electrum/_vendor/pyperclip/__init__.py",
    "content": "\"\"\"\nPyperclip\n\nA cross-platform clipboard module for Python, with copy & paste functions for plain text.\nBy Al Sweigart al@inventwithpython.com\nBSD License\n\nUsage:\n  import pyperclip\n  pyperclip.copy('The text to be copied to the clipboard.')\n  spam = pyperclip.paste()\n\n  if not pyperclip.is_available():\n    print(\"Copy functionality unavailable!\")\n\nOn Windows, no additional modules are needed.\nOn Mac, the pyobjc module is used, falling back to the pbcopy and pbpaste cli\n    commands. (These commands should come with OS X.).\nOn Linux, install xclip, xsel, or wl-clipboard (for \"wayland\" sessions) via package manager.\nFor example, in Debian:\n    sudo apt-get install xclip\n    sudo apt-get install xsel\n    sudo apt-get install wl-clipboard\n\nOtherwise on Linux, you will need the gtk or PyQt5/PyQt4 modules installed.\n\ngtk and PyQt4 modules are not available for Python 3,\nand this module does not work with PyGObject yet.\n\nNote: There seems to be a way to get gtk on Python 3, according to:\n    https://askubuntu.com/questions/697397/python3-is-not-supporting-gtk-module\n\nCygwin is currently not supported.\n\nSecurity Note: This module runs programs with these names:\n    - which\n    - where\n    - pbcopy\n    - pbpaste\n    - xclip\n    - xsel\n    - wl-copy/wl-paste\n    - klipper\n    - qdbus\nA malicious user could rename or add programs with these names, tricking\nPyperclip into running them with whatever permissions the Python process has.\n\n\"\"\"\n__version__ = '1.8.2'\n\nimport contextlib\nimport ctypes\nimport os\nimport platform\nimport subprocess\nimport sys\nimport time\nimport warnings\n\nfrom ctypes import c_size_t, sizeof, c_wchar_p, get_errno, c_wchar\n\n\n# `import PyQt4` sys.exit()s if DISPLAY is not in the environment.\n# Thus, we need to detect the presence of $DISPLAY manually\n# and not load PyQt4 if it is absent.\nHAS_DISPLAY = os.getenv(\"DISPLAY\", False)\n\nEXCEPT_MSG = \"\"\"\n    Pyperclip could not find a copy/paste mechanism for your system.\n    For more information, please visit https://pyperclip.readthedocs.io/en/latest/index.html#not-implemented-error \"\"\"\n\nPY2 = sys.version_info[0] == 2\n\nSTR_OR_UNICODE = unicode if PY2 else str # For paste(): Python 3 uses str, Python 2 uses unicode.\n\nENCODING = 'utf-8'\n\ntry:\n    from shutil import which as _executable_exists\nexcept ImportError:\n    # The \"which\" unix command finds where a command is.\n    if platform.system() == 'Windows':\n        WHICH_CMD = 'where'\n    else:\n        WHICH_CMD = 'which'\n\n    def _executable_exists(name):\n        return subprocess.call([WHICH_CMD, name],\n                            stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0\n\n\n\n# Exceptions\nclass PyperclipException(RuntimeError):\n    pass\n\nclass PyperclipWindowsException(PyperclipException):\n    def __init__(self, message):\n        message += \" (%s)\" % ctypes.WinError()\n        super(PyperclipWindowsException, self).__init__(message)\n\nclass PyperclipTimeoutException(PyperclipException):\n    pass\n\ndef _stringifyText(text):\n    if PY2:\n        acceptedTypes = (unicode, str, int, float, bool)\n    else:\n        acceptedTypes = (str, int, float, bool)\n    if not isinstance(text, acceptedTypes):\n        raise PyperclipException('only str, int, float, and bool values can be copied to the clipboard, not %s' % (text.__class__.__name__))\n    return STR_OR_UNICODE(text)\n\n\ndef init_osx_pbcopy_clipboard():\n\n    def copy_osx_pbcopy(text):\n        text = _stringifyText(text) # Converts non-str values to str.\n        p = subprocess.Popen(['pbcopy', 'w'],\n                             stdin=subprocess.PIPE, close_fds=True)\n        p.communicate(input=text.encode(ENCODING))\n\n    def paste_osx_pbcopy():\n        p = subprocess.Popen(['pbpaste', 'r'],\n                             stdout=subprocess.PIPE, close_fds=True)\n        stdout, stderr = p.communicate()\n        return stdout.decode(ENCODING)\n\n    return copy_osx_pbcopy, paste_osx_pbcopy\n\n\ndef init_osx_pyobjc_clipboard():\n    def copy_osx_pyobjc(text):\n        '''Copy string argument to clipboard'''\n        text = _stringifyText(text) # Converts non-str values to str.\n        newStr = Foundation.NSString.stringWithString_(text).nsstring()\n        newData = newStr.dataUsingEncoding_(Foundation.NSUTF8StringEncoding)\n        board = AppKit.NSPasteboard.generalPasteboard()\n        board.declareTypes_owner_([AppKit.NSStringPboardType], None)\n        board.setData_forType_(newData, AppKit.NSStringPboardType)\n\n    def paste_osx_pyobjc():\n        \"Returns contents of clipboard\"\n        board = AppKit.NSPasteboard.generalPasteboard()\n        content = board.stringForType_(AppKit.NSStringPboardType)\n        return content\n\n    return copy_osx_pyobjc, paste_osx_pyobjc\n\n\ndef init_gtk_clipboard():\n    global gtk\n    import gtk\n\n    def copy_gtk(text):\n        global cb\n        text = _stringifyText(text) # Converts non-str values to str.\n        cb = gtk.Clipboard()\n        cb.set_text(text)\n        cb.store()\n\n    def paste_gtk():\n        clipboardContents = gtk.Clipboard().wait_for_text()\n        # for python 2, returns None if the clipboard is blank.\n        if clipboardContents is None:\n            return ''\n        else:\n            return clipboardContents\n\n    return copy_gtk, paste_gtk\n\n\ndef init_qt_clipboard():\n    global QApplication\n    # $DISPLAY should exist\n\n    # Try to import from qtpy, but if that fails try PyQt5 then PyQt4\n    try:\n        from qtpy.QtWidgets import QApplication\n    except:\n        try:\n            from PyQt6.QtWidgets import QApplication\n        except:\n            try:\n                from PyQt5.QtWidgets import QApplication\n            except:\n                from PyQt4.QtGui import QApplication\n\n    app = QApplication.instance()\n    if app is None:\n        app = QApplication([])\n\n    def copy_qt(text):\n        text = _stringifyText(text) # Converts non-str values to str.\n        cb = app.clipboard()\n        cb.setText(text)\n\n    def paste_qt():\n        cb = app.clipboard()\n        return STR_OR_UNICODE(cb.text())\n\n    return copy_qt, paste_qt\n\n\ndef init_xclip_clipboard():\n    DEFAULT_SELECTION='c'\n    PRIMARY_SELECTION='p'\n\n    def copy_xclip(text, primary=False):\n        text = _stringifyText(text) # Converts non-str values to str.\n        selection=DEFAULT_SELECTION\n        if primary:\n            selection=PRIMARY_SELECTION\n        p = subprocess.Popen(['xclip', '-selection', selection],\n                             stdin=subprocess.PIPE, close_fds=True)\n        p.communicate(input=text.encode(ENCODING))\n\n    def paste_xclip(primary=False):\n        selection=DEFAULT_SELECTION\n        if primary:\n            selection=PRIMARY_SELECTION\n        p = subprocess.Popen(['xclip', '-selection', selection, '-o'],\n                             stdout=subprocess.PIPE,\n                             stderr=subprocess.PIPE,\n                             close_fds=True)\n        stdout, stderr = p.communicate()\n        # Intentionally ignore extraneous output on stderr when clipboard is empty\n        return stdout.decode(ENCODING)\n\n    return copy_xclip, paste_xclip\n\n\ndef init_xsel_clipboard():\n    DEFAULT_SELECTION='-b'\n    PRIMARY_SELECTION='-p'\n\n    def copy_xsel(text, primary=False):\n        text = _stringifyText(text) # Converts non-str values to str.\n        selection_flag = DEFAULT_SELECTION\n        if primary:\n            selection_flag = PRIMARY_SELECTION\n        p = subprocess.Popen(['xsel', selection_flag, '-i'],\n                             stdin=subprocess.PIPE, close_fds=True)\n        p.communicate(input=text.encode(ENCODING))\n\n    def paste_xsel(primary=False):\n        selection_flag = DEFAULT_SELECTION\n        if primary:\n            selection_flag = PRIMARY_SELECTION\n        p = subprocess.Popen(['xsel', selection_flag, '-o'],\n                             stdout=subprocess.PIPE, close_fds=True)\n        stdout, stderr = p.communicate()\n        return stdout.decode(ENCODING)\n\n    return copy_xsel, paste_xsel\n\n\ndef init_wl_clipboard():\n    PRIMARY_SELECTION = \"-p\"\n\n    def copy_wl(text, primary=False):\n        text = _stringifyText(text)  # Converts non-str values to str.\n        args = [\"wl-copy\"]\n        if primary:\n            args.append(PRIMARY_SELECTION)\n        if not text:\n            args.append('--clear')\n            subprocess.check_call(args, close_fds=True)\n        else:\n            pass\n            p = subprocess.Popen(args, stdin=subprocess.PIPE, close_fds=True)\n            p.communicate(input=text.encode(ENCODING))\n\n    def paste_wl(primary=False):\n        args = [\"wl-paste\", \"-n\"]\n        if primary:\n            args.append(PRIMARY_SELECTION)\n        p = subprocess.Popen(args, stdout=subprocess.PIPE, close_fds=True)\n        stdout, _stderr = p.communicate()\n        return stdout.decode(ENCODING)\n\n    return copy_wl, paste_wl\n\n\ndef init_klipper_clipboard():\n    def copy_klipper(text):\n        text = _stringifyText(text) # Converts non-str values to str.\n        p = subprocess.Popen(\n            ['qdbus', 'org.kde.klipper', '/klipper', 'setClipboardContents',\n             text.encode(ENCODING)],\n            stdin=subprocess.PIPE, close_fds=True)\n        p.communicate(input=None)\n\n    def paste_klipper():\n        p = subprocess.Popen(\n            ['qdbus', 'org.kde.klipper', '/klipper', 'getClipboardContents'],\n            stdout=subprocess.PIPE, close_fds=True)\n        stdout, stderr = p.communicate()\n\n        # Workaround for https://bugs.kde.org/show_bug.cgi?id=342874\n        # TODO: https://github.com/asweigart/pyperclip/issues/43\n        clipboardContents = stdout.decode(ENCODING)\n        # even if blank, Klipper will append a newline at the end\n        assert len(clipboardContents) > 0\n        # make sure that newline is there\n        assert clipboardContents.endswith('\\n')\n        if clipboardContents.endswith('\\n'):\n            clipboardContents = clipboardContents[:-1]\n        return clipboardContents\n\n    return copy_klipper, paste_klipper\n\n\ndef init_dev_clipboard_clipboard():\n    def copy_dev_clipboard(text):\n        text = _stringifyText(text) # Converts non-str values to str.\n        if text == '':\n            warnings.warn('Pyperclip cannot copy a blank string to the clipboard on Cygwin. This is effectively a no-op.')\n        if '\\r' in text:\n            warnings.warn('Pyperclip cannot handle \\\\r characters on Cygwin.')\n\n        fo = open('/dev/clipboard', 'wt')\n        fo.write(text)\n        fo.close()\n\n    def paste_dev_clipboard():\n        fo = open('/dev/clipboard', 'rt')\n        content = fo.read()\n        fo.close()\n        return content\n\n    return copy_dev_clipboard, paste_dev_clipboard\n\n\ndef init_no_clipboard():\n    class ClipboardUnavailable(object):\n\n        def __call__(self, *args, **kwargs):\n            raise PyperclipException(EXCEPT_MSG)\n\n        if PY2:\n            def __nonzero__(self):\n                return False\n        else:\n            def __bool__(self):\n                return False\n\n    return ClipboardUnavailable(), ClipboardUnavailable()\n\n\n\n\n# Windows-related clipboard functions:\nclass CheckedCall(object):\n    def __init__(self, f):\n        super(CheckedCall, self).__setattr__(\"f\", f)\n\n    def __call__(self, *args):\n        ret = self.f(*args)\n        if not ret and get_errno():\n            raise PyperclipWindowsException(\"Error calling \" + self.f.__name__)\n        return ret\n\n    def __setattr__(self, key, value):\n        setattr(self.f, key, value)\n\n\ndef init_windows_clipboard():\n    global HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND, HINSTANCE, HMENU, BOOL, UINT, HANDLE\n    from ctypes.wintypes import (HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND,\n                                 HINSTANCE, HMENU, BOOL, UINT, HANDLE)\n\n    windll = ctypes.windll\n    msvcrt = ctypes.CDLL('msvcrt')\n\n    safeCreateWindowExA = CheckedCall(windll.user32.CreateWindowExA)\n    safeCreateWindowExA.argtypes = [DWORD, LPCSTR, LPCSTR, DWORD, INT, INT,\n                                    INT, INT, HWND, HMENU, HINSTANCE, LPVOID]\n    safeCreateWindowExA.restype = HWND\n\n    safeDestroyWindow = CheckedCall(windll.user32.DestroyWindow)\n    safeDestroyWindow.argtypes = [HWND]\n    safeDestroyWindow.restype = BOOL\n\n    OpenClipboard = windll.user32.OpenClipboard\n    OpenClipboard.argtypes = [HWND]\n    OpenClipboard.restype = BOOL\n\n    safeCloseClipboard = CheckedCall(windll.user32.CloseClipboard)\n    safeCloseClipboard.argtypes = []\n    safeCloseClipboard.restype = BOOL\n\n    safeEmptyClipboard = CheckedCall(windll.user32.EmptyClipboard)\n    safeEmptyClipboard.argtypes = []\n    safeEmptyClipboard.restype = BOOL\n\n    safeGetClipboardData = CheckedCall(windll.user32.GetClipboardData)\n    safeGetClipboardData.argtypes = [UINT]\n    safeGetClipboardData.restype = HANDLE\n\n    safeSetClipboardData = CheckedCall(windll.user32.SetClipboardData)\n    safeSetClipboardData.argtypes = [UINT, HANDLE]\n    safeSetClipboardData.restype = HANDLE\n\n    safeGlobalAlloc = CheckedCall(windll.kernel32.GlobalAlloc)\n    safeGlobalAlloc.argtypes = [UINT, c_size_t]\n    safeGlobalAlloc.restype = HGLOBAL\n\n    safeGlobalLock = CheckedCall(windll.kernel32.GlobalLock)\n    safeGlobalLock.argtypes = [HGLOBAL]\n    safeGlobalLock.restype = LPVOID\n\n    safeGlobalUnlock = CheckedCall(windll.kernel32.GlobalUnlock)\n    safeGlobalUnlock.argtypes = [HGLOBAL]\n    safeGlobalUnlock.restype = BOOL\n\n    wcslen = CheckedCall(msvcrt.wcslen)\n    wcslen.argtypes = [c_wchar_p]\n    wcslen.restype = UINT\n\n    GMEM_MOVEABLE = 0x0002\n    CF_UNICODETEXT = 13\n\n    @contextlib.contextmanager\n    def window():\n        \"\"\"\n        Context that provides a valid Windows hwnd.\n        \"\"\"\n        # we really just need the hwnd, so setting \"STATIC\"\n        # as predefined lpClass is just fine.\n        hwnd = safeCreateWindowExA(0, b\"STATIC\", None, 0, 0, 0, 0, 0,\n                                   None, None, None, None)\n        try:\n            yield hwnd\n        finally:\n            safeDestroyWindow(hwnd)\n\n    @contextlib.contextmanager\n    def clipboard(hwnd):\n        \"\"\"\n        Context manager that opens the clipboard and prevents\n        other applications from modifying the clipboard content.\n        \"\"\"\n        # We may not get the clipboard handle immediately because\n        # some other application is accessing it (?)\n        # We try for at least 500ms to get the clipboard.\n        t = time.time() + 0.5\n        success = False\n        while time.time() < t:\n            success = OpenClipboard(hwnd)\n            if success:\n                break\n            time.sleep(0.01)\n        if not success:\n            raise PyperclipWindowsException(\"Error calling OpenClipboard\")\n\n        try:\n            yield\n        finally:\n            safeCloseClipboard()\n\n    def copy_windows(text):\n        # This function is heavily based on\n        # http://msdn.com/ms649016#_win32_Copying_Information_to_the_Clipboard\n\n        text = _stringifyText(text) # Converts non-str values to str.\n\n        with window() as hwnd:\n            # http://msdn.com/ms649048\n            # If an application calls OpenClipboard with hwnd set to NULL,\n            # EmptyClipboard sets the clipboard owner to NULL;\n            # this causes SetClipboardData to fail.\n            # => We need a valid hwnd to copy something.\n            with clipboard(hwnd):\n                safeEmptyClipboard()\n\n                if text:\n                    # http://msdn.com/ms649051\n                    # If the hMem parameter identifies a memory object,\n                    # the object must have been allocated using the\n                    # function with the GMEM_MOVEABLE flag.\n                    count = wcslen(text) + 1\n                    handle = safeGlobalAlloc(GMEM_MOVEABLE,\n                                             count * sizeof(c_wchar))\n                    locked_handle = safeGlobalLock(handle)\n\n                    ctypes.memmove(c_wchar_p(locked_handle), c_wchar_p(text), count * sizeof(c_wchar))\n\n                    safeGlobalUnlock(handle)\n                    safeSetClipboardData(CF_UNICODETEXT, handle)\n\n    def paste_windows():\n        with clipboard(None):\n            handle = safeGetClipboardData(CF_UNICODETEXT)\n            if not handle:\n                # GetClipboardData may return NULL with errno == NO_ERROR\n                # if the clipboard is empty.\n                # (Also, it may return a handle to an empty buffer,\n                # but technically that's not empty)\n                return \"\"\n            locked_handle = safeGlobalLock(handle)\n            return_value = c_wchar_p(locked_handle).value\n            safeGlobalUnlock(handle)\n            return return_value\n\n    return copy_windows, paste_windows\n\n\ndef init_wsl_clipboard():\n    def copy_wsl(text):\n        text = _stringifyText(text) # Converts non-str values to str.\n        p = subprocess.Popen(['clip.exe'],\n                             stdin=subprocess.PIPE, close_fds=True)\n        p.communicate(input=text.encode(ENCODING))\n\n    def paste_wsl():\n        # '-noprofile' speeds up load time\n        p = subprocess.Popen(['powershell.exe', '-noprofile', '-command', 'Get-Clipboard'],\n                             stdout=subprocess.PIPE,\n                             stderr=subprocess.PIPE,\n                             close_fds=True)\n        stdout, stderr = p.communicate()\n        # WSL appends \"\\r\\n\" to the contents.\n        return stdout[:-2].decode(ENCODING)\n\n    return copy_wsl, paste_wsl\n\n\n# Automatic detection of clipboard mechanisms and importing is done in deteremine_clipboard():\ndef determine_clipboard():\n    '''\n    Determine the OS/platform and set the copy() and paste() functions\n    accordingly.\n    '''\n\n    global Foundation, AppKit, gtk, qtpy, PyQt4, PyQt5, PyQt6\n\n    # Setup for the CYGWIN platform:\n    if 'cygwin' in platform.system().lower(): # Cygwin has a variety of values returned by platform.system(), such as 'CYGWIN_NT-6.1'\n        # FIXME: pyperclip currently does not support Cygwin,\n        # see https://github.com/asweigart/pyperclip/issues/55\n        if os.path.exists('/dev/clipboard'):\n            warnings.warn('Pyperclip\\'s support for Cygwin is not perfect, see https://github.com/asweigart/pyperclip/issues/55')\n            return init_dev_clipboard_clipboard()\n\n    # Setup for the WINDOWS platform:\n    elif os.name == 'nt' or platform.system() == 'Windows':\n        return init_windows_clipboard()\n\n    if platform.system() == 'Linux' and os.path.isfile('/proc/version'):\n        with open('/proc/version', 'r') as f:\n            if \"microsoft\" in f.read().lower():\n                return init_wsl_clipboard()\n\n    # Setup for the MAC OS X platform:\n    if os.name == 'mac' or platform.system() == 'Darwin':\n        try:\n            import Foundation  # check if pyobjc is installed\n            import AppKit\n        except ImportError:\n            return init_osx_pbcopy_clipboard()\n        else:\n            return init_osx_pyobjc_clipboard()\n\n    # Setup for the LINUX platform:\n    if HAS_DISPLAY:\n        try:\n            import gtk  # check if gtk is installed\n        except ImportError:\n            pass # We want to fail fast for all non-ImportError exceptions.\n        else:\n            return init_gtk_clipboard()\n\n        if (\n                os.environ.get(\"WAYLAND_DISPLAY\") and\n                _executable_exists(\"wl-copy\")\n        ):\n            return init_wl_clipboard()\n        if _executable_exists(\"xsel\"):\n            return init_xsel_clipboard()\n        if _executable_exists(\"xclip\"):\n            return init_xclip_clipboard()\n        if _executable_exists(\"klipper\") and _executable_exists(\"qdbus\"):\n            return init_klipper_clipboard()\n\n        try:\n            # qtpy is a small abstraction layer that lets you write applications using a single api call to either PyQt or PySide.\n            # https://pypi.python.org/pypi/QtPy\n            import qtpy  # check if qtpy is installed\n        except ImportError:\n            # If qtpy isn't installed, fall back on importing PyQt4.\n            try:\n                import PyQt6  # check if PyQt6 is installed\n            except ImportError:\n                try:\n                    import PyQt5  # check if PyQt5 is installed\n                except ImportError:\n                    try:\n                        import PyQt4  # check if PyQt4 is installed\n                    except ImportError:\n                        pass # We want to fail fast for all non-ImportError exceptions.\n                    else:\n                        return init_qt_clipboard()\n                else:\n                    return init_qt_clipboard()\n            else:\n                return init_qt_clipboard()\n        else:\n            return init_qt_clipboard()\n\n\n    return init_no_clipboard()\n\n\ndef set_clipboard(clipboard):\n    '''\n    Explicitly sets the clipboard mechanism. The \"clipboard mechanism\" is how\n    the copy() and paste() functions interact with the operating system to\n    implement the copy/paste feature. The clipboard parameter must be one of:\n        - pbcopy\n        - pbobjc (default on Mac OS X)\n        - gtk\n        - qt\n        - xclip\n        - xsel\n        - klipper\n        - windows (default on Windows)\n        - no (this is what is set when no clipboard mechanism can be found)\n    '''\n    global copy, paste\n\n    clipboard_types = {\n        \"pbcopy\": init_osx_pbcopy_clipboard,\n        \"pyobjc\": init_osx_pyobjc_clipboard,\n        \"gtk\": init_gtk_clipboard,\n        \"qt\": init_qt_clipboard,  # TODO - split this into 'qtpy', 'pyqt4', and 'pyqt5'\n        \"xclip\": init_xclip_clipboard,\n        \"xsel\": init_xsel_clipboard,\n        \"wl-clipboard\": init_wl_clipboard,\n        \"klipper\": init_klipper_clipboard,\n        \"windows\": init_windows_clipboard,\n        \"no\": init_no_clipboard,\n    }\n\n    if clipboard not in clipboard_types:\n        raise ValueError('Argument must be one of %s' % (', '.join([repr(_) for _ in clipboard_types.keys()])))\n\n    # Sets pyperclip's copy() and paste() functions:\n    copy, paste = clipboard_types[clipboard]()\n\n\ndef lazy_load_stub_copy(text):\n    '''\n    A stub function for copy(), which will load the real copy() function when\n    called so that the real copy() function is used for later calls.\n\n    This allows users to import pyperclip without having determine_clipboard()\n    automatically run, which will automatically select a clipboard mechanism.\n    This could be a problem if it selects, say, the memory-heavy PyQt4 module\n    but the user was just going to immediately call set_clipboard() to use a\n    different clipboard mechanism.\n\n    The lazy loading this stub function implements gives the user a chance to\n    call set_clipboard() to pick another clipboard mechanism. Or, if the user\n    simply calls copy() or paste() without calling set_clipboard() first,\n    will fall back on whatever clipboard mechanism that determine_clipboard()\n    automatically chooses.\n    '''\n    global copy, paste\n    copy, paste = determine_clipboard()\n    return copy(text)\n\n\ndef lazy_load_stub_paste():\n    '''\n    A stub function for paste(), which will load the real paste() function when\n    called so that the real paste() function is used for later calls.\n\n    This allows users to import pyperclip without having determine_clipboard()\n    automatically run, which will automatically select a clipboard mechanism.\n    This could be a problem if it selects, say, the memory-heavy PyQt4 module\n    but the user was just going to immediately call set_clipboard() to use a\n    different clipboard mechanism.\n\n    The lazy loading this stub function implements gives the user a chance to\n    call set_clipboard() to pick another clipboard mechanism. Or, if the user\n    simply calls copy() or paste() without calling set_clipboard() first,\n    will fall back on whatever clipboard mechanism that determine_clipboard()\n    automatically chooses.\n    '''\n    global copy, paste\n    copy, paste = determine_clipboard()\n    return paste()\n\n\ndef is_available():\n    return copy != lazy_load_stub_copy and paste != lazy_load_stub_paste\n\n\n# Initially, copy() and paste() are set to lazy loading wrappers which will\n# set `copy` and `paste` to real functions the first time they're used, unless\n# set_clipboard() or determine_clipboard() is called first.\ncopy, paste = lazy_load_stub_copy, lazy_load_stub_paste\n\n\n\ndef waitForPaste(timeout=None):\n    \"\"\"This function call blocks until a non-empty text string exists on the\n    clipboard. It returns this text.\n\n    This function raises PyperclipTimeoutException if timeout was set to\n    a number of seconds that has elapsed without non-empty text being put on\n    the clipboard.\"\"\"\n    startTime = time.time()\n    while True:\n        clipboardText = paste()\n        if clipboardText != '':\n            return clipboardText\n        time.sleep(0.01)\n\n        if timeout is not None and time.time() > startTime + timeout:\n            raise PyperclipTimeoutException('waitForPaste() timed out after ' + str(timeout) + ' seconds.')\n\n\ndef waitForNewPaste(timeout=None):\n    \"\"\"This function call blocks until a new text string exists on the\n    clipboard that is different from the text that was there when the function\n    was first called. It returns this text.\n\n    This function raises PyperclipTimeoutException if timeout was set to\n    a number of seconds that has elapsed without non-empty text being put on\n    the clipboard.\"\"\"\n    startTime = time.time()\n    originalText = paste()\n    while True:\n        currentText = paste()\n        if currentText != originalText:\n            return currentText\n        time.sleep(0.01)\n\n        if timeout is not None and time.time() > startTime + timeout:\n            raise PyperclipTimeoutException('waitForNewPaste() timed out after ' + str(timeout) + ' seconds.')\n\n\n__all__ = ['copy', 'paste', 'waitForPaste', 'waitForNewPaste', 'set_clipboard', 'determine_clipboard']\n\n\n"
  },
  {
    "path": "electrum/address_synchronizer.py",
    "content": "# Electrum - lightweight Bitcoin client\n# Copyright (C) 2018 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport asyncio\nimport copy\nimport dataclasses\nimport threading\nimport itertools\nfrom collections import defaultdict\nfrom typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence, List\n\nfrom .crypto import sha256\nfrom . import bitcoin, util\nfrom .bitcoin import COINBASE_MATURITY\nfrom .util import profiler, bfh, TxMinedInfo, UnrelatedTransactionException, with_lock, OldTaskGroup\nfrom .transaction import Transaction, TxOutput, TxInput, PartialTxInput, TxOutpoint, PartialTransaction, tx_from_any\nfrom .synchronizer import Synchronizer\nfrom .verifier import SPV\nfrom .blockchain import hash_header, Blockchain\nfrom .i18n import _\nfrom .logging import Logger\nfrom .util import EventListener, event_listener\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .wallet_db import WalletDB\n    from .simple_config import SimpleConfig\n\n\nTX_HEIGHT_FUTURE = -3\nTX_HEIGHT_LOCAL = -2\nTX_HEIGHT_UNCONF_PARENT = -1\nTX_HEIGHT_UNCONFIRMED = 0\n\nTX_TIMESTAMP_INF = 999_999_999_999\nTX_HEIGHT_INF = 10 ** 9\n\n\nfrom enum import IntEnum, auto\n\nclass TxMinedDepth(IntEnum):\n    \"\"\" IntEnum because we call min() in get_deepest_tx_mined_depth_for_txids \"\"\"\n    DEEP = auto()\n    SHALLOW = auto()\n    MEMPOOL = auto()\n    FREE = auto()\n\n\nclass HistoryItem(NamedTuple):\n    txid: str\n    tx_mined_status: TxMinedInfo\n    delta: int\n    fee: Optional[int]\n    balance: int\n\n\nclass AddressSynchronizer(Logger, EventListener):\n    \"\"\" address database \"\"\"\n\n    network: Optional['Network']\n    asyncio_loop: Optional['asyncio.AbstractEventLoop'] = None\n    synchronizer: Optional['Synchronizer']\n    verifier: Optional['SPV']\n\n    def __init__(self, db: 'WalletDB', config: 'SimpleConfig', *, name: str = None):\n        self.db = db\n        self.config = config\n        self.name = name\n        self.network = None\n        Logger.__init__(self)\n        # verifier (SPV) and synchronizer are started in start_network\n        self.synchronizer = None\n        self.verifier = None\n        self.lock = threading.RLock()\n        self.future_tx = {}  # type: Dict[str, int]  # txid -> wanted (abs) height\n        # Txs the server claims are mined but still pending verification:\n        self.unverified_tx = defaultdict(int)  # type: Dict[str, int]  # txid -> height. Access with self.lock.\n        # Txs the server claims are in the mempool:\n        self.unconfirmed_tx = defaultdict(int)  # type: Dict[str, int]  # txid -> height. Access with self.lock.\n        # thread local storage for caching stuff\n        self.threadlocal_cache = threading.local()\n\n        self._get_balance_cache = {}\n        self._get_utxos_cache = {}\n\n        self.load_and_cleanup()\n\n    @with_lock\n    def invalidate_cache(self):\n        self._get_balance_cache.clear()\n        self._get_utxos_cache.clear()\n\n    def diagnostic_name(self):\n        return self.name or \"\"\n\n    @with_lock\n    def load_and_cleanup(self):\n        self.load_local_history()\n        self.check_history()\n        self.load_unverified_transactions()\n        self.remove_local_transactions_we_dont_have()\n\n    def is_mine(self, address: Optional[str]) -> bool:\n        \"\"\"Returns whether an address is in our set.\n\n        Differences between adb.is_mine and wallet.is_mine:\n        - adb.is_mine: addrs that we are watching (e.g. via Synchronizer)\n            - lnwatcher adds its own lightning-related addresses that are not part of the wallet\n        - wallet.is_mine: addrs that are part of the wallet balance or the wallet might sign for\n            - an offline wallet might learn from a PSBT about addrs beyond its gap limit\n        Neither set is guaranteed to be a subset of the other.\n        \"\"\"\n        if not address: return False\n        return self.db.is_addr_in_history(address)\n\n    def get_addresses(self):\n        return sorted(self.db.get_history())\n\n    @with_lock\n    def get_address_history(self, addr: str) -> Dict[str, int]:\n        \"\"\"Returns the history for the address, as a txid->height dict.\n        In addition to what we have from the server, this includes local and future txns.\n        Note: heights are SPV-verified.\n\n        Also see related method db.get_addr_history, which stores the response from the server,\n        so that only includes txns the server sees.\n        \"\"\"\n        h = {}\n        related_txns = self._history_local.get(addr, set())\n        for tx_hash in related_txns:\n            tx_height = self.get_tx_height(tx_hash).height()\n            h[tx_hash] = tx_height\n        return h\n\n    def get_address_history_len(self, addr: str) -> int:\n        \"\"\"Return number of transactions where address is involved.\"\"\"\n        return len(self._history_local.get(addr, ()))\n\n    @with_lock\n    def get_txin_address(self, txin: TxInput) -> Optional[str]:\n        if txin.address:\n            return txin.address\n        prevout_hash = txin.prevout.txid.hex()\n        prevout_n = txin.prevout.out_idx\n        for addr in self.db.get_txo_addresses(prevout_hash):\n            d = self.db.get_txo_addr(prevout_hash, addr)\n            if prevout_n in d:\n                return addr\n        tx = self.db.get_transaction(prevout_hash)\n        if tx:\n            return tx.outputs()[prevout_n].address\n        return None\n\n    @with_lock\n    def get_txin_value(self, txin: TxInput, *, address: str = None) -> Optional[int]:\n        if txin.value_sats() is not None:\n            return txin.value_sats()\n        prevout_hash = txin.prevout.txid.hex()\n        prevout_n = txin.prevout.out_idx\n        if address is None:\n            address = self.get_txin_address(txin)\n        if address:\n            d = self.db.get_txo_addr(prevout_hash, address)\n            try:\n                v, cb = d[prevout_n]\n                return v\n            except KeyError:\n                pass\n        tx = self.db.get_transaction(prevout_hash)\n        if tx:\n            return tx.outputs()[prevout_n].value\n        return None\n\n    @with_lock\n    def load_unverified_transactions(self):\n        # review transactions that are in the history\n        for addr in self.db.get_history():\n            hist = self.db.get_addr_history(addr)\n            for tx_hash, tx_height in hist:\n                # add it in case it was previously unconfirmed\n                self.add_unverified_or_unconfirmed_tx(tx_hash, tx_height)\n\n    def start_network(self, network: Optional['Network']) -> None:\n        assert self.network is None, \"already started\"\n        self.network = network\n        if self.network is not None:\n            self.synchronizer = Synchronizer(self)\n            self.verifier = SPV(self.network, self)\n            self.asyncio_loop = network.asyncio_loop\n            self.register_callbacks()\n            self._update_stored_local_height()\n\n    @event_listener\n    @with_lock\n    def on_event_blockchain_updated(self, *args):\n        self.invalidate_cache()\n        self._update_stored_local_height()\n\n    async def stop(self):\n        if self.network:\n            try:\n                async with OldTaskGroup() as group:\n                    if self.synchronizer:\n                        await group.spawn(self.synchronizer.stop())\n                    if self.verifier:\n                        await group.spawn(self.verifier.stop())\n            finally:  # even if we get cancelled\n                self.synchronizer = None\n                self.verifier = None\n                self.unregister_callbacks()\n                self.network = None\n\n    def add_address(self, address: str) -> None:\n        if address not in self.db.history:\n            self.db.history[address] = []\n        if self.synchronizer:\n            self.synchronizer.add(address)\n        self.up_to_date_changed()\n\n    @with_lock\n    def get_conflicting_transactions(self, tx: Transaction, *, include_self: bool = False) -> Set[str]:\n        \"\"\"Returns a set of transaction hashes from the wallet history that are\n        directly conflicting with tx, i.e. they have common outpoints being\n        spent with tx.\n\n        include_self specifies whether the tx itself should be reported as a\n        conflict (if already in wallet history)\n        \"\"\"\n        conflicting_txns = set()\n        for txin in tx.inputs():\n            if txin.is_coinbase_input():\n                continue\n            prevout_hash = txin.prevout.txid.hex()\n            prevout_n = txin.prevout.out_idx\n            spending_tx_hash = self.db.get_spent_outpoint(prevout_hash, prevout_n)\n            if spending_tx_hash is None:\n                continue\n            # this outpoint has already been spent, by spending_tx\n            # annoying assert that has revealed several bugs over time:\n            assert self.db.get_transaction(spending_tx_hash), \"spending tx not in wallet db\"\n            conflicting_txns |= {spending_tx_hash}\n        if tx_hash := tx.txid():\n            if tx_hash in conflicting_txns:\n                # this tx is already in history, so it conflicts with itself\n                if len(conflicting_txns) > 1:\n                    raise Exception('Found conflicting transactions already in wallet history.')\n                if not include_self:\n                    conflicting_txns -= {tx_hash}\n        return conflicting_txns\n\n    @with_lock\n    def get_transaction(self, txid: str) -> Optional[Transaction]:\n        tx = self.db.get_transaction(txid)\n        if tx:\n            tx.deserialize()\n            for txin in tx._inputs:\n                tx_mined_info = self.get_tx_height(txin.prevout.txid.hex())\n                txin.block_height = tx_mined_info.height()\n                txin.block_txpos = tx_mined_info.txpos\n        return tx\n\n    def add_transaction(self, tx: Transaction, *, allow_unrelated=False, is_new=True) -> bool:\n        \"\"\"\n        Returns whether the tx was successfully added to the wallet history.\n        Note that a transaction may need to be added several times, if our\n        list of addresses has increased. This will return True even if the\n        transaction was already in self.db.\n        \"\"\"\n        assert tx, tx\n        # note: tx.is_complete() is not necessarily True; tx might be partial\n        # but it *needs* to have a txid:\n        tx_hash = tx.txid()\n        if tx_hash is None:\n            raise Exception(\"cannot add tx without txid to wallet history\")\n        # For sanity, try to serialize and deserialize tx early:\n        tx_from_any(str(tx))  # see if raises (no-side-effects)\n        with self.lock:\n            # NOTE: returning if tx in self.transactions might seem like a good idea\n            # BUT we track is_mine inputs in a txn, and during subsequent calls\n            # of add_transaction tx, we might learn of more-and-more inputs of\n            # being is_mine, as we roll the gap_limit forward\n            is_coinbase = tx.inputs()[0].is_coinbase_input()\n            tx_height = self.get_tx_height(tx_hash, force_local_if_missing_tx=False).height()\n            if not allow_unrelated:\n                # note that during sync, if the transactions are not properly sorted,\n                # it could happen that we think tx is unrelated but actually one of the inputs is is_mine.\n                # this is the main motivation for allow_unrelated\n                is_mine = any([self.is_mine(self.get_txin_address(txin)) for txin in tx.inputs()])\n                is_for_me = any([self.is_mine(txo.address) for txo in tx.outputs()])\n                if not is_mine and not is_for_me:\n                    raise UnrelatedTransactionException()\n            # Find all conflicting transactions.\n            # In case of a conflict,\n            #     1. confirmed > mempool > local\n            #     2. this new txn has priority over existing ones\n            # When this method exits, there must NOT be any conflict, so\n            # either keep this txn and remove all conflicting (along with dependencies)\n            #     or drop this txn\n            conflicting_txns = self.get_conflicting_transactions(tx)\n            if conflicting_txns:\n                existing_mempool_txn = any(\n                    self.get_tx_height(tx_hash2).height() in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT)\n                    for tx_hash2 in conflicting_txns)\n                existing_confirmed_txn = any(\n                    self.get_tx_height(tx_hash2).height() > 0\n                    for tx_hash2 in conflicting_txns)\n                if existing_confirmed_txn and tx_height <= 0:\n                    # this is a non-confirmed tx that conflicts with confirmed txns; drop.\n                    return False\n                if existing_mempool_txn and tx_height == TX_HEIGHT_LOCAL:\n                    # this is a local tx that conflicts with non-local txns; drop.\n                    return False\n                # keep this txn and remove all conflicting\n                for tx_hash2 in conflicting_txns:\n                    self.remove_transaction(tx_hash2)\n            # add inputs\n            def add_value_from_prev_output():\n                # note: this takes linear time in num is_mine outputs of prev_tx\n                addr = self.get_txin_address(txi)\n                if addr and self.is_mine(addr):\n                    outputs = self.db.get_txo_addr(prevout_hash, addr)\n                    try:\n                        v, is_cb = outputs[prevout_n]\n                    except KeyError:\n                        pass\n                    else:\n                        self.db.add_txi_addr(tx_hash, addr, ser, v)\n                        self.invalidate_cache()\n            for txi in tx.inputs():\n                if txi.is_coinbase_input():\n                    continue\n                prevout_hash = txi.prevout.txid.hex()\n                prevout_n = txi.prevout.out_idx\n                ser = txi.prevout.to_str()\n                self.db.set_spent_outpoint(prevout_hash, prevout_n, tx_hash)\n                add_value_from_prev_output()\n            # add outputs\n            for n, txo in enumerate(tx.outputs()):\n                v = txo.value\n                ser = tx_hash + ':%d'%n\n                scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey)\n                self.db.add_prevout_by_scripthash(scripthash, prevout=TxOutpoint.from_str(ser), value=v)\n                addr = txo.address\n                if addr and self.is_mine(addr):\n                    self.db.add_txo_addr(tx_hash, addr, n, v, is_coinbase)\n                    self.invalidate_cache()\n                    # give v to txi that spends me\n                    next_tx = self.db.get_spent_outpoint(tx_hash, n)\n                    if next_tx is not None:\n                        self.db.add_txi_addr(next_tx, addr, ser, v)\n                        self._add_tx_to_local_history(next_tx)\n            # add to local history\n            self._add_tx_to_local_history(tx_hash)\n            # save\n            self.db.add_transaction(tx_hash, tx)\n            self.db.add_num_inputs_to_tx(tx_hash, len(tx.inputs()))\n            if is_new:\n                util.trigger_callback('adb_added_tx', self, tx_hash, tx)\n            return True\n\n    @with_lock\n    def remove_transaction(self, tx_hash: str) -> None:\n        \"\"\"Removes a transaction AND all its dependents/children\n        from the wallet history.\n        \"\"\"\n        to_remove = {tx_hash}\n        to_remove |= self.get_depending_transactions(tx_hash)\n        for txid in to_remove:\n            self._remove_transaction(txid)\n\n    def _remove_transaction(self, tx_hash: str) -> None:\n        \"\"\"Removes a single transaction from the wallet history, and attempts\n         to undo all effects of the tx (spending inputs, creating outputs, etc).\n        \"\"\"\n        def remove_from_spent_outpoints():\n            # undo spends in spent_outpoints\n            if tx is not None:\n                # if we have the tx, this branch is faster\n                for txin in tx.inputs():\n                    if txin.is_coinbase_input():\n                        continue\n                    prevout_hash = txin.prevout.txid.hex()\n                    prevout_n = txin.prevout.out_idx\n                    self.db.remove_spent_outpoint(prevout_hash, prevout_n)\n            else:\n                # expensive but always works\n                for prevout_hash, prevout_n in self.db.list_spent_outpoints():\n                    spending_txid = self.db.get_spent_outpoint(prevout_hash, prevout_n)\n                    if spending_txid == tx_hash:\n                        self.db.remove_spent_outpoint(prevout_hash, prevout_n)\n\n        with self.lock:\n            self.logger.info(f\"removing tx from history {tx_hash}\")\n            tx = self.db.remove_transaction(tx_hash)\n            remove_from_spent_outpoints()\n            self._remove_tx_from_local_history(tx_hash)\n            self.invalidate_cache()\n            self.db.remove_txi(tx_hash)\n            self.db.remove_txo(tx_hash)\n            self.db.remove_tx_fee(tx_hash)\n            self.db.remove_verified_tx(tx_hash)\n            self.unverified_tx.pop(tx_hash, None)\n            self.unconfirmed_tx.pop(tx_hash, None)\n            if tx:\n                for idx, txo in enumerate(tx.outputs()):\n                    scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey)\n                    prevout = TxOutpoint(bfh(tx_hash), idx)\n                    self.db.remove_prevout_by_scripthash(scripthash, prevout=prevout, value=txo.value)\n        util.trigger_callback('adb_removed_tx', self, tx_hash, tx)\n\n    @with_lock\n    def get_depending_transactions(self, tx_hash: str) -> Set[str]:\n        \"\"\"Returns all (grand-)children of tx_hash in this wallet.\"\"\"\n        children = set()\n        for n in self.db.get_spent_outpoints(tx_hash):\n            other_hash = self.db.get_spent_outpoint(tx_hash, n)\n            children.add(other_hash)\n            children |= self.get_depending_transactions(other_hash)\n        return children\n\n    @with_lock\n    def receive_tx_callback(self, tx: Transaction, *, tx_height: Optional[int] = None) -> None:\n        txid = tx.txid()\n        assert txid is not None\n        if tx_height is not None:\n            # note: tx_height is only set by the unit tests: to inject a tx into the history\n            self.add_unverified_or_unconfirmed_tx(txid, tx_height)\n        self.add_transaction(tx, allow_unrelated=True)\n\n    @with_lock\n    def receive_history_callback(self, addr: str, hist, tx_fees: Dict[str, int]):\n        old_hist = self.get_address_history(addr)\n        for tx_hash, height in old_hist.items():\n            if (tx_hash, height) not in hist:\n                # make tx local\n                self.unverified_tx.pop(tx_hash, None)\n                self.unconfirmed_tx.pop(tx_hash, None)\n                self.db.remove_verified_tx(tx_hash)\n                if self.verifier:\n                    self.verifier.remove_spv_proof_for_tx(tx_hash)\n        self.db.set_addr_history(addr, hist)\n\n        for tx_hash, tx_height in hist:\n            # add it in case it was previously unconfirmed\n            self.add_unverified_or_unconfirmed_tx(tx_hash, tx_height)\n            # if addr is new, we have to recompute txi and txo\n            tx = self.db.get_transaction(tx_hash)\n            if tx is None:\n                continue\n            self.add_transaction(tx, allow_unrelated=True, is_new=False)\n            # if we already had this tx, see if its height changed (e.g. local->unconfirmed)\n            old_height = old_hist.get(tx_hash, None)\n            if old_height is not None and old_height != tx_height:\n                util.trigger_callback('adb_tx_height_changed', self, tx_hash, old_height, tx_height)\n\n        # Store fees\n        for tx_hash, fee_sat in tx_fees.items():\n            self.db.add_tx_fee_from_server(tx_hash, fee_sat)\n\n    @with_lock\n    @profiler\n    def load_local_history(self):\n        self._history_local = {}  # type: Dict[str, Set[str]]  # address -> set(txid)\n        self._address_history_changed_events = defaultdict(asyncio.Event)  # address -> Event\n        for txid in itertools.chain(self.db.list_txi(), self.db.list_txo()):\n            self._add_tx_to_local_history(txid)\n\n    @with_lock\n    @profiler\n    def check_history(self):\n        hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.db.get_history()))\n        hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.db.get_history()))\n        for addr in hist_addrs_not_mine:\n            self.db.remove_addr_history(addr)\n        for addr in hist_addrs_mine:\n            hist = self.db.get_addr_history(addr)\n            for tx_hash, tx_height in hist:\n                if self.db.get_txi_addresses(tx_hash) or self.db.get_txo_addresses(tx_hash):\n                    continue\n                tx = self.db.get_transaction(tx_hash)\n                if tx is not None:\n                    self.add_transaction(tx, allow_unrelated=True)\n\n    @with_lock\n    def remove_local_transactions_we_dont_have(self):\n        for txid in itertools.chain(self.db.list_txi(), self.db.list_txo()):\n            tx_height = self.get_tx_height(txid).height()\n            if tx_height == TX_HEIGHT_LOCAL and not self.db.get_transaction(txid):\n                self.remove_transaction(txid)\n\n    @with_lock\n    def clear_history(self):\n        self.db.clear_history()\n        self._history_local.clear()\n        self.invalidate_cache()\n\n    @with_lock\n    def _get_tx_sort_key(self, tx_hash: str) -> Tuple[int, int]:\n        \"\"\"Returns a key to be used for sorting txs.\"\"\"\n        tx_mined_info = self.get_tx_height(tx_hash)\n        height = self.tx_height_to_sort_height(tx_mined_info.height())\n        txpos = tx_mined_info.txpos or -1\n        return height, txpos\n\n    @classmethod\n    def tx_height_to_sort_height(cls, height: int = None):\n        \"\"\"Return a height-like value to be used for sorting txs.\"\"\"\n        if height is not None:\n            if height > 0:\n                return height\n            if height == TX_HEIGHT_UNCONFIRMED:\n                return TX_HEIGHT_INF\n            if height == TX_HEIGHT_UNCONF_PARENT:\n                return TX_HEIGHT_INF + 1\n            if height == TX_HEIGHT_FUTURE:\n                return TX_HEIGHT_INF + 2\n            if height == TX_HEIGHT_LOCAL:\n                return TX_HEIGHT_INF + 3\n        return TX_HEIGHT_INF + 100\n\n    def with_local_height_cached(func):\n        # get local height only once, as it's relatively expensive.\n        # take care that nested calls work as expected\n        def f(self, *args, **kwargs):\n            orig_val = getattr(self.threadlocal_cache, 'local_height', None)\n            self.threadlocal_cache.local_height = orig_val or self.get_local_height()\n            try:\n                return func(self, *args, **kwargs)\n            finally:\n                self.threadlocal_cache.local_height = orig_val\n        return f\n\n    @with_lock\n    @with_local_height_cached\n    def get_history(self, domain) -> Sequence[HistoryItem]:\n        domain = set(domain)\n        # 1. Get the history of each address in the domain, maintain the\n        #    delta of a tx as the sum of its deltas on domain addresses\n        tx_deltas = defaultdict(int)  # type: Dict[str, int]\n        for addr in domain:\n            h = self.get_address_history(addr).items()\n            for tx_hash, height in h:\n                tx_deltas[tx_hash] += self.get_tx_delta(tx_hash, addr)\n        # 2. create sorted history\n        history = []\n        for tx_hash in tx_deltas:\n            delta = tx_deltas[tx_hash]\n            tx_mined_status = self.get_tx_height(tx_hash)\n            fee = self.get_tx_fee(tx_hash)\n            history.append((tx_hash, tx_mined_status, delta, fee))\n        history.sort(key = lambda x: self._get_tx_sort_key(x[0]))\n        # 3. add balance\n        h2 = []\n        balance = 0\n        for tx_hash, tx_mined_status, delta, fee in history:\n            balance += delta\n            h2.append(HistoryItem(\n                txid=tx_hash,\n                tx_mined_status=tx_mined_status,\n                delta=delta,\n                fee=fee,\n                balance=balance))\n        # sanity check\n        c, u, x = self.get_balance(domain)\n        if balance != c + u + x:\n            self.logger.error(f'sanity check failed! c={c},u={u},x={x} while history balance={balance}')\n            raise Exception(\"wallet.get_history() failed balance sanity-check\")\n        return h2\n\n    @with_lock\n    def _add_tx_to_local_history(self, txid):\n        for addr in itertools.chain(self.db.get_txi_addresses(txid), self.db.get_txo_addresses(txid)):\n            cur_hist = self._history_local.get(addr, set())\n            cur_hist.add(txid)\n            self._history_local[addr] = cur_hist\n            self._mark_address_history_changed(addr)\n\n    @with_lock\n    def _remove_tx_from_local_history(self, txid):\n        for addr in itertools.chain(self.db.get_txi_addresses(txid), self.db.get_txo_addresses(txid)):\n            cur_hist = self._history_local.get(addr, set())\n            try:\n                cur_hist.remove(txid)\n            except KeyError:\n                pass\n            else:\n                self._history_local[addr] = cur_hist\n                self._mark_address_history_changed(addr)\n\n    def _mark_address_history_changed(self, addr: str) -> None:\n        def set_and_clear():\n            event = self._address_history_changed_events[addr]\n            # history for this address changed, wake up coroutines:\n            event.set()\n            # clear event immediately so that coroutines can wait() for the next change:\n            event.clear()\n        if self.asyncio_loop:\n            self.asyncio_loop.call_soon_threadsafe(set_and_clear)\n\n    async def wait_for_address_history_to_change(self, addr: str) -> None:\n        \"\"\"Wait until the server tells us about a new transaction related to addr.\n\n        Unconfirmed and confirmed transactions are not distinguished, and so e.g. SPV\n        is not taken into account.\n        \"\"\"\n        assert self.is_mine(addr), \"address needs to be is_mine to be watched\"\n        await self._address_history_changed_events[addr].wait()\n\n    @with_lock\n    def add_unverified_or_unconfirmed_tx(self, tx_hash: str, tx_height: int) -> None:\n        assert tx_height >= TX_HEIGHT_UNCONF_PARENT, f\"got {tx_height=} for {tx_hash=}\"  # forbid local/future txs here\n        if self.db.is_in_verified_tx(tx_hash):\n            if tx_height <= 0:\n                # tx was previously SPV-verified but now in mempool (probably reorg)\n                self.db.remove_verified_tx(tx_hash)\n                self.unconfirmed_tx[tx_hash] = tx_height\n                if self.verifier:\n                    self.verifier.remove_spv_proof_for_tx(tx_hash)\n        else:\n            if tx_height > 0:\n                self.unverified_tx[tx_hash] = tx_height\n            else:\n                self.unconfirmed_tx[tx_hash] = tx_height\n\n    @with_lock\n    def remove_unverified_tx(self, tx_hash: str, tx_height: int) -> None:\n        new_height = self.unverified_tx.get(tx_hash)\n        if new_height == tx_height:\n            self.unverified_tx.pop(tx_hash, None)\n\n    def add_verified_tx(self, tx_hash: str, info: TxMinedInfo):\n        # Remove from the unverified map and add to the verified map\n        with self.lock:\n            self.unverified_tx.pop(tx_hash, None)\n            self.db.add_verified_tx(tx_hash, info)\n            self.invalidate_cache()\n        util.trigger_callback('adb_added_verified_tx', self, tx_hash)\n\n    @with_lock\n    def get_unverified_txs(self) -> Dict[str, int]:\n        '''Returns a map from tx hash to transaction height'''\n        return dict(self.unverified_tx)  # copy\n\n    def undo_verifications(self, blockchain: Blockchain, above_height: int) -> Set[str]:\n        '''Used by the verifier when a reorg has happened'''\n        txs = set()\n        with self.lock:\n            for tx_hash in self.db.list_verified_tx():\n                info = self.db.get_verified_tx(tx_hash)\n                tx_height = info._height\n                if tx_height > above_height:\n                    header = blockchain.read_header(tx_height)\n                    if not header or hash_header(header) != info.header_hash:\n                        self.db.remove_verified_tx(tx_hash)\n                        # NOTE: we should add these txns to self.unverified_tx,\n                        # but with what height?\n                        # If on the new fork after the reorg, the txn is at the\n                        # same height, we will not get a status update for the\n                        # address. If the txn is not mined or at a diff height,\n                        # we should get a status update. Unless we put tx into\n                        # unverified_tx, it will turn into local. So we put it\n                        # into unverified_tx with the old height, and if we get\n                        # a status update, that will overwrite it.\n                        self.unverified_tx[tx_hash] = tx_height\n                        txs.add(tx_hash)\n\n        for tx_hash in txs:\n            util.trigger_callback('adb_removed_verified_tx', self, tx_hash)\n        return txs\n\n    def get_local_height(self) -> int:\n        \"\"\" return last known height if we are offline \"\"\"\n        cached_local_height = getattr(self.threadlocal_cache, 'local_height', None)\n        if cached_local_height is not None:\n            return cached_local_height\n        return self.network.get_local_height() if self.network else self.db.get('stored_height', 0)\n\n    def _update_stored_local_height(self) -> None:\n        self.db.put('stored_height', self.get_local_height())\n\n    def set_future_tx(self, txid: str, *, wanted_height: int):\n        \"\"\"Mark a local tx as \"future\" (encumbered by a timelock).\n        wanted_height is the min (abs) block height at which the tx can get into the mempool (be broadcast).\n                      note: tx becomes consensus-valid to be mined in a block at height wanted_height+1\n        In case of a CSV-locked tx with unconfirmed inputs, the wanted_height is a best-case guess.\n        \"\"\"\n        with self.lock:\n            old_height = self.future_tx.get(txid) or None\n            self.future_tx[txid] = wanted_height\n        if old_height != wanted_height:\n            util.trigger_callback('adb_set_future_tx', self, txid)\n\n    def get_tx_height(\n        self,\n        tx_hash: str,\n        *,\n        force_local_if_missing_tx: bool = True,\n    ) -> TxMinedInfo:\n        if tx_hash is None:  # ugly backwards compat...\n            return TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)\n        with self.lock:\n            if verified_tx_mined_info := self.db.get_verified_tx(tx_hash):  # mined and spv-ed\n                conf = max(self.get_local_height() - verified_tx_mined_info._height + 1, 0)\n                tx_mined_info = dataclasses.replace(verified_tx_mined_info, conf=conf)\n            elif tx_hash in self.unverified_tx:  # mined, no spv\n                height = self.unverified_tx[tx_hash]\n                tx_mined_info = TxMinedInfo(_height=height, conf=0)\n            elif tx_hash in self.unconfirmed_tx: # mempool\n                height = self.unconfirmed_tx[tx_hash]\n                tx_mined_info = TxMinedInfo(_height=height, conf=0)\n            elif wanted_height := self.future_tx.get(tx_hash):  # future\n                if wanted_height > self.get_local_height():\n                    tx_mined_info = TxMinedInfo(_height=TX_HEIGHT_FUTURE, conf=0, wanted_height=wanted_height)\n                else:\n                    tx_mined_info = TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)\n            else:  # local\n                tx_mined_info = TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)\n            if tx_mined_info.height() in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE):\n                return tx_mined_info\n            if force_local_if_missing_tx:\n                # It can happen for a txid in any state (unconf/unverified/verified) that we\n                # don't have the raw tx yet, simply due to network timing.\n                # Having only a partial tx is another variant of this.\n                # FIXME in fact even if we have a complete tx saved, the server might have\n                #       a different tx if only the witness differs. We should compare wtxids.\n                tx = self.db.get_transaction(tx_hash)\n                if tx is None or isinstance(tx, PartialTransaction):\n                    return TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)\n            return tx_mined_info\n\n    def up_to_date_changed(self) -> None:\n        # fire triggers\n        util.trigger_callback('adb_set_up_to_date', self)\n\n    def is_up_to_date(self):\n        if not self.synchronizer or not self.verifier:\n            return False\n        return self.synchronizer.is_up_to_date() and self.verifier.is_up_to_date()\n\n    def reset_netrequest_counters(self) -> None:\n        if self.synchronizer:\n            self.synchronizer.reset_request_counters()\n        if self.verifier:\n            self.verifier.reset_request_counters()\n\n    def get_history_sync_state_details(self) -> Tuple[int, int]:\n        nsent, nans = 0, 0\n        if self.synchronizer:\n            n1, n2 = self.synchronizer.num_requests_sent_and_answered()\n            nsent += n1\n            nans += n2\n        if self.verifier:\n            n1, n2 = self.verifier.num_requests_sent_and_answered()\n            nsent += n1\n            nans += n2\n        return nsent, nans\n\n    @with_lock\n    def get_tx_delta(self, tx_hash: str, address: str) -> int:\n        \"\"\"effect of tx on address\"\"\"\n        delta = 0\n        # subtract the value of coins sent from address\n        d = self.db.get_txi_addr(tx_hash, address)\n        for n, v in d:\n            delta -= v\n        # add the value of the coins received at address\n        d = self.db.get_txo_addr(tx_hash, address)\n        for n, (v, cb) in d.items():\n            delta += v\n        return delta\n\n    @with_lock\n    def get_tx_fee(self, txid: str) -> Optional[int]:\n        \"\"\"Returns tx_fee or None. Use server fee only if tx is unconfirmed and not mine.\n\n        Note: being fast is prioritised over completeness here. We try to avoid deserializing\n              the tx, as that is expensive if we are called for the whole history. We sometimes\n              incorrectly early-exit and return None, e.g. for not-all-ismine-input txs,\n              where we could calculate the fee if we deserialized (but to see if we have all\n              the parent txs available, we would have to deserialize first).\n              More expensive but more complete alternative: wallet.get_tx_info(tx).fee\n        \"\"\"\n        # check if stored fee is available\n        fee = self.db.get_tx_fee(txid, trust_server=False)\n        if fee is not None:\n            return fee\n        # delete server-sent fee for confirmed txns\n        confirmed = self.get_tx_height(txid).conf > 0\n        if confirmed:\n            self.db.add_tx_fee_from_server(txid, None)\n        # if all inputs are ismine, try to calc fee now;\n        # otherwise, return stored value\n        num_all_inputs = self.db.get_num_all_inputs_of_tx(txid)\n        if num_all_inputs is not None:\n            # check if tx is mine\n            num_ismine_inputs = self.db.get_num_ismine_inputs_of_tx(txid)\n            assert num_ismine_inputs <= num_all_inputs, (num_ismine_inputs, num_all_inputs)\n            # trust server if tx is unconfirmed and not mine\n            if num_ismine_inputs < num_all_inputs:\n                return None if confirmed else self.db.get_tx_fee(txid, trust_server=True)\n        # lookup tx and deserialize it.\n        # note that deserializing is expensive, hence above hacks\n        tx = self.db.get_transaction(txid)\n        if not tx:\n            return None\n        # compute fee if possible\n        v_in = v_out = 0\n        for txin in tx.inputs():\n            addr = self.get_txin_address(txin)\n            value = self.get_txin_value(txin, address=addr)\n            if value is None:\n                v_in = None\n            elif v_in is not None:\n                v_in += value\n        for txout in tx.outputs():\n            v_out += txout.value\n        if v_in is not None:\n            fee = v_in - v_out\n        else:\n            fee = None\n        # save result\n        self.db.add_tx_fee_we_calculated(txid, fee)\n        self.db.add_num_inputs_to_tx(txid, len(tx.inputs()))\n        return fee\n\n    @with_lock\n    def get_addr_io(self, address: str):\n        h = self.get_address_history(address).items()\n        received = {}  # type: Dict[str, tuple[int, int, int, bool]]\n        sent = {}  # type: Dict[str, tuple[str, int, int]]\n        for tx_hash, height in h:\n            tx_mined_info = self.get_tx_height(tx_hash)\n            txpos = tx_mined_info.txpos if tx_mined_info.txpos is not None else -1\n            d = self.db.get_txo_addr(tx_hash, address)\n            for n, (v, is_cb) in d.items():\n                received[tx_hash + ':%d'%n] = (height, txpos, v, is_cb)\n            l = self.db.get_txi_addr(tx_hash, address)\n            for txi, v in l:\n                sent[txi] = tx_hash, height, txpos\n        return received, sent\n\n    def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]:\n        received, sent = self.get_addr_io(address)\n        out = {}\n        for prevout_str, v in received.items():\n            tx_height, tx_pos, value, is_cb = v\n            prevout = TxOutpoint.from_str(prevout_str)\n            utxo = PartialTxInput(prevout=prevout, is_coinbase_output=is_cb)\n            utxo._trusted_address = address\n            utxo._trusted_value_sats = value\n            utxo.block_height = tx_height\n            utxo.block_txpos = tx_pos\n            if prevout_str in sent:\n                txid, height, pos = sent[prevout_str]\n                utxo.spent_txid = txid\n                utxo.spent_height = height\n            else:\n                utxo.spent_txid = None\n                utxo.spent_height = None\n            out[prevout] = utxo\n        return out\n\n    def get_addr_utxo(self, address: str) -> Dict[TxOutpoint, PartialTxInput]:\n        out = self.get_addr_outputs(address)\n        for k, v in list(out.items()):\n            if v.spent_height is not None:\n                out.pop(k)\n        return out\n\n    # return the total amount ever received by an address\n    def get_addr_received(self, address):\n        received, sent = self.get_addr_io(address)\n        return sum([value for height, pos, value, is_cb in received.values()])\n\n    @with_lock\n    @with_local_height_cached\n    def get_balance(self, domain, *, excluded_addresses: Set[str] = None,\n                    excluded_coins: Set[str] = None) -> Tuple[int, int, int]:\n        \"\"\"Return the balance of a set of addresses:\n        confirmed and matured, unconfirmed, unmatured\n        Note: intended for display-purposes. would need extreme care for \"has enough funds\" checks (see #8835)\n        \"\"\"\n        if excluded_addresses is None:\n            excluded_addresses = set()\n        assert isinstance(excluded_addresses, set), f\"excluded_addresses should be set, not {type(excluded_addresses)}\"\n        domain = set(domain) - excluded_addresses\n        if excluded_coins is None:\n            excluded_coins = set()\n        assert isinstance(excluded_coins, set), f\"excluded_coins should be set, not {type(excluded_coins)}\"\n\n        cache_key = sha256(','.join(sorted(domain)) + ';'\n                           + ','.join(sorted(excluded_coins)))\n        cached_value = self._get_balance_cache.get(cache_key)\n        if cached_value:\n            return cached_value\n\n        coins = {}\n        for address in domain:\n            coins.update(self.get_addr_outputs(address))\n\n        c = u = x = 0\n        mempool_height = self.get_local_height() + 1  # height of next block\n        for utxo in coins.values():  # type: PartialTxInput\n            if utxo.spent_height is not None:\n                continue\n            if utxo.prevout.to_str() in excluded_coins:\n                continue\n            v = utxo.value_sats()\n            tx_height = utxo.block_height\n            is_cb = utxo.is_coinbase_output()\n            if is_cb and tx_height + COINBASE_MATURITY > mempool_height:\n                x += v\n            elif tx_height > 0:\n                c += v\n            else:\n                txid = utxo.prevout.txid.hex()\n                tx = self.db.get_transaction(txid)\n                assert tx is not None # txid comes from get_addr_io\n                # we look at the outputs that are spent by this transaction\n                # if those outputs are ours and confirmed, we count this coin as confirmed\n                confirmed_spent_amount = 0\n                for txin in tx.inputs():\n                    if txin.prevout in coins:\n                        coin = coins[txin.prevout]\n                        if coin.block_height > 0:\n                            confirmed_spent_amount += coin.value_sats()\n                # Compare amount, in case tx has confirmed and unconfirmed inputs, or is a coinjoin.\n                # (fixme: tx may have multiple change outputs)\n                if confirmed_spent_amount >= v:\n                    c += v\n                else:\n                    c += confirmed_spent_amount\n                    u += v - confirmed_spent_amount\n        result = c, u, x\n        # cache result.\n        # Cache needs to be invalidated if a transaction is added to/\n        # removed from history; or on new blocks (maturity...)\n        self._get_balance_cache[cache_key] = result\n        return result\n\n    @with_lock\n    @with_local_height_cached\n    def get_utxos(\n            self,\n            domain,\n            *,\n            excluded_addresses=None,\n            mature_only: bool = False,\n            confirmed_funding_only: bool = False,\n            confirmed_spending_only: bool = False,\n            nonlocal_only: bool = False,\n            block_height: int = None,\n    ) -> Sequence[PartialTxInput]:\n        if block_height is not None:\n            # caller wants the UTXOs we had at a given height; check other parameters\n            assert confirmed_funding_only\n            assert confirmed_spending_only\n            assert nonlocal_only\n        else:\n            block_height = self.get_local_height()\n        coins = []\n        domain = set(domain)\n        if excluded_addresses:\n            domain = set(domain) - set(excluded_addresses)\n        mempool_height = block_height + 1  # height of next block\n        cache_key = sha256(\n            ','.join(sorted(domain))\n            + f\";{mature_only};{confirmed_funding_only};{confirmed_spending_only};{nonlocal_only};{block_height}\"\n        )\n        cached = self._get_utxos_cache.get(cache_key)\n        if cached is not None:\n            return copy.deepcopy(cached)\n        for addr in domain:\n            txos = self.get_addr_outputs(addr)\n            for txo in txos.values():\n                if txo.spent_height is not None:\n                    if not confirmed_spending_only:\n                        continue\n                    if confirmed_spending_only and 0 < txo.spent_height <= block_height:\n                        continue\n                if confirmed_funding_only and not (0 < txo.block_height <= block_height):\n                    continue\n                if nonlocal_only and txo.block_height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE):\n                    continue\n                if (mature_only and txo.is_coinbase_output()\n                        and txo.block_height + COINBASE_MATURITY > mempool_height):\n                    continue\n                coins.append(txo)\n                continue\n        self._get_utxos_cache[cache_key] = copy.deepcopy(coins)\n        return coins\n\n    def is_used(self, address: str) -> bool:\n        \"\"\"Whether any tx ever touched `address`.\"\"\"\n        return self.get_address_history_len(address) != 0\n\n    def is_used_as_from_address(self, address: str) -> bool:\n        \"\"\"Whether any tx ever spent from `address`.\"\"\"\n        received, sent = self.get_addr_io(address)\n        return len(sent) > 0\n\n    def is_empty(self, address: str) -> bool:\n        coins = self.get_addr_utxo(address)\n        return not bool(coins)\n\n    @with_lock\n    @with_local_height_cached\n    def address_is_old(self, address: str, *, req_conf: int = 3) -> bool:\n        \"\"\"Returns whether address has any history that is deeply confirmed.\n        Used for reorg-safe(ish) gap limit roll-forward.\n        \"\"\"\n        max_conf = -1\n        h = self.db.get_addr_history(address)\n        needs_spv_check = not self.config.NETWORK_SKIPMERKLECHECK\n        for tx_hash, tx_height in h:\n            if needs_spv_check:\n                tx_age = self.get_tx_height(tx_hash).conf\n            else:\n                if tx_height <= 0:\n                    tx_age = 0\n                else:\n                    tx_age = self.get_local_height() - tx_height + 1\n            max_conf = max(max_conf, tx_age)\n        return max_conf >= req_conf\n\n    @with_lock\n    def get_spender(self, outpoint: str) -> Optional[str]:\n        \"\"\"\n        returns txid spending outpoint.\n        subscribes to addresses as a side effect.\n        \"\"\"\n        prev_txid, index = outpoint.split(':')\n        spender_txid = self.db.get_spent_outpoint(prev_txid, int(index))\n        # discard local spenders\n        tx_mined_status = self.get_tx_height(spender_txid)\n        if tx_mined_status.height() in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:\n            spender_txid = None\n        if not spender_txid:\n            return None\n        spender_tx = self.get_transaction(spender_txid)\n        for i, o in enumerate(spender_tx.outputs()):\n            if o.address is None:\n                continue\n            if not self.is_mine(o.address):\n                self.add_address(o.address)\n        return spender_txid\n\n    def get_tx_mined_depth(self, txid: str):\n        if not txid:\n            return TxMinedDepth.FREE\n        tx_mined_depth = self.get_tx_height(txid)\n        height, conf = tx_mined_depth.height(), tx_mined_depth.conf\n        if conf > 20:  # FIXME unify with lnutil.REDEEM_AFTER_DOUBLE_SPENT_DELAY ?\n            return TxMinedDepth.DEEP\n        elif conf > 0:\n            return TxMinedDepth.SHALLOW\n        elif height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):\n            return TxMinedDepth.MEMPOOL\n        elif height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE):\n            return TxMinedDepth.FREE\n        elif height > 0 and conf == 0:\n            # unverified but claimed to be mined\n            return TxMinedDepth.MEMPOOL\n        else:\n            raise NotImplementedError()\n\n    def is_deeply_mined(self, txid):\n        return self.get_tx_mined_depth(txid) == TxMinedDepth.DEEP\n"
  },
  {
    "path": "electrum/base_crash_reporter.py",
    "content": "# Electrum - lightweight Bitcoin client\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport asyncio\nimport json\nimport locale\nimport traceback\nimport sys\nimport queue\nfrom typing import TYPE_CHECKING, NamedTuple, Optional, TypedDict\nfrom types import TracebackType\n\nfrom .version import ELECTRUM_VERSION\nfrom . import constants\nfrom .i18n import _\nfrom .util import make_aiohttp_session, error_text_str_to_safe_str\nfrom .logging import describe_os_version, Logger, get_git_version\nfrom .crypto import sha256\n\nif TYPE_CHECKING:\n    from .network import ProxySettings\n\n\n_tainted_by_console = False\ndef taint_reports_by_console_usage():\n    global _tainted_by_console\n    _tainted_by_console = True\n\n\nclass CrashReportResponse(NamedTuple):\n    status: Optional[str]\n    text: str\n    url: Optional[str]\n\n\nclass BaseCrashReporter(Logger):\n    report_server = \"https://crashhub.electrum.org\"\n    issue_template = \"\"\"<h2>Traceback</h2>\n<pre>\n{traceback}\n</pre>\n\n<h2>Additional information</h2>\n<ul>\n  <li>Electrum version: {app_version}</li>\n  <li>Python version: {python_version}</li>\n  <li>Operating system: {os}</li>\n  <li>Wallet type: {wallet_type}</li>\n  <li>Locale: {locale}</li>\n</ul>\n    \"\"\"\n    CRASH_MESSAGE = _('Something went wrong while executing Electrum.')\n    CRASH_TITLE = _('Sorry!')\n    REQUEST_HELP_MESSAGE = _('To help us diagnose and fix the problem, you can send us a bug report that contains '\n                             'useful debug information:')\n    DESCRIBE_ERROR_MESSAGE = _(\"Please briefly describe what led to the error (optional):\")\n    ASK_CONFIRM_SEND = _(\"Do you want to send this report?\")\n    USER_COMMENT_PLACEHOLDER = _(\"Do not enter sensitive/private information here. \"\n                                 \"The report will be visible on the public issue tracker.\")\n\n    exc_args: tuple[type[BaseException], BaseException, TracebackType | None]\n\n    def __init__(\n        self,\n        exctype: type[BaseException],\n        excvalue: BaseException,\n        tb: TracebackType | None,\n    ):\n        Logger.__init__(self)\n        self.exc_args = (exctype, excvalue, tb)\n\n    def send_report(self, asyncio_loop, proxy: 'ProxySettings', *, timeout=None) -> CrashReportResponse:\n        # FIXME the caller needs to catch generic \"Exception\", as this method does not have a well-defined API...\n        if (constants.net.GENESIS[-4:] not in [\n            \"e26f\",  # mainnet\n            \"4943\",  # testnet 3\n            \"f043\",  # testnet 4\n            \"1ef6\",  # signet\n        ] and \".electrum.org\" in BaseCrashReporter.report_server):\n            # Gah! Some kind of altcoin wants to send us crash reports.\n            raise Exception(_(\"Missing report URL.\"))\n        report = self.get_traceback_info(*self.exc_args)\n        report.update(self.get_additional_info())\n        report = json.dumps(report)\n        coro = self.do_post(proxy, BaseCrashReporter.report_server + \"/crash.json\", data=report)\n        response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(timeout)\n        self.logger.info(\n            f\"Crash report sent. Got response [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(response)}\")\n        response = json.loads(response)\n        assert isinstance(response, dict), type(response)\n        # sanitize URL\n        if location := response.get(\"location\"):\n            assert isinstance(location, str)\n            base_issues_url = constants.GIT_REPO_ISSUES_URL\n            if not base_issues_url.endswith(\"/\"):\n                base_issues_url = base_issues_url + \"/\"\n            if not location.startswith(base_issues_url):\n                location = None\n        ret = CrashReportResponse(\n            status=response.get(\"status\"),\n            url=location,\n            text=_(\"Thanks for reporting this issue!\"),\n        )\n        return ret\n\n    async def do_post(self, proxy: 'ProxySettings', url, data) -> str:\n        async with make_aiohttp_session(proxy) as session:\n            async with session.post(url, data=data, raise_for_status=True) as resp:\n                return await resp.text()\n\n    @classmethod\n    def get_traceback_info(\n        cls,\n        exctype: type[BaseException],\n        excvalue: BaseException,\n        tb: TracebackType | None,\n    ) -> TypedDict('TBInfo', {'exc_string': str, 'stack': str, 'id': dict[str, str]}):\n        exc_string = str(excvalue)\n        stack = traceback.extract_tb(tb)\n        readable_trace = cls._get_traceback_str_to_send(exctype, excvalue, tb)\n        _id = {\n            \"file\": stack[-1].filename if len(stack) else '<no stack>',\n            \"name\": stack[-1].name if len(stack) else '<no stack>',\n            \"type\": exctype.__name__\n        }  # note: this is the \"id\" the crash reporter server uses to group together reports.\n        return {\n            \"exc_string\": exc_string,\n            \"stack\": readable_trace,\n            \"id\": _id,\n        }\n\n    @classmethod\n    def get_traceback_groupid_hash(\n        cls,\n        exctype: type[BaseException],\n        excvalue: BaseException,\n        tb: TracebackType | None,\n    ) -> bytes:\n        tb_info = cls.get_traceback_info(exctype, excvalue, tb)\n        _id = tb_info[\"id\"]\n        return sha256(str(_id))\n\n    def get_additional_info(self):\n        app_version = (get_git_version() or ELECTRUM_VERSION)\n        if _tainted_by_console:\n            app_version += \"-consoletaint\"\n        args = {\n            \"app_version\": app_version,\n            \"python_version\": sys.version,\n            \"os\": describe_os_version(),\n            \"wallet_type\": \"unknown\",\n            \"locale\": locale.getlocale()[0] or \"?\",\n            \"description\": self.get_user_description()\n        }\n        try:\n            args[\"wallet_type\"] = self.get_wallet_type()\n        except Exception:\n            # Maybe the wallet isn't loaded yet\n            pass\n        return args\n\n    @classmethod\n    def _get_traceback_str_to_send(\n        cls,\n        exctype: type[BaseException],\n        excvalue: BaseException,\n        tb: TracebackType | None,\n    ) -> str:\n        # make sure that traceback sent to crash reporter contains\n        # e.__context__ and e.__cause__, i.e. if there was a chain of\n        # exceptions, we want the full traceback for the whole chain.\n        return \"\".join(traceback.format_exception(exctype, excvalue, tb))\n\n    def _get_traceback_str_to_display(self) -> str:\n        # overridden in Qt subclass\n        return self._get_traceback_str_to_send(*self.exc_args)\n\n    def get_report_string(self):\n        info = self.get_additional_info()\n        info[\"traceback\"] = self._get_traceback_str_to_display()\n        return self.issue_template.format(**info)\n\n    def get_user_description(self):\n        raise NotImplementedError\n\n    def get_wallet_type(self) -> str:\n        raise NotImplementedError\n\n\nclass EarlyExceptionsQueue:\n    \"\"\"Helper singleton for explicitly sending exceptions to crash reporter.\n\n    Typically the GUIs set up an \"exception hook\" that catches all otherwise\n    uncaught exceptions (which unroll the stack of a thread completely).\n    This class provides methods to report *any* exception, and queueing logic\n    that delays processing until the exception hook is set up.\n    \"\"\"\n\n    _is_exc_hook_ready = False\n    _exc_queue = queue.Queue()\n\n    @classmethod\n    def set_hook_as_ready(cls):\n        \"\"\"Flush the queue and disable it for future exceptions.\"\"\"\n        if cls._is_exc_hook_ready:\n            return\n        cls._is_exc_hook_ready = True\n        while cls._exc_queue.qsize() > 0:\n            e = cls._exc_queue.get()\n            cls._send_exception_to_crash_reporter(e)\n\n    @classmethod\n    def send_exception_to_crash_reporter(cls, e: BaseException):\n        if cls._is_exc_hook_ready:\n            cls._send_exception_to_crash_reporter(e)\n        else:\n            cls._exc_queue.put(e)\n\n    @staticmethod\n    def _send_exception_to_crash_reporter(e: BaseException):\n        assert EarlyExceptionsQueue._is_exc_hook_ready\n        sys.excepthook(type(e), e, e.__traceback__)\n\n\nsend_exception_to_crash_reporter = EarlyExceptionsQueue.send_exception_to_crash_reporter\n\n\ndef trigger_crash():\n    # note: do not change the type of the exception, the message,\n    # or the name of this method. All reports generated through this\n    # method will be grouped together by the crash reporter, and thus\n    # don't spam the issue tracker.\n\n    class TestingException(Exception):\n        pass\n\n    def crash_test():\n        raise TestingException(\"triggered crash for testing purposes\")\n\n    import threading\n    t = threading.Thread(target=crash_test)\n    t.start()\n"
  },
  {
    "path": "electrum/bip21.py",
    "content": "import urllib\nimport urllib.parse\nimport re\nfrom decimal import Decimal\nfrom typing import Optional\n\nfrom . import bitcoin\nfrom .util import format_satoshis_plain\nfrom .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC\nfrom .lnaddr import lndecode, LnDecodeException\n\n# note: when checking against these, use .lower() to support case-insensitivity\nBITCOIN_BIP21_URI_SCHEME = 'bitcoin'\nLIGHTNING_URI_SCHEME = 'lightning'\n\n\nclass InvalidBitcoinURI(Exception):\n    pass\n\n\ndef parse_bip21_URI(uri: str) -> dict:\n    \"\"\"Raises InvalidBitcoinURI on malformed URI.\"\"\"\n\n    if not isinstance(uri, str):\n        raise InvalidBitcoinURI(f\"expected string, not {repr(uri)}\")\n\n    if ':' not in uri:\n        if not bitcoin.is_address(uri):\n            raise InvalidBitcoinURI(\"Not a bitcoin address\")\n        return {'address': uri}\n\n    u = urllib.parse.urlparse(uri)\n    if u.scheme.lower() != BITCOIN_BIP21_URI_SCHEME:\n        raise InvalidBitcoinURI(\"Not a bitcoin URI\")\n    address = u.path\n\n    # python for android fails to parse query\n    if address.find('?') > 0:\n        address, query = u.path.split('?')\n        pq = urllib.parse.parse_qs(query)\n    else:\n        pq = urllib.parse.parse_qs(u.query)\n\n    for k, v in pq.items():\n        if len(v) != 1:\n            raise InvalidBitcoinURI(f'Duplicate Key: {repr(k)}')\n        if k.startswith('req-'):\n            # we have no support for any req-* query parameters\n            raise InvalidBitcoinURI(f'Unsupported Key: {repr(k)}')\n\n    out = {k: v[0] for k, v in pq.items()}\n    if address:\n        if not bitcoin.is_address(address):\n            raise InvalidBitcoinURI(f\"Invalid bitcoin address: {address}\")\n        out['address'] = address\n    if 'amount' in out:\n        am = out['amount']\n        try:\n            m = re.match(r'([0-9.]+)X([0-9])', am)\n            if m:\n                k = int(m.group(2)) - 8\n                amount = Decimal(m.group(1)) * pow(Decimal(10), k)\n            else:\n                amount = Decimal(am) * COIN\n            if amount > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN or amount <= 0:\n                raise InvalidBitcoinURI(f\"amount is out-of-bounds: {amount!r} BTC\")\n            out['amount'] = int(amount)\n        except Exception as e:\n            raise InvalidBitcoinURI(f\"failed to parse 'amount' field: {repr(e)}\") from e\n    if 'message' in out:\n        out['message'] = out['message']\n        out['memo'] = out['message']\n    if 'time' in out:\n        try:\n            out['time'] = int(out['time'])\n        except Exception as e:\n            raise InvalidBitcoinURI(f\"failed to parse 'time' field: {repr(e)}\") from e\n    if 'exp' in out:\n        try:\n            out['exp'] = int(out['exp'])\n        except Exception as e:\n            raise InvalidBitcoinURI(f\"failed to parse 'exp' field: {repr(e)}\") from e\n    if 'sig' in out:\n        try:\n            out['sig'] = bitcoin.base_decode(out['sig'], base=58).hex()\n        except Exception as e:\n            raise InvalidBitcoinURI(f\"failed to parse 'sig' field: {repr(e)}\") from e\n    if 'lightning' in out:\n        try:\n            lnaddr = lndecode(out['lightning'])\n        except LnDecodeException as e:\n            raise InvalidBitcoinURI(f\"Failed to decode 'lightning' field: {e!r}\") from e\n        amount_sat = out.get('amount')\n        if amount_sat:\n            # allow small leeway due to msat precision\n            if lnaddr.get_amount_sat() is None or abs(amount_sat - int(lnaddr.get_amount_sat())) > 1:\n                raise InvalidBitcoinURI(\"Inconsistent lightning field in bip21: amount\")\n        address = out.get('address')\n        ln_fallback_addr = lnaddr.get_fallback_address()\n        if address and ln_fallback_addr:\n            if ln_fallback_addr != address:\n                raise InvalidBitcoinURI(\"Inconsistent lightning field in bip21: address\")\n\n    return out\n\n\ndef create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str],\n                     *, extra_query_params: Optional[dict] = None) -> str:\n    if not bitcoin.is_address(addr):\n        return \"\"\n    if extra_query_params is None:\n        extra_query_params = {}\n    query = []\n    if amount_sat:\n        query.append('amount=%s' % format_satoshis_plain(amount_sat))\n    if message:\n        query.append('message=%s' % urllib.parse.quote(message))\n    for k, v in extra_query_params.items():\n        if not isinstance(k, str) or k != urllib.parse.quote(k):\n            raise Exception(f\"illegal key for URI: {repr(k)}\")\n        v = urllib.parse.quote(v)\n        query.append(f\"{k}={v}\")\n    p = urllib.parse.ParseResult(\n        scheme=BITCOIN_BIP21_URI_SCHEME,\n        netloc='',\n        path=addr,\n        params='',\n        query='&'.join(query),\n        fragment=''\n    )\n    return str(urllib.parse.urlunparse(p))\n"
  },
  {
    "path": "electrum/bip32.py",
    "content": "# Copyright (C) 2018 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nimport binascii\nimport hashlib\nimport struct\nfrom typing import List, Tuple, NamedTuple, Union, Iterable, Sequence, Optional\n\nimport electrum_ecc as ecc\n\nfrom .util import bfh, BitcoinException\nfrom . import constants\nfrom .crypto import hash_160, hmac_oneshot\nfrom .bitcoin import EncodeBase58Check, DecodeBase58Check\nfrom .logging import get_logger\n\n\n_logger = get_logger(__name__)\nBIP32_PRIME = 0x80000000\nUINT32_MAX = (1 << 32) - 1\n\nBIP32_HARDENED_CHAR = \"h\"  # default \"hardened\" char we put in str paths\n\n\ndef protect_against_invalid_ecpoint(func):\n    def func_wrapper(*args):\n        child_index = args[-1]\n        while True:\n            is_prime = child_index & BIP32_PRIME\n            try:\n                return func(*args[:-1], child_index=child_index)\n            except ecc.InvalidECPointException:\n                _logger.warning('bip32 protect_against_invalid_ecpoint: skipping index')\n                child_index += 1\n                is_prime2 = child_index & BIP32_PRIME\n                if is_prime != is_prime2: raise OverflowError()\n    return func_wrapper\n\n\n@protect_against_invalid_ecpoint\ndef CKD_priv(parent_privkey: bytes, parent_chaincode: bytes, child_index: int) -> Tuple[bytes, bytes]:\n    \"\"\"Child private key derivation function (from master private key)\n    If n is hardened (i.e. the 32nd bit is set), the resulting private key's\n    corresponding public key can NOT be determined without the master private key.\n    However, if n is not hardened, the resulting private key's corresponding\n    public key can be determined without the master private key.\n    \"\"\"\n    if child_index < 0: raise ValueError('the bip32 index needs to be non-negative')\n    is_hardened_child = bool(child_index & BIP32_PRIME)\n    return _CKD_priv(parent_privkey=parent_privkey,\n                     parent_chaincode=parent_chaincode,\n                     child_index=int.to_bytes(child_index, length=4, byteorder=\"big\", signed=False),\n                     is_hardened_child=is_hardened_child)\n\n\ndef _CKD_priv(parent_privkey: bytes, parent_chaincode: bytes,\n              child_index: bytes, is_hardened_child: bool) -> Tuple[bytes, bytes]:\n    try:\n        keypair = ecc.ECPrivkey(parent_privkey)\n    except ecc.InvalidECPointException as e:\n        raise BitcoinException('Impossible xprv (not within curve order)') from e\n    parent_pubkey = keypair.get_public_key_bytes(compressed=True)\n    if is_hardened_child:\n        data = bytes([0]) + parent_privkey + child_index\n    else:\n        data = parent_pubkey + child_index\n    I = hmac_oneshot(parent_chaincode, data, hashlib.sha512)\n    I_left = ecc.string_to_number(I[0:32])\n    child_privkey = (I_left + ecc.string_to_number(parent_privkey)) % ecc.CURVE_ORDER\n    if I_left >= ecc.CURVE_ORDER or child_privkey == 0:\n        raise ecc.InvalidECPointException()\n    child_privkey = int.to_bytes(child_privkey, length=32, byteorder='big', signed=False)\n    child_chaincode = I[32:]\n    return child_privkey, child_chaincode\n\n\n\n@protect_against_invalid_ecpoint\ndef CKD_pub(parent_pubkey: bytes, parent_chaincode: bytes, child_index: int) -> Tuple[bytes, bytes]:\n    \"\"\"Child public key derivation function (from public key only)\n    This function allows us to find the nth public key, as long as n is\n    not hardened. If n is hardened, we need the master private key to find it.\n    \"\"\"\n    if child_index < 0: raise ValueError('the bip32 index needs to be non-negative')\n    if child_index & BIP32_PRIME: raise Exception('not possible to derive hardened child from parent pubkey')\n    return _CKD_pub(parent_pubkey=parent_pubkey,\n                    parent_chaincode=parent_chaincode,\n                    child_index=int.to_bytes(child_index, length=4, byteorder=\"big\", signed=False))\n\n\n# helper function, callable with arbitrary 'child_index' byte-string.\n# i.e.: 'child_index' does not need to fit into 32 bits here! (c.f. trustedcoin billing)\ndef _CKD_pub(parent_pubkey: bytes, parent_chaincode: bytes, child_index: bytes) -> Tuple[bytes, bytes]:\n    I = hmac_oneshot(parent_chaincode, parent_pubkey + child_index, hashlib.sha512)\n    pubkey = ecc.ECPrivkey(I[0:32]) + ecc.ECPubkey(parent_pubkey)\n    if pubkey.is_at_infinity():\n        raise ecc.InvalidECPointException()\n    child_pubkey = pubkey.get_public_key_bytes(compressed=True)\n    child_chaincode = I[32:]\n    return child_pubkey, child_chaincode\n\n\ndef xprv_header(xtype: str, *, net=None) -> bytes:\n    if net is None:\n        net = constants.net\n    return net.XPRV_HEADERS[xtype].to_bytes(length=4, byteorder=\"big\")\n\n\ndef xpub_header(xtype: str, *, net=None) -> bytes:\n    if net is None:\n        net = constants.net\n    return net.XPUB_HEADERS[xtype].to_bytes(length=4, byteorder=\"big\")\n\n\nclass InvalidMasterKeyVersionBytes(BitcoinException): pass\n\n\nclass BIP32Node(NamedTuple):\n    xtype: str\n    eckey: Union[ecc.ECPubkey, ecc.ECPrivkey]\n    chaincode: bytes\n    depth: int = 0\n    fingerprint: bytes = b'\\x00'*4  # as in serialized format, this is the *parent's* fingerprint\n    child_number: bytes = b'\\x00'*4\n\n    @classmethod\n    def from_xkey(\n        cls,\n        xkey: str,\n        *,\n        net=None,\n        allow_custom_headers: bool = True,  # to also accept ypub/zpub\n    ) -> 'BIP32Node':\n        if net is None:\n            net = constants.net\n        xkey = DecodeBase58Check(xkey)\n        if len(xkey) != 78:\n            raise BitcoinException('Invalid length for extended key: {}'\n                                   .format(len(xkey)))\n        depth = xkey[4]\n        fingerprint = xkey[5:9]\n        child_number = xkey[9:13]\n        chaincode = xkey[13:13 + 32]\n        header = int.from_bytes(xkey[0:4], byteorder='big')\n        if header in net.XPRV_HEADERS_INV:\n            headers_inv = net.XPRV_HEADERS_INV\n            is_private = True\n        elif header in net.XPUB_HEADERS_INV:\n            headers_inv = net.XPUB_HEADERS_INV\n            is_private = False\n        else:\n            raise InvalidMasterKeyVersionBytes(f'Invalid extended key format: {hex(header)}')\n        xtype = headers_inv[header]\n        if not allow_custom_headers and xtype != \"standard\":\n            raise ValueError(f\"only standard xpub/xprv allowed. found custom xtype={xtype}\")\n        if is_private:\n            eckey = ecc.ECPrivkey(xkey[13 + 33:])\n        else:\n            eckey = ecc.ECPubkey(xkey[13 + 32:])\n        return BIP32Node(xtype=xtype,\n                         eckey=eckey,\n                         chaincode=chaincode,\n                         depth=depth,\n                         fingerprint=fingerprint,\n                         child_number=child_number)\n\n    @classmethod\n    def from_rootseed(cls, seed: bytes, *, xtype: str) -> 'BIP32Node':\n        I = hmac_oneshot(b\"Bitcoin seed\", seed, hashlib.sha512)\n        master_k = I[0:32]\n        master_c = I[32:]\n        return BIP32Node(xtype=xtype,\n                         eckey=ecc.ECPrivkey(master_k),\n                         chaincode=master_c)\n\n    @classmethod\n    def from_bytes(cls, b: bytes) -> 'BIP32Node':\n        if len(b) != 78:\n            raise Exception(f\"unexpected xkey raw bytes len {len(b)} != 78\")\n        xkey = EncodeBase58Check(b)\n        return cls.from_xkey(xkey)\n\n    def to_xprv(self, *, net=None) -> str:\n        payload = self.to_xprv_bytes(net=net)\n        return EncodeBase58Check(payload)\n\n    def to_xprv_bytes(self, *, net=None) -> bytes:\n        if not self.is_private():\n            raise Exception(\"cannot serialize as xprv; private key missing\")\n        payload = (xprv_header(self.xtype, net=net) +\n                   bytes([self.depth]) +\n                   self.fingerprint +\n                   self.child_number +\n                   self.chaincode +\n                   bytes([0]) +\n                   self.eckey.get_secret_bytes())\n        assert len(payload) == 78, f\"unexpected xprv payload len {len(payload)}\"\n        return payload\n\n    def to_xpub(self, *, net=None) -> str:\n        payload = self.to_xpub_bytes(net=net)\n        return EncodeBase58Check(payload)\n\n    def to_xpub_bytes(self, *, net=None) -> bytes:\n        payload = (xpub_header(self.xtype, net=net) +\n                   bytes([self.depth]) +\n                   self.fingerprint +\n                   self.child_number +\n                   self.chaincode +\n                   self.eckey.get_public_key_bytes(compressed=True))\n        assert len(payload) == 78, f\"unexpected xpub payload len {len(payload)}\"\n        return payload\n\n    def to_xkey(self, *, net=None) -> str:\n        if self.is_private():\n            return self.to_xprv(net=net)\n        else:\n            return self.to_xpub(net=net)\n\n    def to_bytes(self, *, net=None) -> bytes:\n        if self.is_private():\n            return self.to_xprv_bytes(net=net)\n        else:\n            return self.to_xpub_bytes(net=net)\n\n    def convert_to_public(self) -> 'BIP32Node':\n        if not self.is_private():\n            return self\n        pubkey = ecc.ECPubkey(self.eckey.get_public_key_bytes())\n        return self._replace(eckey=pubkey)\n\n    def is_private(self) -> bool:\n        return isinstance(self.eckey, ecc.ECPrivkey)\n\n    def subkey_at_private_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP32Node':\n        if path is None:\n            raise Exception(\"derivation path must not be None\")\n        if isinstance(path, str):\n            path = convert_bip32_strpath_to_intpath(path)\n        if not self.is_private():\n            raise Exception(\"cannot do bip32 private derivation; private key missing\")\n        if not path:\n            return self\n        depth = self.depth\n        chaincode = self.chaincode\n        privkey = self.eckey.get_secret_bytes()\n        for child_index in path:\n            parent_privkey = privkey\n            privkey, chaincode = CKD_priv(privkey, chaincode, child_index)\n            depth += 1\n        parent_pubkey = ecc.ECPrivkey(parent_privkey).get_public_key_bytes(compressed=True)\n        fingerprint = hash_160(parent_pubkey)[0:4]\n        child_number = child_index.to_bytes(length=4, byteorder=\"big\")\n        return BIP32Node(xtype=self.xtype,\n                         eckey=ecc.ECPrivkey(privkey),\n                         chaincode=chaincode,\n                         depth=depth,\n                         fingerprint=fingerprint,\n                         child_number=child_number)\n\n    def subkey_at_public_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP32Node':\n        if path is None:\n            raise Exception(\"derivation path must not be None\")\n        if isinstance(path, str):\n            path = convert_bip32_strpath_to_intpath(path)\n        if not path:\n            return self.convert_to_public()\n        depth = self.depth\n        chaincode = self.chaincode\n        pubkey = self.eckey.get_public_key_bytes(compressed=True)\n        for child_index in path:\n            parent_pubkey = pubkey\n            pubkey, chaincode = CKD_pub(pubkey, chaincode, child_index)\n            depth += 1\n        fingerprint = hash_160(parent_pubkey)[0:4]\n        child_number = child_index.to_bytes(length=4, byteorder=\"big\")\n        return BIP32Node(xtype=self.xtype,\n                         eckey=ecc.ECPubkey(pubkey),\n                         chaincode=chaincode,\n                         depth=depth,\n                         fingerprint=fingerprint,\n                         child_number=child_number)\n\n    def calc_fingerprint_of_this_node(self) -> bytes:\n        \"\"\"Returns the fingerprint of this node.\n        Note that self.fingerprint is of the *parent*.\n        \"\"\"\n        # TODO cache this\n        return hash_160(self.eckey.get_public_key_bytes(compressed=True))[0:4]\n\n\ndef xpub_type(x: str) -> str:\n    assert x is not None\n    return BIP32Node.from_xkey(x).xtype\n\n\ndef is_xpub(text: str) -> bool:\n    try:\n        node = BIP32Node.from_xkey(text)\n        return not node.is_private()\n    except Exception:\n        return False\n\n\ndef is_xprv(text: str) -> bool:\n    try:\n        node = BIP32Node.from_xkey(text)\n        return node.is_private()\n    except Exception:\n        return False\n\n\ndef xpub_from_xprv(xprv: str) -> str:\n    return BIP32Node.from_xkey(xprv).to_xpub()\n\n\ndef convert_bip32_strpath_to_intpath(n: str) -> List[int]:\n    \"\"\"Convert bip32 path str to list of uint32 integers with prime flags\n    m/0/-1/1' -> [0, 0x80000001, 0x80000001]\n\n    based on code in trezorlib\n    \"\"\"\n    if not n:\n        return []\n    if n.endswith(\"/\"):\n        n = n[:-1]\n    n = n.split('/')\n    # cut leading \"m\" if present, but do not require it\n    if n[0] == \"m\":\n        n = n[1:]\n    path = []\n    for x in n:\n        if x == '':\n            # gracefully allow repeating \"/\" chars in path.\n            # makes concatenating paths easier\n            continue\n        prime = 0\n        if x.endswith(\"'\") or x.endswith(\"h\"):  # note: some implementations also accept \"H\", \"p\", \"P\"\n            x = x[:-1]\n            prime = BIP32_PRIME\n        if x.startswith('-'):\n            if prime:\n                raise ValueError(f\"bip32 path child index is signalling hardened level in multiple ways\")\n            prime = BIP32_PRIME\n        try:\n            x_int = int(x)\n        except ValueError as e:\n            raise ValueError(f\"failed to parse bip32 path: {(str(e))}\") from None\n        child_index = abs(x_int) | prime\n        if child_index > UINT32_MAX:\n            raise ValueError(f\"bip32 path child index too large: {child_index} > {UINT32_MAX}\")\n        path.append(child_index)\n    return path\n\n\ndef convert_bip32_intpath_to_strpath(path: Sequence[int], *, hardened_char=BIP32_HARDENED_CHAR) -> str:\n    assert isinstance(hardened_char, str), hardened_char\n    assert len(hardened_char) == 1, hardened_char\n    s = \"m/\"\n    for child_index in path:\n        if not isinstance(child_index, int):\n            raise TypeError(f\"bip32 path child index must be int: {child_index}\")\n        if not (0 <= child_index <= UINT32_MAX):\n            raise ValueError(f\"bip32 path child index out of range: {child_index}\")\n        prime = \"\"\n        if child_index & BIP32_PRIME:\n            prime = hardened_char\n            child_index = child_index ^ BIP32_PRIME\n        s += str(child_index) + prime + '/'\n    # cut trailing \"/\"\n    s = s[:-1]\n    return s\n\n\ndef is_bip32_derivation(s: str) -> bool:\n    try:\n        if not (s == 'm' or s.startswith('m/')):\n            return False\n        convert_bip32_strpath_to_intpath(s)\n    except Exception:\n        return False\n    else:\n        return True\n\n\ndef normalize_bip32_derivation(s: Optional[str], *, hardened_char=BIP32_HARDENED_CHAR) -> Optional[str]:\n    if s is None:\n        return None\n    if not is_bip32_derivation(s):\n        raise ValueError(f\"invalid bip32 derivation: {s}\")\n    ints = convert_bip32_strpath_to_intpath(s)\n    return convert_bip32_intpath_to_strpath(ints, hardened_char=hardened_char)\n\n\ndef is_all_public_derivation(path: Union[str, Iterable[int]]) -> bool:\n    \"\"\"Returns whether all levels in path use non-hardened derivation.\"\"\"\n    if isinstance(path, str):\n        path = convert_bip32_strpath_to_intpath(path)\n    for child_index in path:\n        if child_index < 0:\n            raise ValueError('the bip32 index needs to be non-negative')\n        if child_index & BIP32_PRIME:\n            return False\n    return True\n\n\ndef root_fp_and_der_prefix_from_xkey(xkey: str) -> Tuple[Optional[str], Optional[str]]:\n    \"\"\"Returns the root bip32 fingerprint and the derivation path from the\n    root to the given xkey, if they can be determined. Otherwise (None, None).\n    \"\"\"\n    node = BIP32Node.from_xkey(xkey)\n    derivation_prefix = None\n    root_fingerprint = None\n    assert node.depth >= 0, node.depth\n    if node.depth == 0:\n        derivation_prefix = 'm'\n        root_fingerprint = node.calc_fingerprint_of_this_node().hex().lower()\n    elif node.depth == 1:\n        child_number_int = int.from_bytes(node.child_number, 'big')\n        derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int])\n        root_fingerprint = node.fingerprint.hex()\n    return root_fingerprint, derivation_prefix\n\n\ndef is_xkey_consistent_with_key_origin_info(xkey: str, *,\n                                            derivation_prefix: str = None,\n                                            root_fingerprint: str = None) -> bool:\n    bip32node = BIP32Node.from_xkey(xkey)\n    int_path = None\n    if derivation_prefix is not None:\n        int_path = convert_bip32_strpath_to_intpath(derivation_prefix)\n    if int_path is not None and len(int_path) != bip32node.depth:\n        return False\n    if bip32node.depth == 0:\n        if bfh(root_fingerprint) != bip32node.calc_fingerprint_of_this_node():\n            return False\n        if bip32node.child_number != bytes(4):\n            return False\n    if int_path is not None and bip32node.depth > 0:\n        if int.from_bytes(bip32node.child_number, 'big') != int_path[-1]:\n            return False\n    if bip32node.depth == 1:\n        if bfh(root_fingerprint) != bip32node.fingerprint:\n            return False\n    return True\n\n\nclass KeyOriginInfo:\n    \"\"\"\n    Object representing the origin of a key.\n\n    from https://github.com/bitcoin-core/HWI/blob/5f300d3dee7b317a6194680ad293eaa0962a3cc7/hwilib/key.py\n    # Copyright (c) 2020 The HWI developers\n    # Distributed under the MIT software license.\n    \"\"\"\n    def __init__(self, fingerprint: bytes, path: Sequence[int]) -> None:\n        \"\"\"\n        :param fingerprint: The 4 byte BIP 32 fingerprint of a parent key from which this key is derived from\n        :param path: The derivation path to reach this key from the key at ``fingerprint``\n        \"\"\"\n        self.fingerprint: bytes = fingerprint\n        self.path: Sequence[int] = path\n\n    @classmethod\n    def deserialize(cls, s: bytes) -> 'KeyOriginInfo':\n        \"\"\"\n        Deserialize a serialized KeyOriginInfo.\n        They will be serialized in the same way that PSBTs serialize derivation paths\n        \"\"\"\n        fingerprint = s[0:4]\n        s = s[4:]\n        path = list(struct.unpack(\"<\" + \"I\" * (len(s) // 4), s))\n        return cls(fingerprint, path)\n\n    def serialize(self) -> bytes:\n        \"\"\"\n        Serializes the KeyOriginInfo in the same way that derivation paths are stored in PSBTs\n        \"\"\"\n        r = self.fingerprint\n        r += struct.pack(\"<\" + \"I\" * len(self.path), *self.path)\n        return r\n\n    def _path_string(self) -> str:\n        strpath = self.get_derivation_path()\n        if len(strpath) >= 2:\n            assert strpath.startswith(\"m/\")\n        return strpath[1:]  # cut leading \"m\"\n\n    def to_string(self) -> str:\n        \"\"\"\n        Return the KeyOriginInfo as a string in the form <fingerprint>/<index>/<index>/...\n        This is the same way that KeyOriginInfo is shown in descriptors\n        \"\"\"\n        s = binascii.hexlify(self.fingerprint).decode()\n        s += self._path_string()\n        return s\n\n    @classmethod\n    def from_string(cls, s: str) -> 'KeyOriginInfo':\n        \"\"\"\n        Create a KeyOriginInfo from the string\n        :param s: The string to parse\n        \"\"\"\n        s = s.lower()\n        entries = s.split(\"/\")\n        fingerprint = binascii.unhexlify(s[0:8])\n        path: Sequence[int] = []\n        if len(entries) > 1:\n            path = convert_bip32_strpath_to_intpath(s[9:])\n        return cls(fingerprint, path)\n\n    def get_derivation_path(self) -> str:\n        \"\"\"\n        Return the string for just the path\n        \"\"\"\n        return convert_bip32_intpath_to_strpath(self.path)\n\n    def get_full_int_list(self) -> List[int]:\n        \"\"\"\n        Return a list of ints representing this KeyOriginInfo.\n        The first int is the fingerprint, followed by the path\n        \"\"\"\n        xfp = [struct.unpack(\"<I\", self.fingerprint)[0]]\n        xfp.extend(self.path)\n        return xfp\n\n    def __eq__(self, other) -> bool:\n        if not isinstance(other, KeyOriginInfo):\n            return False\n        return self.serialize() == other.serialize()\n\n    def __repr__(self) -> str:\n        return f\"<KeyOriginInfo {self.to_string()}>\"\n"
  },
  {
    "path": "electrum/bip39_recovery.py",
    "content": "# Copyright (C) 2020 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nfrom typing import TYPE_CHECKING, Optional\nimport itertools\n\nfrom . import bitcoin\nfrom .constants import BIP39_WALLET_FORMATS\nfrom .bip32 import BIP32_PRIME, BIP32Node\nfrom .bip32 import convert_bip32_strpath_to_intpath as bip32_str_to_ints\nfrom .bip32 import convert_bip32_intpath_to_strpath as bip32_ints_to_str\nfrom .util import OldTaskGroup, NetworkOfflineException\n\nif TYPE_CHECKING:\n    from .network import Network\n\n\nasync def account_discovery(network: Optional['Network'], get_account_xpub):\n    if network is None:\n        raise NetworkOfflineException()\n    async with OldTaskGroup() as group:\n        account_scan_tasks = []\n        for wallet_format in BIP39_WALLET_FORMATS:\n            account_scan = scan_for_active_accounts(network, get_account_xpub, wallet_format)\n            account_scan_tasks.append(await group.spawn(account_scan))\n    active_accounts = []\n    for task in account_scan_tasks:\n        active_accounts.extend(task.result())\n    return active_accounts\n\n\nasync def scan_for_active_accounts(network: 'Network', get_account_xpub, wallet_format):\n    active_accounts = []\n    account_path = bip32_str_to_ints(wallet_format[\"derivation_path\"])\n    while True:\n        account_xpub = get_account_xpub(account_path)\n        account_node = BIP32Node.from_xkey(account_xpub)\n        has_history = await account_has_history(network, account_node, wallet_format[\"script_type\"])\n        if has_history:\n            account = format_account(wallet_format, account_path)\n            active_accounts.append(account)\n        if not has_history or not wallet_format[\"iterate_accounts\"]:\n            break\n        account_path[-1] = account_path[-1] + 1\n    return active_accounts\n\n\nasync def account_has_history(network: 'Network', account_node: BIP32Node, script_type: str) -> bool:\n    # note: scan both receiving and change addresses. some wallets send change across accounts.\n    path_suffixes = itertools.chain(\n        itertools.product((0,), range(20)),  # ad-hoc gap limits\n        itertools.product((1,), range(10)),\n    )\n    async with OldTaskGroup() as group:\n        get_history_tasks = []\n        for path_suffix in path_suffixes:\n            address_node = account_node.subkey_at_public_derivation(path_suffix)\n            pubkey = address_node.eckey.get_public_key_hex()\n            address = bitcoin.pubkey_to_address(script_type, pubkey)\n            script = bitcoin.address_to_script(address)\n            scripthash = bitcoin.script_to_scripthash(script)\n            get_history = network.get_history_for_scripthash(scripthash)\n            get_history_tasks.append(await group.spawn(get_history))\n    for task in get_history_tasks:\n        history = task.result()\n        if len(history) > 0:\n            return True\n    return False\n\n\ndef format_account(wallet_format, account_path):\n    description = wallet_format[\"description\"]\n    if wallet_format[\"iterate_accounts\"]:\n        account_index = account_path[-1] % BIP32_PRIME\n        description = f'{description} (Account {account_index})'\n    return {\n        \"description\": description,\n        \"derivation_path\": bip32_ints_to_str(account_path),\n        \"script_type\": wallet_format[\"script_type\"],\n    }\n"
  },
  {
    "path": "electrum/bip39_wallet_formats.json",
    "content": "[\n    {\n        \"description\": \"Standard BIP44 legacy\",\n        \"derivation_path\": \"m/44'/0'/0'\",\n        \"script_type\": \"p2pkh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Standard BIP49 compatibility segwit\",\n        \"derivation_path\": \"m/49'/0'/0'\",\n        \"script_type\": \"p2wpkh-p2sh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Standard BIP84 native segwit\",\n        \"derivation_path\": \"m/84'/0'/0'\",\n        \"script_type\": \"p2wpkh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Non-standard legacy\",\n        \"derivation_path\": \"m/0'\",\n        \"script_type\": \"p2pkh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Non-standard compatibility segwit\",\n        \"derivation_path\": \"m/0'\",\n        \"script_type\": \"p2wpkh-p2sh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Non-standard native segwit\",\n        \"derivation_path\": \"m/0'\",\n        \"script_type\": \"p2wpkh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Non-standard legacy on BIP84 path\",\n        \"derivation_path\": \"m/84'/0'/0'\",\n        \"script_type\": \"p2pkh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Non-standard compatibility segwit on BIP84 path\",\n        \"derivation_path\": \"m/84'/0'/0'\",\n        \"script_type\": \"p2wpkh-p2sh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Non-standard legacy on BIP49 path\",\n        \"derivation_path\": \"m/49'/0'/0'\",\n        \"script_type\": \"p2pkh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Non-standard native segwit on BIP49 path\",\n        \"derivation_path\": \"m/49'/0'/0'\",\n        \"script_type\": \"p2wpkh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Copay native segwit\",\n        \"derivation_path\": \"m/44'/0'/0'\",\n        \"script_type\": \"p2wpkh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Coolwallet S derivation path using bip44 but with segwit script format\",\n        \"derivation_path\": \"m/44'/0'/0'\",\n        \"script_type\": \"p2wpkh-p2sh\",\n        \"iterate_accounts\": true\n    },\n    {\n        \"description\": \"Samourai Bad Bank (toxic change)\",\n        \"derivation_path\": \"m/84'/0'/2147483644'\",\n        \"script_type\": \"p2wpkh\",\n        \"iterate_accounts\": false\n    },\n    {\n        \"description\": \"Samourai Whirlpool Pre Mix\",\n        \"derivation_path\": \"m/84'/0'/2147483645'\",\n        \"script_type\": \"p2wpkh\",\n        \"iterate_accounts\": false\n    },\n    {\n        \"description\": \"Samourai Whirlpool Post Mix\",\n        \"derivation_path\": \"m/84'/0'/2147483646'\",\n        \"script_type\": \"p2wpkh\",\n        \"iterate_accounts\": false\n    },\n    {\n        \"description\": \"Samourai Ricochet legacy\",\n        \"derivation_path\": \"m/44'/0'/2147483647'\",\n        \"script_type\": \"p2pkh\",\n        \"iterate_accounts\": false\n    },\n    {\n        \"description\": \"Samourai Ricochet compatibility segwit\",\n        \"derivation_path\": \"m/49'/0'/2147483647'\",\n        \"script_type\": \"p2wpkh-p2sh\",\n        \"iterate_accounts\": false\n    },\n    {\n        \"description\": \"Samourai Ricochet native segwit\",\n        \"derivation_path\": \"m/84'/0'/2147483647'\",\n        \"script_type\": \"p2wpkh\",\n        \"iterate_accounts\": false\n    }\n]\n"
  },
  {
    "path": "electrum/bitcoin.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2011 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import Tuple, TYPE_CHECKING, Optional, Union, Sequence, Mapping, Any\nimport enum\nfrom enum import IntEnum, Enum\n\nimport electrum_ecc as ecc\nfrom electrum_ecc.util import bip340_tagged_hash\n\nfrom .util import bfh, BitcoinException, assert_bytes, to_bytes, inv_dict, is_hex_str, classproperty\nfrom . import segwit_addr\nfrom . import constants\nfrom .crypto import sha256d, sha256, hash_160\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .transaction import OPPushDataGeneric\n\n\n################################## transactions\n\nCOINBASE_MATURITY = 100\nCOIN = 100000000\nTOTAL_COIN_SUPPLY_LIMIT_IN_BTC = 21000000\n\nNLOCKTIME_MIN = 0\nNLOCKTIME_BLOCKHEIGHT_MAX = 500_000_000 - 1\nNLOCKTIME_MAX = 2 ** 32 - 1\n\n# supported types of transaction outputs\n# TODO kill these with fire\nTYPE_ADDRESS = 0\nTYPE_PUBKEY  = 1\nTYPE_SCRIPT  = 2\n\n\nclass opcodes(IntEnum):\n    # push value\n    OP_0 = 0x00\n    OP_FALSE = OP_0\n    OP_PUSHDATA1 = 0x4c\n    OP_PUSHDATA2 = 0x4d\n    OP_PUSHDATA4 = 0x4e\n    OP_1NEGATE = 0x4f\n    OP_RESERVED = 0x50\n    OP_1 = 0x51\n    OP_TRUE = OP_1\n    OP_2 = 0x52\n    OP_3 = 0x53\n    OP_4 = 0x54\n    OP_5 = 0x55\n    OP_6 = 0x56\n    OP_7 = 0x57\n    OP_8 = 0x58\n    OP_9 = 0x59\n    OP_10 = 0x5a\n    OP_11 = 0x5b\n    OP_12 = 0x5c\n    OP_13 = 0x5d\n    OP_14 = 0x5e\n    OP_15 = 0x5f\n    OP_16 = 0x60\n\n    # control\n    OP_NOP = 0x61\n    OP_VER = 0x62\n    OP_IF = 0x63\n    OP_NOTIF = 0x64\n    OP_VERIF = 0x65\n    OP_VERNOTIF = 0x66\n    OP_ELSE = 0x67\n    OP_ENDIF = 0x68\n    OP_VERIFY = 0x69\n    OP_RETURN = 0x6a\n\n    # stack ops\n    OP_TOALTSTACK = 0x6b\n    OP_FROMALTSTACK = 0x6c\n    OP_2DROP = 0x6d\n    OP_2DUP = 0x6e\n    OP_3DUP = 0x6f\n    OP_2OVER = 0x70\n    OP_2ROT = 0x71\n    OP_2SWAP = 0x72\n    OP_IFDUP = 0x73\n    OP_DEPTH = 0x74\n    OP_DROP = 0x75\n    OP_DUP = 0x76\n    OP_NIP = 0x77\n    OP_OVER = 0x78\n    OP_PICK = 0x79\n    OP_ROLL = 0x7a\n    OP_ROT = 0x7b\n    OP_SWAP = 0x7c\n    OP_TUCK = 0x7d\n\n    # splice ops\n    OP_CAT = 0x7e\n    OP_SUBSTR = 0x7f\n    OP_LEFT = 0x80\n    OP_RIGHT = 0x81\n    OP_SIZE = 0x82\n\n    # bit logic\n    OP_INVERT = 0x83\n    OP_AND = 0x84\n    OP_OR = 0x85\n    OP_XOR = 0x86\n    OP_EQUAL = 0x87\n    OP_EQUALVERIFY = 0x88\n    OP_RESERVED1 = 0x89\n    OP_RESERVED2 = 0x8a\n\n    # numeric\n    OP_1ADD = 0x8b\n    OP_1SUB = 0x8c\n    OP_2MUL = 0x8d\n    OP_2DIV = 0x8e\n    OP_NEGATE = 0x8f\n    OP_ABS = 0x90\n    OP_NOT = 0x91\n    OP_0NOTEQUAL = 0x92\n\n    OP_ADD = 0x93\n    OP_SUB = 0x94\n    OP_MUL = 0x95\n    OP_DIV = 0x96\n    OP_MOD = 0x97\n    OP_LSHIFT = 0x98\n    OP_RSHIFT = 0x99\n\n    OP_BOOLAND = 0x9a\n    OP_BOOLOR = 0x9b\n    OP_NUMEQUAL = 0x9c\n    OP_NUMEQUALVERIFY = 0x9d\n    OP_NUMNOTEQUAL = 0x9e\n    OP_LESSTHAN = 0x9f\n    OP_GREATERTHAN = 0xa0\n    OP_LESSTHANOREQUAL = 0xa1\n    OP_GREATERTHANOREQUAL = 0xa2\n    OP_MIN = 0xa3\n    OP_MAX = 0xa4\n\n    OP_WITHIN = 0xa5\n\n    # crypto\n    OP_RIPEMD160 = 0xa6\n    OP_SHA1 = 0xa7\n    OP_SHA256 = 0xa8\n    OP_HASH160 = 0xa9\n    OP_HASH256 = 0xaa\n    OP_CODESEPARATOR = 0xab\n    OP_CHECKSIG = 0xac\n    OP_CHECKSIGVERIFY = 0xad\n    OP_CHECKMULTISIG = 0xae\n    OP_CHECKMULTISIGVERIFY = 0xaf\n\n    # expansion\n    OP_NOP1 = 0xb0\n    OP_CHECKLOCKTIMEVERIFY = 0xb1\n    OP_NOP2 = OP_CHECKLOCKTIMEVERIFY\n    OP_CHECKSEQUENCEVERIFY = 0xb2\n    OP_NOP3 = OP_CHECKSEQUENCEVERIFY\n    OP_NOP4 = 0xb3\n    OP_NOP5 = 0xb4\n    OP_NOP6 = 0xb5\n    OP_NOP7 = 0xb6\n    OP_NOP8 = 0xb7\n    OP_NOP9 = 0xb8\n    OP_NOP10 = 0xb9\n\n    OP_INVALIDOPCODE = 0xff\n\n    def hex(self) -> str:\n        return bytes([self]).hex()\n\n\ndef script_num_to_bytes(i: int) -> bytes:\n    \"\"\"See CScriptNum in Bitcoin Core.\n    Encodes an integer as bytes, to be used in script.\n\n    ported from https://github.com/bitcoin/bitcoin/blob/8cbc5c4be4be22aca228074f087a374a7ec38be8/src/script/script.h#L326\n    \"\"\"\n    if i == 0:\n        return b\"\"\n\n    result = bytearray()\n    neg = i < 0\n    absvalue = abs(i)\n    while absvalue > 0:\n        result.append(absvalue & 0xff)\n        absvalue >>= 8\n\n    if result[-1] & 0x80:\n        result.append(0x80 if neg else 0x00)\n    elif neg:\n        result[-1] |= 0x80\n\n    return bytes(result)\n\n\ndef var_int(i: int) -> bytes:\n    # https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer\n    # https://github.com/bitcoin/bitcoin/blob/efe1ee0d8d7f82150789f1f6840f139289628a2b/src/serialize.h#L247\n    # \"CompactSize\"\n    assert i >= 0, i\n    if i < 0xfd:\n        return int.to_bytes(i, length=1, byteorder=\"little\", signed=False)\n    elif i <= 0xffff:\n        return b\"\\xfd\" + int.to_bytes(i, length=2, byteorder=\"little\", signed=False)\n    elif i <= 0xffffffff:\n        return b\"\\xfe\" + int.to_bytes(i, length=4, byteorder=\"little\", signed=False)\n    else:\n        return b\"\\xff\" + int.to_bytes(i, length=8, byteorder=\"little\", signed=False)\n\n\ndef witness_push(item: bytes) -> bytes:\n    \"\"\"Returns data in the form it should be present in the witness.\"\"\"\n    return var_int(len(item)) + item\n\n\ndef _op_push(i: int) -> bytes:\n    if i < opcodes.OP_PUSHDATA1:\n        return int.to_bytes(i, length=1, byteorder=\"little\", signed=False)\n    elif i <= 0xff:\n        return bytes([opcodes.OP_PUSHDATA1]) + int.to_bytes(i, length=1, byteorder=\"little\", signed=False)\n    elif i <= 0xffff:\n        return bytes([opcodes.OP_PUSHDATA2]) + int.to_bytes(i, length=2, byteorder=\"little\", signed=False)\n    else:\n        return bytes([opcodes.OP_PUSHDATA4]) + int.to_bytes(i, length=4, byteorder=\"little\", signed=False)\n\n\ndef push_script(data: bytes) -> bytes:\n    \"\"\"Returns pushed data to the script, automatically\n    choosing canonical opcodes depending on the length of the data.\n\n    ported from https://github.com/btcsuite/btcd/blob/fdc2bc867bda6b351191b5872d2da8270df00d13/txscript/scriptbuilder.go#L128\n    \"\"\"\n    data_len = len(data)\n\n    # \"small integer\" opcodes\n    if data_len == 0 or data_len == 1 and data[0] == 0:\n        return bytes([opcodes.OP_0])\n    elif data_len == 1 and data[0] <= 16:\n        return bytes([opcodes.OP_1 - 1 + data[0]])\n    elif data_len == 1 and data[0] == 0x81:\n        return bytes([opcodes.OP_1NEGATE])\n\n    return _op_push(data_len) + data\n\n\ndef make_op_return(x: bytes) -> bytes:\n    return bytes([opcodes.OP_RETURN]) + push_script(x)\n\n\ndef add_number_to_script(i: int) -> bytes:\n    return push_script(script_num_to_bytes(i))\n\n\ndef construct_witness(items: Sequence[Union[str, int, bytes]]) -> bytes:\n    \"\"\"Constructs a witness from the given stack items.\"\"\"\n    witness = bytearray()\n    witness += var_int(len(items))\n    for item in items:\n        if type(item) is int:\n            item = script_num_to_bytes(item)\n        elif isinstance(item, (bytes, bytearray)):\n            pass  # use as-is\n        else:\n            assert is_hex_str(item), repr(item)\n            item = bfh(item)\n        witness += witness_push(item)\n    return bytes(witness)\n\n\ndef construct_script(\n    items: Sequence[Union[str, int, bytes, opcodes, 'OPPushDataGeneric']],\n    *,\n    values: Optional[Mapping[int, Any]] = None,  # can be used to substitute into OPPushDataGeneric\n) -> bytes:\n    \"\"\"Constructs bitcoin script from given items.\"\"\"\n    from .transaction import OPPushDataGeneric\n    script = bytearray()\n    values = values or {}\n    for i, item in enumerate(items):\n        if i in values:\n            assert OPPushDataGeneric.is_instance(item), f\"tried to substitute into {item=!r}\"\n            item = values[i]\n        if isinstance(item, opcodes):\n            script += bytes([item])\n        elif type(item) is int:\n            script += add_number_to_script(item)\n        elif isinstance(item, (bytes, bytearray)):\n            script += push_script(item)\n        elif isinstance(item, str):\n            assert is_hex_str(item)\n            script += push_script(bfh(item))\n        else:\n            raise Exception(f'unexpected item for script: {item!r} at idx={i}')\n    return bytes(script)\n\n\ndef relayfee(network: 'Network' = None) -> int:\n    \"\"\"Returns feerate in sat/kbyte.\"\"\"\n    from .fee_policy import FEERATE_MIN_RELAY, FEERATE_DEFAULT_RELAY, FEERATE_MAX_RELAY\n    if network and network.relay_fee is not None:\n        fee = network.relay_fee\n    else:\n        fee = FEERATE_DEFAULT_RELAY\n    # sanity safeguards, as network.relay_fee is coming from a server:\n    fee = min(fee, FEERATE_MAX_RELAY)\n    fee = max(fee, FEERATE_MIN_RELAY)\n    return fee\n\n\n# see https://github.com/bitcoin/bitcoin/blob/a62f0ed64f8bbbdfe6467ac5ce92ef5b5222d1bd/src/policy/policy.cpp#L14\n# and https://github.com/lightningnetwork/lightning-rfc/blob/7e3dce42cbe4fa4592320db6a4e06c26bb99122b/03-transactions.md#dust-limits\nDUST_LIMIT_P2PKH = 546\nDUST_LIMIT_P2SH = 540\nDUST_LIMIT_UNKNOWN_SEGWIT = 354\nDUST_LIMIT_P2WSH = 330\nDUST_LIMIT_P2WPKH = 294\n\n\ndef dust_threshold(network: 'Network' = None) -> int:\n    \"\"\"Returns the dust limit in satoshis.\"\"\"\n    return DUST_LIMIT_P2PKH\n\n\ndef hash_encode(x: bytes) -> str:\n    return x[::-1].hex()\n\n\ndef hash_decode(x: str) -> bytes:\n    return bfh(x)[::-1]\n\n\n############ functions from pywallet #####################\n\ndef hash160_to_b58_address(h160: bytes, addrtype: int) -> str:\n    s = bytes([addrtype]) + h160\n    s = s + sha256d(s)[0:4]\n    return base_encode(s, base=58)\n\n\ndef b58_address_to_hash160(addr: str) -> Tuple[int, bytes]:\n    addr = to_bytes(addr, 'ascii')\n    _bytes = DecodeBase58Check(addr)\n    if len(_bytes) != 21:\n        raise Exception(f'expected 21 payload bytes in base58 address. got: {len(_bytes)}')\n    return _bytes[0], _bytes[1:21]\n\n\ndef hash160_to_p2pkh(h160: bytes, *, net=None) -> str:\n    if net is None: net = constants.net\n    return hash160_to_b58_address(h160, net.ADDRTYPE_P2PKH)\n\n\ndef hash160_to_p2sh(h160: bytes, *, net=None) -> str:\n    if net is None: net = constants.net\n    return hash160_to_b58_address(h160, net.ADDRTYPE_P2SH)\n\n\ndef public_key_to_p2pkh(public_key: bytes, *, net=None) -> str:\n    return hash160_to_p2pkh(hash_160(public_key), net=net)\n\n\ndef hash_to_segwit_addr(h: bytes, witver: int, *, net=None) -> str:\n    if net is None: net = constants.net\n    addr = segwit_addr.encode_segwit_address(net.SEGWIT_HRP, witver, h)\n    assert addr is not None\n    return addr\n\n\ndef public_key_to_p2wpkh(public_key: bytes, *, net=None) -> str:\n    return hash_to_segwit_addr(hash_160(public_key), witver=0, net=net)\n\n\ndef script_to_p2wsh(script: bytes, *, net=None) -> str:\n    return hash_to_segwit_addr(sha256(script), witver=0, net=net)\n\n\ndef p2wsh_nested_script(witness_script: bytes) -> bytes:\n    wsh = sha256(witness_script)\n    return construct_script([0, wsh])\n\n\ndef pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str:\n    from . import descriptor\n    desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type)\n    return desc.expand().address(net=net)\n\n\n# TODO this method is confusingly named\ndef redeem_script_to_address(txin_type: str, scriptcode: bytes, *, net=None) -> str:\n    assert isinstance(scriptcode, bytes)\n    if txin_type == 'p2sh':\n        # given scriptcode is a redeem_script\n        return hash160_to_p2sh(hash_160(scriptcode), net=net)\n    elif txin_type == 'p2wsh':\n        # given scriptcode is a witness_script\n        return script_to_p2wsh(scriptcode, net=net)\n    elif txin_type == 'p2wsh-p2sh':\n        # given scriptcode is a witness_script\n        redeem_script = p2wsh_nested_script(scriptcode)\n        return hash160_to_p2sh(hash_160(redeem_script), net=net)\n    else:\n        raise NotImplementedError(txin_type)\n\n\ndef script_to_address(script: bytes, *, net=None) -> Optional[str]:\n    from .transaction import get_address_from_output_script\n    return get_address_from_output_script(script, net=net)\n\n\ndef address_to_script(addr: str, *, net=None) -> bytes:\n    if net is None: net = constants.net\n    if not is_address(addr, net=net):\n        raise BitcoinException(f\"invalid bitcoin address: {addr}\")\n    witver, witprog = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, addr)\n    if witprog is not None:\n        if not (0 <= witver <= 16):\n            raise BitcoinException(f'impossible witness version: {witver}')\n        return construct_script([witver, bytes(witprog)])\n    addrtype, hash_160_ = b58_address_to_hash160(addr)\n    if addrtype == net.ADDRTYPE_P2PKH:\n        script = pubkeyhash_to_p2pkh_script(hash_160_)\n    elif addrtype == net.ADDRTYPE_P2SH:\n        script = construct_script([opcodes.OP_HASH160, hash_160_, opcodes.OP_EQUAL])\n    else:\n        raise BitcoinException(f'unknown address type: {addrtype}')\n    return script\n\n\nclass OnchainOutputType(Enum):\n    \"\"\"Opaque types of scriptPubKeys.\n    In case of p2sh, p2wsh and similar, no knowledge of redeem script, etc.\n    \"\"\"\n    P2PKH = enum.auto()\n    P2SH = enum.auto()\n    WITVER0_P2WPKH = enum.auto()\n    WITVER0_P2WSH = enum.auto()\n    WITVER1_P2TR = enum.auto()\n\n\ndef address_to_payload(addr: str, *, net=None) -> Tuple[OnchainOutputType, bytes]:\n    \"\"\"Return (type, pubkey hash / witness program) for an address.\"\"\"\n    if net is None: net = constants.net\n    if not is_address(addr, net=net):\n        raise BitcoinException(f\"invalid bitcoin address: {addr}\")\n    witver, witprog = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, addr)\n    if witprog is not None:\n        if witver == 0:\n            if len(witprog) == 20:\n                return OnchainOutputType.WITVER0_P2WPKH, bytes(witprog)\n            elif len(witprog) == 32:\n                return OnchainOutputType.WITVER0_P2WSH, bytes(witprog)\n            else:\n                raise BitcoinException(f\"unexpected length for segwit witver=0 witprog: len={len(witprog)}\")\n        elif witver == 1:\n            if len(witprog) == 32:\n                return OnchainOutputType.WITVER1_P2TR, bytes(witprog)\n            else:\n                raise BitcoinException(f\"unexpected length for segwit witver=1 witprog: len={len(witprog)}\")\n        else:\n            raise BitcoinException(f\"not implemented handling for witver={witver}\")\n    addrtype, hash_160_ = b58_address_to_hash160(addr)\n    if addrtype == net.ADDRTYPE_P2PKH:\n        return OnchainOutputType.P2PKH, hash_160_\n    elif addrtype == net.ADDRTYPE_P2SH:\n        return OnchainOutputType.P2SH, hash_160_\n    raise BitcoinException(f\"unknown address type: {addrtype}\")\n\n\ndef address_to_scripthash(addr: str, *, net=None) -> str:\n    script = address_to_script(addr, net=net)\n    return script_to_scripthash(script)\n\n\ndef script_to_scripthash(script: bytes) -> str:\n    h = sha256(script)\n    return h[::-1].hex()\n\n\ndef pubkeyhash_to_p2pkh_script(pubkey_hash160: bytes) -> bytes:\n    return construct_script([\n        opcodes.OP_DUP,\n        opcodes.OP_HASH160,\n        pubkey_hash160,\n        opcodes.OP_EQUALVERIFY,\n        opcodes.OP_CHECKSIG\n    ])\n\n\n__b58chars = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'\nassert len(__b58chars) == 58\n__b58chars_inv = inv_dict(dict(enumerate(__b58chars)))\n\n__b43chars = b'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:'\nassert len(__b43chars) == 43\n__b43chars_inv = inv_dict(dict(enumerate(__b43chars)))\n\n\nclass BaseDecodeError(BitcoinException): pass\n\n\ndef base_encode(v: bytes, *, base: int) -> str:\n    \"\"\" encode v, which is a string of bytes, to base58.\"\"\"\n    assert_bytes(v)\n    if base not in (58, 43):\n        raise ValueError('not supported base: {}'.format(base))\n    chars = __b58chars\n    if base == 43:\n        chars = __b43chars\n\n    origlen = len(v)\n    v = v.lstrip(b'\\x00')\n    newlen = len(v)\n\n    num = int.from_bytes(v, byteorder='big')\n    string = b\"\"\n    while num:\n        num, idx = divmod(num, base)\n        string = chars[idx:idx + 1] + string\n\n    result = chars[0:1] * (origlen - newlen) + string\n    return result.decode('ascii')\n\n\ndef base_decode(v: Union[bytes, str], *, base: int) -> Optional[bytes]:\n    \"\"\" decode v into a string of len bytes.\n\n    based on the work of David Keijser in https://github.com/keis/base58\n    \"\"\"\n    # assert_bytes(v)\n    v = to_bytes(v, 'ascii')\n    if base not in (58, 43):\n        raise ValueError('not supported base: {}'.format(base))\n    chars = __b58chars\n    chars_inv = __b58chars_inv\n    if base == 43:\n        chars = __b43chars\n        chars_inv = __b43chars_inv\n\n    origlen = len(v)\n    v = v.lstrip(chars[0:1])\n    newlen = len(v)\n\n    num = 0\n    try:\n        for char in v:\n            num = num * base + chars_inv[char]\n    except KeyError:\n        raise BaseDecodeError('Forbidden character {} for base {}'.format(char, base))\n\n    return num.to_bytes(origlen - newlen + (num.bit_length() + 7) // 8, 'big')\n\n\nclass InvalidChecksum(BaseDecodeError):\n    pass\n\n\ndef EncodeBase58Check(vchIn: bytes) -> str:\n    hash = sha256d(vchIn)\n    return base_encode(vchIn + hash[0:4], base=58)\n\n\ndef DecodeBase58Check(psz: Union[bytes, str]) -> bytes:\n    vchRet = base_decode(psz, base=58)\n    payload = vchRet[0:-4]\n    csum_found = vchRet[-4:]\n    csum_calculated = sha256d(payload)[0:4]\n    if csum_calculated != csum_found:\n        raise InvalidChecksum(f'calculated {csum_calculated.hex()}, found {csum_found.hex()}')\n    else:\n        return payload\n\n\n# backwards compat\n# extended WIF for segwit (used in 3.0.x; but still used internally)\n# the keys in this dict should be a superset of what Imported Wallets can import\nWIF_SCRIPT_TYPES = {\n    'p2pkh': 0,\n    'p2wpkh': 1,\n    'p2wpkh-p2sh': 2,\n    'p2sh': 5,\n    'p2wsh': 6,\n    'p2wsh-p2sh': 7\n}\nWIF_SCRIPT_TYPES_INV = inv_dict(WIF_SCRIPT_TYPES)\n\n\ndef is_segwit_script_type(txin_type: str) -> bool:\n    return txin_type in ('p2wpkh', 'p2wpkh-p2sh', 'p2wsh', 'p2wsh-p2sh')\n\n\ndef serialize_privkey(secret: bytes, compressed: bool, txin_type: str, *,\n                      internal_use: bool = False) -> str:\n    # we only export secrets inside curve range\n    secret = ecc.ECPrivkey.normalize_secret_bytes(secret)\n    if internal_use:\n        prefix = bytes([(WIF_SCRIPT_TYPES[txin_type] + constants.net.WIF_PREFIX) & 255])\n    else:\n        prefix = bytes([constants.net.WIF_PREFIX])\n    suffix = b'\\01' if compressed else b''\n    vchIn = prefix + secret + suffix\n    base58_wif = EncodeBase58Check(vchIn)\n    if internal_use:\n        return base58_wif\n    else:\n        return '{}:{}'.format(txin_type, base58_wif)\n\n\ndef deserialize_privkey(key: str) -> Tuple[str, bytes, bool]:\n    if is_minikey(key):\n        return 'p2pkh', minikey_to_private_key(key), False\n\n    txin_type = None\n    if ':' in key:\n        txin_type, key = key.split(sep=':', maxsplit=1)\n        if txin_type not in WIF_SCRIPT_TYPES:\n            raise BitcoinException('unknown script type: {}'.format(txin_type))\n    try:\n        vch = DecodeBase58Check(key)\n    except Exception as e:\n        neutered_privkey = str(key)[:3] + '..' + str(key)[-2:]\n        raise BaseDecodeError(f\"cannot deserialize privkey {neutered_privkey}\") from e\n\n    if txin_type is None:\n        # keys exported in version 3.0.x encoded script type in first byte\n        prefix_value = vch[0] - constants.net.WIF_PREFIX\n        try:\n            txin_type = WIF_SCRIPT_TYPES_INV[prefix_value]\n        except KeyError as e:\n            raise BitcoinException('invalid prefix ({}) for WIF key (1)'.format(vch[0])) from None\n    else:\n        # all other keys must have a fixed first byte\n        if vch[0] != constants.net.WIF_PREFIX:\n            raise BitcoinException('invalid prefix ({}) for WIF key (2)'.format(vch[0]))\n\n    if len(vch) not in [33, 34]:\n        raise BitcoinException('invalid vch len for WIF key: {}'.format(len(vch)))\n    compressed = False\n    if len(vch) == 34:\n        if vch[33] == 0x01:\n            compressed = True\n        else:\n            raise BitcoinException(f'invalid WIF key. length suggests compressed pubkey, '\n                                   f'but last byte is {vch[33]} != 0x01')\n\n    if is_segwit_script_type(txin_type) and not compressed:\n        raise BitcoinException('only compressed public keys can be used in segwit scripts')\n\n    secret_bytes = vch[1:33]\n    # we accept secrets outside curve range; cast into range here:\n    secret_bytes = ecc.ECPrivkey.normalize_secret_bytes(secret_bytes)\n    return txin_type, secret_bytes, compressed\n\n\ndef is_compressed_privkey(sec: str) -> bool:\n    return deserialize_privkey(sec)[2]\n\n\ndef address_from_private_key(sec: str) -> str:\n    txin_type, privkey, compressed = deserialize_privkey(sec)\n    public_key = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)\n    return pubkey_to_address(txin_type, public_key)\n\n\ndef is_segwit_address(addr: str, *, net=None) -> bool:\n    if net is None: net = constants.net\n    try:\n        witver, witprog = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, addr)\n    except Exception as e:\n        return False\n    return witprog is not None\n\n\ndef is_taproot_address(addr: str, *, net=None) -> bool:\n    if net is None: net = constants.net\n    try:\n        witver, witprog = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, addr)\n    except Exception as e:\n        return False\n    return witver == 1\n\n\ndef is_b58_address(addr: str, *, net=None) -> bool:\n    if net is None: net = constants.net\n    try:\n        # test length, checksum, encoding:\n        addrtype, h = b58_address_to_hash160(addr)\n    except Exception as e:\n        return False\n    if addrtype not in [net.ADDRTYPE_P2PKH, net.ADDRTYPE_P2SH]:\n        return False\n    return True\n\n\ndef is_address(addr: str, *, net=None) -> bool:\n    return is_segwit_address(addr, net=net) \\\n           or is_b58_address(addr, net=net)\n\n\ndef is_private_key(key: str, *, raise_on_error=False) -> bool:\n    try:\n        deserialize_privkey(key)\n        return True\n    except BaseException as e:\n        if raise_on_error:\n            raise\n        return False\n\n\n########### end pywallet functions #######################\n\ndef is_minikey(text: str) -> bool:\n    # Minikeys are typically 22 or 30 characters, but this routine\n    # permits any length of 20 or more provided the minikey is valid.\n    # A valid minikey must begin with an 'S', be in base58, and when\n    # suffixed with '?' have its SHA256 hash begin with a zero byte.\n    # They are widely used in Casascius physical bitcoins.\n    return (len(text) >= 20 and text[0] == 'S'\n            and all(ord(c) in __b58chars for c in text)\n            and sha256(text + '?')[0] == 0x00)\n\n\ndef minikey_to_private_key(text: str) -> bytes:\n    return sha256(text)\n\n\ndef _get_dummy_address(purpose: str) -> str:\n    return redeem_script_to_address('p2wsh', sha256(bytes(purpose, \"utf8\")))\n\n\n_dummy_addr_funcs = set()\n\n\nclass DummyAddress:\n    \"\"\"dummy address for fee estimation of funding tx\n    Use e.g. as: DummyAddress.CHANNEL\n    \"\"\"\n    def purpose(func):\n        _dummy_addr_funcs.add(func)\n        return classproperty(func)\n\n    @purpose\n    def CHANNEL(self) -> str:\n        return _get_dummy_address(\"channel\")\n    @purpose\n    def SWAP(self) -> str:\n        return _get_dummy_address(\"swap\")\n\n    @classmethod\n    def is_dummy_address(cls, addr: str) -> bool:\n        return addr in (f(cls) for f in _dummy_addr_funcs)\n\n\nclass DummyAddressUsedInTxException(Exception): pass\n\n\ndef taproot_tweak_pubkey(pubkey32: bytes, h: bytes) -> Tuple[int, bytes]:\n    assert isinstance(pubkey32, bytes), type(pubkey32)\n    assert isinstance(h, bytes), type(h)\n    assert len(pubkey32) == 32, len(pubkey32)\n    int_from_bytes = lambda x: int.from_bytes(x, byteorder=\"big\", signed=False)\n\n    tweak = int_from_bytes(bip340_tagged_hash(b\"TapTweak\", pubkey32 + h))\n    if tweak >= ecc.CURVE_ORDER:\n        raise ValueError\n    P = ecc.ECPubkey(b\"\\x02\" + pubkey32)\n    Q = P + (ecc.GENERATOR * tweak)\n    return 0 if Q.has_even_y() else 1, Q.get_public_key_bytes(compressed=True)[1:]\n\n\ndef taproot_tweak_seckey(seckey0: bytes, h: bytes) -> bytes:\n    assert isinstance(seckey0, bytes), type(seckey0)\n    assert isinstance(h, bytes), type(h)\n    assert len(seckey0) == 32, len(seckey0)\n    int_from_bytes = lambda x: int.from_bytes(x, byteorder=\"big\", signed=False)\n\n    P = ecc.ECPrivkey(seckey0)\n    seckey = P.secret_scalar if P.has_even_y() else ecc.CURVE_ORDER - P.secret_scalar\n    pubkey32 = P.get_public_key_bytes(compressed=True)[1:]\n    tweak = int_from_bytes(bip340_tagged_hash(b\"TapTweak\", pubkey32 + h))\n    if tweak >= ecc.CURVE_ORDER:\n        raise ValueError\n    return int.to_bytes((seckey + tweak) % ecc.CURVE_ORDER, length=32, byteorder=\"big\", signed=False)\n\n\n# a TapTree is either:\n#  - a (leaf_version, script) tuple (leaf_version is 0xc0 for BIP-0342 scripts)\n#  - a list of two elements, each with the same structure as TapTree itself\nTapTreeLeaf = Tuple[int, bytes]\nTapTree = Union[TapTreeLeaf, Sequence['TapTree']]\n\n\ndef taproot_tree_helper(script_tree: TapTree):\n    if isinstance(script_tree, tuple):\n        leaf_version, script = script_tree\n        h = bip340_tagged_hash(b\"TapLeaf\", bytes([leaf_version]) + witness_push(script))\n        return [((leaf_version, script), bytes())], h\n    left, left_h = taproot_tree_helper(script_tree[0])\n    right, right_h = taproot_tree_helper(script_tree[1])\n    ret = [(l, c + right_h) for l, c in left] + [(l, c + left_h) for l, c in right]\n    if right_h < left_h:\n        left_h, right_h = right_h, left_h\n    return ret, bip340_tagged_hash(b\"TapBranch\", left_h + right_h)\n\n\ndef taproot_output_script(internal_pubkey: bytes, *, script_tree: Optional[TapTree]) -> bytes:\n    \"\"\"Given an internal public key and a tree of scripts, compute the output script.\"\"\"\n    assert isinstance(internal_pubkey, bytes), type(internal_pubkey)\n    assert len(internal_pubkey) == 32, len(internal_pubkey)\n    if script_tree is None:\n        merkle_root = bytes()\n    else:\n        _, merkle_root = taproot_tree_helper(script_tree)\n    _, output_pubkey = taproot_tweak_pubkey(internal_pubkey, merkle_root)\n    return construct_script([1, output_pubkey])\n\n\ndef control_block_for_taproot_script_spend(\n    *, internal_pubkey: bytes, script_tree: TapTree, script_num: int,\n) -> Tuple[bytes, bytes]:\n    \"\"\"Constructs the control block necessary for spending a taproot UTXO using a script.\n    script_num indicates which script to use, which indexes into (flattened) script_tree.\n    \"\"\"\n    assert isinstance(internal_pubkey, bytes), type(internal_pubkey)\n    assert len(internal_pubkey) == 32, len(internal_pubkey)\n    info, merkle_root = taproot_tree_helper(script_tree)\n    (leaf_version, leaf_script), merkle_path = info[script_num]\n    output_pubkey_y_parity, _ = taproot_tweak_pubkey(internal_pubkey, merkle_root)\n    pubkey_data = bytes([output_pubkey_y_parity + leaf_version]) + internal_pubkey\n    control_block = pubkey_data + merkle_path\n    return (leaf_script, control_block)\n\n\n# user message signing\ndef usermessage_magic(message: bytes) -> bytes:\n    length = var_int(len(message))\n    return b\"\\x18Bitcoin Signed Message:\\n\" + length + message\n\n\ndef ecdsa_sign_usermessage(ec_privkey, message: Union[bytes, str], *, is_compressed: bool) -> bytes:\n    message = to_bytes(message, 'utf8')\n    msg32 = sha256d(usermessage_magic(message))\n    return ec_privkey.ecdsa_sign_recoverable(msg32, is_compressed=is_compressed)\n\n\ndef verify_usermessage_with_address(address: str, sig65: bytes, message: bytes, *, net=None) -> bool:\n    from electrum_ecc import ECPubkey\n    assert_bytes(sig65, message)\n    if net is None: net = constants.net\n    h = sha256d(usermessage_magic(message))\n    try:\n        public_key, compressed, txin_type_guess = ECPubkey.from_ecdsa_sig65(sig65, h)\n    except Exception as e:\n        return False\n    # check public key using the address\n    pubkey_hex = public_key.get_public_key_hex(compressed)\n    txin_types = (txin_type_guess,) if txin_type_guess else ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh')\n    for txin_type in txin_types:\n        addr = pubkey_to_address(txin_type, pubkey_hex, net=net)\n        if address == addr:\n            break\n    else:\n        return False\n    # check message\n    # note: `$ bitcoin-cli verifymessage` does NOT enforce the low-S rule for ecdsa sigs\n    return public_key.ecdsa_verify(sig65[1:], h, enforce_low_s=False)\n"
  },
  {
    "path": "electrum/blockchain.py",
    "content": "# Electrum - lightweight Bitcoin client\n# Copyright (C) 2012 thomasv@ecdsa.org\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport os\nimport threading\nimport time\nfrom typing import Optional, Dict, Mapping, Sequence, TYPE_CHECKING\n\nfrom . import util\nfrom .bitcoin import hash_encode\nfrom .crypto import sha256d\nfrom . import constants\nfrom .util import bfh, with_lock\nfrom .logging import get_logger, Logger\n\nif TYPE_CHECKING:\n    from .simple_config import SimpleConfig\n\n_logger = get_logger(__name__)\n\nHEADER_SIZE = 80  # bytes\nCHUNK_SIZE = 2016  # num headers in a difficulty retarget period\n\n# see https://github.com/bitcoin/bitcoin/blob/feedb9c84e72e4fff489810a2bbeec09bcda5763/src/chainparams.cpp#L76\nMAX_TARGET = 0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff  # compact: 0x1d00ffff\n\n\nclass MissingHeader(Exception):\n    pass\n\n\nclass InvalidHeader(Exception):\n    pass\n\n\ndef serialize_header(header_dict: dict) -> bytes:\n    s = (\n        int.to_bytes(header_dict['version'], length=4, byteorder=\"little\", signed=False)\n        + bfh(header_dict['prev_block_hash'])[::-1]\n        + bfh(header_dict['merkle_root'])[::-1]\n        + int.to_bytes(int(header_dict['timestamp']), length=4, byteorder=\"little\", signed=False)\n        + int.to_bytes(int(header_dict['bits']), length=4, byteorder=\"little\", signed=False)\n        + int.to_bytes(int(header_dict['nonce']), length=4, byteorder=\"little\", signed=False))\n    return s\n\n\ndef deserialize_header(s: bytes, height: int) -> dict:\n    if not s:\n        raise InvalidHeader('Invalid header: {}'.format(s))\n    if len(s) != HEADER_SIZE:\n        raise InvalidHeader('Invalid header length: {}'.format(len(s)))\n    h = {}\n    h['version'] = int.from_bytes(s[0:4], byteorder='little')\n    h['prev_block_hash'] = hash_encode(s[4:36])\n    h['merkle_root'] = hash_encode(s[36:68])\n    h['timestamp'] = int.from_bytes(s[68:72], byteorder='little')\n    h['bits'] = int.from_bytes(s[72:76], byteorder='little')\n    h['nonce'] = int.from_bytes(s[76:80], byteorder='little')\n    h['block_height'] = height\n    return h\n\n\ndef hash_header(header: dict) -> str:\n    if header is None:\n        return '0' * 64\n    if header.get('prev_block_hash') is None:\n        header['prev_block_hash'] = '00'*32\n    return hash_raw_header(serialize_header(header))\n\n\ndef hash_raw_header(header: bytes) -> str:\n    assert isinstance(header, bytes)\n    return hash_encode(sha256d(header))\n\n\npow_hash_header = hash_header\n\n\n# key: blockhash hex at forkpoint\n# the chain at some key is the best chain that includes the given hash\nblockchains = {}  # type: Dict[str, Blockchain]\nblockchains_lock = threading.RLock()  # lock order: take this last; so after Blockchain.lock\n\n\ndef read_blockchains(config: 'SimpleConfig'):\n    best_chain = Blockchain(config=config,\n                            forkpoint=0,\n                            parent=None,\n                            forkpoint_hash=constants.net.GENESIS,\n                            prev_hash=None)\n    blockchains[constants.net.GENESIS] = best_chain\n    # consistency checks\n    if best_chain.height() > constants.net.max_checkpoint():\n        header_after_cp = best_chain.read_header(constants.net.max_checkpoint()+1)\n        if not header_after_cp or not best_chain.can_connect(header_after_cp, check_height=False):\n            _logger.info(\"[blockchain] deleting best chain. cannot connect header after last cp to last cp.\")\n            os.unlink(best_chain.path())\n            best_chain.update_size()\n    # forks\n    fdir = os.path.join(util.get_headers_dir(config), 'forks')\n    util.make_dir(fdir)\n    # files are named as: fork2_{forkpoint}_{prev_hash}_{first_hash}\n    l = filter(lambda x: x.startswith('fork2_') and '.' not in x, os.listdir(fdir))\n    l = sorted(l, key=lambda x: int(x.split('_')[1]))  # sort by forkpoint\n\n    def delete_chain(filename, reason):\n        _logger.info(f\"[blockchain] deleting chain {filename}: {reason}\")\n        os.unlink(os.path.join(fdir, filename))\n\n    def instantiate_chain(filename):\n        __, forkpoint, prev_hash, first_hash = filename.split('_')\n        forkpoint = int(forkpoint)\n        prev_hash = (64-len(prev_hash)) * \"0\" + prev_hash  # left-pad with zeroes\n        first_hash = (64-len(first_hash)) * \"0\" + first_hash\n        # forks below the max checkpoint are not allowed\n        if forkpoint <= constants.net.max_checkpoint():\n            delete_chain(filename, \"deleting fork below max checkpoint\")\n            return\n        # find parent (sorting by forkpoint guarantees it's already instantiated)\n        for parent in blockchains.values():\n            if parent.check_hash(forkpoint - 1, prev_hash):\n                break\n        else:\n            delete_chain(filename, \"cannot find parent for chain\")\n            return\n        b = Blockchain(config=config,\n                       forkpoint=forkpoint,\n                       parent=parent,\n                       forkpoint_hash=first_hash,\n                       prev_hash=prev_hash)\n        # consistency checks\n        h = b.read_header(b.forkpoint)\n        if first_hash != hash_header(h):\n            delete_chain(filename, \"incorrect first hash for chain\")\n            return\n        if not b.parent.can_connect(h, check_height=False):\n            delete_chain(filename, \"cannot connect chain to parent\")\n            return\n        chain_id = b.get_id()\n        assert first_hash == chain_id, (first_hash, chain_id)\n        blockchains[chain_id] = b\n\n    for filename in l:\n        instantiate_chain(filename)\n\n\ndef get_best_chain() -> 'Blockchain':\n    return blockchains[constants.net.GENESIS]\n\n\n# block hash -> chain work; up to and including that block\n_CHAINWORK_CACHE = {\n    \"0000000000000000000000000000000000000000000000000000000000000000\": 0,  # virtual block at height -1\n}  # type: Dict[str, int]\n\n\ndef init_headers_file_for_best_chain():\n    b = get_best_chain()\n    filename = b.path()\n    length = HEADER_SIZE * len(constants.net.CHECKPOINTS) * CHUNK_SIZE\n    if not os.path.exists(filename) or os.path.getsize(filename) < length:\n        with open(filename, 'wb') as f:\n            if length > 0:\n                f.seek(length - 1)\n                f.write(b'\\x00')\n        util.ensure_sparse_file(filename)\n    with b.lock:\n        b.update_size()\n\n\nclass Blockchain(Logger):\n    \"\"\"\n    Manages blockchain headers and their verification\n    \"\"\"\n\n    def __init__(self, config: 'SimpleConfig', forkpoint: int, parent: Optional['Blockchain'],\n                 forkpoint_hash: str, prev_hash: Optional[str]):\n        assert isinstance(forkpoint_hash, str) and len(forkpoint_hash) == 64, forkpoint_hash\n        assert (prev_hash is None) or (isinstance(prev_hash, str) and len(prev_hash) == 64), prev_hash\n        # assert (parent is None) == (forkpoint == 0)\n        if 0 < forkpoint <= constants.net.max_checkpoint():\n            raise Exception(f\"cannot fork below max checkpoint. forkpoint: {forkpoint}\")\n        Logger.__init__(self)\n        self.config = config\n        self.forkpoint = forkpoint  # height of first header\n        self.parent = parent\n        self._forkpoint_hash = forkpoint_hash  # blockhash at forkpoint. \"first hash\"\n        self._prev_hash = prev_hash  # blockhash immediately before forkpoint\n        self.lock = threading.RLock()\n        self.update_size()\n\n    @property\n    def checkpoints(self):\n        return constants.net.CHECKPOINTS\n\n    def get_max_child(self) -> Optional[int]:\n        children = self.get_direct_children()\n        return max([x.forkpoint for x in children]) if children else None\n\n    def get_max_forkpoint(self) -> int:\n        \"\"\"Returns the max height where there is a fork\n        related to this chain.\n        \"\"\"\n        mc = self.get_max_child()\n        return mc if mc is not None else self.forkpoint\n\n    def get_direct_children(self) -> Sequence['Blockchain']:\n        with blockchains_lock:\n            return list(filter(lambda y: y.parent==self, blockchains.values()))\n\n    def get_parent_heights(self) -> Mapping['Blockchain', int]:\n        \"\"\"Returns map: (parent chain -> height of last common block)\"\"\"\n        with self.lock, blockchains_lock:\n            result = {self: self.height()}\n            chain = self\n            while True:\n                parent = chain.parent\n                if parent is None: break\n                result[parent] = chain.forkpoint - 1\n                chain = parent\n            return result\n\n    def get_height_of_last_common_block_with_chain(self, other_chain: 'Blockchain') -> int:\n        last_common_block_height = 0\n        our_parents = self.get_parent_heights()\n        their_parents = other_chain.get_parent_heights()\n        for chain in our_parents:\n            if chain in their_parents:\n                h = min(our_parents[chain], their_parents[chain])\n                last_common_block_height = max(last_common_block_height, h)\n        return last_common_block_height\n\n    @with_lock\n    def get_branch_size(self) -> int:\n        return self.height() - self.get_max_forkpoint() + 1\n\n    def get_name(self) -> str:\n        return self.get_hash(self.get_max_forkpoint()).lstrip('0')[0:10]\n\n    def check_header(self, header: dict) -> bool:\n        header_hash = hash_header(header)\n        height = header.get('block_height')\n        return self.check_hash(height, header_hash)\n\n    def check_hash(self, height: int, header_hash: str) -> bool:\n        \"\"\"Returns whether the hash of the block at given height\n        is the given hash.\n        \"\"\"\n        assert isinstance(header_hash, str) and len(header_hash) == 64, header_hash  # hex\n        try:\n            return header_hash == self.get_hash(height)\n        except Exception:\n            return False\n\n    def fork(parent, header: dict) -> 'Blockchain':\n        if not parent.can_connect(header, check_height=False):\n            raise Exception(\"forking header does not connect to parent chain\")\n        forkpoint = header.get('block_height')\n        self = Blockchain(config=parent.config,\n                          forkpoint=forkpoint,\n                          parent=parent,\n                          forkpoint_hash=hash_header(header),\n                          prev_hash=parent.get_hash(forkpoint-1))\n        self.assert_headers_file_available(parent.path())\n        open(self.path(), 'w+').close()\n        self.save_header(header)\n        # put into global dict. note that in some cases\n        # save_header might have already put it there but that's OK\n        chain_id = self.get_id()\n        with blockchains_lock:\n            blockchains[chain_id] = self\n        return self\n\n    @with_lock\n    def height(self) -> int:\n        return self.forkpoint + self.size() - 1\n\n    @with_lock\n    def size(self) -> int:\n        return self._size\n\n    @with_lock\n    def update_size(self) -> None:\n        p = self.path()\n        self._size = os.path.getsize(p)//HEADER_SIZE if os.path.exists(p) else 0\n\n    @classmethod\n    def verify_header(cls, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None:\n        _hash = hash_header(header)\n        if expected_header_hash and expected_header_hash != _hash:\n            raise InvalidHeader(\"hash mismatches with expected: {} vs {}\".format(expected_header_hash, _hash))\n        if prev_hash != header.get('prev_block_hash'):\n            raise InvalidHeader(\"prev hash mismatch: %s vs %s\" % (prev_hash, header.get('prev_block_hash')))\n        if constants.net.TESTNET:\n            return\n        bits = cls.target_to_bits(target)\n        if bits != header.get('bits'):\n            raise InvalidHeader(\"bits mismatch: %s vs %s\" % (bits, header.get('bits')))\n        _pow_hash = pow_hash_header(header)\n        pow_hash_as_num = int.from_bytes(bfh(_pow_hash), byteorder='big')\n        if pow_hash_as_num > target:\n            raise InvalidHeader(f\"insufficient proof of work: {pow_hash_as_num} vs target {target}\")\n\n    def verify_chunk(self, index: int, data: bytes) -> None:\n        num = len(data) // HEADER_SIZE\n        start_height = index * CHUNK_SIZE\n        prev_hash = self.get_hash(start_height - 1)\n        target = self.get_target(index-1)\n        for i in range(num):\n            height = start_height + i\n            try:\n                expected_header_hash = self.get_hash(height)\n            except MissingHeader:\n                expected_header_hash = None\n            raw_header = data[i*HEADER_SIZE : (i+1)*HEADER_SIZE]\n            header = deserialize_header(raw_header, index*CHUNK_SIZE + i)\n            self.verify_header(header, prev_hash, target, expected_header_hash)\n            prev_hash = hash_header(header)\n\n    @with_lock\n    def path(self):\n        d = util.get_headers_dir(self.config)\n        if self.parent is None:\n            filename = 'blockchain_headers'\n        else:\n            assert self.forkpoint > 0, self.forkpoint\n            prev_hash = self._prev_hash.lstrip('0')\n            first_hash = self._forkpoint_hash.lstrip('0')\n            basename = f'fork2_{self.forkpoint}_{prev_hash}_{first_hash}'\n            filename = os.path.join('forks', basename)\n        return os.path.join(d, filename)\n\n    @with_lock\n    def save_chunk(self, index: int, chunk: bytes):\n        assert index >= 0, index\n        chunk_within_checkpoint_region = index < len(self.checkpoints)\n        # chunks in checkpoint region are the responsibility of the 'main chain'\n        if chunk_within_checkpoint_region and self.parent is not None:\n            main_chain = get_best_chain()\n            main_chain.save_chunk(index, chunk)\n            return\n\n        delta_height = (index * CHUNK_SIZE - self.forkpoint)\n        delta_bytes = delta_height * HEADER_SIZE\n        # if this chunk contains our forkpoint, only save the part after forkpoint\n        # (the part before is the responsibility of the parent)\n        if delta_bytes < 0:\n            chunk = chunk[-delta_bytes:]\n            delta_bytes = 0\n        truncate = not chunk_within_checkpoint_region\n        self.write(chunk, delta_bytes, truncate)\n        self.swap_with_parent()\n\n    def swap_with_parent(self) -> None:\n        with self.lock, blockchains_lock:\n            # do the swap; possibly multiple ones\n            cnt = 0\n            while True:\n                old_parent = self.parent\n                if not self._swap_with_parent():\n                    break\n                # make sure we are making progress\n                cnt += 1\n                if cnt > len(blockchains):\n                    raise Exception(f'swapping fork with parent too many times: {cnt}')\n                # we might have become the parent of some of our former siblings\n                for old_sibling in old_parent.get_direct_children():\n                    if self.check_hash(old_sibling.forkpoint - 1, old_sibling._prev_hash):\n                        old_sibling.parent = self\n\n    def _swap_with_parent(self) -> bool:\n        \"\"\"Check if this chain became stronger than its parent, and swap\n        the underlying files if so. The Blockchain instances will keep\n        'containing' the same headers, but their ids change and so\n        they will be stored in different files.\"\"\"\n        if self.parent is None:\n            return False\n        if self.parent.get_chainwork() >= self.get_chainwork():\n            return False\n        self.logger.info(f\"swapping {self.forkpoint} {self.parent.forkpoint}\")\n        parent_branch_size = self.parent.height() - self.forkpoint + 1\n        forkpoint = self.forkpoint  # type: Optional[int]\n        parent = self.parent  # type: Optional[Blockchain]\n        child_old_id = self.get_id()\n        parent_old_id = parent.get_id()\n        # swap files\n        # child takes parent's name\n        # parent's new name will be something new (not child's old name)\n        self.assert_headers_file_available(self.path())\n        child_old_name = self.path()\n        with open(self.path(), 'rb') as f:\n            my_data = f.read()\n        self.assert_headers_file_available(parent.path())\n        assert forkpoint > parent.forkpoint, (f\"forkpoint of parent chain ({parent.forkpoint}) \"\n                                              f\"should be at lower height than children's ({forkpoint})\")\n        with open(parent.path(), 'rb') as f:\n            f.seek((forkpoint - parent.forkpoint)*HEADER_SIZE)\n            parent_data = f.read(parent_branch_size*HEADER_SIZE)\n        self.write(parent_data, 0)\n        parent.write(my_data, (forkpoint - parent.forkpoint)*HEADER_SIZE)\n        # swap parameters\n        self.parent, parent.parent = parent.parent, self  # type: Optional[Blockchain], Optional[Blockchain]\n        self.forkpoint, parent.forkpoint = parent.forkpoint, self.forkpoint\n        self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(parent_data[:HEADER_SIZE])\n        self._prev_hash, parent._prev_hash = parent._prev_hash, self._prev_hash\n        # parent's new name\n        os.replace(child_old_name, parent.path())\n        self.update_size()\n        parent.update_size()\n        # update pointers\n        blockchains.pop(child_old_id, None)\n        blockchains.pop(parent_old_id, None)\n        blockchains[self.get_id()] = self\n        blockchains[parent.get_id()] = parent\n        return True\n\n    def get_id(self) -> str:\n        return self._forkpoint_hash\n\n    def assert_headers_file_available(self, path):\n        if os.path.exists(path):\n            return\n        elif not os.path.exists(util.get_headers_dir(self.config)):\n            raise FileNotFoundError('Electrum headers_dir does not exist. Was it deleted while running?')\n        else:\n            raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path))\n\n    @with_lock\n    def write(self, data: bytes, offset: int, truncate: bool = True, *, fsync: bool = True) -> None:\n        filename = self.path()\n        self.assert_headers_file_available(filename)\n        with open(filename, 'rb+') as f:\n            if truncate and offset != self._size * HEADER_SIZE:\n                f.seek(offset)\n                f.truncate()\n            f.seek(offset)\n            f.write(data)\n            if fsync:\n                f.flush()\n                os.fsync(f.fileno())\n        self.update_size()\n\n    @with_lock\n    def save_header(self, header: dict) -> None:\n        delta = header.get('block_height') - self.forkpoint\n        data = serialize_header(header)\n        # headers are only _appended_ to the end:\n        assert delta == self.size(), (delta, self.size())\n        assert len(data) == HEADER_SIZE\n        # note: we don't fsync, to improve perf. losing headers at end of file is ok.\n        self.write(data, delta*HEADER_SIZE, fsync=False)\n        self.swap_with_parent()\n\n    @with_lock\n    def read_header(self, height: int) -> Optional[dict]:\n        if height < 0:\n            return\n        if height < self.forkpoint:\n            return self.parent.read_header(height)\n        if height > self.height():\n            return\n        delta = height - self.forkpoint\n        name = self.path()\n        self.assert_headers_file_available(name)\n        with open(name, 'rb') as f:\n            f.seek(delta * HEADER_SIZE)\n            h = f.read(HEADER_SIZE)\n            if len(h) < HEADER_SIZE:\n                raise Exception('Expected to read a full header. This was only {} bytes'.format(len(h)))\n        if h == bytes([0])*HEADER_SIZE:\n            return None\n        return deserialize_header(h, height)\n\n    def header_at_tip(self) -> Optional[dict]:\n        \"\"\"Return latest header.\"\"\"\n        height = self.height()\n        return self.read_header(height)\n\n    def is_tip_stale(self) -> bool:\n        STALE_DELAY = 8 * 60 * 60  # in seconds\n        header = self.header_at_tip()\n        if not header:\n            return True\n        # note: We check the timestamp only in the latest header.\n        #       The Bitcoin consensus has a lot of leeway here:\n        #       - needs to be greater than the median of the timestamps of the past 11 blocks, and\n        #       - up to at most 2 hours into the future compared to local clock\n        #       so there is ~2 hours of leeway in either direction\n        if header['timestamp'] + STALE_DELAY < time.time():\n            return True\n        return False\n\n    def get_hash(self, height: int) -> str:\n        def is_height_checkpoint():\n            within_cp_range = height <= constants.net.max_checkpoint()\n            at_chunk_boundary = (height+1) % CHUNK_SIZE == 0\n            return within_cp_range and at_chunk_boundary\n\n        if height == -1:\n            return '0000000000000000000000000000000000000000000000000000000000000000'\n        elif height == 0:\n            return constants.net.GENESIS\n        elif is_height_checkpoint():\n            index = height // CHUNK_SIZE\n            h, t = self.checkpoints[index]\n            return h\n        else:\n            header = self.read_header(height)\n            if header is None:\n                raise MissingHeader(height)\n            return hash_header(header)\n\n    def get_target(self, index: int) -> int:\n        # compute target from chunk x, used in chunk x+1\n        if constants.net.TESTNET:\n            return 0\n        if index == -1:\n            return MAX_TARGET\n        if index < len(self.checkpoints):\n            h, t = self.checkpoints[index]\n            return t\n        # new target\n        first = self.read_header(index * CHUNK_SIZE)\n        last = self.read_header((index+1) * CHUNK_SIZE - 1)\n        if not first or not last:\n            raise MissingHeader()\n        bits = last.get('bits')\n        target = self.bits_to_target(bits)\n        nActualTimespan = last.get('timestamp') - first.get('timestamp')\n        nTargetTimespan = 14 * 24 * 60 * 60\n        nActualTimespan = max(nActualTimespan, nTargetTimespan // 4)\n        nActualTimespan = min(nActualTimespan, nTargetTimespan * 4)\n        new_target = min(MAX_TARGET, (target * nActualTimespan) // nTargetTimespan)\n        # not any target can be represented in 32 bits:\n        new_target = self.bits_to_target(self.target_to_bits(new_target))\n        return new_target\n\n    @classmethod\n    def bits_to_target(cls, bits: int) -> int:\n        # arith_uint256::SetCompact in Bitcoin Core\n        if not (0 <= bits < (1 << 32)):\n            raise InvalidHeader(f\"bits should be uint32. got {bits!r}\")\n        bitsN = (bits >> 24) & 0xff\n        bitsBase = bits & 0x7fffff\n        if bitsN <= 3:\n            target = bitsBase >> (8 * (3-bitsN))\n        else:\n            target = bitsBase << (8 * (bitsN-3))\n        if target != 0 and bits & 0x800000 != 0:\n            # Bit number 24 (0x800000) represents the sign of N\n            raise InvalidHeader(\"target cannot be negative\")\n        if (target != 0 and\n                (bitsN > 34 or\n                 (bitsN > 33 and bitsBase > 0xff) or\n                 (bitsN > 32 and bitsBase > 0xffff))):\n            raise InvalidHeader(\"target has overflown\")\n        return target\n\n    @classmethod\n    def target_to_bits(cls, target: int) -> int:\n        # arith_uint256::GetCompact in Bitcoin Core\n        # see https://github.com/bitcoin/bitcoin/blob/7fcf53f7b4524572d1d0c9a5fdc388e87eb02416/src/arith_uint256.cpp#L223\n        c = target.to_bytes(length=32, byteorder='big')\n        bitsN = len(c)\n        while bitsN > 0 and c[0] == 0:\n            c = c[1:]\n            bitsN -= 1\n            if len(c) < 3:\n                c += b'\\x00'\n        bitsBase = int.from_bytes(c[:3], byteorder='big')\n        if bitsBase >= 0x800000:\n            bitsN += 1\n            bitsBase >>= 8\n        return bitsN << 24 | bitsBase\n\n    def chainwork_of_header_at_height(self, height: int) -> int:\n        \"\"\"work done by single header at given height\"\"\"\n        chunk_idx = height // CHUNK_SIZE - 1\n        target = self.get_target(chunk_idx)\n        work = ((2 ** 256 - target - 1) // (target + 1)) + 1\n        return work\n\n    @with_lock\n    def get_chainwork(self, height=None) -> int:\n        if height is None:\n            height = max(0, self.height())\n        if constants.net.TESTNET:\n            # On testnet/regtest, difficulty works somewhat different.\n            # It's out of scope to properly implement that.\n            return height\n        last_retarget = height // CHUNK_SIZE * CHUNK_SIZE - 1\n        cached_height = last_retarget\n        while _CHAINWORK_CACHE.get(self.get_hash(cached_height)) is None:\n            if cached_height <= -1:\n                break\n            cached_height -= CHUNK_SIZE\n        assert cached_height >= -1, cached_height\n        running_total = _CHAINWORK_CACHE[self.get_hash(cached_height)]\n        while cached_height < last_retarget:\n            cached_height += CHUNK_SIZE\n            work_in_single_header = self.chainwork_of_header_at_height(cached_height)\n            work_in_chunk = CHUNK_SIZE * work_in_single_header\n            running_total += work_in_chunk\n            _CHAINWORK_CACHE[self.get_hash(cached_height)] = running_total\n        cached_height += CHUNK_SIZE\n        work_in_single_header = self.chainwork_of_header_at_height(cached_height)\n        work_in_last_partial_chunk = (height % CHUNK_SIZE + 1) * work_in_single_header\n        return running_total + work_in_last_partial_chunk\n\n    def can_connect(self, header: dict, *, check_height: bool = True) -> bool:\n        if header is None:\n            return False\n        height = header['block_height']\n        if check_height and self.height() != height - 1:\n            return False\n        if height == 0:\n            return hash_header(header) == constants.net.GENESIS\n        try:\n            prev_hash = self.get_hash(height - 1)\n        except Exception:\n            return False\n        if prev_hash != header.get('prev_block_hash'):\n            return False\n        try:\n            target = self.get_target(height // CHUNK_SIZE - 1)\n        except MissingHeader:\n            return False\n        try:\n            self.verify_header(header, prev_hash, target)\n        except BaseException as e:\n            return False\n        return True\n\n    def connect_chunk(self, idx: int, data: bytes) -> bool:\n        assert idx >= 0, idx\n        try:\n            self.verify_chunk(idx, data)\n            self.save_chunk(idx, data)\n            return True\n        except BaseException as e:\n            self.logger.info(f'verify_chunk idx {idx} failed: {repr(e)}')\n            return False\n\n    def get_checkpoints(self):\n        # for each chunk, store the hash of the last block and the target after the chunk\n        cp = []\n        n = self.height() // CHUNK_SIZE\n        for index in range(n):\n            h = self.get_hash((index+1) * CHUNK_SIZE -1)\n            target = self.get_target(index)\n            cp.append((h, target))\n        return cp\n\n\ndef check_header(header: dict) -> Optional[Blockchain]:\n    \"\"\"Returns any Blockchain that contains header, or None.\"\"\"\n    if type(header) is not dict:\n        return None\n    with blockchains_lock: chains = list(blockchains.values())\n    for b in chains:\n        if b.check_header(header):\n            return b\n    return None\n\n\ndef can_connect(header: dict) -> Optional[Blockchain]:\n    \"\"\"Returns the Blockchain that has a tip that directly links up\n    with header, or None.\n    \"\"\"\n    with blockchains_lock: chains = list(blockchains.values())\n    for b in chains:\n        if b.can_connect(header):\n            return b\n    return None\n\n\ndef get_chains_that_contain_header(height: int, header_hash: str) -> Sequence[Blockchain]:\n    \"\"\"Returns a list of Blockchains that contain header, best chain first.\"\"\"\n    with blockchains_lock: chains = list(blockchains.values())\n    chains = [chain for chain in chains\n              if chain.check_hash(height=height, header_hash=header_hash)]\n    chains = sorted(chains, key=lambda x: x.get_chainwork(), reverse=True)\n    return chains\n"
  },
  {
    "path": "electrum/chains/mainnet/checkpoints.json",
    "content": "[\n    [\n        \"00000000693067b0e6b440bc51450b9f3850561b07f6d3c021c54fbd6abb9763\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"00000000f037ad09d0b05ee66b8c1da83030abaf909d2b1bf519c3c7d2cd3fdf\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"000000006ce8b5f16fcedde13acbc9641baa1c67734f177d770a4069c06c9de8\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"00000000563298de120522b5ae17da21aaae02eee2d7fcb5be65d9224dbd601c\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"000000009b0a4b2833b4a0aa61171ee75b8eb301ac45a18713795a72e461a946\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"00000000fa8a7363e8f6fdc88ec55edf264c9c7b31268c26e497a4587c750584\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"000000008ac55b5cd76a5c176f2457f0e9df5ff1c719d939f1022712b1ba2092\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"000000007f0c796631f00f542c0b402d638d3518bc208f8c9e5d29d2f169c084\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"00000000ffb062296c9d4eb5f87bbf905d30669d26eab6bced341bd3f1dba5fd\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"0000000074c108842c3ec2252bba62db4050bf0dddfee3ddaa5f847076b8822f\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"0000000067dc2f84a73fbf5d3c70678ce4a1496ef3a62c557bc79cbdd1d49f22\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"00000000dbf06f47c0624262ecb197bccf6bdaaabc2d973708ac401ac8955acc\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"000000009260fe30ec89ef367122f429dcc59f61735760f2b2288f2e854f04ac\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"00000000f9f1a700898c4e0671af6efd441eaf339ba075a5c5c7b0949473c80b\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"000000005107662c86452e7365f32f8ffdc70d8d87aa6f78630a79f7d77fbfe6\",\n        26959535291011309493156476344723991336010898738574164086137773096960\n    ],\n    [\n        \"00000000984f962134a7291e3693075ae03e521f0ee33378ec30a334d860034b\",\n        22791060871177364286867400663010583169263383106957897897309909286912\n    ],\n    [\n        \"000000005e36047e39452a7beaaa6721048ac408a3e75bb60a8b0008713653ce\",\n        20657664212610420653213483117824978239553266057163961604478437687296\n    ],\n    [\n        \"00000000128d789579ffbec00203a371cbb39cee27df35d951fd66e62ed59258\",\n        20055820920770189543295303139304627292355830414308479769458683936768\n    ],\n    [\n        \"000000008dde642fb80481bb5e1671cb04c6716de5b7f783aa3388456d5c8a85\",\n        14823939180767414932263578623363531361763221729526512593941781544960\n    ],\n    [\n        \"000000008135b689ad1557d4e148a8b9e58e2c4a67240fc87962abb69710231a\",\n        10665477591887247494381404907447500979192021944764506987270680608768\n    ],\n    [\n        \"00000000308496ef3e4f9fa542a772df637b4aaf1dcce404424611feacfc09e7\",\n        7129927859545590787920041835044506526699926406309469412482969763840\n    ],\n    [\n        \"000000001a2e0c63d7d012003c9173acfd04ccd6372027718979228c461b5ed5\",\n        5949911473257063494842414979623989957440207170696926280907451531264\n    ],\n    [\n        \"000000002e0c0ac26ccde91b51ab018576b3a126b413e9f6f787b36637f1b174\",\n        5905492491837656485645884063467495540781288435542782321354050895872\n    ],\n    [\n        \"00000000103226f85fe2b68795f087dcec345e523363f18017e60b5c94175355\",\n        4430143390146946405787502162943966061454423600514874825749833973760\n    ],\n    [\n        \"000000001ae6f66fd4de47f8d6f357e798943bbfc4f39ebf14b0975fab059173\",\n        3447600406241317909690675945127070282093452846402311540118831235072\n    ],\n    [\n        \"000000000a3f22690162744d3bc0b674c92e661a25afb3d2ac8b39b27ac14373\",\n        2351604382534916182160036119666703740669209516522695514729880748032\n    ],\n    [\n        \"0000000006dc436c3c515a97446af858c1203a501c85d26c4a30afa380aba4a1\",\n        2098151686442211199940455690614286210348997571531298297574806519808\n    ],\n    [\n        \"000000000943fe1680ffcc498ce50790ff8e842a8af2c157664e4fbc1cb7cb46\",\n        2275790652544821279950241890112140030244814501479017131553197129728\n    ],\n    [\n        \"000000000847b2144376c1fb057ea1d5a027d5a6004277ed4c72422e93df04e9\",\n        1622203955679450683159610732218403647246163922223729367236739072000\n    ],\n    [\n        \"00000000094505954deb1d31382b86d0510fd280a34143400b1856a4d52b4c93\",\n        1551048739079662593758612650769536967206480773659027300489594142720\n    ],\n    [\n        \"000000000109272cecb3f7e98ac12cf149fa8a1b2aaab248e1b006b0dc595a3a\",\n        1389323280429349294447518501872137680563441219958739463959193059328\n    ],\n    [\n        \"0000000009e6aa0fe39b790625ffeb18a2d6ff5060a5bd14e699e83c54109977\",\n        1147152896345386682952518188670047452875537662186691235300769792000\n    ],\n    [\n        \"0000000000d14af55c4eae0121184919baba2deb8bf89c3af6b8e4c4f35c8e4e\",\n        594007861936424273334637371358095438347537381057796937154824241152\n    ],\n    [\n        \"0000000003dfbfa2b33707e691ab2ab7cda7503be2c2cce43d1b21cd1cc757fb\",\n        148501965484106068333659342839523859586884345264449234288706060288\n    ],\n    [\n        \"0000000000c169d181d66d242901f70d006f3e088c1ae9cacb88b94b8266e9c3\",\n        110393429764504113949181711819653188468070301266890302199533928448\n    ],\n    [\n        \"000000000009f7d1439d6a2fc1a456db8e843674275bf0133fc7b43b5f45b96e\",\n        76554528428498296726819074079132986384157750623812250673757552640\n    ],\n    [\n        \"000000000011b8a8fad7973548b50f6d4b2ba1690f7487c374e43248c576354f\",\n        52678642966898219212816601311127992435882858542187514726849708032\n    ],\n    [\n        \"000000000077e856b6cc475d9cf784119811214c9cac8d7b674ec24faa7c2c0c\",\n        43246870766561725070861386869077695524372774526710079316876591104\n    ],\n    [\n        \"00000000004cbb474f2cbf3a65f690efa09804512af3351ba3a0888c806c6625\",\n        37817516728945957090904676150631917288430706594442690521085247488\n    ],\n    [\n        \"0000000000235b1ec6656d8e91f3dde3b6ab9ad7e75b332e4da9355ce60d860e\",\n        29373101246077110899697012205905070265841442578602225419818106880\n    ],\n    [\n        \"00000000002a153a2c95a8e5493db93086b0e3fe590b636a5871ace57523ef93\",\n        20444488966645742314409346972440253478913291170842138088329707520\n    ],\n    [\n        \"00000000000e9550e084908cf91a4e8b74f9f1315d1bc4020709f9e7f261bb18\",\n        19563849255781403323327768731100757126732627316116500830377476096\n    ],\n    [\n        \"00000000002c2cfef3bb85b463d3fcd39b73a6d3d5ae11c1e2a8113e3794f28d\",\n        12545026348036226200394850922278603223904369245268262607334146048\n    ],\n    [\n        \"00000000000fa92b757ee29674aa97e98a49ba3ad340d2baa94155d71648dfe1\",\n        8719867261221084516486306056196045840260667577454435863762042880\n    ],\n    [\n        \"0000000000030571601dbc8e13d00d45004eee6ea8b6ab3cdfb38d2546fee21c\",\n        5942996718418989293499865695368015163438891473576991811912597504\n    ],\n    [\n        \"00000000000bb6adef42e63082b20fd2b1dc1b324c51973512a4c31f29a9986e\",\n        3926013280397599483741094494745234959951218212740030386090803200\n    ],\n    [\n        \"000000000000765094788a98dbb8adac30d248b7129b59b1441ee2b7ef9e332f\",\n        3337321571246095014985518819479127172783474909736415373333364736\n    ],\n    [\n        \"00000000000431a0aa9625f82975709f3c6f4f64d04c559512af051599872084\",\n        2200419182034594781720344474937177839165432393990533906392154112\n    ],\n    [\n        \"00000000000292b850b8f8578e6b4d03cbb4a78ada44afbb4d2f80a16490e8f9\",\n        1861311314983800126815643622927230076368334845814253369901973504\n    ],\n    [\n        \"0000000000025afe84e27423011af25f777e5a94545dbd00fd04bebe9050f7dd\",\n        1653206561150525499452195696179626311675293455763937233695932416\n    ],\n    [\n        \"0000000000000e389cccae2a40437be574fd806909e24136711e7f8bce671d65\",\n        1462200632444444190489436459820840230299714881944341127503020032\n    ],\n    [\n        \"0000000000030510bf6bc1649726cf2e6e4010c64a2c8fd3fde5dc92535ca40e\",\n        1224744150896501443874292381730317417444978877835711165914677248\n    ],\n    [\n        \"00000000000082648057f14fc835779c6ce46a407bafb2e5c2ac1d20d9f4e822\",\n        1036989760889350435547200084292752907272941324136347429599444992\n    ],\n    [\n        \"000000000000f38accd6b22959010471a6d9f159d43bf2a9d4c53c220201254e\",\n        739430030225080220618328322475016688484025266646974337550123008\n    ],\n    [\n        \"0000000000004ed7a73133678b5eb883cd8882bf14dfb26c104ae0c3f94cf4ee\",\n        484975157177710342494716926626447514974484083994735770500857856\n    ],\n    [\n        \"00000000000037bb3ff4cf649a1757d4028ecc10f893529b4a2214792c981f96\",\n        353833947722011807976659613996792948209273674048993161457434624\n    ],\n    [\n        \"0000000000008008f46559fe7f181e9dc0648f213472a1e576e8bf506b88f22f\",\n        390843739553851677760235428436025349398613161749553108945469440\n    ],\n    [\n        \"000000000000691d0c2444db713bf6c088844cc95a37cdc55cc269bb0a31d8c8\",\n        327394795212563108599383268946242257264650552916910648089116672\n    ],\n    [\n        \"00000000000071153b0afcc64a425f8442c29749610797119e732dd4b723f675\",\n        291935447509363748964474894494542149680088347011133317125767168\n    ],\n    [\n        \"000000000000a384acb522e4e5935ad2bc31366ecf1f16f1f11023e967ef033d\",\n        245823858161213192073337185391658632187400443916100519594033152\n    ],\n    [\n        \"0000000000002e532093d43e901292121fb7c6583caf2d13b666fe7e194b4a97\",\n        171262555713783851185422181139260521316022447660158187451973632\n    ],\n    [\n        \"00000000000033e435c4bbddc7eb255146aa7f18e61a832983af3a9ee5dd144d\",\n        110438984653392107399822606842181601255647711092336854093004800\n    ],\n    [\n        \"00000000000028ff4b0bd45f0e3e713f91fa1821d28a276a1a1f32f786662f13\",\n        61993465896324436412959469550829248888675813063783317791309824\n    ],\n    [\n        \"0000000000001ef9c75318e116a607af4de68fb4f67c788677ee6779fb5fa0d5\",\n        47525089675259291211422247200069659468817014361857087365971968\n    ],\n    [\n        \"0000000000000e6e98694ccb8247aad63aaa1e2bec5a7be14329407e4cea6223\",\n        30742228348699538311994447367921718297595975288392383715082240\n    ],\n    [\n        \"000000000000000a2153574b2523a6d1844c3cb82d085e2575846dd8c5d4ebb4\",\n        19547336162709893274575855467812492508787617050928192350584832\n    ],\n    [\n        \"00000000000002a92c1b1ffb2a8388979cf30798e312335ae2a1b922927ee83d\",\n        17248274092338559882155796390905381469049315669915374897332224\n    ],\n    [\n        \"00000000000004d54b1422ce733922e7672a4e2ecc86dcf96c0de06565cddaa6\",\n        15943936487596784557029840069157210316687734428242467413295104\n    ],\n    [\n        \"00000000000009dd91ae96cbbf67af42340b0bc715b3606aa725f630b470262d\",\n        14273467308195657992975774342458504496649432985410431166185472\n    ],\n    [\n        \"00000000000007d33d78522fa95bdcd4a25072aeac844cbe9b6bc5d0cc885d0a\",\n        14930233597189143322113827544414041000381079823613435714732032\n    ],\n    [\n        \"00000000000003dd57f5dd1228f68390b586700063225d26bac972bd120546d2\",\n        15164766714763258952996988973449124317842091658872414191747072\n    ],\n    [\n        \"000000000000076bdeca878b47c392f51fbda543b1e69612cf7d305deb537604\",\n        15357836632983707094928406965317628870031114888441593128288256\n    ],\n    [\n        \"00000000000008eb1bb7e18d9dfe62210d761cbf114d59ca08e4f638b8563e30\",\n        15958672964717750944291813934170287689797412223641384931819520\n    ],\n    [\n        \"00000000000001b0d8d885e4d77d7c51e8f1fdaba68f229ac04d191915845f09\",\n        18362361570655080300849714079315004638119732162003921272832000\n    ],\n    [\n        \"000000000000081baa3a716d5f9ab072c9fc3b798900234c9be23ab02a287c30\",\n        22401652017447755518156310839596703571934659990690572544245760\n    ],\n    [\n        \"00000000000005b88d0224b9b0d4b65d3de9a61d93609bb91c9297440f1c4657\",\n        22607619418140130980719672680045705126213018528712048676700160\n    ],\n    [\n        \"000000000000027d6a6870403fa43a650b7d9a6e61243f375a79ea935ad9ef1f\",\n        24717289559589094364468373797949472355802981654048927838633984\n    ],\n    [\n        \"0000000000000810a3490b86e4f302f6557f9621c5c8620c2b09ec8f0cf72794\",\n        23340814324747679919001773364939281849550099124416593832968192\n    ],\n    [\n        \"000000000000073833bca8d0ea909fde717e251576b7b3ccaaa58ad5d39eed60\",\n        23242391331131109072962566885467580392541369223033474166816768\n    ],\n    [\n        \"000000000000031b7fd2ed1f28ff74e969aa891297706c38bd2e1d3bc48183c4\",\n        21554562042243053719921017803645315870071034703425342074257408\n    ],\n    [\n        \"0000000000000b0738bcba382983811d40b531f2e68cd57126092755f1be4ba6\",\n        20615546854515052444405957679617344022137222968655050411343872\n    ],\n    [\n        \"000000000000000664cbfd5e3fa497c07614c33a0934b83e01fbe980634a9aa4\",\n        19540887421473929614259883543522244007742949396702043752628224\n    ],\n    [\n        \"000000000000021eb520df39289a70e40c59822a8c47924dc4940e7d0c1455c4\",\n        19588382523276445241758125434587686389961661359576757951266816\n    ],\n    [\n        \"0000000000000275e0c41b11bc250fe887c5e60c8ebaaa449f5c28c67133d496\",\n        18009299117968233362105684657812007807160912568078774269116416\n    ],\n    [\n        \"000000000000097fb0fdbeee0cee7e8f4e1a4ef8fad49f3d549624b0d47abed0\",\n        17993483763986497389087426516491816616385967180337839494660096\n    ],\n    [\n        \"000000000000053f199ae19d34365277e534f978ea2f6c69cd4757a4fc099af5\",\n        16574638092431222848464934504874974361824393751455373256032256\n    ],\n    [\n        \"0000000000000217b2e7b4f61682d24b9357d62ad29f27ed45ea2a32dc1f32f6\",\n        17085559845791583266730740536950670241169412424878408752693248\n    ],\n    [\n        \"000000000000039c1d77acd4702393f48ca61983c64fc0209ade141c694b2359\",\n        17870687961287995446644888885900316642120964851955511819501568\n    ],\n    [\n        \"0000000000000ae53f0c78330f6c2fbece2752909bc3742823e4fab29c5fd2b0\",\n        15554707140145502641228553657813466188995512591033787398225920\n    ],\n    [\n        \"00000000000004b4d72b8631a85ec7d226dc696f1913ba1bf735b7c8dec207b8\",\n        16944226977030767532657500340718760127019357828074148225613824\n    ],\n    [\n        \"00000000000006e06735bffb7d2f215dcadd8311fc33f4a46661fdca3dc0560e\",\n        17028747171100603034973679895960153979114298528140818252824576\n    ],\n    [\n        \"000000000000055fc0110d4a38ffb338eabc30c8b0aef355d4643d21b5b6a860\",\n        15614535766060906942258863525753414259523988166363835227176960\n    ],\n    [\n        \"000000000000081b69cb4de006c14084c4861f0e4a140c37200117a738733fe8\",\n        15392654931672180089790308609774483894682932641297604569726976\n    ],\n    [\n        \"00000000000009920770f2d40b5b6a8aba33d969b855c91b0f56e3db9c27e41a\",\n        14444739009842829731785903206212823051010663269705670545375232\n    ],\n    [\n        \"0000000000000791dd1cb7a684a54c72ccde51f459fff0fc3e6e051641b1e941\",\n        13237058963854547748734324548161076199478283141947127217782784\n    ],\n    [\n        \"000000000000019da474a1a598b5cf28534b7fd9b214eed0f36c67c203a9b449\",\n        12305424274651356593961118223415860240572779254789271782948864\n    ],\n    [\n        \"000000000000074333e888bac730f9772b65e4cc9d07edb122c6e3c6606bc8ab\",\n        11046080738989403765716562970384822165842244193743674858799104\n    ],\n    [\n        \"000000000000067080669115c445f378f3dec19787558d0e03263b9dec5d7720\",\n        10007073282210984973971337419529346944295676968729147521105920\n    ],\n    [\n        \"0000000000000304760bf583f4ac241c5ffe77312fa213634eba252c720530f1\",\n        9412783771427520201810837309176674245361798887059324066070528\n    ],\n    [\n        \"000000000000041fb61665c8a31b8b5c3ae8fe81903ea81530c979d5094e6f9d\",\n        8825801199382903987726989797449454220615414953524072026210304\n    ],\n    [\n        \"000000000000022fc7f2a5c87b2bab742d71c4eb662d572df33f18193d6abf0e\",\n        8774971387283464186072960143252932765613148614319486309236736\n    ],\n    [\n        \"000000000000013c6d43ba38bc5f24e699515b9d78602694112fefdc64606640\",\n        8158785580212107593904235970576336449063725988071903546310656\n    ],\n    [\n        \"00000000000001665176b9a810fddf27cca60dfcfd80bf113289fcc8ffed0284\",\n        8002789794116287035234223109988652176644807295346590313611264\n    ],\n    [\n        \"00000000000002dc6ef80f56a00f1091471d942ce9bfb656ebdab4ea0b77eb0b\",\n        7839560629067579481152758851432818444879208153964570478641152\n    ],\n    [\n        \"00000000000002a1fa5546ec48ca88b9e5710e2c6d895bb3675004fdacd6ab13\",\n        7999430563890709006856701613305138698914315019190763857641472\n    ],\n    [\n        \"00000000000000f517517c11e649b98feca7da84ae44fb643de5a86798fe3c31\",\n        9047927233058169382412882048952728634925849476849852060008448\n    ],\n    [\n        \"0000000000000299cab92a923348acf9251f656bcbacdb641fd0a66d895a6e8f\",\n        8296391419817537486273948666838217011279219811331013552898048\n    ],\n    [\n        \"000000000000027508b977f72c3a0f06f1f36e311ad079536630661880934501\",\n        9081029136740872581753422344739175313292014241889017867010048\n    ],\n    [\n        \"00000000000001925959229452cc6fbfef0104ebed7ccd6f584f2439c5dd1f1b\",\n        8230751570811169734692743946971314968326461977249645504495616\n    ],\n    [\n        \"00000000000003b34ca89509da5f558af468c194afaa8d458bbeb07c50cc7c74\",\n        7384127474250891166670391848516180960454656786677558849568768\n    ],\n    [\n        \"0000000000000076559e314ab0c86cc552e34fd79488415d3d17f6ea3c01adb3\",\n        6172230000534146257480611019445716458048957888854766248787968\n    ],\n    [\n        \"000000000000003a58043252cdc30ed2f37fb17e6ef1658324b1478f16c1463b\",\n        5561365017980676031428107027647386014985059524839404952616960\n    ],\n    [\n        \"000000000000011babf767e60240658195b693711c217d7da0d9215ccab45333\",\n        4026319404534786334009451711043898716884778820756489262596096\n    ],\n    [\n        \"000000000000027579d28fb480ccad8e2516d1219d4c1919e3fd4fc0c882955d\",\n        3513558656525386849113615662535622466519417660386833443323904\n    ],\n    [\n        \"0000000000000074546fe07f80ba15fc81897ec56a5535de727df9fda9dab500\",\n        3004083578955603829930099910053556479043735076695139267117056\n    ],\n    [\n        \"00000000000000b6c55833b80c07894f4c4d3bb686e5ddbc1b1d162e22752ca3\",\n        2675541054922611112919804040984964595022815308724929898217472\n    ],\n    [\n        \"00000000000001326f2f970753122e35bfdf3358d046ddf5ea22e57f5d82b00d\",\n        2409843108029446766213067266805752590003732794677225687351296\n    ],\n    [\n        \"00000000000000641084745613912464ff73c974bafd0bf6dd306295f019d306\",\n        2218268905456883731807407021635746739577921454491297946533888\n    ],\n    [\n        \"000000000000011ae105ddb1a5bbac6931a6578d95c201525f3a945276a64559\",\n        1727551573307299192250197436766000536509732237655131060961280\n    ],\n    [\n        \"00000000000000d9b66fee19af89eaaf3f3933d1acd2617924c107f0abbe0a41\",\n        1394031503757574068227953656553224448260418805016069352194048\n    ],\n    [\n        \"0000000000000011956d42670c2f75eeb344ac0657a806775998e2c58fa4b157\",\n        1263610003247723462826224891154624535497729630761756072607744\n    ],\n    [\n        \"00000000000000959b1ea990368fd16d494e68ee13bd7245ddd9cdfba3330100\",\n        1030450001678223668360152541055867895065240185756254103142400\n    ],\n    [\n        \"0000000000000091f86b1e423e24fe358c72db181cfcc2738c85f2f51871a960\",\n        862513010327976103705811440432628413487564277790886242287616\n    ],\n    [\n        \"0000000000000055e146e473b49fe656a1f2f4b8c33e72b80acc18f84d9fcc26\",\n        720982641204331278205950312227594303241470815982254303477760\n    ],\n    [\n        \"000000000000004f6a191a3261274735292bc30a1f79f23a143e4ee7dd2f64c1\",\n        530591525189316709998942710962548491505413142398652303540224\n    ],\n    [\n        \"000000000000005327c8e714272803c60277333362e74ec88b9ffab5410c2358\",\n        410030579894253754102159787320079652501746816512444002729984\n    ],\n    [\n        \"0000000000000002e2a62b8705564c38d6a746fc8e971a450a69989152b5ee97\",\n        310118479516817784682897231521434079438159381558537557639168\n    ],\n    [\n        \"00000000000000202bf3ff30109538bfd9b5075c6438ab5ef64ebe2cf9b61404\",\n        239366800071949252578530950352093786414793290792735831228416\n    ],\n    [\n        \"000000000000001c997105893f5991cb45765ff856b6e503f8466cb22cdd330a\",\n        181156297885756721946540202079438048595571151633323613224960\n    ],\n    [\n        \"0000000000000010c13ce182a3d8fc6748b75640447eb360d7739a5fe984ffc1\",\n        142431093377788751676361246670241704468765375727695350988800\n    ],\n    [\n        \"000000000000000bbb49db68b79ecc8393376d78272d237bb612288af64c1de8\",\n        100696259189502783924473792493100546893980348528488767029248\n    ],\n    [\n        \"0000000000000001bbfd0973c367d30eef2416d9e94bdddea53bccf541a4858f\",\n        68962778243821519216393853205209897734463141354237780295680\n    ],\n    [\n        \"0000000000000004ee5b6ace996ab746f1e6dd952cdbc74c0b4f8b9ac51c7335\",\n        52765641310467331636297188681879886184148735229489015947264\n    ],\n    [\n        \"0000000000000002f2f23b515085d0c9f37a2824304ccb7ca1546a48548d0dac\",\n        44233472386696495417387091608220539804351405166731810832384\n    ],\n    [\n        \"00000000000000045590c3fdeca1753d148a87614a70fa0897a17f90bb321654\",\n        38110290672195532365762668664552282566878756832852091863040\n    ],\n    [\n        \"0000000000000002b704edc0bf1435fe2116040b547adb1bc2d196eb81779834\",\n        29679649578007061283718812081441644170496168236939550392320\n    ],\n    [\n        \"00000000000000038cc59dc6dd68ae0fbe2ded8a3de65dbd9a2f9a36d26772df\",\n        22829202948393929850749706076701368331072452018388575715328\n    ],\n    [\n        \"0000000000000000a979bc50075e7cdf0da5274f7314910b2d798b1aeaf6543f\",\n        19005913916847449503306572434028937600915626422125897711616\n    ],\n    [\n        \"0000000000000001dd8e548c8cf5b77cde6e5631cd542e39f42c41952e5e7085\",\n        15065005852539512185984435657022720640916062598235628240896\n    ],\n    [\n        \"0000000000000002513542a461de351a5a94f96b4bcd3e324a48d2d71b403fe0\",\n        12288698618318346282960995223961541766142764336009759948800\n    ],\n    [\n        \"000000000000000150cc07163e78d599a7e56c0d1040641bffb382705ac17df0\",\n        10284386012808371892335572105827331142617405906583881252864\n    ],\n    [\n        \"00000000000000009051d83d276dad5c547612f67c2907acf6a143039bddb1bb\",\n        8614444778121073626993210829679478604092861119379437256704\n    ],\n    [\n        \"00000000000000000b83d3947d2790ab0bcbbb61eba1eb8d8f0f0eb3e9d461e0\",\n        7065379129219572345353864175298106702426244380437224882176\n    ],\n    [\n        \"00000000000000005a4fbbaeffee6d52fa329dd8c559f90c9b30264c46ad33fd\",\n        6343094824615218102798845742064326605321937397913065881600\n    ],\n    [\n        \"00000000000000006b6834bae83e895a78c5026a8c8141388040d90506cf3148\",\n        5384518863803604621895699676581808210968416076987222720512\n    ],\n    [\n        \"0000000000000000bf3c066c9acdb008e7fff3672f1391b35c8877b76b9e295e\",\n        4405349994161605759458363322921957536960017949107037405184\n    ],\n    [\n        \"00000000000000006bcf448b771c8f4db4e2ca653474e3b29504ec08422b3fba\",\n        3863038134637689339706803268689141874606936642244315185152\n    ],\n    [\n        \"000000000000000098686ab04cc22fec77e4fa2d76d5a3cc0eb8cbf4ed800cdc\",\n        3369574570478873127315415525946742317481702644901195284480\n    ],\n    [\n        \"000000000000000036cc637d80982595b1fa30f877efe8904965e6fd70aeae1a\",\n        3045099693687311168583241534842989903432036285033490677760\n    ],\n    [\n        \"00000000000000000ee9b585e0a707347d7c80f3a905f48fa32d448917335366\",\n        2578448441038522347123624842639328775756428679710156783616\n    ],\n    [\n        \"00000000000000000401800189014bad6a3ca1af029e19b362d6ef3c5425a8dc\",\n        2293149852232440455888971398133692017055281498246925516800\n    ],\n    [\n        \"00000000000000001b44d4645ac00773be676f3de8a8bff1a5fdd1fb04d2b3b2\",\n        2002553378451099534811946324256852041059202347552707969024\n    ],\n    [\n        \"00000000000000003ff2a53152ee98910d7383c0177459ad258c4b2d2c4d4610\",\n        1602972750958019380418919163663316163747908621623690788864\n    ],\n    [\n        \"00000000000000001bb242c9463b511b9e6a99a6d48bd783acb070ca27861c2b\",\n        1555090122338762644529309082074529684497336694348804259840\n    ],\n    [\n        \"000000000000000019d43247356b848a7ef8b1c786d8c833b76e382608cb59e9\",\n        1438882362326364789097016808333128944459434864174551793664\n    ],\n    [\n        \"00000000000000003711b624fbde8c77d4c7e25334cfa8bc176b7248ca67b24b\",\n        1366448002777625511026173062127977611952455397852592472064\n    ],\n    [\n        \"0000000000000000092c1f996e0b6d07fd0e73dfe6409a5c2adc1206e997c3a2\",\n        1130631509982695295834811811892052032638591596239280668672\n    ],\n    [\n        \"000000000000000020ce180d66df9d3c28aee9fcec7896071ec67091a9753283\",\n        982897592923314645728937741958820396011314229953349812224\n    ],\n    [\n        \"000000000000000018d37d53ae02e13634eefb8d9246253e99c1bdf65ac293ea\",\n        903780639904017349860452775965599807564731663176966340608\n    ],\n    [\n        \"00000000000000001607d1a21507dea1c0e5f398daf94d35fb7e0a3238f96a0f\",\n        777796486219054632155478957346406689849105796561635377152\n    ],\n    [\n        \"00000000000000001acae244523061f650ddab9c3271d13c0cd86071ae6e8a5f\",\n        770217816864616291160628694313702426464491250746461782016\n    ],\n    [\n        \"0000000000000000104430189dba1219b0e3dd90824e8c2271609aca5b71250f\",\n        749174812297985386116525053725808178560617045558724395008\n    ],\n    [\n        \"00000000000000001aa260733b6d8f8faa2092af35e55973278bb17f8eaeca6b\",\n        680733321990486529407107157001552378184394215934016880640\n    ],\n    [\n        \"000000000000000009925ad5866a9cb3a1d83d9399137bccc7b5470b38b1db2b\",\n        668970595596618687654683311252875969389523722950049529856\n    ],\n    [\n        \"00000000000000001133acacb92e43e24af63a487923361a4a98c87a5550dffe\",\n        673862533877092685902494685124943911912916060357898797056\n    ],\n    [\n        \"000000000000000018c66b4a76ca69204e24ee069da9368c7a9883adb36c24af\",\n        683252062220249508849116041812776958610205092831121375232\n    ],\n    [\n        \"000000000000000010b13aed220b96c35ccd5f07125b51308db976eefcd718f9\",\n        663358803453687177159928221638562617962497973903752691712\n    ],\n    [\n        \"0000000000000000031b14ece1cfda0e23774e473cd2676834f73155e4f46a2b\",\n        613111582105360026820898034285227810088764320248934432768\n    ],\n    [\n        \"000000000000000010bfa427c8d305d861ab5ee4776d87d6d911f5fb3045c754\",\n        653202279051259096361833571150520065936493508031976308736\n    ],\n    [\n        \"000000000000000005d1e9e192a43a19e2fbd933ffb27df2623187ad5ce10adc\",\n        606439838822957553646521558653356639834299145437709336576\n    ],\n    [\n        \"00000000000000000f9e30784bd647e91f6923263a674c9c5c18084fe79a41f8\",\n        577485176368838834686684127480472050622611986764206702592\n    ],\n    [\n        \"00000000000000000036d3e1c36e4b959a3e4ad6376ce9ae65961e60350c86e8\",\n        568436119447114618883887501211268589217582000336195813376\n    ],\n    [\n        \"00000000000000000b3ec9df7aebc319bb12491ba651337f9b3541e78446eca8\",\n        577075114085443079269506210404847846798089003835028668416\n    ],\n    [\n        \"000000000000000012d24ce222e3c81d4c148f2bce88f752c0dba184c3bc6844\",\n        545227566982404669720599751103563308707559049533419683840\n    ],\n    [\n        \"000000000000000000c4ccbdd98c267bd16bda12b63b648c47af3ac51c1cc574\",\n        566251116039239425785056264238964437451875594947144974336\n    ],\n    [\n        \"00000000000000000056bfec1dca8e82710f411af64b1d3b04a2d2364a81993f\",\n        565860883410058976058672534759150528155363303710710038528\n    ],\n    [\n        \"00000000000000001275d1cadce690546f74f77f6d4a6190e2137a8a819946f6\",\n        552364745922238091561919045022000637317595931246011088896\n    ],\n    [\n        \"000000000000000003816ae80c6413b84cbee2f639ba497ab5872ec9711eb256\",\n        566500670366816952120145379831520408210047884740723212288\n    ],\n    [\n        \"00000000000000000d92953224570f521b09553194da1ca3c4b31a09a238f4f6\",\n        542528489142608155505707877213460200687386787807972294656\n    ],\n    [\n        \"000000000000000006721943f23cfacf20c17c2ad6ea4e902af36b01f92e3c06\",\n        545717322027080804612101478705745866012577831152301113344\n    ],\n    [\n        \"0000000000000000031d9af2fe38cc02410361fb213181fdb667c74e210d54c4\",\n        527827980769521817826567786138322798799309668948178370560\n    ],\n    [\n        \"0000000000000000142e8a13ef6994961655c8e86aece3f0abebd2ee05473e75\",\n        515692606534173891771672037645739723025219384908133171200\n    ],\n    [\n        \"00000000000000000c7a8db37a746d6637ef6a6eab28735608fd715ee2f394e7\",\n        511567664312971151375333957573881285830542480898837708800\n    ],\n    [\n        \"000000000000000007854877c66c71a49af40d20f2d6f817becfe4d66d5e5a81\",\n        496889230460615059653870414954457230681194245244172894208\n    ],\n    [\n        \"000000000000000005ce1d2d10aeb9def4d38233e859d98a4a168ea3fa36687a\",\n        473325989086544548323169648982069700877697035484407005184\n    ],\n    [\n        \"000000000000000007c71decfe74855ad99dc2aa4a2e713165db5a8d6da5f32a\",\n        454358737757395076722955683517864397151243915416267915264\n    ],\n    [\n        \"000000000000000008ce4f34161be6760569877c685e37ebebce3546ea42a767\",\n        443316987659242217350916733941384923365365929826941140992\n    ],\n    [\n        \"0000000000000000086233f4843682eb47bacb58930a5577fbfd5c9ebd57ddf9\",\n        442802913227320896234856097023585967110900073490544590848\n    ],\n    [\n        \"000000000000000010a904eee4fc763c6b88d378884f368fd652f63c1af71580\",\n        433057199397126884276233483897801969646324654385408245760\n    ],\n    [\n        \"00000000000000000c114754749d622d4fa2f78c84d7147c345b2b99a8e83d2e\",\n        409419129139225030716120689261979366152221060879441985536\n    ],\n    [\n        \"000000000000000000a5039e32cc9a89aeffbde1391e8bc9ae9724127904f01d\",\n        370716507988397359530778284103407727265240291588416995328\n    ],\n    [\n        \"000000000000000003b0b73d9b3259c318cca48a6335b5d64545583f7f3773fa\",\n        340818253309165415058055171484606858815006633875327680512\n    ],\n    [\n        \"00000000000000000198bcc5bd65fd0ccd1c7e3b49e0170ea80296cbfee05042\",\n        288495652867775987986282369150900282132304927019642126336\n    ],\n    [\n        \"00000000000000000a60f379d3dc1413491f360809a97cbb02c81442c613dce7\",\n        259524902203633530447121351815377152077137395840706412544\n    ],\n    [\n        \"0000000000000000038973a5f8ba8cdc7e371dcc8f4b24337ef695f24b962907\",\n        237834253647442358407456603145452341381064939329604812800\n    ],\n    [\n        \"000000000000000004b8ec471974913d052a3af7dc2a8c6f01c2ac2f3d1f7b19\",\n        224600391397450328424792273873642383828872941895338164224\n    ],\n    [\n        \"0000000000000000075d572eef1c4210adc7abf4e40986d7f0a80003853bfec4\",\n        187067719845325692996306936867878122094522982476155977728\n    ],\n    [\n        \"0000000000000000074f9edbfc07648dc74392ba8248f0983ffea63431b3bc20\",\n        164898540577033087399552264895286015147022701908103004160\n    ],\n    [\n        \"000000000000000003c4a4d9c62b3a7f4893afe14eef8a6a377229d23ad4b1ea\",\n        170169861298531990750482624090969781281789404909188153344\n    ],\n    [\n        \"00000000000000000404b6939e6c35a5448386e5d58f318c82ce2fefb7d73e47\",\n        162900609378736249874251099581569547607832255884553093120\n    ],\n    [\n        \"0000000000000000034656c96781091b5fbc799c881ea85b41cba0b88128eff7\",\n        161578008857017275969393492955354620126364423170461532160\n    ],\n    [\n        \"0000000000000000045645e2acd740a88d2b3a09369e9f0f80d5376e4b6c5189\",\n        150883090635422687830679296233896712896447026244773478400\n    ],\n    [\n        \"00000000000000000381e6a138308c6547d6fe3eb3437250ffefdebbf71eefd1\",\n        150899178845446426410002882396535253739927398750206558208\n    ],\n    [\n        \"0000000000000000012100ddbb2102e65fb1ebbf104ead754a4110abffc4b8bc\",\n        138784382553152119468195441786396823230753870240366460928\n    ],\n    [\n        \"0000000000000000046f56e59b9b1293b5e7c1587aa6d29c4f3f79b98cf22ee6\",\n        135262935280049154152065372885142255350817451144176992256\n    ],\n    [\n        \"000000000000000001bd1c291e91f4476f93454d4542d2ed7e44fc86902c93bb\",\n        137505556928474480767543871928291413858290772017802117120\n    ],\n    [\n        \"000000000000000001c37a483375ff6fd6ed7c5b79d80167b027a8fdb0721dcd\",\n        128713911367130082233924624261304605948946745676720504832\n    ],\n    [\n        \"0000000000000000051804b4c2da5298c4573386bf1d4242bf0e26a49ec32e42\",\n        126333978716874242627475052620752087219210710628817698816\n    ],\n    [\n        \"0000000000000000034bff7888f1f7294311f0199322f77c1457018c875bd9e1\",\n        126278605342839049377710151409810132688161986656629424128\n    ],\n    [\n        \"00000000000000000506b43c9283ccbc40f583e0c734e4a8af2ce6a4262c6221\",\n        133533639774706835230353390473157702360903922769486413824\n    ],\n    [\n        \"000000000000000003937068e19a0750a33978050f019d2b60f430e3da707db9\",\n        124022888639743237872084547350559836284832548627419234304\n    ],\n    [\n        \"000000000000000002e2f6ec3c9eb965aa706c788da7dede201b6b4b8fae3971\",\n        122123731568103772089607259872577666017242529148853813248\n    ],\n    [\n        \"000000000000000000b3076636b13562bb4315f895bcb324e0c962763c2196b1\",\n        119378259820331825692479928211144812308894309500762193920\n    ],\n    [\n        \"00000000000000000025b8961d1d0cfba33b0205ec10b3ce541618e352b0bbd5\",\n        111759931157462873316041289986819959868258380300102402048\n    ],\n    [\n        \"00000000000000000421d58b78b9f063a4b20e181d55c9c79082f9e4b8b30925\",\n        104283029085035157753191385936387396702868516379761311744\n    ],\n    [\n        \"0000000000000000027fd968d41741f31c73c4a3b304472da0165245278e2ea3\",\n        106299667504289830835845558415962632664710558339861315584\n    ],\n    [\n        \"00000000000000000364a23184b8a2c009d13172094421c22e4d9bc85dcf90a5\",\n        105881374043672627773432318187360570734220873198601240576\n    ],\n    [\n        \"0000000000000000042a2ed4a504424060407825d774a54f2e148fa769ee72ff\",\n        95668727978371040303278646201741713440261619517174579200\n    ],\n    [\n        \"0000000000000000025f769f13f2806fed19d9948b1a7ef19048177789afc5d3\",\n        94012390634764280055243391736606357298689315295029362688\n    ],\n    [\n        \"000000000000000000b3ff31d54e9e83515ee18360c7dc59e30697d083c745ff\",\n        86923102180582917240747796162767475850640519180006195200\n    ],\n    [\n        \"0000000000000000021ecdcb2368ce66c23efd8bd8ab6a88a8bb70571c6e67f0\",\n        84861566431029438820446406485131195674434646972185968640\n    ],\n    [\n        \"000000000000000001972cb33b862b27c1dc3f3a723f7d1cfd69aebe0409126c\",\n        80022382513656536844370512820784980102919810105407963136\n    ],\n    [\n        \"000000000000000000cb26d2b1018d80670ccc41d89c7da92175bd6b00f27a3e\",\n        68605739707508652902977299640495787127103841947617329152\n    ],\n    [\n        \"00000000000000000276deb4022f66cacd929c690cd6b4f7e740836b614b21f4\",\n        63859343606086615291372321518809062931940920926127783936\n    ],\n    [\n        \"000000000000000000587912ced677698c86eec8b1d70144dccb1c6b0bad0f17\",\n        61163258921643354765656928775243357859392914550528409600\n    ],\n    [\n        \"0000000000000000009f989a246ac4221ebdced8ccebae9b8d5c83b69bb5e7c8\",\n        58509826700983959310706392369835644790490546910263246848\n    ],\n    [\n        \"000000000000000000038bed8b89c4e82c13076dd64dc5f7a349c39d3921d607\",\n        56672777602924507578641088682504585686103825941044133888\n    ],\n    [\n        \"00000000000000000122f47d580700a3a5b4b6cb46669a36e4fa974c720ab6cd\",\n        53958359841942568206719748916397287559357255547625668608\n    ],\n    [\n        \"00000000000000000172ad9ea56a90bdfed0f364a902500e9ff4d74f000ced99\",\n        51764751112426770751506128647798102319231116027761786880\n    ],\n    [\n        \"00000000000000000201d7429db233c7055e9699c5bfb57b167ca8d0c710dc71\",\n        51649140486907347007064544362790913467244253139882213376\n    ],\n    [\n        \"000000000000000000c0549b2a8adbefbf6c909f61fdc4d6087c44a549cf8201\",\n        48144529712666433692552181910809237167694270386587828224\n    ],\n    [\n        \"0000000000000000015b6789cdc5dc13766f58b38f16d5b35bf79ce4b040f7fd\",\n        45240046586752885057924289339576851866807485277820420096\n    ],\n    [\n        \"0000000000000000013a31b29f845d97465bff53f901027f8ab4b1a2f59118a8\",\n        39718797393257298660757754408019939605415460564426031104\n    ],\n    [\n        \"00000000000000000088cdeaa7389a7de9f09e3a28b3647630fea3bd1b107134\",\n        37880625861940376795251270290737354395669643839013912576\n    ],\n    [\n        \"000000000000000001389446206ebcd378c32cd00b4920a8a1ba7b540ca7d699\",\n        38043004539854389433075372490391464304285496568268718080\n    ],\n    [\n        \"000000000000000000f41e2b7f056b6edef47477d0d0f5833d5d4a047151f2dc\",\n        33509870757351677175294676059494700127350769223450230784\n    ],\n    [\n        \"0000000000000000010e0373719b7538e713e47d8d7189826dce4264d85a79b8\",\n        31340207270661909233492904963194738468218672502370467840\n    ],\n    [\n        \"00000000000000000053e2d10bd703ad5b7787614965711d6170b69b133aa366\",\n        29201223626342991605750065618903157022235193117232857088\n    ],\n    [\n        \"000000000000000000cbeff0b533f8e1189cf09dfbebf57a8ebe349362811b80\",\n        30353962581764818649842367179120467226026534727449575424\n    ],\n    [\n        \"000000000000000000d0ad638ad61e7c4c3113618b8b26b2044347c00c042278\",\n        29217311836366730185073651781541697865715565622665936896\n    ],\n    [\n        \"000000000000000000a7bda943639876a2d7a8caf4cac45678fb237d59c28ba1\",\n        24433127148609864747615599184820261456796420809345204224\n    ],\n    [\n        \"000000000000000000fb6c6a307c8363e923873499ba6299597769c10a438e61\",\n        23988269434232535193761088780698748366141469438183997440\n    ],\n    [\n        \"0000000000000000006f408147ffbcaa0fb1dcf1f199c527ffdaf159d86e5cd9\",\n        22526487188587264742197108840494583820145762956159746048\n    ],\n    [\n        \"000000000000000000e3be3cf7343d7792c0d47d3c39ddb9ceaf19961e9eeab4\",\n        18556440756915402760741928101946749165024073301499052032\n    ],\n    [\n        \"000000000000000000b3fb09d6def197657e20f9c1d5e9680cfcac1e1f9aa269\",\n        19758940920085072387393228723348383373068660102939017216\n    ],\n    [\n        \"000000000000000000bfe71f044145e1b42fdfb3a523ee2a215e80fa6afc2a98\",\n        20014481558369106100835306608979160026489460596213284864\n    ],\n    [\n        \"000000000000000000cee3bff56ee49c0f96d1cbd17fa17dc6f84b3f48aed765\",\n        16946123176864917983795071264823963343174695083267063808\n    ],\n    [\n        \"00000000000000000089ef13654974b8896b0b0909dd9ae8e350b8a8a7807ce3\",\n        14392961660539521116256653268419249019684881662910398464\n    ],\n    [\n        \"0000000000000000003105a067417c318dab31e25ae1583fa2b27be226945fdd\",\n        13960450711994363030255127593764523087979983609872252928\n    ],\n    [\n        \"000000000000000000720da39f66f29337b9a29223e1ce05fd5ee57bb72a9223\",\n        12101157559014734955774763823279522156034099347349045248\n    ],\n    [\n        \"0000000000000000006a8957cbd52c2038861514f106f7f9f76392d5cb83fd4c\",\n        10356793971791534424976101420669664288187918308140384256\n    ],\n    [\n        \"0000000000000000006b68e55432541794388c94fe9e805652038e7b3cac0681\",\n        9378292318569022964986206758839123913433917663832178688\n    ],\n    [\n        \"00000000000000000001c9deea9f0302eadb1250df1ad53da802dfb40d47face\",\n        8964447668935855171055978546867850348456065181232922624\n    ],\n    [\n        \"00000000000000000013aaa8778111530a626a3fe57e4e6f4a878c92669b04d1\",\n        8192878571041388924351625416816775770172128369752145920\n    ],\n    [\n        \"0000000000000000002f67aa98789b98304a32e54bffbb34c8693eb0acac4c30\",\n        7786052052270684126234611299412205796254663675224260608\n    ],\n    [\n        \"0000000000000000002e5f072398ee27b25b6cdcf69051bcdbbece417093c979\",\n        7678459224733657715202292429397298472913633233275453440\n    ],\n    [\n        \"00000000000000000028d7447c20ade2053bbaf49e8a16eb5fb1bc74335d0d18\",\n        7021961458254440109762706424650140438182306270565892096\n    ],\n    [\n        \"00000000000000000042d89446b9043387be2d4c09aa9e9524176c5754616510\",\n        6702918573828378664524678433037841287557455508299317248\n    ],\n    [\n        \"00000000000000000018ec4d369bab2c13174834a02138decea7c85685d46bd6\",\n        6505870154073602347674948421782035713149324747260035072\n    ],\n    [\n        \"0000000000000000000d4a6c2237c6c46b963b17f60d9c850c4915518deb6678\",\n        6259542822111302646229226565336702507884435252736688128\n    ],\n    [\n        \"00000000000000000031adb986da21237ce06b57ae5390b7f0f890ab8e21b66a\",\n        5456617206587901877414813377199700077413780408546361344\n    ],\n    [\n        \"000000000000000000031df41201cd3789559333cd9529f99834a805014c9b13\",\n        5309609141393698345581459330931267317315649121846034432\n    ],\n    [\n        \"00000000000000000020c68bfc8de14bc9dd2d6cf45161a67e0c6455cf28cfd8\",\n        5026314587016750785722693470327208449351582469580652544\n    ],\n    [\n        \"00000000000000000009dce52e227d46a6bdf38a8c1f2e88c6044893289c2bf0\",\n        5205879062684137510961952799929229129995569309608312832\n    ],\n    [\n        \"0000000000000000002eca92f4e44dcf144115851689ace0ff4ce271792f16fe\",\n        4531442825108320403104334767545311437480985430866264064\n    ],\n    [\n        \"00000000000000000000943de85f4495f053ff55f27d135edc61c27990c2eec5\",\n        4219470685603665866184576203153693664105230070242607104\n    ],\n    [\n        \"0000000000000000001d9d48d93793aaa85b5f6d17c176d4ef905c7e7112b1cf\",\n        4007526641161212986792514236082843733160766044725313536\n    ],\n    [\n        \"0000000000000000001877e616b546d1ba5cf9e8b8edd9eba480a4fbb9f02bce\",\n        3840827764407250199942201944063224491938810378873470976\n    ],\n    [\n        \"00000000000000000025eb2c783f2f29d68ab4260f4b0248450c0038debc7ba4\",\n        3769176185135465353474348091454476000617158630021529600\n    ],\n    [\n        \"0000000000000000000c61b8a7779dcc46e88ca343b9a3fcc6763917fe3b87e2\",\n        3616317728887026217259424694800679959591344645351669760\n    ],\n    [\n        \"00000000000000000003dba9fedba6a0b92b640167eeda0d41485a3c85ac4ac6\",\n        3753318892370425056811838111019504329853891761930240000\n    ],\n    [\n        \"0000000000000000001ac75bed7eb6169255893f99de28f24e3e0e57b6f7db7b\",\n        3752507758961706405692235065937346792777982719368888320\n    ],\n    [\n        \"0000000000000000000e5796e9c5cdc8a8a2de84fd17287d7dfe89074de31766\",\n        4052052750044136275098507698196378011637603685579620352\n    ],\n    [\n        \"00000000000000000015fe695e8d2e5ed3a7de81d3818ef43a444e1ee7b3ace2\",\n        4774638159061819979596346127394133648234752261950013440\n    ],\n    [\n        \"00000000000000000015a08d0a60237487070fe0d956d5fb5fd9d21ad6d7b2d3\",\n        5279534360700703025330663904443631645337169341976674304\n    ],\n    [\n        \"00000000000000000008f4f64baaa9b28d4476f2a000c459df492d5664320b12\",\n        4798269179035823348880781507454323228379569035237392384\n    ],\n    [\n        \"00000000000000000028a69d9498c46b2b073752133e3e9e585965e7dab55065\",\n        4581847093576588582947343450056030606262879232408420352\n    ],\n    [\n        \"00000000000000000014dbca1d9ea7256a3993253c033a50d8b3064a2cbd056b\",\n        4636475101776743072223960781733299832971578678999777280\n    ],\n    [\n        \"00000000000000000019046cf62aa17f6e526636c71c09161c8e730b64d755ae\",\n        4447653474738502407900799312400854215681091162244907008\n    ],\n    [\n        \"00000000000000000017e5c36734296b27065045f181e028c0d91cebb336d50c\",\n        4440088742263677654396177039706714734771352055402463232\n    ],\n    [\n        \"0000000000000000002296c06935b34f3ed946d98781ff471a99101796e8611b\",\n        4442250303185290059812200289574302117357423179633524736\n    ],\n    [\n        \"0000000000000000001ccf7aa37a7f07e4d709eef9c6c4abd0b808686b14c314\",\n        4226119056551884143559484765457720035561644907380604928\n    ],\n    [\n        \"0000000000000000000de3e7a7711130dbac9fb0a14e5ad6ab72d080182f3321\",\n        4217024131862773934699503234743726606330326039165665280\n    ],\n    [\n        \"0000000000000000000e6829c1245de98ce5a35c177a75f67e9c1678cb6e24aa\",\n        4243570847603252455305754966045185171099356397876281344\n    ],\n    [\n        \"00000000000000000001b2505c11119fcf29be733ec379f686518bf1090a522a\",\n        4022508494445492072607020209303018350395259009223360512\n    ],\n    [\n        \"0000000000000000000a4adf6c5192128535d4dcb56cfb5753755f8d392b26bf\",\n        4021030916290150529756716283937142188262386861422411776\n    ],\n    [\n        \"0000000000000000000485ab94f5ea60203aacfc9740b3e42700d7e7012f76d7\",\n        3614033401827878015998272335407144409231622422786998272\n    ],\n    [\n        \"0000000000000000000cbc6dfb3f2afbd6ed1427e30ed1f3167898ac4aa4c673\",\n        3638558860803927897868648370584956354584468626790678528\n    ],\n    [\n        \"0000000000000000001d9865df58f5f300552699fefc09aa840ba25ac044a534\",\n        3397669776434136486181562425402160438435718857259745280\n    ],\n    [\n        \"000000000000000000115eb6c10b7a98bf23a46002baec8fbbbb2cf0583439a6\",\n        2974300520630483197933400799376074857018768662277914624\n    ],\n    [\n        \"000000000000000000113978c5b95531173923ba81ed4d1df3b09db37ae0f0cf\",\n        2990922178751847556822131306978557143801315583089180672\n    ],\n    [\n        \"000000000000000000096b8d24db6471fb5871e9ae8bd1d7384fbee9c80a6052\",\n        2699909434228155498652331786772923585210445951064342528\n    ],\n    [\n        \"00000000000000000016e0dd8fe86bf34feaa611b4c52180b6822b5ad31b68ff\",\n        2647377219375933524160418539145769508351933111739613184\n    ],\n    [\n        \"00000000000000000011e20e47a868d12a2bf3de814ebd067e83514aa2725745\",\n        2502742632840755378666227277045667991877723059489079296\n    ],\n    [\n        \"0000000000000000000c48f6bed594da7bb5e75731b4e78501670e834d426e87\",\n        2267299103571658911252368261549572946260211294613274624\n    ],\n    [\n        \"0000000000000000000f7871dc40f51b1ecd6343a6d9fd614d0e2235a7d9e3fd\",\n        2112846149036891759953684644743283440459952687539027968\n    ],\n    [\n        \"0000000000000000001558c0f33a360d105b52a749103eb2abd4a66a68d52664\",\n        2072520395859657486634608572838975759381606196813234176\n    ],\n    [\n        \"0000000000000000000676463abf3771ea01e0f8c948d1c93658a1d82d95df5a\",\n        1969073848467738847181233556694484530967339635488849920\n    ],\n    [\n        \"0000000000000000000e24396612da4ec125ee6c0b4507e854c5cfed1884cd30\",\n        2119459443945814095658556318611324621123895782295994368\n    ],\n    [\n        \"00000000000000000002fb021eeb13e47021920faf6e5daa3c40bc552c4d248e\",\n        2078088717097888226752964612051624797686495299801972736\n    ],\n    [\n        \"000000000000000000067b904af747b653ba448a79779f7846bf1ea5537b8a4d\",\n        2093644940525638357414324633411056914147713045789409280\n    ],\n    [\n        \"000000000000000000080ae07ccf2f1b6d1d089f5dcbc1fac50a6b93d005f1e0\",\n        2082043540528505650049623783208955059537684253263265792\n    ],\n    [\n        \"00000000000000000008f9ddf24dbec1459689fc399329e9738b2795860e4361\",\n        1953761695813422977307213550702116033770404430236090368\n    ],\n    [\n        \"0000000000000000000aacba541ebb7b56b0831e4ae33faf20ff1e528bb9a657\",\n        1824503568004603261415443256727022530945994444270206976\n    ],\n    [\n        \"00000000000000000010fe23dd08a4b6465c4850984bb538e9dfcb93995a23cc\",\n        1743137387349479903250289511035208906392689711805104128\n    ],\n    [\n        \"0000000000000000001166c174a9d34b0743953e724162fe44388e38d078204c\",\n        1734095076719313606895363312975193263350078457161711616\n    ],\n    [\n        \"00000000000000000006da92c61b6b63ea910be27cab5fd951137105314f2969\",\n        1740794600224838465872409004248364704712181251938713600\n    ],\n    [\n        \"000000000000000000043f26353c41c2343a277ad72f115171fb49d3be52dbbc\",\n        1628687194130096895725758951785196783123433634364653568\n    ],\n    [\n        \"0000000000000000000bc6800858a1b3be08fb26b55d4b989c95e06ad50a350c\",\n        1937788944419033539314165479165359776648584743473905664\n    ],\n    [\n        \"0000000000000000000c799dc0e36302db7fbb471711f140dc308508ef19e343\",\n        1832085838499075985755083973639154607251969422303166464\n    ],\n    [\n        \"0000000000000000000de98650125747f239134cf7e2b7362033e325a8003a14\",\n        1689336589076054705025375464973257095873115523033071616\n    ],\n    [\n        \"0000000000000000001138f586983520b0de3645c0873164f4b214b90cf3aedc\",\n        1674005436900453533413418811078063286996924790657253376\n    ],\n    [\n        \"0000000000000000000e87ecbff47d9ab75e78d92328d5951351f9702597dace\",\n        1780912820169571750977100152906426673601736600243404800\n    ],\n    [\n        \"00000000000000000007c4dac98234149700771e9d1756956660b63cca88c36b\",\n        1963213226902041926479236780515292236058519345991516160\n    ],\n    [\n        \"00000000000000000003030a3de58b57be352e2ca79016cefe19777e02ba0520\",\n        1707948812427463753688699391317898960128433823967870976\n    ],\n    [\n        \"0000000000000000000cfd1300625612513c6cd1413245fcdaf1eeb766e33a93\",\n        1708005810991319658902509335026374895166200405337047040\n    ],\n    [\n        \"0000000000000000000830b0a5ac4b78b5eb99209ebb4790be1fae1428c7f77c\",\n        1554226608711362053849117616927967595838003183165112320\n    ],\n    [\n        \"0000000000000000000ed5cf2e86791b44abce69e178e58613e64ed47e1c02a3\",\n        1600203988720154928752887338080389143353359165034594304\n    ],\n    [\n        \"0000000000000000000aac5c93f7945c60d82828990448cde97d3d7128830a6d\",\n        1590739304116800001454600275103718494518067345886281728\n    ],\n    [\n        \"000000000000000000049a66ca322371799e1cb51d85c8937764ba6a2abb8ed9\",\n        1535456543183121267670627692621392373016562041515671552\n    ],\n    [\n        \"0000000000000000000657c7aa925caa49d18e0c02cab9992be315012d8fab06\",\n        1554222224206450061140363005873469446988944215367483392\n    ],\n    [\n        \"000000000000000000061250f1186194229157967d10a01a2b36ab19d4304da5\",\n        1395807138732878832030429199485686097922398375169228800\n    ],\n    [\n        \"0000000000000000000d2e17e6d3179b4182518bd678f20bbda8b29e5e494d54\",\n        1397005570075490172423356221048513449998516239854469120\n    ],\n    [\n        \"00000000000000000005e2dea23567cb4fe092a354e7d1b50b59571715de22f6\",\n        1348156339349342073285316259199804406349536350538039296\n    ],\n    [\n        \"00000000000000000005e17383e25f65b531d50060b99ed66f673ea251949e4b\",\n        1605902383604108119230963505243149930846997646019657728\n    ],\n    [\n        \"000000000000000000090386439b3e1c7dc56d2e450694e910b366895f05b9ef\",\n        1532070243889425565609149754863988745260019245813596160\n    ],\n    [\n        \"000000000000000000046f183ba323cfceb2d11660376c59fb55e8521c4d32a5\",\n        1407282849589201081744164532792174352192736757496676352\n    ],\n    [\n        \"00000000000000000006d248288fe5c88d55836f0ffcd9acae8333c824106a54\",\n        1443989924712404039437768281050676516514415159246061568\n    ],\n    [\n        \"0000000000000000000a047b7c5b3b06db1bd4b858e757c7214d192cc491e2e9\",\n        1449469094350757594478113895488529861555105250349678592\n    ],\n    [\n        \"0000000000000000000ea5abc8d23ce15f85afbdf574da6c82c67bef5df0d752\",\n        1308244191135472445492091830103155433365752488721907712\n    ],\n    [\n        \"00000000000000000008ad1e1340f31a30d1fee7d0e3e56a0cf7dd571dc653aa\",\n        1294666840924668357381979598007221164113148875397660672\n    ],\n    [\n        \"00000000000000000004f29390852281bae27d3662f648020bb47cced0d883b8\",\n        1257769770588612382309009370720465882998915202417688576\n    ],\n    [\n        \"00000000000000000008aa78bb2eb233395c99d1276ab90a7fe728882b5c2907\",\n        1240994654795328278613867476210548386499304408689410048\n    ],\n    [\n        \"0000000000000000000b842cbf7dfe7345b48deb00b76298f58ae0f58ce821ae\",\n        1256955714176619069383569918268642913356966847991250944\n    ],\n    [\n        \"000000000000000000065952ab35814a9a021f9e5138623779c93c6c56ad6cf4\",\n        1232968087803106959787092839109270560155354027163385856\n    ],\n    [\n        \"00000000000000000000b136fc67072ab98643c2346ff07c4076d94c35d9481c\",\n        1165190949371886336955396954992052935118810155482873856\n    ],\n    [\n        \"00000000000000000005a2db60197fa9012b70f75e4745b362afc16a052d6ee6\",\n        1143226041264440196997713775641159917616401145294487552\n    ],\n    [\n        \"000000000000000000035d5c18a83cb7e0ad0880a3b7936d277becaad9d5a00f\",\n        1308153578033957929511163201643527023818533820904243200\n    ],\n    [\n        \"0000000000000000000d882df633c1ec8ef9ded1dbf09f02114cf9c999d1dde1\",\n        1076379879376199359324913638762382564863378102643851264\n    ],\n    [\n        \"00000000000000000006248c28751a176336f5c070f901dc86df190c391d761d\",\n        1280876111474813957445809627925710317539675495922139136\n    ],\n    [\n        \"00000000000000000001464428893b618817bff3128a6e17a2c043de53ca4673\",\n        1352521844740049480301990665795127943729248621043908608\n    ],\n    [\n        \"00000000000000000009cbc816ab1d430e7a9cc24ffdb6702870112c84a9657c\",\n        1877009475827353279654828838027187714710153476809162752\n    ],\n    [\n        \"0000000000000000000b964e653343c6ae97d73db7f526cbc3187ff7829b0c42\",\n        1971793703014811657512010614168169533666919325951328256\n    ],\n    [\n        \"00000000000000000003c1a7a8f2e45be0b6ba0d2fac227ff2a43cf8b7eaec29\",\n        1859734526474102007161661283304481249417820354151186432\n    ],\n    [\n        \"00000000000000000000019999808926fea81b376f1060e7411bc3a5d2853594\",\n        1733053026051896673114684085689466553557063777258569728\n    ],\n    [\n        \"000000000000000000098d881262beb65d58d46176ae565dee9bb050f7b3d516\",\n        1530484514612921535942898756820491578183692559004467200\n    ],\n    [\n        \"0000000000000000000019cb70a38e25e348efd12435797631ec48088e4abe63\",\n        1463986190114365453164631096931900700789347628299059200\n    ],\n    [\n        \"0000000000000000000515117969caee77eadd03b30d8d50d044269172d844d8\",\n        1419099090327021431837841324664685500406654972106637312\n    ],\n    [\n        \"0000000000000000000e1c751629fb7e8112c37eda91f9f6d8e223cd2ee50c05\",\n        1355224161267474319797749279050820351032592440315871232\n    ],\n    [\n        \"000000000000000000029da63650d127e160033c93393da77302320bd8ee4958\",\n        1342441867947378242875139851503883739742681654295003136\n    ],\n    [\n        \"00000000000000000006c0cd9b33f4d579e31ba1e6bbf4c5297324fa78fbf151\",\n        1244706868954148772026104835685647745369230477348569088\n    ],\n    [\n        \"000000000000000000013712fc242ee6dd28476d0e9c931c75f83e6974c6bccc\",\n        1188998811044006745492934980917001185509005296607952896\n    ],\n    [\n        \"00000000000000000001871f43a65da0527d3455aea40b0af27e0ef31b3d6b98\",\n        1207017664730659447571468211219560238858343288930304000\n    ],\n    [\n        \"0000000000000000000a8c54b4dfa4ba0152a0c4d5bcbf999025cbcbf0efc039\",\n        1114247386799443053935571112778061457902663314832359424\n    ],\n    [\n        \"00000000000000000004680d21d3c5e94dc03d197a3bb5653b1f9d7d5015a2a9\",\n        1110710552837102268873518195482888052995095958078357504\n    ],\n    [\n        \"00000000000000000000135a8473d7d3a3b091c928246c65ce2a396dd2a5ca9a\",\n        1106174051754827146215413957762136710502083943464960000\n    ],\n    [\n        \"00000000000000000002d5d3caf5cd22bd5cc211e4fb7e283c1713dabaa96df6\",\n        1011873581609325297224157601300783981224824207994519552\n    ],\n    [\n        \"000000000000000000001fc83bc1767e7aa5c31bf9dc4aeb505247f6fca1d96d\",\n        1010078857598682948440603476326208385676686722831745024\n    ],\n    [\n        \"00000000000000000004ddb634472f403757e89fefe4590b70a3841e7b307bf6\",\n        963971403944167623177113627223675088972581362965938176\n    ],\n    [\n        \"000000000000000000041e39e07eefe69a84140b1020fe46b329a20ce5b74a77\",\n        978555728783092703397868198169350877225727913812295680\n    ],\n    [\n        \"00000000000000000005f63eed68a9a3979f0db78aec853df62aae1234f43306\",\n        982035564181577583246111171756048347095528689197121536\n    ],\n    [\n        \"000000000000000000076c23a2f567ea68fa523055dac2a0d94579ad277459d8\",\n        943064623022149056932209915691668660376403247938666496\n    ],\n    [\n        \"00000000000000000001a5cc47cfd6abaf57dd33f891342f35bd8b8176f8867d\",\n        955133703543227653230735945040239725552721938878562304\n    ],\n    [\n        \"00000000000000000002c1452eafc55a3a7b2e23b87dc94c53257be3b9fd2d92\",\n        904852201212495269232856372055468724544479235670016000\n    ],\n    [\n        \"00000000000000000005c7686bd1dd938bf1e1849ae17d71efcd868e897a2ae2\",\n        862674725460762741916416231468109512880228678412271616\n    ],\n    [\n        \"00000000000000000008b5ffa0ae1b604dd27bf4af84602ea53f7920320a3c96\",\n        901734818220068453308327912307284892863553131555848192\n    ],\n    [\n        \"000000000000000000050c04aa3e3ca62420b6366e20ebea29ed3042320d2e4b\",\n        890244492347372894565410542152469475763018189902970880\n    ],\n    [\n        \"0000000000000000000882e7306788b58cb5805c04f432203863a8e8c8290902\",\n        911713951399763858433822672345071673321763838959288320\n    ],\n    [\n        \"000000000000000000018b957c6b644c23f532ab7d8fbbeca6fa77ad078a6b29\",\n        924766622522766152396299781586060796970310972500606976\n    ],\n    [\n        \"0000000000000000000528876999d7bdfc25ad64e66ffe7f27a1e371bd9835c5\",\n        973529624652311728262165726029639579921131161797001216\n    ],\n    [\n        \"00000000000000000001095f6deb27964f80c74f38217a32044c20265e0f40e3\",\n        956871428990014096800480126306339386063092842672160768\n    ],\n    [\n        \"0000000000000000000073616e48a83e3e27b70ba27cd38c683d0014189b41b9\",\n        950899733299880027476699870079860653644778702301560832\n    ],\n    [\n        \"000000000000000000016ca393ebd4f388b294134f5633a62d4268b3e4b544dc\",\n        870306687010904716955275873664553942808871958151692288\n    ],\n    [\n        \"000000000000000000028a4784e2bd24e775437108c0e7a90469bff4a62895d2\",\n        841292956506611632223096322365470292302662385308532736\n    ],\n    [\n        \"0000000000000000000136eb131bc275961ef87f4a06f56d849ef7de6067a83c\",\n        859664032087861081904916640712713969859737457373741056\n    ],\n    [\n        \"0000000000000000000009f301f2215237cab791aedc296f102fd7b9dfbff456\",\n        757060771140682373435345150716700036747812369126653952\n    ],\n    [\n        \"00000000000000000007ac57aa98595dd1daa3db89f17a817b445b083d696e0f\",\n        731886405437657570669286679473162061734238931073892352\n    ],\n    [\n        \"00000000000000000006e6f83c247026057e769aace3815f8138941b256a3ad2\",\n        733349368576625804490408567990711061036914519549411328\n    ],\n    [\n        \"00000000000000000001348162a93f4734709f6a142b19aeefd8714f46d0b8f9\",\n        729612308889970685728561745873455525355654300037021696\n    ],\n    [\n        \"0000000000000000000428fc10bebcc825140bc83b01b8be32488bcd408e1389\",\n        787270009984312136754615316208945606764100494789967872\n    ],\n    [\n        \"0000000000000000000152525a810f2033976f89ba434694c0fe5bf97730f625\",\n        762342638057996256581733267702136683580848909336969216\n    ],\n    [\n        \"00000000000000000001fe51e048f4f42e4212098de90827f929403e17b71988\",\n        790751306884434347505776493480475792916920926107336704\n    ],\n    [\n        \"000000000000000000064d0f3321a86b27d4883ab1ccedd12cf0941fb74f01a7\",\n        717191006474295341826748628480199835971598529354268672\n    ],\n    [\n        \"000000000000000000053916f5f3b68319baf095c4205ad4edca517cd89b4a51\",\n        685105199528332699160504931662746558558072186305773568\n    ],\n    [\n        \"00000000000000000006813d0260b4726b64e83025e904165ffaaf06d17227d7\",\n        688509036841676372057001313638142781710850853198364672\n    ],\n    [\n        \"00000000000000000005c39a2670a916dc4075afaaf8fe30e495c13ba323e141\",\n        626181838016062686207286970262124176054603953970610176\n    ],\n    [\n        \"00000000000000000005e07bf1518ede202f3a18b1a710e9700046755b3c7550\",\n        619023402996415923713925321951479821824329196375113728\n    ],\n    [\n        \"00000000000000000005776a4242de5ebfc59d247a114188851e3d24fd173f61\",\n        575524729764536260159429050275345090310309676098519040\n    ],\n    [\n        \"00000000000000000000eb00afd3cdc013b3033d0419037fa9c6f4243b5a7a79\",\n        562973353703138465897895804931977651737504527419441152\n    ],\n    [\n        \"0000000000000000000060df647feb8b98f0b5e7ce312c35101bd5c6de001fcc\",\n        553442901526103647968289576137834770166328191306694656\n    ],\n    [\n        \"000000000000000000044cb8431ecd498363f5ec16e05553b7f1f69b1a3a6a93\",\n        561592234655860762640193322765060764283929671166328832\n    ],\n    [\n        \"00000000000000000001c951f76bd2d92beda3c1ab615d57558ecf654c900042\",\n        544090752548823200194704196893283275123549878964191232\n    ],\n    [\n        \"000000000000000000036eb2f0d13b6e4fdb17da672479713a9e08d0f0875dbf\",\n        526200511006255617572972890856003254679941608705622016\n    ],\n    [\n        \"000000000000000000033c59708025b67a8b8518d3760151eb4980e7a3a21e62\",\n        514982024438103606772841406080073066221062670505738240\n    ],\n    [\n        \"0000000000000000000062afa9e3b9df5eee17c2edfc89f60778afe3abccb593\",\n        532311049351936122673982497141590033985123062667804672\n    ],\n    [\n        \"00000000000000000003a3bbc3dac0c578948918c796426cda4c1958fc8454d3\",\n        500073246235691066104245617101534263137552502634840064\n    ],\n    [\n        \"00000000000000000000dc8c2caadf5e8e8635adc7ff4d90dcfba264a3493e6a\",\n        515199788182065911307653755120147792390991404454641664\n    ],\n    [\n        \"000000000000000000034e1a8f7c1efee7c36209a1556a377568d6368431dd17\",\n        514581572989474939373253596435908804673676944988962816\n    ],\n    [\n        \"00000000000000000000967ad630a7aac2aeed84cc4c10ef1bce932cefdb374e\",\n        484696787509332636501824648976526249487752436350189568\n    ],\n    [\n        \"00000000000000000002f985a10a40897d04888ac10c8ac75867a50b016eb6ef\",\n        497866378763321402697758053004132675777872044494946304\n    ],\n    [\n        \"000000000000000000027ecc78c2da1cc5c0b0496706baa7e4d7c80812c10bf3\",\n        471981723264553781113452590931894587216745823226298368\n    ],\n    [\n        \"000000000000000000047381dac259c4a8ce569c8498a2b9d11db6c080500343\",\n        470321457404545875398373204961928889706416683857477632\n    ],\n    [\n        \"00000000000000000001df9394219ced52cb3789dc1c31408d6e01bec37c0b2a\",\n        441737408381628076124145537003663826407985955181953024\n    ],\n    [\n        \"0000000000000000000353813d30e99afcc4579d469b57718f4521d570b3dce6\",\n        431604817530012926192239390058441836232711374861500416\n    ],\n    [\n        \"000000000000000000009af85a7f58a31a21d77ad6dbeb8f52a11de6ec716b79\",\n        416823189970048174077527321660349349771911273121841152\n    ],\n    [\n        \"00000000000000000000cc85f351633ed7b58e8b827ad0176798c4905084e95c\",\n        396710004437100288117208210992507862855406329465405440\n    ],\n    [\n        \"0000000000000000000125ab365abaa653d41dd6acb9ecddf9bb9a944d1e0bfd\",\n        400552292241643231889165698417718970914081776120889344\n    ],\n    [\n        \"0000000000000000000078f4e50a09391670d061deea0b3b87c3fa60cfaaf782\",\n        374406027949793378682501776760424667692437142927048704\n    ],\n    [\n        \"00000000000000000001595b16989798f8997d89ac778ccc7d0d6c7e88ab4ec9\",\n        368311566122123513513592411007997767500471904222838784\n    ],\n    [\n        \"000000000000000000026ab1b5e445f9320cf2ec3dc6720b501877d4e0b40eff\",\n        383255420363831995852225088422521761376453814474768384\n    ],\n    [\n        \"0000000000000000000268358721568565da476fdf07250d18ee210bc2f874c0\",\n        357069695527774208266769667274744118513278471102267392\n    ],\n    [\n        \"000000000000000000005b3388af9acbfd8c00b5b3ec888a75085011cc69cbb2\",\n        329879919066870090376508314646890389215599502072741888\n    ],\n    [\n        \"000000000000000000010e2bc83838d1b9479b88341e4d0033dff524d16d5ef5\",\n        339749439623765677783137798322223448447336014535458816\n    ],\n    [\n        \"0000000000000000000350156217f3a450f8994e7054b631db2f0e07f43f268d\",\n        321145985282180614537323094086577881890135649195917312\n    ],\n    [\n        \"00000000000000000000c1d14709a7659153a2299083637a01475b7865926b0b\",\n        324317443835188673869825090173572216042789022814175232\n    ],\n    [\n        \"000000000000000000023aeab989430385cf0c085e9e25580d1813ea3daee028\",\n        312072983117630369221114618645075196904111619969122304\n    ],\n    [\n        \"00000000000000000002bf1e60049e942ac34b728911adda77d704cc8401e84b\",\n        305996059309608474887223697110640892108382252455428096\n    ],\n    [\n        \"000000000000000000021342b77cc83903ed85341a53b9ec571fb7b0a503124c\",\n        324234138241860812403487480138107387910668634659225600\n    ],\n    [\n        \"00000000000000000000a2a87e6a371a8d2e63eb61a051333c7a3251b717c723\",\n        319495949933634025142671133910441198360944101354897408\n    ],\n    [\n        \"0000000000000000000132f5df736574a143d1e41e1a0cdb9c7d1656a906124c\",\n        322033116776040472608672730780036665683066800249503744\n    ],\n    [\n        \"0000000000000000000112beeccb1e3ba4e55ee2987685cc397cdd21e4c65fe2\",\n        322192420454509541026756932426802740532209296896688128\n    ],\n    [\n        \"000000000000000000016e792fc3bd650030e9acd074a910c7905bbe9bb79899\",\n        339134147434449367654574047007649893296060866934865920\n    ],\n    [\n        \"00000000000000000001a481e800d3a60641bfc442db64a2c2d9d11d3a298705\",\n        328583567114557579488061646200271046177164689907122176\n    ],\n    [\n        \"00000000000000000001b39765b103785831b8f5a23d9fb42187226d1faf82f3\",\n        297348354121521522320212493955458645481078099598639104\n    ],\n    [\n        \"0000000000000000000314bc1981218b70bf539ae13ac1e41eefc1ad7a605049\",\n        310338180674118587457206844748640968959780028040609792\n    ],\n    [\n        \"0000000000000000000073036581ef712215c5f9aebfeb7d2fba84a2f71dd69f\",\n        301319254070149585548971905645948786445483268317904896\n    ],\n    [\n        \"000000000000000000028030eb24a8d0bd042bd589839edc932c876dda457c9c\",\n        290914823913990887674279873321841567628552684544458752\n    ],\n    [\n        \"00000000000000000000a98d2c6a1875258ba9611e24b421c88325ec5155e2c8\",\n        304956931645466202912380877194579614881406884417372160\n    ],\n    [\n        \"00000000000000000001924bab37e9d87715e84aa7bcd0b52405f893dfe7005f\",\n        292880543616200952099263829421844968289989913814761472\n    ],\n    [\n        \"000000000000000000007c7decd9c85fe0c5273691cf361b548855d91176fdab\",\n        281789207690496729853016065226361096453821041746116608\n    ],\n    [\n        \"00000000000000000001b4ceba765aa1ecec00c4e3710b7d5115a90060c48cf6\",\n        265227471136262937983931908702020177275080014169112576\n    ],\n    [\n        \"0000000000000000000026f3661ace0cda87b922d13d35280c9ac61c6ad364cf\",\n        263561359269705708657179707992723614632672251070119936\n    ],\n    [\n        \"00000000000000000001a2a4d658523967c398ae9c4adda72c02f4072ec398f7\",\n        259426771137696584301581483600969249970065617906040832\n    ],\n    [\n        \"0000000000000000000087427d61da6ce8b57e2726a9c62be8637cc906bad7e5\",\n        248423125310232216230425940495448355115076101789974528\n    ],\n    [\n        \"000000000000000000002428ab473fa8570115d4a000e637814b86248d35d370\",\n        245573197117436955539928755071651603226747033331171328\n    ],\n    [\n        \"000000000000000000006d0b2fecfc61125fe5a7c6387fdca048c4b3cd5ffa84\",\n        244083926948996765466279200227113710829717638069878784\n    ],\n    [\n        \"00000000000000000001651e40ff05e359a13e1740d9c21f400abdc5f1b287c9\",\n        249381870384321288544767557745710236775970393538166784\n    ],\n    [\n        \"0000000000000000000268d24974a0f3d8ec73bfc6b0a470c6bc891951b1e3b2\",\n        236140665550103308105842173161300712617887644698804224\n    ],\n    [\n        \"00000000000000000001a3361e40ce264e71620908f8e8cd555b4182f7813bb7\",\n        243826702660826526552675351696555645018258193942315008\n    ],\n    [\n        \"000000000000000000009e2b057f15897a5bbe33a9f5a4ca96d426368554f34e\",\n        240389250809824242889060284970006947356027440601235456\n    ],\n    [\n        \"000000000000000000023d9b58d8793462a5d4fd778b8fe1ba9d8cd8f26816eb\",\n        236991259503029893604236717733941589335327397438816256\n    ],\n    [\n        \"00000000000000000002065cd1b5c268e8e375d1a176da03d22479d0c02e2da6\",\n        221874948068116364721256005509157074063026087146815488\n    ],\n    [\n        \"00000000000000000000ca2f64794d814b198eb237974573222e4521545582d5\",\n        218766334085513534214236767869969540080217918627905536\n    ],\n    [\n        \"000000000000000000008765cffc1a859242108b85e6415ae144ec918a05787e\",\n        226329605058700956815940836879276304706937369537806336\n    ],\n    [\n        \"000000000000000000006ebc27176d134d4885a125cd7e3a13783a6c9bcd12c9\",\n        221600185760298154972633712760606412855330771828736000\n    ],\n    [\n        \"00000000000000000000a4d531b0328083c2f19a7f6c31e22e18cb73ff7a4d2c\",\n        212309419851785605121612888279029001699378008653037568\n    ],\n    [\n        \"0000000000000000000108312d8ba9c66bf9c806b9c7bf2e923d282508941005\",\n        213268164925874677435954505529290883360272300401229824\n    ],\n    [\n        \"000000000000000000009d9d77c8ae464658da8ad3ba1b2d07eca1937b9cef39\",\n        230505115236555346453248764446346725294094368813088768\n    ],\n    [\n        \"000000000000000000011a2a7498613bba536d6f0f5649aca4e6a484d6312211\",\n        213504928191122283708703502472190921209456561473191936\n    ],\n    [\n        \"00000000000000000000a4b1cede1eba74d67df322ca415468f277aa7b508f47\",\n        211248369663083369602997013090476980227107801626836992\n    ],\n    [\n        \"000000000000000000016e02290e770058422eef8df58d85c037a2d21ca9afb4\",\n        208285905844213629387798143934561074546265226362224640\n    ],\n    [\n        \"000000000000000000010ee7b1a4f44b8cf580f3fe692f362c43741f10b81c8b\",\n        207862070369387667541519075333073352470565005924761600\n    ],\n    [\n        \"00000000000000000000b34c3fbb0151989a03d8fe589af6a5621528e6786c29\",\n        198173776015521112096746848576997112333265829097373696\n    ],\n    [\n        \"000000000000000000019f01622aa9ccac55b505788f3310454eb40145b7d73a\",\n        189398920184986370975851924841368549083251610109345792\n    ],\n    [\n        \"000000000000000000010919a63dfd4bb86037ad9f2c26cb8e4f3b18d345cb4a\",\n        178729958232470779672965025562539683039763302545620992\n    ],\n    [\n        \"00000000000000000001d4b4cbfc67c0904765c9b6d6dd00ac3a72fd4efaac90\",\n        183753139359977093002831090332585547778320742695829504\n    ],\n    [\n        \"0000000000000000000095fdebd0bf892913da1bf6f109a2fbddd7b4d13bb8fb\",\n        172847414142213895427195194110856643885648174060142592\n    ],\n    [\n        \"000000000000000000014b809628248ecd176df547224411fedafd577041929d\",\n        177049231349540241317030788004915957567158980121198592\n    ],\n    [\n        \"00000000000000000001c83578eeac30ea13eacc3668da7e37c8aed04992ec95\",\n        180571450295507717349901668451762199644529777549770752\n    ],\n    [\n        \"0000000000000000000144a6109dda22b210490144247f34903e23ea98281065\",\n        181918954805126809840485465867526612588652547354394624\n    ],\n    [\n        \"00000000000000000001d39c254559a52d78a36cb5373269db169857d9aa490e\",\n        181841495218348271985820670571392649588610782929616896\n    ],\n    [\n        \"0000000000000000000159186ff01cb830839027afce1c072816ad1caf6735c2\",\n        184058593202179251712735660462623250929428832597311488\n    ],\n    [\n        \"00000000000000000000fd7f37b8fb43c92b11c9d2809f115d22a6b5dbeb7836\",\n        190300666695219538076383598383154495706379320488361984\n    ],\n    [\n        \"00000000000000000001209d8bd5b083d0eec0a0cfb72a9f922a5f46e3b4744f\",\n        214194756963942469886095641713233006794734161633476608\n    ]\n]"
  },
  {
    "path": "electrum/chains/mainnet/fallback_lnnodes.json",
    "content": "{\n  \"0294ac3e099def03c12a37e30fe5364b1223fd60069869142ef96580c8439c2e0a\": {\n    \"host\": \"8.210.134.135\",\n    \"port\": 26658\n  },\n  \"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f\": {\n    \"host\": \"3.33.236.230\",\n    \"port\": 9735\n  },\n  \"03cde60a6323f7122d5178255766e38114b4722ede08f7c9e0c5df9b912cc201d6\": {\n    \"host\": \"34.65.85.39\",\n    \"port\": 9745\n  },\n  \"027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190\": {\n    \"host\": \"3.230.33.224\",\n    \"port\": 9735\n  },\n  \"033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025\": {\n    \"host\": \"34.65.85.39\",\n    \"port\": 9735\n  },\n  \"02f1a8c87607f415c8f22c00593002775941dea48869ce23096af27b0cfdcc0b69\": {\n    \"host\": \"52.13.118.208\",\n    \"port\": 9735\n  },\n  \"034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5\": {\n    \"host\": \"213.174.156.79\",\n    \"port\": 9735\n  },\n  \"033e9ce4e8f0e68f7db49ffb6b9eecc10605f3f3fcb3c630545887749ab515b9c7\": {\n    \"host\": \"213.174.156.72\",\n    \"port\": 9735\n  },\n  \"035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226\": {\n    \"host\": \"170.75.163.209\",\n    \"port\": 9735\n  },\n  \"037659a0ac8eb3b8d0a720114efc861d3a940382dcfa1403746b4f8f6b2e8810ba\": {\n    \"host\": \"34.78.139.195\",\n    \"port\": 9735\n  },\n  \"03a93b87bf9f052b8e862d51ebbac4ce5e97b5f4137563cd5128548d7f5978dda9\": {\n    \"host\": \"134.209.139.244\",\n    \"port\": 9735\n  },\n  \"0288be11d147e1525f7f234f304b094d6627d2c70f3313d7ba3696887b261c4447\": {\n    \"host\": \"18.219.93.203\",\n    \"port\": 9735\n  },\n  \"0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e\": {\n    \"host\": \"164.92.106.32\",\n    \"port\": 9735\n  },\n  \"02c197ffa4c2aa4105dd4c4b7279ba1b9061b22910ebbfa759b0001bed9ee48a16\": {\n    \"host\": \"18.181.210.139\",\n    \"port\": 9735\n  },\n  \"03c8e5f583585cac1de2b7503a6ccd3c12ba477cfd139cd4905be504c2f48e86bd\": {\n    \"host\": \"34.73.189.183\",\n    \"port\": 9735\n  },\n  \"021c97a90a411ff2b10dc2a8e32de2f29d2fa49d41bfbb52bd416e460db0747d0d\": {\n    \"host\": \"54.184.88.251\",\n    \"port\": 9735\n  },\n  \"037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590\": {\n    \"host\": \"185.5.53.91\",\n    \"port\": 9735\n  },\n  \"0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3\": {\n    \"host\": \"57.129.59.146\",\n    \"port\": 9735\n  },\n  \"02e4971e61a3f55718ae31e2eed19aaf2e32caf3eb5ef5ff03e01aa3ada8907e78\": {\n    \"host\": \"52.38.27.190\",\n    \"port\": 9735\n  },\n  \"0391904d140fdf88d19423513945a5fcc49c606521b65a85f6d6fe46ebdd1c7665\": {\n    \"host\": \"5.75.184.195\",\n    \"port\": 35933\n  },\n  \"026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2\": {\n    \"host\": \"45.86.229.190\",\n    \"port\": 9735\n  },\n  \"027ce055380348d7812d2ae7745701c9f93e70c1adeb2657f053f91df4f2843c71\": {\n    \"host\": \"157.90.112.145\",\n    \"port\": 9735\n  },\n  \"029efe15ef5f0fcc2fdd6b910405e78056b28c9b64e1feff5f13b8dce307e67cad\": {\n    \"host\": \"103.126.161.206\",\n    \"port\": 9742\n  },\n  \"03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e\": {\n    \"host\": \"3.132.230.42\",\n    \"port\": 9735\n  },\n  \"03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c\": {\n    \"host\": \"63.35.146.37\",\n    \"port\": 9735\n  },\n  \"03037dc08e9ac63b82581f79b662a4d0ceca8a8ca162b1af3551595b8f2d97b70a\": {\n    \"host\": \"34.68.41.206\",\n    \"port\": 9735\n  }\n}"
  },
  {
    "path": "electrum/chains/mainnet/servers.json",
    "content": "{\n    \"5.9.83.108\": {\n      \"pruning\": \"-\",\n      \"s\": \"50002\",\n      \"version\": \"1.6.0\"\n    },\n    \"104.248.139.211\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"128.0.190.26\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"142.93.6.38\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"157.245.172.236\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"159.65.53.177\": {\n        \"pruning\": \"-\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"167.172.42.31\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"188.230.155.0\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"22mgr2fndslabzvx4sj7ialugn2jv3cfqjb3dnj67a6vnrkp7g4l37ad.onion\": {\n        \"pruning\": \"-\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"2AZZARITA.hopto.org\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"2electrumx.hopto.me\": {\n        \"pruning\": \"-\",\n        \"s\": \"56022\",\n        \"t\": \"56021\",\n        \"version\": \"1.4.2\"\n    },\n    \"2ex.digitaleveryware.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"37.205.9.165\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"68.183.188.105\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"73.92.198.54\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"89.248.168.53\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"E-X.not.fyi\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4\"\n    },\n    \"VPS.hsmiths.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4\"\n    },\n    \"alviss.coinjoined.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"assuredly.not.fyi\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"bejqtnc64qttdempkczylydg7l3ordwugbdar7yqbndck53ukx7wnwad.onion\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.5\"\n    },\n    \"bitcoin.aranguren.org\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"bitcoin.lu.ke\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"bitcoins.sk\": {\n        \"pruning\": \"-\",\n        \"s\": \"56002\",\n        \"t\": \"56001\",\n        \"version\": \"1.4.2\"\n    },\n    \"blackie.c3-soft.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"57002\",\n        \"t\": \"57001\",\n        \"version\": \"1.4.5\"\n    },\n    \"blkhub.net\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"blockstream.info\": {\n        \"pruning\": \"-\",\n        \"s\": \"700\",\n        \"t\": \"110\",\n        \"version\": \"1.4\"\n    },\n    \"btc.electroncash.dk\": {\n        \"pruning\": \"-\",\n        \"s\": \"60002\",\n        \"t\": \"60001\",\n        \"version\": \"1.4.5\"\n    },\n    \"btc.litepay.ch\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"btc.ocf.sh\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"btce.iiiiiii.biz\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"de.poiuty.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50004\",\n        \"version\": \"1.4.5\"\n    },\n    \"e.keff.org\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4\"\n    },\n    \"e2.keff.org\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4\"\n    },\n    \"eai.coincited.net\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"ecdsa.net\": {\n        \"pruning\": \"-\",\n        \"s\": \"110\",\n        \"t\": \"50001\",\n        \"version\": \"1.4\"\n    },\n    \"egyh5mutxwcvwhlvjubf6wytwoq5xxvfb2522ocx77puc6ihmffrh6id.onion\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"electrum.bitaroo.net\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"electrum.blockstream.info\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4\"\n    },\n    \"electrum.dcn.io\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"electrum.diynodes.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50022\",\n        \"version\": \"1.4\"\n    },\n    \"electrum.emzy.de\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"electrum.hodlister.co\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4\"\n    },\n    \"electrum.hsmiths.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4\"\n    },\n    \"electrum.jhoenicke.de\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.6\"\n    },\n    \"electrum.pabu.io\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"electrum.qtornado.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4\"\n    },\n    \"electrum3.hodlister.co\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4\"\n    },\n    \"electrum5.hodlister.co\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4\"\n    },\n    \"electrumx.alexridevski.net\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"electrumx.erbium.eu\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4\"\n    },\n    \"electrumx.schulzemic.net\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"elx.bitske.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"ex.btcmp.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"ex03.axalgo.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion\": {\n        \"pruning\": \"-\",\n        \"t\": \"110\",\n        \"version\": \"1.4\"\n    },\n    \"exs.dyshek.org\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"fortress.qtornado.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"443\",\n        \"version\": \"1.5\"\n    },\n    \"fulcrum.grey.pw\": {\n        \"pruning\": \"-\",\n        \"s\": \"51002\",\n        \"t\": \"51001\",\n        \"version\": \"1.4.5\"\n    },\n    \"fulcrum.sethforprivacy.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4\"\n    },\n    \"gall.pro\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"guichet.centure.cc\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"hodlers.beer\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"horsey.cryptocowboys.net\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"kareoke.qoppa.org\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"kittycp2gatrqhlwpmbczk5rblw62enrpo2rzwtkfrrr27hq435d4vid.onion\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"node.degga.net\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"node1.btccuracao.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"nuzzg3pku3xbctgamzq3pf7ztakkiidnmmier64arqwh3ajdddovatad.onion\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"qly7g5n5t3f3h23xvbp44vs6vpmayurno4basuu5rcvrupli7y2jmgid.onion\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"rzspa374ob3hlyjptkdgz6a62wim2mpanuw6m3shlwn2cxg2smy3p7yd.onion\": {\n        \"pruning\": \"-\",\n        \"s\": \"50004\",\n        \"t\": \"50003\",\n        \"version\": \"1.4.2\"\n    },\n    \"skbxmit.coinjoined.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"smmalis37.ddns.net\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"stavver.dyshek.org\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"tardis.bauerj.eu\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4\"\n    },\n    \"ty6cgwaf2pbc244gijtmpfvte3wwfp32wgz57eltjkgtsel2q7jufjyd.onion\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"udfpzbte2hommnvag5f3qlouqkhvp3xybhlus2yvfeqdwlhjroe4bbyd.onion\": {\n        \"pruning\": \"-\",\n        \"s\": \"60002\",\n        \"t\": \"60001\",\n        \"version\": \"1.4.5\"\n    },\n    \"v7gtzf7nua6hdmb2wtqaqioqmesdb4xrlly4zwr7bvayxv2bpg665pqd.onion\": {\n        \"pruning\": \"-\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"v7o2hkemnt677k3jxcbosmjjxw3p5khjyu7jwv7orfy6rwtkizbshwqd.onion\": {\n        \"pruning\": \"-\",\n        \"t\": \"57001\",\n        \"version\": \"1.4.5\"\n    },\n    \"venmrle3xuwkgkd42wg7f735l6cghst3sdfa3w3ryib2rochfhld6lid.onion\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"vmd71287.contaboserver.net\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"vmd84592.contaboserver.net\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"version\": \"1.4.2\"\n    },\n    \"wsw6tua3xl24gsmi264zaep6seppjyrkyucpsmuxnjzyt3f3j6swshad.onion\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    },\n    \"xtrum.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4.2\"\n    }\n}\n"
  },
  {
    "path": "electrum/chains/mutinynet/fallback_lnnodes.json",
    "content": "{\n  \"02465ed5be53d04fde66c9418ff14a5f2267723810176c9212b722e542dc1afb1b\": {\n    \"host\": \"45.79.52.207\",\n    \"port\": 9735\n  },\n  \"032ae843e4d7d177f151d021ac8044b0636ec72b1ce3ffcde5c04748db2517ab03\": {\n    \"host\": \"45.79.201.241\",\n    \"port\": 9735\n  },\n  \"0220566172d9e324b41ec6f74ca44d377d3faf72ddb310fd263e6d5bcde4882492\": {\n    \"host\": \"185.90.61.24\",\n    \"port\": 9735\n  },\n  \"035a4e767bb1be29ed20219b40f07d9be03656a5f83485821878963c05290a877c\": {\n    \"host\": \"54.158.203.78\",\n    \"port\": 9746\n  }\n}"
  },
  {
    "path": "electrum/chains/mutinynet/servers.json",
    "content": "{\n    \"5.9.83.108\": {\n        \"pruning\": \"-\",\n        \"s\": \"51234\",\n        \"version\": \"1.4\"\n    }\n}"
  },
  {
    "path": "electrum/chains/regtest/servers.json",
    "content": "{\n    \"127.0.0.1\": {\n        \"pruning\": \"-\",\n        \"s\": \"51002\",\n        \"t\": \"51001\",\n        \"version\": \"1.4\"\n    }\n}\n"
  },
  {
    "path": "electrum/chains/signet/checkpoints.json",
    "content": "[\n    [\n        \"000000c0a3841a6ae64c45864ae25314b40fd522bfb299a4b6bd5ef288cae74d\",\n        0\n    ],\n    [\n        \"000001299957ea59afccbd3f1c4719a466350aea3fda78d419dfde37f9823420\",\n        0\n    ],\n    [\n        \"000000647c13ba87cb352cbbb464d47f15f1733332c3b910888ff36d858961ce\",\n        0\n    ],\n    [\n        \"00000096b144d9ab24117213adf6e9e52afb606629f30f10d084ebb4bb80e3c3\",\n        0\n    ],\n    [\n        \"0000000d41b251c50f6e7e54855b68f4a57004164b9d6014f84c111cd60c2680\",\n        0\n    ],\n    [\n        \"00000102ea9911f35f8ee910b2bf56946b8163766b8d71b6aae668b2bd1f1501\",\n        0\n    ],\n    [\n        \"00000108f2a56ca7ccdb6bef0372eead4ba9eab39b2795be544fc8e90b93c01c\",\n        0\n    ],\n    [\n        \"0000014938ef851ea035d13c02ccdca9e8c3499d33466daab04a1a0a798bbbfc\",\n        0\n    ],\n    [\n        \"000000739a899ef6a642dc478966abc174a789a88280d83579cc057b95d19aef\",\n        0\n    ],\n    [\n        \"000000d5237e345c02c467e9d4eda4a207a35af96473daca024c6cd363c7db83\",\n        0\n    ],\n    [\n        \"00000000bbf300f7561f228cf55f51237beb76f23a4370363ea41e930ebf63a1\",\n        0\n    ],\n    [\n        \"0000007dcebd40572e399d89055df55c19ea5a67d2a29155e0dd2f35313aeec0\",\n        0\n    ],\n    [\n        \"0000013d4b88ae84d4b29f64e2ae2466dd0130dfb2f2ce8d9496a469a543fe55\",\n        0\n    ],\n    [\n        \"0000014ce59f8d976fe17e7cc8d8e5698890995b98073c78bd7c493ba72a2e62\",\n        0\n    ],\n    [\n        \"000000933087962ab5518c60cc4ca4bdbfeb6dbc7fd3d3d420bc8ef46dc53202\",\n        0\n    ],\n    [\n        \"000000b622bcdf74d742db0b4d9653034d2549a4e9642962091cad05b1082645\",\n        0\n    ],\n    [\n        \"000000dfd2d4606684bbeb504959ce225c2ad84d0343c35cdad78d59dc86395c\",\n        0\n    ],\n    [\n        \"00000120bd3e4e26b6b1418860ead049560f7beec69a4a496cbc780e3f34d8bb\",\n        0\n    ],\n    [\n        \"000000db40817d098b504b8a524b2df500b6f6c9b9abd954c4116dda7620e0dc\",\n        0\n    ],\n    [\n        \"000000dbc1b9e4c662ac78c95fc46e689b54d0c2c284dde285dd54dd9b24b123\",\n        0\n    ],\n    [\n        \"00000041d0bd45cf26d854ff7188e773b87466fd33bfdbf7397ecdade8724318\",\n        0\n    ],\n    [\n        \"0000002327e4edf0f436fbb87392a68c2bfeb6284d215704a017e741fdc1154a\",\n        0\n    ],\n    [\n        \"000000f198a4c393044bd9b3c4b340bebe811ce721b5d6788f931282f5eac3fd\",\n        0\n    ],\n    [\n        \"00000106876fcfbb809096bf96df6dfe1de43e408bee2f88500954d38bf3cb72\",\n        0\n    ],\n    [\n        \"0000003e477f554425d1b570dc23535f5365e0424a1b4a4b81dc373db1421430\",\n        0\n    ],\n    [\n        \"0000002cd0191c4d8cc646fe277a6d9be51d5d5e116ed75491291bf18ab7fe68\",\n        0\n    ],\n    [\n        \"000000940edf438dc29af2fe8e77d831eb3e2434f372d68598da354310289d4a\",\n        0\n    ],\n    [\n        \"00000103744843bf10a3f0ddd264fae8c7199fadd7187883975c3dc9976b6a61\",\n        0\n    ],\n    [\n        \"00000141abbf269a681f83cf6b243e505d5e29f483f18f366f3cb9419bace51d\",\n        0\n    ],\n    [\n        \"000000d0a1eb4b089678e0ebd64f211e0b9cb68f2f1fd1b56ab1d5eca03d167d\",\n        0\n    ],\n    [\n        \"0000005a587db11708cfd476c5a8308e9ddb210dbda90c64ccae2092fe218367\",\n        0\n    ],\n    [\n        \"0000011dd3d96c272e17eb43ddf33651238871aa525c9b7885e88ee4c5898337\",\n        0\n    ],\n    [\n        \"000000055aee09f4d9a965638fea3e45130a3f601363d0614e618481bad3b519\",\n        0\n    ],\n    [\n        \"00000038bcef9e43f8a75438a925403aeb1b77ed9013c1c719b57c77e8137d2e\",\n        0\n    ],\n    [\n        \"0000003ce9a4b46fbb431c198aedf59ef322c0791f03e3bf153548fea29dc375\",\n        0\n    ],\n    [\n        \"000000715d041a78f798c6ef7cce244a0a7751acc839446bfe52d84087696162\",\n        0\n    ],\n    [\n        \"0000005bcfde62a40e676a73f6fd481b4d5d524891b7b5a7e5874da0af640885\",\n        0\n    ],\n    [\n        \"0000003a0ebac1a5bf4ec8e7d9f94d672f9ce217eecbcfe1837b0b4d68bb7efb\",\n        0\n    ],\n    [\n        \"00000002b09a09076aaa8cdeddf164c14bd69c0d3c7c700cc2a1e70d5782fba3\",\n        0\n    ],\n    [\n        \"0000010e5f6260be27d95a9c6b77b3427f675ac6a79ffa848d2065122e0a49fb\",\n        0\n    ],\n    [\n        \"00000027c6cadacbf945fde93ca6fa9b2c404216b7b54d46d12acd6bd2084403\",\n        0\n    ],\n    [\n        \"0000011d5a1230c91fcb07e1ffed7c74a77824f49632d7a85984afc5fdb84210\",\n        0\n    ],\n    [\n        \"000001420feee79c0518790bae4fcf3d11379f46b7c92568d5d49ce061c14aaf\",\n        0\n    ],\n    [\n        \"0000012beab6aa1841d9ca386986bf4a1227e4a1c9c4f6bada49dfc5c45a7b4c\",\n        0\n    ],\n    [\n        \"000001425fa8c62dfd856ae0fee3b36add930a5826778f62c54c5e7a089cb2cd\",\n        0\n    ],\n    [\n        \"00000047aa330a7bfd668afa6fceec2d71b0a01d4c3e940f01fea16cf8a5495a\",\n        0\n    ],\n    [\n        \"000001138c3f76d7b699e3f610946b35ab8d0fb670d7b277e848cf93e2963478\",\n        0\n    ],\n    [\n        \"0000011579b1ffe43056593c6d2541b20ecad58654a70b926b80f02bf7e3afa2\",\n        0\n    ],\n    [\n        \"0000002a0af0dd33f39c900d2b08ca7b53ec7ad5cf8cdac6c0a594c824528554\",\n        0\n    ],\n    [\n        \"000000174c981f1f0f785a066db5ab640fd81db7131d3bc6c0ae5f1c881a5869\",\n        0\n    ],\n    [\n        \"00000010b2e80adbf6ebef457bfe030f028fd7c054eb91967067ecbe32391e6a\",\n        0\n    ],\n    [\n        \"00000056b45f9fba1d07427704b8929aa5a6273864c3c28fbf0561a970927eba\",\n        0\n    ],\n    [\n        \"0000015fee8d0e568e47beb80e459c1e55f0939bcfb67c8d71d6aea4bbef07da\",\n        0\n    ],\n    [\n        \"00000026f4c92697d195eb8380852e8e2f72f00bd237ed7b6a34ab27d46df667\",\n        0\n    ],\n    [\n        \"0000006997e8fb35db20f6cba346e8a4f5ad3e53b2816b04b5f15ee7d300c507\",\n        0\n    ],\n    [\n        \"00000153eeef8fe2c11990a87b680c86595bd1eefe62e912bd4d2d0181dd275c\",\n        0\n    ],\n    [\n        \"0000011255d8a68c30b8dc8f9c0d5bd83484dbd1a67c19f9773e942b869184d4\",\n        0\n    ],\n    [\n        \"00000127cd6eb4b0bb794bafce0990f25ad88da0338aaa643b08f73679ee7c40\",\n        0\n    ],\n    [\n        \"000001351829786299ce95aa0e4fff6ca6fb0579176476ebb23cd9c5b18dc38b\",\n        0\n    ],\n    [\n        \"0000003cd9b6c744cb5c8fdc012c200c9fbb72c8e82a535286ad29e9daae0b7b\",\n        0\n    ],\n    [\n        \"000001195e3dc8e195350793514679ce2b2951c8e69e2ab759627b7d4dae0174\",\n        0\n    ],\n    [\n        \"000001469586fbf43e84517bd8aebd65f556a3f8caae21676fde3d8a37d548c5\",\n        0\n    ],\n    [\n        \"000000f0298f92b43e87c972155819d6e6fa80a99a309a7a63a447748fdbcae2\",\n        0\n    ],\n    [\n        \"000000793ff1190b63052655624f2e3e771257ee5e4a95d0fe8bf8900119f4a7\",\n        0\n    ],\n    [\n        \"0000002b535d083651a6a816526ab64e18a3473117e11bf9fdb37409fd2b8476\",\n        0\n    ],\n    [\n        \"00000056df0365d1e9ac806b3124919922d8d2fa528ec230562b384ef957e049\",\n        0\n    ],\n    [\n        \"000000e4d73bc1ff88044907fdabcd18fb4feeae8d89214e00a835497eff9d44\",\n        0\n    ],\n    [\n        \"000001164ada2f23afdc6e7eb7a549d0240ec00795c97a6b7b0da459ba144236\",\n        0\n    ],\n    [\n        \"0000003a02c60020ea02296f487408b092ea86fcace4b71fe65ddf8e9d1227af\",\n        0\n    ],\n    [\n        \"00000011c1bc4de2a5565d4f6b52e2d20830ae03cc33da840752a9b082f22970\",\n        0\n    ],\n    [\n        \"000000590d9132c7ec8cf2fdc08d841aa30f1a6c98baaa41bb2f2ced082ffc56\",\n        0\n    ],\n    [\n        \"000000aad06d12ca4f1614a04529b9b0db229edd3b999c8f2387bf4499e7e823\",\n        0\n    ],\n    [\n        \"000000f21e5a641ff87de60c94f9f95c28291989fa52980862a96b1da9420c20\",\n        0\n    ],\n    [\n        \"000000389253c73a42abfce6f70dd8b4b47c1e94ff70f3650136315f1c7c3d51\",\n        0\n    ],\n    [\n        \"00000052d02504f8361173e62831ffb1ed06502e39bfe56e7b2e8534527acf54\",\n        0\n    ],\n    [\n        \"0000006e903dbc8f9c6a155c31ac0eabc64a0f538ec1956c12bf0296365ea10f\",\n        0\n    ],\n    [\n        \"0000012d6d51f5edeb1bc181aa56058ba399c56a233196a609d17ea9fe1260f4\",\n        0\n    ],\n    [\n        \"00000078fe34ae6dcfebd6c54750fc5b23c9ccfccb85410a67dc873950089470\",\n        0\n    ],\n    [\n        \"0000008ba099907877add94606ade4d8c4fce72e78e4c9a7211511e191cb3090\",\n        0\n    ],\n    [\n        \"000000ba16aeb089d8272fda74e376bc74063e202b41c1d910466b28d0e22aa1\",\n        0\n    ],\n    [\n        \"000000e9305b2194cc90a8f36ac85d6e335f9c40eebe31a63111b1a2cd62ef60\",\n        0\n    ],\n    [\n        \"000000b20594e8e7b7c883ebcd4fb5038ac6e5490d8089643e63fcbbed54159c\",\n        0\n    ],\n    [\n        \"000000ebaf4db2459b51830938659a587f72bd56fc713f8c15bbd877eb6e4cdf\",\n        0\n    ],\n    [\n        \"00000121189334c3922651f449fb752026ac9d3c83ca36f1b379629acb8a4bc8\",\n        0\n    ],\n    [\n        \"0000011331e953cf2ebd0231d00cf8ce3b0925d4642d32359922d87dde28e6a5\",\n        0\n    ],\n    [\n        \"000001088db354ec53ca636d01718cbd5948d240d37b2bde660cf46e64b7bd13\",\n        0\n    ],\n    [\n        \"000000a163f70345b7c90e1053b582aa415f8211ced4f9693edf493032d8b969\",\n        0\n    ],\n    [\n        \"000000116824a8e4a6c2d6d0e32b8df377f701091d6a4ff2457790052fec8c2f\",\n        0\n    ],\n    [\n        \"00000071aed5672329b4ceaa9fe0dc56a73a9aaa6ae368e83fd49d2fc613d182\",\n        0\n    ],\n    [\n        \"0000006980fba2dab31f8a62e96484952512fefd1ef1a7d1ac6b2b70520298d4\",\n        0\n    ],\n    [\n        \"0000010a5c600b597167962263ee3f34a9844f11cf62eae87254140fd1ca4c4b\",\n        0\n    ],\n    [\n        \"00000032c5455af9f5ecbd52120902a2476b525bdb1bdc95304c72a75298576c\",\n        0\n    ],\n    [\n        \"00000145fda2984b2467e98dca41b15a210817ce8507ed6470bfabaf6e1d58a0\",\n        0\n    ],\n    [\n        \"000000fc300d0bb620acb44dcb5fd4a75746de32a250724e26346798781b762c\",\n        0\n    ],\n    [\n        \"0000013835a5ae1a46e514eca524b79ea5acbf16bbd16d4913d5711fbfd0b43a\",\n        0\n    ],\n    [\n        \"000001105408d97b67a382a8682793c6fa17e8a9e14431ff46931852d98809a9\",\n        0\n    ],\n    [\n        \"000000b246b4b35ffc56055ff91448ba0c395a7697369680460aab62560c71c8\",\n        0\n    ],\n    [\n        \"0000001af62d9a537a82ca68c50cab0726fe74a209358fa7ae02aa12fb68dc03\",\n        0\n    ],\n    [\n        \"000000357adc640f81f63c32cc2bf924953a9087465478eace40ad368bbb610b\",\n        0\n    ],\n    [\n        \"00000074d93559ca744568b640e31497e54448f5e81eb458d5e6309a866fbdc0\",\n        0\n    ],\n    [\n        \"0000000e5f9f2f28a8e84fba06965b939eb33d10f367b285e0b765f73432293e\",\n        0\n    ],\n    [\n        \"000000bc8fbad304cc9f7a2129b3ada77205993d6797aaa3c412d47378cfe1d3\",\n        0\n    ],\n    [\n        \"0000001bfaddc170487792c89290b630bf2987cf465bceee6228c98339d5b51c\",\n        0\n    ],\n    [\n        \"0000005d328174d05468a64f3a1c9b876607b229803896be461d4bf71a604ad6\",\n        0\n    ],\n    [\n        \"000000767328d1681b20d958b8e3ace547f35d780e3fbca8f6917896d7a040fb\",\n        0\n    ],\n    [\n        \"0000013f4af09699526f35b5d991b6acc392ad3b71d97955acbc061896f613da\",\n        0\n    ],\n    [\n        \"000000a5bee294beb00596090e6f193fe72463b22feda343c108ed89f81cb395\",\n        0\n    ],\n    [\n        \"0000008c2e331069893d102b28aae90710ece9fd4926d005de976f97e44b66e7\",\n        0\n    ],\n    [\n        \"0000015205ced605eded9c5d62e3cf8ed5229f9a72e7beb4bc8d63655b36abf3\",\n        0\n    ],\n    [\n        \"0000013ae7e94521be9703ffe12b1214d47fcf56fb1268a6652db590ec06bbc2\",\n        0\n    ],\n    [\n        \"000000d18fb8d06bda4847c4a629e17295a2fa1092e14ffbff0d97a2eae72993\",\n        0\n    ],\n    [\n        \"0000010f9a0dae27ef69bef205f2e6d900e120b29e684a2a08286abc95579840\",\n        0\n    ],\n    [\n        \"0000006e29c2511c1ad31fe21d0064594c557e5fac6f73f7961f2b53cf36b983\",\n        0\n    ],\n    [\n        \"000000dffd144d2e1eac01a786558c04cb65a8ed0002397db7b43011c223174e\",\n        0\n    ],\n    [\n        \"0000008365805832101e989fbf04bdca8c364b8afcb794b2166db74a4a92cf8f\",\n        0\n    ],\n    [\n        \"0000004a9fde1918b96cb079d4b48a98098231d34f162e8e898919e0573ada05\",\n        0\n    ],\n    [\n        \"00000063f777d9004ebcf1685a07dc2e36c3ed40758d5ed5b7dc82ca2165f8da\",\n        0\n    ],\n    [\n        \"00000084364ae6e59f9223629293d017a1718f587e08b45478e79ca3681ba813\",\n        0\n    ],\n    [\n        \"000000e826cbbeb467e73577566db478f432295952489e154a3de4db0012dc91\",\n        0\n    ],\n    [\n        \"000000d2f92728dbccd1782fde6a555a89f47ba4102d3bc0e0d07ac6c99cb468\",\n        0\n    ],\n    [\n        \"00000066d082b9414191fbf2b18746513e10f403eb3d6e837f4a007375f3bcbf\",\n        0\n    ],\n    [\n        \"0000007b872779a56424e1eae91113b3f43e7da689c5d2e9e86c5b8bbe2f3315\",\n        0\n    ],\n    [\n        \"000000057927903d3c9ea07cc6344cfa7299541470c7c97caf24b9d9e39bfb38\",\n        0\n    ],\n    [\n        \"0000000842d0fa799de1397043352af7fe7da168314c9a0a173fa13304d396a8\",\n        0\n    ],\n    [\n        \"0000000b66f892759e13adc4249d1200a4c9bba92d56830816d5804cd2254d29\",\n        0\n    ],\n    [\n        \"0000000bd582d47ad9e2457a02af92c63fd1be6bd7aec7f71d7aec5e145e2bed\",\n        0\n    ],\n    [\n        \"0000000111a8861cafcd74da47754738fef19072b5da46ee663a52e11f042d6b\",\n        0\n    ],\n    [\n        \"0000000c210b5e309f204bea96516a1d2b147b4855f2afc501445463ca55726f\",\n        0\n    ],\n    [\n        \"0000000f0532cadd9795ad911a851d75ce9e198bf671a0e1299a39b86f9ff0ea\",\n        0\n    ],\n    [\n        \"00000006a125ea1b6fecbdbc49a106a8ab3c8eb6e3ed6f49daf33818ba635d59\",\n        0\n    ],\n    [\n        \"000000021f43833bc6dc117c0a99abd8428b30e042865f2bdfe89258ab5ce450\",\n        0\n    ],\n    [\n        \"0000000bb33efcb77b81fb0ee743523a02a93f662d2ea8fb083662bbb47e02ca\",\n        0\n    ],\n    [\n        \"0000000c62a5d5e42d512c83c542bd936bb5f735cb416d555476be79185f6206\",\n        0\n    ],\n    [\n        \"0000000d896f71fab78384d2c78c8e6a2367315d282464be8233b0bc21d877bd\",\n        0\n    ],\n    [\n        \"00000000f5d2c35c12db500f5d250ad24e8f363d2937d1c487b5f86731adb552\",\n        0\n    ],\n    [\n        \"00000000d72f5a316078cd8747e8b8027baa8f661649bf1a8ed722e4f4231d3a\",\n        0\n    ],\n    [\n        \"000000079f91871f027e6e3fc37dd638759f1e7b8498fd71c7ff397132b20f86\",\n        0\n    ],\n    [\n        \"00000004cafb2f9b4aa75f426cfcd4bd2774cf14b3e7d89a8b1945e5d8de8195\",\n        0\n    ],\n    [\n        \"000000034c3e13346b9843061f780d74ce2c18a264610b26f26576f7631c7aa1\",\n        0\n    ],\n    [\n        \"000000110f528afa3f436cdb42d15287c378559a85d1e1e110a11ffd5309d4e7\",\n        0\n    ],\n    [\n        \"0000000019963af20c504e4e78fc7c5bbc5fd52efaa19b4ee778f6f213072c93\",\n        0\n    ],\n    [\n        \"000000046df3b5b7b222fe177bfc016e92d760bf31d8670be3e5b2318a57c27f\",\n        0\n    ],\n    [\n        \"000000003da79540fc14abc1d14ee5b4cd635e4e903cf116862abc21e82db694\",\n        0\n    ],\n    [\n        \"000000102cd648540dabef56265de9ebfec7c43bd3192d873613f06e9bac372e\",\n        0\n    ]\n]"
  },
  {
    "path": "electrum/chains/signet/fallback_lnnodes.json",
    "content": "{\n  \"02357a375a846279fc1e8413f5e182652a125e5f6a4f4653bffabebb8177a6d8aa\": {\n    \"host\": \"34.68.95.152\",\n    \"port\": 9735\n  },\n  \"0305061295fa30847df41ae6ee809b560e78d65c2a7337a41c725ea3920b65e08a\": {\n    \"host\": \"34.124.125.201\",\n    \"port\": 9735\n  },\n  \"027554f8d4d99a43cf1b49d274f698ee5045273cd377206eba62ea308b4386a4fa\": {\n    \"host\": \"35.247.14.99\",\n    \"port\": 9735\n  },\n  \"0244bb7ba2392ab2d493ad04ad4afcd482ca44a2bfe5b42bcc830bfe00e5b08082\": {\n    \"host\": \"34.138.100.228\",\n    \"port\": 9735\n  },\n  \"03adf6efe5346d455172c750a655b07fb85be4f50f5b555f9f91a853a6b448c3bf\": {\n    \"host\": \"34.74.81.232\",\n    \"port\": 9735\n  },\n  \"03ea42c9408a73dabdcb5655e2923956d132fbb25cb71e7c00a29e10c73e937e64\": {\n    \"host\": \"34.138.237.159\",\n    \"port\": 9735\n  },\n  \"024d899b60d5de58e8d66af042445323a48b6962d6c667c033802421dc49abc232\": {\n    \"host\": \"34.75.211.29\",\n    \"port\": 9735\n  },\n  \"02e8430ba207ce87bd2d4ab36497b9eac10e6d5d86f9fda8aa270c48877e0a8259\": {\n    \"host\": \"34.73.252.102\",\n    \"port\": 9735\n  },\n  \"0265ed138065b84d6b9448f9e0a2fd4ceb63fef08efe1dfc949a63d5d43110e4c0\": {\n    \"host\": \"175.45.182.145\",\n    \"port\": 39735\n  },\n  \"0307238136c48cd35084c4efadc486143a7e8a7acd8ff8ac053fdab4efabc551c4\": {\n    \"host\": \"104.244.73.68\",\n    \"port\": 9735\n  },\n  \"020ee56ff81d12d17d5d3eea5306a8982a5763522ca73e0e220ce282030543c90c\": {\n    \"host\": \"84.247.50.180\",\n    \"port\": 44149\n  },\n  \"0271cf3881e6eadad960f47125434342e57e65b98a78afa99f9b4191c02dd7ab3b\": {\n    \"host\": \"signet-eclair.wakiyamap.dev\",\n    \"port\": 9735\n  }\n}"
  },
  {
    "path": "electrum/chains/signet/servers.json",
    "content": "{\n    \"signet-electrumx.wakiyamap.dev\": {\n        \"pruning\": \"-\",\n        \"s\": \"50002\",\n        \"t\": \"50001\",\n        \"version\": \"1.4\"\n    },\n    \"electrum.emzy.de\": {\n        \"pruning\": \"-\",\n        \"s\": \"53002\",\n        \"version\": \"1.4\"\n    },\n    \"mempool.space\": {\n        \"pruning\": \"-\",\n        \"s\": \"60602\",\n        \"version\": \"1.4\"\n    }\n}\n"
  },
  {
    "path": "electrum/chains/testnet/checkpoints.json",
    "content": "[\n    [\n        \"00000000864b744c5025331036aa4a16e9ed1cbb362908c625272150fa059b29\",\n        0\n    ],\n    [\n        \"000000002e9ccffc999166ccf8d72129e1b2e9c754f6c90ad2f77cab0d9fb4c7\",\n        0\n    ],\n    [\n        \"0000000009b9f0436a9c733e2c9a9d9c8fe3475d383bdc1beb7bfa995f90be70\",\n        0\n    ],\n    [\n        \"000000000a9c9c79f246042b9e2819822287f2be7cd6487aecf7afab6a88bed5\",\n        0\n    ],\n    [\n        \"000000003a7002e1247b0008cba36cd46f57cd7ce56ac9d9dc5644265064df09\",\n        0\n    ],\n    [\n        \"00000000061e01e82afff6e7aaea4eb841b78cc0eed3af11f6706b14471fa9c8\",\n        0\n    ],\n    [\n        \"000000003911e011ae2459e44d4581ac69ba703fb26e1421529bd326c538f12d\",\n        0\n    ],\n    [\n        \"000000000a5984d6c73396fe40de392935f5fc2a8e48eedf38034ce0a3178a60\",\n        0\n    ],\n    [\n        \"000000000786bdc642fa54c0a791d58b732ed5676516fffaeca04492be97c243\",\n        0\n    ],\n    [\n        \"000000001359c49f9618f3ee69afbd1b3196f1832acc47557d42256fcc6b7f48\",\n        0\n    ],\n    [\n        \"00000000270dde98d582af35dff5aed02087dad8529dc5c808c67573d6dabaf4\",\n        0\n    ],\n    [\n        \"00000000425c160908c215c4adf998771a2d1c472051bc58320696f3a5eb0644\",\n        0\n    ],\n    [\n        \"0000000006a5976471986377805d4a148d8822bb7f458138c83f167d197817c9\",\n        0\n    ],\n    [\n        \"000000000318394ea17038ef369f3cccc79b3d7dfda957af6c8cd4a471ffa814\",\n        0\n    ],\n    [\n        \"000000000ad4f9d0b8e86871478cc849f7bc42fb108ebec50e4a795afc284926\",\n        0\n    ],\n    [\n        \"000000000207e63e68f2a7a4c067135883d726fd65e3620142fb9bdf50cce1f6\",\n        0\n    ],\n    [\n        \"00000000003b426d2c12ee66b2eedb4dcc05d5e158685b222240d31e43687762\",\n        0\n    ],\n    [\n        \"00000000017cf6ee86e3d483f9a978ded72be1fa5af37d287a71c5dfb87cdd83\",\n        0\n    ],\n    [\n        \"00000000004b1d9fe16fc0c72cfa0395c98a3e460cd2affb8640e28bca295a4a\",\n        0\n    ],\n    [\n        \"0000000046d191b09f7726e4f8bfaffed6c30734afbf1f95e6bddbe0b07d9e88\",\n        0\n    ],\n    [\n        \"0000000082cec8200e9ea055c2991bf74560eb7e7140691ea53e7828dbdc9553\",\n        0\n    ],\n    [\n        \"000000003775b96d6b362d4804afe2d9c3cf3cbb46a45c3ccc377c94e83edd23\",\n        0\n    ],\n    [\n        \"00000000037835a92404acb2f18768a49d4f93685ead30aad6bb3b073f411e02\",\n        0\n    ],\n    [\n        \"0000000006cf75d17706d1f62e6b08e6ba5facfde38a8920b7d808a6b6781ff2\",\n        0\n    ],\n    [\n        \"0000000003dff257cdae43703fcd0ca91fda0970f5fc04258b4608fb1942a6f6\",\n        0\n    ],\n    [\n        \"0000000000532d97d18867658e08c789f627535652382147e33bf8626d4131bc\",\n        0\n    ],\n    [\n        \"000000000266dfb79bb11dedd0ae748505863ab3ab731269cd71a2c2fbd159b3\",\n        0\n    ],\n    [\n        \"00000000349ff0119d5c0dd8ffad8bf41cd6126a88416148b81fa4dcaebc42e1\",\n        0\n    ],\n    [\n        \"000000003c61939b4799eeea4335218d30de9b1071605126d719dce0f0d14810\",\n        0\n    ],\n    [\n        \"000000003d9284570ed648d2b12ad24046ac8b9abcf05c4e9813ea110490cf73\",\n        0\n    ],\n    [\n        \"0000000001360b66e6dc0ccfbd75356034e721ae55c3d5c71a58be5d281c252b\",\n        0\n    ],\n    [\n        \"000000000c114f42504916bfb2ee26ed8307b3f7f74226c1cfe1f5302ec23d26\",\n        0\n    ],\n    [\n        \"0000000007acac3fcf97b4ca81821263b704364adaa2736fce0a0722bfed4f8d\",\n        0\n    ],\n    [\n        \"00000000059768ef7731d27f9c2be48c6e16d7cb56680625f08ff25ead504280\",\n        0\n    ],\n    [\n        \"000000000351c8908f1f52518ce4bd251b896ca3fbccb69a2607db6624bafcfc\",\n        0\n    ],\n    [\n        \"0000000068d7ccae048e212e9e2ecb4d944f583b4490df4fbf654b4915597052\",\n        0\n    ],\n    [\n        \"000000000e2aaa36417187233ff55325473bd5b7a164b358da60c96d1920fd77\",\n        0\n    ],\n    [\n        \"000000001eb11ef6dbe0647bc87a8d218f6e59c2b9690f17edcf0dbd39cd0308\",\n        0\n    ],\n    [\n        \"00000000022e7855e24cc3fff67ce093242434a8ffa45882333a0f08a40aad9c\",\n        0\n    ],\n    [\n        \"000000000210130ff4e3186258c09a8463c1e196f5c5432b4c7b6954e907bf63\",\n        0\n    ],\n    [\n        \"0000000000e01372ede322bf88ee5ed8a46dd4fd8df832eca16180263fc8b1ef\",\n        0\n    ],\n    [\n        \"00000000a0701896e26d5d884834b267512e0af52c92edc4bccf1c5c803d3c4f\",\n        0\n    ],\n    [\n        \"00000000869fc8d9ac1588f3e5bdfd60253e9824083800b7794010e0e9c6b6fe\",\n        0\n    ],\n    [\n        \"000000001d43b3165ec30736f28f0761600b092686f861db23ec38f2d92b0ec6\",\n        0\n    ],\n    [\n        \"000000000ef4092da8c2056e5933de0e1530194c3ad941a9b393fbb26f98862e\",\n        0\n    ],\n    [\n        \"0000000001e3fed39f70023909f962bea146b03bc8e94e5d19d7da93123f4f64\",\n        0\n    ],\n    [\n        \"0000000000b4b8c877bbe3cde97649845290bb78999ecff4621b9bf2ab16aa2e\",\n        0\n    ],\n    [\n        \"00000000006095ba3b4742883a0ec427a3fd685ffb65b987ea77ebfedea7da82\",\n        0\n    ],\n    [\n        \"000000000168f0a76a6068a34fc042553aff4aa63b906028f28c2a4c327328e1\",\n        0\n    ],\n    [\n        \"0000000000af10f3079b4989ac4ff0baaecab38220510cdae9672d6922e93919\",\n        0\n    ],\n    [\n        \"0000000000312791ada0f6a4c5eaf2a1cd57cd06f5970a8ab49923817b862c35\",\n        0\n    ],\n    [\n        \"000000000055f3d4f45c4d199d9c230cb2cfeb68c8e934cfd061bd616358655a\",\n        0\n    ],\n    [\n        \"000000000036b6129bb5a786bfdd75cb4b932f7dcae9da469d3ba35096f1e821\",\n        0\n    ],\n    [\n        \"00000000002fbccf271c13e486673251ecd7951ecc12ee73c4390e0ff09e9b59\",\n        0\n    ],\n    [\n        \"0000000000314e297a81bf002fc40eb391d8883ea45ee4e782385aa0fdba6452\",\n        0\n    ],\n    [\n        \"00000000d3c473819ec3b3c268f7b555df22772e407bc8f246a47cfc579ec61f\",\n        0\n    ],\n    [\n        \"0000000075a438fda6bdb391263d0a2a6e8e68edd9dd8f70fe5734eab9351eb8\",\n        0\n    ],\n    [\n        \"0000000017ebae0a2bec50008b4a4ea8839798cbd9ff228e76aba087d0ff1736\",\n        0\n    ],\n    [\n        \"000000000800466ba31c0bbc12b125f16d05ed27788de045e25d6f093817d29c\",\n        0\n    ],\n    [\n        \"00000000002163c41f2264f202e611aeb9ba6c0a3ee95cd8e5e7e571edc64edf\",\n        0\n    ],\n    [\n        \"0000000000de9882d417786fce8c755cfaad17f40cda744d4badedfe5e414e31\",\n        0\n    ],\n    [\n        \"00000000002af352cf41f60a5ebf033bf7e4967c0597cee706ba877b795aefb4\",\n        0\n    ],\n    [\n        \"0000000000009ca0030f1dd0b09cc628f2d4d278c87b20781a1b136dc395debf\",\n        0\n    ],\n    [\n        \"00000000ffd27370a76d06a0da0e3805f47e35e2cf584d73d2c5ecaa2e525642\",\n        0\n    ],\n    [\n        \"00000000720da6910aa75099baa020cb8db37e1dc19cdff66152225b7609c23a\",\n        0\n    ],\n    [\n        \"000000000a5c2cc704bce5e8527ce91bac7430c659624ecd86e6a1bb9b697962\",\n        0\n    ],\n    [\n        \"00000000084273545134e9a06483c8fab00c2b0628056bb1967f310c74a971bc\",\n        0\n    ],\n    [\n        \"0000000002f66f4da52804647b1c3e1f89d17bdb05e9cd4ebbd922007c773f21\",\n        0\n    ],\n    [\n        \"00000000c46146c9d0a67a354b3f82947e52670a3bded6d8513ab34a68ae18bd\",\n        0\n    ],\n    [\n        \"000000002f61c429d7dbe7bde75796086efe574998766806138710a2d6001eba\",\n        0\n    ],\n    [\n        \"0000000001daf3e3e78a57df2c2d2ddd14093d10515925e75c818bec3bbd30c2\",\n        0\n    ],\n    [\n        \"0000000002e133a7427a9aac6ceca969b27507c14111a45512cdf8f52a436de0\",\n        0\n    ],\n    [\n        \"0000000000f7c4374d458666740de1d0e8c55229a209ced7c38e38708781487c\",\n        0\n    ],\n    [\n        \"000000000035bb9ea329ba30b83eeb4ea6f57c2fe703b97f9b879f21e22643e0\",\n        0\n    ],\n    [\n        \"00000000001220503e0aaee266bca85de09ce97b0091f24972d1ad1c8afe8609\",\n        0\n    ],\n    [\n        \"000000000010a614c60457f8d2ae2bb826d037f52113252888fadda8ed773c9c\",\n        0\n    ],\n    [\n        \"00000000585a8b882ecff8aa8434feeac4ef199ca669bd81ed473e37f0bb4528\",\n        0\n    ],\n    [\n        \"000000009504ffdb5fe82ad88218fb5e75a8bc185247e30e22d23b9fd9b7f282\",\n        0\n    ],\n    [\n        \"000000000ddec7d73bcd653168d82e34cf5746e006bccda8a9c031c3289b9568\",\n        0\n    ],\n    [\n        \"000000000cb6620ee4e8cb8b6b4d51251e5961f7ae2e83538ab3a4fef3bcc773\",\n        0\n    ],\n    [\n        \"000000000239224a0841738513c1eda712b73266ea958aa75f44a3985ebfab82\",\n        0\n    ],\n    [\n        \"00000000002630c7c3586fcc19079300403c54dc293bcfdf8a9981f85a5c31bc\",\n        0\n    ],\n    [\n        \"000000000028d8c34f44e51fd71f5401094a983f6566e6d08ce86ec5d1bd639c\",\n        0\n    ],\n    [\n        \"00000000000dca95f1828adc3c37b4625f60aeb35a6614a4358322b7a6bc2f7d\",\n        0\n    ],\n    [\n        \"00000000d72ec84fda18959ddc474d1a31a3a13b1d94695136c4810af8c01a0b\",\n        0\n    ],\n    [\n        \"00000000327c29604996eb7f0a208160969ee4408a1cad277a956334f94e0f35\",\n        0\n    ],\n    [\n        \"000000000e1bd41d009c1910fcfee7bf1cc1adb04b0b7a632ac36c1092f01bb7\",\n        0\n    ],\n    [\n        \"000000000201a5afed48b9d095b949229e9882ef8bc96767be3097c87264dfb6\",\n        0\n    ],\n    [\n        \"00000000003f28e8f3f9c80b1269bb0aa3b57501c12458550ef04fd43aca6a33\",\n        0\n    ],\n    [\n        \"000000000029e09fc14e38a6a0103c8c67383f41af7d76998055682525f4ca89\",\n        0\n    ],\n    [\n        \"00000000285ce297602995582ba5d32d583d618a6a92643566e25dd36cf2b7ab\",\n        0\n    ],\n    [\n        \"00000000657045fa54fac52b8480dc84bd4c418940ba63679f4bd6add6a39962\",\n        0\n    ],\n    [\n        \"0000000017b7bb58be05a47ff7c4ead27db750813d6bcf3f99cbcc35324cf445\",\n        0\n    ],\n    [\n        \"00000000003a310e39b6df17f17450496b4f5c1593399bfa1ab8b4d39bac9b25\",\n        0\n    ],\n    [\n        \"00000000000bfbc5294f003548a9636ebbcea3ba42577821266317676fbc363c\",\n        0\n    ],\n    [\n        \"000000002329351dd70c24da2eea5ac19f65b6053c4611aa4eb93bcc2783c57e\",\n        0\n    ],\n    [\n        \"000000004ce02f1005aa6fa4d158c6e4fce95ab053d88ae74881dd080c24e057\",\n        0\n    ],\n    [\n        \"0000000000fdaaa54cdaade8cfb75245de0747c60c0307ad11be9fe154535565\",\n        0\n    ],\n    [\n        \"0000000003dc49f7472f960eedb4fb2d1ccc8b0530ca6c75ed2bba9718b6f297\",\n        0\n    ],\n    [\n        \"00000000014ca604d769d4b99fff03ae3ac84d1e8eb991c5dac7c3cd4d9e68ee\",\n        0\n    ],\n    [\n        \"0000000000190ab8ecef3a3d5583563851672d81a4d4d952b8cf3bd503c655e5\",\n        0\n    ],\n    [\n        \"00000000001204d263b607987fab11e1c19c94b7e3e674cc73cc2fb7b05fbf07\",\n        0\n    ],\n    [\n        \"0000000000141e8d7f7ac359a8ae58e35ce6010c25ddd6f1881f41c0b939332e\",\n        0\n    ],\n    [\n        \"00000000946344dd06ef5ddd13fb74f20c475daf911ff4e3f1dcdf64c330e274\",\n        0\n    ],\n    [\n        \"00000000ec77a7892e48b85bcbaf404d16d7fc93747d7e9e3ba6195a9b6f1525\",\n        0\n    ],\n    [\n        \"0000000018a305c04dea8e93e423ce9569872e0ec5af49d23a0e3872b0ad6297\",\n        0\n    ],\n    [\n        \"00000000055e32c5f8a86c9a712eeb6440bbf9810ae6da12d0cea2493138a885\",\n        0\n    ],\n    [\n        \"0000000001913fcbe67badbce4234e86e35a1ea867ecd69814b5f5ab039b7d4b\",\n        0\n    ],\n    [\n        \"00000000002c71fe4403aee704720ceafd21f9f8c9c97a8bfbd25bb46223aa40\",\n        0\n    ],\n    [\n        \"0000000000343a42da0c811836d0785c272591facd816f0e7fdcfb1109d8f9a8\",\n        0\n    ],\n    [\n        \"00000000000309b182608b3eea7fafd0d72e3c79a0a3a9cda03cde3947e332e1\",\n        0\n    ],\n    [\n        \"00000000000204cc04e421c3958a64d7bc024a474ce792d42ab5b48a5a6f3927\",\n        0\n    ],\n    [\n        \"000000005eaa010e7255bd37e0b00780575074a74d889e17c4dbc578f917348d\",\n        0\n    ],\n    [\n        \"00000000a0d425f62d9196c069286dc6635ded9d027de40070d397e45bd63e0e\",\n        0\n    ],\n    [\n        \"000000003355fd37068ce2d5d2a94ef964eeb9b687f21f4a00850a3e6cc4a71f\",\n        0\n    ],\n    [\n        \"000000000ca9148dabe9424cd8c96860c90d836ab25970a3e91856764e2e640c\",\n        0\n    ],\n    [\n        \"0000000000bde23f829dde8edef35436be4b8978da21fd2c3a8100ef5334e3cc\",\n        0\n    ],\n    [\n        \"000000000028bb26f1427fbfabeae65d55a9e59e18230713e40f0f7c9c2dee12\",\n        0\n    ],\n    [\n        \"00000000002ac05422d254e597ee6b5e0f8be9b3e2f887486442d720c7766919\",\n        0\n    ],\n    [\n        \"00000000000e36d0b6f187dd9601b1d1dcd987c3e0f6a081ffd039c7c5e32462\",\n        0\n    ],\n    [\n        \"0000000000048d7b1f2a2a11fda34a5cfeea067ab03e482931e5a0f463f438ba\",\n        0\n    ],\n    [\n        \"00000000f780ab88c8a4f4247573a749fbb087a4e3fb6a7d29926de8a9ab3462\",\n        0\n    ],\n    [\n        \"000000000313bbe6a940e6a8c40ba091aa1ebbaad135bbbff3ed8ae07cf574d2\",\n        0\n    ],\n    [\n        \"000000001d4ab29721aa2722482562670a0d71dc1eb73231c5dafb64756b04e8\",\n        0\n    ],\n    [\n        \"0000000006588bcbdec38d19962b96cf0352cbf1b90f3379cc6787d018cdb96d\",\n        0\n    ],\n    [\n        \"000000000022e79539a21ac24f9daa2cbddf2bb4a3125f88a5efc20d13ea856b\",\n        0\n    ],\n    [\n        \"0000000000dd284b7fee584cc578a10fbe57e8efe6bf6ebacb23c0ac5d46cdf7\",\n        0\n    ],\n    [\n        \"00000000001451143787f411c93d5506065c3fb597966f2fd7a4a5c078ee6aa2\",\n        0\n    ],\n    [\n        \"00000000000ca977394af1e414dc1f9d83efa007f7226e11d3a00f59a1fdfad1\",\n        0\n    ],\n    [\n        \"0000000000011f8caa80580e7a796bbce5b84e60731bf48e03c6ff5c6bba868e\",\n        0\n    ],\n    [\n        \"000000000001705beb1376af1af08b437acef6befbe7d3b60c5fbaf6bb7f38c9\",\n        0\n    ],\n    [\n        \"000000000000c838f1f45422d93ca9b5838368a37423efa8439ee24b2bf247a2\",\n        0\n    ],\n    [\n        \"00000000000111ad857d31d07fdc8b32d17af2522c18bdaccfef449b29d17362\",\n        0\n    ],\n    [\n        \"000000000000312a7718fc616b0ecfdbf6066f71ec1a4a8c43f50f02f61cc398\",\n        0\n    ],\n    [\n        \"0000000000007d232b217a59b804ef67091c5720a5460c2c16bf97b97a24801e\",\n        0\n    ],\n    [\n        \"000000000000177235c33695aced585685b4c500eb76e72caad02e17503900eb\",\n        0\n    ],\n    [\n        \"00000000000037f5c5890da7a8e2acd2b0669ad7db648ac43140c637a1c81637\",\n        0\n    ],\n    [\n        \"0000000000002123904063f223bc35135c426a4f9a0b74c1907e837b810f0321\",\n        0\n    ],\n    [\n        \"0000000000000961db809da357d91a9341170fafef9f24896d8730bd05cf3f96\",\n        0\n    ],\n    [\n        \"000000000d2e8fcd05eb874e98cfc3a6e239f6974950e6f50b0487513ecab760\",\n        0\n    ],\n    [\n        \"00000000017e362508c8db23fae0431eaed708d9db13e48fd5d318066bf6733f\",\n        0\n    ],\n    [\n        \"000000000011b2bc4fe36f90b7ba5a62f974db250bfdc285b70c71148023c7e3\",\n        0\n    ],\n    [\n        \"000000000001be28570b378dd5dd2eb3aa495c229913b6757fe8900dfa3cce99\",\n        0\n    ],\n    [\n        \"0000000000242bd0bb16d0a5324e0b4b5a83697dabb3b4a059084557478e50b9\",\n        0\n    ],\n    [\n        \"0000000000d8ce69d18da32ed52e503d6b5ad48d970b90545f956b2d2af2edf6\",\n        0\n    ],\n    [\n        \"0000000000366655bf0cb3dd0cd7801e0adbd26b5b441b77a9e3642597effb00\",\n        0\n    ],\n    [\n        \"00000000000dc7aa00d4607ca8374d40d1187f1c084b620edb45fc39bc8d2db8\",\n        0\n    ],\n    [\n        \"000000000003baf60d9c6e70a765cf517f66a124509191188e9547ad09edf68b\",\n        0\n    ],\n    [\n        \"000000000000e0f476893b8fb4d37e855353075fde73dbc1fe181cc956349f19\",\n        0\n    ],\n    [\n        \"00000000000032ed16b7de758abadf4a4fb2df7a101ff275c51f29e1555a89a5\",\n        0\n    ],\n    [\n        \"0000000000000a564d03f0f2fe20f6fb5f038d931f732d817641cd7fff3b0acd\",\n        0\n    ],\n    [\n        \"000000000000011aa4d0fdcea8d4ca85cd5d548e322e2b6abd17f8444be855c5\",\n        0\n    ],\n    [\n        \"0000000000000610588540267a0eb544531047d4c8af0f21fca7cd3d96205cfc\",\n        0\n    ],\n    [\n        \"00000000000002770dab5e14843149df8f76b8dc8458ed3ed2ed8a14a6e2e564\",\n        0\n    ],\n    [\n        \"00000000000006b70ebc9f75bd32f466602cbd4b86c3c2d2379059542bb8bec6\",\n        0\n    ],\n    [\n        \"00000000000000ef579af389fa7674f98a2371063fa8b218c5ca0ad94e21b896\",\n        0\n    ],\n    [\n        \"000000000000021b6108dc988f9153383f9501ab9001109aa87902ddd4c8a4d1\",\n        0\n    ],\n    [\n        \"000000000000022c02ff22bc0af5201f0e1a14a75879c494731e4fbf999218c8\",\n        0\n    ],\n    [\n        \"000000000000032651c988edc1ccd08e82b888cbb8135e24a958ac0c0b640d5d\",\n        0\n    ],\n    [\n        \"000000000000015aefdfa0790bed326c38c358c07aac0674f5b2e771258b8df3\",\n        0\n    ],\n    [\n        \"00000000000000822e1534c86afef911b67d3fa20cf2b12d93d20d64005f54d7\",\n        0\n    ],\n    [\n        \"00000000000000338b871276768c923b1c603fd6150bd054c2287e532e61de7f\",\n        0\n    ],\n    [\n        \"00000000000002d0af52c0cae894bf836b61137ace2bd7500abd13a584c02741\",\n        0\n    ],\n    [\n        \"000000006f8443a458f38d8731821c07a2fda0ecdbb1cf797f541844d468ce0c\",\n        0\n    ],\n    [\n        \"0000000000b6fbd8b4e227f5514979a61d8b0b918d2adc154e585ca926386704\",\n        0\n    ],\n    [\n        \"000000000f4f5e49b10278e27d9dee15b92f9d4a257138a206831e0c00188767\",\n        0\n    ],\n    [\n        \"0000000002c7e9769bd8ae9906fc5682e937b5c31ab5b5b86e4d70af2c15a95c\",\n        0\n    ],\n    [\n        \"0000000000f68a1db8cd387e0a2f93f45149fe1ee4a230bb386313bdd42058e8\",\n        0\n    ],\n    [\n        \"0000000000f0f65c360c8f0f9853ad1142f16675dc1175d61afdbef977776b25\",\n        0\n    ],\n    [\n        \"000000000004f734e634156511cbef7dfefebdf317e7488aa6c2562572d7ecb7\",\n        0\n    ],\n    [\n        \"0000000000002a46a7a16787e8317dc567ae26816324c2035be0186ba54d5cb8\",\n        0\n    ],\n    [\n        \"000000000001a593e6f01875b77e270163538d88452779bb557df7c2607c28e0\",\n        0\n    ],\n    [\n        \"0000000000004f24cfafa10bd50a452535f64be577a6161e51c7c71542f654c4\",\n        0\n    ],\n    [\n        \"00000000597cce73e84b63f08cfcb9b01f5e7621752d8c8e08fabbd6ab5c0dd5\",\n        0\n    ],\n    [\n        \"000000007cad379df01247771fff471bc99faea1b86218602f45ab13efc5e9f6\",\n        0\n    ],\n    [\n        \"000000000d6085aab25892be49c49d6c0a3949befdc3ddce2faa46b104e1e804\",\n        0\n    ],\n    [\n        \"0000000002be5996786b42d6a229093896aea9966b1854ea261e01e84da1f420\",\n        0\n    ],\n    [\n        \"00000000002684b72056e270b115d80b12b2f68eac7412355287226aecd9b5e0\",\n        0\n    ],\n    [\n        \"0000000079ea27efb24366c87856a9e371c56fcbd59d09d3164a5c2fc15fcbca\",\n        0\n    ],\n    [\n        \"000000001694120525dba4548ca54087544da1fbefa51c38f0208d683418825d\",\n        0\n    ],\n    [\n        \"000000000693e80d372938f3553151ab9d0a9a6922182591c701df739dc9a502\",\n        0\n    ],\n    [\n        \"0000000002950d9cb23c8511937811910b712f73d448e6fdc2e39e029b86848b\",\n        0\n    ],\n    [\n        \"000000000091c40056c6a48f33db17764af89c01f62ae653aa5e494146164cee\",\n        0\n    ],\n    [\n        \"00000000001f373c47e1a39af4e1ebcd8c88411ec49d6bd520c2781564070971\",\n        0\n    ],\n    [\n        \"00000000000809ca4b2170c57958709b867095b1972d80a2ee55359fbd0940fe\",\n        0\n    ],\n    [\n        \"0000000000038e7bd66fc3308447b1370dbdd0661c427c512bdbc641ff360fb2\",\n        0\n    ],\n    [\n        \"000000009a3325df76e2de1fc1970cc2f241fa8a41da9ad745a0d9666d9ff51d\",\n        0\n    ],\n    [\n        \"000000003176e92ff837bf43a48a995c1a321b166475f586ffb4b962e0254a4a\",\n        0\n    ],\n    [\n        \"0000000001ae3292e81ca3859b75bccd5bff825cd9f496efd085160c716ed05e\",\n        0\n    ],\n    [\n        \"00000000033bdac4f0d36bb912fba28bb5caa54d1b611759a10f79ff3c969cf2\",\n        0\n    ],\n    [\n        \"00000000004c6db7fa0e2c9f08693abfeb128c5827b511a5c46c623a103b416b\",\n        0\n    ],\n    [\n        \"00000000003d87f48bb95e9431760d0c5f4f93c77d02fce9dd1673e9f5b01029\",\n        0\n    ],\n    [\n        \"00000000000e214fc3d8b97571eb75d248ca29f8e25a584c33de8488ceee72b0\",\n        0\n    ],\n    [\n        \"00000000000133269b7159b828700d02de770a8cbd91f3d166e6bbc95d8e0dfc\",\n        0\n    ],\n    [\n        \"000000000000cc92e2dd933a08f7fd87f84451627982fb66583587858217c059\",\n        0\n    ],\n    [\n        \"00000000000030708136c20c4c8216314005b3cb5c551ded33b26cf64d2ff47d\",\n        0\n    ],\n    [\n        \"00000000c472a1341d479ed02f31b699e448c035049a7092670b38f4ec6121f0\",\n        0\n    ],\n    [\n        \"000000000a358834d6eed41b9b7161a338aba53828111414cdea7552ed15548a\",\n        0\n    ],\n    [\n        \"000000000e13e77372daea775c8358916e57ed11835899c14e5140ed9be11089\",\n        0\n    ],\n    [\n        \"00000000008252cd0931f94b2465bd4f93e4bfeec6697962c5b034cf3d12cf7c\",\n        0\n    ],\n    [\n        \"00000000019812cd6cde3a43831234be71e68118be24a80161349b8b327acb5b\",\n        0\n    ],\n    [\n        \"00000000005865499f301adfb59f8380743e4c3b3ab220ca4eb97dc6628df626\",\n        0\n    ],\n    [\n        \"000000000015f77e1e61329560a4378eb401fa5bf0ef90b0a014a4d7857ca7a8\",\n        0\n    ],\n    [\n        \"00000000e9cbcbb625e8a463ba8e7f14be46ba9538ffe93338784ccad3d992e8\",\n        0\n    ],\n    [\n        \"000000000fb27169efcc2873cfaac223ebb91cc5e1e5ad7e9a312d42bedf7c42\",\n        0\n    ],\n    [\n        \"000000000c9c96d62ebfbf3fa4003f1d46d175140ab084dee17e8125fa40f24a\",\n        0\n    ],\n    [\n        \"000000000311e3a766b1ab2064b68a344a561eb496d595126808ffb166c71cc1\",\n        0\n    ],\n    [\n        \"00000000677568c82262ac3a4ca3f909bdfb0b35145ad490fa3fbdc719d06b91\",\n        0\n    ],\n    [\n        \"000000000ee77ba9ab657e51fd9140f5c9b46731d9341e98188f929c97d04746\",\n        0\n    ],\n    [\n        \"0000000008a67eb9c91a6d74168f3f385270fa942ea00bdd31924d1b6ea11148\",\n        0\n    ],\n    [\n        \"00000000017f93c9e0026e90d579e18c83b4a8557f0c00e9b85ab164cf4466c5\",\n        0\n    ],\n    [\n        \"0000000000994efa379235c03711a8e6b29895d928b5fde96cb01c02374c0602\",\n        0\n    ],\n    [\n        \"00000000b3be9f23c943d71d7c7dbdf6dd672d77a712f6c83e9796a85e4379f2\",\n        0\n    ],\n    [\n        \"000000000713e1089b0b2bdcba462b740c9396f822f1c73e090713978a7f1314\",\n        0\n    ],\n    [\n        \"0000000002fc44d358401a7ac9ce4ddcb17f3cbac08e40242e755e60ab2292ed\",\n        0\n    ],\n    [\n        \"00000000021ef2c04fd30be7049f73b9a8353ac96a467dd5f0b9c1457be1bc5e\",\n        0\n    ],\n    [\n        \"000000000023b95b440ccbbdcb914172cf675cd15d6111bd7f5a436a4925d36e\",\n        0\n    ],\n    [\n        \"00000000001983521dbffd1b742a6d4b5dfda3f46579fbbdd83a2ebf9a039bec\",\n        0\n    ],\n    [\n        \"0000000000044d53dbea312432e68fa90dc2148946f613216dbdeec86f6a67c1\",\n        0\n    ],\n    [\n        \"00000000000107667692f12d21a55a72ff1dce828f96872e36c35bfbae475a8d\",\n        0\n    ],\n    [\n        \"000000000000252d1d0c01744ec25af801ef7c57e2581c95295070b6a8a85bd5\",\n        0\n    ],\n    [\n        \"000000001c1da54e16dc06158677024d9e74bff39bfaec83434ac33673fcc251\",\n        0\n    ],\n    [\n        \"00000000b4d0c6ae86bfdf7ba4c205fc3e6b3b6d63836b85e30e9d8bac922301\",\n        0\n    ],\n    [\n        \"000000002b16179cb022bf678bd847dd6fc1908d0df04abf0c7874981eb33ee7\",\n        0\n    ],\n    [\n        \"000000000e6783554aae41856424d184dc4fa061f40676efd107e6f933a25641\",\n        0\n    ],\n    [\n        \"00000000005ae4acbab519895b4b523d97a09e381c9e4b044e642f73b8c0f1b0\",\n        0\n    ],\n    [\n        \"000000000010372b59c9595d947064804b75ab21868dd075a3842ab7d2df6181\",\n        0\n    ],\n    [\n        \"00000000002f9f587ea304093be049d3142ac0c92f9c68928a4f82d12b929b69\",\n        0\n    ],\n    [\n        \"000000000005d4cae51b3c76dc3c61bed0c265c4f228c0c4d1d3d147146c34eb\",\n        0\n    ],\n    [\n        \"000000000001a5b6c0e0a0b485a490cb52ccdf9b22596656039b51545bb07be5\",\n        0\n    ],\n    [\n        \"000000000000d723d0976338edf55d08edab995dd6283cbb688855f0dca6e8f5\",\n        0\n    ],\n    [\n        \"00000000bfebfae90208a82c7fa06c0f61674dbf1e4f9162e370656c38d611bb\",\n        0\n    ],\n    [\n        \"000000000c91cd144b2a92ab5024c87f70cc1d76a4a7f26a82a98c5aaad62850\",\n        0\n    ],\n    [\n        \"00000000077c8114eb5cfb69c3924c699d0c70334360dd1daa95db0db4816953\",\n        0\n    ],\n    [\n        \"000000000348a6443e091db8f68e88a10afad7c6e3e5392247902c4b4feade43\",\n        0\n    ],\n    [\n        \"0000000000d63b70351e05829ad8a56336521b361b0d50eb7ea1f5b46c25b00a\",\n        0\n    ],\n    [\n        \"00000000004658603163f0ede572120a1bbfce8d313aa282ae54d2ffd9fe9079\",\n        0\n    ],\n    [\n        \"0000000000048063b410c793db34856f23acfb19a0ce72f5997fa572773378c8\",\n        0\n    ],\n    [\n        \"00000000000228fb6e587fa593ff8b4764064bba8bfc2f43ba5b1f12af33d04a\",\n        0\n    ],\n    [\n        \"00000000000082e3ddb75c0ea2a98922b1556ce10346f9bb0cedd97ccb3fdf62\",\n        0\n    ],\n    [\n        \"00000000000005571b54d4886b44b81c21dfbefa554cd7c23430e5aeff6b5ae2\",\n        0\n    ],\n    [\n        \"00000000306a603ca1a0d961e08e103a9f13f3615163c3373d1bd2a67cadc2a7\",\n        0\n    ],\n    [\n        \"00000000195d93ba7ae19832b622de86ebdadf3c78f1751ef2b2e9b0e3a530d8\",\n        0\n    ],\n    [\n        \"0000000000476d0d00cbc68bb20b4893f0e608b02a1e029b8c6c73e169c49e69\",\n        0\n    ],\n    [\n        \"000000000051348044bc10fc05960c244c3ccd3b3b6c145ffd9958a1c8bc0215\",\n        0\n    ],\n    [\n        \"0000000001e4df369203badca9aedc28c240d592b12d284ce0b0463fc7537c09\",\n        0\n    ],\n    [\n        \"000000000091cc1ccd448b0ec9185618a84dea96f52477cfb9b9ca2b60cebe83\",\n        0\n    ],\n    [\n        \"000000000024a50299c0ef0c6dec9c64336b6cf5c1a1b0013e22fd4fcee1d7d1\",\n        0\n    ],\n    [\n        \"00000000000349248c1df06c3783d1270cd97ce7f605b9036fca0fdc2f0fbb96\",\n        0\n    ],\n    [\n        \"000000000001afe6793e7427a3d780876d26eb7f2ded92563f991bf7302aea69\",\n        0\n    ],\n    [\n        \"0000000000007148006e139e24d9fccc307661c9a0cbcd1af983487c2f0780c9\",\n        0\n    ],\n    [\n        \"0000000000002734722a341984738177a3f6f264291424e4984f2128d921bf29\",\n        0\n    ],\n    [\n        \"000000000109b02caaa95e49a477757a41a42daed40e92f54fa09e63f5538cd2\",\n        0\n    ],\n    [\n        \"000000009a11c7ff8b8fa7fbff5a04c25906f701ab5bd67195736f9ccc839ab9\",\n        0\n    ],\n    [\n        \"000000002b1d77f8e0cd60af1c62ef6d381e8905665b15a7fbc546d0c1a45e18\",\n        0\n    ],\n    [\n        \"0000000002588cb017de9e2f23cea7edc5082f1b3faec890f9252d556efeac40\",\n        0\n    ],\n    [\n        \"00000000008b07f177adc24a4b1a64d2dbcfbcc903ba861d493e11d6b33af7dc\",\n        0\n    ],\n    [\n        \"0000000000bab8db5020aa8e052165275e8eb3e7c843533246bf6e4c8374757e\",\n        0\n    ],\n    [\n        \"0000000000138488fdca8bfc327e6dbd6c72c5f1dc5868d9c0ea886665b9b56b\",\n        0\n    ],\n    [\n        \"0000000000094021fc954efbf08be667fef1b817e8715d4093a561fc30264aa7\",\n        0\n    ],\n    [\n        \"000000000000e8183e64072db79adfc6c09b650c4178001be3fade4050b06005\",\n        0\n    ],\n    [\n        \"0000000000004c93e8661c75974cd191c68dd66999da4f70d039c0ba4a12b970\",\n        0\n    ],\n    [\n        \"00000000000021c675b3ec404bb996f5e68f9eeceeac6946e5a6822987824d33\",\n        0\n    ],\n    [\n        \"0000000000000ad85684d30f25d1ec34638f099df2f33b418a07307c68fe3c2d\",\n        0\n    ],\n    [\n        \"000000000009c6add76ac42a1942c4ce74d25d1b8975d4e3ac8932185e785a44\",\n        0\n    ],\n    [\n        \"000000001e7d828d354716881683eb6fb5caec5d91afce298e4e3bcee9574924\",\n        0\n    ],\n    [\n        \"000000000a0e438ab203d8fd3e56100f2f14759f704bff6c699df0bb4e9aad64\",\n        0\n    ],\n    [\n        \"000000000b7d5c2895df8bc1fdf5d31e0f663564cb5cff3b18642c44a71b6248\",\n        0\n    ],\n    [\n        \"000000000193209ecd92fce00a75975446423d94a325ed525c15d5ab921da273\",\n        0\n    ],\n    [\n        \"000000000020835bdc30ac67efdbc785d15186914bc14e86387f97450df46418\",\n        0\n    ],\n    [\n        \"00000000000c9078321f0030214c75e170b01ec664d39bab1b1e48460a54eb63\",\n        0\n    ],\n    [\n        \"00000000000ac68b63d486ade190dc9108eb3730d25e7537649fe21c30e0121f\",\n        0\n    ],\n    [\n        \"000000000002a94dfc5f4b677b251a7a7647dbb99c0803df8658222227fe3e3f\",\n        0\n    ],\n    [\n        \"000000000000b076bbef0e50593b1595ffb3d571e7ad95dbdf06dca8824ef7f3\",\n        0\n    ],\n    [\n        \"000000000000167075c8bcd24233d25cd268271c0e8fcb6f301ee1b6f6ff0341\",\n        0\n    ],\n    [\n        \"00000000013107aa587bcf12ac445330ff0325d73c5253f7e6a49ed8c50257bb\",\n        0\n    ],\n    [\n        \"00000000090ff53d49c9ffd51511af8d5cba2038a8e25e3b17186b1bc941f43d\",\n        0\n    ],\n    [\n        \"000000000d9e704d5607f77f8983cc56069571a3761d5bd5da55f05ec5d8e844\",\n        0\n    ],\n    [\n        \"0000000002b2b4c0950fb6390f0ae860840e84eb0a82e5e8a9bc37c14bbf43b0\",\n        0\n    ],\n    [\n        \"0000000000be10137a2434dce1d97850b768ce878c1c80ec905f6e9f21e65fa7\",\n        0\n    ],\n    [\n        \"00000000005cd966f80183d4c048e63a5c14f649298dfd261d989d9e3c026bf4\",\n        0\n    ],\n    [\n        \"00000000000e8f30e55006a4082380c4b1a372b7ad919d3a9b0a52fe5ee881d3\",\n        0\n    ],\n    [\n        \"0000000000018c70a4c27bdba237ad19ebae5d3ca23f1394ccc746d73669a1c4\",\n        0\n    ],\n    [\n        \"0000000000022acc8432c883953227786f7a6560aeaf0176d232c8affa5b25b4\",\n        0\n    ],\n    [\n        \"0000000000001854e95b28b4efcb2cfeb08c76d8cf1fb03f2055b3fb758f3a1c\",\n        0\n    ],\n    [\n        \"000000000000187080c2c39f5a3ea8be72ac4d3ec0d16b21cd34f1541bef23be\",\n        0\n    ],\n    [\n        \"0000000000001593766a3c63b524f658ec7690df467cc7bbcebbdb56385500d4\",\n        0\n    ],\n    [\n        \"00000000000012d6966dc51a41f2c617192169ec8418405e164ba83b9f7ecdfe\",\n        0\n    ],\n    [\n        \"0000000000001d0c7d0a2605e127b00448b71e756ad96625116ab8ca18f74900\",\n        0\n    ],\n    [\n        \"000000000009cb439ea49282d257595ad1f7602856c16cc26fff423f7783c792\",\n        0\n    ],\n    [\n        \"0000000000889282b98336c994d7420a639221e0484b511227fd616d78dbd028\",\n        0\n    ],\n    [\n        \"000000000071a4a2ad6767864bd21239c74c9912a40ca9fd3b209e21b66460d9\",\n        0\n    ],\n    [\n        \"0000000000f3ed2c3c9a7c3a7291e859cecba8cf9243d23a4892e6be8ea9b70f\",\n        0\n    ],\n    [\n        \"00000000006a4258ffdff8b7f6f4f685ce18c6eb1d7a1cf501ca9e02fcb7620a\",\n        0\n    ],\n    [\n        \"00000000004af78f1a109d1267a9c24d69c6a4b30fea49f0efa6c8834cf394f9\",\n        0\n    ],\n    [\n        \"0000000000193bf3efbb145747198470a81b2cd33c991057676742d5c22a64b2\",\n        0\n    ],\n    [\n        \"000000000006b436798c7e4a8c3bdbf054a66707feee5a18ce9ca57eb95bb48a\",\n        0\n    ],\n    [\n        \"0000000000001db50c7caa3a02ea4f173343f958f334a8bf3f8638add9e69b34\",\n        0\n    ],\n    [\n        \"0000000000003c621629cc0bcec5968d61d2e42c6673de4d46555118ad5001d8\",\n        0\n    ],\n    [\n        \"0000000000001262bef2918265f6dd4534013a4650444054fb4f5e490c5ed57b\",\n        0\n    ],\n    [\n        \"0000000000000120ceee972d70cc84430006645997c7337976c673bd75cbef2b\",\n        0\n    ],\n    [\n        \"00000000ba16134dc0c418a116b97ad5deccd6bf6e3daa028a8a6a80d7823faf\",\n        0\n    ],\n    [\n        \"00000000a1a00d6d6fe0660e63402a5a7c7248589211594d37fd800456ce84b6\",\n        0\n    ],\n    [\n        \"00000000394766cec78f962c29aaa715b66e3ad34e1f2323dba45e087cb3b395\",\n        0\n    ],\n    [\n        \"0000000008b15a3020676f5e084210ecc05f646885eca1cf6a10e9ae9e3995cc\",\n        0\n    ],\n    [\n        \"0000000002cf7eb98abe784f6e516670a88b9028a6faabfd099a364c2dc5c42b\",\n        0\n    ],\n    [\n        \"000000000054015fec337a9ee43eea501d2292f031f5bc1f09758d20f5cd3135\",\n        0\n    ],\n    [\n        \"0000000000068d24d31a9f1192d848155a2f90939627bc456c9a337135a923fa\",\n        0\n    ],\n    [\n        \"000000000006262bd09358258edcc455f9ba46b7f9d6e69d0f6b9da89488a4a5\",\n        0\n    ],\n    [\n        \"000000000002327bf77ae67961463ea98a78dab06c24ac7d58b1727c5f856626\",\n        0\n    ],\n    [\n        \"0000000000006672235c1606fbacd7861b16b267d203b4d687708eeb1fc25e6d\",\n        0\n    ],\n    [\n        \"000000000000ac0c9a39a47313a8715f125c46d6ea8be8741b99b1db4a8aae47\",\n        0\n    ],\n    [\n        \"0000000000007e93f6578e7856aae0ecf6341e1312664d9e1d812ff254c37ae6\",\n        0\n    ],\n    [\n        \"0000000000002a980acdb1443926875e7d4a57859b2b45ce3fa92c7716319f62\",\n        0\n    ],\n    [\n        \"0000000000683bfd82c63514bc58a80daf699a6bcd040bb2a499540baf52463d\",\n        0\n    ],\n    [\n        \"00000000373e6262928d7a6cac965b294aef35f90b72c85100ef91501775e06a\",\n        0\n    ],\n    [\n        \"0000000000f7bc44061b65c62d4d7747138df127dd2a30f583c3ebb66a25c7a4\",\n        0\n    ],\n    [\n        \"000000000212a71c38d0e13ab7c5646c949d4b7ca23afedbe351a43b7607043b\",\n        0\n    ],\n    [\n        \"0000000000a836e88f76ee5dcca1e884572f32f4460a3b024280738d76e98ced\",\n        0\n    ],\n    [\n        \"0000000000413f6c1b1c9841961636bb3290f2410ba0731f3522c4ff3faa2e0e\",\n        0\n    ],\n    [\n        \"0000000000082336107412226110ab2a53016d4faad4deec048828507a300248\",\n        0\n    ],\n    [\n        \"000000000000a91e7a3f35a23f01621dd051e314da617714991467131808d3bf\",\n        0\n    ],\n    [\n        \"000000000000cd6576950f6f238227c3ba7f62405ed1bf3af4878c6dc1b04635\",\n        0\n    ],\n    [\n        \"0000000000674099e9741e44da03e9531402a2607a19a65660b57470340828db\",\n        0\n    ],\n    [\n        \"0000000030c4744001ae85f9e6b46ed0664449927b86b8fbf25b22b851d23671\",\n        0\n    ],\n    [\n        \"00000000002f5095ad1a12eb9eedf88ce1e7268368461b6b4e10051148f436cb\",\n        0\n    ],\n    [\n        \"000000000057d3e2a77eadb8b9613cb839ab02a96094dd5d0a6d1f09026c3936\",\n        0\n    ],\n    [\n        \"00000000004e0a28be887d6ed037cd9102cbbda7d6c9e584ba51f2c2dce96232\",\n        0\n    ],\n    [\n        \"0000000000211346d8099f7ecea72481c4cd45591f5e0d7e347725ac2162f142\",\n        0\n    ],\n    [\n        \"0000000000199ae9fc06c5acee766db6033b86f76c266cadefe1461c611c2198\",\n        0\n    ],\n    [\n        \"00000000004c9e5748558d4f5a75bc824171e3b958152dfd6844330f1e907f8c\",\n        0\n    ],\n    [\n        \"0000000000137addf1521361dad1ee007eb9e6dd4eb8441492ebfaa3c240d556\",\n        0\n    ],\n    [\n        \"000000000054d4c77bb7964e5327c35760d87b890ea336aec5ecdeb783350738\",\n        0\n    ],\n    [\n        \"00000000006b7b06d04818e97a4df66164b471912f88d9cd02de4af6c8bbe74f\",\n        0\n    ],\n    [\n        \"0000000000380fa9858e3e90335c061a3776a26bee1e8b6851de33ec63670782\",\n        0\n    ],\n    [\n        \"00000000000842598b03fb79ce7386e9f9181a02dcf1effc8f70d3ff7368ccd5\",\n        0\n    ],\n    [\n        \"000000000003d3475edecd733fc7b82432882d9c9f1350a98ef8921b87db4dec\",\n        0\n    ],\n    [\n        \"00000000000000e330a8d57a38dbcc0b0a5dc7a4210f231b8082b9be5f9e4bce\",\n        0\n    ],\n    [\n        \"000000000000218ff87fd50cfba2fd04203a78d2600cb2c4dcb039d803426e19\",\n        0\n    ],\n    [\n        \"00000000007c96e6e3ed3146260348ac79ea7dc2ec2ae6bf8dc203400a37721d\",\n        0\n    ],\n    [\n        \"000000005abaa10bf7260470c28ba32f1755b4cfd3734aad580681e39a9605a5\",\n        0\n    ],\n    [\n        \"00000000005e77c226e6fffccafa56055e68f0ea0a30101e6a243ab9b3e07db0\",\n        0\n    ],\n    [\n        \"0000000000e989fe27f85b89c1e852d7bc94b09033cc6c8b32fbbbd9383a9ae1\",\n        0\n    ],\n    [\n        \"000000000091a1e962438583146293ef34156962445ffc5e81e4d0fe327d37ac\",\n        0\n    ],\n    [\n        \"0000000000477978a6903217e2817d10e99bdfedb4f8bc396b96fd5b0b93b522\",\n        0\n    ],\n    [\n        \"00000000000bfd9e5f13a9c03c48e8b58a937cf1ae2849160f1ca11f8fcced3c\",\n        0\n    ],\n    [\n        \"00000000000158dd3c31b6379887b4353ef2898c03b7ce55458fcd57cb6f0639\",\n        0\n    ],\n    [\n        \"00000000000029d7009eb56b9d38366005576b82a9b59fc845522a34ad36a38a\",\n        0\n    ],\n    [\n        \"0000000000e6e207a82b8ad7136352204bb8e9ccfcd25885a715d3c65cbee997\",\n        0\n    ],\n    [\n        \"0000000000fadc4429f50fc534ccac4db5e51a313df25034d6c5c25f7e83448c\",\n        0\n    ],\n    [\n        \"000000000019c58defcfdab6c6ab9497685e61118effda4c2613bf44be19fcbd\",\n        0\n    ],\n    [\n        \"000000000006cf444d846093c5045d42ddc0986ca805f261476d0fd2eb474c39\",\n        0\n    ],\n    [\n        \"0000000000d0856a3d6a1e5b1ac7e388cc029bd8410b3b1489598974fe470568\",\n        0\n    ],\n    [\n        \"00000000003d9aae63ed532b78082ca5386211e22410fd24ebd5318d1a4cd1da\",\n        0\n    ],\n    [\n        \"00000000000345003879f86021a6d5e3fe93813246818c145947b7e225691177\",\n        0\n    ],\n    [\n        \"00000000000175393730cde3e49de7af2b81ae736eee005a9f9c4a1e878c52ec\",\n        0\n    ],\n    [\n        \"00000000000087a8c621c879aec2a897258632d6aa631b9a38ba4d564e08682a\",\n        0\n    ],\n    [\n        \"0000000000002ea641b2975935bd9caf337b51ac9f9bb90a54f6ea6ee5d3112b\",\n        0\n    ],\n    [\n        \"0000000000000c544f9b6a8cbab6d25caf949875622bf75139234850b10affe1\",\n        0\n    ],\n    [\n        \"0000000000000f66fc4e37232a29f3389c493863a980d58a1d570eddd5268999\",\n        0\n    ],\n    [\n        \"00000000001213fe2bbb8aacb1fc14983586e09db964151cb507956a81b35f25\",\n        0\n    ],\n    [\n        \"0000000000ba82c2160602ddc1913bc4c133ad0af8848e014367c84110d00e05\",\n        0\n    ],\n    [\n        \"0000000000b7a98b364b1cf9521275a915c7a1b3a0f0c052c7d8efb620ec0870\",\n        0\n    ],\n    [\n        \"000000000047dc62db23540ab4aee43e54812aedb623a2a158aa3244fc784722\",\n        0\n    ],\n    [\n        \"00000000005291002da10e53c3855882251a6e5a425b5e639ef9be3bd05767ca\",\n        0\n    ],\n    [\n        \"00000000005ffbcbc0d9b380584bdc78050a6f0c3582b4c9c5103a150cbc71f5\",\n        0\n    ],\n    [\n        \"00000000000a7a69cc06b0a68b27a8fa5d29727ec3b6db8d32d61cf7489b5ff3\",\n        0\n    ],\n    [\n        \"000000000007212eb8c49758d98cefaa6098da2b877a6055be341f5f7c0ad301\",\n        0\n    ],\n    [\n        \"000000000068d1099d8cf3f43f6d164f2925b1d52ede75640cc65ca020e1de1c\",\n        0\n    ],\n    [\n        \"0000000008d5ddef4468a4414bd08184c2eba0ec536b85a743b1091828a6a884\",\n        0\n    ],\n    [\n        \"000000000acae40db93b589783b0cde70b98552955cb3c12f08de1b417d9008d\",\n        0\n    ],\n    [\n        \"000000000066a51eaa3a54036f338719da3d5779180c0bc3787b533410de90e5\",\n        0\n    ],\n    [\n        \"00000000008b521677a6e897950aac69640e52efb01b7af10bba3820ecd09a89\",\n        0\n    ],\n    [\n        \"00000000001823f0e399311cab0fcf57403e094feebf99b22030bafd2004da87\",\n        0\n    ],\n    [\n        \"00000000000bf821c2abf5bcd00ca96439ddf5b0b593be5601145fda5338efdc\",\n        0\n    ],\n    [\n        \"000000000003f4fd19b2af0141289177014ecc6dce6ea8fb50bab93d4a291095\",\n        0\n    ],\n    [\n        \"00000000000011842d892a02e55ca594caddc9f3cea1979ddffefc070eda8498\",\n        0\n    ],\n    [\n        \"000000000000208aa0259d20f51c0e7b8895e18a93aea79af9b3832e710ef134\",\n        0\n    ],\n    [\n        \"00000000000007218f849e72dee1f7fb6fcf36f3b6745c6468187ed2ed13287f\",\n        0\n    ],\n    [\n        \"00000000000f79f656cae641c2b74554c6ecd673c0c7550671c4c2af940661b3\",\n        0\n    ],\n    [\n        \"0000000000199b4d178c05fd1c3154c9a4632eadc7bfc734c4522176c977ce8a\",\n        0\n    ],\n    [\n        \"00000000085d0682d481635cb2e6de2e4d9884589455a86194f0b222f9acb3c6\",\n        0\n    ],\n    [\n        \"00000000015972a5a6786a14b009bf582c4bbf7b9854591dd8d26f82b43ddaef\",\n        0\n    ],\n    [\n        \"000000000064bf72b7bdbfcbe96dbbd0efcaf7aa94c0f92cb4e6662819468fe4\",\n        0\n    ],\n    [\n        \"00000000003df36b7962bb4ad62266c462382eddc93f4bfeac464b95f7a89ee9\",\n        0\n    ],\n    [\n        \"000000000006516d3a9f424eb61db5dfb85aeee29708b78c65d24827bd926263\",\n        0\n    ],\n    [\n        \"000000000001c1709fe1b294712638db356e89155650f6fbecde79ec47a92af7\",\n        0\n    ],\n    [\n        \"000000000000dfc23251344b593c16c28cd195abcb337519d7bc82175721a033\",\n        0\n    ],\n    [\n        \"0000000000000aae2dd2bf0b8581d137fcfa3d9c4cadbe3ef3834d7cae4268c0\",\n        0\n    ],\n    [\n        \"000000000000092a5baff3d9a5ae87689b2afe668e71bac3b342c7d383f0060f\",\n        0\n    ],\n    [\n        \"00000000000fa906eeff7d2e126698d88b8cda01d32ea2c039c26984daaa17a3\",\n        0\n    ],\n    [\n        \"00000000002d4315e5bdc2bcfdb245b914130764a50943a2b2e02ea3acf5c47b\",\n        0\n    ],\n    [\n        \"0000000000fc2bc9bb83e04cbe922d64719295bfef6320027725402306bcf1a0\",\n        0\n    ],\n    [\n        \"000000000142690e7c334b97612746d6db208e6153bdfa8479d86d1b575feacd\",\n        0\n    ],\n    [\n        \"0000000000629a7820e8cdbbed18dcfe16c992152badc745ca73b9b34e53fb0d\",\n        0\n    ],\n    [\n        \"000000000023c2e9dbf3fe03248e40f4ec3fb2dc81ac573d5a6a4f490c701877\",\n        0\n    ],\n    [\n        \"000000000013658a43b6d1c4be95fa36e32d3edf80716de3a8f7e98858016adb\",\n        0\n    ],\n    [\n        \"000000000007c847295d8c4b6da9d8a64b57c3a2307e64387bf8882b9d35d6de\",\n        0\n    ],\n    [\n        \"0000000000032bf90b823332af80bd2ea18f411f081c7dca8f2fe79d9215526b\",\n        0\n    ],\n    [\n        \"000000000000001bc0655da6f24c6952e811006897a0c6dd8b6bd94f178636c8\",\n        0\n    ],\n    [\n        \"0000000000001e1d09b15393190cf686e25488db7fcbc2f1ebacc8165fe6e3a0\",\n        0\n    ],\n    [\n        \"00000000000cc79ae066badb4157def4067057cefd705bf87f1d832845a7ab36\",\n        0\n    ],\n    [\n        \"000000000014408398244b94b4eff6b54875802ede6df2d1d21915333a195719\",\n        0\n    ],\n    [\n        \"0000000000114135a1bc757110c05162fa649b694db9569be117e34832c87257\",\n        0\n    ],\n    [\n        \"00000000009b15fb2bcee1af904989ba0761e4cddc6b3ee214c0bb07dac6211f\",\n        0\n    ],\n    [\n        \"000000000012be506dde2c54adf355bdb41a457b0abec436202a3be73f0b052c\",\n        0\n    ],\n    [\n        \"00000000000963760ceb5fc65570650d494805e05c9d753f3ea6d44247ad3d08\",\n        0\n    ],\n    [\n        \"00000000000bfec54977673f68b6fe5f088398e697d778fa7987f8bab6a70825\",\n        0\n    ],\n    [\n        \"000000000000e7f428bb413c17032c0031af0d26133ba93f744a5a0c16cf7e1a\",\n        0\n    ],\n    [\n        \"00000000000036bc80378323c6eaff8ab350b6d89955f602960cb7c93d2feb4c\",\n        0\n    ],\n    [\n        \"00000000000f0d5edcaeba823db17f366be49a80d91d15b77747c2e017b8c20a\",\n        0\n    ],\n    [\n        \"00000000001ff8fd57798082ab5a7452ada211e1c3be38745155505601498829\",\n        0\n    ],\n    [\n        \"000000000020f960b535eac585e5810ad64f158c1142f0eecd925c8058172933\",\n        0\n    ],\n    [\n        \"0000000000067bd89409368d221507a160e5c45972eeb01efe210054fe8e7d85\",\n        0\n    ],\n    [\n        \"00000000003521f2d5ea3232d4835ca6c6bae083ba90458f67d4cd765ce93b09\",\n        0\n    ],\n    [\n        \"000000000005ab3ff3a0c484eff7b571fb78ce27d93f77a480074232e5ce0c1d\",\n        0\n    ],\n    [\n        \"00000000001048c9eca7cc1cbb86946c04498052071f7e7c775bba565ada337c\",\n        0\n    ],\n    [\n        \"00000000000154caacde41be616f924d7d478812148242fba85605eefec9ac61\",\n        0\n    ],\n    [\n        \"000000000000c34f75bd6f338c0206a31a8d5021cc2ded51e88a6ef4fe686d10\",\n        0\n    ],\n    [\n        \"0000000000001e0581d86c49a6ca14ba88639ef908abb09210b57989e06b1a1f\",\n        0\n    ],\n    [\n        \"0000000000d0e6dc0bf830b50bde3e400e16ec4f772f92a55390e62d4aa73af3\",\n        0\n    ],\n    [\n        \"00000000069c2501a2f32cc69af72a602ff674438ae04dd05516f72a71b9ab26\",\n        0\n    ],\n    [\n        \"0000000000c926b38954550c9b8d363ff058c2eb135eebdb3e640cfa67df803d\",\n        0\n    ],\n    [\n        \"000000000011e9ad9c18e9e2095c3662af5be1e918dff653758583aa45dc8197\",\n        0\n    ],\n    [\n        \"0000000000f311624ff4dcdf07400d0d2fec8b16b14c1c16babc377a2d85ad21\",\n        0\n    ],\n    [\n        \"00000000002e455cabfdc2a8955e8ddfe717b12efe5b80937b0c0ad6ac977fc5\",\n        0\n    ],\n    [\n        \"00000000000fed8889a22339b340f599ac7908e790bfc3cfca9b78078a52d228\",\n        0\n    ],\n    [\n        \"0000000000012ca4492956b3f859b00e5db14b54d422cd95c68c7150743db365\",\n        0\n    ],\n    [\n        \"0000000000004c58e8f7bac59eb4a036764a4d8e0da51c0290858ab14fb72481\",\n        0\n    ],\n    [\n        \"0000000000002f60bc99563ff5b4b800c176fe8bde95e8f968fd6b53d74c9cef\",\n        0\n    ],\n    [\n        \"0000000000000bffd10a3fb0b5b86d8b2561f39d07f8a4c41dfa08e3e49b7db5\",\n        0\n    ],\n    [\n        \"00000000000006a296be9cd8fd4e3145c146863adbe08b71831abb8a869d032c\",\n        0\n    ],\n    [\n        \"0000000000000c557f496e82891039ff22e277bd604be6e2e8b95e519bee91f9\",\n        0\n    ],\n    [\n        \"0000000000399b30d2111c4bf3051c1f7f2f35bba7ff290d92393341ae47df55\",\n        0\n    ],\n    [\n        \"000000001f88733439e4e8d3c474504aed62037faa16f3845b4c671f69732e26\",\n        0\n    ],\n    [\n        \"0000000018aa2f93d2ab76a7e2f1bf5b565b4a1b0ececb6ee46490984f6c0d4b\",\n        0\n    ],\n    [\n        \"0000000005e22674fcf65ce7be896a0557205ab26d1f76d73a717f5f14a6d6ad\",\n        0\n    ],\n    [\n        \"0000000000223d866b324c097973210f8fc715c9535908359d61d8e1ab2f0100\",\n        0\n    ],\n    [\n        \"00000000002b321fd6452ab43849bd7a781953ec4485554e0fdc579f2a52c90a\",\n        0\n    ],\n    [\n        \"0000000000173132748c51b5754b0341232325bd118455bf3c8d25164d3eb92a\",\n        0\n    ],\n    [\n        \"00000000000143158cdea5fbb9453bbe1a7a900e6feba1e2193e4f5c106d9fba\",\n        0\n    ],\n    [\n        \"0000000000014677751456af5630025b3d9921a4eafb4d36a06498f0c6a84c56\",\n        0\n    ],\n    [\n        \"000000000000243976cf2d30ecd3cb1fd0b805fba4da92d2758f78e1c6f8ae92\",\n        0\n    ],\n    [\n        \"0000000000001323db1ab3f247bcb1e92592004b43e4bed0966ed09f675cf269\",\n        0\n    ],\n    [\n        \"000000000000017a410c22c4b6caf710f5ccf005d644caf276ea8626a538798d\",\n        0\n    ],\n    [\n        \"0000000000170b2b1374e3a0dfdce2fbc5e302e1e0e9fb419dc057c9959902d1\",\n        0\n    ],\n    [\n        \"000000000015b4fad4d929630487680cda2d3aada138c58cc08241ef6dd4ab09\",\n        0\n    ],\n    [\n        \"00000000000abebab869f1620843d413a3d9e06dc7d9f5201a414d547ace1f99\",\n        0\n    ],\n    [\n        \"00000000000b0bdaf05c2fe8b12ebd2372f49d8eabcfbccdadd68b5e5b7c9565\",\n        0\n    ],\n    [\n        \"00000000000ca1af42ee1be2c8895d94f39dab5fcdbe0b4b4065f4be534e7294\",\n        0\n    ],\n    [\n        \"000000000069d0cc8c0452bf86cff87db05232f801a162acab2d080d6e4e9ea9\",\n        0\n    ],\n    [\n        \"000000000019c7f7685f5bdc3afbb5e978cb3f4f70fea7b2b410139741303b53\",\n        0\n    ],\n    [\n        \"00000000000d3874ce21db78f4d1883ad9ae8b26c1d7c13f3d723ff85629d595\",\n        0\n    ],\n    [\n        \"0000000000033f87c25275ff72b58630d8da90221f2c84bcbd77c8e615709f8b\",\n        0\n    ],\n    [\n        \"000000000000dc72adaaae6483eb6737de7d21b3a24b2426330e80b078ceaed1\",\n        0\n    ],\n    [\n        \"00000000000002fb1337228db02ac464565271f22f045c1b6ee5e449f057a829\",\n        0\n    ],\n    [\n        \"00000000000001902376ff640d3088899af0819dbd15f602156a13ac2fc8e94e\",\n        0\n    ],\n    [\n        \"000000000000007ee49761a1c8284a3b8acefa39e37e455be4773d648e2db794\",\n        0\n    ],\n    [\n        \"00000000000005b4d495a77f57018dbc72bf47993d494349329a3c653f04ab93\",\n        0\n    ],\n    [\n        \"000000000000009dcb3ae6d68828e2f5ccfd58780abb260354e74484106f81ce\",\n        0\n    ],\n    [\n        \"00000000a3ceb118021fb42d39be52db951c6f852bb9a241046e972706f7329a\",\n        0\n    ],\n    [\n        \"00000000574e8e1c27fa54c77b4e7cd1b79de070f0d3ad5b383206ab9777d983\",\n        0\n    ],\n    [\n        \"0000000039d562f640c1743421d53e7e04c3e8ba222c339fff6f3d25b1d4a7fe\",\n        0\n    ],\n    [\n        \"000000000001cb1559d55c697871e18d5c26800f77fb11587241bfbec3b15e26\",\n        0\n    ],\n    [\n        \"000000000006e01a93090319756c7ca826ef655feb0cc2ef9abcc59d67de5e5b\",\n        0\n    ],\n    [\n        \"000000000000a81aaf5a4c013032638a077af6aad8bc449d74daef8ad3a74419\",\n        0\n    ],\n    [\n        \"00000000000087d0574963c1582f2161298e2de5e48f74566291ef9afc2be24a\",\n        0\n    ],\n    [\n        \"0000000000033251e71c347cd663945fb68efe82a8c6666c0b41e93f1c46658d\",\n        0\n    ],\n    [\n        \"000000000000f592857e6f0e4711b5b93fdf95f2b21a5963bde15be750a07908\",\n        0\n    ],\n    [\n        \"0000000000004353c8426e18b942a5012934ddac8322b86d6ab98ed7c0ee86ed\",\n        0\n    ],\n    [\n        \"00000000004f027845b699f42e7d0d30c530e99524c5f97186ce6a250a5fac42\",\n        0\n    ],\n    [\n        \"000000002fc6407edc060df90785082834867331e6746a43ed34a26fbdc5df64\",\n        0\n    ],\n    [\n        \"0000000000048733007c91ea3665bd4e1653b10799e3f43abee0fe830ffbb3ad\",\n        0\n    ],\n    [\n        \"0000000000025a9b1c5afceba0c78c4b0320797acdc1ad50b4e040f148fbff7f\",\n        0\n    ],\n    [\n        \"00000000007ca6d026d27387edc1c5570de41c61bacbcb1dad2c0f300b49e637\",\n        0\n    ],\n    [\n        \"00000000000258f683a77ad509da82a4fab24188fdb4b4690e212c50794a9abb\",\n        0\n    ],\n    [\n        \"0000000000015111bce7b6ac13c930484e14e31e13e43355cb4d63c8f1782440\",\n        0\n    ],\n    [\n        \"000000000001ca074fdecac7749d95f28f10c83a7e13787fd865bfbe505382bc\",\n        0\n    ],\n    [\n        \"0000000000001c11a6505dd44ab405fdc07ddfc015f3c1166a5d9352ab58b52c\",\n        0\n    ],\n    [\n        \"0000000000000c83f7f8e1cab4efa08d6c68c4555fb6ab542e01b87edd8f56ac\",\n        0\n    ],\n    [\n        \"00000000000009561d0ceba15388573d2a994aff24512ec3ed7d7881aa0997dd\",\n        0\n    ],\n    [\n        \"00000000007dc7cfbbb94db1fbc076a70a1252fd595686b4d75b2ea77ed6ee9e\",\n        0\n    ],\n    [\n        \"00000000000251feb68a8c90852f73aeb29ebda191038737b7edd37c9475f4ac\",\n        0\n    ],\n    [\n        \"0000000000013f9a97045ea9047654e514951288911b2c3986787c27bab49106\",\n        0\n    ],\n    [\n        \"0000000006e8c37735c61f22bec69f4cb7eba03172349e7012b7704652f3e83a\",\n        0\n    ],\n    [\n        \"0000000001f341add5657043d8e50e53ba079fe24966a2668f904be5579c84b9\",\n        0\n    ],\n    [\n        \"000000000029a6275cd477d77939424bd183c2f1308a9912f45aa7cc9ed13b56\",\n        0\n    ],\n    [\n        \"00000000000a0336239e5e1faedf5bd2eedf38c9a5ba34a832356aea70aeb102\",\n        0\n    ],\n    [\n        \"000000000003c1a2b25093a64eb624055f6a3a26e18b8e7ea2d9382ec7a3609a\",\n        0\n    ],\n    [\n        \"000000000001bd89bf7e8740ce22adfa6e8793bd1716a647e558ed1742ee8329\",\n        0\n    ],\n    [\n        \"0000000000001320421f1bb2c94000e11a621f581fc277c0e2911c3b89f680bd\",\n        0\n    ],\n    [\n        \"000000000054ce90a949f5ae2d43c4ace599668c6ccbc50620f6d5705922ea7c\",\n        0\n    ],\n    [\n        \"00000000200d16fea4857e6b73169cc593421a57971acdbcaf87a31d7d8d72c8\",\n        0\n    ],\n    [\n        \"0000000000e75602181c88f713b91c49de291ed878be305d25b75c0ec5fbe942\",\n        0\n    ],\n    [\n        \"000000000081f8169c3c3665f20351dc0fe499612ae232ec0b55858a8e5dc6e9\",\n        0\n    ],\n    [\n        \"0000000000d7ad232e7593fb435d125343b8113bbdb3705ab58ac0e18c26cc79\",\n        0\n    ],\n    [\n        \"0000000000076df615d887e33193ca2dc0f2fc0e70744512c95da6242e9b1a81\",\n        0\n    ],\n    [\n        \"0000000000084a62093d1929843e74456686429b698a7ea9b1901c1565779f58\",\n        0\n    ],\n    [\n        \"00000000000251d1da01e9de9fcaf3ca3a64bff78a5faf51a8e697dfab6b5e4b\",\n        0\n    ],\n    [\n        \"000000000000609a8798996b1f1fe0b66060a628eadc380d0d369a2318c2d0ec\",\n        0\n    ],\n    [\n        \"00000000000014770aeab044a022e86d888a6ede75b6474022c71aead3a1db74\",\n        0\n    ],\n    [\n        \"00000000000004101d04ebc90ade5d4b911aa13c038ecf25e9887d877203ddb8\",\n        0\n    ],\n    [\n        \"000000007c700410b61eb7ff1aaccbfc3a79e4e4484ad7a2b0eda4d91dc4b613\",\n        0\n    ],\n    [\n        \"00000000055ff438a031413ee042fd3c0a2b69be98690542806ff123b7988024\",\n        0\n    ],\n    [\n        \"000000002eca5f9f2c3b656d2550662fdee4c95da133eade51a5cae653bc69fe\",\n        0\n    ],\n    [\n        \"000000000c679b76ccf0c5b943095fdee8fa466311edbea2c4a05f9430ffef3f\",\n        0\n    ],\n    [\n        \"00000000007c6f494e32d5d9de58fa008a770fdc0a7b4a141be5b7c2de3ab970\",\n        0\n    ],\n    [\n        \"0000000000d5dcd5a26c8ad29c1293e70401e2f90d8288469df3816b8cc6d4aa\",\n        0\n    ],\n    [\n        \"00000000000d754d94f36cacbfb620710672afb1558499cabe17ca62c54a7d3a\",\n        0\n    ],\n    [\n        \"000000000004096bb78fba714b130f7f1f929e2803c75a7a85619f7a2b86567f\",\n        0\n    ],\n    [\n        \"0000000000020e686c38d44c35896df35f9f1b7723a82a826a5e2393c25ef68c\",\n        0\n    ],\n    [\n        \"000000000000504f9af6885c0cb6484109ea205a956c8efae9557a1f5b9233da\",\n        0\n    ],\n    [\n        \"0000000000000e8746e52e4320ec17e66434a3936a3825f7046fe874e92275fb\",\n        0\n    ],\n    [\n        \"0000000000000f48d818a9a026270c9f733f629959bea25192596d59874b1ce2\",\n        0\n    ],\n    [\n        \"00000000eaa9214cb05b241828a1cfb0c4209fb7ea64429815d61f7c1d98939e\",\n        0\n    ],\n    [\n        \"000000001f7f915a6002cce4edd5cba392307f3a199a520ee8937327a9135162\",\n        0\n    ],\n    [\n        \"0000000009674ee0c606d687bdcddf8e023462927e2902b3381bc4bb862a7397\",\n        0\n    ],\n    [\n        \"0000000001f3f3528c083a4b11eb2f04d8bbeca92b57f05d8282909bde78bc77\",\n        0\n    ],\n    [\n        \"000000000131917ac459aefb91774dbb42caeca497afc0cfd1766e0338cc7f88\",\n        0\n    ],\n    [\n        \"000000000027634444081e1289354cb50034a506bb306a2ac1d8280683771c5c\",\n        0\n    ],\n    [\n        \"000000000017a852acff78fbee573329d45bb8b121e9f6fc1e4f687bb3778ada\",\n        0\n    ],\n    [\n        \"000000000006789e1a00eca982fb2827f680b254c4a0ecb005af4464f3585a02\",\n        0\n    ],\n    [\n        \"0000000000015d2e9f54b1e9419d6b32ce68ae626cdd7f2a1954f22ca39ae0fa\",\n        0\n    ],\n    [\n        \"0000000000002f7893bc169165ed9fefb434b6201103f23cc84a747a68ff8797\",\n        0\n    ],\n    [\n        \"00000000000008471ccf356a18dd48aa12506ef0b6162cb8f98a8d8bb0465902\",\n        0\n    ],\n    [\n        \"0000000000000596f00b9db53c4111bcde16f3781471c5307af1a996e34ec20a\",\n        0\n    ],\n    [\n        \"000000000000007b5d2406f64f5f5833c063a6906552e815e603140c00bca951\",\n        0\n    ],\n    [\n        \"0000000093ca5d935740a1b25f10ce092fd777c2bb521f3156619389ae78931e\",\n        0\n    ],\n    [\n        \"00000000292f3a48559527341f72400a0f8a783aebcaae5bfa0e390dfaa5286b\",\n        0\n    ],\n    [\n        \"000000001e852ed7ddf0108d1fce0f4f686f43c8c1b85bcb12c43e564dc7630e\",\n        0\n    ],\n    [\n        \"000000000c4bea8fb1e7f3a1f3e6c6b3f71388c0ec7eef3de381853767e89f87\",\n        0\n    ],\n    [\n        \"00000000029ef31a21711b55c4300efa38ace0b706091e373f48285286f2c578\",\n        0\n    ],\n    [\n        \"0000000000979060786bb008f193d3917e28667bb1b28329f3adadc172e4cce7\",\n        0\n    ],\n    [\n        \"000000000019030ceb98013b1627517b45b04ee055ef445813bbebaa25fa1ed3\",\n        0\n    ],\n    [\n        \"00000000000adf202247bb794fc9a3c82cd8767143f1e6ed5f60940ee18b09a8\",\n        0\n    ],\n    [\n        \"000000000000b19061e2481d8be6183b3d881b0d58601072d2a32729435f6af3\",\n        0\n    ],\n    [\n        \"0000000000007a6d34f59b29e8d4da53e51e3414acd18527466d064945fe19fc\",\n        0\n    ],\n    [\n        \"0000000000002e66ca213a2c3e9eb5fa62de29feb83880a0bd29f90fca8ad199\",\n        0\n    ],\n    [\n        \"0000000000000b4ca10aa100728d0928f37db5296303db1b74ffe29e4a17b6cd\",\n        0\n    ],\n    [\n        \"0000000000000143309f6b19567955743775f61f8dc6932c0b46cf5fb11c6c72\",\n        0\n    ],\n    [\n        \"00000000000000b04d5409b3ac60cc18c0b9a3d58b303594635a8f75a9d2abd5\",\n        0\n    ],\n    [\n        \"000000000000040a2699f62a552703a278608248c2ce823f4cd8845376e9a371\",\n        0\n    ],\n    [\n        \"00000000000005cfcb850db7e83d4963994f958bae9b1de1483f5aeb3d449925\",\n        0\n    ],\n    [\n        \"00000000000190f80220e70c1481153671a7c90fd856988c183ab0e3d9313df8\",\n        0\n    ],\n    [\n        \"000000009374563a06178641d06776f66554c2a094b5319f0801fe35cef72ccf\",\n        0\n    ],\n    [\n        \"00000000003e4e6e5e8e4a89e7de50eed104d4a49d2992ff101b6740beec7cb5\",\n        0\n    ],\n    [\n        \"0000000000618cd377d14aaa441cbdb92527894f98da316eca81664f8ab5488d\",\n        0\n    ],\n    [\n        \"00000000000d977ab2897885fee712f58612fce8c10ffbe9400326fe3429b77b\",\n        0\n    ],\n    [\n        \"00000000000c3575b487dd0f938c5bc744fa65ca4ca3a9c981b8bda903ec110b\",\n        0\n    ],\n    [\n        \"0000000000247ac689595ed8d62678bfe53e5af13c0f5455e558f5e6bb375c16\",\n        0\n    ],\n    [\n        \"0000000000093d175376aa621176511f335a48f824b66d998e8082f85134a48b\",\n        0\n    ],\n    [\n        \"000000000000c0c0448fe922f2c737946297d35f2c25ad7cc223e11bbe58e1f8\",\n        0\n    ],\n    [\n        \"00000000000016abe4e7c10ddb658bb089b2ef3b1de3f3329097cf679eedf2b5\",\n        0\n    ],\n    [\n        \"000000000000242757cea5b68c52b83dd8c2eb9257492074fc69dfa30bd4cbf4\",\n        0\n    ],\n    [\n        \"00000000000006813f3dd7726a509fbe3101835db155dfd35d44aeae6aedb316\",\n        0\n    ],\n    [\n        \"000000000000053cc4f39cff1c8cee1aff7e289a85dee84164d2d981afc8f17a\",\n        0\n    ],\n    [\n        \"00000000000000789724805cf1d37ef689acf52c47a460507f540d5e5ca79bfa\",\n        0\n    ],\n    [\n        \"00000000000003d71618bb8952887f65540270a5e54d6246b9419e08831b5e4e\",\n        0\n    ],\n    [\n        \"0000000000000251a513a33eadfad67c015f6e3b291dfd0ae1cc4bb3a43006dc\",\n        0\n    ],\n    [\n        \"00000000968009e3f8d6e6071e7def68298307717a9af6c2d44986deaae297d5\",\n        0\n    ],\n    [\n        \"0000000062bcacb734df83bbfa3e1b9a8dfa570ecffb6c29eaaf8e9498cccd30\",\n        0\n    ],\n    [\n        \"000000001d4618c0931bd3c25ee592c35341f30ff3b549a671f637b3c26ef414\",\n        0\n    ],\n    [\n        \"000000000418b329df96a004f1b652ad06a7ca295f9f2e711c412d00493f5a86\",\n        0\n    ],\n    [\n        \"000000000302bfb88e9027237d023c4b969e106c9a7a23a84103776de7880836\",\n        0\n    ],\n    [\n        \"000000000069b9f7d9134fd93c8b7e3af8b26bbcbb5553af02fb6ed644d7fca5\",\n        0\n    ],\n    [\n        \"00000000000411ec444240ee91e2777ad18b80dee854e3e838e32209e84774fa\",\n        0\n    ],\n    [\n        \"0000000000007c73f322eba4dee5463305227c7e1a8139f1b7b296444f265052\",\n        0\n    ],\n    [\n        \"00000000000129adf0f9c0242aedbb9d87935d67ee4ddea758c00344d4b6a29e\",\n        0\n    ],\n    [\n        \"000000000000343594e671158b6e1b4b6499f6ad66e2aeabf1f6d295d3dba850\",\n        0\n    ],\n    [\n        \"000000000000320f0d5c22ba22b588b97a0e02273034bcd53669b1c8c4eeda1b\",\n        0\n    ],\n    [\n        \"0000000000001e8cdb2d98471a5c60bdbddbe644b9ad08e17a97b3a7dce1e332\",\n        0\n    ],\n    [\n        \"0000000000000026c9994ccdd027e86f51a2e36812f754bd855a7f9b1ca56511\",\n        0\n    ],\n    [\n        \"00000000000002746a820a2c08b35b8d0493c4b5d468fcc971b9c88003e84849\",\n        0\n    ],\n    [\n        \"000000000002949f844e92645df73ce9c093e5aac0d962a0fa13eb076eec835c\",\n        0\n    ],\n    [\n        \"00000000000156fbda67468ae2863993b98a41227c420246e4bc4e68c84df0e8\",\n        0\n    ],\n    [\n        \"000000000003b43c6c807122c8dd10e2a0cffbf72946f41c97c1ab82d416f74d\",\n        0\n    ],\n    [\n        \"000000000004e0635c2438b1b649007e5d424b3de846299a8db53049ebf4da0c\",\n        0\n    ],\n    [\n        \"00000000000258e4b79e3cca2ab7d12b35ba77fc491572f2e794f0a10f5236d9\",\n        0\n    ],\n    [\n        \"0000000000f5816875d9fece105e499b0467b8fb23ea973c48d828a235acdebd\",\n        0\n    ],\n    [\n        \"000000000001353bbaec810af7a4c74b4964ae072361c0889ed6d59cf16db6fd\",\n        0\n    ],\n    [\n        \"00000000000b354d8c389473670ca6bed7e3dffa069f270d35ec9dad810af141\",\n        0\n    ],\n    [\n        \"000000000002fa1f39e7cd8730fa08085ba2b532146ad1ef3b400a13e835ca36\",\n        0\n    ],\n    [\n        \"000000000000d2c7943eee59652a9783bff27e474a92ec206c5c6e3cdd58d0d7\",\n        0\n    ],\n    [\n        \"00000000000036034181b4d9a84a97490b49fbee4262b9cfb25a7bfc9c0eec9f\",\n        0\n    ],\n    [\n        \"00000000000007deb59381cce692f152fc902732d96a7e7d463bc83915b37c0a\",\n        0\n    ],\n    [\n        \"00000000ea7d32833462c0f72ade0cae4766e6065caa4e510331929c56d16632\",\n        0\n    ],\n    [\n        \"000000000068fce0ddd370d4c8f9129a7bc7843e75fc57666202d3b90239e269\",\n        0\n    ],\n    [\n        \"0000000026b4a2212c9c9493f8bd9d5331cab6d8eda8ee017410e58a783ca069\",\n        0\n    ],\n    [\n        \"0000000009535ea2dc7e83c31cd17f1db1bb78b0a678fc0610844273de143bf5\",\n        0\n    ],\n    [\n        \"00000000008607cbd5baca91d5b8b82ee965aace335744a3e21578af22bee8ba\",\n        0\n    ],\n    [\n        \"000000000030dcedae0f5e98c4e176f9569ce76c4d4135bb028fc3144ef381d9\",\n        0\n    ],\n    [\n        \"0000000000297c3f0e3fa85731222ba934a955bf513247a72a33c74c498cadbe\",\n        0\n    ],\n    [\n        \"0000000000020a0d4a1e8120cbdb486e758b58919c9df12e0edc8ca1f2795e94\",\n        0\n    ],\n    [\n        \"000000000000078773afc9023182bfb6534a60158672e6bc6e8aa5052854da80\",\n        0\n    ],\n    [\n        \"00000000000102ecdd67800807d9e137357805b9bbf8a439ed86bde5b19fbeb7\",\n        0\n    ],\n    [\n        \"0000000000005c3d2e3c7ee737c67ab465533acb233e0df902c1525fc11c3a55\",\n        0\n    ],\n    [\n        \"0000000000001a77771650cdbbceff87caa4461391ba6a4ddc9815b5b0ab47b0\",\n        0\n    ],\n    [\n        \"000000000000071ec390bbd28fa2a84e52ab5b32901d0723d22646b04ae01dc3\",\n        0\n    ],\n    [\n        \"00000000000005c3ec3194f710c6f26ee736d59cc935ddfa574440f39846433a\",\n        0\n    ],\n    [\n        \"00000000000001cc3df6924591939269d61ead563b9eb68402a2ca01d7ff99e2\",\n        0\n    ],\n    [\n        \"000000008c778b3554ceaf3a13a856acbfe46b5750fb86fd92ba30651c2852f4\",\n        0\n    ],\n    [\n        \"00000000107ca31f75f8ea76073dda3c33330b2706c1ec20c3ec240e853b65c5\",\n        0\n    ],\n    [\n        \"0000000006ba99b08e7f2869ce113e2ad7464891de7b4cfa96f330d706a2da46\",\n        0\n    ],\n    [\n        \"000000000f31036bd51b2818f6dfb90ada9be5019abf55fb15694b181e269865\",\n        0\n    ],\n    [\n        \"00000000004fcc101bc47eb7a379b9f608d5c00ac04d2d0ea165ae2937070796\",\n        0\n    ],\n    [\n        \"000000000044d5ca3eda838edef0df7e69e1934047f8482822ce58ff7a18466d\",\n        0\n    ],\n    [\n        \"000000000029bdfb157be6d400c4dd3370d98afdd8cd3db6f1ada8c19bbf4650\",\n        0\n    ],\n    [\n        \"000000000005e9699ad8035caa4f73af781ac2040c87b8aa77459b3607209aa8\",\n        0\n    ],\n    [\n        \"000000000001c0ba033f7d85beeaa167c9bde0e192240653a7ff6d9b81679842\",\n        0\n    ],\n    [\n        \"0000000000000e0176111f29e800b49c7b8c7226dbbf4df715f1a4f06bcaaa49\",\n        0\n    ],\n    [\n        \"00000000ac3bb2cf42192e9053f5384355228a2b3d70b4ece4d45773a5d5ddd2\",\n        0\n    ],\n    [\n        \"000000000f29f7b60842b1044b2db7998e9bcbd92f8ec6fe8d159c6d582f1f1a\",\n        0\n    ],\n    [\n        \"00000000352f86bc5f9760961a25de009940508bb2cd0b37f378fbc87dc97eef\",\n        0\n    ],\n    [\n        \"000000000e9b3086008679ed57f59857f64c3954368ba1088117dbf88d5839cd\",\n        0\n    ],\n    [\n        \"000000000015324bd8fed0e61b62bd1d6c663b862cb98ea03c494a92e4a8d0af\",\n        0\n    ],\n    [\n        \"000000000020475a181b7a084b341860a72fc0c1fdfcc13a85adeb0471444b0f\",\n        0\n    ],\n    [\n        \"0000000000031905c508a975707b74f24e733880382775ee0e6250666473e1d8\",\n        0\n    ],\n    [\n        \"000000000000ca38b15d2ea33a6eef505a9c661540a18882f79ba9a3c575a9bd\",\n        0\n    ],\n    [\n        \"000000000002739979a7a89fa279303b6606885e750b19e91ed637d7f222b392\",\n        0\n    ],\n    [\n        \"00000000000091e935fc266facc2c92759d5468a39aee5be6b76b519a9bc7567\",\n        0\n    ],\n    [\n        \"00000000000006e339938254208203b67c3c400f703fc29535fc646699e36e58\",\n        0\n    ],\n    [\n        \"00000000000008f6f1d1150d77f93a7f1baa24b65ceb471b1825b2e92ca6997c\",\n        0\n    ],\n    [\n        \"000000000000004894e1edcc5421dbcec77d47c5c50bf27b2cff3f1c242c9eb3\",\n        0\n    ],\n    [\n        \"000000000000054e97fb5e1a8bd7900f7c329385895761aaa40d11b3c75b0c8e\",\n        0\n    ],\n    [\n        \"0000000000000600f4bcc5a89527eede43d1d3342dc12eee1371ab534b0102dc\",\n        0\n    ],\n    [\n        \"00000000d1ad5c3ef8c3bb4610b34c264e4ca1ea51c4c8bac18b215e7dc96948\",\n        0\n    ],\n    [\n        \"0000000062f6a07ae11f9724b8ba9dc2b7348ffd02b59edd3cd2bf387fab9723\",\n        0\n    ],\n    [\n        \"000000000014e4c97c9b09ff20203213f3336b0927fd19d214cef1f544756e39\",\n        0\n    ],\n    [\n        \"0000000000d004681880e127aed3fa73255a2e75c2e5c8580cd555526614b294\",\n        0\n    ],\n    [\n        \"000000000008093189bba28d40662d6964afc1c0fc9b5c1681bbe32e8bee6c0b\",\n        0\n    ],\n    [\n        \"00000000002df10cf8165b2204ef4db6721c8c2119d60463b040fbc81c266bbf\",\n        0\n    ],\n    [\n        \"00000000000c28c789e7cd9800b98c1dd32e2dda54048116ff47ed856a14acfb\",\n        0\n    ],\n    [\n        \"000000000003e8e7755d9b8299b28c71d9f0e18909f25bc9f3eeec3464ece1dd\",\n        0\n    ],\n    [\n        \"0000000000004b95a0103abe2cb97806caca76f6922d9c5df003cf4a467df822\",\n        0\n    ],\n    [\n        \"0000000000005f12d2ab72bfa715860444c281265ef77e09dc2d041ce89506c0\",\n        0\n    ],\n    [\n        \"00000000000016eeedb3f367daaee93334188db877fb01cd0282b990f60812b3\",\n        0\n    ],\n    [\n        \"00000000000001daf3bd8306b6f6899af8aa656d87ac2aa37d493fdcb0cb3000\",\n        0\n    ],\n    [\n        \"0000000000000390b86892ad0bed9b520783056961cad7362ace8049aa00471c\",\n        0\n    ],\n    [\n        \"00000000000002105d01b4de7d3e3ada9c757a239151d50b5dd193e3951a23cc\",\n        0\n    ],\n    [\n        \"00000000000002362fa802df308201a4b1fff2fd8a91892915a46f5d54098ff4\",\n        0\n    ],\n    [\n        \"00000000000004fb8aa6c6aecb64b9d8d7e691a6cd56fad69fc5278b9e8d98cb\",\n        0\n    ],\n    [\n        \"00000000000000ce3bd9752b2508ddae1ee71332e905163a3c0d7e10b8c472f7\",\n        0\n    ],\n    [\n        \"00000000000002d0d8520982f15a45d4a405334c61886b6d13d95843386af647\",\n        0\n    ],\n    [\n        \"00000000cafd25502ad67d5d409edfc98f5bbd3173e86e085c69658d58da5f70\",\n        0\n    ],\n    [\n        \"00000000b01e0675317a29a07731ea092fa029016a40ed8bb4fc17cde50eda05\",\n        0\n    ],\n    [\n        \"000000002676805396ed2883ccc8ad401aa0a974627559fbae2416ba5c54999c\",\n        0\n    ],\n    [\n        \"0000000000030ab759158f3d425824228dc5c91f32db91d404bee29ee3a41878\",\n        0\n    ],\n    [\n        \"00000000000da1c8040ec08e7490fb201ca1fb3571f29c0efd3351ae197d3017\",\n        0\n    ],\n    [\n        \"000000000004e3cba890c16ffc7d1c019d4ab88afa39315164e1b08b8e6a9330\",\n        0\n    ],\n    [\n        \"00000000000bdcfb630b43977be44529e54daa02d199014a0967deac669bd060\",\n        0\n    ],\n    [\n        \"000000000007254038f9c621d6df0d9fbd90b5697e4170cd6090daaf579f3790\",\n        0\n    ],\n    [\n        \"000000000002263e27ea1cec943632bf469a28b067f0bfde3b9a6b48540981b4\",\n        0\n    ],\n    [\n        \"000000000000f194a8d17e683d17f222d23a9032f034d4dc4497263fd785dfa0\",\n        0\n    ],\n    [\n        \"00000000000036e359b7b07044e3cd5b132a3c72501a0f3f9ccde167f5316bba\",\n        0\n    ],\n    [\n        \"0000000000000b10e98a90e0fd1ffbf7d5fc5a76e8e6e960c6fb158711af6f48\",\n        0\n    ],\n    [\n        \"0000000000000104e1e4303b8dae78389bb4e6c38f3eb3fe42aec6464bd5c397\",\n        0\n    ],\n    [\n        \"00000000000000bde368a635921f5ad25aeb4b784651de24d624cf20c27691c7\",\n        0\n    ],\n    [\n        \"0000000081a626a33cff134e7e56dc0f0a67b1735c96256774885d5d095807c0\",\n        0\n    ],\n    [\n        \"0000000055d357cdf39130eb767f416101e79025515906bea528f43cb6446920\",\n        0\n    ],\n    [\n        \"0000000012558b30f9c1a156fd80b02451e8dfcc7fe0350fb4adeeb84951a0a6\",\n        0\n    ],\n    [\n        \"000000000001a4868924fc7cca0334ffc4dd49c07fb841c1da059a7c219bdf95\",\n        0\n    ],\n    [\n        \"00000000000010086bd2bba88c71b08cfc7e24183d610a2803e6d382049d52c0\",\n        0\n    ],\n    [\n        \"0000000000018c83992fe05d820b097228de93787e3f59e65cb89ad4c385e364\",\n        0\n    ],\n    [\n        \"00000000000023ab80324770ff4c6802d09e5e1e7de78d2a8e64783904d47f19\",\n        0\n    ],\n    [\n        \"000000000000287fa294ea557835d8c98bfe94c4d8b18d5b10f1b62d68957113\",\n        0\n    ],\n    [\n        \"000000000001d842f5a0dff13820ba1e151fd8c886e28e648a0be41f3a3f1cb3\",\n        0\n    ],\n    [\n        \"000000000000906854973b2ec51409f0b78b25b074eef3f0dbb31e1060c07c3d\",\n        0\n    ],\n    [\n        \"00000000000009e694e22b97a4757bffef74f0ccd832398b3e815171636e3a85\",\n        0\n    ],\n    [\n        \"0000000000000594b95678610bd47671b1142eb575d1c1d4a0073f69a71a3c65\",\n        0\n    ],\n    [\n        \"00000000000002ac6d5c058c9932f350aeef84f6e334f4e01b40be4db537f8c2\",\n        0\n    ],\n    [\n        \"00000000000000c9a91d8277c58eab3bfda59d3068142dd54216129e5597ccbd\",\n        0\n    ],\n    [\n        \"0000000000000051bff2f64c9078fb346d6a2a209ba5c3ffa0048c6b7027e47f\",\n        0\n    ],\n    [\n        \"000000000000df3c366a105ce9ed82a4917c9e19f0736493894feaba2542c7cd\",\n        0\n    ],\n    [\n        \"0000000000007c8006959f91675b2dbf6264a1172279c826ae7f561b70e88b12\",\n        0\n    ],\n    [\n        \"0000000000015ab3720de7669e8731c84c392aae3509d937b8d883c304e0ca86\",\n        0\n    ],\n    [\n        \"0000000000016d7156ee43da389020fb5d30f05e11498c54f7e324561d6a6039\",\n        0\n    ],\n    [\n        \"0000000000009c9592f83d63fe39839080ced253e1d71c52bce576f823b7722a\",\n        0\n    ],\n    [\n        \"00000000003dee6b438ddf51b831fbedb9d2ee91644aaf5866e3a85c740b3a99\",\n        0\n    ],\n    [\n        \"00000000000155f5594d8a3ade605d1504ee9a6f6389f1c4516e974698ebb9e4\",\n        0\n    ],\n    [\n        \"000000000001e21adfc306bf4aa2ad90e3c2aa4a43263d1bbdc70bf9f1593416\",\n        0\n    ],\n    [\n        \"0000000000008218e84ba7d9850a5c12b77ec5d1348e7cbdfdcb86f8fe929682\",\n        0\n    ],\n    [\n        \"00000000000054fb41b42b30fff1738104c3edca6dab47c75e4d3565bc4b9e34\",\n        0\n    ],\n    [\n        \"0000000000002763b825c315ba35959dcc1bd8114627949ede769ac2eece8248\",\n        0\n    ],\n    [\n        \"00000000000007437044da0baed38a28e2991c6a527f495e91739a8d9c35acbb\",\n        0\n    ],\n    [\n        \"000000000000032d74ad8eb0a0be6b39b8e095bd9ca8537da93aae15087aafaf\",\n        0\n    ],\n    [\n        \"000000000000006d4025181f5b54cca6d730cc26313817c6529ba9ed62cc83b3\",\n        0\n    ],\n    [\n        \"000000001c3ad81ffea0b74d356b6886fd3381506b7c568f96c88a78815ede09\",\n        0\n    ],\n    [\n        \"000000000140739d224af1254712d8c4e9fb9082b381baf22c628e459157ce49\",\n        0\n    ],\n    [\n        \"000000000306491c835f1a03c8d1e17645435296d3593dacba8ab1a7d9341d38\",\n        0\n    ],\n    [\n        \"000000000002b383618b228eb8e4cfcf269ba647b91ac6d60ddd070295709ad1\",\n        0\n    ],\n    [\n        \"000000000000c90fc724a76407b4405032474fc8d1649817f7ad238b96856c6a\",\n        0\n    ],\n    [\n        \"0000000000002d5a62b323a5f213152dd84e2f415a3c6c28043c0ccaaddb3229\",\n        0\n    ],\n    [\n        \"0000000000008c086a21457ba523b682356c760538000a480650cd667a29647a\",\n        0\n    ],\n    [\n        \"00000000000007c586d36266aa83d8cc702aa29f31e3cc01c6eeac5a0f5f9887\",\n        0\n    ],\n    [\n        \"0000000000013bf175e35603f24758bf8d40b1f5c266e707e3ba4de6fae43a7f\",\n        0\n    ],\n    [\n        \"00000000000096841c486983a4333afb2525549abe57e7263723b9782e9cfef1\",\n        0\n    ],\n    [\n        \"00000000000012dfd7c4e1f40a1dd4833da2d010a33fc65c053871884146c941\",\n        0\n    ],\n    [\n        \"0000000000000b47eb6bc8c6562b5a30cefcf81623a37f6f61cc7497a530eb33\",\n        0\n    ],\n    [\n        \"0000000000000021ca4558aeb796f900e581c029d751f89e1a69ae9ba9f6ebb3\",\n        0\n    ],\n    [\n        \"00000000000000a5bf9029aebb1956200304ffee31bc09f1323ae412d81fa2b2\",\n        0\n    ],\n    [\n        \"0000000000000046f38ada53de3346d8191f69c8f3c0ba9e1950f5bf291989c4\",\n        0\n    ],\n    [\n        \"00000000658b5a572ea407ac49a1dccf85d67d0adfc5f613b17fa3fff1d99d51\",\n        0\n    ],\n    [\n        \"000000005d6be9ae758c520b0061feee99cd0a231f982cc074e4d0ced1f96952\",\n        0\n    ],\n    [\n        \"0000000001aa4671747707d329a94c398c04aaf2268e551ac5d6a7f29ffd4acd\",\n        0\n    ],\n    [\n        \"0000000004b441b97963463faca7a933469fabfa3e7b243621159e445e5c192a\",\n        0\n    ],\n    [\n        \"0000000002ce8842113bc875330fa77f3b984a90806a5ec0bb73321fef3c76c6\",\n        0\n    ],\n    [\n        \"0000000000019761bf9a1c6f679b880e9fb45b3f6dc1accdbdcfce01368c9377\",\n        0\n    ],\n    [\n        \"0000000000008a069efd1a7923557be3d9584d307b2555dc0a56d66e74e083e1\",\n        0\n    ],\n    [\n        \"000000000001c14cec52030659ef7d45318ca574f1633ef69e9c8c9bd7e45289\",\n        0\n    ],\n    [\n        \"0000000000009cfccb8a27f66f1d9ff40c9d47449f78d82fee2465daca582ab7\",\n        0\n    ],\n    [\n        \"0000000000007f30cfae7fbb8ff965f70d500b98be202b1dd57ea418500c922d\",\n        0\n    ],\n    [\n        \"0000000000002cbd2dbab4352fe4979e0d5afc47f21ef575ae0e3bb620a5478a\",\n        0\n    ],\n    [\n        \"000000000000017a872a5c7a15b3cb6e1ecf9e009759848b85c19ca6e7bd16d2\",\n        0\n    ],\n    [\n        \"00000000000001ade79216032b49854c966a1061fd3f8c6c56a0d38d0024629e\",\n        0\n    ],\n    [\n        \"0000000000000090b8dfe4dde9f9f8d675642db97b3649bd147f60d1fc64cd76\",\n        0\n    ],\n    [\n        \"0000000000000109ed5f0d6fc387ad1bc45db1e522f76adce131067fc64440ec\",\n        0\n    ],\n    [\n        \"000000000000003105650f0b8e7b4cb466cd32ff5608f59906879aff5cad64a7\",\n        0\n    ],\n    [\n        \"0000000000000113d4262419a8aa3a4fe928c0ea81893a2d2ffee5258b2085d8\",\n        0\n    ],\n    [\n        \"00000000000000f15b8a196b1c3568d14b5a7856da2fef7a7f5548266582ff28\",\n        0\n    ],\n    [\n        \"0000000000000034fb9e91c8b5f7147bd1a4f089d19a266d183df6f8497d1dff\",\n        0\n    ],\n    [\n        \"000000000000005e51ad800c9e8ab11abb4b945f5ea86b120fa140c8af6301e0\",\n        0\n    ],\n    [\n        \"00000000000000e903f2002fd08a732fd5380ea1f2dac26bb84d57e247af8ac2\",\n        0\n    ],\n    [\n        \"000000000015115dac432884296259f508dae6b6f5f15cef17939840f5a295c3\",\n        0\n    ],\n    [\n        \"000000000029913c80e5f49d413603d91f5fd67b76a7e187f76c077973be6f8a\",\n        0\n    ],\n    [\n        \"00000000002e864e470ccec1fec0ca5f2053c9a9b8978a40f3482b4d30f683a9\",\n        0\n    ],\n    [\n        \"00000000001ccf523df85df9abdb7c5bbad5c5fcbd12a4a8eb4700de7291f03b\",\n        0\n    ],\n    [\n        \"00000000002aa81027df021e3ccde48dff6e7f01a4aba27727308f2ce17f2f1a\",\n        0\n    ],\n    [\n        \"000000000015a577d71d65bde7e8f5359458336218dc024584f7510b38dc1259\",\n        0\n    ],\n    [\n        \"00000000003aef1877bcc6817cac497aeb95af3336ba2908e8194f96a2c9fc29\",\n        0\n    ],\n    [\n        \"00000000000ccd42d542ddca68300ec2a9db2564327108234641535fd51aa7f3\",\n        0\n    ],\n    [\n        \"000000000000a2652b2e523866f3c4d5c07dc1c204d439b627f2ab2848bfa139\",\n        0\n    ],\n    [\n        \"0000000000002c065179a394d8da754c2e2db5fed21def076c16c24a902b448d\",\n        0\n    ],\n    [\n        \"000000000000175a878558186e53b559e494ce7e9f687bf0462d63169bfcce03\",\n        0\n    ],\n    [\n        \"00000000000007524a71cc81cadbd1ddf9d38848fa8081ad2a72eade4b70d1c1\",\n        0\n    ],\n    [\n        \"0000000000000159321405d24d99131df6bf69ffeca30c4a949926807c4175ad\",\n        0\n    ],\n    [\n        \"000000000000016c271ae44c8dca3567b332ec178a243be2a7dfa7a0aef270c3\",\n        0\n    ],\n    [\n        \"00000000000000a7d62de601cdf73e25c49c1c99717c94ffd574fc657fd42fa8\",\n        0\n    ],\n    [\n        \"0000000000000052d492170de491c1355d640bae48f4d954009e963f6f9a18c3\",\n        0\n    ],\n    [\n        \"000000006f5707f2f707b9ddcce2739723e911210b131da4ca1efdff581212ad\",\n        0\n    ],\n    [\n        \"00000000021be68dc9c33db0c2222e97cd2c06fc43834e8f5292133c45c2abb4\",\n        0\n    ],\n    [\n        \"00000000019ca3eaf7c39f70a7a1a736f74021abf885bebc5d91aa946496bac5\",\n        0\n    ],\n    [\n        \"00000000006e4752fbe2627ebb2d0118f7437908a8219f973324727195335209\",\n        0\n    ],\n    [\n        \"00000000038471612a0955307f367071888985707ec0e42c82f9145caed8fea1\",\n        0\n    ],\n    [\n        \"000000000004604d2d7d921b21d86f2ade82ded3af33877ec59d47072023d763\",\n        0\n    ],\n    [\n        \"000000000034a3e45665a8dcbb94e7a218375a5199b3f3ca2cc7b5fe151bb198\",\n        0\n    ],\n    [\n        \"0000000000043fb2c2ff5db60c6d2d35a633746e8585e04a096a9b55a4787fe6\",\n        0\n    ],\n    [\n        \"0000000000020d4d8735b66134c1fcdd1d3f3d135b9ff3f70968ef96c227fb75\",\n        0\n    ],\n    [\n        \"0000000000004f3f4dc1fa11a6ad9bd320413b042eb599c4599a14d341f6825f\",\n        0\n    ],\n    [\n        \"0000000000001e0a495d23acf46a44f8b569ada39ac70730da5e9109871b77e9\",\n        0\n    ],\n    [\n        \"00000000000002257a08acca858f239fabb258a7cc1665fc464f6e18e9372d32\",\n        0\n    ],\n    [\n        \"00000000000002845d416fbfa05a5d40ba5ba5418a64f06443042a53cf1fd608\",\n        0\n    ],\n    [\n        \"00000000000000fee91a2ae8b8d1bb9a687c9b28b0185723c8ff6ffdac2e9ce4\",\n        0\n    ],\n    [\n        \"00000000000001d6874b4d88e387098c0b7100ff674d99781fc7045a78216a15\",\n        0\n    ],\n    [\n        \"00000000000144a03e701c199673d72fc63766bcf0cdaf565f4c941c7ef72971\",\n        0\n    ],\n    [\n        \"000000009b6cc4d8aee22cca6880e4d7bb30bff2851034ad437d63d3a7278de7\",\n        0\n    ],\n    [\n        \"0000000023e998d64618475e31b4aee9d83d2bc32cb6d062aa97c0b4651fed08\",\n        0\n    ],\n    [\n        \"0000000000036f4bf6b42a7776a97872fa24362064c5bc4bc946acb70ab6fbf4\",\n        0\n    ],\n    [\n        \"0000000001e2252455ffd0cf0b4109ace996a0d2a03999f5cc5c5e08fb6130ac\",\n        0\n    ],\n    [\n        \"0000000000002713db42d53f0c2d86c904f4e0338652acc1cbda953c530a15bb\",\n        0\n    ],\n    [\n        \"000000000001b075f9ccc604a50326732f5d42373c4a831978be0e2d830cac75\",\n        0\n    ],\n    [\n        \"0000000000000bfa7d93c6b36298b933b1a652c95ee9f0de4151e007f3180391\",\n        0\n    ],\n    [\n        \"000000000002c60a0af1cfeb9c26c60970b354897fd0a94c8e5c414d0767b06b\",\n        0\n    ],\n    [\n        \"0000000000001f2d9462507a9408859fb0b5f97013d6b4577337b0382340c5aa\",\n        0\n    ],\n    [\n        \"0000000000000b7428e0d3c6c7fd2df623a74125db4989b1c61c78eeed1bcde5\",\n        0\n    ],\n    [\n        \"00000000000002e8b4f1fa041a37515c1b76d59994792f1c772c9a4993c194dc\",\n        0\n    ],\n    [\n        \"0000000000094e70c0cf5185b480542a1faa8392a3f2f7f583d91e033856d7ce\",\n        0\n    ],\n    [\n        \"000000005b036d8c18ed5d1219e4137bd71438c9b1ba7ff4d10a626e9a7bcc98\",\n        0\n    ],\n    [\n        \"0000000008745d4a943e958f5cb5084646c0fe1cae57eeab666c3ad0d4ff1dec\",\n        0\n    ],\n    [\n        \"00000000000f8c5b3455e540d074b5c71709e37f8950975953798d27bdc701fa\",\n        0\n    ],\n    [\n        \"0000000000050885884f7ac233bb174cf7b33c037f81907f7766afe9d0ad9091\",\n        0\n    ],\n    [\n        \"000000000002d7cd1043ccd0581a47d6fdf82a7cf1646b61495f917a48ebeb5c\",\n        0\n    ],\n    [\n        \"000000000003a2b3e3d7ef47829db1672bfd79e49f32ef3a04ec7c4df355392b\",\n        0\n    ],\n    [\n        \"0000000000032a6c7e5bc3878c1815bc6759594a4736638fdacaa5642be3e649\",\n        0\n    ],\n    [\n        \"000000000001386a3904f0ba4f25dc7ace09b67a6fe8977e7aecc55813fa9ac5\",\n        0\n    ],\n    [\n        \"0000000000003fe030a2231da87076679c1d38d323bf56b45ceb49a5128fb4b1\",\n        0\n    ],\n    [\n        \"000000000000147cd3b6195c6a727cd4fe6b3a879d7934e52bf29020ed9c6fcc\",\n        0\n    ],\n    [\n        \"00000000000003ed5a0a7176f3f1b3ed26510045af2860e5b6313b358774fbad\",\n        0\n    ],\n    [\n        \"00000000000000c2952ac8a580895ac13799a9c29badb6599bc4a86c1fc83b6e\",\n        0\n    ],\n    [\n        \"0000000000000056f49d6f7b8243eecf6597946158efe044b07fd091398e380d\",\n        0\n    ],\n    [\n        \"000000000000006b039683c36b18ec712346521edce4dc5b81cdaf6475d89bd7\",\n        0\n    ],\n    [\n        \"00000000000000525de83fba2439549ef0ed78d6d08516a0513abb972b0fca95\",\n        0\n    ],\n    [\n        \"000000000000006c5403ae9c42acf37362885c75c1a71a6b7fe20f9cfc5304a7\",\n        0\n    ],\n    [\n        \"000000000000006f881a62bc5ec9d4c4da83ddc6619a7eee82617e26e2c7ef3c\",\n        0\n    ],\n    [\n        \"000000000000012941300197c5b6627a66f9cf48ae9c6791b36c63c0218a1be9\",\n        0\n    ],\n    [\n        \"00000000000002cd7ec2e00992a4dc6c5e0a56cfbc19b5afa9730bd94f174b5b\",\n        0\n    ],\n    [\n        \"000000000022e09ee2ee7b3fd223cb9ccfe11058cca5ad0c705fe5a0c26b28dc\",\n        0\n    ],\n    [\n        \"0000000007d35ebaf81412d40d1224bdc5792bfbc70827c09f05dc5fb168e67f\",\n        0\n    ],\n    [\n        \"00000000328e1b1aecf68947ad53fb11c58a383704ddbb8b29704669e22225bd\",\n        0\n    ],\n    [\n        \"000000000003d3b3f171fd10fda1be9d4464b1438bb9443081c2c224a047cc4e\",\n        0\n    ],\n    [\n        \"000000000001e3c5dcea0586d3c8f69c0f35658fae283d29f64df9b5301bc721\",\n        0\n    ],\n    [\n        \"00000000000ce5f3757a0cab09a8cb131b3f2c63303375ad1c84fe423866d33f\",\n        0\n    ],\n    [\n        \"00000000000ca01b96070fb643bcebbc862cff4da78dcd52de1418c940d4f466\",\n        0\n    ],\n    [\n        \"0000000000006eb74e5036cf42888759c4ebf91a5eb128463e60ae9ab02876a3\",\n        0\n    ],\n    [\n        \"000000000003aae0765dfee956b322477d786a2cde617ff073e0bc4eeaf7c252\",\n        0\n    ],\n    [\n        \"00000000000033421d804b4bc0f7dc61715d2fc0cc2a98904ff5e1f9ef909010\",\n        0\n    ],\n    [\n        \"0000000000002a24b916b5f03bd47250276ad32f08a1684334c7f181b0b7a055\",\n        0\n    ],\n    [\n        \"00000000000002a7399ec806255c4ae63d7583001bbde70e2038e9b90fb824f4\",\n        0\n    ],\n    [\n        \"00000000000000ec89aaa13c7222b3ec787a487cdc7a17c1ee87ce313e6ed4d3\",\n        0\n    ],\n    [\n        \"00000000000001564cf9db3397bd0983a68f450d5b7e59824339fe1d46ba1c75\",\n        0\n    ],\n    [\n        \"00000000000e932953388774b6b3492d8756f936d74fda1d33eace33538fb0bb\",\n        0\n    ],\n    [\n        \"0000000084c2d56f703e72f6ad637105409552792ee482bbc14376cfb29c30d9\",\n        0\n    ],\n    [\n        \"00000000392f30ba333fac2e4937e162105ba2b20fe953848b1a4c004f460223\",\n        0\n    ],\n    [\n        \"00000000000842b42c56e4dc573efd9b6b6864dba81730c4f95b837d52078ad5\",\n        0\n    ],\n    [\n        \"0000000003e4cca12f6109687fcccfc5c3827bf3bca2487096fec0293b4b351e\",\n        0\n    ],\n    [\n        \"00000000007b7eece3ebbf77ed583a711c8427284ea9b556ec67efd14e7f5d90\",\n        0\n    ],\n    [\n        \"000000000002c0e026657401be7998fce1618869ec073a49ac935a15d16c5741\",\n        0\n    ],\n    [\n        \"00000000000cf19ef67151f6d06b426371dfa63d9d2bbd6024cca520cf4d96b4\",\n        0\n    ],\n    [\n        \"0000000000019a6ef183423833a4347d77e8687b4fc83a85f4c98c579631acbe\",\n        0\n    ],\n    [\n        \"000000000000a292b9ff43becd4770243d2750e2b3c4e81a6ed79b8abd2f5052\",\n        0\n    ],\n    [\n        \"000000000000280db4a9a31097024bc81f0358ba624f1f8dd83a2362a156a817\",\n        0\n    ],\n    [\n        \"00000000000009b17b295d898cda8899ce547183fd63fa901b9f502aed00c45d\",\n        0\n    ],\n    [\n        \"0000000000000013f5c40f6b0e7e8fe854045135564a4df6ff4ca736861d7ea8\",\n        0\n    ],\n    [\n        \"000000000000c39ffca7d1daad0d4f8af9ee108443bb1b4352cd740fd8297aef\",\n        0\n    ],\n    [\n        \"000000000002f42ee90d7d459393eb90e2ea5a3ed292394ce1dc5f7a42d66ce0\",\n        0\n    ],\n    [\n        \"0000000000010d6bd31805e0a9b8629192c0ad704641d2b08c28865052bbf469\",\n        0\n    ],\n    [\n        \"0000000001015f5067612dc0d681d71b33d278c50ca88d7756322ab90f753290\",\n        0\n    ],\n    [\n        \"000000000003dadd324301ee6157c29e7aa9f120edefaf05369d849510e6d60c\",\n        0\n    ],\n    [\n        \"000000000000a62107ea11c5db9929d819181d8903624e9088b8700d1dc66ea7\",\n        0\n    ],\n    [\n        \"00000000000022b91e1b652f626cd3a81bfb2ff70717ace53c488dd45c75fcbb\",\n        0\n    ],\n    [\n        \"0000000000002845027a6a08c436c6e99aa8af0f7c744a722fd598ba0f66f4cb\",\n        0\n    ],\n    [\n        \"000000000000ae5347baecbcb3cd01265f0e52c8819f830dcfc6dafa1ec4327a\",\n        0\n    ],\n    [\n        \"0000000000008dd3169522647ae90ca0a3acc405f0e8c2b53dab013433708921\",\n        0\n    ],\n    [\n        \"00000000000023abea5dd709951fb1fa5c34a75670ddc7eea46d2d23c6033669\",\n        0\n    ],\n    [\n        \"00000000000006fe20edd4be3beabc4432fbe410ab53466660105ced53056190\",\n        0\n    ],\n    [\n        \"000000000000003f6d6889d2917ba88f6e286c156028baebf05be409e1b97ef8\",\n        0\n    ],\n    [\n        \"000000000000005d871f102aaa25e60855c96c1aa8404f004db1c8bbfab341e9\",\n        0\n    ],\n    [\n        \"0000000000000197fac06dd6c7f80c838b6a21f1ce72f10aa6ba0aff40c3cb92\",\n        0\n    ],\n    [\n        \"0000000000000289a999cf132efbee896d8c22e2f9d1036381b00d72c41660e3\",\n        0\n    ],\n    [\n        \"00000000e9f6bd4700dea0c0841272461e4e9d125b8fe2c35a2ca39f77269321\",\n        0\n    ],\n    [\n        \"00000000f91f03ac1d08214a3646c2bef1878961a8c40d867254d733fd9cb2a3\",\n        0\n    ],\n    [\n        \"000000003d42ef351c6a1fb5e2d43d1a28ca095052be35ad9bb901b097c667c8\",\n        0\n    ],\n    [\n        \"000000000014b426a9844698b6369c0e2befe4e369f1dd01c157dbdd472c9136\",\n        0\n    ],\n    [\n        \"000000000016dfa525db05b9db92a080e0da65a4a0b15e538649eb4c0c670cf4\",\n        0\n    ],\n    [\n        \"0000000000027a82eb5b1ab46a276a9aa19e3a1e52e2328c07a50db314664148\",\n        0\n    ],\n    [\n        \"000000000026945c53ba1f9b0c34f9e502f3aa64c9979ce583b93daf347d2292\",\n        0\n    ],\n    [\n        \"00000000000f64a42d38e16119aa724e6d859d8b7ed2964bd0929a226e57c838\",\n        0\n    ],\n    [\n        \"0000000000011bee42dca16315be14fd0be451e4385c787a66c7dc6c0a498ce2\",\n        0\n    ],\n    [\n        \"0000000000007fcace99545546c5ee4df862e21840543865ad0944ca7b82baf7\",\n        0\n    ],\n    [\n        \"0000000000003b3a9be8e418e11db77aa16dbf9f04a9b43b34466e7b41520fa2\",\n        0\n    ],\n    [\n        \"00000000000004ae741f8cd7f6f20231f8be6b89946e50339f0089a2e5c6d4d6\",\n        0\n    ],\n    [\n        \"0000000000000379b21385de297e65a62e4d15ee27fbf1e3b4fa7a46b4a274ba\",\n        0\n    ],\n    [\n        \"000000000001fd6b7db603c305be360c602800e5d9068bd65bae111b4561d5ab\",\n        0\n    ],\n    [\n        \"000000003925c7eb3144eb77e7891a607152b662b161cd4a052e2a5689c4b694\",\n        0\n    ],\n    [\n        \"000000000000a8476194924cd6612277821149e22f7326a054c09c7d55b8a9d5\",\n        0\n    ],\n    [\n        \"0000000009ddc12332eb5903b89ddfd116bfd9b300c4d70821e749a302fa438b\",\n        0\n    ],\n    [\n        \"0000000000028fe3bfc47a9ad8a71c90fa3edea0c1d04f823c5a9d8674b9d1c5\",\n        0\n    ],\n    [\n        \"000000000000075849c07342e632fa3f2b4e137de35703e91c62cb568a8583ea\",\n        0\n    ],\n    [\n        \"0000000000001100406d8447ce19989346956134e2dabb87f93ff1b32208dc21\",\n        0\n    ],\n    [\n        \"0000000000006a8a2fd9d16a22f28523940811b3c4f179f888249b6f5f19c708\",\n        0\n    ],\n    [\n        \"000000000001af7c8a48d294945d937c3f1ab297617bab1a0eb1d9a40e543139\",\n        0\n    ],\n    [\n        \"00000000000040eafb8f54cb988a19d0370379be0b2917787e640720677ba6de\",\n        0\n    ],\n    [\n        \"000000000000025f7bc6cb5759f267fd649620c69f6518213729bb6aeb4d98d3\",\n        0\n    ],\n    [\n        \"0000000000000217a8588f1af88d2f73a96a658f0aea62de5c53b5b348346456\",\n        0\n    ],\n    [\n        \"00000000000001b8aa8353bbafb6f47125f67a711c0a2a7a00bfebff5a8df093\",\n        0\n    ],\n    [\n        \"000000004ca77c8921259d7da52f341526df3f34edb62e3e2888b7ce42b8c29f\",\n        0\n    ],\n    [\n        \"000000005c8253a86af2492291e888d78d0a69a7a657a221e59b23eb6291fcff\",\n        0\n    ],\n    [\n        \"0000000000fba14ebb3757a9348a05b07ec207b25aaffeac4118237e665fc566\",\n        0\n    ],\n    [\n        \"0000000008f01a3c024cb6d1814e54659c72b17e34e2b60fd35af2184b6bd3ea\",\n        0\n    ],\n    [\n        \"0000000003da1325f0d607889753f3a7214c3e559b9834c6f0e37bd52e14eaec\",\n        0\n    ],\n    [\n        \"0000000000d303f0b50fc25ea141ad3c26d0dfe61fa4cfcc6875edbcef902163\",\n        0\n    ],\n    [\n        \"00000000002131de3bcff721c93c169e34450054c18fc02cd5a8e08c7c3fd567\",\n        0\n    ],\n    [\n        \"00000000000c69cdb751a4ef5f527ae244909ddfda10a4caed4d6f8dd44e51fe\",\n        0\n    ],\n    [\n        \"0000000000024819bfbc99fd2032441181dcb2456ada1d047c4b6b7829be62a0\",\n        0\n    ],\n    [\n        \"00000000000077021c5164bc1014b24abd321f160bb914a1257a86645f923385\",\n        0\n    ],\n    [\n        \"00000000000038e149b42e964bdeb10f01fbbfd38ce57ec25eb3fdfb712cf9b0\",\n        0\n    ],\n    [\n        \"000000000000047dd3d1ce9862add6979aa622a7cb2141b4c6ec569b172dd776\",\n        0\n    ],\n    [\n        \"0000000090c401521295d1040e0f9b6cb65da914085bb9346e60477837dab234\",\n        0\n    ],\n    [\n        \"00000000f36784781eaf4b0d3ef92525b6cf55e910c782bd4f355b71ee40dc36\",\n        0\n    ],\n    [\n        \"000000001d3848f040d48696a9e258798bea34969e810ad01e8092183f201dfd\",\n        0\n    ],\n    [\n        \"0000000007658642f1e8ac45feec2766358f425030b14ad824f3a6df30b9eb15\",\n        0\n    ],\n    [\n        \"00000000028e5b819d9e197b1d3f1246a2a6990d8e2360371dbf258c2c5861fb\",\n        0\n    ],\n    [\n        \"00000000002a8dbd19a807d955c7d01962fea32f5ae027345121176ac10c20f4\",\n        0\n    ],\n    [\n        \"0000000000144908febd5cbacd1d9b828817f0350211be3248a1ec2d3ac3e251\",\n        0\n    ],\n    [\n        \"00000000000a302f19d696c7be172c6ac92ec2adf956417bba482d3e5285e5d7\",\n        0\n    ],\n    [\n        \"000000000000a289eb62cae8c41644d7c9de31148f711744aa5409164b90d6e3\",\n        0\n    ],\n    [\n        \"000000000000036a6f6002c633b6be318745d2f2ff1520daa6a49db7649bca67\",\n        0\n    ],\n    [\n        \"0000000000000293db488f4a3c7289489664e6e7e1ec917dc58c83ec828a4730\",\n        0\n    ],\n    [\n        \"0000000000000e24d4ce3b9247d6316791438ab82ea755e788112bb9729730cf\",\n        0\n    ],\n    [\n        \"00000000000003a18b92493908ebe4ccecf24bfeda95bf3b8a026e3c01af116a\",\n        0\n    ],\n    [\n        \"0000000000000007a2b7ba9dd58c20651b477daf83df5a7ac24b856b22f1fb25\",\n        0\n    ],\n    [\n        \"000000000000000ce321e0271dd532a6ce58737151baa84a77a585df614c2ab6\",\n        0\n    ],\n    [\n        \"000000000000004ebec3379d6a8569295a2d0a0c0e0c815d2b01803315032185\",\n        0\n    ],\n    [\n        \"000000000000001bb9ed28d9b0a70fee0b6d42f91f3db53f2086eef4daabce30\",\n        0\n    ],\n    [\n        \"000000007c5711c573d147a6fae21faf529c039220c97dfe2ba96e732d88fa89\",\n        0\n    ],\n    [\n        \"000000008e5a5e820d1a10dbeecf6f6df3bf7ab56e46eec275d8ca1a52e86b68\",\n        0\n    ],\n    [\n        \"000000003fa06ace5db33de18cf03b0c56d4e62cdaf8ab533919953c22bffaf1\",\n        0\n    ],\n    [\n        \"000000000000e6442b0c74fa811319edf2edd5f8d9b2e3ee831b4bdee644fbd0\",\n        0\n    ],\n    [\n        \"00000000011d0c3f98e9c3db6b51468be632bdef0c47f5e45871b771e5b0bc57\",\n        0\n    ],\n    [\n        \"000000000000e3c0978d872ed3b3a43f6f319995459105159b5f4e92143d40d2\",\n        0\n    ],\n    [\n        \"000000000000cdf25c3e15601dcb798c6cf8d2dd89002a4e046b746be6b87fa0\",\n        0\n    ],\n    [\n        \"000000000000521507052d13f4fac6c01c0099466720bea95c2e9349aef7fa5f\",\n        0\n    ],\n    [\n        \"00000000000064823750f1a6b7cd1748dfcc73376086cfdba987d2a36fcddb71\",\n        0\n    ],\n    [\n        \"0000000000000b4a41be0612f47a58efb899dc1cc0965c1c1fac89e1ea69f587\",\n        0\n    ],\n    [\n        \"00000000000010aab857bf7d475d9a594dca8b1144597a9e69c70f20fdd20b4f\",\n        0\n    ],\n    [\n        \"0000000000000c264f193e8d5099f2c20c08fdf9e5ca9006fb53778c0d8eb869\",\n        0\n    ],\n    [\n        \"00000000000002adcce72a5cce517f1afc33c765927b77ccbce5cdc6f5f68e45\",\n        0\n    ],\n    [\n        \"00000000b179a6096a58938311b3b8cc4479ccdf3909667a58598acc4ebd0192\",\n        0\n    ],\n    [\n        \"000000004e86c06d23b8a4c20e6cb5a4c51cad24fca30e41695f8ad00852a88e\",\n        0\n    ],\n    [\n        \"000000000bafa134d62d9df490ffdbc1f2b86b4373b86c079c5b730034aad214\",\n        0\n    ],\n    [\n        \"00000000033e9b623ca1d89418114f63af55e042dafbfe97952e7a5fe7a3ebf4\",\n        0\n    ],\n    [\n        \"000000000119025b6c9bbc3390708b1a77e85eda69fcb79666418ac2cb874a17\",\n        0\n    ],\n    [\n        \"000000000000feafbf3a525a1dd7950fa53f7df1b0210e79337ce588d35a8b9a\",\n        0\n    ],\n    [\n        \"0000000000007044088a1cc9ddc0c3779c0e156dee10fa15a760897ed4249f8f\",\n        0\n    ],\n    [\n        \"000000000001a10e8b1ad577278f946252298b49b74ac9db70ea80c0a9c12db3\",\n        0\n    ],\n    [\n        \"000000000001281354a7d86b3c750681283276c0bdde2b18c38d8354138ca4e1\",\n        0\n    ],\n    [\n        \"0000000000000398b17fcd5d4d59ccb31d642f7b60c2a4d4d2aa7239ebc0efa9\",\n        0\n    ],\n    [\n        \"00000000000021a571a2c475115fe723b593633efb85bf0ec0f7d67b780e70c3\",\n        0\n    ],\n    [\n        \"00000000000002d1506c82becd7b480c85402d27f23a1248cfa128b7a8c009a6\",\n        0\n    ],\n    [\n        \"00000000000001978f804f5cf8e4a0dc0c454fce0f0e2614510b8eae6e504b2e\",\n        0\n    ],\n    [\n        \"00000000000001c4558889a43ac35208f502bccd9d38c741571723e9d79bcc26\",\n        0\n    ],\n    [\n        \"000000000000005c782bbbc75358216e1ffc37973cd43a474b87dfbac4c61fab\",\n        0\n    ],\n    [\n        \"0000000053bffe3e3db3672c5f050fa54239f93833ec5c38af92e83dec71a9fc\",\n        0\n    ],\n    [\n        \"000000000001362fd5182f1cbfc1981937cd67ba54bc7b6d7f0a68f94e369f0a\",\n        0\n    ],\n    [\n        \"000000000386ae84caa25e9dfa7816594b7c30a079e340bfcd951be2b5c092b2\",\n        0\n    ],\n    [\n        \"0000000003cc09a351d647c0e12063d45b20e6f99c27c18ea62342b9d246581d\",\n        0\n    ],\n    [\n        \"0000000002527c4756350bafee88786cd7ea27bc802f482c4e50cafc547ff9f7\",\n        0\n    ],\n    [\n        \"00000000003d7288f44aa0b725af7816d2d333e118de12c390423d641139d5d5\",\n        0\n    ],\n    [\n        \"000000000008c0a0fadcfbe27a880ce9c387425d3a2c6b06c1a599e4ce51ec92\",\n        0\n    ],\n    [\n        \"00000000000158ab2486a8f1251c5c94502763ced9eb85847bb9d2eb476b515a\",\n        0\n    ],\n    [\n        \"000000000000c817e5775378accf08412657e2557d2895df0fbb8475b5e190ba\",\n        0\n    ],\n    [\n        \"00000000000078d59d08215b3aecdf0e0665d3a16ae1716e408df790a3566e72\",\n        0\n    ],\n    [\n        \"0000000000002208404b39b95cc20845de19b47e05e8146146056d3d9bb382ae\",\n        0\n    ],\n    [\n        \"0000000000000543e9315ca8b3b72bd3590f24535e4ddc6ccb1050b607777530\",\n        0\n    ],\n    [\n        \"00000000000000abb8d3ffd3cc347cee5c092dde5355a7dc5d288036a28760fb\",\n        0\n    ],\n    [\n        \"000000000000008bfbcce7d768df6f4610205dcb40173e8c4c417a2325487f34\",\n        0\n    ],\n    [\n        \"00000000209e49391ad09577f87d1e0ffda27d2e749fd305c51692112627c99d\",\n        0\n    ],\n    [\n        \"000000000005561eb4b2e0cb8107c81617284e7bcd7d390d16a3cd5925cf42a9\",\n        0\n    ],\n    [\n        \"00000000006b24215c790a371bc18c53c83ff35e2c82d459bb6240cd9615dde5\",\n        0\n    ],\n    [\n        \"0000000000af315d6fbde8488d68dbd055a56d79555ed32c3ad4d70286b4df2a\",\n        0\n    ],\n    [\n        \"00000000019e49bc89fcabc4050521fb8835f926a62cc10b68e9618ffc117162\",\n        0\n    ],\n    [\n        \"00000000009c0dcde4e694463245e8e5e45d2897e7fa67772ce0ef37094f3afd\",\n        0\n    ],\n    [\n        \"000000000005efbda8c010f29a5b81606d186459047ce4b7eacde8d9659dce97\",\n        0\n    ],\n    [\n        \"0000000000051c1655579a441a7f4d543c323d482405cf1d1250c3ccb665d426\",\n        0\n    ],\n    [\n        \"0000000000007f13adadd1fc6462fbc5231425b81826af4e5f0cbb0de54a5b3a\",\n        0\n    ],\n    [\n        \"00000000000011e00df09353fcb53766447279b96228da0525d769f33026bebb\",\n        0\n    ],\n    [\n        \"0000000000002b91e6bb56015e0e60dc650a63666aa3943058e9641d4d679fa3\",\n        0\n    ],\n    [\n        \"00000000000008e4d5fbcf207583267efff33e6c8d0a5fbdaa5704aeb674fe29\",\n        0\n    ],\n    [\n        \"000000000000018aeeabcb422b5b0a46cf3a5f2458125c043c5781ffafeffbf9\",\n        0\n    ],\n    [\n        \"000000000000004ca501cc9138ef5fef4b7b235682b81ab9719b3cf215e94f73\",\n        0\n    ],\n    [\n        \"000000000000002b5bb1c4c43059575556a0ed10099ce5095f805d3d9ae10cab\",\n        0\n    ],\n    [\n        \"00000000000000018523a377832f154b2b65142098bd18dc175273c92ec938c8\",\n        0\n    ],\n    [\n        \"000000000000001f58a18e73b959b3fba53f697f78aedeb431d4b5df42cc2eb9\",\n        0\n    ],\n    [\n        \"000000000000002df432cf9306f7eae841b8e5b7c137c5763fd4bfb46f8a309c\",\n        0\n    ],\n    [\n        \"0000000000000004941ebdbe86526e806cd13ed226daafd0dce886bfa23d2352\",\n        0\n    ],\n    [\n        \"000000000000000cef306e6a9eea2d7c83d051eab259bc3f6e985de5f4ac3d6a\",\n        0\n    ],\n    [\n        \"00000000816ec1fe265e8abbf9f1de03498abf8cb6cda9d29a7ec6c8518524a8\",\n        0\n    ],\n    [\n        \"00000000000011d74604be4f183ed34f00c15d7218834802163c2728d0338535\",\n        0\n    ],\n    [\n        \"000000000762bc47cfbc6be9269fdd65dde20d2c88719ffd90a6f2945f7c6fe9\",\n        0\n    ],\n    [\n        \"0000000000002db5f8794e7dae8b50458ecd05742d4d371123252e7472573619\",\n        0\n    ],\n    [\n        \"000000000002b4f1a7ae7549fa44b1b320421aa1b59e1c5ca19b086873109677\",\n        0\n    ],\n    [\n        \"00000000000047b8f650640a6c7ade46b2116b25e9e31138272ed319ea5b2844\",\n        0\n    ],\n    [\n        \"0000000000001256a95ea8f9b361e7eabc372d62653e9eaa0dd7fabccc61af5e\",\n        0\n    ],\n    [\n        \"00000000000035948053b0e71b73d618b490cf735780b470d55d96be66abd773\",\n        0\n    ],\n    [\n        \"0000000000002e1c730a9822a12a74ad4891d1083ae398430520d835487c3dd8\",\n        0\n    ],\n    [\n        \"00000000000014437f28476cbe0cb6637ae1615a20e661daa90bbfc291f00660\",\n        0\n    ],\n    [\n        \"0000000000002f1c8dd72d46575a0c4c98a1197313ad1450c21b190086d40a89\",\n        0\n    ],\n    [\n        \"00000000000008260b87de90b439d1e8e854eafa3c271bb9218994e9d903a779\",\n        0\n    ],\n    [\n        \"00000000000003476bac3afdde7dbf55d6a974817a87ce6cf4b20564916bb48b\",\n        0\n    ],\n    [\n        \"000000000000006b4e74497ddcfac98e9965bfc81a5087f5f091de0d9f5118f6\",\n        0\n    ],\n    [\n        \"0000000000000033ded358c7074b4e19f90fe1db1f258cf6ed9fd0227923d09e\",\n        0\n    ],\n    [\n        \"00000000000000048fc3781990e18e064e3d5f3d73bab1a199d0a6519f4eda1b\",\n        0\n    ],\n    [\n        \"000000000000000e1871dc667a7e6596c15a320c1f2a9a81a784aa14c62f15f3\",\n        0\n    ],\n    [\n        \"0000000043dcbf92d928170baccef4bafe45f5009ad6e8c7a4fbc924bf1e659b\",\n        0\n    ],\n    [\n        \"00000000a5409910e5907b6b1db12c4bd8a8063e15f39e749488fa9c035de6e9\",\n        0\n    ],\n    [\n        \"0000000000008c9975f8e4192fae850a9247e14e38638ec745dd42c230137728\",\n        0\n    ],\n    [\n        \"0000000000003f0df4894e1939e7ab333536e1b71a06b676419e699210a9780a\",\n        0\n    ],\n    [\n        \"0000000000003771556faa6de8495f58a1b1eee15abdc71f3fee10e03e72756e\",\n        0\n    ],\n    [\n        \"0000000000000f9c4ee2d147531d9381bd7fb8140d4d7be0f8f058b4017133ab\",\n        0\n    ],\n    [\n        \"0000000000001833e3116f1c5f9a1c5be36918c19d6f0850842f2b54b6e674d8\",\n        0\n    ],\n    [\n        \"0000000000005c08d8ac7add24fb3a01857e68b0a806951986d0f412a9ada58a\",\n        0\n    ],\n    [\n        \"0000000000001f70115a7353a76b574f844b9ca9d551d346c741c005cfe2a06c\",\n        0\n    ],\n    [\n        \"00000000000028fb7a7dfb30d02e93afa2cd462c8b4a12b022036714c9e6d2f1\",\n        0\n    ],\n    [\n        \"000000000000386fa93b299ece82b0faab9e169c3167c671517090a2aafb3825\",\n        0\n    ],\n    [\n        \"00000000000002a47ca39571cb0a79edbcdbfac91f9297776c3c2a9d8deec299\",\n        0\n    ],\n    [\n        \"00000000000001cc4e0aee1cc285d411eb1cb56ac4c8fe2978e63ade53607002\",\n        0\n    ],\n    [\n        \"0000000000000016bb8db548039b254578f550bb702c66eed1c441ed7fbec8d3\",\n        0\n    ],\n    [\n        \"000000000000000f0cecd63a2292a5cd53a542fba78ed0d6fd3d93e9f963bca2\",\n        0\n    ],\n    [\n        \"000000000000001781f58897dbcf54dc50fc2c4e5c949090a79aecd98723608a\",\n        0\n    ],\n    [\n        \"0000000000000011ec668d99fd0aacd40bfc8ccf7b364d4879248e8e628bc5b5\",\n        0\n    ],\n    [\n        \"000000000000001a381d52991c224ed1c6d7c4c2ee763098e022d0c04eb78381\",\n        0\n    ],\n    [\n        \"000000005b04a3f6f1e7f273875826c8538d9bcee2ce58a98e61ced5bc1fb902\",\n        0\n    ],\n    [\n        \"0000000049e220ce8e607b6cd231aa1f6ba7758521195d8c60afb920900ed146\",\n        0\n    ],\n    [\n        \"000000002467012239401cdfe357bf8d49f4bc74be65a3145925a230dfb360f1\",\n        0\n    ],\n    [\n        \"000000000bb9e52739adc38fb6924c68ed1f1962a45e75aaa18066bb7700cfa6\",\n        0\n    ],\n    [\n        \"000000000001e91216fc79f80d58182417dee38dc449e592328991a344079a0d\",\n        0\n    ],\n    [\n        \"000000000000ae1204d3836b126e30685d7391787820e6f8481afa7f4891d88b\",\n        0\n    ],\n    [\n        \"000000000005fb7034adda5521e1deacec2a95f6ce7f65df5123742f4350a633\",\n        0\n    ],\n    [\n        \"00000000000006a1154387c23fd70c2cea8036dab861d51fdf687639b2881ef8\",\n        0\n    ],\n    [\n        \"000000000000fde46c03685cff7a1eff85bcb8e1604577e0bca9e3dc1cf3690d\",\n        0\n    ],\n    [\n        \"000000000000d85499ad0085b4c7b9960d28e542c1ff7d8422ccf2a94fbc33f5\",\n        0\n    ],\n    [\n        \"00000000000013b7bc74a6565c5c0a7d32d21567623ae8e2d18b43d5cb3c9040\",\n        0\n    ],\n    [\n        \"0000000000000f26070144a87fe5ebe3676f0e6a2a2eefbf6556401293baca89\",\n        0\n    ],\n    [\n        \"0000000000000302522fb697dbe69844a8cdd7696faf16a5c8a43842c2a3bdce\",\n        0\n    ],\n    [\n        \"00000000000000b34c0ff70cd3b532a9cb1633896d2e683aa53827c6e0f1a25b\",\n        0\n    ],\n    [\n        \"0000000000000018e983ee8b65a40d3587cab91a4fa8d29b68353778a6b7f862\",\n        0\n    ],\n    [\n        \"00000000982be2494520c626711cf47b5bfff996c7f74189f9a9898a96057b11\",\n        0\n    ],\n    [\n        \"00000000c1379d510ab27bb0a267a32ccf5af9e698fde308635634515496b25b\",\n        0\n    ],\n    [\n        \"00000000000183573830f2976678ca06a90570c40090b7cdb52b3d3940eabffe\",\n        0\n    ],\n    [\n        \"0000000004799482beb4d1622b71685fd616c923ec99a91f2b6309195814194e\",\n        0\n    ],\n    [\n        \"000000000009dddcccfbcc285ce93d763201404707b6ff30740bd8e508a411a9\",\n        0\n    ],\n    [\n        \"0000000000bfc6f8a9569e69ec3b57a385d81e920a3ec84d0e97a070f27107af\",\n        0\n    ],\n    [\n        \"00000000000022d8d77623449f408a6dec9e7fa847c08c8c246b049c15f9d054\",\n        0\n    ],\n    [\n        \"000000000006daa9307e62107c6984c2b90dae469f1fd1bb156dd7681de0eade\",\n        0\n    ],\n    [\n        \"000000000002cffd12d7d9867a7837ae6b45b383c74f0563ac5709e4eb28cbf5\",\n        0\n    ],\n    [\n        \"0000000000008653ff3c6a0a517ba04729eb63a0ae60c7baa975a407fd561bbf\",\n        0\n    ],\n    [\n        \"00000000000005e761b3a0236e409d3ac72dc993e9cc6835f3504e62b3786d5e\",\n        0\n    ],\n    [\n        \"0000000000000bb93ca70e18034211414c6769524031248b7345401ff7dcdc6a\",\n        0\n    ],\n    [\n        \"000000000000004f9e54d85a4de7e13a6e64b8145713d4402927196d6c788c01\",\n        0\n    ],\n    [\n        \"00000000000000d19157da447106f9827f098b01f43810b3fd97eb13488599c9\",\n        0\n    ],\n    [\n        \"0000000000004db0a4da49aef66b6b47c3d2fe28152c4eed5327f36a2e1049fc\",\n        0\n    ],\n    [\n        \"0000000000657115d4dbbab98df8f80baa50f6906250c62d9d634a45f2512b9f\",\n        0\n    ],\n    [\n        \"00000000000dca4d6732b889fb9b824a7b80a1f509f81c52a02805ad4b008184\",\n        0\n    ],\n    [\n        \"0000000000cbbcd73925dafd962774ac5457ba8ed5b60966f19915e2a92d985a\",\n        0\n    ],\n    [\n        \"00000000006b6eba960b8ff4e8a84b343167ae795a05d508b36261b36a0183fd\",\n        0\n    ],\n    [\n        \"00000000007a284f086b9118b29d4f1bb80b36097f1676ed923caa96f86259f8\",\n        0\n    ],\n    [\n        \"000000000002065b5c30c9149183ff50f298ae4afc718002cbbd0963b07b5747\",\n        0\n    ],\n    [\n        \"000000000000e2b2be6cc8930626a1cca225656f4dfd4323bc114bff9381de4d\",\n        0\n    ],\n    [\n        \"0000000000014bdabb06ea628423a845a974dc4f78b841cbfd68fac5b5ae4bff\",\n        0\n    ],\n    [\n        \"000000000000075561bf4f468313206932c444c56f8a0bdfe7d786674f39195a\",\n        0\n    ],\n    [\n        \"00000000000029bc154a17276deb51e57fa4c67be260879e9e90c4a99e484812\",\n        0\n    ],\n    [\n        \"00000000000001df7d47715a9ee239c41ea4f6927fc10513fa8e196d030a7c48\",\n        0\n    ],\n    [\n        \"0000000000000190df480d22883145a8137ca5364e6370181e481f363b2dd942\",\n        0\n    ],\n    [\n        \"000000000000009f95d274cfccced7b153b6bdc7a1f479086aba1af3ee51e2d0\",\n        0\n    ],\n    [\n        \"000000000000000c555ee275a7c75daeea5fc8c9cc3589ce8ffc485b0e2f9c84\",\n        0\n    ],\n    [\n        \"000000000000156317725ec06f7396591ade0dff87ba7e5592ae7ca8922397c9\",\n        0\n    ],\n    [\n        \"00000000004bd131ef3e9b59e54b2713dc907f11ef9bcc6bf3b0d7ad791500f7\",\n        0\n    ],\n    [\n        \"0000000000c373a55ff87189465e900beb21621c91cfd99f6c303a78206c17ae\",\n        0\n    ],\n    [\n        \"0000000000380431e66d9fd20f35080dd6078d0506716b8a5d7e39613a98403d\",\n        0\n    ],\n    [\n        \"000000000025fc2f72b36008becacae888c81811238a88086b45b28c5c394067\",\n        0\n    ],\n    [\n        \"000000000033b750e7cb47cdfe186e26d38c5c461e0d8395e489c0614056041d\",\n        0\n    ],\n    [\n        \"00000000001331a7e5cdf0b13e176b4f9bdabe9e5b2db5356d59cb0dfe1f0e46\",\n        0\n    ],\n    [\n        \"000000000006eebd9a33e800fe588dcd34bd363357b593f6808e397893c6028f\",\n        0\n    ],\n    [\n        \"0000000000028315ddfff7fb95659327af8eb64092b7d130fbcbcaa784a3d4ec\",\n        0\n    ],\n    [\n        \"00000000000029d270b7cabee53577c273d9c01c87c140e5343757d3953d4b63\",\n        0\n    ],\n    [\n        \"0000000000000fef07c22f210c2b7ad92b06570cdad0c82e195835e0d0378aa4\",\n        0\n    ],\n    [\n        \"00000000000002b56610f192a962de55e7060686e269e3cbea07c55eaf0ce120\",\n        0\n    ],\n    [\n        \"00000000000003c962e2f34b9e525dec519a6393abe3f17db6af189455cd7baa\",\n        0\n    ],\n    [\n        \"0000000000000057e6976dff338a43dfa1925a5a43e97de23f01b96eb0b839af\",\n        0\n    ],\n    [\n        \"000000000000002db1fe2ead78cdd2d14392b5deea255c363b5e734adc9b467b\",\n        0\n    ],\n    [\n        \"0000000000000019b25e3df2e045927729efd35896654add081f6aaeebd71f47\",\n        0\n    ],\n    [\n        \"00000000000000063c34230877c91acd3017621542e7ba4d9b7ef64d0b5cbe93\",\n        0\n    ],\n    [\n        \"0000000000000028e023aee554cec2607f92c29e99611eb14736e2347e7bf42c\",\n        0\n    ],\n    [\n        \"00000000000000369d46aac842d5505dba93ade92d5d3be1157e00ae5c047cb5\",\n        0\n    ],\n    [\n        \"000000000000002077102f09a5563275c1efdbea9f6395f5146f1d6037970d7b\",\n        0\n    ],\n    [\n        \"00000000fe37a092e270e704da91edd5d7c4234766916e878c8b0a02ef64870b\",\n        0\n    ],\n    [\n        \"000000000039588e49d18e9ef322feab3d3cb6d14893262060890a9331b32f73\",\n        0\n    ],\n    [\n        \"00000000006613c34279b3c58d8d54667ae590630aef080faa50f07ccedd3bfe\",\n        0\n    ],\n    [\n        \"0000000000869e7bc5a3b34ebc2729be68c77c712317e79af584564555957b8a\",\n        0\n    ],\n    [\n        \"000000000009637269a88dd5ac43497b8f27ae313211a3061470e8137d555e59\",\n        0\n    ],\n    [\n        \"0000000000004a8598c4be94b043e7fbc600226fcd7547b83b64020d09a61fc4\",\n        0\n    ],\n    [\n        \"000000000000100695b5f09c3a1e8c29fbe67df508481578d2948258a04d0fdd\",\n        0\n    ],\n    [\n        \"000000000001bd1d57b3da64d209230bd6e314724b25a9b850e6181f49a6cd03\",\n        0\n    ],\n    [\n        \"000000000000d00e36399e3a40021ac0a6adfe27973edff1ff66d6420df5a98a\",\n        0\n    ],\n    [\n        \"000000000000a63aa2dcd75bf8fbccd35e3f00b684df023adbe2785eb4d8de82\",\n        0\n    ],\n    [\n        \"000000000000308579c854b0c09965e609ba1c63f109f98e7cab1be5cbf364b3\",\n        0\n    ],\n    [\n        \"00000000000000dba13892fba29915fb44c60e1a179d0022ca33e94c63a158bb\",\n        0\n    ],\n    [\n        \"0000000000000384bfe36508812b0068cdf46b0cf1942db32cdc17a7f5dba83f\",\n        0\n    ],\n    [\n        \"00000000000000f2916cb59cb479f2edcf2c2fcdee3547e772b904db17fc6d93\",\n        0\n    ],\n    [\n        \"00000000000000313b0f9b9b90f1dc0194a5d54abe389817e195501274970183\",\n        0\n    ],\n    [\n        \"0000000000000028d70cea4d1f3e5f3601b8c72c7e844f434972b2dcc343d3b2\",\n        0\n    ],\n    [\n        \"000000000000007514deae00c442bc8c8121f2a943083c45a223ebdef390bc39\",\n        0\n    ],\n    [\n        \"0000000096309ddb64473b6a32ef5814b5b197afd1de684c25b9978abb17d5bd\",\n        0\n    ],\n    [\n        \"000000002201773b4ba28e91606a3925ce89fd0558a4d016565b6468d4966184\",\n        0\n    ],\n    [\n        \"0000000000001cd5ff5078c2311f2f9ee10089d2630029c07febd67016245583\",\n        0\n    ],\n    [\n        \"00000000001b93e7b197099b50a98462f56421e91533bbb5a5e6316d7748d271\",\n        0\n    ],\n    [\n        \"00000000000001d5b12737375e9a24d85e35425534b80eb33f1f6067a3c9ee9e\",\n        0\n    ],\n    [\n        \"000000000000a528259f38acd4ff2e48aa80c51e8d32d92ef22ac0319b7a4123\",\n        0\n    ],\n    [\n        \"0000000000001d7a4a2d35e55b00c097a4e3cfc78b27acec46fc35e14ec3e71d\",\n        0\n    ],\n    [\n        \"0000000000017069a0165b85c9a568d821ce1a23aa87fc32730ff2054d4e147c\",\n        0\n    ],\n    [\n        \"00000000000163737ba8bd06de10bfd9610f897832edd1b9c00986b8db41e294\",\n        0\n    ],\n    [\n        \"0000000000000f2a5f2ec3414e50228ea1a647b12b9af39005299c318adfb469\",\n        0\n    ],\n    [\n        \"0000000000002ee72d7899719ed3a4f101a4bacb5a341e5de28354b668e5f534\",\n        0\n    ],\n    [\n        \"00000000000000855dac9ef2f084ae3ba0609977b2d47ca7174ecc1219866c20\",\n        0\n    ],\n    [\n        \"0000000000002037546232e2809ff4f0ea5416827c134afe7107d74a4939fc68\",\n        0\n    ],\n    [\n        \"000000007b4f678d7e1d8b9653e1761e05d464eec7da2e65d5d3f44db1853c55\",\n        0\n    ],\n    [\n        \"000000000000635cc05816a3b7d24b63cabcbfc4debbfa5410df25cf5c618351\",\n        0\n    ],\n    [\n        \"00000000000064fa9ff8ad2346da5d75e029ee1d2c3d9499b2656c6a6d562c91\",\n        0\n    ],\n    [\n        \"000000000000a1b3bcce8190963b2d801579b0f1fed6129ad4e21a223f52357f\",\n        0\n    ],\n    [\n        \"0000000000005a78a3d8d9f33b6ece86970c75fb3b608125078d61c77bbc0788\",\n        0\n    ],\n    [\n        \"000000000000d48d0c9e7933b8749939110764bc1a827f0242c43233af5512db\",\n        0\n    ],\n    [\n        \"0000000000001725a132f59b477a352d478e0f6049f295fc609eed2f5e4ca3f1\",\n        0\n    ],\n    [\n        \"000000000000e6308f8f026cb8b44bb0d4b15d2710446c7f3719662fe7aa5138\",\n        0\n    ],\n    [\n        \"000000000000edd1062f75807cc0de78a3e19fa07763e23f744d544b1d5cce0f\",\n        0\n    ],\n    [\n        \"000000000000217223dd962210aaf8ce96de32318adf53387ebf53f3b2b30539\",\n        0\n    ],\n    [\n        \"000000000000021e3df79714b32722b720f916a9fbbf9102e7d753a7f242d8b1\",\n        0\n    ],\n    [\n        \"00000000000002c7dc3f4aca17976008f1a3f2547bcb15b3a138af988f3e4f2d\",\n        0\n    ],\n    [\n        \"00000000000000398aaeea76bd1c64e573b103ed06c11bc954e74c4c0b437add\",\n        0\n    ],\n    [\n        \"000000000000001c1bf7b854488eee158545d3216e54ef714b83471d3a73c48e\",\n        0\n    ],\n    [\n        \"000000000000002784b2be9801c004cefaf2b95f7626b19bec5aac1dd514fdf6\",\n        0\n    ],\n    [\n        \"000000008e2cab9ea92cb5ef0416b8cbf824b37596d3a5b60406f1b3028f7faa\",\n        0\n    ],\n    [\n        \"00000000104964a57ca5cbaa3008047f78705c4113033c750409a4b259936ba3\",\n        0\n    ],\n    [\n        \"000000002f524bc577bcce253fde6910eba3ef06d15dd27232131b5e4345fe47\",\n        0\n    ],\n    [\n        \"0000000000052c7116b0b643643af581ddca810363916a440bda2ace38add369\",\n        0\n    ],\n    [\n        \"000000000000f00137a5682f26cd18abf5c6f3ef61f910c59a59d798477cb152\",\n        0\n    ],\n    [\n        \"000000000002a0894c9aec54bfdcdb4a064e4507a6de62fa990177980ae17fde\",\n        0\n    ],\n    [\n        \"000000000000be2b65c45a2b5d56f1be99f2975b1a9a52d27ace45f5928c7020\",\n        0\n    ],\n    [\n        \"00000000000057d1aa7d139b6f28389f9f00682367e11cd6e179db6d78c21c5a\",\n        0\n    ],\n    [\n        \"0000000000006e312b5a03b58c2ce0ea1f4e575290499e3e8628e55f57819810\",\n        0\n    ],\n    [\n        \"0000000000009679b04fec87a5ef0cf0ceb8d799a6d01d5c87c2fd24c29468cd\",\n        0\n    ],\n    [\n        \"00000000000026a1b24da68760401f5ae058d964a1830f8823c7958d6f0cb115\",\n        0\n    ],\n    [\n        \"0000000000000ac34da5d627b47fc7f7b25599081a8b187ae52d70f05db9bcb6\",\n        0\n    ],\n    [\n        \"000000000000025312bb3c989c7bff8448905d7e591c6d3cb16d3a560e632157\",\n        0\n    ],\n    [\n        \"000000000000005e28077829e782ff8df6e9b907bb9df6493e4462d8285ea9e9\",\n        0\n    ],\n    [\n        \"00000000000000498293c5ba0eea98e1bc9bc21178cb13033a11f8b49f5e774f\",\n        0\n    ],\n    [\n        \"000000000000387bb87b8e7873d04bf3c2f70af050336a8536ad3f7735119f58\",\n        0\n    ],\n    [\n        \"0000000041190a125657d94ea0ea26b2238b7a68f5eae7af62eaf902ac585923\",\n        0\n    ],\n    [\n        \"000000000045beda231f53f748d8d0a0adcb0090603945f5664273e5f28e4c6e\",\n        0\n    ],\n    [\n        \"00000000000c12330af00f371874b47f1f29a7d9bbb89f5d0a1e3a2dd53eaeb2\",\n        0\n    ],\n    [\n        \"0000000001e172477961534ad79703de1bef9d7b11b27417c19dc4a8a1f3acf6\",\n        0\n    ],\n    [\n        \"000000000022c7e8bfb93d5a5289cddd8d3083699997533f9f74cfe634f71f71\",\n        0\n    ],\n    [\n        \"000000000024222a6945a270088ba37d36c48661d153c85676f87a879bdfe080\",\n        0\n    ],\n    [\n        \"0000000000020caf59c7f3798fb533287568e2c5924b2cc5dfcb1da89786879d\",\n        0\n    ],\n    [\n        \"000000000001a5f0697c842dd77697934e30bd8798c4a4aff1b7441dc6f23dca\",\n        0\n    ],\n    [\n        \"0000000000000ba524c5e6b6bf1262f8ab7d9f74879bb41df26f1f5e5d13b4de\",\n        0\n    ],\n    [\n        \"00000000000005f7c7e2f10a0f7d9d371a350ee6e6fb567b5ba6e7a43a9a2bf3\",\n        0\n    ],\n    [\n        \"000000000000060cec749aeb779875b1be98f34ddcf541454cf4f1aff8e5d998\",\n        0\n    ],\n    [\n        \"00000000000001f8ba23177cda6b436e7a2301e211fa35198854077254abeed9\",\n        0\n    ],\n    [\n        \"0000000000000067d5b94b837fd148ab7eed397d1c0acd4599d7a8880c60e83c\",\n        0\n    ],\n    [\n        \"000000001620cde7948f7134c978193490736b7ed19effdceeb142ac5c60c4f7\",\n        0\n    ],\n    [\n        \"0000000000010b0daf2238423537f93efeabece0cba64ad3b5f8ac73c7382a99\",\n        0\n    ],\n    [\n        \"000000000005b0676f677edb2b6710516e06bba963c09dafbfc2aac314126423\",\n        0\n    ],\n    [\n        \"000000000007ccc73eca4f241bc1ff75302682c9fdb0939b6f706b9da00a52a9\",\n        0\n    ],\n    [\n        \"00000000000a2a1678999a3493f1d603901e35518d90b3e739ce6daf127ebe26\",\n        0\n    ],\n    [\n        \"000000000003b394dc98407ee9fd88e4f4e22507a788f915dcf72cc34c87cadb\",\n        0\n    ],\n    [\n        \"00000000000351a1e8062b8fe34889239ba38ca52b3f7e44c7122fc1f0fe6e6e\",\n        0\n    ],\n    [\n        \"000000000000df60569c57f26ca8796e636eed1357c8f0369d0821c0919ca7bc\",\n        0\n    ],\n    [\n        \"00000000000087b5af3cfb531ded71bda8932cc80a453f22ead0bf93f50b08e0\",\n        0\n    ],\n    [\n        \"00000000000057424fa933cffa9bd1116892a76ac912f2c8ee7313b2bdee3351\",\n        0\n    ],\n    [\n        \"00000000000015c3e9e26653d1ef02aaa6d024489f90e83588f5f9ebf28d7b63\",\n        0\n    ],\n    [\n        \"00000000000009016aa385782a3ec788fb22b41412855c89aeb73147743a6f19\",\n        0\n    ],\n    [\n        \"00000000000001317fe059d7442ae8afc5aebccaf4d37ed31813a5caa3638c87\",\n        0\n    ],\n    [\n        \"000000000000001d8f256752a735a1de33b92a928052f71b738d19ff366db867\",\n        0\n    ],\n    [\n        \"000000000000001e9cf12031d4ab136ffd2fbc280607c4f996685d4aef460a11\",\n        0\n    ],\n    [\n        \"00000000000000211e7a6e837358cae29446cb9c6dd5aa15c2284b5314f3bf46\",\n        0\n    ],\n    [\n        \"0000000067af625d738843195c7dbf37d509f859a1875ae674e9a1dc8ad89e0a\",\n        0\n    ],\n    [\n        \"00000000bba6b47cc6dcf36f0a6e33c7c60e9bb23ee1f72f62cb4e90d7f04332\",\n        0\n    ],\n    [\n        \"0000000000041dde2cdeb55e44cc00c8d4c1f448c9220ed2c0153de7a2d55779\",\n        0\n    ],\n    [\n        \"000000000000288c3e28aed36614437d861224fe8ccaf182e727b3e1fe1d633d\",\n        0\n    ],\n    [\n        \"0000000000047b66b2cf5b617d9e7ece2ea2be7f886eca7e7f8f831a4c71a8e4\",\n        0\n    ],\n    [\n        \"0000000000007a9fa498e65a666bd261e9a5ed00e6c3026e7916554841bea631\",\n        0\n    ],\n    [\n        \"000000000006b245a6d974176dde00529110ea44bcbe8f78c567fea3e47a3d78\",\n        0\n    ],\n    [\n        \"000000000002e1faeafaceacace03930ea6ea7d8d42ea4cbf141c2b578f3128b\",\n        0\n    ],\n    [\n        \"00000000000167a7dbf143b59eaf118c2eeb5556061758b56981a4cbc6c7a08e\",\n        0\n    ],\n    [\n        \"000000000000a352cbb22a1652956aa5d66610b696a19118b933048538192d2b\",\n        0\n    ],\n    [\n        \"00000000000002c418d64c823e2ec8c77220717c0c7fe4e34b25afc4c0bad5e3\",\n        0\n    ],\n    [\n        \"0000000000000d7373e7a596fa105a5d1b7dfddfbc274d45c864c724f0b9a2fc\",\n        0\n    ],\n    [\n        \"00000000000002c3c89de3acbb14f5e38a6f5c3aed34d4c6c45b2222fc0fe3ab\",\n        0\n    ],\n    [\n        \"000000000000001178732537c78ae21bbe8f9fed898f3b2c63692b9c93aba4e9\",\n        0\n    ],\n    [\n        \"00000000dd9c07faaa65b8ce71e266699422567278b94487e9ebe4227d1ef2d9\",\n        0\n    ],\n    [\n        \"00000000d226cc764f56ca5ec5a62562cdfc1bf3a4435350f0a27afdc5f94a79\",\n        0\n    ],\n    [\n        \"0000000000009baebc276aeb84b5ccf3fd7aa95efc67c0f982bdfb084e40e9df\",\n        0\n    ],\n    [\n        \"0000000000005d841db70c88b40085e6003fa220b0c91a810e04fb010e7f84e1\",\n        0\n    ],\n    [\n        \"0000000000004a5013716cac6fe9e3d3c91b5688463feca69fc90045f00e2a17\",\n        0\n    ],\n    [\n        \"000000000000cedd7a99d53af44ec7144316eec8aa0b04164f2618d090fa32fa\",\n        0\n    ],\n    [\n        \"000000000000e311840eae32a946c81d8ed9a7fd5d1788444b5376a56def23df\",\n        0\n    ],\n    [\n        \"000000000000c3babf9150364160f0bc91c0cafbf63849cf70a29574ed12751f\",\n        0\n    ],\n    [\n        \"000000000000521dd5efaa7a298cecdd047c6ed85f981ff9185763beb9519c49\",\n        0\n    ],\n    [\n        \"00000000000092695a2324a45f524b51ab700b458e5995790522f9d4002e374b\",\n        0\n    ],\n    [\n        \"0000000000000925141aa9c81dd875e55a986bcec38a9ad9e627c81ee6437a88\",\n        0\n    ],\n    [\n        \"0000000000000ae07f7535851cb685259a447d1ad5d3206fc4ee3693bb7421a3\",\n        0\n    ],\n    [\n        \"00000000000000d41e23f89aad486957071e016d836382605770b65d7539d161\",\n        0\n    ],\n    [\n        \"0000000000000016b6843174d892b13a0fa39cf807879b56b723075de7492118\",\n        0\n    ],\n    [\n        \"000000000000001a7c215c98d09179f0558b18f6987150cfcf5e57afca65b98a\",\n        0\n    ],\n    [\n        \"0000000000000027150d0a4ebf9001e210ebed81ab239535aca8fb5a489a1ead\",\n        0\n    ],\n    [\n        \"00000000000000312d9a4fbd4bbde5e3f2266047e65f9a5e84474d62afea0514\",\n        0\n    ],\n    [\n        \"00000000f82b6cc148557cb060b8cc3d697e38250630bc2e7188ad4500291b5e\",\n        0\n    ],\n    [\n        \"0000000083ccf3997bad3cb32d0e46ff7875a0f454a3c48c2ff910d010801ad2\",\n        0\n    ],\n    [\n        \"000000000000f40bd4d7c3d374a984d0c8a744c3816b713e8f43b9dcf75f7848\",\n        0\n    ],\n    [\n        \"0000000000002c0374e865c02fed16da853c3086a28b0c212591469c95a71205\",\n        0\n    ],\n    [\n        \"0000000000008301f16f29f38442d1b1f521650eba2382ea1e0055a291ca6422\",\n        0\n    ],\n    [\n        \"000000000000a954b023180407c904341edba6375a982627b0724afc56f16505\",\n        0\n    ],\n    [\n        \"000000000000c59c851bff2090533bc3009ae76cb1ee89e247dfaf11a5c77e7f\",\n        0\n    ],\n    [\n        \"000000000000d779b30aab849a7f8a3af7f283bb95579d2d05714b6c3aeed955\",\n        0\n    ],\n    [\n        \"00000000000016bca79f9de99fd3d0399f812f0fd5be4f84bd7ee442b846498f\",\n        0\n    ],\n    [\n        \"000000000000dfdeb639143c64a17b99b2025a7d9bbb53e993ceb7c1656ceac1\",\n        0\n    ],\n    [\n        \"00000000000023e3d31bc565be6041cad487f77bb23109aeaa804d72bb22d6df\",\n        0\n    ],\n    [\n        \"00000000000007ebb67ae92e144382f52aebbc63a4604c8a07bfebfcf8a19546\",\n        0\n    ],\n    [\n        \"0000000000000136c4a1582c01a5824f4fde4ddf91d653899b53994c4da9a3e1\",\n        0\n    ],\n    [\n        \"000000000000001c197b662a51a6b9d5a4eb9521bc52c82f06aee07e8b58f47a\",\n        0\n    ],\n    [\n        \"00000000ff1ae8e1ad7dc6a82747e125d99e099969d5fff2f193246529b225c9\",\n        0\n    ],\n    [\n        \"0000000048af658629f65a2ef6052fc8cca3234f33d3fc329a0a2b4a73fadefc\",\n        0\n    ],\n    [\n        \"0000000000008cef9b1eb41f402fa0f1f6a1ff6641ef3484d7decd9ceb7f1efe\",\n        0\n    ],\n    [\n        \"0000000000003ff9199a773f976eda5420300f1f42f213d9d793ca002c17a5fe\",\n        0\n    ],\n    [\n        \"0000000000001c6f16503cf6ce37c09f31eceac19e9a46eda67061bec5c6abac\",\n        0\n    ],\n    [\n        \"0000000000002ee7040adca7f697117c59b8df1dc65519e3145d720b01add98e\",\n        0\n    ],\n    [\n        \"0000000000009718224588a74633c646a7539d05ed503064e38f7734b146be9e\",\n        0\n    ],\n    [\n        \"00000000000046fd759769b3296aa5636c3d113a309d633743e092b463072842\",\n        0\n    ],\n    [\n        \"0000000000003594adc1bf018bbb36c059ac293164d83357817eb9b7b3ea320a\",\n        0\n    ],\n    [\n        \"00000000000069f72a110745c74ead6d9f488906ee79cd2e6dfaa77f9371c300\",\n        0\n    ],\n    [\n        \"0000000000001763cf5fffd8e1b0122a7d4bb0e1c2cb17bdf22fa25b70fa6e49\",\n        0\n    ],\n    [\n        \"0000000000000c91abea6900d1ee2c168bc34abef1260776a164caecaaf283db\",\n        0\n    ],\n    [\n        \"000000000000034624e683ff5df51b1a78fe027c67633c77258d3b1fca48a124\",\n        0\n    ],\n    [\n        \"00000000000000b8afa5359b5cc460a77b047cbf8b1aaf640b8779c036c1cc77\",\n        0\n    ],\n    [\n        \"000000000000001733362d084551627b7a71cf84b9365d4a8b1131d8e1f0fae9\",\n        0\n    ],\n    [\n        \"000000000000001c3cd1aef22fb58f8917110976d342a0573aed0a466702adca\",\n        0\n    ],\n    [\n        \"0000000000000015a4454fac29770dce9fc9786152a68807322ef00d74da0640\",\n        0\n    ],\n    [\n        \"0000000000000005628d26b0af6507d1218a6665e8e6867ab37b84a69ba15cd8\",\n        0\n    ],\n    [\n        \"000000000000001551c40d57827ecb548a09f2512ab66a3d3dd86f00f8083ae1\",\n        0\n    ],\n    [\n        \"0000000000000014b20ef449f75f3c015bcef0e19d302e440f9ecb4c183300dc\",\n        0\n    ],\n    [\n        \"000000000000000e814b868e01fe7a4a78d966e4ef73f5293c633005ef5718dc\",\n        0\n    ],\n    [\n        \"000000000000001e4d4ba22dad9356f9753b1065765799c080807a075964f8a8\",\n        0\n    ],\n    [\n        \"000000000000000c2726580d5aaf194818abcb0dc9275266fa6604b792dcc41c\",\n        0\n    ],\n    [\n        \"000000000000002660dbfdb21d80939f5341395eacbd2edd67a61abd58044345\",\n        0\n    ],\n    [\n        \"0000000000000027fb8a498f621a696f6ff1d9b45839940a0a45700f2e211bc5\",\n        0\n    ],\n    [\n        \"000000000000001d3fc884a35029348110a0af44cde5f299c89a89b2f5e3c1e3\",\n        0\n    ],\n    [\n        \"000000000000000edcb8421d37f2d46963dcd2ab31a0359574b49918627c4772\",\n        0\n    ],\n    [\n        \"00000000000000148683ea86525485d22e85bda982e8682f5010865ca3ff3da2\",\n        0\n    ],\n    [\n        \"000000000000000329cf100ca7c05279430275cedc3f4573dce6ac1d418b7734\",\n        0\n    ],\n    [\n        \"00000000000000054a4bd09b4cf258f0bdd86feb97fdc38d66753f3e04a70524\",\n        0\n    ],\n    [\n        \"000000000000000faf34df569486fe78f0b17236e2e855a507e5d36759b95751\",\n        0\n    ],\n    [\n        \"0000000000000008c72a42e89efffa7da953b94c2217f50328d23a098933d6cc\",\n        0\n    ],\n    [\n        \"0000000000000db7c34e4a5e1e2445b7f4e07231e5736251103a1d1dc5b5943b\",\n        0\n    ],\n    [\n        \"000000007795c87d221390511e079257789f1563bcc772571e4481ca3b448832\",\n        0\n    ],\n    [\n        \"0000000004e5b3b03b0223706a7ce40d576d64026784c2ff8614e6562f4d018c\",\n        0\n    ],\n    [\n        \"00000000004a0ea723460162104583b750bea77bac8c967d801c11f5058ccc04\",\n        0\n    ],\n    [\n        \"0000000000000ce34ac861ba5d727e47e9e19cf0db6df7926ad1871e9b94066e\",\n        0\n    ],\n    [\n        \"0000000000001ac9fe6e55ac14cb60f3bc8e2bbd3e35b86a39a64aaa8b71ca54\",\n        0\n    ],\n    [\n        \"0000000000001673268e6db4c8f163bfe25914d4bd50673da5715d8313b7d191\",\n        0\n    ],\n    [\n        \"000000000000068296732b209e33279ba86496a74bff278c6b66e8004c8d29c5\",\n        0\n    ],\n    [\n        \"00000000000091c08da0ddc2340050cac7b30bc5c1c83dd3e7dedf5b08dc3078\",\n        0\n    ],\n    [\n        \"000000000000770a2e7934ceffff83c29b049fa202759b4faf4598fc0fa67ea3\",\n        0\n    ],\n    [\n        \"0000000000001b5d77a10cd4842db233e04c268e57e3b2669aea7701da12adf1\",\n        0\n    ],\n    [\n        \"0000000000000182a40214f1c538f5a44b696d630664456aeaab29264a6f184b\",\n        0\n    ],\n    [\n        \"00000000000001245d616ac4b49515cfed74ba0ff4b7e8934bf18848075937b9\",\n        0\n    ],\n    [\n        \"00000000000000321a47c18d57d5d8d058bfecb43cc19af593359a66851fc605\",\n        0\n    ],\n    [\n        \"00000000000000231e08d8080db64b437e37f17295c82b8561e528829970e9b2\",\n        0\n    ],\n    [\n        \"000000000000000fb903e2942029e3a65aa8d26efb48aa8537aafb3314ff6d60\",\n        0\n    ],\n    [\n        \"000000000000000109729ddaea18bfb7a1f3dda86e11332946cc34d27a988420\",\n        0\n    ],\n    [\n        \"00000000000019477d9b273423ac45be472ac63d88616e5169efe9e3bdb03fb9\",\n        0\n    ],\n    [\n        \"00000000ea0c1249ac03ea5667ca8ff4f327468d873b6a9bb78206e9c3b8cd63\",\n        0\n    ],\n    [\n        \"000000000006321457285a8cca551c77825721f502b83ca06972d697c9e5ce1f\",\n        0\n    ],\n    [\n        \"00000000000214094cbb0ffedab25fb82fcb3db22ee6031e6b952b73fbacfedb\",\n        0\n    ],\n    [\n        \"00000000000541d2b25e067f1707b06d91f19351b84d41aae164ad41facec281\",\n        0\n    ],\n    [\n        \"00000000000164bec334a2c1f79e4af9dce78d838d573b90fc5acd16f544b3da\",\n        0\n    ],\n    [\n        \"0000000000030c337a7f2bde97cbf6a7c71a0b4d24e1e4600e46eac009069221\",\n        0\n    ],\n    [\n        \"00000000000555a5e32651e24a8b83f4aadfd689ba44d49e63329bd2ed484078\",\n        0\n    ],\n    [\n        \"00000000000207e99a06cfc158f5d7d8cb27234cab15ab00fec24fd8b8956aa4\",\n        0\n    ],\n    [\n        \"000000000000c819f87342c54f0d4970443264a296a88bf38848ef1bcefa05af\",\n        0\n    ],\n    [\n        \"0000000000001838f689e4d297db56c3d50753378cb2458dbb5f4392ea90c585\",\n        0\n    ],\n    [\n        \"0000000000000996e7334726403dcb4d2818096f645f6782222e0c9fa8ab366d\",\n        0\n    ],\n    [\n        \"00000000000002502c65e6a2ea56681d90d9cc6e774929a6ac17c1adcb0c6aeb\",\n        0\n    ],\n    [\n        \"00000000000000f2b4ec952983f6e68cb3a46d34738c38a958de3ced1da54e42\",\n        0\n    ],\n    [\n        \"00000000ab4d311932bee7b754ad97e081cc24b0225d3089dae7440fc084c623\",\n        0\n    ],\n    [\n        \"000000001104264440198251f8c9046dca4a3ffbacbc48be03baf996c3a48094\",\n        0\n    ],\n    [\n        \"0000000000023cb28ed8ef3637a80e73ec92ea7210088053002be02818fb9f98\",\n        0\n    ],\n    [\n        \"0000000000002bb738283a319f1f1b8bbe365b7f6f295982c29836d193808f6f\",\n        0\n    ],\n    [\n        \"0000000000001a15e8f691a8bc14f3d694abef473c5818017ed391615b45fa39\",\n        0\n    ],\n    [\n        \"0000000000733fe4dca7d1dae0a748bbd9b3b99e687b97281b806a199e35ca54\",\n        0\n    ],\n    [\n        \"00000000002636ed79a1d367fefb464aa532b26dbbe8687ffa5d5f26eeee06dd\",\n        0\n    ],\n    [\n        \"000000000006f45c16402f05d9075db49d3571cf5273cf4cbeaa2aa295f7c833\",\n        0\n    ],\n    [\n        \"000000000000dcaf9949232b5cf92cc2976ee59521f93ec2197a9c762c6a3c54\",\n        0\n    ],\n    [\n        \"0000000000006c7706b78284427681abcb62c432adc364975590f3b33d94b773\",\n        0\n    ],\n    [\n        \"0000000000000efdfdbe08218cf961d25cc1956d464a9f067f25545301b79222\",\n        0\n    ],\n    [\n        \"00000000000009ca0d54316bdadcbaa48c53fab88b9fb0556472ed9e91751602\",\n        0\n    ],\n    [\n        \"00000000000002395eb7f05c543dbbc241cd4b5d64b3c948f64d8ac2083b197c\",\n        0\n    ],\n    [\n        \"00000000000000e9883c7e7284799cdb9ff4e4208a313050a182172c950f7e73\",\n        0\n    ],\n    [\n        \"0000000000000010b95f7caba61a90def8a4b527ce0574092718f478af780c10\",\n        0\n    ],\n    [\n        \"000000000000001fa1eeb7f86389fada9fa3e05a8e497c23a08ed6a1dda63a8b\",\n        0\n    ],\n    [\n        \"000000000000001086a413f0cebe3ab3aad747f6b34a5038856592675f4efef9\",\n        0\n    ],\n    [\n        \"0000000000000028a8f135e691760cbe5c4b7a0d88a4318b8fb353aca220025e\",\n        0\n    ],\n    [\n        \"000000000000002460d6c79c6e2cde645eec7b64d4fd48a1c71d756cc9c3dcb1\",\n        0\n    ],\n    [\n        \"00000000000cae97fda2191050892ea192e7c15881773b17e3bc1331822fe4bc\",\n        0\n    ],\n    [\n        \"00000000002d5f11ec72e37b756a94058c299fc647d1603efd6067e20c24d306\",\n        0\n    ],\n    [\n        \"0000000000035d32b34aa575bbf8420ca6caa6646840539c47d842c8b6700771\",\n        0\n    ],\n    [\n        \"00000000000038ea640ea920adb8ac40cccdc56ada27cb52e3ae06ba0580572d\",\n        0\n    ],\n    [\n        \"000000000063dfc0373ccbd4d400980fe603c0af468d21d3ff8eb568b480ea65\",\n        0\n    ],\n    [\n        \"000000000000a46891a576d73cad07d20f3ad9308657bc676b12a4f066915ce2\",\n        0\n    ],\n    [\n        \"00000000000d58b8cf55bd312e564ec6d2959162b546dba8849b0e4e0dae37a2\",\n        0\n    ],\n    [\n        \"000000000001e72b87fa955829ec8dc21878f11db8181c7475e2d03c79bd0e13\",\n        0\n    ],\n    [\n        \"00000000000032848796aec72da3f9dc0ed83ccb99023e9afafd7f3d9bdf7103\",\n        0\n    ],\n    [\n        \"0000000000002e93d090b9aa138cd12a5362e0cd4232a3b96561fa7bf280a103\",\n        0\n    ],\n    [\n        \"0000000000000dbc7b9754521c68f2553265437d589fb6b2615dfe4d960ad690\",\n        0\n    ],\n    [\n        \"0000000000000fe4edb18d30fbc59db0c34a39b33c878108825a7d8df4d99b2f\",\n        0\n    ],\n    [\n        \"000000000000003fe4956957ad4d5c19c79613f9840ab51bb841da535b449861\",\n        0\n    ],\n    [\n        \"00000000000000fd3eef340a6953ffc756eab83dbe091bda721387f7249835c6\",\n        0\n    ],\n    [\n        \"0000000000000062e7ccfc5414355a0a1b5151b496d1b77abe7606920c0f5251\",\n        0\n    ],\n    [\n        \"000000000000001f6d8dc4976552a596eff2eb0df15b0d9ee61a55091a2050c2\",\n        0\n    ],\n    [\n        \"0000000000000018a7ce07d0ac46c3eebd72bd2db0db627675e146fcf9278e4e\",\n        0\n    ],\n    [\n        \"000000000000000be86cb1664031f8666023b52d247063327613b00619f66514\",\n        0\n    ],\n    [\n        \"000000000000000c1589a255f9fe686ee448e8bf60424529c1f842b71bb317c6\",\n        0\n    ],\n    [\n        \"000000000000000a2d14a86314974abad5934ed38f63276fec039d636f33d652\",\n        0\n    ],\n    [\n        \"000000000000001737d639951d593f9d269bafda7d5fe5a667d41f8d8d2c9cfe\",\n        0\n    ],\n    [\n        \"00000000000000033f81ecaba0452707d61b7e76d1b18809b20db18de30f3b00\",\n        0\n    ],\n    [\n        \"000000007f31278326e2cf458be3fbc904d4f98f3b348c8f2f3042d590fe2ddd\",\n        0\n    ],\n    [\n        \"0000000000018ca40c2e36e0d484b57d41714c2bf5bf69ab06eb214c252d63f3\",\n        0\n    ],\n    [\n        \"0000000000026de8f53cc5ecf634c484105891ef12e8abfc3e83d750c07d89b5\",\n        0\n    ],\n    [\n        \"00000000000d340049196286b501419b72c66bc6a45ad177690e0ff641c6418b\",\n        0\n    ],\n    [\n        \"00000000000e70c72347a1043a7e1fd35483242689618a5576fdac0845dc96a0\",\n        0\n    ],\n    [\n        \"000000000005aba9cf40fb0f07b3226bddeab8b997de4e827ff9d9f7198f7ffd\",\n        0\n    ],\n    [\n        \"00000000000c1c2b6e4b064438ecedc3028edb111fa571062d62f84912f407c0\",\n        0\n    ],\n    [\n        \"0000000000016e1ed29108b0224b172a3247f61b7589526566cc62ac69ce9b5e\",\n        0\n    ],\n    [\n        \"000000000000f0e2e3a76f57f843ef8823216e1789c9cfb97f17f87b719a22fd\",\n        0\n    ],\n    [\n        \"00000000000022998f49c0c1a4519125272270b9d59857f0d302c76017171c84\",\n        0\n    ],\n    [\n        \"00000000000008588b178655052953dc2eccf8c0a0648f15c1d5ceeedda91372\",\n        0\n    ],\n    [\n        \"000000002cc217b3217e3016d6e6eba2584619caacfe944342ae905fe24c87ef\",\n        0\n    ],\n    [\n        \"00000000000030120f45e78e7852729d6925cdb3c5dbc87eb8f167674e722bf3\",\n        0\n    ],\n    [\n        \"000000000000355aff4c8c416f3ec39b77071a6614cf604c213f3e52a405a221\",\n        0\n    ],\n    [\n        \"000000001d5087abd326f27fcff310c58999d8881e5bf0cf30c826177680508e\",\n        0\n    ],\n    [\n        \"0000000000002393b75ee9ce1a0433cbefb3fad205406fab65bbc0a61a757149\",\n        0\n    ],\n    [\n        \"000000000000c57b6c1be1998660d54eb13f10ba3371cb735859dbb7c4396799\",\n        0\n    ],\n    [\n        \"000000000000bbe163a8a5f68e9ca2c99bf9af0a9fbecad4170384a0bf907b26\",\n        0\n    ],\n    [\n        \"0000000000005d2078380dd0389664a0de79dd7c6a8c1b94e0522b0bec15f74d\",\n        0\n    ],\n    [\n        \"00000000000029b25af79b8d27068c32865d2a41817fd1a34586ee280c7455ad\",\n        0\n    ],\n    [\n        \"0000000000003af3be1fa30b8c225a0b9061aa07e3389cb44161d11b15ee59b5\",\n        0\n    ],\n    [\n        \"00000000000006450af5e500b7a650da7a2043c7f3936e3aa93cd08c22457341\",\n        0\n    ],\n    [\n        \"000000000000a012ef365be0880e3d7ba88a9a8378429733d608c6cb6d7fe59d\",\n        0\n    ],\n    [\n        \"00000000b3a9e0680e5ecae9a639b6ccb19456c59eb66ea1b53495f8cb579874\",\n        0\n    ],\n    [\n        \"00000000015eb6c7abe02adb992d359926f511d9830c6a18b8cfb8b3bec85cc3\",\n        0\n    ],\n    [\n        \"000000007699589244ef2cc09903aba91032e7e86d4e773ef639ff0150db2fe7\",\n        0\n    ],\n    [\n        \"000000000000fe9483a6e60c2589355742d93b30406a2d273e10ec58558cf34c\",\n        0\n    ],\n    [\n        \"000000000000b02a657620c34404936792268e4453882f203ae7add74573083b\",\n        0\n    ],\n    [\n        \"0000000000002c7630b21c1ec243de251dcee944ef0ec3cd9d559c34f1fa7d4f\",\n        0\n    ],\n    [\n        \"0000000000018c93ec3fde56ceea4839e6d08856aca055508c562d96e16dbd71\",\n        0\n    ],\n    [\n        \"00000000000015db7b745e849b5a05399ef66a96d31809fcd79556ed7479965d\",\n        0\n    ],\n    [\n        \"00000000776cbc7a008bdfa21270d6f18c58d8a794dbf7355cc71ea8a0c8c063\",\n        0\n    ],\n    [\n        \"0000000000002ffc1ec2f0f2a757d589e452794b76e50366a452ad3d318f15c2\",\n        0\n    ],\n    [\n        \"0000000000002d727d581f7f0a93b74f5585e62dcc6fdc9cc3b8b19eff10f700\",\n        0\n    ],\n    [\n        \"00000000000010bb6ada118d713af3ca721db8622bf222477c3d208f8b3061c9\",\n        0\n    ],\n    [\n        \"000000000000e03cf9e4498fba163d2bcd2646991441169481b940d291fd075f\",\n        0\n    ],\n    [\n        \"00000000000026e1da1de58906d3d12221a79250f10abb77623a230c8a62fdea\",\n        0\n    ],\n    [\n        \"0000000000002475dbf8647609a128ac7211c4ca7b728a989f1b5481e626a328\",\n        0\n    ],\n    [\n        \"0000000000001f2e545dd92ec4a9410e3d6a5bf11a6bccf97f049310538ded2f\",\n        0\n    ],\n    [\n        \"000000000000ac4ecb8bc7ff25f1aa9633a9cfa28d0bef19870c51e0a9431f30\",\n        0\n    ],\n    [\n        \"0000000000003c493ecc2121c5c3bdca88a141444b2631d6a9b720a504bf455a\",\n        0\n    ],\n    [\n        \"00000000ad4ae3258584eca0ac04c31af2ea25d1d2b811279994c017136d160c\",\n        0\n    ],\n    [\n        \"0000000000edeb17c5a1443fbd6f9f9ff20a1691bde5fa42ced3fbc6413a5e01\",\n        0\n    ],\n    [\n        \"000000000b61f092a5c1660d0f8f6fb3294c9b065c4ef2943a2955a2d03c3708\",\n        0\n    ],\n    [\n        \"0000000000000ece5c82d89b13143a1a28db9b12bac561928c0a1d709faeb479\",\n        0\n    ],\n    [\n        \"000000000000000929103fb8a1aa3f8215c61e990f02cc6085627a6cbe197e00\",\n        0\n    ],\n    [\n        \"00000000000002c9b7ef56519aeb047fa614198081393923ce8f78db40e7ff43\",\n        0\n    ],\n    [\n        \"00000000534f7159b8c190680b93775e91460d5203160789615a92e821e3beca\",\n        0\n    ],\n    [\n        \"0000000000003d1a6c7cf11ac53c59a12fe127eb9c2058cc6a2e5229136c8c3a\",\n        0\n    ],\n    [\n        \"0000000000001749960590ed08ebd150823d21b32020ad8b219adce32e7344c8\",\n        0\n    ],\n    [\n        \"0000000000003384f9d5b10cb0321baf1a99b5f37458155ab794649b12eec2a4\",\n        0\n    ],\n    [\n        \"0000000000000bd9def7511faf3303d889daae01de92f5716a6409c44f9ccdba\",\n        0\n    ],\n    [\n        \"00000000000014b324e5afdc4b5899942a9bc776d9cb00ec2af5b795c3a74fda\",\n        0\n    ],\n    [\n        \"000000000000415e1f654f779786094eab0dc703010b3f686baf7defb83343d0\",\n        0\n    ],\n    [\n        \"0000000000001c9b4a7c7e351e10f47852dc9d5c6b7c7c4518591fb6ddaab3a0\",\n        0\n    ],\n    [\n        \"00000000000052b7b9fea051b3fe3244d1b86516aaea33f2d5a55e977fe9c026\",\n        0\n    ],\n    [\n        \"00000000293536c90c630e207de478560eaaac89ae8afc33333aa7963dd8b7b2\",\n        0\n    ],\n    [\n        \"000000001a45d126188b331059d04a98df3588887adf6a2b520225a2a2b03567\",\n        0\n    ],\n    [\n        \"000000000010a96f7c94770392a3fb38777d9d75ff755b5043919475ac396121\",\n        0\n    ],\n    [\n        \"0000000000007e19b090529e49795e4c115c7f00d327da0568fec93d542ec878\",\n        0\n    ],\n    [\n        \"00000000000016a2c2a305974b890779ed07afecdc45e9de20397f52a88efe36\",\n        0\n    ],\n    [\n        \"00000000001588ca17c8d2f9051d75f085daaa519b85c7e25d14b5871c4cf25a\",\n        0\n    ],\n    [\n        \"0000000000000e31ba2c3036fb984c5faf3e2860e41799aedd75b469287e8f34\",\n        0\n    ],\n    [\n        \"0000000000003236b8dea540661d16c62aed83879951331d7ce97290a761006a\",\n        0\n    ],\n    [\n        \"0000000000001f30b54b080cec64d819fb2270932883cc048b7e614ff3405b1f\",\n        0\n    ],\n    [\n        \"0000000000002d5f3ebb70abd3119117802b2878af6fcefb58f43794a152b6fe\",\n        0\n    ],\n    [\n        \"00000000000001ba1b76793d0ef69a1cf35b8af6421daba0224db06bacafbc1e\",\n        0\n    ],\n    [\n        \"0000000071289acecc04f86e0d71e4dd9af80c1d62d7d3d8d2a4730b458c1694\",\n        0\n    ],\n    [\n        \"000000000077d6b5ce95ac4bb2607afebae36fae00f4d9f668275a1d42025a1d\",\n        0\n    ],\n    [\n        \"000000000001f970e0880b7f3f9456e5b9211d2e9a407a664a0cf5d79fb0d07e\",\n        0\n    ],\n    [\n        \"0000000000000c5cda8f16ddba06e9307903d6e75b6f0fede174f9d1214e85af\",\n        0\n    ],\n    [\n        \"000000000000339de4dd4e26cfeb620ee6958b460f30d7bfb31f027736a51fbf\",\n        0\n    ],\n    [\n        \"00000000000039e58cfdcc21226f2499dc5f88a8c28e39c20687d2ad5d4f41ef\",\n        0\n    ],\n    [\n        \"0000000000002db5a8e0bdced8ffd6e4803bc51a425436e2c05f90a1c69cac03\",\n        0\n    ],\n    [\n        \"000000000000c76e1a3b84465e3ef6de278a563e602e2e9040c18a19053bec8c\",\n        0\n    ],\n    [\n        \"0000000000002cd1ffe2aec0e5d9d6147b21e89f638b3960daac6c5ff98f6083\",\n        0\n    ],\n    [\n        \"00000000000016305181caa8d88541b5495065c6231b67cc3df5430b4d0b8d46\",\n        0\n    ],\n    [\n        \"0000000000000161c619a838b35752b87b780f702d0e314a6acf517af3e36232\",\n        0\n    ],\n    [\n        \"0000000023588058c8895766d31ad2c44ccee1150dcfdb6218add8711b205544\",\n        0\n    ],\n    [\n        \"00000000004acb3364caf13a892af294d3ac17340c3713f38e866647e7d2c2d9\",\n        0\n    ],\n    [\n        \"0000000000c238dddd6860a7b597f0974e5f870b39f79f00035734e287d8891c\",\n        0\n    ],\n    [\n        \"0000000003f232ff7c873bd2668c438e93974a157e8e44dd6057a23c406a04bb\",\n        0\n    ],\n    [\n        \"0000000000003684f0ccaceb330a90045b3f5b6e790c55fd9214adb694bc6151\",\n        0\n    ],\n    [\n        \"0000000000000ca6a9286942ffa849e82cf75b15f785bcc0be82009625341703\",\n        0\n    ],\n    [\n        \"0000000000002a7dfd1764890b04221aae58e78638dad039a952be3aa2b270f4\",\n        0\n    ],\n    [\n        \"0000000000006b39c0cef1f968bfa7373478222c7804c47671e92b81df352e0f\",\n        0\n    ],\n    [\n        \"0000000000007f70368bff3515e745dbd8da7bc9bf5846bbec997c76c4e9f598\",\n        0\n    ],\n    [\n        \"00000000000016ba9493ea10dfdc68483cdd3a1246bfe5a42c3042fe46e31452\",\n        0\n    ],\n    [\n        \"000000003315cf62b16bd31e5ffff48a77c4332466e75653de573596dc9e6811\",\n        0\n    ],\n    [\n        \"00000000000112b139086612b6ab6d98836eff016c28da0b68cf51a63d1c5ab8\",\n        0\n    ],\n    [\n        \"000000000000740c7bd5664ec75f595399815676a1cb31da175f55b216c0489c\",\n        0\n    ],\n    [\n        \"00000000000040e34183c66e6d8803b523f132aaa0c53eb0d27581f1cd202fdc\",\n        0\n    ],\n    [\n        \"0000000000212b96599a158f45a8f8ace26be06d8c77ca7b5dc4f4cd5d2479ee\",\n        0\n    ],\n    [\n        \"00000000000047ec63694024c7f4a742b506dcdf87dd6d99ef5166fe070ffb20\",\n        0\n    ],\n    [\n        \"000000000008bdd2ec2fc909564ad5770d9ab7f88c95c7a2b0efd6452b7ffd12\",\n        0\n    ],\n    [\n        \"0000000079fcb43bb7271e55d15c27b718cefec8d1d22a1cead4061896d25f12\",\n        0\n    ],\n    [\n        \"000000000000e0d307898c7a1ef362c31bb7c4436075444c6f8cb235f02239a5\",\n        0\n    ],\n    [\n        \"0000000000001a9256df4ffa52b4e184e713b5a0f5315b0d4ce1a3e77c916464\",\n        0\n    ],\n    [\n        \"0000000000006b3b9c9117ab64f197146a313f0874535fa4c9a3de4fc37b0461\",\n        0\n    ],\n    [\n        \"00000000e14400a3e77585e044d3a526fad991db8af00392286b7fc143e69b12\",\n        0\n    ],\n    [\n        \"00000000000e8b4bd2bdd5ad3c06cb2fa234c271ec25ca87ce3941dcbc49afa8\",\n        0\n    ],\n    [\n        \"00000000017ed9122f0371f08682c5fe6aa8418b265efbab0993b1ed00a89f45\",\n        0\n    ],\n    [\n        \"00000000006f039a8997a1b95777b781d9650bb184cd80eabab31aac94a278f1\",\n        0\n    ],\n    [\n        \"000000000000e4b80abe3cd2441ea4eb0a5cdb6b89c0e8ff3f369cfcedd0cb68\",\n        0\n    ],\n    [\n        \"00000000000026117fc0fe66f1a60bd42bc2d091d13ffb349dc60277da717fb5\",\n        0\n    ],\n    [\n        \"000000000000085133f8602b7408ee4d7b9e777174d520da980bac6a2bc746ad\",\n        0\n    ],\n    [\n        \"000000000000b0429a45f601bc84f00778ce1e7f5c3636a7c215ad6721c7927c\",\n        0\n    ],\n    [\n        \"0000000000001cf9d3f7f37d9c3999b05d52e6ba5434931741aeacf262bcb9c1\",\n        0\n    ],\n    [\n        \"00000000000007f291ae30aeb2f85877116865e06e3f10658c3b3d8de7ba00fe\",\n        0\n    ],\n    [\n        \"0000000000000c460fafe518178f959512c3966bbf6569869e51ca81d69533ba\",\n        0\n    ],\n    [\n        \"00000000000001ded7aca43353f5258fabb96db1b785fab0daef4c427e9fe170\",\n        0\n    ],\n    [\n        \"0000000000000002bb51deb7934ee1abaddb6607b267a9f7947f279c2848f064\",\n        0\n    ],\n    [\n        \"000000000000004b2fac327d5bdd9b7e12512547ec70fd7c3bfa0e73c8bc17da\",\n        0\n    ],\n    [\n        \"0000000000000002b5ae6d256f769f4c445f595277e679ff34b78dee498566f9\",\n        0\n    ],\n    [\n        \"000000000000000760ab2425e4fbdbdb5fd588b5e2bbc15d57e1431e90699fdc\",\n        0\n    ],\n    [\n        \"000000000000000047c410d3799304f0e61c423993f6442291ae77cd06bdb113\",\n        0\n    ],\n    [\n        \"0000000038cd3783bb8bea435b42e3870544e610dcd642da655faf8c4011d455\",\n        0\n    ],\n    [\n        \"000000000000048d4df1b36218595e7626bced446edf8efb8394b68a3fbd1e6e\",\n        0\n    ],\n    [\n        \"0000000000003fb4daabfe5cd01b7da1808668ab18c68353d1ee0546453682fc\",\n        0\n    ],\n    [\n        \"000000000000277e15671d375ce9a4499b66f1f8b33c4f6afd74b5ea20158614\",\n        0\n    ],\n    [\n        \"0000000000002ab11ec0ab446ac363dfc93a9bf09a28d83a85b9bc163d5fa5fd\",\n        0\n    ],\n    [\n        \"00000000000002d74bb89f8320bd80b07f7b8bc4b77987039e6fce894d63c966\",\n        0\n    ],\n    [\n        \"0000000000000f1c86e38fec5fb04c19440c066205f89f0ba151cfad2a41cbfb\",\n        0\n    ],\n    [\n        \"0000000000000168baff0b9e9c678a5153004f7f241f52ef1e49d88c1bad2ef6\",\n        0\n    ],\n    [\n        \"0000000000001b3bd58011210021d92656719030cfde3c033bedb3c1e6a81eef\",\n        0\n    ],\n    [\n        \"00000000afe6a4017bfcd2c3c9a21efe4c41951888a9c97c5c3d3bba9f30ea68\",\n        0\n    ],\n    [\n        \"0000000000000836cb2bd14463313660efed9d0e23339fcaa97c54295e9716fd\",\n        0\n    ],\n    [\n        \"000000000003e336698dce2d615f693645b4d04f8e5f98d02d94fca99bd5a60d\",\n        0\n    ],\n    [\n        \"00000000035b7a00de4f346dfda49c16db195867f3c395d5d83785acf51908a5\",\n        0\n    ],\n    [\n        \"00000000000018ba5cb649b9e7a5458467e7275e4e3284dd1d2dee9f0a64a2f2\",\n        0\n    ],\n    [\n        \"000000000000239369e70e689f53bd3ab435c3eb6e5a26cccd52dcfdab18b255\",\n        0\n    ],\n    [\n        \"00000000000030694285ec9b1b6349dd97757b16355fe07de048f969119575d9\",\n        0\n    ],\n    [\n        \"0000000000000657b62914a9c16779754d11d1c444b3bd0965a571f7afdeab21\",\n        0\n    ],\n    [\n        \"0000000000000e1429ee6f6266eaa4a21b71335503eb73bdfb4f736a1d9fcd3c\",\n        0\n    ],\n    [\n        \"00000000000021d4eb583b65086025e1147e7783b71b61e94b0b1d01dac32479\",\n        0\n    ],\n    [\n        \"0000000000000afd677ed22ae10ce1ddc3c9f3e73e1c1d1398cdfb581cd43cb3\",\n        0\n    ],\n    [\n        \"00000000000003fdd00758300566d8f27cc9461e70c64f3b1ed8e20c1c2f26ff\",\n        0\n    ],\n    [\n        \"000000000000003057478e768b1d129fe5f1acb1a99818df164966c68cf22471\",\n        0\n    ],\n    [\n        \"000000000000000072bdeb5162f6c77003778bf3c77520028687e0137a58d8dc\",\n        0\n    ],\n    [\n        \"00000000000000070be95e5fb581cd857fee7617219ce4047d31aea097cf9fb7\",\n        0\n    ],\n    [\n        \"000000000000000737a7a678467547316706f3ceaf9d8d0a56e50d63bb834a74\",\n        0\n    ],\n    [\n        \"000000000000000591b541ed7088c4ce52fd10a0b99a4b5db377a3c1ab198756\",\n        0\n    ],\n    [\n        \"00000000193b4863d9b143d45b6db44ab8706e1eb4cf76e960b6390d8654f317\",\n        0\n    ],\n    [\n        \"000000000000d99c10079c94e38f6153dabe29d5b0315968016e70049f0ba72e\",\n        0\n    ],\n    [\n        \"00000000001603022822335be28b6e82ae1761ff293913016f577fc6db2e2d46\",\n        0\n    ],\n    [\n        \"0000000000002ad9d4f5d7246b599fc9acdffd10b30eeb1adea05d38ddb9986a\",\n        0\n    ],\n    [\n        \"000000000000050756381c62b7009c293513d5141798b5061530fd74aac98bb1\",\n        0\n    ],\n    [\n        \"0000000000000dfe8be3b1e0dd83a4babb473a79242f40264f21936baac8facb\",\n        0\n    ],\n    [\n        \"000000000000f77717ca2bb34b74dacd3ee6cda1e660f18498a386da0a884bbb\",\n        0\n    ],\n    [\n        \"00000000000a110d1be3a230af9e48d99457e00a17ace1b3c5d425e95611bf05\",\n        0\n    ],\n    [\n        \"000000000003d50b2c17cbf2d5df1556f0849a33096d72bdb3e64c426202961e\",\n        0\n    ],\n    [\n        \"00000000000067dc944a93268621c566e31b879bdab5c1966510d73254aa48c8\",\n        0\n    ],\n    [\n        \"00000000000035fabad3658e0d84928ec43f7b66b9efb872d907e1af0a86dde3\",\n        0\n    ],\n    [\n        \"0000000000000a1a7a49cee85178df151625b1f9fe1857ad2bf0d47aabe0c2b5\",\n        0\n    ],\n    [\n        \"000000000000021c7bed7852ba91de97ffea03f5f3ae1d6b7dfdfd002ff6b01f\",\n        0\n    ],\n    [\n        \"0000000000000031bdc46f3de83a4d5a9b164348aa33393ff74fb67c22f73dd9\",\n        0\n    ],\n    [\n        \"00000000000000388320f1a185ea53ee5bafeb4bb7b23ae05355db3ec3b9f3d5\",\n        0\n    ],\n    [\n        \"00000000ea0ddfa9fcf128f566b08d4341b608b85d2d0cdd05156a6b5b49b663\",\n        0\n    ],\n    [\n        \"0000000000016eb90eda23984b9615fa7f989aef9b339d8521624793c32aec32\",\n        0\n    ],\n    [\n        \"00000000249b02d570ff2e7a1886eab612d591c58c15caffac878f9d7e69a2a2\",\n        0\n    ],\n    [\n        \"0000000000001601a74f55eb45449c945c069cd228bbd28b662a4cf929a07b26\",\n        0\n    ],\n    [\n        \"0000000000001d54ce071b6e1adc6489ead4869d27cf4467e09865417d7ede04\",\n        0\n    ],\n    [\n        \"00000000000006b263d940ab956c185c9e8a39806c5d3ac9f04dda3b8cade6a6\",\n        0\n    ],\n    [\n        \"00000000000018eb028b23c464eda0f6addbb9c36adc134fd7300f6c3306df95\",\n        0\n    ],\n    [\n        \"00000000000008e0b67636b199f21f04fd3d413e9678c39d27e30536a1b249c0\",\n        0\n    ],\n    [\n        \"00000000000008df86828e07c01137240d45cf9ed5fff249d071d45786d1f4b3\",\n        0\n    ],\n    [\n        \"0000000000001bba9b529e6aa2cf8cdc83264c806e079f5127831fcf44166bb8\",\n        0\n    ],\n    [\n        \"00000000000025e87dc84bc04654c2b30a87d862c0a3420ecd3084670b647d7e\",\n        0\n    ],\n    [\n        \"00000000e916d01f1d6d4f170d88cd65a778a940ce11c55996a397f21030dfcf\",\n        0\n    ],\n    [\n        \"000000001ea4d2da640a2a166e8a51a3cc5557fca1c2de40acc810ed9f9d29ad\",\n        0\n    ],\n    [\n        \"00000000000018a01a5f01b8b543f5c211f55d532a03c9a04384ae4257bf61e6\",\n        0\n    ],\n    [\n        \"0000000000000690cedd07d7bdd3ca2bd2029abe58919600b4fabe99fe4e412c\",\n        0\n    ],\n    [\n        \"0000000000002ee49f8029332f30be5783dc2edca08ad279f114bf6c231d1dbf\",\n        0\n    ],\n    [\n        \"0000000000003aca3ef92013e9c559c124b27d9df45bc5b9c620f30987cd6d71\",\n        0\n    ],\n    [\n        \"0000000000003b73bb514fba5f8f063fba66c91534f1f6ec4ad49ea89bcaeb4a\",\n        0\n    ],\n    [\n        \"0000000000001e461eef655c3ee4f013a079ff05e750578cf3195399f274688e\",\n        0\n    ],\n    [\n        \"0000000000001d16126b33bf95b244af4526a6f2af6afd2d96797f4b592624d5\",\n        0\n    ],\n    [\n        \"00000000000020dba1c8f9785e2279ea96a7885a13124af2baeeadc75927ec9d\",\n        0\n    ],\n    [\n        \"00000000000003e1975d0b4a32af071ea8584ef8b575527e2d81cac453e6a4ce\",\n        0\n    ],\n    [\n        \"000000007cbf5b51fb38ed9058deb8e967476ec30fb04487496f1f7802a9eefc\",\n        0\n    ],\n    [\n        \"000000000000041a6a7ad96bda602e7d528542ec9694ab3ef72908d59a660150\",\n        0\n    ],\n    [\n        \"000000000000afa91fc6839e2d12370cafca0ab3679e3906a436e1d6e5ec038c\",\n        0\n    ],\n    [\n        \"0000000002b73257cf72d60433d542fea3fdb0362158a238338e49d6beb109b5\",\n        0\n    ],\n    [\n        \"00000000000018fe0e90bfafc82833f8993636604d963ff226d6605a3e81c887\",\n        0\n    ],\n    [\n        \"000000000000028245c822181fcd9bf9e1f22d6a547fea3ca46de3a3070fc28f\",\n        0\n    ],\n    [\n        \"00000000debae16db51d4ef186b18f53de6749b95fd75efe3e5f628e85cd1d42\",\n        0\n    ],\n    [\n        \"0000000020703f051f8a4d4fbfdd21f7a857570dfc0617ffb7287a7b259cafd0\",\n        0\n    ],\n    [\n        \"0000000000000225bc005b876850b2cdea99b82bd6a8637ecc7f45c49d4848de\",\n        0\n    ],\n    [\n        \"0000000000000178dee2f53404f5a999e1c60913658ce963a671ba778ec88fb0\",\n        0\n    ],\n    [\n        \"00000000000013acc2e31eb20cc2f99d213fc962710967d20aaeb2976b6bfdc5\",\n        0\n    ],\n    [\n        \"000000000000042447cf56d117295a3117a19412861e7b3987846c31ef6b0797\",\n        0\n    ],\n    [\n        \"0000000000001c84c583dc747fe32cac73edd483cab5a3119f68baa4110929b7\",\n        0\n    ],\n    [\n        \"000000000000011875b33bcaaf4dc9535a659684fbe4c780ddf88ad607d922e3\",\n        0\n    ],\n    [\n        \"00000000000017e9433e75fedbf0da7bcfeab74ad1f8ced7dff6dc2bfbd12d96\",\n        0\n    ],\n    [\n        \"0000000000002cff5e0c5e2d765b92df11d26d4b308146af9c3d971945407061\",\n        0\n    ],\n    [\n        \"000000000000083c1306d75a0c18b0942d0ad0aecb878e24c164a9caa3fb2ad3\",\n        0\n    ],\n    [\n        \"000000008addb7a8e8081084ce3290e7f3806ec3cccf747d487f1e1540f7c398\",\n        0\n    ],\n    [\n        \"000000000000048f3edca928245bf247645385b9a493abfd1b5800ad16cc0d1f\",\n        0\n    ],\n    [\n        \"000000000000103e9be4a5421fbc8bfcb1551b0f24857ce6c37a1c343aeae48c\",\n        0\n    ],\n    [\n        \"00000000000015a51f35436fb51d0fece5cc4e436e8a064b03d8605e47bcd16d\",\n        0\n    ],\n    [\n        \"0000000000000f080e1a6f704a41308db272cde3db3ce7ecec09a95b41964e83\",\n        0\n    ],\n    [\n        \"0000000000001fe64f07e3a57ba7f1f3a621ce247fbb352b80b2d0c5b70ec6de\",\n        0\n    ],\n    [\n        \"00000000000026ced680e9c4cfd857f9e3620c09853402e3adfee74143bad265\",\n        0\n    ],\n    [\n        \"000000000000090b4e9b558c597679770325c9b22acc7561662002db0cf7e710\",\n        0\n    ],\n    [\n        \"0000000000001e1214657a7da88bd6370b3dd6f4ab346f0a8d94e4e987f87503\",\n        0\n    ],\n    [\n        \"0000000000000a15c42118c002d59514deb80f9f5466d27963c04330e0e21012\",\n        0\n    ],\n    [\n        \"00000000000007a242146cd528e009c348cc08eb1f95f4d670c7dc158f2bbfd3\",\n        0\n    ],\n    [\n        \"00000000000001e36bbd55b594405000f74d9617538306d1a80beca516598c19\",\n        0\n    ],\n    [\n        \"0000000000000039a5021d0c7f2786e5809c059d1bbc282c75f1b23aab96a88e\",\n        0\n    ],\n    [\n        \"00000000fbd8390594a4ba46a5bdabdca857c83875b424d25a77b0eea3dc3237\",\n        0\n    ],\n    [\n        \"00000000062932ff37ac56e6fa111466872a8dd63a40e075cf5ac17711cfc54d\",\n        0\n    ],\n    [\n        \"000000000acbd85b88534f49001b7ece8fd3bb6a7d9ac1579e266c504d4806b9\",\n        0\n    ],\n    [\n        \"0000000000000c69ee7072ca34b8e0de031d0e1a94ac4808fa9e0d5d54bfe09b\",\n        0\n    ],\n    [\n        \"00000000000136712b53c3fa040965e914ae2c77cc575b3043ca721288720a7d\",\n        0\n    ],\n    [\n        \"00000000000013bbcc926582af7d0a9916b373a8a915c5db4b7fc5384ecc57ec\",\n        0\n    ],\n    [\n        \"0000000000001d67d8a253a97e007bc9b7610d837a4b152012dcf33e8ec52677\",\n        0\n    ],\n    [\n        \"0000000000020090573bf3f7cecba4b23ab397d04fa40ff74fea9689841969a4\",\n        0\n    ],\n    [\n        \"000000000000045744fc4904ecdf15ad85d3069df9992d4077511bc8cf476149\",\n        0\n    ],\n    [\n        \"000000000137d3cf68eae26792414e81cabe328d7364b5c407e0b4ce0a2f5a4c\",\n        0\n    ],\n    [\n        \"0000000000000419e67c1b77993aec325ee5315eec3fe8b4d165898e5c77fe96\",\n        0\n    ],\n    [\n        \"0000000000001cf0d2c8b2717072cd15f060e622bc73414e357324c792e1aa59\",\n        0\n    ],\n    [\n        \"00000000000018c48cf760d66c867ffcd0e113a4eef5b5f85d0dab65b727b460\",\n        0\n    ],\n    [\n        \"0000000000002f9b1c3e3dd47a73bc9711b6f755d72514ec5d81ffb9524bb626\",\n        0\n    ],\n    [\n        \"0000000000001bc7822a17596f813039ba79ddd9e114f81a3dcdc596533cb493\",\n        0\n    ],\n    [\n        \"0000000006202b1fafe78be2c564ece74bea3a1ebc294497935854cbdcf2533c\",\n        0\n    ],\n    [\n        \"0000000000005d7567c3851bdbc75d1c6c9372db838ad0f1e6bd472b5a100825\",\n        0\n    ],\n    [\n        \"0000000002590ac2a684ff9da31826f8354865c081b243ee04abac8e799f74e4\",\n        0\n    ],\n    [\n        \"000000000256d70b8e6030ee6b56ff47872fb089a5d007adb5057e7c4930d0e3\",\n        0\n    ],\n    [\n        \"0000000000030d7ea2ccfee60b00002337ba48466d40bf541c27b01b5b40d0ce\",\n        0\n    ],\n    [\n        \"00000000000002ea99316525bd7497de5d9fb6ae98d6e69008e4036b9f1a96f8\",\n        0\n    ],\n    [\n        \"00000000000017633c6c08c6e76e58397497d88a74a77ca1428e7daf7260c9ab\",\n        0\n    ],\n    [\n        \"00000000000017b937aa4b9327600dd33804deeddcdf6abfd847455c3365fda1\",\n        0\n    ],\n    [\n        \"0000000000001bbbc95f689697902e961c1ae60b0ba48630cb275292128d457f\",\n        0\n    ],\n    [\n        \"000000000bccd05d924bd0583dbbb9f2b09cbbd04633e2905e44d9fa9347a150\",\n        0\n    ],\n    [\n        \"000000000d269ad70d5bef52c953079d690c8c20a3a9265c0dd4c37ab311f47d\",\n        0\n    ],\n    [\n        \"0000000007406099654ec641983c8ba1027fbf288eb23e7352302dc0670ed259\",\n        0\n    ],\n    [\n        \"0000000000003e6d99ffbf2efbfec8eac8a595214064640922bfa725967de2c5\",\n        0\n    ],\n    [\n        \"0000000000002107d0e8507d9168c684c45d4ee62dceaa757890d8e34320ef99\",\n        0\n    ],\n    [\n        \"00000000003493e7933f834a0f38e56e46cfd0dafba38c4d972ddb5a2e491e10\",\n        0\n    ],\n    [\n        \"0000000000001be81450f65ba6bf3274e4681762d78f22827e7ebf30e6aab8b1\",\n        0\n    ],\n    [\n        \"00000000000024530945d47086a0f0f15e2063d70fe5435f40796b9d754406bb\",\n        0\n    ],\n    [\n        \"000000000000ab099a527c022454155a19f1c6ca6065201817400bfeed39cc4d\",\n        0\n    ],\n    [\n        \"000000000000081b516b4f53c442ead11bb3e64e1d3f6fb628d3d6428f88371d\",\n        0\n    ],\n    [\n        \"0000000000000d2d06a76e91d8f3e6a3634de9f1e53c4ecfdc94122b696bd4c6\",\n        0\n    ],\n    [\n        \"00000000000001e9af5fb09645618a1ea84b537f0532511d921e0bd03e79a34a\",\n        0\n    ],\n    [\n        \"00000000aa98879ff0193e4d5b5fccda8a680513bcf062330aec102cfddebbed\",\n        0\n    ],\n    [\n        \"0000000000002bb5eaa52edd3ae8571422d99a23a71feb313866f1c177c5fa52\",\n        0\n    ],\n    [\n        \"00000000037d95e70f0026b2747c3302f84668ee3e112cd335efe0b71de93742\",\n        0\n    ],\n    [\n        \"0000000000fcc1b88fd91cf4cf853f6a2218f750d1f5fd4b7c3d0c4309c2d48b\",\n        0\n    ],\n    [\n        \"000000000005bbb90407d1cd8864a4e0acdca910cefcecc42d90b2b80579a233\",\n        0\n    ],\n    [\n        \"0000000000002f7c9303dfb193c69d0807f9bad18bd1ee5b705680afc2ec4a23\",\n        0\n    ],\n    [\n        \"0000000000046dba12423dc70bff9d5c8257c5acb4bd0118d459defc904c84e3\",\n        0\n    ],\n    [\n        \"000000000002b9f9c12c825135a280583c0b838b18e45da81aa44c3adc60fe49\",\n        0\n    ],\n    [\n        \"0000000000001681ca0ed67c0cb2c566c238ea955a4699e281de06d36285a51d\",\n        0\n    ],\n    [\n        \"0000000000003a24f300fdc9cf9bbbd23a151ee9e2a79afcb6e9414663addad9\",\n        0\n    ],\n    [\n        \"0000000000000d1d80f15f3afe5c108688f0fd8266f7fe06dbb6234ee4c9d66e\",\n        0\n    ],\n    [\n        \"000000008c6a6332a8c9351fd6e6af767a849eaa31838bdefefc36e8905ee111\",\n        0\n    ],\n    [\n        \"000000000d2ec08ea27ca63313014891f00250ab2d0e9e8a1368541766a3637e\",\n        0\n    ],\n    [\n        \"0000000002fe57f4c0d782dae857905dd16c6ca3eab6cb68f14ac96f9c5e903b\",\n        0\n    ],\n    [\n        \"0000000000934f789f1d061a0f385037de57c46644929ceebf8ae7a7e8a797d5\",\n        0\n    ],\n    [\n        \"000000000039aee8e147cd4bbe399ecb938d22e2b0d403d4b81ad3a36d2ebf0b\",\n        0\n    ],\n    [\n        \"00000000000626939a469fdcf2109460bed2634dfc5a799b267c3053761cd78b\",\n        0\n    ],\n    [\n        \"0000000000091af9d3c8ea0d3a35a15376126f4a3cda6905c7feab16db260ede\",\n        0\n    ],\n    [\n        \"0000000000008fe017eccdd0f1e295efd15813211320952b9649df11cdd099b7\",\n        0\n    ],\n    [\n        \"0000000000002edf8ddf0646a8e66768a7067d866d6feb3c7832cce16087d4a5\",\n        0\n    ],\n    [\n        \"0000000000001f290b0b8f50d90e11aafcddff259207a8234e2764be0a6102d3\",\n        0\n    ],\n    [\n        \"0000000000000039501569acd824c3e514ecc1046076c62756c850696a27aea1\",\n        0\n    ],\n    [\n        \"00000000000002d87f5b96dbaa8ad6c601c886c1a5a3cab8ea2c8732201aaaf6\",\n        0\n    ],\n    [\n        \"0000000050d5bb27724bd6a99a31160f8554f2e4868631c719ae0a2ec0b73aac\",\n        0\n    ],\n    [\n        \"000000000feaf2eaf424a989ebdd2ba9f5ecac93ee5cd495cc9645f3af3d4702\",\n        0\n    ],\n    [\n        \"000000000f79be6b3023a12b57ad8132f0987442e2e7f40d2e04d3a2699f8699\",\n        0\n    ],\n    [\n        \"0000000002c329dfd4f21d40ecec041beba86df7a01c39d0775133278b88a980\",\n        0\n    ],\n    [\n        \"00000000000016229b9dafbf5fabb6dca805de40ac63709bba75352e6f662352\",\n        0\n    ],\n    [\n        \"0000000000b9ea4cfabe7ac942aa7bdd91689b03fbce3035cda3371e57b3c140\",\n        0\n    ],\n    [\n        \"000000000b510434311994128aa067b94020fee893e67dc72998eceeb4650d1c\",\n        0\n    ],\n    [\n        \"0000000007b3d6eca2c9d0a8d05c884412cfede2aede572076c7263a38d6306f\",\n        0\n    ],\n    [\n        \"00000000006dd9fff347f4c9490b29544fec60a3e0504883d13ec43cea0e5260\",\n        0\n    ],\n    [\n        \"000000000000e2719af18f948f63b60d06fc7f35bd7cae88ef43d2c9edd611d4\",\n        0\n    ],\n    [\n        \"0000000000004ff5733da92e83ad0970b7eefa8719ebe7346f65c8da62be79e8\",\n        0\n    ],\n    [\n        \"0000000000000247d49cdb1e1ce982cfafcd8ceedb9bb1090d2cf8dd52fd7362\",\n        0\n    ],\n    [\n        \"0000000007aab1ae7198606046c23cb4616edafd19c0410660dba6ef065dc522\",\n        0\n    ],\n    [\n        \"000000000d6a048170bb18c0421b153bf709cc85e090602c2a5b0a626dbcc3f4\",\n        0\n    ],\n    [\n        \"00000000069a824155cbf3f02f60363bf5b41ab7e8a83fe647fc4169b34f2808\",\n        0\n    ],\n    [\n        \"00000000009d32bba8dd11262ada78187ff56417fa1df97b3444196c987683f9\",\n        0\n    ],\n    [\n        \"0000000002468dbde6d879e2027447918f4bfeba25ff32a4f19edd953d42aba8\",\n        0\n    ],\n    [\n        \"0000000000e92e9a687fc2fd40b221a02f76ad72228503660b5b6702db9757a2\",\n        0\n    ],\n    [\n        \"0000000002549c36360bcdf2c495f9093f117ac4a28e205d954ec803dfe7aa57\",\n        0\n    ],\n    [\n        \"0000000000e1deed95098ca573091dbb6b696787ba265277bc2ff65ae73cfde0\",\n        0\n    ],\n    [\n        \"000000000bfbb8b7a1941598009350c5d741e05c6575323591a04d57c206e205\",\n        0\n    ],\n    [\n        \"000000000dff311ba823e48522c286a4962bf9e9bd7a0dde66aa5e5d3d9796d9\",\n        0\n    ],\n    [\n        \"00000000033b0dc90fb39c5c46e144dd08a52572967a712988a7b543195185c9\",\n        0\n    ],\n    [\n        \"0000000000003830feb05937c05d8250b9a465c6a5d29dae49b127dd9abc99e4\",\n        0\n    ],\n    [\n        \"0000000000014d788b90d6a52d49e8e74a054e26429c25ef8a4f3cddf9463e6a\",\n        0\n    ],\n    [\n        \"000000000000007cf539b80a7e7f7d38a511f7c3fc94161cfb3b960d70c54359\",\n        0\n    ],\n    [\n        \"0000000000003c7c0f340bbd67e0dc9dd89a340829de0c4a3f1d53db38499a79\",\n        0\n    ],\n    [\n        \"00000000000017e131be27185a58f3f9f0e239b1fd15571f500bc01aabf3640f\",\n        0\n    ],\n    [\n        \"0000000000002f688fa71606e8feea09acf4d97493f71adab27bf782c1c11658\",\n        0\n    ],\n    [\n        \"0000000000000b38da817e3cd6a112626414b5a7af0cd8dede33f8facb87e58c\",\n        0\n    ],\n    [\n        \"000000000000020f5ce46cceddf4b42457687c20c2fba80ec2c797354ae32deb\",\n        0\n    ],\n    [\n        \"00000000000000b1c5d067792aae5ae74e4cf30b0f28caa064b5d736b2b9ea08\",\n        0\n    ],\n    [\n        \"000000000000000bd96df6cfb597259a34b8abed672dc344c6de48c4c26e4b6d\",\n        0\n    ],\n    [\n        \"00000000000000061b79bafcf1a446752df3217b60b83bdd768a3030f04b21bf\",\n        0\n    ],\n    [\n        \"0000000008f3bcc3504c464581b257b1236e0bfed8b0472b58281a161fd1287d\",\n        0\n    ],\n    [\n        \"00000000000019b757e0f7768982ecb439e31b022efd60bfb09509e2c920d063\",\n        0\n    ],\n    [\n        \"0000000000001e5a69bda72ad2c5f4bea6018f11ec4e3bfb6f974e08c81814b9\",\n        0\n    ],\n    [\n        \"00000000000002bdd9c3c6c9359582934a594ca82d6c9104f15277ed10349bb4\",\n        0\n    ],\n    [\n        \"00000000000015b7c29f74c5dad9d6121d0d4d3af9f44333b2008fe5bcbe2eed\",\n        0\n    ],\n    [\n        \"0000000000003638e630bf33aedb0b0152837f695224c199ef96c248a6b22f16\",\n        0\n    ],\n    [\n        \"00000000000011be4e7ceacd148ea83abee5b61063cf679c879d0e13d0a40655\",\n        0\n    ],\n    [\n        \"0000000000002542493ba1cf5396d5209244f266a2c5764d4bdd843c5986d82a\",\n        0\n    ],\n    [\n        \"00000000000039197e58f1ef6c98eee126a41ef156036ea51114b15708401701\",\n        0\n    ],\n    [\n        \"00000000000004b893eeb6b43cf0723b5f0fc2c6e245e6671f543421ef8c5f69\",\n        0\n    ],\n    [\n        \"00000000000024bc586bca1c86f4254b3d74666f22ba2f5742f588d7d50ca6bd\",\n        0\n    ],\n    [\n        \"000000000dd0843d947ba235aaf2cb1b5e2320f1b15d1f79d32ccb01040735f0\",\n        0\n    ],\n    [\n        \"0000000000002e9524d79e1972ecece3c432eae5e8c2aba1e5d61584c8b3c780\",\n        0\n    ],\n    [\n        \"0000000000003c02f0d0743d391caac03c631ebb5518a7e4261487482586587f\",\n        0\n    ],\n    [\n        \"00000000000015a72bd8520f0d8fb9d7d658d0e62cae6830bf94a2efdfd85e65\",\n        0\n    ],\n    [\n        \"0000000002f4dcab6bbc94ae9a1b0761ab7b52cd56272b6183715fa1a959101e\",\n        0\n    ],\n    [\n        \"000000000000005ca13d4fc8bebb498e46dd764ec5f727ffff7ac9c90da8ed28\",\n        0\n    ],\n    [\n        \"0000000000003ba5816aaac8c6efd085bc08676ff2b51e7c3d94263b4a555c17\",\n        0\n    ],\n    [\n        \"0000000000000881711d6afcd9ad84823ef6cce98b1cd8a90422867f23644a37\",\n        0\n    ],\n    [\n        \"0000000000003d627ca2ad44d333543e86cc377e63ca507ddea5aca7ab4a0829\",\n        0\n    ],\n    [\n        \"0000000006ae50b5cb9404746f3702b2400a3743a05893cbac1b372aeb122834\",\n        0\n    ],\n    [\n        \"000000000000197307176984fc5e3b612c9b6a89510e5a3604711f90739846d7\",\n        0\n    ],\n    [\n        \"0000000000001709130852704239c0328a00d406ef2914dabab52a137bf59aaa\",\n        0\n    ],\n    [\n        \"0000000000002d8039783ae0208445429241d2f5b4485192e1c50f6921579daf\",\n        0\n    ],\n    [\n        \"00000000000028ab4cf7ef62580408801e9c4a3b955f6f1dcfbf339f08f335ac\",\n        0\n    ],\n    [\n        \"0000000000001730f6260b57593f30c6a24ad49f5afde41daf8dad5bb83cd737\",\n        0\n    ],\n    [\n        \"00000000000026800054645b160790f1d226f87a2fd809acbe06d018ffc13fae\",\n        0\n    ],\n    [\n        \"000000000112993137d21171ba742be3beef4af19eba815f326fd6ecbf54b547\",\n        0\n    ],\n    [\n        \"000000000000236d85b62dda3bfe9850e4e523e3876684461b7e1107153556a2\",\n        0\n    ],\n    [\n        \"0000000000003fe1c0f4a23adfb74f07bbd4cfbce44db35119332a5c43500bf8\",\n        0\n    ],\n    [\n        \"000000000000381ff5b96685ca16305b427c4a1dcba2d410e6ee0598619f75f9\",\n        0\n    ],\n    [\n        \"0000000000001d8eea8071cc6e93b6dd058924a6fe171438974c304374e1f1cf\",\n        0\n    ],\n    [\n        \"0000000004da6faf93f8594e82f9d282d95583e72f79365083c00fc2f4fe249a\",\n        0\n    ],\n    [\n        \"000000000000254e1d80fc91204b555a4269d18d892c60a845c907175fbc018d\",\n        0\n    ],\n    [\n        \"0000000000001a945f9ea3b8d08ea7befd5074bacd9732a05d1dda579b197538\",\n        0\n    ],\n    [\n        \"00000000000028ca1712b95cf77145ff45e9049c5c39458d18223b3e17812d60\",\n        0\n    ],\n    [\n        \"000000000b06ec0c9b1443bfddc2b3376e71789298ebe3575be1b0f5a34a37f2\",\n        0\n    ],\n    [\n        \"0000000000006562376c1164d7b6f5281d64fa7dd0112bd34e4ec9c7537698f6\",\n        0\n    ],\n    [\n        \"00000000000027ab842f086d6c6691aca8f51967c1c88281a4e49ad89878a806\",\n        0\n    ],\n    [\n        \"000000000000ed2292a4f779dcc4ea3315b855243a3b70804da3168377f408ca\",\n        0\n    ],\n    [\n        \"000000000000c287b24e25768d8524cf3d8b892da291f868446cff373f878777\",\n        0\n    ],\n    [\n        \"000000000bf32a8711ff9536b7ccece30a2d6ac40538123206862e01a8ef2350\",\n        0\n    ],\n    [\n        \"0000000037bfd22d6171e4f1c7c7cb1cd73b91a19a3cb473bd9f33335150e001\",\n        0\n    ],\n    [\n        \"000000000000045f2524a3a3faee2a2d36cdfc9531f429670106622d0c1bb558\",\n        0\n    ],\n    [\n        \"0000000000007e262915f1b1ac2bbfb97ef84bfece7bb8ae3340fe4eafe7017c\",\n        0\n    ],\n    [\n        \"000000000000727ee42b831c566ed9c2ad92e9b14db0c8058b9d6d6066752e71\",\n        0\n    ],\n    [\n        \"00000000000002227876ff70290cd31e6b08ecdbdc2dd34ff0c69412ad863f4a\",\n        0\n    ],\n    [\n        \"00000000000013b9729fe455d8409547ba3dd30d62eb1661ce6a38bfcd358d02\",\n        0\n    ],\n    [\n        \"000000000e61c29a6e384bb7f4685608774b790cb7dcaea51fb99f8a10e2e03a\",\n        0\n    ],\n    [\n        \"00000000075f63db5180b1afce047b919bd4c06cd81d6dbe5224189cb6fb3e51\",\n        0\n    ],\n    [\n        \"0000000000a83265ca1a81e9d610bc6de821e07739c89a137b93812b545f5488\",\n        0\n    ],\n    [\n        \"000000000003ba66b509ba5ab7cdab0c705c8b4145e1aade97b86e0f8a1ec6c1\",\n        0\n    ],\n    [\n        \"000000000002b0ad2b5fd6f15946ba21f819f074af0b96cbcd9a58f5be2ff6d0\",\n        0\n    ],\n    [\n        \"000000000000103e6ffc413e63a85bcbba243a9c2acf45b433d6dc36acb6ae1f\",\n        0\n    ],\n    [\n        \"0000000000002226a204b75f5086a8ef6576e8e79af7e904bc9a75287d518b81\",\n        0\n    ],\n    [\n        \"000000000000738f4fc499a14bcd7bca5c16273f59d7695decaa9b177cea0873\",\n        0\n    ],\n    [\n        \"0000000000ec7ebd6c07b47fdcdb2d924e0659a22d2b2bbc0f5baa15cf940825\",\n        0\n    ],\n    [\n        \"000000000002c9d4f581fb0fe2f40a9a24db42de1a85064b4a304a25338a62d2\",\n        0\n    ],\n    [\n        \"0000000000003ede1becaaff56312310787669e07c38decfb43fc6df9f7950d9\",\n        0\n    ],\n    [\n        \"0000000000f76fd9b371fb6231cbf696f0e02967dcbbbbc32558b08a6bdf1c0d\",\n        0\n    ],\n    [\n        \"0000000000009c6e722f7908de61abcf6edb2e530e13c97b4b226a2e1dd9cb27\",\n        0\n    ],\n    [\n        \"00000000000008730a8476c058a573bc8100dff1da03b3d942a8164a107a00b0\",\n        0\n    ],\n    [\n        \"000000000a2ff7a14ba1fddfe3239d3202f310490841a5d1294d53df83a471af\",\n        0\n    ],\n    [\n        \"0000000000a5584c9d4f74046f9fe4e3c9080a0bf3025330065b9607c2142a2b\",\n        0\n    ],\n    [\n        \"000000000000d55a8cc2e86b72a1a0502a25962b257e0cac2203b79270dc54ae\",\n        0\n    ],\n    [\n        \"0000000000a2f8ff2e196c0def66a352622984d03216ef0610b17813ca4accfd\",\n        0\n    ],\n    [\n        \"00000000077267090ddd0a5287bff41a19e4aff8772befcf9f6147d5dddc96c2\",\n        0\n    ],\n    [\n        \"00000000001c56edcba9d4ae7b7372bcc7f8ed6db3c3f7fcbbed555028cccf27\",\n        0\n    ],\n    [\n        \"0000000000009c304f5a5cc26c3bce7378205db954646d85049dcf77ef98eead\",\n        0\n    ],\n    [\n        \"000000000000987179da28a7bbe0cda559b26e25249d826406f002c65a5ffcf1\",\n        0\n    ],\n    [\n        \"000000000c08930caf906884d095bf68c98781a191bdff9b8677fe25660173e8\",\n        0\n    ],\n    [\n        \"00000000000070168a5836c8e1676f6b9fa8e316fce6c38b91d6ce7226e7e897\",\n        0\n    ],\n    [\n        \"0000000000001d7cdc82f066c2adf9b6b8658d531a6dce864bab36660c7e7518\",\n        0\n    ],\n    [\n        \"0000000000000213c8fac899b3ceae3b36a66a63e94e570b3642c8bc4459fea9\",\n        0\n    ],\n    [\n        \"000000000000803753b883992776bcd9b0f4c3553ad19e524e82362180a777d5\",\n        0\n    ],\n    [\n        \"0000000001286d5f36e3d987a6d6ee4c2990799738733331b53e1606e5db55b9\",\n        0\n    ],\n    [\n        \"000000000000d656dbf9b01aa7cd45bbc7ca206e5fdee253c1138c8d01dde543\",\n        0\n    ],\n    [\n        \"0000000000004ed5d68da0abdc500801ecbcb13c2a0d0cd22c664c48cf1fea0e\",\n        0\n    ],\n    [\n        \"0000000000aa80295620b2da59ab5af04caf0bc3a91660a61458b08d9a0ee5b8\",\n        0\n    ],\n    [\n        \"000000000000f9389fa442251d5fa38a34a628b26d3dc573da88427c0e330067\",\n        0\n    ],\n    [\n        \"00000000000072ab2052ab08cad2d2e73a46c385a828682335828607d9237d09\",\n        0\n    ],\n    [\n        \"000000000000f6fd78689cd91839bad209db4d373546fd657801b00f3186fce5\",\n        0\n    ],\n    [\n        \"000000000ef3589b0434e77221be33e79b0751e3063b0d0ee03bd70ae43adee5\",\n        0\n    ],\n    [\n        \"0000000000009c9c3db06deb8bc77fab2765817b2293aab7393e527a5ef33e5e\",\n        0\n    ],\n    [\n        \"000000000c17c570e13201f910b29ec68a66a21f4a2ed7866987905012c1330e\",\n        0\n    ],\n    [\n        \"000000000000095a4b6c94eaf359cbc8301ae2568927eb9b151266a0286748a7\",\n        0\n    ],\n    [\n        \"0000000000004cb69211ccf74e6d84ccb6dd421c8dcf16795e8d3b476ff7473e\",\n        0\n    ],\n    [\n        \"00000000000006db56d24caed9018565840fce8363a7cd9e912504114d0b4c3b\",\n        0\n    ],\n    [\n        \"0000000000004a15f1dc5253af6641c141a8f4566ac14fac2842ca6ba53bddb6\",\n        0\n    ],\n    [\n        \"0000000000002aa51c0812c9a1438b0c3bd8e95330a8c4fac62439d2797d168a\",\n        0\n    ],\n    [\n        \"0000000000002af6ab6d23bf33123e2393c1c9df7e7c2e4f722aa130b0f0db97\",\n        0\n    ],\n    [\n        \"000000000dc6e875c0a047d3d05dd885c93ca1de0d9cd1a08ee6523e321ca9eb\",\n        0\n    ],\n    [\n        \"0000000000006a0b4e485b2ad027de8edecfa97a822a87e9623b21be8353cf91\",\n        0\n    ],\n    [\n        \"0000000000006ccdf09196936d0cf74da3a3c433487cd13e3386bbf0e84e64a6\",\n        0\n    ],\n    [\n        \"000000000000134acb738b7e99f9991d63519e28da5a1c50c60a31b596f39034\",\n        0\n    ],\n    [\n        \"000000000000bb7e53229e6557a53be46d98a9dbdc4c827b2b094492d775d1a6\",\n        0\n    ],\n    [\n        \"000000000000e8f54f39df60e09d81653a5f80707d084edcd6e8bfdb7427c588\",\n        0\n    ],\n    [\n        \"00000000034b27b5cea0fe55f4dac79fc32e25b4a09ce643f99bbad2e30e9335\",\n        0\n    ],\n    [\n        \"00000000000042edd05861e88b20e03ec95a7a5ba8f60e595ac044d77ac5198a\",\n        0\n    ],\n    [\n        \"00000000000021bb194f73f614ab1f4216227f09789edaa2677da691ef5d73bc\",\n        0\n    ],\n    [\n        \"0000000000009b2dea9b3b6c8025c7e632fd7918316400a79170d7114fde31cc\",\n        0\n    ],\n    [\n        \"000000000000b6e17bb1f2574590815ad6d20456e9d7f219221c58e3aa3767fb\",\n        0\n    ],\n    [\n        \"0000000000009c762fd6211c015e5c6a42723136e950cb2333d30504ca2b0173\",\n        0\n    ],\n    [\n        \"000000000000733c889bfdba15a7e9880890a8ae6814d4310ee6896e8e7c1dec\",\n        0\n    ],\n    [\n        \"00000000000022d2fcb2ebb92f8cea3f174969805f69de7518ebd8bf26140255\",\n        0\n    ],\n    [\n        \"000000000982ba146e85fa32f2c3ecc0321d865a7960a6cda8928983ea679b0c\",\n        0\n    ],\n    [\n        \"000000000000e004dc4ba124a02643e3a2cc4f36665c75d97c2590be07d1e304\",\n        0\n    ],\n    [\n        \"000000000000a7d0dd7994be5e5acd32495145008362e072eb02b297e1431207\",\n        0\n    ],\n    [\n        \"00000000000011af0549ad5774dcb338f3c8e6757ff7ba2bf4da4f6932651862\",\n        0\n    ],\n    [\n        \"00000000000016a682c3e571c0fa6f982f5193f4e416f474c8a7ddc79d88dda6\",\n        0\n    ],\n    [\n        \"0000000000009d072bc459efeef121a836c353d88a8dea2e73ff17c2f2933c67\",\n        0\n    ],\n    [\n        \"00000000000091d01b9faa03a2f5076d194b5f889425e2a85b2064aa4b4a8258\",\n        0\n    ],\n    [\n        \"0000000000004b035f4693d1d32e518c7977a8ea2e77b7b6f4f51b10763ff505\",\n        0\n    ],\n    [\n        \"000000000000c330f00b190b1ee5e60aa8cc3871472a02d98bade6dada6351ed\",\n        0\n    ],\n    [\n        \"0000000000003f970f8df4843d6a66b74793b5b7ff3bfe0dc128f6eb00b242ed\",\n        0\n    ],\n    [\n        \"0000000000000a649d33eb372d40599bdc493d6baa50cfdd667073897c09487a\",\n        0\n    ],\n    [\n        \"000000000000026b4a6c9b6241aaa2b3f5927e6fff5fa0f2c2fdfb4ef39f00fd\",\n        0\n    ],\n    [\n        \"00000000000000789d9b77f871481ec9cd1b8b877fe885c49eec83e97df988d3\",\n        0\n    ],\n    [\n        \"0000000000000028abd92b26a2328ff5bf15d3e9104d2491d23d5f789bed2c1f\",\n        0\n    ],\n    [\n        \"00000000078b55bf69f546804a25e86ab88ab12b3aa0d614952c23e82f7b2c99\",\n        0\n    ],\n    [\n        \"00000000000030ca4bda5ab3c07f5476eacb61ed54cf7080b3ce112efbb0703a\",\n        0\n    ],\n    [\n        \"000000000000175f0ffd64634d958ebc5ea9659e5b9c0e91c2b31cbc9db3a8ab\",\n        0\n    ],\n    [\n        \"000000000000355f3794e3a81787699b8fd649eded08f829d1e6e47e5f0b9daf\",\n        0\n    ],\n    [\n        \"0000000000002e5396ffff0cb008c6a5c773ac7b56456f3574442ff22da9c8c8\",\n        0\n    ],\n    [\n        \"00000000000013f7a1e25a9fd016d1ec5abeba0dc69d1af2b656e86f19fc73bc\",\n        0\n    ],\n    [\n        \"00000000000018cf2326052e78cf74dd3671c63bf64b58c5dc4b1ac964b712fd\",\n        0\n    ],\n    [\n        \"0000000000000f321300ff44eb5bc2806eb15ac3e73ecd8dadc08e00921d35a1\",\n        0\n    ],\n    [\n        \"0000000000000b3af62155af09d7d3612eed2522fbc7f9eb1751c67c56e20903\",\n        0\n    ],\n    [\n        \"0000000000002cbf583243f17ff8b6ad7e4477d3e58e41cb6589882fbed15406\",\n        0\n    ],\n    [\n        \"00000000000001b336201e7d8876fde8def3923ceb44bad051cde464431d4937\",\n        0\n    ],\n    [\n        \"000000000000000cf89bb1e492b07c8e771b880995d293bbb5a2b6312d9a1584\",\n        0\n    ],\n    [\n        \"0000000006b21f9e08db1b242dec441df0a12a8e5b93d5fb39fe6069cebd197b\",\n        0\n    ],\n    [\n        \"0000000000002d81bfd983a54e8fb65d48868e361c5524fed9646bd999dbe894\",\n        0\n    ],\n    [\n        \"00000000011b6d38162fa8128f2a69ce0ab991bbbc619482312665414d32ff2c\",\n        0\n    ],\n    [\n        \"0000000000003ae808923e1323e4790f207895a1e04353a6dc5c74502401ca17\",\n        0\n    ],\n    [\n        \"000000000000253ef210a4ee95c1950a3095825114dd67ea0fb7a7bf0f75006c\",\n        0\n    ],\n    [\n        \"0000000000001c6a7b8d2fa299c8ed9433e00defee0f97b5d93427afc753edeb\",\n        0\n    ],\n    [\n        \"0000000000002c54c1b035f0d6768d24899d2c79933aba096074302075389ec9\",\n        0\n    ],\n    [\n        \"000000000000373ab0c2c8fddb53cc8c1890448d11deb2abbb3e36c986d2cb17\",\n        0\n    ],\n    [\n        \"00000000000031898465c27a7055012064a6184c41f8cbc89700419b802744d2\",\n        0\n    ],\n    [\n        \"0000000000003aa31991360c4b63ed0760d83ea3eb3279789b406498db586bea\",\n        0\n    ],\n    [\n        \"0000000000000e5f6c671e63b383a90347b7d95313fe06570e5641442d30025a\",\n        0\n    ],\n    [\n        \"00000000000000eb1edebd17248bc5dbdb02eb7010fd41eb425a6c78975dadea\",\n        0\n    ],\n    [\n        \"00000000000000c2d84b76a602b417861cfe4580194ea4076405872b1e954808\",\n        0\n    ],\n    [\n        \"0000000000000013d788f8faccb96b3d702a1a2a94f547be4552946cb04d6b02\",\n        0\n    ],\n    [\n        \"0000000000000007ddb93d3d5d2cf8521ad8d8bfeaa031138e4271669a0000b6\",\n        0\n    ],\n    [\n        \"000000000a43bfdeb9b9f513518ed0b4a523075c3ead0b049913821466f40624\",\n        0\n    ],\n    [\n        \"0000000000001b9b9cff2195dc6868da50a4ae6eb4f3661f31feeeaf2a5fed7a\",\n        0\n    ],\n    [\n        \"000000000000346a1dfdcfc23f8bf474030c42683288ef5e6419eac5e55720f4\",\n        0\n    ],\n    [\n        \"0000000000001274395c0a6d1b509d8b242ef570c99167f388848999c9d00b87\",\n        0\n    ],\n    [\n        \"0000000000000684d1b654d0c2a3c9fc1919d2f20948106d48d2365bf1d52d11\",\n        0\n    ],\n    [\n        \"0000000000003d31b55f30b2d91979b6c4aa249ab5e782bfd61c6cd99a199962\",\n        0\n    ],\n    [\n        \"0000000000003b4e4f3fe1eb3f17922cdff9538b862fdd4be332b38825132504\",\n        0\n    ],\n    [\n        \"00000000000002b62a4334c696eab73339ab3cc497716b163beaa5c568be70d3\",\n        0\n    ],\n    [\n        \"0000000000003b0a1b1ddad166962bd5d29335590c53ef5d6079fabc90e238d3\",\n        0\n    ],\n    [\n        \"000000000000289da5bf12022ca27bf0b8b1f097a9f1e612302a64714f737c35\",\n        0\n    ],\n    [\n        \"00000000000001b05c01730ed74a9dfd86fdfcaa28f97f9128867b5d6dddaa87\",\n        0\n    ],\n    [\n        \"000000000000016a9af16f51a69f5ca833eb52c97cb062c4255f97296850f8a8\",\n        0\n    ],\n    [\n        \"00000000000000576c2c704125e6704e9e1b1b0b5269f0ad7e14291f8bb875ba\",\n        0\n    ],\n    [\n        \"0000000000000036675b4282e91cbcf341e0b5c68085b5c002f7c623afb01506\",\n        0\n    ],\n    [\n        \"000000000000000029531ba2b1f27a4a1ac57a946ef322848417ec83d7cebf96\",\n        0\n    ],\n    [\n        \"0000000000000000764a389696612dc41f5ac2c5ecebb887e296c226303754d0\",\n        0\n    ],\n    [\n        \"000000002c3a2e26f7891923807fe41d14ff232de4a9496bf99afbc53af94006\",\n        0\n    ],\n    [\n        \"000000000a27812c8ea52565338cd4881b783d18f0d4bd9a7ba8f4ea4b8d1e6e\",\n        0\n    ],\n    [\n        \"00000000000044c58e6a1e82f51dc23b11ba4b6c4eed7ad317654198e9a51022\",\n        0\n    ],\n    [\n        \"0000000008c67913ebb49ff07a2776c081113fb20adb56b1f8c8fcd1094721f2\",\n        0\n    ],\n    [\n        \"0000000001cdc1f92512c05d201fb32ea48b23e1d2568899ab06f14ffa329848\",\n        0\n    ],\n    [\n        \"0000000000003eef51183eb12076427c8c223ab73760373c163bb4da036f7cf2\",\n        0\n    ],\n    [\n        \"000000000000743bc379610841698c4d333e5cc721b8ad95d027b4231a365b1f\",\n        0\n    ],\n    [\n        \"0000000000005e9274e286a066e145689530db6183a175f8e6721c44171cdd16\",\n        0\n    ],\n    [\n        \"00000000000021a56fbf607f3805c158393c0d8b3683633afb0d0ac7d64d0496\",\n        0\n    ],\n    [\n        \"0000000000001e223329a43536dfaabc1467f2ad7d44e55478f62fd80d9e48b6\",\n        0\n    ],\n    [\n        \"000000000004c61719fe71b56a2d8706b5119e2df77da7eb9330aeda1825fa4b\",\n        0\n    ],\n    [\n        \"0000000000001e89565a9c898bd51bede84e2f231aa29a38f3b47b987a2ab0e1\",\n        0\n    ],\n    [\n        \"000000000000712743a8df968020c48e3955170166e90ee9a2e02cb460349ddd\",\n        0\n    ],\n    [\n        \"00000000000019192acf1ff42fd9fd8d9aac23e51837f4698a8f02596f6cb7e9\",\n        0\n    ],\n    [\n        \"0000000000000f4f49cebf0d2f8731f5136903bbe9c8bde1b1ca94048119fd3a\",\n        0\n    ],\n    [\n        \"0000000000000141958bef54ea397bde360d8ac3eb117c1dd785f8006856ac92\",\n        0\n    ],\n    [\n        \"00000000069650f051d27f2cb54045c19304eb99425c042e010ee982daaa9002\",\n        0\n    ],\n    [\n        \"00000000000087523d9819f2661f47140dda70be7ae7af0b2e75324dc3d6fdbb\",\n        0\n    ],\n    [\n        \"000000000000384f0be3649578b7ff6b578e5976e18f79829e5a40c975f54ca6\",\n        0\n    ],\n    [\n        \"0000000000001a9f45b1b809fe1000a7177d284475c0db05c0bb18645e1f04a8\",\n        0\n    ],\n    [\n        \"00000000000374404f82c755ad76a64a96ff4ff449c365ed7769ed50437bb09e\",\n        0\n    ],\n    [\n        \"00000000002b4291a859b5489268c232ca7f86f76944690b668d82b94538e0f8\",\n        0\n    ],\n    [\n        \"00000000000aa4227550ccf273d27412aba102a2a90520140b715c95fb4822c9\",\n        0\n    ],\n    [\n        \"000000000001cad06fc2b9a27871b4e6b8c643d6fb3dc559073c2e2aa51ea4cb\",\n        0\n    ],\n    [\n        \"000000000000ac7c3ceff73b328f172c1e1906f0d706885cfaa4f60b6bad42c0\",\n        0\n    ],\n    [\n        \"0000000000002c0c170a510d880aae66dc7beb2ff0f4d5db3effd802e0a2e89d\",\n        0\n    ],\n    [\n        \"0000000000000db55eafce7adecec679b6793d81a60f33c19a7e9055ed47ca41\",\n        0\n    ],\n    [\n        \"00000000000003201aa00eb05c7b2bef9b4972ebc640a04be522fb749a12afc6\",\n        0\n    ],\n    [\n        \"000000000000004faab34097be7f67a51b3ff1e7b2527d750697691b2753a996\",\n        0\n    ],\n    [\n        \"000000000fc33ffa85423a62215dd3707a31dd5b13f2b9ba868060670d5c456b\",\n        0\n    ],\n    [\n        \"0000000000030b22b40f979c6104cdb59113cf76a15fdd676317c721c1c9a4ba\",\n        0\n    ],\n    [\n        \"00000000000096a0202c2467783d606246faae84a897c96e5c05c42602d2bd72\",\n        0\n    ],\n    [\n        \"0000000000013b2316c180c52c60e4f38866430e94e46d2c7f0b63a6cad57616\",\n        0\n    ],\n    [\n        \"000000000002bd04bc3dbd8a3a91e1d658ee83acf1a7a66f1c0a5cccc70e88d5\",\n        0\n    ],\n    [\n        \"00000000000026d791eacacc6c13689a83e057c77cf2986a3da3a43e1c6924ba\",\n        0\n    ],\n    [\n        \"00000000000a03a783e832ace54ae9a71ceb9888102e1f91332005c5ef5dd139\",\n        0\n    ],\n    [\n        \"000000000003d078e9ac9a2a9cc3b31190e1ce78bcc449a6b471ccb4cb6fceb7\",\n        0\n    ],\n    [\n        \"0000000000007d94fd9860466fc526d3f9b327ad9653ac7297bfb9991a2fdca1\",\n        0\n    ],\n    [\n        \"0000000000000f7bfcc5d12df9a0989837a98d371f495627660321ad857912e5\",\n        0\n    ],\n    [\n        \"0000000000000303be9bf59b1161dbf359d6c65d99a38eb03619428d0d0c090f\",\n        0\n    ],\n    [\n        \"00000000000002dcad26269a9f62413ec0a71f0fb228872030f1617cf634fe3c\",\n        0\n    ],\n    [\n        \"00000000000000bc9f405c6a6088973fae16ee48061ab01e97c8e4193c88509c\",\n        0\n    ],\n    [\n        \"0000000000000040d4a88b68cc37ca890f3ab23958f567253ef2ce87eaa36bb0\",\n        0\n    ],\n    [\n        \"000000000000000f961ed7d57a81e0c232669ff9e8ccc4867632545016c78ae2\",\n        0\n    ],\n    [\n        \"0000000000000003c996d48410bf2660521bfa1151fd316d65600f72c4adf778\",\n        0\n    ],\n    [\n        \"00000000001c7e8824a780a745fd728e6106a49a50b19b45e0653fccdde06b90\",\n        0\n    ],\n    [\n        \"000000000000094011e26b89dfbd3355d001db8b07b29474a5ced7b5883ed2cf\",\n        0\n    ],\n    [\n        \"0000000000002ed4eb9f36f5bad0ac0f0c4c0e6a0b02d9e6267472f0ec96a06f\",\n        0\n    ],\n    [\n        \"0000000004fe1536748aef5d7286f1699f4cd0f8afc845aae9ff90435e81713e\",\n        0\n    ],\n    [\n        \"0000000006d789445291bccf72dd7f126f3e1d3d03319df61683079826b94f89\",\n        0\n    ],\n    [\n        \"000000000dfaf0283c12eee49338b0fd14181a50302074e51d928ce2280b8814\",\n        0\n    ],\n    [\n        \"000000000117b2c05c27806291a5596c1d65e784ca67e886701df617cae96bc1\",\n        0\n    ],\n    [\n        \"0000000000001db99ab74f6cae68d350a095d5967636070247363b6b2f9fe87d\",\n        0\n    ],\n    [\n        \"00000000000010047f90a66416b399f265a55d9dbeed30cd320ae2c1cfe4ace3\",\n        0\n    ],\n    [\n        \"00000000000021366ac9446eed3b71b3d437ac3fdc6647dafc52803fa8b1e920\",\n        0\n    ],\n    [\n        \"0000000000000dec6efe565871e1b9212c8b6463cc644c4e79e32ac2758bf4a9\",\n        0\n    ],\n    [\n        \"0000000000db56b3d3e67e1581d443fbf695834694b7af2c7d9f9e96f017659f\",\n        0\n    ],\n    [\n        \"000000000af313478dc82a2e3a71147d678b41a83149cd0c1b0016c796e666f6\",\n        0\n    ],\n    [\n        \"0000000005e838486868ee22a713d261e0bab20414bd5805fd914075906e27e9\",\n        0\n    ],\n    [\n        \"000000000000343b7029da5dcf93316a95e2b83a34647370529efb7a1f3fbe96\",\n        0\n    ],\n    [\n        \"0000000000002797189eb8edebadad2192491f56fc93ab08424e5e00161c3fca\",\n        0\n    ],\n    [\n        \"000000000000948cfcdc2645c13d25093500954bd8c49fd3534f695e31d0f383\",\n        0\n    ],\n    [\n        \"00000000000015db4d59d0de0af7880fb9f710f20909ec39c42eeb49e96eb5cc\",\n        0\n    ],\n    [\n        \"00000000000002ee9a047e4a3123d527298be5eb4a1b4280b98937f85ac908b7\",\n        0\n    ],\n    [\n        \"0000000000009260261421c980b8cf2dcddc6a18c49bd1bc804dfa6f0df359bf\",\n        0\n    ],\n    [\n        \"0000000000003b963afa52c2187dee9fb9b2cceb799994f7e71f4aefe9255e84\",\n        0\n    ],\n    [\n        \"0000000000000d9d821f0edd2940e911b16ffb7d21ef2f43b089c18f0152c1d4\",\n        0\n    ],\n    [\n        \"000000000000004117fdabe3fad201f486169db3445064231bfbd78a74fa7f3e\",\n        0\n    ],\n    [\n        \"000000000000007ec434b48b3414447b10656380c1ad15ca0caedeccf95c5cdb\",\n        0\n    ],\n    [\n        \"0000000003c5bf907909fc3656595f18ec47a05c4cfdf3b682d004a333a1d9ba\",\n        0\n    ],\n    [\n        \"0000000000ac007e356a1bedb70fe31ca56a970f746e3b65d6f54d002b4d439a\",\n        0\n    ],\n    [\n        \"00000000014b2c8f10765caec75f6fc14888d231b1acb22307f7a13c926e28db\",\n        0\n    ],\n    [\n        \"00000000079cea540986fd74da98751311b2735eb07a745b7c683eaa3f15b830\",\n        0\n    ],\n    [\n        \"000000000d27fe2305af6080e0d5eee7344b0489cb1c0da19e2d9c537f698126\",\n        0\n    ],\n    [\n        \"000000000a1ab2ea2304c980c0e84e2920317b554e638cb5da8ed1a6bc6f30e2\",\n        0\n    ],\n    [\n        \"00000000006e744c182c88b16e773daaa57c890c3bdfa3ba32590acbecdee7b2\",\n        0\n    ],\n    [\n        \"000000000379c716212dcfba61597cfc096cd8c73d38b1fd3e2ee5e10eec7776\",\n        0\n    ],\n    [\n        \"000000000000f0051043df801de031dac8d58b7309a586ad22f94467e8192532\",\n        0\n    ],\n    [\n        \"00000000002354f15017f05b70a8d4cf2ced4e9c699e4e729cee091579dcb332\",\n        0\n    ],\n    [\n        \"000000000004dd9488c9451e76542f6b5100d0b01998f9c50691916c62b32d76\",\n        0\n    ],\n    [\n        \"00000000000297f0a1b8437c1c361cb06c230a7fca5a16cc10fc5a9fd5444d3d\",\n        0\n    ],\n    [\n        \"0000000000003dd884b7190ae6d8535515bb332b4726e45fbe9309e12bd824f6\",\n        0\n    ],\n    [\n        \"00000000000004aa0500e9668e48ce227ec9d9ae2f68fac9edfea161c2f653e3\",\n        0\n    ],\n    [\n        \"0000000000a0cbcd8a3d3106851c1f0bb80890c8040b8204f9b89853f646b800\",\n        0\n    ],\n    [\n        \"0000000000399d0689f2c25d5b15321a28e3ff7a4ef283522b8cbafb9b2d4015\",\n        0\n    ],\n    [\n        \"00000000004b2624e3a7089c4e438e32193f42a7821455e9d3bc82d525002eaf\",\n        0\n    ],\n    [\n        \"000000000b219e11ff7d9ca4bab7bc32b30cf6c07fe8b757bba4f1ce7a4ba782\",\n        0\n    ],\n    [\n        \"000000000000672ec8538e66c5e7880d89d47166238043bd05cfb2b6975202d1\",\n        0\n    ],\n    [\n        \"0000000001b26f4ec0022ba8732d14e031adf2529f6ace8b2c4c6f44500bd102\",\n        0\n    ],\n    [\n        \"0000000000b667df345a9d121c978608de555b7bc028c89841dc77aaee768b88\",\n        0\n    ],\n    [\n        \"00000000008b87d9107d8d72e57191e3482d0cc563988c0a505c40545c960090\",\n        0\n    ],\n    [\n        \"00000000003842b24eca3193c245c6d9548ab6aa12f585375f24c571ac92a6ed\",\n        0\n    ],\n    [\n        \"000000000000b40b38ed6426cdf09de75db790198c862dfab1910cd3a180c823\",\n        0\n    ],\n    [\n        \"00000000000166d9973adc0bf4f130ac15f29b10a6c04cb8e25ce9e6bf07b148\",\n        0\n    ],\n    [\n        \"0000000000006bb52313e489ba4ced2480a5b39764c76eee634a1d997d1ea92d\",\n        0\n    ],\n    [\n        \"0000000000003e2b6d802c3854c819713661bd5437374ceea6193de52f69ee84\",\n        0\n    ],\n    [\n        \"000000000be0c0b056526b6b891040109d45adfb3f34b65c05169695f64b989c\",\n        0\n    ],\n    [\n        \"00000000095fea1fe858606e16d44802a2a2ea55a4cf8c20e4f489a8ef840866\",\n        0\n    ],\n    [\n        \"00000000012593712d8d9c7cc07edb52cc2926dcd3cd9d5776c256d9059facad\",\n        0\n    ],\n    [\n        \"000000000000c427ba2ce29f13fdafc925c19929c5512bc0e542f7d201de6c50\",\n        0\n    ],\n    [\n        \"0000000002bd5cabd8b2b9c2c1122e681f6e0d19aa8ac8733fa1e89a544a726a\",\n        0\n    ],\n    [\n        \"00000000007f2a119a05c0113ed25aeea6d6f42c29eabe17fd4fd0eed2218857\",\n        0\n    ],\n    [\n        \"0000000000a65e7bc4ea0927f20073536ba2bb15185487eb3653ca382c2529bd\",\n        0\n    ],\n    [\n        \"0000000000026e2c50b815fe655f39ddd2a5ad67363eb346030c72ee468fd861\",\n        0\n    ],\n    [\n        \"0000000000cb8615b605a76852358420fec223cb1f45b58f36fef593feb775a6\",\n        0\n    ],\n    [\n        \"000000000000a9d62c3c9e45990ee3e3a0afe8ba09ee76ea8508f3501490f752\",\n        0\n    ],\n    [\n        \"00000000000d6d07bf649e10f2a43b6f3ffbb3d3b8d44d88dd56968bda1e4576\",\n        0\n    ],\n    [\n        \"0000000000016cdf6d13beb2a804607bba4cb410019bc15a0033d428f0885dad\",\n        0\n    ],\n    [\n        \"00000000000046d0925f6aa3cb1035ce6baa03c895dde5869b63770a95135f25\",\n        0\n    ],\n    [\n        \"0000000000000acc63320accbe3cfa9536987b7f46e995cbc9b6edd22631587c\",\n        0\n    ],\n    [\n        \"00000000000003492c43af082403eb2bd11e02087afb5c590ba8af828c6a4b08\",\n        0\n    ],\n    [\n        \"0000000000e1a369418931a7c1be9dca780253897ba64322ec598ad0ff68cfef\",\n        0\n    ],\n    [\n        \"0000000006b04f242391578d9aa5233c26e6a18557ef7440ea8b05d4e7554180\",\n        0\n    ],\n    [\n        \"000000000c92c04b11e0408a047f4b3d78b420dfd05668f78cc077e03ec81093\",\n        0\n    ],\n    [\n        \"0000000003574dd81c51433aac32302d0e30e39703522f39a254207ef5e64dd9\",\n        0\n    ],\n    [\n        \"0000000000db3d7596d805ac36b95ff841bdb7f95f60ebe2422c2c6ce1b76ed5\",\n        0\n    ],\n    [\n        \"00000000000bb41a19a11b12d37b2a5cf561ae02d7ff916d37f79d8adc6f2d05\",\n        0\n    ],\n    [\n        \"00000000000067675fe5b48bb05cc19526474522d1ebf71a3bbe991d95156e57\",\n        0\n    ],\n    [\n        \"0000000000002a8601d3b33b5341689ebaef6e99051f5e34330af0775953714b\",\n        0\n    ],\n    [\n        \"000000000000cded0095c064ee88824555bf50916806d36d27a5d5975abf8c7f\",\n        0\n    ],\n    [\n        \"00000000000007d4f762e90fdb571facd1c5bcfe43f501bae3294c604427cbc0\",\n        0\n    ],\n    [\n        \"0000000000000837e0ed6ba79a36a447df9395d7220c177a7dc44489766383be\",\n        0\n    ],\n    [\n        \"000000000000024903240e9236ffa291ae98d057f09c79792bba7c12f49ee3eb\",\n        0\n    ],\n    [\n        \"0000000000000000237351eac112eff88756b58451d6a272477690b1c2e50b40\",\n        0\n    ],\n    [\n        \"0000000000000030bb4840680a28b8fa5d6fdec773404f12751b9311815a8dcf\",\n        0\n    ],\n    [\n        \"00000000069d1ee42ce15859a6c76d7b3e5c4c7b76caa80b26933758b0e55f9b\",\n        0\n    ],\n    [\n        \"0000000000006369a1181c0496b55cbcf0ed789c7e851b4bc7311246dee03229\",\n        0\n    ],\n    [\n        \"0000000003fc3d65eae8617cdc1efc6f903bb17fdd30e6a0556fc785e33b0741\",\n        0\n    ],\n    [\n        \"000000000053dc70c6af06084c6ee584e753ae6bfd18e4f748226ec8e7d8f146\",\n        0\n    ],\n    [\n        \"000000000165d59df8044f95ae2ffdea9a6d3e218e5543a4ac442d90fcd1116c\",\n        0\n    ],\n    [\n        \"000000000015c229ad9ccf607263dec26e90ba17b011c993a31b521047295b98\",\n        0\n    ],\n    [\n        \"000000000009b5115bb2cf88e82053430e3bda4bf7a5776100b647d11739effc\",\n        0\n    ],\n    [\n        \"000000000001e854436057811dd868b4be38acbfd420d90abf59e88cca893921\",\n        0\n    ],\n    [\n        \"000000000000e89f85eedc448bf69d39ded3ec12829776e4512706983d678faa\",\n        0\n    ],\n    [\n        \"00000000000003aeb06e89a38668d5b16d0dce97815493cc4a964f3a4c4cb208\",\n        0\n    ],\n    [\n        \"000000000000079d25e51546fdac3e854af63829cf1956ca82477fbe6e373bcd\",\n        0\n    ],\n    [\n        \"00000000000001a4644c28121e5b90a9dc5f574ee7a6b1531ab2a2a73bb027e0\",\n        0\n    ],\n    [\n        \"000000000000010d3b4c4e22444c3bbcbe32d95462da935e94df0600c36d5d66\",\n        0\n    ],\n    [\n        \"00000000000000223ce8299ac8293241676dde2a26dc673ce3aaa0d603300226\",\n        0\n    ],\n    [\n        \"000000000ece8881ee79a62cd6a94669b5529ebda8ea35cb8a5b16f22157bff4\",\n        0\n    ],\n    [\n        \"000000000051d022d139ac89c2b6adda09b91ae70bd9dcadd963cdef1ba3b69a\",\n        0\n    ],\n    [\n        \"000000000339ded080a76b132a9ce123ed47b91b849809887179947af7013ec9\",\n        0\n    ],\n    [\n        \"00000000002f914fdbdf00942b25df72be09c52a35fe5b718da39483543ddafb\",\n        0\n    ],\n    [\n        \"00000000016b02a598d4e3e3b3cd7a31660f5cc96c68286b5e228c79ed4c5d23\",\n        0\n    ],\n    [\n        \"000000000126e5cd7cef8f07840a563c23c28b60d043cedf8b4cd9ec0c72a648\",\n        0\n    ],\n    [\n        \"000000000b0f3b81a581d5407f719ad5eae9eb3f9294403ad85e5f1555af22dd\",\n        0\n    ],\n    [\n        \"000000000d3061ac9ed91baebf222a531a5473e7d8f979e1bee457e3e1cf6dc6\",\n        0\n    ],\n    [\n        \"0000000000000f1528cfa6f357f44f677bfd4c92f77e863e1945a86da556f20c\",\n        0\n    ],\n    [\n        \"0000000003b0f3fc912d574cc0bb312cb35859cd829c54a9db605cbb9bd52675\",\n        0\n    ],\n    [\n        \"0000000000035d15430eeea3107cf09fbe0bb6ad2a7b21553b7c3f578d2c2b92\",\n        0\n    ],\n    [\n        \"0000000000005cb1be53a0827995ac577379392c904740854d6f48c2b6c38936\",\n        0\n    ],\n    [\n        \"00000000000026834560e7c8db380e9c26ecd5f8d6a7c1baae85385f7ba79f03\",\n        0\n    ],\n    [\n        \"000000000001d0bec9f90d83e118c9e6685eda9c57b2337f4b149f2174138085\",\n        0\n    ],\n    [\n        \"00000000000003b8d4d3278ae3147815537a87f9265ec6653798f30e45be92ce\",\n        0\n    ],\n    [\n        \"000000000000038c72c5f71a607f5ed676253259f02b6022603b75f51a02d622\",\n        0\n    ],\n    [\n        \"0000000000000d3dc9970c1feef03569f332a1d7ecbac5b980f382e49e5c6218\",\n        0\n    ],\n    [\n        \"0000000001ffe2564d69c58d1440f82873b47bcf513c85df334b4f1ffa537f0b\",\n        0\n    ],\n    [\n        \"0000000000886705178f570b099f8b2841312d41e819884d32eb76a2d928841f\",\n        0\n    ],\n    [\n        \"0000000003cee4fba7e8d113973432642840f3a914a79164694cd752e06bc4fa\",\n        0\n    ],\n    [\n        \"0000000002f1db982508e0bc313b7268f000587bfd1b247e28e553ffbd5ff58f\",\n        0\n    ],\n    [\n        \"000000000026008cf3dff1367884185aa740203ea5149785fbdbfe40d4ed2851\",\n        0\n    ],\n    [\n        \"000000000014798b39735397575ae0f4f389907d296fd63c9fb7029b0f7d7891\",\n        0\n    ],\n    [\n        \"0000000000088f1729049693bfe1d19d256e2951970df4bf0b102f716899b894\",\n        0\n    ],\n    [\n        \"000000000000f29d3ab2228ccce57b3f1b3ece81937bf780bb7fdd853ce2753f\",\n        0\n    ],\n    [\n        \"000000000b021a42f081054616a39f815bb183eefff8cc0f852a28ab8a531103\",\n        0\n    ],\n    [\n        \"00000000023bf7dd7ed3005de65b537c2c8876f2fbba8848132397f7c2e19b8a\",\n        0\n    ],\n    [\n        \"000000000235d39c23216dfa2f37e027e5cfaec163e0f932bdb2f16cfcd6f59a\",\n        0\n    ],\n    [\n        \"0000000000021dc9864764ff14afdee090b2e338f414d4e51b652b8102c00f21\",\n        0\n    ],\n    [\n        \"0000000000000a15856916bee7d7605138b2baabd2e30c124b41b1061196a5ca\",\n        0\n    ],\n    [\n        \"00000000000017d756a7caa1493256118efe69d7505f1add42ab9f66dca8e8bf\",\n        0\n    ],\n    [\n        \"00000000000007e7c316d7143d8c4ecd91aa79405907215b87635f1aeb1b54c0\",\n        0\n    ],\n    [\n        \"00000000000010b865a46467e275a653fbe7db33a3295f9586c969f7af4d91cb\",\n        0\n    ],\n    [\n        \"000000000000eb5a46ee76387df02b00449911bde789a09ad2817e5f8f768771\",\n        0\n    ],\n    [\n        \"0000000000001c8fccc5f507a27f1ebe034fedf98135f291096b17de6e490d73\",\n        0\n    ],\n    [\n        \"00000000000005cf2c8aca82ce281df04e64b8191f96711394a96df2a49ae7c8\",\n        0\n    ],\n    [\n        \"000000000000027621b9a7ee46c8632ee23b4963a27baab60c8e2b2874ca9e34\",\n        0\n    ],\n    [\n        \"000000000e9f58dc29cc60e6afae3b3b6603c69f888e2b88569103926dbfac77\",\n        0\n    ],\n    [\n        \"00000000000012f42df0d44cd1e5bfa062f3bb2f0760cc581d659cbd07c1bd96\",\n        0\n    ],\n    [\n        \"000000000e2fbad37dc42819b05e83fe3d302c87265c46d82f62eb7dff3efd7f\",\n        0\n    ],\n    [\n        \"00000000008c95ceb4b1b636e726c3e7cdf80c74dafab1c3d13cb130cee40664\",\n        0\n    ],\n    [\n        \"00000000000277a72404c15eeca384bbe32fd4b546e5de1d0d39ed6a5a9a3519\",\n        0\n    ],\n    [\n        \"00000000000dffbab9cc1c0744c8585aec07ba68ab4df86628e079cffadc4ff4\",\n        0\n    ],\n    [\n        \"00000000000cea01bccef6472e11cf5080901900592072b3c7dc109183ec60f4\",\n        0\n    ],\n    [\n        \"0000000000010e6f1b85517945fc164c74b6e60d85c9948484f9047caee44de4\",\n        0\n    ],\n    [\n        \"000000000000386c87e9fa493f75937b7654721e8e411e8899b18e49e79c4f8f\",\n        0\n    ],\n    [\n        \"00000000000000ec39027769af5367e61b3589d7f665296f5a01aea47a367837\",\n        0\n    ],\n    [\n        \"000000000000021fef1e6512646e72fee67cb9678d7a3d3208a108f0d7a11c22\",\n        0\n    ],\n    [\n        \"00000000000001b04b9a0c761f676bfeb77f47f7270799abd749afb210335acc\",\n        0\n    ],\n    [\n        \"000000000000003e289ce157208586cb8a0ecfc45984c217ce6ae38b28101396\",\n        0\n    ],\n    [\n        \"0000000000face4d3828727f7b30d3d7c7ba25e80f39076942d64f2b8266524f\",\n        0\n    ],\n    [\n        \"000000000244a130e6c88855ed65f6e08cf1e24dd05a4562afa9e0aeac869dde\",\n        0\n    ],\n    [\n        \"000000000104f9420a1ceb288ce3da17e22d3bbc4a427727a44438c4df52afbf\",\n        0\n    ],\n    [\n        \"0000000002e43dff8a2776df4b712344e210bfbf92e06d90661c59576488cb04\",\n        0\n    ],\n    [\n        \"000000000083c35652af9b1bab1e9e6cf7ee37ce9912cddb887ce29787aa115c\",\n        0\n    ],\n    [\n        \"00000000000ffb2fca7f2c1713f8737863bbe8d7ca9bcec97d4ae783b782a1ba\",\n        0\n    ],\n    [\n        \"000000000001416d9ae0fef049dff1ecfe2cc84573a80a8414254cfc07d9b539\",\n        0\n    ],\n    [\n        \"0000000000006988c41ed46e245865a8ff5f8837cd31443cb2ba89c75d90b8e6\",\n        0\n    ],\n    [\n        \"0000000000009d35aa98fa14329c525c8b5065beeab1a5e902611fefbe3f37f6\",\n        0\n    ],\n    [\n        \"00000000000031fcc3682ae1e909d1d3b4efd1e58cafbe6eef8d3ae5ad4c7665\",\n        0\n    ],\n    [\n        \"0000000000071a3ae9a9912d030e3c724dd325ddfdb0349a0e0babd1a20f5e5f\",\n        0\n    ],\n    [\n        \"000000000fcb06fa14d45c00dbb937cb5e456d37862814a97d79527e44ef75a9\",\n        0\n    ],\n    [\n        \"000000000000586af55ff52933e66d8fa10a4b87b0a85a369cb1679f216361d3\",\n        0\n    ],\n    [\n        \"000000000349ccaf247304093a4bc06d2699b1c08600375ac4532beb386bf056\",\n        0\n    ],\n    [\n        \"0000000000001d3c3190d3ffa359605719bc0007d750667b693ecccdf2ae4a53\",\n        0\n    ],\n    [\n        \"00000000000047a5bd3d3a42273f93dbf0dd06058bf90e256c9ed63e911bbcc8\",\n        0\n    ],\n    [\n        \"0000000000005910b434e32db0053bb9eac75e1aa1f707cf6e04e1513be209b5\",\n        0\n    ],\n    [\n        \"00000000000017c0df4e9859b84a83e36bd23377792a7ab218e19bc1712fb0d7\",\n        0\n    ],\n    [\n        \"00000000000022140a4f1b51dd23bb5e3a33611941628a88a38441457dcf6b70\",\n        0\n    ],\n    [\n        \"0000000008700121530a0b7cecb70331bff9678570e9791a3ddd1a4f3d7bb4ea\",\n        0\n    ],\n    [\n        \"00000000000005d11acd598fe7a7382712449e4dfc942469d762d87c7648c7de\",\n        0\n    ],\n    [\n        \"0000000000000d245ef989d5ae64249984177dd10e1c0b2f04077276d89a0b09\",\n        0\n    ],\n    [\n        \"0000000000003b8f9bade81533ef7ec7133274c55d8e29f6db4bb6545c2f97d2\",\n        0\n    ],\n    [\n        \"0000000000a0489089b019a7fae1ca64ec8ef179b352e84561ac0a055675dbca\",\n        0\n    ],\n    [\n        \"00000000062041b268ffc19bba83c5db5fbbec734c4c2f969d0f02cb33dd22ae\",\n        0\n    ],\n    [\n        \"0000000003ffa51fff511505a918462d890a0d8e9b3038b43e01ced914ee023c\",\n        0\n    ],\n    [\n        \"00000000062afff83211b48e13f909c97c8a6cbb0d702df73ac553a7871c2e57\",\n        0\n    ],\n    [\n        \"000000000280223436db85632d21bb9673ea9ad270260b46b77fc6d8c2b23e6d\",\n        0\n    ],\n    [\n        \"000000000000097ffea9886e82d5fed96fcb91c1b3c337b445aaaa47966cd15e\",\n        0\n    ],\n    [\n        \"0000000000005f791780e489a7dbca3f372bb36ad7bad1f1d4d8584f7fba980e\",\n        0\n    ],\n    [\n        \"0000000000002b8b3a8285b208bc7cba07e08f42c9705367cb5d2cc99604c346\",\n        0\n    ],\n    [\n        \"00000000000002cd2f944504f4aa6c2e20dea005ca70568b18607a91c590ef11\",\n        0\n    ],\n    [\n        \"0000000000003eacdfd49b651e0871ecbac1edea9e8c07014c19f2b888d28b08\",\n        0\n    ],\n    [\n        \"000000000000274849103a544fe5e64a0d97ff81f0cb4fcbea269ffe01babdea\",\n        0\n    ],\n    [\n        \"0000000000000279b536695aeb598fd6c01adc9f19e5b5bfb9de513a477adef0\",\n        0\n    ],\n    [\n        \"0000000000082feae81a5123d7562cacd2b60476acdc27687b57cb0cb023bbac\",\n        0\n    ],\n    [\n        \"000000000000db4029f289ef085ce1e55939b3225ca0e3297d0a66abd9d54c39\",\n        0\n    ],\n    [\n        \"0000000002583614b4cf8c846f81f4452602f5a3663fb684a7f8d2382f83e9b3\",\n        0\n    ],\n    [\n        \"0000000000d62998c90ece4773c2251143d3b16891fff7066a3fe7fe42fd6978\",\n        0\n    ],\n    [\n        \"0000000000ce441f798e9188f58d9075e7a6b54010a69b8320d106584581c929\",\n        0\n    ],\n    [\n        \"000000000063c5f940edd14e3d91bcdb145ed07d983d72dfe0776474fd5daf93\",\n        0\n    ],\n    [\n        \"00000000002fcd1ab04af227ead9b7db9c87758953c4dacbff4b6428f6d0c6d2\",\n        0\n    ],\n    [\n        \"00000000000bd2a500f81c5500a50af4c59b04b6c73dac9bbee911ac8b5e688c\",\n        0\n    ],\n    [\n        \"00000000000b362c449fea49d85cdbecf81ca83e55b71476e06da440ec60dc81\",\n        0\n    ],\n    [\n        \"0000000000036d40982a6d48c72de13c448f6796c55c52fc19d58b3372ec253b\",\n        0\n    ],\n    [\n        \"0000000000002f469ca442dcc594acaa75b0c4cef2f4bdd4c5460ade34f956c3\",\n        0\n    ],\n    [\n        \"0000000000000225af0f1a97bb54a20992fddef86ef259a52629abaa28b45e1f\",\n        0\n    ],\n    [\n        \"0000000000000be9a2718132ed7891e27316eac5fca42dc196bcaa0e317e80bb\",\n        0\n    ],\n    [\n        \"000000000000013c85749bdbe405cb0dc0bdd4e361281ba7a198ce820ab0405e\",\n        0\n    ],\n    [\n        \"00000000000000f7939565cafaca6f31c9f61f8321ca0fa69211594cb96e7117\",\n        0\n    ],\n    [\n        \"00000000007d22b331772cf0dd919000ff5ec754e1b9254b90fea7d694501a85\",\n        0\n    ],\n    [\n        \"0000000000c41b7447a1a24841abdb7c3f2b3d68bf7707966b3124ff594c1da2\",\n        0\n    ],\n    [\n        \"00000000007976d00aac373b41e6f3e574ff607beba8551dcb7fe6e0a6a5990c\",\n        0\n    ],\n    [\n        \"0000000000014c8c2b476c525b656908b1ad6cc5f7593f63147b882ca38c5870\",\n        0\n    ],\n    [\n        \"0000000000bd0a837703bb57aafa84dacb49a264b5335c42442f0e83fcac2841\",\n        0\n    ],\n    [\n        \"00000000003ea8019a2641d1778bb416f6d9e0ab9b068323654bc55cf7b2d304\",\n        0\n    ],\n    [\n        \"00000000000d0a442a0865a501b427e6425151f5923606bed29e94a72f35a07e\",\n        0\n    ],\n    [\n        \"0000000000030dd08b942ffb56e9a8eda95a7e6f28a03b9d1f488a29c7fc14cb\",\n        0\n    ],\n    [\n        \"000000000629ebd7d6c56c34e858ac6ee105aec8d8db05db5f4b1e7bfec8033a\",\n        0\n    ],\n    [\n        \"000000000038d3dc0b68fcd3275c38b3225bb99bca973a513afb2fe505362d6b\",\n        0\n    ],\n    [\n        \"000000000072b439db6364b048becc5254682c31214f44522566290171447c9b\",\n        0\n    ],\n    [\n        \"0000000000eb455b495292a97133957356fb0e145a593c9920e99bbc1fcbb91d\",\n        0\n    ],\n    [\n        \"0000000000f3c0c53d7b41b7366a44090d09af626aab3f207f94e0506eb2abd0\",\n        0\n    ],\n    [\n        \"000000000014463bee2df062f3a0d84752b55b96c6cc16595899fb2452f23960\",\n        0\n    ],\n    [\n        \"0000000000089435da00b1ff30d48724160cb59877b19bcb3f449d9c279824b9\",\n        0\n    ],\n    [\n        \"00000000000033ddc62f3518f87c4acdf8c0962e5e0753c86e023537bc73906d\",\n        0\n    ],\n    [\n        \"000000000000cfaf02a175dc7a7efdb4ee10eb83d3a04678f0adb5c300b9acfe\",\n        0\n    ],\n    [\n        \"00000000000014a5fd14c81bd67ad64fb81d9672b8d03877a9d3824c97a715c6\",\n        0\n    ],\n    [\n        \"0000000000000b79c643a371a491bb6a27726102f8797c7d08ddf87ed6b334af\",\n        0\n    ],\n    [\n        \"00000000000000499f0d78b9f43fe1f8d4a981828049a2546059d1392f2babc7\",\n        0\n    ],\n    [\n        \"000000000000006494d55236e0ced5ddb0a42dade401b3d77d456e447f666ddc\",\n        0\n    ],\n    [\n        \"0000000000000003ad1c7710bc02bcdd76fe6fca54869899b9048091cccb93b0\",\n        0\n    ],\n    [\n        \"00000000089b0706b3a6b5808ddafb57be5f77e51009afbc81aba91ead1af1c8\",\n        0\n    ],\n    [\n        \"0000000000038dffc62eb25ad2301fc45ccc82a9ca0f3a730d7d25f4cc7695f9\",\n        0\n    ],\n    [\n        \"0000000000007cdc062f569a2227502e75afa99c7ff7c9fd3e043ee61f5e2899\",\n        0\n    ],\n    [\n        \"00000000000050f82f9fe703482a89ba92aad0be11aa686fcafba4c7ca0c4b01\",\n        0\n    ],\n    [\n        \"0000000000005d08ba1d46fef300bd60382be096d46c036622a9c52466fba6fc\",\n        0\n    ],\n    [\n        \"0000000000001c124114694c9c3bbce398d3470bbd8d0795152541a5de1e8189\",\n        0\n    ],\n    [\n        \"00000000000047ae2c50d65efcc15af2014d5b0049d244d8dc78b482151f0e62\",\n        0\n    ],\n    [\n        \"00000000000064a0f4a34ab095a76b9c394069f55bd6bd2b783b5de028b9f87c\",\n        0\n    ],\n    [\n        \"000000000000f33234204685893b980d24bac914bc8b1978c69d8580fb63f542\",\n        0\n    ],\n    [\n        \"0000000000003aac07cd725764eadfa1d887b0d4508858f31e46e711aa801e8a\",\n        0\n    ],\n    [\n        \"00000000000005c12a0970101bbe661f57f63686c48825136bd298b951e86e2a\",\n        0\n    ],\n    [\n        \"00000000000003c4ea51aa96323c5708289580f8e7b6e89d0d3bdc08d0db94ff\",\n        0\n    ],\n    [\n        \"000000000000016b84c8036b3b3fcd2f31928c949d4d35b4400ae80ba23b247a\",\n        0\n    ],\n    [\n        \"00000000031da21f07983eac7205006b839fad1e14f339c065c3ba0789f47a8a\",\n        0\n    ],\n    [\n        \"00000000010a23cc7ddef199edbe37e42acd2461bc9848710e467739423ce2dc\",\n        0\n    ],\n    [\n        \"0000000001ce777eefe9a1f1aee20f33629aba343e5782b3f4ec02e41e282a85\",\n        0\n    ],\n    [\n        \"00000000017972967a46f08c4e79bc29283566c539ff8590247819223cf3f1d0\",\n        0\n    ],\n    [\n        \"000000000000fdb8834f2e67f6bbb4409935c299d46392c8db426359deeffc27\",\n        0\n    ],\n    [\n        \"00000000000023f8eba6eb6d59206adf050dc76b2dbf41b66421df4060c7007b\",\n        0\n    ],\n    [\n        \"000000000000327a31e6e0bf7261edf25cba59c81b895c9d1568cfb9e12371aa\",\n        0\n    ],\n    [\n        \"00000000000033faab4075e74d8b908cc5b586bee4b99464163798ebde37925c\",\n        0\n    ],\n    [\n        \"00000000000062006eae8582aa0d3b6877216793ddd70a1028d65e0cb2c475d3\",\n        0\n    ],\n    [\n        \"0000000009cde9229b24519ef150d00e78249587cd2be3a293b2ccf807f7b1a8\",\n        0\n    ],\n    [\n        \"000000000fcbf2f23f11d11aec4df1375ee8ad149fe2c32b52615fe01b327b15\",\n        0\n    ],\n    [\n        \"000000000a26326c9fe65505e8dbb95f2d10bc5915912be2cf3a3c66f4617c2b\",\n        0\n    ],\n    [\n        \"00000000005e1f0b9aad53b290a662096d88bb719b451007ea9e23ef990a42b7\",\n        0\n    ],\n    [\n        \"00000000000061bdb1e6cba904842454e0eaa79cfef1d2de098cdf62fae9f67e\",\n        0\n    ],\n    [\n        \"000000000000238380880d6af14a72d9f5cfae0a6032eaa0a4ef1291161a83d3\",\n        0\n    ],\n    [\n        \"00000000000007f7088a701efba448b2a3465fb0a03ef2a83215bf5cd9138fdb\",\n        0\n    ],\n    [\n        \"0000000008b6f1711e2cad14256e563ca3e7927957c3d6779ff381f5c5cd3220\",\n        0\n    ],\n    [\n        \"00000000081b05a0ae2b231dc43bddea3c258f70a3ca20500773e29a7b5d75ad\",\n        0\n    ],\n    [\n        \"0000000000000003a9eb8f76429fa078e2e20af75ce5b75da0937ed06cea43a7\",\n        0\n    ],\n    [\n        \"000000000bcaea5fa7f75beb66951d22d9fb7330bad2b84b0e2039a61f9d5a4c\",\n        0\n    ],\n    [\n        \"0000000000000558d535b20ba51c6212e683e0414f8430ce4a9a5aada7c7ac4a\",\n        0\n    ],\n    [\n        \"000000000000379c6eeca100e6f171c71df430611b187043c2b422449560dce1\",\n        0\n    ],\n    [\n        \"0000000005c7361afbd59825f0243a94c47f62dfc2856f490ce11df9f449b787\",\n        0\n    ],\n    [\n        \"00000000076248cbffb87ade5efc8be5dfe3329a2ce1d2a488fdb23d48efb75a\",\n        0\n    ],\n    [\n        \"000000000e283546c050293ac632a61036c18dee9bb810835b661fdfb08f6635\",\n        0\n    ],\n    [\n        \"000000000321148782953d6b0b23eebf7b7feda7df39204ae52e28d224752b65\",\n        0\n    ],\n    [\n        \"000000000077be32cafd14823697979dfcb4843cc11d8d44f2afff9bad1b6d0f\",\n        0\n    ],\n    [\n        \"000000000000567176a554c5a19cab87b7f0a86d98402a09d6613c0f389ca567\",\n        0\n    ],\n    [\n        \"0000000000000b1996700d0427eb92eb52986a3f4639c9e11b8ce37f2dda66aa\",\n        0\n    ],\n    [\n        \"0000000000001b3e599b0d31519e75ec45aefd549df5b6028aac6d4d69cc829e\",\n        0\n    ],\n    [\n        \"00000000000046df035e6fe190b203dcb62c9a1a7cdb08389c79d3987c6dd8d7\",\n        0\n    ],\n    [\n        \"00000000000003e9e7a602aaebb90b73d5004844d5f6f24925b14188e34afc2e\",\n        0\n    ],\n    [\n        \"0000000000000e6263183a20de2b098d31b20c4b857f6d11c523755f5c287fa2\",\n        0\n    ],\n    [\n        \"00000000000002cb548427aef898e94253e0a0ec8ee53c000813551229eeaf6c\",\n        0\n    ],\n    [\n        \"000000000000004ccbf0d92b3aa2285be79b17fbe0fe261ee1c682cd1dd927e7\",\n        0\n    ],\n    [\n        \"00000000000000314c5668fca293d377ad4715bd2b5379318ae2e81fadcb1502\",\n        0\n    ],\n    [\n        \"000000000073f9d5a07e50d1701103c1c12506c54af4e2d96736b267e3b8b3ae\",\n        0\n    ],\n    [\n        \"0000000000ed2345b418284bb0378152d0bc8f360844955f1bdc1797f557a621\",\n        0\n    ],\n    [\n        \"0000000000001da2c63c314956dd2c633f40f8cfefc034479bd60089fad02b88\",\n        0\n    ],\n    [\n        \"00000000091985d3f909245582e26d7836edecc5eb47515e590a5d0b08940de9\",\n        0\n    ],\n    [\n        \"0000000006a654ca1a6bb988584b478197fd0eddbfa9edebae55e2f419712212\",\n        0\n    ],\n    [\n        \"000000000c36097cf9734e7a3fde243870fc9122121e34a9a22fd235c0ce3bdd\",\n        0\n    ],\n    [\n        \"0000000000001ed546608547145905444cf18e7d27d537bc3253b295ebf6164c\",\n        0\n    ],\n    [\n        \"00000000011c08a153b85244c93e601cfd715845ef7bfb8678d52d72eeb33fe2\",\n        0\n    ],\n    [\n        \"00000000003950ad79520b26976725c64556e65d21c7b5029d467decc056d30f\",\n        0\n    ],\n    [\n        \"0000000000003ea7f0a49bb8e6be6c09de704de3a8d329e018d5f34cd88433d7\",\n        0\n    ],\n    [\n        \"000000000002048815f8a57a4688e0ff2ec7b0c6dfd5a855082097e16c1b4f0a\",\n        0\n    ],\n    [\n        \"00000000000021f8ee9909f7102cab8462d8d0ac43a451a864bc792dba0541c5\",\n        0\n    ],\n    [\n        \"0000000004515238ac80d8e7ad1403ef3c97a9651493c9de89a4d6ce06bfe858\",\n        0\n    ],\n    [\n        \"000000000ed4662b59df94409e92509ea67f0753396010c136f5652cca559592\",\n        0\n    ],\n    [\n        \"000000000faacffdbbc3459c6490f7d2d058c40913ec9b1efe2ce0765af4c993\",\n        0\n    ],\n    [\n        \"0000000000001adb00aa71e333150e5ed06bb90e92cb871d74e7fad5c076071a\",\n        0\n    ],\n    [\n        \"0000000000b3091c11666e2e25f66ceb5cc89c43fe4cad628ea96bc5688ed556\",\n        0\n    ],\n    [\n        \"0000000000001796f7dca239170f18edea5d2eba4ad7ace947c4986c9bf5ca06\",\n        0\n    ],\n    [\n        \"0000000003504db8c65ba8631a483894da912c31ab48805d9d75a69456e6cf93\",\n        0\n    ],\n    [\n        \"0000000003f643be16ab8974aa0dc202a16c77a333918b715dc068859ed60ff1\",\n        0\n    ],\n    [\n        \"000000000ff7209cd7acc0e8be21437ec4afce623a80a44c05a655aa3423eae7\",\n        0\n    ],\n    [\n        \"0000000003d1fa3d1c9a8082d1c12da56b3f537b5cab23295dc03187923dc33c\",\n        0\n    ],\n    [\n        \"0000000000392fc59c41a98a986cd160c1ca491fbe3d638bf9acfc9f565344c3\",\n        0\n    ],\n    [\n        \"00000000000077d2ef23f91f9eaec66263551275f1cd58e42f212cbef1f1514c\",\n        0\n    ],\n    [\n        \"00000000005360c2362e89b2001957e8563f2f29bfc5c67453643f972603d457\",\n        0\n    ],\n    [\n        \"000000000ec466e23f09fad26ed11d1611d948e82327c21392bf1ecc761d9dfb\",\n        0\n    ],\n    [\n        \"0000000000bd6cf941aa95726e74c57689dbacda79c61f61bd099a3da98ca08c\",\n        0\n    ],\n    [\n        \"00000000006dcdae735bd55c3a6472c3d8b62d6ada8734b396ef768dc160edcc\",\n        0\n    ],\n    [\n        \"0000000000e8d3a687d796d63c201570bf58e3a2bfc56ff5b2b98dba0590e8c9\",\n        0\n    ],\n    [\n        \"000000000077c5459d889081076e7bc3596d1afff69401e3ba9a747a41527075\",\n        0\n    ],\n    [\n        \"0000000000288bc04badcaeb4dad4c95188aa2af23b9713ab516acb7a2ba08a1\",\n        0\n    ],\n    [\n        \"00000000000018f0c4517333aad28a6bd17bf6599b23f30e736f8b81453c91b5\",\n        0\n    ],\n    [\n        \"0000000000002bf35f1ddf7f173451b8fdd99b09ca7e7acdaacd741a2ac4ee78\",\n        0\n    ],\n    [\n        \"0000000000001b73317bb3e44d0e0c478b607ffca894cc64c4cb7aa46372e395\",\n        0\n    ],\n    [\n        \"00000000000036f9c27a5fb4a1e3408c0b5af23433a8fb914d1ecd980a78cd8a\",\n        0\n    ],\n    [\n        \"0000000000000db0c1c9f82751891801d681a1c45fb1c41595c81443f0a1e950\",\n        0\n    ],\n    [\n        \"00000000004a8225a3861f2d8fd6a2c4f2c97f9580269a3d47c4fd36e1f835e1\",\n        0\n    ],\n    [\n        \"00000000005bda823369e5f94c49834cd98af02630cf33bf63a5126d81aeedad\",\n        0\n    ],\n    [\n        \"00000000096bb045d967239d012773b9c74995439fa29169586283b9771ffe11\",\n        0\n    ],\n    [\n        \"000000000fbb4587b5a097ed17d5a3f4611a55c5697df344759e90b2074df8be\",\n        0\n    ],\n    [\n        \"000000000a34e610a3b5a3cea87bf4dc50cf5ef4f4256fbbbf713850a1931750\",\n        0\n    ],\n    [\n        \"000000000c7115c2136182db49423e85b3f03b72df0095d8b48d41a6e7a5c4c9\",\n        0\n    ],\n    [\n        \"000000000dc75a70535dd6998830eab1201680703badef3cc0c506845b05b411\",\n        0\n    ],\n    [\n        \"0000000006209e2c1585e816bf6ff79aaa52d709e3e15d8c79b907b4b9af482c\",\n        0\n    ],\n    [\n        \"000000000df4fa6c4fb5d4277ca22c2b3432aa3d3d4d34c93fae21c5c6e18f13\",\n        0\n    ],\n    [\n        \"000000000cc1645c4f9dcba4ab347a632d3c3d00b0ae56f72f9a8791c2589ca7\",\n        0\n    ],\n    [\n        \"00000000035de13d1b950ba799262cc730f65cda1ab33c86cfed67c14ab59b12\",\n        0\n    ],\n    [\n        \"0000000000003c0666573e408040e408df8265c1b24d4ce53cf6efa00c289d51\",\n        0\n    ],\n    [\n        \"00000000002a301f000f086912f45dfa1a6608d6d2a14f1eceb23f852cae290e\",\n        0\n    ],\n    [\n        \"00000000000774b67925ebdf31b1f455db3ad534fd9aec0e43bb79925c77b221\",\n        0\n    ],\n    [\n        \"0000000000001b8f1f932360a5936ec6f007ea22ccea5579cc8667f7616977cb\",\n        0\n    ],\n    [\n        \"00000000000035a0698b037925a892e4766d659ff3f7979209552a9ca646203c\",\n        0\n    ],\n    [\n        \"00000000000001437edac6d3e1bb669d907b93ad506f8c096a99b8a0f8cca1b1\",\n        0\n    ],\n    [\n        \"0000000000000e1daa4bf888d63a83097c13cc37318174868d6cf47fd5951b38\",\n        0\n    ],\n    [\n        \"0000000000000067094d226dce682adf9b13225891e3c4bb800cf7087bd20804\",\n        0\n    ],\n    [\n        \"000000000888d73db90a3cfc554070f42ab6e66d7ed603ac31dcf75f56ea2496\",\n        0\n    ],\n    [\n        \"0000000000dc11380abc06c3c9919c140ff2f79ee854457b7ec6219111b24a07\",\n        0\n    ],\n    [\n        \"0000000000002519e28eaeb2a1c9f7dbab73f220408b2cadffc4edf2ba252e4c\",\n        0\n    ],\n    [\n        \"00000000005aaf523f208621a67f1d79bbc49a55d23fc174fd51d5b20ab046b2\",\n        0\n    ],\n    [\n        \"00000000000017eb2b540a7ad338edb2291d3a718624670f0e1eee89e78bab4a\",\n        0\n    ],\n    [\n        \"0000000000000d3119520427d81eecb014cc8911390560ccfaa21932801a5e91\",\n        0\n    ],\n    [\n        \"00000000000d6e620685d7f868d6164e548c0296b8e32263cd557cb3a38826a1\",\n        0\n    ],\n    [\n        \"00000000060ba49b218cee56dd5fa41d66c45476fd9f3da6ec338df98f8f3af8\",\n        0\n    ],\n    [\n        \"0000000000180f7317cc1d110cb221b6767d908b0c6582c859e78c9f04dfb800\",\n        0\n    ],\n    [\n        \"00000000072fea2d890c6368162518bb9ad345adae68b106cf15cb20ff241350\",\n        0\n    ],\n    [\n        \"000000000026fe3d8e277d69708a69cef90a7f2a761796602ce9112644518dae\",\n        0\n    ],\n    [\n        \"000000000045c77f25f1807d143280fa8464ac6df9c7a4b56bc4e34a9416b9ce\",\n        0\n    ],\n    [\n        \"00000000000542f8da30137f2283a54e81f586985f924561e558d097bf972a9e\",\n        0\n    ],\n    [\n        \"0000000000001c3c44d10ea8bb958ca80dbfa5733ba66b8bed179b05d6106d5a\",\n        0\n    ],\n    [\n        \"000000000001c1c53fede30f9b35b3cc05605514e2ab909f4bcff86bde884f7c\",\n        0\n    ],\n    [\n        \"00000000000009b60a8790fe0524faa3c2399a3791fdcc217d21d02cd935c287\",\n        0\n    ],\n    [\n        \"0000000000000d86b1ab286a7d2b41db146575311106f58cf3117d831ca81ddd\",\n        0\n    ],\n    [\n        \"00000000000002b444cde3ce10aff0f4557d918ed7f7d3d33e217e2c4b4d6ef8\",\n        0\n    ],\n    [\n        \"000000000000006c1bcb0b8650557494a2dcbaebb6372dd6a893f372da0cad56\",\n        0\n    ],\n    [\n        \"0000000000000032288278d76032074a25b0f45b2b470a99b08641a0b789b085\",\n        0\n    ],\n    [\n        \"0000000000412b6c96e0c0382e0d8d154f567562dbdd6d515ed3b008fe4c8d7b\",\n        0\n    ],\n    [\n        \"00000000032d4d5006d3753adb0a9c6e0a0a320adb6de9a14fe89c184977fedc\",\n        0\n    ],\n    [\n        \"000000000b8aa72c0093b8dbf56ea257c2aa752467ce63be33b4009429932b3b\",\n        0\n    ],\n    [\n        \"00000000000036cb2a9820fe0fa9cd6b28a0226ff4d7c0e06f3f9de336746dcd\",\n        0\n    ],\n    [\n        \"0000000001010dfc436ee032a16952fd8ad1342752e7ff2b0244d150dc6118a4\",\n        0\n    ],\n    [\n        \"000000000023567623e752cfe32267a6ed9fbbff4f167d88f1e3f9c3528cfb54\",\n        0\n    ],\n    [\n        \"0000000000001ad1b428da9f6e81ee3ad109b4c31532842f59dcd03fa1d830fc\",\n        0\n    ],\n    [\n        \"000000000000555f57a00b2f4ef10748595582e34ad13241838217e295eb9d46\",\n        0\n    ],\n    [\n        \"000000000000bafd77624538518bee5f02e497de67285b907c7e7a1de38905a5\",\n        0\n    ],\n    [\n        \"0000000000000aa8dd2bdc793b12bbf8e548fba5e8b35944631c326d4eb4b0ee\",\n        0\n    ],\n    [\n        \"0000000000000579a497cd1f9e7bde38f0baf65366cc6ad2073159977fc9f332\",\n        0\n    ],\n    [\n        \"0000000000000030ba581e96eee41480ed631a7c2f6d67503f316760491079ed\",\n        0\n    ],\n    [\n        \"000000000000007ce65511b1c34e1445607ea53bdecf9fae9b2e0eca10a6064c\",\n        0\n    ],\n    [\n        \"000000000721e295cbeeb7e1d460d55d6f48396139e3bbe5000b76256d87e936\",\n        0\n    ],\n    [\n        \"0000000004db6aaa2d703469e774cdc2dfa758072c3b32bc1498494b7333ee8f\",\n        0\n    ],\n    [\n        \"00000000010de49ec83b2a5ad987e222639c489a53bf7f6b1a9e74baee07dc05\",\n        0\n    ],\n    [\n        \"0000000006380beeb928bed1a6b15d78f1456b775a4fc40ddbe98b7189dc000c\",\n        0\n    ],\n    [\n        \"0000000001699320ce4fabdade058c18d404468378882bb8e2e3130502210956\",\n        0\n    ],\n    [\n        \"000000000086702466b4b3d53a45c78acd02e6fcd4060915482f7bcb8ab18825\",\n        0\n    ],\n    [\n        \"00000000001fd44a586b1f3c45d531d74a21364cbfa32922a5dff0747295c201\",\n        0\n    ],\n    [\n        \"0000000004600d0aa3fae03ccfb2f52c6cda7a2f0c0377ac93f0ced5705c8083\",\n        0\n    ],\n    [\n        \"000000000f3524398295e430afabf4f947faceaaa913e7a50d52bbd04fdd945c\",\n        0\n    ],\n    [\n        \"000000000fa978185c0f6b6b08a5d3c920528c3044da2af5de67066adf8c20d8\",\n        0\n    ],\n    [\n        \"0000000003285aa7311c3522e58954a630cd15df159f312bbf77fe30572c6680\",\n        0\n    ],\n    [\n        \"0000000000000fb07884c4ff829424937d02586452ba4eb6285feaa27d68d59b\",\n        0\n    ],\n    [\n        \"00000000001944317acf0e191bd6dc7e743cdec33fe09ee3c0d4b540e36a8202\",\n        0\n    ],\n    [\n        \"000000000004af4f58f7974bf4434017104050fd14cfcde5473e84239c9e8507\",\n        0\n    ],\n    [\n        \"0000000000002531b5caa3f5f0b58102a04c4462998dfe8790001b39823619c7\",\n        0\n    ],\n    [\n        \"000000000000fd6e966ce4b55f4c8319fda3aeb7ccc7f0162879f85050454499\",\n        0\n    ],\n    [\n        \"00000000000001f4b22e4bce9c874dc7f81fd81de30890bf5c0037c132f399da\",\n        0\n    ],\n    [\n        \"000000000000013703a8d800d1e40ca4db69243003ce1f32eef75cecfbce34c6\",\n        0\n    ],\n    [\n        \"000000000a9261a8d5f86317e4a4ad56ff96a95b3ded98f22ece31e9e7e1699c\",\n        0\n    ],\n    [\n        \"00000000000ffc1562d4903732ec53396d298a488ed52b2d4099148721f47185\",\n        0\n    ],\n    [\n        \"00000000000f57177045b1df6c0010e813089f7fccddf88ca9050008e043d5b1\",\n        0\n    ],\n    [\n        \"00000000002f57d47c85dbe00927776e12bf3c332d7848e2668f7cc1412be086\",\n        0\n    ],\n    [\n        \"0000000000e1cc3c5215f5a848f065ebb3a43d30bc4580681ccc32dd7093e82c\",\n        0\n    ],\n    [\n        \"00000000001b961551a1797ace18d54443928afab8b53a9d883b4d33c7065794\",\n        0\n    ],\n    [\n        \"000000000006eb5f5c91d53b92f2f237abfe4215e16aba860b9c83ff4531a363\",\n        0\n    ],\n    [\n        \"0000000000028a7ed33b6625a08785a54f58c6e40c686a8868bec43ea07cf524\",\n        0\n    ],\n    [\n        \"0000000000004c23f5e2291ea84114504ed53c0cdfe39fd0962ad51e3f44a71a\",\n        0\n    ],\n    [\n        \"000000000000359cd2356a0ef9843a6b0b089469e20f39d86de6336167e3bb31\",\n        0\n    ],\n    [\n        \"00000000000007e77bf73bf030331c461ab57c17c7b52e1789b1992a3bc6a036\",\n        0\n    ],\n    [\n        \"0000000000eb8fd6531c52d17c8b78367415b12b946fc6c906d63239b85194cc\",\n        0\n    ],\n    [\n        \"0000000005b862746087e30836e21d6169e88dcc891edc9965ea8d3c6a5d1ca8\",\n        0\n    ],\n    [\n        \"00000000004c3948942914866eed7fdb5b21c8d6f7926e633d4ee463661f1236\",\n        0\n    ],\n    [\n        \"00000000000f838dc5fe195485eefc05e57064ff5744c8ce5a40fb038d611ed3\",\n        0\n    ],\n    [\n        \"000000000018fa4d5ad9133b828d1d4847017f9c28f266858380ac84aa48bf20\",\n        0\n    ],\n    [\n        \"000000000034b78e1088b935f089917350ccd7183dde2e70482bbe70fb802377\",\n        0\n    ],\n    [\n        \"000000000000023432a73d4cdb6a9b81e64cfb0e7465e2cdfb3df78140622485\",\n        0\n    ],\n    [\n        \"0000000000000dbd6ea9ea2f6c0846f78890747eb7de4b83727a4e9f769a3bc7\",\n        0\n    ],\n    [\n        \"0000000000004add97d7579bf34384dd3164cb25d4c5add6534401910de4f2c7\",\n        0\n    ],\n    [\n        \"00000000000002053b7bf544342efc88c71785472fb1f4f92b592d9bba7ad26d\",\n        0\n    ],\n    [\n        \"0000000000000c2e80a064b77191131706bebc1196bac9ac913aa63f44a3584f\",\n        0\n    ],\n    [\n        \"000000000000008a76191fb3b5e341056c982b5a0631c81a7d23652224968841\",\n        0\n    ],\n    [\n        \"00000000004fcd575e591cec98931fe9b35df1224b6ee9cda2915ef2d6a907fc\",\n        0\n    ],\n    [\n        \"0000000000001d6b8d2caf598365e3f41789bde086859a9c093ef5c6ac4531f5\",\n        0\n    ],\n    [\n        \"00000000000d778a5dcf7e84197ba476035f802a17562af2a6746cc46c6a8ed0\",\n        0\n    ],\n    [\n        \"0000000000029a99f42369b9f1aea9bbd47390d2bacad09a40637536183ee2a5\",\n        0\n    ],\n    [\n        \"000000000004e100d497e03ae45b7d039127985bb66f822f7d0b68a2919e0c1a\",\n        0\n    ],\n    [\n        \"00000000000a961916fe000f2b7761f867a16f2e8a29bb6cc58dbac63c26b71b\",\n        0\n    ],\n    [\n        \"000000000004e13f7bd19836f89f3a8757ce80ebc7c4ce2041dfe196966b9b76\",\n        0\n    ],\n    [\n        \"000000000001d5530f7faa64ac828bac0dd66f058bdda86f320ccb4ea6b4705c\",\n        0\n    ],\n    [\n        \"000000000000184818f0675a919869bce1ad0484f5a0734a8a3c6678716b35db\",\n        0\n    ],\n    [\n        \"0000000000001d3cef6965a0591426ac446aec5e714b6da2f698f240a8c7b297\",\n        0\n    ],\n    [\n        \"0000000000000a6251cb71b0e7c79d577ba70f502a0fc8a235188856f062148e\",\n        0\n    ],\n    [\n        \"0000000001a0d7ae29d0fb16055f476831d372d3c3b9aa6ffd30fc3136df2618\",\n        0\n    ],\n    [\n        \"0000000001b5c42166bf291e425ffe152401360ef771fe656a37612a6bc9c32b\",\n        0\n    ],\n    [\n        \"000000000ee998ed1c31cd407a8ab2de2d119c1d38fe166f3a133e1b529162ec\",\n        0\n    ],\n    [\n        \"000000000f6d809b34d3a42d9b4c93271c62e225e5a7e1fd20a9ed92af3bd988\",\n        0\n    ],\n    [\n        \"0000000009a167f6d9d8cc6852aaf12be5f4b3819929a6e12a372f66296a8e8b\",\n        0\n    ],\n    [\n        \"000000000345f1453151412ea96a82be32be8fb6ce6d646ee608b8bc8ae146b3\",\n        0\n    ],\n    [\n        \"0000000000be58836d26e311860860cf884d9d098abc2cdd6423ba1b2b2e41d3\",\n        0\n    ],\n    [\n        \"00000000000010d95c3b409b8ea4df1780d3c9f9c7ca087b24c7c0c141fc9c97\",\n        0\n    ],\n    [\n        \"000000000b81334ce2ff301cfc3c94c86dd02bbf72cf3772278eff39cd71832c\",\n        0\n    ],\n    [\n        \"000000000c2bf411d96473fa1c463950040d008ebedda1189a1cdb54d7bb31f2\",\n        0\n    ],\n    [\n        \"000000000e7ece2a317027c09c9cc2210c177a865bf9b7c85012b9854f2bd850\",\n        0\n    ],\n    [\n        \"000000002731fab33587e08e23ac94c1d373a15d7b9875766188845ee5a969eb\",\n        0\n    ],\n    [\n        \"0000000008a30b71218412c43ed85a015fc4fbeb1dd2243992578b501407a0e5\",\n        0\n    ],\n    [\n        \"000000000f7e4666ec661ed4ad11b4767cdfcce1208e8cba788d74a9bad05b1f\",\n        0\n    ],\n    [\n        \"000000002503bfac95c7fbd3dcf3ff56b9d555c5e604f5f36d87f2e19eb0d353\",\n        0\n    ],\n    [\n        \"0000000000006abbe507bdea8936ccfb72c08195aaa81dc11b1d3f550d4c54dc\",\n        0\n    ],\n    [\n        \"000000000000ad055b6e8966c482af5305e07754af2d8d520d5c70bece826807\",\n        0\n    ],\n    [\n        \"000000000b342baee465a347a4569cf50ab95376e59f87bdde38d9433f6428de\",\n        0\n    ],\n    [\n        \"0000000008a12eeaba440680a497d1323f27c4a27342087ec4c0460b12a7805f\",\n        0\n    ],\n    [\n        \"00000000086695a3bcdb7a9ad86f91f6da9da221197983bcfc7ee5a2ba69da9a\",\n        0\n    ],\n    [\n        \"000000000bab17aa2bb225314742c07ccb247feef53a4cbf08d65622e7bac06c\",\n        0\n    ],\n    [\n        \"0000000007f776d9cd5e718b2677c24596029fe9c8694a208b75e1170f534735\",\n        0\n    ],\n    [\n        \"00000000000004ec0928faac1f3a1031b2ab5e462936419c0ae17124dabec4af\",\n        0\n    ],\n    [\n        \"000000000006a2cc94305823c0a5d30a03f1340d45c36ce754cd299631bac67a\",\n        0\n    ],\n    [\n        \"000000000acbd3a599053a3b03901ea2bb4cff6ca66824b064f2d901ad66bdfe\",\n        0\n    ],\n    [\n        \"0000000000001148be4d8ab2acf66cf0dc68f636fa394a059cc6c50a57ac579e\",\n        0\n    ],\n    [\n        \"0000000000eae9077d1dfe58b371c399d3e73f6dff18e9f7bd52fa5528734af8\",\n        0\n    ],\n    [\n        \"0000000000343826ded7a6de205d98ba2b4f4f64ce05af8275e6bce684c62631\",\n        0\n    ],\n    [\n        \"0000000000094454634a162499692b4b625fbfb119df402e2c42dffe54feadca\",\n        0\n    ],\n    [\n        \"000000000029f9cd34b0f879107f884772c7a345a4e625cad45055f5df847629\",\n        0\n    ],\n    [\n        \"0000000000067abee9818a634edc244ff42eb6b71cd6ee88d41b1791d7bac8c3\",\n        0\n    ],\n    [\n        \"000000000000c4c96ca78a2607c59b175dbc83c6a386964230dd9a29d59f3789\",\n        0\n    ],\n    [\n        \"000000000000715f720c3ed3e8a988499cb39f9b84f448fc7e14546cc62c44c6\",\n        0\n    ],\n    [\n        \"000000000000358b18bde41a77fb5e97ee1185e1be83b832c18d0c2ae38a073a\",\n        0\n    ],\n    [\n        \"0000000000000375efa661ab6166a51e19497512052940d8218b8d893fcacaa6\",\n        0\n    ],\n    [\n        \"00000000000003af2ce5930d4c686f2630ad58447ef6e5da36fb6d0c5c73acb6\",\n        0\n    ],\n    [\n        \"000000000000008ff35d010d7f94b841f37f2c9801291acd029e49975c6941c0\",\n        0\n    ],\n    [\n        \"0000000000000011f4f1c7929b38eadfa067cf148026cf40a4cd0f87747c07b1\",\n        0\n    ],\n    [\n        \"00000000027219ff91c48b554b87d0375e2231946d6eb4ecc547f4a7234f6d18\",\n        0\n    ],\n    [\n        \"000000006354b8f3148131ffdec12cd0b63c76c0c2a0e037d0725d2c24511ef1\",\n        0\n    ],\n    [\n        \"000000000000037c25c08ec40b55159f87e78cf00df700e2ecf9d5fe537a0e7f\",\n        0\n    ],\n    [\n        \"00000000000d74e182a79f8ceda21b27f63c6622c66e75512b617e3ce8a736f2\",\n        0\n    ],\n    [\n        \"0000000000cc362906f644f2a649a9f17a9e3a08e123bf08971d829cb92e448c\",\n        0\n    ],\n    [\n        \"00000000001dd7a9253268e93bc0ca0cdda096cbbf14445a5a979a17607207cf\",\n        0\n    ],\n    [\n        \"0000000000060fb6d03127b2d0669e1b061b730c20ead99200d5ec8f0028975f\",\n        0\n    ],\n    [\n        \"00000000000008637fd1b69af2bf8b94065fa444382cac2b4745484b203e6822\",\n        0\n    ],\n    [\n        \"0000000006c7f6d4c0bf9d2b313d3039a3864c9baedc9fa0f829b8c2c65690ad\",\n        0\n    ],\n    [\n        \"00000000002512b588ec16f52ce7ea23ce183d5956b381a951f35373814b5382\",\n        0\n    ],\n    [\n        \"00000000000c3374ff0eecba8f087b61d7e35d8e66b57d47b81e0c64c83ac526\",\n        0\n    ],\n    [\n        \"00000000005e81310d1afe6822be62de5c2137334ae33ab48770023f07c59c84\",\n        0\n    ],\n    [\n        \"000000000002802db6ef175423f18eaeb1a1b4a31b93ddb9508a78c37380ff1c\",\n        0\n    ],\n    [\n        \"00000000000e324cc1ce853af182634f58096aaef2725d9220fd6a7ec349703a\",\n        0\n    ],\n    [\n        \"000000000009a845400bba282bd1e2907e6423dcf74fb1fb6f96ebaf6abd9b84\",\n        0\n    ],\n    [\n        \"000000000000d81431902f3998dc0283a54ef5652fc8df091bb65d80c996d98c\",\n        0\n    ],\n    [\n        \"00000000000066bd25372ccecbe69d487fb2190d81a8e67679e484d541ff83f1\",\n        0\n    ],\n    [\n        \"00000000000002a470b897fa91318a1f34ad9d7dfa1203cfa6b7d106bd8cca07\",\n        0\n    ],\n    [\n        \"00000000000000a9846dea5cb42c990fc532488d38e92ee7faccf66f91fcdaa1\",\n        0\n    ],\n    [\n        \"000000000000004084ee64746c686cb4a021ad22a97588f6c503188db6d1d917\",\n        0\n    ],\n    [\n        \"00000000000000142ce2586d6439bfafd0d53d32ff41c1d56602e394f32edfda\",\n        0\n    ],\n    [\n        \"0000000000000071fcc3a3cbd37aa7feb1c54580e532ef7219a9a24648b9ab09\",\n        0\n    ],\n    [\n        \"000000000e155f0afa09985dc14fe86f8121819c6d5e455e7773e1b27290b2e6\",\n        0\n    ],\n    [\n        \"0000000000001a44a33ef9aaa52457c6ba952ee215bf1c75afb31b51ec65a59b\",\n        0\n    ],\n    [\n        \"000000000000001dab82737f9c30cd23319dc19f129f1a6a958e11443bf96c63\",\n        0\n    ],\n    [\n        \"000000000000320acee35e29f344c28a6b0e61d930e3454b41160e02547fab38\",\n        0\n    ],\n    [\n        \"00000000000023a7455cfc34eb472b2cf2e3cea92c075207aedb73a843683e5b\",\n        0\n    ],\n    [\n        \"00000000000003cc17e82a58382a746c79ce383fe25387a18dc4a7162695b24b\",\n        0\n    ],\n    [\n        \"00000000000004a50ed62cf7fb18ee8d8375e9df71fa6577ea66690d2560d769\",\n        0\n    ],\n    [\n        \"00000000000011c599d9481072c314e1d6f9dd7ae862a8c8afae2777b71d0340\",\n        0\n    ],\n    [\n        \"0000000000001b1e2885bf70ac03266d7bbd77d99da7216be9d7e3055281243a\",\n        0\n    ],\n    [\n        \"0000000000002f9ffc72d5ed2db1dbd5d4421e848a486ec0740cc23099a1d6d7\",\n        0\n    ],\n    [\n        \"000000000000181ad0f3edb9bd9dbff6a4bc4081c72e6882ecb0a7ced233037e\",\n        0\n    ],\n    [\n        \"0000000000000360d9d309f32e14dc4dc2cccc57715bcd0afcfaab3c305fea59\",\n        0\n    ],\n    [\n        \"00000000000001954c349d2b1dfe936f34ec475aad948cb5c1033ca5d0a7e385\",\n        0\n    ],\n    [\n        \"00000000000000800aedc44c721f51e6ac502ecac49734e1e085d6f04f45d265\",\n        0\n    ],\n    [\n        \"00000000cd7d09dda2b4d4116552fab663ad600ec316fb81fee153ec10e56da1\",\n        0\n    ],\n    [\n        \"00000000000032520799302b06cec40a5181120451a0f61183a5c60d75c77a40\",\n        0\n    ],\n    [\n        \"00000000000033982563fa344700e74b209035cadaa0ef965b99d1fb7b6dcd32\",\n        0\n    ],\n    [\n        \"00000000000028b83e4a14503b6ec74f8333a77e02b4911af4fec814eef81695\",\n        0\n    ],\n    [\n        \"0000000000006c0b30adc040e886bd7788d62a8b41ea1db9dcbc0799e6f2e737\",\n        0\n    ],\n    [\n        \"000000000000014a918d5e373be7f26e3a140a7fae398f32479971242815aba2\",\n        0\n    ],\n    [\n        \"0000000000002e992b9179f941e33aacc33e1fbc0d68f47ccf5df1f9156770b4\",\n        0\n    ],\n    [\n        \"0000000000000d09de158157254cd426a88b8b61073a81775aa14b90df3d5133\",\n        0\n    ],\n    [\n        \"0000000000006e2930da07f5be918af235aac0d8c51b20314399066a7ae9b8db\",\n        0\n    ],\n    [\n        \"0000000000007d6168adc3577a27c3093c228d1d29b924e44ee6553d19627e13\",\n        0\n    ],\n    [\n        \"000000000000195f3294a3e529151b1fbdeec735d0ca2f2787714dc387392753\",\n        0\n    ],\n    [\n        \"0000000000000101d519d0775e4fddd0c73d80a210cbfe720899f8cbe41976bf\",\n        0\n    ],\n    [\n        \"00000000000000133a76f07bc3fd48a608e48a3e4cda84e577c8b923bb224f47\",\n        0\n    ],\n    [\n        \"00000000c577e64709e648ee42fa3dba41d59175acdcd99b644ba7b5ca2e5c40\",\n        0\n    ],\n    [\n        \"0000000000001b8dbc7720c23866311b9599e45256b568286099d90661c90e3d\",\n        0\n    ],\n    [\n        \"00000000000005caae0c34a171ff49b9e887357644f44a4917ffccdb1f42dbc4\",\n        0\n    ],\n    [\n        \"00000000002f91f1dd7cfb25d8ee2f61a995e50d0ec22b077c042e1de7b5f6d9\",\n        0\n    ],\n    [\n        \"0000000000002dec1164348f1b13e7198015ebb69d21d17f1db540f9f82254d2\",\n        0\n    ],\n    [\n        \"000000000002082aabff07e386e279c12fe27d45e9929b13ee218bcd42cc917a\",\n        0\n    ],\n    [\n        \"00000000000016e4462971bf4c30bcef675d766829063f337523bbdf39ad2dc8\",\n        0\n    ],\n    [\n        \"0000000000000ff9b10fecfaf454ecc47fe228d9f8412b1e24aa3c9634109660\",\n        0\n    ],\n    [\n        \"000000000000262c9ede4171783f97e7f559fd7dbdcb95e7d89414f3e91774e9\",\n        0\n    ],\n    [\n        \"00000000000020a280fe02e039f9ac8f443afcc0f17869f2bbbe9f983b7ae4f8\",\n        0\n    ],\n    [\n        \"0000000000000d214d8dbb194e92c0de0d817ccde5d3662b20bf41c39ba7c46c\",\n        0\n    ],\n    [\n        \"00000000000001140f819a403f931fe6ead3c996781715ae1a2bae7108964dd1\",\n        0\n    ],\n    [\n        \"000000000000006d98a1ac170738d4d9d31173f18d3c4eea36b8b2b39d6c4dc6\",\n        0\n    ],\n    [\n        \"0000000057a23ba17eab7ea1ac8f5607497d80f17add1683166cb7dcc4bfb1ef\",\n        0\n    ],\n    [\n        \"0000000000003a5db5ddfc931eb257c1fd8240f56289b987ee7e12563ece7e91\",\n        0\n    ],\n    [\n        \"00000000000000ef1e4978348aa888a63d97fb20961e22f51b88e80c73a0adf6\",\n        0\n    ],\n    [\n        \"00000000000000ded525277b70d1c22012dbef28f1abcc4519c9b1330d1c6b98\",\n        0\n    ],\n    [\n        \"00000000001884ae12a9ddd3a274af90e28fe77c7d53d4e25ca614cde1aba3ae\",\n        0\n    ],\n    [\n        \"00000000003db05cf4d82f61a1d52cc5d958c21f516aa3c348d39a4d7aa37f3c\",\n        0\n    ],\n    [\n        \"0000000000121061c34c8807ba8036093628ada5e046d36d257a2b520d29bd30\",\n        0\n    ],\n    [\n        \"0000000000001c7d96178988db6aa1450281a8208b67ba6da8d02e25a9ad9cd2\",\n        0\n    ],\n    [\n        \"00000000000038a153341ef0baee829b4e5c6208710b145cd4492f644b825ddc\",\n        0\n    ],\n    [\n        \"0000000000000d2f02bf068bd1344713ac3a52dae68b2431d860e85ba727dcbb\",\n        0\n    ],\n    [\n        \"0000000000000535659de712f2344e83fc893955480c4e0d18a9d959c4ae35e2\",\n        0\n    ],\n    [\n        \"000000000000033daebb675ae26fe3e3a2d0fcbf281f70fb40d1b1b1f41104bb\",\n        0\n    ],\n    [\n        \"00000000000001373a277a18b25690db1720f038e373e45b56bfe4402418a293\",\n        0\n    ],\n    [\n        \"000000000000002e92327500efa91d3c0352f394b2c6b205e1b2a8a0323f1ac4\",\n        0\n    ],\n    [\n        \"00000000fdc454e72a6d98495fefe2197fea1ad03e6202c23843cce9a2962f09\",\n        0\n    ],\n    [\n        \"0000000000009d979e5a4dfd64efe714f96e3a77f9296a917d6b109e76457c36\",\n        0\n    ],\n    [\n        \"0000000000001cdafba80309177c007468e7a5e9512663ad5247659194f18802\",\n        0\n    ],\n    [\n        \"0000000000018ff4e57f8865ea7de7725896df3ab4e5c6077b0a6c20df6511bd\",\n        0\n    ],\n    [\n        \"0000000000040f4902037267b28a2ee8e87dca565d23f0e9723dd437782c77b1\",\n        0\n    ],\n    [\n        \"0000000000004397345295d78ae41518a4af486f7ebc990dcc67bb3750c828ab\",\n        0\n    ],\n    [\n        \"0000000000002174f6cdf300e2f9c2706850e0dea1f0119d9b0914328b93a4bb\",\n        0\n    ],\n    [\n        \"0000000000039a00fb8ff791b9b2d83ba37485801d856bd9e7925ae540e4dc5e\",\n        0\n    ],\n    [\n        \"00000000000004694e5fba3428cd48c9858cf6932f64be4f290c95fd9e393063\",\n        0\n    ],\n    [\n        \"00000000000019a0ae2b62d65095889aa0fe28a6629f465e51cc06d56061d134\",\n        0\n    ],\n    [\n        \"00000000000005be0e01d3cfb0e689bc7d9fe50a74f3dbb8e5cedc5792f8f1c7\",\n        0\n    ],\n    [\n        \"00000000000000227b2f3dd83533e9fefb68bc59d62ec71244acf616552533ef\",\n        0\n    ],\n    [\n        \"000000000000000bdf712efe085711c72387eaa76f5d79606d135d147bba6fa5\",\n        0\n    ],\n    [\n        \"000000009f5b37f19334d54e6a778650ddfa021ed10b3f0948952c858f71aa11\",\n        0\n    ],\n    [\n        \"000000000000389f3959130c192582eac8607121d2c8452166a4a715fb5c770b\",\n        0\n    ],\n    [\n        \"000000000001b49aebd3997878570e8d1d78aa4062bdb83f72b178faea1e1599\",\n        0\n    ],\n    [\n        \"000000000000ce62dcb191b681f6d552389dabb663c0cfe0680c6e308a443ac4\",\n        0\n    ],\n    [\n        \"00000000000053cfbd088bd7c065ff1b24bdd0b343d976de7d5f0da1b2cd19d9\",\n        0\n    ],\n    [\n        \"000000000002e5e6a1f262f0dffb01eba71ff07acb5ff901d8e977d8fc17322f\",\n        0\n    ],\n    [\n        \"000000000001b494fefce702f5ea369d5dc91b4e517c570b656a83795bc69e44\",\n        0\n    ],\n    [\n        \"000000000000c42a983d3e4a9d7b331ef07831d89e321cf28bfb9e049bffcaa7\",\n        0\n    ],\n    [\n        \"000000000000d69faa189d6bece30215641349a60f7eef40c28b3e0d8fafdb3c\",\n        0\n    ],\n    [\n        \"000000000000062f8df83420fc0db7bc81dcabafa3de817eae9a7988f727e4f5\",\n        0\n    ],\n    [\n        \"00000000000001b2ef133ac5d1c35068e8a28a35bf39527ec6e9fe1c22faa94f\",\n        0\n    ],\n    [\n        \"00000000000001b0a6a20b50dcab2725f528f062e1b8eef6ce04bf1398cca5b9\",\n        0\n    ],\n    [\n        \"000000000000a1a77fd497e4166f9d49336e2fa1910be398e99e673a8c164db2\",\n        0\n    ],\n    [\n        \"0000000000025e2694cc21be786db74fd365dbb0f4da514148ad99e4f2ebee1a\",\n        0\n    ],\n    [\n        \"0000000000030422a5adcc3e6715937cf36b180d78a5171a786413afe5cf92c6\",\n        0\n    ],\n    [\n        \"00000000000267cfac724ff60632456a778438c2754351694c8924f4bbcd5c79\",\n        0\n    ],\n    [\n        \"000000000001c6b251f0b7a1dab3490af8bd59c132c47c92093e385a654d00b7\",\n        0\n    ],\n    [\n        \"000000000002a52cea59e1aee38b14ab4df4a61d3c33dcf239c617ba0c2a8414\",\n        0\n    ],\n    [\n        \"0000000000000035326f862d2769a4179098a9790328285e41c25c591f3d8b47\",\n        0\n    ],\n    [\n        \"0000000000035473af9fbe2ddfbec0f70188b341f330329c5b0bcaa5159de523\",\n        0\n    ],\n    [\n        \"00000000000007f012e2d4c8a02bc44092f4c2caf2a8b42db8111df9a40c301d\",\n        0\n    ],\n    [\n        \"0000000000003dbab565a793a2174c70e3d9bd059a72d70d5962f33afb0508e6\",\n        0\n    ],\n    [\n        \"0000000000000875cfa1b9ef3980e432e0b94e631fbbe26c5cf4be48d1f80d5f\",\n        0\n    ],\n    [\n        \"00000000000000d7aac746ab42e51f5ea6bda208909be3b73ee35b1d529f0549\",\n        0\n    ],\n    [\n        \"000000000000006b4a360564ade70833bc8ca4077e54693f58c7d658fa42060e\",\n        0\n    ],\n    [\n        \"0000000000002d474eabb223a62c70ae60261eb7e1b60774ab7b89bd1b4e862b\",\n        0\n    ],\n    [\n        \"0000000070ac5425cbc994b059cd7e379e600cd608256ca371c6bdcd80673e70\",\n        0\n    ],\n    [\n        \"000000000000157983c64e148ee82a1b956b3dfc5961e4193c6ed9a01babf7aa\",\n        0\n    ],\n    [\n        \"0000000000001bdb3697b4cf9c73e5494a6aceab8e26a767af183a14adfbee96\",\n        0\n    ],\n    [\n        \"0000000000001024636de30482825e12a55ab284beac76dd32bd84008e7a56dc\",\n        0\n    ],\n    [\n        \"000000000000077901852d1720e9fe55b317b1d707acfe7eeead14ca39d3e858\",\n        0\n    ],\n    [\n        \"0000000000064673f804cd7023f8d6a4167c96a058fa2f6abeb8c52b23b8e48c\",\n        0\n    ],\n    [\n        \"00000000000d74afb5c3e78596646e72c459064f491fcac9392f4662191fad85\",\n        0\n    ],\n    [\n        \"00000000000284052da60a4948011dd9b8b47ee9600070c167a3fd38269089f9\",\n        0\n    ],\n    [\n        \"00000000e8160e59aea361907f43ae001e0f57f81be07d8fae7eedaa0ac7e874\",\n        0\n    ],\n    [\n        \"0000000000351cc9f437d474751a41873e425adfd90f1e04a685b62b22b735ef\",\n        0\n    ],\n    [\n        \"000000000019ec9bd658f85ef2a8fb8b7117cbab8224b3f6da58ec84323283b7\",\n        0\n    ],\n    [\n        \"000000000018fe65ad2c59fd8c42a9f8b8355e09339c68bed4dfb8dc7a96c464\",\n        0\n    ],\n    [\n        \"000000000011ea14747c257f6a6b70fd8ffa1d1acb450ce487d75e0afdcc4a6c\",\n        0\n    ],\n    [\n        \"0000000000112673666bbeb30ac2600f5c33f50dcd3f45a84a0b369c82b2265a\",\n        0\n    ],\n    [\n        \"000000000006fc35a1b46ee63f58174344798c3945aeff136b18696fc8b37db9\",\n        0\n    ],\n    [\n        \"0000000000041cabad64c90b9a6011cc275bc4274d4483c9d08c61354f61f87b\",\n        0\n    ],\n    [\n        \"000000000000c49853970e4dcc57747e09c025b20a1b5399feea8e4da52e838f\",\n        0\n    ],\n    [\n        \"0000000000005bdae23cd7d0fd132f647baeff5bcf27c128af9d4452918ab935\",\n        0\n    ],\n    [\n        \"00000000000005f2a7320a92d69ca7a43fd50c0f33663a3620fd3b1997d7ee07\",\n        0\n    ],\n    [\n        \"00000000000000fdb92b762b90c620b7fbcfd638d2cb5885970e0d0bc78b39dc\",\n        0\n    ],\n    [\n        \"00000000000000f8d6ac6a65d665cd5dec111aa7ce9d35e8ec8e1f35838be3ac\",\n        0\n    ],\n    [\n        \"000000000000000c4d57eea373335af6bd32da3ff748277e80fc8c8448a50363\",\n        0\n    ],\n    [\n        \"000000004b67788b148a9ec401da418e9b3d498064158e9c05a15e88fff770ea\",\n        0\n    ],\n    [\n        \"000000000133f2f73a74c399ffe67f7a3556db333a70eb3cba385e3c8a8a0c2d\",\n        0\n    ],\n    [\n        \"000000000c4ed1736d5cce4a0ec7633800645cd1b9a5d360fb68df326f48df14\",\n        0\n    ],\n    [\n        \"000000000950c87b961f40073b6b940765037a331dc0f7abbfd4c2a93db2def7\",\n        0\n    ],\n    [\n        \"000000000000333379fa00b0b1ec72dfe6177075d25800caae3bbea5ce0ed6fb\",\n        0\n    ],\n    [\n        \"0000000000002b26a1064652fe46da8356565adcc084ea5998dee2b19de66dfb\",\n        0\n    ],\n    [\n        \"00000000000077c470e5a167d683ea0584a8f7a4b20c63ad5ce502f054cee10b\",\n        0\n    ],\n    [\n        \"0000000000001583cd8eb780acf4ea25ff4d915d02ba99ca280b26ed5b4ca7bd\",\n        0\n    ],\n    [\n        \"0000000000007e9819e4ef5116a0a1ecfd973e15de5786879a3538fe9070de0c\",\n        0\n    ],\n    [\n        \"000000000000352b18af7591d822bc692a5a360cfee0d261d53f91e5d221bbe4\",\n        0\n    ],\n    [\n        \"0000000000001e7f3b9aefdb3dd75677b1b182467941550bab8cbf7906a555cb\",\n        0\n    ],\n    [\n        \"0000000000000fb3fd1f154a1669e779555793c5d1d1c18cab76cbfae70b27d2\",\n        0\n    ],\n    [\n        \"00000000000003b68cfba858740b14b9493bad9e6eb3bea3a4fcbe08cca911f4\",\n        0\n    ],\n    [\n        \"000000000000002a909cd56c02a14c89deb9acd9ce0e28a0bc34aecb4ad9921b\",\n        0\n    ],\n    [\n        \"000000000000008316ea71503de71d0f539ee3cce6b1fb3106ec49a580cdf012\",\n        0\n    ],\n    [\n        \"00000000c8cd6b15785e1789435a1fae16fc4b6528144c5d49c5b071dd16b0dd\",\n        0\n    ],\n    [\n        \"0000000000001837a16eae4f4d934fba9bd58abee864ee10c1a6768f13e276ba\",\n        0\n    ],\n    [\n        \"0000000000003559bab6ad82347a5bcd62b07572a7de146234a373b9d09b5bc8\",\n        0\n    ],\n    [\n        \"00000000000020cbeb38ca68af925b736cd6a0694a8152b5ea9c6b06af972e9d\",\n        0\n    ],\n    [\n        \"00000000000886977197c1d7ce749af9951e7cb08c448eb1c1034509048793b7\",\n        0\n    ],\n    [\n        \"00000000002b307d0991eab1cc694be1eb3c607ebf1ff0fadfedbb038baabac6\",\n        0\n    ],\n    [\n        \"00000000000c6570eaea754f2f6075ea71a68b46c90c08b271d08009eaefdfc6\",\n        0\n    ],\n    [\n        \"0000000000004a6b77b80f36d0b4c843642c472b14060c659db079488394a548\",\n        0\n    ],\n    [\n        \"00000000000026c176ab34dee51c7d6bcd101ace82b9d3a27cae9158fe4615e7\",\n        0\n    ],\n    [\n        \"00000000000008740ccd0cd6f875599e851bb50daa188da402dc2e82cbe63432\",\n        0\n    ],\n    [\n        \"000000000000027e5f3c4cc7fb4929504cf0a67192473b936ae4ffad6121eba8\",\n        0\n    ],\n    [\n        \"0000000000000528bf19fababa130f28a8082d004750ff42ca1c7f2bde6be14e\",\n        0\n    ],\n    [\n        \"000000000000014455b175ee9e2218fea02a4f64ca4219dda6cb8014c266fdc6\",\n        0\n    ]\n]"
  },
  {
    "path": "electrum/chains/testnet/fallback_lnnodes.json",
    "content": "{\n  \"038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9\": {\n    \"host\": \"203.132.95.10\",\n    \"port\": 9735\n  },\n  \"03236a685d30096b26692dce0cf0fa7c8528bdf61dbf5363a3ef6d5c92733a3016\": {\n    \"host\": \"50.116.3.223\",\n    \"port\": 9734\n  },\n  \"03d5e17a3c213fe490e1b0c389f8cfcfcea08a29717d50a9f453735e0ab2a7c003\": {\n    \"host\": \"3.16.119.191\",\n    \"port\": 9735\n  },\n  \"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134\": {\n    \"host\": \"34.250.234.192\",\n    \"port\": 9735\n  },\n  \"0260d9119979caedc570ada883ff614c6efb93f7f7382e25d73ecbeba0b62df2d7\": {\n    \"host\": \"88.99.209.230\",\n    \"port\": 9735\n  },\n  \"023ea0a53af875580899da0ab0a21455d9c19160c4ea1b7774c9d4be6810b02d2c\": {\n    \"host\": \"160.16.233.215\",\n    \"port\": 9735\n  },\n  \"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f\": {\n    \"host\": \"197.155.6.173\",\n    \"port\": 9735\n  },\n  \"030f0bf260acdbd3edcad84d7588ec7c5df4711e87e6a23016f989b8d3a4147230\": {\n    \"host\": \"163.172.94.64\",\n    \"port\": 9735\n  },\n  \"02312627fdf07fbdd7e5ddb136611bdde9b00d26821d14d94891395452f67af248\": {\n    \"host\": \"23.237.77.12\",\n    \"port\": 9735\n  },\n  \"02ae2f22b02375e3e9b4b4a2db4f12e1b50752b4062dbefd6e01332acdaf680379\": {\n    \"host\": \"197.155.6.172\",\n    \"port\": 9735\n  },\n  \"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36\": {\n    \"host\": \"23.239.23.44\",\n    \"port\": 9740\n  },\n  \"02889be42fc32093d2dcbfa59369df262e3577b333d8a45e5859dcdd6a4139839a\": {\n    \"host\": \"2a09:8280:1::42:a6f3\",\n    \"port\": 9735\n  },\n  \"021713d5331898c206b57c4f7d40635079de9a97d97782646f31dac18a53f2d979\": {\n    \"host\": \"2a09:8280:1::15:a57c\",\n    \"port\": 9735\n  }\n}"
  },
  {
    "path": "electrum/chains/testnet/servers.json",
    "content": "{\n    \"blackie.c3-soft.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"57006\",\n        \"t\": \"57005\",\n        \"version\": \"1.4.5\"\n    },\n    \"blockstream.info\": {\n        \"pruning\": \"-\",\n        \"s\": \"993\",\n        \"t\": \"143\",\n        \"version\": \"1.4\"\n    },\n    \"electrum.blockstream.info\": {\n        \"pruning\": \"-\",\n        \"s\": \"60002\",\n        \"t\": \"60001\",\n        \"version\": \"1.4\"\n    },\n    \"explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion\": {\n        \"pruning\": \"-\",\n        \"t\": \"143\",\n        \"version\": \"1.4\"\n    },\n    \"testnet.aranguren.org\": {\n        \"pruning\": \"-\",\n        \"s\": \"51002\",\n        \"t\": \"51001\",\n        \"version\": \"1.4.2\"\n    },\n    \"testnet.hsmiths.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"53012\",\n        \"version\": \"1.4.2\"\n    },\n    \"testnet.qtornado.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"51002\",\n        \"t\": \"51001\",\n        \"version\": \"1.5\"\n    },\n    \"tn.not.fyi\": {\n        \"pruning\": \"-\",\n        \"s\": \"55002\",\n        \"t\": \"55001\",\n        \"version\": \"1.4\"\n    }\n}\n"
  },
  {
    "path": "electrum/chains/testnet4/checkpoints.json",
    "content": "[\n    [\n        \"00000000962a7fc2ef639196051fe181ed53ac6aa4cdfead14dca90f58aa36bc\",\n        0\n    ],\n    [\n        \"000000002ad661157c553c0bbbb2490407adb1c8ac09f2b2a7174f87eeeb64bf\",\n        0\n    ],\n    [\n        \"000000000be3ff43cde9eed4d6b2d4ad16c4f9509ccb94e1001af68e2f6647b3\",\n        0\n    ],\n    [\n        \"00000000001ef2e4c2fc174354ed357cf313725fc336092733b2699d36342ff8\",\n        0\n    ],\n    [\n        \"000000000025269f9fa4b0832ccbfef682d59c0fa8845b0c22cc24a1973f011a\",\n        0\n    ],\n    [\n        \"000000000014b2d6b2ad804d5deb8d5b4a58caf152f6cea5600af0d9348dff29\",\n        0\n    ],\n    [\n        \"000000000003c067c302d43c9499da6e382260252a2a29caf9748ee6972d5f01\",\n        0\n    ],\n    [\n        \"000000000002180d23f15ba0b8161d9d38d03c61ab51d050c57928e1a7d98e0c\",\n        0\n    ],\n    [\n        \"000000000000ed8722220a13b09d968a59686af5fc5c1e0a86371a498209fa72\",\n        0\n    ],\n    [\n        \"0000000000003e82df3830ff7c05a58745a463a59d1097e160e47ac7aeb5323a\",\n        0\n    ],\n    [\n        \"0000000000000c3f18b9a30269c4b53dd107bacf20482e4ec660e9970999a99f\",\n        0\n    ],\n    [\n        \"00000000000002901853780dc8a63efd4d72359d8de7e14dc0398ccfc53d45cd\",\n        0\n    ],\n    [\n        \"00000000000000a6ff1615113d25eeed8554813e4994f8ef7ce96458083d14cf\",\n        0\n    ],\n    [\n        \"000000000000006e2d4fa8204c67c0986f9bb0214990b11043d0653d50755f54\",\n        0\n    ],\n    [\n        \"00000000eaf8e0ea253d833614892aed70c55e5dc4b4d6709dd6420b8284debb\",\n        0\n    ],\n    [\n        \"0000000000000063d3ca489d113ded6196c99f3785b61a8ded9254ebb96bc765\",\n        0\n    ],\n    [\n        \"000000000000003f684cab6cdb7fe6e98cb13318bb45acdc2d2e2d7405b8bcbe\",\n        0\n    ],\n    [\n        \"000000000000001f735b5a23732fb201cf6343b373c94a35f04e6b6075591889\",\n        0\n    ],\n    [\n        \"000000001c247a1eb479ecc56ea7d7529f0c4afb6b7025f437a7d235454cd6a4\",\n        0\n    ],\n    [\n        \"00000000acd1400a4801f361d675644993ad05e5b735a881f26746ece767521e\",\n        0\n    ],\n    [\n        \"00000000542792e54a720567ba66157d48cdae7bfd01c1b678d0f07a2ed56e99\",\n        0\n    ],\n    [\n        \"00000000ca301f565989627133247615bc937b52c68f8f4b342b6c2aeebff7ba\",\n        0\n    ],\n    [\n        \"00000000e4ad2ec95dfddce6a554f626c9995e465b067e72528f6ae164fc58d2\",\n        0\n    ],\n    [\n        \"00000000761cd6bff5e11258943e401e1bb094a8013e810e1d6031ce273a4b7c\",\n        0\n    ],\n    [\n        \"00000000082032b915c151f1bb9892fe924013539924a34ec8b9bdea16eb7374\",\n        0\n    ],\n    [\n        \"0000000000853161fa2a440407ce597524e85db6a27f5dffd35162b38e7627e7\",\n        0\n    ],\n    [\n        \"00000000001c7ef1bace4a08448d9e462ff8b2e8c389f16026834c5d0c97252c\",\n        0\n    ],\n    [\n        \"000000000ae11eebf9807d5ac0f9966282c59a1e613e229aa2bf7aea266d4535\",\n        0\n    ],\n    [\n        \"00000000973e9926efc6a2a11413f7125242911ae10925e3616e872307a3e401\",\n        0\n    ],\n    [\n        \"0000000000000003d66460a7e1ed89080427ac2004b2b3adae05e6e89725dc1f\",\n        0\n    ],\n    [\n        \"000000009f8fe2dbafd82a0432d69d620d0ebeea033c8a2621bf06a6e6d3b5c4\",\n        0\n    ],\n    [\n        \"0000000000000003ac6ce3118b61709d347142cc04daeb2c973b937904e4390b\",\n        0\n    ],\n    [\n        \"0000000000003844aab8ac81fb69b47e83c037cee505bfd5cb6522aa4d2351bc\",\n        0\n    ],\n    [\n        \"00000000000060397e5918c319db04ffac74f2a5c7724083112fca8958ea54de\",\n        0\n    ],\n    [\n        \"00000000213b70c1bcec26b90a5503960a95d7a5fa5d9de498b9d794afeaebd5\",\n        0\n    ],\n    [\n        \"0000000000000e5f474161d1b68932ab607035f5026f1ac4aa5816f24e76017a\",\n        0\n    ],\n    [\n        \"00000000b56d4faf52043bfe98e126c69fb51671ba9bcea67c191620255d291d\",\n        0\n    ],\n    [\n        \"0000000000052b94098008985919f6b525ead0d5cb5608fd60015f2647701ea1\",\n        0\n    ],\n    [\n        \"00000000e5c06639c50bdb710b84ebf58d1df666db033157db685ec592932276\",\n        0\n    ],\n    [\n        \"000000006f596242c0b5dd79b5f638e95a215252c44374133a5b263fcb5e9f89\",\n        0\n    ],\n    [\n        \"00000000a2dc7b8c63c737543dd41e5a7a529d1824c32e2d454cae998c5a7298\",\n        0\n    ],\n    [\n        \"000000008a995172d20119cc9b48a28ea4bf350711c2925849e5db760d0bde05\",\n        0\n    ],\n    [\n        \"000000000bcec41b64702945a8cc7aceb06bc51fa93528b39619180ddc03e9bd\",\n        0\n    ],\n    [\n        \"00000000003f767fd29141200c4b64fc0b224edcf28ef65f66c6345b76a60f32\",\n        0\n    ],\n    [\n        \"00000000046883bd13b616c3525b243bbf3c7a9688a66f8ce46506ec25f6e798\",\n        0\n    ],\n    [\n        \"00000000068670ee282a076d17900a07da93da50b3705405b1e7ca8616856535\",\n        0\n    ],\n    [\n        \"0000000026b1f5d8ffa89385b048ca9004b97b412eee44000810ea8177a4bef4\",\n        0\n    ],\n    [\n        \"0000000000000002e72644dda2132c93ac54edd6c155f47c51ca8cf4b918c0bf\",\n        0\n    ],\n    [\n        \"00000000000000000a20f50c208d1ae3c3397fc6059b2cf1fe6b698e42023178\",\n        0\n    ],\n    [\n        \"000000000a0b5c3e57dd4c3b751d13562a92953d794e19aff8f9e2ca6607604f\",\n        0\n    ],\n    [\n        \"0000000003b9f6443b23bf86f200ef85005dfc508564960812eb85774327e6ce\",\n        0\n    ],\n    [\n        \"000000000668f8e866d39a9ec07937e716a6035a02cfd0757ee716ac7cdca2fd\",\n        0\n    ],\n    [\n        \"00000000041c239cab44d0afc2014a1351250375e796c692298ba8a8fcdb41de\",\n        0\n    ],\n    [\n        \"0000000008c8148de5f2a96c0b82b32f8726ef7061386d370a68e078ade8e3e5\",\n        0\n    ],\n    [\n        \"00000000089663c835fe83325ffaced64ad9c924468d1e1339f400bf8ed57883\",\n        0\n    ],\n    [\n        \"00000000039229460de251e6b8b303841f76a1bc022025e8ca66a6ace20ffdf3\",\n        0\n    ],\n    [\n        \"000000006b606bff2dfa618d2975fdfc2bea491f558ebe16be2ca7a0aa1c0181\",\n        0\n    ],\n    [\n        \"0000000005dfc1e853b8d644598b77bdd97ef7578f9ccbb840aa3860fda9ddac\",\n        0\n    ],\n    [\n        \"0000000002e9de570afba91146a43378199cc3b38fd52b2f4f1989734d22084a\",\n        0\n    ],\n    [\n        \"000000000d1359300f79d95e0db59f4c14099684718838720db7569d98348844\",\n        0\n    ]\n]"
  },
  {
    "path": "electrum/chains/testnet4/servers.json",
    "content": "{\n    \"testnet4-electrumx.wakiyamap.dev\": {\n        \"pruning\": \"-\",\n        \"s\": \"51002\",\n        \"t\": \"51001\",\n        \"version\": \"1.4\"\n    },\n    \"blackie.c3-soft.com\": {\n        \"pruning\": \"-\",\n        \"s\": \"57010\",\n        \"t\": \"57009\",\n        \"version\": \"1.4\"\n    },\n    \"mempool.space\": {\n        \"pruning\": \"-\",\n        \"s\": \"40002\",\n        \"version\": \"1.4\"\n    }\n}\n"
  },
  {
    "path": "electrum/channel_db.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2018 The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport ipaddress\nimport time\nimport random\nimport os\nfrom collections import defaultdict\nfrom typing import Sequence, List, Tuple, Optional, Dict, NamedTuple, TYPE_CHECKING, Set\nimport binascii\nimport base64\nimport asyncio\nimport threading\nfrom enum import IntEnum\nimport functools\n\nfrom aiorpcx import NetAddress\nfrom electrum_ecc import ECPubkey\n\nfrom .sql_db import SqlDB, sql\nfrom . import constants, util\nfrom .util import profiler, get_headers_dir, is_ip_address, json_normalize, UserFacingException, is_private_netaddress\nfrom .lntransport import LNPeerAddr\nfrom .lnutil import (ShortChannelID, validate_features, IncompatibleOrInsaneFeatures,\n                     InvalidGossipMsg, GossipForwardingMessage, GossipTimestampFilter)\nfrom .lnverifier import LNChannelVerifier, verify_sig_for_channel_update\nfrom .lnmsg import decode_msg\nfrom .crypto import sha256d\nfrom .lnmsg import FailedToParseMsg\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .lnchannel import Channel\n    from .lnrouter import RouteEdge\n    from .simple_config import SimpleConfig\n\n\nFLAG_DISABLE   = 1 << 1\nFLAG_DIRECTION = 1 << 0\n\n\nclass ChannelDBNotLoaded(UserFacingException): pass\n\n\nclass ChannelInfo(NamedTuple):\n    short_channel_id: ShortChannelID\n    node1_id: bytes\n    node2_id: bytes\n    capacity_sat: Optional[int]\n    raw: Optional[bytes] = None\n\n    @staticmethod\n    def from_msg(payload: dict) -> 'ChannelInfo':\n        features = int.from_bytes(payload['features'], 'big')\n        features = validate_features(features)\n        channel_id = payload['short_channel_id']\n        node_id_1 = payload['node_id_1']\n        node_id_2 = payload['node_id_2']\n        assert list(sorted([node_id_1, node_id_2])) == [node_id_1, node_id_2]\n        capacity_sat = None\n        return ChannelInfo(\n            short_channel_id = ShortChannelID.normalize(channel_id),\n            node1_id = node_id_1,\n            node2_id = node_id_2,\n            capacity_sat = capacity_sat,\n            raw = payload.get('raw')\n        )\n\n    @staticmethod\n    def from_raw_msg(raw: bytes) -> 'ChannelInfo':\n        payload_dict = decode_msg(raw)[1]\n        payload_dict['raw'] = raw\n        return ChannelInfo.from_msg(payload_dict)\n\n    @staticmethod\n    def from_route_edge(route_edge: 'RouteEdge') -> 'ChannelInfo':\n        node1_id, node2_id = sorted([route_edge.start_node, route_edge.end_node])\n        return ChannelInfo(\n            short_channel_id=route_edge.short_channel_id,\n            node1_id=node1_id,\n            node2_id=node2_id,\n            capacity_sat=None,\n        )\n\n\nclass Policy(NamedTuple):\n    key: bytes\n    cltv_delta: int\n    htlc_minimum_msat: int\n    htlc_maximum_msat: Optional[int]\n    fee_base_msat: int\n    fee_proportional_millionths: int\n    channel_flags: int\n    message_flags: int\n    timestamp: int\n    raw: Optional[bytes] = None\n\n    @staticmethod\n    def from_msg(payload: dict) -> 'Policy':\n        return Policy(\n            key                         = payload['short_channel_id'] + payload['start_node'],\n            cltv_delta                  = payload['cltv_expiry_delta'],\n            htlc_minimum_msat           = payload['htlc_minimum_msat'],\n            htlc_maximum_msat           = payload.get('htlc_maximum_msat', None),\n            fee_base_msat               = payload['fee_base_msat'],\n            fee_proportional_millionths = payload['fee_proportional_millionths'],\n            message_flags               = int.from_bytes(payload['message_flags'], \"big\"),\n            channel_flags               = int.from_bytes(payload['channel_flags'], \"big\"),\n            timestamp                   = payload['timestamp'],\n            raw                         = payload.get('raw'),\n        )\n\n    @staticmethod\n    def from_raw_msg(key: bytes, raw: bytes) -> 'Policy':\n        payload = decode_msg(raw)[1]\n        payload['start_node'] = key[8:]\n        payload['raw'] = raw\n        return Policy.from_msg(payload)\n\n    @staticmethod\n    def from_route_edge(route_edge: 'RouteEdge') -> 'Policy':\n        return Policy(\n            key=route_edge.short_channel_id + route_edge.start_node,\n            cltv_delta=route_edge.cltv_delta,\n            htlc_minimum_msat=0,\n            htlc_maximum_msat=None,\n            fee_base_msat=route_edge.fee_base_msat,\n            fee_proportional_millionths=route_edge.fee_proportional_millionths,\n            channel_flags=0,\n            message_flags=0,\n            timestamp=0,\n        )\n\n    def is_disabled(self):\n        return self.channel_flags & FLAG_DISABLE\n\n    @property\n    def short_channel_id(self) -> ShortChannelID:\n        return ShortChannelID.normalize(self.key[0:8])\n\n    @property\n    def start_node(self) -> bytes:\n        return self.key[8:]\n\n\nclass NodeInfo(NamedTuple):\n    node_id: bytes\n    features: int\n    timestamp: int\n    alias: str\n    raw: Optional[bytes]\n\n    @staticmethod\n    def from_msg(payload) -> Tuple['NodeInfo', Sequence['LNPeerAddr']]:\n        node_id = payload['node_id']\n        features = int.from_bytes(payload['features'], \"big\")\n        features = validate_features(features)\n        addresses = NodeInfo.parse_addresses_field(payload['addresses'])\n        peer_addrs = []\n        for host, port in addresses:\n            try:\n                peer_addrs.append(LNPeerAddr(host=host, port=port, pubkey=node_id))\n            except ValueError:\n                pass\n        alias = payload['alias'].rstrip(b'\\x00')\n        try:\n            alias = alias.decode('utf8')\n        except Exception:\n            alias = ''\n        timestamp = payload['timestamp']\n        node_info = NodeInfo(\n            node_id=node_id,\n            features=features,\n            timestamp=timestamp,\n            alias=alias,\n            raw=payload.get('raw'))\n        return node_info, peer_addrs\n\n    @staticmethod\n    def from_raw_msg(raw: bytes) -> Tuple['NodeInfo', Sequence['LNPeerAddr']]:\n        payload_dict = decode_msg(raw)[1]\n        payload_dict['raw'] = raw\n        return NodeInfo.from_msg(payload_dict)\n\n    @staticmethod\n    def to_addresses_field(hostname: str, port: int) -> bytes:\n        \"\"\"Encodes a hostname/port pair into a BOLT-7 'addresses' field.\"\"\"\n        if (NodeInfo.invalid_announcement_hostname(hostname)\n                or port is None or port <= 0 or port > 65535):\n            return b''\n        port_bytes = port.to_bytes(2, 'big')\n        if is_ip_address(hostname):  # ipv4 or ipv6\n            ip_addr = ipaddress.ip_address(hostname)\n            if ip_addr.version == 4:\n                return b'\\x01' + ip_addr.packed + port_bytes\n            elif ip_addr.version == 6:\n                return b'\\x02' + ip_addr.packed + port_bytes\n            return b''\n        elif hostname.endswith('.onion'):  # Tor onion v3\n            onion_addr: bytes = base64.b32decode(hostname[:-6], casefold=True)\n            return b'\\x04' + onion_addr + port_bytes\n        else:\n            try:\n                hostname_ascii: bytes = hostname.encode('ascii')\n            except UnicodeEncodeError:\n                # encoding single characters to punycode (according to spec) doesn't make sense\n                # as you can't differentiate them from regular ascii? encoding the whole string to punycode\n                # doesn't work either as the receiver would interpret it as regular ascii.\n                # hostname_ascii: bytes = hostname.encode('punycode')\n                return b''\n            if len(hostname_ascii) + 3 > 258:  # + 1 byte for length and 2 for port\n                return b''  # too long\n            return b'\\x05' + len(hostname_ascii).to_bytes(1, \"big\") + hostname_ascii + port_bytes\n\n    @staticmethod\n    def invalid_announcement_hostname(hostname: Optional[str]) -> bool:\n        \"\"\"Returns True if hostname unsuited for publishing in a NodeAnnouncement.\"\"\"\n        if (hostname is None or hostname == \"\"\n                or is_private_netaddress(hostname)\n                or hostname.startswith(\"http://\")  # not catching 'http' due to onion addresses\n                or hostname.startswith(\"https://\")):\n            return True\n        if hostname.endswith('.onion'):\n            if len(hostname) != 62:  # not an onion v3 link (probably onion v2)\n                return True\n        return False\n\n    @staticmethod\n    def parse_addresses_field(addresses_field):\n        buf = addresses_field\n\n        def read(n):\n            nonlocal buf\n            data, buf = buf[0:n], buf[n:]\n            return data\n\n        addresses = []\n        while buf:\n            atype = ord(read(1))\n            if atype == 0:\n                pass\n            elif atype == 1:  # IPv4\n                ipv4_addr = '.'.join(map(lambda x: '%d' % x, read(4)))\n                port = int.from_bytes(read(2), 'big')\n                if is_ip_address(ipv4_addr) and port != 0:\n                    addresses.append((ipv4_addr, port))\n            elif atype == 2:  # IPv6\n                ipv6_addr = b':'.join([binascii.hexlify(read(2)) for i in range(8)])\n                ipv6_addr = ipv6_addr.decode('ascii')\n                port = int.from_bytes(read(2), 'big')\n                if is_ip_address(ipv6_addr) and port != 0:\n                    addresses.append((ipv6_addr, port))\n            elif atype == 3:  # onion v2\n                read(12)  # we skip onion v2 as it is deprecated\n            elif atype == 4:  # onion v3\n                host = base64.b32encode(read(35)) + b'.onion'\n                host = host.decode('ascii').lower()\n                port = int.from_bytes(read(2), 'big')\n                addresses.append((host, port))\n            elif atype == 5:  # dns hostname\n                len_hostname = int.from_bytes(read(1), 'big')\n                host = read(len_hostname).decode('ascii')\n                port = int.from_bytes(read(2), 'big')\n                if not NodeInfo.invalid_announcement_hostname(host) and port > 0:\n                    addresses.append((host, port))\n            else:\n                # unknown address type\n                # we don't know how long it is -> have to escape\n                # if there are other addresses we could have parsed later, they are lost.\n                break\n        return addresses\n\n\nclass UpdateStatus(IntEnum):\n    ORPHANED   = 0\n    EXPIRED    = 1\n    DEPRECATED = 2\n    UNCHANGED  = 3\n    GOOD       = 4\n\n\nclass CategorizedChannelUpdates(NamedTuple):\n    orphaned: List    # no channel announcement for channel update\n    expired: List     # update older than two weeks\n    deprecated: List  # update older than database entry\n    unchanged: List   # unchanged policies\n    good: List        # good updates\n\n\ndef get_mychannel_info(short_channel_id: ShortChannelID,\n                       my_channels: Dict[ShortChannelID, 'Channel']) -> Optional[ChannelInfo]:\n    chan = my_channels.get(short_channel_id)\n    if not chan:\n        return\n    raw_msg, _ = chan.construct_channel_announcement_without_sigs()\n    ci = ChannelInfo.from_raw_msg(raw_msg)\n    return ci._replace(capacity_sat=chan.constraints.capacity)\n\n\ndef get_mychannel_policy(short_channel_id: bytes, node_id: bytes,\n                         my_channels: Dict[ShortChannelID, 'Channel']) -> Optional[Policy]:\n    chan = my_channels.get(short_channel_id)  # type: Optional[Channel]\n    if not chan:\n        return\n    if node_id == chan.node_id:  # incoming direction (to us)\n        remote_update_raw = chan.get_remote_update()\n        if not remote_update_raw:\n            return\n        now = int(time.time())\n        remote_update_decoded = decode_msg(remote_update_raw)[1]\n        remote_update_decoded['timestamp'] = now\n        remote_update_decoded['start_node'] = node_id\n        return Policy.from_msg(remote_update_decoded)\n    elif node_id == chan.get_local_pubkey():  # outgoing direction (from us)\n        local_update_decoded = decode_msg(chan.get_outgoing_gossip_channel_update())[1]\n        local_update_decoded['start_node'] = node_id\n        return Policy.from_msg(local_update_decoded)\n\n\nclass _LoadDataAborted(Exception): pass\n\n\ncreate_channel_info = \"\"\"\nCREATE TABLE IF NOT EXISTS channel_info (\nshort_channel_id BLOB(8),\nmsg BLOB,\nPRIMARY KEY(short_channel_id)\n)\"\"\"\n\ncreate_policy = \"\"\"\nCREATE TABLE IF NOT EXISTS policy (\nkey BLOB(41),\nmsg BLOB,\nPRIMARY KEY(key)\n)\"\"\"\n\ncreate_address = \"\"\"\nCREATE TABLE IF NOT EXISTS address (\nnode_id BLOB(33),\nhost STRING(256),\nport INTEGER NOT NULL,\ntimestamp INTEGER,\nPRIMARY KEY(node_id, host, port)\n)\"\"\"\n\ncreate_node_info = \"\"\"\nCREATE TABLE IF NOT EXISTS node_info (\nnode_id BLOB(33),\nmsg BLOB,\nPRIMARY KEY(node_id)\n)\"\"\"\n\n\nclass ChannelDB(SqlDB):\n\n    NUM_MAX_RECENT_PEERS = 20\n    PRIVATE_CHAN_UPD_CACHE_TTL_NORMAL = 600\n    PRIVATE_CHAN_UPD_CACHE_TTL_SHORT = 120\n\n    def __init__(self, network: 'Network'):\n        path = self.get_file_path(network.config)\n        super().__init__(network.asyncio_loop, path, commit_interval=100)\n        self.lock = threading.RLock()\n        self.num_nodes = 0\n        self.num_channels = 0\n        self.num_policies = 0\n        self._channel_updates_for_private_channels = {}  # type: Dict[Tuple[bytes, bytes], Tuple[dict, int]]\n        # note: ^ we could maybe move this cache into PaySession instead of being global.\n        #       That would only make sense though if PaySessions were never too short\n        #       (e.g. consider trampoline forwarding).\n        self.ca_verifier = LNChannelVerifier(network, self)\n\n        # initialized in load_data\n        # note: modify/iterate needs self.lock\n        self._channels = {}  # type: Dict[ShortChannelID, ChannelInfo]\n        self._policies = {}  # type: Dict[Tuple[bytes, ShortChannelID], Policy]  # (node_id, scid) -> Policy\n        self._nodes = {}  # type: Dict[bytes, NodeInfo]  # node_id -> NodeInfo\n        # node_id -> NetAddress -> timestamp\n        self._addresses = defaultdict(dict)  # type: Dict[bytes, Dict[NetAddress, int]]\n        self._channels_for_node = defaultdict(set)  # type: Dict[bytes, Set[ShortChannelID]]\n        self._recent_peers = []  # type: List[bytes]  # list of node_ids\n        self._chans_with_0_policies = set()  # type: Set[ShortChannelID]\n        self._chans_with_1_policies = set()  # type: Set[ShortChannelID]\n        self._chans_with_2_policies = set()  # type: Set[ShortChannelID]\n\n        self.forwarding_lock = threading.RLock()\n        self.fwd_channels = []  # type: List[GossipForwardingMessage]\n        self.fwd_orphan_channels = [] # type: List[GossipForwardingMessage]\n        self.fwd_channel_updates = []  # type: List[GossipForwardingMessage]\n        self.fwd_node_announcements = []  # type: List[GossipForwardingMessage]\n\n        self.data_loaded = asyncio.Event()\n        self.network = network # only for callback\n\n    @classmethod\n    def get_file_path(cls, config: 'SimpleConfig') -> str:\n        return os.path.join(get_headers_dir(config), 'gossip_db')\n\n    def update_counts(self):\n        self.num_nodes = len(self._nodes)\n        self.num_channels = len(self._channels)\n        self.num_policies = len(self._policies)\n        util.trigger_callback('channel_db', self.num_nodes, self.num_channels, self.num_policies)\n        util.trigger_callback('ln_gossip_sync_progress')\n\n    def get_channel_ids(self):\n        with self.lock:\n            return set(self._channels.keys())\n\n    def add_recent_peer(self, peer: LNPeerAddr):\n        now = int(time.time())\n        node_id = peer.pubkey\n        with self.lock:\n            self._addresses[node_id][peer.net_addr()] = now\n            # list is ordered\n            if node_id in self._recent_peers:\n                self._recent_peers.remove(node_id)\n            self._recent_peers.insert(0, node_id)\n            self._recent_peers = self._recent_peers[:self.NUM_MAX_RECENT_PEERS]\n        self._db_save_node_address(peer, now)\n\n    def get_200_randomly_sorted_nodes_not_in(self, node_ids):\n        with self.lock:\n            unshuffled = set(self._nodes.keys()) - node_ids\n        return random.sample(list(unshuffled), min(200, len(unshuffled)))\n\n    def get_last_good_address(self, node_id: bytes) -> Optional[LNPeerAddr]:\n        \"\"\"Returns latest address we successfully connected to, for given node.\"\"\"\n        addr_to_ts = self._addresses.get(node_id)\n        if not addr_to_ts:\n            return None\n        addr = sorted(list(addr_to_ts), key=lambda a: addr_to_ts[a], reverse=True)[0]\n        try:\n            return LNPeerAddr(str(addr.host), addr.port, node_id)\n        except ValueError:\n            return None\n\n    def get_recent_peers(self):\n        if not self.data_loaded.is_set():\n            raise ChannelDBNotLoaded(\"channelDB data not loaded yet!\")\n        with self.lock:\n            ret = [self.get_last_good_address(node_id)\n                   for node_id in self._recent_peers]\n            return ret\n\n    # note: currently channel announcements are trusted by default (trusted=True);\n    #       they are not SPV-verified. Verifying them would make the gossip sync\n    #       even slower; especially as servers will start throttling us.\n    #       It would probably put significant strain on servers if all clients\n    #       verified the complete gossip.\n    def add_channel_announcements(self, msg_payloads, *, trusted=True):\n        # note: signatures have already been verified.\n        if type(msg_payloads) is dict:\n            msg_payloads = [msg_payloads]\n        added = 0\n        for msg in msg_payloads:\n            short_channel_id = ShortChannelID(msg['short_channel_id'])\n            if short_channel_id in self._channels:\n                continue\n            if constants.net.rev_genesis_bytes() != msg['chain_hash']:\n                self.logger.info(\"ChanAnn has unexpected chain_hash {}\".format(msg['chain_hash'].hex()))\n                continue\n            try:\n                channel_info = ChannelInfo.from_msg(msg)\n            except IncompatibleOrInsaneFeatures as e:\n                self.logger.info(f\"unknown or insane feature bits: {e!r}\")\n                continue\n            if trusted:\n                added += 1\n                self.add_verified_channel_info(msg)\n            else:\n                added += self.ca_verifier.add_new_channel_info(short_channel_id, msg)\n\n        self.update_counts()\n\n    def add_verified_channel_info(self, msg: dict, *, capacity_sat: int = None) -> None:\n        try:\n            channel_info = ChannelInfo.from_msg(msg)\n        except IncompatibleOrInsaneFeatures:\n            return\n        channel_info = channel_info._replace(capacity_sat=capacity_sat)\n        with self.lock:\n            self._channels[channel_info.short_channel_id] = channel_info\n            self._channels_for_node[channel_info.node1_id].add(channel_info.short_channel_id)\n            self._channels_for_node[channel_info.node2_id].add(channel_info.short_channel_id)\n        self._update_num_policies_for_chan(channel_info.short_channel_id)\n        if 'raw' in msg:\n            self._db_save_channel(channel_info.short_channel_id, msg['raw'])\n        with self.forwarding_lock:\n            if fwd_msg := GossipForwardingMessage.from_payload(msg):\n                self.fwd_channels.append(fwd_msg)\n\n    def policy_changed(self, old_policy: Policy, new_policy: Policy, verbose: bool) -> bool:\n        changed = False\n        if old_policy.cltv_delta != new_policy.cltv_delta:\n            changed |= True\n            if verbose:\n                self.logger.info(f'cltv_expiry_delta: {old_policy.cltv_delta} -> {new_policy.cltv_delta}')\n        if old_policy.htlc_minimum_msat != new_policy.htlc_minimum_msat:\n            changed |= True\n            if verbose:\n                self.logger.info(f'htlc_minimum_msat: {old_policy.htlc_minimum_msat} -> {new_policy.htlc_minimum_msat}')\n        if old_policy.htlc_maximum_msat != new_policy.htlc_maximum_msat:\n            changed |= True\n            if verbose:\n                self.logger.info(f'htlc_maximum_msat: {old_policy.htlc_maximum_msat} -> {new_policy.htlc_maximum_msat}')\n        if old_policy.fee_base_msat != new_policy.fee_base_msat:\n            changed |= True\n            if verbose:\n                self.logger.info(f'fee_base_msat: {old_policy.fee_base_msat} -> {new_policy.fee_base_msat}')\n        if old_policy.fee_proportional_millionths != new_policy.fee_proportional_millionths:\n            changed |= True\n            if verbose:\n                self.logger.info(f'fee_proportional_millionths: {old_policy.fee_proportional_millionths} -> {new_policy.fee_proportional_millionths}')\n        if old_policy.channel_flags != new_policy.channel_flags:\n            changed |= True\n            if verbose:\n                self.logger.info(f'channel_flags: {old_policy.channel_flags} -> {new_policy.channel_flags}')\n        if old_policy.message_flags != new_policy.message_flags:\n            changed |= True\n            if verbose:\n                self.logger.info(f'message_flags: {old_policy.message_flags} -> {new_policy.message_flags}')\n        if not changed and verbose:\n            self.logger.info(f'policy unchanged: {old_policy.timestamp} -> {new_policy.timestamp}')\n        return changed\n\n    def add_channel_update(\n            self, payload, *, max_age=None, verify=True, verbose=True) -> UpdateStatus:\n        now = int(time.time())\n        short_channel_id = ShortChannelID(payload['short_channel_id'])\n        timestamp = payload['timestamp']\n        if max_age and now - timestamp > max_age:\n            return UpdateStatus.EXPIRED\n        if timestamp - now > 60:\n            return UpdateStatus.DEPRECATED\n        channel_info = self._channels.get(short_channel_id)\n        if not channel_info:\n            return UpdateStatus.ORPHANED\n        flags = int.from_bytes(payload['channel_flags'], 'big')\n        direction = flags & FLAG_DIRECTION\n        start_node = channel_info.node1_id if direction == 0 else channel_info.node2_id\n        payload['start_node'] = start_node\n        # compare updates to existing database entries\n        short_channel_id = ShortChannelID(payload['short_channel_id'])\n        key = (start_node, short_channel_id)\n        old_policy = self._policies.get(key)\n        if old_policy and timestamp <= old_policy.timestamp + 60:\n            return UpdateStatus.DEPRECATED\n        if verify:\n            self.verify_channel_update(payload)\n        policy = Policy.from_msg(payload)\n        with self.lock:\n            self._policies[key] = policy\n        self._update_num_policies_for_chan(short_channel_id)\n        if 'raw' in payload:\n            self._db_save_policy(policy.key, payload['raw'])\n        if old_policy and not self.policy_changed(old_policy, policy, verbose):\n            return UpdateStatus.UNCHANGED\n        else:\n            if policy.message_flags & 0b10 == 0:  # check if its `dont_forward`\n                with self.forwarding_lock:\n                    if fwd_msg := GossipForwardingMessage.from_payload(payload):\n                        self.fwd_channel_updates.append(fwd_msg)\n            return UpdateStatus.GOOD\n\n    def add_channel_updates(self, payloads, max_age=None) -> CategorizedChannelUpdates:\n        orphaned = []\n        expired = []\n        deprecated = []\n        unchanged = []\n        good = []\n        for payload in payloads:\n            r = self.add_channel_update(payload, max_age=max_age, verbose=False, verify=True)\n            if r == UpdateStatus.ORPHANED:\n                orphaned.append(payload)\n            elif r == UpdateStatus.EXPIRED:\n                expired.append(payload)\n            elif r == UpdateStatus.DEPRECATED:\n                deprecated.append(payload)\n            elif r == UpdateStatus.UNCHANGED:\n                unchanged.append(payload)\n            elif r == UpdateStatus.GOOD:\n                good.append(payload)\n        self.update_counts()\n        return CategorizedChannelUpdates(\n            orphaned=orphaned,\n            expired=expired,\n            deprecated=deprecated,\n            unchanged=unchanged,\n            good=good)\n\n    def create_database(self):\n        c = self.conn.cursor()\n        c.execute(create_node_info)\n        c.execute(create_address)\n        c.execute(create_policy)\n        c.execute(create_channel_info)\n        self.conn.commit()\n\n    @sql\n    def _db_save_policy(self, key: bytes, msg: bytes):\n        # 'msg' is a 'channel_update' message\n        c = self.conn.cursor()\n        c.execute(\"\"\"REPLACE INTO policy (key, msg) VALUES (?,?)\"\"\", [key, msg])\n\n    @sql\n    def _db_delete_policy(self, node_id: bytes, short_channel_id: ShortChannelID):\n        key = short_channel_id + node_id\n        c = self.conn.cursor()\n        c.execute(\"\"\"DELETE FROM policy WHERE key=?\"\"\", (key,))\n\n    @sql\n    def _db_save_channel(self, short_channel_id: ShortChannelID, msg: bytes):\n        # 'msg' is a 'channel_announcement' message\n        c = self.conn.cursor()\n        c.execute(\"REPLACE INTO channel_info (short_channel_id, msg) VALUES (?,?)\", [short_channel_id, msg])\n\n    @sql\n    def _db_delete_channel(self, short_channel_id: ShortChannelID):\n        c = self.conn.cursor()\n        c.execute(\"\"\"DELETE FROM channel_info WHERE short_channel_id=?\"\"\", (short_channel_id,))\n\n    @sql\n    def _db_save_node_info(self, node_id: bytes, msg: bytes):\n        # 'msg' is a 'node_announcement' message\n        c = self.conn.cursor()\n        c.execute(\"REPLACE INTO node_info (node_id, msg) VALUES (?,?)\", [node_id, msg])\n\n    @sql\n    def _db_save_node_address(self, peer: LNPeerAddr, timestamp: int):\n        c = self.conn.cursor()\n        c.execute(\"REPLACE INTO address (node_id, host, port, timestamp) VALUES (?,?,?,?)\",\n                  (peer.pubkey, peer.host, peer.port, timestamp))\n\n    @sql\n    def _db_save_node_addresses(self, node_addresses: Sequence[LNPeerAddr]):\n        c = self.conn.cursor()\n        for addr in node_addresses:\n            c.execute(\"SELECT * FROM address WHERE node_id=? AND host=? AND port=?\", (addr.pubkey, addr.host, addr.port))\n            r = c.fetchall()\n            if r == []:\n                c.execute(\"INSERT INTO address (node_id, host, port, timestamp) VALUES (?,?,?,?)\", (addr.pubkey, addr.host, addr.port, 0))\n\n    @classmethod\n    def verify_channel_update(cls, payload, *, start_node: bytes = None) -> None:\n        short_channel_id = payload['short_channel_id']\n        short_channel_id = ShortChannelID(short_channel_id)\n        if constants.net.rev_genesis_bytes() != payload['chain_hash']:\n            raise InvalidGossipMsg('wrong chain hash')\n        start_node = payload.get('start_node', None) or start_node\n        assert start_node is not None\n        if not verify_sig_for_channel_update(payload, start_node):\n            raise InvalidGossipMsg(f'failed verifying channel update for {short_channel_id}')\n\n    @classmethod\n    def verify_channel_announcement(cls, payload) -> None:\n        h = sha256d(payload['raw'][2+256:])\n        pubkeys = [payload['node_id_1'], payload['node_id_2'], payload['bitcoin_key_1'], payload['bitcoin_key_2']]\n        sigs = [payload['node_signature_1'], payload['node_signature_2'], payload['bitcoin_signature_1'], payload['bitcoin_signature_2']]\n        for pubkey, sig in zip(pubkeys, sigs):\n            if not ECPubkey(pubkey).ecdsa_verify(sig, h):\n                raise InvalidGossipMsg('signature failed')\n\n    @classmethod\n    def verify_node_announcement(cls, payload) -> None:\n        pubkey = payload['node_id']\n        signature = payload['signature']\n        h = sha256d(payload['raw'][66:])\n        if not ECPubkey(pubkey).ecdsa_verify(signature, h):\n            raise InvalidGossipMsg('signature failed')\n\n    def add_node_announcements(self, msg_payloads):\n        # note: signatures have already been verified.\n        if type(msg_payloads) is dict:\n            msg_payloads = [msg_payloads]\n        new_nodes = set()  # type: Set[bytes]\n        for msg_payload in msg_payloads:\n            try:\n                node_info, node_addresses = NodeInfo.from_msg(msg_payload)\n            except IncompatibleOrInsaneFeatures:\n                continue\n            node_id = node_info.node_id\n            # Ignore node if it has no associated channel (DoS protection)\n            if node_id not in self._channels_for_node:\n                #self.logger.info('ignoring orphan node_announcement')\n                continue\n            node = self._nodes.get(node_id)\n            if node and node.timestamp >= node_info.timestamp:\n                continue\n            new_nodes.add(node_id)\n            # save\n            with self.lock:\n                self._nodes[node_id] = node_info\n            if 'raw' in msg_payload:\n                self._db_save_node_info(node_id, msg_payload['raw'])\n            with self.lock:\n                for addr in node_addresses:\n                    net_addr = NetAddress(addr.host, addr.port)\n                    self._addresses[node_id][net_addr] = self._addresses[node_id].get(net_addr) or 0\n            self._db_save_node_addresses(node_addresses)\n            with self.forwarding_lock:\n                if fwd_msg := GossipForwardingMessage.from_payload(msg_payload):\n                    self.fwd_node_announcements.append(fwd_msg)\n\n        self.update_counts()\n\n    def get_old_policies(self, delta) -> Sequence[Tuple[bytes, ShortChannelID]]:\n        with self.lock:\n            _policies = self._policies.copy()\n        now = int(time.time())\n        return list(k for k, v in _policies.items() if v.timestamp <= now - delta)\n\n    def prune_old_policies(self, delta):\n        old_policies = self.get_old_policies(delta)\n        if old_policies:\n            for key in old_policies:\n                node_id, scid = key\n                with self.lock:\n                    self._policies.pop(key)\n                self._db_delete_policy(*key)\n                self._update_num_policies_for_chan(scid)\n            self.update_counts()\n            self.logger.info(f'Deleting {len(old_policies)} old policies')\n\n    def prune_orphaned_channels(self):\n        with self.lock:\n            orphaned_chans = self._chans_with_0_policies.copy()\n        if orphaned_chans:\n            for short_channel_id in orphaned_chans:\n                self.remove_channel(short_channel_id)\n            self.update_counts()\n            self.logger.info(f'Deleting {len(orphaned_chans)} orphaned channels')\n\n    def _get_channel_update_for_private_channel(\n        self,\n        start_node_id: bytes,\n        short_channel_id: ShortChannelID,\n        *,\n        now: int = None,  # unix ts\n    ) -> Optional[dict]:\n        if now is None:\n            now = int(time.time())\n        key = (start_node_id, short_channel_id)\n        chan_upd_dict, cache_expiration = self._channel_updates_for_private_channels.get(key, (None, 0))\n        if cache_expiration < now:\n            chan_upd_dict = None  # already expired\n            # TODO rm expired entries from cache (note: perf vs thread-safety)\n        return chan_upd_dict\n\n    def add_channel_update_for_private_channel(\n        self,\n        msg_payload: dict,\n        start_node_id: bytes,\n        *,\n        cache_ttl: int = None,  # seconds\n    ) -> bool:\n        \"\"\"Returns True iff the channel update was successfully added and it was different than\n        what we had before (if any).\n        \"\"\"\n        if not verify_sig_for_channel_update(msg_payload, start_node_id):\n            return False  # ignore\n        now = int(time.time())\n        short_channel_id = ShortChannelID(msg_payload['short_channel_id'])\n        msg_payload['start_node'] = start_node_id\n        prev_chanupd = self._get_channel_update_for_private_channel(start_node_id, short_channel_id, now=now)\n        if prev_chanupd == msg_payload:\n            return False\n        if cache_ttl is None:\n            cache_ttl = self.PRIVATE_CHAN_UPD_CACHE_TTL_NORMAL\n        cache_expiration = now + cache_ttl\n        key = (start_node_id, short_channel_id)\n        with self.lock:\n            self._channel_updates_for_private_channels[key] = msg_payload, cache_expiration\n        return True\n\n    def remove_channel(self, short_channel_id: ShortChannelID):\n        # FIXME what about rm-ing policies?\n        with self.lock:\n            channel_info = self._channels.pop(short_channel_id, None)\n            if channel_info:\n                self._channels_for_node[channel_info.node1_id].remove(channel_info.short_channel_id)\n                self._channels_for_node[channel_info.node2_id].remove(channel_info.short_channel_id)\n        self._update_num_policies_for_chan(short_channel_id)\n        # delete from database\n        self._db_delete_channel(short_channel_id)\n\n    def get_node_addresses(self, node_id: bytes) -> Sequence[Tuple[str, int, int]]:\n        \"\"\"Returns list of (host, port, timestamp).\"\"\"\n        addr_to_ts = self._addresses.get(node_id)\n        if not addr_to_ts:\n            return []\n        return [(str(net_addr.host), net_addr.port, ts)\n                for net_addr, ts in addr_to_ts.items()]\n\n    def handle_abort(func):\n        @functools.wraps(func)\n        def wrapper(self: 'ChannelDB', *args, **kwargs):\n            try:\n                return func(self, *args, **kwargs)\n            except _LoadDataAborted:\n                return\n        return wrapper\n\n    @sql\n    @profiler\n    @handle_abort\n    def load_data(self):\n        if self.data_loaded.is_set():\n            return\n\n        # Note: this method takes several seconds... mostly due to lnmsg.decode_msg being slow.\n        def maybe_abort():\n            if self.stopping:\n                self.logger.info(\"load_data() was asked to stop. exiting early.\")\n                raise _LoadDataAborted()\n        c = self.conn.cursor()\n        c.execute(\"\"\"SELECT * FROM address\"\"\")\n        for x in c:\n            maybe_abort()\n            node_id, host, port, timestamp = x\n            try:\n                net_addr = NetAddress(host, port)\n            except Exception:\n                continue\n            self._addresses[node_id][net_addr] = int(timestamp or 0)\n\n        def newest_ts_for_node_id(node_id):\n            newest_ts = 0\n            for addr, ts in self._addresses[node_id].items():\n                newest_ts = max(newest_ts, ts)\n            return newest_ts\n        sorted_node_ids = sorted(self._addresses.keys(), key=newest_ts_for_node_id, reverse=True)\n        self._recent_peers = sorted_node_ids[:self.NUM_MAX_RECENT_PEERS]\n        c.execute(\"\"\"SELECT * FROM channel_info\"\"\")\n        for short_channel_id, msg in c:\n            maybe_abort()\n            try:\n                ci = ChannelInfo.from_raw_msg(msg)\n            except IncompatibleOrInsaneFeatures:\n                continue\n            except FailedToParseMsg:\n                continue\n            self._channels[ShortChannelID.normalize(short_channel_id)] = ci\n        c.execute(\"\"\"SELECT * FROM node_info\"\"\")\n        for node_id, msg in c:\n            maybe_abort()\n            try:\n                node_info, node_addresses = NodeInfo.from_raw_msg(msg)\n            except IncompatibleOrInsaneFeatures:\n                continue\n            except FailedToParseMsg:\n                continue\n            # don't load node_addresses because they dont have timestamps\n            self._nodes[node_id] = node_info\n        c.execute(\"\"\"SELECT * FROM policy\"\"\")\n        for key, msg in c:\n            maybe_abort()\n            try:\n                p = Policy.from_raw_msg(key, msg)\n            except FailedToParseMsg:\n                continue\n            self._policies[(p.start_node, p.short_channel_id)] = p\n        for channel_info in self._channels.values():\n            self._channels_for_node[channel_info.node1_id].add(channel_info.short_channel_id)\n            self._channels_for_node[channel_info.node2_id].add(channel_info.short_channel_id)\n            self._update_num_policies_for_chan(channel_info.short_channel_id)\n        self.logger.info(f'data loaded. {len(self._channels)} chans. {len(self._policies)} policies. '\n                         f'{len(self._channels_for_node)} nodes.')\n        self.update_counts()\n        (nchans_with_0p, nchans_with_1p, nchans_with_2p) = self.get_num_channels_partitioned_by_policy_count()\n        self.logger.info(f'num_channels_partitioned_by_policy_count. '\n                         f'0p: {nchans_with_0p}, 1p: {nchans_with_1p}, 2p: {nchans_with_2p}')\n        self.asyncio_loop.call_soon_threadsafe(self.data_loaded.set)\n        util.trigger_callback('gossip_db_loaded')\n\n    def _update_num_policies_for_chan(self, short_channel_id: ShortChannelID) -> None:\n        channel_info = self.get_channel_info(short_channel_id)\n        if channel_info is None:\n            with self.lock:\n                self._chans_with_0_policies.discard(short_channel_id)\n                self._chans_with_1_policies.discard(short_channel_id)\n                self._chans_with_2_policies.discard(short_channel_id)\n            return\n        p1 = self.get_policy_for_node(short_channel_id, channel_info.node1_id)\n        p2 = self.get_policy_for_node(short_channel_id, channel_info.node2_id)\n        with self.lock:\n            self._chans_with_0_policies.discard(short_channel_id)\n            self._chans_with_1_policies.discard(short_channel_id)\n            self._chans_with_2_policies.discard(short_channel_id)\n            if p1 is not None and p2 is not None:\n                self._chans_with_2_policies.add(short_channel_id)\n            elif p1 is None and p2 is None:\n                self._chans_with_0_policies.add(short_channel_id)\n            else:\n                self._chans_with_1_policies.add(short_channel_id)\n\n    def get_num_channels_partitioned_by_policy_count(self) -> Tuple[int, int, int]:\n        nchans_with_0p = len(self._chans_with_0_policies)\n        nchans_with_1p = len(self._chans_with_1_policies)\n        nchans_with_2p = len(self._chans_with_2_policies)\n        return nchans_with_0p, nchans_with_1p, nchans_with_2p\n\n    def get_policy_for_node(\n            self,\n            short_channel_id: ShortChannelID,\n            node_id: bytes,\n            *,\n            my_channels: Dict[ShortChannelID, 'Channel'] = None,\n            private_route_edges: Dict[ShortChannelID, 'RouteEdge'] = None,\n            now: int = None,  # unix ts\n    ) -> Optional['Policy']:\n        channel_info = self.get_channel_info(short_channel_id)\n        if channel_info is not None:  # publicly announced channel\n            policy = self._policies.get((node_id, short_channel_id))\n            if policy:\n                return policy\n        elif chan_upd_dict := self._get_channel_update_for_private_channel(node_id, short_channel_id, now=now):\n            return Policy.from_msg(chan_upd_dict)\n        # check if it's one of our own channels\n        if my_channels:\n            policy = get_mychannel_policy(short_channel_id, node_id, my_channels)\n            if policy:\n                return policy\n        if private_route_edges:\n            route_edge = private_route_edges.get(short_channel_id, None)\n            if route_edge:\n                return Policy.from_route_edge(route_edge)\n\n    def get_channel_info(\n            self,\n            short_channel_id: ShortChannelID,\n            *,\n            my_channels: Dict[ShortChannelID, 'Channel'] = None,\n            private_route_edges: Dict[ShortChannelID, 'RouteEdge'] = None,\n    ) -> Optional[ChannelInfo]:\n        ret = self._channels.get(short_channel_id)\n        if ret:\n            return ret\n        # check if it's one of our own channels\n        if my_channels:\n            channel_info = get_mychannel_info(short_channel_id, my_channels)\n            if channel_info:\n                return channel_info\n        if private_route_edges:\n            route_edge = private_route_edges.get(short_channel_id)\n            if route_edge:\n                return ChannelInfo.from_route_edge(route_edge)\n\n    def get_channels_for_node(\n            self,\n            node_id: bytes,\n            *,\n            my_channels: Dict[ShortChannelID, 'Channel'] = None,\n            private_route_edges: Dict[ShortChannelID, 'RouteEdge'] = None,\n    ) -> Set[ShortChannelID]:\n        \"\"\"Returns the set of short channel IDs where node_id is one of the channel participants.\"\"\"\n        if not self.data_loaded.is_set():\n            raise ChannelDBNotLoaded(\"channelDB data not loaded yet!\")\n        relevant_channels = self._channels_for_node.get(node_id) or set()\n        relevant_channels = set(relevant_channels)  # copy\n        # add our own channels  # TODO maybe slow?\n        if my_channels:\n            for chan in my_channels.values():\n                if node_id in (chan.node_id, chan.get_local_pubkey()):\n                    relevant_channels.add(chan.short_channel_id)\n        # add private channels  # TODO maybe slow?\n        if private_route_edges:\n            for route_edge in private_route_edges.values():\n                if node_id in (route_edge.start_node, route_edge.end_node):\n                    relevant_channels.add(route_edge.short_channel_id)\n        return relevant_channels\n\n    def get_endnodes_for_chan(self, short_channel_id: ShortChannelID, *,\n                              my_channels: Dict[ShortChannelID, 'Channel'] = None) -> Optional[Tuple[bytes, bytes]]:\n        channel_info = self.get_channel_info(short_channel_id)\n        if channel_info is not None:  # publicly announced channel\n            return channel_info.node1_id, channel_info.node2_id\n        # check if it's one of our own channels\n        if not my_channels:\n            return\n        chan = my_channels.get(short_channel_id)  # type: Optional[Channel]\n        if not chan:\n            return\n        return chan.get_local_pubkey(), chan.node_id\n\n    def get_node_info_for_node_id(self, node_id: bytes) -> Optional['NodeInfo']:\n        return self._nodes.get(node_id)\n\n    def get_node_infos(self) -> Dict[bytes, NodeInfo]:\n        with self.lock:\n            return self._nodes.copy()\n\n    def get_node_policies(self) -> Dict[Tuple[bytes, ShortChannelID], Policy]:\n        with self.lock:\n            return self._policies.copy()\n\n    def get_node_by_prefix(self, prefix):\n        with self.lock:\n            for k in self._addresses.keys():\n                if k.startswith(prefix):\n                    return k\n        raise Exception('node not found')\n\n    def clear_forwarding_gossip(self) -> None:\n        with self.forwarding_lock:\n            self.fwd_channels.clear()\n            self.fwd_channel_updates.clear()\n            self.fwd_node_announcements.clear()\n\n    def filter_orphan_channel_anns(\n        self, channel_anns: List[GossipForwardingMessage]\n    ) -> Tuple[List, List]:\n        \"\"\"Check if the channel announcements we want to forward have at least 1 update\"\"\"\n        to_forward_anns = []\n        orphaned_channel_anns = []\n        for channel in channel_anns:\n            if channel.scid is None:\n                continue\n            elif (channel.scid in self._chans_with_1_policies\n                  or channel.scid in self._chans_with_2_policies):\n                to_forward_anns.append(channel)\n                continue\n            orphaned_channel_anns.append(channel)\n        return to_forward_anns, orphaned_channel_anns\n\n    def set_fwd_channel_anns_ts(self, channel_anns: List[GossipForwardingMessage]) \\\n        -> List[GossipForwardingMessage]:\n        \"\"\"Set the timestamps of the passed channel announcements from the corresponding policies\"\"\"\n        timestamped_chan_anns: List[GossipForwardingMessage] = []\n        with self.lock:\n            policies = self._policies.copy()\n            channels = self._channels.copy()\n\n        for chan_ann in channel_anns:\n            if chan_ann.timestamp is not None:\n                timestamped_chan_anns.append(chan_ann)\n                continue\n\n            scid = chan_ann.scid\n            if (channel_info := channels.get(scid)) is None:\n                continue\n\n            policy1 = policies.get((channel_info.node1_id, scid))\n            policy2 = policies.get((channel_info.node2_id, scid))\n            potential_timestamps = []\n            for policy in [policy1, policy2]:\n                if policy is not None:\n                    potential_timestamps.append(policy.timestamp)\n            if not potential_timestamps:\n                continue\n            chan_ann.timestamp = min(potential_timestamps)\n            timestamped_chan_anns.append(chan_ann)\n        return timestamped_chan_anns\n\n    def get_forwarding_gossip_batch(self) -> List[GossipForwardingMessage]:\n        with self.forwarding_lock:\n            fwd_gossip = self.fwd_channel_updates + self.fwd_node_announcements\n            channel_anns = self.fwd_channels.copy()\n            self.clear_forwarding_gossip()\n\n        fwd_chan_anns1, _ = self.filter_orphan_channel_anns(self.fwd_orphan_channels)\n        fwd_chan_anns2, self.fwd_orphan_channels = self.filter_orphan_channel_anns(channel_anns)\n        channel_anns = self.set_fwd_channel_anns_ts(fwd_chan_anns1 + fwd_chan_anns2)\n        return channel_anns + fwd_gossip\n\n    def get_gossip_in_timespan(self, timespan: GossipTimestampFilter) \\\n        -> List[GossipForwardingMessage]:\n        \"\"\"Return a list of gossip messages matching the requested timespan.\"\"\"\n        forwarding_gossip = []\n        with self.lock:\n            chans = self._channels.copy()\n            policies = self._policies.copy()\n            nodes = self._nodes.copy()\n\n        for short_id, chan in chans.items():\n            # fetching the timestamp from the channel update (according to BOLT-07)\n            chan_up_n1 = policies.get((chan.node1_id, short_id))\n            chan_up_n2 = policies.get((chan.node2_id, short_id))\n            updates = []\n            for policy in [chan_up_n1, chan_up_n2]:\n                if policy and policy.raw and timespan.in_range(policy.timestamp):\n                    if policy.message_flags & 0b10 == 0:  # check that its not \"dont_forward\"\n                        updates.append(GossipForwardingMessage(\n                            msg=policy.raw,\n                            timestamp=policy.timestamp))\n            if not updates or chan.raw is None:\n                continue\n            chan_ann_ts = min(update.timestamp for update in updates)\n            channel_announcement = GossipForwardingMessage(msg=chan.raw, timestamp=chan_ann_ts)\n            forwarding_gossip.extend([channel_announcement] + updates)\n\n        for node_ann in nodes.values():\n            if timespan.in_range(node_ann.timestamp) and node_ann.raw:\n                forwarding_gossip.append(GossipForwardingMessage(\n                    msg=node_ann.raw,\n                    timestamp=node_ann.timestamp))\n        return forwarding_gossip\n\n    def get_channels_in_range(self, first_blocknum: int, number_of_blocks: int) -> List[ShortChannelID]:\n        with self.lock:\n            channels = self._channels.copy()\n        scids: List[ShortChannelID] = []\n        for scid in channels:\n            if first_blocknum <= scid.block_height < first_blocknum + number_of_blocks:\n                scids.append(scid)\n        scids.sort()\n        return scids\n\n    def get_gossip_for_scid_request(self, scid: ShortChannelID) -> List[bytes]:\n        requested_gossip = []\n\n        chan_ann = self._channels.get(scid)\n        if not chan_ann or not chan_ann.raw:\n            return []\n        chan_up1 = self._policies.get((chan_ann.node1_id, scid))\n        chan_up2 = self._policies.get((chan_ann.node2_id, scid))\n        node_ann1 = self._nodes.get(chan_ann.node1_id)\n        node_ann2 = self._nodes.get(chan_ann.node2_id)\n\n        for msg in [chan_ann, chan_up1, chan_up2, node_ann1, node_ann2]:\n            if msg and msg.raw:\n                requested_gossip.append(msg.raw)\n        return requested_gossip\n\n    def to_dict(self) -> dict:\n        \"\"\" Generates a graph representation in terms of a dictionary.\n\n        The dictionary contains only native python types and can be encoded\n        to json.\n        \"\"\"\n        with self.lock:\n            graph = {'nodes': [], 'channels': []}\n\n            # gather nodes\n            for pk, nodeinfo in self._nodes.items():\n                # use _asdict() to convert NamedTuples to json encodable dicts\n                graph['nodes'].append(\n                    nodeinfo._asdict(),\n                )\n                graph['nodes'][-1]['addresses'] = [\n                    {'host': str(addr.host), 'port': addr.port, 'timestamp': ts}\n                    for addr, ts in self._addresses[pk].items()\n                ]\n\n            # gather channels\n            for cid, channelinfo in self._channels.items():\n                graph['channels'].append(\n                    channelinfo._asdict(),\n                )\n                policy1 = self._policies.get(\n                    (channelinfo.node1_id, channelinfo.short_channel_id))\n                policy2 = self._policies.get(\n                    (channelinfo.node2_id, channelinfo.short_channel_id))\n                graph['channels'][-1]['policy1'] = policy1._asdict() if policy1 else None\n                graph['channels'][-1]['policy2'] = policy2._asdict() if policy2 else None\n\n        # need to use json_normalize otherwise json encoding in rpc server fails\n        graph = json_normalize(graph)\n        return graph\n"
  },
  {
    "path": "electrum/coinchooser.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 kyuupichan@gmail\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nfrom collections import defaultdict\nfrom math import floor, log10\nfrom typing import NamedTuple, List, Callable, Sequence, Dict, Tuple, Mapping, Type, TYPE_CHECKING\nfrom decimal import Decimal\n\nfrom .bitcoin import sha256, COIN, is_address\nfrom .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput\nfrom .util import NotEnoughFunds\nfrom .logging import Logger\n\nif TYPE_CHECKING:\n    from .simple_config import SimpleConfig\n\n\n# A simple deterministic PRNG.  Used to deterministically shuffle a\n# set of coins - the same set of coins should produce the same output.\n# Although choosing UTXOs \"randomly\" we want it to be deterministic,\n# so if sending twice from the same UTXO set we choose the same UTXOs\n# to spend.  This prevents attacks on users by malicious or stale\n# servers.\nclass PRNG:\n    def __init__(self, seed):\n        self.sha = sha256(seed)\n        self.pool = bytearray()\n\n    def get_bytes(self, n: int) -> bytes:\n        while len(self.pool) < n:\n            self.pool.extend(self.sha)\n            self.sha = sha256(self.sha)\n        result, self.pool = self.pool[:n], self.pool[n:]\n        return bytes(result)\n\n    def randint(self, start, end):\n        # Returns random integer in [start, end)\n        n = end - start\n        r = 0\n        p = 1\n        while p < n:\n            r = self.get_bytes(1)[0] + (r << 8)\n            p = p << 8\n        return start + (r % n)\n\n    def choice(self, seq):\n        return seq[self.randint(0, len(seq))]\n\n    def shuffle(self, x):\n        for i in reversed(range(1, len(x))):\n            # pick an element in x[:i+1] with which to exchange x[i]\n            j = self.randint(0, i+1)\n            x[i], x[j] = x[j], x[i]\n\n\nclass Bucket(NamedTuple):\n    desc: str\n    weight: int                   # as in BIP-141\n    value: int                    # in satoshis\n    effective_value: int          # estimate of value left after subtracting fees. in satoshis\n    coins: List[PartialTxInput]   # UTXOs\n    min_height: int               # min block height where a coin was confirmed\n    witness: bool                 # whether any coin uses segwit\n\n\nclass ScoredCandidate(NamedTuple):\n    penalty: float\n    tx: PartialTransaction\n    buckets: List[Bucket]\n\n\ndef strip_unneeded(bkts: List[Bucket], sufficient_funds: Callable) -> List[Bucket]:\n    '''Remove buckets that are unnecessary in achieving the spend amount'''\n    if sufficient_funds([], bucket_value_sum=0):\n        # none of the buckets are needed\n        return []\n    bkts = sorted(bkts, key=lambda bkt: bkt.value, reverse=True)\n    bucket_value_sum = 0\n    for i in range(len(bkts)):\n        bucket_value_sum += (bkts[i]).value\n        if sufficient_funds(bkts[:i+1], bucket_value_sum=bucket_value_sum):\n            return bkts[:i+1]\n    raise Exception(\"keeping all buckets is still not enough\")\n\n\nclass CoinChooserBase(Logger):\n\n    def __init__(self, *, enable_output_value_rounding: bool):\n        Logger.__init__(self)\n        self.enable_output_value_rounding = enable_output_value_rounding\n\n    def keys(self, coins: Sequence[PartialTxInput]) -> Sequence[str]:\n        raise NotImplementedError\n\n    def bucketize_coins(\n        self,\n        coins: Sequence[PartialTxInput],\n        *,\n        fee_estimator_vb: Callable[[int | float | Decimal], int],\n    ):\n        keys = self.keys(coins)\n        buckets = defaultdict(list)  # type: Dict[str, List[PartialTxInput]]\n        for key, coin in zip(keys, coins):\n            buckets[key].append(coin)\n        # fee_estimator returns fee to be paid, for given vbytes.\n        # guess whether it is just returning a constant as follows.\n        constant_fee = fee_estimator_vb(2000) == fee_estimator_vb(200)\n\n        def make_Bucket(desc: str, coins: List[PartialTxInput]):\n            witness = any(coin.is_segwit(guess_for_address=True) for coin in coins)\n            # note that we're guessing whether the tx uses segwit based\n            # on this single bucket\n            weight = sum(Transaction.estimated_input_weight(coin, witness)\n                         for coin in coins)\n            value = sum(coin.value_sats() for coin in coins)\n            min_height = min(coin.block_height for coin in coins)\n            assert min_height is not None\n            # the fee estimator is typically either a constant or a linear function,\n            # so the \"function:\" effective_value(bucket) will be homomorphic for addition\n            # i.e. effective_value(b1) + effective_value(b2) = effective_value(b1 + b2)\n            if constant_fee:\n                effective_value = value\n            else:\n                # when converting from weight to vBytes, instead of rounding up,\n                # keep fractional part, to avoid overestimating fee\n                fee = fee_estimator_vb(Decimal(weight) / 4)\n                effective_value = value - fee\n            return Bucket(desc=desc,\n                          weight=weight,\n                          value=value,\n                          effective_value=effective_value,\n                          coins=coins,\n                          min_height=min_height,\n                          witness=witness)\n\n        return list(map(make_Bucket, buckets.keys(), buckets.values()))\n\n    def penalty_func(\n        self,\n        base_tx: Transaction,\n        *,\n        tx_from_buckets: Callable[[List[Bucket]], Tuple[PartialTransaction, List[PartialTxOutput]]],\n    ) -> Callable[[List[Bucket]], ScoredCandidate]:\n        raise NotImplementedError\n\n    def _change_amounts(self, tx: PartialTransaction, count: int, fee_estimator_numchange) -> List[int]:\n        # Break change up if bigger than max_change\n        output_amounts = [o.value for o in tx.outputs()]\n        # Don't split change of less than 0.02 BTC\n        max_change = max([0.02 * COIN] + output_amounts) * 1.25\n\n        # Use N change outputs\n        for n in range(1, count + 1):\n            # How much is left if we add this many change outputs?\n            change_amount = max(0, tx.get_fee() - fee_estimator_numchange(n))\n            if change_amount // n <= max_change:\n                break\n\n        # Get a handle on the precision of the output amounts; round our\n        # change to look similar\n        def trailing_zeroes(val):\n            s = str(val)\n            return len(s) - len(s.rstrip('0'))\n\n        zeroes = [trailing_zeroes(i) for i in output_amounts]\n        min_zeroes = min([8] + zeroes)\n        max_zeroes = max([0] + zeroes)\n\n        if n > 1:\n            zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1)\n        else:\n            # if there is only one change output, this will ensure that we aim\n            # to have one that is exactly as precise as the most precise output\n            zeroes = [min_zeroes]\n\n        # Calculate change; randomize it a bit if using more than 1 output\n        remaining = change_amount\n        amounts = []\n        while n > 1:\n            average = remaining / n\n            amount = self.p.randint(int(average * 0.7), int(average * 1.3))\n            precision = min(self.p.choice(zeroes), int(floor(log10(amount))))\n            amount = int(round(amount, -precision))\n            amounts.append(amount)\n            remaining -= amount\n            n -= 1\n\n        # Last change output.  Round down to maximum precision but lose\n        # no more than 10**max_dp_to_round_for_privacy\n        # e.g. a max of 2 decimal places means losing 100 satoshis to fees\n        # don't round if the fee estimator is set to 0 fixed fee, so a 0 fee tx remains a 0 fee tx\n        is_zero_fee_tx = True if fee_estimator_numchange(1) == 0 else False\n        output_value_rounding = self.enable_output_value_rounding and not is_zero_fee_tx\n        max_dp_to_round_for_privacy = 2 if output_value_rounding else 0\n        N = int(pow(10, min(max_dp_to_round_for_privacy, zeroes[0])))\n        amount = (remaining // N) * N\n        amounts.append(amount)\n\n        assert sum(amounts) <= change_amount\n\n        return amounts\n\n    def _change_outputs(self, tx: PartialTransaction, change_addrs, fee_estimator_numchange,\n                        dust_threshold) -> List[PartialTxOutput]:\n        amounts = self._change_amounts(tx, len(change_addrs), fee_estimator_numchange)\n        assert min(amounts) >= 0\n        assert len(change_addrs) >= len(amounts)\n        assert all([isinstance(amt, int) for amt in amounts])\n        # If change is above dust threshold after accounting for the\n        # size of the change output, add it to the transaction.\n        amounts = [amount for amount in amounts if amount >= dust_threshold]\n        change = [PartialTxOutput.from_address_and_value(addr, amount)\n                  for addr, amount in zip(change_addrs, amounts)]\n        for c in change:\n            c.is_change = True\n        return change\n\n    def _construct_tx_from_selected_buckets(\n            self, *, buckets: Sequence[Bucket],\n            base_tx: PartialTransaction, change_addrs,\n            fee_estimator_w, dust_threshold,\n            base_weight,\n            BIP69_sort: bool,\n    ) -> Tuple[PartialTransaction, List[PartialTxOutput]]:\n        # make a copy of base_tx so it won't get mutated\n        tx = PartialTransaction.from_io(base_tx.inputs()[:], base_tx.outputs()[:], BIP69_sort=BIP69_sort)\n\n        tx.add_inputs([coin for b in buckets for coin in b.coins], BIP69_sort=BIP69_sort)\n        tx_weight = self._get_tx_weight(buckets, base_weight=base_weight)\n\n        # change is sent back to sending address unless specified\n        if not change_addrs:\n            change_addrs = [tx.inputs()[0].address]\n            # note: this is not necessarily the final \"first input address\"\n            # because the inputs had not been sorted at this point\n            assert is_address(change_addrs[0])\n\n        # This takes a count of change outputs and returns a tx fee\n        output_weight = 4 * Transaction.estimated_output_size_for_address(change_addrs[0])\n        fee_estimator_numchange = lambda count: fee_estimator_w(tx_weight + count * output_weight)\n        change = self._change_outputs(tx, change_addrs, fee_estimator_numchange, dust_threshold)\n        tx.add_outputs(change, BIP69_sort=BIP69_sort)\n\n        return tx, change\n\n    def _get_tx_weight(self, buckets: Sequence[Bucket], *, base_weight: int) -> int:\n        \"\"\"Given a collection of buckets, return the total weight of the\n        resulting transaction.\n        base_weight is the weight of the tx that includes the fixed (non-change)\n        outputs and potentially some fixed inputs. Note that the change outputs\n        at this point are not yet known so they are NOT accounted for.\n        \"\"\"\n        total_weight = base_weight + sum(bucket.weight for bucket in buckets)\n        is_segwit_tx = any(bucket.witness for bucket in buckets)\n        if is_segwit_tx:\n            total_weight += 2  # marker and flag\n            # non-segwit inputs were previously assumed to have\n            # a witness of '' instead of '00' (hex)\n            # note that mixed legacy/segwit buckets are already ok\n            num_legacy_inputs = sum((not bucket.witness) * len(bucket.coins)\n                                    for bucket in buckets)\n            total_weight += num_legacy_inputs\n\n        return total_weight\n\n    def make_tx(\n            self, *,\n            coins: Sequence[PartialTxInput],\n            inputs: List[PartialTxInput],\n            outputs: List[PartialTxOutput],\n            change_addrs: Sequence[str],\n            fee_estimator_vb: Callable[[int | float | Decimal], int],\n            dust_threshold: int,\n            BIP69_sort: bool = True,\n    ) -> PartialTransaction:\n        \"\"\"Select unspent coins to spend to pay outputs.  If the change is\n        greater than dust_threshold (after adding the change output to\n        the transaction) it is kept, otherwise none is sent and it is\n        added to the transaction fee.\n\n        `inputs` and `outputs` are guaranteed to be a subset of the\n        inputs and outputs of the resulting transaction.\n        `coins` are further UTXOs we can choose from.\n\n        Note: fee_estimator_vb expects virtual bytes\n        \"\"\"\n        # Deterministic randomness from coins\n        utxos = [c.prevout.serialize_to_network() for c in coins]\n        self.p = PRNG(b''.join(sorted(utxos)))\n\n        assert len(outputs) > 0 or len(change_addrs) == 1, \\\n            \"sweeps with 0 outputs should not use multiple change addresses\"\n\n        # Copy the outputs so when adding change we don't modify \"outputs\"\n        base_tx = PartialTransaction.from_io(inputs[:], outputs[:], BIP69_sort=BIP69_sort)\n        input_value = base_tx.input_value()\n\n        # Weight of the transaction with no inputs and no change\n        # Note: this will use legacy tx serialization as the need for \"segwit\"\n        # would be detected from inputs. The only side effect should be that the\n        # marker and flag are excluded, which is compensated in get_tx_weight()\n        # FIXME calculation will be off by this (2 wu) in case of RBF batching\n        base_weight = base_tx.estimated_weight()\n        # by setting spent_amount = dust_threshold if there are no outputs we ensure that\n        # enough inputs are added so there is always at least a change output created\n        # as txs have to have at least 1 output according to consensus rules\n        spent_amount = base_tx.output_value() if outputs else dust_threshold\n\n        def fee_estimator_w(weight):\n            return fee_estimator_vb(Transaction.virtual_size_from_weight(weight))\n\n        def sufficient_funds(buckets: List[Bucket], *, bucket_value_sum: int) -> bool:\n            '''Given a list of buckets, return True if it has enough\n            value to pay for the transaction'''\n            # assert bucket_value_sum == sum(bucket.value for bucket in buckets)  # expensive!\n            total_input = input_value + bucket_value_sum\n            if total_input < spent_amount:  # shortcut for performance\n                return False\n            # any bitcoin tx must have at least 1 input by consensus\n            # (check we add some new UTXOs now or already have some fixed inputs)\n            if not buckets and not inputs:\n                return False\n            # note re performance: so far this was constant time\n            # what follows is linear in len(buckets)\n            total_weight = self._get_tx_weight(buckets, base_weight=base_weight)\n            return total_input >= spent_amount + fee_estimator_w(total_weight)\n\n        def tx_from_buckets(buckets):\n            return self._construct_tx_from_selected_buckets(\n                buckets=buckets,\n                base_tx=base_tx,\n                change_addrs=change_addrs,\n                fee_estimator_w=fee_estimator_w,\n                dust_threshold=dust_threshold,\n                base_weight=base_weight,\n                BIP69_sort=BIP69_sort,\n            )\n        # Collect the coins into buckets\n        all_buckets = self.bucketize_coins(coins, fee_estimator_vb=fee_estimator_vb)\n        # Filter some buckets out. Only keep those that have positive effective value.\n        # Note that this filtering is intentionally done on the bucket level\n        # instead of per-coin, as each bucket should be either fully spent or not at all.\n        # (e.g. CoinChooserPrivacy ensures that same-address coins go into one bucket)\n        all_buckets = list(filter(lambda b: b.effective_value > 0, all_buckets))\n        # Choose a subset of the buckets\n        scored_candidate = self.choose_buckets(all_buckets, sufficient_funds,\n                                               self.penalty_func(base_tx, tx_from_buckets=tx_from_buckets))\n        tx = scored_candidate.tx\n\n        self.logger.info(f\"using {len(tx.inputs())} inputs\")\n        self.logger.info(f\"using buckets: {[bucket.desc for bucket in scored_candidate.buckets]}\")\n\n        return tx\n\n    def choose_buckets(self, buckets: List[Bucket],\n                       sufficient_funds: Callable,\n                       penalty_func: Callable[[List[Bucket]], ScoredCandidate]) -> ScoredCandidate:\n        raise NotImplemented('To be subclassed')\n\n\nclass CoinChooserRandom(CoinChooserBase):\n\n    def bucket_candidates_any(\n        self,\n        buckets: List[Bucket],\n        sufficient_funds: Callable,\n    ) -> List[List[Bucket]]:\n        '''Returns a list of bucket sets.'''\n        if not buckets:\n            if sufficient_funds([], bucket_value_sum=0):\n                return [[]]\n            else:\n                raise NotEnoughFunds()\n\n        candidates = set()\n\n        # Add all singletons\n        for n, bucket in enumerate(buckets):\n            if sufficient_funds([bucket], bucket_value_sum=bucket.value):\n                candidates.add((n,))\n\n        # And now some random ones\n        attempts = min(100, (len(buckets) - 1) * 10 + 1)\n        permutation = list(range(len(buckets)))\n        for i in range(attempts):\n            # Get a random permutation of the buckets, and\n            # incrementally combine buckets until sufficient\n            self.p.shuffle(permutation)\n            bkts = []\n            bucket_value_sum = 0\n            for count, index in enumerate(permutation):\n                bucket = buckets[index]\n                bkts.append(bucket)\n                bucket_value_sum += bucket.value\n                if sufficient_funds(bkts, bucket_value_sum=bucket_value_sum):\n                    candidates.add(tuple(sorted(permutation[:count + 1])))\n                    break\n            else:\n                # note: this assumes that the effective value of any bkt is >= 0\n                raise NotEnoughFunds()\n\n        candidates = [[buckets[n] for n in c] for c in candidates]\n        return [strip_unneeded(c, sufficient_funds) for c in candidates]\n\n    def bucket_candidates_prefer_confirmed(\n        self,\n        buckets: List[Bucket],\n        sufficient_funds: Callable,\n    ) -> List[List[Bucket]]:\n        \"\"\"Returns a list of bucket sets preferring confirmed coins.\n\n        Any bucket can be:\n        1. \"confirmed\" if it only contains confirmed coins; else\n        2. \"unconfirmed\" if it does not contain coins with unconfirmed parents\n        3. other: e.g. \"unconfirmed parent\" or \"local\"\n\n        This method tries to only use buckets of type 1, and if the coins there\n        are not enough, tries to use the next type but while also selecting\n        all buckets of all previous types.\n        \"\"\"\n        conf_buckets = [bkt for bkt in buckets if bkt.min_height > 0]\n        unconf_buckets = [bkt for bkt in buckets if bkt.min_height == 0]\n        other_buckets = [bkt for bkt in buckets if bkt.min_height < 0]\n\n        bucket_sets = [conf_buckets, unconf_buckets, other_buckets]\n        already_selected_buckets = []\n        already_selected_buckets_value_sum = 0\n\n        for bkts_choose_from in bucket_sets:\n            try:\n                def sfunds(\n                    bkts: List[Bucket], *, bucket_value_sum: int,\n                    already_selected_buckets_value_sum=already_selected_buckets_value_sum,\n                    already_selected_buckets=already_selected_buckets,\n                ):\n                    bucket_value_sum += already_selected_buckets_value_sum\n                    return sufficient_funds(already_selected_buckets + bkts,\n                                            bucket_value_sum=bucket_value_sum)\n\n                candidates = self.bucket_candidates_any(bkts_choose_from, sfunds)\n                break\n            except NotEnoughFunds:\n                already_selected_buckets += bkts_choose_from\n                already_selected_buckets_value_sum += sum(bucket.value for bucket in bkts_choose_from)\n        else:\n            raise NotEnoughFunds()\n\n        candidates = [(already_selected_buckets + c) for c in candidates]\n        return [strip_unneeded(c, sufficient_funds) for c in candidates]\n\n    def choose_buckets(self, buckets, sufficient_funds, penalty_func):\n        candidates = self.bucket_candidates_prefer_confirmed(buckets, sufficient_funds)\n        scored_candidates = [penalty_func(cand) for cand in candidates]\n        winner = min(scored_candidates, key=lambda x: x.penalty)\n        self.logger.info(f\"Total number of buckets: {len(buckets)}\")\n        self.logger.info(f\"Num candidates considered: {len(candidates)}. \"\n                         f\"Winning penalty: {winner.penalty}\")\n        return winner\n\n\nclass CoinChooserPrivacy(CoinChooserRandom):\n    \"\"\"Attempts to better preserve user privacy.\n    First, if any coin is spent from a user address, all coins are.\n    Compared to spending from other addresses to make up an amount, this reduces\n    information leakage about sender holdings.  It also helps to\n    reduce blockchain UTXO bloat, and reduce future privacy loss that\n    would come from reusing that address' remaining UTXOs.\n    Second, it penalizes change that is quite different to the sent amount.\n    Third, it penalizes change that is too big.\n    \"\"\"\n\n    def keys(self, coins):\n        return [coin.scriptpubkey.hex() for coin in coins]\n\n    def penalty_func(self, base_tx, *, tx_from_buckets):\n        if _outputs := base_tx.outputs():\n            min_change = min(o.value for o in _outputs) * 0.75\n            max_change = max(o.value for o in _outputs) * 1.33\n        else:\n            min_change = 0\n            max_change = 0.02 * COIN\n\n        def penalty(buckets: List[Bucket]) -> ScoredCandidate:\n            # Penalize using many buckets (~inputs)\n            badness = len(buckets) - 1\n            tx, change_outputs = tx_from_buckets(buckets)\n            change = sum(o.value for o in change_outputs)\n            # Penalize change not roughly in output range\n            if change == 0:\n                pass  # no change is great!\n            elif change < min_change:\n                badness += (min_change - change) / (min_change + 10000)\n                # Penalize really small change; under 1 mBTC ~= using 1 more input\n                if change < COIN / 1000:\n                    badness += 1\n            elif change > max_change:\n                badness += (change - max_change) / (max_change + 10000)\n                # Penalize large change; 5 BTC excess ~= using 1 more input\n                badness += change / (COIN * 5)\n            return ScoredCandidate(badness, tx, buckets)\n\n        return penalty\n\n\nCOIN_CHOOSERS = {\n    'Privacy': CoinChooserPrivacy,\n}  # type: Mapping[str, Type[CoinChooserBase]]\n\n\ndef get_name(config: 'SimpleConfig') -> str:\n    kind = config.WALLET_COIN_CHOOSER_POLICY\n    if kind not in COIN_CHOOSERS:\n        kind = config.cv.WALLET_COIN_CHOOSER_POLICY.get_default_value()\n    return kind\n\n\ndef get_coin_chooser(config: 'SimpleConfig') -> CoinChooserBase:\n    klass = COIN_CHOOSERS[get_name(config)]\n    # note: we enable enable_output_value_rounding by default as\n    #       - for sacrificing a few satoshis\n    #       + it gives better privacy for the user re change output\n    #       + it also helps the network as a whole as fees will become noisier\n    #         (trying to counter the heuristic that \"whole integer sat/byte feerates\" are common)\n    coinchooser = klass(\n        enable_output_value_rounding=config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING,\n    )\n    return coinchooser\n"
  },
  {
    "path": "electrum/commands.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2011 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport io\nimport sys\nimport datetime\nimport time\nimport argparse\nimport json\nimport ast\nimport binascii\nimport base64\nimport asyncio\nimport inspect\nfrom asyncio import CancelledError\nfrom collections import defaultdict\nfrom functools import wraps\nfrom decimal import Decimal, InvalidOperation\nfrom typing import Optional, TYPE_CHECKING, Dict, List, Any, Union\nimport os\nimport re\n\nimport electrum_ecc as ecc\n\nfrom . import util\nfrom .lnmsg import OnionWireSerializer\nfrom .lnworker import LN_P2P_NETWORK_TIMEOUT\nfrom .logging import Logger\nfrom .onion_message import create_blinded_path, send_onion_message_to\nfrom .submarine_swaps import NostrTransport\nfrom .util import (\n    bfh, json_decode, json_normalize, is_hash256_str, is_hex_str, to_bytes, parse_max_spend, to_decimal,\n    UserFacingException, InvalidPassword\n)\nfrom . import bitcoin\nfrom .bitcoin import is_address,  hash_160, COIN\nfrom .bip32 import BIP32Node\nfrom .i18n import _\nfrom .transaction import (\n    Transaction, multisig_script, PartialTransaction, PartialTxOutput, tx_from_any, PartialTxInput, TxOutpoint,\n    convert_raw_tx_to_hex\n)\nfrom . import transaction\nfrom .invoices import Invoice, PR_PAID, PR_UNPAID, PR_EXPIRED\nfrom .synchronizer import Notifier\nfrom .wallet import (\n    Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet, BumpFeeStrategy,\n    Imported_Wallet\n)\nfrom .address_synchronizer import TX_HEIGHT_LOCAL\nfrom .mnemonic import Mnemonic\nfrom .lnutil import (channel_id_from_funding_tx, LnFeatures, SENT, RECEIVED, MIN_FINAL_CLTV_DELTA_ACCEPTED,\n                     PaymentFeeBudget, NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE)\nfrom .plugin import run_hook, DeviceMgr, Plugins\nfrom .version import ELECTRUM_VERSION\nfrom .simple_config import SimpleConfig\nfrom .fee_policy import FeePolicy, FEE_ETA_TARGETS, FEERATE_DEFAULT_RELAY\nfrom . import GuiImportError\nfrom . import crypto\nfrom . import constants\nfrom . import descriptor\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .daemon import Daemon\n    from electrum.lnworker import PaymentInfo\n\n\nknown_commands = {}  # type: Dict[str, Command]\n\n\nclass NotSynchronizedException(UserFacingException):\n    pass\n\n\ndef satoshis_or_max(amount):\n    return satoshis(amount) if not parse_max_spend(amount) else amount\n\n\ndef satoshis(amount):\n    # satoshi conversion must not be performed by the parser\n    return int(COIN*to_decimal(amount)) if amount is not None else None\n\n\ndef format_satoshis(x: Union[float, int, Decimal, None]) -> Optional[str]:\n    \"\"\"\n    input: satoshis as a Number\n    output: str formatted as bitcoin amount\n    \"\"\"\n    if x is None:\n        return None\n    return util.format_satoshis_plain(x, is_max_allowed=False)\n\n\nclass Command:\n    def __init__(self, func, name, s):\n        self.name = name\n        self.requires_network = 'n' in s  # better name would be \"requires daemon\"\n        self.requires_wallet = 'w' in s\n        self.requires_password = 'p' in s\n        self.requires_lightning = 'l' in s\n        self.parse_docstring(func.__doc__)\n        varnames = func.__code__.co_varnames[1:func.__code__.co_argcount]\n        self.defaults = func.__defaults__\n        if self.defaults:\n            n = len(self.defaults)\n            self.params = list(varnames[:-n])\n            self.options = list(varnames[-n:])\n        else:\n            self.params = list(varnames)\n            self.options = []\n            self.defaults = []\n\n        # sanity checks\n        if self.requires_password:\n            assert self.requires_wallet\n        for varname in ('wallet_path', 'wallet'):\n            if varname in varnames:\n                assert varname in self.options, f\"cmd: {self.name}: {varname} not in options {self.options}\"\n        assert not ('wallet_path' in varnames and 'wallet' in varnames)\n        if self.requires_wallet:\n            assert 'wallet' in varnames\n\n    def parse_docstring(self, docstring):\n        docstring = docstring or ''\n        docstring = docstring.strip()\n        self.description = docstring\n        self.arg_descriptions = {}\n        self.arg_types = {}\n        for x in re.finditer(r'arg:(.*?):(.*?):(.*)$', docstring, flags=re.MULTILINE):\n            self.arg_descriptions[x.group(2)] = x.group(3)\n            self.arg_types[x.group(2)] = x.group(1)\n            self.description = self.description.replace(x.group(), '')\n        self.short_description = self.description.split('.')[0]\n\n\ndef command(s):\n    def decorator(func):\n        if hasattr(func, '__wrapped__'):\n            # plugin command function\n            name = func.plugin_name + '_' + func.__name__\n            known_commands[name] = Command(func.__wrapped__, name, s)\n        else:\n            # regular command function\n            name = func.__name__\n            known_commands[name] = Command(func, name, s)\n\n        @wraps(func)\n        async def func_wrapper(*args, **kwargs):\n            cmd_runner = args[0]  # type: Commands\n            cmd = known_commands[name]  # type: Command\n            password = kwargs.get('password')\n            daemon = cmd_runner.daemon\n            if daemon:\n                if 'wallet_path' in cmd.options or cmd.requires_wallet:\n                    kwargs['wallet_path'] = daemon.config.maybe_complete_wallet_path(kwargs.get('wallet_path'))\n                if 'wallet' in cmd.options:\n                    wallet_path = kwargs.pop('wallet_path', None) # unit tests may set wallet and not wallet_path\n                    wallet = kwargs.get('wallet', None)           # run_offline_command sets both\n                    if wallet is None and wallet_path is not None:\n                        wallet = daemon.get_wallet(wallet_path)\n                        if wallet is None:\n                            raise UserFacingException('wallet not loaded')\n                        kwargs['wallet'] = wallet\n                    if cmd.requires_password and password is None and wallet and wallet.has_password():\n                        password = wallet.get_unlocked_password()\n                        if password:\n                            kwargs['password'] = password\n                        else:\n                            raise UserFacingException('Password required. Unlock the wallet, or add a --password option to your command')\n            wallet = kwargs.get('wallet')  # type: Optional[Abstract_Wallet]\n            if cmd.requires_wallet and not wallet:\n                raise UserFacingException('wallet not loaded')\n            if cmd.requires_password and wallet.has_password():\n                if password is None:\n                    raise UserFacingException('Password required')\n                try:\n                    wallet.check_password(password)\n                except InvalidPassword as e:\n                    raise UserFacingException(str(e)) from None\n            if cmd.requires_lightning and (not wallet or not wallet.has_lightning()):\n                raise UserFacingException('Lightning not enabled in this wallet')\n            return await func(*args, **kwargs)\n        return func_wrapper\n    return decorator\n\n\nclass Commands(Logger):\n\n    def __init__(self, *, config: 'SimpleConfig',\n                 network: 'Network' = None,\n                 daemon: 'Daemon' = None, callback=None):\n        Logger.__init__(self)\n        self.config = config\n        self.daemon = daemon\n        self.network = network\n        self._callback = callback\n\n    def _run(self, method, args, password_getter=None, **kwargs):\n        \"\"\"This wrapper is called from unit tests and the Qt python console.\"\"\"\n        cmd = known_commands[method]\n        password = kwargs.get('password', None)\n        wallet = kwargs.get('wallet', None)\n        if (cmd.requires_password and wallet and wallet.has_password()\n                and password is None):\n            password = password_getter()\n            if password is None:\n                return\n\n        f = getattr(self, method)\n        if cmd.requires_password:\n            kwargs['password'] = password\n\n        if 'wallet' in kwargs:\n            sig = inspect.signature(f)\n            if 'wallet' not in sig.parameters:\n                kwargs.pop('wallet')\n\n        coro = f(*args, **kwargs)\n        fut = asyncio.run_coroutine_threadsafe(coro, util.get_asyncio_loop())\n        result = fut.result()\n\n        if self._callback:\n            self._callback()\n        return result\n\n    @command('n')\n    async def getinfo(self):\n        \"\"\" network info \"\"\"\n        net_params = self.network.get_parameters()\n        response = {\n            'network': constants.net.NET_NAME,\n            'path': self.network.config.path,\n            'server': net_params.server.host,\n            'blockchain_height': self.network.get_local_height(),\n            'server_height': self.network.get_server_height(),\n            'spv_nodes': len(self.network.get_interfaces()),\n            'connected': self.network.is_connected(),\n            'auto_connect': net_params.auto_connect,\n            'version': ELECTRUM_VERSION,\n            'fee_estimates': self.network.fee_estimates.get_data()\n        }\n        return response\n\n    @command('n')\n    async def stop(self):\n        \"\"\"Stop daemon\"\"\"\n        await self.daemon.stop()\n        return \"Daemon stopped\"\n\n    @command('n')\n    async def list_wallets(self):\n        \"\"\"List wallets open in daemon\"\"\"\n        return [\n            {\n                'path': w.db.storage.path,\n                'synchronized': w.is_up_to_date(),\n                'unlocked': not w.has_password() or (w.get_unlocked_password() is not None),\n            }\n            for w in self.daemon.get_wallets().values()\n        ]\n\n    @command('n')\n    async def load_wallet(self, wallet_path=None, password=None):\n        \"\"\"\n        Load the wallet in memory\n        \"\"\"\n        wallet = self.daemon.load_wallet(wallet_path, password, upgrade=True)\n        if wallet is None:\n            raise UserFacingException('could not load wallet')\n        run_hook('load_wallet', wallet, None)\n        return wallet_path\n\n    @command('n')\n    async def close_wallet(self, wallet_path=None):\n        \"\"\"Close wallet\"\"\"\n        return await self.daemon._stop_wallet(wallet_path)\n\n    @command('')\n    async def create(self, passphrase=None, password=None, encrypt_file=True, seed_type=None, wallet_path=None):\n        \"\"\"Create a new wallet.\n        If you want to be prompted for an argument, type '?' or ':' (concealed)\n\n        arg:str:passphrase:Seed extension\n        arg:str:seed_type:The type of wallet to create, e.g. 'standard' or 'segwit'\n        arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password\n        \"\"\"\n        d = create_new_wallet(\n            path=wallet_path,\n            passphrase=passphrase,\n            password=password,\n            encrypt_file=encrypt_file,\n            seed_type=seed_type,\n            config=self.config)\n        return {\n            'seed': d['seed'],\n            'path': d['wallet'].storage.path,\n            'msg': d['msg'],\n        }\n\n    @command('')\n    async def restore(self, text, passphrase=None, password=None, encrypt_file=True, wallet_path=None):\n        \"\"\"Restore a wallet from text. Text can be a seed phrase, a master\n        public key, a master private key, a list of bitcoin addresses\n        or bitcoin private keys.\n        If you want to be prompted for an argument, type '?' or ':' (concealed)\n\n        arg:str:text:seed phrase\n        arg:str:passphrase:Seed extension\n        arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password\n        \"\"\"\n        # TODO create a separate command that blocks until wallet is synced\n        d = restore_wallet_from_text(\n            text,\n            path=wallet_path,\n            passphrase=passphrase,\n            password=password,\n            encrypt_file=encrypt_file,\n            config=self.config)\n        return {\n            'path': d['wallet'].storage.path,\n            'msg': d['msg'],\n        }\n\n    @command('wp')\n    async def password(self, password=None, new_password=None, encrypt_file=None, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Change wallet password.\n\n        arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password (default=true)\n        arg:str:new_password:New Password\n        \"\"\"\n        if wallet.storage.is_encrypted_with_hw_device() and new_password:\n            raise UserFacingException(\"Can't change the password of a wallet encrypted with a hw device.\")\n        if encrypt_file is None:\n            if not password and new_password:\n                # currently no password, setting one now: we encrypt by default\n                encrypt_file = True\n            else:\n                encrypt_file = wallet.storage.is_encrypted()\n        wallet.update_password(password, new_password, encrypt_storage=encrypt_file)\n        wallet.save_db()\n        return {'password': wallet.has_password()}\n\n    @command('w')\n    async def get(self, key, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Return item from wallet storage\n\n        arg:str:key:storage key\n        \"\"\"\n        return wallet.db.get(key)\n\n    @command('')\n    async def getconfig(self, key):\n        \"\"\"Return the current value of a configuration variable.\n\n        arg:str:key:name of the configuration variable\n        \"\"\"\n        if Plugins.is_plugin_enabler_config_key(key):\n            return self.config.get(key)\n        else:\n            cv = self.config.cv.from_key(key)\n            return cv.get()\n\n    @classmethod\n    def _setconfig_normalize_value(cls, key, value):\n        if key not in (SimpleConfig.RPC_USERNAME.key(), SimpleConfig.RPC_PASSWORD.key()):\n            value = json_decode(value)\n            # call literal_eval for backward compatibility (see #4225)\n            try:\n                value = ast.literal_eval(value)\n            except Exception:\n                pass\n        return value\n\n    def _setconfig(self, key, value):\n        value = self._setconfig_normalize_value(key, value)\n        if self.daemon and key == SimpleConfig.RPC_USERNAME.key():\n            self.daemon.commands_server.rpc_user = value\n        if self.daemon and key == SimpleConfig.RPC_PASSWORD.key():\n            self.daemon.commands_server.rpc_password = value\n        if Plugins.is_plugin_enabler_config_key(key):\n            self.config.set_key(key, value)\n        else:\n            cv = self.config.cv.from_key(key)\n            cv.set(value)\n\n    @command('')\n    async def setconfig(self, key, value):\n        \"\"\"\n        Set a configuration variable.\n\n        arg:str:key:name of the configuration variable\n        arg:str:value:value. may be a string or a Python expression.\n        \"\"\"\n        self._setconfig(key, value)\n\n    @command('')\n    async def unsetconfig(self, key):\n        \"\"\"\n        Clear a configuration variable.\n        The variable will be reset to its default value.\n\n        arg:str:key:name of the configuration variable\n        \"\"\"\n        self._setconfig(key, None)\n\n    @command('')\n    async def listconfig(self):\n        \"\"\"Returns the list of all configuration variables. \"\"\"\n        return self.config.list_config_vars()\n\n    @command('')\n    async def helpconfig(self, key):\n        \"\"\"Returns help about a configuration variable.\n\n        arg:str:key:name of the configuration variable\n        \"\"\"\n        cv = self.config.cv.from_key(key)\n        short = cv.get_short_desc()\n        long = cv.get_long_desc()\n        if short and long:\n            return short + \"\\n---\\n\\n\" + long\n        elif short or long:\n            return short or long\n        else:\n            return f\"No description available for '{key}'\"\n\n    @command('')\n    async def make_seed(self, nbits=None, language=None, seed_type=None):\n        \"\"\"\n        Create a seed\n\n        arg:int:nbits:Number of bits of entropy\n        arg:str:seed_type:The type of seed to create, e.g. 'standard' or 'segwit'\n        arg:str:language:Default language for wordlist\n        \"\"\"\n        s = Mnemonic(language).make_seed(seed_type=seed_type, num_bits=nbits)\n        return s\n\n    @command('n')\n    async def getaddresshistory(self, address):\n        \"\"\"\n        Return the transaction history of any address. Note: This is a\n        walletless server query, results are not checked by SPV.\n\n        arg:str:address:Bitcoin address\n        \"\"\"\n        sh = bitcoin.address_to_scripthash(address)\n        return await self.network.get_history_for_scripthash(sh)\n\n    @command('wp')\n    async def unlock(self, wallet: Abstract_Wallet = None, password=None):\n        \"\"\"Unlock the wallet (store the password in memory).\"\"\"\n        wallet.unlock(password)\n\n    @command('w')\n    async def listunspent(self, wallet: Abstract_Wallet = None):\n        \"\"\"List unspent outputs. Returns the list of unspent transaction\n        outputs in your wallet.\"\"\"\n        coins = []\n        for txin in wallet.get_utxos():\n            d = txin.to_json()\n            v = d.pop(\"value_sats\")\n            d[\"value\"] = format_satoshis(v)\n            coins.append(d)\n        return coins\n\n    @command('n')\n    async def getaddressunspent(self, address):\n        \"\"\"\n        Returns the UTXO list of any address. Note: This\n        is a walletless server query, results are not checked by SPV.\n\n        arg:str:address:Bitcoin address\n        \"\"\"\n        sh = bitcoin.address_to_scripthash(address)\n        return await self.network.listunspent_for_scripthash(sh)\n\n    @command('')\n    async def serialize(self, jsontx):\n        \"\"\"Create a signed raw transaction from a json tx template.\n\n        Example value for \"jsontx\" arg: {\n            \"inputs\": [\n                {\"prevout_hash\": \"9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539\", \"prevout_n\": 1,\n                 \"value_sats\": 1000000, \"privkey\": \"p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD\"}\n            ],\n            \"outputs\": [\n                {\"address\": \"tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd\", \"value_sats\": 990000}\n            ]\n        }\n        arg:json:jsontx:Transaction in json\n        \"\"\"\n        keypairs = {}\n        inputs = []  # type: List[PartialTxInput]\n        locktime = jsontx.get('locktime', 0)\n        for txin_idx, txin_dict in enumerate(jsontx.get('inputs')):\n            if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None:\n                prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n']))\n            elif txin_dict.get('output'):\n                prevout = TxOutpoint.from_str(txin_dict['output'])\n            else:\n                raise UserFacingException(f\"missing prevout for txin {txin_idx}\")\n            txin = PartialTxInput(prevout=prevout)\n            try:\n                txin._trusted_value_sats = int(txin_dict.get('value') or txin_dict['value_sats'])\n            except KeyError:\n                raise UserFacingException(f\"missing 'value_sats' field for txin {txin_idx}\")\n            nsequence = txin_dict.get('nsequence', None)\n            if nsequence is not None:\n                txin.nsequence = nsequence\n            sec = txin_dict.get('privkey')\n            if sec:\n                txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)\n                pubkey = ecc.ECPrivkey(privkey).get_public_key_bytes(compressed=compressed)\n                keypairs[pubkey] = privkey\n                desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type)\n                txin.script_descriptor = desc\n            inputs.append(txin)\n\n        outputs = []  # type: List[PartialTxOutput]\n        for txout_idx, txout_dict in enumerate(jsontx.get('outputs')):\n            try:\n                txout_addr = txout_dict['address']\n            except KeyError:\n                raise UserFacingException(f\"missing 'address' field for txout {txout_idx}\")\n            try:\n                txout_val = int(txout_dict.get('value') or txout_dict['value_sats'])\n            except KeyError:\n                raise UserFacingException(f\"missing 'value_sats' field for txout {txout_idx}\")\n            txout = PartialTxOutput.from_address_and_value(txout_addr, txout_val)\n            outputs.append(txout)\n\n        tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime)\n        tx.sign(keypairs)\n        return tx.serialize()\n\n    @command('')\n    async def signtransaction_with_privkey(self, tx, privkey):\n        \"\"\"Sign a transaction with private keys passed as parameter.\n\n        arg:tx:tx:Transaction to sign\n        arg:str:privkey:private key or list of private keys\n        \"\"\"\n        tx = tx_from_any(tx)\n\n        txins_dict = defaultdict(list)\n        for txin in tx.inputs():\n            txins_dict[txin.address].append(txin)\n\n        if not isinstance(privkey, list):\n            privkey = [privkey]\n\n        for priv in privkey:\n            txin_type, priv2, compressed = bitcoin.deserialize_privkey(priv)\n            pubkey = ecc.ECPrivkey(priv2).get_public_key_bytes(compressed=compressed)\n            desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type)\n            address = desc.expand().address()\n            if address in txins_dict.keys():\n                for txin in txins_dict[address]:\n                    txin.script_descriptor = desc\n                tx.sign({pubkey: priv2})\n\n        return tx.serialize()\n\n    @command('wp')\n    async def signtransaction(self, tx, password=None, wallet: Abstract_Wallet = None, ignore_warnings: bool=False):\n        \"\"\"\n        Sign a transaction with the current wallet.\n\n        arg:tx:tx:transaction\n        arg:bool:ignore_warnings:ignore warnings\n        \"\"\"\n        tx = tx_from_any(tx)\n        wallet.sign_transaction(tx, password, ignore_warnings=ignore_warnings)\n        return tx.serialize()\n\n    @command('')\n    async def deserialize(self, tx):\n        \"\"\"\n        Deserialize a transaction\n\n        arg:str:tx:Serialized transaction\n        \"\"\"\n        tx = tx_from_any(tx)\n        return tx.to_json()\n\n    @command('n')\n    async def broadcast(self, tx):\n        \"\"\"\n        Broadcast a transaction to the network.\n\n        arg:str:tx:Serialized transaction (must be hexadecimal)\n        \"\"\"\n        tx = Transaction(tx)\n        await self.network.broadcast_transaction(tx)\n        return tx.txid()\n\n    @command('')\n    async def createmultisig(self, num, pubkeys):\n        \"\"\"\n        Create multisig 'n of m' address\n\n        arg:int:num:Number of cosigners required\n        arg:json:pubkeys:List of public keys\n        \"\"\"\n        assert isinstance(pubkeys, list), (type(num), type(pubkeys))\n        redeem_script = multisig_script(pubkeys, num)\n        address = bitcoin.hash160_to_p2sh(hash_160(redeem_script))\n        return {'address': address, 'redeemScript': redeem_script.hex()}\n\n    @command('w')\n    async def freeze(self, address: str, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Freeze address. Freeze the funds at one of your wallet\\'s addresses\n\n        arg:str:address:Bitcoin address\n        \"\"\"\n        return wallet.set_frozen_state_of_addresses([address], True)\n\n    @command('w')\n    async def unfreeze(self, address: str, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Unfreeze address. Unfreeze the funds at one of your wallet\\'s address\n\n        arg:str:address:Bitcoin address\n        \"\"\"\n        return wallet.set_frozen_state_of_addresses([address], False)\n\n    @command('w')\n    async def freeze_utxo(self, coin: str, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Freeze a UTXO so that the wallet will not spend it.\n\n        arg:str:coin:outpoint, in the <txid:index> format\n        \"\"\"\n        wallet.set_frozen_state_of_coins([coin], True)\n        return True\n\n    @command('w')\n    async def unfreeze_utxo(self, coin: str, wallet: Abstract_Wallet = None):\n        \"\"\"Unfreeze a UTXO so that the wallet might spend it.\n\n        arg:str:coin:outpoint\n        \"\"\"\n        wallet.set_frozen_state_of_coins([coin], False)\n        return True\n\n    @command('wp')\n    async def getprivatekeys(self, address, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses.\n\n        arg:str:address:Bitcoin address\n        \"\"\"\n        if isinstance(address, str):\n            address = address.strip()\n        if is_address(address):\n            return wallet.export_private_key(address, password)\n        domain = address\n        return [wallet.export_private_key(address, password) for address in domain]\n\n    @command('wp')\n    async def getprivatekeyforpath(self, path, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"Get private key corresponding to derivation path (address index).\n\n        arg:str:path:Derivation path. Can be either a str such as \"m/0/50\", or a list of ints such as [0, 50].\n        \"\"\"\n        return wallet.export_private_key_for_path(path, password)\n\n    @command('w')\n    async def ismine(self, address, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Check if address is in wallet. Return true if and only address is in wallet\n\n        arg:str:address:Bitcoin address\n        \"\"\"\n        return wallet.is_mine(address)\n\n    @command('')\n    async def dumpprivkeys(self):\n        \"\"\"Deprecated.\"\"\"\n        return \"This command is deprecated. Use a pipe instead: 'electrum listaddresses | electrum getprivatekeys - '\"\n\n    @command('')\n    async def validateaddress(self, address):\n        \"\"\"Check that an address is valid.\n\n        arg:str:address:Bitcoin address\n        \"\"\"\n        return is_address(address)\n\n    @command('w')\n    async def getpubkeys(self, address, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Return the public keys for a wallet address.\n\n        arg:str:address:Bitcoin address\n        \"\"\"\n        return wallet.get_public_keys(address)\n\n    @command('w')\n    async def getbalance(self, wallet: Abstract_Wallet = None):\n        \"\"\"Return the balance of your wallet. \"\"\"\n        c, u, x = wallet.get_balance()\n        l = wallet.lnworker.get_balance() if wallet.lnworker else None\n        out = {\"confirmed\": format_satoshis(c)}\n        if u:\n            out[\"unconfirmed\"] = format_satoshis(u)\n        if x:\n            out[\"unmatured\"] = format_satoshis(x)\n        if l:\n            out[\"lightning\"] = format_satoshis(l)\n        return out\n\n    @command('n')\n    async def getaddressbalance(self, address):\n        \"\"\"\n        Return the balance of any address. Note: This is a walletless\n        server query, results are not checked by SPV.\n\n        arg:str:address:Bitcoin address\n        \"\"\"\n        sh = bitcoin.address_to_scripthash(address)\n        out = await self.network.get_balance_for_scripthash(sh)\n        out[\"confirmed\"] = format_satoshis(out[\"confirmed\"])\n        out[\"unconfirmed\"] = format_satoshis(out[\"unconfirmed\"])\n        return out\n\n    @command('n')\n    async def getmerkle(self, txid, height):\n        \"\"\"Get Merkle branch of a transaction included in a block. Electrum\n        uses this to verify transactions (Simple Payment Verification).\n\n        arg:txid:txid:Transaction ID\n        arg:int:height:Block height\n        \"\"\"\n        return await self.network.get_merkle_for_transaction(txid, int(height))\n\n    @command('n')\n    async def getservers(self):\n        \"\"\"Return the list of known servers (candidates for connecting).\"\"\"\n        return self.network.get_servers()\n\n    @command('')\n    async def version(self):\n        \"\"\"Return the version of Electrum.\"\"\"\n        return ELECTRUM_VERSION\n\n    @command('')\n    async def version_info(self):\n        \"\"\"Return information about dependencies, such as their version and path.\"\"\"\n        ret = {\n            \"electrum.version\": ELECTRUM_VERSION,\n            \"electrum.path\": os.path.dirname(os.path.realpath(__file__)),\n            \"python.version\": sys.version,\n            \"python.path\": sys.executable,\n        }\n        # add currently running GUI\n        if self.daemon and self.daemon.gui_object:\n            ret.update(self.daemon.gui_object.version_info())\n        # always add Qt GUI, so we get info even when running this from CLI\n        try:\n            from .gui.qt import ElectrumGui as QtElectrumGui\n            ret.update(QtElectrumGui.version_info())\n        except GuiImportError:\n            pass\n        # Add shared libs (.so/.dll), and non-pure-python dependencies.\n        # Such deps can be installed in various ways - often via the Linux distro's pkg manager,\n        # instead of using pip, hence it is useful to list them for debugging.\n        from electrum_ecc import ecc_fast\n        ret.update(ecc_fast.version_info())\n        from . import qrscanner\n        ret.update(qrscanner.version_info())\n        ret.update(DeviceMgr.version_info())\n        ret.update(crypto.version_info())\n        # add some special cases\n        import aiohttp\n        ret[\"aiohttp.version\"] = aiohttp.__version__\n        import aiorpcx\n        ret[\"aiorpcx.version\"] = aiorpcx._version_str\n        import certifi\n        ret[\"certifi.version\"] = certifi.__version__\n        import dns\n        ret[\"dnspython.version\"] = dns.__version__\n        import ssl\n        ret[\"openssl.version\"] = ssl.OPENSSL_VERSION\n\n        return ret\n\n    @command('w')\n    async def getmpk(self, wallet: Abstract_Wallet = None):\n        \"\"\"Get master public key. Return your wallet\\'s master public key\"\"\"\n        return wallet.get_master_public_key()\n\n    @command('wp')\n    async def getmasterprivate(self, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"Get master private key. Return your wallet\\'s master private key\"\"\"\n        return str(wallet.keystore.get_master_private_key(password))\n\n    @command('')\n    async def convert_xkey(self, xkey, xtype):\n        \"\"\"Convert xtype of a master key. e.g. xpub -> ypub\n\n        arg:str:xkey:the key\n        arg:str:xtype:the type, eg 'xpub'\n        \"\"\"\n        try:\n            node = BIP32Node.from_xkey(xkey)\n        except Exception:\n            raise UserFacingException('xkey should be a master public/private key')\n        return node._replace(xtype=xtype).to_xkey()\n\n    @command('wp')\n    async def getseed(self, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"Get seed phrase. Print the generation seed of your wallet.\"\"\"\n        s = wallet.get_seed(password)\n        return s\n\n    @command('wp')\n    async def importprivkey(self, privkey, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"Import a private key or a list of private keys.\n\n        arg:str:privkey:Private key. Type \\'?\\' to get a prompt.\n        \"\"\"\n        if not wallet.can_import_privkey():\n            return \"Error: This type of wallet cannot import private keys. Try to create a new wallet with that key.\"\n        assert isinstance(wallet, Imported_Wallet)\n        keys = privkey.split()\n        if not keys:\n            return \"Error: no keys given\"\n        elif len(keys) == 1:\n            try:\n                addr = wallet.import_private_key(keys[0], password)\n                out = \"Keypair imported: \" + addr\n            except Exception as e:\n                out = \"Error: \" + repr(e)\n            return out\n        else:\n            good_inputs, bad_inputs = wallet.import_private_keys(keys, password)\n            return {\n                \"good_keys\": len(good_inputs),\n                \"bad_keys\": len(bad_inputs),\n            }\n\n    async def _resolver(self, x, wallet: Abstract_Wallet):\n        if x is None:\n            return None\n        out = await wallet.contacts.resolve(x)\n        return out['address']\n\n    @command('n')\n    async def sweep(self, privkey, destination, fee=None, feerate=None, imax=100):\n        \"\"\"\n        Sweep private keys. Returns a transaction that spends UTXOs from\n        privkey to a destination address. The transaction will not be broadcast.\n\n        arg:str:privkey:Private key. Type \\'?\\' to get a prompt.\n        arg:str:destination:Bitcoin address, contact or alias\n        arg:decimal:fee:Transaction fee (absolute, in BTC)\n        arg:decimal:feerate:Transaction fee rate (in sat/vbyte)\n        arg:int:imax:Maximum number of inputs\n        \"\"\"\n        from .wallet import sweep\n        fee_policy = self._get_fee_policy(fee, feerate)\n        privkeys = privkey.split()\n        #dest = self._resolver(destination)\n        tx = await sweep(\n            privkeys,\n            network=self.network,\n            to_address=destination,\n            fee_policy=fee_policy,\n            imax=imax,\n        )\n        return tx.serialize() if tx else None\n\n    @command('wp')\n    async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"Sign a message with a key. Use quotes if your message contains\n        whitespaces\n\n        arg:str:address:Bitcoin address\n        arg:str:message:Clear text message. Use quotes if it contains spaces.\n        \"\"\"\n        sig = wallet.sign_message(address, message, password)\n        return base64.b64encode(sig).decode('ascii')\n\n    @command('')\n    async def verifymessage(self, address, signature, message):\n        \"\"\"Verify a signature.\n\n        arg:str:address:Bitcoin address\n        arg:str:message:Clear text message. Use quotes if it contains spaces.\n        arg:str:signature:The signature, base64-encoded.\n        \"\"\"\n        try:\n            sig = base64.b64decode(signature, validate=True)\n        except binascii.Error:\n            return False\n        message = util.to_bytes(message)\n        return bitcoin.verify_usermessage_with_address(address, sig, message)\n\n    def _get_fee_policy(self, fee: str, feerate: str):\n        if fee is not None and feerate is not None:\n            raise Exception('Cannot set both fee and feerate')\n        if fee is not None:\n            fee_sats = satoshis(fee)\n            fee_policy = FeePolicy(f'fixed:{fee_sats}')\n        elif feerate is not None:\n            sat_per_kvbyte = int(1000 * to_decimal(feerate))\n            fee_policy = FeePolicy(f'feerate:{sat_per_kvbyte}')\n        else:\n            fee_policy = FeePolicy(self.config.FEE_POLICY)\n        return fee_policy\n\n    @command('wp')\n    async def payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,\n                    unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):\n        \"\"\"Create an on-chain transaction.\n\n        arg:str:destination:Bitcoin address, contact or alias\n        arg:decimal_or_max:amount:Amount to be sent (in BTC). Type '!' to send the maximum available.\n        arg:decimal:fee:Transaction fee (absolute, in BTC)\n        arg:decimal:feerate:Transaction fee rate (in sat/vbyte)\n        arg:str:from_addr:Source address (must be a wallet address; use sweep to spend from non-wallet address)\n        arg:str:change_addr:Change address. Default is a spare address, or the source address if it's not in the wallet\n        arg:bool:rbf:Whether to signal opt-in Replace-By-Fee in the transaction (true/false)\n        arg:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet\n        arg:int:locktime:Set locktime block number\n        arg:bool:unsigned:Do not sign transaction\n        arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address)\n        \"\"\"\n        return await self.paytomany(\n            outputs=[(destination, amount),],\n            fee=fee,\n            feerate=feerate,\n            from_addr=from_addr,\n            from_coins=from_coins,\n            change_addr=change_addr,\n            unsigned=unsigned,\n            rbf=rbf,\n            password=password,\n            locktime=locktime,\n            addtransaction=addtransaction,\n            wallet=wallet,\n        )\n\n    @command('wp')\n    async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,\n                        unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):\n        \"\"\"Create a multi-output transaction.\n\n        arg:json:outputs:json list of [\"address\", \"amount in BTC\"]\n        arg:bool:rbf:Whether to signal opt-in Replace-By-Fee in the transaction (true/false)\n        arg:decimal:fee:Transaction fee (absolute, in BTC)\n        arg:decimal:feerate:Transaction fee rate (in sat/vbyte)\n        arg:str:from_addr:Source address (must be a wallet address; use sweep to spend from non-wallet address)\n        arg:str:change_addr:Change address. Default is a spare address, or the source address if it's not in the wallet\n        arg:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet\n        arg:int:locktime:Set locktime block number\n        arg:bool:unsigned:Do not sign transaction\n        arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address)\n        \"\"\"\n        fee_policy = self._get_fee_policy(fee, feerate)\n        domain_addr = from_addr.split(',') if from_addr else None\n        domain_coins = from_coins.split(',') if from_coins else None\n        change_addr = await self._resolver(change_addr, wallet)\n        if domain_addr is not None:\n            resolvers = [self._resolver(addr, wallet) for addr in domain_addr]\n            domain_addr = await asyncio.gather(*resolvers)\n        final_outputs = []\n        for address, amount in outputs:\n            address = await self._resolver(address, wallet)\n            amount_sat = satoshis_or_max(amount)\n            final_outputs.append(PartialTxOutput.from_address_and_value(address, amount_sat))\n        coins = wallet.get_spendable_coins(domain_addr)\n        if domain_coins is not None:\n            coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]\n        tx = wallet.make_unsigned_transaction(\n            outputs=final_outputs,\n            fee_policy=fee_policy,\n            change_addr=change_addr,\n            coins=coins,\n            rbf=rbf,\n            locktime=locktime,\n        )\n        if not unsigned:\n            wallet.sign_transaction(tx, password)\n        result = tx.serialize()\n        if addtransaction:\n            await self.addtransaction(result, wallet=wallet)\n        return result\n\n    def get_year_timestamps(self, year: int) -> dict[str, Any]:\n        kwargs = {}\n        if year:\n            start_date = datetime.datetime(year, 1, 1)\n            end_date = datetime.datetime(year+1, 1, 1)\n            kwargs['from_timestamp'] = time.mktime(start_date.timetuple())\n            kwargs['to_timestamp'] = time.mktime(end_date.timetuple())\n        return kwargs\n\n    @command('w')\n    async def onchain_capital_gains(self, year=None, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Capital gains, using utxo pricing.\n        This cannot be used with lightning.\n\n        arg:int:year:Show cap gains for a given year\n        \"\"\"\n        kwargs = self.get_year_timestamps(year)\n        from .exchange_rate import FxThread\n        fx = self.daemon.fx if self.daemon else FxThread(config=self.config)\n        return json_normalize(wallet.get_onchain_capital_gains(fx, **kwargs))\n\n    @command('wp')\n    async def bumpfee(self, tx, new_fee_rate, from_coins=None, decrease_payment=False, password=None, unsigned=False, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Bump the fee for an unconfirmed transaction.\n        'tx' can be either a raw hex tx or a txid. If txid, the corresponding tx must already be part of the wallet history.\n\n        arg:str:tx:Serialized transaction (hexadecimal)\n        arg:str:new_fee_rate: The Updated/Increased Transaction fee rate (in sats/vbyte)\n        arg:bool:decrease_payment:Whether payment amount will be decreased (true/false)\n        arg:bool:unsigned:Do not sign transaction\n        arg:json:from_coins:Coins that may be used to inncrease the fee (must be in wallet)\n        \"\"\"\n        if is_hash256_str(tx):  # txid\n            tx = wallet.db.get_transaction(tx)\n            if tx is None:\n                raise UserFacingException(\"Transaction not in wallet.\")\n        else:  # raw tx\n            try:\n                tx = Transaction(tx)\n                tx.deserialize()\n            except transaction.SerializationError as e:\n                raise UserFacingException(f\"Failed to deserialize transaction: {e}\") from e\n        domain_coins = from_coins.split(',') if from_coins else None\n        coins = wallet.get_spendable_coins(None)\n        if domain_coins is not None:\n            coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]\n        tx.add_info_from_wallet(wallet)\n        await tx.add_info_from_network(self.network)\n        new_tx = wallet.bump_fee(\n            tx=tx,\n            coins=coins,\n            strategy=BumpFeeStrategy.DECREASE_PAYMENT if decrease_payment else BumpFeeStrategy.PRESERVE_PAYMENT,\n            new_fee_rate=new_fee_rate)\n        if not unsigned:\n            wallet.sign_transaction(new_tx, password)\n        return new_tx.serialize()\n\n    @command('w')\n    async def onchain_history(\n        self, show_fiat=False, year=None, show_addresses=False,\n        from_height=None, to_height=None,\n        wallet: Abstract_Wallet = None,\n    ):\n        \"\"\"Wallet onchain history. Returns the transaction history of your wallet.\n\n        arg:bool:show_addresses:Show input and output addresses\n        arg:bool:show_fiat:Show fiat value of transactions\n        arg:int:year:Show history for a given year\n        arg:int:from_height:Only show transactions that confirmed after(inclusive) given block height\n        arg:int:to_height:Only show transactions that confirmed before(exclusive) given block height\n        \"\"\"\n        # trigger lnwatcher callbacks for their side effects: setting labels and accounting_addresses\n        if not self.network and wallet.lnworker:\n            await wallet.lnworker.lnwatcher.trigger_callbacks(requires_synchronizer=False)\n\n        kwargs = self.get_year_timestamps(year)\n        kwargs['from_height'] = from_height\n        kwargs['to_height'] = to_height\n        onchain_history = wallet.get_onchain_history(**kwargs)\n        out = [x.to_dict() for x in onchain_history.values()]\n        if show_fiat:\n            from .exchange_rate import FxThread\n            fx = self.daemon.fx if self.daemon else FxThread(config=self.config)\n        else:\n            fx = None\n        for item in out:\n            if show_addresses:\n                tx = wallet.db.get_transaction(item['txid'])\n                item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs()))\n                item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value_sat': x.value},\n                                           tx.outputs()))\n            if fx:\n                fiat_fields = wallet.get_tx_item_fiat(tx_hash=item['txid'], amount_sat=item['amount_sat'], fx=fx, tx_fee=item['fee_sat'])\n                item.update(fiat_fields)\n        return json_normalize(out)\n\n    @command('wl')\n    async def lightning_history(self, wallet: Abstract_Wallet = None):\n        \"\"\" lightning history. \"\"\"\n        lightning_history = wallet.lnworker.get_lightning_history() if wallet.lnworker else {}\n        sorted_hist= sorted(lightning_history.values(), key=lambda x: x.timestamp)\n        return json_normalize([x.to_dict() for x in sorted_hist])\n\n    @command('w')\n    async def setlabel(self, key, label, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Assign a label to an item. Item may be a bitcoin address or a\n        transaction ID\n\n        arg:str:key:Key\n        arg:str:label:Label\n        \"\"\"\n        wallet.set_label(key, label)\n\n    @command('w')\n    async def listcontacts(self, wallet: Abstract_Wallet = None):\n        \"\"\"Show your list of contacts\"\"\"\n        return wallet.contacts\n\n    @command('w')\n    async def getopenalias(self, key, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Retrieve alias. Lookup in your list of contacts, and for an OpenAlias DNS record.\n\n        arg:str:key:the alias to be retrieved\n        \"\"\"\n        d = await wallet.contacts.resolve(key)\n        if d.get(\"type\") == \"openalias\":\n            # we always validate DNSSEC now\n            d[\"validated\"] = True\n        return d\n\n    @command('w')\n    async def searchcontacts(self, query, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Search through your wallet contacts, return matching entries.\n\n        arg:str:query:Search query\n        \"\"\"\n        results = {}\n        for key, value in wallet.contacts.items():\n            if query.lower() in key.lower():\n                results[key] = value\n        return results\n\n    @command('w')\n    async def listaddresses(self, receiving=False, change=False, labels=False, frozen=False, unused=False, funded=False, balance=False, wallet: Abstract_Wallet = None):\n        \"\"\"List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results.\n\n        arg:bool:receiving:Show only receiving addresses\n        arg:bool:change:Show only change addresses\n        arg:bool:frozen:Show only frozen addresses\n        arg:bool:unused:Show only unused addresses\n        arg:bool:funded:Show only funded addresses\n        arg:bool:balance:Show the balances of listed addresses\n        arg:bool:labels:Show the labels of listed addresses\n        \"\"\"\n        out = []\n        for addr in wallet.get_addresses():\n            if frozen and not wallet.is_frozen_address(addr):\n                continue\n            if receiving and wallet.is_change(addr):\n                continue\n            if change and not wallet.is_change(addr):\n                continue\n            if unused and wallet.adb.is_used(addr):\n                continue\n            if funded and wallet.adb.is_empty(addr):\n                continue\n            item = addr\n            if labels or balance:\n                item = (item,)\n            if balance:\n                item += (format_satoshis(sum(wallet.get_addr_balance(addr))),)\n            if labels:\n                item += (repr(wallet.get_label_for_address(addr)),)\n            out.append(item)\n        return out\n\n    @command('n')\n    async def gettransaction(self, txid, wallet: Abstract_Wallet = None):\n        \"\"\"Retrieve a transaction.\n\n        arg:txid:txid:Transaction ID\n        \"\"\"\n        tx = None\n        if wallet:\n            tx = wallet.db.get_transaction(txid)\n        if tx is None:\n            raw = await self.network.get_transaction(txid)\n            if raw:\n                tx = Transaction(raw)\n            else:\n                raise UserFacingException(\"Unknown transaction\")\n        if tx.txid() != txid:\n            raise UserFacingException(\"Mismatching txid\")\n        return tx.serialize()\n\n    @command('')\n    async def encrypt(self, pubkey, message) -> str:\n        \"\"\"\n        Encrypt a message with a public key. Use quotes if the message contains whitespaces.\n\n        arg:str:pubkey:Public key\n        arg:str:message:Clear text message. Use quotes if it contains spaces.\n        \"\"\"\n        if not is_hex_str(pubkey):\n            raise UserFacingException(f\"pubkey must be a hex string instead of {repr(pubkey)}\")\n        try:\n            message = to_bytes(message)\n        except TypeError:\n            raise UserFacingException(f\"message must be a string-like object instead of {repr(message)}\")\n        public_key = ecc.ECPubkey(bfh(pubkey))\n        encrypted = crypto.ecies_encrypt_message(public_key, message)\n        return encrypted.decode('utf-8')\n\n    @command('wp')\n    async def decrypt(self, pubkey, encrypted, password=None, wallet: Abstract_Wallet = None) -> str:\n        \"\"\"Decrypt a message encrypted with a public key.\n\n        arg:str:encrypted:Encrypted message\n        arg:str:pubkey:Public key of one of your wallet addresses\n        \"\"\"\n        if not is_hex_str(pubkey):\n            raise UserFacingException(f\"pubkey must be a hex string instead of {repr(pubkey)}\")\n        if not isinstance(encrypted, (str, bytes, bytearray)):\n            raise UserFacingException(f\"encrypted must be a string-like object instead of {repr(encrypted)}\")\n        decrypted = wallet.decrypt_message(pubkey, encrypted, password)\n        return decrypted.decode('utf-8')\n\n    @command('w')\n    async def get_request(self, request_id, wallet: Abstract_Wallet = None):\n        \"\"\"Returns a payment request\n\n        arg:str:request_id:The request ID, as seen in list_requests or add_request\n        \"\"\"\n        r = wallet.get_request(request_id)\n        if not r:\n            raise UserFacingException(\"Request not found\")\n        return wallet.export_request(r)\n\n    @command('w')\n    async def get_invoice(self, invoice_id, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Returns an invoice (request for outgoing payment)\n\n        arg:str:invoice_id:The invoice ID, as seen in list_invoices\n        \"\"\"\n        r = wallet.get_invoice(invoice_id)\n        if not r:\n            raise UserFacingException(\"Request not found\")\n        return wallet.export_invoice(r)\n\n    def _filter_invoices(self, _list, wallet, pending, expired, paid):\n        if pending:\n            f = PR_UNPAID\n        elif expired:\n            f = PR_EXPIRED\n        elif paid:\n            f = PR_PAID\n        else:\n            f = None\n        if f is not None:\n            _list = [x for x in _list if f == wallet.get_invoice_status(x)]\n        return _list\n\n    @command('w')\n    async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Returns the list of incoming payment requests saved in the wallet.\n        arg:bool:paid:Show only paid requests\n        arg:bool:pending:Show only pending requests\n        arg:bool:expired:Show only expired requests\n        \"\"\"\n        l = wallet.get_sorted_requests()\n        l = self._filter_invoices(l, wallet, pending, expired, paid)\n        return [wallet.export_request(x) for x in l]\n\n    @command('w')\n    async def list_invoices(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Returns the list of invoices (requests for outgoing payments) saved in the wallet.\n        arg:bool:paid:Show only paid invoices\n        arg:bool:pending:Show only pending invoices\n        arg:bool:expired:Show only expired invoices\n        \"\"\"\n        l = wallet.get_invoices()\n        l = self._filter_invoices(l, wallet, pending, expired, paid)\n        return [wallet.export_invoice(x) for x in l]\n\n    @command('w')\n    async def createnewaddress(self, wallet: Abstract_Wallet = None):\n        \"\"\"Create a new receiving address, beyond the gap limit of the wallet\"\"\"\n        return wallet.create_new_address(False)\n\n    @command('w')\n    async def changegaplimit(self, new_limit, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Change the gap limit of the wallet.\n\n        arg:int:new_limit:new gap limit\n        arg:bool:iknowwhatimdoing:Acknowledge that I understand the full implications of what I am about to do\n        \"\"\"\n        if not iknowwhatimdoing:\n            raise UserFacingException(\n                \"WARNING: Are you SURE you want to change the gap limit?\\n\"\n                \"It makes recovering your wallet from seed difficult!\\n\"\n                \"Please do your research and make sure you understand the implications.\\n\"\n                \"Typically only merchants and power users might want to do this.\\n\"\n                \"To proceed, try again, with the --iknowwhatimdoing option.\")\n        if not isinstance(wallet, Deterministic_Wallet):\n            raise UserFacingException(\"This wallet is not deterministic.\")\n        return wallet.change_gap_limit(new_limit)\n\n    @command('wn')\n    async def getminacceptablegap(self, wallet: Abstract_Wallet = None):\n        \"\"\"Returns the minimum value for gap limit that would be sufficient to discover all\n        known addresses in the wallet.\n        \"\"\"\n        if not isinstance(wallet, Deterministic_Wallet):\n            raise UserFacingException(\"This wallet is not deterministic.\")\n        if not wallet.is_up_to_date():\n            raise NotSynchronizedException(\"Wallet not fully synchronized.\")\n        return wallet.min_acceptable_gap()\n\n    @command('w')\n    async def getunusedaddress(self, wallet: Abstract_Wallet = None):\n        \"\"\"Returns the first unused address of the wallet, or None if all addresses are used.\n        An address is considered as used if it has received a transaction, or if it is used in a payment request.\"\"\"\n        return wallet.get_unused_address()\n\n    @command('w')\n    async def add_request(self, amount, memo='', expiry=3600, lightning=False, force=False, wallet: Abstract_Wallet = None):\n        \"\"\"Create a payment request, using the first unused address of the wallet.\n\n        The address will be considered as used after this operation.\n        If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet.\n\n        arg:decimal:amount:Requested amount (in btc)\n        arg:str:memo:Description of the request\n        arg:bool:force:Create new address beyond gap limit, if no more addresses are available.\n        arg:bool:lightning:Create lightning request.\n        arg:int:expiry:Time in seconds.\n        \"\"\"\n        amount = satoshis(amount)\n        if not lightning:\n            addr = wallet.get_unused_address()\n            if addr is None:\n                if force:\n                    addr = wallet.create_new_address(False)\n                else:\n                    return False\n        else:\n            addr = None\n        expiry = int(expiry) if expiry else None\n        key = wallet.create_request(amount, memo, expiry, addr)\n        req = wallet.get_request(key)\n        return wallet.export_request(req)\n\n    @command('wnl')\n    async def add_hold_invoice(\n            self,\n            payment_hash: str,\n            amount: Optional[Decimal] = None,\n            memo: str = \"\",\n            expiry: int = 3600,\n            min_final_cltv_expiry_delta: int = MIN_FINAL_CLTV_DELTA_ACCEPTED * 2,\n            wallet: Abstract_Wallet = None\n    ) -> dict:\n        \"\"\"\n        Create a lightning hold invoice for the given payment hash. Hold invoices have to get settled manually later.\n        HTLCs will get failed automatically if block_height + 144 > htlc.cltv_abs, if the intention is to\n        settle them as late as possible a safety margin of some blocks should be used to prevent them\n        from getting failed accidentally.\n\n        arg:str:payment_hash:Hex encoded payment hash to be used for the invoice\n        arg:decimal:amount:Optional requested amount (in btc)\n        arg:str:memo:Optional description of the invoice\n        arg:int:expiry:Optional expiry in seconds (default: 3600s)\n        arg:int:min_final_cltv_expiry_delta:Optional min final cltv expiry delta (default: 294 blocks)\n        \"\"\"\n        assert len(payment_hash) == 64, f\"Invalid payment hash length: {len(payment_hash)} != 64\"\n        assert not wallet.lnworker.get_payment_info(bfh(payment_hash), direction=RECEIVED), \"Payment hash already used!\"\n        assert payment_hash not in wallet.lnworker.dont_expire_htlcs, \"Payment hash already used!\"\n        assert wallet.lnworker.get_preimage(bfh(payment_hash)) is None, \"Already got a preimage for this payment hash!\"\n        assert MIN_FINAL_CLTV_DELTA_ACCEPTED < min_final_cltv_expiry_delta < 576, \"Use a sane min_final_cltv_expiry_delta value\"\n        amount = amount if amount and satoshis(amount) > 0 else None  # make amount either >0 or None\n        inbound_capacity = wallet.lnworker.num_sats_can_receive()\n        assert inbound_capacity > satoshis(amount or 0), \\\n            f\"Not enough inbound capacity [{inbound_capacity} sat] to receive this payment\"\n\n        wallet.lnworker.add_payment_info_for_hold_invoice(\n            bfh(payment_hash),\n            lightning_amount_sat=satoshis(amount) if amount else None,\n            min_final_cltv_delta=min_final_cltv_expiry_delta,\n            exp_delay=expiry,\n        )\n        info = wallet.lnworker.get_payment_info(bfh(payment_hash), direction=RECEIVED)\n        lnaddr, invoice = wallet.lnworker.get_bolt11_invoice(\n            payment_info=info,\n            message=memo,\n            fallback_address=None\n        )\n        # this prevents incoming htlcs from getting expired while the preimage isn't set.\n        # If their blocks to expiry fall below MIN_FINAL_CLTV_DELTA_ACCEPTED they will get failed.\n        wallet.lnworker.dont_expire_htlcs[payment_hash] = MIN_FINAL_CLTV_DELTA_ACCEPTED\n        wallet.set_label(payment_hash, memo)\n        result = {\n            \"invoice\": invoice\n        }\n        return result\n\n    @command('wnl')\n    async def settle_hold_invoice(self, preimage: str, wallet: Abstract_Wallet = None) -> dict:\n        \"\"\"\n        Settles lightning hold invoice with the given preimage.\n        Doesn't block until actual settlement of the HTLCs.\n\n        arg:str:preimage:Hex encoded preimage of the invoice to be settled\n        \"\"\"\n        assert len(preimage) == 64, f\"Invalid payment_hash length: {len(preimage)} != 64\"\n        payment_hash: str = crypto.sha256(bfh(preimage)).hex()\n        assert payment_hash not in wallet.lnworker._preimages, f\"Invoice {payment_hash=} already settled\"\n        info = wallet.lnworker.get_payment_info(bfh(payment_hash), direction=RECEIVED)\n        assert info, f\"Couldn't find lightning invoice for {payment_hash=}\"\n        assert payment_hash in wallet.lnworker.dont_expire_htlcs, f\"Invoice {payment_hash=} not a hold invoice?\"\n        assert wallet.lnworker.is_complete_mpp(bfh(payment_hash)), \\\n            f\"MPP incomplete, cannot settle hold invoice {payment_hash} yet\"\n        assert (wallet.lnworker.get_payment_mpp_amount_msat(bfh(payment_hash)) or 0) >= (info.amount_msat or 0)\n        wallet.lnworker.save_preimage(bfh(payment_hash), bfh(preimage))\n        util.trigger_callback('wallet_updated', wallet)\n        result = {\n            \"settled\": payment_hash\n        }\n        return result\n\n    @command('wnl')\n    async def cancel_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict:\n        \"\"\"\n        Cancels lightning hold invoice 'payment_hash'.\n\n        arg:str:payment_hash:Payment hash in hex of the hold invoice\n        \"\"\"\n        assert wallet.lnworker.get_payment_info(bfh(payment_hash), direction=RECEIVED), \\\n            f\"Couldn't find lightning invoice for payment hash {payment_hash}\"\n        assert payment_hash not in wallet.lnworker._preimages, \"Cannot cancel anymore, preimage already given.\"\n        assert payment_hash in wallet.lnworker.dont_expire_htlcs, f\"{payment_hash=} not a hold invoice?\"\n        # set to PR_UNPAID so it can get deleted\n        wallet.lnworker.set_payment_status(bfh(payment_hash), PR_UNPAID, direction=RECEIVED)\n        wallet.lnworker.delete_payment_info(payment_hash, direction=RECEIVED)\n        wallet.set_label(payment_hash, None)\n        del wallet.lnworker.dont_expire_htlcs[payment_hash]\n        while wallet.lnworker.is_complete_mpp(bfh(payment_hash)):\n            # block until the htlcs got failed\n            await asyncio.sleep(0.1)\n        result = {\n            \"cancelled\": payment_hash\n        }\n        return result\n\n    @command('wnl')\n    async def check_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict:\n        \"\"\"\n        Checks the status of a lightning hold invoice 'payment_hash'.\n        Returns: {\n            \"status\": unpaid | paid | settled | unknown (cancelled or not found),\n            \"received_amount_sat\": currently received amount (pending htlcs or final after settling),\n            \"invoice_amount_sat\": Invoice amount, Optional (only if invoice is found),\n            \"closest_htlc_expiry_height\": Closest absolute expiry height of all received htlcs\n            (Note: HTLCs will get failed automatically if block_height + 144 > htlc_expiry_height)\n        }\n\n        arg:str:payment_hash:Payment hash in hex of the hold invoice\n        \"\"\"\n        assert len(payment_hash) == 64, f\"Invalid payment_hash length: {len(payment_hash)} != 64\"\n        info: Optional['PaymentInfo'] = wallet.lnworker.get_payment_info(bfh(payment_hash), direction=RECEIVED)\n        is_complete_mpp: bool = wallet.lnworker.is_complete_mpp(bfh(payment_hash))\n        amount_sat = (wallet.lnworker.get_payment_mpp_amount_msat(bfh(payment_hash)) or 0) // 1000\n        result = {\n            \"status\": \"unknown\",\n            \"received_amount_sat\": amount_sat,\n        }\n        if info is None:\n            pass\n        elif not is_complete_mpp and not wallet.lnworker.get_preimage_hex(payment_hash):\n            # is_complete_mpp is False for settled payments\n            result[\"status\"] = \"unpaid\"\n        elif is_complete_mpp and payment_hash in wallet.lnworker.dont_expire_htlcs:\n            result[\"status\"] = \"paid\"\n            payment_key: str = wallet.lnworker._get_payment_key(bfh(payment_hash)).hex()\n            htlc_status = wallet.lnworker.received_mpp_htlcs[payment_key]\n            result[\"closest_htlc_expiry_height\"] = min(\n                mpp_htlc.htlc.cltv_abs for mpp_htlc in htlc_status.htlcs\n            )\n        elif wallet.lnworker.get_preimage_hex(payment_hash) is not None:\n            result[\"status\"] = \"settled\"\n            plist = wallet.lnworker.get_payments(status='settled')[bfh(payment_hash)]\n            _dir, amount_msat, _fee, _ts = wallet.lnworker.get_payment_value(None, plist)\n            result[\"received_amount_sat\"] = amount_msat // 1000\n            result['preimage'] = wallet.lnworker.get_preimage_hex(payment_hash)\n        if info is not None:\n            result[\"invoice_amount_sat\"] = (info.amount_msat or 0) // 1000\n        return result\n\n    @command('wl')\n    async def export_lightning_preimage(self, payment_hash: str, wallet: 'Abstract_Wallet' = None) -> Optional[str]:\n        \"\"\"\n        Returns the stored preimage of the given payment_hash if it is known.\n\n        arg:str:payment_hash: Hash of the preimage\n        \"\"\"\n        preimage = wallet.lnworker.get_preimage_hex(payment_hash)\n        assert preimage is None or crypto.sha256(bytes.fromhex(preimage)).hex() == payment_hash\n        return preimage\n\n    @command('w')\n    async def addtransaction(self, tx, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Add a transaction to the wallet history, without broadcasting it.\n\n        arg:tx:tx:Transaction, in hexadecimal format.\n        \"\"\"\n        tx = Transaction(tx)\n        if not wallet.adb.add_transaction(tx):\n            return False\n        wallet.save_db()\n        return tx.txid()\n\n    @command('w')\n    async def delete_request(self, request_id, wallet: Abstract_Wallet = None):\n        \"\"\"Remove an incoming payment request\n\n        arg:str:request_id:The request ID, as returned in list_invoices\n        \"\"\"\n        return wallet.delete_request(request_id)\n\n    @command('w')\n    async def delete_invoice(self, invoice_id, wallet: Abstract_Wallet = None):\n        \"\"\"Remove an outgoing payment invoice\n\n        arg:str:invoice_id:The invoice ID, as returned in list_invoices\n        \"\"\"\n        return wallet.delete_invoice(invoice_id)\n\n    @command('w')\n    async def clear_requests(self, wallet: Abstract_Wallet = None):\n        \"\"\"Remove all payment requests\"\"\"\n        wallet.clear_requests()\n        return True\n\n    @command('w')\n    async def clear_invoices(self, wallet: Abstract_Wallet = None):\n        \"\"\"Remove all invoices\"\"\"\n        wallet.clear_invoices()\n        return True\n\n    @command('n')\n    async def notify(self, address: str, URL: Optional[str]):\n        \"\"\"\n        Watch an address. Every time the address changes, a http POST is sent to the URL.\n        Call with an empty URL to stop watching an address.\n\n        arg:str:address:Bitcoin address\n        arg:str:URL:The callback URL\n        \"\"\"\n        if not hasattr(self, \"_notifier\"):\n            self._notifier = Notifier(self.network)\n        if URL:\n            await self._notifier.start_watching_addr(address, URL)\n        else:\n            await self._notifier.stop_watching_addr(address)\n        return True\n\n    @command('wn')\n    async def is_synchronized(self, wallet: Abstract_Wallet = None):\n        \"\"\" return wallet synchronization status \"\"\"\n        return wallet.is_up_to_date()\n\n    @command('wn')\n    async def wait_for_sync(self, wallet: Abstract_Wallet = None):\n        \"\"\"Block until the wallet synchronization finishes.\"\"\"\n        while True:\n            if wallet.is_up_to_date():\n                return True\n            await wallet.up_to_date_changed_event.wait()\n\n    @command('n')\n    async def getfeerate(self):\n        \"\"\"\n        Return current fee estimate given network conditions (in sat/kvByte).\n        To change the fee policy, use 'getconfig/setconfig fee_policy'\n        \"\"\"\n        fee_policy = FeePolicy(self.config.FEE_POLICY)\n        description = fee_policy.get_target_text()\n        feerate = fee_policy.fee_per_kb(self.network)\n        tooltip = fee_policy.get_estimate_text(self.network)\n        return {\n            'policy': fee_policy.get_descriptor(),\n            'description': description,\n            'sat/kvB': feerate,\n            'tooltip': tooltip,\n        }\n\n    @command('n')\n    async def test_inject_fee_etas(self, fee_est):\n        \"\"\"\n        Inject fee estimates into the network object, as if they were coming from connected servers.\n        `setconfig 'test_disable_automatic_fee_eta_update' true` to prevent Network from overriding\n        the configured fees.\n        Useful on regtest.\n\n        arg:str:fee_est:dict of ETA-based fee estimates, encoded as str\n        \"\"\"\n        if not isinstance(fee_est, dict):\n            fee_est = ast.literal_eval(fee_est)\n        assert isinstance(fee_est, dict), f\"unexpected type for fee_est. got {repr(fee_est)}\"\n        # populate missing high-block-number estimates using default relay fee.\n        # e.g. {\"25\": 2222} -> {\"25\": 2222, \"144\": 1000, \"1008\": 1000}\n        furthest_estimate = max(fee_est.keys()) if fee_est else 0\n        further_fee_est = {\n            eta_target: FEERATE_DEFAULT_RELAY for eta_target in FEE_ETA_TARGETS\n            if eta_target > furthest_estimate\n        }\n        fee_est.update(further_fee_est)\n        self.network.update_fee_estimates(fee_est=fee_est)\n\n    @command('w')\n    async def removelocaltx(self, txid, wallet: Abstract_Wallet = None):\n        \"\"\"Remove a 'local' transaction from the wallet, and its dependent\n        transactions.\n\n        arg:txid:txid:Transaction ID\n        \"\"\"\n        height = wallet.adb.get_tx_height(txid).height()\n        if height != TX_HEIGHT_LOCAL:\n            raise UserFacingException(\n                f'Only local transactions can be removed. '\n                f'This tx has height: {height} != {TX_HEIGHT_LOCAL}')\n        wallet.adb.remove_transaction(txid)\n        wallet.save_db()\n\n    @command('wn')\n    async def get_tx_status(self, txid, wallet: Abstract_Wallet = None):\n        \"\"\"Returns some information regarding the tx. For now, only confirmations.\n        The transaction must be related to the wallet.\n\n        arg:txid:txid:Transaction ID\n        \"\"\"\n        if not wallet.db.get_transaction(txid):\n            raise UserFacingException(\"Transaction not in wallet.\")\n        return {\n            \"confirmations\": wallet.adb.get_tx_height(txid).conf,\n        }\n\n    @command('')\n    async def help(self):\n        \"\"\"Show help about a command\"\"\"\n        # for the python console\n        return sorted(known_commands.keys())\n\n    # lightning network commands\n    @command('wnl')\n    async def add_peer(self, connection_string, timeout=20, gossip=False, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Connect to a lightning node\n\n        arg:str:connection_string:Lightning network node ID or network address\n        arg:bool:gossip:Apply command to your gossip node instead of wallet node\n        arg:int:timeout:Timeout in seconds (default=20)\n        \"\"\"\n        lnworker = self.network.lngossip if gossip else wallet.lnworker\n        peer = await lnworker.lnpeermgr.add_peer(connection_string)\n        try:\n            await util.wait_for2(peer.initialized, timeout=LN_P2P_NETWORK_TIMEOUT)\n        except (CancelledError, Exception) as e:\n            #  FIXME often simply CancelledError and real cause (e.g. timeout) remains hidden\n            raise UserFacingException(f\"Connection failed: {repr(e)}\")\n        return True\n\n    @command('wnl')\n    async def gossip_info(self, wallet: Abstract_Wallet = None):\n        \"\"\"Display statistics about lightninig gossip\"\"\"\n        lngossip = self.network.lngossip\n        channel_db = lngossip.channel_db\n        forwarded = dict([(key.hex(), p._num_gossip_messages_forwarded) for key, p in wallet.lnworker.lnpeermgr.peers.items()]),\n        out = {\n            'received': {\n                'channel_announcements': lngossip._num_chan_ann,\n                'channel_updates': lngossip._num_chan_upd,\n                'channel_updates_good': lngossip._num_chan_upd_good,\n                'node_announcements': lngossip._num_node_ann,\n            },\n            'database': {\n                'nodes': channel_db.num_nodes,\n                'channels': channel_db.num_channels,\n                'channel_policies': channel_db.num_policies,\n            },\n            'forwarded': forwarded,\n        }\n        return out\n\n    @command('wnl')\n    async def list_peers(self, gossip=False, wallet: Abstract_Wallet = None):\n        \"\"\"\n        List lightning peers of your node\n\n        arg:bool:gossip:Apply command to your gossip node instead of wallet node\n        \"\"\"\n        lnworker = self.network.lngossip if gossip else wallet.lnworker\n        return [{\n            'node_id': p.pubkey.hex(),\n            'address': p.transport.name(),\n            'initialized': p.is_initialized(),\n            'features': str(LnFeatures(p.features)),\n            'channels': [c.funding_outpoint.to_str() for c in p.channels.values()],\n        } for p in lnworker.lnpeermgr.peers.values()]\n\n    @command('wpnl')\n    async def open_channel(self, connection_string, amount, push_amount=0, public=False, zeroconf=False, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Open a lightning channel with a peer\n\n        arg:str:connection_string:Lightning network node ID or network address\n        arg:decimal_or_max:amount:funding amount (in BTC)\n        arg:decimal:push_amount:Push initial amount (in BTC)\n        arg:bool:public:The channel will be announced\n        arg:bool:zeroconf:request zeroconf channel\n        \"\"\"\n        if not wallet.can_have_lightning():\n            raise UserFacingException(\"This wallet cannot create new channels\")\n        funding_sat = satoshis(amount)\n        push_sat = satoshis(push_amount)\n        peer = await wallet.lnworker.lnpeermgr.add_peer(connection_string)\n        chan, funding_tx = await wallet.lnworker.open_channel_with_peer(\n            peer, funding_sat,\n            push_sat=push_sat,\n            public=public,\n            zeroconf=zeroconf,\n            password=password)\n        return chan.funding_outpoint.to_str()\n\n    @command('')\n    async def decode_invoice(self, invoice: str):\n        \"\"\"\n        Decode a lightning invoice\n\n        arg:str:invoice:Lightning invoice (bolt 11)\n        \"\"\"\n        invoice = Invoice.from_bech32(invoice)\n        return invoice.to_debug_json()\n\n    @command('wnpl')\n    async def lnpay(\n        self,\n        invoice: str,\n        timeout: int = 120,\n        max_cltv: Optional[int] = None,\n        max_fee_msat: Optional[int] = None,\n        password=None,\n        wallet: Abstract_Wallet = None\n    ):\n        \"\"\"\n        Pay a lightning invoice\n        Note: it is *not* safe to try paying the same invoice multiple times with a timeout.\n              It is only safe to retry paying the same invoice if there are no more pending HTLCs\n              with the same payment_hash.  # FIXME should there even be a default timeout? just block forever.\n\n        arg:str:invoice:Lightning invoice (bolt 11)\n        arg:int:timeout:Timeout in seconds (default=120)\n        arg:int:max_cltv:Maximum total time lock for the route (default=4032+invoice_final_cltv_delta)\n        arg:int:max_fee_msat:Maximum absolute fee budget for the payment (if unset, the default is a percentage fee based on config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS)\n        \"\"\"\n        # note: The \"timeout\" param works via black magic.\n        #       The CLI-parser stores it in the config, and the argname matches config.cv.CLI_TIMEOUT.key().\n        #       - it works when calling the CLI and there is also a daemon (online command)\n        #       - FIXME it does NOT work when calling an offline command (-o)\n        #       - FIXME it does NOT work when calling RPC directly (e.g. curl)\n        lnworker = wallet.lnworker\n        lnaddr = lnworker._check_bolt11_invoice(invoice)  # also checks if amount is given\n        payment_hash = lnaddr.paymenthash\n        invoice_obj = Invoice.from_bech32(invoice)\n        assert not max_fee_msat or max_fee_msat < max(invoice_obj.amount_msat // 2, 1_000_000), \\\n                                    f\"{max_fee_msat=} > max(invoice amount msat / 2, 1_000_000)\"\n        wallet.save_invoice(invoice_obj)\n        if max_cltv is not None:\n            # The cltv budget excludes the final cltv delta which is why it is deducted here\n            # so the whole used cltv is <= max_cltv\n            assert max_cltv <= NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, \\\n                    f\"{max_cltv=} > {NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE=}\"\n            max_cltv_remaining = max_cltv - lnaddr.get_min_final_cltv_delta()\n            assert max_cltv_remaining > 0, f\"{max_cltv=} - {lnaddr.get_min_final_cltv_delta()=} < 1\"\n            max_cltv = max_cltv_remaining\n        budget = PaymentFeeBudget.from_invoice_amount(\n            config=wallet.config,\n            invoice_amount_msat=invoice_obj.amount_msat,\n            max_cltv_delta=max_cltv,\n            max_fee_msat=max_fee_msat,\n        )\n        success, log = await lnworker.pay_invoice(invoice_obj, budget=budget)\n        return {\n            'payment_hash': payment_hash.hex(),\n            'success': success,\n            'preimage': lnworker.get_preimage(payment_hash).hex() if success else None,\n            'log': [x.formatted_tuple() for x in log]\n        }\n\n    @command('wl')\n    async def nodeid(self, wallet: Abstract_Wallet = None):\n        \"\"\"Return the Lightning Node ID of a wallet\"\"\"\n        listen_addr = self.config.LIGHTNING_LISTEN\n        return wallet.lnworker.node_keypair.pubkey.hex() + (('@' + listen_addr) if listen_addr else '')\n\n    @command('wl')\n    async def list_channels(self, public: bool = False, private: bool = False, active: bool = False, open: bool = False, wallet: Abstract_Wallet = None):\n        \"\"\"Return the list of channels in the wallet\n\n        arg:bool:public:list only public channels\n        arg:bool:private:list only private channels\n        arg:bool:open:list only open channels\n        arg:bool:active:list only active channels\n        \"\"\"\n        from .lnutil import LOCAL, REMOTE, format_short_channel_id\n        if public and private:\n            raise Exception(\"incompatible options\")\n        def _filter(chan):\n            if public and not chan.is_public():\n                return False\n            if private and chan.is_public():\n                return False\n            if active and not chan.is_redeemed():\n                return False\n            if open and not chan.is_open():\n                return False\n            return True\n\n        return [\n            {\n                'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None,\n                'channel_id': chan.channel_id.hex(),\n                'channel_point': chan.funding_outpoint.to_str(),\n                'closing_txid': chan.get_closing_height()[0] if chan.get_closing_height() else None,\n                'state': chan.get_state().name,\n                'peer_state': chan.peer_state.name,\n                'remote_pubkey': chan.node_id.hex(),\n                'local_balance': chan.balance(LOCAL)//1000,\n                'remote_balance': chan.balance(REMOTE)//1000,\n                'local_ctn': chan.get_latest_ctn(LOCAL),\n                'remote_ctn': chan.get_latest_ctn(REMOTE),\n                'local_reserve': chan.config[REMOTE].reserve_sat,  # their config has our reserve\n                'remote_reserve': chan.config[LOCAL].reserve_sat,\n                'local_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(LOCAL, direction=SENT) // 1000,\n                'remote_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(REMOTE, direction=SENT) // 1000,\n            } for chan in wallet.lnworker.channels.values() if _filter(chan)\n        ]\n\n    @command('wl')\n    async def list_channel_backups(self, wallet: Abstract_Wallet = None):\n        \"\"\"Return the list of channel backups in the wallet\"\"\"\n        # FIXME: we need to be online to display capacity of backups\n        from .lnutil import LOCAL, REMOTE, format_short_channel_id\n        return [\n            {\n                'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None,\n                'channel_id': chan.channel_id.hex(),\n                'channel_point': chan.funding_outpoint.to_str(),\n                'closing_txid': chan.get_closing_height()[0] if chan.get_closing_height() else None,\n                'state': chan.get_state().name,\n            } for chan in wallet.lnworker.channel_backups.values()\n        ]\n\n    @command('wnl')\n    async def enable_htlc_settle(self, b: bool, wallet: Abstract_Wallet = None):\n        \"\"\"\n        command used in regtests\n\n        arg:bool:b:boolean\n        \"\"\"\n        wallet.lnworker.enable_htlc_settle = b\n\n    @command('n')\n    async def clear_ln_blacklist(self):\n        if self.network.path_finder:\n            self.network.path_finder.clear_blacklist()\n\n    @command('n')\n    async def reset_liquidity_hints(self):\n        if self.network.path_finder:\n            self.network.path_finder.liquidity_hints.reset_liquidity_hints()\n            self.network.path_finder.clear_blacklist()\n\n    @command('wnpl')\n    async def close_channel(self, channel_point, force=False, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Close a lightning channel.\n        Returns txid of closing tx.\n\n        arg:str:channel_point:channel point\n        arg:bool:force:Force closes (broadcast local commitment transaction)\n        \"\"\"\n        txid, index = channel_point.split(':')\n        chan_id, _ = channel_id_from_funding_tx(txid, int(index))\n        if chan_id not in wallet.lnworker.channels:\n            raise UserFacingException(f'Unknown channel {channel_point}')\n        coro = wallet.lnworker.force_close_channel(chan_id) if force else wallet.lnworker.close_channel(chan_id)\n        return await coro\n\n    @command('wnpl')\n    async def request_force_close(self, channel_point, connection_string=None, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Requests the remote to force close a channel.\n        If a connection string is passed, can be used without having state or any backup for the channel.\n        Assumes that channel was originally opened with the same local peer (node_keypair).\n\n        arg:str:connection_string:Lightning network node ID or network address\n        arg:str:channel_point:channel point\n        \"\"\"\n        txid, index = channel_point.split(':')\n        chan_id, _ = channel_id_from_funding_tx(txid, int(index))\n        if chan_id not in wallet.lnworker.channels and chan_id not in wallet.lnworker.channel_backups:\n            raise UserFacingException(f'Unknown channel {channel_point}')\n        await wallet.lnworker.request_force_close(chan_id, connect_str=connection_string)\n\n    @command('wpl')\n    async def export_channel_backup(self, channel_point, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Returns an encrypted channel backup\n\n        arg:str:channel_point:Channel outpoint\n        \"\"\"\n        txid, index = channel_point.split(':')\n        chan_id, _ = channel_id_from_funding_tx(txid, int(index))\n        if chan_id not in wallet.lnworker.channels:\n            raise UserFacingException(f'Unknown channel {channel_point}')\n        return wallet.lnworker.export_channel_backup(chan_id)\n\n    @command('wl')\n    async def import_channel_backup(self, encrypted, wallet: Abstract_Wallet = None):\n        \"\"\"\n        arg:str:encrypted:Encrypted channel backup\n        \"\"\"\n        return wallet.lnworker.import_channel_backup(encrypted)\n\n    @command('wnpl')\n    async def get_channel_ctx(self, channel_point, password=None, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):\n        \"\"\"\n        return the current commitment transaction of a channel\n\n        arg:str:channel_point:Channel outpoint\n        arg:bool:iknowwhatimdoing:Acknowledge that I understand the full implications of what I am about to do\n        \"\"\"\n        if not iknowwhatimdoing:\n            raise UserFacingException(\n                \"WARNING: this command is potentially unsafe.\\n\"\n                \"To proceed, try again, with the --iknowwhatimdoing option.\")\n        txid, index = channel_point.split(':')\n        chan_id, _ = channel_id_from_funding_tx(txid, int(index))\n        if chan_id not in wallet.lnworker.channels:\n            raise UserFacingException(f'Unknown channel {channel_point}')\n        chan = wallet.lnworker.channels[chan_id]\n        tx = chan.force_close_tx()\n        return tx.serialize()\n\n    @command('wnl')\n    async def get_watchtower_ctn(self, channel_point, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Return the local watchtower's ctn of channel. used in regtests\n\n        arg:str:channel_point:Channel outpoint (txid:index)\n        \"\"\"\n        return wallet.lnworker.get_watchtower_ctn(channel_point)\n\n    @command('wnpl')\n    async def rebalance_channels(self, from_scid, dest_scid, amount, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Rebalance channels.\n        If trampoline is used, channels must be with different trampolines.\n\n        arg:str:from_scid:Short channel ID\n        arg:str:dest_scid:Short channel ID\n        arg:decimal:amount:Amount (in BTC)\n\n        \"\"\"\n        from .lnutil import ShortChannelID\n        from_scid = ShortChannelID.from_str(from_scid)\n        dest_scid = ShortChannelID.from_str(dest_scid)\n        from_channel = wallet.lnworker.get_channel_by_short_id(from_scid)\n        dest_channel = wallet.lnworker.get_channel_by_short_id(dest_scid)\n        amount_sat = satoshis(amount)\n        success, log = await wallet.lnworker.rebalance_channels(\n            from_channel,\n            dest_channel,\n            amount_msat=amount_sat * 1000,\n        )\n        return {\n            'success': success,\n            'log': [x.formatted_tuple() for x in log]\n        }\n\n    @command('wnl')\n    async def get_submarine_swap_providers(self, query_time=15, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Queries nostr relays for available submarine swap providers.\n\n        To configure one of the providers use:\n        setconfig swapserver_npub 'npub...'\n\n        arg:int:query_time:Optional timeout how long the relays should be queried for provider announcements. Default: 15 sec\n        \"\"\"\n        sm = wallet.lnworker.swap_manager\n        async with sm.create_transport() as transport:\n            assert isinstance(transport, NostrTransport)\n            await asyncio.sleep(query_time)\n            offers = transport.get_recent_offers()\n        result = {}\n        for offer in offers:\n            result[offer.server_npub] = {\n                \"percentage_fee\": float(offer.pairs.percentage),\n                \"max_forward_sat\": offer.pairs.max_forward,\n                \"max_reverse_sat\": offer.pairs.max_reverse,\n                \"min_amount_sat\": offer.pairs.min_amount,\n                \"prepayment\": 2 * offer.pairs.mining_fee,\n            }\n        return result\n\n    @command('wnpl')\n    async def normal_swap(self, onchain_amount, lightning_amount, password=None, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Normal submarine swap: send on-chain BTC, receive on Lightning\n\n        arg:decimal_or_dryrun:lightning_amount:Amount to be received, in BTC. Set it to 'dryrun' to receive a value\n        arg:decimal_or_dryrun:onchain_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value\n        \"\"\"\n        sm = wallet.lnworker.swap_manager\n        assert self.config.SWAPSERVER_NPUB or self.config.SWAPSERVER_URL, \\\n            \"Configure swap provider first. See 'get_submarine_swap_providers'.\"\n        async with sm.create_transport() as transport:\n            try:\n                await asyncio.wait_for(sm.is_initialized.wait(), timeout=15)\n            except asyncio.TimeoutError:\n                raise TimeoutError(\"Could not find configured swap provider. Setup another one. See 'get_submarine_swap_providers'\")\n            if lightning_amount == 'dryrun':\n                onchain_amount_sat = satoshis(onchain_amount)\n                lightning_amount_sat = sm.get_recv_amount(onchain_amount_sat, is_reverse=False)\n                txid = None\n            elif onchain_amount == 'dryrun':\n                lightning_amount_sat = satoshis(lightning_amount)\n                onchain_amount_sat = sm.get_send_amount(lightning_amount_sat, is_reverse=False)\n                txid = None\n            else:\n                lightning_amount_sat = satoshis(lightning_amount)\n                onchain_amount_sat = satoshis(onchain_amount)\n                txid = await wallet.lnworker.swap_manager.normal_swap(\n                    transport=transport,\n                    lightning_amount_sat=lightning_amount_sat,\n                    expected_onchain_amount_sat=onchain_amount_sat,\n                    password=password,\n                )\n\n        return {\n            'txid': txid,\n            'lightning_amount': format_satoshis(lightning_amount_sat),\n            'onchain_amount': format_satoshis(onchain_amount_sat),\n        }\n\n    @command('wnpl')\n    async def reverse_swap(\n        self, lightning_amount, onchain_amount, prepayment='dryrun', password=None, wallet: Abstract_Wallet = None,\n    ):\n        \"\"\"\n        Reverse submarine swap: send on Lightning, receive on-chain\n\n        arg:decimal_or_dryrun:lightning_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value\n        arg:decimal_or_dryrun:onchain_amount:Amount to be received, in BTC. Set it to 'dryrun' to receive a value\n        arg:decimal_or_dryrun:prepayment:Lightning payment required by the swap provider in order to cover their mining fees. This is included in lightning_amount. However, this part of the operation is not trustless; the provider is trusted to fail this payment if the swap fails.\n        \"\"\"\n        sm = wallet.lnworker.swap_manager\n        assert self.config.SWAPSERVER_NPUB or self.config.SWAPSERVER_URL, \\\n            \"Configure swap provider first. See 'get_submarine_swap_providers'.\"\n        async with sm.create_transport() as transport:\n            try:\n                await asyncio.wait_for(sm.is_initialized.wait(), timeout=15)\n            except asyncio.TimeoutError:\n                raise TimeoutError(\"Could not find configured swap provider. Setup another one. See 'get_submarine_swap_providers'\")\n            if onchain_amount == 'dryrun':\n                lightning_amount_sat = satoshis(lightning_amount)\n                onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True)\n                assert prepayment == \"dryrun\", f\"Cannot use {prepayment=} in dryrun. Set it to 'dryrun'.\"\n                prepayment_sat = 2 * sm.mining_fee\n                funding_txid = None\n            elif lightning_amount == 'dryrun':\n                onchain_amount_sat = satoshis(onchain_amount)\n                lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True)\n                assert prepayment == \"dryrun\", f\"Cannot use {prepayment=} in dryrun. Set it to 'dryrun'.\"\n                prepayment_sat = 2 * sm.mining_fee\n                funding_txid = None\n            else:\n                lightning_amount_sat = satoshis(lightning_amount)\n                claim_fee = sm.get_fee_for_txbatcher()\n                onchain_amount_sat = satoshis(onchain_amount) + claim_fee\n                assert prepayment != \"dryrun\", \"Provide the 'prepayment' obtained from the dryrun.\"\n                prepayment_sat = satoshis(prepayment)\n                funding_txid = await wallet.lnworker.swap_manager.reverse_swap(\n                    transport=transport,\n                    lightning_amount_sat=lightning_amount_sat,\n                    expected_onchain_amount_sat=onchain_amount_sat,\n                    prepayment_sat=prepayment_sat,\n                )\n        return {\n            'funding_txid': funding_txid,\n            'lightning_amount': format_satoshis(lightning_amount_sat),\n            'onchain_amount': format_satoshis(onchain_amount_sat),\n            'prepayment': format_satoshis(prepayment_sat)\n        }\n\n    @command('n')\n    async def convert_currency(self, from_amount=1, from_ccy='', to_ccy=''):\n        \"\"\"\n        Converts the given amount of currency to another using the\n        configured exchange rate source.\n\n        arg:decimal:from_amount:Amount to convert (default=1)\n        arg:str:from_ccy:Currency to convert from\n        arg:str:to_ccy:Currency to convert to\n        \"\"\"\n        if not self.daemon.fx.is_enabled():\n            raise UserFacingException(\"FX is disabled. To enable, run: 'electrum setconfig use_exchange_rate true'\")\n        # Currency codes are uppercase\n        from_ccy = from_ccy.upper()\n        to_ccy = to_ccy.upper()\n        # Default currencies\n        if from_ccy == '':\n            from_ccy = \"BTC\" if to_ccy != \"BTC\" else self.daemon.fx.ccy\n        if to_ccy == '':\n            to_ccy = \"BTC\" if from_ccy != \"BTC\" else self.daemon.fx.ccy\n        # Get current rates\n        rate_from = self.daemon.fx.exchange.get_cached_spot_quote(from_ccy)\n        rate_to = self.daemon.fx.exchange.get_cached_spot_quote(to_ccy)\n        # Test if currencies exist\n        if rate_from.is_nan():\n            raise UserFacingException(f'Currency to convert from ({from_ccy}) is unknown or rate is unavailable')\n        if rate_to.is_nan():\n            raise UserFacingException(f'Currency to convert to ({to_ccy}) is unknown or rate is unavailable')\n        # Conversion\n        try:\n            from_amount = to_decimal(from_amount)\n            to_amount = from_amount / rate_from * rate_to\n        except InvalidOperation:\n            raise Exception(\"from_amount is not a number\")\n        return {\n            \"from_amount\": self.daemon.fx.ccy_amount_str(from_amount, add_thousands_sep=False, ccy=from_ccy),\n            \"to_amount\": self.daemon.fx.ccy_amount_str(to_amount, add_thousands_sep=False, ccy=to_ccy),\n            \"from_ccy\": from_ccy,\n            \"to_ccy\": to_ccy,\n            \"source\": self.daemon.fx.exchange.name(),\n        }\n\n    @command('wnl')\n    async def send_onion_message(self, node_id_or_blinded_path_hex: str, message: str, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Send an onion message with onionmsg_tlv.message payload to node_id.\n\n        arg:str:node_id_or_blinded_path_hex:node id or blinded path\n        arg:str:message:Message to send\n        \"\"\"\n        assert wallet\n        assert wallet.lnworker\n        assert node_id_or_blinded_path_hex\n        assert message\n\n        node_id_or_blinded_path = bfh(node_id_or_blinded_path_hex)\n        assert len(node_id_or_blinded_path) >= 33\n\n        destination_payload = {\n            'message': {'text': message.encode('utf-8')}\n        }\n\n        try:\n            send_onion_message_to(wallet.lnworker, node_id_or_blinded_path, destination_payload)\n            return {'success': True}\n        except Exception as e:\n            msg = str(e)\n\n        return {\n            'success': False,\n            'msg': msg\n        }\n\n    @command('wnl')\n    async def get_blinded_path_via(self, node_id: str, dummy_hops: int = 0, wallet: Abstract_Wallet = None):\n        \"\"\"\n        Create a blinded path with node_id as introduction point. Introduction point must be direct peer of me.\n\n        arg:str:node_id:Node pubkey in hex format\n        arg:int:dummy_hops:Number of dummy hops to add\n        \"\"\"\n        # TODO: allow introduction_point to not be a direct peer and construct a route\n        assert wallet\n        assert node_id\n\n        pubkey = bfh(node_id)\n        assert len(pubkey) == 33, 'invalid node_id'\n\n        peer = wallet.lnworker.lnpeermgr.peers[pubkey]\n        assert peer, 'node_id not a peer'\n\n        path = [pubkey, wallet.lnworker.node_keypair.pubkey]\n        session_key = os.urandom(32)\n        blinded_path = create_blinded_path(session_key, path=path, final_recipient_data={}, dummy_hops=dummy_hops)\n\n        with io.BytesIO() as blinded_path_fd:\n            OnionWireSerializer.write_field(\n                fd=blinded_path_fd,\n                field_type='blinded_path',\n                count=1,\n                value=blinded_path)\n            encoded_blinded_path = blinded_path_fd.getvalue()\n\n        return encoded_blinded_path.hex()\n\n\ndef plugin_command(s, plugin_name):\n    \"\"\"Decorator to register a cli command inside a plugin. To be used within a commands.py file\n    in the plugins root.\"\"\"\n    # atm all plugin commands require a daemon, cannot be run in 'offline' mode:\n    if 'n' not in s:\n        s += 'n'\n    def decorator(func):\n        assert len(plugin_name) > 0, \"Plugin name must not be empty\"\n        func.plugin_name = plugin_name\n        name = plugin_name + '_' + func.__name__\n        if name in known_commands or hasattr(Commands, name):\n            raise Exception(f\"Command name {name} already exists. Plugin commands should not overwrite other commands.\")\n        assert inspect.iscoroutinefunction(func), f\"Plugin commands must be a coroutine: {name}\"\n\n        @command(s)\n        @wraps(func)\n        async def func_wrapper(*args, **kwargs):\n            cmd_runner = args[0]  # type: Commands\n            daemon = cmd_runner.daemon\n            assert daemon is not None\n            kwargs['plugin'] = daemon._plugins.get_plugin(plugin_name)\n            return await func(*args, **kwargs)\n\n        setattr(Commands, name, func_wrapper)\n        return func_wrapper\n    return decorator\n\n\ndef eval_bool(x: str) -> bool:\n    if x == 'false':\n        return False\n    if x == 'true':\n        return True\n    # assume python, raise if malformed\n    return bool(ast.literal_eval(x))\n\n\n# don't use floats because of rounding errors\njson_loads = lambda x: json.loads(x, parse_float=lambda x: str(to_decimal(x)))\n\n\ndef check_txid(txid):\n    if not is_hash256_str(txid):\n        raise UserFacingException(f\"{repr(txid)} is not a txid\")\n    return txid\n\n\narg_types = {\n    'int': int,\n    'bool': eval_bool,\n    'str': str,\n    'txid': check_txid,\n    'tx': convert_raw_tx_to_hex,\n    'json': json_loads,\n    'decimal': lambda x: str(to_decimal(x)),\n    'decimal_or_dryrun': lambda x: str(to_decimal(x)) if x != 'dryrun' else x,\n    'decimal_or_max': lambda x: str(to_decimal(x)) if not parse_max_spend(x) else x,\n}\n\nconfig_variables = {\n    'addrequest': {\n        'ssl_privkey': 'Path to your SSL private key, needed to sign the request.',\n        'ssl_chain': 'Chain of SSL certificates, needed for signed requests. Put your certificate at the top and the root CA at the end',\n        'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \\\"(\\'file:///var/www/\\',\\'https://electrum.org/\\')\\\"',\n    },\n    'listrequests': {\n        'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \\\"(\\'file:///var/www/\\',\\'https://electrum.org/\\')\\\"',\n    }\n}\n\n\ndef set_default_subparser(self, name, args=None):\n    \"\"\"see http://stackoverflow.com/questions/5176691/argparse-how-to-specify-a-default-subcommand\"\"\"\n    subparser_found = False\n    for arg in sys.argv[1:]:\n        if arg in ['-h', '--help', '--version']:  # global help/version if no subparser\n            break\n    else:\n        for x in self._subparsers._actions:\n            if not isinstance(x, argparse._SubParsersAction):\n                continue\n            for sp_name in x._name_parser_map.keys():\n                if sp_name in sys.argv[1:]:\n                    subparser_found = True\n        if not subparser_found:\n            # insert default in first position, this implies no\n            # global options without a sub_parsers specified\n            if args is None:\n                sys.argv.insert(1, name)\n            else:\n                args.insert(0, name)\n\n\nargparse.ArgumentParser.set_default_subparser = set_default_subparser\n\n\n# workaround https://bugs.python.org/issue23058\n# see https://github.com/nickstenning/honcho/pull/121\n\ndef subparser_call(self, parser, namespace, values, option_string=None):\n    from argparse import ArgumentError, SUPPRESS, _UNRECOGNIZED_ARGS_ATTR\n    parser_name = values[0]\n    arg_strings = values[1:]\n    # set the parser name if requested\n    if self.dest is not SUPPRESS:\n        setattr(namespace, self.dest, parser_name)\n    # select the parser\n    try:\n        parser = self._name_parser_map[parser_name]\n    except KeyError:\n        tup = parser_name, ', '.join(self._name_parser_map)\n        msg = _('unknown parser {!r} (choices: {})').format(*tup)\n        raise ArgumentError(self, msg)\n    # parse all the remaining options into the namespace\n    # store any unrecognized options on the object, so that the top\n    # level parser can decide what to do with them\n    namespace, arg_strings = parser.parse_known_args(arg_strings, namespace)\n    if arg_strings:\n        vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])\n        getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)\n\n\nargparse._SubParsersAction.__call__ = subparser_call\n\n\ndef add_network_options(parser):\n    group = parser.add_argument_group('network options')\n    group.add_argument(\n        \"-f\", \"--serverfingerprint\", dest=SimpleConfig.NETWORK_SERVERFINGERPRINT.key(), default=None,\n        help=\"only allow connecting to servers with a matching SSL certificate SHA256 fingerprint. \" +\n        \"To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.\")\n    group.add_argument(\n        \"-1\", \"--oneserver\", action=\"store_true\", dest=SimpleConfig.NETWORK_ONESERVER.key(), default=None,\n        help=\"connect to one server only\")\n    group.add_argument(\n        \"-s\", \"--server\", dest=SimpleConfig.NETWORK_SERVER.key(), default=None,\n        help=\"set server host:port:protocol, where protocol is either t (tcp) or s (ssl)\")\n    group.add_argument(\n        \"-p\", \"--proxy\", dest=SimpleConfig.NETWORK_PROXY.key(), default=None,\n        help=\"set proxy [type:]host:port (or 'none' to disable proxy), where type is socks4 or socks5\")\n    group.add_argument(\n        \"--proxyuser\", dest=SimpleConfig.NETWORK_PROXY_USER.key(), default=None,\n        help=\"set proxy username\")\n    group.add_argument(\n        \"--proxypassword\", dest=SimpleConfig.NETWORK_PROXY_PASSWORD.key(), default=None,\n        help=\"set proxy password\")\n    group.add_argument(\n        \"--noonion\", action=\"store_true\", dest=SimpleConfig.NETWORK_NOONION.key(), default=None,\n        help=\"do not try to connect to onion servers\")\n    group.add_argument(\n        \"--skipmerklecheck\", action=\"store_true\", dest=SimpleConfig.NETWORK_SKIPMERKLECHECK.key(), default=None,\n        help=\"Tolerate invalid merkle proofs from Electrum server\")\n\n\ndef add_global_options(parser, suppress=False):\n    group = parser.add_argument_group('global options')\n    group.add_argument(\n        \"-v\", dest=\"verbosity\", default='',\n        help=argparse.SUPPRESS if suppress else \"Set verbosity (log levels)\")\n    group.add_argument(\n        \"-D\", \"--dir\", dest=\"electrum_path\",\n        help=argparse.SUPPRESS if suppress else \"electrum directory\")\n    group.add_argument(\n        \"-w\", \"--wallet\", dest=\"wallet_path\",\n        help=argparse.SUPPRESS if suppress else \"wallet path\")\n    group.add_argument(\n        \"-P\", \"--portable\", action=\"store_true\", dest=\"portable\", default=False,\n        help=argparse.SUPPRESS if suppress else \"Use local 'electrum_data' directory\")\n    for chain in constants.NETS_LIST:\n        group.add_argument(\n            f\"--{chain.cli_flag()}\", action=\"store_true\", dest=chain.config_key(), default=False,\n            help=argparse.SUPPRESS if suppress else f\"Use {chain.NET_NAME} chain\")\n    group.add_argument(\n        \"-o\", \"--offline\", action=\"store_true\", dest=SimpleConfig.NETWORK_OFFLINE.key(), default=None,\n        help=argparse.SUPPRESS if suppress else \"Run offline\")\n    group.add_argument(\n        \"--rpcuser\", dest=SimpleConfig.RPC_USERNAME.key(), default=argparse.SUPPRESS,\n        help=argparse.SUPPRESS if suppress else \"RPC user\")\n    group.add_argument(\n        \"--rpcpassword\", dest=SimpleConfig.RPC_PASSWORD.key(), default=argparse.SUPPRESS,\n        help=argparse.SUPPRESS if suppress else \"RPC password\")\n    group.add_argument(\n        \"--forgetconfig\", action=\"store_true\", dest=SimpleConfig.CONFIG_FORGET_CHANGES.key(), default=None,\n        help=argparse.SUPPRESS if suppress else \"Forget config on exit\")\n\n\ndef get_simple_parser():\n    \"\"\" simple parser that figures out the path of the config file and ignore unknown args \"\"\"\n    from optparse import OptionParser, BadOptionError, AmbiguousOptionError\n\n    class PassThroughOptionParser(OptionParser):\n        # see https://stackoverflow.com/questions/1885161/how-can-i-get-optparses-optionparser-to-ignore-invalid-options\n        def _process_args(self, largs, rargs, values):\n            while rargs:\n                try:\n                    OptionParser._process_args(self, largs, rargs, values)\n                except (BadOptionError, AmbiguousOptionError) as e:\n                    largs.append(e.opt_str)\n\n    parser = PassThroughOptionParser()\n    parser.add_option(\"-D\", \"--dir\", dest=\"electrum_path\", help=\"electrum directory\")\n    parser.add_option(\"-P\", \"--portable\", action=\"store_true\", dest=\"portable\", default=False, help=\"Use local 'electrum_data' directory\")\n    for chain in constants.NETS_LIST:\n        parser.add_option(f\"--{chain.cli_flag()}\", action=\"store_true\", dest=chain.config_key(), default=False, help=f\"Use {chain.NET_NAME} chain\")\n    return parser\n\n\ndef get_parser():\n    # create main parser\n    parser = argparse.ArgumentParser(\n        epilog=\"Run 'electrum help <command>' to see the help for a command\")\n    parser.add_argument(\"--version\", dest=\"cmd\", action='store_const', const='version', help=\"Return the version of Electrum.\")\n    add_global_options(parser)\n    subparsers = parser.add_subparsers(dest='cmd', metavar='<command>')\n    # gui\n    parser_gui = subparsers.add_parser('gui', description=\"Run Electrum's Graphical User Interface.\", help=\"Run GUI (default)\")\n    parser_gui.add_argument(\"url\", nargs='?', default=None, help=\"bitcoin URI (or bip70 file)\")\n    parser_gui.add_argument(\"-g\", \"--gui\", dest=SimpleConfig.GUI_NAME.key(), help=\"select graphical user interface\", choices=['qt', 'text', 'stdio', 'qml'])\n    parser_gui.add_argument(\"-m\", action=\"store_true\", dest=SimpleConfig.GUI_QT_HIDE_ON_STARTUP.key(), default=False, help=\"hide GUI on startup\")\n    parser_gui.add_argument(\"-L\", \"--lang\", dest=SimpleConfig.LOCALIZATION_LANGUAGE.key(), default=None, help=\"default language used in GUI\")\n    parser_gui.add_argument(\"--daemon\", action=\"store_true\", dest=\"daemon\", default=False, help=\"keep daemon running after GUI is closed\")\n    parser_gui.add_argument(\"--nosegwit\", action=\"store_true\", dest=SimpleConfig.WIZARD_DONT_CREATE_SEGWIT.key(), default=False, help=\"Do not create segwit wallets\")\n    add_network_options(parser_gui)\n    add_global_options(parser_gui)\n    # daemon\n    parser_daemon = subparsers.add_parser('daemon', help=\"Run Daemon\")\n    parser_daemon.add_argument(\"-d\", \"--detached\", action=\"store_true\", dest=\"detach\", default=False, help=\"run daemon in detached mode\")\n    # FIXME: all these options are rpc-server-side. The CLI client-side cannot use e.g. --rpcport,\n    #        instead it reads it from the daemon lockfile.\n    parser_daemon.add_argument(\"--rpchost\", dest=SimpleConfig.RPC_HOST.key(), default=argparse.SUPPRESS, help=\"RPC host\")\n    parser_daemon.add_argument(\"--rpcport\", dest=SimpleConfig.RPC_PORT.key(), type=int, default=argparse.SUPPRESS, help=\"RPC port\")\n    parser_daemon.add_argument(\"--rpcsock\", dest=SimpleConfig.RPC_SOCKET_TYPE.key(), default=None, help=\"what socket type to which to bind RPC daemon\", choices=['unix', 'tcp', 'auto'])\n    parser_daemon.add_argument(\"--rpcsockpath\", dest=SimpleConfig.RPC_SOCKET_FILEPATH.key(), help=\"where to place RPC file socket\")\n    add_network_options(parser_daemon)\n    add_global_options(parser_daemon)\n    # commands\n    for cmdname in sorted(known_commands.keys()):\n        cmd = known_commands[cmdname]\n        p = subparsers.add_parser(\n            cmdname,\n            description=cmd.description,\n            help=cmd.short_description,\n            formatter_class=argparse.RawDescriptionHelpFormatter,\n            epilog=\"Run 'electrum -h' to see the list of global options\",\n        )\n        for optname, default in zip(cmd.options, cmd.defaults):\n            if optname in ['wallet_path', 'wallet', 'plugin']:\n                continue\n            if optname == 'password':\n                p.add_argument(\"--password\", dest='password', help=\"Wallet password. Use '--password :' if you want a prompt.\")\n                continue\n            help = cmd.arg_descriptions.get(optname)\n            if not help:\n                print(f'undocumented argument {cmdname}::{optname}', file=sys.stderr)\n            action = \"store_true\" if default is False else 'store'\n            if action == 'store':\n                type_descriptor = cmd.arg_types.get(optname)\n                _type = arg_types.get(type_descriptor, str)\n                p.add_argument('--' + optname, dest=optname, action=action, default=default, help=help, type=_type)\n            else:\n                p.add_argument('--' + optname, dest=optname, action=action, default=default, help=help)\n        add_global_options(p, suppress=True)\n\n        for param in cmd.params:\n            if param in ['wallet_path', 'wallet']:\n                continue\n            help = cmd.arg_descriptions.get(param)\n            if not help:\n                print(f'undocumented argument {cmdname}::{param}', file=sys.stderr)\n            type_descriptor = cmd.arg_types.get(param)\n            _type = arg_types.get(type_descriptor)\n            if help is not None and _type is None:\n                print(f'unknown type \\'{_type}\\' for {cmdname}::{param}', file=sys.stderr)\n            p.add_argument(param, help=help, type=_type)\n\n        cvh = config_variables.get(cmdname)\n        if cvh:\n            group = p.add_argument_group('configuration variables', '(set with setconfig/getconfig)')\n            for k, v in cvh.items():\n                group.add_argument(k, nargs='?', help=v)\n\n    # 'gui' is the default command\n    # note: set_default_subparser modifies sys.argv\n    parser.set_default_subparser('gui')\n    return parser\n"
  },
  {
    "path": "electrum/constants.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2018 The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport os\nimport json\nfrom typing import Sequence, Tuple, Mapping, Type, List, Optional\n\nfrom .lntransport import LNPeerAddr\nfrom .util import inv_dict, all_subclasses, classproperty\nfrom . import bitcoin\n\n\ndef read_json(filename, default=None):\n    path = os.path.join(os.path.dirname(__file__), filename)\n    try:\n        with open(path, 'r') as f:\n            r = json.loads(f.read())\n    except Exception:\n        if default is None:\n            # Sometimes it's better to hard-fail: the file might be missing\n            # due to a packaging issue, which might otherwise go unnoticed.\n            raise\n        r = default\n    return r\n\n\ndef create_fallback_node_list(fallback_nodes_dict: dict[str, dict]) -> List[LNPeerAddr]:\n    \"\"\"Take a json dict of fallback nodes like: k:node_id, v:{k:'host', k:'port'} and return LNPeerAddr list\"\"\"\n    fallback_nodes = []\n    for node_id, address in fallback_nodes_dict.items():\n        fallback_nodes.append(\n            LNPeerAddr(host=address['host'], port=int(address['port']), pubkey=bytes.fromhex(node_id)))\n    return fallback_nodes\n\n\nGIT_REPO_URL = \"https://github.com/spesmilo/electrum\"\nGIT_REPO_ISSUES_URL = \"https://github.com/spesmilo/electrum/issues\"\nRELEASE_NOTES_URL = \"https://raw.githubusercontent.com/spesmilo/electrum/refs/heads/master/RELEASE-NOTES\"\nBIP39_WALLET_FORMATS = read_json('bip39_wallet_formats.json')\n\n\nclass AbstractNet:\n\n    NET_NAME: str\n    TESTNET: bool\n    WIF_PREFIX: int\n    ADDRTYPE_P2PKH: int\n    ADDRTYPE_P2SH: int\n    SEGWIT_HRP: str\n    BOLT11_HRP: str\n    GENESIS: str\n    BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS: int = 0\n    BIP44_COIN_TYPE: int\n    LN_REALM_BYTE: int\n    DEFAULT_PORTS: Mapping[str, str]\n    LN_DNS_SEEDS: Sequence[str]\n    XPRV_HEADERS: Mapping[str, int]\n    XPRV_HEADERS_INV: Mapping[int, str]\n    XPUB_HEADERS: Mapping[str, int]\n    XPUB_HEADERS_INV: Mapping[int, str]\n\n    @classmethod\n    def max_checkpoint(cls) -> int:\n        return max(0, len(cls.CHECKPOINTS) * 2016 - 1)\n\n    @classmethod\n    def rev_genesis_bytes(cls) -> bytes:\n        return bytes.fromhex(cls.GENESIS)[::-1]\n\n    @classmethod\n    def set_as_network(cls) -> None:\n        global net\n        net = cls\n\n    _cached_default_servers = None\n    @classproperty\n    def DEFAULT_SERVERS(cls) -> Mapping[str, Mapping[str, str]]:\n        if cls._cached_default_servers is None:\n            default_file = {} if cls.TESTNET else None  # for mainnet we hard-fail if the file is missing.\n            cls._cached_default_servers = read_json(os.path.join('chains', cls.NET_NAME, 'servers.json'), default_file)\n        return cls._cached_default_servers\n\n    _cached_fallback_lnnodes = None\n    @classproperty\n    def FALLBACK_LN_NODES(cls) -> Sequence[LNPeerAddr]:\n        if cls._cached_fallback_lnnodes is None:\n            default_file = {} if cls.TESTNET else None  # for mainnet we hard-fail if the file is missing.\n            d = read_json(os.path.join('chains', cls.NET_NAME, 'fallback_lnnodes.json'), default_file)\n            cls._cached_fallback_lnnodes = create_fallback_node_list(d)\n        return cls._cached_fallback_lnnodes\n\n    _cached_checkpoints = None\n    @classproperty\n    def CHECKPOINTS(cls) -> Sequence[Tuple[str, int]]:\n        if cls._cached_checkpoints is None:\n            default_file = [] if cls.TESTNET else None  # for mainnet we hard-fail if the file is missing.\n            cls._cached_checkpoints = read_json(os.path.join('chains', cls.NET_NAME, 'checkpoints.json'), default_file)\n        return cls._cached_checkpoints\n\n    @classmethod\n    def datadir_subdir(cls) -> Optional[str]:\n        \"\"\"The name of the folder in the filesystem.\n        None means top-level, used by mainnet.\n        \"\"\"\n        return cls.NET_NAME\n\n    @classmethod\n    def cli_flag(cls) -> str:\n        \"\"\"as used in e.g. `$ run_electrum --testnet4`\"\"\"\n        return cls.NET_NAME\n\n    @classmethod\n    def config_key(cls) -> str:\n        \"\"\"as used for SimpleConfig.get()\"\"\"\n        return cls.NET_NAME\n\n\nclass BitcoinMainnet(AbstractNet):\n\n    NET_NAME = \"mainnet\"\n    TESTNET = False\n    WIF_PREFIX = 0x80\n    ADDRTYPE_P2PKH = 0\n    ADDRTYPE_P2SH = 5\n    SEGWIT_HRP = \"bc\"\n    BOLT11_HRP = SEGWIT_HRP\n    GENESIS = \"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\"\n    DEFAULT_PORTS = {'t': '50001', 's': '50002'}\n    BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS = 497000\n\n    XPRV_HEADERS = {\n        'standard':    0x0488ade4,  # xprv\n        'p2wpkh-p2sh': 0x049d7878,  # yprv\n        'p2wsh-p2sh':  0x0295b005,  # Yprv\n        'p2wpkh':      0x04b2430c,  # zprv\n        'p2wsh':       0x02aa7a99,  # Zprv\n    }\n    XPRV_HEADERS_INV = inv_dict(XPRV_HEADERS)\n    XPUB_HEADERS = {\n        'standard':    0x0488b21e,  # xpub\n        'p2wpkh-p2sh': 0x049d7cb2,  # ypub\n        'p2wsh-p2sh':  0x0295b43f,  # Ypub\n        'p2wpkh':      0x04b24746,  # zpub\n        'p2wsh':       0x02aa7ed3,  # Zpub\n    }\n    XPUB_HEADERS_INV = inv_dict(XPUB_HEADERS)\n    BIP44_COIN_TYPE = 0\n    LN_REALM_BYTE = 0\n    LN_DNS_SEEDS = [\n        'nodes.lightning.directory.',\n        'lseed.bitcoinstats.com.',\n        'lseed.darosior.ninja',\n    ]\n\n    @classmethod\n    def datadir_subdir(cls):\n        return None\n\n\nclass BitcoinTestnet(AbstractNet):\n\n    NET_NAME = \"testnet\"\n    TESTNET = True\n    WIF_PREFIX = 0xef\n    ADDRTYPE_P2PKH = 111\n    ADDRTYPE_P2SH = 196\n    SEGWIT_HRP = \"tb\"\n    BOLT11_HRP = SEGWIT_HRP\n    GENESIS = \"000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943\"\n    DEFAULT_PORTS = {'t': '51001', 's': '51002'}\n\n    XPRV_HEADERS = {\n        'standard':    0x04358394,  # tprv\n        'p2wpkh-p2sh': 0x044a4e28,  # uprv\n        'p2wsh-p2sh':  0x024285b5,  # Uprv\n        'p2wpkh':      0x045f18bc,  # vprv\n        'p2wsh':       0x02575048,  # Vprv\n    }\n    XPRV_HEADERS_INV = inv_dict(XPRV_HEADERS)\n    XPUB_HEADERS = {\n        'standard':    0x043587cf,  # tpub\n        'p2wpkh-p2sh': 0x044a5262,  # upub\n        'p2wsh-p2sh':  0x024289ef,  # Upub\n        'p2wpkh':      0x045f1cf6,  # vpub\n        'p2wsh':       0x02575483,  # Vpub\n    }\n    XPUB_HEADERS_INV = inv_dict(XPUB_HEADERS)\n    BIP44_COIN_TYPE = 1\n    LN_REALM_BYTE = 1\n    LN_DNS_SEEDS = [  # TODO investigate this again\n        #'test.nodes.lightning.directory.',  # times out.\n        #'lseed.bitcoinstats.com.',  # ignores REALM byte and returns mainnet peers...\n    ]\n\n\nclass BitcoinTestnet4(BitcoinTestnet):\n\n    NET_NAME = \"testnet4\"\n    GENESIS = \"00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043\"\n    LN_DNS_SEEDS = []\n\n\nclass BitcoinRegtest(BitcoinTestnet):\n\n    NET_NAME = \"regtest\"\n    SEGWIT_HRP = \"bcrt\"\n    BOLT11_HRP = SEGWIT_HRP\n    GENESIS = \"0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206\"\n    LN_DNS_SEEDS = []\n\n\nclass BitcoinSimnet(BitcoinTestnet):\n\n    NET_NAME = \"simnet\"\n    WIF_PREFIX = 0x64\n    ADDRTYPE_P2PKH = 0x3f\n    ADDRTYPE_P2SH = 0x7b\n    SEGWIT_HRP = \"sb\"\n    BOLT11_HRP = SEGWIT_HRP\n    GENESIS = \"683e86bd5c6d110d91b94b97137ba6bfe02dbbdb8e3dff722a669b5d69d77af6\"\n    LN_DNS_SEEDS = []\n\n\nclass BitcoinSignet(BitcoinTestnet):\n\n    NET_NAME = \"signet\"\n    BOLT11_HRP = \"tbs\"\n    GENESIS = \"00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6\"\n    LN_DNS_SEEDS = []\n\n\nclass BitcoinMutinynet(BitcoinTestnet):\n\n    NET_NAME = \"mutinynet\"\n    BOLT11_HRP = \"tbs\"\n    GENESIS = \"00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6\"\n    LN_DNS_SEEDS = []\n\n\nNETS_LIST = tuple(all_subclasses(AbstractNet))  # type: Sequence[Type[AbstractNet]]\nNETS_LIST = tuple(sorted(NETS_LIST, key=lambda x: x.NET_NAME))\n\nassert len(NETS_LIST) == len(set([chain.NET_NAME for chain in NETS_LIST])), \"NET_NAME must be unique for each concrete AbstractNet\"\nassert len(NETS_LIST) == len(set([chain.datadir_subdir() for chain in NETS_LIST])), \"datadir must be unique for each concrete AbstractNet\"\nassert len(NETS_LIST) == len(set([chain.cli_flag() for chain in NETS_LIST])), \"cli_flag must be unique for each concrete AbstractNet\"\nassert len(NETS_LIST) == len(set([chain.config_key() for chain in NETS_LIST])), \"config_key must be unique for each concrete AbstractNet\"\n\n# don't import net directly, import the module instead (so that net is singleton)\nnet = BitcoinMainnet  # type: Type[AbstractNet]\n"
  },
  {
    "path": "electrum/contacts.py",
    "content": "# Electrum - Lightweight Bitcoin Client\n# Copyright (c) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport re\nfrom typing import Optional, Tuple, Dict, Any, TYPE_CHECKING\nimport asyncio\nimport dns\nfrom dns.exception import DNSException\n\nfrom . import bitcoin\nfrom . import dnssec\nfrom .util import read_json_file, write_json_file, to_string, is_valid_email\nfrom .logging import Logger, get_logger\nfrom .util import trigger_callback, get_asyncio_loop\n\nif TYPE_CHECKING:\n    from .wallet_db import WalletDB\n    from .simple_config import SimpleConfig\n\n\n_logger = get_logger(__name__)\n\n\nclass AliasNotFoundException(Exception):\n    pass\n\n\nclass Contacts(dict, Logger):\n\n    def __init__(self, db: 'WalletDB'):\n        Logger.__init__(self)\n        self.db = db\n        d = self.db.get('contacts', {})\n        try:\n            self.update(d)\n        except Exception:\n            return\n        # backward compatibility\n        for k, v in self.items():\n            _type, n = v\n            if _type == 'address' and bitcoin.is_address(n):\n                self.pop(k)\n                self[n] = ('address', k)\n\n    def save(self):\n        self.db.put('contacts', dict(self))\n        trigger_callback('contacts_updated')\n\n    def import_file(self, path):\n        data = read_json_file(path)\n        data = self._validate(data)\n        self.update(data)\n        self.save()\n\n    def export_file(self, path):\n        write_json_file(path, self)\n\n    def __setitem__(self, key, value):\n        dict.__setitem__(self, key, value)\n        self.save()\n\n    def pop(self, key):\n        if key in self.keys():\n            res = dict.pop(self, key)\n            self.save()\n            return res\n        return None\n\n    async def resolve(self, k) -> dict:\n        if bitcoin.is_address(k):\n            return {\n                'address': k,\n                'type': 'address'\n            }\n        for address, (_type, label) in self.items():\n            if k.casefold() != label.casefold():\n                continue\n            if _type in ('address', 'lnaddress'):\n                return {\n                    'address': address,\n                    'type': 'contact'\n                }\n        if openalias := await self.resolve_openalias(k):\n            return openalias\n        raise AliasNotFoundException(\"Invalid Bitcoin address or alias\", k)\n\n    @classmethod\n    async def resolve_openalias(cls, url: str) -> Dict[str, Any]:\n        out = await cls._resolve_openalias(url)\n        if out:\n            address, name = out\n            return {\n                'address': address,\n                'name': name,\n                'type': 'openalias',\n            }\n        return {}\n\n    def by_name(self, name):\n        for k in self.keys():\n            _type, addr = self[k]\n            if addr.casefold() == name.casefold():\n                return {\n                    'name': addr,\n                    'type': _type,\n                    'address': k\n                }\n        return None\n\n    def fetch_openalias(self, config: 'SimpleConfig'):\n        self.alias_info = None\n        alias = config.OPENALIAS_ID\n        if alias:\n            alias = str(alias)\n            async def f():\n                self.alias_info = await self._resolve_openalias(alias)\n                trigger_callback('alias_received')\n            asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop())\n\n    @classmethod\n    async def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str]]:\n        # support email-style addresses, per the OA standard\n        url = url.replace('@', '.')\n        try:\n            records, validated = await dnssec.query(url, dns.rdatatype.TXT)\n        except DNSException as e:\n            _logger.info(f'Error resolving openalias: {repr(e)}')\n            return None\n        if not validated:  # enforce DNSSEC validation. without it, DNS is completely insecure\n            _logger.info(f\"DNSSEC validation failed for {url=!r}, or maybe dependencies are missing and could not even try.\")\n            return None\n        prefix = 'btc'\n        for record in records:\n            if record.rdtype != dns.rdatatype.TXT:\n                continue\n            string = to_string(record.strings[0], 'utf8')\n            if string.startswith('oa1:' + prefix):\n                address = cls.find_regex(string, r'recipient_address=([A-Za-z0-9]+)')\n                name = cls.find_regex(string, r'recipient_name=([^;]+)')\n                if not name:\n                    name = address\n                if not address:\n                    continue\n                return address, name\n        return None\n\n    @staticmethod\n    def find_regex(haystack, needle):\n        regex = re.compile(needle)\n        try:\n            return regex.search(haystack).groups()[0]\n        except AttributeError:\n            return None\n\n    def _validate(self, data):\n        for k, v in list(data.items()):\n            if k == 'contacts':\n                return self._validate(v)\n            if not (bitcoin.is_address(k) or is_valid_email(k)):\n                data.pop(k)\n            else:\n                _type, _ = v\n                if _type not in ('address', 'lnaddress'):\n                    data.pop(k)\n        return data\n\n"
  },
  {
    "path": "electrum/crypto.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2018 The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport base64\nimport binascii\nimport os\nimport sys\nimport hashlib\nimport hmac\nfrom typing import Union, Mapping, Optional\n\nimport electrum_ecc as ecc\n\nfrom .util import assert_bytes, InvalidPassword, to_bytes, to_string, WalletFileException, versiontuple\nfrom .i18n import _\nfrom .logging import get_logger\n\n_logger = get_logger(__name__)\n\n\nHAS_PYAES = False\ntry:\n    import pyaes\nexcept Exception:\n    pass\nelse:\n    HAS_PYAES = True\n\nHAS_CRYPTODOME = False\nMIN_CRYPTODOME_VERSION = \"3.7\"\ntry:\n    import Cryptodome\n    if versiontuple(Cryptodome.__version__) < versiontuple(MIN_CRYPTODOME_VERSION):\n        _logger.warning(f\"found module 'Cryptodome' but it is too old: {Cryptodome.__version__}<{MIN_CRYPTODOME_VERSION}\")\n        raise Exception()\n    from Cryptodome.Cipher import ChaCha20_Poly1305 as CD_ChaCha20_Poly1305\n    from Cryptodome.Cipher import ChaCha20 as CD_ChaCha20\n    from Cryptodome.Cipher import AES as CD_AES\nexcept Exception:\n    pass\nelse:\n    HAS_CRYPTODOME = True\n\nHAS_CRYPTOGRAPHY = False\nMIN_CRYPTOGRAPHY_VERSION = \"2.1\"\ntry:\n    import cryptography\n    if versiontuple(cryptography.__version__) < versiontuple(MIN_CRYPTOGRAPHY_VERSION):\n        _logger.warning(f\"found module 'cryptography' but it is too old: {cryptography.__version__}<{MIN_CRYPTOGRAPHY_VERSION}\")\n        raise Exception()\n    from cryptography import exceptions\n    from cryptography.hazmat.primitives.ciphers import Cipher as CG_Cipher\n    from cryptography.hazmat.primitives.ciphers import algorithms as CG_algorithms\n    from cryptography.hazmat.primitives.ciphers import modes as CG_modes\n    from cryptography.hazmat.backends import default_backend as CG_default_backend\n    import cryptography.hazmat.primitives.ciphers.aead as CG_aead\nexcept Exception:\n    pass\nelse:\n    HAS_CRYPTOGRAPHY = True\n\n\nif not (HAS_CRYPTODOME or HAS_CRYPTOGRAPHY):\n    sys.exit(f\"Error: at least one of ('pycryptodomex', 'cryptography') needs to be installed.\")\n\n\ndef version_info() -> Mapping[str, Optional[str]]:\n    ret = {}\n    if HAS_PYAES:\n        ret[\"pyaes.version\"] = \".\".join(map(str, pyaes.VERSION[:3]))\n    else:\n        ret[\"pyaes.version\"] = None\n    if HAS_CRYPTODOME:\n        ret[\"cryptodome.version\"] = Cryptodome.__version__\n        if hasattr(Cryptodome, \"__path__\"):\n            ret[\"cryptodome.path\"] = \", \".join(Cryptodome.__path__ or [])\n    else:\n        ret[\"cryptodome.version\"] = None\n    if HAS_CRYPTOGRAPHY:\n        ret[\"cryptography.version\"] = cryptography.__version__\n        if hasattr(cryptography, \"__path__\"):\n            ret[\"cryptography.path\"] = \", \".join(cryptography.__path__ or [])\n    else:\n        ret[\"cryptography.version\"] = None\n    return ret\n\n\nclass InvalidPadding(Exception):\n    pass\n\n\nclass CiphertextFormatError(Exception):\n    pass\n\n\ndef append_PKCS7_padding(data: bytes) -> bytes:\n    assert_bytes(data)\n    padlen = 16 - (len(data) % 16)\n    return data + bytes([padlen]) * padlen\n\n\ndef strip_PKCS7_padding(data: bytes) -> bytes:\n    assert_bytes(data)\n    if len(data) % 16 != 0 or len(data) == 0:\n        raise InvalidPadding(\"invalid length\")\n    padlen = data[-1]\n    if not (0 < padlen <= 16):\n        raise InvalidPadding(\"invalid padding byte (out of range)\")\n    for i in data[-padlen:]:\n        if i != padlen:\n            raise InvalidPadding(\"invalid padding byte (inconsistent)\")\n    return data[0:-padlen]\n\n\ndef aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:\n    assert_bytes(key, iv, data)\n    data = append_PKCS7_padding(data)\n    if HAS_CRYPTODOME:\n        e = CD_AES.new(key, CD_AES.MODE_CBC, iv).encrypt(data)\n    elif HAS_CRYPTOGRAPHY:\n        cipher = CG_Cipher(CG_algorithms.AES(key), CG_modes.CBC(iv), backend=CG_default_backend())\n        encryptor = cipher.encryptor()\n        e = encryptor.update(data) + encryptor.finalize()\n    elif HAS_PYAES:\n        aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv)\n        aes = pyaes.Encrypter(aes_cbc, padding=pyaes.PADDING_NONE)\n        e = aes.feed(data) + aes.feed()  # empty aes.feed() flushes buffer\n    else:\n        raise Exception(\"no AES backend found\")\n    return e\n\n\ndef aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:\n    assert_bytes(key, iv, data)\n    if HAS_CRYPTODOME:\n        cipher = CD_AES.new(key, CD_AES.MODE_CBC, iv)\n        data = cipher.decrypt(data)\n    elif HAS_CRYPTOGRAPHY:\n        cipher = CG_Cipher(CG_algorithms.AES(key), CG_modes.CBC(iv), backend=CG_default_backend())\n        decryptor = cipher.decryptor()\n        data = decryptor.update(data) + decryptor.finalize()\n    elif HAS_PYAES:\n        aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv)\n        aes = pyaes.Decrypter(aes_cbc, padding=pyaes.PADDING_NONE)\n        data = aes.feed(data) + aes.feed()  # empty aes.feed() flushes buffer\n    else:\n        raise Exception(\"no AES backend found\")\n    try:\n        return strip_PKCS7_padding(data)\n    except InvalidPadding:\n        raise InvalidPassword()\n\n\ndef EncodeAES_bytes(secret: bytes, msg: bytes) -> bytes:\n    assert_bytes(msg)\n    iv = bytes(os.urandom(16))\n    ct = aes_encrypt_with_iv(secret, iv, msg)\n    return iv + ct\n\n\ndef DecodeAES_bytes(secret: bytes, ciphertext: bytes) -> bytes:\n    assert_bytes(ciphertext)\n    iv, e = ciphertext[:16], ciphertext[16:]\n    s = aes_decrypt_with_iv(secret, iv, e)\n    return s\n\n\nPW_HASH_VERSION_LATEST = 1\nKNOWN_PW_HASH_VERSIONS = (1, 2,)\nSUPPORTED_PW_HASH_VERSIONS = (1,)\nassert PW_HASH_VERSION_LATEST in KNOWN_PW_HASH_VERSIONS\nassert PW_HASH_VERSION_LATEST in SUPPORTED_PW_HASH_VERSIONS\n\n\nclass UnexpectedPasswordHashVersion(InvalidPassword, WalletFileException):\n    def __init__(self, version):\n        InvalidPassword.__init__(self)\n        WalletFileException.__init__(self)\n        self.version = version\n\n    def __str__(self):\n        return \"{unexpected}: {version}\\n{instruction}\".format(\n            unexpected=_(\"Unexpected password hash version\"),\n            version=self.version,\n            instruction=_('You are most likely using an outdated version of Electrum. Please update.'))\n\n\nclass UnsupportedPasswordHashVersion(InvalidPassword, WalletFileException):\n    def __init__(self, version):\n        InvalidPassword.__init__(self)\n        WalletFileException.__init__(self)\n        self.version = version\n\n    def __str__(self):\n        return \"{unsupported}: {version}\\n{instruction}\".format(\n            unsupported=_(\"Unsupported password hash version\"),\n            version=self.version,\n            instruction=f\"To open this wallet, try 'git checkout password_v{self.version}'.\\n\"\n                        \"Alternatively, restore from seed.\")\n\n\ndef _hash_password(password: Union[bytes, str], *, version: int) -> bytes:\n    pw = to_bytes(password, 'utf8')\n    if version not in SUPPORTED_PW_HASH_VERSIONS:\n        raise UnsupportedPasswordHashVersion(version)\n    if version == 1:\n        return sha256d(pw)\n    else:\n        assert version not in KNOWN_PW_HASH_VERSIONS\n        raise UnexpectedPasswordHashVersion(version)\n\n\ndef _pw_encode_raw(data: bytes, password: Union[bytes, str], *, version: int) -> bytes:\n    if version not in KNOWN_PW_HASH_VERSIONS:\n        raise UnexpectedPasswordHashVersion(version)\n    # derive key from password\n    secret = _hash_password(password, version=version)\n    # encrypt given data\n    ciphertext = EncodeAES_bytes(secret, data)\n    return ciphertext\n\n\ndef _pw_decode_raw(data_bytes: bytes, password: Union[bytes, str], *, version: int) -> bytes:\n    if version not in KNOWN_PW_HASH_VERSIONS:\n        raise UnexpectedPasswordHashVersion(version)\n    # derive key from password\n    secret = _hash_password(password, version=version)\n    # decrypt given data\n    try:\n        d = DecodeAES_bytes(secret, data_bytes)\n    except Exception as e:\n        raise InvalidPassword() from e\n    return d\n\n\ndef pw_encode_bytes(data: bytes, password: Union[bytes, str], *, version: int) -> str:\n    \"\"\"plaintext bytes -> base64 ciphertext\"\"\"\n    ciphertext = _pw_encode_raw(data, password, version=version)\n    ciphertext_b64 = base64.b64encode(ciphertext)\n    return ciphertext_b64.decode('utf8')\n\n\ndef pw_decode_bytes(data: str, password: Union[bytes, str], *, version:int) -> bytes:\n    \"\"\"base64 ciphertext -> plaintext bytes\"\"\"\n    if version not in KNOWN_PW_HASH_VERSIONS:\n        raise UnexpectedPasswordHashVersion(version)\n    try:\n        data_bytes = bytes(base64.b64decode(data, validate=True))\n    except binascii.Error as e:\n        raise CiphertextFormatError(\"ciphertext not valid base64\") from e\n    return _pw_decode_raw(data_bytes, password, version=version)\n\n\ndef pw_encode_with_version_and_mac(data: bytes, password: Union[bytes, str]) -> str:\n    \"\"\"plaintext bytes -> base64 ciphertext\"\"\"\n    # https://crypto.stackexchange.com/questions/202/should-we-mac-then-encrypt-or-encrypt-then-mac\n    # Encrypt-and-MAC. The MAC will be used to detect invalid passwords\n    version = PW_HASH_VERSION_LATEST\n    mac = sha256(data)[0:4]\n    ciphertext = _pw_encode_raw(data, password, version=version)\n    ciphertext_b64 = base64.b64encode(bytes([version]) + ciphertext + mac)\n    return ciphertext_b64.decode('utf8')\n\n\ndef pw_decode_with_version_and_mac(data: str, password: Union[bytes, str]) -> bytes:\n    \"\"\"base64 ciphertext -> plaintext bytes\"\"\"\n    try:\n        data_bytes = bytes(base64.b64decode(data, validate=True))\n    except binascii.Error as e:\n        raise CiphertextFormatError(\"ciphertext not valid base64\") from e\n    version = int(data_bytes[0])\n    encrypted = data_bytes[1:-4]\n    mac = data_bytes[-4:]\n    if version not in KNOWN_PW_HASH_VERSIONS:\n        raise UnexpectedPasswordHashVersion(version)\n    decrypted = _pw_decode_raw(encrypted, password, version=version)\n    if sha256(decrypted)[0:4] != mac:\n        raise InvalidPassword()\n    return decrypted\n\n\ndef pw_encode(data: str, password: Union[bytes, str, None], *, version: int) -> str:\n    \"\"\"plaintext str -> base64 ciphertext\"\"\"\n    if not password:\n        return data\n    plaintext_bytes = to_bytes(data, \"utf8\")\n    return pw_encode_bytes(plaintext_bytes, password, version=version)\n\n\ndef pw_decode(data: str, password: Union[bytes, str, None], *, version: int) -> str:\n    \"\"\"base64 ciphertext -> plaintext str\"\"\"\n    if password is None:\n        return data\n    plaintext_bytes = pw_decode_bytes(data, password, version=version)\n    try:\n        plaintext_str = to_string(plaintext_bytes, \"utf8\")\n    except UnicodeDecodeError as e:\n        raise InvalidPassword() from e\n    return plaintext_str\n\n\ndef sha256(x: Union[bytes, str]) -> bytes:\n    x = to_bytes(x, 'utf8')\n    return bytes(hashlib.sha256(x).digest())\n\n\ndef sha256d(x: Union[bytes, str]) -> bytes:\n    x = to_bytes(x, 'utf8')\n    out = bytes(sha256(sha256(x)))\n    return out\n\n\ndef hash_160(x: bytes) -> bytes:\n    return ripemd(sha256(x))\n\ndef ripemd(x: bytes) -> bytes:\n    try:\n        md = hashlib.new('ripemd160')\n        md.update(x)\n        return md.digest()\n    except BaseException:\n        # ripemd160 is not guaranteed to be available in hashlib on all platforms.\n        # Historically, our Android builds had hashlib/openssl which did not have it.\n        # see https://github.com/spesmilo/electrum/issues/7093\n        # We bundle a pure python implementation as fallback that gets used now:\n        from . import ripemd\n        md = ripemd.new(x)\n        return md.digest()\n\n\ndef hmac_oneshot(key: bytes, msg: bytes, digest) -> bytes:\n    return hmac.digest(key, msg, digest)\n\n\ndef chacha20_poly1305_encrypt(\n        *,\n        key: bytes,\n        nonce: bytes,\n        associated_data: bytes = None,\n        data: bytes\n) -> bytes:\n    assert isinstance(key, (bytes, bytearray))\n    assert isinstance(nonce, (bytes, bytearray))\n    assert isinstance(associated_data, (bytes, bytearray, type(None)))\n    assert isinstance(data, (bytes, bytearray))\n    assert len(key) == 32, f\"unexpected key size: {len(key)} (expected: 32)\"\n    assert len(nonce) == 12, f\"unexpected nonce size: {len(nonce)} (expected: 12)\"\n    if HAS_CRYPTODOME:\n        cipher = CD_ChaCha20_Poly1305.new(key=key, nonce=nonce)\n        if associated_data is not None:\n            cipher.update(associated_data)\n        ciphertext, mac = cipher.encrypt_and_digest(plaintext=data)\n        return ciphertext + mac\n    if HAS_CRYPTOGRAPHY:\n        a = CG_aead.ChaCha20Poly1305(key)\n        return a.encrypt(nonce, data, associated_data)\n    raise Exception(\"no chacha20 backend found\")\n\n\ndef chacha20_poly1305_decrypt(\n        *,\n        key: bytes,\n        nonce: bytes,\n        associated_data: bytes = None,\n        data: bytes\n) -> bytes:\n    assert isinstance(key, (bytes, bytearray))\n    assert isinstance(nonce, (bytes, bytearray))\n    assert isinstance(associated_data, (bytes, bytearray, type(None)))\n    assert isinstance(data, (bytes, bytearray))\n    assert len(key) == 32, f\"unexpected key size: {len(key)} (expected: 32)\"\n    assert len(nonce) == 12, f\"unexpected nonce size: {len(nonce)} (expected: 12)\"\n    if HAS_CRYPTODOME:\n        cipher = CD_ChaCha20_Poly1305.new(key=key, nonce=nonce)\n        if associated_data is not None:\n            cipher.update(associated_data)\n        # raises ValueError if not valid (e.g. incorrect MAC)\n        return cipher.decrypt_and_verify(ciphertext=data[:-16], received_mac_tag=data[-16:])\n    if HAS_CRYPTOGRAPHY:\n        a = CG_aead.ChaCha20Poly1305(key)\n        try:\n            return a.decrypt(nonce, data, associated_data)\n        except cryptography.exceptions.InvalidTag as e:\n            raise ValueError(\"invalid tag\") from e\n    raise Exception(\"no chacha20 backend found\")\n\n\ndef chacha20_encrypt(*, key: bytes, nonce: bytes, data: bytes) -> bytes:\n    \"\"\"note: for any new protocol you design, please consider using chacha20_poly1305_encrypt instead\n             (for its Authenticated Encryption property).\n    \"\"\"\n    assert isinstance(key, (bytes, bytearray))\n    assert isinstance(nonce, (bytes, bytearray))\n    assert isinstance(data, (bytes, bytearray))\n    assert len(key) == 32, f\"unexpected key size: {len(key)} (expected: 32)\"\n    assert len(nonce) in (8, 12), f\"unexpected nonce size: {len(nonce)} (expected: 8 or 12)\"\n    if HAS_CRYPTODOME:\n        cipher = CD_ChaCha20.new(key=key, nonce=nonce)\n        return cipher.encrypt(data)\n    if HAS_CRYPTOGRAPHY:\n        nonce = bytes(16 - len(nonce)) + nonce  # cryptography wants 16 byte nonces\n        algo = CG_algorithms.ChaCha20(key=key, nonce=nonce)\n        cipher = CG_Cipher(algo, mode=None, backend=CG_default_backend())\n        encryptor = cipher.encryptor()\n        return encryptor.update(data)\n    raise Exception(\"no chacha20 backend found\")\n\n\ndef chacha20_decrypt(*, key: bytes, nonce: bytes, data: bytes) -> bytes:\n    assert isinstance(key, (bytes, bytearray))\n    assert isinstance(nonce, (bytes, bytearray))\n    assert isinstance(data, (bytes, bytearray))\n    assert len(key) == 32, f\"unexpected key size: {len(key)} (expected: 32)\"\n    assert len(nonce) in (8, 12), f\"unexpected nonce size: {len(nonce)} (expected: 8 or 12)\"\n    if HAS_CRYPTODOME:\n        cipher = CD_ChaCha20.new(key=key, nonce=nonce)\n        return cipher.decrypt(data)\n    if HAS_CRYPTOGRAPHY:\n        nonce = bytes(16 - len(nonce)) + nonce  # cryptography wants 16 byte nonces\n        algo = CG_algorithms.ChaCha20(key=key, nonce=nonce)\n        cipher = CG_Cipher(algo, mode=None, backend=CG_default_backend())\n        decryptor = cipher.decryptor()\n        return decryptor.update(data)\n    raise Exception(\"no chacha20 backend found\")\n\n\ndef ecies_encrypt_message(\n    ec_pubkey: 'ecc.ECPubkey',\n    message: bytes,\n    *,\n    magic: bytes = b'BIE1',\n) -> bytes:\n    \"\"\"\n        ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac\n    \"\"\"\n    assert_bytes(message)\n    ephemeral = ecc.ECPrivkey.generate_random_key()\n    ecdh_key = (ec_pubkey * ephemeral.secret_scalar).get_public_key_bytes(compressed=True)\n    key = hashlib.sha512(ecdh_key).digest()\n    iv, key_e, key_m = key[0:16], key[16:32], key[32:]\n    ciphertext = aes_encrypt_with_iv(key_e, iv, message)\n    ephemeral_pubkey = ephemeral.get_public_key_bytes(compressed=True)\n    encrypted = magic + ephemeral_pubkey + ciphertext\n    mac = hmac_oneshot(key_m, encrypted, hashlib.sha256)\n    return base64.b64encode(encrypted + mac)\n\n\ndef ecies_decrypt_message(\n    ec_privkey: 'ecc.ECPrivkey',\n    encrypted: Union[str, bytes],\n    *,\n    magic: bytes = b'BIE1',\n) -> bytes:\n    encrypted = base64.b64decode(encrypted, validate=True)  # type: bytes\n    if len(encrypted) < 85:\n        raise Exception('invalid ciphertext: length')\n    magic_found = encrypted[:4]\n    ephemeral_pubkey_bytes = encrypted[4:37]\n    ciphertext = encrypted[37:-32]\n    mac = encrypted[-32:]\n    if magic_found != magic:\n        raise Exception('invalid ciphertext: invalid magic bytes')\n    try:\n        ephemeral_pubkey = ecc.ECPubkey(ephemeral_pubkey_bytes)\n    except ecc.InvalidECPointException as e:\n        raise Exception('invalid ciphertext: invalid ephemeral pubkey') from e\n    ecdh_key = (ephemeral_pubkey * ec_privkey.secret_scalar).get_public_key_bytes(compressed=True)\n    key = hashlib.sha512(ecdh_key).digest()\n    iv, key_e, key_m = key[0:16], key[16:32], key[32:]\n    if mac != hmac_oneshot(key_m, encrypted[:-32], hashlib.sha256):\n        raise InvalidPassword()\n    return aes_decrypt_with_iv(key_e, iv, ciphertext)\n\n\ndef get_ecdh(priv: bytes, pub: bytes) -> bytes:\n    pt = ecc.ECPubkey(pub) * ecc.string_to_number(priv)\n    return sha256(pt.get_public_key_bytes())\n\ndef privkey_to_pubkey(priv: bytes) -> bytes:\n    return ecc.ECPrivkey(priv[:32]).get_public_key_bytes()\n"
  },
  {
    "path": "electrum/currencies.json",
    "content": "{\n    \"Bit2C\": [\n        \"ILS\"\n    ],\n    \"BitFinex\": [\n        \"EUR\",\n        \"GBP\",\n        \"JPY\",\n        \"TRY\",\n        \"USD\",\n        \"UST\"\n    ],\n    \"BitFlyer\": [\n        \"JPY\"\n    ],\n    \"BitPay\": [\n        \"AED\",\n        \"AFN\",\n        \"ALL\",\n        \"AMD\",\n        \"ANG\",\n        \"AOA\",\n        \"APE\",\n        \"ARS\",\n        \"AUD\",\n        \"AWG\",\n        \"AZN\",\n        \"BAM\",\n        \"BBD\",\n        \"BCH\",\n        \"BDT\",\n        \"BGN\",\n        \"BHD\",\n        \"BIF\",\n        \"BMD\",\n        \"BND\",\n        \"BOB\",\n        \"BRL\",\n        \"BSD\",\n        \"BTC\",\n        \"BTN\",\n        \"BWP\",\n        \"BYN\",\n        \"BZD\",\n        \"CAD\",\n        \"CDF\",\n        \"CHF\",\n        \"CLF\",\n        \"CLP\",\n        \"CNY\",\n        \"COP\",\n        \"CRC\",\n        \"CUP\",\n        \"CVE\",\n        \"CZK\",\n        \"DAI\",\n        \"DJF\",\n        \"DKK\",\n        \"DOP\",\n        \"DZD\",\n        \"EGP\",\n        \"ETB\",\n        \"ETH\",\n        \"EUR\",\n        \"FJD\",\n        \"FKP\",\n        \"GBP\",\n        \"GEL\",\n        \"GHS\",\n        \"GIP\",\n        \"GMD\",\n        \"GNF\",\n        \"GTQ\",\n        \"GYD\",\n        \"HKD\",\n        \"HNL\",\n        \"HRK\",\n        \"HTG\",\n        \"HUF\",\n        \"IDR\",\n        \"ILS\",\n        \"INR\",\n        \"IQD\",\n        \"IRR\",\n        \"ISK\",\n        \"JEP\",\n        \"JMD\",\n        \"JOD\",\n        \"JPY\",\n        \"KES\",\n        \"KGS\",\n        \"KHR\",\n        \"KMF\",\n        \"KPW\",\n        \"KRW\",\n        \"KWD\",\n        \"KYD\",\n        \"KZT\",\n        \"LAK\",\n        \"LBP\",\n        \"LKR\",\n        \"LRD\",\n        \"LSL\",\n        \"LTC\",\n        \"LYD\",\n        \"MAD\",\n        \"MDL\",\n        \"MGA\",\n        \"MKD\",\n        \"MMK\",\n        \"MNT\",\n        \"MOP\",\n        \"MRU\",\n        \"MUR\",\n        \"MVR\",\n        \"MWK\",\n        \"MXN\",\n        \"MYR\",\n        \"MZN\",\n        \"NAD\",\n        \"NGN\",\n        \"NIO\",\n        \"NOK\",\n        \"NPR\",\n        \"NZD\",\n        \"OMR\",\n        \"PAB\",\n        \"PAX\",\n        \"PEN\",\n        \"PGK\",\n        \"PHP\",\n        \"PKR\",\n        \"PLN\",\n        \"PYG\",\n        \"QAR\",\n        \"RON\",\n        \"RSD\",\n        \"RUB\",\n        \"RWF\",\n        \"SAR\",\n        \"SBD\",\n        \"SCR\",\n        \"SDG\",\n        \"SEK\",\n        \"SGD\",\n        \"SHP\",\n        \"SLL\",\n        \"SOS\",\n        \"SRD\",\n        \"STN\",\n        \"SVC\",\n        \"SYP\",\n        \"SZL\",\n        \"THB\",\n        \"TJS\",\n        \"TMT\",\n        \"TND\",\n        \"TOP\",\n        \"TRY\",\n        \"TTD\",\n        \"TWD\",\n        \"TZS\",\n        \"UAH\",\n        \"UGX\",\n        \"USD\",\n        \"UYU\",\n        \"UZS\",\n        \"VES\",\n        \"VND\",\n        \"VUV\",\n        \"WST\",\n        \"XAF\",\n        \"XAG\",\n        \"XAU\",\n        \"XCD\",\n        \"XOF\",\n        \"XPF\",\n        \"XRP\",\n        \"YER\",\n        \"ZAR\",\n        \"ZMW\",\n        \"ZWL\"\n    ],\n    \"BitStamp\": [\n        \"USD\",\n        \"EUR\",\n        \"GBP\"\n    ],\n    \"Bitbank\": [\n        \"JPY\"\n    ],\n    \"Bitso\": [\n        \"MXN\"\n    ],\n    \"Bitvalor\": [\n        \"BRL\"\n    ],\n    \"BlockchainInfo\": [\n        \"ARS\",\n        \"AUD\",\n        \"BRL\",\n        \"CAD\",\n        \"CHF\",\n        \"CLP\",\n        \"CNY\",\n        \"CZK\",\n        \"DKK\",\n        \"EUR\",\n        \"GBP\",\n        \"HKD\",\n        \"HRK\",\n        \"HUF\",\n        \"INR\",\n        \"ISK\",\n        \"JPY\",\n        \"KRW\",\n        \"NZD\",\n        \"PLN\",\n        \"RON\",\n        \"RUB\",\n        \"SEK\",\n        \"SGD\",\n        \"THB\",\n        \"TRY\",\n        \"TWD\",\n        \"USD\"\n    ],\n    \"Bylls\": [\n        \"CAD\"\n    ],\n    \"CoinCap\": [\n        \"USD\"\n    ],\n    \"CoinDesk\": [\n        \"AED\",\n        \"AFN\",\n        \"ALL\",\n        \"AMD\",\n        \"ANG\",\n        \"AOA\",\n        \"ARS\",\n        \"AUD\",\n        \"AWG\",\n        \"AZN\",\n        \"BAM\",\n        \"BBD\",\n        \"BDT\",\n        \"BGN\",\n        \"BHD\",\n        \"BIF\",\n        \"BMD\",\n        \"BND\",\n        \"BOB\",\n        \"BRL\",\n        \"BSD\",\n        \"BTC\",\n        \"BTN\",\n        \"BWP\",\n        \"BYR\",\n        \"BZD\",\n        \"CAD\",\n        \"CDF\",\n        \"CHF\",\n        \"CLF\",\n        \"CLP\",\n        \"CNY\",\n        \"COP\",\n        \"CRC\",\n        \"CUC\",\n        \"CUP\",\n        \"CVE\",\n        \"CZK\",\n        \"DJF\",\n        \"DKK\",\n        \"DOP\",\n        \"DZD\",\n        \"EGP\",\n        \"ERN\",\n        \"ETB\",\n        \"EUR\",\n        \"FJD\",\n        \"FKP\",\n        \"GBP\",\n        \"GEL\",\n        \"GGP\",\n        \"GHS\",\n        \"GIP\",\n        \"GMD\",\n        \"GNF\",\n        \"GTQ\",\n        \"GYD\",\n        \"HKD\",\n        \"HNL\",\n        \"HRK\",\n        \"HTG\",\n        \"HUF\",\n        \"IDR\",\n        \"ILS\",\n        \"IMP\",\n        \"INR\",\n        \"IQD\",\n        \"IRR\",\n        \"ISK\",\n        \"JEP\",\n        \"JMD\",\n        \"JOD\",\n        \"JPY\",\n        \"KES\",\n        \"KGS\",\n        \"KHR\",\n        \"KMF\",\n        \"KPW\",\n        \"KRW\",\n        \"KWD\",\n        \"KYD\",\n        \"KZT\",\n        \"LAK\",\n        \"LBP\",\n        \"LKR\",\n        \"LRD\",\n        \"LSL\",\n        \"LYD\",\n        \"MAD\",\n        \"MDL\",\n        \"MGA\",\n        \"MKD\",\n        \"MMK\",\n        \"MNT\",\n        \"MOP\",\n        \"MRU\",\n        \"MUR\",\n        \"MVR\",\n        \"MWK\",\n        \"MXN\",\n        \"MYR\",\n        \"MZN\",\n        \"NAD\",\n        \"NGN\",\n        \"NIO\",\n        \"NOK\",\n        \"NPR\",\n        \"NZD\",\n        \"OMR\",\n        \"PAB\",\n        \"PEN\",\n        \"PGK\",\n        \"PHP\",\n        \"PKR\",\n        \"PLN\",\n        \"PYG\",\n        \"QAR\",\n        \"RON\",\n        \"RSD\",\n        \"RUB\",\n        \"RWF\",\n        \"SAR\",\n        \"SBD\",\n        \"SCR\",\n        \"SDG\",\n        \"SEK\",\n        \"SGD\",\n        \"SHP\",\n        \"SLL\",\n        \"SOS\",\n        \"SRD\",\n        \"STD\",\n        \"STN\",\n        \"SVC\",\n        \"SYP\",\n        \"SZL\",\n        \"THB\",\n        \"TJS\",\n        \"TMT\",\n        \"TND\",\n        \"TOP\",\n        \"TRY\",\n        \"TTD\",\n        \"TWD\",\n        \"TZS\",\n        \"UAH\",\n        \"UGX\",\n        \"USD\",\n        \"UYU\",\n        \"UZS\",\n        \"VES\",\n        \"VND\",\n        \"VUV\",\n        \"WST\",\n        \"XAF\",\n        \"XAG\",\n        \"XAU\",\n        \"XBT\",\n        \"XCD\",\n        \"XDR\",\n        \"XOF\",\n        \"XPF\",\n        \"YER\",\n        \"ZAR\",\n        \"ZMW\",\n        \"ZWL\"\n    ],\n    \"CoinGecko\": [\n        \"AED\",\n        \"ARS\",\n        \"AUD\",\n        \"BCH\",\n        \"BDT\",\n        \"BHD\",\n        \"BMD\",\n        \"BNB\",\n        \"BRL\",\n        \"BTC\",\n        \"CAD\",\n        \"CHF\",\n        \"CLP\",\n        \"CNY\",\n        \"CZK\",\n        \"DKK\",\n        \"DOT\",\n        \"EOS\",\n        \"ETH\",\n        \"EUR\",\n        \"GBP\",\n        \"GEL\",\n        \"HKD\",\n        \"HUF\",\n        \"IDR\",\n        \"ILS\",\n        \"INR\",\n        \"JPY\",\n        \"KRW\",\n        \"KWD\",\n        \"LKR\",\n        \"LTC\",\n        \"MMK\",\n        \"MXN\",\n        \"MYR\",\n        \"NGN\",\n        \"NOK\",\n        \"NZD\",\n        \"PHP\",\n        \"PKR\",\n        \"PLN\",\n        \"RUB\",\n        \"SAR\",\n        \"SEK\",\n        \"SGD\",\n        \"THB\",\n        \"TRY\",\n        \"TWD\",\n        \"UAH\",\n        \"USD\",\n        \"VEF\",\n        \"VND\",\n        \"XAG\",\n        \"XAU\",\n        \"XDR\",\n        \"XLM\",\n        \"XRP\",\n        \"YFI\",\n        \"ZAR\"\n    ],\n    \"Coinbase\": [\n        \"ABT\",\n        \"ACH\",\n        \"ACS\",\n        \"ACX\",\n        \"ADA\",\n        \"AED\",\n        \"AFN\",\n        \"AKT\",\n        \"ALL\",\n        \"AMD\",\n        \"AMP\",\n        \"ANG\",\n        \"ANT\",\n        \"AOA\",\n        \"APE\",\n        \"APT\",\n        \"ARB\",\n        \"ARS\",\n        \"ASM\",\n        \"AST\",\n        \"ATA\",\n        \"AUD\",\n        \"AVT\",\n        \"AWG\",\n        \"AXL\",\n        \"AXS\",\n        \"AZN\",\n        \"BAL\",\n        \"BAM\",\n        \"BAT\",\n        \"BBD\",\n        \"BCH\",\n        \"BDT\",\n        \"BGN\",\n        \"BHD\",\n        \"BIF\",\n        \"BIT\",\n        \"BLZ\",\n        \"BMD\",\n        \"BND\",\n        \"BNT\",\n        \"BOB\",\n        \"BRL\",\n        \"BSD\",\n        \"BSV\",\n        \"BTC\",\n        \"BTN\",\n        \"BWP\",\n        \"BYN\",\n        \"BYR\",\n        \"BZD\",\n        \"C98\",\n        \"CAD\",\n        \"CDF\",\n        \"CHF\",\n        \"CHZ\",\n        \"CLF\",\n        \"CLP\",\n        \"CLV\",\n        \"CNH\",\n        \"CNY\",\n        \"COP\",\n        \"COW\",\n        \"CRC\",\n        \"CRO\",\n        \"CRV\",\n        \"CTX\",\n        \"CUC\",\n        \"CUP\",\n        \"CVC\",\n        \"CVE\",\n        \"CVX\",\n        \"CZK\",\n        \"DAI\",\n        \"DAR\",\n        \"DDX\",\n        \"DIA\",\n        \"DJF\",\n        \"DKK\",\n        \"DNT\",\n        \"DOP\",\n        \"DOT\",\n        \"DYP\",\n        \"DZD\",\n        \"EEK\",\n        \"EGP\",\n        \"ELA\",\n        \"ENJ\",\n        \"ENS\",\n        \"EOS\",\n        \"ERN\",\n        \"ETB\",\n        \"ETC\",\n        \"ETH\",\n        \"EUR\",\n        \"FET\",\n        \"FIL\",\n        \"FIS\",\n        \"FJD\",\n        \"FKP\",\n        \"FLR\",\n        \"FOX\",\n        \"FTM\",\n        \"GAL\",\n        \"GBP\",\n        \"GEL\",\n        \"GFI\",\n        \"GGP\",\n        \"GHS\",\n        \"GIP\",\n        \"GLM\",\n        \"GMD\",\n        \"GMT\",\n        \"GNF\",\n        \"GNO\",\n        \"GNT\",\n        \"GRT\",\n        \"GST\",\n        \"GTC\",\n        \"GTQ\",\n        \"GYD\",\n        \"HFT\",\n        \"HKD\",\n        \"HNL\",\n        \"HNT\",\n        \"HRK\",\n        \"HTG\",\n        \"HUF\",\n        \"ICP\",\n        \"IDR\",\n        \"ILS\",\n        \"ILV\",\n        \"IMP\",\n        \"IMX\",\n        \"INJ\",\n        \"INR\",\n        \"INV\",\n        \"IQD\",\n        \"IRR\",\n        \"ISK\",\n        \"JEP\",\n        \"JMD\",\n        \"JOD\",\n        \"JPY\",\n        \"JTO\",\n        \"JUP\",\n        \"KES\",\n        \"KGS\",\n        \"KHR\",\n        \"KMF\",\n        \"KNC\",\n        \"KPW\",\n        \"KRL\",\n        \"KRW\",\n        \"KSM\",\n        \"KWD\",\n        \"KYD\",\n        \"KZT\",\n        \"LAK\",\n        \"LBP\",\n        \"LCX\",\n        \"LDO\",\n        \"LIT\",\n        \"LKR\",\n        \"LPT\",\n        \"LRC\",\n        \"LRD\",\n        \"LSL\",\n        \"LTC\",\n        \"LTL\",\n        \"LVL\",\n        \"LYD\",\n        \"MAD\",\n        \"MDL\",\n        \"MDT\",\n        \"MGA\",\n        \"MIR\",\n        \"MKD\",\n        \"MKR\",\n        \"MLN\",\n        \"MMK\",\n        \"MNT\",\n        \"MOP\",\n        \"MPL\",\n        \"MRO\",\n        \"MRU\",\n        \"MTL\",\n        \"MUR\",\n        \"MVR\",\n        \"MWK\",\n        \"MXC\",\n        \"MXN\",\n        \"MYR\",\n        \"MZN\",\n        \"NAD\",\n        \"NCT\",\n        \"NGN\",\n        \"NIO\",\n        \"NKN\",\n        \"NMR\",\n        \"NOK\",\n        \"NPR\",\n        \"NZD\",\n        \"OGN\",\n        \"OMG\",\n        \"OMR\",\n        \"ORN\",\n        \"OXT\",\n        \"PAB\",\n        \"PAX\",\n        \"PEN\",\n        \"PGK\",\n        \"PHP\",\n        \"PKR\",\n        \"PLA\",\n        \"PLN\",\n        \"PLU\",\n        \"PNG\",\n        \"POL\",\n        \"PRO\",\n        \"PRQ\",\n        \"PYG\",\n        \"PYR\",\n        \"QAR\",\n        \"QNT\",\n        \"RAD\",\n        \"RAI\",\n        \"RBN\",\n        \"REN\",\n        \"REP\",\n        \"REQ\",\n        \"RGT\",\n        \"RLC\",\n        \"RLY\",\n        \"RON\",\n        \"RPL\",\n        \"RSD\",\n        \"RUB\",\n        \"RWF\",\n        \"SAR\",\n        \"SBD\",\n        \"SCR\",\n        \"SDG\",\n        \"SEI\",\n        \"SEK\",\n        \"SGD\",\n        \"SHP\",\n        \"SKK\",\n        \"SKL\",\n        \"SLL\",\n        \"SNT\",\n        \"SNX\",\n        \"SOL\",\n        \"SOS\",\n        \"SPA\",\n        \"SRD\",\n        \"SSP\",\n        \"STD\",\n        \"STG\",\n        \"STX\",\n        \"SUI\",\n        \"SVC\",\n        \"SYN\",\n        \"SYP\",\n        \"SZL\",\n        \"THB\",\n        \"TIA\",\n        \"TJS\",\n        \"TMM\",\n        \"TMT\",\n        \"TND\",\n        \"TOP\",\n        \"TRB\",\n        \"TRU\",\n        \"TRY\",\n        \"TTD\",\n        \"TVK\",\n        \"TWD\",\n        \"TZS\",\n        \"UAH\",\n        \"UGX\",\n        \"UMA\",\n        \"UNI\",\n        \"UPI\",\n        \"USD\",\n        \"UST\",\n        \"UYU\",\n        \"UZS\",\n        \"VEF\",\n        \"VES\",\n        \"VET\",\n        \"VGX\",\n        \"VND\",\n        \"VUV\",\n        \"WST\",\n        \"XAF\",\n        \"XAG\",\n        \"XAU\",\n        \"XBC\",\n        \"XCD\",\n        \"XCN\",\n        \"XDR\",\n        \"XLM\",\n        \"XOF\",\n        \"XPD\",\n        \"XPF\",\n        \"XPT\",\n        \"XRP\",\n        \"XTZ\",\n        \"XYO\",\n        \"YER\",\n        \"YFI\",\n        \"ZAR\",\n        \"ZEC\",\n        \"ZEN\",\n        \"ZMK\",\n        \"ZMW\",\n        \"ZRX\",\n        \"ZWD\"\n    ],\n    \"CointraderMonitor\": [\n        \"BRL\"\n    ],\n    \"Kraken\": [\n        \"CAD\",\n        \"EUR\",\n        \"GBP\",\n        \"JPY\",\n        \"USD\"\n    ],\n    \"Walltime\": [\n        \"BRL\"\n    ],\n    \"Yadio\": [\n        \"AED\",\n        \"ALL\",\n        \"ANG\",\n        \"AOA\",\n        \"ARS\",\n        \"AUD\",\n        \"AZN\",\n        \"BDT\",\n        \"BGN\",\n        \"BHD\",\n        \"BIF\",\n        \"BMD\",\n        \"BOB\",\n        \"BRL\",\n        \"BTC\",\n        \"BWP\",\n        \"BYN\",\n        \"BZD\",\n        \"CAD\",\n        \"CDF\",\n        \"CHF\",\n        \"CLP\",\n        \"CNY\",\n        \"COP\",\n        \"CRC\",\n        \"CUP\",\n        \"CZK\",\n        \"DJF\",\n        \"DKK\",\n        \"DOP\",\n        \"DZD\",\n        \"EGP\",\n        \"ETB\",\n        \"EUR\",\n        \"GBP\",\n        \"GEL\",\n        \"GHS\",\n        \"GNF\",\n        \"GTQ\",\n        \"HKD\",\n        \"HNL\",\n        \"HUF\",\n        \"IDR\",\n        \"ILS\",\n        \"INR\",\n        \"IRR\",\n        \"IRT\",\n        \"ISK\",\n        \"JMD\",\n        \"JOD\",\n        \"JPY\",\n        \"KES\",\n        \"KGS\",\n        \"KRW\",\n        \"KZT\",\n        \"LBP\",\n        \"LKR\",\n        \"MAD\",\n        \"MGA\",\n        \"MLC\",\n        \"MRU\",\n        \"MWK\",\n        \"MXN\",\n        \"MYR\",\n        \"NAD\",\n        \"NGN\",\n        \"NIO\",\n        \"NOK\",\n        \"NPR\",\n        \"NZD\",\n        \"PAB\",\n        \"PEN\",\n        \"PHP\",\n        \"PKR\",\n        \"PLN\",\n        \"PYG\",\n        \"QAR\",\n        \"RON\",\n        \"RSD\",\n        \"RUB\",\n        \"RWF\",\n        \"SAR\",\n        \"SEK\",\n        \"SGD\",\n        \"SYP\",\n        \"THB\",\n        \"TND\",\n        \"TRY\",\n        \"TTD\",\n        \"TWD\",\n        \"TZS\",\n        \"UAH\",\n        \"UGX\",\n        \"USD\",\n        \"UYU\",\n        \"UZS\",\n        \"VES\",\n        \"VND\",\n        \"XAF\",\n        \"XAG\",\n        \"XAU\",\n        \"XOF\",\n        \"XPT\",\n        \"ZAR\",\n        \"ZMW\"\n    ],\n    \"Zaif\": [\n        \"JPY\"\n    ]\n}"
  },
  {
    "path": "electrum/daemon.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport asyncio\nimport ast\nimport errno\nimport os\nimport time\nimport traceback\nimport sys\nimport threading\nfrom typing import Dict, Optional, Tuple, Callable, Union, Sequence, Mapping, TYPE_CHECKING\nfrom base64 import b64decode, b64encode\nimport json\nimport socket\n\nimport aiohttp\nfrom aiohttp import web, client_exceptions\nfrom aiorpcx import ignore_after\n\nfrom . import util\nfrom .network import Network\nfrom .util import (\n    json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare, InvalidPassword,\n    log_exceptions, randrange, OldTaskGroup, UserFacingException, JsonRPCError\n)\nfrom .wallet import Wallet, Abstract_Wallet\nfrom .storage import WalletStorage\nfrom .wallet_db import WalletDB, WalletUnfinished\nfrom .commands import known_commands, Commands\nfrom .simple_config import SimpleConfig\nfrom .exchange_rate import FxThread\nfrom .logging import get_logger, Logger\nfrom . import GuiImportError\nfrom .plugin import run_hook, Plugins\n\nif TYPE_CHECKING:\n    from electrum import gui\n\n\n_logger = get_logger(__name__)\n\n\nclass DaemonNotRunning(Exception):\n    pass\n\n\ndef get_rpcsock_defaultpath(config: SimpleConfig):\n    return os.path.join(config.path, 'daemon_rpc_socket')\n\n\ndef get_rpcsock_default_type(config: SimpleConfig):\n    if config.RPC_PORT:\n        return 'tcp'\n    # Use unix domain sockets when available,\n    # with the extra paranoia that in case windows \"implements\" them,\n    # we want to test it before making it the default there.\n    if hasattr(socket, 'AF_UNIX') and sys.platform != 'win32':\n        return 'unix'\n    return 'tcp'\n\n\ndef get_lockfile(config: SimpleConfig):\n    return os.path.join(config.path, 'daemon')\n\n\ndef remove_lockfile(lockfile):\n    os.unlink(lockfile)\n\n\ndef get_file_descriptor(config: SimpleConfig):\n    '''Tries to create the lockfile, using O_EXCL to\n    prevent races.  If it succeeds, it returns the FD.\n    Otherwise, try and connect to the server specified in the lockfile.\n    If this succeeds, the server is returned.  Otherwise, remove the\n    lockfile and try again.'''\n    lockfile = get_lockfile(config)\n    while True:\n        try:\n            return os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)\n        except OSError:\n            pass\n        try:\n            request(config, 'ping')\n            return None\n        except DaemonNotRunning:\n            # Couldn't connect; remove lockfile and try again.\n            remove_lockfile(lockfile)\n\n\ndef request(config: SimpleConfig, endpoint, args=(), timeout: Union[float, int] = 60):\n    lockfile = get_lockfile(config)\n    for attempt in range(5):\n        create_time = None  # type: Optional[float | int]\n        path = None\n        try:\n            with open(lockfile) as f:\n                socktype, address, create_time = ast.literal_eval(f.read())\n                int(create_time)  # raise if not numeric\n                if socktype == 'unix':\n                    path = address\n                    (host, port) = \"127.0.0.1\", 0\n                    # We still need a host and port for e.g. HTTP Host header\n                elif socktype == 'tcp':\n                    (host, port) = address\n                else:\n                    raise Exception(f\"corrupt lockfile; socktype={socktype!r}\")\n        except Exception:\n            raise DaemonNotRunning()\n        rpc_user, rpc_password = get_rpc_credentials(config)\n        server_url = 'http://%s:%d' % (host, port)\n        auth = aiohttp.BasicAuth(login=rpc_user, password=rpc_password)\n        loop = util.get_asyncio_loop()\n\n        async def request_coroutine(\n            *, socktype=socktype, path=path, auth=auth, server_url=server_url, endpoint=endpoint,\n        ):\n            if socktype == 'unix':\n                connector = aiohttp.UnixConnector(path=path)\n            elif socktype == 'tcp':\n                connector = None # This will transform into TCP.\n            else:\n                raise Exception(f\"impossible socktype ({socktype!r})\")\n            async with aiohttp.ClientSession(auth=auth, connector=connector) as session:\n                c = util.JsonRPCClient(session, server_url)\n                return await c.request(endpoint, *args)\n\n        try:\n            fut = asyncio.run_coroutine_threadsafe(request_coroutine(), loop)\n            return fut.result(timeout=timeout)\n        except aiohttp.client_exceptions.ClientConnectorError as e:\n            _logger.info(f\"failed to connect to JSON-RPC server {e}\")\n            # We cannot communicate with the daemon.\n            # If daemon's creation time is very recent, it might still be starting up.\n            # In any other case, we raise: - too old create_time means daemon is likely dead,\n            #                              - create_time in future means our clock cannot be trusted.\n            if not (create_time <= time.time() <= create_time + 1.0):\n                raise DaemonNotRunning()\n        # Sleep a bit and try again; daemon might have just been started\n        time.sleep(1.0)\n    # how did we even get here?! the clock must be going haywire.\n    _logger.error(f\"Failed to connect to JSON-RPC server. Exhausted all attempts.\")\n    raise DaemonNotRunning()\n\n\ndef wait_until_daemon_becomes_ready(*, config: SimpleConfig, timeout=5) -> bool:\n    t0 = time.monotonic()\n    while True:\n        if time.monotonic() > t0 + timeout:\n            return False  # timeout\n        try:\n            request(config, 'ping')\n            return True  # success\n        except DaemonNotRunning:\n            time.sleep(0.05)\n            continue\n\n\ndef get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:\n    rpc_user = config.RPC_USERNAME or None\n    rpc_password = config.RPC_PASSWORD or None\n    if rpc_user is None or rpc_password is None:\n        rpc_user = 'user'\n        bits = 128\n        nbytes = bits // 8 + (bits % 8 > 0)\n        pw_int = randrange(pow(2, bits))\n        pw_b64 = b64encode(\n            pw_int.to_bytes(nbytes, 'big'), b'-_')\n        rpc_password = to_string(pw_b64, 'ascii')\n        config.RPC_USERNAME = rpc_user\n        config.RPC_PASSWORD = rpc_password\n    return rpc_user, rpc_password\n\n\nclass AuthenticationError(Exception):\n    pass\n\n\nclass AuthenticationInvalidOrMissing(AuthenticationError):\n    pass\n\n\nclass AuthenticationCredentialsInvalid(AuthenticationError):\n    pass\n\n\nclass AuthenticatedServer(Logger):\n\n    def __init__(self, rpc_user, rpc_password):\n        Logger.__init__(self)\n        self.rpc_user = rpc_user\n        self.rpc_password = rpc_password\n        self.auth_lock = asyncio.Lock()\n        self._methods = {}  # type: Dict[str, Callable]\n\n    def register_method(self, name: str, f):\n        assert name not in self._methods, f\"name collision for {name}\"\n        self._methods[name] = f\n\n    async def authenticate(self, headers):\n        if self.rpc_password == '':\n            # RPC authentication is disabled\n            return\n        auth_string = headers.get('Authorization', None)\n        if auth_string is None:\n            raise AuthenticationInvalidOrMissing('CredentialsMissing')\n        basic, _, encoded = auth_string.partition(' ')\n        if basic != 'Basic':\n            raise AuthenticationInvalidOrMissing('UnsupportedType')\n        encoded = to_bytes(encoded, 'utf8')\n        credentials = to_string(b64decode(encoded, validate=True), 'utf8')\n        username, _, password = credentials.partition(':')\n        if not (constant_time_compare(username, self.rpc_user)\n                and constant_time_compare(password, self.rpc_password)):\n            await asyncio.sleep(0.050)\n            raise AuthenticationCredentialsInvalid('Invalid Credentials')\n\n    async def handle(self, request):\n        async with self.auth_lock:\n            try:\n                await self.authenticate(request.headers)\n            except AuthenticationInvalidOrMissing:\n                return web.Response(headers={\"WWW-Authenticate\": \"Basic realm=Electrum\"},\n                                    text='Unauthorized', status=401)\n            except AuthenticationCredentialsInvalid:\n                return web.Response(text='Forbidden', status=403)\n        try:\n            request = await request.text()\n            request = json.loads(request)\n            method = request['method']\n            _id = request['id']\n            params = request.get('params', [])  # type: Union[Sequence, Mapping]\n            if method not in self._methods:\n                raise Exception(f\"attempting to use unregistered method: {method}\")\n            f = self._methods[method]\n        except Exception as e:\n            self.logger.exception(\"invalid request\")\n            return web.Response(text='Invalid Request', status=500)\n        response = {\n            'id': _id,\n            'jsonrpc': '2.0',\n        }\n        try:\n            if isinstance(params, dict):\n                response['result'] = await f(**params)\n            else:\n                response['result'] = await f(*params)\n        except UserFacingException as e:\n            response['error'] = {\n                'code': JsonRPCError.Codes.USERFACING,\n                'message': str(e),\n            }\n        except BaseException as e:\n            self.logger.exception(\"internal error while executing RPC\")\n            response['error'] = {\n                'code': JsonRPCError.Codes.INTERNAL,\n                'message': \"internal error while executing RPC\",\n                'data': {\n                    \"exception\": repr(e),\n                    \"traceback\": \"\".join(traceback.format_exception(e)),\n                },\n            }\n        return web.json_response(response)\n\n\nclass CommandsServer(AuthenticatedServer):\n\n    def __init__(self, daemon: 'Daemon', fd):\n        rpc_user, rpc_password = get_rpc_credentials(daemon.config)\n        AuthenticatedServer.__init__(self, rpc_user, rpc_password)\n        self.daemon = daemon\n        self.fd = fd\n        self.config = daemon.config\n        sockettype = self.config.RPC_SOCKET_TYPE\n        self.socktype = sockettype if sockettype != 'auto' else get_rpcsock_default_type(self.config)\n        self.sockpath = self.config.RPC_SOCKET_FILEPATH or get_rpcsock_defaultpath(self.config)\n        self.host = self.config.RPC_HOST\n        self.port = self.config.RPC_PORT\n        self.app = web.Application()\n        self.app.router.add_post(\"/\", self.handle)\n        self.register_method('ping', self.ping)\n        self.register_method('gui', self.gui)\n        self.cmd_runner = Commands(config=self.config, network=self.daemon.network, daemon=self.daemon)\n        for cmdname in known_commands:\n            self.register_method(cmdname, getattr(self.cmd_runner, cmdname))\n        self.register_method('run_cmdline', self.run_cmdline)\n\n    def _socket_config_str(self) -> str:\n        if self.socktype == 'unix':\n            return f\"<socket type={self.socktype}, path={self.sockpath}>\"\n        elif self.socktype == 'tcp':\n            return f\"<socket type={self.socktype}, host={self.host}, port={self.port}>\"\n        else:\n            raise Exception(f\"unknown socktype '{self.socktype!r}'\")\n\n    async def run(self):\n        self.runner = web.AppRunner(self.app)\n        await self.runner.setup()\n        if self.socktype == 'unix':\n            site = web.UnixSite(self.runner, self.sockpath)\n        elif self.socktype == 'tcp':\n            site = web.TCPSite(self.runner, self.host, self.port)\n        else:\n            raise Exception(f\"unknown socktype '{self.socktype!r}'\")\n        try:\n            await site.start()\n        except Exception as e:\n            raise Exception(f\"failed to start CommandsServer at {self._socket_config_str()}. got exc: {e!r}\") from None\n        socket = site._server.sockets[0]\n        if self.socktype == 'unix':\n            addr = self.sockpath\n        elif self.socktype == 'tcp':\n            addr = socket.getsockname()\n        else:\n            raise Exception(f\"impossible socktype ({self.socktype!r})\")\n        os.write(self.fd, bytes(repr((self.socktype, addr, time.time())), 'utf8'))\n        os.close(self.fd)\n        self.logger.info(f\"now running and listening. socktype={self.socktype}, addr={addr}\")\n\n    async def ping(self):\n        return True\n\n    async def gui(self, config_options):\n        # note: \"config_options\" is coming from the short-lived CLI-invocation,\n        #        while self.config is the config of the long-lived daemon process.\n        #       \"config_options\" should have priority.\n        if self.daemon.gui_object:\n            if hasattr(self.daemon.gui_object, 'new_window'):\n                if config_options.get(SimpleConfig.NETWORK_OFFLINE.key()) and not self.config.NETWORK_OFFLINE:\n                    raise UserFacingException(\n                        \"error: current GUI is running online, so it cannot open a new wallet offline.\")\n                path = config_options.get('wallet_path') or self.config.get_wallet_path()\n                self.daemon.gui_object.new_window(path, config_options.get('url'))\n                return True\n            else:\n                raise UserFacingException(\"error: current GUI does not support multiple windows\")\n        else:\n            raise UserFacingException(\"error: Electrum is running in daemon mode. Please stop the daemon first.\")\n\n    async def run_cmdline(self, config_options):\n        cmdname = config_options['cmd']\n        cmd = known_commands.get(cmdname)\n        if not cmd:\n            return f\"unknown command: {cmdname}\"\n        # arguments passed to function\n        args = [config_options.get(x) for x in cmd.params]\n        # decode json arguments\n        args = [json_decode(i) for i in args]\n        # options\n        kwargs = {}\n        for x in cmd.options:\n            kwargs[x] = config_options.get(x)\n        if 'wallet_path' in cmd.options or 'wallet' in cmd.options:\n            wallet_path = config_options.get('wallet_path')\n            if len(self.daemon._wallets) > 1 and wallet_path is None:\n                raise UserFacingException(\"error: wallet not specified\")\n            kwargs['wallet_path'] = wallet_path\n        func = getattr(self.cmd_runner, cmd.name)\n        # execute requested command now.  note: cmd can raise, the caller (self.handle) will wrap it.\n        result = await func(*args, **kwargs)\n        return result\n\n\nclass Daemon(Logger):\n\n    network: Optional[Network] = None\n    gui_object: Optional['gui.BaseElectrumGui'] = None\n\n    @profiler\n    def __init__(\n        self,\n        config: SimpleConfig,\n        fd=None,\n        *,\n        listen_jsonrpc: bool = True,\n        start_network: bool = True,  # setting to False allows customising network settings before starting it\n    ):\n        Logger.__init__(self)\n        self.config = config\n        self.listen_jsonrpc = listen_jsonrpc\n        if fd is None and listen_jsonrpc:\n            fd = get_file_descriptor(config)\n            if fd is None:\n                raise Exception('failed to lock daemon; already running?')\n        self._plugins = None  # type: Optional[Plugins]\n        self.asyncio_loop = util.get_asyncio_loop()\n        if not self.config.NETWORK_OFFLINE:\n            self.network = Network(config, daemon=self)\n        self.fx = FxThread(config=config)\n        # wallet_key -> wallet\n        self._wallets = {}  # type: Dict[str, Abstract_Wallet]\n        self._wallet_lock = threading.RLock()\n\n        self._stop_entered = False\n        self._stopping_soon_or_errored = threading.Event()\n        self._stopped_event = threading.Event()\n\n        self.taskgroup = OldTaskGroup()\n        asyncio.run_coroutine_threadsafe(self._run(), self.asyncio_loop)\n        if start_network and self.network:\n            self.start_network()\n        # Setup commands server\n        self.commands_server = None\n        if listen_jsonrpc:\n            self.commands_server = CommandsServer(self, fd)\n            asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.commands_server.run()), self.asyncio_loop)\n\n    @log_exceptions\n    async def _run(self):\n        self.logger.info(\"starting taskgroup.\")\n        try:\n            async with self.taskgroup as group:\n                await group.spawn(asyncio.Event().wait)  # run forever (until cancel)\n        except Exception as e:\n            self.logger.exception(\"taskgroup died.\")\n            util.send_exception_to_crash_reporter(e)\n        finally:\n            self.logger.info(\"taskgroup stopped.\")\n            # note: we could just \"await self.stop()\", but in that case GUI users would\n            #       not see the exception (especially if the GUI did not start yet).\n            self._stopping_soon_or_errored.set()\n\n    def start_network(self):\n        self.logger.info(f\"starting network.\")\n        assert not self.config.NETWORK_OFFLINE\n        assert self.network\n        self.network.start(jobs=[self.fx.run])\n        # prepare lightning functionality, also load channel db early\n        if self.config.LIGHTNING_USE_GOSSIP:\n            self.network.start_gossip()\n\n    @staticmethod\n    def _wallet_key_from_path(path) -> str:\n        \"\"\"This does stricter path standardization than 'standardize_path'.\n        It is used for keying the _wallets dict,\n        but MUST NOT be used as a *path* for the actual filesystem operations. (see #8495)\n        \"\"\"\n        path = standardize_path(path)\n        # The extra normalisation makes it even harder to open the same wallet file multiple times simultaneously.\n        # - \"realpath\" resolves symlinks:\n        #   note: the path returned by realpath has been observed NOT to work for FS operations!\n        #         (e.g. for Cryptomator WinFSP/FUSE mounts, see #8495).\n        #         It is okay for us to use it for computing a canonical wallet *key*, but cannot be used as a path!\n        try:\n            path = os.path.realpath(path, strict=False)\n        except OSError as e:  # see #10182\n            _logger.warning(f\"could not parse {path!r}: {e!r}\")\n            path = path\n        # - \"normcase\" does Windows-specific case and slash normalisation:\n        path = os.path.normcase(path)\n        # - prepend header to break usage of wallet keys as fs paths\n        header = \"WALLETKEY-\"\n        return header + str(path)\n\n    def with_wallet_lock(func):\n        def func_wrapper(self: 'Daemon', *args, **kwargs):\n            with self._wallet_lock:\n                return func(self, *args, **kwargs)\n        return func_wrapper\n\n    @with_wallet_lock\n    def load_wallet(\n        self,\n        path,\n        password: Optional[str],\n        *,\n        upgrade: bool = False,\n        force_check_password: bool = False,\n    ) -> Optional[Abstract_Wallet]:\n        \"\"\"\n        force_check_password: if False, the password arg is only used if it needed to decrypt the storage.\n                              if True, the password arg is always validated.\n        \"\"\"\n        assert password != ''\n        path = standardize_path(path)\n        wallet_key = self._wallet_key_from_path(path)\n        # wizard will be launched if we return\n        if wallet := self._wallets.get(wallet_key):\n            if force_check_password:\n                wallet.check_password(password)\n            if self.config.get('wallet_path') is None:\n                self.config.CURRENT_WALLET = path\n            return wallet\n        wallet = self._load_wallet(\n            path, password, upgrade=upgrade, config=self.config, force_check_password=force_check_password)\n        if self.network:\n            wallet.start_network(self.network)\n        elif wallet.lnworker:\n            # in offline mode, we need to trigger callbacks\n            coro = wallet.lnworker.lnwatcher.trigger_callbacks(requires_synchronizer=False)\n            asyncio.run_coroutine_threadsafe(coro, self.asyncio_loop)\n        self.add_wallet(wallet)\n        if self.config.get('wallet_path') is None:\n            self.config.CURRENT_WALLET = path\n        self.update_recently_opened_wallets(path)\n        return wallet\n\n\n    @staticmethod\n    @profiler\n    def _load_wallet(\n            path,\n            password: Optional[str],\n            *,\n            upgrade: bool = False,\n            config: SimpleConfig,\n            force_check_password: bool = False,  # if set, always validate password\n    ) -> Optional[Abstract_Wallet]:\n        path = standardize_path(path)\n        storage = WalletStorage(path, allow_partial_writes=config.WALLET_PARTIAL_WRITES)\n        if not storage.file_exists():\n            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)\n        if storage.is_encrypted():\n            if not password:\n                raise InvalidPassword('No password given')\n            storage.decrypt(password)\n        # read data, pass it to db\n        db = WalletDB(storage.read(), storage=storage, upgrade=upgrade)\n        if db.get_action():\n            raise WalletUnfinished(db)\n        wallet = Wallet(db, config=config)\n        if force_check_password:\n            wallet.check_password(password)\n        return wallet\n\n    @with_wallet_lock\n    def add_wallet(self, wallet: Abstract_Wallet) -> None:\n        path = wallet.storage.path\n        wallet_key = self._wallet_key_from_path(path)\n        self._wallets[wallet_key] = wallet\n        run_hook('daemon_wallet_loaded', self, wallet)\n\n    def get_wallet(self, path: str) -> Optional[Abstract_Wallet]:\n        wallet_key = self._wallet_key_from_path(path)\n        return self._wallets.get(wallet_key)\n\n    @with_wallet_lock\n    def get_wallets(self) -> Dict[str, Abstract_Wallet]:\n        return dict(self._wallets)  # copy\n\n    def delete_wallet(self, path: str) -> bool:\n        self.stop_wallet(path)\n        if os.path.exists(path):\n            os.unlink(path)\n            self.update_recently_opened_wallets(path, remove=True)\n            if self.config.CURRENT_WALLET == path:\n                self.config.CURRENT_WALLET = None\n            return True\n        return False\n\n    def stop_wallet(self, path: str) -> bool:\n        \"\"\"Returns True iff a wallet was found.\"\"\"\n        assert util.get_running_loop() != util.get_asyncio_loop(), 'must not be called from asyncio thread'\n        fut = asyncio.run_coroutine_threadsafe(self._stop_wallet(path), self.asyncio_loop)\n        return fut.result()\n\n    @with_wallet_lock\n    async def _stop_wallet(self, path: str) -> bool:\n        \"\"\"Returns True iff a wallet was found.\"\"\"\n        path = standardize_path(path)\n        wallet_key = self._wallet_key_from_path(path)\n        wallet = self._wallets.pop(wallet_key, None)\n        if not wallet:\n            return False\n        await wallet.stop()\n        if self.config.get('wallet_path') is None:\n            wallet_paths = [w.db.storage.path for w in self._wallets.values()\n                            if w.db.storage and w.db.storage.path]\n            if self.config.CURRENT_WALLET == path and wallet_paths:\n                self.config.CURRENT_WALLET = wallet_paths[0]\n        return True\n\n    def run_daemon(self):\n        if 'wallet_path' in self.config.cmdline_options:\n            self.logger.warning(\"Ignoring parameter 'wallet_path' for daemon. \"\n                                \"Use the load_wallet command instead.\")\n        # init plugins\n        self._plugins = Plugins(self.config, 'cmdline')\n        # block until we are stopping\n        try:\n            self._stopping_soon_or_errored.wait()\n        except KeyboardInterrupt:\n            self.logger.info(\"got KeyboardInterrupt\")\n        # we either initiate shutdown now,\n        # or it has already been initiated (in which case this is a no-op):\n        self.logger.info(\"run_daemon is calling stop()\")\n        asyncio.run_coroutine_threadsafe(self.stop(), self.asyncio_loop).result()\n        # wait until \"stop\" finishes:\n        self._stopped_event.wait()\n\n    async def stop(self):\n        if self._stop_entered:\n            return\n        self._stop_entered = True\n        self._stopping_soon_or_errored.set()\n        self.logger.info(\"stop() entered. initiating shutdown\")\n        try:\n            if self.gui_object:\n                self.gui_object.stop()\n            self.logger.info(\"stopping all wallets\")\n            async with OldTaskGroup() as group:\n                for k, wallet in self._wallets.items():\n                    await group.spawn(wallet.stop())\n            self.logger.info(\"stopping network and taskgroup\")\n            async with ignore_after(2):\n                async with OldTaskGroup() as group:\n                    if self.network:\n                        await group.spawn(self.network.stop(full_shutdown=True))\n                    await group.spawn(self.taskgroup.cancel_remaining())\n            if self._plugins:\n                self.logger.info(\"stopping plugins\")\n                self._plugins.stop()\n                async with ignore_after(1):\n                    await self._plugins.stopped_event_async.wait()\n        finally:\n            if self.listen_jsonrpc:\n                self.logger.info(\"removing lockfile\")\n                remove_lockfile(get_lockfile(self.config))\n            self.logger.info(\"stopped\")\n            self._stopped_event.set()\n\n    def run_gui(self) -> None:\n        assert self.config\n        threading.current_thread().name = 'GUI'\n        gui_name = self.config.GUI_NAME\n        if gui_name in ['lite', 'classic']:\n            gui_name = 'qt'\n        self._plugins = Plugins(self.config, gui_name)  # init plugins\n        self.logger.info(f'launching GUI: {gui_name}')\n        try:\n            try:\n                gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum'])\n            except GuiImportError as e:\n                sys.exit(str(e))\n            self.gui_object = gui.ElectrumGui(config=self.config, daemon=self, plugins=self._plugins)\n            if not self._stop_entered:\n                self.gui_object.main()\n            else:\n                # If daemon.stop() was called before gui_object got created, stop gui now.\n                self.gui_object.stop()\n        except BaseException as e:\n            self.logger.error(f'GUI raised exception: {repr(e)}. shutting down.')\n            raise\n        finally:\n            # app will exit now\n            asyncio.run_coroutine_threadsafe(self.stop(), self.asyncio_loop).result()\n\n    @with_wallet_lock\n    def check_password_for_directory(self, *, old_password, new_password=None, wallet_dir: str) -> Tuple[bool, bool, list[str]]:\n        \"\"\"Checks password against all wallets (in dir), returns whether they can be unified and whether they are already.\n        If new_password is not None, update all wallet passwords to new_password.\n        \"\"\"\n        assert os.path.exists(wallet_dir), f\"path {wallet_dir!r} does not exist\"\n        succeeded = []\n        failed = []\n        is_unified = True\n        for filename in os.listdir(wallet_dir):\n            path = os.path.join(wallet_dir, filename)\n            path = standardize_path(path)\n            if not os.path.isfile(path):\n                continue\n            wallet = self.get_wallet(path)\n            # note: we only create a new wallet object if one was not loaded into the daemon already.\n            #       This is to avoid having two wallet objects contending for the same file.\n            #       Take care: this only works if the daemon knows about all wallet objects.\n            #                  if other code already has created a Wallet() for a file but did not tell the daemon,\n            #                  hard-to-understand bugs will follow...\n            if wallet is None:\n                try:\n                    wallet = self._load_wallet(path, old_password, upgrade=True, config=self.config)\n                except util.InvalidPassword:\n                    pass\n                except Exception:\n                    self.logger.exception(f'failed to load wallet at {path!r}:')\n            if wallet is None:\n                failed.append(path)\n                continue\n            if not wallet.storage.is_encrypted():\n                is_unified = False\n            try:\n                try:\n                    wallet.check_password(old_password)\n                    old_password_real = old_password\n                except util.InvalidPassword:\n                    wallet.check_password(None)\n                    old_password_real = None\n            except Exception:\n                failed.append(path)\n                continue\n            if new_password:\n                self.logger.info(f'updating password for wallet: {path!r}')\n                wallet.update_password(old_password_real, new_password, encrypt_storage=True)\n            succeeded.append(path)\n\n        can_be_unified = failed == []\n        is_unified = can_be_unified and is_unified\n        return can_be_unified, is_unified, succeeded\n\n    @with_wallet_lock\n    def update_password_for_directory(\n            self,\n            *,\n            old_password,\n            new_password,\n            wallet_dir: Optional[str] = None,\n    ) -> bool:\n        \"\"\"returns whether password is unified\"\"\"\n        if new_password is None:\n            # we opened a non-encrypted wallet\n            return False\n        if wallet_dir is None:\n            wallet_dir = os.path.dirname(self.config.get_wallet_path())\n        can_be_unified, is_unified, _ = self.check_password_for_directory(\n            old_password=old_password, new_password=None, wallet_dir=wallet_dir)\n        if not can_be_unified:\n            return False\n        if is_unified and old_password == new_password:\n            return True\n        self.check_password_for_directory(\n            old_password=old_password, new_password=new_password, wallet_dir=wallet_dir)\n        return True\n\n    def update_recently_opened_wallets(self, wallet_path, *, remove: bool = False):\n        recent = self.config.RECENTLY_OPEN_WALLET_FILES or []\n        if wallet_path in recent:\n            recent.remove(wallet_path)\n        if not remove:\n            recent.insert(0, wallet_path)\n            recent = [path for path in recent if os.path.exists(path)]\n            recent = recent[:5]\n        self.config.RECENTLY_OPEN_WALLET_FILES = recent\n        util.trigger_callback('recently_opened_wallets_update')\n"
  },
  {
    "path": "electrum/descriptor.py",
    "content": "# Copyright (c) 2017 Andrew Chow\n# Copyright (c) 2023 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n#\n# forked from https://github.com/bitcoin-core/HWI/blob/5f300d3dee7b317a6194680ad293eaa0962a3cc7/hwilib/descriptor.py\n#\n# Output Script Descriptors\n# See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md\n#\n# TODO allow xprv\n# TODO hardened derivation\n# TODO allow WIF privkeys\n# TODO impl ADDR descriptors\n# TODO impl RAW descriptors\n\nfrom binascii import unhexlify\nimport enum\nfrom enum import Enum\nfrom typing import (\n    List,\n    NamedTuple,\n    Optional,\n    Tuple,\n    Sequence,\n    Mapping,\n    Set,\n    Union,\n)\n\nimport electrum_ecc as ecc\n\nfrom .bip32 import convert_bip32_strpath_to_intpath, BIP32Node, KeyOriginInfo, BIP32_PRIME\nfrom . import bitcoin\nfrom .bitcoin import construct_script, opcodes, construct_witness, taproot_output_script\nfrom . import constants\nfrom .crypto import hash_160, sha256\nfrom . import segwit_addr\n\n\nMAX_TAPROOT_DEPTH = 128\n\n# we guess that signatures will be 72 bytes long\n# note: DER-encoded ECDSA signatures are 71 or 72 bytes in practice\n#       See https://bitcoin.stackexchange.com/questions/77191/what-is-the-maximum-size-of-a-der-encoded-ecdsa-signature\n#       We assume low S (as that is a bitcoin standardness rule).\n#       We do not assume low R (even though the sigs we create conform), as external sigs,\n#       e.g. from a hw signer cannot be expected to have a low R.\nDUMMY_DER_SIG = 72 * b\"\\x00\"\n\n\nclass ExpandedScripts:\n\n    def __init__(\n        self,\n        *,\n        output_script: bytes,  # \"scriptPubKey\"\n        redeem_script: Optional[bytes] = None,\n        witness_script: Optional[bytes] = None,\n        scriptcode_for_sighash: Optional[bytes] = None\n    ):\n        self.output_script = output_script\n        self.redeem_script = redeem_script\n        self.witness_script = witness_script\n        self.scriptcode_for_sighash = scriptcode_for_sighash\n\n    @property\n    def scriptcode_for_sighash(self) -> Optional[bytes]:\n        if self._scriptcode_for_sighash:\n            return self._scriptcode_for_sighash\n        return self.witness_script or self.redeem_script or self.output_script\n\n    @scriptcode_for_sighash.setter\n    def scriptcode_for_sighash(self, value: Optional[bytes]):\n        self._scriptcode_for_sighash = value\n\n    def address(self, *, net=None) -> Optional[str]:\n        return bitcoin.script_to_address(self.output_script, net=net)\n\n\nclass ScriptSolutionInner(NamedTuple):\n    witness_items: Optional[Sequence] = None\n\n\nclass ScriptSolutionTop(NamedTuple):\n    witness: Optional[bytes] = None\n    script_sig: Optional[bytes] = None\n\n\nclass MissingSolutionPiece(Exception): pass\n\n\ndef PolyMod(c: int, val: int) -> int:\n    \"\"\"\n    :meta private:\n    Function to compute modulo over the polynomial used for descriptor checksums\n    From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp\n    \"\"\"\n    c0 = c >> 35\n    c = ((c & 0x7ffffffff) << 5) ^ val\n    if (c0 & 1):\n        c ^= 0xf5dee51989\n    if (c0 & 2):\n        c ^= 0xa9fdca3312\n    if (c0 & 4):\n        c ^= 0x1bab10e32d\n    if (c0 & 8):\n        c ^= 0x3706b1677a\n    if (c0 & 16):\n        c ^= 0x644d626ffd\n    return c\n\n\n_INPUT_CHARSET = \"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\\\"\\\\ \"\n_INPUT_CHARSET_INV = {c: i for (i, c) in enumerate(_INPUT_CHARSET)}\n_CHECKSUM_CHARSET = \"qpzry9x8gf2tvdw0s3jn54khce6mua7l\"\n\ndef DescriptorChecksum(desc: str) -> str:\n    \"\"\"\n    Compute the checksum for a descriptor\n\n    :param desc: The descriptor string to compute a checksum for\n    :return: A checksum\n    \"\"\"\n    c = 1\n    cls = 0\n    clscount = 0\n    for ch in desc:\n        try:\n            pos = _INPUT_CHARSET_INV[ch]\n        except KeyError:\n            return \"\"\n        c = PolyMod(c, pos & 31)\n        cls = cls * 3 + (pos >> 5)\n        clscount += 1\n        if clscount == 3:\n            c = PolyMod(c, cls)\n            cls = 0\n            clscount = 0\n    if clscount > 0:\n        c = PolyMod(c, cls)\n    for j in range(0, 8):\n        c = PolyMod(c, 0)\n    c ^= 1\n\n    ret = [''] * 8\n    for j in range(0, 8):\n        ret[j] = _CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]\n    return ''.join(ret)\n\ndef AddChecksum(desc: str) -> str:\n    \"\"\"\n    Compute and attach the checksum for a descriptor\n\n    :param desc: The descriptor string to add a checksum to\n    :return: Descriptor with checksum\n    \"\"\"\n    return desc + \"#\" + DescriptorChecksum(desc)\n\n\nclass PubkeyProvider(object):\n    \"\"\"\n    A public key expression in a descriptor.\n    Can contain the key origin info, the pubkey itself, and subsequent derivation paths for derivation from the pubkey\n    The pubkey can be a typical pubkey or an extended pubkey.\n    \"\"\"\n    def __init__(\n        self,\n        origin: Optional['KeyOriginInfo'],\n        pubkey: str,\n        deriv_path: Optional[str]\n    ) -> None:\n        \"\"\"\n        :param origin: The key origin if one is available\n        :param pubkey: The public key. Either a hex string or a serialized extended pubkey\n        :param deriv_path: Additional derivation path (suffix) if the pubkey is an extended pubkey\n        \"\"\"\n        self.origin = origin\n        self.pubkey = pubkey\n        self.deriv_path = deriv_path\n        if deriv_path:\n            wildcard_count = deriv_path.count(\"*\")\n            if wildcard_count > 1:\n                raise ValueError(\"only one wildcard(*) is allowed in a descriptor\")\n            if wildcard_count == 1:\n                if deriv_path[-1] != \"*\":\n                    raise ValueError(\"wildcard in descriptor only allowed in last position\")\n            if deriv_path[0] != \"/\":\n                raise ValueError(f\"deriv_path suffix must start with a '/'. got {deriv_path!r}\")\n        # Make ExtendedKey from pubkey if it isn't hex\n        self.extkey = None\n        try:\n            unhexlify(self.pubkey)\n            # Is hex, normal pubkey\n        except Exception:\n            # Not hex, maybe xpub (but don't allow ypub/zpub)\n            self.extkey = BIP32Node.from_xkey(pubkey, allow_custom_headers=False)\n        if deriv_path and self.extkey is None:\n            raise ValueError(\"deriv_path suffix present for simple pubkey\")\n\n    @classmethod\n    def parse(cls, s: str) -> 'PubkeyProvider':\n        \"\"\"\n        Deserialize a key expression from the string into a ``PubkeyProvider``.\n\n        :param s: String containing the key expression\n        :return: A new ``PubkeyProvider`` containing the details given by ``s``\n        \"\"\"\n        origin = None\n        deriv_path = None\n\n        if s[0] == \"[\":\n            end = s.index(\"]\")\n            origin = KeyOriginInfo.from_string(s[1:end])\n            s = s[end + 1:]\n\n        pubkey = s\n        slash_idx = s.find(\"/\")\n        if slash_idx != -1:\n            pubkey = s[:slash_idx]\n            deriv_path = s[slash_idx:]\n\n        return cls(origin, pubkey, deriv_path)\n\n    def to_string(self) -> str:\n        \"\"\"\n        Serialize the pubkey expression to a string to be used in a descriptor\n\n        :return: The pubkey expression as a string\n        \"\"\"\n        s = \"\"\n        if self.origin:\n            s += \"[{}]\".format(self.origin.to_string())\n        s += self.pubkey\n        if self.deriv_path:\n            s += self.deriv_path\n        return s\n\n    def get_pubkey_bytes(self, *, pos: Optional[int] = None) -> bytes:\n        if self.is_range() and pos is None:\n            raise ValueError(\"pos must be set for ranged descriptor\")\n        # note: if not ranged, we ignore pos.\n        if self.extkey is not None:\n            compressed = True  # bip32 implies compressed pubkeys\n            if self.deriv_path is None:\n                assert not self.is_range()\n                return self.extkey.eckey.get_public_key_bytes(compressed=compressed)\n            else:\n                path_str = self.deriv_path[1:]\n                if self.is_range():\n                    assert path_str[-1] == \"*\"\n                    path_str = path_str[:-1] + str(pos)\n                path = convert_bip32_strpath_to_intpath(path_str)\n                child_key = self.extkey.subkey_at_public_derivation(path)\n                return child_key.eckey.get_public_key_bytes(compressed=compressed)\n        else:\n            assert not self.is_range()\n            return unhexlify(self.pubkey)\n\n    def get_full_derivation_path(self, *, pos: Optional[int] = None) -> str:\n        \"\"\"\n        Returns the full derivation path at the given position, including the origin\n        \"\"\"\n        if self.is_range() and pos is None:\n            raise ValueError(\"pos must be set for ranged descriptor\")\n        path = self.origin.get_derivation_path() if self.origin is not None else \"m\"\n        path += self.deriv_path if self.deriv_path is not None else \"\"\n        if path[-1] == \"*\":\n            path = path[:-1] + str(pos)\n        return path\n\n    def get_full_derivation_int_list(self, *, pos: Optional[int] = None) -> List[int]:\n        \"\"\"\n        Returns the full derivation path as an integer list at the given position.\n        Includes the origin and master key fingerprint as an int\n        \"\"\"\n        if self.is_range() and pos is None:\n            raise ValueError(\"pos must be set for ranged descriptor\")\n        path: List[int] = self.origin.get_full_int_list() if self.origin is not None else []\n        path.extend(self.get_der_suffix_int_list(pos=pos))\n        return path\n\n    def get_der_suffix_int_list(self, *, pos: Optional[int] = None) -> List[int]:\n        if not self.deriv_path:\n            return []\n        der_suffix = self.deriv_path\n        assert (wc_count := der_suffix.count(\"*\")) <= 1, wc_count\n        der_suffix = der_suffix.replace(\"*\", str(pos))\n        return convert_bip32_strpath_to_intpath(der_suffix)\n\n    def __lt__(self, other: 'PubkeyProvider') -> bool:\n        return self.pubkey < other.pubkey\n\n    def is_range(self) -> bool:\n        if not self.deriv_path:\n            return False\n        if self.deriv_path[-1] == \"*\":  # TODO hardened\n            return True\n        return False\n\n    def has_uncompressed_pubkey(self) -> bool:\n        if self.is_range():  # bip32 implies compressed\n            return False\n        return b\"\\x04\" == self.get_pubkey_bytes()[:1]\n\n\nclass Descriptor(object):\n    r\"\"\"\n    An abstract class for Descriptors themselves.\n    Descriptors can contain multiple :class:`PubkeyProvider`\\ s and multiple ``Descriptor`` as subdescriptors.\n\n    Note: a significant portion of input validation logic is in parse_descriptor(),\n          maybe these checks should be moved to (or also done in) this class?\n          For example, sh() must be top-level, or segwit mandates compressed pubkeys,\n          or bare-multisig cannot have >3 pubkeys.\n    \"\"\"\n    def __init__(\n        self,\n        pubkeys: List['PubkeyProvider'],\n        subdescriptors: List['Descriptor'],\n        name: str\n    ) -> None:\n        r\"\"\"\n        :param pubkeys: The :class:`PubkeyProvider`\\ s that are part of this descriptor\n        :param subdescriptor: The ``Descriptor``\\ s that are part of this descriptor\n        :param name: The name of the function for this descriptor\n        \"\"\"\n        self.pubkeys = pubkeys\n        self.subdescriptors = subdescriptors\n        self.name = name\n\n    def to_string_no_checksum(self) -> str:\n        \"\"\"\n        Serializes the descriptor as a string without the descriptor checksum\n\n        :return: The descriptor string\n        \"\"\"\n        return \"{}({}{})\".format(\n            self.name,\n            \",\".join([p.to_string() for p in self.pubkeys]),\n            self.subdescriptors[0].to_string_no_checksum() if len(self.subdescriptors) > 0 else \"\"\n        )\n\n    def to_string(self) -> str:\n        \"\"\"\n        Serializes the descriptor as a string with the checksum\n\n        :return: The descriptor with a checksum\n        \"\"\"\n        return AddChecksum(self.to_string_no_checksum())\n\n    def expand(self, *, pos: Optional[int] = None) -> \"ExpandedScripts\":\n        \"\"\"\n        Returns the scripts for a descriptor at the given `pos` for ranged descriptors.\n        \"\"\"\n        raise NotImplementedError(\"The Descriptor base class does not implement this method\")\n\n    def _satisfy_inner(\n        self,\n        *,\n        sigdata: Mapping[bytes, bytes] = None,  # pubkey -> sig\n        allow_dummy: bool = False,\n    ) -> ScriptSolutionInner:\n        raise NotImplementedError(\"The Descriptor base class does not implement this method\")\n\n    def satisfy(\n        self,\n        *,\n        sigdata: Mapping[bytes, bytes] = None,  # pubkey -> sig\n        allow_dummy: bool = False,\n    ) -> ScriptSolutionTop:\n        \"\"\"Construct a witness and/or scriptSig to be used in a txin, to satisfy the bitcoin SCRIPT.\n\n        Raises MissingSolutionPiece if satisfaction is not yet possible due to e.g. missing a signature,\n        unless `allow_dummy` is set to True, in which case dummy data is used where needed (e.g. for size estimation).\n        \"\"\"\n        assert not self.is_range()\n        sol = self._satisfy_inner(sigdata=sigdata, allow_dummy=allow_dummy)\n        witness = None\n        script_sig = None\n        if self.is_segwit():\n            witness = construct_witness(sol.witness_items)\n        else:\n            script_sig = construct_script(sol.witness_items)\n        return ScriptSolutionTop(\n            witness=witness,\n            script_sig=script_sig,\n        )\n\n    def get_satisfaction_progress(\n        self,\n        *,\n        sigdata: Mapping[bytes, bytes] = None,  # pubkey -> sig\n    ) -> Tuple[int, int]:\n        \"\"\"Returns (num_sigs_we_have, num_sigs_required) towards satisfying this script.\n        Besides signatures, later this can also consider hash-preimages.\n        \"\"\"\n        assert not self.is_range()\n        nhave, nreq = 0, 0\n        for desc in self.subdescriptors:\n            a, b = desc.get_satisfaction_progress(sigdata=sigdata)\n            nhave += a\n            nreq += b\n        return nhave, nreq\n\n    def is_range(self) -> bool:\n        for pubkey in self.pubkeys:\n            if pubkey.is_range():\n                return True\n        for desc in self.subdescriptors:\n            if desc.is_range():\n                return True\n        return False\n\n    def is_segwit(self) -> bool:\n        return any([desc.is_segwit() for desc in self.subdescriptors])\n\n    def is_taproot(self) -> bool:\n        return False\n\n    def get_all_pubkeys(self) -> Set[bytes]:\n        \"\"\"Returns set of pubkeys that appear at any level in this descriptor.\"\"\"\n        assert not self.is_range()\n        all_pubkeys = set([p.get_pubkey_bytes() for p in self.pubkeys])\n        for desc in self.subdescriptors:\n            all_pubkeys |= desc.get_all_pubkeys()\n        return all_pubkeys\n\n    def get_simple_singlesig(self) -> Optional['Descriptor']:\n        \"\"\"Returns innermost pk/pkh/wpkh descriptor, or None if we are not a simple singlesig.\n\n        note: besides pk,pkh,sh(wpkh),wpkh, overly complicated stuff such as sh(pk),wsh(sh(pkh),etc is also accepted\n        \"\"\"\n        if len(self.subdescriptors) == 1:\n            return self.subdescriptors[0].get_simple_singlesig()\n        return None\n\n    def get_simple_multisig(self) -> Optional['MultisigDescriptor']:\n        \"\"\"Returns innermost multi descriptor, or None if we are not a simple multisig.\"\"\"\n        if len(self.subdescriptors) == 1:\n            return self.subdescriptors[0].get_simple_multisig()\n        return None\n\n    def to_legacy_electrum_script_type(self) -> str:\n        if isinstance(self, PKDescriptor):\n            return \"p2pk\"\n        elif isinstance(self, PKHDescriptor):\n            return \"p2pkh\"\n        elif isinstance(self, WPKHDescriptor):\n            return \"p2wpkh\"\n        elif isinstance(self, SHDescriptor) and isinstance(self.subdescriptors[0], WPKHDescriptor):\n            return \"p2wpkh-p2sh\"\n        elif isinstance(self, SHDescriptor) and isinstance(self.subdescriptors[0], MultisigDescriptor):\n            return \"p2sh\"\n        elif isinstance(self, WSHDescriptor) and isinstance(self.subdescriptors[0], MultisigDescriptor):\n            return \"p2wsh\"\n        elif (isinstance(self, SHDescriptor) and isinstance(self.subdescriptors[0], WSHDescriptor)\n              and isinstance(self.subdescriptors[0].subdescriptors[0], MultisigDescriptor)):\n            return \"p2wsh-p2sh\"\n        return \"unknown\"\n\n\nclass PKDescriptor(Descriptor):\n    \"\"\"\n    A descriptor for ``pk()`` descriptors\n    \"\"\"\n    def __init__(\n        self,\n        pubkey: 'PubkeyProvider'\n    ) -> None:\n        \"\"\"\n        :param pubkey: The :class:`PubkeyProvider` for this descriptor\n        \"\"\"\n        super().__init__([pubkey], [], \"pk\")\n\n    def expand(self, *, pos: Optional[int] = None) -> \"ExpandedScripts\":\n        pubkey = self.pubkeys[0].get_pubkey_bytes(pos=pos)\n        script = construct_script([pubkey, opcodes.OP_CHECKSIG])\n        return ExpandedScripts(output_script=script)\n\n    def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner:\n        if sigdata is None: sigdata = {}\n        assert not self.is_range()\n        assert not self.subdescriptors\n        pubkey = self.pubkeys[0].get_pubkey_bytes()\n        sig = sigdata.get(pubkey)\n        if sig is None and allow_dummy:\n            sig = DUMMY_DER_SIG\n        if sig is None:\n            raise MissingSolutionPiece(f\"no sig for {pubkey.hex()}\")\n        return ScriptSolutionInner(\n            witness_items=(sig,),\n        )\n\n    def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]:\n        if sigdata is None: sigdata = {}\n        signatures = list(sigdata.values())\n        return len(signatures), 1\n\n    def get_simple_singlesig(self) -> Optional['Descriptor']:\n        return self\n\n\nclass PKHDescriptor(Descriptor):\n    \"\"\"\n    A descriptor for ``pkh()`` descriptors\n    \"\"\"\n    def __init__(\n        self,\n        pubkey: 'PubkeyProvider'\n    ) -> None:\n        \"\"\"\n        :param pubkey: The :class:`PubkeyProvider` for this descriptor\n        \"\"\"\n        super().__init__([pubkey], [], \"pkh\")\n\n    def expand(self, *, pos: Optional[int] = None) -> \"ExpandedScripts\":\n        pubkey = self.pubkeys[0].get_pubkey_bytes(pos=pos)\n        pkh = hash_160(pubkey)\n        script = bitcoin.pubkeyhash_to_p2pkh_script(pkh)\n        return ExpandedScripts(output_script=script)\n\n    def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner:\n        if sigdata is None: sigdata = {}\n        assert not self.is_range()\n        assert not self.subdescriptors\n        pubkey = self.pubkeys[0].get_pubkey_bytes()\n        sig = sigdata.get(pubkey)\n        if sig is None and allow_dummy:\n            sig = DUMMY_DER_SIG\n        if sig is None:\n            raise MissingSolutionPiece(f\"no sig for {pubkey.hex()}\")\n        return ScriptSolutionInner(\n            witness_items=(sig, pubkey),\n        )\n\n    def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]:\n        if sigdata is None: sigdata = {}\n        signatures = list(sigdata.values())\n        return len(signatures), 1\n\n    def get_simple_singlesig(self) -> Optional['Descriptor']:\n        return self\n\n\nclass WPKHDescriptor(Descriptor):\n    \"\"\"\n    A descriptor for ``wpkh()`` descriptors\n    \"\"\"\n    def __init__(\n        self,\n        pubkey: 'PubkeyProvider'\n    ) -> None:\n        \"\"\"\n        :param pubkey: The :class:`PubkeyProvider` for this descriptor\n        \"\"\"\n        super().__init__([pubkey], [], \"wpkh\")\n\n    def expand(self, *, pos: Optional[int] = None) -> \"ExpandedScripts\":\n        pkh = hash_160(self.pubkeys[0].get_pubkey_bytes(pos=pos))\n        output_script = construct_script([0, pkh])\n        scriptcode = bitcoin.pubkeyhash_to_p2pkh_script(pkh)\n        return ExpandedScripts(\n            output_script=output_script,\n            scriptcode_for_sighash=scriptcode,\n        )\n\n    def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner:\n        if sigdata is None: sigdata = {}\n        assert not self.is_range()\n        assert not self.subdescriptors\n        pubkey = self.pubkeys[0].get_pubkey_bytes()\n        sig = sigdata.get(pubkey)\n        if sig is None and allow_dummy:\n            sig = DUMMY_DER_SIG\n        if sig is None:\n            raise MissingSolutionPiece(f\"no sig for {pubkey.hex()}\")\n        return ScriptSolutionInner(\n            witness_items=(sig, pubkey),\n        )\n\n    def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]:\n        if sigdata is None: sigdata = {}\n        signatures = list(sigdata.values())\n        return len(signatures), 1\n\n    def is_segwit(self) -> bool:\n        return True\n\n    def get_simple_singlesig(self) -> Optional['Descriptor']:\n        return self\n\n\nclass MultisigDescriptor(Descriptor):\n    \"\"\"\n    A descriptor for ``multi()`` and ``sortedmulti()`` descriptors\n    \"\"\"\n    def __init__(\n        self,\n        pubkeys: List['PubkeyProvider'],\n        thresh: int,\n        is_sorted: bool\n    ) -> None:\n        r\"\"\"\n        :param pubkeys: The :class:`PubkeyProvider`\\ s for this descriptor\n        :param thresh: The number of keys required to sign this multisig\n        :param is_sorted: Whether this is a ``sortedmulti()`` descriptor\n        \"\"\"\n        super().__init__(pubkeys, [], \"sortedmulti\" if is_sorted else \"multi\")\n        if not (1 <= thresh <= len(pubkeys) <= 15):\n            raise ValueError(f'{thresh=}, {len(pubkeys)=}')\n        self.thresh = thresh\n        self.is_sorted = is_sorted\n        if self.is_sorted:\n            if not self.is_range():\n                # sort xpubs using the order of pubkeys\n                der_pks = [p.get_pubkey_bytes() for p in self.pubkeys]\n                self.pubkeys = [x[1] for x in sorted(zip(der_pks, self.pubkeys))]\n            else:\n                # not possible to sort according to final order in expanded scripts,\n                # but for easier visual comparison, we do a lexicographical sort\n                self.pubkeys.sort()\n\n    def to_string_no_checksum(self) -> str:\n        return \"{}({},{})\".format(self.name, self.thresh, \",\".join([p.to_string() for p in self.pubkeys]))\n\n    def expand(self, *, pos: Optional[int] = None) -> \"ExpandedScripts\":\n        der_pks = [p.get_pubkey_bytes(pos=pos) for p in self.pubkeys]\n        if self.is_sorted:\n            der_pks.sort()\n        script = construct_script([self.thresh, *der_pks, len(der_pks), opcodes.OP_CHECKMULTISIG])\n        return ExpandedScripts(output_script=script)\n\n    def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner:\n        if sigdata is None: sigdata = {}\n        assert not self.is_range()\n        assert not self.subdescriptors\n        der_pks = [p.get_pubkey_bytes() for p in self.pubkeys]\n        if self.is_sorted:\n            der_pks.sort()\n        signatures = []\n        for pubkey in der_pks:\n            if sig := sigdata.get(pubkey):\n                signatures.append(sig)\n                if len(signatures) >= self.thresh:\n                    break\n        if allow_dummy:\n            dummy_sig = DUMMY_DER_SIG\n            signatures += (self.thresh - len(signatures)) * [dummy_sig]\n        if len(signatures) < self.thresh:\n            raise MissingSolutionPiece(f\"not enough sigs\")\n        assert len(signatures) == self.thresh, f\"thresh={self.thresh}, but got {len(signatures)} sigs\"\n        return ScriptSolutionInner(\n            witness_items=(0, *signatures),\n        )\n\n    def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]:\n        if sigdata is None: sigdata = {}\n        signatures = list(sigdata.values())\n        return len(signatures), self.thresh\n\n    def get_simple_multisig(self) -> Optional['MultisigDescriptor']:\n        return self\n\n\nclass SHDescriptor(Descriptor):\n    \"\"\"\n    A descriptor for ``sh()`` descriptors\n    \"\"\"\n    def __init__(\n        self,\n        subdescriptor: 'Descriptor'\n    ) -> None:\n        \"\"\"\n        :param subdescriptor: The :class:`Descriptor` that is a sub-descriptor for this descriptor\n        \"\"\"\n        super().__init__([], [subdescriptor], \"sh\")\n\n    def expand(self, *, pos: Optional[int] = None) -> \"ExpandedScripts\":\n        assert len(self.subdescriptors) == 1\n        sub_scripts = self.subdescriptors[0].expand(pos=pos)\n        redeem_script = sub_scripts.output_script\n        witness_script = sub_scripts.witness_script\n        script = construct_script([opcodes.OP_HASH160, hash_160(redeem_script), opcodes.OP_EQUAL])\n        return ExpandedScripts(\n            output_script=script,\n            redeem_script=redeem_script,\n            witness_script=witness_script,\n            scriptcode_for_sighash=sub_scripts.scriptcode_for_sighash,\n        )\n\n    def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner:\n        raise Exception(\"does not make sense for sh()\")\n\n    def satisfy(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionTop:\n        assert not self.is_range()\n        assert len(self.subdescriptors) == 1\n        subdesc = self.subdescriptors[0]\n        redeem_script = self.expand().redeem_script\n        witness = None\n        if isinstance(subdesc, (WSHDescriptor, WPKHDescriptor)):  # witness_v0 nested in p2sh\n            witness = subdesc.satisfy(sigdata=sigdata, allow_dummy=allow_dummy).witness\n            script_sig = construct_script([redeem_script])\n        else:  # legacy p2sh\n            subsol = subdesc._satisfy_inner(sigdata=sigdata, allow_dummy=allow_dummy)\n            script_sig = construct_script([*subsol.witness_items, redeem_script])\n        return ScriptSolutionTop(\n            witness=witness,\n            script_sig=script_sig,\n        )\n\n\nclass WSHDescriptor(Descriptor):\n    \"\"\"\n    A descriptor for ``wsh()`` descriptors\n    \"\"\"\n    def __init__(\n        self,\n        subdescriptor: 'Descriptor'\n    ) -> None:\n        \"\"\"\n        :param subdescriptor: The :class:`Descriptor` that is a sub-descriptor for this descriptor\n        \"\"\"\n        super().__init__([], [subdescriptor], \"wsh\")\n\n    def expand(self, *, pos: Optional[int] = None) -> \"ExpandedScripts\":\n        assert len(self.subdescriptors) == 1\n        sub_scripts = self.subdescriptors[0].expand(pos=pos)\n        witness_script = sub_scripts.output_script\n        output_script = construct_script([0, sha256(witness_script)])\n        return ExpandedScripts(\n            output_script=output_script,\n            witness_script=witness_script,\n        )\n\n    def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner:\n        raise Exception(\"does not make sense for wsh()\")\n\n    def satisfy(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionTop:\n        assert not self.is_range()\n        assert len(self.subdescriptors) == 1\n        subsol = self.subdescriptors[0]._satisfy_inner(sigdata=sigdata, allow_dummy=allow_dummy)\n        witness_script = self.expand().witness_script\n        witness = construct_witness([*subsol.witness_items, witness_script])\n        return ScriptSolutionTop(\n            witness=witness,\n        )\n\n    def is_segwit(self) -> bool:\n        return True\n\n\nclass TRDescriptor(Descriptor):\n    \"\"\"\n    A descriptor for ``tr()`` descriptors\n    \"\"\"\n    def __init__(\n        self,\n        internal_key: 'PubkeyProvider',\n        desc_tree: List[Union['Descriptor', List]] = None,\n    ) -> None:\n        r\"\"\"\n        :param internal_key: The :class:`PubkeyProvider` that is the internal key for this descriptor\n        :param desc_tree: Taproot script binary tree, as a nested list of Descriptors\n        \"\"\"\n        if desc_tree is None:\n            desc_tree = []\n        self.desc_tree = desc_tree\n        desc_list = []\n        if desc_tree:\n            if self.get_max_tree_depth() > MAX_TAPROOT_DEPTH:\n                raise ValueError(f\"tr() supports at most {MAX_TAPROOT_DEPTH} nesting levels\")\n            def flatten(tree_node):\n                if isinstance(tree_node, Descriptor):\n                    return [tree_node]\n                assert len(tree_node) == 2, len(tree_node)\n                return flatten(tree_node[0]) + flatten(tree_node[1])\n            desc_list = flatten(desc_tree)\n        super().__init__(\n            pubkeys=[internal_key],\n            subdescriptors=desc_list,  # FIXME we could do without the flattened list (dupl)\n            name=\"tr\",\n        )\n\n    def to_string_no_checksum(self) -> str:\n        ret = f\"{self.name}({self.pubkeys[0].to_string()}\"\n        if self.desc_tree:\n            ret += \",\"\n            def tree_to_str(tree_node):\n                if isinstance(tree_node, Descriptor):\n                    return tree_node.to_string_no_checksum()\n                assert len(tree_node) == 2, len(tree_node)\n                return \"{\" + tree_to_str(tree_node[0]) + \",\" + tree_to_str(tree_node[1]) + \"}\"\n            ret += tree_to_str(self.desc_tree)\n        ret += \")\"\n        return ret\n\n    def is_segwit(self) -> bool:\n        return True\n\n    def is_taproot(self) -> bool:\n        return True\n\n    # TODO add more test vectors from BIP-0386\n    def expand(self, *, pos: Optional[int] = None) -> \"ExpandedScripts\":\n        internal_pubkey = self.pubkeys[0].get_pubkey_bytes(pos=pos)\n        script_tree = None\n        if self.desc_tree:\n            def transform(tree_node):\n                if isinstance(tree_node, Descriptor):\n                    leaf_version = 0xc0\n                    leaf_script = tree_node.expand(pos=pos).scriptcode_for_sighash  # FIXME maybe rename scriptcode_for_sighash\n                    return (leaf_version, leaf_script)\n                assert len(tree_node) == 2, len(tree_node)\n                return [transform(tree_node[0]), transform(tree_node[1])]\n            script_tree = transform(self.desc_tree)\n        output_script = taproot_output_script(internal_pubkey, script_tree=script_tree)\n        return ExpandedScripts(\n            output_script=output_script,\n        )\n\n    def get_max_tree_depth(self) -> Optional[int]:\n        if not self.desc_tree:\n            return None\n        def depth(tree_node) -> int:\n            if isinstance(tree_node, Descriptor):\n                return 0\n            assert len(tree_node) == 2, len(tree_node)\n            return 1 + max(depth(tree_node[0]), depth(tree_node[1]))\n        return depth(self.desc_tree)\n\n\ndef _get_func_expr(s: str) -> Tuple[str, str]:\n    \"\"\"\n    Get the function name and then the expression inside\n\n    :param s: The string that begins with a function name\n    :return: The function name as the first element of the tuple, and the expression contained within the function as the second element\n    :raises: ValueError: if a matching pair of parentheses cannot be found\n    \"\"\"\n    try:\n        start = s.index(\"(\")\n        end = s.rindex(\")\")\n        return s[0:start], s[start + 1:end]\n    except ValueError:\n        raise ValueError(\"A matching pair of parentheses cannot be found\")\n\n\ndef _get_const(s: str, const: str) -> str:\n    \"\"\"\n    Get the first character of the string, make sure it is the expected character,\n    and return the rest of the string\n\n    :param s: The string that begins with a constant character\n    :param const: The constant character\n    :return: The remainder of the string without the constant character\n    :raises: ValueError: if the first character is not the constant character\n    \"\"\"\n    if s[0] != const:\n        raise ValueError(f\"Expected '{const}' but got '{s[0]}'\")\n    return s[1:]\n\n\ndef _get_expr(s: str) -> Tuple[str, str]:\n    \"\"\"\n    Extract the expression that ``s`` begins with.\n\n    This will return the initial part of ``s``, up to the first comma or closing brace,\n    skipping ones that are surrounded by braces.\n\n    :param s: The string to extract the expression from\n    :return: A pair with the first item being the extracted expression and the second the rest of the string\n    \"\"\"\n    level: int = 0\n    for i, c in enumerate(s):\n        if c in [\"(\", \"{\"]:\n            level += 1\n        elif level > 0 and c in [\")\", \"}\"]:\n            level -= 1\n        elif level == 0 and c in [\")\", \"}\", \",\"]:\n            break\n    else:\n        return s, \"\"\n    return s[0:i], s[i:]\n\ndef parse_pubkey(expr: str, *, ctx: '_ParseDescriptorContext') -> Tuple['PubkeyProvider', str]:\n    \"\"\"\n    Parses an individual pubkey expression from a string that may contain more than one pubkey expression.\n\n    :param expr: The expression to parse a pubkey expression from\n    :return: The :class:`PubkeyProvider` that is parsed as the first item of a tuple, and the remainder of the expression as the second item.\n    \"\"\"\n    end = len(expr)\n    comma_idx = expr.find(\",\")\n    next_expr = \"\"\n    if comma_idx != -1:\n        end = comma_idx\n        next_expr = expr[end + 1:]\n    pubkey_provider = PubkeyProvider.parse(expr[:end])\n    permit_uncompressed = ctx in (_ParseDescriptorContext.TOP, _ParseDescriptorContext.P2SH)\n    if not permit_uncompressed and pubkey_provider.has_uncompressed_pubkey():\n        raise ValueError(\"uncompressed pubkeys are not allowed\")\n    return pubkey_provider, next_expr\n\n\nclass _ParseDescriptorContext(Enum):\n    \"\"\"\n    :meta private:\n\n    Enum representing the level that we are in when parsing a descriptor.\n    Some expressions aren't allowed at certain levels, this helps us track those.\n    \"\"\"\n\n    TOP = enum.auto()     # The top level, not within any descriptor\n    P2SH = enum.auto()    # Within an sh() descriptor\n    P2WPKH = enum.auto()  # Within wpkh() descriptor\n    P2WSH = enum.auto()   # Within a wsh() descriptor\n    P2TR = enum.auto()    # Within a tr() descriptor\n\n\ndef _parse_descriptor(desc: str, *, ctx: '_ParseDescriptorContext') -> 'Descriptor':\n    \"\"\"\n    :meta private:\n\n    Parse a descriptor given the context level we are in.\n    Used recursively to parse subdescriptors\n\n    :param desc: The descriptor string to parse\n    :param ctx: The :class:`_ParseDescriptorContext` indicating the level we are in\n    :return: The parsed descriptor\n    :raises: ValueError: if the descriptor is malformed\n    \"\"\"\n    func, expr = _get_func_expr(desc)\n    if func == \"pk\":\n        pubkey, expr = parse_pubkey(expr, ctx=ctx)\n        if expr:\n            raise ValueError(\"more than one pubkey in pk descriptor\")\n        return PKDescriptor(pubkey)\n    if func == \"pkh\":\n        if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH or ctx == _ParseDescriptorContext.P2WSH):\n            raise ValueError(\"Can only have pkh at top level, in sh(), or in wsh()\")\n        pubkey, expr = parse_pubkey(expr, ctx=ctx)\n        if expr:\n            raise ValueError(\"More than one pubkey in pkh descriptor\")\n        return PKHDescriptor(pubkey)\n    if func == \"sortedmulti\" or func == \"multi\":\n        if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH or ctx == _ParseDescriptorContext.P2WSH):\n            raise ValueError(\"Can only have multi/sortedmulti at top level, in sh(), or in wsh()\")\n        is_sorted = func == \"sortedmulti\"\n        comma_idx = expr.index(\",\")\n        thresh = int(expr[:comma_idx])\n        expr = expr[comma_idx + 1:]\n        pubkeys = []\n        while expr:\n            pubkey, expr = parse_pubkey(expr, ctx=ctx)\n            pubkeys.append(pubkey)\n        if len(pubkeys) == 0 or len(pubkeys) > 15:\n            raise ValueError(\"Cannot have {} keys in a multisig; must have between 1 and 15 keys, inclusive\".format(len(pubkeys)))\n        elif thresh < 1:\n            raise ValueError(\"Multisig threshold cannot be {}, must be at least 1\".format(thresh))\n        elif thresh > len(pubkeys):\n            raise ValueError(\"Multisig threshold cannot be larger than the number of keys; threshold is {} but only {} keys specified\".format(thresh, len(pubkeys)))\n        if ctx == _ParseDescriptorContext.TOP and len(pubkeys) > 3:\n            raise ValueError(\"Cannot have {} pubkeys in bare multisig: only at most 3 pubkeys\")\n        return MultisigDescriptor(pubkeys, thresh, is_sorted)\n    if func == \"wpkh\":\n        if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH):\n            raise ValueError(\"Can only have wpkh() at top level or inside sh()\")\n        pubkey, expr = parse_pubkey(expr, ctx=_ParseDescriptorContext.P2WPKH)\n        if expr:\n            raise ValueError(\"More than one pubkey in pkh descriptor\")\n        return WPKHDescriptor(pubkey)\n    if func == \"sh\":\n        if ctx != _ParseDescriptorContext.TOP:\n            raise ValueError(\"Can only have sh() at top level\")\n        subdesc = _parse_descriptor(expr, ctx=_ParseDescriptorContext.P2SH)\n        return SHDescriptor(subdesc)\n    if func == \"wsh\":\n        if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH):\n            raise ValueError(\"Can only have wsh() at top level or inside sh()\")\n        subdesc = _parse_descriptor(expr, ctx=_ParseDescriptorContext.P2WSH)\n        return WSHDescriptor(subdesc)\n    if func == \"tr\":\n        if ctx != _ParseDescriptorContext.TOP:\n            raise ValueError(\"Can only have tr at top level\")\n        internal_key, expr = parse_pubkey(expr, ctx=ctx)\n        desc_tree = []\n        if expr:\n            def parse_tree(tree_str):\n                if len(tree_str) == 0:\n                    raise ValueError(\"Invalid Taproot tree expression\")\n                if tree_str[0] != \"{\":  # leaf\n                    sarg, remaining = _get_expr(tree_str)\n                    return _parse_descriptor(sarg, ctx=_ParseDescriptorContext.P2TR), remaining\n                if len(tree_str) < len(\"{x,y}\") or tree_str[-1] != \"}\":\n                    raise ValueError(\"Invalid Taproot tree expression\")\n                left, remaining = parse_tree(tree_str[1:])\n                if remaining[0] != \",\": raise ValueError\n                right, remaining = parse_tree(remaining[1:])\n                if remaining[0] != \"}\": raise ValueError\n                return [left, right], remaining[1:]\n            desc_tree, _remaining = parse_tree(expr)\n            if len(_remaining) != 0: raise ValueError\n        return TRDescriptor(internal_key, desc_tree)\n    if ctx == _ParseDescriptorContext.P2SH:\n        raise ValueError(\"A function is needed within P2SH\")\n    elif ctx == _ParseDescriptorContext.P2WSH:\n        raise ValueError(\"A function is needed within P2WSH\")\n    raise ValueError(\"{} is not a valid descriptor function\".format(func))\n\n\ndef parse_descriptor(desc: str) -> 'Descriptor':\n    \"\"\"\n    Parse a descriptor string into a :class:`Descriptor`.\n    Validates the checksum if one is provided in the string\n\n    :param desc: The descriptor string\n    :return: The parsed :class:`Descriptor`\n    :raises: ValueError: if the descriptor string is malformed\n    \"\"\"\n    i = desc.find(\"#\")\n    if i != -1:\n        checksum = desc[i + 1:]\n        desc = desc[:i]\n        computed = DescriptorChecksum(desc)\n        if computed != checksum:\n            raise ValueError(\"The checksum does not match; Got {}, expected {}\".format(checksum, computed))\n    return _parse_descriptor(desc, ctx=_ParseDescriptorContext.TOP)\n\n\n#####\n\n\nclass NotLegacySinglesigScriptType(Exception): pass\n\n\ndef get_singlesig_descriptor_from_legacy_leaf(*, pubkey: str, script_type: str) -> Optional[Descriptor]:\n    pubkey = PubkeyProvider.parse(pubkey)\n    if script_type == 'p2pk':\n        return PKDescriptor(pubkey=pubkey)\n    elif script_type == 'p2pkh':\n        return PKHDescriptor(pubkey=pubkey)\n    elif script_type == 'p2wpkh':\n        return WPKHDescriptor(pubkey=pubkey)\n    elif script_type == 'p2wpkh-p2sh':\n        wpkh = WPKHDescriptor(pubkey=pubkey)\n        return SHDescriptor(subdescriptor=wpkh)\n    else:\n        raise NotLegacySinglesigScriptType(f\"unexpected {script_type=}\")\n\n\ndef create_dummy_descriptor_from_address(addr: Optional[str]) -> 'Descriptor':\n    # It's not possible to tell the script type in general just from an address.\n    # - \"1\" addresses are of course p2pkh\n    # - \"3\" addresses are p2sh but we don't know the redeem script...\n    # - \"bc1\" addresses (if they are 42-long) are p2wpkh\n    # - \"bc1\" addresses that are 62-long are p2wsh but we don't know the script...\n    # If we don't know the script, we _guess_ it is pubkeyhash.\n    # As this method is used e.g. for tx size estimation,\n    # the estimation will not be precise.\n    def guess_script_type(addr: Optional[str]) -> str:\n        if addr is None:\n            return 'p2wpkh'  # the default guess\n        witver, witprog = segwit_addr.decode_segwit_address(constants.net.SEGWIT_HRP, addr)\n        if witprog is not None:\n            return 'p2wpkh'\n        addrtype, hash_160_ = bitcoin.b58_address_to_hash160(addr)\n        if addrtype == constants.net.ADDRTYPE_P2PKH:\n            return 'p2pkh'\n        elif addrtype == constants.net.ADDRTYPE_P2SH:\n            return 'p2wpkh-p2sh'\n        raise Exception(f'unrecognized address: {repr(addr)}')\n\n    script_type = guess_script_type(addr)\n    # guess pubkey-len to be 33-bytes:\n    pubkey = ecc.GENERATOR.get_public_key_bytes(compressed=True).hex()\n    desc = get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=script_type)\n    return desc\n"
  },
  {
    "path": "electrum/dns_hacks.py",
    "content": "# Copyright (C) 2020 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nimport sys\nimport socket\nimport concurrent\nfrom concurrent import futures\nimport ipaddress\nimport asyncio\n\nimport dns\nimport dns.asyncresolver\n\nfrom .logging import get_logger\nfrom .util import get_asyncio_loop\nfrom . import util\n\n_logger = get_logger(__name__)\n\n\ndef configure_dns_resolver() -> None:\n    # Store this somewhere so we can un-monkey-patch:\n    if not hasattr(socket, \"_getaddrinfo\"):\n        socket._getaddrinfo = socket.getaddrinfo\n    if sys.platform == 'win32':\n        # On Windows, socket.getaddrinfo takes a mutex, and might hold it for up to 10 seconds\n        # when dns-resolving. To speed it up drastically, we resolve dns ourselves, outside that lock.\n        # See https://github.com/spesmilo/electrum/issues/4421\n        try:\n            _prepare_windows_dns_hack()\n        except Exception as e:\n            _logger.exception('failed to apply windows dns hack.')\n        else:\n            socket.getaddrinfo = _fast_getaddrinfo\n\n\ndef _prepare_windows_dns_hack():\n    # enable dns cache\n    resolver = dns.asyncresolver.get_default_resolver()\n    if resolver.cache is None:\n        resolver.cache = dns.resolver.Cache()\n    # ensure overall timeout for requests is long enough\n    resolver.lifetime = max(resolver.lifetime or 1, 30.0)\n\n\ndef _is_force_system_dns_for_host(host: str) -> bool:\n    return str(host) in ('localhost', 'localhost.',)\n\n\ndef _fast_getaddrinfo(host, *args, **kwargs):\n    def needs_dns_resolving(host):\n        try:\n            ipaddress.ip_address(host)\n            return False  # already valid IP\n        except ValueError:\n            pass  # not an IP\n        if _is_force_system_dns_for_host(host):\n            return False\n        return True\n\n    def resolve_with_dnspython(host):\n        addrs = []\n        expected_errors = (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,\n                           concurrent.futures.CancelledError, concurrent.futures.TimeoutError)\n        loop = get_asyncio_loop()\n        assert util.get_running_loop() != loop, 'must not be called from asyncio thread'\n        ipv6_fut = asyncio.run_coroutine_threadsafe(\n            dns.asyncresolver.resolve(host, dns.rdatatype.AAAA),\n            loop,\n        )\n        ipv4_fut = asyncio.run_coroutine_threadsafe(\n            dns.asyncresolver.resolve(host, dns.rdatatype.A),\n            loop,\n        )\n        # try IPv6\n        try:\n            answers = ipv6_fut.result()\n            addrs += [str(answer) for answer in answers]\n        except expected_errors as e:\n            pass\n        except BaseException as e:\n            _logger.info(f'dnspython failed to resolve dns (AAAA) for {repr(host)} with error: {repr(e)}')\n        # try IPv4\n        try:\n            answers = ipv4_fut.result()\n            addrs += [str(answer) for answer in answers]\n        except expected_errors as e:\n            # dns failed for some reason, e.g. dns.resolver.NXDOMAIN this is normal.\n            # Simply report back failure; except if we already have some results.\n            if not addrs:\n                raise socket.gaierror(11001, 'getaddrinfo failed') from e\n        except BaseException as e:\n            # Possibly internal error in dnspython :( see #4483 and #5638\n            _logger.info(f'dnspython failed to resolve dns (A) for {repr(host)} with error: {repr(e)}')\n        if addrs:\n            return addrs\n        # Fall back to original socket.getaddrinfo to resolve dns.\n        return [host]\n\n    addrs = [host]\n    if needs_dns_resolving(host):\n        addrs = resolve_with_dnspython(host)\n    list_of_list_of_socketinfos = [socket._getaddrinfo(addr, *args, **kwargs) for addr in addrs]\n    list_of_socketinfos = [item for lst in list_of_list_of_socketinfos for item in lst]\n    return list_of_socketinfos\n"
  },
  {
    "path": "electrum/dnssec.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n# Check DNSSEC trust chain.\n# Todo: verify expiration dates\n#\n# Based on\n#  http://backreference.org/2010/11/17/dnssec-verification-with-dig/\n#  https://github.com/rthalley/dnspython/blob/master/tests/test_dnssec.py\n\nimport logging\n\nimport dns\nimport dns.name\nimport dns.asyncquery\nimport dns.dnssec\nimport dns.message\nimport dns.asyncresolver\nimport dns.rdatatype\nimport dns.rdtypes.ANY.NS\nimport dns.rdtypes.ANY.CNAME\nimport dns.rdtypes.ANY.DLV\nimport dns.rdtypes.ANY.DNSKEY\nimport dns.rdtypes.ANY.DS\nimport dns.rdtypes.ANY.NSEC\nimport dns.rdtypes.ANY.NSEC3\nimport dns.rdtypes.ANY.NSEC3PARAM\nimport dns.rdtypes.ANY.RRSIG\nimport dns.rdtypes.ANY.SOA\nimport dns.rdtypes.ANY.TXT\nimport dns.rdtypes.IN.A\nimport dns.rdtypes.IN.AAAA\n\nfrom .logging import get_logger\nfrom typing import Tuple\n\n\n_logger = get_logger(__name__)\n\n\n# hard-coded trust anchors (root KSKs)\ntrust_anchors = [\n    # KSK-2017:\n    dns.rrset.from_text('.', 1    , 'IN', 'DNSKEY', '257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU='),\n    # KSK-2010:\n    dns.rrset.from_text('.', 15202, 'IN', 'DNSKEY', '257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0='),\n]\n\n\nasync def _check_query(ns, sub, _type, keys) -> dns.rrset.RRset:\n    q = dns.message.make_query(sub, _type, want_dnssec=True)\n    response = await dns.asyncquery.tcp(q, ns, timeout=5)\n    assert response.rcode() == 0, 'No answer'\n    answer = response.answer\n    assert len(answer) != 0, ('No DNS record found', sub, _type)\n    assert len(answer) != 1, ('No DNSSEC record found', sub, _type)\n    if answer[0].rdtype == dns.rdatatype.RRSIG:\n        rrsig, rrset = answer\n    elif answer[1].rdtype == dns.rdatatype.RRSIG:\n        rrset, rrsig = answer\n    else:\n        raise Exception('No signature set in record')\n    if keys is None:\n        keys = {dns.name.from_text(sub):rrset}\n    dns.dnssec.validate(rrset, rrsig, keys)\n    return rrset\n\n\nasync def _get_and_validate(ns, url, _type) -> dns.rrset.RRset:\n    # get trusted root key\n    root_rrset = None\n    for dnskey_rr in trust_anchors:\n        try:\n            # Check if there is a valid signature for the root dnskey\n            root_rrset = await _check_query(ns, '', dns.rdatatype.DNSKEY, {dns.name.root: dnskey_rr})\n            break\n        except dns.dnssec.ValidationFailure:\n            # It's OK as long as one key validates\n            continue\n    if not root_rrset:\n        raise dns.dnssec.ValidationFailure('None of the trust anchors found in DNS')\n    keys = {dns.name.root: root_rrset}\n    # top-down verification\n    parts = url.split('.')\n    for i in range(len(parts), 0, -1):\n        sub = '.'.join(parts[i-1:])\n        name = dns.name.from_text(sub)\n        # If server is authoritative, don't fetch DNSKEY\n        query = dns.message.make_query(sub, dns.rdatatype.NS)\n        response = await dns.asyncquery.udp(query, ns, 3)\n        assert response.rcode() == dns.rcode.NOERROR, \"query error\"\n        rrset = response.authority[0] if len(response.authority) > 0 else response.answer[0]\n        rr = rrset[0]\n        if rr.rdtype == dns.rdatatype.SOA:\n            continue\n        # get DNSKEY (self-signed)\n        rrset = await _check_query(ns, sub, dns.rdatatype.DNSKEY, None)\n        # get DS (signed by parent)\n        ds_rrset = await _check_query(ns, sub, dns.rdatatype.DS, keys)\n        # verify that a signed DS validates DNSKEY\n        for ds in ds_rrset:\n            for dnskey in rrset:\n                htype = 'SHA256' if ds.digest_type == 2 else 'SHA1'\n                good_ds = dns.dnssec.make_ds(name, dnskey, htype)\n                if ds == good_ds:\n                    break\n            else:\n                continue\n            break\n        else:\n            raise Exception(\"DS does not match DNSKEY\")\n        # set key for next iteration\n        keys = {name: rrset}\n    # get TXT record (signed by zone)\n    rrset = await _check_query(ns, url, _type, keys)\n    return rrset\n\n\nasync def query(url: str, rtype: dns.rdatatype.RdataType) -> Tuple[dns.rrset.RRset, bool]:\n    \"\"\"Try to do DNS resolution, including DNSSEC.\n    'validated' shows whether the DNSSEC checks passed. DNS is completely INSECURE without DNSSEC,\n    so the caller must carefully consider whether the response can be used for anything if validated=False.\n    \"\"\"\n    # FIXME this method is not using the network proxy. (although the proxy might not support UDP?)\n    # 8.8.8.8 is Google's public DNS server\n    nameservers = ['8.8.8.8']\n    ns = nameservers[0]\n    try:\n        out = await _get_and_validate(ns, url, rtype)\n        validated = True\n    except Exception as e:\n        log_level = logging.WARNING if isinstance(e, ImportError) else logging.INFO\n        _logger.log(log_level, f\"DNSSEC error: {repr(e)}\")\n        out = await dns.asyncresolver.resolve(url, rtype)\n        validated = False\n    return out, validated\n"
  },
  {
    "path": "electrum/exchange_rate.py",
    "content": "import asyncio\nfrom datetime import datetime\nimport inspect\nimport sys\nimport os\nimport json\nimport time\nimport csv\nimport decimal\nfrom decimal import Decimal\nfrom typing import Sequence, Optional, Mapping, Dict, Union, Tuple\n\nfrom aiorpcx.curio import timeout_after, ignore_after\nimport aiohttp\n\nfrom . import util\nfrom .bitcoin import COIN\nfrom .i18n import _\nfrom .util import (\n    ThreadJob, make_dir, log_exceptions, OldTaskGroup, make_aiohttp_session, resource_path, EventListener,\n    event_listener, to_decimal, timestamp_to_datetime\n)\nfrom .util import NetworkRetryManager\nfrom .network import Network\nfrom .simple_config import SimpleConfig\nfrom .logging import Logger\n\n\n# See https://en.wikipedia.org/wiki/ISO_4217\nCCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,\n                  'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0,\n                  'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3,\n                  'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0,\n                  'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0,\n                  'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0,\n                  # Cryptocurrencies\n                  'BTC': 8, 'LTC': 6, 'XRP': 4, 'ETH': 8,\n                  }\n\nSPOT_RATE_REFRESH_TARGET = 150      # approx. every 2.5 minutes, try to refresh spot price\nSPOT_RATE_CLOSE_TO_STALE = 450      # try harder to fetch an update if price is getting old\nSPOT_RATE_EXPIRY = 600              # spot price becomes stale after 10 minutes -> we no longer show/use it\n\n\nclass ExchangeBase(Logger):\n\n    def __init__(self, on_quotes, on_history):\n        Logger.__init__(self)\n        self._history = {}  # type: Dict[str, Dict[str, str | float]]\n        self._quotes = {}  # type: Dict[str, Optional[Decimal]]\n        self._quotes_timestamp = 0  # type: Union[int, float]\n        self.on_quotes = on_quotes\n        self.on_history = on_history\n\n    async def get_raw(self, site, get_string):\n        # APIs must have https\n        url = ''.join(['https://', site, get_string])\n        network = Network.get_instance()\n        proxy = network.proxy if network else None\n        async with make_aiohttp_session(proxy) as session:\n            async with session.get(url) as response:\n                response.raise_for_status()\n                return await response.text()\n\n    async def get_json(self, site, get_string):\n        # APIs must have https\n        url = ''.join(['https://', site, get_string])\n        network = Network.get_instance()\n        proxy = network.proxy if network else None\n        async with make_aiohttp_session(proxy) as session:\n            async with session.get(url) as response:\n                response.raise_for_status()\n                # set content_type to None to disable checking MIME type\n                return await response.json(content_type=None)\n\n    async def get_csv(self, site, get_string):\n        raw = await self.get_raw(site, get_string)\n        reader = csv.DictReader(raw.split('\\n'))\n        return list(reader)\n\n    def name(self):\n        return self.__class__.__name__\n\n    async def update_safe(self, ccy: str) -> None:\n        try:\n            self.logger.info(f\"getting fx quotes for {ccy}\")\n            self._quotes = await self.get_rates(ccy)\n            assert all(isinstance(rate, (Decimal, type(None))) for rate in self._quotes.values()), \\\n                f\"fx rate must be Decimal, got {self._quotes}\"\n        except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as e:\n            self.logger.info(f\"failed fx quotes: {repr(e)}\")\n            self.on_quotes()\n        except Exception as e:\n            self.logger.exception(f\"failed fx quotes: {repr(e)}\")\n            self.on_quotes()\n        else:\n            self.logger.debug(\"received fx quotes\")\n            self._quotes_timestamp = time.time()\n            self.on_quotes(received_new_data=True)\n\n    @staticmethod\n    def _read_historical_rates_from_file(\n        *, exchange_name: str, ccy: str, cache_dir: str,\n    ) -> Tuple[Optional[Dict[str, str]], Optional[float]]:\n        filename = os.path.join(cache_dir, f\"{exchange_name}_{ccy}\")\n        if not os.path.exists(filename):\n            return None, None\n        timestamp = os.stat(filename).st_mtime\n        try:\n            with open(filename, 'r', encoding='utf-8') as f:\n                h = json.loads(f.read())\n        except Exception:\n            return None, None\n        if not h:  # e.g. empty dict\n            return None, None\n        # cast rates to str\n        h = {date_str: str(rate) for (date_str, rate) in h.items()}\n        return h, timestamp\n\n    def read_historical_rates(self, ccy: str, cache_dir: str) -> Optional[dict]:\n        h, timestamp = self._read_historical_rates_from_file(\n            exchange_name=self.name(),\n            ccy=ccy,\n            cache_dir=cache_dir,\n        )\n        if not h:\n            return None\n        assert timestamp is not None\n        h['timestamp'] = timestamp\n        self._history[ccy] = h\n        self.on_history()\n        return h\n\n    @staticmethod\n    def _write_historical_rates_to_file(\n        *, exchange_name: str, ccy: str, cache_dir: str, history: Dict[str, str],\n    ) -> None:\n        # sanity check types of history dict\n        assert 'timestamp' not in history\n        for key, rate in history.items():\n            assert isinstance(key, str), f\"{exchange_name=}. {ccy=}. {key=!r}. {rate=!r}\"\n            assert isinstance(rate, str), f\"{exchange_name=}. {ccy=}. {key=!r}. {rate=!r}\"\n        # write to file\n        filename = os.path.join(cache_dir, f\"{exchange_name}_{ccy}\")\n        with open(filename, 'w', encoding='utf-8') as f:\n            f.write(json.dumps(history, sort_keys=True))\n\n    @log_exceptions\n    async def get_historical_rates_safe(self, ccy: str, cache_dir: str) -> None:\n        try:\n            self.logger.info(f\"requesting fx history for {ccy}\")\n            h_new = await self.request_history(ccy)\n            self.logger.debug(f\"received fx history for {ccy}\")\n        except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as e:\n            self.logger.info(f\"failed fx history: {repr(e)}\")\n            return\n        except Exception as e:\n            self.logger.exception(f\"failed fx history: {repr(e)}\")\n            return\n        # cast rates to str\n        h_new = {date_str: str(rate) for (date_str, rate) in h_new.items()}  # type: Dict[str, str]\n        # merge old history and new history. resolve duplicate dates using new data.\n        h_old, _timestamp = self._read_historical_rates_from_file(\n            exchange_name=self.name(), ccy=ccy, cache_dir=cache_dir,\n        )\n        h_old = h_old or {}\n        h = {**h_old, **h_new}\n        # write merged data to disk cache\n        self._write_historical_rates_to_file(\n            exchange_name=self.name(), ccy=ccy, cache_dir=cache_dir, history=h,\n        )\n        h['timestamp'] = time.time()  # note: this is the only item in h that has a float value\n        self._history[ccy] = h\n        self.on_history()\n\n    def get_historical_rates(self, ccy: str, cache_dir: str) -> None:\n        if ccy not in self.history_ccys():\n            return\n        h = self._history.get(ccy)\n        if h is None:\n            h = self.read_historical_rates(ccy, cache_dir)\n        if h is None or h['timestamp'] < time.time() - 24*3600:\n            util.get_asyncio_loop().create_task(self.get_historical_rates_safe(ccy, cache_dir))\n\n    def history_ccys(self) -> Sequence[str]:\n        return []\n\n    def historical_rate(self, ccy: str, d_t: datetime) -> Decimal:\n        date_str = d_t.strftime('%Y-%m-%d')\n        rate = self._history.get(ccy, {}).get(date_str) or 'NaN'\n        try:\n            return Decimal(rate)\n        except Exception:  # guard against garbage coming from exchange\n            #self.logger.debug(f\"found corrupted historical_rate: {rate=!r}. for {ccy=} at {date_str}\")\n            return Decimal('NaN')\n\n    async def request_history(self, ccy: str) -> Dict[str, Union[str, float]]:\n        raise NotImplementedError()  # implemented by subclasses\n\n    async def get_rates(self, ccy: str) -> Mapping[str, Optional[Decimal]]:\n        raise NotImplementedError()  # implemented by subclasses\n\n    async def get_currencies(self) -> Sequence[str]:\n        rates = await self.get_rates('')\n        return sorted([str(a) for (a, b) in rates.items() if b is not None and len(a)==3])\n\n    def get_cached_spot_quote(self, ccy: str) -> Decimal:\n        \"\"\"Returns the cached exchange rate as a Decimal\"\"\"\n        if ccy == 'BTC':\n            return Decimal(1)\n        rate = self._quotes.get(ccy)\n        if not rate:  # don't return 0 to prevent DivisionByZero exceptions\n            return Decimal('NaN')\n        if self._quotes_timestamp + SPOT_RATE_EXPIRY < time.time():\n            # Our rate is stale. Probably better to return no rate than an incorrect one.\n            return Decimal('NaN')\n        return Decimal(rate)\n\n\nclass Yadio(ExchangeBase):\n\n    async def get_currencies(self):\n        dicts = await self.get_json('api.yadio.io', '/currencies')\n        return list(dicts.keys())\n\n    async def get_rates(self, ccy: str) -> Mapping[str, Optional[Decimal]]:\n        json = await self.get_json('api.yadio.io', '/rate/%s/BTC' % ccy)\n        return {ccy: to_decimal(json['rate'])}\n\n\nclass BitcoinAverage(ExchangeBase):\n    # note: historical rates used to be freely available\n    # but this is no longer the case. see #5188\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('apiv2.bitcoinaverage.com', '/indices/global/ticker/short')\n        return dict([(r.replace(\"BTC\", \"\"), to_decimal(json[r]['last']))\n                     for r in json if r != 'timestamp'])\n\n\nclass Bitcointoyou(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('bitcointoyou.com', \"/API/ticker.aspx\")\n        return {'BRL': to_decimal(json['ticker']['last'])}\n\n\nclass BitcoinVenezuela(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('api.bitcoinvenezuela.com', '/')\n        rates = [(r, to_decimal(json['BTC'][r])) for r in json['BTC']\n                 if json['BTC'][r] is not None]  # Giving NULL for LTC\n        return dict(rates)\n\n    def history_ccys(self):\n        return ['ARS', 'EUR', 'USD', 'VEF']\n\n    async def request_history(self, ccy):\n        json = await self.get_json('api.bitcoinvenezuela.com', \"/historical/index.php?coin=BTC\")\n        return json[ccy + '_BTC']\n\n\nclass Bitbank(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('public.bitbank.cc', '/btc_jpy/ticker')\n        return {'JPY': to_decimal(json['data']['last'])}\n\n\nclass BitFinex(ExchangeBase):\n\n    async def get_currencies(self):\n        json = await self.get_json(\n            'api-pub.bitfinex.com',\n            f\"/v2/conf/pub:list:pair:exchange\")\n        pairs = [pair for pair in json[0]\n                 if len(pair) == 6 and pair[:3] == \"BTC\"]\n        return [pair[3:] for pair in pairs]\n\n    def history_ccys(self):\n        return CURRENCIES[self.name()]\n\n    async def get_rates(self, ccy):\n        # ref https://docs.bitfinex.com/reference/rest-public-ticker\n        json = await self.get_json(\n            'api-pub.bitfinex.com',\n            f\"/v2/ticker/tBTC{ccy}\")\n        return {ccy: to_decimal(json[6])}\n\n    async def request_history(self, ccy):\n        # ref https://docs.bitfinex.com/reference/rest-public-candles\n        history = await self.get_json(\n            'api.bitfinex.com',\n            f\"/v2/candles/trade:1D:tBTC{ccy}/hist?limit=10000\")\n        return dict([(timestamp_to_datetime(h[0] // 1000, utc=True).strftime('%Y-%m-%d'), str(h[2]))\n                     for h in history])\n\n\nclass BitFlyer(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('bitflyer.jp', '/api/echo/price')\n        return {'JPY': to_decimal(json['mid'])}\n\n\nclass BitPay(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('bitpay.com', '/api/rates')\n        return dict([(r['code'], to_decimal(r['rate'])) for r in json])\n\n\nclass Bitso(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('api.bitso.com', '/v2/ticker')\n        return {'MXN': to_decimal(json['last'])}\n\n\nclass BitStamp(ExchangeBase):\n\n    async def get_currencies(self):\n        # ref https://www.bitstamp.net/api/#tag/Tickers/operation/GetCurrencyPairTickers\n        json = await self.get_json(\n            'www.bitstamp.net',\n            f\"/api/v2/ticker/\")\n        pairs = [ticker[\"pair\"] for ticker in json]\n        pairs = [pair for pair in pairs\n                 if len(pair) == 7 and pair[:4] == \"BTC/\"]\n        return [pair[4:] for pair in pairs]\n\n    async def get_rates(self, ccy):\n        # ref https://www.bitstamp.net/api/#tag/Tickers/operation/GetMarketTicker\n        if ccy in CURRENCIES[self.name()]:\n            json = await self.get_json('www.bitstamp.net', f'/api/v2/ticker/btc{ccy.lower()}/')\n            return {ccy: to_decimal(json['last'])}\n        return {}\n\n    def history_ccys(self):\n        return CURRENCIES[self.name()]\n\n    async def request_history(self, ccy):\n        # ref https://www.bitstamp.net/api/#tag/Market-info/operation/GetOHLCData\n        merged_history = {}\n        history_starts = 1313625600  # for BTCUSD pair (probably earliest)\n        items_per_request = 1000\n        step = 86400\n\n        async def populate_history(endtime: int):\n            history = await self.get_json(\n                'www.bitstamp.net',\n                f\"/api/v2/ohlc/btc{ccy.lower()}/?step={step}&limit={items_per_request}&end={endtime}\")\n            history = dict([\n                (timestamp_to_datetime(int(h[\"timestamp\"]), utc=True).strftime('%Y-%m-%d'), str(h[\"close\"]))\n                for h in history[\"data\"][\"ohlc\"]])\n            merged_history.update(history)\n\n        async with OldTaskGroup() as group:\n            endtime = int(time.time())\n            while True:\n                if endtime < history_starts:\n                    break\n                await group.spawn(populate_history(endtime=endtime))\n                endtime = endtime - items_per_request * step\n        return merged_history\n\n\nclass Bitvalor(ExchangeBase):\n\n    async def get_rates(self,ccy):\n        json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')\n        return {'BRL': to_decimal(json['ticker_1h']['total']['last'])}\n\n\nclass BlockchainInfo(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('blockchain.info', '/ticker')\n        return dict([(r, to_decimal(json[r]['15m'])) for r in json])\n\n\nclass Bylls(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('bylls.com', '/api/price?from_currency=BTC&to_currency=CAD')\n        return {'CAD': to_decimal(json['public_price']['to_price'])}\n\n\nclass Coinbase(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('api.coinbase.com',\n                             '/v2/exchange-rates?currency=BTC')\n        return {ccy: to_decimal(rate) for (ccy, rate) in json[\"data\"][\"rates\"].items()}\n\n\nclass CoinCap(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('api.coincap.io', '/v2/rates/bitcoin/')\n        return {'USD': to_decimal(json['data']['rateUsd'])}\n\n    def history_ccys(self):\n        return ['USD']\n\n    async def request_history(self, ccy):\n        # Currently 2000 days is the maximum in 1 API call\n        # (and history starts on 2017-03-23)\n        history = await self.get_json('api.coincap.io',\n                                      '/v2/assets/bitcoin/history?interval=d1&limit=2000')\n        return dict([(timestamp_to_datetime(h['time']/1000, utc=True).strftime('%Y-%m-%d'), str(h['priceUsd']))\n                     for h in history['data']])\n\n\nclass CoinDesk(ExchangeBase):\n\n    async def get_currencies(self):\n        dicts = await self.get_json('api.coindesk.com',\n                              '/v1/bpi/supported-currencies.json')\n        return [d['currency'] for d in dicts]\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('api.coindesk.com',\n                             '/v1/bpi/currentprice/%s.json' % ccy)\n        result = {ccy: to_decimal(json['bpi'][ccy]['rate_float'])}\n        return result\n\n    def history_starts(self):\n        return {'USD': '2012-11-30', 'EUR': '2013-09-01'}\n\n    def history_ccys(self):\n        return self.history_starts().keys()\n\n    async def request_history(self, ccy):\n        start = self.history_starts()[ccy]\n        end = datetime.today().strftime('%Y-%m-%d')\n        # Note ?currency and ?index don't work as documented.  Sigh.\n        query = ('/v1/bpi/historical/close.json?start=%s&end=%s'\n                 % (start, end))\n        json = await self.get_json('api.coindesk.com', query)\n        return json['bpi']\n\n\nclass CoinGecko(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('api.coingecko.com', '/api/v3/exchange_rates')\n        return dict([(ccy.upper(), to_decimal(d['value']))\n                     for ccy, d in json['rates'].items()])\n\n    def history_ccys(self):\n        # CoinGecko seems to have historical data for all ccys it supports\n        return CURRENCIES[self.name()]\n\n    async def request_history(self, ccy):\n        # ref https://docs.coingecko.com/v3.0.1/reference/coins-id-market-chart\n        num_days = 365\n        # Setting `num_days = \"max\"` started erroring (around 2024-04) with:\n        # > Your request exceeds the allowed time range. Public API users are limited to querying\n        # > historical data within the past 365 days. Upgrade to a paid plan to enjoy full historical data access\n        history = await self.get_json('api.coingecko.com',\n                                      f\"/api/v3/coins/bitcoin/market_chart?vs_currency={ccy}&days={num_days}\")\n\n        return dict([(timestamp_to_datetime(h[0]/1000, utc=True).strftime('%Y-%m-%d'), str(h[1]))\n                     for h in history['prices']])\n\n\nclass Bit2C(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('bit2c.co.il', '/Exchanges/BtcNis/Ticker.json')\n        return {'ILS': to_decimal(json['ll'])}\n\n    def history_ccys(self):\n        return CURRENCIES[self.name()]\n\n    async def request_history(self, ccy):\n        history = await self.get_json('bit2c.co.il',\n                                      '/Exchanges/BtcNis/KLines?resolution=1D&from=1357034400&to=%s' % int(time.time()))\n\n        return dict([(timestamp_to_datetime(h[0], utc=True).strftime('%Y-%m-%d'), str(h[6]))\n                     for h in history])\n\n\nclass CointraderMonitor(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('cointradermonitor.com', '/api/pbb/v1/ticker')\n        return {'BRL': to_decimal(json['last'])}\n\n\nclass itBit(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        ccys = ['USD', 'EUR', 'SGD']\n        json = await self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy)\n        result = dict.fromkeys(ccys)\n        if ccy in ccys:\n            result[ccy] = to_decimal(json['lastPrice'])\n        return result\n\n\nclass Kraken(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        # ref https://docs.kraken.com/api/docs/rest-api/get-ticker-information\n        ccys = ['EUR', 'USD', 'CAD', 'GBP', 'JPY']\n        pairs = ['XBT%s' % c for c in ccys]\n        json = await self.get_json('api.kraken.com',\n                             '/0/public/Ticker?pair=%s' % ','.join(pairs))\n        return dict((k[-3:], to_decimal(v['c'][0]))\n                     for k, v in json['result'].items())\n\n    # async def request_history(self, ccy):\n    #     # ref https://docs.kraken.com/api/docs/rest-api/get-ohlc-data\n    #     pass  # limited to last 720 steps (step can by 1 day / 7 days / 15 days)\n\n\nclass MercadoBitcoin(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')\n        return {'BRL': to_decimal(json['ticker_1h']['exchanges']['MBT']['last'])}\n\n\nclass Winkdex(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('winkdex.com', '/api/v0/price')\n        return {'USD': to_decimal(json['price']) / 100}\n\n    def history_ccys(self):\n        return ['USD']\n\n    async def request_history(self, ccy):\n        json = await self.get_json('winkdex.com',\n                             \"/api/v0/series?start_time=1342915200\")\n        history = json['series'][0]['results']\n        return dict([(h['timestamp'][:10], str(to_decimal(h['price']) / 100))\n                     for h in history])\n\n\nclass Zaif(ExchangeBase):\n    async def get_rates(self, ccy):\n        json = await self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy')\n        return {'JPY': to_decimal(json['last_price'])}\n\n\nclass Bitragem(ExchangeBase):\n\n    async def get_rates(self,ccy):\n        json = await self.get_json('api.bitragem.com', '/v1/index?asset=BTC&market=BRL')\n        return {'BRL': to_decimal(json['response']['index'])}\n\n\nclass Biscoint(ExchangeBase):\n\n    async def get_rates(self,ccy):\n        json = await self.get_json('api.biscoint.io', '/v1/ticker?base=BTC&quote=BRL')\n        return {'BRL': to_decimal(json['data']['last'])}\n\n\nclass Walltime(ExchangeBase):\n\n    async def get_rates(self, ccy):\n        json = await self.get_json('s3.amazonaws.com',\n                             '/data-production-walltime-info/production/dynamic/walltime-info.json')\n        return {'BRL': to_decimal(json['BRL_XBT']['last_inexact'])}\n\n\ndef dictinvert(d):\n    inv = {}\n    for k, vlist in d.items():\n        for v in vlist:\n            keys = inv.setdefault(v, [])\n            keys.append(k)\n    return inv\n\ndef get_exchanges_and_currencies():\n    # load currencies.json from disk\n    path = resource_path('currencies.json')\n    try:\n        with open(path, 'r', encoding='utf-8') as f:\n            return json.loads(f.read())\n    except Exception:\n        pass\n    # or if not present, generate it now.\n    print(\"cannot find currencies.json. will regenerate it now.\")\n    d = {}\n    is_exchange = lambda obj: (inspect.isclass(obj)\n                               and issubclass(obj, ExchangeBase)\n                               and obj != ExchangeBase)\n    exchanges = dict(inspect.getmembers(sys.modules[__name__], is_exchange))\n\n    async def get_currencies_safe(name, exchange):\n        try:\n            d[name] = await exchange.get_currencies()\n            print(name, \"ok\")\n        except Exception:\n            print(name, \"error\")\n\n    async def query_all_exchanges_for_their_ccys_over_network():\n        async with timeout_after(10):\n            async with OldTaskGroup() as group:\n                for name, klass in exchanges.items():\n                    exchange = klass(None, None)\n                    await group.spawn(get_currencies_safe(name, exchange))\n\n    loop = asyncio.new_event_loop()\n    try:\n        loop.run_until_complete(query_all_exchanges_for_their_ccys_over_network())\n    except Exception as e:\n        pass\n    finally:\n        loop.close()\n    with open(path, 'w', encoding='utf-8') as f:\n        f.write(json.dumps(d, indent=4, sort_keys=True))\n    return d\n\n\nCURRENCIES = get_exchanges_and_currencies()\n\n\ndef get_exchanges_by_ccy(history=True):\n    if not history:\n        return dictinvert(CURRENCIES)\n    d = {}\n    exchanges = CURRENCIES.keys()\n    for name in exchanges:\n        klass = globals()[name]\n        exchange = klass(None, None)\n        d[name] = exchange.history_ccys()\n    return dictinvert(d)\n\n\nclass FxThread(ThreadJob, EventListener, NetworkRetryManager[str]):\n\n    def __init__(self, *, config: SimpleConfig):\n        ThreadJob.__init__(self)\n        NetworkRetryManager.__init__(\n            self,\n            max_retry_delay_normal=SPOT_RATE_REFRESH_TARGET,\n            init_retry_delay_normal=SPOT_RATE_REFRESH_TARGET,\n            max_retry_delay_urgent=SPOT_RATE_REFRESH_TARGET,\n            init_retry_delay_urgent=1,\n        )  # note: we poll every 5 seconds for action, so we won't attempt connections more frequently than that.\n        self.config = config\n        self.register_callbacks()\n        self.ccy = self.get_currency()\n        self.history_used_spot = False\n        self.ccy_combo = None\n        self.hist_checkbox = None\n        self.cache_dir = os.path.join(config.path, 'cache')  # type: str\n        self._trigger = asyncio.Event()\n        self._trigger.set()\n        self.set_exchange(self.config_exchange())\n        make_dir(self.cache_dir)\n\n    @event_listener\n    def on_event_proxy_set(self, *args):\n        self._clear_addr_retry_times()\n        self._trigger.set()\n\n    @staticmethod\n    def get_currencies(history: bool) -> Sequence[str]:\n        d = get_exchanges_by_ccy(history)\n        return sorted(d.keys())\n\n    @staticmethod\n    def get_exchanges_by_ccy(ccy: str, history: bool) -> Sequence[str]:\n        d = get_exchanges_by_ccy(history)\n        return d.get(ccy, [])\n\n    @staticmethod\n    def remove_thousands_separator(text: str) -> str:\n        return text.replace(util.THOUSANDS_SEP, \"\")\n\n    def ccy_amount_str(self, amount, *, add_thousands_sep: bool = False, ccy=None) -> str:\n        prec = CCY_PRECISIONS.get(self.ccy if ccy is None else ccy, 2)\n        fmt_str = \"{:%s.%df}\" % (\",\" if add_thousands_sep else \"\", max(0, prec))\n        try:\n            rounded_amount = round(amount, prec)\n        except decimal.InvalidOperation:\n            rounded_amount = amount\n        text = fmt_str.format(rounded_amount)\n        # replace \",\" -> THOUSANDS_SEP\n        # replace \".\" -> DECIMAL_POINT\n        dp_loc = text.find(\".\")\n        text = text.replace(\",\", util.THOUSANDS_SEP)\n        if dp_loc == -1:\n            return text\n        return text[:dp_loc] + util.DECIMAL_POINT + text[dp_loc+1:]\n\n    def ccy_precision(self, ccy=None) -> int:\n        return CCY_PRECISIONS.get(self.ccy if ccy is None else ccy, 2)\n\n    async def run(self):\n        while True:\n            # keep polling and see if we should refresh spot price or historical prices\n            manually_triggered = False\n            async with ignore_after(5):\n                await self._trigger.wait()\n                self._trigger.clear()\n                manually_triggered = True\n            if not self.is_enabled():\n                continue\n            if manually_triggered and self.has_history():  # maybe refresh historical prices\n                self.exchange.get_historical_rates(self.ccy, self.cache_dir)\n            now = time.time()\n            if not manually_triggered and self.exchange._quotes_timestamp + SPOT_RATE_REFRESH_TARGET > now:\n                continue  # last quote still fresh\n            # If the last quote is relatively recent, we poll at fixed time intervals.\n            # Once it gets close to cache expiry, we change to an exponential backoff, to try to get\n            # a quote before it expires. Also, on Android, we might come back from a sleep after a long time,\n            # with the last quote close to expiry or already expired, in that case we go into exponential backoff.\n            is_urgent = self.exchange._quotes_timestamp + SPOT_RATE_CLOSE_TO_STALE < now\n            addr_name = \"spot-urgent\" if is_urgent else \"spot\"  # this separates retry-counters\n            if self._can_retry_addr(addr_name, urgent=is_urgent):\n                self._trying_addr_now(addr_name)\n                # refresh spot price\n                await self.exchange.update_safe(self.ccy)\n\n    def is_enabled(self) -> bool:\n        return self.config.FX_USE_EXCHANGE_RATE\n\n    def set_enabled(self, b: bool) -> None:\n        self.config.FX_USE_EXCHANGE_RATE = b\n        self.trigger_update()\n\n    def can_have_history(self):\n        return self.is_enabled() and self.ccy in self.exchange.history_ccys()\n\n    def has_history(self) -> bool:\n        return self.can_have_history() and self.config.FX_HISTORY_RATES\n\n    def get_currency(self) -> str:\n        '''Use when dynamic fetching is needed'''\n        return self.config.FX_CURRENCY\n\n    def config_exchange(self):\n        return self.config.FX_EXCHANGE\n\n    def set_currency(self, ccy: str):\n        self.ccy = ccy\n        self.config.FX_CURRENCY = ccy\n        self.trigger_update()\n        self.on_quotes()\n\n    def trigger_update(self):\n        self._clear_addr_retry_times()\n        loop = util.get_asyncio_loop()\n        loop.call_soon_threadsafe(self._trigger.set)\n\n    def set_exchange(self, name):\n        class_ = globals().get(name) or globals().get(self.config.cv.FX_EXCHANGE.get_default_value())\n        self.logger.info(f\"using exchange {name}\")\n        if self.config_exchange() != name:\n            self.config.FX_EXCHANGE = name\n        assert issubclass(class_, ExchangeBase), f\"unexpected type {class_} for {name}\"\n        self.exchange = class_(self.on_quotes, self.on_history)  # type: ExchangeBase\n        # A new exchange means new fx quotes, initially empty.  Force\n        # a quote refresh\n        self.trigger_update()\n        self.exchange.read_historical_rates(self.ccy, self.cache_dir)\n\n    def on_quotes(self, *, received_new_data: bool = False):\n        if received_new_data:\n            self._clear_addr_retry_times()\n        util.trigger_callback('on_quotes')\n\n    def on_history(self):\n        util.trigger_callback('on_history')\n\n    def exchange_rate(self) -> Decimal:\n        \"\"\"Returns the exchange rate as a Decimal\"\"\"\n        if not self.is_enabled():\n            return Decimal('NaN')\n        return self.exchange.get_cached_spot_quote(self.ccy)\n\n    def format_amount(self, btc_balance, *, timestamp: int = None) -> str:\n        if timestamp is None:\n            rate = self.exchange_rate()\n        else:\n            rate = self.timestamp_rate(timestamp)\n        return '' if rate.is_nan() else \"%s\" % self.value_str(btc_balance, rate)\n\n    def format_amount_and_units(self, btc_balance, *, timestamp: int = None) -> str:\n        if timestamp is None:\n            rate = self.exchange_rate()\n        else:\n            rate = self.timestamp_rate(timestamp)\n        return '' if rate.is_nan() else \"%s %s\" % (self.value_str(btc_balance, rate), self.ccy)\n\n    def get_fiat_status_text(self, btc_balance, base_unit, decimal_point):\n        rate = self.exchange_rate()\n        if rate.is_nan():\n            return _(\"  (No FX rate available)\")\n        amount = 1000 if decimal_point == 0 else 1\n        value = self.value_str(amount * COIN / (10**(8 - decimal_point)), rate)\n        return \" %d %s~%s %s\" % (amount, base_unit, value, self.ccy)\n\n    def fiat_value(self, satoshis, rate) -> Decimal:\n        return Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate)\n\n    def value_str(self, satoshis, rate, *, add_thousands_sep: bool = None) -> str:\n        fiat_val = self.fiat_value(satoshis, rate)\n        return self.format_fiat(fiat_val, add_thousands_sep=add_thousands_sep)\n\n    def format_fiat(self, value: Decimal, *, add_thousands_sep: bool = None) -> str:\n        if value.is_nan():\n            return _(\"No data\")\n        if add_thousands_sep is None:\n            add_thousands_sep = True\n        return self.ccy_amount_str(value, add_thousands_sep=add_thousands_sep)\n\n    def history_rate(self, d_t: Optional[datetime]) -> Decimal:\n        if d_t is None:\n            return Decimal('NaN')\n        rate = self.exchange.historical_rate(self.ccy, d_t)\n        # Frequently there is no rate for today, until tomorrow :)\n        # Use spot quotes in that case\n        if rate.is_nan() and (datetime.today().date() - d_t.date()).days <= 2:\n            rate = self.exchange.get_cached_spot_quote(self.ccy)\n            self.history_used_spot = True\n        if rate is None:\n            rate = 'NaN'\n        return Decimal(rate)\n\n    def historical_value_str(self, satoshis, d_t: Optional[datetime]) -> str:\n        return self.format_fiat(self.historical_value(satoshis, d_t))\n\n    def historical_value(self, satoshis, d_t: Optional[datetime]) -> Decimal:\n        return self.fiat_value(satoshis, self.history_rate(d_t))\n\n    def timestamp_rate(self, timestamp: Optional[int]) -> Decimal:\n        from .util import timestamp_to_datetime\n        date = timestamp_to_datetime(timestamp)\n        return self.history_rate(date)\n\n\nassert globals().get(SimpleConfig.FX_EXCHANGE.get_default_value()), f\"default exchange {SimpleConfig.FX_EXCHANGE.get_default_value()} does not exist\"\n"
  },
  {
    "path": "electrum/fee_policy.py",
    "content": "from typing import Optional, Sequence, Tuple, Union, TYPE_CHECKING, Dict\nfrom decimal import Decimal\nfrom numbers import Real\nfrom enum import IntEnum\nimport math\n\nfrom .i18n import _\nfrom .util import NoDynamicFeeEstimates, quantize_feerate, format_fee_satoshis, FEERATE_PRECISION\nfrom . import util, constants\nfrom .logging import Logger\n\nif TYPE_CHECKING:\n    from .network import Network\n\n# 1008 = max conf target of core's estimatesmartfee, requesting more results in rpc error.\n# estimatesmartfee guarantees that the fee will get accepted into the mempool\nFEE_ETA_TARGETS = [1008, 144, 25, 10, 5, 2, 1]\nFEE_DEPTH_TARGETS = [10_000_000, 5_000_000, 2_000_000, 1_000_000,\n                     800_000, 600_000, 400_000, 250_000, 100_000]\nFEERATE_STATIC_VALUES = [1000, 2000, 5000, 10000, 20000, 30000,\n                         50000, 70000, 100000, 150000, 200000, 300000]\n\n# satoshi per kbyte\nFEERATE_MAX_DYNAMIC = 1500000\nFEERATE_WARNING_HIGH_FEE = 600000\nFEERATE_FALLBACK_STATIC_FEE = 150000\nFEERATE_REGTEST_STATIC_FEE = FEERATE_FALLBACK_STATIC_FEE  # hardcoded fee used on regtest\nFEERATE_MIN_RELAY = 100\nFEERATE_DEFAULT_RELAY = 1000  # conservative \"min relay fee\"\nFEERATE_MAX_RELAY = 50000\nassert FEERATE_MIN_RELAY <= FEERATE_DEFAULT_RELAY <= FEERATE_MAX_RELAY\n\n# warn user if fee/amount for on-chain tx is higher than this\nFEE_RATIO_HIGH_WARNING = 0.05\n\n# note: make sure the network is asking for estimates for these targets\nFEE_LN_ETA_TARGET = 2\nFEE_LN_LOW_ETA_TARGET = 25\nFEE_LN_MINIMUM_ETA_TARGET = 1008\n\n\n# The min feerate_per_kw that can be used in lightning so that\n# the resulting onchain tx pays the min relay fee.\n# This would be FEERATE_DEFAULT_RELAY / 4 if not for rounding errors,\n# see https://github.com/ElementsProject/lightning/commit/2e687b9b352c9092b5e8bd4a688916ac50b44af0\nFEERATE_PER_KW_MIN_RELAY_LIGHTNING = 253\n\n\ndef closest_index(value, array) -> int:\n    dist = list(map(lambda x: abs(x - value), array))\n    return min(range(len(dist)), key=dist.__getitem__)\n\n\nclass FeeMethod(IntEnum):\n    # note: careful changing these names! they appear in the config files.\n    FIXED = 0    # fixed absolute fee\n    FEERATE = 1  # fixed fee rate\n    ETA = 2      # dynamic, ETA based\n    MEMPOOL = 3  # dynamic, mempool based\n\n    @classmethod\n    def slider_values(cls):\n        return [FeeMethod.FEERATE, FeeMethod.ETA, FeeMethod.MEMPOOL]\n\n    def name_for_GUI(self):\n        names = {\n            FeeMethod.FIXED: _('FIXED'),\n            FeeMethod.FEERATE: _('Feerate'),\n            FeeMethod.ETA: _('ETA'),\n            FeeMethod.MEMPOOL: _('Mempool')\n        }\n        return names[self]\n\n    @classmethod\n    def slider_index_of_method(cls, method):\n        try:\n            i = FeeMethod.slider_values().index(method)\n        except ValueError:\n            i = -1\n        return i\n\n\nclass FeePolicy(Logger):\n    # object associated to a fee slider\n\n    def __init__(self, descriptor: str):\n        Logger.__init__(self)\n        try:\n            name, value = descriptor.split(':')\n            self.method = FeeMethod[name.upper()]\n            self.value = int(value)  # target (e.g. num blocks, nbytes from mempool tip, sat/kbyte)\n        except Exception:\n            self.logger.warning(f\"Could not parse fee policy descriptor '{descriptor}'. Falling back to 'eta:2'\")\n            self.method = FeeMethod.ETA\n            self.value = 2\n\n    def __repr__(self):\n        return self.get_descriptor()\n\n    def get_descriptor(self) -> str:\n        return self.method.name.lower() + ':' + str(self.value)\n\n    def set_method(self, method: FeeMethod):\n        assert isinstance(method, FeeMethod)\n        self.method = method\n        # default values\n        if self.method == FeeMethod.MEMPOOL:\n            self.value = 1000000 # 1 mb from tip\n        elif self.method == FeeMethod.ETA:\n            self.value = 2 # 2 blocks\n        elif self.method == FeeMethod.FEERATE:\n            self.value = 5000 # sats per vkb\n        else:\n            self.value = 10 # sats\n\n    def _get_array(self) -> Sequence[int]:\n        if self.method == FeeMethod.MEMPOOL:\n            return FEE_DEPTH_TARGETS\n        elif self.method == FeeMethod.ETA:\n            return FEE_ETA_TARGETS\n        elif self.method == FeeMethod.FEERATE:\n            return FEERATE_STATIC_VALUES\n        else:\n            raise Exception('')\n\n    def set_value_from_slider_pos(self, slider_pos: int):\n        array = self._get_array()\n        slider_pos = max(0, min(slider_pos, len(array)-1))\n        self.value = array[slider_pos]\n\n    def get_slider_pos(self) -> int:\n        array = self._get_array()\n        return closest_index(self.value, array)\n\n    def get_slider_max(self) -> int:\n        array = self._get_array()\n        maxp = len(array) - 1\n        return maxp\n\n    @property\n    def use_dynamic_estimates(self):\n        return self.method in [FeeMethod.ETA, FeeMethod.MEMPOOL]\n\n    @classmethod\n    def depth_target(cls, slider_pos: int) -> int:\n        \"\"\"Returns mempool depth target in bytes for a fee slider position.\"\"\"\n        slider_pos = max(slider_pos, 0)\n        slider_pos = min(slider_pos, len(FEE_DEPTH_TARGETS)-1)\n        return FEE_DEPTH_TARGETS[slider_pos]\n\n    def eta_target(self, slider_pos: int) -> int:\n        \"\"\"Returns 'num blocks' ETA target for a fee slider position.\"\"\"\n        return FEE_ETA_TARGETS[slider_pos]\n\n    @classmethod\n    def eta_tooltip(cls, x):\n        if x < 0:\n            return _('Low fee')\n        elif x == 1:\n            return _('In the next block')\n        elif x == 144:\n            return _('Within one day')\n        elif x == 1008:\n            return _(\"Within one week\")\n        else:\n            return _('Within {} blocks').format(x)\n\n    def get_target_text(self):\n        \"\"\" Description of what the target is: static fee / num blocks to confirm in / mempool depth \"\"\"\n        if self.method == FeeMethod.ETA:\n            return self.eta_tooltip(self.value)\n        elif self.method == FeeMethod.MEMPOOL:\n            return self.depth_tooltip(self.value)\n        elif self.method == FeeMethod.FEERATE:\n            fee_per_byte = self.value/1000\n            return format_fee_satoshis(fee_per_byte) + f\" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}\"\n        elif self.method == FeeMethod.FIXED:\n            return f'{self.value} {util.UI_UNIT_NAME_FIXED_SAT}'\n\n    def get_estimate_text(self, network: 'Network') -> str:\n        \"\"\"\n        Description of the current fee estimate corresponding to the target\n        \"\"\"\n        fee_per_kb = self.fee_per_kb(network)\n        fee_per_byte = fee_per_kb/1000 if fee_per_kb is not None else None\n        tooltip = ''\n        if self.use_dynamic_estimates:\n            if fee_per_byte is not None:\n                tooltip = format_fee_satoshis(fee_per_byte) + f\" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}\"\n        elif self.method == FeeMethod.FEERATE:\n            assert fee_per_kb is not None\n            assert fee_per_byte is not None\n            if network and network.mempool_fees.has_data():\n                depth = network.mempool_fees.fee_to_depth(fee_per_byte)\n                tooltip = self.depth_tooltip(depth)\n            if network and network.fee_estimates.has_data():\n                eta = network.fee_estimates.fee_to_eta(fee_per_kb)\n                tooltip += '\\n' + self.eta_tooltip(eta)\n        return tooltip\n\n    def get_tooltip(self, network: 'Network'):\n        target = self.get_target_text()\n        estimate = self.get_estimate_text(network)\n        if self.use_dynamic_estimates:\n            return _('Target') + ': ' + target + '\\n' + _('Current rate') + ': ' + estimate\n        else:\n            return _('Fixed rate') + ': ' + target + '\\n' + _('Estimate') + ': ' + estimate\n\n    @classmethod\n    def depth_tooltip(cls, depth: Optional[int]) -> str:\n        \"\"\"Returns text tooltip for given mempool depth (in vbytes).\"\"\"\n        if depth is None:\n            return \"unknown from tip\"\n        depth_mb = cls.get_depth_mb_str(depth)\n        return _(\"{} from tip\").format(depth_mb)\n\n    @classmethod\n    def get_depth_mb_str(cls, depth: int) -> str:\n        # e.g. 500_000 -> \"0.50 MB\"\n        depth_mb = \"{:.2f}\".format(depth / 1_000_000)  # maybe .rstrip(\"0\") ?\n        return f\"{depth_mb} {util.UI_UNIT_NAME_MEMPOOL_MB}\"\n\n    def fee_per_kb(self, network: 'Network') -> Optional[int]:\n        \"\"\"Returns sat/kvB fee to pay for a txn.\n        Note: might return None.\n        \"\"\"\n        if self.method == FeeMethod.FEERATE:\n            fee_rate = self.value\n        elif self.method == FeeMethod.MEMPOOL:\n            if network:\n                fee_rate = network.mempool_fees.depth_to_fee(self.get_slider_pos())\n            else:\n                fee_rate = None\n        elif self.method == FeeMethod.ETA:\n            if network:\n                fee_rate = network.fee_estimates.eta_to_fee(self.get_slider_pos())\n            else:\n                fee_rate = None\n        elif self.method == FeeMethod.FIXED:\n            fee_rate = None\n        else:\n            raise Exception(self.method)\n        if fee_rate is not None:\n            fee_rate = int(fee_rate)\n        return fee_rate\n\n    def fee_per_byte(self, network: 'Network') -> Optional[int]:\n        \"\"\"Returns sat/vB fee to pay for a txn.\n        Note: might return None.\n        \"\"\"\n        fee_per_kb = self.fee_per_kb(network)\n        return fee_per_kb / 1000 if fee_per_kb is not None else None\n\n    def estimate_fee(\n            self, size: Union[int, float, Decimal], *,\n            network: 'Network' = None,\n            allow_fallback_to_static_rates: bool = False,\n    ) -> int:\n        if self.method == FeeMethod.FIXED:\n            return self.value\n        fee_per_kb = self.fee_per_kb(network)\n        if fee_per_kb is None and self.use_dynamic_estimates:\n            if allow_fallback_to_static_rates:\n                fee_per_kb = FEERATE_FALLBACK_STATIC_FEE\n            else:\n                raise NoDynamicFeeEstimates()\n\n        return self.estimate_fee_for_feerate(fee_per_kb=fee_per_kb, size=size)\n\n    @classmethod\n    def estimate_fee_for_feerate(\n        cls,\n        *,\n        fee_per_kb: Union[int, float, Decimal],\n        size: Union[int, float, Decimal],\n    ) -> int:\n        # note: 'size' is in vbytes\n        size = Decimal(size)\n        fee_per_kb = Decimal(fee_per_kb)\n        fee_per_byte = fee_per_kb / 1000\n        # to be consistent with what is displayed in the GUI,\n        # the calculation needs to use the same precision:\n        fee_per_byte = quantize_feerate(fee_per_byte)\n        return math.ceil(fee_per_byte * size)\n\n\nclass FixedFeePolicy(FeePolicy):\n    def __init__(self, fee):\n        FeePolicy.__init__(self, 'fixed:%d' % fee)\n\n\ndef impose_hard_limits_on_fee(func):\n    def get_fee_within_limits(self, *args, **kwargs):\n        fee = func(self, *args, **kwargs)\n        if fee is None:\n            return fee\n        fee = min(FEERATE_MAX_DYNAMIC, fee)\n        # Clamp dynamic feerates with conservative min relay fee,\n        # to ensure txs propagate well:\n        fee = max(FEERATE_DEFAULT_RELAY, fee)\n        return fee\n    return get_fee_within_limits\n\n\nclass FeeHistogram:\n\n    def __init__(self):\n        self._data = None # type: Optional[Sequence[Tuple[Union[float, int], int]]]\n\n    def has_data(self) -> bool:\n        return self._data is not None\n\n    def set_data(self, data):\n        self._data = data\n\n    def fee_to_depth(self, target_fee: Real) -> Optional[int]:\n        \"\"\"For a given sat/vbyte fee, returns an estimate of how deep\n        it would be in the current mempool in vbytes.\n        Pessimistic == overestimates the depth.\n        \"\"\"\n        if self._data is None:\n            return None\n        depth = 0\n        for fee, s in self._data:\n            depth += s\n            if fee <= target_fee:\n                break\n        return depth\n\n    @impose_hard_limits_on_fee\n    def depth_target_to_fee(self, target: int) -> Optional[int]:\n        \"\"\"Returns fee in sat/kbyte.\n        target: desired mempool depth in vbytes\n        \"\"\"\n        if self._data is None:\n            return None\n        depth = 0\n        for fee, s in self._data:\n            depth += s\n            if depth > target:\n                break\n        else:\n            return 0\n        # add one sat/byte as currently that is the max precision of the histogram\n        # note: precision depends on server.\n        #       old ElectrumX <1.16 has 1 s/b prec, >=1.16 has 0.1 s/b prec.\n        #       electrs seems to use untruncated double-precision floating points.\n        #       # TODO decrease this to 0.1 s/b next time we bump the required protocol version\n        fee += 1\n        # convert to sat/kbyte\n        return int(fee * 1000)\n\n    def depth_to_fee(self, slider_pos) -> Optional[int]:\n        \"\"\"Returns fee in sat/kbyte.\"\"\"\n        target = FeePolicy.depth_target(slider_pos)\n        return self.depth_target_to_fee(target)\n\n    def get_capped_data(self):\n        \"\"\" used by QML \"\"\"\n        data = self._data or [[FEERATE_DEFAULT_RELAY/1000, 1]]\n        # cap the histogram to a limited number of megabytes\n        bytes_limit = 10*1000*1000\n        bytes_current = 0\n        capped_histogram = []\n        for item in sorted(data, key=lambda x: x[0], reverse=True):\n            if bytes_current >= bytes_limit:\n                break\n            slot = min(item[1], bytes_limit - bytes_current)\n            bytes_current += slot\n            # round & limit precision\n            value = int(item[0] * 10**FEERATE_PRECISION) / 10**FEERATE_PRECISION\n            capped_histogram.append([\n                max(FEERATE_MIN_RELAY/1000, value),  # clamped to [FEERATE_MIN_RELAY/1000, inf)\n                slot,  # width of bucket\n                bytes_current,  # cumulative depth at far end of bucket\n            ])\n        return capped_histogram, bytes_current\n\n\nclass FeeTimeEstimates:\n\n    def __init__(self):\n        self.data = {} # type: Dict[int, int]\n\n    def get_data(self):\n        return self.data\n\n    def has_data(self) -> bool:\n        \"\"\"Returns if we have estimates for *all* targets requested.\n        Note: if wanting an estimate for a specific target, instead of checking has_data(),\n              just try to do the estimate and handle a potential None result. That way,\n              estimation works for targets we have, even if some targets are missing.\n        \"\"\"\n        targets = set(FEE_ETA_TARGETS)\n        targets.discard(1)  # rm \"next block\" target\n        return all(target in self.data for target in targets)\n\n    def set_data(self, nblock_target: int, fee_per_kb: int):\n        assert isinstance(nblock_target, int), f\"expected int, got {nblock_target!r}\"\n        assert isinstance(fee_per_kb, int), f\"expected int, got {fee_per_kb!r}\"\n        self.data[nblock_target] = fee_per_kb\n\n    def fee_to_eta(self, fee_per_kb: Optional[int]) -> int:\n        \"\"\"Returns 'num blocks' ETA estimate for given fee rate,\n        or -1 for low fee.\n        \"\"\"\n        import operator\n        lst = list(self.data.items())\n        next_block_fee = self.eta_target_to_fee(1)\n        if next_block_fee is not None:\n            lst += [(1, next_block_fee)]\n        if not lst or fee_per_kb is None:\n            return -1\n        dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), lst)\n        min_target, min_value = min(dist, key=operator.itemgetter(1))\n        if fee_per_kb < self.data.get(FEE_ETA_TARGETS[0])/2:\n            min_target = -1\n        return min_target\n\n    def eta_to_fee(self, slider_pos) -> Optional[int]:\n        \"\"\"Returns fee in sat/kbyte.\"\"\"\n        slider_pos = max(slider_pos, 0)\n        slider_pos = min(slider_pos, len(FEE_ETA_TARGETS) - 1)\n        if slider_pos < len(FEE_ETA_TARGETS) - 1:\n            num_blocks = FEE_ETA_TARGETS[int(slider_pos)]\n            fee = self.eta_target_to_fee(num_blocks)\n        else:\n            fee = self.eta_target_to_fee(1)\n        return fee\n\n    @impose_hard_limits_on_fee\n    def eta_target_to_fee(self, num_blocks: int) -> Optional[int]:\n        \"\"\"Returns fee in sat/kbyte.\"\"\"\n        if num_blocks == 1:\n            fee = self.data.get(2)\n            if fee is not None:\n                fee += fee / 2\n                fee = int(fee)\n        else:\n            fee = self.data.get(num_blocks)\n            if fee is not None:\n                fee = int(fee)\n        # fallback for regtest\n        if fee is None and constants.net is constants.BitcoinRegtest:\n            return FEERATE_REGTEST_STATIC_FEE\n        return fee\n"
  },
  {
    "path": "electrum/gui/__init__.py",
    "content": "# To create a new GUI, please add its code to this directory.\n# Three objects are passed to the ElectrumGui: config, daemon and plugins\n# The Wallet object is instantiated by the GUI\n\n# Notifications about network events are sent to the GUI by using network.register_callback()\n\nfrom typing import TYPE_CHECKING, Mapping, Optional\n\nif TYPE_CHECKING:\n    from . import qt\n    from electrum.simple_config import SimpleConfig\n    from electrum.daemon import Daemon\n    from electrum.plugin import Plugins\n\n\nclass BaseElectrumGui:\n    def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'):\n        self.config = config\n        self.daemon = daemon\n        self.plugins = plugins\n\n    def main(self) -> None:\n        raise NotImplementedError()\n\n    def stop(self) -> None:\n        \"\"\"Stops the GUI.\n        This method must be thread-safe.\n        \"\"\"\n        pass\n\n    @classmethod\n    def version_info(cls) -> Mapping[str, Optional[str]]:\n        return {}\n"
  },
  {
    "path": "electrum/gui/common_qt/__init__.py",
    "content": "# Copyright (C) 2023 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\n"
  },
  {
    "path": "electrum/gui/common_qt/i18n.py",
    "content": "from PyQt6.QtCore import QTranslator\n\nfrom electrum.i18n import _\n\n\nclass ElectrumTranslator(QTranslator):\n    \"\"\"Delegator for Qt translations to gettext\"\"\"\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        # explicit enumeration of translatable strings from Qt standard library, so these\n        # will be included in the electrum gettext translation template\n        self._strings = [_('&Undo'), _('&Redo'), _('Cu&t'), _('&Copy'), _('&Paste'), _('Select All'),\n                         _('Copy &Link Location')]\n\n    def translate(self, context, source_text: str, disambiguation, n):\n        return _(source_text, context=context)\n"
  },
  {
    "path": "electrum/gui/common_qt/plugins.py",
    "content": "import sys\nfrom typing import TYPE_CHECKING, Optional\n\nfrom PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject\n\nfrom electrum.logging import get_logger\n\nif TYPE_CHECKING:\n    from electrum.gui.qml import ElectrumQmlApplication\n    from electrum.plugin import BasePlugin\n\n\nclass PluginQObject(QObject):\n    logger = get_logger(__name__)\n\n    pluginChanged = pyqtSignal()\n    busyChanged = pyqtSignal()\n    pluginEnabledChanged = pyqtSignal()\n\n    def __init__(self, plugin: 'BasePlugin', parent: Optional['ElectrumQmlApplication']):\n        super().__init__(parent)\n\n        self._busy = False\n\n        self.plugin = plugin\n        self.app = parent\n\n    @pyqtProperty(str, notify=pluginChanged)\n    def name(self): return self._name\n\n    @pyqtProperty(bool, notify=busyChanged)\n    def busy(self): return self._busy\n\n    # below only used for QML, not compatible yet with Qt\n\n    @pyqtProperty(bool, notify=pluginEnabledChanged)\n    def pluginEnabled(self): return self.plugin.is_enabled()\n\n    @pluginEnabled.setter\n    def pluginEnabled(self, enabled):\n        if enabled != self.plugin.is_enabled():\n            self.logger.debug(f'can {self.plugin.can_user_disable()}, {self.plugin.is_available()}')\n            if not self.plugin.can_user_disable() and not enabled:\n                return\n            if enabled:\n                self.app.plugins.enable(self.plugin.name)\n            else:\n                self.app.plugins.disable(self.plugin.name)\n            self.pluginEnabledChanged.emit()\n\n"
  },
  {
    "path": "electrum/gui/common_qt/util.py",
    "content": "import queue\nimport sys\nfrom functools import wraps\nfrom typing import Optional, NamedTuple, Callable\nimport os.path\n\nfrom PyQt6 import QtGui\nfrom PyQt6.QtCore import Qt, QThread, pyqtSignal\nfrom PyQt6.QtGui import QColor, QPen, QPaintDevice, QFontDatabase, QImage\nimport qrcode\n\nfrom electrum.i18n import _\nfrom electrum.logging import Logger\nfrom electrum.util import EventListener, event_listener\n\n_cached_font_ids: dict[str, int] = {}\n\n\ndef get_font_id(filename: str) -> int:\n    font_id = _cached_font_ids.get(filename)\n    if font_id is not None:\n        return font_id\n    # font_id will be negative on error\n    font_id = QFontDatabase.addApplicationFont(\n        os.path.join(os.path.dirname(__file__), '..', 'fonts', filename)\n    )\n    _cached_font_ids[filename] = font_id\n    return font_id\n\n\ndef draw_qr(\n    *,\n    qr: Optional[qrcode.main.QRCode],\n    paint_device: QPaintDevice,  # target to paint on\n    is_enabled: bool = True,\n    min_boxsize: int = 2,  # min size in pixels of single black/white unit box of the qr code\n) -> None:\n    \"\"\"Draw 'qr' onto 'paint_device'.\n    - qr.box_size is ignored. We will calculate our own boxsize to fill the whole size of paint_device.\n    - qr.border is respected.\n    \"\"\"\n    black = QColor(0, 0, 0, 255)\n    grey = QColor(196, 196, 196, 255)\n    white = QColor(255, 255, 255, 255)\n    black_pen = QPen(black) if is_enabled else QPen(grey)\n    black_pen.setJoinStyle(Qt.PenJoinStyle.MiterJoin)\n\n    if not qr:\n        qp = QtGui.QPainter()\n        qp.begin(paint_device)\n        qp.setBrush(white)\n        qp.setPen(white)\n        r = qp.viewport()\n        qp.drawRect(0, 0, r.width(), r.height())\n        qp.end()\n        return\n\n    # note: next line can raise qrcode.exceptions.DataOverflowError (or ValueError)\n    matrix = qr.get_matrix()  # includes qr.border\n    k = len(matrix)\n    qp = QtGui.QPainter()\n    qp.begin(paint_device)\n    r = qp.viewport()\n    framesize = min(r.width(), r.height())\n    boxsize = int(framesize / k)\n    if boxsize < min_boxsize:\n        # The amount of data is still within what can fit into a QR code,\n        # however we don't have enough pixels to draw it.\n        qp.setBrush(white)\n        qp.setPen(white)\n        qp.drawRect(0, 0, r.width(), r.height())\n        qp.setBrush(black)\n        qp.setPen(black)\n        qp.drawText(0, 20, _(\"Cannot draw QR code\") + \":\")\n        qp.drawText(0, 40, _(\"Not enough space available.\"))\n        qp.end()\n        return\n    size = k * boxsize\n    left = (framesize - size) / 2\n    top = (framesize - size) / 2\n    # Draw white background with margin\n    qp.setBrush(white)\n    qp.setPen(white)\n    qp.drawRect(0, 0, framesize, framesize)\n    # Draw qr code\n    qp.setBrush(black if is_enabled else grey)\n    qp.setPen(black_pen)\n    for r in range(k):\n        for c in range(k):\n            if matrix[r][c]:\n                qp.drawRect(\n                    int(left + c * boxsize), int(top + r * boxsize),\n                    boxsize - 1, boxsize - 1)\n    qp.end()\n\n\ndef paintQR(data) -> Optional[QImage]:\n    if not data:\n        return None\n\n    # Create QR code\n    qr = qrcode.QRCode()\n    qr.add_data(data)\n\n    # Create a QImage to draw on\n    matrix = qr.get_matrix()\n    k = len(matrix)\n    boxsize = 5\n    size = k * boxsize\n\n    # Create the image with appropriate size\n    base_img = QImage(size, size, QImage.Format.Format_ARGB32)\n\n    # Use draw_qr to paint on the image\n    draw_qr(\n        qr=qr,\n        paint_device=base_img,\n        is_enabled=True,\n        min_boxsize=boxsize\n    )\n\n    return base_img\n\n\nclass TaskThread(QThread, Logger):\n    \"\"\"Thread that runs background tasks.  Callbacks are guaranteed\n    to happen in the context of its parent.\"\"\"\n\n    class Task(NamedTuple):\n        task: Callable\n        cb_success: Optional[Callable]\n        cb_done: Optional[Callable]\n        cb_error: Optional[Callable]\n        cancel: Optional[Callable] = None\n\n    doneSig = pyqtSignal(object, object, object)\n\n    def __init__(self, parent, on_error=None):\n        QThread.__init__(self, parent)\n        Logger.__init__(self)\n        self.on_error = on_error\n        self.tasks = queue.Queue()\n        self._cur_task = None  # type: Optional[TaskThread.Task]\n        self._stopping = False\n        self.doneSig.connect(self.on_done)\n        self.start()\n\n    def add(self, task, on_success=None, on_done=None, on_error=None, *, cancel=None):\n        if self._stopping:\n            self.logger.warning(f\"stopping or already stopped but tried to add new task.\")\n            return\n        on_error = on_error or self.on_error\n        task_ = TaskThread.Task(task, on_success, on_done, on_error, cancel=cancel)\n        self.tasks.put(task_)\n\n    def run(self):\n        while True:\n            if self._stopping:\n                break\n            task = self.tasks.get()  # type: TaskThread.Task\n            self._cur_task = task\n            if not task or self._stopping:\n                break\n            try:\n                result = task.task()\n                self.doneSig.emit(result, task.cb_done, task.cb_success)\n            except BaseException:\n                self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error)\n\n    def on_done(self, result, cb_done, cb_result):\n        # This runs in the parent's thread.\n        if cb_done:\n            cb_done()\n        if cb_result:\n            cb_result(result)\n\n    def stop(self):\n        self._stopping = True\n        # try to cancel currently running task now.\n        # if the task does not implement \"cancel\", we will have to wait until it finishes.\n        task = self._cur_task\n        if task and task.cancel:\n            task.cancel()\n        # cancel the remaining tasks in the queue\n        while True:\n            try:\n                task = self.tasks.get_nowait()\n            except queue.Empty:\n                break\n            if task and task.cancel:\n                task.cancel()\n        self.tasks.put(None)  # in case the thread is still waiting on the queue\n        self.exit()\n        self.wait()\n\n\nclass QtEventListener(EventListener):\n    qt_callback_signal = pyqtSignal(tuple)\n\n    def register_callbacks(self):\n        self.qt_callback_signal.connect(self.on_qt_callback_signal)\n        EventListener.register_callbacks(self)\n\n    def unregister_callbacks(self):\n        try:\n            self.qt_callback_signal.disconnect()\n        except (RuntimeError, TypeError):  # wrapped Qt object might be deleted\n            # \"TypeError: disconnect() failed between 'qt_callback_signal' and all its connections\"\n            pass\n        EventListener.unregister_callbacks(self)\n\n    def on_qt_callback_signal(self, args):\n        func = args[0]\n        return func(self, *args[1:])\n\n\n# decorator for members of the QtEventListener class\ndef qt_event_listener(func):\n    func = event_listener(func)\n\n    @wraps(func)\n    def decorator(self, *args):\n        self.qt_callback_signal.emit((func,) + args)\n    return decorator\n"
  },
  {
    "path": "electrum/gui/default_lang.py",
    "content": "# Copyright (C) 2023 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n#\n# Note: try not to import modules from electrum, or at least from GUIs.\n#       This is to avoid evaluating module-level string-translations before we get\n#       a chance to set the default language.\n\nimport os\nfrom typing import Optional\n\nfrom electrum.i18n import languages\n\n\njLocale = None\nif \"ANDROID_DATA\" in os.environ:\n    from jnius import autoclass, cast\n    jLocale = autoclass(\"java.util.Locale\")\n\n\ndef get_default_language(*, gui_name: Optional[str] = None) -> str:\n    if gui_name == \"qt\":\n        from PyQt6.QtCore import QLocale\n        name = QLocale.system().name()\n        return name if name in languages else \"en_UK\"\n    elif gui_name == \"qml\":\n        from PyQt6.QtCore import QLocale\n        # On Android QLocale does not return the system locale\n        try:\n            name = str(jLocale.getDefault().toString())\n        except Exception:\n            name = QLocale.system().name()\n        return name if name in languages else \"en_GB\"\n    return \"\"\n"
  },
  {
    "path": "electrum/gui/fonts/PTMono.LICENSE",
    "content": "Copyright (c) 2011, ParaType Ltd. (http://www.paratype.com/public),\nwith Reserved Font Names \"PT Sans\", \"PT Serif\", \"PT Mono\" and \"ParaType\".\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttp://scripts.sil.org/OFL\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "electrum/gui/messages.py",
    "content": "from electrum.i18n import _\nfrom electrum.submarine_swaps import MIN_FINAL_CLTV_DELTA_FOR_CLIENT\n\n\ndef to_rtf(msg):\n    return '\\n'.join(['<p>' + x + '</p>' for x in msg.split('\\n\\n')])\n\n\nMSG_COOPERATIVE_CLOSE = _(\n\"\"\"Your node will negotiate the transaction fee with the remote node. This method of closing the channel usually results in the lowest fees.\"\"\"\n)\n\nMSG_REQUEST_FORCE_CLOSE = _(\n\"\"\"If you request a force-close, your node will pretend that it has lost its data and ask the remote node to broadcast their latest state. Doing so from time to time helps make sure that nodes are honest, because your node can punish them if they broadcast a revoked state.\"\"\"\n)\n\nMSG_CREATED_NON_RECOVERABLE_CHANNEL = _(\n\"\"\"The channel you created is not recoverable from seed.\nTo prevent fund losses, please save this backup on another device.\nIt may be imported in another Electrum wallet with the same seed.\"\"\"\n)\n\nMSG_LIGHTNING_WARNING = _(\n\"\"\"Electrum uses static channel backups. If you lose your wallet file, you will need to request your channel to be force-closed by the remote peer in order to recover your funds. This assumes that the remote peer is reachable, and has not lost its own data.\"\"\"\n)\n\nMSG_THIRD_PARTY_PLUGIN_WARNING = ' '.join([\n    '<b>' + _('Warning: Third-party plugins have access to your wallet!') + '</b>',\n    '<br/><br/>',\n    _('Installing this plugin will grant third-party software access to your wallet. You must trust the plugin not to be malicious.'),\n    _('You should at minimum check who the author of the plugin is, and be careful of imposters.'),\n    '<br/><br/>',\n    _('Third-party plugins are not endorsed by Electrum.'),\n    _('Electrum will not be responsible in case of theft, loss of funds or privacy that might result from third-party plugins.'),\n    '<br/><br/>',\n    _('To install this plugin, please enter your plugin authorization password') + ':'\n])\n\nMSG_CONFLICTING_BACKUP_INSTANCE = _(\n\"\"\"Another instance of this wallet (same seed) has an open channel with the same remote node. If you create this channel, you will not be able to use both wallets at the same time.\n\nAre you sure?\"\"\"\n)\n\nMSG_LN_EXPLAIN_SCB_BACKUPS = \"\".join([\n    _(\"Channel backups can be imported in another instance of the same wallet.\"), \" \",\n    _(\"In the Electrum mobile app, use the 'Send' button to scan this QR code.\"), \" \",\n    \"\\n\\n\",\n    _(\"Please note that channel backups cannot be used to restore your channels.\"), \" \",\n    _(\"If you lose your wallet file, the only thing you can do with a backup is to request your channel to be closed, so that your funds will be sent on-chain.\"),\n])\n\nMSG_CAPITAL_GAINS = _(\n\"\"\"This summary covers only on-chain transactions (no lightning!). Capital gains are computed by attaching an acquisition price to each UTXO in the wallet, and uses the order of blockchain events (not FIFO).\"\"\"\n)\n\nMSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP = _(\n\"\"\"This channel is with a non-trampoline node; it cannot be used if trampoline is enabled.\nIf you want to keep using this channel, you need to disable trampoline routing in your preferences.\"\"\"\n)\n\nMSG_FREEZE_ADDRESS = _(\"When you freeze an address, the funds in that address will not be used for sending bitcoins.\")\nMSG_FREEZE_COIN = _(\"When you freeze a coin, it will not be used for sending bitcoins.\")\n\nMSG_FORWARD_SWAP_FUNDING_MEMPOOL = (\n    _('Your funding transaction has been broadcast.') + \" \" +\n    _(\"Please remain online until the funding transaction is confirmed.\") + \"\\n\\n\" +\n    _('The swap will be finalized once your transaction is confirmed.') + \" \" +\n    _(\"After the funding transaction is mined, the server will reveal the preimage needed to \"\n      \"fulfill the pending received lightning HTLCs. The HTLCs expire in {} blocks. \"\n      \"You will need to be online after the funding transaction is confirmed but before the HTLCs expire, \"\n      \"to claim your money. If you go offline for several days while the swap is pending, \"\n      \"you risk losing the swap amount!\").format(MIN_FINAL_CLTV_DELTA_FOR_CLIENT)\n)\n\nMSG_REVERSE_SWAP_FUNDING_MEMPOOL = (\n    _('The funding transaction has been detected.') + \" \" +\n    _('Your claiming transaction will be broadcast when the funding transaction is confirmed.') + \" \" +\n    _(\"If you go offline before broadcasting the claiming transaction and let the swap time out, \"\n      \"you will not get back the already pre-paid mining fees.\")\n)\n\nMSG_FORCE_CLOSE_WARNING = (\n    _('You will need to come back online after the commitment transaction is confirmed, in order to broadcast second-stage htlc transactions.') + ' ' +\n    _('If you remain offline for more than {} blocks, your channel counterparty will be able to sweep those funds.')\n)\n\nMSG_FORWARD_SWAP_WARNING = (\n    _('You will need to come back online after the funding transaction is confirmed, in order to settle the swap.') + ' ' +\n    _('If you remain offline for more than {} blocks, your channel will be force closed and you might lose the funds you sent in the swap.')\n)\n\nMSG_REVERSE_SWAP_WARNING = (\n    _('You will need to come back online after the funding transaction is confirmed, in order to settle the swap.') + ' ' +\n    _('If you remain offline for more than {} blocks, the swap will be cancelled and you will lose the prepaid mining fees.')\n)\n\nMSG_LN_UTXO_RESERVE = (\n    _(\"You do not have enough on-chain funds to protect your Lightning channels.\") + ' ' +\n    _(\"You should have at least {} on-chain in order to be able to sweep channel outputs.\")\n)\n\n# not to be translated\nMSG_TERMS_OF_USE = (\n\"\"\"1. Electrum is distributed under the MIT licence by Electrum Technologies GmbH. Most notably, this means that the Electrum software is provided as is, and that it comes without warranty.\n\n2. We are neither a bank nor a financial service provider. In addition, we do not store user account data, and we are not an intermediary in the interaction between our software and the Bitcoin blockchain. Therefore, we do not have the possibility to freeze funds or to undo a fraudulent transaction.\n\n3. We do not provide private user support. All issue resolutions are public, and take place on Github or public forums. If someone posing as 'Electrum support' proposes to help you via a private channel, this person is most likely an imposter trying to steal your bitcoins.\"\"\"\n)\nTERMS_OF_USE_LATEST_VERSION : int = 1  # bump this if we want users re-prompted due to changes\n\n\nMSG_CONNECTMODE_AUTOCONNECT = _('Auto-connect')\nMSG_CONNECTMODE_MANUAL = _('Manual server selection')\nMSG_CONNECTMODE_ONESERVER = _('Connect only to a single server')\n\nMSG_CONNECTMODE_SERVER_HELP = _(\n    \"Electrum connects to a unique server in order to receive your transaction history. \"\n    \"This server will learn your wallet addresses.\"\n)\nMSG_CONNECTMODE_NODES_HELP = _(\n    \"In addition to your history server, Electrum will try to maintain connections with ~10 extra servers, in order to download block headers and find out the longest blockchain. \"\n    \"These servers are only used for block header notifications and fee estimates; they do not learn your wallet addresses. \"\n    \"Getting block headers from multiple sources is useful to detect lagging servers and forks. \"\n    \"Fork detection is security-critical for determining number of confirmations.\"\n)\n\nMSG_CONNECTMODE_AUTOCONNECT_HELP = _(\n    \"Electrum will always use a history server that is on the longest blockchain. \"\n    \"If your current server is unresponsive or lagging, Electrum will switch to another server.\"\n)\n\nMSG_CONNECTMODE_MANUAL_HELP = _(\n    \"Electrum will stay with the server you selected. It will warn you if your server is lagging.\"\n)\n\nMSG_CONNECTMODE_ONESERVER_HELP = _(\n    \"Electrum will stay with the server you selected, and it will not connect to additional nodes. \"\n    \"This will disable fork detection. \"\n    \"This mode is only intended for connecting to your own fully trusted server. \"\n    \"Using this option on a public server is a security risk and is discouraged.\"\n)\n\nMSG_SUBMARINE_PAYMENT_HELP_TEXT = ''.join((\n    _(\"Submarine Payments use a reverse submarine swap to do on-chain transactions directly \"\n      \"from your lightning balance.\"), '\\n\\n',\n    _(\"Submarine Payments happen in two stages. In the first stage, your wallet sends a lightning \"\n      \"payment to the submarine swap provider. The swap provider will lock funds to a \"\n      \"funding output in an on-chain transaction (the funding transaction).\"), '\\n',\n    _(\"Once the funding transaction has one confirmation, your wallet will broadcast a claim \"\n      \"transaction as the second stage of the payment. This claim transaction spends the funding \"\n      \"output to the payee's address.\"), '\\n\\n',\n    _(\"Warning:\"), '\\n',\n    _('The funding transaction is not visible to the payee. They will only see a pending '\n      'transaction in the mempool after your wallet broadcasts the claim transaction. '\n      'Since confirmation of the funding transaction can take over 30 minutes, avoid using '\n      'Submarine Payments when the payee expects to see the transaction within a limited '\n      'time frame (e.g., an online shop checkout). Use a regular on-chain payment instead.'),\n))\n\nMSG_RELAYFEE = ' '.join([\n    _(\"This transaction requires a higher fee, or it will not be propagated by your current server.\"),\n    _(\"Try to raise your transaction fee, or use a server with a lower relay fee.\")\n])\n"
  },
  {
    "path": "electrum/gui/qml/__init__.py",
    "content": "import os\nimport signal\nimport sys\nimport threading\nfrom typing import TYPE_CHECKING\n\ntry:\n    import PyQt6\nexcept Exception as e:\n    from electrum import GuiImportError\n    raise GuiImportError(\n        \"Error: Could not import PyQt6. On Linux systems, \"\n        \"you may try 'sudo apt-get install python3-pyqt6'\") from e\n\ntry:\n    import PyQt6.QtQml\nexcept Exception as e:\n    from electrum import GuiImportError\n    raise GuiImportError(\n        \"Error: Could not import PyQt6.QtQml. On Linux systems, \"\n        \"you may try 'sudo apt-get install python3-pyqt6.qtquick'\") from e\n\nfrom PyQt6.QtCore import (Qt, QCoreApplication, QLocale, QTimer, QT_VERSION_STR, PYQT_VERSION_STR)\nfrom PyQt6.QtGui import QGuiApplication\n\nfrom electrum.plugin import run_hook\nfrom electrum.util import profiler\nfrom electrum.logging import Logger\nfrom electrum.gui import BaseElectrumGui\nfrom electrum.gui.common_qt.i18n import ElectrumTranslator\n\n\nif TYPE_CHECKING:\n    from electrum.daemon import Daemon\n    from electrum.simple_config import SimpleConfig\n    from electrum.plugin import Plugins\n\nfrom .qeapp import ElectrumQmlApplication, Exception_Hook\n\n\nclass ElectrumGui(BaseElectrumGui, Logger):\n    @profiler\n    def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'):\n        BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins)\n        Logger.__init__(self)\n\n        # uncomment to debug plugin and import tracing\n        # os.environ['QML_IMPORT_TRACE'] = '1'\n        # os.environ['QT_DEBUG_PLUGINS'] = '1'\n\n        os.environ['QT_ANDROID_DISABLE_ACCESSIBILITY'] = '1'\n\n        # set default locale to en_GB. This is for l10n (e.g. number formatting, number input etc),\n        # but not for i18n, which is handled by the Translator\n        # this can be removed once the backend wallet is fully l10n aware\n        QLocale.setDefault(QLocale('en_GB'))\n\n        self.logger.info(f\"Qml GUI starting up... Qt={QT_VERSION_STR}, PyQt={PYQT_VERSION_STR}\")\n        self.logger.info(\"CWD=%s\" % os.getcwd())\n        # Uncomment this call to verify objects are being properly\n        # GC-ed when windows are closed\n        #plugins.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,\n        #                            ElectrumWindow], interval=5)])\n\n        if hasattr(Qt, \"AA_ShareOpenGLContexts\"):\n            QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts)\n        if hasattr(QGuiApplication, 'setDesktopFileName'):\n            QGuiApplication.setDesktopFileName('electrum')\n\n        if \"QT_QUICK_CONTROLS_STYLE\" not in os.environ:\n            os.environ[\"QT_QUICK_CONTROLS_STYLE\"] = \"Material\"\n\n        self.gui_thread = threading.current_thread()\n        self.app = ElectrumQmlApplication(sys.argv, config=config, daemon=daemon, plugins=plugins)\n        self.translator = ElectrumTranslator()\n        self.app.installTranslator(self.translator)\n\n        # timer\n        self.timer = QTimer(self.app)\n        self.timer.setSingleShot(False)\n        self.timer.setInterval(500)  # msec\n        self.timer.timeout.connect(lambda: None)  # periodically enter python scope\n\n        # hook for crash reporter\n        Exception_Hook.maybe_setup(slot=self.app.appController.crash)\n\n        # Initialize any QML plugins\n        run_hook('init_qml', self.app)\n        self.app.engine.load('electrum/gui/qml/components/main.qml')\n\n    def close(self):\n        self.app.quit()\n\n    def main(self):\n        if not self.app._valid:\n            return\n\n        self.timer.start()\n        signal.signal(signal.SIGINT, lambda *args: self._handle_sigint())\n\n        self.logger.info('Entering main loop')\n        self.app.exec()\n\n    def _handle_sigint(self):\n        self.app.appController.wantClose = True\n        self.stop()\n\n    def stop(self):\n        self.logger.info('closing GUI')\n        self.app.quit()\n"
  },
  {
    "path": "electrum/gui/qml/android_res/layout/scanner_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<FrameLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <FrameLayout\n        android:id=\"@+id/content_frame\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <TextView\n        android:id=\"@+id/hint\"\n        android:layout_gravity=\"center|top\"\n        android:gravity=\"center\"\n        android:text=\"Scan a QR code.\"\n        android:layout_width=\"wrap_content\"\n        android:textColor=\"#ffffff\"\n        android:shadowColor=\"#000000\"\n        android:shadowDx=\"1\"\n        android:shadowDy=\"1\"\n        android:shadowRadius=\"2\"\n        android:textSize=\"15sp\"\n        android:padding=\"14dp\"\n        android:layout_height=\"wrap_content\" />\n\n    <Button\n        android:id=\"@+id/paste_btn\"\n        android:layout_gravity=\"center|bottom\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"Paste from clipboard 📋\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "electrum/gui/qml/auth.py",
    "content": "from functools import wraps, partial\n\nfrom PyQt6.QtCore import pyqtSignal, pyqtSlot\n\nfrom electrum.logging import get_logger\n\n\ndef auth_protect(func=None, reject=None, method='payment_auth', message=''):\n    \"\"\"\n    Supported methods:\n        * payment_auth: If the user has enabled the 'Payment authentication' config\n                        they need to authenticate to continue. If biometrics are enabled they\n                        can authenticate using the Android system dialog, else they will see the\n                        wallet password dialog.\n                        If the option is disabled they will have to confirm a dialog.\n        * wallet: Same as payment_auth, but not dependent on user configuration,\n                  always requires authentication.\n        * wallet_password_only: No biometric/system authentication, user has to enter wallet password.\n    \"\"\"\n    if func is None:\n        return partial(auth_protect, reject=reject, method=method, message=message)\n\n    @wraps(func)\n    def wrapper(self, *args, **kwargs):\n        _logger = get_logger(__name__)\n        _logger.debug(f'{str(self)}.{func.__name__}')\n        if hasattr(self, '__auth_fcall'):\n            _logger.debug('object already has a pending authed function call')\n            raise Exception('object already has a pending authed function call')\n        setattr(self, '__auth_fcall', (func, args, kwargs, reject))\n        getattr(self, 'authRequired').emit(method, message)\n\n    return wrapper\n\n\nclass AuthMixin:\n    _auth_logger = get_logger(__name__)\n    authRequired = pyqtSignal([str, str], arguments=['method', 'authMessage'])\n\n    @pyqtSlot()\n    def authProceed(self):\n        self._auth_logger.debug('Proceeding with authed fn()')\n        try:\n            self._auth_logger.debug(str(getattr(self, '__auth_fcall')))\n            (func, args, kwargs, reject) = getattr(self, '__auth_fcall')\n            r = func(self, *args, **kwargs)\n            return r\n        except Exception as e:\n            self._auth_logger.error(f'Error executing wrapped fn(): {repr(e)}')\n            raise e\n        finally:\n            delattr(self, '__auth_fcall')\n\n    @pyqtSlot()\n    def authCancel(self):\n        self._auth_logger.debug('Cancelling authed fn()')\n        if not hasattr(self, '__auth_fcall'):\n            return\n\n        try:\n            (func, args, kwargs, reject) = getattr(self, '__auth_fcall')\n            if reject is not None:\n                if hasattr(self, reject):\n                    getattr(self, reject)()\n                else:\n                    self._auth_logger.error(f'Reject method \"{reject}\" not defined')\n        except Exception as e:\n            self._auth_logger.error(f'Error executing reject function \"{reject}\": {repr(e)}')\n            raise e\n        finally:\n            delattr(self, '__auth_fcall')\n"
  },
  {
    "path": "electrum/gui/qml/components/About.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nPane {\n    objectName: 'About'\n\n    property string title: qsTr(\"About Electrum\")\n\n    Flickable {\n        anchors.fill: parent\n        contentHeight: rootLayout.height\n        interactive: height < contentHeight\n\n        GridLayout {\n            id: rootLayout\n            columns: 2\n            width: parent.width\n\n            Item {\n                Layout.columnSpan: 2\n                Layout.alignment: Qt.AlignHCenter\n                Layout.preferredWidth: parent.width\n                Layout.preferredHeight: parent.width * 3/4 // reduce height, empty space in png\n\n                Image {\n                    id: electrum_logo\n                    width: parent.width\n                    height: width\n                    source: '../../icons/electrum_presplash.png'\n                }\n            }\n\n            Label {\n                text: qsTr('Version')\n                Layout.alignment: Qt.AlignRight\n            }\n            Label {\n                text: BUILD.electrum_version\n            }\n            Label {\n                text: qsTr('Protocol version')\n                Layout.alignment: Qt.AlignRight\n            }\n            Label {\n                text: BUILD.protocol_version\n            }\n            Label {\n                text: qsTr('Qt Version')\n                Layout.alignment: Qt.AlignRight\n            }\n            Label {\n                text: BUILD.qt_version\n            }\n            Label {\n                text: qsTr('PyQt Version')\n                Layout.alignment: Qt.AlignRight\n            }\n            Label {\n                text: BUILD.pyqt_version\n            }\n            Label {\n                text: qsTr('License')\n                Layout.alignment: Qt.AlignRight\n            }\n            Label {\n                text: qsTr('MIT License')\n            }\n            Label {\n                text: qsTr('Homepage')\n                Layout.alignment: Qt.AlignRight\n            }\n            Label {\n                text: '<a href=\"https://electrum.org\">https://electrum.org</a>'\n                textFormat: Text.RichText\n                onLinkActivated: Qt.openUrlExternally(link)\n            }\n            Label {\n                text: qsTr('Developers')\n                Layout.alignment: Qt.AlignRight\n            }\n            Label {\n                text: 'Thomas Voegtlin\\nSomberNight\\nSander van Grieken'\n            }\n            Item {\n                width: 1\n                height: constants.paddingXLarge\n                Layout.columnSpan: 2\n            }\n            Label {\n                text: qsTr('Distributed by Electrum Technologies GmbH')\n                Layout.columnSpan: 2\n                Layout.alignment: Qt.AlignHCenter\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/AddressDetails.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: root\n    width: parent.width\n    height: parent.height\n    padding: 0\n\n    property string address\n\n    signal addressDetailsChanged\n    signal addressDeleted\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            leftMargin: constants.paddingLarge\n            rightMargin: constants.paddingLarge\n            topMargin: constants.paddingLarge\n\n            contentHeight: rootLayout.height\n            clip:true\n            interactive: height < contentHeight\n\n            GridLayout {\n                id: rootLayout\n                width: parent.width\n\n                columns: 2\n\n                Heading {\n                    Layout.columnSpan: 2\n                    text: qsTr('Address details')\n                }\n\n                RowLayout {\n                    Layout.columnSpan: 2\n                    Label {\n                        text: qsTr('Address')\n                        color: Material.accentColor\n                    }\n\n                    Tag {\n                        visible: addressdetails.isFrozen\n                        text: qsTr('Frozen')\n                        labelcolor: 'white'\n                    }\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n\n                    RowLayout {\n                        width: parent.width\n                        Label {\n                            text: root.address\n                            font.pixelSize: constants.fontSizeLarge\n                            font.family: FixedFont\n                            Layout.fillWidth: true\n                            wrapMode: Text.Wrap\n                        }\n                        ToolButton {\n                            icon.source: '../../icons/share.png'\n                            icon.color: 'transparent'\n                            onClicked: {\n                                var dialog = app.genericShareDialog.createObject(root,\n                                    { title: qsTr('Address'), text: root.address }\n                                )\n                                dialog.open()\n                            }\n                        }\n                    }\n                }\n\n                Label {\n                    text: qsTr('Balance')\n                    color: Material.accentColor\n                }\n\n                FormattedAmount {\n                    amount: addressdetails.balance\n                }\n\n                Label {\n                    text: qsTr('Transactions')\n                    color: Material.accentColor\n                }\n\n                Label {\n                    text: addressdetails.numTx\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    text: qsTr('Label')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    id: labelContent\n\n                    property bool editmode: false\n\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n\n                    RowLayout {\n                        width: parent.width\n                        Label {\n                            visible: !labelContent.editmode\n                            text: addressdetails.label\n                            wrapMode: Text.Wrap\n                            Layout.fillWidth: true\n                            font.pixelSize: constants.fontSizeLarge\n                        }\n                        ToolButton {\n                            visible: !labelContent.editmode\n                            icon.source: '../../icons/pen.png'\n                            icon.color: 'transparent'\n                            onClicked: {\n                                labelEdit.text = addressdetails.label\n                                labelContent.editmode = true\n                                labelEdit.focus = true\n                            }\n                        }\n                        TextField {\n                            id: labelEdit\n                            visible: labelContent.editmode\n                            text: addressdetails.label\n                            font.pixelSize: constants.fontSizeLarge\n                            Layout.fillWidth: true\n                        }\n                        ToolButton {\n                            visible: labelContent.editmode\n                            icon.source: '../../icons/confirmed.png'\n                            icon.color: 'transparent'\n                            onClicked: {\n                                labelContent.editmode = false\n                                addressdetails.setLabel(labelEdit.text)\n                            }\n                        }\n                        ToolButton {\n                            visible: labelContent.editmode\n                            icon.source: '../../icons/closebutton.png'\n                            icon.color: 'transparent'\n                            onClicked: labelContent.editmode = false\n                        }\n                    }\n                }\n\n                Heading {\n                    Layout.columnSpan: 2\n                    text: qsTr('Technical Properties')\n                }\n\n                Label {\n                    Layout.topMargin: constants.paddingSmall\n                    text: qsTr('Script type')\n                    color: Material.accentColor\n                }\n\n                Label {\n                    Layout.topMargin: constants.paddingSmall\n                    Layout.fillWidth: true\n                    text: addressdetails.scriptType\n                }\n\n                Label {\n                    visible: addressdetails.derivationPath\n                    text: qsTr('Derivation path')\n                    color: Material.accentColor\n                }\n\n                Label {\n                    visible: addressdetails.derivationPath\n                    text: addressdetails.derivationPath\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    visible: addressdetails.pubkeys.length\n                    text: addressdetails.pubkeys.length > 1\n                        ? qsTr('Public keys')\n                        : qsTr('Public key')\n                    color: Material.accentColor\n                }\n\n                Repeater {\n                    model: addressdetails.pubkeys\n                    delegate: TextHighlightPane {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        RowLayout {\n                            width: parent.width\n                            Label {\n                                text: modelData\n                                Layout.fillWidth: true\n                                wrapMode: Text.Wrap\n                                font.pixelSize: constants.fontSizeLarge\n                                font.family: FixedFont\n                            }\n                            ToolButton {\n                                icon.source: '../../icons/share.png'\n                                enabled: modelData\n                                onClicked: {\n                                    var dialog = app.genericShareDialog.createObject(root, {\n                                        title: qsTr('Public key'),\n                                        text: modelData\n                                    })\n                                    dialog.open()\n                                }\n                            }\n                        }\n                    }\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    visible: !Daemon.currentWallet.isWatchOnly\n                    text: qsTr('Private key')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    visible: !Daemon.currentWallet.isWatchOnly\n                    RowLayout {\n                        width: parent.width\n                        Label {\n                            id: privateKeyText\n                            Layout.fillWidth: true\n                            visible: addressdetails.privkey\n                            text: addressdetails.privkey\n                            wrapMode: Text.Wrap\n                            font.pixelSize: constants.fontSizeLarge\n                            font.family: FixedFont\n                        }\n                        Label {\n                            id: showPrivateKeyText\n                            Layout.fillWidth: true\n                            visible: !addressdetails.privkey\n                            horizontalAlignment: Text.AlignHCenter\n                            text: qsTr('Tap to show private key')\n                            wrapMode: Text.Wrap\n                            font.pixelSize: constants.fontSizeLarge\n                        }\n                        ToolButton {\n                            icon.source: '../../icons/share.png'\n                            visible: addressdetails.privkey\n                            onClicked: {\n                                var dialog = app.genericShareDialog.createObject(root, {\n                                    title: qsTr('Private key'),\n                                    text: addressdetails.privkey\n                                })\n                                dialog.open()\n                            }\n                        }\n\n                        MouseArea {\n                            anchors.fill: parent\n                            enabled: !addressdetails.privkey\n                            onClicked: addressdetails.requestShowPrivateKey()\n                        }\n                    }\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: addressdetails.isFrozen ? qsTr('Unfreeze address') : qsTr('Freeze address')\n                onClicked: addressdetails.freeze(!addressdetails.isFrozen)\n                icon.source: '../../icons/freeze.png'\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                visible: Daemon.currentWallet.canSignMessage\n                text: qsTr('Sign/Verify')\n                icon.source: '../../icons/pen.png'\n                onClicked: {\n                    var dialog = app.signVerifyMessageDialog.createObject(app, {\n                        address: root.address\n                    })\n                    dialog.open()\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                visible: addressdetails.canDelete\n                text: qsTr('Delete')\n                onClicked: {\n                    var confirmdialog = app.messageDialog.createObject(root, {\n                        text: qsTr('Are you sure you want to delete this address from the wallet?'),\n                        yesno: true\n                    })\n                    confirmdialog.accepted.connect(function () {\n                        var success = addressdetails.deleteAddress()\n                        if (success) {\n                            addressDeleted()\n                            app.stack.pop()\n                        }\n                    })\n                    confirmdialog.open()\n                }\n                icon.source: '../../icons/delete.png'\n            }\n        }\n    }\n\n    AddressDetails {\n        id: addressdetails\n        wallet: Daemon.currentWallet\n        address: root.address\n        onFrozenChanged: addressDetailsChanged()\n        onLabelChanged: addressDetailsChanged()\n        onAuthRequired: (method, authMessage) => {\n            app.handleAuthRequired(addressdetails, method, authMessage)\n        }\n        onAddressDeleteFailed: (message) => {\n            var dialog = app.messageDialog.createObject(root, {\n                text: message\n            })\n            dialog.open()\n        }\n    }\n\n    Binding {\n        target: AppController\n        property: 'secureWindow'\n        value: Boolean(addressdetails.privkey)\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/Addresses.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\nimport QtQml.Models\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: rootItem\n    objectName: 'Addresses'\n\n    padding: 0\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        ColumnLayout {\n            id: layout\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            Pane {\n                id: filtersPane\n                Layout.fillWidth: true\n                GridLayout {\n                    columns: 3\n                    width: parent.width\n\n                    CheckBox {\n                        id: showUsed\n                        text: qsTr('Show Used')\n                        enabled: listview.filterModel.showAddressesCoins != 2\n                        onCheckedChanged: {\n                            listview.filterModel.showUsed = checked\n                            if (activeFocus) {\n                                Config.addresslistShowUsed = checked\n                            }\n                        }\n                        Component.onCompleted: {\n                            checked = Config.addresslistShowUsed\n                            listview.filterModel.showUsed = checked\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        Layout.alignment: Qt.AlignRight\n                        Label {\n                            text: qsTr('Show')\n                        }\n                        ElComboBox {\n                            id: showCoinsAddresses\n                            textRole: 'text'\n                            valueRole: 'value'\n                            model: ListModel {\n                                id: showCoinsAddressesModel\n                                Component.onCompleted: {\n                                    // we need to fill the model like this, as ListElement can't evaluate script\n                                    showCoinsAddressesModel.append({'text': qsTr('Addresses'), 'value': 1})\n                                    showCoinsAddressesModel.append({'text': qsTr('Coins'), 'value': 2})\n                                    showCoinsAddressesModel.append({'text': qsTr('Both'), 'value': 3})\n                                    listview.filterModel.showAddressesCoins = Config.addresslistShowType\n                                    for (let i=0; i < showCoinsAddressesModel.count; i++) {\n                                        if (showCoinsAddressesModel.get(i).value == listview.filterModel.showAddressesCoins) {\n                                            showCoinsAddresses.currentIndex = i\n                                            break\n                                        }\n                                    }\n                                }\n                            }\n                            onCurrentValueChanged: {\n                                if (activeFocus && currentValue) {\n                                    listview.filterModel.showAddressesCoins = currentValue\n                                    Config.addresslistShowType = currentValue\n                                }\n                            }\n                        }\n                    }\n                    TextField {\n                        id: searchEdit\n                        Layout.fillWidth: true\n                        Layout.columnSpan: 3\n\n                        placeholderText: qsTr('search')\n                        inputMethodHints: Qt.ImhNoPredictiveText\n\n                        onTextChanged: listview.filterModel.filterText = text\n\n                        Image {\n                            anchors.right: parent.right\n                            anchors.verticalCenter: parent.verticalCenter\n                            source: Qt.resolvedUrl('../../icons/zoom.png')\n                            sourceSize.width: constants.iconSizeMedium\n                            sourceSize.height: constants.iconSizeMedium\n                        }\n                    }\n                }\n            }\n\n            Frame {\n                id: channelsFrame\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n\n                verticalPadding: 0\n                horizontalPadding: 0\n                background: PaneInsetBackground {}\n\n                ElListView {\n                    id: listview\n\n                    anchors.fill: parent\n                    clip: true\n\n                    property QtObject backingModel: Daemon.currentWallet.addressCoinModel\n                    property QtObject filterModel: Daemon.currentWallet.addressCoinModel.filterModel\n                    property bool selectMode: false\n                    property bool freeze: true\n                    model: visualModel\n                    currentIndex: -1\n\n                    section.property: 'type'\n                    section.criteria: ViewSection.FullString\n                    section.delegate: sectionDelegate\n\n                    function getSelectedItems() {\n                        var items = []\n                        for (let i = 0; i < selectedGroup.count; i++) {\n                            let modelitem = selectedGroup.get(i).model\n                            if (modelitem.outpoint)\n                                items.push(modelitem.outpoint)\n                            else\n                                items.push(modelitem.address)\n                        }\n                        return items\n                    }\n\n                    DelegateModel {\n                        id: visualModel\n                        model: listview.filterModel\n                        groups: [\n                            DelegateModelGroup {\n                                id: selectedGroup;\n                                name: 'selected'\n                                onCountChanged: {\n                                    if (count == 0)\n                                        listview.selectMode = false\n                                }\n                            }\n                        ]\n\n                        delegate: Loader {\n                            id: loader\n                            width: parent.width\n\n                            sourceComponent: model.outpoint ? _coinDelegate : _addressDelegate\n\n                            function toggle() {\n                                loader.DelegateModel.inSelected = !loader.DelegateModel.inSelected\n                            }\n\n                            Component {\n                                id: _addressDelegate\n                                AddressDelegate {\n                                    id: addressDelegate\n                                    width: parent.width\n                                    property bool selected: loader.DelegateModel.inSelected\n                                    highlighted: selected\n                                    onClicked: {\n                                        if (!listview.selectMode) {\n                                            var page = app.stack.push(Qt.resolvedUrl('AddressDetails.qml'), {\n                                                address: model.address\n                                            })\n                                            page.addressDetailsChanged.connect(function() {\n                                                // update listmodel when details change\n                                                listview.backingModel.updateAddress(model.address)\n                                            })\n                                            page.addressDeleted.connect(function() {\n                                                // update listmodel when address removed\n                                                listview.backingModel.deleteAddress(model.address)\n                                            })\n                                        } else {\n                                            loader.toggle()\n                                        }\n                                    }\n                                    onPressAndHold: {\n                                        loader.toggle()\n                                        if (!listview.selectMode && selectedGroup.count > 0)\n                                            listview.selectMode = true\n                                    }\n                                }\n                            }\n                            Component {\n                                id: _coinDelegate\n                                Pane {\n                                    height: coinDelegate.height\n                                    padding: 0\n                                    background: Rectangle {\n                                        color: Qt.darker(constants.darkerBackground, 1.10)\n                                    }\n\n                                    CoinDelegate {\n                                        id: coinDelegate\n                                        width: parent.width\n                                        property bool selected: loader.DelegateModel.inSelected\n                                        highlighted: selected\n                                        indent: listview.filterModel.showAddressesCoins == 2 ? 0 : constants.paddingLarge * 2\n                                        onClicked: {\n                                            if (!listview.selectMode) {\n                                                var page = app.stack.push(Qt.resolvedUrl('TxDetails.qml'), {\n                                                    txid: model.txid\n                                                })\n                                            } else {\n                                                loader.toggle()\n                                            }\n                                        }\n                                        onPressAndHold: {\n                                            loader.toggle()\n                                            if (!listview.selectMode && selectedGroup.count > 0)\n                                                listview.selectMode = true\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                    }\n                    add: Transition {\n                        NumberAnimation { properties: \"opacity\"; from: 0.0; to: 1.0; duration: 300\n                            easing.type: Easing.OutQuad\n                        }\n                    }\n\n                    onSelectModeChanged: {\n                        if (selectMode) {\n                            listview.freeze = !selectedGroup.get(0).model.held\n                        }\n                    }\n\n                    ScrollIndicator.vertical: ScrollIndicator { }\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: listview.freeze ? qsTr('Freeze') : qsTr('Unfreeze')\n                icon.source: '../../icons/freeze.png'\n                visible: listview.selectMode\n                onClicked: {\n                    var items = listview.getSelectedItems()\n                    listview.backingModel.setFrozenForItems(listview.freeze, items)\n                    selectedGroup.remove(0, selectedGroup.count)\n                }\n            }\n            // FlatButton {\n            //     Layout.fillWidth: true\n            //     Layout.preferredWidth: 1\n            //     text: qsTr('Pay from...')\n            //     icon.source: '../../icons/tab_send.png'\n            //     visible: listview.selectMode\n            //     enabled: false // TODO\n            //     onClicked: {\n            //         //\n            //     }\n            // }\n        }\n\n    }\n\n    Component {\n        id: sectionDelegate\n        Item {\n            id: root\n            width: ListView.view.width\n            height: childrenRect.height\n            required property string section\n            property string section_label: section == 'receive'\n                ? qsTr('receive addresses')\n                : section == 'change'\n                    ? qsTr('change addresses')\n                    : section == 'imported'\n                        ? qsTr('imported addresses')\n                        : section + ' ' + qsTr('addresses')\n\n            ColumnLayout {\n                width: parent.width\n                Heading {\n                    Layout.leftMargin: constants.paddingLarge\n                    Layout.rightMargin: constants.paddingLarge\n                    text: root.section_label\n                }\n            }\n        }\n    }\n\n    Component.onCompleted: {\n        Daemon.currentWallet.addressCoinModel.initModel()\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/BIP39RecoveryDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n    title: qsTr(\"Detect BIP39 accounts\")\n\n    property string seed\n    property string seedExtraWords\n    property string walletType\n\n    property string derivationPath\n    property string scriptType\n\n    needsSystemBarPadding: false\n\n    z: 1 // raise z so it also covers wizard dialog\n\n    anchors.centerIn: parent\n\n    padding: 0\n\n    width: parent.width * 4/5\n    height: parent.height * 4/5\n\n    ColumnLayout {\n        id: rootLayout\n        width: parent.width\n        height: parent.height\n\n        InfoTextArea {\n            Layout.fillWidth: true\n            Layout.margins: constants.paddingMedium\n\n            text: bip39RecoveryListModel.state == Bip39RecoveryListModel.Scanning\n                ? qsTr('Scanning for accounts...')\n                : bip39RecoveryListModel.state == Bip39RecoveryListModel.Success\n                    ? listview.count > 0\n                        ? qsTr('Choose an account to restore.')\n                        : qsTr('No existing accounts found.')\n                    : bip39RecoveryListModel.state == Bip39RecoveryListModel.Failed\n                        ? qsTr('Recovery failed')\n                        : qsTr('Recovery cancelled')\n            iconStyle: bip39RecoveryListModel.state == Bip39RecoveryListModel.Scanning\n                ? InfoTextArea.IconStyle.Spinner\n                : bip39RecoveryListModel.state == Bip39RecoveryListModel.Success\n                    ? InfoTextArea.IconStyle.Info\n                    : InfoTextArea.IconStyle.Error\n        }\n\n        Frame {\n            id: accountsFrame\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            Layout.topMargin: constants.paddingLarge\n            Layout.bottomMargin: constants.paddingLarge\n            Layout.leftMargin: constants.paddingMedium\n            Layout.rightMargin: constants.paddingMedium\n\n            verticalPadding: 0\n            horizontalPadding: 0\n            background: PaneInsetBackground {}\n\n            ColumnLayout {\n                spacing: 0\n                anchors.fill: parent\n\n                ListView {\n                    id: listview\n                    Layout.preferredWidth: parent.width\n                    Layout.fillHeight: true\n                    clip: true\n                    model: bip39RecoveryListModel\n\n                    delegate: ItemDelegate {\n                        width: ListView.view.width\n                        height: itemLayout.height\n\n                        onClicked: {\n                            dialog.derivationPath = model.derivation_path\n                            dialog.scriptType = model.script_type\n                            dialog.doAccept()\n                        }\n\n                        GridLayout {\n                            id: itemLayout\n                            columns: 3\n                            rowSpacing: 0\n\n                            anchors {\n                                left: parent.left\n                                right: parent.right\n                                leftMargin: constants.paddingMedium\n                                rightMargin: constants.paddingMedium\n                            }\n\n                            Item {\n                                Layout.columnSpan: 3\n                                Layout.preferredHeight: constants.paddingLarge\n                                Layout.preferredWidth: 1\n                            }\n                            Image {\n                                Layout.rowSpan: 3\n                                source: Qt.resolvedUrl('../../icons/wallet.png')\n                            }\n                            Label {\n                                Layout.columnSpan: 2\n                                Layout.fillWidth: true\n                                text: model.description\n                                wrapMode: Text.Wrap\n                            }\n                            Label {\n                                text: qsTr('script type')\n                                color: Material.accentColor\n                            }\n                            Label {\n                                Layout.fillWidth: true\n                                text: model.script_type\n                            }\n                            Label {\n                                text: qsTr('derivation path')\n                                color: Material.accentColor\n                            }\n                            Label {\n                                Layout.fillWidth: true\n                                text: model.derivation_path\n                            }\n                            Item {\n                                Layout.columnSpan: 3\n                                Layout.preferredHeight: constants.paddingLarge\n                                Layout.preferredWidth: 1\n                            }\n                        }\n                    }\n\n                    ScrollIndicator.vertical: ScrollIndicator { }\n                }\n            }\n        }\n    }\n\n    Bip39RecoveryListModel {\n        id: bip39RecoveryListModel\n    }\n\n    Component.onCompleted: {\n        bip39RecoveryListModel.startScan(walletType, seed, seedExtraWords)\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/BalanceDetails.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: rootItem\n    objectName: 'BalanceDetails'\n\n    padding: 0\n\n    ColumnLayout {\n        id: rootLayout\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            contentHeight: flickableRoot.height\n            clip:true\n            interactive: height < contentHeight\n\n            Pane {\n                id: flickableRoot\n                width: parent.width\n                padding: constants.paddingLarge\n\n                ColumnLayout {\n                    width: parent.width\n                    spacing: constants.paddingLarge\n\n                    InfoTextArea {\n                        Layout.fillWidth: true\n                        Layout.bottomMargin: constants.paddingLarge\n                        visible: Daemon.currentWallet.synchronizing || !Network.isConnected\n                        text: Daemon.currentWallet.synchronizing\n                                  ? qsTr('Your wallet is not synchronized. The displayed balance may be inaccurate.')\n                                  : qsTr('Your wallet is not connected to an Electrum server. The displayed balance may be outdated.')\n                        iconStyle: InfoTextArea.IconStyle.Warn\n                    }\n\n                    Heading {\n                        text: qsTr('Wallet balance')\n                    }\n\n                    Piechart {\n                        id: piechart\n\n                        property real total: 0\n\n                        visible: total > 0\n                        Layout.preferredWidth: parent.width\n                        implicitHeight: 220 // TODO: sane value dependent on screen\n                        innerOffset: 6\n                        function updateSlices() {\n                            var p = Daemon.currentWallet.getBalancesForPiechart()\n                            total = p['total']\n                            piechart.slices = [\n                                { v: p['lightning']/total,\n                                    color: constants.colorPiechartLightning, text: qsTr('Lightning') },\n                                { v: p['confirmed']/total,\n                                    color: constants.colorPiechartOnchain, text: qsTr('On-chain') },\n                                { v: p['frozen']/total,\n                                    color: constants.colorPiechartFrozen, text: qsTr('On-chain (frozen)') },\n                                { v: p['unconfirmed']/total,\n                                    color: constants.colorPiechartUnconfirmed, text: qsTr('Unconfirmed') },\n                                { v: p['unmatured']/total,\n                                    color: constants.colorPiechartUnmatured, text: qsTr('Unmatured') },\n                                { v: p['f_lightning']/total,\n                                    color: constants.colorPiechartLightningFrozen, text: qsTr('Frozen Lightning') },\n                            ]\n                        }\n                    }\n\n                    GridLayout {\n                        Layout.alignment: Qt.AlignHCenter\n                        visible: Daemon.currentWallet\n                        columns: 2\n\n                        RowLayout {\n                            Rectangle {\n                                Layout.preferredWidth: constants.iconSizeXSmall\n                                Layout.preferredHeight: constants.iconSizeXSmall\n                                border.color: constants.colorPiechartTotal\n                                color: 'transparent'\n                                radius: constants.iconSizeXSmall/2\n                            }\n                            Label {\n                                text: qsTr('Total')\n                            }\n                        }\n                        FormattedAmount {\n                            amount: Daemon.currentWallet.totalBalance\n                        }\n\n                        RowLayout {\n                            visible: Daemon.currentWallet.isLightning\n                            Rectangle {\n                                Layout.preferredWidth: constants.iconSizeXSmall\n                                Layout.preferredHeight: constants.iconSizeXSmall\n                                color: constants.colorPiechartLightning\n                            }\n                            Label {\n                                text: qsTr('Lightning')\n                            }\n                        }\n                        FormattedAmount {\n                            visible: Daemon.currentWallet.isLightning\n                            amount: Daemon.currentWallet.lightningBalance\n                        }\n\n                        RowLayout {\n                            visible: Daemon.currentWallet.isLightning && !Daemon.currentWallet.lightningBalanceFrozen.isEmpty\n                            Rectangle {\n                                Layout.leftMargin: constants.paddingLarge\n                                Layout.preferredWidth: constants.iconSizeXSmall\n                                Layout.preferredHeight: constants.iconSizeXSmall\n                                color: constants.colorPiechartLightningFrozen\n                            }\n                            Label {\n                                text: qsTr('Frozen')\n                            }\n                        }\n                        FormattedAmount {\n                            visible: Daemon.currentWallet.isLightning && !Daemon.currentWallet.lightningBalanceFrozen.isEmpty\n                            amount: Daemon.currentWallet.lightningBalanceFrozen\n                        }\n\n                        RowLayout {\n                            visible: Daemon.currentWallet.isLightning || !Daemon.currentWallet.frozenBalance.isEmpty\n                            Rectangle {\n                                Layout.preferredWidth: constants.iconSizeXSmall\n                                Layout.preferredHeight: constants.iconSizeXSmall\n                                color: constants.colorPiechartOnchain\n                            }\n                            Label {\n                                text: qsTr('On-chain')\n                            }\n                        }\n                        FormattedAmount {\n                            visible: Daemon.currentWallet.isLightning || !Daemon.currentWallet.frozenBalance.isEmpty\n                            amount: Daemon.currentWallet.confirmedBalance\n                        }\n\n                        RowLayout {\n                            visible: !Daemon.currentWallet.frozenBalance.isEmpty\n                            Rectangle {\n                                Layout.leftMargin: constants.paddingLarge\n                                Layout.preferredWidth: constants.iconSizeXSmall\n                                Layout.preferredHeight: constants.iconSizeXSmall\n                                color: constants.colorPiechartFrozen\n                            }\n                            Label {\n                                text: qsTr('Frozen')\n                            }\n                        }\n                        FormattedAmount {\n                            amount: Daemon.currentWallet.frozenBalance\n                            visible: !Daemon.currentWallet.frozenBalance.isEmpty\n                        }\n\n                        RowLayout {\n                            visible: !Daemon.currentWallet.unconfirmedBalance.isEmpty\n                            Rectangle {\n                                Layout.preferredWidth: constants.iconSizeXSmall\n                                Layout.preferredHeight: constants.iconSizeXSmall\n                                color: constants.colorPiechartUnconfirmed\n                            }\n                            Label {\n                                text: qsTr('Unconfirmed')\n                            }\n                        }\n                        FormattedAmount {\n                            amount: Daemon.currentWallet.unconfirmedBalance\n                            visible: !Daemon.currentWallet.unconfirmedBalance.isEmpty\n                        }\n                    }\n\n                    Heading {\n                        text: qsTr('Lightning Liquidity')\n                        visible: Daemon.currentWallet.isLightning\n                    }\n                    GridLayout {\n                        Layout.alignment: Qt.AlignHCenter\n                        visible: Daemon.currentWallet && Daemon.currentWallet.isLightning\n                        columns: 2\n                        Label {\n                            text: qsTr('Can send')\n                        }\n                        FormattedAmount {\n                            amount: Daemon.currentWallet.lightningCanSend\n                        }\n                        Label {\n                            text: qsTr('Can receive')\n                        }\n                        FormattedAmount {\n                            amount: Daemon.currentWallet.lightningCanReceive\n                        }\n                    }\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Lightning swap');\n                visible: Daemon.currentWallet.isLightning\n                enabled: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0\n                icon.source: Qt.resolvedUrl('../../icons/update.png')\n                onClicked: app.startSwap()\n            }\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Open Channel')\n                visible: Daemon.currentWallet.isLightning\n                enabled: Daemon.currentWallet.confirmedBalance.satsInt > 0\n                onClicked: {\n                    var dialog = openChannelDialog.createObject(rootItem)\n                    dialog.open()\n                }\n                icon.source: '../../icons/lightning.png'\n            }\n\n        }\n\n    }\n\n    Component {\n        id: openChannelDialog\n        OpenChannelDialog {\n            onClosed: destroy()\n        }\n    }\n\n    Connections {\n        target: Daemon.currentWallet\n        function onBalanceChanged() {\n            piechart.updateSlices()\n        }\n    }\n\n    Component.onCompleted: {\n        piechart.updateSlices()\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ChannelDetails.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: root\n    width: parent.width\n    height: parent.height\n    padding: 0\n\n    property string channelid\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.preferredWidth: parent.width\n            Layout.fillHeight: true\n\n            leftMargin: constants.paddingLarge\n            rightMargin: constants.paddingLarge\n            topMargin: constants.paddingLarge\n\n            contentHeight: rootLayout.height\n            clip:true\n            interactive: height < contentHeight\n\n            ColumnLayout {\n                id: rootLayout\n                width: parent.width\n\n                Heading {\n                    text: !channeldetails.isBackup ? qsTr('Lightning Channel') : qsTr('Channel Backup')\n                }\n\n                GridLayout {\n                    Layout.fillWidth: true\n                    columns: 2\n\n                    Label {\n                        visible: channeldetails.name\n                        text: qsTr('Node name')\n                        color: Material.accentColor\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        visible: channeldetails.name\n                        text: channeldetails.name\n                    }\n\n                    Label {\n                        text: qsTr('State')\n                        color: Material.accentColor\n                    }\n\n                    Label {\n                        text: channeldetails.state\n                        color: channeldetails.state == 'OPEN'\n                                ? constants.colorChannelOpen\n                                : Material.foreground\n                    }\n\n                    Label {\n                        Layout.columnSpan: 2\n                        Layout.topMargin: constants.paddingSmall\n                        text: qsTr('Capacity and ratio')\n                        color: Material.accentColor\n                    }\n\n                    TextHighlightPane {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        padding: constants.paddingLarge\n\n                        GridLayout {\n                            width: parent.width\n                            columns: 2\n                            rowSpacing: constants.paddingSmall\n\n                            ChannelBar {\n                                Layout.columnSpan: 2\n                                Layout.fillWidth: true\n                                Layout.topMargin: constants.paddingLarge\n                                Layout.bottomMargin: constants.paddingXLarge\n                                visible: channeldetails.stateCode != ChannelDetails.Redeemed\n                                    && channeldetails.stateCode != ChannelDetails.Closed\n                                    && !channeldetails.isBackup\n                                capacity: channeldetails.capacity\n                                localCapacity: channeldetails.localCapacity\n                                remoteCapacity: channeldetails.remoteCapacity\n                                canSend: channeldetails.canSend\n                                canReceive: channeldetails.canReceive\n                                frozenForSending: channeldetails.frozenForSending\n                                frozenForReceiving: channeldetails.frozenForReceiving\n                            }\n\n                            Label {\n                                text: qsTr('Capacity')\n                                color: Material.accentColor\n                            }\n\n                            FormattedAmount {\n                                amount: channeldetails.capacity\n                            }\n\n                            Label {\n                                text: qsTr('Local balance')\n                                color: Material.accentColor\n                            }\n\n                            FormattedAmount {\n                                visible: channeldetails.isOpen\n                                amount: channeldetails.localCapacity\n                            }\n\n                            Label {\n                                visible: !channeldetails.isOpen\n                                text: qsTr('n/a (channel not open)')\n                            }\n\n                            Label {\n                                text: qsTr('Can send')\n                                color: Material.accentColor\n                            }\n\n                            RowLayout {\n                                visible: channeldetails.isOpen\n                                FormattedAmount {\n                                    visible: !channeldetails.frozenForSending\n                                    amount: channeldetails.canSend\n                                    singleLine: false\n                                }\n                                Label {\n                                    visible: channeldetails.frozenForSending\n                                    text: qsTr('n/a (frozen)')\n                                }\n                                Item {\n                                    Layout.fillWidth: true\n                                    Layout.preferredHeight: 1\n                                }\n                                Pane {\n                                    background: Rectangle { color: Material.dialogColor }\n                                    padding: 0\n                                    FlatButton {\n                                        Layout.minimumWidth: implicitWidth\n                                        text: channeldetails.frozenForSending ? qsTr('Unfreeze') : qsTr('Freeze')\n                                        onClicked: channeldetails.freezeForSending()\n                                    }\n                                }\n                            }\n\n                            Label {\n                                visible: !channeldetails.isOpen\n                                text: qsTr('n/a (channel not open)')\n                            }\n\n                            Label {\n                                text: qsTr('Can receive')\n                                color: Material.accentColor\n                            }\n\n                            RowLayout {\n                                visible: channeldetails.isOpen\n                                FormattedAmount {\n                                    visible: !channeldetails.frozenForReceiving\n                                    amount: channeldetails.canReceive\n                                    singleLine: false\n                                }\n\n                                Label {\n                                    visible: channeldetails.frozenForReceiving\n                                    text: qsTr('n/a (frozen)')\n                                }\n                                Item {\n                                    Layout.fillWidth: true\n                                    Layout.preferredHeight: 1\n                                }\n                                Pane {\n                                    background: Rectangle { color: Material.dialogColor }\n                                    padding: 0\n                                    FlatButton {\n                                        Layout.minimumWidth: implicitWidth\n                                        text: channeldetails.frozenForReceiving ? qsTr('Unfreeze') : qsTr('Freeze')\n                                        onClicked: channeldetails.freezeForReceiving()\n                                    }\n                                }\n                            }\n\n                            Label {\n                                visible: !channeldetails.isOpen\n                                text: qsTr('n/a (channel not open)')\n                            }\n                        }\n                    }\n\n                    Heading {\n                        Layout.columnSpan: 2\n                        text: qsTr('Technical properties')\n                    }\n\n                    Label {\n                        text: qsTr('Short channel ID')\n                        color: Material.accentColor\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        text: channeldetails.shortCid\n                    }\n\n                    Label {\n                        text: qsTr('Local SCID alias')\n                        color: Material.accentColor\n                        visible: channeldetails.localScidAlias\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        text: channeldetails.localScidAlias\n                        visible: channeldetails.localScidAlias\n                    }\n\n                    Label {\n                        text: qsTr('Remote SCID alias')\n                        color: Material.accentColor\n                        visible: channeldetails.remoteScidAlias\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        text: channeldetails.remoteScidAlias\n                        visible: channeldetails.remoteScidAlias\n                    }\n\n                    Label {\n                        visible: !channeldetails.isBackup\n                        text: qsTr('Initiator')\n                        color: Material.accentColor\n                    }\n\n                    Label {\n                        visible: !channeldetails.isBackup\n                        text: channeldetails.initiator\n                    }\n\n                    Label {\n                        text: qsTr('Channel type')\n                        color: Material.accentColor\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        text: channeldetails.channelType\n                        wrapMode: Text.Wrap\n                    }\n\n                    Label {\n                        text: qsTr('Current feerate')\n                        color: Material.accentColor\n                        visible: channeldetails.currentFeerate\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        text: channeldetails.currentFeerate\n                        visible: channeldetails.currentFeerate\n                    }\n\n                    Label {\n                        visible: channeldetails.isBackup\n                        text: qsTr('Backup type')\n                        color: Material.accentColor\n                    }\n\n                    Label {\n                        visible: channeldetails.isBackup\n                        text: channeldetails.backupType == 'imported'\n                                  ? qsTr('imported')\n                                  : channeldetails.backupType == 'on-chain'\n                                      ? qsTr('on-chain')\n                                      : '?'\n                    }\n\n                    Label {\n                        Layout.columnSpan: 2\n                        Layout.topMargin: constants.paddingSmall\n                        text: qsTr('Remote node ID')\n                        color: Material.accentColor\n                    }\n\n                    TextHighlightPane {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        RowLayout {\n                            width: parent.width\n                            Label {\n                                text: channeldetails.pubkey\n                                font.pixelSize: constants.fontSizeLarge\n                                font.family: FixedFont\n                                Layout.fillWidth: true\n                                wrapMode: Text.Wrap\n                            }\n                            ToolButton {\n                                icon.source: '../../icons/share.png'\n                                icon.color: 'transparent'\n                                onClicked: {\n                                    var dialog = app.genericShareDialog.createObject(root, {\n                                        title: qsTr('Channel node ID'),\n                                        text: channeldetails.pubkey\n                                    })\n                                    dialog.open()\n                                }\n                            }\n                        }\n                    }\n\n                    Label {\n                        Layout.columnSpan: 2\n                        Layout.topMargin: constants.paddingSmall\n                        text: qsTr('Funding Outpoint')\n                        color: Material.accentColor\n                    }\n\n                    TextHighlightPane {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        RowLayout {\n                            width: parent.width\n                            Label {\n                                text: channeldetails.fundingOutpoint.txid + ':' +  channeldetails.fundingOutpoint.index\n                                font.pixelSize: constants.fontSizeLarge\n                                font.family: FixedFont\n                                Layout.fillWidth: true\n                                wrapMode: Text.Wrap\n\n                                TapHandler {\n                                    onTapped: {\n                                        app.stack.push(Qt.resolvedUrl('TxDetails.qml'), {\n                                            txid: channeldetails.fundingOutpoint.txid\n                                        })\n                                    }\n                                }\n                            }\n                            ToolButton {\n                                icon.source: '../../icons/share.png'\n                                icon.color: 'transparent'\n                                onClicked: {\n                                    var dialog = app.genericShareDialog.createObject(root, {\n                                        title: qsTr('Funding Outpoint'),\n                                        text: channeldetails.fundingOutpoint.txid + ':' + channeldetails.fundingOutpoint.index\n                                    })\n                                    dialog.open()\n                                }\n                            }\n                        }\n                    }\n\n                    Label {\n                        Layout.columnSpan: 2\n                        Layout.topMargin: constants.paddingSmall\n                        visible: channeldetails.closingTxid\n                        text: qsTr('Closing transaction')\n                        color: Material.accentColor\n                    }\n\n                    TextHighlightPane {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        visible: channeldetails.closingTxid\n\n                        RowLayout {\n                            width: parent.width\n                            Label {\n                                text: channeldetails.closingTxid\n                                font.pixelSize: constants.fontSizeLarge\n                                font.family: FixedFont\n                                Layout.fillWidth: true\n                                wrapMode: Text.Wrap\n\n                                TapHandler {\n                                    onTapped: {\n                                        app.stack.push(Qt.resolvedUrl('TxDetails.qml'), {\n                                            txid: channeldetails.closingTxid\n                                        })\n                                    }\n                                }\n                            }\n                            ToolButton {\n                                icon.source: '../../icons/share.png'\n                                icon.color: 'transparent'\n                                onClicked: {\n                                    var dialog = app.genericShareDialog.createObject(root, {\n                                        title: qsTr('Channel close transaction'),\n                                        text: channeldetails.closingTxid\n                                    })\n                                    dialog.open()\n                                }\n                            }\n                        }\n                    }\n\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                visible: !channeldetails.isBackup\n                text: qsTr('Backup')\n                icon.source: '../../icons/file.png'\n                onClicked: {\n                    var dialog = app.genericShareDialog.createObject(root, {\n                        title: qsTr('Channel Backup for %1').arg(channeldetails.shortCid),\n                        text_qr: channeldetails.channelBackup(),\n                        text_help: channeldetails.channelBackupHelpText(),\n                        iconSource: Qt.resolvedUrl('../../icons/file.png')\n                    })\n                    dialog.open()\n                }\n            }\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Close channel');\n                visible: channeldetails.canClose\n                icon.source: '../../icons/closebutton.png'\n                onClicked: {\n                    var dialog = closechannel.createObject(root, { channelid: channelid })\n                    dialog.open()\n                }\n            }\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Delete channel');\n                visible: channeldetails.canDelete\n                icon.source: '../../icons/delete.png'\n                onClicked: {\n                    var dialog = app.messageDialog.createObject(root, {\n                        title: qsTr('Are you sure?'),\n                        text: channeldetails.isBackup ? '' : qsTr('This will purge associated transactions from your wallet history.'),\n                        yesno: true\n                    })\n                    dialog.accepted.connect(function() {\n                        channeldetails.deleteChannel()\n                        app.stack.pop()\n                        Daemon.currentWallet.historyModel.initModel(true) // needed here?\n                        Daemon.currentWallet.channelModel.removeChannel(channelid)\n                    })\n                    dialog.open()\n                }\n            }\n        }\n\n    }\n\n    ChannelDetails {\n        id: channeldetails\n        wallet: Daemon.currentWallet\n        channelid: root.channelid\n        onTrampolineFrozenInGossipMode: {\n            var dialog = app.messageDialog.createObject(root, {\n                title: qsTr('Cannot unfreeze channel'),\n                text: [qsTr('Non-Trampoline channels cannot be used for sending while in trampoline mode.'),\n                        qsTr('Disable trampoline mode to enable sending from this channel.')].join(' ')\n            })\n            dialog.open()\n        }\n    }\n\n    Component {\n        id: closechannel\n        CloseChannelDialog {}\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ChannelOpenProgressDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n    width: parent.width\n    height: parent.height\n\n    title: qsTr('Opening Channel...')\n\n    allowClose: false\n\n    property alias state: s.state\n    property alias error: errorText.text\n    property alias info: infoText.text\n    property alias peer: peerText.text\n\n    property string channelBackup\n\n    function reset() {\n        state = ''\n        errorText.text = ''\n        peerText.text = ''\n        channelBackup = ''\n    }\n\n    Item {\n        id: s\n        state: ''\n        states: [\n            State {\n                name: 'success'\n                PropertyChanges { target: dialog; allowClose: true }\n                PropertyChanges { target: stateText; text: qsTr('Success!') }\n                PropertyChanges { target: infoText; visible: true }\n                PropertyChanges { target: icon; source: '../../icons/confirmed.png' }\n            },\n            State {\n                name: 'failed'\n                PropertyChanges { target: dialog; allowClose: true }\n                PropertyChanges { target: stateText; text: qsTr('Problem opening channel') }\n                PropertyChanges { target: errorText; visible: true }\n                PropertyChanges { target: icon; source: '../../icons/warning.png' }\n            }\n        ]\n    }\n\n    ColumnLayout {\n        id: content\n        anchors.centerIn: parent\n        width: parent.width\n        spacing: constants.paddingLarge\n\n        RowLayout {\n                Layout.alignment: Qt.AlignHCenter\n            Image {\n                id: icon\n                source: ''\n                visible: source != ''\n                Layout.preferredWidth: constants.iconSizeLarge\n                Layout.preferredHeight: constants.iconSizeLarge\n            }\n            BusyIndicator {\n                id: spinner\n                running: visible\n                visible: s.state == ''\n                Layout.preferredWidth: constants.iconSizeLarge\n                Layout.preferredHeight: constants.iconSizeLarge\n            }\n\n            Label {\n                id: stateText\n                text: qsTr('Opening Channel...')\n                font.pixelSize: constants.fontSizeXXLarge\n            }\n        }\n\n        TextHighlightPane {\n            Layout.alignment: Qt.AlignHCenter\n            Layout.preferredWidth: dialog.width * 3/4\n            Label {\n                id: peerText\n                font.pixelSize: constants.fontSizeMedium\n                width: parent.width\n                wrapMode: Text.Wrap\n                horizontalAlignment: Text.AlignHCenter\n            }\n        }\n\n        InfoTextArea {\n            id: errorText\n            Layout.alignment: Qt.AlignHCenter\n            Layout.preferredWidth: dialog.width * 2/3\n            visible: false\n            iconStyle: InfoTextArea.IconStyle.Error\n            textFormat: TextEdit.PlainText\n        }\n\n        InfoTextArea {\n            id: infoText\n            Layout.alignment: Qt.AlignHCenter\n            Layout.preferredWidth: dialog.width * 2/3\n            visible: false\n            textFormat: TextEdit.PlainText\n        }\n    }\n\n    onClosed: {\n        if (!dialog.channelBackup)\n            return\n\n        var sharedialog = app.genericShareDialog.createObject(app, {\n            title: qsTr('Save Channel Backup'),\n            text_qr: dialog.channelBackup,\n            text_help: qsTr('The channel you created is not recoverable from seed.')\n            + ' ' + qsTr('To prevent fund losses, please save this backup on another device.')\n            + ' ' + qsTr('It may be imported in another Electrum wallet with the same seed.')\n        })\n        sharedialog.open()\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/Channels.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: root\n    objectName: 'Channels'\n\n    padding: 0\n\n    ColumnLayout {\n        id: layout\n        width: parent.width\n        height: parent.height\n        spacing: 0\n\n        GridLayout {\n            id: summaryLayout\n            Layout.preferredWidth: parent.width\n            Layout.topMargin: constants.paddingLarge\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n\n            columns: 2\n\n            Heading {\n                Layout.columnSpan: 2\n                text: qsTr('Lightning Channels')\n            }\n\n            Label {\n                Layout.columnSpan: 2\n                text: qsTr('You have %1 open channels').arg(Daemon.currentWallet.channelModel.numOpenChannels)\n                color: Material.accentColor\n            }\n\n            Label {\n                text: qsTr('You can send') + ':'\n                color: Material.accentColor\n            }\n\n            FormattedAmount {\n                amount: Daemon.currentWallet.lightningCanSend\n            }\n\n            Label {\n                text: qsTr('You can receive') + ':'\n                color: Material.accentColor\n            }\n\n            FormattedAmount {\n                amount: Daemon.currentWallet.lightningCanReceive\n            }\n        }\n\n        Frame {\n            id: channelsFrame\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            Layout.topMargin: constants.paddingLarge\n            Layout.bottomMargin: constants.paddingLarge\n            Layout.leftMargin: constants.paddingMedium\n            Layout.rightMargin: constants.paddingMedium\n\n            verticalPadding: 0\n            horizontalPadding: 0\n            background: PaneInsetBackground {}\n\n            ColumnLayout {\n                spacing: 0\n                anchors.fill: parent\n\n                ElListView {\n                    id: listview\n                    Layout.preferredWidth: parent.width\n                    Layout.fillHeight: true\n                    clip: true\n                    model: Daemon.currentWallet.channelModel\n\n                    section.property: 'is_backup'\n                    section.criteria: ViewSection.FullString\n                    section.delegate: RowLayout {\n                        width: ListView.view.width\n                        required property string section\n                        Label {\n                            visible: section == 'true'\n                            text: qsTr('Channel backups')\n                            Layout.alignment: Qt.AlignHCenter\n                            Layout.topMargin: constants.paddingLarge\n                            font.pixelSize: constants.fontSizeSmall\n                            color: Material.accentColor\n                        }\n                    }\n\n                    delegate: ChannelDelegate {\n                        onClicked: {\n                            app.stack.push(Qt.resolvedUrl('ChannelDetails.qml'), { 'channelid': model.cid })\n                        }\n                    }\n\n                    ScrollIndicator.vertical: ScrollIndicator { }\n\n                    Label {\n                        visible: listview.model.count == 0\n                        anchors.centerIn: parent\n                        width: listview.width * 4/5\n                        font.pixelSize: constants.fontSizeXXLarge\n                        color: constants.mutedForeground\n                        text: qsTr('No Lightning channels yet in this wallet')\n                        wrapMode: Text.Wrap\n                        horizontalAlignment: Text.AlignHCenter\n                    }\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Swap');\n                enabled: Daemon.currentWallet.lightningCanSend.satsInt > 0 ||\n                    (Daemon.currentWallet.lightningCanReceive.satsInt > 0 && Daemon.currentWallet.confirmedBalance.satsInt > 0)\n                icon.source: Qt.resolvedUrl('../../icons/update.png')\n                onClicked: app.startSwap()\n            }\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                enabled: Daemon.currentWallet.canHaveLightning && Daemon.currentWallet.confirmedBalance.satsInt > 0\n                text: qsTr('Open Channel')\n                onClicked: {\n                    if (Daemon.currentWallet.channelModel.count == 0) {\n                        var txt = Daemon.currentWallet.channelModel.lightningWarningMessage() + '\\n\\n' +\n                            qsTr('Do you want to create your first channel?')\n                        var confirmdialog = app.messageDialog.createObject(root, {\n                            text: txt,\n                            yesno: true\n                        })\n                        confirmdialog.accepted.connect(function () {\n                            var dialog = openChannelDialog.createObject(root)\n                            dialog.open()\n                        })\n                        confirmdialog.open()\n                    } else {\n                        var dialog = openChannelDialog.createObject(root)\n                        dialog.open()\n                    }\n                }\n                icon.source: '../../icons/lightning.png'\n            }\n        }\n\n    }\n\n    Component {\n        id: openChannelDialog\n        OpenChannelDialog {\n            onClosed: destroy()\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/CloseChannelDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    width: parent.width\n    height: parent.height\n\n    property string channelid\n\n    title: qsTr('Close Channel')\n    iconSource: Qt.resolvedUrl('../../icons/lightning_disconnected.png')\n\n    property string _closing_method\n\n    padding: 0\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.preferredWidth: parent.width\n            Layout.fillHeight: true\n\n            leftMargin: constants.paddingLarge\n            rightMargin: constants.paddingLarge\n\n            contentHeight: rootLayout.height\n            clip:true\n            interactive: height < contentHeight\n\n            GridLayout {\n                id: rootLayout\n                width: parent.width\n                columns: 2\n\n                Label {\n                    Layout.fillWidth: true\n                    visible: channeldetails.name\n                    text: qsTr('Channel name')\n                    color: Material.accentColor\n                }\n\n                Label {\n                    Layout.fillWidth: true\n                    visible: channeldetails.name\n                    text: channeldetails.name\n                }\n\n                Label {\n                    text: qsTr('Short channel ID')\n                    color: Material.accentColor\n                }\n\n                Label {\n                    text: channeldetails.shortCid\n                }\n\n                Label {\n                    text: qsTr('Remote node ID')\n                    Layout.columnSpan: 2\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n\n                    Label {\n                        width: parent.width\n                        text: channeldetails.pubkey\n                        font.pixelSize: constants.fontSizeLarge\n                        font.family: FixedFont\n                        Layout.fillWidth: true\n                        wrapMode: Text.Wrap\n                    }\n                }\n\n                Item { Layout.preferredHeight: constants.paddingMedium; Layout.preferredWidth: 1; Layout.columnSpan: 2 }\n\n                InfoTextArea {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    Layout.bottomMargin: constants.paddingLarge\n                    text: channeldetails.messageForceClose\n                }\n\n                Label {\n                    text: qsTr('Choose closing method')\n                    Layout.columnSpan: 2\n                    color: Material.accentColor\n                }\n\n                ColumnLayout {\n                    Layout.columnSpan: 2\n                    Layout.alignment: Qt.AlignHCenter\n\n                    ButtonGroup {\n                        id: closetypegroup\n                    }\n\n                    ElRadioButton {\n                        id: closetypeCoop\n                        ButtonGroup.group: closetypegroup\n                        property string closetype: 'cooperative'\n                        enabled: !channeldetails.isClosing && channeldetails.canCoopClose\n                        text: qsTr('Cooperative close')\n                    }\n                    ElRadioButton {\n                        id: closetypeRemoteForce\n                        ButtonGroup.group: closetypegroup\n                        property string closetype: 'remote_force'\n                        enabled: !channeldetails.isClosing && channeldetails.canRequestForceClose\n                        text: qsTr('Request Force-close')\n                    }\n                    ElRadioButton {\n                        id: closetypeLocalForce\n                        ButtonGroup.group: closetypegroup\n                        property string closetype: 'local_force'\n                        enabled: !channeldetails.isClosing && channeldetails.canLocalForceClose && !channeldetails.isBackup\n                        text: qsTr('Local Force-close')\n                    }\n                }\n\n                ColumnLayout {\n                    Layout.columnSpan: 2\n                    Layout.maximumWidth: parent.width\n\n                    InfoTextArea {\n                        id: errorText\n                        Layout.alignment: Qt.AlignHCenter\n                        Layout.maximumWidth: parent.width\n                        visible: !channeldetails.isClosing && errorText.text\n                        iconStyle: InfoTextArea.IconStyle.Error\n                    }\n                    Label {\n                        Layout.alignment: Qt.AlignHCenter\n                        text: qsTr('Closing...')\n                        visible: channeldetails.isClosing\n                    }\n                    BusyIndicator {\n                        Layout.alignment: Qt.AlignHCenter\n                        visible: channeldetails.isClosing\n                    }\n                }\n            }\n        }\n\n        FlatButton {\n            Layout.columnSpan: 2\n            Layout.fillWidth: true\n            text: qsTr('Close channel')\n            icon.source: '../../icons/closebutton.png'\n            enabled: !channeldetails.isClosing\n            onClicked: {\n                if (closetypegroup.checkedButton.closetype == 'local_force') {\n                    showBackupThenClose()\n                } else {\n                    doCloseChannel()\n                }\n            }\n        }\n    }\n\n    function showBackupThenClose() {\n        var sharedialog = app.genericShareDialog.createObject(app, {\n            title: qsTr('Save channel backup and force close'),\n            text_qr: channeldetails.channelBackup(),\n            text_help: channeldetails.messageForceCloseBackup,\n            helpTextIconStyle: InfoTextArea.IconStyle.Warn\n        })\n        sharedialog.closed.connect(function() {\n            doCloseChannel()\n        })\n        sharedialog.open()\n    }\n\n    function doCloseChannel() {\n        _closing_method = closetypegroup.checkedButton.closetype\n        channeldetails.closeChannel(_closing_method)\n    }\n\n    function showCloseMessage(text) {\n        var msgdialog = app.messageDialog.createObject(app, {\n            text: text\n        })\n        msgdialog.open()\n    }\n\n    ChannelDetails {\n        id: channeldetails\n        wallet: Daemon.currentWallet\n        channelid: dialog.channelid\n\n        onAuthRequired: (method, authMessage) => {\n            app.handleAuthRequired(channeldetails, method, authMessage)\n        }\n\n        onChannelChanged: {\n            if (!channeldetails.canClose || channeldetails.isClosing)\n                return\n\n            // init default choice\n            if (channeldetails.canCoopClose)\n                closetypeCoop.checked = true\n            else if (channeldetails.canRequestForceClose)\n                closetypeRemoteForce.checked = true\n            else\n                closetypeLocalForce.checked = true\n        }\n\n        onChannelCloseSuccess: {\n            if (_closing_method == 'local_force') {\n                showCloseMessage(qsTr('Channel closed. You may need to wait at least %1 blocks, because of CSV delays').arg(channeldetails.toSelfDelay))\n            } else if (_closing_method == 'remote_force') {\n                showCloseMessage(qsTr('Request sent'))\n            } else if (_closing_method == 'cooperative') {\n                showCloseMessage(qsTr('Channel closed'))\n            }\n            dialog.close()\n        }\n\n        onChannelCloseFailed: (message) => {\n            errorText.text = message\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ConfirmTxDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    required property QtObject finalizer\n    required property Amount satoshis\n    property string address\n    property string message\n    property bool showOptions: true\n    property alias amountLabelText: amountLabel.text\n    property alias sendButtonText: sendButton.text\n\n    title: qsTr('Transaction Fee')\n    iconSource: Qt.resolvedUrl('../../icons/question.png')\n\n    // copy these to finalizer\n    onAddressChanged: finalizer.address = address\n    onSatoshisChanged: finalizer.amount = satoshis\n\n    width: parent.width\n    height: parent.height\n    padding: 0\n\n    function updateAmountText() {\n        if (finalizer.valid) {\n            btcValue.text = Config.formatSats(finalizer.effectiveAmount, false)\n            fiatValue.text = Daemon.fx.enabled\n                ? Daemon.fx.fiatValue(finalizer.effectiveAmount, false)\n                : ''\n        } else {\n            btcValue.text = Config.formatSats(finalizer.amount, false)\n            fiatValue.text = Daemon.fx.enabled\n                ? Daemon.fx.fiatValue(finalizer.amount, false)\n                : ''\n        }\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            leftMargin: constants.paddingLarge\n            rightMargin: constants.paddingLarge\n\n            contentHeight: rootLayout.height\n            clip: true\n            interactive: height < contentHeight\n\n            GridLayout {\n                id: rootLayout\n                width: parent.width\n\n                columns: 2\n\n                Label {\n                    id: amountLabel\n                    Layout.columnSpan: 2\n                    text: qsTr('Amount to send')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    GridLayout {\n                        columns: 2\n                        Label {\n                            id: btcValue\n                            Layout.alignment: Qt.AlignRight\n                            font.pixelSize: constants.fontSizeXLarge\n                            font.family: FixedFont\n                            font.bold: true\n                        }\n\n                        Label {\n                            Layout.fillWidth: true\n                            text: Config.baseUnit\n                            color: Material.accentColor\n                            font.pixelSize: constants.fontSizeXLarge\n                        }\n\n                        Label {\n                            id: fiatValue\n                            Layout.alignment: Qt.AlignRight\n                            visible: Daemon.fx.enabled\n                            font.pixelSize: constants.fontSizeMedium\n                            color: constants.mutedForeground\n                        }\n\n                        Label {\n                            Layout.fillWidth: true\n                            visible: Daemon.fx.enabled\n                            text: Daemon.fx.fiatCurrency\n                            font.pixelSize: constants.fontSizeMedium\n                            color: constants.mutedForeground\n                        }\n                        Component.onCompleted: updateAmountText()\n                        Connections {\n                            target: finalizer\n                            function onEffectiveAmountChanged() {\n                                updateAmountText()\n                            }\n                        }\n                    }\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    text: qsTr('Fee')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    height: feepicker.height\n\n                    FeePicker {\n                        id: feepicker\n                        width: parent.width\n                        finalizer: dialog.finalizer\n\n                        Label {\n                            visible: !finalizer.extraFee.isEmpty\n                            text: qsTr('Extra fee')\n                            color: Material.accentColor\n                        }\n\n                        FormattedAmount {\n                            visible: !finalizer.extraFee.isEmpty\n                            amount: finalizer.extraFee\n                        }\n                    }\n                }\n\n                ToggleLabel {\n                    id: optionstoggle\n                    Layout.columnSpan: 2\n                    labelText: qsTr('Options')\n                    color: Material.accentColor\n                    visible: showOptions\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    visible: optionstoggle.visible && !optionstoggle.collapsed\n                    height: optionslayout.height\n\n                    GridLayout {\n                        id: optionslayout\n                        width: parent.width\n                        columns: 2\n\n                        ElCheckBox {\n                            Layout.fillWidth: true\n                            text: qsTr('Use multiple change addresses')\n                            onCheckedChanged: {\n                                if (activeFocus) {\n                                    Daemon.currentWallet.multipleChange = checked\n                                    finalizer.doUpdate()\n                                }\n                            }\n                            Component.onCompleted: {\n                                checked = Daemon.currentWallet.multipleChange\n                            }\n                        }\n\n                        HelpButton {\n                            heading: qsTr('Use multiple change addresses')\n                            helptext: [qsTr('In some cases, use up to 3 change addresses in order to break up large coin amounts and obfuscate the recipient address.'),\n                                       qsTr('This may result in higher transactions fees.')].join(' ')\n                        }\n\n                        ElCheckBox {\n                            Layout.fillWidth: true\n                            text: Config.shortDescFor('WALLET_COIN_CHOOSER_OUTPUT_ROUNDING')\n                            onCheckedChanged: {\n                                if (activeFocus) {\n                                    Config.outputValueRounding = checked\n                                    finalizer.doUpdate()\n                                }\n                            }\n                            Component.onCompleted: {\n                                checked = Config.outputValueRounding\n                            }\n                        }\n\n                        HelpButton {\n                            heading: Config.shortDescFor('WALLET_COIN_CHOOSER_OUTPUT_ROUNDING')\n                            helptext: Config.longDescFor('WALLET_COIN_CHOOSER_OUTPUT_ROUNDING')\n                        }\n\n                    }\n                }\n\n                InfoTextArea {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    Layout.topMargin: constants.paddingLarge\n                    Layout.bottomMargin: constants.paddingLarge\n                    visible: finalizer.warning != ''\n                    text: finalizer.warning\n                    iconStyle: InfoTextArea.IconStyle.Warn\n                }\n\n                ToggleLabel {\n                    id: inputs_label\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingMedium\n                    visible: finalizer.valid\n\n                    labelText: qsTr('Inputs (%1)').arg(finalizer.inputs.length)\n                    color: Material.accentColor\n                }\n\n                Repeater {\n                    model: inputs_label.collapsed\n                        ? undefined\n                        : finalizer.inputs\n                    delegate: TxInput {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        visible: finalizer.valid\n\n                        idx: index\n                        model: modelData\n                    }\n                }\n\n                ToggleLabel {\n                    id: outputs_label\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingMedium\n                    visible: finalizer.valid\n\n                    labelText: qsTr('Outputs (%1)').arg(finalizer.outputs.length)\n                    color: Material.accentColor\n                }\n\n                Repeater {\n                    model: outputs_label.collapsed\n                        ? undefined\n                        : finalizer.outputs\n                    delegate: TxOutput {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        visible: finalizer.valid\n\n                        allowShare: false\n                        allowClickAddress: false\n\n                        idx: index\n                        model: modelData\n                    }\n                }\n\n            }\n        }\n\n        FlatButton {\n            id: sendButton\n            Layout.fillWidth: true\n            text: (Daemon.currentWallet.isWatchOnly || !Daemon.currentWallet.canSignWithoutCosigner)\n                    ? qsTr('Finalize...')\n                    : qsTr('Pay...')\n            icon.source: '../../icons/confirmed.png'\n            enabled: finalizer.valid\n            onClicked: doAccept()\n        }\n    }\n\n    onClosed: doReject()\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/Constants.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nItem {\n    readonly property int paddingXXSmall: 4\n    readonly property int paddingXSmall: 6\n    readonly property int paddingSmall: 8\n    readonly property int paddingMedium: 12\n    readonly property int paddingLarge: 16\n    readonly property int paddingXLarge: 20\n    readonly property int paddingXXLarge: 28\n\n    readonly property int fontSizeXSmall: 10\n    readonly property int fontSizeSmall: 12\n    readonly property int fontSizeMedium: 15\n    readonly property int fontSizeLarge: 18\n    readonly property int fontSizeXLarge: 22\n    readonly property int fontSizeXXLarge: 28\n\n    readonly property int iconSizeXSmall: 12\n    readonly property int iconSizeSmall: 16\n    readonly property int iconSizeMedium: 24\n    readonly property int iconSizeLarge: 32\n    readonly property int iconSizeXLarge: 48\n    readonly property int iconSizeXXLarge: 64\n\n    readonly property int fingerWidth: 64 // TODO: determine finger width from screen dimensions and resolution\n\n    property color mutedForeground: 'gray' //Qt.lighter(Material.background, 2)\n    property color darkerBackground: Qt.darker(Material.background, 1.20)\n    property color lighterBackground: Qt.lighter(Material.background, 1.10)\n    property color darkerDialogBackground: Qt.darker(Material.dialogColor, 1.20)\n    property color notificationBackground: Qt.lighter(Material.background, 1.5)\n\n    property color colorCredit: \"#ff80ff80\"\n    property color colorDebit: \"#ffff8080\"\n\n    property color colorInfo: Material.accentColor\n    property color colorWarning: 'yellow'\n    property color colorError: '#ffff8080'\n    property color colorProgress: '#ffffff80'\n    property color colorDone: '#ff80ff80'\n    property color colorValidBackground: '#ff008000'\n    property color colorInvalidBackground: '#ff800000'\n    property color colorAcceptable: '#ff8080ff'\n    property color colorOk: colorDone\n\n    property color colorLightningLocal: \"#6060ff\"\n    property color colorLightningLocalReserve: \"#0000a0\"\n    property color colorLightningRemote: \"yellow\"\n    property color colorLightningRemoteReserve: Qt.darker(colorLightningRemote, 1.5)\n    property color colorChannelOpen: \"#ff80ff80\"\n\n    property color colorPiechartTotal: Material.accentColor\n    property color colorPiechartOnchain: Qt.darker(Material.accentColor, 1.50)\n    property color colorPiechartFrozen: 'gray'\n    property color colorPiechartLightning: 'orange'\n    property color colorPiechartLightningFrozen: Qt.darker('orange', 1.20)\n    property color colorPiechartUnconfirmed: Qt.darker(Material.accentColor, 2.00)\n    property color colorPiechartUnmatured: 'magenta'\n\n    property color colorPiechartParticipant: 'gray'\n    property color colorPiechartSignature: 'yellow'\n\n    property color colorAddressExternal: \"#8af296\" //Qt.rgba(0,1,0,0.5)\n    property color colorAddressInternal: \"#ffff00\" //Qt.rgba(1,0.93,0,0.75)\n    property color colorAddressUsed: Qt.rgba(0.5,0.5,0.5,1)\n    property color colorAddressUsedWithBalance: Qt.rgba(0.75,0.75,0.75,1)\n    property color colorAddressFrozen: Qt.rgba(0.5,0.5,1,1)\n    property color colorAddressBilling: \"#8cb3f2\"\n    property color colorAddressSwap: colorAddressBilling\n    property color colorAddressAccounting: \"#ff9b45\"\n\n    function colorAlpha(baseColor, alpha) {\n        return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, alpha)\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/CpfpBumpFeeDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    required property QtObject cpfpfeebumper\n\n    title: qsTr('Bump Fee')\n    iconSource: Qt.resolvedUrl('../../icons/rocket.png')\n\n    width: parent.width\n    height: parent.height\n    padding: 0\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            leftMargin: constants.paddingLarge\n            rightMargin: constants.paddingLarge\n\n            contentHeight: rootLayout.height\n            clip: true\n            interactive: height < contentHeight\n\n            GridLayout {\n                id: rootLayout\n                width: parent.width\n\n                columns: 2\n\n                Label {\n                    Layout.fillWidth: true\n                    text: qsTr('A CPFP is a transaction that sends an unconfirmed output back to yourself, with a high fee.')\n                    wrapMode: Text.Wrap\n                }\n\n                HelpButton {\n                    heading: qsTr('CPFP - Child Pays For Parent')\n                    helptext: qsTr('A CPFP is a transaction that sends an unconfirmed output back to yourself, with a high fee. The goal is to have miners confirm the parent transaction in order to get the fee attached to the child transaction.')\n                    + '<br/><br/>' + qsTr('The proposed fee is computed using your fee/vkB settings, applied to the total size of both child and parent transactions. After you broadcast a CPFP transaction, it is normal to see a new unconfirmed transaction in your history.')\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    text: qsTr('Child tx fee')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    height: feepicker_childinfo.height\n\n                    FeePicker {\n                        id: feepicker_childinfo\n                        width: parent.width\n                        finalizer: dialog.cpfpfeebumper\n                        targetLabel: qsTr('Target total')\n                        feeLabel: qsTr('Fee for child')\n                        feeRateLabel: qsTr('Fee rate for child')\n                        showPicker: false\n                    }\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    text: qsTr('Total')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n\n                    GridLayout {\n                        width: parent.width\n                        columns: 2\n\n                        Label {\n                            Layout.preferredWidth: 1\n                            Layout.fillWidth: true\n                            text: qsTr('Total size')\n                            color: Material.accentColor\n                        }\n\n                        Label {\n                            Layout.preferredWidth: 2\n                            Layout.fillWidth: true\n                            text: cpfpfeebumper.valid\n                                ? cpfpfeebumper.totalSize + ' ' + UI_UNIT_NAME.TXSIZE_VBYTES\n                                : ''\n                        }\n\n                        Label {\n                            Layout.preferredWidth: 1\n                            Layout.fillWidth: true\n                            text: qsTr('Total fee')\n                            color: Material.accentColor\n                        }\n\n                        FormattedAmount {\n                            Layout.preferredWidth: 2\n                            Layout.fillWidth: true\n                            amount: cpfpfeebumper.totalFee\n                            valid: cpfpfeebumper.valid\n                        }\n\n                        Label {\n                            Layout.preferredWidth: 1\n                            Layout.fillWidth: true\n                            text: qsTr('Total fee rate')\n                            color: Material.accentColor\n                        }\n\n                        RowLayout {\n                            Layout.preferredWidth: 2\n                            Layout.fillWidth: true\n                            Label {\n                                text: cpfpfeebumper.valid ? cpfpfeebumper.totalFeeRate : ''\n                                font.family: FixedFont\n                            }\n\n                            Label {\n                                visible: cpfpfeebumper.valid\n                                text: UI_UNIT_NAME.FEERATE_SAT_PER_VB\n                                color: Material.accentColor\n                            }\n                        }\n\n                        FeePicker {\n                            id: feepicker\n                            Layout.columnSpan: 2\n                            Layout.fillWidth: true\n                            finalizer: dialog.cpfpfeebumper\n                            showTxInfo: false\n                            allowPickerFeeRates: false\n                        }\n                    }\n                }\n\n                InfoTextArea {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    Layout.topMargin: constants.paddingLarge\n                    visible: cpfpfeebumper.warning != ''\n                    text: cpfpfeebumper.warning\n                    iconStyle: InfoTextArea.IconStyle.Warn\n                }\n\n                ToggleLabel {\n                    id: inputs_label\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingMedium\n\n                    visible: cpfpfeebumper.valid\n                    labelText: qsTr('Inputs (%1)').arg(cpfpfeebumper.inputs.length)\n                    color: Material.accentColor\n                }\n\n                Repeater {\n                    model: inputs_label.collapsed || !inputs_label.visible\n                        ? undefined\n                        : cpfpfeebumper.inputs\n                    delegate: TxInput {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        idx: index\n                        model: modelData\n                    }\n                }\n\n                ToggleLabel {\n                    id: outputs_label\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingMedium\n\n                    visible: cpfpfeebumper.valid\n                    labelText: qsTr('Outputs (%1)').arg(cpfpfeebumper.outputs.length)\n                    color: Material.accentColor\n                }\n\n                Repeater {\n                    model: outputs_label.collapsed || !outputs_label.visible\n                        ? undefined\n                        : cpfpfeebumper.outputs\n                    delegate: TxOutput {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        allowShare: false\n                        allowClickAddress: false\n\n                        idx: index\n                        model: modelData\n                    }\n                }\n\n            }\n        }\n\n        FlatButton {\n            id: sendButton\n            Layout.fillWidth: true\n            text: qsTr('Ok')\n            icon.source: '../../icons/confirmed.png'\n            enabled: cpfpfeebumper.valid\n            onClicked: doAccept()\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ExceptionDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport QtQml\n\nimport \"controls\"\n\nElDialog\n{\n    id: root\n\n    property var crashData\n\n    property bool _sending: false\n\n    width: parent.width\n    height: parent.height\n    z: 1000  // assure topmost of all other dialogs. note: child popups need even higher!\n    // disable padding in ElDialog as it is overwritten here and shows no effect, this dialog needs padding though\n    needsSystemBarPadding: false\n\n    header: null\n\n    ColumnLayout {\n        anchors.topMargin: app.statusBarHeight  // edge-to-edge layout padding\n        anchors.bottomMargin: app.navigationBarHeight\n        anchors.fill: parent\n        enabled: !_sending\n\n        Image {\n            Layout.alignment: Qt.AlignCenter\n            Layout.preferredWidth: 128\n            Layout.preferredHeight: 128\n            source: '../../icons/bug.png'\n        }\n        Label {\n            text: qsTr('Sorry!')\n            font.pixelSize: constants.fontSizeLarge\n        }\n\n        Label {\n            Layout.fillWidth: true\n            text: qsTr('Something went wrong while executing Electrum.')\n        }\n        Label {\n            Layout.fillWidth: true\n            text: qsTr('To help us diagnose and fix the problem, you can send us a bug report that contains useful debug information:')\n            wrapMode: Text.Wrap\n        }\n        Button {\n            Layout.alignment: Qt.AlignCenter\n            text: qsTr('Show report contents')\n            onClicked: {\n                if (crashData.traceback)\n                    console.log('traceback: ' + crashData.traceback.stack)\n                var dialog = report.createObject(app, {\n                    reportText: crashData.reportstring\n                })\n                dialog.open()\n            }\n        }\n        Label {\n            Layout.fillWidth: true\n            text: qsTr('Please briefly describe what led to the error (optional):')\n        }\n        TextArea {\n            id: user_text\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            background: Rectangle {\n                color: Qt.darker(Material.background, 1.25)\n            }\n        }\n        Label {\n            text: qsTr('Do you want to send this report?')\n        }\n        RowLayout {\n            Button {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 3\n                text: qsTr('Send Bug Report')\n                onClicked: {\n                    var dialog = app.messageDialog.createObject(app, {\n                        text: qsTr('Confirm to send bugreport?'),\n                        yesno: true,\n                        z: 1001  // assure topmost of all other dialogs\n                    })\n                    dialog.accepted.connect(function() {\n                        AppController.sendReport(user_text.text)\n                    })\n                    dialog.open()\n                }\n            }\n            Button {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 2\n                text: qsTr('Not Now')\n                onClicked: close()\n            }\n        }\n    }\n\n    BusyIndicator {\n        anchors.centerIn: parent\n        running: _sending\n    }\n\n    Component {\n        id: report\n        ElDialog {\n            property string reportText\n\n            width: parent.width\n            height: parent.height\n            z: 1001  // above root\n            needsSystemBarPadding: false\n\n            header: null\n\n            Flickable {\n                anchors.fill: parent\n                anchors.topMargin: app.statusBarHeight\n                anchors.bottomMargin: app.navigationBarHeight\n                contentHeight: reportLabel.implicitHeight\n                interactive: height < contentHeight\n\n                Label {\n                    id: reportLabel\n                    text: reportText\n                    wrapMode: Text.Wrap\n                    width: parent.width\n                }\n            }\n            onClosed: destroy()\n        }\n    }\n\n    Connections {\n        target: AppController\n        function onSendingBugreportSuccess(text) {\n            _sending = false\n            var dialog = app.messageDialog.createObject(app, {\n                text: text,\n                richText: true,\n                z: 1001  // assure topmost of all other dialogs\n            })\n            dialog.open()\n            close()\n        }\n        function onSendingBugreportFailure(text) {\n            _sending = false\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: text,\n                richText: true,\n                z: 1001  // assure topmost of all other dialogs\n            })\n            dialog.open()\n        }\n        function onSendingBugreport() {\n            _sending = true\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ExportTxDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    required property string text\n    property string text_qr\n    // if text_qr is undefined text will be used\n    property string text_help\n    property string text_warn\n    property string tx_label\n\n    title: qsTr('Share Transaction')\n\n    width: parent.width\n    height: parent.height\n\n    padding: 0\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            contentHeight: rootLayout.height\n            clip:true\n            interactive: height < contentHeight\n\n            ColumnLayout {\n                id: rootLayout\n                width: parent.width\n                spacing: constants.paddingMedium\n\n                TextHighlightPane {\n                    Layout.fillWidth: true\n                    Layout.leftMargin: constants.paddingMedium\n                    Layout.rightMargin: constants.paddingMedium\n                    padding: constants.paddingMedium\n                    ColumnLayout {\n                        width: parent.width\n                        QRImage {\n                            id: qr\n                            qrdata: dialog.text_qr\n                            Layout.alignment: Qt.AlignHCenter\n                            Layout.topMargin: constants.paddingMedium\n                            Layout.bottomMargin: constants.paddingMedium\n                        }\n                    }\n                }\n\n                InfoTextArea {\n                    Layout.fillWidth: true\n                    Layout.margins: constants.paddingLarge\n                    visible: dialog.text_help\n                    text: dialog.text_help\n                }\n\n                InfoTextArea {\n                    Layout.fillWidth: true\n                    Layout.margins: constants.paddingLarge\n                    Layout.topMargin: dialog.text_help\n                        ? 0\n                        : constants.paddingLarge\n                    visible: dialog.text_warn\n                    text: dialog.text_warn\n                    iconStyle: InfoTextArea.IconStyle.Warn\n                }\n            }\n        }\n\n        ButtonContainer {\n            id: buttons\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Copy')\n                icon.source: '../../icons/copy_bw.png'\n                onClicked: {\n                    AppController.textToClipboard(dialog.text)\n                    toaster.show(this, qsTr('Copied!'))\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Share')\n                icon.source: '../../icons/share.png'\n                onClicked: {\n                    AppController.doShare(dialog.text, dialog.title)\n                }\n            }\n            function beforeLayout() {\n                var export_tx_buttons = app.pluginsComponentsByName('export_tx_button')\n                for (var i=0; i < export_tx_buttons.length; i++) {\n                    var b = export_tx_buttons[i].createObject(buttons, {\n                        dialog: dialog\n                    })\n                    b.Layout.fillWidth = true\n                    b.Layout.preferredWidth = 1\n                    buttons.addItem(b)\n                }\n            }\n        }\n    }\n\n    Toaster {\n        id: toaster\n    }\n\n    Connections {\n        target: dialog.enter\n        function onRunningChanged() {\n            if (!dialog.enter.running) {\n                qr.render = true\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/GenericShareDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    property string text\n    property string text_qr\n    // If text is set, it is displayed as a string and also used as data in the QR code shown.\n    // text_qr can also be set if we want to show different data in the QR code.\n    // If only text_qr is set, the QR code is shown but the string itself is not,\n    //     however the copy button still exposes the string.\n\n    property string text_help\n    property int helpTextIconStyle: InfoTextArea.IconStyle.Info\n\n    title: ''\n\n    width: parent.width\n    height: parent.height\n\n    padding: 0\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.fillHeight: true\n            Layout.fillWidth: true\n\n            contentHeight: rootLayout.height\n            clip:true\n            interactive: height < contentHeight\n\n            ColumnLayout {\n                id: rootLayout\n                width: parent.width\n                spacing: constants.paddingMedium\n\n                TextHighlightPane {\n                    Layout.alignment: Qt.AlignHCenter\n                    Layout.fillWidth: true\n                    Layout.leftMargin: constants.paddingMedium\n                    Layout.rightMargin: constants.paddingMedium\n\n                    ColumnLayout {\n                        width: parent.width\n\n                        QRImage {\n                            id: qr\n                            render: dialog.enter ? false : true\n                            qrdata: dialog.text_qr ? dialog.text_qr : dialog.text\n                            Layout.alignment: Qt.AlignHCenter\n                            Layout.topMargin: constants.paddingMedium\n                            Layout.bottomMargin: constants.paddingMedium\n                        }\n\n                        TextHighlightPane {\n                            Layout.fillWidth: true\n                            visible: dialog.text\n\n                            Label {\n                                width: parent.width\n                                text: dialog.text\n                                wrapMode: Text.Wrap\n                                font.pixelSize: constants.fontSizeLarge\n                                font.family: FixedFont\n                                maximumLineCount: 4\n                                elide: Text.ElideRight\n                            }\n                        }\n                    }\n                }\n\n                InfoTextArea {\n                    Layout.leftMargin: constants.paddingMedium\n                    Layout.rightMargin: constants.paddingMedium\n                    iconStyle: helpTextIconStyle\n                    visible: dialog.text_help\n                    text: dialog.text_help\n                    Layout.fillWidth: true\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n\n                text: qsTr('Copy')\n                icon.source: '../../icons/copy_bw.png'\n                onClicked: {\n                    AppController.textToClipboard(dialog.text ? dialog.text : dialog.text_qr)\n                    toaster.show(this, qsTr('Copied!'))\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n\n                text: qsTr('Share')\n                icon.source: '../../icons/share.png'\n                onClicked: {\n                    AppController.doShare(dialog.text ? dialog.text : dialog.text_qr, dialog.title)\n                }\n            }\n        }\n    }\n\n    Connections {\n        target: dialog.enter\n        function onRunningChanged() {\n            if (!dialog.enter.running) {\n                qr.render = true\n            }\n        }\n    }\n\n    Toaster {\n        id: toaster\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/History.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\nimport QtQml.Models\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: rootItem\n    visible: Daemon.currentWallet\n    padding: 0\n    clip: true\n\n    background: Rectangle {\n        color: constants.darkerBackground\n    }\n\n    ElListView {\n        id: listview\n        width: parent.width\n        height: parent.height\n        boundsBehavior: Flickable.StopAtBounds\n\n        model: visualModel\n\n        header: Item {\n            width: parent.width\n            height: headerLayout.height\n            ColumnLayout {\n                id: headerLayout\n                anchors.centerIn: parent\n                BalanceSummary {\n                    Layout.topMargin: constants.paddingXLarge\n                    Layout.bottomMargin: constants.paddingXLarge\n                }\n            }\n        }\n        headerPositioning: ListView.InlineHeader\n\n        readonly property variant sectionLabels: {\n            'local': qsTr('Local'),\n            'mempool': qsTr('Mempool'),\n            'today': qsTr('Today'),\n            'yesterday': qsTr('Yesterday'),\n            'lastweek': qsTr('Last week'),\n            'lastmonth': qsTr('Last month'),\n            'older': qsTr('Older')\n        }\n\n        section.property: 'section'\n        section.criteria: ViewSection.FullString\n        section.delegate: RowLayout {\n            width: ListView.view.width\n            required property string section\n            Label {\n                text: listview.sectionLabels[section]\n                Layout.alignment: Qt.AlignHCenter\n                Layout.topMargin: constants.paddingLarge\n                font.pixelSize: constants.fontSizeMedium\n                color: Material.accentColor\n            }\n        }\n\n        DelegateModel {\n            id: visualModel\n            model: Daemon.currentWallet.historyModel\n\n            groups: [\n                DelegateModelGroup { name: 'today'; includeByDefault: false },\n                DelegateModelGroup { name: 'yesterday'; includeByDefault: false },\n                DelegateModelGroup { name: 'lastweek'; includeByDefault: false },\n                DelegateModelGroup { name: 'lastmonth'; includeByDefault: false },\n                DelegateModelGroup { name: 'older'; includeByDefault: false }\n            ]\n\n            delegate: HistoryItemDelegate { }\n        }\n\n        ScrollIndicator.vertical: ScrollIndicator { }\n\n        Label {\n            visible: Daemon.currentWallet.historyModel.count == 0 && !Daemon.currentWallet.synchronizing\n            anchors.centerIn: parent\n            width: listview.width * 4/5\n            font.pixelSize: constants.fontSizeXXLarge\n            color: constants.mutedForeground\n            text: qsTr('No transactions in this wallet yet')\n            wrapMode: Text.Wrap\n            horizontalAlignment: Text.AlignHCenter\n        }\n    }\n\n    MouseArea {\n        id: vdragscroll\n        anchors {\n            top: listview.top\n            right: listview.right\n            bottom: listview.bottom\n        }\n        width: constants.fingerWidth\n        drag.target: dragb\n        onPressedChanged: if (pressed) {\n            dragb.y = mouseY + listview.y - dragb.height/2\n        }\n    }\n\n    Rectangle {\n        id: dragb\n        anchors.right: vdragscroll.left\n        width: postext.width + constants.paddingXXLarge\n        height: postext.height + constants.paddingXXLarge\n        radius: constants.paddingXSmall\n\n        color: constants.colorAlpha(Material.accentColor, 0.33)\n        border.color: Material.accentColor\n        opacity : vdragscroll.drag.active ? 1 : 0\n        Behavior on opacity { NumberAnimation { duration: 300 } }\n\n        onYChanged: {\n            if (vdragscroll.drag.active) {\n                listview.contentY =\n                    Math.max(listview.originY,\n                    Math.min(listview.contentHeight - listview.height + listview.originY,\n                        ((y-listview.y)/(listview.height - dragb.height)) * (listview.contentHeight - listview.height + listview.originY) ))\n            }\n        }\n        Label {\n            id: postext\n            anchors.centerIn: parent\n            text: dragb.opacity\n                    ? listview.itemAt(0,listview.contentY + (dragb.y + dragb.height/2)).delegateModel.date\n                    : ''\n            font.pixelSize: constants.fontSizeLarge\n        }\n    }\n\n    Connections {\n        target: Network\n        function onHeightChanged(height) {\n            Daemon.currentWallet.historyModel.updateBlockchainHeight(height)\n        }\n    }\n\n    Connections {\n        target: Daemon\n        function onWalletLoaded() {\n            listview.positionViewAtBeginning()\n        }\n    }\n\n    StackView.onVisibleChanged: {\n        // refresh model if History becomes visible and the model is dirty.\n        if (StackView.visible) {\n            Daemon.currentWallet.historyModel.initModel(false)\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ImportAddressesKeysDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: root\n\n    title: Daemon.currentWallet.isWatchOnly\n            ? qsTr('Import additional addresses')\n            : qsTr('Import additional keys')\n\n    property bool valid: false\n\n    width: parent.width\n    height: parent.height\n\n    padding: 0\n\n    function verify(text) {\n        if (Daemon.currentWallet.isWatchOnly)\n            return bitcoin.isAddressList(text)\n        else\n            return bitcoin.isPrivateKeyList(text)\n    }\n\n    onAccepted: {\n        if (Daemon.currentWallet.isWatchOnly)\n            Daemon.currentWallet.importAddresses(import_ta.text)\n        else\n            Daemon.currentWallet.importPrivateKeys(import_ta.text)\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        ColumnLayout {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n\n            Label {\n                Layout.fillWidth: true\n                wrapMode: Text.Wrap\n                text: (Daemon.currentWallet.isWatchOnly\n                        ? qsTr('Enter, paste or scan additional addresses')\n                        : qsTr('Enter, paste or scan additional private keys')) +\n                      '. ' + qsTr('You can add multiple, each on a separate line.')\n            }\n\n            RowLayout {\n                ElTextArea {\n                    id: import_ta\n                    Layout.fillWidth: true\n                    Layout.fillHeight: true\n                    font.family: FixedFont\n                    wrapMode: TextEdit.WrapAnywhere\n                    onTextChanged: valid = verify(text)\n                    inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase\n                    background: PaneInsetBackground {\n                        baseColor: constants.darkerDialogBackground\n                    }\n                }\n                ColumnLayout {\n                    Layout.alignment: Qt.AlignTop\n                    ToolButton {\n                        icon.source: '../../icons/paste.png'\n                        icon.height: constants.iconSizeMedium\n                        icon.width: constants.iconSizeMedium\n                        onClicked: {\n                            if (verify(AppController.clipboardToText())) {\n                                if (import_ta.text != '')\n                                    import_ta.text = import_ta.text + '\\n'\n                                import_ta.text = import_ta.text + AppController.clipboardToText()\n                            }\n                        }\n                    }\n                    ToolButton {\n                        icon.source: '../../icons/qrcode.png'\n                        icon.height: constants.iconSizeMedium\n                        icon.width: constants.iconSizeMedium\n                        scale: 1.2\n                        onClicked: {\n                            var dialog = app.scanDialog.createObject(app, {\n                                hint: Daemon.currentWallet.isWatchOnly\n                                    ? qsTr('Scan another address')\n                                    : qsTr('Scan another private key')\n                            })\n                            dialog.onFoundText.connect(function(data) {\n                                if (verify(data)) {\n                                    if (import_ta.text != '')\n                                        import_ta.text = import_ta.text + ',\\n'\n                                    import_ta.text = import_ta.text + data\n                                }\n                                dialog.close()\n                            })\n                            dialog.open()\n                        }\n                    }\n                }\n            }\n\n            Item {\n                Layout.preferredWidth: 1\n                Layout.fillHeight: true\n            }\n        }\n\n        FlatButton {\n            Layout.fillWidth: true\n            icon.source: '../../icons/add.png'\n            text: qsTr('Import')\n            enabled: valid\n            onClicked: doAccept()\n        }\n    }\n\n    Bitcoin {\n        id: bitcoin\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ImportChannelBackupDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: root\n\n    property bool valid: false\n\n    width: parent.width\n    height: parent.height\n\n    padding: 0\n\n    title: qsTr('Import channel backup')\n    iconSource: Qt.resolvedUrl('../../icons/file.png')\n\n    function verifyChannelBackup(text) {\n        return valid = Daemon.currentWallet.isValidChannelBackup(text)\n    }\n\n    onAccepted: {\n        Daemon.currentWallet.importChannelBackup(channelbackup_ta.text)\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        RowLayout {\n            Layout.fillWidth: true\n            Layout.leftMargin: constants.paddingLarge\n\n            TextArea {\n                id: channelbackup_ta\n                Layout.fillWidth: true\n                Layout.minimumHeight: 80\n                font.family: FixedFont\n                focus: true\n                wrapMode: TextEdit.WrapAnywhere\n                onTextChanged: verifyChannelBackup(text)\n            }\n            ColumnLayout {\n                ToolButton {\n                    icon.source: '../../icons/paste.png'\n                    icon.height: constants.iconSizeMedium\n                    icon.width: constants.iconSizeMedium\n                    onClicked: {\n                        channelbackup_ta.text = AppController.clipboardToText()\n                    }\n                }\n                ToolButton {\n                    icon.source: '../../icons/qrcode.png'\n                    icon.height: constants.iconSizeMedium\n                    icon.width: constants.iconSizeMedium\n                    scale: 1.2\n                    onClicked: {\n                        var dialog = app.scanDialog.createObject(app, {\n                            hint:  qsTr('Scan a channel backup')\n                        })\n                        dialog.onFoundText.connect(function(data) {\n                            channelbackup_ta.text = data\n                            dialog.close()\n                        })\n                        dialog.open()\n                    }\n                }\n            }\n        }\n\n        TextArea {\n            id: validationtext\n            visible: text\n            Layout.fillWidth: true\n            Layout.leftMargin: constants.paddingLarge\n\n            readOnly: true\n            wrapMode: TextInput.WordWrap\n            background: Rectangle {\n                color: 'transparent'\n            }\n        }\n\n        Item { Layout.preferredWidth: 1; Layout.fillHeight: true }\n\n        FlatButton {\n            Layout.fillWidth: true\n            enabled: valid\n            text: qsTr('Import')\n            onClicked: doAccept()\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/InvoiceDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    property Invoice invoice\n    property bool payImmediately: false\n    property string broadcastTxid\n\n    signal doPay\n    signal invoiceAmountChanged\n\n    title: invoice.invoiceType == Invoice.OnchainInvoice ? qsTr('On-chain Invoice') : qsTr('Lightning Invoice')\n    iconSource: Qt.resolvedUrl('../../icons/tab_send.png')\n\n    padding: 0\n\n    property bool _canMax: invoice.invoiceType == Invoice.OnchainInvoice\n\n    property Amount _invoice_amount: invoice.amount\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.preferredWidth: parent.width\n            Layout.fillHeight: true\n\n            leftMargin: constants.paddingLarge\n            rightMargin: constants.paddingLarge\n\n            contentHeight: rootLayout.height\n            clip:true\n            interactive: height < contentHeight\n\n            GridLayout {\n                id: rootLayout\n                width: parent.width\n\n                columns: 2\n\n                InfoTextArea {\n                    id: helpText\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    Layout.bottomMargin: constants.paddingLarge\n                    visible: text\n                    text:  invoice.userinfo ? invoice.userinfo : invoice.statusString\n                    iconStyle: invoice.status == Invoice.Failed || invoice.status == Invoice.Unknown\n                        ? InfoTextArea.IconStyle.Warn\n                        : invoice.status == Invoice.Expired\n                            ? InfoTextArea.IconStyle.Error\n                            : invoice.status == Invoice.Inflight || invoice.status == Invoice.Routing || invoice.status == Invoice.Unconfirmed\n                                ? InfoTextArea.IconStyle.Progress\n                                : invoice.status == Invoice.Paid\n                                    ? InfoTextArea.IconStyle.Done\n                                    : invoice.status == Invoice.Unpaid && invoice.expiration > 0\n                                        ? invoice.canPay\n                                            ? InfoTextArea.IconStyle.Pending\n                                            : InfoTextArea.IconStyle.Error\n                                        : InfoTextArea.IconStyle.Info\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    visible: invoice.invoiceType == Invoice.OnchainInvoice\n                    text: qsTr('Address')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    visible: invoice.invoiceType == Invoice.OnchainInvoice\n                    leftPadding: constants.paddingMedium\n\n                    RowLayout {\n                        width: parent.width\n                        Label {\n                            text: invoice.address\n                            font.pixelSize: constants.fontSizeLarge\n                            font.family: FixedFont\n                            Layout.fillWidth: true\n                            wrapMode: Text.Wrap\n                        }\n                        ToolButton {\n                            icon.source: '../../icons/share.png'\n                            icon.color: 'transparent'\n                            onClicked: {\n                                var dialog = app.genericShareDialog.createObject(app, {\n                                    title: qsTr('Address'),\n                                    text: invoice.address\n                                })\n                                dialog.open()\n                            }\n                        }\n                    }\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    text: qsTr('Description')\n                    visible: invoice.message\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n\n                    visible: invoice.message\n                    leftPadding: constants.paddingMedium\n\n                    Label {\n                        text: invoice.message\n                        width: parent.width\n                        font.pixelSize: constants.fontSizeXLarge\n                        wrapMode: Text.Wrap\n                        elide: Text.ElideRight\n                    }\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    text: qsTr('Amount to send')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    id: amountContainer\n\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    Layout.alignment: Qt.AlignHCenter\n\n                    leftPadding: constants.paddingXLarge\n                    rightPadding: constants.paddingXLarge\n\n                    property bool editmode: false\n\n                    RowLayout {\n                        id: amountLayout\n                        width: parent.width\n\n                        GridLayout {\n                            visible: !amountContainer.editmode\n                            columns: 2\n\n                            Label {\n                                Layout.columnSpan: 2\n                                Layout.fillWidth: true\n                                visible: _invoice_amount.isMax\n                                font.pixelSize: constants.fontSizeXLarge\n                                font.bold: true\n                                text: qsTr('All on-chain funds')\n                            }\n\n                            Label {\n                                Layout.columnSpan: 2\n                                Layout.fillWidth: true\n                                visible: _invoice_amount.isEmpty\n                                font.pixelSize: constants.fontSizeXLarge\n                                color: constants.mutedForeground\n                                text: qsTr('not specified')\n                            }\n\n                            Label {\n                                Layout.alignment: Qt.AlignRight\n                                visible: !_invoice_amount.isMax && !_invoice_amount.isEmpty\n                                font.pixelSize: constants.fontSizeXLarge\n                                font.family: FixedFont\n                                font.bold: true\n                                text: invoice.invoiceType == Invoice.LightningInvoice\n                                    ? Config.formatMilliSats(invoice.amount, false)\n                                    : Config.formatSats(invoice.amount, false)\n                            }\n\n                            Label {\n                                Layout.fillWidth: true\n                                visible: !_invoice_amount.isMax && !_invoice_amount.isEmpty\n                                text: Config.baseUnit\n                                color: Material.accentColor\n                                font.pixelSize: constants.fontSizeXLarge\n                            }\n\n                            Label {\n                                id: fiatValue\n                                Layout.alignment: Qt.AlignRight\n                                visible: Daemon.fx.enabled && !_invoice_amount.isMax && !_invoice_amount.isEmpty\n                                text: Daemon.fx.fiatValue(invoice.amount, false)\n                                font.pixelSize: constants.fontSizeMedium\n                                color: constants.mutedForeground\n                            }\n\n                            Label {\n                                Layout.fillWidth: true\n                                visible: Daemon.fx.enabled && !_invoice_amount.isMax && !_invoice_amount.isEmpty\n                                text: Daemon.fx.fiatCurrency\n                                font.pixelSize: constants.fontSizeMedium\n                                color: constants.mutedForeground\n                            }\n\n                        }\n\n                        GridLayout {\n                            Layout.fillWidth: true\n                            visible: amountContainer.editmode\n                            enabled: !(invoice.status == Invoice.Expired && _invoice_amount.isEmpty)\n\n                            columns: 3\n\n                            BtcField {\n                                id: amountBtc\n                                Layout.preferredWidth: amountFontMetrics.advanceWidth('0') * 14 + leftPadding + rightPadding\n                                fiatfield: amountFiat\n                                readOnly: amountMax.checked\n                                msatPrecision: invoice.invoiceType == Invoice.LightningInvoice\n                                color: readOnly\n                                    ? Material.accentColor\n                                    : Material.foreground\n                                onTextAsSatsChanged: {\n                                    if (!amountMax.checked)\n                                        invoice.amountOverride.copyFrom(textAsSats)\n                                }\n                                Connections {\n                                    target: invoice.amountOverride\n                                    function onSatsIntChanged() {\n                                        console.log('amountOverride satsIntChanged, sats=' + invoice.amountOverride.satsInt)\n                                        if (amountMax.checked)  // amountOverride updated by max amount estimate\n                                            amountBtc.text = Config.formatSatsForEditing(invoice.amountOverride.satsInt)\n                                    }\n                                }\n                            }\n\n                            Label {\n                                Layout.fillWidth: amountMax.visible ? false : true\n                                Layout.columnSpan: amountMax.visible ? 1 : 2\n\n                                text: Config.baseUnit\n                                color: Material.accentColor\n                            }\n\n                            Switch {\n                                id: amountMax\n                                Layout.fillWidth: true\n\n                                text: qsTr('Max')\n                                visible: _canMax\n                                checked: false\n                                onCheckedChanged: {\n                                    if (activeFocus) {\n                                        invoice.amountOverride.isMax = checked\n                                        if (checked) {\n                                            maxAmountMessage.text = ''\n                                            invoice.updateMaxAmount()\n                                        }\n                                    }\n                                }\n                            }\n\n                            FiatField {\n                                id: amountFiat\n                                Layout.preferredWidth: amountFontMetrics.advanceWidth('0') * 14 + leftPadding + rightPadding\n                                btcfield: amountBtc\n                                visible: Daemon.fx.enabled\n                                readOnly: amountMax.checked\n                                color: readOnly\n                                    ? Material.accentColor\n                                    : Material.foreground\n                            }\n\n                            Label {\n                                Layout.columnSpan: 2\n                                visible: Daemon.fx.enabled\n                                text: Daemon.fx.fiatCurrency\n                                color: Material.accentColor\n                            }\n\n                            InfoTextArea {\n                                Layout.topMargin: constants.paddingMedium\n                                Layout.fillWidth: true\n                                Layout.columnSpan: 3\n                                id: maxAmountMessage\n                                visible: amountMax.checked && text\n                                compact: true\n                                Connections {\n                                    target: invoice\n                                    function onMaxAmountMessage(message) {\n                                        maxAmountMessage.text = message\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                }\n\n                Heading {\n                    Layout.columnSpan: 2\n                    visible: invoice.invoiceType == Invoice.LightningInvoice\n                    text: qsTr('Technical properties')\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    visible: invoice.invoiceType == Invoice.LightningInvoice\n                    text: qsTr('Remote Pubkey')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n\n                    visible: invoice.invoiceType == Invoice.LightningInvoice\n                    leftPadding: constants.paddingMedium\n\n                    RowLayout {\n                        width: parent.width\n                        Label {\n                            id: pubkeyLabel\n                            Layout.fillWidth: true\n                            text: 'pubkey' in invoice.lnprops ? invoice.lnprops.pubkey : ''\n                            font.family: FixedFont\n                            wrapMode: Text.Wrap\n                        }\n                        ToolButton {\n                            icon.source: '../../icons/share.png'\n                            icon.color: 'transparent'\n                            enabled: pubkeyLabel.text\n                            onClicked: {\n                                var dialog = app.genericShareDialog.createObject(app,\n                                    { title: qsTr('Node public key'), text: invoice.lnprops.pubkey }\n                                )\n                                dialog.open()\n                            }\n                        }\n                    }\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    visible: invoice.invoiceType == Invoice.LightningInvoice\n                    text: qsTr('Payment hash')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n\n                    visible: invoice.invoiceType == Invoice.LightningInvoice\n                    leftPadding: constants.paddingMedium\n\n                    RowLayout {\n                        width: parent.width\n                        Label {\n                            id: paymenthashLabel\n                            Layout.fillWidth: true\n                            text: 'payment_hash' in invoice.lnprops ? invoice.lnprops.payment_hash : ''\n                            font.family: FixedFont\n                            wrapMode: Text.Wrap\n                        }\n                        ToolButton {\n                            icon.source: '../../icons/share.png'\n                            icon.color: 'transparent'\n                            enabled: paymenthashLabel.text\n                            onClicked: {\n                                var dialog = app.genericShareDialog.createObject(app, {\n                                    title: qsTr('Payment hash'),\n                                    text: invoice.lnprops.payment_hash\n                                })\n                                dialog.open()\n                            }\n                        }\n                    }\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    visible: 'r' in invoice.lnprops && invoice.lnprops.r.length\n                    text: qsTr('Routing hints')\n                    color: Material.accentColor\n                }\n\n                Repeater {\n                    visible: 'r' in invoice.lnprops && invoice.lnprops.r.length\n                    model: invoice.lnprops.r\n\n                    TextHighlightPane {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        RowLayout {\n                            width: parent.width\n\n                            Label {\n                                text: modelData.scid\n                            }\n                            Label {\n                                Layout.fillWidth: true\n                                text: modelData.node\n                                wrapMode: Text.Wrap\n                            }\n                        }\n                    }\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    visible: invoice.invoiceType == Invoice.LightningInvoice && invoice.address\n                    text: qsTr('Fallback address')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    visible: invoice.invoiceType == Invoice.LightningInvoice && invoice.address\n                    leftPadding: constants.paddingMedium\n\n                    RowLayout {\n                        width: parent.width\n                        Label {\n                            text: invoice.address\n                            font.family: FixedFont\n                            Layout.fillWidth: true\n                            wrapMode: Text.Wrap\n                        }\n                        ToolButton {\n                            icon.source: '../../icons/share.png'\n                            icon.color: 'transparent'\n                            onClicked: {\n                                var dialog = app.genericShareDialog.createObject(app, {\n                                    title: qsTr('Address'),\n                                    text: invoice.address\n                                })\n                                dialog.open()\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Save')\n                icon.source: '../../icons/save.png'\n                enabled: !invoice.isSaved && invoice.canSave\n                onClicked: {\n                    if (invoice.amount.isEmpty) {\n                        invoice.amountOverride = Config.unitsToSats(amountBtc.text)\n                        if (amountMax.checked)\n                            invoice.amountOverride.isMax = true\n                    }\n                    if (invoice.saveInvoice()) {\n                        app.stack.push(Qt.resolvedUrl('Invoices.qml'))\n                        dialog.close()\n                    }\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Pay...')\n                icon.source: '../../icons/confirmed.png'\n                enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay\n                onClicked: {\n                    if (invoice.amount.isEmpty) {\n                        invoice.amountOverride = Config.unitsToSats(amountBtc.text)\n                        if (amountMax.checked)\n                            invoice.amountOverride.isMax = true\n                    }\n                    doPay() // only signal here\n                }\n            }\n        }\n\n    }\n\n    Component.onCompleted: {\n        if (invoice.amount.isEmpty && !invoice.status == Invoice.Expired) {\n            amountContainer.editmode = true\n        } else if (invoice.amount.isMax) {\n            amountMax.checked = true\n        }\n        if (payImmediately) {\n            if (invoice.canPay) {\n                doPay()\n            }\n        }\n    }\n\n    Connections {\n        target: Daemon.currentWallet\n        function onBroadcastSucceeded(txid) {\n            if (dialog.broadcastTxid == txid) {\n                // our txid was broadcast successfully, close invoicedialog and show success popup\n                dialog.close()\n                var successdialog = app.messageDialog.createObject(mainView, {\n                    text: qsTr('Payment sent.')\n                })\n                successdialog.open()\n            }\n        }\n    }\n\n    FontMetrics {\n        id: amountFontMetrics\n        font: amountBtc.font\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/Invoices.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\nimport QtQml.Models\nimport QtQml\n\nimport \"controls\"\n\nPane {\n    id: root\n\n    padding: 0\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        ColumnLayout {\n            Layout.fillWidth: true\n            Layout.margins: constants.paddingLarge\n\n            InfoTextArea {\n                Layout.fillWidth: true\n                Layout.bottomMargin: constants.paddingLarge\n                visible: !Config.userKnowsPressAndHold\n                text: qsTr('To access this list from the main screen, press and hold the Send button')\n            }\n\n            Heading {\n                text: qsTr('Saved Invoices')\n            }\n\n            Frame {\n                background: PaneInsetBackground {}\n\n                verticalPadding: 0\n                horizontalPadding: 0\n                Layout.fillHeight: true\n                Layout.fillWidth: true\n\n                ElListView {\n                    id: listview\n                    anchors.fill: parent\n                    clip: true\n                    currentIndex: -1\n\n                    model: DelegateModel {\n                        id: delegateModel\n                        model: Daemon.currentWallet.invoiceModel\n                        delegate: InvoiceDelegate {\n                            onClicked: {\n                                var dialog = app.stack.getRoot().openInvoice(model.key)\n                                dialog.invoiceAmountChanged.connect(function () {\n                                    Daemon.currentWallet.invoiceModel.initModel()\n                                })\n                                listview.currentIndex = -1\n                            }\n                            onPressAndHold: listview.currentIndex = index\n                        }\n                    }\n\n                    add: Transition {\n                        NumberAnimation { properties: 'scale'; from: 0.75; to: 1; duration: 500 }\n                        NumberAnimation { properties: 'opacity'; from: 0; to: 1; duration: 500 }\n                    }\n                    addDisplaced: Transition {\n                        SpringAnimation { properties: 'y'; duration: 200; spring: 5; damping: 0.5; mass: 2 }\n                    }\n\n                    remove: Transition {\n                        NumberAnimation { properties: 'scale'; to: 0.75; duration: 300 }\n                        NumberAnimation { properties: 'opacity'; to: 0; duration: 300 }\n                    }\n                    removeDisplaced: Transition {\n                        SequentialAnimation {\n                            PauseAnimation { duration: 200 }\n                            SpringAnimation { properties: 'y'; duration: 100; spring: 5; damping: 0.5; mass: 2 }\n                        }\n                    }\n\n                    ScrollIndicator.vertical: ScrollIndicator { }\n                }\n            }\n\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Delete')\n                icon.source: '../../icons/delete.png'\n                visible: listview.currentIndex >= 0\n                onClicked: {\n                    Daemon.currentWallet.deleteInvoice(listview.currentItem.getKey())\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('View')\n                icon.source: '../../icons/tab_receive.png'\n                visible: listview.currentIndex >= 0\n                onClicked: {\n                    var dialog = app.stack.getRoot().openInvoice(listview.currentItem.getKey())\n                    dialog.invoiceAmountChanged.connect(function () {\n                        Daemon.currentWallet.invoiceModel.initModel()\n                    })\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/LightningPaymentDetails.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: root\n    width: parent.width\n    height: parent.height\n\n    property string key\n    property alias label: lnpaymentdetails.label\n\n    signal detailsChanged\n\n    Flickable {\n        anchors.fill: parent\n        contentHeight: rootLayout.height\n        clip: true\n        interactive: height < contentHeight\n\n        GridLayout {\n            id: rootLayout\n            width: parent.width\n            columns: 2\n\n            Heading {\n                Layout.columnSpan: 2\n                text: qsTr('Lightning payment details')\n            }\n\n            InfoTextArea {\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n                Layout.bottomMargin: constants.paddingLarge\n                visible: text\n                text:  lnpaymentdetails.status ? qsTr('Paid') : ''\n                iconStyle: InfoTextArea.IconStyle.Done\n            }\n\n            Label {\n                text: qsTr('Date')\n                color: Material.accentColor\n            }\n\n            Label {\n                text: lnpaymentdetails.date\n            }\n\n            Label {\n                text: lnpaymentdetails.amount.msatsInt > 0\n                        ? qsTr('Amount received')\n                        : qsTr('Amount sent')\n                color: Material.accentColor\n            }\n\n            FormattedAmount {\n                amount: lnpaymentdetails.amount\n                timestamp: lnpaymentdetails.timestamp\n            }\n\n            Label {\n                visible: lnpaymentdetails.amount.msatsInt < 0\n                text: qsTr('Transaction fee')\n                color: Material.accentColor\n            }\n\n            FormattedAmount {\n                visible: lnpaymentdetails.amount.msatsInt < 0\n                amount: lnpaymentdetails.fee\n                timestamp: lnpaymentdetails.timestamp\n            }\n\n            Label {\n                Layout.columnSpan: 2\n                Layout.topMargin: constants.paddingSmall\n                text: qsTr('Label')\n                color: Material.accentColor\n            }\n\n            TextHighlightPane {\n                id: labelContent\n\n                property bool editmode: false\n\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n\n                RowLayout {\n                    width: parent.width\n                    Label {\n                        visible: !labelContent.editmode\n                        text: lnpaymentdetails.label\n                        wrapMode: Text.Wrap\n                        Layout.fillWidth: true\n                        font.pixelSize: constants.fontSizeLarge\n                    }\n                    ToolButton {\n                        visible: !labelContent.editmode\n                        icon.source: '../../icons/pen.png'\n                        icon.color: 'transparent'\n                        onClicked: {\n                            labelEdit.text = lnpaymentdetails.label\n                            labelContent.editmode = true\n                            labelEdit.focus = true\n                        }\n                    }\n                    TextField {\n                        id: labelEdit\n                        visible: labelContent.editmode\n                        text: lnpaymentdetails.label\n                        font.pixelSize: constants.fontSizeLarge\n                        Layout.fillWidth: true\n                    }\n                    ToolButton {\n                        visible: labelContent.editmode\n                        icon.source: '../../icons/confirmed.png'\n                        icon.color: 'transparent'\n                        onClicked: {\n                            labelContent.editmode = false\n                            lnpaymentdetails.setLabel(labelEdit.text)\n                        }\n                    }\n                    ToolButton {\n                        visible: labelContent.editmode\n                        icon.source: '../../icons/closebutton.png'\n                        icon.color: 'transparent'\n                        onClicked: labelContent.editmode = false\n                    }\n                }\n            }\n\n            Heading {\n                Layout.columnSpan: 2\n                text: qsTr('Technical properties')\n            }\n\n            Label {\n                Layout.columnSpan: 2\n                Layout.topMargin: constants.paddingSmall\n                text: qsTr('Payment hash')\n                color: Material.accentColor\n                visible: lnpaymentdetails.paymentHash\n            }\n\n            TextHighlightPane {\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n                visible: lnpaymentdetails.paymentHash\n\n                RowLayout {\n                    width: parent.width\n                    Label {\n                        text: lnpaymentdetails.paymentHash\n                        font.pixelSize: constants.fontSizeLarge\n                        font.family: FixedFont\n                        Layout.fillWidth: true\n                        wrapMode: Text.Wrap\n                    }\n                    ToolButton {\n                        icon.source: '../../icons/share.png'\n                        icon.color: 'transparent'\n                        onClicked: {\n                            var dialog = app.genericShareDialog.createObject(root,\n                                { title: qsTr('Payment hash'), text: lnpaymentdetails.paymentHash }\n                            )\n                            dialog.open()\n                        }\n                    }\n                }\n            }\n\n            Label {\n                Layout.columnSpan: 2\n                Layout.topMargin: constants.paddingSmall\n                text: qsTr('Preimage')\n                color: Material.accentColor\n                visible: lnpaymentdetails.preimage\n            }\n\n            TextHighlightPane {\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n                visible: lnpaymentdetails.preimage\n\n                RowLayout {\n                    width: parent.width\n                    Label {\n                        text: lnpaymentdetails.preimage\n                        font.pixelSize: constants.fontSizeLarge\n                        font.family: FixedFont\n                        Layout.fillWidth: true\n                        wrapMode: Text.Wrap\n                    }\n                    ToolButton {\n                        icon.source: '../../icons/share.png'\n                        icon.color: 'transparent'\n                        onClicked: {\n                            var dialog = app.genericShareDialog.createObject(root,\n                                { title: qsTr('Preimage'), text: lnpaymentdetails.preimage }\n                            )\n                            dialog.open()\n                        }\n                    }\n                }\n            }\n\n        }\n    }\n\n    LnPaymentDetails {\n        id: lnpaymentdetails\n        wallet: Daemon.currentWallet\n        key: root.key\n        onLabelChanged: root.detailsChanged()\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/LnurlPayRequestDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    title: qsTr('LNURL Payment request')\n    iconSource: '../../../icons/link.png'\n\n    property InvoiceParser invoiceParser\n\n    padding: 0\n    needsSystemBarPadding: false\n\n    property bool commentValid: comment.text.length <= invoiceParser.lnurlData['comment_allowed']\n    property bool amountValid: amountBtc.textAsSats.satsInt >= parseInt(invoiceParser.lnurlData['min_sendable_sat'])\n        && amountBtc.textAsSats.satsInt <= parseInt(invoiceParser.lnurlData['max_sendable_sat'])\n    property bool valid: commentValid && amountValid\n\n    ColumnLayout {\n        width: parent.width\n\n        spacing: 0\n\n        GridLayout {\n            id: rootLayout\n            columns: 2\n\n            Layout.fillWidth: true\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n            Layout.bottomMargin: constants.paddingLarge\n\n            InfoTextArea {\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n                compact: true\n                visible: invoiceParser.lnurlData['min_sendable_sat'] != invoiceParser.lnurlData['max_sendable_sat']\n                text: qsTr('Amount must be between %1 and %2 %3').arg(Config.formatSats(invoiceParser.lnurlData['min_sendable_sat'])).arg(Config.formatSats(invoiceParser.lnurlData['max_sendable_sat'])).arg(Config.baseUnit)\n            }\n\n            Label {\n                text: qsTr('Provider')\n                color: Material.accentColor\n            }\n            Label {\n                Layout.fillWidth: true\n                text: invoiceParser.lnurlData['domain']\n            }\n            Label {\n                text: qsTr('Description')\n                color: Material.accentColor\n            }\n            Label {\n                Layout.fillWidth: true\n                text: invoiceParser.lnurlData['metadata_plaintext']\n                wrapMode: Text.Wrap\n            }\n\n            Label {\n                text: qsTr('Amount')\n                color: Material.accentColor\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                BtcField {\n                    id: amountBtc\n                    Layout.preferredWidth: rootLayout.width /3\n                    text: Config.formatSatsForEditing(invoiceParser.lnurlData['min_sendable_sat'])\n                    enabled: invoiceParser.lnurlData['min_sendable_sat'] != invoiceParser.lnurlData['max_sendable_sat']\n                    color: Material.foreground // override gray-out on disabled\n                    fiatfield: amountFiat\n                    onTextAsSatsChanged: {\n                        invoiceParser.amountOverride = textAsSats\n                    }\n                }\n                Label {\n                    text: Config.baseUnit\n                    color: Material.accentColor\n                }\n            }\n\n            Item { visible: Daemon.fx.enabled; Layout.preferredWidth: 1; Layout.preferredHeight: 1 }\n\n            RowLayout {\n                visible: Daemon.fx.enabled\n                FiatField {\n                    id: amountFiat\n                    Layout.preferredWidth: rootLayout.width / 3\n                    btcfield: amountBtc\n                }\n                Label {\n                    text: Daemon.fx.fiatCurrency\n                    color: Material.accentColor\n                }\n            }\n\n            Label {\n                Layout.columnSpan: 2\n                visible: invoiceParser.lnurlData['comment_allowed'] > 0\n                text: qsTr('Message')\n                color: Material.accentColor\n            }\n            ElTextArea {\n                id: comment\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n                Layout.minimumHeight: 160\n                visible: invoiceParser.lnurlData['comment_allowed'] > 0\n                wrapMode: TextEdit.Wrap\n                placeholderText: qsTr('Enter an (optional) message for the receiver')\n                color: text.length > invoiceParser.lnurlData['comment_allowed'] ? constants.colorError : Material.foreground\n            }\n\n            Label {\n                Layout.columnSpan: 2\n                Layout.leftMargin: constants.paddingLarge\n                visible: invoiceParser.lnurlData['comment_allowed'] > 0\n                text: qsTr('%1 characters remaining').arg(Math.max(0, (invoiceParser.lnurlData['comment_allowed'] - comment.text.length) ))\n                color: constants.mutedForeground\n                font.pixelSize: constants.fontSizeSmall\n            }\n        }\n\n        FlatButton {\n            Layout.topMargin: constants.paddingLarge\n            Layout.fillWidth: true\n            text: qsTr('Pay...')\n            icon.source: '../../icons/confirmed.png'\n            enabled: valid\n            onClicked: {\n                invoiceParser.lnurlGetInvoice(comment.text)\n                dialog.close()\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/LnurlWithdrawRequestDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    title: qsTr('LNURL Withdraw request')\n    iconSource: '../../../icons/link.png'\n\n    property Wallet wallet: Daemon.currentWallet\n    property RequestDetails requestDetails\n\n    padding: 0\n    needsSystemBarPadding: false\n\n    property int walletCanReceive: 0\n    property int providerMinWithdrawable: parseInt(requestDetails.lnurlData['min_withdrawable_sat'])\n    property int providerMaxWithdrawable: parseInt(requestDetails.lnurlData['max_withdrawable_sat'])\n    property int effectiveMinWithdrawable: Math.max(providerMinWithdrawable, 1)\n    property int effectiveMaxWithdrawable: Math.min(providerMaxWithdrawable, walletCanReceive)\n    property bool insufficientLiquidity: effectiveMinWithdrawable > walletCanReceive\n    property bool liquidityWarning: providerMaxWithdrawable > walletCanReceive\n\n    property bool amountValid: !dialog.insufficientLiquidity &&\n        amountBtc.textAsSats.satsInt >= dialog.effectiveMinWithdrawable &&\n        amountBtc.textAsSats.satsInt <= dialog.effectiveMaxWithdrawable\n    property bool valid: amountValid\n\n    Component.onCompleted: {\n        dialog.walletCanReceive = wallet.lightningCanReceive.satsInt\n    }\n\n    Connections {\n        // assign walletCanReceive directly to prevent a binding loop\n        target: wallet\n        function onLightningCanReceiveChanged() {\n            if (!requestDetails.busy) {\n                // don't assign while busy to prevent the view from changing while receiving\n                // the incoming payment\n                dialog.walletCanReceive = wallet.lightningCanReceive.satsInt\n            }\n        }\n    }\n\n    ColumnLayout {\n        width: parent.width\n\n        GridLayout {\n            id: rootLayout\n            columns: 2\n\n            Layout.fillWidth: true\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n            Layout.bottomMargin: constants.paddingLarge\n\n            InfoTextArea {\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n                compact: true\n                visible: dialog.insufficientLiquidity\n                text: qsTr('Too little incoming liquidity to satisfy this withdrawal request.')\n                          + '\\n\\n'\n                          + qsTr('Can receive: %1')\n                            .arg(Config.formatSats(dialog.walletCanReceive) + ' ' + Config.baseUnit)\n                          + '\\n'\n                          + qsTr('Minimum withdrawal amount: %1')\n                            .arg(Config.formatSats(dialog.providerMinWithdrawable) + ' ' + Config.baseUnit)\n                          + '\\n\\n'\n                          + qsTr('Do a submarine swap in the \\'Channels\\' tab to get more incoming liquidity.')\n                iconStyle: InfoTextArea.IconStyle.Error\n            }\n\n            InfoTextArea {\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n                compact: true\n                visible: !dialog.insufficientLiquidity && dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable\n                text: qsTr('Amount must be between %1 and %2 %3')\n                .arg(Config.formatSats(dialog.effectiveMinWithdrawable))\n                .arg(Config.formatSats(dialog.effectiveMaxWithdrawable))\n                .arg(Config.baseUnit)\n            }\n\n            InfoTextArea {\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n                compact: true\n                visible: dialog.liquidityWarning && !dialog.insufficientLiquidity\n                text: qsTr('The maximum withdrawable amount (%1) is larger than what your channels can receive (%2).')\n                            .arg(Config.formatSats(dialog.providerMaxWithdrawable) + ' ' + Config.baseUnit)\n                            .arg(Config.formatSats(dialog.walletCanReceive) + ' ' + Config.baseUnit)\n                        + ' '\n                        + qsTr('You may need to do a submarine swap to increase your incoming liquidity.')\n                iconStyle: InfoTextArea.IconStyle.Warn\n            }\n\n            Label {\n                text: qsTr('Provider')\n                color: Material.accentColor\n            }\n            Label {\n                Layout.fillWidth: true\n                text: requestDetails.lnurlData['domain']\n            }\n            Label {\n                text: qsTr('Description')\n                color: Material.accentColor\n                visible: requestDetails.lnurlData['default_description']\n            }\n            Label {\n                Layout.fillWidth: true\n                text: requestDetails.lnurlData['default_description']\n                visible: requestDetails.lnurlData['default_description']\n                wrapMode: Text.Wrap\n            }\n\n            Label {\n                text: qsTr('Amount')\n                color: Material.accentColor\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                BtcField {\n                    id: amountBtc\n                    Layout.preferredWidth: rootLayout.width / 3\n                    text: Config.formatSatsForEditing(dialog.effectiveMaxWithdrawable)\n                    enabled: !dialog.insufficientLiquidity && (dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable)\n                    color: Material.foreground // override gray-out on disabled\n                    fiatfield: amountFiat\n                }\n                Label {\n                    text: Config.baseUnit\n                    color: Material.accentColor\n                }\n            }\n\n            Item { visible: Daemon.fx.enabled; Layout.preferredWidth: 1; Layout.preferredHeight: 1 }\n\n            RowLayout {\n                visible: Daemon.fx.enabled\n                FiatField {\n                    id: amountFiat\n                    Layout.preferredWidth: rootLayout.width / 3\n                    btcfield: amountBtc\n                    enabled: !dialog.insufficientLiquidity && (dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable)\n                    color: Material.foreground\n                }\n                Label {\n                    text: Daemon.fx.fiatCurrency\n                    color: Material.accentColor\n                }\n            }\n        }\n\n        FlatButton {\n            Layout.topMargin: constants.paddingLarge\n            Layout.fillWidth: true\n            text: qsTr('Withdraw...')\n            icon.source: '../../icons/confirmed.png'\n            enabled: valid && !requestDetails.busy\n            onClicked: {\n                var satsAmount = amountBtc.textAsSats.satsInt;\n                requestDetails.lnurlRequestWithdrawal(satsAmount);\n                dialog.close();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/LoadingWalletDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    title: qsTr('Loading Wallet')\n    iconSource: Qt.resolvedUrl('../../icons/wallet.png')\n\n    resizeWithKeyboard: false\n\n    x: Math.floor((parent.width - implicitWidth) / 2)\n    y: Math.floor((parent.height - implicitHeight) / 2)\n    // anchors.centerIn: parent // this strangely pixelates the spinner\n    needsSystemBarPadding: false\n\n    function open() {\n        showTimer.start()\n    }\n\n    ColumnLayout {\n        width: parent.width\n\n        BusyIndicator {\n            Layout.alignment: Qt.AlignHCenter\n\n            running: Daemon.loading\n        }\n\n        Item {\n            Layout.preferredHeight: 20\n        }\n    }\n\n    Connections {\n        target: Daemon\n        function onLoadingChanged() {\n            console.log('daemon loading ' + Daemon.loading)\n            if (!Daemon.loading) {\n                showTimer.stop()\n                if (dialog.visible) {\n                    dialog.close()\n                } else {\n                    // if the dialog wasn't visible its onClosed callbacks don't get called, so it\n                    // needs to be destroyed manually\n                    Qt.callLater(function() { dialog.destroy() })\n                }\n            }\n        }\n    }\n\n    Timer {\n        id: showTimer\n        interval: 250\n        repeat: false\n        onTriggered: dialog.visible = true\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/MessageDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n    title: yesno ? qsTr(\"Question\") : qsTr(\"Message\")\n    iconSource: yesno\n        ? Qt.resolvedUrl('../../icons/question.png')\n        : Qt.resolvedUrl('../../icons/info.png')\n\n    property bool yesno: false\n    property alias text: message.text\n    property bool richText: false\n\n    z: 1 // raise z so it also covers dialogs using overlay as parent\n\n    anchors.centerIn: parent\n\n    padding: 0\n    needsSystemBarPadding: false\n\n    width: rootLayout.width\n\n    ColumnLayout {\n        id: rootLayout\n        width: dialog.parent.width * 2/3\n\n        ColumnLayout {\n            visible: text\n            Layout.margins: constants.paddingMedium\n            Layout.fillWidth: true\n\n            TextArea {\n                id: message\n                Layout.fillWidth: true\n                readOnly: true\n                wrapMode: TextInput.WordWrap\n                textFormat: richText ? TextEdit.RichText : TextEdit.PlainText\n                background: Rectangle {\n                    color: 'transparent'\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                textUnderIcon: false\n                text: qsTr('Ok')\n                icon.source: Qt.resolvedUrl('../../icons/confirmed.png')\n                visible: !yesno\n                onClicked: doAccept()\n            }\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                textUnderIcon: false\n                text: qsTr('No')\n                icon.source: Qt.resolvedUrl('../../icons/closebutton.png')\n                visible: yesno\n                onClicked: doReject()\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                textUnderIcon: false\n                text: qsTr('Yes')\n                icon.source: Qt.resolvedUrl('../../icons/confirmed.png')\n                visible: yesno\n                onClicked: doAccept()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/NetworkOverview.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport \"controls\"\n\nPane {\n    id: root\n    objectName: 'NetworkOverview'\n\n    padding: 0\n\n    property string title: qsTr(\"Network\")\n\n    function _getFeerateColor(sat_per_vbyte) {\n        // To display a nice quickly graspable view of the mempool fee histogram, we map\n        // feerates to fixed colors. E.g. when the histogram is full of red, the user can\n        // instantly see fees are high.\n        // In the 1-600 s/b range, play with hue:\n        var hsv_hue = (2/3-(2/3*(\n            Math.log(\n                Math.min(600, Math.max(sat_per_vbyte, 1))\n            )\n            /Math.log(600))\n        ))\n        // In the 0-1 s/b range, play with value:\n        var hsv_value = Math.min(sat_per_vbyte, 1)\n        return Qt.hsva(hsv_hue, 0.8, hsv_value, 1)\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            Layout.topMargin: constants.paddingLarge\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n\n            contentHeight: contentLayout.height\n            clip: true\n            interactive: height < contentHeight\n\n            GridLayout {\n                id: contentLayout\n                width: parent.width\n                columns: 2\n                Heading {\n                    Layout.columnSpan: 2\n                    text: qsTr('On-chain')\n                }\n                Label {\n                    text: qsTr('Network') + ':'\n                    color: Material.accentColor\n                }\n                Label {\n                    text: Network.networkName\n                }\n                Label {\n                    text: qsTr('Status') + ':'\n                    color: Material.accentColor\n                }\n                Label {\n                    text: Network.status\n                }\n                Label {\n                    text: qsTr('Server') + ':'\n                    color: Material.accentColor\n                }\n                Label {\n                    text: Network.serverWithStatus\n                    wrapMode: Text.WrapAnywhere\n                    Layout.fillWidth: true\n                }\n                Label {\n                    text: qsTr('Local Height:');\n                    color: Material.accentColor\n                }\n                Label {\n                    text: Network.height\n                }\n                Label {\n                    text: qsTr('Server Height:');\n                    color: Material.accentColor\n                    visible: Network.serverHeight != 0 && Network.serverHeight != Network.height\n                }\n                Label {\n                    text: Network.serverHeight + \" \" + (Network.serverHeight < Network.height ? \"(lagging)\" : \"(syncing...)\")\n                    visible: Network.serverHeight != 0 && Network.serverHeight != Network.height\n                }\n                Label {\n                    text: qsTr('Chain tips:');\n                    color: Material.accentColor\n                    visible: opacity > 0\n                    opacity: Network.chaintips > 1 ? 1 : 0\n                    Behavior on opacity { NumberAnimation { duration: 1000 } }\n                }\n                RowLayout {\n                    visible: opacity > 0\n                    opacity: Network.chaintips > 1 ? 1 : 0\n                    Behavior on opacity { NumberAnimation { duration: 1000 } }\n                    OnchainNetworkStatusIndicator {\n                        sourceSize.width: constants.iconSizeSmall\n                        sourceSize.height: constants.iconSizeSmall\n                    }\n                    Label {\n                        text: Network.chaintips\n                    }\n                }\n                Heading {\n                    Layout.columnSpan: 2\n                    text: qsTr('Mempool fees')\n                }\n                Item {\n                    id: histogramRoot\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    implicitHeight: histogramLayout.height\n\n                    ColumnLayout {\n                        id: histogramLayout\n                        width: parent.width\n                        spacing: 0\n                        RowLayout {\n                            Layout.fillWidth: true\n                            height: 28\n                            spacing: 0\n                            Repeater {\n                                model: Network.feeHistogram.histogram\n                                Rectangle {\n                                    Layout.preferredWidth: 300 * (modelData[1] / Network.feeHistogram.total)\n                                    Layout.fillWidth: true\n                                    height: parent.height\n                                    color: _getFeerateColor(modelData[0])\n                                    ToolTip.text: (qsTr(\"%1 around depth %2\")\n                                        .arg(modelData[0] + \" \" + UI_UNIT_NAME.FEERATE_SAT_PER_VB)\n                                        .arg((modelData[2]/1000000).toFixed(2) + \" \" + UI_UNIT_NAME.MEMPOOL_MB)\n                                    )\n                                    ToolTip.visible: ma.containsMouse\n                                    MouseArea {\n                                        id: ma\n                                        anchors.fill: parent\n                                        hoverEnabled: true\n                                    }\n                                }\n                            }\n                        }\n                        RowLayout {\n                            Layout.fillWidth: true\n                            height: 3\n                            spacing: 0\n\n                            Repeater {\n                                model: Network.feeHistogram.total / 1000000\n                                RowLayout {\n                                    height: parent.height\n                                    spacing: 0\n                                    Rectangle {\n                                        Layout.preferredWidth: 1\n                                        Layout.fillWidth: false\n                                        height: parent.height\n                                        width: 1\n                                        color: 'white'\n                                    }\n                                    Item {\n                                        Layout.fillWidth: true\n                                        Layout.preferredHeight: parent.height\n                                    }\n                                }\n                            }\n                            Rectangle {\n                                Layout.preferredWidth: 1\n                                Layout.fillWidth: false\n                                height: parent.height\n                                width: 1\n                                color: 'white'\n                            }\n                        }\n                        RowLayout {\n                            Layout.fillWidth: true\n                            Label {\n                                text: '<-- ' + Math.ceil(Network.feeHistogram.max_fee) + \" \" + UI_UNIT_NAME.FEERATE_SAT_PER_VB\n                                font.pixelSize: constants.fontSizeXSmall\n                                color: Material.accentColor\n                            }\n                            Label {\n                                Layout.fillWidth: true\n                                horizontalAlignment: Text.AlignRight\n                                text: Math.floor(Network.feeHistogram.min_fee) + \" \" + UI_UNIT_NAME.FEERATE_SAT_PER_VB + ' -->'\n                                font.pixelSize: constants.fontSizeXSmall\n                                color: Material.accentColor\n                            }\n                        }\n                    }\n                }\n\n                Heading {\n                    Layout.columnSpan: 2\n                    text: qsTr('Lightning')\n                }\n\n                Label {\n                    text: (Config.useGossip ? qsTr('Gossip') : qsTr('Trampoline')) + ':'\n                    color: Material.accentColor\n                }\n                ColumnLayout {\n                    visible: Config.useGossip\n                    Label {\n                        text: qsTr('%1 peers').arg(Network.gossipInfo.peers)\n                    }\n                    Label {\n                        text: qsTr('%1 channels to fetch').arg(Network.gossipInfo.unknown_channels)\n                    }\n                    Label {\n                        text: qsTr('%1 nodes, %2 channels').arg(Network.gossipInfo.db_nodes).arg(Network.gossipInfo.db_channels)\n                    }\n                }\n                Label {\n                    text: qsTr('enabled');\n                    visible: !Config.useGossip\n                }\n\n                Label {\n                    visible: Daemon.currentWallet.isLightning\n                    text: qsTr('Channel peers:');\n                    color: Material.accentColor\n                }\n                Label {\n                    visible: Daemon.currentWallet.isLightning\n                    text: Daemon.currentWallet.lightningNumPeers\n                }\n\n                Heading {\n                    Layout.columnSpan: 2\n                    text: qsTr('Proxy')\n                }\n\n                Label {\n                    text: qsTr('Proxy') + ':'\n                    color: Material.accentColor\n                }\n                Label {\n                    text: Network.proxy.enabled ? qsTr('enabled') : qsTr('disabled')\n                }\n\n                Label {\n                    visible: Network.proxy.enabled\n                    text: qsTr('Proxy server:');\n                    color: Material.accentColor\n                }\n                Label {\n                    visible: Network.proxy.enabled\n                    text: Network.proxy.host ? Network.proxy.host + ':' + Network.proxy.port : ''\n                }\n\n                Label {\n                    visible: Network.proxy.enabled\n                    text: qsTr('Proxy type:');\n                    color: Material.accentColor\n                }\n                RowLayout {\n                    Image {\n                        visible: Network.isProxyTor\n                        Layout.preferredWidth: constants.iconSizeMedium\n                        Layout.preferredHeight: constants.iconSizeMedium\n                        source: '../../icons/tor_logo.png'\n                    }\n                    Label {\n                        visible: Network.proxy.enabled\n                        text: Network.isProxyTor ? 'TOR' : (Network.proxy.mode || '')\n                    }\n                }\n\n            }\n\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Server Settings');\n                icon.source: '../../icons/network.png'\n                onClicked: {\n                    var dialog = serverConfig.createObject(root)\n                    dialog.open()\n                }\n            }\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Proxy Settings');\n                icon.source: '../../icons/status_connected_proxy.png'\n                onClicked: {\n                    var dialog = proxyConfig.createObject(root)\n                    dialog.open()\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Nostr Settings');\n                icon.source: '../../icons/nostr.png'\n                onClicked: {\n                    var dialog = nostrConfig.createObject(root)\n                    dialog.open()\n                }\n            }\n        }\n    }\n\n    Component {\n        id: serverConfig\n        ServerConfigDialog {\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: proxyConfig\n        ProxyConfigDialog {\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: nostrConfig\n        NostrConfigDialog {\n            onClosed: destroy()\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/NewWalletWizard.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nimport \"wizard\"\n\nWizard {\n    id: walletwizard\n\n    wizardTitle: qsTr('New Wallet')\n\n    signal walletCreated\n\n    property string path\n\n    wiz: Daemon.newWalletWizard\n\n    Component.onCompleted: {\n        var view = wiz.startWizard()\n        _loadNextComponent(view)\n    }\n\n    onAccepted: {\n        console.log('Finished new wallet wizard')\n        wiz.createStorage(wizard_data, Daemon.singlePasswordEnabled, Daemon.singlePassword)\n    }\n\n    Connections {\n        target: wiz\n        function onCreateSuccess() {\n            walletwizard.path = wiz.path\n            walletwizard.walletCreated()\n        }\n        function onCreateError(error) {\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: error\n            })\n            dialog.open()\n        }\n    }\n}\n\n"
  },
  {
    "path": "electrum/gui/qml/components/NostrConfigDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: rootItem\n\n    title: qsTr('Nostr relays')\n    iconSource: Qt.resolvedUrl('../../icons/nostr.png')\n\n    width: parent.width\n    height: parent.height\n\n    padding: 0\n\n    property bool valid: true\n\n    function clean_array(text) {\n        var relays = []\n        const fragments = text.split(\"\\n\")\n        fragments.forEach(function(fragment) {\n            fragment = fragment.trim()\n            if (fragment != \"\" && !relays.includes(fragment))\n                relays.push(fragment)\n        })\n        return relays\n    }\n\n    function verify(text) {\n        const re=/^wss?:\\/\\/([a-zA-Z0-9\\-]+\\.)+[a-zA-Z]+(\\/.*)?$/\n        const relays = clean_array(text)\n        var isvalid = relays.every(function(relay) {\n            return re.test(relay)\n        })\n        return isvalid\n    }\n\n    ColumnLayout {\n        width: parent.width\n        height: parent.height\n        spacing: 0\n\n        ColumnLayout {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n\n            TextHighlightPane {\n                Layout.fillWidth: true\n                Label {\n                    text: qsTr('Enter the list of Nostr relays') + '<br/><br/>' +\n                        qsTr('Nostr relays are used to send and receive submarine swap offers.') +\n                        ' ' + qsTr('For multisig wallets, nostr is also used to relay transactions to your co-signers.') +\n                        ' ' + qsTr('Connections to nostr are only made when required, and ephemerally.')\n                    width: parent.width\n                    wrapMode: Text.Wrap\n                }\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                ElTextArea {\n                    id: relays_ta\n                    Layout.fillWidth: true\n                    Layout.fillHeight: true\n                    font.family: FixedFont\n                    wrapMode: TextEdit.WrapAnywhere\n                    onTextChanged: valid = verify(text)\n                    inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase\n                    background: PaneInsetBackground {\n                        baseColor: constants.darkerDialogBackground\n                    }\n                }\n                ColumnLayout {\n                    Layout.alignment: Qt.AlignTop\n                    ToolButton {\n                        icon.source: '../../icons/paste.png'\n                        icon.height: constants.iconSizeMedium\n                        icon.width: constants.iconSizeMedium\n                        onClicked: {\n                            if (verify(AppController.clipboardToText())) {\n                                if (!relays_ta.text.endsWith('\\n'))\n                                    relays_ta.text = relays_ta.text + '\\n'\n                                relays_ta.text = relays_ta.text + AppController.clipboardToText()\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        FlatButton {\n            Layout.fillWidth: true\n            text: qsTr('Ok')\n            enabled: valid\n            icon.source: '../../icons/confirmed.png'\n            onClicked: {\n                Config.nostrRelays = clean_array(relays_ta.text).join(\",\")\n                rootItem.close()\n            }\n        }\n    }\n\n\n    Component.onCompleted: {\n        relays_ta.text = Config.nostrRelays.replace(/,/g, \"\\n\")\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/NostrSwapServersDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n    title: qsTr(\"Select Swap Provider\")\n\n    property QtObject swaphelper\n\n    property string selectedPubkey\n\n    needsSystemBarPadding: false\n\n    anchors.centerIn: parent\n\n    padding: 0\n\n    width: parent.width * 4/5\n    height: parent.height * 4/5\n\n    ColumnLayout {\n        id: rootLayout\n        width: parent.width\n        height: parent.height\n\n        Frame {\n            id: accountsFrame\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            Layout.topMargin: constants.paddingLarge\n            Layout.bottomMargin: constants.paddingLarge\n            Layout.leftMargin: constants.paddingMedium\n            Layout.rightMargin: constants.paddingMedium\n\n            verticalPadding: 0\n            horizontalPadding: 0\n            background: PaneInsetBackground {}\n\n            ColumnLayout {\n                spacing: 0\n                anchors.fill: parent\n\n                ListView {\n                    id: listview\n                    Layout.preferredWidth: parent.width\n                    Layout.fillHeight: true\n                    clip: true\n                    model: swaphelper.availableSwapServers\n\n                    Connections {\n                        target: swaphelper\n                        function onOffersUpdated() {\n                            if (dialog.selectedPubkey) {\n                                listview.currentIndex = swaphelper.availableSwapServers.indexFor(dialog.selectedPubkey)\n                            }\n                            console.log(\"swapserver list refreshed\")\n                        }\n                    }\n\n                    delegate: ItemDelegate {\n                        width: ListView.view.width\n                        height: itemLayout.height\n\n                        onClicked: {\n                            dialog.selectedPubkey = model.npub\n                            dialog.doAccept()\n                        }\n\n                        GridLayout {\n                            id: itemLayout\n                            columns: 3\n                            rowSpacing: 0\n\n                            anchors {\n                                left: parent.left\n                                right: parent.right\n                                leftMargin: constants.paddingMedium\n                                rightMargin: constants.paddingMedium\n                            }\n\n                            Item {\n                                Layout.columnSpan: 3\n                                Layout.preferredHeight: constants.paddingLarge\n                                Layout.preferredWidth: 1\n                            }\n                            Rectangle {\n                                Layout.rowSpan: 5\n                                Layout.alignment: Qt.AlignVCenter\n                                Layout.fillHeight: true\n                                Layout.preferredWidth: 10\n                                color: model.color\n\n                            }\n                            Label {\n                                text: qsTr('Pubkey')\n                                color: Material.accentColor\n                            }\n                            Label {\n                                Layout.fillWidth: true\n                                // only show the prefix of the pubkey for readability, but\n                                // keep it long enough so that collisions are hard to brute-force:\n                                text: model.server_pubkey.substring(0,32)\n                                wrapMode: Text.Wrap\n                            }\n                            Label {\n                                text: qsTr('Fee')\n                                color: Material.accentColor\n                            }\n                            Label {\n                                Layout.fillWidth: true\n                                text: model.percentage_fee + '% + ' + model.mining_fee + ' sat'\n                            }\n                            Label {\n                                text: qsTr('Last seen')\n                                color: Material.accentColor\n                            }\n                            Label {\n                                Layout.fillWidth: true\n                                text: model.timestamp\n                                wrapMode: Text.Wrap\n                            }\n                            Label {\n                                text: qsTr('Max Forward')\n                                color: Material.accentColor\n                            }\n                            RowLayout{\n                                Layout.fillWidth: true\n                                Label {\n                                    text: Config.formatSats(model.max_forward_amount)\n                                }\n                                Label {\n                                    text: Config.baseUnit\n                                    color: Material.accentColor\n                                }\n                            }\n                            Label {\n                                text: qsTr('Max Reverse')\n                                color: Material.accentColor\n                            }\n                            RowLayout{\n                                Layout.fillWidth: true\n                                Label {\n                                    text: Config.formatSats(model.max_reverse_amount)\n                                }\n                                Label {\n                                    text: Config.baseUnit\n                                    color: Material.accentColor\n                                }\n                            }\n                            Item {\n                                Layout.columnSpan: 3\n                                Layout.preferredHeight: constants.paddingLarge\n                                Layout.preferredWidth: 1\n                            }\n                        }\n                    }\n\n                    ScrollIndicator.vertical: ScrollIndicator { }\n\n                    Label {\n                        visible: swaphelper.availableSwapServers.count == 0\n                        anchors.centerIn: parent\n                        width: listview.width * 4/5\n                        font.pixelSize: constants.fontSizeXXLarge\n                        color: constants.mutedForeground\n                        text: qsTr('No swap providers found')\n                        wrapMode: Text.Wrap\n                        horizontalAlignment: Text.AlignHCenter\n                    }\n\n                }\n            }\n        }\n    }\n\n    Component.onCompleted: {\n        if (dialog.selectedPubkey) {\n            listview.currentIndex = swaphelper.availableSwapServers.indexFor(dialog.selectedPubkey)\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/NotificationPopup.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\nimport QtQuick.Controls.Material.impl\n\nItem {\n    id: root\n\n    property string message\n    property string wallet_name\n    property bool _hide: true\n\n    clip:true\n\n    layer.enabled: height > 0\n    layer.effect: ElevationEffect {\n        elevation: constants.paddingXLarge\n        fullWidth: true\n    }\n\n    states: [\n        State {\n            name: 'expanded'; when: !_hide\n            PropertyChanges { target: root; height: layout.implicitHeight }\n        }\n    ]\n\n    transitions: [\n        Transition {\n            from: ''; to: 'expanded'; reversible: true\n            NumberAnimation { target: root; properties: 'height'; duration: 300; easing.type: Easing.OutQuad }\n        }\n    ]\n\n    function show(wallet_name, message) {\n        root.wallet_name = wallet_name\n        root.message = message\n        root._hide = false\n        closetimer.start()\n    }\n\n    Rectangle {\n        id: rect\n        width: root.width\n        height: layout.height\n        color: constants.colorAlpha(Material.dialogColor, 0.8)\n        anchors.bottom: root.bottom\n\n        ColumnLayout {\n            id: layout\n            width: parent.width\n            spacing: 0\n\n            RowLayout {\n                Layout.margins: constants.paddingLarge\n                spacing: constants.paddingSmall\n\n                Image {\n                    source: '../../icons/info.png'\n                    Layout.preferredWidth: constants.iconSizeLarge\n                    Layout.preferredHeight: constants.iconSizeLarge\n                }\n\n                Label {\n                    id: messageLabel\n                    Layout.fillWidth: true\n                    font.pixelSize: constants.fontSizeLarge\n                    color: Material.foreground\n                    wrapMode: Text.Wrap\n                    text: root.message\n                }\n            }\n            Rectangle {\n                Layout.preferredHeight: 2\n                Layout.fillWidth: true\n                color: Material.accentColor\n            }\n        }\n\n        RowLayout {\n            visible: root.wallet_name && root.wallet_name != Daemon.currentWallet.name\n            anchors.right: rect.right\n            anchors.bottom: rect.bottom\n\n            RowLayout {\n                Layout.margins: constants.paddingSmall\n                Image {\n                    source: '../../icons/wallet.png'\n                    Layout.preferredWidth: constants.iconSizeXSmall\n                    Layout.preferredHeight: constants.iconSizeXSmall\n                }\n\n                Label {\n                    font.pixelSize: constants.fontSizeSmall\n                    color: Material.accentColor\n                    text: root.wallet_name\n                }\n            }\n        }\n    }\n\n    MouseArea {\n        // capture all clicks\n        anchors.fill: parent\n    }\n\n    Timer {\n        id: closetimer\n        interval: 5000\n        repeat: false\n        onTriggered: _hide = true\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/OpenChannelDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: root\n\n    title: qsTr(\"Open Lightning Channel\")\n    iconSource: Qt.resolvedUrl('../../icons/lightning.png')\n\n    padding: 0\n\n    width: parent.width\n    height: parent.height\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.preferredWidth: parent.width\n            Layout.fillHeight: true\n\n            leftMargin: constants.paddingLarge\n            rightMargin: constants.paddingLarge\n\n            contentHeight: rootLayout.height\n            clip:true\n            interactive: height < contentHeight\n\n            GridLayout {\n                id: rootLayout\n                width: parent.width\n\n                columns: 3\n\n                InfoTextArea {\n                    Layout.fillWidth: true\n                    Layout.columnSpan: 3\n                    visible: !Daemon.currentWallet.lightningHasDeterministicNodeId\n                    iconStyle: InfoTextArea.IconStyle.Warn\n                    text: Daemon.currentWallet.seedType == 'segwit'\n                        ? [ qsTr('Your channels cannot be recovered from seed, because they were created with an old version of Electrum.'), ' ',\n                            qsTr('This means that you must save a backup of your wallet every time you create a new channel.'),\n                            '\\n\\n',\n                            qsTr('If you want this wallet to have recoverable channels, you must close your existing channels and restore this wallet from seed.')\n                          ].join('')\n                        : [ qsTr('Your channels cannot be recovered from seed.'), ' ',\n                            qsTr('This means that you must save a backup of your wallet every time you create a new channel.'),\n                            '\\n\\n',\n                            qsTr('If you want to have recoverable channels, you must create a new wallet with an Electrum seed')\n                          ].join('')\n                }\n\n                InfoTextArea {\n                    Layout.fillWidth: true\n                    Layout.columnSpan: 3\n                    visible: Daemon.currentWallet.lightningHasDeterministicNodeId && !Config.useRecoverableChannels\n                    iconStyle: InfoTextArea.IconStyle.Warn\n                    text: [ qsTr('You currently have recoverable channels setting disabled.'),\n                            qsTr('This means your channels cannot be recovered from seed.')\n                          ].join(' ')\n                }\n\n                Label {\n                    text: qsTr('Node')\n                    Layout.columnSpan: 3\n                    color: Material.accentColor\n                }\n\n                // gossip\n                TextArea {\n                    id: node\n                    visible: Config.useGossip\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    font.family: FixedFont\n                    wrapMode: Text.Wrap\n                    placeholderText: qsTr('Paste or scan node uri/pubkey')\n                    inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase\n                    onTextChanged: {\n                        if (activeFocus)\n                            channelopener.connectStr = text\n                    }\n                    onActiveFocusChanged: {\n                        if (!activeFocus)\n                            channelopener.connectStr = text\n                    }\n                }\n\n                RowLayout {\n                    visible: Config.useGossip\n                    spacing: 0\n                    ToolButton {\n                        icon.source: '../../icons/paste.png'\n                        icon.height: constants.iconSizeMedium\n                        icon.width: constants.iconSizeMedium\n                        onClicked: {\n                            var cliptext = AppController.clipboardToText()\n                            if (!cliptext)\n                                return\n                            if (channelopener.validateConnectString(cliptext)) {\n                                channelopener.connectStr = cliptext\n                                node.text = channelopener.connectStr\n                            } else {\n                                var dialog = app.messageDialog.createObject(app, {\n                                    text: qsTr('Invalid node-id or connect string')\n                                })\n                                dialog.open()\n                            }\n                        }\n                    }\n                    ToolButton {\n                        icon.source: '../../icons/qrcode.png'\n                        icon.height: constants.iconSizeMedium\n                        icon.width: constants.iconSizeMedium\n                        scale: 1.2\n                        onClicked: {\n                            var dialog = app.scanDialog.createObject(app, {\n                                hint: qsTr('Scan a node-id or a connect string')\n                            })\n                            dialog.onFoundText.connect(function(data) {\n                                if (channelopener.validateConnectString(data)) {\n                                    channelopener.connectStr = data\n                                    node.text = channelopener.connectStr\n                                } else {\n                                    var errdialog = app.messageDialog.createObject(app, {\n                                        text: qsTr('Invalid node-id or connect string')\n                                    })\n                                    errdialog.open()\n                                }\n                                dialog.close()\n                            })\n                            dialog.open()\n                        }\n                    }\n                }\n\n                // trampoline\n                ComboBox {\n                    visible: !Config.useGossip\n                    Layout.columnSpan: 3\n                    Layout.fillWidth: true\n                    model: channelopener.trampolineNodeNames\n                    onCurrentValueChanged: {\n                        if (activeFocus)\n                            channelopener.connectStr = currentValue\n                    }\n                    // preselect a random node\n                    Component.onCompleted: {\n                        if (!Config.useGossip) {\n                            currentIndex = Math.floor(Math.random() * channelopener.trampolineNodeNames.length)\n                            channelopener.connectStr = currentValue\n                        }\n                    }\n                }\n\n                Label {\n                    text: qsTr('Amount')\n                    Layout.columnSpan: 3\n                    color: Material.accentColor\n                }\n\n                BtcField {\n                    id: amountBtc\n                    fiatfield: amountFiat\n                    Layout.preferredWidth: amountFontMetrics.advanceWidth('0') * 14 + leftPadding + rightPadding\n                    onTextAsSatsChanged: {\n                        if (!is_max.checked)\n                            channelopener.amount = amountBtc.textAsSats\n                    }\n                    readOnly: is_max.checked\n                    color: readOnly\n                        ? Material.accentColor\n                        : Material.foreground\n\n                    Connections {\n                        target: channelopener.amount\n                        function onSatsIntChanged() {\n                            if (is_max.checked)  // amount updated by max amount estimate\n                                amountBtc.text = Config.formatSatsForEditing(channelopener.amount.satsInt)\n                        }\n                    }\n                }\n\n                RowLayout {\n                    Layout.fillWidth: true\n                    Label {\n                        text: Config.baseUnit\n                        color: Material.accentColor\n                    }\n                    Switch {\n                        id: is_max\n                        text: qsTr('Max')\n                        onCheckedChanged: {\n                            if (activeFocus) {\n                                channelopener.amount.isMax = checked\n                                if (checked) {\n                                    channelopener.updateMaxAmount()\n                                }\n                            }\n                        }\n                    }\n                }\n\n                Item { width: 1; height: 1; visible: Daemon.fx.enabled }\n\n                FiatField {\n                    id: amountFiat\n                    Layout.preferredWidth: amountFontMetrics.advanceWidth('0') * 14 + leftPadding + rightPadding\n                    btcfield: amountBtc\n                    visible: Daemon.fx.enabled\n                    readOnly: is_max.checked\n                    color: readOnly\n                        ? Material.accentColor\n                        : Material.foreground\n                }\n\n                Label {\n                    visible: Daemon.fx.enabled\n                    text: Daemon.fx.fiatCurrency\n                    color: Material.accentColor\n                    Layout.fillWidth: true\n                }\n\n                Item { visible: Daemon.fx.enabled ; height: 1; width: 1 }\n\n                InfoTextArea {\n                    id: warning\n                    Layout.topMargin: constants.paddingMedium\n                    Layout.fillWidth: true\n                    Layout.columnSpan: 3\n                    text: channelopener.warning\n                    visible: text\n                    compact: true\n                }\n\n            }\n        }\n\n        FlatButton {\n            Layout.fillWidth: true\n            text: qsTr('Open Channel...')\n            icon.source: '../../icons/confirmed.png'\n            enabled: channelopener.valid\n            onClicked: channelopener.openChannel()\n        }\n    }\n\n    Component {\n        id: confirmOpenChannelDialog\n        ConfirmTxDialog {\n            amountLabelText: qsTr('Channel capacity')\n            sendButtonText: qsTr('Open Channel')\n            finalizer: channelopener.finalizer\n        }\n    }\n\n    ChannelOpener {\n        id: channelopener\n        wallet: Daemon.currentWallet\n        onAuthRequired: (method, authMessage) => {\n            app.handleAuthRequired(channelopener, method, authMessage)\n        }\n        onValidationError: (code, message) => {\n            if (code == 'invalid_nodeid') {\n                var dialog = app.messageDialog.createObject(app, {\n                    title: qsTr('Error'),\n                    iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                    text: message\n                })\n                dialog.open()\n            }\n        }\n        onConflictingBackup: (message) => {\n            var dialog = app.messageDialog.createObject(app, {\n                text: message,\n                yesno: true\n            })\n            dialog.open()\n            dialog.accepted.connect(function() {\n                channelopener.openChannel(true)\n            })\n        }\n        onFinalizerChanged: {\n            var dialog = confirmOpenChannelDialog.createObject(app, {\n                satoshis: channelopener.amount\n            })\n            dialog.accepted.connect(function() {\n                dialog.finalizer.signAndSend()\n            })\n            dialog.open()\n        }\n        onChannelOpening: (peer) => {\n            console.log('Channel is opening')\n            app.channelOpenProgressDialog.reset()\n            app.channelOpenProgressDialog.peer = peer\n            app.channelOpenProgressDialog.open()\n        }\n        onChannelOpenError: (message) => {\n            app.channelOpenProgressDialog.state = 'failed'\n            app.channelOpenProgressDialog.error = message\n        }\n        onChannelOpenSuccess: (cid, has_onchain_backup, min_depth, tx_complete) => {\n            var message = qsTr('Channel established.') + ' '\n                    + qsTr('This channel will be usable after %1 confirmations').arg(min_depth)\n            if (!tx_complete) {\n                message = message + '\\n\\n' + qsTr('Please sign and broadcast the funding transaction.')\n                channelopener.wallet.historyModel.initModel(true) // local tx doesn't trigger model update\n            }\n            app.channelOpenProgressDialog.state = 'success'\n            app.channelOpenProgressDialog.info = message\n            if (!has_onchain_backup) {\n                app.channelOpenProgressDialog.channelBackup = channelopener.channelBackup(cid)\n            }\n            // TODO: handle incomplete TX\n            root.close()\n        }\n    }\n\n    FontMetrics {\n        id: amountFontMetrics\n        font: amountBtc.font\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/OpenWalletDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: openwalletdialog\n\n    property string name\n    property string path\n    property bool isStartup\n\n    property bool _invalidPassword: false\n    property bool _unlockClicked: false\n\n    title: qsTr('Open Wallet')\n    iconSource: Qt.resolvedUrl('../../icons/wallet.png')\n\n    focus: true\n\n    width: parent.width * 4/5\n    anchors.centerIn: parent\n\n    padding: 0\n    needsSystemBarPadding: false\n\n    ColumnLayout {\n        spacing: 0\n        width: parent.width\n\n        ColumnLayout {\n            id: rootLayout\n            Layout.fillWidth: true\n            Layout.leftMargin: constants.paddingXXLarge\n            Layout.rightMargin: constants.paddingXXLarge\n            spacing: constants.paddingLarge\n\n            InfoTextArea {\n                id: notice\n                text: Daemon.singlePasswordEnabled || isStartup\n                    ? qsTr('Please enter password')\n                    : qsTr('Wallet <b>%1</b> requires password to unlock').arg(name)\n                iconStyle: InfoTextArea.IconStyle.Warn\n                Layout.fillWidth: true\n            }\n\n            Label {\n                text: qsTr('Password')\n                Layout.fillWidth: true\n                color: Material.accentColor\n            }\n\n            PasswordField {\n                id: password\n                Layout.fillWidth: true\n                Layout.leftMargin: constants.paddingXLarge\n\n                onTextChanged: {\n                    unlockButton.enabled = true\n                    _unlockClicked = false\n                    _invalidPassword = false\n                }\n                onAccepted: {\n                    unlock()\n                }\n            }\n\n            Label {\n                Layout.alignment: Qt.AlignHCenter\n                text: _invalidPassword && _unlockClicked ? qsTr(\"Invalid Password\") : ''\n                color: constants.colorError\n                font.pixelSize: constants.fontSizeLarge\n            }\n        }\n\n        FlatButton {\n            id: unlockButton\n            Layout.fillWidth: true\n            icon.source: '../../icons/unlock.png'\n            text: qsTr(\"Unlock\")\n            onClicked: {\n                unlock()\n            }\n        }\n\n    }\n\n    function unlock() {\n        unlockButton.enabled = false\n        _unlockClicked = true\n        Daemon.loadWallet(openwalletdialog.path, password.text)\n    }\n\n    function maybeUnlockAnyOtherWallet() {\n        // try to open any other wallet with the password the user entered, hack to improve ux for\n        // users with non-unified wallet password.\n        // we should only fall back to opening a random wallet if:\n        // - the user did not select a specific wallet, otherwise this is confusing\n        // - there can be more than one password, otherwise this scan would be pointless\n        if (Daemon.availableWallets.rowCount() <= 1 || password.text === '') {\n            return false\n        }\n        if (Config.walletDidUseSinglePassword) {\n            // the last time the wallet was unlocked all wallets used the same password.\n            // trying to decrypt all of them now is most probably useless.\n            return false\n        }\n        if (!openwalletdialog.isStartup) {\n            return false  // this dialog got opened because the user clicked on a specific wallet\n        }\n        let wallet_paths = Daemon.getWalletsUnlockableWithPassword(password.text)\n        if (wallet_paths && wallet_paths.length > 0) {\n            console.log('could not unlock recent wallet, falling back to: ' + wallet_paths[0])\n            Daemon.loadWallet(wallet_paths[0], password.text)\n            return true\n        }\n        return false\n    }\n\n    Connections {\n        target: Daemon\n        function onWalletRequiresPassword() {\n            if (maybeUnlockAnyOtherWallet()) {\n                password.text = ''  // reset pw so we cannot end up in a loop\n                return\n            }\n            console.log('invalid password')\n            _invalidPassword = true\n            password.tf.forceActiveFocus()\n        }\n        function onWalletLoaded() {\n            openwalletdialog.close()\n        }\n    }\n\n    Component.onCompleted: {\n        password.tf.forceActiveFocus()\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/OtpDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    title: qsTr('Trustedcoin')\n    iconSource: Qt.resolvedUrl('../../../plugins/trustedcoin/trustedcoin-status.png')\n\n    property string otpauth\n\n    property bool _waiting: false\n    property string _otpError\n\n    focus: true\n\n    ColumnLayout {\n        width: parent.width\n\n        Label {\n            text: qsTr('Enter Authenticator code')\n            font.pixelSize: constants.fontSizeLarge\n            Layout.alignment: Qt.AlignHCenter\n        }\n\n        TextField {\n            id: otpEdit\n            Layout.preferredWidth: fontMetrics.advanceWidth(passwordCharacter) * 6\n            Layout.alignment: Qt.AlignHCenter\n            font.pixelSize: constants.fontSizeXXLarge\n            maximumLength: 6\n            inputMethodHints: Qt.ImhSensitiveData | Qt.ImhDigitsOnly\n            echoMode: TextInput.Password\n            focus: true\n            enabled: !_waiting\n            Keys.onPressed: _otpError = ''\n            onTextChanged: {\n                if (text.length == 6) {\n                    _waiting = true\n                    Daemon.currentWallet.submitOtp(otpEdit.text)\n                }\n            }\n        }\n\n        Label {\n            Layout.topMargin: constants.paddingMedium\n            Layout.bottomMargin: constants.paddingMedium\n            Layout.alignment: Qt.AlignHCenter\n            Layout.fillWidth: true\n            wrapMode: Text.Wrap\n\n            text: _otpError\n            color: constants.colorError\n\n            BusyIndicator {\n                anchors.centerIn: parent\n                width: constants.iconSizeXLarge\n                height: constants.iconSizeXLarge\n                visible: _waiting\n                running: _waiting\n            }\n        }\n    }\n\n    Connections {\n        target: Daemon.currentWallet\n        function onOtpSuccess() {\n            _waiting = false\n            otpauth = otpEdit.text\n            dialog.accept()\n        }\n        function onOtpFailed(code, message) {\n            _waiting = false\n            _otpError = message\n            otpEdit.text = ''\n        }\n    }\n\n    FontMetrics {\n        id: fontMetrics\n        font: otpEdit.font\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/PasswordDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: passworddialog\n\n    title: qsTr(\"Enter Password\")\n    iconSource: Qt.resolvedUrl('../../icons/lock.png')\n\n    property bool confirmPassword: false\n    property string infotext\n    property string errorMessage\n\n    signal passwordEntered(string password)\n\n    anchors.centerIn: parent\n    width: parent.width * 4/5\n    padding: 0\n    needsSystemBarPadding: false\n\n    ColumnLayout {\n        id: rootLayout\n        width: parent.width\n        spacing: 0\n\n        ColumnLayout {\n            id: password_layout\n            Layout.leftMargin: constants.paddingXXLarge\n            Layout.rightMargin: constants.paddingXXLarge\n\n            InfoTextArea {\n                visible: infotext\n                text: infotext\n                Layout.bottomMargin: constants.paddingMedium\n                Layout.fillWidth: true\n            }\n\n            Label {\n                Layout.fillWidth: true\n                text: qsTr('Password')\n                color: Material.accentColor\n            }\n\n            PasswordField {\n                id: pw_1\n                Layout.leftMargin: constants.paddingXLarge\n            }\n\n            Label {\n                Layout.fillWidth: true\n                text: qsTr('Password (again)')\n                visible: confirmPassword\n                color: Material.accentColor\n            }\n\n            PasswordField {\n                id: pw_2\n                Layout.leftMargin: constants.paddingXLarge\n                visible: confirmPassword\n                showReveal: false\n                echoMode: pw_1.echoMode\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                Layout.rightMargin: constants.paddingXLarge\n                Layout.topMargin: constants.paddingLarge\n                Layout.bottomMargin: constants.paddingLarge\n\n                visible: confirmPassword\n\n                Label {\n                    text: qsTr('Strength')\n                    color: Material.accentColor\n                    font.pixelSize: constants.fontSizeSmall\n                }\n\n                PasswordStrengthIndicator {\n                    Layout.fillWidth: true\n                    password: pw_1.text\n                }\n            }\n\n            Label {\n                Layout.maximumWidth: parent.width\n                Layout.alignment: Qt.AlignHCenter\n                text: errorMessage\n                wrapMode: Text.Wrap\n                visible: errorMessage\n                color: constants.colorError\n                font.pixelSize: constants.fontSizeLarge\n            }\n        }\n\n        FlatButton {\n            Layout.fillWidth: true\n            text: qsTr(\"Ok\")\n            icon.source: '../../icons/confirmed.png'\n            enabled: confirmPassword ? pw_1.text.length >= 6 && pw_1.text == pw_2.text : true\n            onClicked: {\n                passwordEntered(pw_1.text)\n            }\n        }\n    }\n\n    function clearPassword() {\n        pw_1.text = \"\"\n        pw_2.text = \"\"\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/Preferences.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: preferences\n    objectName: 'Properties'\n\n    property string title: qsTr(\"Preferences\")\n\n    padding: 0\n\n    property var _baseunits: ['BTC','mBTC','bits','sat']\n\n    ColumnLayout {\n        anchors.fill: parent\n\n        Flickable {\n            Layout.fillHeight: true\n            Layout.fillWidth: true\n\n            contentHeight: prefsPane.height\n            interactive: height < contentHeight\n            clip: true\n\n            Pane {\n                id: prefsPane\n                width: parent.width\n\n                GridLayout {\n                    columns: 2\n                    width: parent.width\n\n                    PrefsHeading {\n                        Layout.columnSpan: 2\n                        text: qsTr('User Interface')\n                    }\n\n                    Label {\n                        text: qsTr('Language')\n                    }\n\n                    ElComboBox {\n                        id: language\n                        textRole: 'text'\n                        valueRole: 'value'\n                        model: Config.languagesAvailable\n                        onCurrentValueChanged: {\n                            if (activeFocus) {\n                                if (Config.language != currentValue) {\n                                    Config.language = currentValue\n                                    var dialog = app.messageDialog.createObject(app, {\n                                        text: qsTr('Please restart Electrum to activate the new GUI settings')\n                                    })\n                                    dialog.open()\n                                }\n                            }\n                        }\n                    }\n\n                    Label {\n                        text: qsTr('Base unit')\n                    }\n\n                    ElComboBox {\n                        id: baseUnit\n                        model: _baseunits\n                        onCurrentValueChanged: {\n                            if (activeFocus)\n                                Config.baseUnit = currentValue\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        spacing: 0\n                        Switch {\n                            id: thousands\n                            onCheckedChanged: {\n                                if (activeFocus)\n                                    Config.thousandsSeparator = checked\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Add thousands separators to bitcoin amounts')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    RowLayout {\n                        spacing: 0\n                        Switch {\n                            id: fiatEnable\n                            onCheckedChanged: {\n                                if (activeFocus)\n                                    Daemon.fx.enabled = checked\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Fiat Currency')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    ElComboBox {\n                        id: currencies\n                        model: Daemon.fx.currencies\n                        enabled: Daemon.fx.enabled\n                        onCurrentValueChanged: {\n                            if (activeFocus)\n                                Daemon.fx.fiatCurrency = currentValue\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        spacing: 0\n                        Switch {\n                            id: historicRates\n                            enabled: Daemon.fx.enabled\n                            onCheckedChanged: {\n                                if (activeFocus)\n                                    Daemon.fx.historicRates = checked\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Historic rates')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    Label {\n                        text: qsTr('Exchange rate provider')\n                        enabled: Daemon.fx.enabled\n                    }\n\n                    ElComboBox {\n                        id: rateSources\n                        enabled: Daemon.fx.enabled\n                        model: Daemon.fx.rateSources\n                        onModelChanged: {\n                            currentIndex = rateSources.indexOfValue(Daemon.fx.rateSource)\n                        }\n                        onCurrentValueChanged: {\n                            if (activeFocus)\n                                Daemon.fx.rateSource = currentValue\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        spacing: 0\n                        Switch {\n                            id: syncLabels\n                            onCheckedChanged: {\n                                if (activeFocus)\n                                    AppController.setPluginEnabled('labels', checked)\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Synchronize labels')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        spacing: 0\n                        Switch {\n                            id: psbtNostr\n                            onCheckedChanged: {\n                                if (activeFocus)\n                                    AppController.setPluginEnabled('psbt_nostr', checked)\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Nostr Cosigner')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        spacing: 0\n                        enabled: AppController.isAndroid()\n                        Switch {\n                            id: setMaxBrightnessOnQrDisplay\n                            onCheckedChanged: {\n                                if (activeFocus)\n                                    Config.setMaxBrightnessOnQrDisplay = checked\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Increase brightness when displaying QR codes')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    PrefsHeading {\n                        Layout.columnSpan: 2\n                        text: qsTr('Security')\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        spacing: 0\n\n                        property bool noWalletPassword: Daemon.currentWallet ? Daemon.currentWallet.verifyPassword('') : true\n                        enabled: Daemon.currentWallet && !noWalletPassword\n\n                        Switch {\n                            id: paymentAuthentication\n                            // showing the toggle as checked even if the wallet has no password would be misleading\n                            checked: Config.paymentAuthentication && !(Daemon.currentWallet && parent.noWalletPassword)\n                            onCheckedChanged: {\n                                if (activeFocus) {\n                                    // will request authentication when checked = false\n                                    console.log('paymentAuthentication: ' + checked)\n                                    Config.paymentAuthentication = checked;\n                                }\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Request authentication for payments')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        spacing: 0\n                        // isAvailable checks phone support and if a fingerprint is enrolled on the system\n                        enabled: Biometrics.isAvailable && Daemon.currentWallet\n\n                        Connections {\n                            target: Biometrics\n                            function onEnablingFailed(error) {\n                                if (error === 'CANCELLED') {\n                                    return // don't show error popup\n                                }\n                                var err = app.messageDialog.createObject(app, {\n                                    text: qsTr('Failed to enable biometric authentication: ') + error\n                                })\n                                err.open()\n                            }\n                        }\n\n                        Switch {\n                            id: useBiometrics\n                            checked: Biometrics.isEnabled\n                            onCheckedChanged: {\n                                if (activeFocus) {\n                                    useBiometrics.focus = false\n                                    if (checked) {\n                                        if (Daemon.singlePasswordEnabled) {\n                                            Biometrics.enable(Daemon.singlePassword)\n                                        } else {\n                                            useBiometrics.checked = false\n                                            var err = app.messageDialog.createObject(app, {\n                                                title: qsTr('Unavailable'),\n                                                text: [\n                                                    qsTr(\"Cannot activate biometric authentication because you have wallets with different passwords.\"),\n                                                    qsTr(\"To use biometric authentication you first need to change all wallet passwords to the same password.\")\n                                                ].join(\"\\n\")\n                                            })\n                                            err.open()\n                                        }\n                                    } else {\n                                        Biometrics.disableProtected()\n                                    }\n                                }\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Biometric authentication')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        spacing: 0\n                        enabled: AppController.isAndroid()\n                        Switch {\n                            id: disableScreenshots\n                            onCheckedChanged: {\n                                if (activeFocus)\n                                    Config.alwaysAllowScreenshots = !checked\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Protect secrets from screenshots')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    PrefsHeading {\n                        Layout.columnSpan: 2\n                        text: qsTr('Wallet behavior')\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        spacing: 0\n                        Switch {\n                            id: spendUnconfirmed\n                            onCheckedChanged: {\n                                if (activeFocus)\n                                    Config.spendUnconfirmed = checked\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Spend unconfirmed')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        spacing: 0\n                        Switch {\n                            id: freezeReusedAddressUtxos\n                            onCheckedChanged: {\n                                if (activeFocus)\n                                    Config.freezeReusedAddressUtxos = checked\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: Config.shortDescFor('WALLET_FREEZE_REUSED_ADDRESS_UTXOS')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    PrefsHeading {\n                        Layout.columnSpan: 2\n                        text: qsTr('Lightning')\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        text: Config.shortDescFor('LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS')\n                        wrapMode: Text.Wrap\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        text: qsTr('<b>%1%</b> of payment').arg(maxfeeslider._fees[maxfeeslider.value]/10000)\n                        wrapMode: Text.Wrap\n                    }\n\n                    Slider {\n                        id: maxfeeslider\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        Layout.leftMargin: constants.paddingXLarge\n                        Layout.rightMargin: constants.paddingXLarge\n\n                        property var _fees: [500, 1000, 3000, 5000, 10000, 20000, 30000, 50000]\n\n                        snapMode: Slider.SnapOnRelease\n                        stepSize: 1\n                        from: 0\n                        to: _fees.length - 1\n\n                        onValueChanged: {\n                            if (activeFocus)\n                                Config.lightningPaymentFeeMaxMillionths = _fees[value]\n                        }\n\n                        Component.onCompleted: {\n                            value = _fees.indexOf(Config.lightningPaymentFeeMaxMillionths)\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        spacing: 0\n                        Switch {\n                            id: useTrampolineRouting\n                            onCheckedChanged: {\n                                if (activeFocus) {\n                                    if (!checked) {\n                                        var dialog = app.messageDialog.createObject(app, {\n                                            title: qsTr('Are you sure?'),\n                                            text: qsTr('Electrum will have to download the Lightning Network graph, which is not recommended on mobile.'),\n                                            yesno: true\n                                        })\n                                        dialog.accepted.connect(function() {\n                                            Config.useGossip = true\n                                        })\n                                        dialog.rejected.connect(function() {\n                                            checked = true // revert\n                                        })\n                                        dialog.open()\n                                    } else {\n                                        Config.useGossip = !checked\n                                    }\n                                }\n\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Trampoline routing')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        spacing: 0\n                        Switch {\n                            id: useRecoverableChannels\n                            onCheckedChanged: {\n                                if (activeFocus) {\n                                    if (!checked) {\n                                        var dialog = app.messageDialog.createObject(app, {\n                                            title: qsTr('Are you sure?'),\n                                            text: qsTr('This option allows you to recover your lightning funds if you lose your device, or if you uninstall this app while lightning channels are active. Do not disable it unless you know how to recover channels from backups.'),\n                                            yesno: true\n                                        })\n                                        dialog.accepted.connect(function() {\n                                            Config.useRecoverableChannels = false\n                                        })\n                                        dialog.rejected.connect(function() {\n                                            checked = true // revert\n                                        })\n                                        dialog.open()\n                                    } else {\n                                        Config.useRecoverableChannels = checked\n                                    }\n                                }\n                            }\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Create recoverable channels')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n\n                    PrefsHeading {\n                        Layout.columnSpan: 2\n                        text: qsTr('Advanced')\n                    }\n\n                    RowLayout {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        spacing: 0\n                        Switch {\n                            id: enableDebugLogs\n                            onCheckedChanged: {\n                                if (activeFocus)\n                                    Config.enableDebugLogs = checked\n                            }\n                            enabled: Config.canToggleDebugLogs\n                        }\n                        Label {\n                            Layout.fillWidth: true\n                            text: qsTr('Enable debug logs (for developers)')\n                            wrapMode: Text.Wrap\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    Component.onCompleted: {\n        language.currentIndex = language.indexOfValue(Config.language)\n        baseUnit.currentIndex = _baseunits.indexOf(Config.baseUnit)\n        thousands.checked = Config.thousandsSeparator\n        currencies.currentIndex = currencies.indexOfValue(Daemon.fx.fiatCurrency)\n        historicRates.checked = Daemon.fx.historicRates\n        rateSources.currentIndex = rateSources.indexOfValue(Daemon.fx.rateSource)\n        fiatEnable.checked = Daemon.fx.enabled\n        spendUnconfirmed.checked = Config.spendUnconfirmed\n        freezeReusedAddressUtxos.checked = Config.freezeReusedAddressUtxos\n        useTrampolineRouting.checked = !Config.useGossip\n        enableDebugLogs.checked = Config.enableDebugLogs\n        disableScreenshots.checked = !Config.alwaysAllowScreenshots && AppController.isAndroid()\n        setMaxBrightnessOnQrDisplay.checked = Config.setMaxBrightnessOnQrDisplay && AppController.isAndroid()\n        useRecoverableChannels.checked = Config.useRecoverableChannels\n        syncLabels.checked = AppController.isPluginEnabled('labels')\n        psbtNostr.checked = AppController.isPluginEnabled('psbt_nostr')\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ProxyConfigDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: rootItem\n\n    title: qsTr('Proxy settings')\n    iconSource: Qt.resolvedUrl('../../icons/status_connected_proxy.png')\n\n    width: parent.width\n    height: parent.height\n\n    padding: 0\n\n    ColumnLayout {\n        width: parent.width\n        height: parent.height\n        spacing: 0\n\n        ProxyConfig {\n            Layout.fillWidth: true\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n            id: proxyconfig\n        }\n\n        Item { Layout.fillHeight: true; Layout.preferredWidth: 1 }\n\n        FlatButton {\n            Layout.fillWidth: true\n            text: qsTr('Ok')\n            icon.source: '../../icons/confirmed.png'\n            onClicked: {\n                Network.proxy = proxyconfig.toProxyDict()\n                rootItem.close()\n            }\n        }\n    }\n\n\n    Component.onCompleted: {\n        var p = Network.proxy\n\n        proxyconfig.proxy_enabled = p['enabled']\n        proxyconfig.proxy_address = p['host']\n        proxyconfig.proxy_port = p['port']\n        proxyconfig.username = p['user']\n        proxyconfig.password = p['password']\n        proxyconfig.proxy_type = proxyconfig.proxy_type_map.map(function(x) {\n            return x.value\n        }).indexOf(p['mode'])\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/RbfBumpFeeDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    required property QtObject rbffeebumper\n\n    title: qsTr('Bump Fee')\n    iconSource: Qt.resolvedUrl('../../icons/rocket.png')\n\n    width: parent.width\n    height: parent.height\n    padding: 0\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            leftMargin: constants.paddingLarge\n            rightMargin: constants.paddingLarge\n\n            contentHeight: rootLayout.height\n            clip: true\n            interactive: height < contentHeight\n\n            GridLayout {\n                id: rootLayout\n\n                width: parent.width\n\n                columns: 2\n\n                InfoTextArea {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    Layout.bottomMargin: constants.paddingLarge\n                    text: qsTr('Move the slider to increase your transaction\\'s fee. This will improve its position in the mempool')\n                }\n\n                Label {\n                    Layout.fillWidth: true\n                    text: qsTr('Method')\n                    color: Material.accentColor\n                }\n\n                RowLayout {\n                    ElComboBox {\n                        id: bumpMethodComboBox\n\n                        textRole: 'text'\n                        valueRole: 'value'\n\n                        model: rbffeebumper.bumpMethodsAvailable\n                        onCurrentValueChanged: {\n                            if (activeFocus)\n                                rbffeebumper.bumpMethod = currentValue\n                        }\n                        Component.onCompleted: {\n                            currentIndex = indexOfValue(rbffeebumper.bumpMethod)\n                        }\n                    }\n                    Item { Layout.fillWidth: true;  Layout.preferredHeight: 1 }\n                }\n\n                Label {\n                    text: qsTr('Old fee')\n                    color: Material.accentColor\n                }\n\n                FormattedAmount {\n                    amount: rbffeebumper.oldfee\n                }\n\n                Label {\n                    text: qsTr('Old fee rate')\n                    color: Material.accentColor\n                }\n\n                RowLayout {\n                    Label {\n                        id: oldfeeRate\n                        text: rbffeebumper.oldfeeRate\n                        font.family: FixedFont\n                    }\n\n                    Label {\n                        text: UI_UNIT_NAME.FEERATE_SAT_PER_VB\n                        color: Material.accentColor\n                    }\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    text: qsTr('New fee')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    height: feepicker.height\n\n                    FeePicker {\n                        id: feepicker\n                        width: parent.width\n                        finalizer: dialog.rbffeebumper\n                        allowPickerAbsFees: false\n                    }\n                }\n\n                ToggleLabel {\n                    id: optionstoggle\n                    Layout.columnSpan: 2\n                    labelText: qsTr('Options')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    visible: !optionstoggle.collapsed\n                    height: optionslayout.height\n\n                    GridLayout {\n                        id: optionslayout\n                        width: parent.width\n                        columns: 2\n\n                        ElCheckBox {\n                            Layout.fillWidth: true\n                            text: qsTr('Enable output value rounding')\n                            onCheckedChanged: {\n                                if (activeFocus) {\n                                    Config.outputValueRounding = checked\n                                    rbffeebumper.doUpdate()\n                                }\n                            }\n                            Component.onCompleted: {\n                                checked = Config.outputValueRounding\n                            }\n                        }\n\n                        HelpButton {\n                            heading: qsTr('Enable output value rounding')\n                            helptext: qsTr('In some cases, use up to 3 change addresses in order to break up large coin amounts and obfuscate the recipient address.')\n                                    + ' ' + qsTr('This may result in higher transactions fees.')\n                        }\n                    }\n                }\n\n                InfoTextArea {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    Layout.topMargin: constants.paddingLarge\n                    iconStyle: InfoTextArea.IconStyle.Warn\n                    visible: rbffeebumper.warning != ''\n                    text: rbffeebumper.warning\n                }\n\n                ToggleLabel {\n                    id: inputs_label\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingMedium\n\n                    visible: rbffeebumper.valid\n                    labelText: qsTr('Inputs (%1)').arg(rbffeebumper.inputs.length)\n                    color: Material.accentColor\n                }\n\n                Repeater {\n                    model: inputs_label.collapsed || !inputs_label.visible\n                        ? undefined\n                        : rbffeebumper.inputs\n                    delegate: TxInput {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        idx: index\n                        model: modelData\n                    }\n                }\n\n                ToggleLabel {\n                    id: outputs_label\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingMedium\n\n                    visible: rbffeebumper.valid\n                    labelText: qsTr('Outputs (%1)').arg(rbffeebumper.outputs.length)\n                    color: Material.accentColor\n                }\n\n                Repeater {\n                    model: outputs_label.collapsed || !outputs_label.visible\n                        ? undefined\n                        : rbffeebumper.outputs\n                    delegate: TxOutput {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        allowShare: false\n                        allowClickAddress: false\n\n                        idx: index\n                        model: modelData\n                    }\n                }\n\n            }\n        }\n\n        FlatButton {\n            id: sendButton\n            Layout.fillWidth: true\n            text: qsTr('Ok')\n            icon.source: '../../icons/confirmed.png'\n            enabled: rbffeebumper.valid\n            onClicked: doAccept()\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/RbfCancelDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    required property QtObject txcanceller\n\n    title: qsTr('Cancel Transaction')\n\n    width: parent.width\n    height: parent.height\n    padding: 0\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            leftMargin: constants.paddingLarge\n            rightMargin: constants.paddingLarge\n\n            contentHeight: rootLayout.height\n            clip: true\n            interactive: height < contentHeight\n\n            GridLayout {\n                id: rootLayout\n                width: parent.width\n                columns: 2\n\n                InfoTextArea {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    Layout.bottomMargin: constants.paddingLarge\n                    text: qsTr('Cancel an unconfirmed transaction by double-spending its inputs back to your wallet with a higher fee.')\n                }\n\n                Label {\n                    text: qsTr('Old fee')\n                    color: Material.accentColor\n                }\n\n                FormattedAmount {\n                    amount: txcanceller.oldfee\n                }\n\n                Label {\n                    text: qsTr('Old fee rate')\n                    color: Material.accentColor\n                }\n\n                RowLayout {\n                    Label {\n                        id: oldfeeRate\n                        text: txcanceller.oldfeeRate\n                        font.family: FixedFont\n                    }\n\n                    Label {\n                        text: UI_UNIT_NAME.FEERATE_SAT_PER_VB\n                        color: Material.accentColor\n                    }\n                }\n\n                Label {\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingSmall\n                    text: qsTr('New fee')\n                    color: Material.accentColor\n                }\n\n                TextHighlightPane {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    height: feepicker.height\n\n                    FeePicker {\n                        id: feepicker\n                        width: parent.width\n                        finalizer: dialog.txcanceller\n                        allowPickerAbsFees: false\n                    }\n                }\n\n                InfoTextArea {\n                    Layout.columnSpan: 2\n                    Layout.fillWidth: true\n                    Layout.topMargin: constants.paddingLarge\n                    iconStyle: InfoTextArea.IconStyle.Warn\n                    visible: txcanceller.warning != ''\n                    text: txcanceller.warning\n                }\n\n                ToggleLabel {\n                    id: inputs_label\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingMedium\n\n                    visible: txcanceller.valid\n                    labelText: qsTr('Inputs (%1)').arg(txcanceller.inputs.length)\n                    color: Material.accentColor\n                }\n\n                Repeater {\n                    model: inputs_label.collapsed || !inputs_label.visible\n                        ? undefined\n                        : txcanceller.inputs\n                    delegate: TxInput {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        idx: index\n                        model: modelData\n                    }\n                }\n\n                ToggleLabel {\n                    id: outputs_label\n                    Layout.columnSpan: 2\n                    Layout.topMargin: constants.paddingMedium\n\n                    visible: txcanceller.valid\n                    labelText: qsTr('Outputs (%1)').arg(txcanceller.outputs.length)\n                    color: Material.accentColor\n                }\n\n                Repeater {\n                    model: outputs_label.collapsed || !outputs_label.visible\n                        ? undefined\n                        : txcanceller.outputs\n                    delegate: TxOutput {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        allowShare: false\n                        allowClickAddress: false\n\n                        idx: index\n                        model: modelData\n                    }\n                }\n\n            }\n        }\n\n        FlatButton {\n            id: confirmButton\n            Layout.fillWidth: true\n            text: qsTr('Ok')\n            icon.source: '../../icons/confirmed.png'\n            enabled: txcanceller.valid\n            onClicked: doAccept()\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ReceiveDetailsDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\nimport QtQml.Models\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    title: qsTr('Create Invoice')\n    iconSource: Qt.resolvedUrl('../../icons/tab_receive.png')\n\n    property alias amount: amountBtc.text\n    property alias description: message.text\n    property alias expiry: expires.currentValue\n    property bool isLightning: false\n\n    padding: 0\n    needsSystemBarPadding: false\n\n    ColumnLayout {\n        width: parent.width\n\n        GridLayout {\n            id: form\n            Layout.fillWidth: true\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n            Layout.bottomMargin: constants.paddingLarge\n\n            rowSpacing: constants.paddingSmall\n            columnSpacing: constants.paddingSmall\n            columns: 4\n\n\n            Label {\n                text: qsTr('Message')\n            }\n\n            TextField {\n                id: message\n                placeholderText: qsTr('Description of payment request')\n                Layout.columnSpan: 3\n                Layout.fillWidth: true\n            }\n\n            Label {\n                text: qsTr('Amount')\n                wrapMode: Text.WordWrap\n                Layout.rightMargin: constants.paddingXLarge\n            }\n\n            BtcField {\n                id: amountBtc\n                fiatfield: amountFiat\n                Layout.fillWidth: true\n            }\n\n            Label {\n                Layout.columnSpan: 2\n                Layout.rightMargin: constants.paddingXLarge\n                text: Config.baseUnit\n                color: Material.accentColor\n            }\n\n            Item { visible: Daemon.fx.enabled; width: 1; height: 1 }\n\n            FiatField {\n                id: amountFiat\n                Layout.fillWidth: true\n                btcfield: amountBtc\n                visible: Daemon.fx.enabled\n            }\n\n            Label {\n                Layout.columnSpan: 2\n                Layout.rightMargin: constants.paddingXLarge\n                visible: Daemon.fx.enabled\n                text: Daemon.fx.fiatCurrency\n                color: Material.accentColor\n            }\n\n            Label {\n                text: qsTr('Expires after')\n                Layout.fillWidth: false\n            }\n\n            RequestExpiryComboBox {\n                id: expires\n                Layout.columnSpan: 3\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Onchain')\n                icon.source: '../../icons/bitcoin.png'\n                onClicked: { dialog.isLightning = false; doAccept() }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                enabled: Daemon.currentWallet.isLightning && (Daemon.currentWallet.lightningCanReceive.satsInt\n                            > amountBtc.textAsSats.satsInt || Daemon.currentWallet.canGetZeroconfChannel)\n                text: qsTr('Lightning')\n                icon.source: '../../icons/lightning.png'\n                onClicked: {\n                    if (Daemon.currentWallet.lightningCanReceive.satsInt > amountBtc.textAsSats.satsInt) {\n                        // can receive on existing channel\n                        dialog.isLightning = true\n                        doAccept()\n                    } else if (Daemon.currentWallet.canGetZeroconfChannel && amountBtc.textAsSats.satsInt\n                                >= Daemon.currentWallet.minChannelFunding.satsInt) {\n                        // ask for confirmation of zeroconf channel to prevent fee surprise\n                        var confirmdialog = app.messageDialog.createObject(dialog, {\n                            title: qsTr('Confirm just-in-time channel'),\n                            text: [qsTr('Receiving this payment will purchase a Lightning channel from your service provider.'),\n                                   qsTr('Fees will be deducted from the payment.'),\n                                   qsTr('Do you want to continue?')].join(' '),\n                            yesno: true\n                        })\n                        confirmdialog.accepted.connect(function () {\n                            dialog.isLightning = true\n                            doAccept()\n                        })\n                        confirmdialog.open()\n                    } else {\n                        // show error that amnt > 200k is necessary to get zeroconf channel\n                        var confirmdialog = app.messageDialog.createObject(dialog, {\n                            title: qsTr(\"Amount too low\"),\n                            text: [qsTr(\"You don't have channels with enough inbound liquidity to receive this payment.\"),\n                                   qsTr(\"Request at least %1 to open a channel just-in-time.\").arg(\n                                       Config.formatSats(Daemon.currentWallet.minChannelFunding.satsInt, true))].join(' ')\n                        })\n                        confirmdialog.open()\n                    }\n                    // can't get zeroconf channel and doesn't have enough inbound liquidity\n                }\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ReceiveDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\nimport QtQml.Models\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    title: qsTr('Receive Payment')\n    iconSource: Qt.resolvedUrl('../../icons/tab_receive.png')\n\n    property string key\n    property bool isLightning: request.isLightning\n\n    property string _bolt11: request.bolt11\n    property string _bip21uri: request.bip21\n    property string _address: request.address\n    property bool _render_qr: false // delay qr rendering until dialog is shown\n\n    signal requestPaid\n\n    padding: 0\n\n    function getPaidTxid() {\n        return request.paidTxid\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.preferredWidth: parent.width\n            Layout.fillHeight: true\n\n            leftMargin: constants.paddingLarge\n            rightMargin: constants.paddingLarge\n\n            contentHeight: rootLayout.height\n            clip: true\n            interactive: height < contentHeight\n\n            ColumnLayout {\n                id: rootLayout\n                width: parent.width\n                spacing: constants.paddingMedium\n\n                TextHighlightPane {\n                    Layout.alignment: Qt.AlignHCenter\n                    Layout.fillWidth: true\n\n                    ColumnLayout {\n                        width: parent.width\n                        Rectangle {\n                            id: qrbg\n                            Layout.alignment: Qt.AlignHCenter\n                            Layout.topMargin: constants.paddingSmall\n                            Layout.bottomMargin: constants.paddingSmall\n\n                            Layout.preferredWidth: dialog.width * 7/8\n                            Layout.preferredHeight: dialog.width * 7/8\n\n                            color: 'white'\n\n                            QRImage {\n                                anchors.centerIn: parent\n                                qrdata: _bolt11\n                                    ? _bolt11\n                                    : _bip21uri\n                                        ? _bip21uri\n                                        : _address\n                                render: _render_qr\n                                enableToggleText: true\n                            }\n                        }\n                    }\n                }\n\n                Rectangle {\n                    height: 1\n                    Layout.alignment: Qt.AlignHCenter\n                    Layout.preferredWidth: qrbg.width\n                    color: Material.accentColor\n                }\n\n                GridLayout {\n                    columns: 2\n                    Layout.maximumWidth: qrbg.width\n                    Layout.alignment: Qt.AlignHCenter\n\n                    Label {\n                        text: qsTr('Status')\n                        color: Material.accentColor\n                    }\n                    Label {\n                        text: request.status_str\n                    }\n                    Label {\n                        text: qsTr('Message')\n                        color: Material.accentColor\n                    }\n                    Label {\n                        visible: request.message\n                        Layout.fillWidth: true\n                        text: request.message\n                        wrapMode: Text.Wrap\n                    }\n                    Label {\n                        visible: !request.message\n                        Layout.fillWidth: true\n                        text: qsTr('unspecified')\n                        color: constants.mutedForeground\n                    }\n                    Label {\n                        text: qsTr('Amount')\n                        color: Material.accentColor\n                    }\n                    FormattedAmount {\n                        visible: !request.amount.isEmpty\n                        valid: !request.amount.isEmpty\n                        amount: request.amount\n                    }\n                    Label {\n                        visible: request.amount.isEmpty\n                        text: qsTr('unspecified')\n                        color: constants.mutedForeground\n                    }\n                }\n\n                Rectangle {\n                    height: 1\n                    Layout.alignment: Qt.AlignHCenter\n                    Layout.preferredWidth: qrbg.width\n                    color: Material.accentColor\n                }\n\n            }\n\n        }\n\n        ButtonContainer {\n            id: buttons\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n\n                icon.source: '../../icons/copy_bw.png'\n                icon.color: 'transparent'\n                text: 'Copy'\n                onClicked: {\n                    AppController.textToClipboard(_bolt11\n                        ? _bolt11.toLowerCase()\n                        : _bip21uri\n                            ? _bip21uri\n                            : _address\n                    )\n                    toaster.show(this, qsTr('Copied!'))\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n\n                icon.source: '../../icons/share.png'\n                text: 'Share'\n                onClicked: {\n                    enabled = false\n                    AppController.doShare(\n                        _bolt11\n                            ? _bolt11.toLowerCase()\n                            : _bip21uri\n                                ? _bip21uri\n                                : _address,\n                        _bolt11 || _bip21uri\n                            ? qsTr('Payment Request')\n                            : qsTr('Onchain address')\n                    )\n                    enabled = true\n                }\n            }\n        }\n    }\n\n    RequestDetails {\n        id: request\n        wallet: Daemon.currentWallet\n        onStatusChanged: {\n            if (status == RequestDetails.Paid || status == RequestDetails.Unconfirmed) {\n                requestPaid()\n            }\n        }\n    }\n\n    Toaster {\n        id: toaster\n    }\n\n    Component.onCompleted: {\n        request.key = dialog.key\n    }\n\n    // hack. delay qr rendering until dialog is shown\n    Connections {\n        target: dialog.enter\n        function onRunningChanged() {\n            if (!dialog.enter.running) {\n                dialog._render_qr = true\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ReceiveRequests.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\nimport QtQml.Models\nimport QtQml\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: root\n    objectName: 'ReceiveRequests'\n\n    padding: 0\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        ColumnLayout {\n            Layout.fillWidth: true\n            Layout.margins: constants.paddingLarge\n\n            InfoTextArea {\n                Layout.fillWidth: true\n                Layout.bottomMargin: constants.paddingLarge\n                visible: !Config.userKnowsPressAndHold\n                text: qsTr('To access this list from the main screen, press and hold the Receive button')\n            }\n\n            Heading {\n                text: qsTr('Pending requests')\n            }\n\n            Frame {\n                background: PaneInsetBackground {}\n\n                verticalPadding: 0\n                horizontalPadding: 0\n                Layout.fillHeight: true\n                Layout.fillWidth: true\n\n                ElListView {\n                    id: listview\n                    anchors.fill: parent\n                    clip: true\n                    currentIndex: -1\n\n                    model: DelegateModel {\n                        id: delegateModel\n                        model: Daemon.currentWallet.requestModel\n                        delegate: InvoiceDelegate {\n                            onClicked: {\n                                app.stack.getRoot().openRequest(model.key)\n                                listview.currentIndex = -1\n                            }\n                            onPressAndHold: listview.currentIndex = index\n                        }\n                    }\n\n                    add: Transition {\n                        NumberAnimation { properties: 'scale'; from: 0.75; to: 1; duration: 500 }\n                        NumberAnimation { properties: 'opacity'; from: 0; to: 1; duration: 500 }\n                    }\n                    addDisplaced: Transition {\n                        SpringAnimation { properties: 'y'; duration: 200; spring: 5; damping: 0.5; mass: 2 }\n                    }\n\n                    remove: Transition {\n                        NumberAnimation { properties: 'scale'; to: 0.75; duration: 300 }\n                        NumberAnimation { properties: 'opacity'; to: 0; duration: 300 }\n                    }\n                    removeDisplaced: Transition {\n                        SequentialAnimation {\n                            PauseAnimation { duration: 200 }\n                            SpringAnimation { properties: 'y'; duration: 100; spring: 5; damping: 0.5; mass: 2 }\n                        }\n                    }\n\n                    ScrollIndicator.vertical: ScrollIndicator { }\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Delete')\n                icon.source: '../../icons/delete.png'\n                visible: listview.currentIndex >= 0\n                onClicked: {\n                    Daemon.currentWallet.deleteRequest(listview.currentItem.getKey())\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('View')\n                icon.source: '../../icons/tab_receive.png'\n                visible: listview.currentIndex >= 0\n                onClicked: {\n                    app.stack.getRoot().openRequest(listview.currentItem.getKey())\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ScanDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\n\nimport org.electrum\n\nimport \"controls\"\n\n// currently not used on android, kept for future use when qt6 camera stops crashing\nElDialog {\n    id: scanDialog\n\n    property string error\n    property string hint\n\n    signal foundText(data: string)\n    signal foundBinary(data: Bytes)\n\n    width: parent.width\n    height: parent.height\n    padding: 0\n\n    header: null\n    topPadding: 0 // dialog needs topPadding override\n\n    function doClose() {\n        qrscan.stop()\n        Qt.callLater(doReject)\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        QRScan {\n            id: qrscan\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            hint: scanDialog.hint\n            onFoundText: (data) => {\n                scanDialog.foundText(data)\n            }\n        }\n\n        FlatButton {\n            id: button\n            Layout.fillWidth: true\n            text: qsTr('Cancel')\n            icon.source: '../../icons/closebutton.png'\n            onClicked: doReject()\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/SendDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\n// currently not used on android, kept for future use when qt6 camera stops crashing\nElDialog {\n    id: dialog\n\n    property InvoiceParser invoiceParser\n    property PIResolver piResolver\n\n    signal txFound(data: string)\n    signal channelBackupFound(data: string)\n\n    header: null\n    padding: 0\n    topPadding: 0\n\n    onAboutToHide: {\n        console.log('about to hide')\n        qrscan.stop()\n    }\n\n    function restart() {\n        qrscan.restart()\n    }\n\n    function dispatch(data) {\n        data = data.trim()\n        if (bitcoin.isRawTx(data)) {\n            txFound(data)\n        } else if (Daemon.currentWallet.isValidChannelBackup(data)) {\n            channelBackupFound(data)\n        } else {\n            piResolver.recipient = data\n        }\n    }\n\n    // override\n    function doClose() {\n        console.log('SendDialog doClose override') // doesn't trigger when going back??\n        qrscan.stop()\n        Qt.callLater(doReject)\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        QRScan {\n            id: qrscan\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            hint: Daemon.currentWallet.isLightning\n                ? qsTr('Scan an Invoice, an Address, an LNURL, a PSBT or a Channel Backup')\n                : qsTr('Scan an Invoice, an Address, an LNURL or a PSBT')\n\n            onFoundText: (data) => {\n                dialog.dispatch(data)\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                enabled: !invoiceParser.busy && !piResolver.busy\n                icon.source: '../../icons/copy_bw.png'\n                text: qsTr('Paste')\n                onClicked: {\n                    qrscan.stop()\n                    dialog.dispatch(AppController.clipboardToText())\n                }\n            }\n        }\n\n    }\n\n    Bitcoin {\n        id: bitcoin\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ServerConfigDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: rootItem\n\n    title: qsTr('Server settings')\n    iconSource: Qt.resolvedUrl('../../icons/network.png')\n\n    width: parent.width\n    height: parent.height\n\n    padding: 0\n\n    ColumnLayout {\n        width: parent.width\n        height: parent.height\n        spacing: 0\n\n        ColumnLayout {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n\n            ServerConfig {\n                id: serverconfig\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n            }\n        }\n\n        FlatButton {\n            Layout.fillWidth: true\n            text: qsTr('Ok')\n            enabled: serverconfig.addressValid\n            icon.source: '../../icons/confirmed.png'\n            onClicked: {\n                let auto_connect = serverconfig.serverConnectMode == ServerConnectModeComboBox.Mode.Autoconnect\n                let server = serverconfig.address\n                let one_server = serverconfig.serverConnectMode == ServerConnectModeComboBox.Mode.Single\n                Network.setServerParameters(server, auto_connect, one_server)\n                rootItem.close()\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/ServerConnectWizard.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport \"wizard\"\n\nWizard {\n    id: serverconnectwizard\n\n    wizardTitle: qsTr('Network configuration')\n\n    enter: null // disable transition\n\n    wiz: Daemon.serverConnectWizard\n    finishButtonText: qsTr('Next')\n\n    onAccepted: {\n        var proxy = wizard_data['proxy']\n        if (proxy && proxy['enabled'] == true) {\n            Network.proxy = proxy\n        } else {\n            Network.proxy = {'enabled': false}\n        }\n        Network.setServerParameters(wizard_data['server'], wizard_data['autoconnect'], wizard_data['one_server'])\n    }\n\n    Component.onCompleted: {\n        var view = wiz.startWizard()\n        _loadNextComponent(view)\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/SignVerifyMessageDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: dialog\n\n    enum Check {\n        Unknown,\n        Valid,\n        Invalid\n    }\n\n    property string address\n\n    property bool _addressValid: false\n    property bool _addressMine: false\n    property int _verified: SignVerifyMessageDialog.Check.Unknown\n\n    implicitHeight: parent.height\n    implicitWidth: parent.width\n\n    title: qsTr('Sign/Verify Message')\n    iconSource: Qt.resolvedUrl('../../icons/pen.png')\n\n    padding: 0\n\n    function validateAddress() {\n        // TODO: not all types of addresses are valid (e.g. p2wsh)\n        _addressValid = bitcoin.isAddress(addressField.text)\n        _addressMine = Daemon.currentWallet.isAddressMine(addressField.text)\n    }\n\n    ColumnLayout {\n        width: parent.width\n        height: parent.height\n        spacing: constants.paddingLarge\n\n        ColumnLayout {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n\n            Label {\n                text: qsTr('Address')\n                color: Material.accentColor\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                TextField {\n                    id: addressField\n                    Layout.fillWidth: true\n                    placeholderText: qsTr('Address')\n                    font.family: FixedFont\n                    onTextChanged: {\n                        validateAddress()\n                        _verified = SignVerifyMessageDialog.Check.Unknown\n                    }\n                }\n                ToolButton {\n                    icon.source: '../../icons/paste.png'\n                    icon.color: 'transparent'\n                    onClicked: {\n                        addressField.text = AppController.clipboardToText()\n                    }\n                }\n            }\n\n            Label {\n                text: qsTr('Message')\n                color: Material.accentColor\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n                ElTextArea {\n                    id: plaintext\n                    Layout.fillWidth: true\n                    Layout.fillHeight: true\n                    font.family: FixedFont\n                    wrapMode: TextInput.Wrap\n                    background: PaneInsetBackground {\n                        baseColor: constants.darkerDialogBackground\n                    }\n                    onTextChanged: _verified = SignVerifyMessageDialog.Check.Unknown\n                }\n                ColumnLayout {\n                    Layout.alignment: Qt.AlignTop\n                    ToolButton {\n                        icon.source: '../../icons/paste.png'\n                        icon.color: 'transparent'\n                        onClicked: {\n                            plaintext.text = AppController.clipboardToText()\n                        }\n                    }\n                    ToolButton {\n                        icon.source: '../../icons/share.png'\n                        icon.color: enabled ? 'transparent' : Material.iconDisabledColor\n                        enabled: plaintext.text\n                        onClicked: {\n                            var dialog = app.genericShareDialog.createObject(app, {\n                                title: qsTr('Message'),\n                                text_qr: plaintext.text\n                            })\n                            dialog.open()\n                        }\n                    }\n                }\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                Label {\n                    text: qsTr('Signature')\n                    color: Material.accentColor\n                }\n                Label {\n                    Layout.alignment: Qt.AlignRight\n                    visible: _verified != SignVerifyMessageDialog.Check.Unknown\n                    text: _verified == SignVerifyMessageDialog.Check.Valid\n                        ? qsTr('Valid!')\n                        : qsTr('Invalid!')\n                    color: _verified == SignVerifyMessageDialog.Check.Valid\n                        ? constants.colorDone\n                        : constants.colorError\n                }\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                ElTextArea {\n                    id: signature\n                    Layout.fillWidth: true\n                    Layout.maximumHeight: fontMetrics.lineSpacing * 4 + topPadding + bottomPadding\n                    Layout.minimumHeight: fontMetrics.lineSpacing * 4 + topPadding + bottomPadding\n                    font.family: FixedFont\n                    wrapMode: TextInput.Wrap\n                    inputMethodHints: Qt.ImhNoPredictiveText\n\n                    background: PaneInsetBackground {\n                        baseColor: _verified == SignVerifyMessageDialog.Check.Unknown\n                            ? constants.darkerDialogBackground\n                            : _verified == SignVerifyMessageDialog.Check.Valid\n                                ? constants.colorValidBackground\n                                : constants.colorInvalidBackground\n                    }\n                    onTextChanged: _verified = SignVerifyMessageDialog.Check.Unknown\n                }\n                ColumnLayout {\n                    Layout.alignment: Qt.AlignTop\n                    ToolButton {\n                        icon.source: '../../icons/paste.png'\n                        icon.color: 'transparent'\n                        onClicked: {\n                            signature.text = AppController.clipboardToText()\n                        }\n                    }\n                    ToolButton {\n                        icon.source: '../../icons/share.png'\n                        icon.color: enabled ? 'transparent' : Material.iconDisabledColor\n                        enabled: signature.text\n                        onClicked: {\n                            var dialog = app.genericShareDialog.createObject(app, {\n                                title: qsTr('Message signature'),\n                                text_qr: signature.text\n                            })\n                            dialog.open()\n                        }\n                    }\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Sign')\n                visible: Daemon.currentWallet.canSignMessage\n                enabled: _addressMine\n                icon.source: '../../icons/seal.png'\n                onClicked: {\n                    Daemon.currentWallet.signMessage(addressField.text, plaintext.text)\n                    // emits messageSigned(sig)\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                enabled: _addressValid && signature.text\n                text: qsTr('Verify')\n                icon.source: '../../icons/confirmed.png'\n                onClicked: {\n                    var result = Daemon.verifyMessage(addressField.text, plaintext.text, signature.text)\n                    _verified = result\n                        ? SignVerifyMessageDialog.Check.Valid\n                        : SignVerifyMessageDialog.Check.Invalid\n                }\n            }\n        }\n    }\n\n    Connections {\n        target: Daemon.currentWallet\n        function onMessageSigned(sig) {\n            signature.text = sig\n        }\n    }\n\n    Component.onCompleted: {\n        addressField.text = address\n    }\n\n    Bitcoin {\n        id: bitcoin\n    }\n\n    FontMetrics {\n        id: fontMetrics\n        font: signature.font\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/SwapDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nElDialog {\n    id: root\n\n    required property QtObject swaphelper\n\n    implicitHeight: parent.height\n    implicitWidth: parent.width\n\n    title: qsTr('Lightning Swap')\n    iconSource: Qt.resolvedUrl('../../icons/update.png')\n\n    padding: 0\n\n    ColumnLayout {\n        width: parent.width\n        height: parent.height\n        spacing: constants.paddingLarge\n\n        InfoTextArea {\n            id: userinfoText\n            Layout.leftMargin: constants.paddingXXLarge\n            Layout.rightMargin: constants.paddingXXLarge\n            Layout.fillWidth: true\n            Layout.alignment: Qt.AlignHCenter\n            visible: swaphelper.userinfo != ''\n            text: swaphelper.userinfo\n            iconStyle: swaphelper.state == SwapHelper.Started || swaphelper.state == SwapHelper.Initializing\n                ? InfoTextArea.IconStyle.Spinner\n                : swaphelper.state == SwapHelper.Failed || swaphelper.state == SwapHelper.Cancelled\n                    ? InfoTextArea.IconStyle.Error\n                    : swaphelper.state == SwapHelper.Success\n                        ? InfoTextArea.IconStyle.Done\n                        : swaphelper.state == SwapHelper.NoService\n                            ? InfoTextArea.IconStyle.Warn\n                            : InfoTextArea.IconStyle.Info\n        }\n\n        GridLayout {\n            id: layout\n            columns: 2\n            Layout.preferredWidth: parent.width\n            Layout.leftMargin: constants.paddingXXLarge\n            Layout.rightMargin: constants.paddingXXLarge\n\n            RowLayout {\n                Layout.preferredWidth: 1\n                Layout.fillWidth: true\n                Label {\n                    Layout.preferredWidth: 1\n                    Layout.fillWidth: true\n                    text: qsTr('You send')\n                    color: Material.accentColor\n                }\n                Image {\n                    Layout.preferredWidth: constants.iconSizeSmall\n                    Layout.preferredHeight: constants.iconSizeSmall\n                    source: swaphelper.isReverse\n                        ? '../../icons/lightning.png'\n                        : '../../icons/bitcoin.png'\n                }\n            }\n\n            RowLayout {\n                Layout.preferredWidth: 1\n                Layout.fillWidth: true\n                Label {\n                    id: tosend\n                    text: Config.formatSats(swaphelper.tosend)\n                    font.family: FixedFont\n                }\n                Label {\n                    text: Config.baseUnit\n                    color: Material.accentColor\n                }\n            }\n\n            RowLayout {\n                Layout.preferredWidth: 1\n                Layout.fillWidth: true\n                Label {\n                    Layout.preferredWidth: 1\n                    Layout.fillWidth: true\n                    text: qsTr('You receive')\n                    color: Material.accentColor\n                }\n                Image {\n                    Layout.preferredWidth: constants.iconSizeSmall\n                    Layout.preferredHeight: constants.iconSizeSmall\n                    source: swaphelper.isReverse\n                        ? '../../icons/bitcoin.png'\n                        : '../../icons/lightning.png'\n                }\n            }\n\n            RowLayout {\n                Layout.preferredWidth: 1\n                Layout.fillWidth: true\n                Label {\n                    id: toreceive\n                    text: Config.formatSats(swaphelper.toreceive)\n                    font.family: FixedFont\n                }\n                Label {\n                    text: Config.baseUnit\n                    color: Material.accentColor\n                }\n            }\n\n            Label {\n                Layout.preferredWidth: 1\n                Layout.fillWidth: true\n                text: qsTr('Server fee')\n                color: Material.accentColor\n            }\n\n            RowLayout {\n                Layout.preferredWidth: 1\n                Layout.fillWidth: true\n                Label {\n                    text: Config.formatSats(swaphelper.serverMiningfee)\n                    font.family: FixedFont\n                }\n                Label {\n                    text: Config.baseUnit\n                    color: Material.accentColor\n                }\n                Label {\n                    text: swaphelper.serverfeeperc\n                        ? '+ ' + swaphelper.serverfeeperc\n                        : ''\n                }\n            }\n\n            Label {\n                Layout.preferredWidth: 1\n                Layout.fillWidth: true\n                text: qsTr('Mining fee')\n                color: Material.accentColor\n            }\n\n            RowLayout {\n                Layout.preferredWidth: 1\n                Layout.fillWidth: true\n                Label {\n                    text: Config.formatSats(swaphelper.miningfee)\n                    font.family: FixedFont\n                    visible: swaphelper.valid\n                }\n                Label {\n                    text: Config.baseUnit\n                    color: Material.accentColor\n                    visible: swaphelper.valid\n                }\n            }\n        }\n\n        Slider {\n            id: swapslider\n            Layout.fillWidth: true\n\n            Layout.topMargin: constants.paddingLarge\n            Layout.bottomMargin: constants.paddingLarge\n            Layout.leftMargin: constants.paddingXXLarge + (parent.width - 2 * constants.paddingXXLarge) * swaphelper.leftVoid\n            Layout.rightMargin: constants.paddingXXLarge + (parent.width - 2 * constants.paddingXXLarge) * swaphelper.rightVoid\n\n            property real scenter: -swapslider.from / (swapslider.to - swapslider.from)\n\n            enabled: swaphelper.state == SwapHelper.ServiceReady || swaphelper.state == SwapHelper.Failed\n\n            background: Rectangle {\n                x: swapslider.leftPadding\n                y: swapslider.topPadding + swapslider.availableHeight / 2 - height / 2\n                implicitWidth: 200\n                implicitHeight: 4\n                width: swapslider.availableWidth\n                height: implicitHeight\n                radius: 2\n                color: enabled\n                    ? Material.accentColor\n                    : Material.sliderDisabledColor\n\n                // full width somehow misaligns with handle, define rangeWidth\n                property int rangeWidth: width - swapslider.leftPadding\n\n                Rectangle {\n                    x: swapslider.visualPosition > swapslider.scenter\n                        ? swapslider.scenter * parent.rangeWidth\n                        : swapslider.visualPosition * parent.rangeWidth\n                    y: enabled ? -1 : 0\n                    width: swapslider.visualPosition > swapslider.scenter\n                        ? (swapslider.visualPosition-swapslider.scenter) * parent.rangeWidth\n                        : (swapslider.scenter-swapslider.visualPosition) * parent.rangeWidth\n                    height: enabled ? parent.height + 2 : parent.height\n                    color: enabled\n                        ? constants.colorOk\n                        : Material.sliderDisabledColor\n                }\n\n                Rectangle {\n                    x: - (swapslider.parent.width - 2 * constants.paddingXXLarge) * swaphelper.leftVoid\n                    z: -1\n                    // width makes rectangle go outside the control, into the Layout margins\n                    width: swapslider.parent.width - 2 * constants.paddingXXLarge - swapslider.leftPadding - swapslider.rightPadding\n                    height: parent.height\n                    color: Material.sliderDisabledColor\n                }\n\n                Rectangle {\n                    x: swapslider.scenter * parent.rangeWidth\n                    y: -4\n                    width: 1\n                    height: parent.height + 2*4\n                    color: parent.color\n                }\n            }\n\n            from: swaphelper.rangeMin\n            to: swaphelper.rangeMax\n\n            onValueChanged: {\n                if (activeFocus)\n                    swaphelper.sliderPos = value\n            }\n        }\n\n        RowLayout {\n            Layout.fillWidth: true\n            Layout.topMargin: -constants.paddingXXLarge\n            Layout.leftMargin: constants.paddingXXLarge + swapslider.leftPadding\n            Layout.rightMargin: constants.paddingXXLarge + swapslider.rightPadding\n            Label {\n                text: '<-- ' + qsTr('Add receiving capacity')\n                font.pixelSize: constants.fontSizeXSmall\n                color: Material.accentColor\n            }\n            Label {\n                Layout.fillWidth: true\n                horizontalAlignment: Text.AlignRight\n                text: qsTr('Add sending capacity') + ' -->'\n                font.pixelSize: constants.fontSizeXSmall\n                color: Material.accentColor\n            }\n        }\n\n\n        Pane {\n            Layout.alignment: Qt.AlignHCenter\n            visible: _swaphelper.isNostr()\n            background: Rectangle { color: constants.darkerDialogBackground }\n            padding: 0\n\n            FlatButton {\n                text: qsTr('Choose swap provider')\n                enabled: _swaphelper.state != SwapHelper.Initializing\n                    && _swaphelper.state != SwapHelper.Started\n                    && _swaphelper.state != SwapHelper.Success\n                    && _swaphelper.availableSwapServers.count\n                onClicked: {\n                    var dialog = app.nostrSwapServersDialog.createObject(app, {\n                        swaphelper: _swaphelper,\n                        selectedPubkey: Config.swapServerNPub\n                    })\n                    dialog.accepted.connect(function() {\n                        if (Config.swapServerNPub != dialog.selectedPubkey) {\n                            Config.swapServerNPub = dialog.selectedPubkey\n                            _swaphelper.setReadyState()\n                        }\n                    })\n                    dialog.open()\n                }\n            }\n        }\n\n        Item { Layout.fillHeight: true; Layout.preferredWidth: 1 }\n\n        ButtonContainer {\n            Layout.columnSpan: 2\n            Layout.fillWidth: true\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Ok')\n                icon.source: Qt.resolvedUrl('../../icons/confirmed.png')\n                visible: !swaphelper.canCancel\n                enabled: swaphelper.valid && (swaphelper.state == SwapHelper.ServiceReady || swaphelper.state == SwapHelper.Failed)\n\n                onClicked: {\n                    swaphelper.executeSwap()\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Cancel')\n                icon.source: Qt.resolvedUrl('../../icons/closebutton.png')\n                visible: swaphelper.canCancel\n\n                onClicked: {\n                    swaphelper.cancelNormalSwap()\n                }\n            }\n        }\n    }\n\n    Connections {\n        target: swaphelper\n        function onSliderPosChanged() {\n            swapslider.value = swaphelper.sliderPos\n        }\n        function onStateChanged() {\n            if (swaphelper.state == SwapHelper.Success) {\n                var dialog = app.messageDialog.createObject(app, {\n                    title: qsTr('Success!'),\n                    text: Config.getTranslatedMessage(swaphelper.isReverse\n                            ? 'MSG_REVERSE_SWAP_FUNDING_MEMPOOL'\n                            : 'MSG_FORWARD_SWAP_FUNDING_MEMPOOL')\n                })\n                dialog.accepted.connect(function() {\n                    Qt.callLater(root.close)\n                })\n                dialog.open()\n            }\n        }\n    }\n\n    Component.onCompleted: {\n        swapslider.value = swaphelper.sliderPos\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/SweepDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\n\nimport org.electrum\n\nimport \"controls\"\n\nElDialog {\n    id: root\n\n    title: qsTr('Sweep private keys')\n    iconSource: Qt.resolvedUrl('../../icons/sweep.png')\n\n    property bool valid: false\n    property string privateKeys\n\n    width: parent.width\n    height: parent.height\n    padding: 0\n\n    function verifyPrivateKey(key) {\n        valid = false\n        validationtext.text = ''\n        key = key.trim()\n\n        if (!key) {\n            return false\n        }\n\n        if (!bitcoin.isPrivateKeyList(key)) {\n            validationtext.text = qsTr('Error: invalid private key(s)')\n            return false\n        }\n\n        return valid = true\n    }\n\n    function addPrivateKey(key) {\n        if (sweepkeys.text.includes(key))\n            return\n        if (sweepkeys.text && !sweepkeys.text.endsWith('\\n'))\n            sweepkeys.text = sweepkeys.text + '\\n'\n        sweepkeys.text = sweepkeys.text + key + '\\n'\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        ColumnLayout {\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n\n            ColumnLayout {\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n\n                RowLayout {\n                    Layout.fillWidth: true\n                    TextHighlightPane {\n                        Layout.fillWidth: true\n                        Label {\n                            text: qsTr('Enter the list of private keys to sweep into this wallet')\n                            width: parent.width\n                            wrapMode: Text.Wrap\n                        }\n                    }\n                    HelpButton {\n                        heading: qsTr('Sweep private keys')\n                        helptext: qsTr('This will create a transaction sending all funds associated with the private keys to the current wallet') +\n                        '<br/><br/>' + qsTr('WIF keys are typed in Electrum, based on script type.') + '<br/><br/>' +\n                        qsTr('A few examples') + ':<br/>' +\n                        '<tt><b>p2pkh</b>:KxZcY47uGp9a...       \\t-> 1DckmggQM...<br/>' +\n                        '<b>p2wpkh-p2sh</b>:KxZcY47uGp9a... \\t-> 3NhNeZQXF...<br/>' +\n                        '<b>p2wpkh</b>:KxZcY47uGp9a...      \\t-> bc1q3fjfk...</tt>'\n                    }\n                }\n                RowLayout {\n                    Layout.fillWidth: true\n                    Layout.fillHeight: true\n\n                    ElTextArea {\n                        id: sweepkeys\n                        Layout.fillWidth: true\n                        Layout.fillHeight: true\n                        Layout.minimumHeight: 160\n                        font.family: FixedFont\n                        wrapMode: TextEdit.WrapAnywhere\n                        onTextChanged: {\n                            if (anyActiveFocus) {\n                                verifyPrivateKey(text)\n                            }\n                        }\n                        inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase\n                        background: PaneInsetBackground {\n                            baseColor: constants.darkerDialogBackground\n                        }\n                    }\n                    ColumnLayout {\n                        Layout.alignment: Qt.AlignTop\n                        ToolButton {\n                            icon.source: '../../icons/paste.png'\n                            icon.height: constants.iconSizeMedium\n                            icon.width: constants.iconSizeMedium\n                            onClicked: {\n                                if (verifyPrivateKey(AppController.clipboardToText()))\n                                    addPrivateKey(AppController.clipboardToText())\n                            }\n                        }\n                        ToolButton {\n                            icon.source: '../../icons/qrcode.png'\n                            icon.height: constants.iconSizeMedium\n                            icon.width: constants.iconSizeMedium\n                            scale: 1.2\n                            onClicked: {\n                                var dialog = app.scanDialog.createObject(app, {\n                                    hint: qsTr('Scan a private key')\n                                })\n                                dialog.onFoundText.connect(function(data) {\n                                    if (verifyPrivateKey(data))\n                                        addPrivateKey(data)\n                                    dialog.close()\n                                })\n                                dialog.open()\n                            }\n                        }\n                    }\n                }\n\n                InfoTextArea {\n                    id: validationtext\n                    iconStyle: InfoTextArea.IconStyle.Warn\n                    Layout.fillWidth: true\n                    Layout.margins: constants.paddingMedium\n                    visible: text\n                }\n            }\n        }\n\n        FlatButton {\n            Layout.fillWidth: true\n            Layout.preferredWidth: 1\n            enabled: valid\n            icon.source: '../../icons/tab_send.png'\n            text: qsTr('Sweep...')\n            onClicked: {\n                console.log('sweeping')\n                root.privateKeys = sweepkeys.text\n                root.accept()\n            }\n        }\n\n    }\n\n    Bitcoin {\n        id: bitcoin\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/TermsOfUseWizard.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport \"wizard\"\n\nWizard {\n    id: termsofusewizard\n\n    wizardTitle: \"\"\n    iconSource: \"\"\n    header: null\n\n    enter: null // disable transition\n\n    wiz: Daemon.termsOfUseWizard\n    finishButtonText: qsTr('Next')\n\n    Component.onCompleted: {\n        var view = wiz.startWizard()\n        _loadNextComponent(view)\n    }\n}\n\n"
  },
  {
    "path": "electrum/gui/qml/components/TxDetails.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: root\n    width: parent.width\n    height: parent.height\n    padding: 0\n\n    property string txid\n    property string rawtx\n    property alias label: txdetails.label\n\n    signal detailsChanged\n    signal closed\n\n    function close() {\n        app.stack.pop()\n    }\n\n    StackView.onRemoved: {\n        closed()\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            contentHeight: flickableRoot.height\n            clip: true\n            interactive: height < contentHeight\n\n            Pane {\n                id: flickableRoot\n                width: parent.width\n                padding: constants.paddingLarge\n\n                GridLayout {\n                    width: parent.width\n                    columns: 2\n\n                    Heading {\n                        Layout.columnSpan: 2\n                        text: qsTr('On-chain Transaction')\n                    }\n\n                    InfoTextArea {\n                        id: warn\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        Layout.bottomMargin: constants.paddingLarge\n                        visible: txdetails.warning\n                        text: txdetails.warning\n                        iconStyle: InfoTextArea.IconStyle.Warn\n                    }\n\n                    InfoTextArea {\n                        id: txinfo\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        Layout.bottomMargin: constants.paddingLarge\n                        visible: (txdetails.isUnrelated || !txdetails.isMined) && !broadcastinfo.visible\n                        text: txdetails.isUnrelated\n                            ? qsTr('Transaction is unrelated to this wallet.')\n                            : txdetails.isRemoved ? qsTr('This transaction has been replaced or removed and is no longer valid')\n                            : txdetails.inMempool\n                                ? qsTr('This transaction is still unconfirmed.') +\n                                    (txdetails.canBump || txdetails.canCancel\n                                        ? txdetails.canCancel\n                                            ? '\\n' + qsTr('You can bump its fee to speed up its confirmation, or cancel this transaction.')\n                                            : '\\n' + qsTr('You can bump its fee to speed up its confirmation.')\n                                        : '')\n                                : txdetails.lockDelay\n                                    ? qsTr('This transaction is local to your wallet and locked for the next %1 blocks.').arg(txdetails.lockDelay)\n                                    : txdetails.isComplete\n                                        ? qsTr('This transaction is local to your wallet. It has not been published yet.')\n                                        : txdetails.canSign\n                                            ? qsTr('This transaction is not fully signed and can be signed by this wallet.')\n                                            : qsTr('This transaction is not fully signed.') + '\\n' +\n                                              (txdetails.wallet.isWatchOnly\n                                                  ? qsTr('Present this transaction to the signing wallet.')\n                                                  : qsTr('Present this transaction to the next cosigner.'))\n                        iconStyle: txdetails.isUnrelated || txdetails.isRemoved\n                            ? InfoTextArea.IconStyle.Warn\n                            : InfoTextArea.IconStyle.Info\n                    }\n\n                    InfoTextArea {\n                        id: broadcastinfo\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n                        Layout.bottomMargin: constants.paddingLarge\n                        visible: text\n                    }\n\n                    Label {\n                        Layout.preferredWidth: 1\n                        Layout.fillWidth: true\n                        visible: !txdetails.isUnrelated && txdetails.amount.satsInt != 0\n                        text: txdetails.amount.satsInt > 0\n                                ? qsTr('Amount received onchain')\n                                : qsTr('Amount sent onchain')\n                        color: Material.accentColor\n                        wrapMode: Text.Wrap\n                    }\n\n                    FormattedAmount {\n                        Layout.preferredWidth: 1\n                        Layout.fillWidth: true\n                        visible: !txdetails.isUnrelated && txdetails.amount.satsInt != 0\n                        amount: txdetails.amount\n                        timestamp: txdetails.timestamp\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        visible: !txdetails.isUnrelated && txdetails.lnAmount.satsInt != 0\n                        text: txdetails.lnAmount.satsInt > 0\n                                ? qsTr('Amount received in channels')\n                                : qsTr('Amount withdrawn from channels')\n                        color: Material.accentColor\n                        wrapMode: Text.Wrap\n                    }\n\n                    FormattedAmount {\n                        visible: !txdetails.isUnrelated && txdetails.lnAmount.satsInt != 0\n                        Layout.preferredWidth: 1\n                        Layout.fillWidth: true\n                        amount: txdetails.lnAmount.isEmpty ? txdetails.amount : txdetails.lnAmount\n                        timestamp: txdetails.timestamp\n                    }\n\n                    Label {\n                        visible: !txdetails.fee.isEmpty\n                        text: qsTr('Transaction fee')\n                        color: Material.accentColor\n                    }\n\n                    RowLayout {\n                        Layout.fillWidth: true\n                        visible: !txdetails.fee.isEmpty\n                        FormattedAmount {\n                            Layout.fillWidth: true\n                            amount: txdetails.fee\n                            timestamp: txdetails.timestamp\n                        }\n                    }\n\n                    Label {\n                        Layout.preferredWidth: 1\n                        Layout.fillWidth: true\n                        visible: txdetails.feeRateStr != \"\"\n                        text: qsTr('Transaction fee rate')\n                        color: Material.accentColor\n                        wrapMode: Text.Wrap\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        visible: txdetails.feeRateStr != \"\"\n                        text: txdetails.feeRateStr\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        text: qsTr('Status')\n                        color: Material.accentColor\n                    }\n\n                    Label {\n                        Layout.fillWidth: true\n                        text: txdetails.status\n                    }\n\n                    Label {\n                        text: qsTr('Mempool depth')\n                        color: Material.accentColor\n                        visible: txdetails.mempoolDepth\n                    }\n\n                    Label {\n                        text: txdetails.mempoolDepth\n                        visible: txdetails.mempoolDepth\n                    }\n\n                    Label {\n                        visible: txdetails.isMined\n                        text: qsTr('Date')\n                        color: Material.accentColor\n                    }\n\n                    Label {\n                        visible: txdetails.isMined\n                        text: txdetails.date\n                    }\n\n                    Label {\n                        Layout.columnSpan: 2\n                        Layout.topMargin: constants.paddingSmall\n                        visible: !txdetails.isUnrelated\n                        text: qsTr('Label')\n                        color: Material.accentColor\n                    }\n\n                    TextHighlightPane {\n                        id: labelContent\n\n                        property bool editmode: false\n\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        visible: !txdetails.isUnrelated\n\n                        RowLayout {\n                            width: parent.width\n                            Label {\n                                visible: !labelContent.editmode\n                                text: txdetails.label\n                                wrapMode: Text.Wrap\n                                Layout.fillWidth: true\n                                font.pixelSize: constants.fontSizeLarge\n                            }\n                            ToolButton {\n                                visible: !labelContent.editmode\n                                icon.source: '../../icons/pen.png'\n                                icon.color: 'transparent'\n                                onClicked: {\n                                    labelEdit.text = txdetails.label\n                                    labelContent.editmode = true\n                                    labelEdit.focus = true\n                                }\n                            }\n                            TextField {\n                                id: labelEdit\n                                visible: labelContent.editmode\n                                text: txdetails.label\n                                font.pixelSize: constants.fontSizeLarge\n                                Layout.fillWidth: true\n                            }\n                            ToolButton {\n                                visible: labelContent.editmode\n                                icon.source: '../../icons/confirmed.png'\n                                icon.color: 'transparent'\n                                onClicked: {\n                                    labelContent.editmode = false\n                                    txdetails.setLabel(labelEdit.text)\n                                }\n                            }\n                            ToolButton {\n                                visible: labelContent.editmode\n                                icon.source: '../../icons/closebutton.png'\n                                icon.color: 'transparent'\n                                onClicked: labelContent.editmode = false\n                            }\n                        }\n                    }\n\n                    Heading {\n                        Layout.columnSpan: 2\n                        text: qsTr('Technical properties')\n                    }\n\n                    Label {\n                        visible: txdetails.isMined\n                        text: qsTr('Mined at')\n                        color: Material.accentColor\n                    }\n\n                    Label {\n                        visible: txdetails.isMined\n                        text: txdetails.shortId\n                    }\n\n                    Label {\n                        Layout.columnSpan: 2\n                        Layout.topMargin: constants.paddingSmall\n                        text: qsTr('Transaction ID')\n                        color: Material.accentColor\n                    }\n\n                    TextHighlightPane {\n                        Layout.columnSpan: 2\n                        Layout.fillWidth: true\n\n                        RowLayout {\n                            width: parent.width\n                            Label {\n                                text: txdetails.txid\n                                font.pixelSize: constants.fontSizeLarge\n                                font.family: FixedFont\n                                Layout.fillWidth: true\n                                wrapMode: Text.Wrap\n                            }\n                            ToolButton {\n                                icon.source: '../../icons/share.png'\n                                icon.color: 'transparent'\n                                enabled: txdetails.txid\n                                onClicked: {\n                                    var dialog = app.genericShareDialog.createObject(root,\n                                        { title: qsTr('Transaction ID'), text: txdetails.txid }\n                                    )\n                                    dialog.open()\n                                }\n                            }\n                        }\n                    }\n\n                    ToggleLabel {\n                        id: inputs_label\n                        Layout.columnSpan: 2\n                        Layout.topMargin: constants.paddingMedium\n\n                        labelText: qsTr('Inputs (%1)').arg(txdetails.inputs.length)\n                        color: Material.accentColor\n                    }\n\n                    Repeater {\n                        model: inputs_label.collapsed\n                            ? undefined\n                            : txdetails.inputs\n                        delegate: TxInput {\n                            Layout.columnSpan: 2\n                            Layout.fillWidth: true\n\n                            idx: index\n                            model: modelData\n                        }\n                    }\n\n                    ToggleLabel {\n                        id: outputs_label\n                        Layout.columnSpan: 2\n                        Layout.topMargin: constants.paddingMedium\n\n                        labelText: qsTr('Outputs (%1)').arg(txdetails.outputs.length)\n                        color: Material.accentColor\n                    }\n\n                    Repeater {\n                        model: outputs_label.collapsed\n                            ? undefined\n                            : txdetails.outputs\n                        delegate: TxOutput {\n                            Layout.columnSpan: 2\n                            Layout.fillWidth: true\n\n                            idx: index\n                            model: modelData\n                        }\n                    }\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                id: feebumpButton\n                icon.source: '../../icons/add.png'\n                text: qsTr('Bump fee...')\n                visible: txdetails.canBump || txdetails.canCpfp\n                onClicked: {\n                    if (txdetails.canBump) {\n                        var dialog = rbfBumpFeeDialog.createObject(root, { txid: txdetails.txid })\n                    } else {\n                        var dialog = cpfpBumpFeeDialog.createObject(root, { txid: txdetails.txid })\n                    }\n                    dialog.open()\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                id: cancelButton\n                icon.source: '../../icons/closebutton.png'\n                text: qsTr('Cancel Tx...')\n                visible: txdetails.canCancel\n                onClicked: {\n                    var dialog = rbfCancelDialog.createObject(root, { txid: txdetails.txid })\n                    dialog.open()\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                icon.source: '../../icons/key.png'\n                text: txdetails.shouldConfirm\n                    ? qsTr('Sign...')\n                    : qsTr('Sign')\n                visible: txdetails.canSign\n                onClicked: {\n                    if (txdetails.shouldConfirm) {\n                        var dialog = app.messageDialog.createObject(app, {\n                            text: qsTr('Confirm signing transaction despite warnings?'),\n                            yesno: true\n                        })\n                        dialog.accepted.connect(function() {\n                            txdetails.sign()\n                        })\n                        dialog.open()\n                    } else {\n                        txdetails.sign()\n                    }\n                }\n            }\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                icon.source: '../../icons/tab_send.png'\n                text: qsTr('Broadcast')\n                visible: txdetails.canBroadcast\n                enabled: !txdetails.lockDelay\n                onClicked: txdetails.broadcast()\n            }\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                icon.source: '../../icons/qrcode_white.png'\n                text: qsTr('Share...')\n                enabled: !txdetails.isUnrelated && !txdetails.isRemoved\n                onClicked: {\n                    var msg = ''\n                    if (txdetails.isComplete) {\n                        if (!txdetails.isMined && !txdetails.mempoolDepth) // local\n                            if (txdetails.lockDelay) {\n                                msg = qsTr('This transaction is fully signed, but can only be broadcast after %1 blocks.').arg(txdetails.lockDelay)\n                            } else {\n                                // TODO: iff offline wallet?\n                                // TODO: or also if just temporarily offline?\n                                msg = qsTr('This transaction is fully signed, but has not been broadcast yet.')\n                            }\n                    } else if (txdetails.wallet.isWatchOnly) {\n                        msg = qsTr('This transaction should be signed. Present this QR code to the signing device')\n                    } else if (txdetails.wallet.isMultisig && txdetails.wallet.walletType != '2fa') {\n                        if (txdetails.canSign) {\n                            msg = qsTr('Note: this wallet can sign, but has not signed this transaction yet')\n                        } else {\n                            msg = qsTr('Transaction is partially signed by this wallet. Present this QR code to the next co-signer')\n                        }\n                    }\n\n                    app.stack.getRoot().showExport(txdetails.getSerializedTx(), msg)\n                }\n            }\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                icon.source: '../../icons/save.png'\n                text: qsTr('Save')\n                visible: txdetails.canSaveAsLocal\n                onClicked: txdetails.save()\n            }\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                icon.source: '../../icons/delete.png'\n                text: qsTr('Remove...')\n                visible: txdetails.canRemove\n                onClicked: txdetails.removeLocalTx()\n            }\n\n        }\n\n    }\n\n    TxDetails {\n        id: txdetails\n        wallet: Daemon.currentWallet\n        onLabelChanged: root.detailsChanged()\n        onConfirmRemoveLocalTx: (message) => {\n            var dialog = app.messageDialog.createObject(app, { text: message, yesno: true })\n            dialog.accepted.connect(function() {\n                txdetails.removeLocalTx(true)\n                root.enabled = false\n            })\n            dialog.open()\n        }\n        Component.onCompleted: {\n            if (root.txid) {\n                txdetails.txid = root.txid\n            } else if (root.rawtx) {\n                txdetails.rawtx = root.rawtx\n            }\n        }\n    }\n\n    Connections {\n        target: Daemon.currentWallet\n        function onSaveTxSuccess(txid) {\n            if (txid != txdetails.txid)\n                return\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Transaction added to wallet history.'),\n                text: qsTr('Note: this is an offline transaction, if you want the network to see it, you need to broadcast it.')\n            })\n            dialog.open()\n            root.close()\n        }\n        function onSaveTxError(txid, code, message) {\n            if (txid != txdetails.txid)\n                return\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: message\n            })\n            dialog.open()\n        }\n        function onBroadcastSucceeded() {\n            broadcastinfo.text = qsTr('Transaction was broadcast successfully')\n            broadcastinfo.iconStyle = InfoTextArea.IconStyle.Info\n        }\n        function onBroadcastFailed(txid, code, message) {\n            broadcastinfo.text = qsTr('Broadcast of transaction failed')\n            broadcastinfo.iconStyle = InfoTextArea.IconStyle.Warn\n        }\n    }\n\n    Component {\n        id: rbfBumpFeeDialog\n        RbfBumpFeeDialog {\n            id: dialog\n            required property string txid\n            rbffeebumper: TxRbfFeeBumper {\n                id: rbffeebumper\n                wallet: Daemon.currentWallet\n                txid: dialog.txid\n            }\n            onAccepted: {\n                txdetails.rawtx = rbffeebumper.getNewTx()\n                if (txdetails.wallet.canSignWithoutCosigner) {\n                    txdetails.signAndBroadcast()\n                } else {\n                    var dialog = app.messageDialog.createObject(app, {\n                        title: qsTr('Transaction fee updated.'),\n                        text: qsTr('You still need to sign and broadcast this transaction.')\n                    })\n                    dialog.open()\n                }\n            }\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: cpfpBumpFeeDialog\n        CpfpBumpFeeDialog {\n            id: dialog\n            required property string txid\n            cpfpfeebumper: TxCpfpFeeBumper {\n                id: cpfpfeebumper\n                wallet: Daemon.currentWallet\n                txid: dialog.txid\n            }\n\n            onAccepted: {\n                // replaces parent tx with cpfp tx\n                txdetails.rawtx = cpfpfeebumper.getNewTx()\n                if (txdetails.wallet.canSignWithoutCosigner) {\n                    txdetails.signAndBroadcast()\n                } else {\n                    var dialog = app.messageDialog.createObject(app, {\n                        title: qsTr('CPFP fee bump transaction created.'),\n                        text: qsTr('You still need to sign and broadcast this transaction.')\n                    })\n                    dialog.open()\n                }\n            }\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: rbfCancelDialog\n        RbfCancelDialog {\n            id: dialog\n            required property string txid\n            txcanceller: TxCanceller {\n                id: txcanceller\n                wallet: Daemon.currentWallet\n                txid: dialog.txid\n            }\n\n            onAccepted: {\n                txdetails.rawtx = txcanceller.getNewTx()\n                if (txdetails.wallet.canSignWithoutCosigner) {\n                    txdetails.signAndBroadcast()\n                } else {\n                    var dialog = app.messageDialog.createObject(app, {\n                        title: qsTr('Cancel transaction created.'),\n                        text: qsTr('You still need to sign and broadcast this transaction.')\n                    })\n                    dialog.open()\n                }\n            }\n            onClosed: destroy()\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/WalletDetails.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: rootItem\n    objectName: 'WalletDetails'\n\n    padding: 0\n\n    property bool _is2fa: Daemon.currentWallet && Daemon.currentWallet.walletType == '2fa'\n\n    function enableLightning() {\n        var dialog = app.messageDialog.createObject(rootItem, {\n            title: qsTr('Enable Lightning for this wallet?'),\n            yesno: true\n        })\n        dialog.accepted.connect(function() {\n            Daemon.currentWallet.enableLightning()\n        })\n        dialog.open()\n    }\n\n    function importAddressesKeys() {\n        var dialog = importAddressesKeysDialog.createObject(rootItem)\n        dialog.open()\n    }\n\n    ColumnLayout {\n        id: rootLayout\n        anchors.fill: parent\n        spacing: 0\n\n        Flickable {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            contentHeight: flickableRoot.height\n            clip: true\n            interactive: height < contentHeight\n\n            Pane {\n                id: flickableRoot\n                width: parent.width\n                padding: constants.paddingLarge\n\n                ColumnLayout {\n                    width: parent.width\n                    spacing: constants.paddingLarge\n\n                    Heading {\n                        text: qsTr('Wallet details')\n                    }\n\n                    GridLayout {\n                        columns: 3\n                        Layout.alignment: Qt.AlignHCenter\n\n                        Tag {\n                            Layout.alignment: Qt.AlignHCenter\n                            text: Daemon.currentWallet.walletType\n                            font.pixelSize: constants.fontSizeSmall\n                            font.bold: true\n                            iconSource: '../../../icons/wallet.png'\n                        }\n                        Tag {\n                            Layout.alignment: Qt.AlignHCenter\n                            text: Daemon.currentWallet.txinType\n                            font.pixelSize: constants.fontSizeSmall\n                            font.bold: true\n                            iconSource: '../../../icons/script_white.png'\n                        }\n                        Tag {\n                            Layout.alignment: Qt.AlignHCenter\n                            text: qsTr('HD')\n                            visible: Daemon.currentWallet.isDeterministic\n                            font.pixelSize: constants.fontSizeSmall\n                            font.bold: true\n                            iconSource: '../../../icons/hd_white.png'\n                        }\n                        Tag {\n                            Layout.alignment: Qt.AlignHCenter\n                            text: qsTr('Watch only')\n                            visible: Daemon.currentWallet.isWatchOnly\n                            font.pixelSize: constants.fontSizeSmall\n                            font.bold: true\n                            iconSource: '../../../icons/eye1.png'\n                        }\n                        Tag {\n                            Layout.alignment: Qt.AlignHCenter\n                            text: qsTr('Encrypted')\n                            visible: Daemon.currentWallet.isEncrypted\n                            font.pixelSize: constants.fontSizeSmall\n                            font.bold: true\n                            iconSource: '../../../icons/key.png'\n                        }\n                        Tag {\n                            Layout.alignment: Qt.AlignHCenter\n                            text: qsTr('HW')\n                            visible: Daemon.currentWallet.isHardware\n                            font.pixelSize: constants.fontSizeSmall\n                            font.bold: true\n                            iconSource: '../../../icons/seed.png'\n                        }\n                        Tag {\n                            Layout.alignment: Qt.AlignHCenter\n                            text: qsTr('Lightning')\n                            visible: Daemon.currentWallet.isLightning\n                            font.pixelSize: constants.fontSizeSmall\n                            font.bold: true\n                            iconSource: '../../../icons/lightning.png'\n                        }\n                        Tag {\n                            Layout.alignment: Qt.AlignHCenter\n                            text: qsTr('Seed')\n                            visible: Daemon.currentWallet.hasSeed\n                            font.pixelSize: constants.fontSizeSmall\n                            font.bold: true\n                            iconSource: '../../../icons/seed.png'\n                        }\n                    }\n\n                    GridLayout {\n                        Layout.preferredWidth: parent.width\n                        visible: Daemon.currentWallet\n                        columns: 2\n\n                        Label {\n                            Layout.columnSpan: 2\n                            Layout.topMargin: constants.paddingSmall\n                            visible: Daemon.currentWallet.hasSeed\n                            text: qsTr('Seed')\n                            color: Material.accentColor\n                        }\n\n                        TextHighlightPane {\n                            Layout.columnSpan: 2\n                            Layout.fillWidth: true\n                            visible: Daemon.currentWallet.hasSeed\n                            RowLayout {\n                                width: parent.width\n                                Label {\n                                    id: seedText\n                                    visible: false\n                                    Layout.fillWidth: true\n                                    text: Daemon.currentWallet.seed\n                                    wrapMode: Text.Wrap\n                                    font.family: FixedFont\n                                    font.pixelSize: constants.fontSizeMedium\n                                }\n                                Label {\n                                    id: showSeedText\n                                    Layout.fillWidth: true\n                                    horizontalAlignment: Text.AlignHCenter\n                                    text: qsTr('Tap to show seed')\n                                    wrapMode: Text.Wrap\n                                    font.pixelSize: constants.fontSizeLarge\n                                }\n                                MouseArea {\n                                    anchors.fill: parent\n                                    onClicked: {\n                                        if (showSeedText.visible) {\n                                            Daemon.currentWallet.requestShowSeed()\n                                        } else {\n                                            seedText.visible = false\n                                            showSeedText.visible = true\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        Label {\n                            id: seed_extension_label\n                            Layout.columnSpan: 2\n                            Layout.topMargin: constants.paddingSmall\n                            visible: seedText.visible && Daemon.currentWallet.seedPassphrase\n                            text: qsTr('Seed Extension')\n                            color: Material.accentColor\n                        }\n\n                        TextHighlightPane {\n                            Layout.columnSpan: 2\n                            Layout.fillWidth: true\n                            visible: seed_extension_label.visible\n                            Label {\n                                width: parent.width\n                                text: Daemon.currentWallet.seedPassphrase\n                                wrapMode: Text.Wrap\n                                font.family: FixedFont\n                                font.pixelSize: constants.fontSizeMedium\n                            }\n                        }\n\n                        Label {\n                            Layout.columnSpan: 2\n                            Layout.topMargin: constants.paddingSmall\n                            visible: Daemon.currentWallet.isLightning\n                            text: qsTr('Lightning Node ID')\n                            color: Material.accentColor\n                        }\n\n                        TextHighlightPane {\n                            Layout.columnSpan: 2\n                            Layout.fillWidth: true\n                            visible: Daemon.currentWallet.isLightning\n\n                            RowLayout {\n                                width: parent.width\n                                Label {\n                                    Layout.fillWidth: true\n                                    text: Daemon.currentWallet.lightningNodePubkey\n                                    wrapMode: Text.Wrap\n                                    font.family: FixedFont\n                                    font.pixelSize: constants.fontSizeMedium\n                                }\n                                ToolButton {\n                                    icon.source: '../../icons/share.png'\n                                    icon.color: 'transparent'\n                                    onClicked: {\n                                        var dialog = app.genericShareDialog.createObject(rootItem, {\n                                            title: qsTr('Lightning Node ID'),\n                                            text: Daemon.currentWallet.lightningNodePubkey\n                                        })\n                                        dialog.open()\n                                    }\n                                }\n                            }\n                        }\n\n                        Label {\n                            visible: _is2fa\n                            text: qsTr('2FA')\n                            color: Material.accentColor\n                        }\n\n                        Label {\n                            Layout.fillWidth: true\n                            visible: _is2fa\n                            text: Daemon.currentWallet.canSignWithoutServer\n                                    ? qsTr('disabled (can sign without server)')\n                                    : qsTr('enabled')\n                        }\n\n                        Label {\n                            visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer\n                            text: qsTr('Remaining TX')\n                            color: Material.accentColor\n                        }\n\n                        Label {\n                            Layout.fillWidth: true\n                            visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer\n                            text: 'tx_remaining' in Daemon.currentWallet.billingInfo\n                                    ? Daemon.currentWallet.billingInfo['tx_remaining']\n                                    : qsTr('unknown')\n                        }\n\n                        Label {\n                            Layout.columnSpan: 2\n                            Layout.topMargin: constants.paddingSmall\n                            visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer\n                            text: qsTr('Billing')\n                            color: Material.accentColor\n                        }\n\n                        TextHighlightPane {\n                            Layout.columnSpan: 2\n                            Layout.fillWidth: true\n                            visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer\n\n                            ColumnLayout {\n                                spacing: 0\n\n                                ButtonGroup {\n                                    id: billinggroup\n                                    onCheckedButtonChanged: {\n                                        Config.trustedcoinPrepay = checkedButton.value\n                                    }\n                                }\n\n                                Repeater {\n                                    model: AppController.plugin('trustedcoin').billingModel\n                                    delegate: RowLayout {\n                                        RadioButton {\n                                            ButtonGroup.group: billinggroup\n                                            property string value: modelData.value\n                                            text: modelData.text\n                                            checked: modelData.value == Config.trustedcoinPrepay\n                                        }\n                                        Label {\n                                            text: Config.formatSats(modelData.sats_per_tx)\n                                            font.family: FixedFont\n                                        }\n                                        Label {\n                                            text: Config.baseUnit + '/tx'\n                                            color: Material.accentColor\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        Repeater {\n                            id: keystores\n                            model: Daemon.currentWallet.keystores\n                            delegate: ColumnLayout {\n                                Layout.columnSpan: 2\n                                Layout.topMargin: constants.paddingSmall\n                                RowLayout {\n                                    Label {\n                                        text: qsTr('Keystore')\n                                        color: Material.accentColor\n                                    }\n                                    Label {\n                                        text: '#' + index\n                                        visible: keystores.count > 1\n                                    }\n                                    Image {\n                                        Layout.preferredWidth: constants.iconSizeXSmall\n                                        Layout.preferredHeight: constants.iconSizeXSmall\n                                        source: modelData.watch_only ? '../../icons/eye1.png' : '../../icons/key.png'\n                                    }\n                                }\n                                TextHighlightPane {\n                                    Layout.fillWidth: true\n                                    leftPadding: constants.paddingLarge\n\n                                    GridLayout {\n                                        width: parent.width\n                                        columns: 2\n\n                                        Label {\n                                            text: qsTr('Keystore type')\n                                            visible: modelData.keystore_type\n                                            color: Material.accentColor\n                                        }\n                                        Label {\n                                            Layout.fillWidth: true\n                                            text: modelData.keystore_type\n                                            visible: modelData.keystore_type\n                                        }\n\n                                        Label {\n                                            text: modelData.watch_only\n                                                ? qsTr('Imported addresses')\n                                                : qsTr('Imported keys')\n                                            visible: modelData.num_imported\n                                            color: Material.accentColor\n                                        }\n                                        Label {\n                                            Layout.fillWidth: true\n                                            text: modelData.num_imported\n                                            visible: modelData.num_imported\n                                        }\n\n                                        Label {\n                                            text: qsTr('Derivation prefix')\n                                            visible: modelData.derivation_prefix\n                                            color: Material.accentColor\n                                        }\n                                        Label {\n                                            Layout.fillWidth: true\n                                            text: modelData.derivation_prefix\n                                            visible: modelData.derivation_prefix\n                                            font.family: FixedFont\n                                        }\n\n                                        Label {\n                                            text: qsTr('BIP32 fingerprint')\n                                            visible: modelData.fingerprint\n                                            color: Material.accentColor\n                                        }\n                                        Label {\n                                            Layout.fillWidth: true\n                                            text: modelData.fingerprint\n                                            visible: modelData.fingerprint\n                                            font.family: FixedFont\n                                        }\n\n                                        Label {\n                                            Layout.columnSpan: 2\n                                            visible: modelData.master_pubkey\n                                            text: qsTr('Master Public Key')\n                                            color: Material.accentColor\n                                        }\n                                        RowLayout {\n                                            Layout.fillWidth: true\n                                            Layout.columnSpan: 2\n                                            Layout.leftMargin: constants.paddingLarge\n                                            visible: modelData.master_pubkey\n                                            Label {\n                                                text: modelData.master_pubkey\n                                                wrapMode: Text.Wrap\n                                                Layout.fillWidth: true\n                                                font.family: FixedFont\n                                                font.pixelSize: constants.fontSizeMedium\n                                            }\n                                            ToolButton {\n                                                icon.source: '../../icons/share.png'\n                                                icon.color: 'transparent'\n                                                onClicked: {\n                                                    var dialog = app.genericShareDialog.createObject(rootItem, {\n                                                        title: qsTr('Master Public Key'),\n                                                        text: modelData.master_pubkey\n                                                    })\n                                                    dialog.open()\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                    }\n                }\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Delete Wallet...')\n                onClicked: Daemon.checkThenDeleteWallet(Daemon.currentWallet)\n                icon.source: '../../icons/delete.png'\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Change Password')\n                onClicked: Daemon.startChangePassword()\n                icon.source: '../../icons/lock.png'\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                visible: Daemon.currentWallet.walletType == 'imported'\n                text: Daemon.currentWallet.isWatchOnly\n                        ? qsTr('Add addresses')\n                        : qsTr('Add keys')\n                icon.source: '../../icons/add.png'\n                onClicked: rootItem.importAddressesKeys()\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Enable Lightning')\n                onClicked: rootItem.enableLightning()\n                visible: Daemon.currentWallet && Daemon.currentWallet.canHaveLightning && !Daemon.currentWallet.isLightning\n                icon.source: '../../icons/lightning.png'\n            }\n        }\n    }\n\n    Connections {\n        target: Daemon\n        function onWalletLoaded() {\n            Daemon.availableWallets.reload()\n            app.stack.pop()\n        }\n        function onRequestNewPassword() { // new unified password (all wallets)\n            var dialog = app.passwordDialog.createObject(app, {\n                confirmPassword: true,\n                title: qsTr('Enter new password'),\n                infotext: qsTr('If you forget your password, you\\'ll need to restore from seed. Please make sure you have your seed stored safely')\n            })\n            dialog.passwordEntered.connect(function(password) {\n                dialog.close()\n                var success = Daemon.setPassword(password)\n                if (success && Biometrics.isEnabled) {\n                    if (Biometrics.isAvailable) {\n                        // also update the biometric authentication\n                        Biometrics.enable(password)\n                    } else {\n                        // disable biometric authentication as it is not available\n                        Biometrics.disable()\n                    }\n                }\n                var done_dialog = app.messageDialog.createObject(app, {\n                    title: success ? qsTr('Success') : qsTr('Error'),\n                    iconSource: success\n                        ? Qt.resolvedUrl('../../icons/info.png')\n                        : Qt.resolvedUrl('../../icons/warning.png'),\n                    text: success ? qsTr('Password changed') : qsTr('Password change failed')\n                })\n                done_dialog.open()\n            })\n            dialog.open()\n        }\n        function onWalletDeleteError(code, message) {\n            if (code == 'unpaid_requests') {\n                var dialog = app.messageDialog.createObject(app, {\n                    title: qsTr('Warning'),\n                    text: message,\n                    yesno: true\n                })\n                dialog.accepted.connect(function() {\n                    Daemon.checkThenDeleteWallet(Daemon.currentWallet, true)\n                })\n                dialog.open()\n            } else if (code == 'balance') {\n                var dialog = app.messageDialog.createObject(app, {\n                    title: qsTr('Warning'),\n                    text: message,\n                    yesno: true\n                })\n                dialog.accepted.connect(function() {\n                    Daemon.checkThenDeleteWallet(Daemon.currentWallet, true, true)\n                })\n                dialog.open()\n            } else {\n                var dialog = app.messageDialog.createObject(app, {\n                    title: qsTr('Error'),\n                    iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                    text: message\n                })\n                dialog.open()\n            }\n        }\n    }\n\n    Connections {\n        target: Daemon.currentWallet\n        function onRequestNewPassword() { // new wallet password\n            var dialog = app.passwordDialog.createObject(app, {\n                confirmPassword: true,\n                title: qsTr('Enter new password'),\n                infotext: qsTr('If you forget your password, you\\'ll need to restore from seed. Please make sure you have your seed stored safely')\n                        + (Daemon.availableWallets.rowCount() > 1 && Config.walletShouldUseSinglePassword\n                        ? \"\\n\\n\" + qsTr('The new password needs to match the password of any other existing wallet.')\n                        : \"\")\n            })\n            dialog.passwordEntered.connect(function(password) {\n                if (Config.walletShouldUseSinglePassword  // android\n                        && Daemon.availableWallets.rowCount() > 1  // has more than one wallet\n                        && Daemon.numWalletsWithPassword(password) < 1  // no other wallet uses this new password\n                ) {\n                    dialog.errorMessage = [\n                        qsTr('You need to use the password of any other existing wallet.'),\n                        qsTr('Using different wallet passwords is not supported.'),\n                    ].join(\"\\n\")\n                    dialog.clearPassword()\n                    return\n                } else {\n                    var success = Daemon.currentWallet.setPassword(password)\n                    if (success && Config.walletShouldUseSinglePassword) {\n                        Daemon.singlePassword = password\n                    }\n                    var error_msg = qsTr('Password change failed')\n                }\n                dialog.close()\n                if (success && Biometrics.isEnabled) {\n                    // unlikely to happen as this means the user somehow moved from\n                    // a unified password to differing passwords\n                    Biometrics.disable()\n                }\n                var done_dialog = app.messageDialog.createObject(app, {\n                    title: success ? qsTr('Success') : qsTr('Error'),\n                    iconSource: success\n                        ? Qt.resolvedUrl('../../icons/info.png')\n                        : Qt.resolvedUrl('../../icons/warning.png'),\n                    text: success ? qsTr('Password changed') : error_msg\n                })\n                done_dialog.open()\n            })\n            dialog.open()\n        }\n        function onSeedRetrieved() {\n            seedText.visible = true\n            showSeedText.visible = false\n        }\n    }\n\n    Connections {\n        target: Biometrics\n        function onEnablingFailed(error) {\n            if (error === 'CANCELLED') {\n                var biometrics_disabled_dialog = app.messageDialog.createObject(app, {\n                    title: qsTr('Biometric Authentication'),\n                    iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                    text: qsTr('Biometric authentication disabled. You can enable it again in the settings.')\n                })\n                biometrics_disabled_dialog.open()\n                return\n            }\n            var err = app.messageDialog.createObject(app, {\n                text: qsTr('Failed to update biometric authentication to new password: ') + error\n            })\n            err.open()\n        }\n    }\n\n    Component {\n        id: importAddressesKeysDialog\n        ImportAddressesKeysDialog {\n            width: parent.width\n            height: parent.height\n            onClosed: destroy()\n        }\n    }\n\n    Binding {\n        target: AppController\n        property: 'secureWindow'\n        value: seedText.visible\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/WalletMainView.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\nimport QtQuick.Controls.Material\nimport QtQml\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nItem {\n    id: mainView\n\n    property string title: Daemon.currentWallet.name\n\n    property var _sendDialog\n\n    property string _request_amount\n    property string _request_description\n    property string _request_expiry\n\n    function openInvoice(key) {\n        invoice.key = key\n        var dialog = invoiceDialog.createObject(app, { invoice: invoice })\n        dialog.open()\n        return dialog\n    }\n\n    function openRequest(key) {\n        var dialog = receiveDialog.createObject(app, { key: key })\n        dialog.open()\n        return dialog\n    }\n\n    function openSendDialog() {\n        // Qt based send dialog if not on android\n        if (!AppController.isAndroid()) {\n            _sendDialog = qtSendDialog.createObject(mainView, {invoiceParser: invoiceParser, piResolver: piResolver})\n            _sendDialog.open()\n            return\n        }\n\n        // Android based send dialog if on android\n        var scanner = app.scanDialog.createObject(mainView, {\n            hint: Daemon.currentWallet.isLightning\n                ? qsTr('Scan an Invoice, an Address, an LNURL, a PSBT or a Channel Backup')\n                : qsTr('Scan an Invoice, an Address, an LNURL or a PSBT')\n        })\n        scanner.onFoundText.connect(function(data) {\n            data = data.trim()\n            if (bitcoin.isRawTx(data)) {\n                app.stack.push(Qt.resolvedUrl('TxDetails.qml'), { rawtx: data })\n            } else if (Daemon.currentWallet.isValidChannelBackup(data)) {\n                var dialog = app.messageDialog.createObject(app, {\n                    title: qsTr('Import Channel Backup?'),\n                    yesno: true\n                })\n                dialog.accepted.connect(function() {\n                    Daemon.currentWallet.importChannelBackup(data)\n                })\n                dialog.open()\n            } else {\n                piResolver.recipient = data\n            }\n            //scanner.destroy()  // TODO\n        })\n        scanner.open()\n    }\n\n    function closeSendDialog() {\n        if (!AppController.isAndroid()) {\n            if (_sendDialog) {\n                _sendDialog.doClose()\n                _sendDialog = null\n            }\n        }\n    }\n\n    function restartSendDialog() {\n        if (!AppController.isAndroid()) {\n            if (_sendDialog) {\n                _sendDialog.restart()\n            }\n            return\n        } else {\n            openSendDialog()\n        }\n    }\n\n    function showExport(data, helptext) {\n        var dialog = exportTxDialog.createObject(app, {\n            text: data[0],\n            text_qr: data[1],\n            text_help: helptext,\n            text_warn: data[2]\n                ? ''\n                : [qsTr('Warning: Some data (prev txs / \"full utxos\") was left out of the QR code as it would not fit.'),\n                   qsTr('This might cause issues if signing offline.'),\n                   qsTr('As a workaround, copy to clipboard or use the Share option instead.')].join(' '),\n            tx_label: data[3]\n        })\n        dialog.open()\n    }\n\n    function payOnchain(invoicedialog, invoice) {\n        var dialog = confirmPaymentDialog.createObject(mainView, {\n                address: invoice.address,\n                satoshis: invoice.amountOverride.isEmpty\n                    ? invoice.amount\n                    : invoice.amountOverride,\n                message: invoice.message\n        })\n        var canComplete = !Daemon.currentWallet.isWatchOnly && Daemon.currentWallet.canSignWithoutCosigner\n        dialog.accepted.connect(function() {\n            if (invoice.canSave)\n                if (!invoice.saveInvoice())\n                    return\n            if (!canComplete) {\n                if (Daemon.currentWallet.isWatchOnly) {\n                    dialog.finalizer.saveOrShow()\n                } else {\n                    dialog.finalizer.sign()\n                }\n            } else {\n                // store txid in invoicedialog so the dialog can detect broadcast success\n                invoicedialog.broadcastTxid = dialog.finalizer.finalizedTxid\n                dialog.finalizer.signAndSend()\n            }\n        })\n        dialog.open()\n    }\n\n    function createRequest(lightning, reuse_address) {\n        var qamt = Config.unitsToSats(_request_amount)\n        Daemon.currentWallet.createRequest(qamt, _request_description, _request_expiry, lightning, reuse_address)\n    }\n\n    function startSweep() {\n        var dialog = sweepDialog.createObject(app)\n        dialog.accepted.connect(function() {\n            var finalizerDialog = confirmSweepDialog.createObject(mainView, {\n                privateKeys: dialog.privateKeys,\n                message: qsTr('Sweep transaction'),\n                showOptions: false,\n                amountLabelText: qsTr('Total sweep amount'),\n                sendButtonText: Daemon.currentWallet.isWatchOnly\n                    ? qsTr('Sweep...')\n                    : qsTr('Sweep')\n            })\n            finalizerDialog.accepted.connect(function() {\n                if (Daemon.currentWallet.isWatchOnly) {\n                    var confirmdialog = app.messageDialog.createObject(mainView, {\n                        title: qsTr('Confirm Sweep'),\n                        text: qsTr('Current wallet is watch-only. You might not be able to spend from these addresses.\\n\\nAre you sure?'),\n                        yesno: true\n                    })\n                    confirmdialog.accepted.connect(function() {\n                        finalizerDialog.finalizer.send()\n                        close()\n                    })\n                    confirmdialog.open()\n                    return\n                }\n                console.log(\"Sending sweep transaction\")\n                finalizerDialog.finalizer.send()\n            })\n            finalizerDialog.open()\n        })\n        dialog.open()\n    }\n\n    property QtObject menu: Menu {\n        id: menu\n\n        parent: Overlay.overlay\n        dim: true\n        modal: true\n        Overlay.modal: Rectangle {\n            color: \"#44000000\"\n        }\n\n        property int implicitChildrenWidth: 64\n        width: implicitChildrenWidth + 60 + constants.paddingLarge\n\n        MenuItem {\n            icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor\n            icon.source: '../../icons/wallet.png'\n            action: Action {\n                text: qsTr('Wallet details')\n                enabled: app.stack.currentItem.objectName != 'WalletDetails'\n                onTriggered: menu.openPage(Qt.resolvedUrl('WalletDetails.qml'))\n            }\n        }\n        MenuItem {\n            icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor\n            icon.source: '../../icons/tab_addresses.png'\n            action: Action {\n                text: qsTr('Addresses/Coins');\n                onTriggered: menu.openPage(Qt.resolvedUrl('Addresses.qml'));\n                enabled: app.stack.currentItem.objectName != 'Addresses'\n            }\n        }\n        MenuItem {\n            icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor\n            icon.source: '../../icons/lightning.png'\n            action: Action {\n                text: qsTr('Channels');\n                enabled: Daemon.currentWallet.isLightning && app.stack.currentItem.objectName != 'Channels'\n                onTriggered: menu.openPage(Qt.resolvedUrl('Channels.qml'))\n            }\n        }\n\n        MenuItem {\n            icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor\n            icon.source: '../../icons/pen.png'\n            action: Action {\n                text: Daemon.currentWallet.canSignMessage\n                    ? qsTr('Sign/Verify Message')\n                    : qsTr('Verify Message')\n                onTriggered: {\n                    var dialog = app.signVerifyMessageDialog.createObject(app)\n                    dialog.open()\n                    menu.deselect()\n                }\n            }\n        }\n\n        MenuItem {\n            icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor\n            icon.source: '../../icons/sweep.png'\n            action: Action {\n                text: qsTr('Sweep key(s)')\n                onTriggered: {\n                    startSweep()\n                    menu.deselect()\n                }\n            }\n        }\n\n        MenuSeparator { }\n\n        MenuItem {\n            icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor\n            icon.source: '../../icons/file.png'\n            action: Action {\n                text: qsTr('Other wallets')\n                enabled: app.stack.currentItem.objectName != 'Wallets'\n                onTriggered: menu.openPage(Qt.resolvedUrl('Wallets.qml'))\n            }\n        }\n\n        function openPage(url) {\n            stack.pushOnRoot(url)\n            deselect()\n        }\n\n        function deselect() {\n            currentIndex = -1\n        }\n\n        // determine widest element and store in implicitChildrenWidth\n        function updateImplicitWidth() {\n            for (let i = 0; i < menu.count; i++) {\n                var item = menu.itemAt(i)\n                var txt = item.text\n                var txtwidth = fontMetrics.advanceWidth(txt)\n                if (txtwidth > menu.implicitChildrenWidth) {\n                    menu.implicitChildrenWidth = txtwidth\n                }\n            }\n        }\n\n        FontMetrics {\n            id: fontMetrics\n            font: menu.font\n        }\n\n        Component.onCompleted: updateImplicitWidth()\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        History {\n            id: history\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n        }\n\n        ButtonContainer {\n            id: buttonContainer\n            Layout.fillWidth: true\n\n            FlatButton {\n                id: receiveButton\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                icon.source: '../../icons/tab_receive.png'\n                text: qsTr('Receive')\n                onClicked: {\n                    var dialog = receiveDetailsDialog.createObject(mainView)\n                    dialog.open()\n                }\n                onPressAndHold: {\n                    Config.userKnowsPressAndHold = true\n                    Daemon.currentWallet.deleteExpiredRequests()\n                    app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml'))\n                    AppController.haptic()\n                }\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                icon.source: '../../icons/tab_send.png'\n                text: qsTr('Send')\n                enabled: !invoiceParser.busy && !piResolver.busy && !requestDetails.busy\n                onClicked: openSendDialog()\n                onPressAndHold: {\n                    Config.userKnowsPressAndHold = true\n                    app.stack.push(Qt.resolvedUrl('Invoices.qml'))\n                    AppController.haptic()\n                }\n            }\n        }\n    }\n\n    PIResolver {\n        id: piResolver\n        wallet: Daemon.currentWallet\n\n        onResolveError: (code, message) => {\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: message\n            })\n            dialog.open()\n        }\n\n        onInvoiceResolved: (pi) => {\n            invoiceParser.fromResolvedPaymentIdentifier(pi)\n        }\n\n        onRequestResolved: (pi) => {\n            requestDetails.fromResolvedPaymentIdentifier(pi)\n        }\n    }\n\n    RequestDetails {\n        id: requestDetails\n        wallet: Daemon.currentWallet\n        onNeedsLNURLUserInput: {\n            closeSendDialog()\n            var dialog = lnurlWithdrawDialog.createObject(app, {\n                requestDetails: requestDetails\n            })\n            dialog.open()\n        }\n        onLnurlError: (code, message) => {\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: message\n            })\n            dialog.open()\n        }\n    }\n\n    Invoice {\n        id: invoice\n        wallet: Daemon.currentWallet\n    }\n\n    InvoiceParser {\n        id: invoiceParser\n        wallet: Daemon.currentWallet\n        onValidationError: (code, message) => {\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: message\n            })\n            dialog.closed.connect(function() {\n                restartSendDialog()\n            })\n            dialog.open()\n        }\n        onValidationWarning: (code, message) => {\n            if (code == 'no_channels') {\n                var dialog = app.messageDialog.createObject(app, {\n                    text: message\n                })\n                dialog.closed.connect(function() {\n                    restartSendDialog()\n                })\n                dialog.open()\n                // TODO: ask user to open a channel, if funds allow\n                // and maybe store invoice if expiry allows\n            }\n        }\n        onValidationSuccess: {\n            closeSendDialog()\n            var dialog = invoiceDialog.createObject(app, {\n                invoice: invoiceParser,\n                payImmediately: invoiceParser.isLnurlPay\n            })\n            dialog.open()\n        }\n        onInvoiceCreateError: (code, message) => {\n            var msg = qsTr('Cannot save invoice') + ': ' + message\n            var dialog = app.messageDialog.createObject(app, {\n                text: msg\n            })\n            dialog.open()\n        }\n        onLnurlRetrieved: {\n            closeSendDialog()\n            if (invoiceParser.invoiceType === Invoice.Type.LNURLPayRequest) {\n                var dialog = lnurlPayDialog.createObject(app, {\n                    invoiceParser: invoiceParser\n                })\n            } else {\n                console.log(\"Unsupported LNURL type:\", invoiceParser.invoiceType)\n                return\n            }\n            dialog.open()\n        }\n        onLnurlError: (code, message) => {\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: message\n            })\n            dialog.open()\n        }\n    }\n\n    Bitcoin {\n        id: bitcoin\n    }\n\n    Connections {\n        target: Daemon\n        function onWalletLoaded() {\n            if (!Daemon.currentWallet) {  // wallet got deleted\n                app.stack.replaceRoot('Wallets.qml')\n                return\n            }\n            infobanner.hide() // start hidden when switching wallets\n        }\n    }\n\n    Connections {\n        target: app\n        function onPendingIntentChanged() {\n            if (app.pendingIntent) {\n                piResolver.recipient = app.pendingIntent\n                app.pendingIntent = \"\"\n            }\n        }\n    }\n\n    Connections {\n        target: Daemon.currentWallet\n        function onRequestCreateSuccess(key) {\n            openRequest(key)\n        }\n        function onRequestCreateError(error) {\n            console.log(error)\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: error\n            })\n            dialog.open()\n        }\n        function onOtpRequested() {\n            console.log('OTP requested')\n            var dialog = otpDialog.createObject(mainView)\n            dialog.open()\n        }\n        function onBroadcastFailed(txid, code, message) {\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: message\n            })\n            dialog.open()\n        }\n        function onPaymentFailed(invoice_id, message) {\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: message ? message : qsTr('Payment failed')\n            })\n            dialog.open()\n        }\n        function onImportChannelBackupFailed(message) {\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: message\n            })\n            dialog.open()\n        }\n        function onBalanceChanged() {\n            // ln low reserve warning\n            if (Daemon.currentWallet.isLowReserve) {\n                var message = [\n                    qsTr('You do not have enough on-chain funds to protect your Lightning channels.'),\n                    qsTr('You should have at least %1 on-chain in order to be able to sweep channel outputs.').arg(Config.formatSats(Config.lnUtxoReserve) + ' ' + Config.baseUnit)\n                ].join(' ')\n                infobanner.show(message, function() {\n                    var dialog = app.messageDialog.createObject(app, {\n                        text: message + '\\n\\n' + qsTr('Do you want to perform a swap?'),\n                        yesno: true\n                    })\n                    dialog.accepted.connect(function() {\n                        app.startSwap()\n                    })\n                    dialog.open()\n                })\n            } else {\n                infobanner.hide()\n            }\n        }\n    }\n\n    Component {\n        id: invoiceDialog\n        InvoiceDialog {\n            id: _invoiceDialog\n\n            width: parent.width\n            height: parent.height\n\n            onDoPay: {\n                var lninvoiceButPayOnchain = false\n                if (invoice.invoiceType == Invoice.LightningInvoice && invoice.address) {\n                    // ln invoice with fallback\n                    var amountToSend = invoice.amountOverride.isEmpty\n                        ? invoice.amount.satsInt\n                        : invoice.amountOverride.satsInt\n                    if (amountToSend > Daemon.currentWallet.lightningCanSend.satsInt) {\n                        lninvoiceButPayOnchain = true\n                    }\n                }\n                if (invoice.invoiceType == Invoice.OnchainInvoice) {\n                    payOnchain(_invoiceDialog, invoice)\n                } else if (invoice.invoiceType == Invoice.LightningInvoice) {\n                    if (lninvoiceButPayOnchain) {\n                        var dialog = app.messageDialog.createObject(mainView, {\n                            title: qsTr('Insufficient balance to pay over Lightning. Pay on-chain instead?'),\n                            yesno: true\n                        })\n                        dialog.accepted.connect(function() {\n                            payOnchain(_invoiceDialog, invoice)\n                        })\n                        dialog.open()\n                    } else {\n                        console.log('About to pay lightning invoice')\n                        invoice.payLightningInvoice()\n                    }\n                }\n            }\n\n            onClosed: destroy()\n\n            Connections {\n                target: Daemon.currentWallet\n                function onSaveTxSuccess(txid) {\n                    _invoiceDialog.close()\n                }\n            }\n        }\n    }\n\n    Component {\n        id: qtSendDialog\n        SendDialog {\n            width: parent.width\n            height: parent.height\n\n            onTxFound: (data) => {\n                app.stack.push(Qt.resolvedUrl('TxDetails.qml'), { rawtx: data })\n                close()\n            }\n            onChannelBackupFound: (data) => {\n                if (!Daemon.currentWallet.isLightning) {\n                    var dialog = app.messageDialog.createObject(app, {\n                        title: qsTr('Cannot import Channel Backup, Lightning not enabled.')\n                    })\n                    dialog.open()\n                    return\n                }\n\n                var dialog = app.messageDialog.createObject(app, {\n                    title: qsTr('Import Channel Backup?'),\n                    yesno: true\n                })\n                dialog.accepted.connect(function() {\n                    Daemon.currentWallet.importChannelBackup(data)\n                    close()\n                })\n                dialog.rejected.connect(function() {\n                    close()\n                })\n                dialog.open()\n            }\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: receiveDetailsDialog\n\n        ReceiveDetailsDialog {\n            id: _receiveDetailsDialog\n            width: parent.width * 0.9\n            anchors.centerIn: parent\n            onAccepted: {\n                console.log('accepted')\n                _request_amount = _receiveDetailsDialog.amount\n                _request_description = _receiveDetailsDialog.description\n                _request_expiry = _receiveDetailsDialog.expiry\n                createRequest(_receiveDetailsDialog.isLightning, false)\n            }\n            onRejected: {\n                console.log('rejected')\n            }\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: receiveDialog\n        ReceiveDialog {\n            width: parent.width\n            height: parent.height\n\n            onRequestPaid: {\n                close()\n                var capturedHistoryModel = Daemon.currentWallet.historyModel\n                if (isLightning) {\n                    var page = app.stack.push(Qt.resolvedUrl('LightningPaymentDetails.qml'), {'key': key})\n                    var capturedKey = key\n                    page.detailsChanged.connect(function() {\n                            capturedHistoryModel.updateTxLabel(capturedKey, page.label)\n                        }\n                    )\n                } else {\n                    let paidTxid = getPaidTxid()\n                    var page = app.stack.push(Qt.resolvedUrl('TxDetails.qml'), {'txid': paidTxid})\n                    page.detailsChanged.connect(function() {\n                            capturedHistoryModel.updateTxLabel(paidTxid, page.label)\n                        }\n                    )\n                }\n            }\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: confirmPaymentDialog\n        ConfirmTxDialog {\n            id: _confirmPaymentDialog\n            title: qsTr('Confirm Payment')\n            finalizer: TxFinalizer {\n                wallet: Daemon.currentWallet\n                canRbf: true\n                onFinished: (signed, saved, complete) => {\n                    if (!complete) {\n                        var msg\n                        if (wallet.isWatchOnly) {\n                            // tx created in watchonly wallet. Show QR for signer(s)\n                            if (wallet.isMultisig) {\n                                msg = qsTr('Transaction created. Present this QR code to one of the co-cigners or signing devices')\n                            } else {\n                                msg = qsTr('Transaction created. Present this QR code to the signing device')\n                            }\n                        } else {\n                            if (signed) {\n                                msg = qsTr('Transaction created and partially signed by this wallet. Present this QR code to the next co-signer')\n                            } else {\n                                msg = qsTr('Transaction created but not signed by this wallet yet. Sign the transaction and present this QR code to the next co-signer')\n                            }\n                        }\n                        showExport(getSerializedTx(), msg)\n                    }\n                    _confirmPaymentDialog.destroy()\n                }\n                onSignError: (message) => {\n                    var dialog = app.messageDialog.createObject(mainView, {\n                        title: qsTr('Error'),\n                        text: [qsTr('Could not sign tx'), message].join('\\n\\n'),\n                        iconSource: '../../../icons/warning.png'\n                    })\n                    dialog.open()\n                }\n            }\n\n            // TODO: lingering confirmPaymentDialogs can raise exceptions in\n            // the child finalizer when currentWallet disappears, but we need\n            // it long enough for the finalizer to finish..\n            // onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: confirmSweepDialog\n        ConfirmTxDialog {\n            id: _confirmSweepDialog\n\n            property string privateKeys\n            title: qsTr('Confirm Sweep')\n            satoshis: MAX\n            finalizer: SweepFinalizer {\n                wallet: Daemon.currentWallet\n                canRbf: true\n                privateKeys: _confirmSweepDialog.privateKeys\n            }\n        }\n    }\n\n    Component {\n        id: lnurlPayDialog\n        LnurlPayRequestDialog {\n            width: parent.width * 0.9\n            anchors.centerIn: parent\n\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: lnurlWithdrawDialog\n        LnurlWithdrawRequestDialog {\n            width: parent.width * 0.9\n            anchors.centerIn: parent\n\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: otpDialog\n        OtpDialog {\n            width: parent.width * 2/3\n            anchors.centerIn: parent\n\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: exportTxDialog\n        ExportTxDialog {\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: sweepDialog\n        SweepDialog {\n            onClosed: destroy()\n        }\n    }\n\n    Component.onCompleted: {\n        console.log(\"WalletMainView completed: \", Daemon.currentWallet.name)\n        if (app.pendingIntent) {\n            piResolver.recipient = app.pendingIntent\n            app.pendingIntent = \"\"\n        }\n    }\n}\n\n"
  },
  {
    "path": "electrum/gui/qml/components/WalletSummary.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nItem {\n    id: root\n    clip: true\n    implicitHeight: 0\n\n    function open() {\n        state = 'opened'\n    }\n    function close() {\n        state = ''\n    }\n    function toggle() {\n        if (state == 'opened')\n            state = ''\n        else\n            state = 'opened'\n    }\n\n    states: [\n        State {\n            name: 'opened'\n            PropertyChanges { target: root; implicitHeight: detailsPane.height }\n        }\n    ]\n\n    transitions: [\n        Transition {\n            from: ''\n            to: 'opened'\n            NumberAnimation { target: root; properties: 'implicitHeight'; duration: 200 }\n        },\n        Transition {\n            from: 'opened'\n            to: ''\n            NumberAnimation { target: root; properties: 'implicitHeight'; duration: 100 }\n        }\n    ]\n\n    Pane {\n        id: detailsPane\n        width: parent.width\n        anchors.bottom: parent.bottom\n        padding: 0\n        background: Rectangle {\n            color: Material.dialogColor\n        }\n\n        ColumnLayout {\n            id: rootLayout\n            width: parent.width\n            spacing: constants.paddingXLarge\n\n            Item { Layout.preferredWidth: 1; Layout.preferredHeight: 1 }\n\n            RowLayout {\n                Layout.fillWidth: true\n                FlatButton {\n                    text: qsTr('More details')\n                    Layout.fillWidth: true\n                    Layout.preferredWidth: 1\n                    enabled: app.stack.currentItem.objectName != 'WalletDetails'\n                    onClicked: {\n                        root.close()\n                        app.stack.pushOnRoot(Qt.resolvedUrl('WalletDetails.qml'))\n                    }\n                }\n                FlatButton {\n                    text: qsTr('Switch wallet')\n                    Layout.fillWidth: true\n                    icon.source: '../../icons/file.png'\n                    Layout.preferredWidth: 1\n                    enabled: app.stack.currentItem.objectName != 'Wallets'\n                    onClicked: {\n                        root.close()\n                        app.stack.pushOnRoot(Qt.resolvedUrl('Wallets.qml'))\n                    }\n                }\n            }\n        }\n    }\n\n    property string formattedTotalBalance\n    property string formattedTotalBalanceFiat\n\n    function setBalances() {\n        root.formattedTotalBalance = Config.formatSats(Daemon.currentWallet.totalBalance)\n        if (Daemon.fx.enabled) {\n            root.formattedTotalBalanceFiat = Daemon.fx.fiatValue(Daemon.currentWallet.totalBalance, false)\n        }\n    }\n\n\n    // instead of all these explicit connections, we should expose\n    // formatted balances directly as a property\n    Connections {\n        target: Config\n        function onBaseUnitChanged() { setBalances() }\n        function onThousandsSeparatorChanged() { setBalances() }\n    }\n\n    Connections {\n        target: Daemon\n        function onWalletLoaded() { setBalances() }\n    }\n\n    Connections {\n        target: Daemon.fx\n        function onEnabledUpdated() { setBalances() }\n        function onQuotesUpdated() { setBalances() }\n    }\n\n    Connections {\n        target: Daemon.currentWallet\n        function onBalanceChanged() {\n            setBalances()\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/Wallets.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nPane {\n    id: rootItem\n    objectName: 'Wallets'\n\n    padding: 0\n\n    property string title: qsTr('Wallets')\n\n    function createWallet() {\n        var dialog = app.newWalletWizard.createObject(app)\n        dialog.open()\n        dialog.walletCreated.connect(function() {\n            Daemon.availableWallets.reload()\n            // and load the new wallet\n            Daemon.loadWallet(dialog.path, dialog.wizard_data['password'])\n        })\n    }\n\n    ColumnLayout {\n        id: rootLayout\n        anchors.fill: parent\n        spacing: 0\n\n        ColumnLayout {\n            Layout.fillWidth: true\n            Layout.margins: constants.paddingLarge\n\n            Heading {\n                text: qsTr('Wallets')\n            }\n\n            Frame {\n                id: detailsFrame\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n                verticalPadding: 0\n                horizontalPadding: 0\n                background: PaneInsetBackground {}\n\n                ElListView {\n                    id: listview\n                    anchors.fill: parent\n                    clip: true\n                    model: Daemon.availableWallets\n\n                    delegate: ItemDelegate {\n                        width: ListView.view.width\n                        height: row.height\n\n                        onClicked: {\n                            if (!Daemon.currentWallet || Daemon.currentWallet.name != model.name) {\n                                if (!Daemon.loading) // wallet load in progress\n                                    Daemon.loadWallet(model.path)\n                            } else {\n                                app.stack.pop()\n                            }\n                        }\n\n                        RowLayout {\n                            id: row\n                            spacing: 10\n                            x: constants.paddingSmall\n                            width: parent.width - 2 * constants.paddingSmall\n\n                            Image {\n                                id: walleticon\n                                source: \"../../icons/wallet.png\"\n                                fillMode: Image.PreserveAspectFit\n                                Layout.preferredWidth: constants.iconSizeLarge\n                                Layout.preferredHeight: constants.iconSizeLarge\n                                Layout.topMargin: constants.paddingSmall\n                                Layout.bottomMargin: constants.paddingSmall\n                            }\n\n                            Label {\n                                Layout.fillWidth: true\n                                font.pixelSize: constants.fontSizeLarge\n                                text: model.name\n                                elide: Label.ElideRight\n                                color: model.active ? Material.foreground : Qt.darker(Material.foreground, 1.20)\n                            }\n\n                            Tag {\n                                visible: Daemon.currentWallet && model.name == Daemon.currentWallet.name\n                                text: qsTr('Current')\n                                border.color: Material.foreground\n                                font.bold: true\n                                labelcolor: Material.foreground\n                            }\n                            Tag {\n                                visible: model.active\n                                text: qsTr('Active')\n                                border.color: 'green'\n                                labelcolor: 'green'\n                            }\n                            Tag {\n                                visible: !model.active\n                                text: qsTr('Not loaded')\n                                border.color: 'grey'\n                                labelcolor: 'grey'\n                            }\n                        }\n                    }\n\n                    ScrollIndicator.vertical: ScrollIndicator { }\n                }\n            }\n\n        }\n\n        FlatButton {\n            Layout.fillWidth: true\n            text: qsTr('Create Wallet')\n            icon.source: '../../icons/add.png'\n            onClicked: {\n                if (Daemon.availableWallets.rowCount() > 0 && Config.walletShouldUseSinglePassword\n                    && (!Daemon.singlePassword || Daemon.numWalletsWithPassword(Daemon.singlePassword) < 1)) {\n                    // if the user has wallets but hasn't unlocked any wallet yet force them to do so.\n                    // this ensures they know at least one wallets password and can complete the wizard\n                    // where they will need to enter the password of an existing wallet.\n                    var dialog = app.messageDialog.createObject(app, {\n                        title: qsTr('Wallet unlock required'),\n                        text: qsTr(\"You have to unlock any existing wallet first before creating a new wallet.\"),\n                    })\n                    dialog.open()\n                } else {\n                    rootItem.createWallet()\n                }\n            }\n        }\n    }\n\n    Connections {\n        target: Daemon\n        function onWalletLoaded() {\n            if (app.stack.currentItem.objectName == 'Wallets')\n                if (app.stack.getRoot().objectName == 'Wallets') {\n                    app.stack.replaceRoot('WalletMainView.qml')\n                } else {\n                    app.stack.pop()\n                }\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/AddressDelegate.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nItemDelegate {\n    id: delegate\n    width: ListView.view.width\n    height: delegateLayout.height\n    highlighted: ListView.isCurrentItem\n\n    font.pixelSize: constants.fontSizeMedium // set default font size for child controls\n\n    ColumnLayout {\n        id: delegateLayout\n        width: parent.width\n        spacing: 0\n\n        GridLayout {\n            columns: 2\n            Layout.topMargin: constants.paddingSmall\n            Layout.leftMargin: constants.paddingLarge\n            Layout.rightMargin: constants.paddingLarge\n\n            Label {\n                id: indexLabel\n                font.bold: true\n                text: model.addridx < 10\n                    ? '#' + ('0'+model.addridx).slice(-2)\n                    : '#' + model.addridx\n                Layout.fillWidth: true\n            }\n            Label {\n                font.family: FixedFont\n                text: model.address\n                elide: Text.ElideMiddle\n                Layout.fillWidth: true\n            }\n\n            Rectangle {\n                id: useIndicator\n                Layout.preferredWidth: constants.iconSizeMedium\n                Layout.preferredHeight: constants.iconSizeMedium\n                color: model.held\n                        ? constants.colorAddressFrozen\n                        : model.numtx > 0\n                            ? model.balance.satsInt == 0\n                                ? constants.colorAddressUsed\n                                : constants.colorAddressUsedWithBalance\n                            : model.type == 'change'\n                                ? constants.colorAddressInternal\n                                : constants.colorAddressExternal\n            }\n\n            RowLayout {\n                Label {\n                    id: labelLabel\n                    font.pixelSize: model.label != '' ? constants.fontSizeLarge : constants.fontSizeSmall\n                    text: model.label != '' ? model.label : qsTr('<no label>')\n                    opacity: model.label != '' ? 1.0 : 0.8\n                    elide: Text.ElideRight\n                    maximumLineCount: 2\n                    wrapMode: Text.WordWrap\n                    Layout.fillWidth: true\n                }\n                Label {\n                    font.family: FixedFont\n                    text: Config.formatSats(model.balance, false)\n                    visible: model.balance.satsInt != 0\n                }\n                Label {\n                    color: Material.accentColor\n                    text: Config.baseUnit + ','\n                    visible: model.balance.satsInt != 0\n                }\n                Label {\n                    text: model.numtx\n                    visible: model.numtx > 0\n                }\n                Label {\n                    color: Material.accentColor\n                    text: qsTr('tx')\n                    visible: model.numtx > 0\n                }\n            }\n        }\n\n        Item {\n            Layout.preferredWidth: 1\n            Layout.preferredHeight: constants.paddingSmall\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/BalanceSummary.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nItem {\n    id: root\n\n    implicitWidth: balancePane.implicitWidth\n    implicitHeight: balancePane.implicitHeight\n\n    property string formattedConfirmedBalance\n    property string formattedTotalBalance\n    property string formattedTotalBalanceFiat\n    property string formattedLightningBalance\n\n    function setBalances() {\n        root.formattedConfirmedBalance = Config.formatSats(Daemon.currentWallet.confirmedBalance)\n        root.formattedTotalBalance = Config.formatSats(Daemon.currentWallet.totalBalance)\n        root.formattedLightningBalance = Config.formatSats(Daemon.currentWallet.lightningBalance)\n        if (Daemon.fx.enabled) {\n            root.formattedTotalBalanceFiat = Daemon.fx.fiatValue(Daemon.currentWallet.totalBalance, false)\n        }\n    }\n\n    TextHighlightPane {\n        id: balancePane\n        leftPadding: constants.paddingXLarge\n        rightPadding: constants.paddingXLarge\n\n        GridLayout {\n            id: balanceLayout\n            columns: 3\n            opacity: Daemon.currentWallet.synchronizing || !Network.isConnected ? 0 : 1\n\n            Label {\n                font.pixelSize: constants.fontSizeXLarge\n                text: qsTr('Balance') + ':'\n                color: Material.accentColor\n            }\n\n            Label {\n                Layout.alignment: Qt.AlignRight\n                font.pixelSize: constants.fontSizeXLarge\n                font.family: FixedFont\n                text: formattedTotalBalance\n            }\n            Label {\n                font.pixelSize: constants.fontSizeXLarge\n                color: Material.accentColor\n                text: Config.baseUnit\n            }\n\n            Item {\n                visible: Daemon.fx.enabled\n                Layout.preferredWidth: 1\n            }\n            Label {\n                Layout.alignment: Qt.AlignRight\n                visible: Daemon.fx.enabled\n                font.pixelSize: constants.fontSizeLarge\n                font.family: FixedFont\n                color: constants.mutedForeground\n                text: formattedTotalBalanceFiat\n            }\n            Label {\n                visible: Daemon.fx.enabled\n                font.pixelSize: constants.fontSizeLarge\n                color: constants.mutedForeground\n                text: Daemon.fx.fiatCurrency\n            }\n\n            RowLayout {\n                Layout.alignment: Qt.AlignRight\n                visible: Daemon.currentWallet.isLightning\n                Image {\n                    Layout.preferredWidth: constants.iconSizeSmall\n                    Layout.preferredHeight: constants.iconSizeSmall\n                    source: '../../../icons/lightning.png'\n                }\n                Label {\n                    text: qsTr('Lightning') + ':'\n                    font.pixelSize: constants.fontSizeSmall\n                    color: Material.accentColor\n                }\n            }\n            Label {\n                visible: Daemon.currentWallet.isLightning\n                Layout.alignment: Qt.AlignRight\n                text: formattedLightningBalance\n                font.family: FixedFont\n            }\n            Label {\n                visible: Daemon.currentWallet.isLightning\n                font.pixelSize: constants.fontSizeSmall\n                color: Material.accentColor\n                text: Config.baseUnit\n            }\n\n            RowLayout {\n                Layout.alignment: Qt.AlignRight\n                visible: Daemon.currentWallet.isLightning\n                Image {\n                    Layout.preferredWidth: constants.iconSizeSmall\n                    Layout.preferredHeight: constants.iconSizeSmall\n                    source: '../../../icons/bitcoin.png'\n                }\n                Label {\n                    text: qsTr('On-chain') + ':'\n                    font.pixelSize: constants.fontSizeSmall\n                    color: Material.accentColor\n                }\n            }\n            Label {\n                id: formattedConfirmedBalanceLabel\n                visible: Daemon.currentWallet.isLightning\n                Layout.alignment: Qt.AlignRight\n                text: formattedConfirmedBalance\n                font.family: FixedFont\n            }\n            Label {\n                visible: Daemon.currentWallet.isLightning\n                font.pixelSize: constants.fontSizeSmall\n                color: Material.accentColor\n                text: Config.baseUnit\n            }\n        }\n\n    }\n\n    Label {\n        opacity: Daemon.currentWallet.synchronizing && Network.isConnected ? 1 : 0\n        anchors.centerIn: balancePane\n        text: Daemon.currentWallet.synchronizingProgress\n        color: Material.accentColor\n        font.pixelSize: constants.fontSizeLarge\n    }\n\n    Label {\n        opacity: !Network.isConnected ? 1 : 0\n        anchors.centerIn: balancePane\n        text: Network.serverStatus\n        color: Material.accentColor\n        font.pixelSize: constants.fontSizeLarge\n    }\n\n    MouseArea {\n        anchors.fill: parent\n        onClicked: {\n            app.stack.push(Qt.resolvedUrl('../BalanceDetails.qml'))\n        }\n    }\n\n    // instead of all these explicit connections, we should expose\n    // formatted balances directly as a property\n    Connections {\n        target: Config\n        function onBaseUnitChanged() { setBalances() }\n        function onThousandsSeparatorChanged() { setBalances() }\n    }\n\n    Connections {\n        target: Daemon\n        function onWalletLoaded() {\n            setBalances()\n        }\n    }\n\n    Connections {\n        target: Daemon.fx\n        function onEnabledUpdated() { setBalances() }\n        function onQuotesUpdated() { setBalances() }\n    }\n\n    Connections {\n        target: Daemon.currentWallet\n        function onBalanceChanged() {\n            setBalances()\n        }\n    }\n\n    FontMetrics {\n        id: fontMetrics\n        font: formattedConfirmedBalanceLabel.font\n    }\n\n    Component.onCompleted: setBalances()\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/BtcField.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nTextField {\n    id: amount\n\n    required property TextField fiatfield\n    property bool msatPrecision: false\n\n    font.family: FixedFont\n    placeholderText: qsTr('Amount')\n    inputMethodHints: Qt.ImhDigitsOnly\n    validator: RegularExpressionValidator {\n        regularExpression: msatPrecision ? Config.btcAmountRegexMsat : Config.btcAmountRegex\n    }\n\n    property Amount textAsSats\n    onTextChanged: {\n        textAsSats = Config.unitsToSats(amount.text)\n        if (fiatfield.activeFocus)\n            return\n        fiatfield.text = text == '' ? '' : Daemon.fx.fiatValue(amount.textAsSats)\n    }\n\n    Connections {\n        target: Config\n        function onBaseUnitChanged() {\n            amount.text = amount.textAsSats.satsInt != 0\n                ? Config.satsToUnits(amount.textAsSats)\n                : ''\n        }\n    }\n\n    Component.onCompleted: amount.textChanged()\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ButtonContainer.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nContainer {\n    id: root\n\n    property bool showSeparator: true\n\n    property Item _layout\n\n    function fillContentItem() {\n        var contentRoot = containerLayout.createObject(root)\n\n        contentRoot.children.length = 0 // empty array\n        let total = contentChildren.length\n\n        let rowheight = 0\n        for (let i = 0; i < contentChildren.length; i++) {\n            rowheight = Math.max(rowheight, root.itemAt(i).implicitHeight)\n        }\n\n        for (let i = 0; i < contentChildren.length; i++) {\n            var button = root.itemAt(i)\n\n            contentRoot.children.push(verticalSeparator.createObject(_layout, {\n                pheight: rowheight * 2/3,\n                master_idx: i\n            }))\n\n            contentRoot.children.push(button)\n        }\n\n        contentItem = contentRoot\n    }\n\n    // override this function to dynamically add buttons.\n    function beforeLayout() {}\n\n    Component.onCompleted: {\n        beforeLayout()\n        fillContentItem()\n    }\n\n    Component {\n        id: containerLayout\n        RowLayout {\n            spacing: 0\n        }\n    }\n\n    Component {\n        id: verticalSeparator\n        Rectangle {\n            required property int pheight\n            required property int master_idx\n            Layout.fillWidth: false\n            Layout.preferredWidth: showSeparator ? 2 : 0\n            Layout.preferredHeight: pheight\n            Layout.alignment: Qt.AlignVCenter\n            color: constants.darkerBackground\n            Component.onCompleted: {\n                // create binding here, we need to be able to have stable ref master_idx\n                visible = Qt.binding(function() {\n                    let anybefore_visible = false\n                    for (let j = master_idx-1; j >= 0; j--) {\n                        anybefore_visible = anybefore_visible || root.itemAt(j).visible\n                    }\n                    return root.itemAt(master_idx).visible && anybefore_visible\n                })\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ChannelBar.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nItem {\n    property Amount capacity\n    property Amount localCapacity\n    property Amount remoteCapacity\n    property Amount canSend\n    property Amount canReceive\n    property bool frozenForSending: false\n    property bool frozenForReceiving: false\n\n    height: 10\n    implicitWidth: 100\n\n    function update() {\n        Qt.callLater(do_update)\n    }\n\n    function do_update() {\n        var cap = capacity.satsInt * 1000\n        var twocap = cap * 2\n        l1.width = width * (cap - localCapacity.msatsInt) / twocap\n        if (frozenForSending) {\n            l2.width = width * localCapacity.msatsInt / twocap\n            l3.width = 0\n        } else {\n            l2.width = width * (localCapacity.msatsInt - canSend.msatsInt) / twocap\n            l3.width = width * canSend.msatsInt / twocap\n        }\n        if (frozenForReceiving) {\n            r3.width = 0\n            r2.width = width * remoteCapacity.msatsInt / twocap\n        } else {\n            r3.width = width * canReceive.msatsInt / twocap\n            r2.width = width * (remoteCapacity.msatsInt - canReceive.msatsInt) / twocap\n        }\n        r1.width = width * (cap - remoteCapacity.msatsInt) / twocap\n    }\n\n    onWidthChanged: update()\n    onFrozenForSendingChanged: update()\n    onFrozenForReceivingChanged: update()\n\n    Connections {\n        target: localCapacity\n        function onMsatsIntChanged() { update() }\n    }\n\n    Connections {\n        target: remoteCapacity\n        function onMsatsIntChanged() { update() }\n    }\n\n    Connections {\n        target: canSend\n        function onMsatsIntChanged() { update() }\n    }\n\n    Connections {\n        target: canReceive\n        function onMsatsIntChanged() { update() }\n    }\n\n    Rectangle {\n        id: l1\n        x: 0\n        height: parent.height\n        color: 'gray'\n    }\n    Rectangle {\n        id: l2\n        anchors.left: l1.right\n        height: parent.height\n        color: constants.colorLightningLocalReserve\n    }\n    Rectangle {\n        id: l3\n        anchors.left: l2.right\n        height: parent.height\n        color: constants.colorLightningLocal\n    }\n    Rectangle {\n        id: r3\n        anchors.left: l3.right\n        height: parent.height\n        color: constants.colorLightningRemote\n    }\n    Rectangle {\n        id: r2\n        anchors.left: r3.right\n        height: parent.height\n        color: constants.colorLightningRemoteReserve\n    }\n    Rectangle {\n        id: r1\n        anchors.left: r2.right\n        height: parent.height\n        color: 'gray'\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ChannelDelegate.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nItemDelegate {\n    id: root\n    height: item.height\n    width: ListView.view.width\n\n    font.pixelSize: constants.fontSizeSmall // set default font size for child controls\n\n    property bool _closed: model.state_code == ChannelDetails.Closed\n                            || model.state_code == ChannelDetails.Redeemed\n\n    GridLayout {\n        id: item\n\n        anchors {\n            left: parent.left\n            right: parent.right\n            leftMargin: constants.paddingSmall\n            rightMargin: constants.paddingMedium\n        }\n\n        columns: 2\n\n        Rectangle {\n            Layout.columnSpan: 2\n            Layout.fillWidth: true\n            Layout.preferredHeight: constants.paddingXXSmall\n            color: 'transparent'\n        }\n\n        Image {\n            id: walleticon\n            source: model.is_backup\n                        ? model.is_imported\n                            ? '../../../icons/cloud_no.png'\n                            : '../../../icons/lightning_disconnected.png'\n                        : model.is_trampoline\n                            ? '../../../icons/kangaroo.png'\n                            : '../../../icons/lightning.png'\n            fillMode: Image.PreserveAspectFit\n            Layout.rowSpan: 3\n            Layout.preferredWidth: constants.iconSizeLarge\n            Layout.preferredHeight: constants.iconSizeLarge\n            opacity: _closed ? 0.5 : 1.0\n\n            Image {\n                visible: model.is_trampoline\n                source: '../../../icons/lightning.png'\n                anchors {\n                    right: parent.right\n                    bottom: parent.bottom\n                }\n                width: parent.width * 1/3\n                height: parent.height * 1/3\n            }\n        }\n\n        RowLayout {\n            Layout.fillWidth: true\n            Label {\n                Layout.fillWidth: true\n                text: model.node_alias ? model.node_alias : model.node_id\n                font.family: model.node_alias ? app.font.family : FixedFont\n                font.pixelSize: model.node_alias ? constants.fontSizeMedium : constants.fontSizeSmall\n                elide: Text.ElideRight\n                wrapMode: Text.Wrap\n                maximumLineCount: model.node_alias ? 2 : 1\n                color: _closed ? constants.mutedForeground : Material.foreground\n            }\n\n            Label {\n                text: model.state\n                font.pixelSize: constants.fontSizeMedium\n                color: _closed\n                        ? constants.mutedForeground\n                        : model.state == 'OPEN'\n                            ? constants.colorChannelOpen\n                            : Material.foreground\n            }\n        }\n\n        RowLayout {\n            Layout.fillWidth: true\n            Label {\n                Layout.fillWidth: true\n                text: model.short_cid\n                color: constants.mutedForeground\n            }\n\n            Label {\n                text: Config.formatSats(model.capacity)\n                font.family: FixedFont\n                color: _closed ? constants.mutedForeground : Material.foreground\n            }\n\n            Label {\n                text: Config.baseUnit\n                color: _closed ? constants.mutedForeground : Material.accentColor\n            }\n        }\n\n        ChannelBar {\n            Layout.fillWidth: true\n            visible: !_closed && !model.is_backup\n            capacity: model.capacity\n            localCapacity: model.local_capacity\n            remoteCapacity: model.remote_capacity\n            canSend: model.can_send\n            canReceive: model.can_receive\n            frozenForSending: model.send_frozen\n            frozenForReceiving: model.receive_frozen\n        }\n\n        Item {\n            visible: _closed\n            Layout.fillWidth: true\n            height: 1\n        }\n\n        Item {\n            Layout.columnSpan: 2\n            Layout.fillWidth: true\n            Layout.preferredHeight: constants.paddingXXSmall\n        }\n\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/CoinDelegate.qml",
    "content": "import QtQuick 2.6\nimport QtQuick.Controls 2.0\nimport QtQuick.Layouts 1.0\nimport QtQuick.Controls.Material 2.0\n\nimport org.electrum 1.0\n\nItemDelegate {\n    id: delegate\n    width: ListView.view.width\n    height: delegateLayout.height\n    highlighted: ListView.isCurrentItem\n    font.pixelSize: constants.fontSizeMedium // set default font size for child controls\n\n    property int indent: 0\n\n    ColumnLayout {\n        id: delegateLayout\n        width: parent.width\n        spacing: 0\n\n        GridLayout {\n            columns: 2\n            Layout.topMargin: constants.paddingSmall\n            Layout.bottomMargin: constants.paddingSmall\n            Layout.leftMargin: constants.paddingLarge + indent\n            Layout.rightMargin: constants.paddingLarge\n\n            Rectangle {\n                id: useIndicator\n                Layout.rowSpan: 2\n                Layout.preferredWidth: constants.iconSizeSmall\n                Layout.preferredHeight: constants.iconSizeSmall\n                Layout.alignment: Qt.AlignTop\n                color: model.held\n                        ? constants.colorAddressFrozen\n                        : constants.colorAddressUsedWithBalance\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                Label {\n                    font.family: FixedFont\n                    text: model.outpoint\n                    elide: Text.ElideMiddle\n                    Layout.fillWidth: true\n                }\n                // Label {\n                //     Layout.preferredWidth: implicitWidth\n                //     visible: model.short_id\n                //     font.family: FixedFont\n                //     font.pixelSize: constants.fontSizeSmall\n                //     text: '[' + model.short_id + ']'\n                // }\n                Item {\n                    Layout.fillWidth: true\n                    Layout.alignment: Qt.AlignLeft | Qt.AlignTop\n                    visible: !model.short_id\n                    Image {\n                        source: Qt.resolvedUrl('../../../icons/unconfirmed.png')\n                        sourceSize.width: constants.iconSizeSmall\n                        sourceSize.height: constants.iconSizeSmall\n                    }\n                }\n                Label {\n                    Layout.leftMargin: constants.paddingMedium\n                    Layout.minimumWidth: implicitWidth\n                    Layout.preferredWidth: implicitWidth\n                    horizontalAlignment: Text.AlignRight\n                    font.family: FixedFont\n                    text: Config.formatSats(model.amount, false)\n                    visible: model.amount.satsInt != 0\n                }\n                Label {\n                    Layout.minimumWidth: implicitWidth\n                    Layout.preferredWidth: implicitWidth\n                    color: Material.accentColor\n                    text: Config.baseUnit\n                    visible: model.amount.satsInt != 0\n                }\n            }\n\n            Label {\n                id: labelLabel\n                Layout.fillWidth: true\n                visible: model.label\n                font.pixelSize: constants.fontSizeMedium\n                text: model.label\n                elide: Text.ElideRight\n                maximumLineCount: 2\n                wrapMode: Text.WordWrap\n            }\n\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ElCheckBox.qml",
    "content": "import QtQuick 2.6\nimport QtQuick.Controls 2.0\nimport QtQuick.Controls.Material 2.0\n\nCheckBox {\n    Component.onCompleted: contentItem.wrapMode = Text.Wrap\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ElComboBox.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\n\nComboBox {\n    id: cb\n\n    property int implicitChildrenWidth: 64\n\n    // make combobox implicit width a multiple of 32, so it aligns with others\n    implicitWidth: Math.ceil(implicitChildrenWidth/32)*32 + 2 * constants.paddingXLarge\n\n    // redefine contentItem, as the default crops the text easily\n    contentItem: Label {\n        id: contentLabel\n        text: cb.currentText\n        padding: constants.paddingLarge\n        rightPadding: constants.paddingXXLarge\n        font.pixelSize: constants.fontSizeMedium\n    }\n\n    // determine widest element and store in implicitChildrenWidth\n    function updateImplicitWidth() {\n        for (let i = 0; i < cb.count; i++) {\n            var txt = cb.textAt(i)\n            var txtwidth = fontMetrics.advanceWidth(txt)\n            if (txtwidth > cb.implicitChildrenWidth) {\n                cb.implicitChildrenWidth = txtwidth\n            }\n        }\n    }\n\n    FontMetrics {\n        id: fontMetrics\n        font: contentLabel.font\n    }\n\n    Component.onCompleted: updateImplicitWidth()\n    onModelChanged: updateImplicitWidth()\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ElDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nDialog {\n    id: abstractdialog\n\n    property bool allowClose: true\n    property string iconSource\n    property bool resizeWithKeyboard: true\n    // inheriting classes can set needsSystemBarPadding this false to disable padding\n    property bool needsSystemBarPadding: true\n\n    property bool _result: false\n    // workaround: remember opened state, to inhibit closed -> closed event\n    property bool _wasOpened: false\n\n    // Add bottom padding for Android navigation bar if needed\n    bottomPadding: needsSystemBarPadding ? app.navigationBarHeight : 0\n\n    // called to finally close dialog after checks by onClosing handler in main.qml\n    function doClose() {\n        doReject()\n    }\n\n    // avoid potential multiple signals, only emit once\n    function doAccept() {\n        if (_result)\n            return\n        _result = true\n        accept()\n    }\n\n    // avoid potential multiple signals, only emit once\n    function doReject() {\n        if (_result)\n            return\n        _result = true\n        reject()\n    }\n\n    parent: resizeWithKeyboard ? app.keyboardFreeZone : Overlay.overlay\n    modal: true\n    Overlay.modal: Rectangle {\n        color: \"#aa000000\"\n    }\n\n    closePolicy: allowClose\n        ? Popup.CloseOnEscape | Popup.CloseOnPressOutside\n        : Popup.NoAutoClose\n\n    onOpenedChanged: {\n        if (opened) {\n            app.activeDialogs.push(abstractdialog)\n            _wasOpened = true\n            _result = false\n        } else {\n            if (!_wasOpened)\n                return\n            if (app.activeDialogs.indexOf(abstractdialog) < 0) {\n                console.log('dialog should exist in activeDialogs!')\n                app.activeDialogs.pop()\n                return\n            }\n            app.activeDialogs.splice(app.activeDialogs.indexOf(abstractdialog),1)\n        }\n    }\n\n    header: ColumnLayout {\n        spacing: 0\n\n        // Add top padding for status bar on Android when using edge-to-edge\n        Item {\n            visible: needsSystemBarPadding && app.statusBarHeight > 0\n            Layout.fillWidth: true\n            Layout.preferredHeight: app.statusBarHeight\n        }\n\n        RowLayout {\n            spacing: 0\n\n            Image {\n                visible: iconSource\n                source: iconSource\n                Layout.preferredWidth: constants.iconSizeXLarge\n                Layout.preferredHeight: constants.iconSizeXLarge\n                Layout.leftMargin: constants.paddingMedium\n                Layout.topMargin: constants.paddingMedium\n                Layout.bottomMargin: constants.paddingMedium\n            }\n\n            Label {\n                text: title\n                wrapMode: Text.Wrap\n                elide: Label.ElideRight\n                Layout.fillWidth: true\n                leftPadding: constants.paddingXLarge\n                topPadding: constants.paddingXLarge\n                bottomPadding: constants.paddingXLarge\n                rightPadding: constants.paddingXLarge\n                font.bold: true\n                font.pixelSize: constants.fontSizeMedium\n            }\n        }\n\n        Rectangle {\n            Layout.fillWidth: true\n            Layout.leftMargin: constants.paddingXXSmall\n            Layout.rightMargin: constants.paddingXXSmall\n            height: 1\n            color: Qt.rgba(0,0,0,0.5)\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ElListView.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nListView {\n    id: root\n\n    // avoid interference with android back-gesture by defining deadzones\n    // you can override to 0 if listview is away from left or right edge.\n    property int exclusionZone: constants.fingerWidth / 2\n    property int leftExclusionZone: exclusionZone\n    property int rightExclusionZone: exclusionZone\n\n    MouseArea {\n        anchors {top: root.top; left: root.left; bottom: root.bottom }\n        visible: leftExclusionZone > 0\n        width: leftExclusionZone\n    }\n\n    MouseArea {\n        anchors { top: root.top; right: root.right; bottom: root.bottom }\n        visible: rightExclusionZone > 0\n        width: rightExclusionZone\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ElRadioButton.qml",
    "content": "import QtQuick 2.6\nimport QtQuick.Controls 2.0\nimport QtQuick.Controls.Material 2.0\n\nRadioButton {\n    Component.onCompleted: contentItem.wrapMode = Text.Wrap\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ElTextArea.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\n// this component adds (auto)scrolling to the bare TextArea, to make it\n// workable if text overflows the available space.\n// This unfortunately hides many signals and properties from the TextArea,\n// so add signals propagation and property aliases when needed.\nFlickable {\n    id: root\n\n    property alias text: edit.text\n    property alias wrapMode: edit.wrapMode\n    property alias background: rootpane.background\n    property alias font: edit.font\n    property alias inputMethodHints: edit.inputMethodHints\n    property alias placeholderText: edit.placeholderText\n    property alias color: edit.color\n    readonly property bool anyActiveFocus: activeFocus || edit.activeFocus\n\n    contentWidth: rootpane.width\n    contentHeight: rootpane.height\n    clip: true\n\n    boundsBehavior: Flickable.StopAtBounds\n    flickableDirection: Flickable.VerticalFlick\n\n    function ensureVisible(r) {\n        r.x = r.x + rootpane.leftPadding\n        r.y = r.y + rootpane.topPadding\n        var w = width - rootpane.leftPadding - rootpane.rightPadding\n        var h = height - rootpane.topPadding - rootpane.bottomPadding\n        if (contentX >= r.x)\n            contentX = r.x\n        else if (contentX+w <= r.x+r.width)\n            contentX = r.x+r.width-w\n        if (contentY >= r.y)\n            contentY = r.y\n        else if (contentY+h <= r.y+r.height)\n            contentY = r.y+r.height-h\n    }\n\n    Pane {\n        id: rootpane\n        width: root.width\n        height: Math.max(root.height, edit.height + topPadding + bottomPadding)\n        padding: constants.paddingXSmall\n        TextArea {\n            id: edit\n            width: parent.width\n            focus: true\n            wrapMode: TextEdit.Wrap\n            onCursorRectangleChanged: root.ensureVisible(cursorRectangle)\n            onTextChanged: root.textChanged()\n            background: Rectangle {\n                color: 'transparent'\n            }\n        }\n        MouseArea {\n            // remaining area clicks focus textarea\n            width: parent.width\n            anchors.top: edit.bottom\n            anchors.bottom: parent.bottom\n            onClicked: edit.forceActiveFocus()\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/FeeMethodComboBox.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nElComboBox {\n    id: control\n\n    required property QtObject feeslider\n\n    textRole: 'text'\n    valueRole: 'value'\n\n    model: [\n        { text: qsTr('ETA'), value: FeeSlider.FSMethod.ETA },\n        { text: qsTr('Mempool'), value: FeeSlider.FSMethod.MEMPOOL },\n        { text: qsTr('Feerate'), value: FeeSlider.FSMethod.FEERATE },\n        { text: qsTr('Manual'), value: FeeSlider.FSMethod.MANUAL }\n    ]\n    onCurrentValueChanged: {\n        if (activeFocus)\n            feeslider.method = currentValue\n    }\n    Component.onCompleted: {\n        currentIndex = indexOfValue(feeslider.method)\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/FeePicker.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nItem {\n    id: root\n\n    required property QtObject finalizer\n\n    default property alias additionalItems: rootLayout.children\n\n    property string targetLabel: qsTr('Target')\n    property string feeLabel: qsTr('Mining fee')\n    property string feeRateLabel: qsTr('Fee rate')\n\n    property bool showTxInfo: true\n    property bool showPicker: true\n    property bool allowPickerAbsFees: true\n    property bool allowPickerFeeRates: true\n\n    property bool manualFeeEntry: finalizer.method == FeeSlider.FSMethod.MANUAL\n\n    implicitHeight: rootLayout.height\n\n    GridLayout {\n        id: rootLayout\n        width: parent.width\n        columns: 2\n\n        Label {\n            Layout.fillWidth: true\n            Layout.preferredWidth: 1\n            text: feeLabel\n            color: Material.accentColor\n            visible: showTxInfo\n        }\n\n        FormattedAmount {\n            Layout.fillWidth: true\n            Layout.preferredWidth: 2\n            amount: finalizer.fee\n            valid: finalizer.valid\n            visible: showTxInfo\n        }\n\n        Label {\n            Layout.fillWidth: true\n            Layout.preferredWidth: 1\n            text: feeRateLabel\n            color: Material.accentColor\n            visible: showTxInfo\n        }\n\n        RowLayout {\n            Layout.fillWidth: true\n            Layout.preferredWidth: 2\n            visible: showTxInfo\n            Label {\n                id: feeRate\n                text: finalizer.valid ? finalizer.feeRate : ''\n                font.family: FixedFont\n            }\n\n            Label {\n                Layout.fillWidth: true\n                text: finalizer.valid ? UI_UNIT_NAME.FEERATE_SAT_PER_VBYTE : ''\n                color: Material.accentColor\n            }\n        }\n\n        Label {\n            Layout.fillWidth: true\n            Layout.preferredWidth: 1\n            text: targetLabel\n            color: Material.accentColor\n            visible: showPicker && !manualFeeEntry\n        }\n\n        Label {\n            Layout.fillWidth: true\n            Layout.preferredWidth: 2\n            text: finalizer.target\n            visible: showPicker && !manualFeeEntry\n        }\n\n        RowLayout {\n            Layout.columnSpan: 2\n            Layout.fillWidth: true\n            visible: showPicker\n\n            Slider {\n                id: feeslider\n                Layout.fillWidth: true\n                leftPadding: constants.paddingMedium\n                enabled: !manualFeeEntry\n\n                snapMode: Slider.SnapOnRelease\n                stepSize: 1\n                from: 0\n                to: finalizer.sliderSteps\n\n                onValueChanged: {\n                    if (activeFocus)\n                        finalizer.sliderPos = value\n                }\n                Component.onCompleted: {\n                    value = finalizer.sliderPos\n                }\n                Connections {\n                    target: finalizer\n                    function onSliderPosChanged() {\n                        feeslider.value = finalizer.sliderPos\n                    }\n                }\n            }\n\n            FeeMethodComboBox {\n                id: target\n                feeslider: finalizer\n            }\n        }\n\n        RowLayout {\n            Layout.columnSpan: 2\n            Layout.fillWidth: true\n            visible: showPicker && manualFeeEntry && allowPickerFeeRates\n\n            Label {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Rate')\n                color: Material.accentColor\n            }\n\n            TextField {\n                id: rate\n                Layout.fillWidth: true\n                Layout.preferredWidth: 2\n                text: finalizer.userFeerate\n                color: finalizer.isUserFeerateLast ? Material.foreground : Material.accentColor\n                inputMethodHints: Qt.ImhDigitsOnly\n                validator: RegularExpressionValidator {\n                    regularExpression: /^[0-9]*\\.[0-9]?$/\n                }\n                onTextEdited: {\n                    finalizer.userFeerate = text\n                }\n            }\n\n            Label {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                color: Material.accentColor\n                text: UI_UNIT_NAME.FEERATE_SAT_PER_VBYTE\n            }\n        }\n\n        RowLayout {\n            Layout.columnSpan: 2\n            Layout.fillWidth: true\n            visible: showPicker && manualFeeEntry && allowPickerAbsFees\n\n            Label {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                color: Material.accentColor\n                text: qsTr('Total')\n            }\n\n            TextField {\n                id: absolute\n                Layout.fillWidth: true\n                Layout.preferredWidth: 2\n                text: finalizer.userFee\n                color: finalizer.isUserFeerateLast ? Material.accentColor : Material.foreground\n                inputMethodHints: Qt.ImhDigitsOnly\n                validator: RegularExpressionValidator {\n                    regularExpression: /^[0-9]*$/\n                }\n                onTextEdited: {\n                    finalizer.userFee = text\n                }\n            }\n\n            Label {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                color: Material.accentColor\n                text: UI_UNIT_NAME.FIXED_SAT\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/FiatField.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nTextField {\n    id: amountFiat\n\n    required property TextField btcfield\n\n    font.family: FixedFont\n    placeholderText: qsTr('Amount')\n    inputMethodHints: Qt.ImhDigitsOnly\n    validator: RegularExpressionValidator {\n        regularExpression: Daemon.fx.fiatAmountRegex\n    }\n\n    onTextChanged: {\n        if (amountFiat.activeFocus)\n            btcfield.text = text == ''\n                ? ''\n                : Config.satsToUnits(Daemon.fx.satoshiValue(amountFiat.text))\n    }\n\n    Connections {\n        target: Daemon.fx\n        function onQuotesUpdated() {\n            amountFiat.text = btcfield.text == ''\n                ? ''\n                : Daemon.fx.fiatValue(Config.unitsToSats(btcfield.text))\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/FlatButton.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\nimport QtQuick.Controls.impl\nimport QtQuick.Controls.Material.impl\n\nTabButton {\n    id: control\n    checkable: false\n\n    property bool textUnderIcon: true\n\n    font.pixelSize: constants.fontSizeSmall\n    icon.width: constants.iconSizeMedium\n    icon.height: constants.iconSizeMedium\n    display: textUnderIcon ? IconLabel.TextUnderIcon : IconLabel.TextBesideIcon\n\n    contentItem: IconLabel {\n        spacing: control.spacing\n        mirrored: control.mirrored\n        display: control.display\n\n        icon: control.icon\n        text: control.text\n        font: control.font\n        color: !control.enabled ? control.Material.hintTextColor : control.down || control.checked ? control.Material.accentColor : control.Material.foreground\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/FormattedAmount.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nGridLayout {\n    required property Amount amount\n    property bool showAlt: true\n    property bool singleLine: true\n    property bool valid: true\n    property bool historic: Daemon.fx.historicRates\n    property int timestamp: 0\n\n    columns: !valid\n                ? 1\n                : singleLine\n                    ? 3\n                    : 2\n\n    Item {\n        visible: !valid // empty placeholder if not valid\n        Layout.preferredWidth: 1\n        Layout.preferredHeight: 1\n    }\n    Label {\n        visible: valid\n        text: amount.msatsInt != 0 ? Config.formatMilliSats(amount) : Config.formatSats(amount)\n        font.family: FixedFont\n    }\n    Label {\n        visible: valid\n        text: Config.baseUnit\n        color: Material.accentColor\n    }\n\n    Label {\n        id: fiatLabel\n        Layout.columnSpan: singleLine ? 1 : 2\n        visible: showAlt && Daemon.fx.enabled && valid\n        font.pixelSize: constants.fontSizeSmall\n    }\n\n    function setFiatValue() {\n        if (showAlt)\n            if (historic && timestamp)\n                fiatLabel.text = '(' + Daemon.fx.fiatValueHistoric(amount, timestamp) + ' ' + Daemon.fx.fiatCurrency + ')'\n            else\n                fiatLabel.text = Daemon.fx.isRecent(timestamp)\n                    ? '(' + Daemon.fx.fiatValue(amount) + ' ' + Daemon.fx.fiatCurrency + ')'\n                    : ''\n    }\n\n    onAmountChanged: setFiatValue()\n\n    Connections {\n        target: Daemon.fx\n        function onQuotesUpdated() { setFiatValue() }\n    }\n\n    Connections {\n        target: amount\n        function onValueChanged() {\n            setFiatValue()\n        }\n    }\n\n    Component.onCompleted: setFiatValue()\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/Heading.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nRowLayout {\n    id: root\n\n    property string text\n    property alias font: label.font\n\n    Layout.fillWidth: true\n    Layout.topMargin: constants.paddingMedium\n    Layout.bottomMargin: constants.paddingMedium\n\n    spacing: constants.paddingLarge\n\n    Rectangle {\n        color: constants.mutedForeground\n        height: 1\n        Layout.fillWidth: true\n    }\n\n    Label {\n        id: label\n        Layout.leftMargin: constants.paddingMedium\n        Layout.rightMargin: constants.paddingMedium\n        text: root.text\n        color: constants.mutedForeground\n        font.pixelSize: constants.fontSizeLarge\n    }\n\n    Rectangle {\n        color: constants.mutedForeground\n        height: 1\n        Layout.fillWidth: true\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/HelpButton.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nToolButton {\n    id: root\n    property string heading\n    property string helptext\n\n    icon.source: Qt.resolvedUrl('../../../icons/info.png')\n    icon.color: 'transparent'\n    onClicked: {\n        var dialog = app.helpDialog.createObject(app, {\n            heading: root.heading,\n            text: root.helptext\n        })\n        dialog.open()\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/HelpDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nElDialog {\n    id: dialog\n\n    header: Item { }\n    footer: Item { }\n\n    property string text\n    property string heading\n\n    z: 1 // raise z so it also covers dialogs using overlay as parent\n\n    anchors.centerIn: parent\n\n    padding: 0\n    needsSystemBarPadding: false\n\n    width: parent.width * 4/5\n\n    Overlay.modal: Rectangle {\n        color: \"#aa000000\"\n    }\n\n    background: Rectangle {\n        color: \"transparent\"\n    }\n\n    Pane {\n        id: rootPane\n        width: parent.width\n        implicitHeight: rootLayout.height + topPadding + bottomPadding\n        padding: constants.paddingLarge\n        background: Rectangle {\n            color: constants.lighterBackground\n        }\n        ColumnLayout {\n            id: rootLayout\n            width: parent.width\n            spacing: constants.paddingLarge\n\n            RowLayout {\n                Layout.fillWidth: true\n                Image {\n                    source: Qt.resolvedUrl('../../../icons/info.png')\n                    Layout.preferredWidth: constants.iconSizeSmall\n                    Layout.preferredHeight: constants.iconSizeSmall\n                }\n                Label {\n                    text: dialog.heading\n                    font.pixelSize: constants.fontSizeMedium\n                    font.underline: true\n                    font.italic: true\n                }\n            }\n            Label {\n                id: message\n                Layout.fillWidth: true\n                text: dialog.text\n                font.pixelSize: constants.fontSizeSmall\n                wrapMode: TextInput.WordWrap\n                textFormat: TextEdit.RichText\n                background: Rectangle {\n                    color: 'transparent'\n                }\n            }\n            Item {\n                height: constants.paddingLarge\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/HistoryItemDelegate.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nItem {\n    id: delegate\n    width: ListView.view.width\n    height: delegateLayout.height\n\n    // expose delegate model for scroll indicator\n    property var delegateModel: model\n\n    ColumnLayout {\n        id: delegateLayout\n        width: parent.width\n        spacing: 0\n\n        ItemDelegate {\n            Layout.fillWidth: true\n            Layout.preferredHeight: txinfo.height\n\n            onClicked: {\n                if (model.lightning) {\n                    var page = app.stack.push(Qt.resolvedUrl('../LightningPaymentDetails.qml'), {'key': model.key})\n                    page.detailsChanged.connect(function() {\n                        // update listmodel when details change\n                        visualModel.model.updateTxLabel(model.key, page.label)\n                    })\n                } else {\n                    var page = app.stack.push(Qt.resolvedUrl('../TxDetails.qml'), {'txid': model.key})\n                    page.detailsChanged.connect(function() {\n                        // update listmodel when details change\n                        visualModel.model.updateTxLabel(model.key, page.label)\n                    })\n                }\n            }\n\n            GridLayout {\n                id: txinfo\n                columns: 3\n\n                x: constants.paddingSmall\n                width: delegate.width - 2*constants.paddingSmall\n\n                Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: constants.paddingSmall }\n\n                Image {\n                    readonly property variant tx_icons : [\n                        '../../../icons/unconfirmed.png',\n                        '../../../icons/clock1.png',\n                        '../../../icons/clock2.png',\n                        '../../../icons/clock3.png',\n                        '../../../icons/clock4.png',\n                        '../../../icons/clock5.png',\n                        '../../../icons/confirmed_bw.png'\n                    ]\n\n                    Layout.preferredWidth: constants.iconSizeLarge\n                    Layout.preferredHeight: constants.iconSizeLarge\n                    Layout.alignment: Qt.AlignVCenter\n                    Layout.rowSpan: 2\n                    source: model.lightning\n                        ? '../../../icons/lightning.png'\n                        : model.complete && model.section != 'local'\n                            ? tx_icons[Math.min(6,model.confirmations)]\n                            : '../../../icons/offline_tx.png'\n                }\n\n                Label {\n                    Layout.fillWidth: true\n                    font.pixelSize: model.label !== '' ? constants.fontSizeLarge : constants.fontSizeMedium\n                    text: model.label !== '' ? model.label : qsTr('<no label>')\n                    color: model.label !== '' ? Material.foreground : constants.mutedForeground\n                    wrapMode: Text.Wrap\n                    maximumLineCount: 2\n                    elide: Text.ElideRight\n                }\n                Label {\n                    id: valueLabel\n                    font.family: FixedFont\n                    font.pixelSize: constants.fontSizeMedium\n                    Layout.alignment: Qt.AlignRight\n                    font.bold: true\n                    color: model.value.satsInt >= 0 ? constants.colorCredit : constants.colorDebit\n\n                    function updateText() {\n                        text = Config.formatSats(model.value)\n                    }\n                    Component.onCompleted: updateText()\n                }\n                Label {\n                    font.pixelSize: constants.fontSizeSmall\n                    text: model.date ? model.date : ''\n                    color: constants.mutedForeground\n                }\n                Label {\n                    id: fiatLabel\n                    font.pixelSize: constants.fontSizeSmall\n                    Layout.alignment: Qt.AlignRight\n                    color: constants.mutedForeground\n\n                    function updateText() {\n                        if (!Daemon.fx.enabled) {\n                            text = ''\n                        } else if (Daemon.fx.historicRates && model.timestamp) {\n                            text = Daemon.fx.fiatValueHistoric(model.value, model.timestamp) + ' ' + Daemon.fx.fiatCurrency\n                        } else {\n                            if (Daemon.fx.isRecent(model.timestamp)) {\n                                text = Daemon.fx.fiatValue(model.value, false) + ' ' + Daemon.fx.fiatCurrency\n                            } else {\n                                text = ''\n                            }\n                        }\n                    }\n                    Component.onCompleted: updateText()\n                }\n                Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: constants.paddingSmall }\n            }\n        }\n\n        Rectangle {\n            visible: delegate.ListView.section == delegate.ListView.nextSection\n            Layout.preferredWidth: parent.width * 2/3\n            Layout.alignment: Qt.AlignHCenter\n            Layout.preferredHeight: constants.paddingXXSmall\n            color: Material.background\n        }\n\n    }\n    // as the items in the model are not bindings to QObjects,\n    // hook up events that might change the appearance\n    Connections {\n        target: Config\n        function onBaseUnitChanged() { valueLabel.updateText() }\n        function onThousandsSeparatorChanged() { valueLabel.updateText() }\n    }\n\n    Connections {\n        target: Daemon.fx\n        function onHistoricRatesChanged() { fiatLabel.updateText() }\n        function onQuotesUpdated() { fiatLabel.updateText() }\n        function onHistoryUpdated() { fiatLabel.updateText() }\n        function onEnabledUpdated() { fiatLabel.updateText() }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/InfoBanner.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\nimport QtQuick.Controls.Material.impl\n\nItem {\n    id: root\n\n    property string message\n    property bool autohide: false\n    property color color: constants.colorAlpha(constants.colorWarning, 0.1)\n    property url icon: Qt.resolvedUrl('../../../icons/warning.png')\n    property alias font: messageLabel.font\n\n    property bool _hide: true\n    property var _clicked_fn\n\n    clip:true\n    z: 1\n    layer.enabled: height > 0\n    layer.effect: ElevationEffect {\n        elevation: constants.paddingXLarge\n        fullWidth: true\n    }\n\n    state: 'hidden'\n\n    states: [\n        State {\n            name: 'hidden'; when: _hide\n            PropertyChanges { target: root; implicitHeight: 0 }\n        },\n        State {\n            name: 'expanded'; when: !_hide\n            PropertyChanges { target: root; implicitHeight: layout.implicitHeight }\n        }\n    ]\n\n    transitions: [\n        Transition {\n            from: 'hidden'; to: 'expanded'\n            SequentialAnimation {\n                PropertyAction  { target: root; property: 'visible'; value: true }\n                NumberAnimation { target: root; properties: 'implicitHeight'; duration: 300; easing.type: Easing.OutQuad }\n            }\n        },\n        Transition {\n            from: 'expanded'; to: 'hidden'\n            SequentialAnimation {\n                NumberAnimation { target: root; properties: 'implicitHeight'; duration: 100; easing.type: Easing.OutQuad }\n                PropertyAction  { target: root; property: 'visible'; value: false }\n            }\n        }\n    ]\n\n    function show(message, on_clicked=undefined) {\n        root.message = message\n        root._clicked_fn = on_clicked\n        root._hide = false\n        if (autohide)\n            closetimer.start()\n    }\n\n    function hide() {\n        closetimer.stop()\n        root._hide = true\n    }\n\n    Rectangle {\n        id: rect\n        width: root.width\n        height: layout.height\n        color: root.color\n        anchors.bottom: root.bottom\n\n        ColumnLayout {\n            id: layout\n            width: parent.width\n            spacing: 0\n\n            RowLayout {\n                Layout.margins: constants.paddingLarge\n                spacing: constants.paddingSmall\n\n                Image {\n                    source: root.icon\n                    Layout.preferredWidth: constants.iconSizeLarge\n                    Layout.preferredHeight: constants.iconSizeLarge\n                }\n\n                Label {\n                    id: messageLabel\n                    Layout.fillWidth: true\n                    font.pixelSize: constants.fontSizeSmall\n                    color: Material.foreground\n                    wrapMode: Text.Wrap\n                    text: root.message\n                }\n            }\n            Rectangle {\n                Layout.preferredHeight: 2\n                Layout.fillWidth: true\n                color: Material.accentColor\n            }\n        }\n    }\n\n    MouseArea {\n        anchors.fill: parent\n        onClicked: {\n            if (root._clicked_fn)\n                root._clicked_fn()\n        }\n    }\n\n    Timer {\n        id: closetimer\n        interval: 5000\n        repeat: false\n        onTriggered: _hide = true\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/InfoTextArea.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nTextHighlightPane {\n    enum IconStyle {\n        None,\n        Info,\n        Warn,\n        Error,\n        Progress,\n        Pending,\n        Done,\n        Spinner\n    }\n\n    property alias text: infotext.text\n    property int iconStyle: InfoTextArea.IconStyle.Info\n    property alias textFormat: infotext.textFormat\n    property bool compact: false\n\n    borderColor: iconStyle == InfoTextArea.IconStyle.Info\n        ? constants.colorInfo\n        : iconStyle == InfoTextArea.IconStyle.Warn\n            ? constants.colorWarning\n            : iconStyle == InfoTextArea.IconStyle.Error\n                ? constants.colorError\n                : iconStyle == InfoTextArea.IconStyle.Progress || iconStyle == InfoTextArea.IconStyle.Spinner\n                    ? constants.colorProgress\n                    : iconStyle == InfoTextArea.IconStyle.Done\n                        ? constants.colorDone\n                        : constants.colorInfo\n    padding: compact ? constants.paddingMedium : constants.paddingXLarge\n\n    RowLayout {\n        width: parent.width\n        spacing: compact ? constants.paddingMedium : constants.paddingLarge\n\n        Image {\n            Layout.preferredWidth: compact ? constants.iconSizeSmall : constants.iconSizeMedium\n            Layout.preferredHeight: compact ? constants.iconSizeSmall : constants.iconSizeMedium\n            visible: iconStyle != InfoTextArea.IconStyle.Spinner && iconStyle != InfoTextArea.IconStyle.None\n            source: iconStyle == InfoTextArea.IconStyle.Info\n                ? \"../../../icons/info.png\"\n                : iconStyle == InfoTextArea.IconStyle.Warn\n                    ? \"../../../icons/warning.png\"\n                    : iconStyle == InfoTextArea.IconStyle.Error\n                        ? \"../../../icons/expired.png\"\n                        : iconStyle == InfoTextArea.IconStyle.Progress\n                            ? \"../../../icons/unconfirmed.png\"\n                            : iconStyle == InfoTextArea.IconStyle.Pending\n                                ? \"../../../icons/unpaid.png\"\n                                : iconStyle == InfoTextArea.IconStyle.Done\n                                    ? \"../../../icons/confirmed.png\"\n                                    : \"\"\n        }\n\n        Item {\n            Layout.preferredWidth: compact ? constants.iconSizeSmall : constants.iconSizeMedium\n            Layout.preferredHeight: compact ? constants.iconSizeSmall : constants.iconSizeMedium\n            visible: iconStyle == InfoTextArea.IconStyle.Spinner\n\n            BusyIndicator {\n                anchors.centerIn: parent\n                scale: 0.66\n                smooth: true\n                running: visible\n            }\n        }\n\n        Label {\n            id: infotext\n            Layout.fillWidth: true\n            wrapMode: Text.Wrap\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/InvoiceDelegate.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\nimport QtQuick.Controls.Material\n\nItemDelegate {\n    id: root\n\n    height: item.height\n    width: ListView.view.width\n    font.pixelSize: constants.fontSizeSmall // set default font size for child controls\n\n    highlighted: ListView.isCurrentItem\n\n    function getKey() {\n        return model.key\n    }\n\n    GridLayout {\n        id: item\n\n        anchors {\n            left: parent.left\n            right: parent.right\n            leftMargin: constants.paddingSmall\n            rightMargin: constants.paddingSmall\n        }\n\n        columns: 2\n\n        Rectangle {\n            Layout.columnSpan: 2\n            Layout.fillWidth: true\n            Layout.preferredHeight: constants.paddingXXSmall\n            color: 'transparent'\n        }\n\n        Image {\n            Layout.rowSpan: 2\n            Layout.preferredWidth: constants.iconSizeLarge\n            Layout.preferredHeight: constants.iconSizeLarge\n            source: model.is_lightning\n                ? \"../../../icons/lightning.png\"\n                : \"../../../icons/bitcoin.png\"\n\n            Image {\n                visible: model.onchain_fallback\n                z: -1\n                source: \"../../../icons/bitcoin.png\"\n                anchors {\n                    right: parent.right\n                    bottom: parent.bottom\n                }\n                width: parent.width /2\n                height: parent.height /2\n            }\n        }\n\n        RowLayout {\n            Layout.fillWidth: true\n            Label {\n                Layout.fillWidth: true\n                text: model.message\n                    ? model.message\n                    : model.type == 'request'\n                        ? model.address\n                        : ''\n                elide: Text.ElideRight\n                wrapMode: Text.Wrap\n                maximumLineCount: 2\n                font.pixelSize: model.message ? constants.fontSizeMedium : constants.fontSizeSmall\n            }\n\n            Label {\n                id: amount\n                text: model.amount.isEmpty\n                    ? ''\n                    : model.amount.isMax\n                        ? 'MAX'\n                        : Config.formatSats(model.amount)\n                font.pixelSize: constants.fontSizeMedium\n                font.family: FixedFont\n            }\n\n            Label {\n                text: model.amount.isEmpty || model.amount.isMax ? '' : Config.baseUnit\n                font.pixelSize: constants.fontSizeMedium\n                color: Material.accentColor\n            }\n        }\n\n        RowLayout {\n            Layout.fillWidth: true\n            Label {\n                text: model.status_str\n                color: Material.accentColor\n            }\n            Item {\n                Layout.fillWidth: true\n                Layout.preferredHeight: status_icon.height\n                Image {\n                    id: status_icon\n                    source: model.status == 0\n                                ? '../../../icons/unpaid.png'\n                                : model.status == 1\n                                    ? '../../../icons/expired.png'\n                                    : model.status == 3\n                                        ? '../../../icons/confirmed.png'\n                                        : model.status == 7\n                                            ? '../../../icons/unconfirmed.png'\n                                            : ''\n                    width: constants.iconSizeSmall\n                    height: constants.iconSizeSmall\n                }\n            }\n            Label {\n                id: fiatValue\n                visible: Daemon.fx.enabled && !model.amount.isMax\n                Layout.alignment: Qt.AlignRight\n                text: model.amount.isEmpty ? '' : Daemon.fx.fiatValue(model.amount, false)\n                font.family: FixedFont\n                font.pixelSize: constants.fontSizeSmall\n            }\n            Label {\n                visible: Daemon.fx.enabled && !model.amount.isMax\n                Layout.alignment: Qt.AlignRight\n                text: model.amount.isEmpty ? '' : Daemon.fx.fiatCurrency\n                font.pixelSize: constants.fontSizeSmall\n                color: Material.accentColor\n            }\n        }\n\n        Rectangle {\n            Layout.columnSpan: 2\n            Layout.fillWidth: true\n            Layout.preferredHeight: constants.paddingXXSmall\n            color: 'transparent'\n        }\n\n    }\n\n    Connections {\n        target: Config\n        function onBaseUnitChanged() {\n            amount.text = model.amount.isEmpty ? '' : Config.formatSats(model.amount)\n        }\n        function onThousandsSeparatorChanged() {\n            amount.text = model.amount.isEmpty ? '' : Config.formatSats(model.amount)\n        }\n    }\n    Connections {\n        target: Daemon.fx\n        function onQuotesUpdated() {\n            fiatValue.text = model.amount.isEmpty ? '' : Daemon.fx.fiatValue(model.amount, false)\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/LightningNetworkStatusIndicator.qml",
    "content": "import QtQuick\n\nimport org.electrum 1.0\n\nItem {\n    id: root\n    visible: Config.useGossip\n    implicitWidth: constants.iconSizeMedium\n    implicitHeight: constants.iconSizeMedium\n\n    property int gossipProgress: Network.gossipInfo.db_channels\n        ? (100 * Network.gossipInfo.db_channels / (Network.gossipInfo.unknown_channels + Network.gossipInfo.db_channels))\n        : 0\n\n    Image {\n        sourceSize.width: root.implicitWidth\n        sourceSize.height: root.implicitHeight\n\n        source: '../../../icons/lightning.png'\n    }\n    Image {\n        sourceSize.width: root.implicitWidth\n        sourceSize.height: root.implicitHeight\n        fillMode: Image.Pad\n        horizontalAlignment: Image.AlignLeft\n        verticalAlignment: Image.AlignTop\n\n        source: '../../../icons/lightning_disconnected.png'\n\n        height: constants.iconSizeMedium * (100 - gossipProgress) / 100\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml",
    "content": "import QtQuick\n\nImage {\n    id: root\n\n    sourceSize.width: constants.iconSizeMedium\n    sourceSize.height: constants.iconSizeMedium\n\n    property bool connected: Network.isConnected\n    property bool lagging: connected && Network.isLagging\n    property bool fork: connected && Network.chaintips > 1\n    property bool syncing: connected && Daemon.currentWallet && Daemon.currentWallet.synchronizing\n    property bool proxy: connected && Network.proxy.enabled\n\n    // ?: in order to keep this a binding..\n    source: Qt.resolvedUrl(!connected\n                ? '../../../icons/status_disconnected.png'\n                : syncing\n                    ? '../../../icons/status_waiting.png'\n                    : lagging\n                        ? fork\n                            ? '../../../icons/status_lagging_fork.png'\n                            : '../../../icons/status_lagging.png'\n                        : fork\n                            ? proxy\n                                ? '../../../icons/status_connected_proxy_fork.png'\n                                : '../../../icons/status_connected_fork.png'\n                            : proxy\n                                ? '../../../icons/status_connected_proxy.png'\n                                : '../../../icons/status_connected.png')\n\n\n    states: [\n        State {\n            name: 'disconnected'\n            when: !connected\n            PropertyChanges { target: root; rotation: 0 }\n            PropertyChanges { target: root; scale: 1.0 }\n        },\n        State {\n            name: 'normal'\n            when: !(syncing || fork)\n            PropertyChanges { target: root; rotation: 0 }\n            PropertyChanges { target: root; scale: 1.0 }\n        },\n        State {\n            name: 'syncing'\n            when: syncing\n            PropertyChanges { target: spin; running: true }\n            PropertyChanges { target: root; scale: 1.0 }\n        },\n        State {\n            name: 'forked'\n            when: fork\n            PropertyChanges { target: root; rotation: 0 }\n            PropertyChanges { target: pulse; running: true }\n        }\n    ]\n\n    RotationAnimation {\n        id: spin\n        target: root\n        from: 0\n        to: 360\n        duration: 1000\n        loops: Animation.Infinite\n    }\n\n    SequentialAnimation {\n        id: pulse\n        loops: Animation.Infinite\n        PauseAnimation { duration: 1000 }\n        NumberAnimation { target: root; property: 'scale'; from: 1.0; to: 1.5; duration: 200; easing.type: Easing.InCubic }\n        NumberAnimation { target: root; property: 'scale'; to: 1.0; duration: 500; easing.type: Easing.OutCubic }\n        PauseAnimation { duration: 30000 }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/PaneInsetBackground.qml",
    "content": "import QtQuick\nimport QtQuick.Controls.Material\n\nRectangle {\n    property color baseColor: Material.background\n    Rectangle {\n        anchors { left: parent.left; top: parent.top; right: parent.right }\n        height: 1\n        color: Qt.darker(baseColor, 1.50)\n    }\n    Rectangle {\n        anchors { left: parent.left; top: parent.top; bottom: parent.bottom }\n        width: 1\n        color: Qt.darker(baseColor, 1.50)\n    }\n    Rectangle {\n        anchors { left: parent.left; bottom: parent.bottom; right: parent.right }\n        height: 1\n        color: Qt.lighter(baseColor, 1.50)\n    }\n    Rectangle {\n        anchors { right: parent.right; top: parent.top; bottom: parent.bottom }\n        width: 1\n        color: Qt.lighter(baseColor, 1.50)\n    }\n    color: Qt.darker(baseColor, 1.15)\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/PasswordField.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nRowLayout {\n    id: root\n    property alias text: password_tf.text\n    property alias tf: password_tf\n    property alias echoMode: password_tf.echoMode\n    property bool showReveal: true\n\n    signal accepted\n\n    TextField {\n        id: password_tf\n        echoMode: TextInput.Password\n        inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoAutoUppercase | Qt.ImhPreferLowercase\n        Layout.fillWidth: true\n        Layout.minimumWidth: fontMetrics.advanceWidth('X') * 16\n        onAccepted: root.accepted()\n    }\n    ToolButton {\n        id: revealButton\n        enabled: root.showReveal\n        opacity: root.showReveal ? 1 : 0\n\n        icon.source: '../../../icons/eye1.png'\n        onClicked: {\n            password_tf.echoMode = password_tf.echoMode == TextInput.Password ? TextInput.Normal : TextInput.Password\n        }\n    }\n\n    FontMetrics {\n        id: fontMetrics\n        font: password_tf.font\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/PasswordStrengthIndicator.qml",
    "content": "import QtQuick 2.6\nimport QtQuick.Controls 2.1\nimport QtQuick.Controls.Material 2.0\n\nRectangle {\n    property string password\n    property int strength: 0\n    property color strengthColor\n    property string strengthText\n\n    onPasswordChanged: checkPasswordStrength(password)\n\n    function checkPasswordStrength() {\n        var _strength = Daemon.passwordStrength(password)\n        var map = {\n            0: [constants.colorError, qsTr('Weak')],\n            1: [constants.colorAcceptable, qsTr('Medium')],\n            2: [constants.colorDone, qsTr('Strong')],\n            3: [constants.colorDone, qsTr('Very Strong')]\n        }\n        strength = password.length ? _strength + 1 : 0\n        strengthText = password.length ? map[_strength][1] : ''\n        strengthColor = map[_strength][0]\n    }\n\n    height: strengthLabel.height\n    color: 'transparent'\n    border.color: Material.foreground\n\n    Rectangle {\n        id: strengthBar\n        x: 1\n        y: 1\n        width: (parent.width - 2) * strength / 4\n        height: parent.height - 2\n        color: strengthColor\n        Label {\n            id: strengthLabel\n            anchors.centerIn: parent\n            text: strengthText\n            color: strength <= 2 ? Material.foreground : '#004000'\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/Piechart.qml",
    "content": "import QtQuick\n\nCanvas {\n    id: piechart\n\n    property var slices\n\n    property int innerOffset: 10\n    property int legendOffset: 8\n    property bool showLegend: true\n\n    onSlicesChanged: piechart.requestPaint()\n\n    onPaint: {\n        var startR = -Math.PI/2\n\n        var ctx = getContext('2d')\n        ctx.reset()\n\n        ctx.font = \"\" + constants.fontSizeSmall + \"px '\" + app.font.family + \"', sans-serif\"\n        ctx.strokeStyle = Qt.rgba(1, 1, 1, 1)\n        ctx.lineWidth = 2\n        var pcx = width/2\n        var pcy = height/2\n        var radius = height/3\n\n        var endR = startR\n        for (const i in slices) {\n            var slice = slices[i]\n            if (slice.v == 0)\n                continue\n            startR = endR\n            endR = startR + 2*Math.PI*(slice.v)\n\n            // displace origin\n            var phi = startR + (endR - startR)/2\n            var dx = Math.cos(phi) * innerOffset\n            var dy = Math.sin(phi) * innerOffset\n\n            ctx.lineWidth = 2\n            ctx.fillStyle = slice.color\n            ctx.beginPath()\n            ctx.moveTo(pcx+dx, pcy+dy)\n            ctx.arc(pcx+dx, pcy+dy, radius, startR, endR, false)\n            ctx.lineTo(pcx+dx, pcy+dy)\n            ctx.fill()\n            // ctx.stroke()\n\n            if (!showLegend)\n                continue\n\n            // displace legend\n            var dx = Math.cos(phi) * (radius + innerOffset + legendOffset)\n            var dy = Math.sin(phi) * (radius + innerOffset + legendOffset)\n            var dx2 = Math.cos(phi) * (radius + innerOffset + 2 * legendOffset)\n            var dy2 = Math.sin(phi) * (radius + innerOffset + 2 * legendOffset)\n            ctx.lineWidth = 1\n            ctx.beginPath()\n            if (dx > 0) {\n                var ddx = ctx.measureText(slice.text).width + 2 * constants.paddingMedium\n                var xtext = pcx+dx2 + constants.paddingMedium\n            } else {\n                var ddx = -(ctx.measureText(slice.text).width + 2 * constants.paddingMedium)\n                var xtext = pcx+dx2+ddx + constants.paddingMedium\n            }\n            ctx.moveTo(pcx+dx, pcy+dy)\n            ctx.lineTo(pcx+dx2, pcy+dy2)\n            ctx.lineTo(pcx+dx2+ddx, pcy+dy2)\n            ctx.moveTo(pcx+dx2, pcy+dy2)\n\n            ctx.fillText(slice.text, xtext, pcy+dy2 - constants.paddingXSmall)\n            ctx.stroke()\n        }\n\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/PrefsHeading.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nHeading {\n    id: root\n\n    Layout.topMargin: constants.paddingXLarge\n    Layout.bottomMargin: constants.paddingMedium\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ProxyConfig.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nItem {\n    id: pc\n\n    implicitHeight: rootLayout.height\n\n    property alias proxy_enabled: proxy_enabled_cb.checked\n    property alias proxy_type: proxytype.currentIndex\n    property alias proxy_address: address.text\n    property alias proxy_port: port.text\n    property alias username: username_tf.text\n    property alias password: password_tf.text\n\n    property var proxy_type_map:  [\n        { text: qsTr('SOCKS5/TOR'), value: 'socks5' },\n        { text: qsTr('SOCKS4'), value: 'socks4' }\n    ]\n\n    property bool _probing: false\n\n    function toProxyDict() {\n        var p = {}\n        p['enabled'] = pc.proxy_enabled\n        var type = proxy_type_map[pc.proxy_type]['value']\n        p['mode'] = type\n        p['host'] = pc.proxy_address\n        p['port'] = pc.proxy_port\n        p['user'] = pc.username\n        p['password'] = pc.password\n        return p\n    }\n\n    ColumnLayout {\n        id: rootLayout\n\n        width: parent.width\n        spacing: constants.paddingLarge\n\n        CheckBox {\n            id: proxy_enabled_cb\n            text: qsTr('Enable Proxy')\n        }\n\n        ElComboBox {\n            id: proxytype\n            enabled: proxy_enabled_cb.checked\n\n            textRole: 'text'\n            valueRole: 'value'\n            model: proxy_type_map\n        }\n\n        GridLayout {\n            columns: 2\n            Layout.fillWidth: true\n\n            Label {\n                text: qsTr(\"Address\")\n                enabled: address.enabled\n            }\n\n            TextField {\n                id: address\n                enabled: proxy_enabled_cb.checked\n                inputMethodHints: Qt.ImhNoPredictiveText\n            }\n\n            Label {\n                text: qsTr(\"Port\")\n                enabled: port.enabled\n            }\n\n            TextField {\n                id: port\n                enabled: proxy_enabled_cb.checked\n                inputMethodHints: Qt.ImhDigitsOnly\n            }\n\n            Label {\n                text: qsTr(\"Username\")\n                enabled: username_tf.enabled\n            }\n\n            TextField {\n                id: username_tf\n                enabled: proxy_enabled_cb.checked\n                inputMethodHints: Qt.ImhNoPredictiveText\n            }\n\n            Label {\n                text: qsTr(\"Password\")\n                enabled: password_tf.enabled\n            }\n\n            PasswordField {\n                id: password_tf\n                enabled: proxy_enabled_cb.checked\n            }\n        }\n\n        Pane {\n            Layout.alignment: Qt.AlignHCenter\n            Layout.topMargin: constants.paddingLarge\n            padding: 0\n            background: Rectangle {\n                color: constants.darkerDialogBackground\n            }\n            FlatButton {\n                enabled: proxy_enabled_cb.checked && !_probing\n                text: qsTr('Detect Tor proxy')\n                onClicked: {\n                    _probing = true\n                    Network.probeTor()\n                }\n            }\n        }\n\n        BusyIndicator {\n            id: spinner\n            Layout.alignment: Qt.AlignHCenter\n            Layout.topMargin: constants.paddingSmall\n            Layout.preferredWidth: constants.iconSizeXLarge\n            Layout.preferredHeight: constants.iconSizeXLarge\n            running: visible\n            visible: _probing\n        }\n    }\n\n    Connections {\n        target: Network\n        function onTorProbeFinished(host, port) {\n            _probing = false\n            if (host && port) {\n                proxytype.currentIndex = 0\n                proxy_port = \"\"+port\n                proxy_address = host\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/QRImage.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\n\nItem {\n    id: root\n    property string qrdata\n    property bool render: true // init to false, then set true if render needs delay\n    property bool enableToggleText: false  // if true, clicking the QR code shows the encoded text\n    property bool isTextState: false    // internal state, if the above is enabled\n\n    property var _qrprops: QRIP.getDimensions(qrdata)\n\n    width: r.width\n    height: r.height\n\n    Rectangle {\n        id: r\n        width: _qrprops.qr_pixelsize\n        height: width\n        color: 'white'\n    }\n\n    Image {\n        source: qrdata && render ? 'image://qrgen/' + qrdata : ''\n        visible: !isTextState\n\n        Rectangle {  // container for logo inside qr code\n            visible: root.render && _qrprops.valid\n            color: 'white'\n            x: (parent.width - width) / 2\n            y: (parent.height - height) / 2\n            width: _qrprops.icon_pixelsize\n            height: _qrprops.icon_pixelsize\n\n            Image {\n                visible: _qrprops.valid\n                source: '../../../icons/electrum.png'\n                x: 1\n                y: 1\n                width: parent.width - 2\n                height: parent.height - 2\n                scale: 0.9\n            }\n        }\n        Label {\n            visible: !_qrprops.valid\n            text: qsTr('Data too big for QR')\n            anchors.centerIn: parent\n        }\n    }\n\n    Label {\n        visible: isTextState\n        text: qrdata\n        wrapMode: Text.WrapAnywhere\n        elide: Text.ElideRight\n        anchors.centerIn: parent\n        horizontalAlignment: Qt.AlignHCenter\n        verticalAlignment: Qt.AlignVCenter\n        color: 'black'\n        font.family: FixedFont\n        font.pixelSize: text.length < 64\n            ? constants.fontSizeXLarge\n            : constants.fontSizeMedium\n        width: r.width\n        height: r.height\n    }\n\n    MouseArea {\n        anchors.fill: parent\n        onClicked: {\n            if (enableToggleText) {\n                root.isTextState = !root.isTextState\n            }\n        }\n    }\n\n    onVisibleChanged: {\n        if (root.visible) {\n            // set max brightness to make qr code easier to scan\n            if (AppController.isMaxBrightnessOnQrDisplayEnabled()) {\n                AppController.setMaxScreenBrightness()\n            }\n        } else {\n            AppController.resetScreenBrightness()\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/QRScan.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtMultimedia\nimport QtQml\n\nimport org.electrum 1.0\n\nItem {\n    id: scanner\n\n    property bool active: false\n    property string url\n    property string hint\n\n    signal foundText(data: string)\n\n    function restart() {\n        console.log('qrscan.restart')\n        qr.reset()\n        start()\n    }\n\n    function start() {\n        console.log('qrscan.start')\n        loader.item.startTimer.start()\n    }\n\n    function stop() {\n        console.log('qrscan.stop')\n        scanner.active = false\n    }\n\n    Item {\n        id: points\n        z: 100\n        anchors.fill: parent\n    }\n\n    Loader {\n        id: loader\n        anchors.fill: parent\n        sourceComponent: scancomp\n        onStatusChanged: {\n            if (loader.status == Loader.Ready) {\n                console.log('camera loaded')\n            } else if (loader.status == Loader.Error) {\n                console.log('camera load error')\n            }\n        }\n    }\n\n    Component {\n        id: scancomp\n\n        Item {\n            property alias vo: _vo\n            property alias ic: _ic\n            property alias startTimer: _startTimer\n\n            VideoOutput {\n                id: _vo\n                anchors.fill: parent\n\n                fillMode: VideoOutput.PreserveAspectCrop\n\n                Rectangle {\n                    width: parent.width\n                    height: (parent.height - parent.width) / 2\n                    anchors.top: parent.top\n                    color: Qt.rgba(0,0,0,0.5)\n                }\n                Rectangle {\n                    width: parent.width\n                    height: (parent.height - parent.width) / 2\n                    anchors.bottom: parent.bottom\n                    color: Qt.rgba(0,0,0,0.5)\n                }\n                InfoTextArea {\n                    visible: scanner.hint\n                    background.opacity: 0.5\n                    iconStyle: InfoTextArea.IconStyle.None\n                    anchors {\n                        top: parent.top\n                        topMargin: constants.paddingXLarge\n                        left: parent.left\n                        leftMargin: constants.paddingXXLarge\n                        right: parent.right\n                        rightMargin: constants.paddingXXLarge\n                    }\n                    text: scanner.hint\n                }\n\n                Component.onCompleted: {\n                    startTimer.start()\n                }\n            }\n\n            ImageCapture {\n                id: _ic\n\n            }\n\n            MediaDevices {\n                id: mediaDevices\n            }\n\n            Camera {\n                id: camera\n                cameraDevice: mediaDevices.defaultVideoInput\n                active: scanner.active\n                focusMode: Camera.FocusModeAutoNear\n                customFocusPoint: Qt.point(0.5, 0.5)\n\n                onErrorOccurred: {\n                    console.log('camera error: ' + errorString)\n                }\n            }\n\n            CaptureSession {\n                videoOutput: _vo\n                imageCapture: _ic\n                camera: camera\n            }\n\n            Timer {\n                id: _startTimer\n                interval: 500\n                repeat: false\n                onTriggered: scanner.active = true\n            }\n\n        }\n    }\n\n    Component {\n        id: r\n        Rectangle {\n            property int cx\n            property int cy\n            width: 15\n            height: 15\n            x: cx - width/2\n            y: cy - height/2\n            radius: 5\n            visible: scanner._pointsVisible\n        }\n    }\n\n    Connections {\n        target: qr\n        function onDataChanged() {\n            console.log('QR DATA: ' + qr.data)\n            scanner.active = false\n            scanner.foundText(qr.data)\n        }\n    }\n\n    QRParser {\n        id: qr\n        videoSink: loader.item.vo.videoSink\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/RequestExpiryComboBox.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nElComboBox {\n    id: expires\n\n    textRole: 'text'\n    valueRole: 'value'\n\n    model: ListModel {\n        id: expiresmodel\n        Component.onCompleted: {\n            // we need to fill the model like this, as ListElement can't evaluate script\n            expiresmodel.append({'text': qsTr('10 minutes'), 'value': 10*60})\n            expiresmodel.append({'text': qsTr('1 hour'), 'value': 60*60})\n            expiresmodel.append({'text': qsTr('1 day'), 'value': 24*60*60})\n            expiresmodel.append({'text': qsTr('1 week'), 'value': 7*24*60*60})\n            expiresmodel.append({'text': qsTr('Never'), 'value': 0})\n            expires.currentIndex = 0\n            for (let i=0; i < expiresmodel.count; i++) {\n                if (expiresmodel.get(i).value == Config.requestExpiry) {\n                    expires.currentIndex = i\n                    break\n                }\n            }\n        }\n    }\n\n    onCurrentValueChanged: {\n        if (activeFocus && currentValue)\n            Config.requestExpiry = currentValue\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/SeedKeyboard.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nItem {\n    id: root\n\n    signal keyEvent(keycode: int, text: string)\n\n    property int hpadding: 0\n    property int vpadding: 15\n\n    property int keywidth: (root.width - 2 * hpadding) / 10 - keyhspacing\n    property int keyheight: (root.height - 2 * vpadding) / 4 - keyvspacing\n    property int keyhspacing: 2\n    property int keyvspacing: 5\n\n    function emitKeyEvent(key, keycode) {\n        keyEvent(keycode, key)\n    }\n\n    ColumnLayout {\n        id: rootLayout\n        x: hpadding\n        y: vpadding\n        width: parent.width - 2*hpadding\n        spacing: keyvspacing\n        RowLayout {\n            Layout.alignment: Qt.AlignHCenter\n            spacing: keyhspacing\n            Repeater {\n                model: ['q','w','e','r','t','y','u','i','o','p']\n                delegate: SeedKeyboardKey {\n                    key: modelData\n                    kbd: root\n                    implicitWidth: keywidth\n                    implicitHeight: keyheight\n                }\n            }\n        }\n        RowLayout {\n            Layout.alignment: Qt.AlignHCenter\n            spacing: keyhspacing\n            Repeater {\n                model: ['a','s','d','f','g','h','j','k','l']\n                delegate: SeedKeyboardKey {\n                    key: modelData\n                    kbd: root\n                    implicitWidth: keywidth\n                    implicitHeight: keyheight\n                }\n            }\n            // spacer\n            Item { Layout.preferredHeight: 1; Layout.preferredWidth: keywidth / 2 }\n        }\n        RowLayout {\n            Layout.alignment: Qt.AlignHCenter\n            spacing: keyhspacing\n            Repeater {\n                model: ['z','x','c','v','b','n','m']\n                delegate: SeedKeyboardKey {\n                    key: modelData\n                    kbd: root\n                    implicitWidth: keywidth\n                    implicitHeight: keyheight\n                }\n            }\n            // spacer\n            Item { Layout.preferredHeight: 1; Layout.preferredWidth: keywidth }\n        }\n        RowLayout {\n            Layout.alignment: Qt.AlignHCenter\n            SeedKeyboardKey {\n                key: ' '\n                keycode: Qt.Key_Space\n                kbd: root\n                implicitWidth: keywidth * 5\n                implicitHeight: keyheight\n            }\n            SeedKeyboardKey {\n                key: '<'\n                keycode: Qt.Key_Backspace\n                kbd: root\n                implicitWidth: keywidth\n                implicitHeight: keyheight\n            }\n            // spacer\n            Item { Layout.preferredHeight: 1; Layout.preferredWidth: keywidth / 2 }\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/SeedKeyboardKey.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nPane {\n    id: root\n\n    property string key\n    property int keycode: -1\n\n    property QtObject kbd\n    padding: 1\n\n    function emitKeyEvent() {\n        if (keycode == -1) {\n            keycode = parseInt(key, 36) - 9 + 0x40 // map a-z char to key code\n        }\n        kbd.keyEvent(keycode, key)\n    }\n\n    FlatButton {\n        anchors.fill: parent\n\n        focusPolicy: Qt.NoFocus\n        autoRepeat: true\n        autoRepeatDelay: 750\n\n        padding: 0\n\n        onClicked: {\n            emitKeyEvent()\n        }\n\n        // send keyevent again, otherwise it is ignored\n        onDoubleClicked: {\n            emitKeyEvent()\n        }\n\n        Label {\n            anchors.centerIn: parent\n            text: key\n            font.pixelSize: Math.max(root.height * 0.67, constants.fontSizeSmall)\n            verticalAlignment: Text.AlignVCenter\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/SeedTextArea.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nPane {\n    id: root\n    implicitHeight: rootLayout.height\n    padding: 0\n\n    property string text\n    property bool readOnly: false\n    property alias placeholderText: seedtextarea.placeholderText\n    property string indicatorText\n    property bool indicatorValid\n\n    property var _suggestions: []\n\n    onTextChanged: {\n        if (seedtextarea.text != text)\n            seedtextarea.text = text\n    }\n\n    background: Rectangle {\n        color: \"transparent\"\n    }\n\n    ColumnLayout {\n        id: rootLayout\n        width: parent.width\n        spacing: 0\n\n        TextArea {\n            id: seedtextarea\n            Layout.fillWidth: true\n            Layout.minimumHeight: fontMetrics.height * 3 + topPadding + bottomPadding\n\n            rightPadding: constants.paddingLarge\n            leftPadding: constants.paddingLarge\n\n            wrapMode: TextInput.WordWrap\n            font.bold: true\n            font.pixelSize: constants.fontSizeLarge\n            font.family: FixedFont\n            inputMethodHints: Qt.ImhSensitiveData | Qt.ImhLowercaseOnly | Qt.ImhNoPredictiveText\n            readOnly: AppController.isAndroid()\n\n            background: Rectangle {\n                color: constants.darkerBackground\n            }\n\n            onTextChanged: {\n                // work around Qt issue, TextArea fires spurious textChanged events\n                // NOTE: might be Qt virtual keyboard, or Qt upgrade from 5.15.2 to 5.15.7\n                if (root.text != text)\n                    root.text = text\n\n                // update suggestions\n                _suggestions = bitcoin.mnemonicsFor(seedtextarea.text.split(' ').pop())\n                // TODO: cursorPosition only on suggestion apply\n                cursorPosition = text.length\n            }\n\n            Rectangle {\n                anchors.fill: contentText\n                color: root.indicatorValid ? 'green' : 'red'\n                border.color: Material.accentColor\n                radius: 2\n            }\n            Label {\n                id: contentText\n                text: root.indicatorText\n                anchors.right: parent.right\n                anchors.bottom: parent.bottom\n                leftPadding: root.indicatorText != '' ? constants.paddingLarge : 0\n                rightPadding: root.indicatorText != '' ? constants.paddingLarge : 0\n                font.bold: false\n                font.pixelSize: constants.fontSizeSmall\n            }\n        }\n\n        Flickable {\n            Layout.preferredWidth: parent.width\n            Layout.minimumHeight: fontMetrics.lineSpacing + 2*constants.paddingXXSmall + 2*constants.paddingXSmall + 2\n            implicitHeight: wordsLayout.height\n\n            visible: !readOnly\n            flickableDirection: Flickable.HorizontalFlick\n            contentWidth: wordsLayout.width\n            interactive: wordsLayout.width > width\n\n            RowLayout {\n                id: wordsLayout\n                Repeater {\n                    model: _suggestions\n                    Rectangle {\n                        Layout.margins: constants.paddingXXSmall\n                        width: suggestionLabel.width\n                        height: suggestionLabel.height\n                        color: constants.lighterBackground\n                        radius: constants.paddingXXSmall\n                        Label {\n                            id: suggestionLabel\n                            text: modelData\n                            padding: constants.paddingXSmall\n                            leftPadding: constants.paddingSmall\n                            rightPadding: constants.paddingSmall\n                        }\n                        MouseArea {\n                            anchors.fill: parent\n                            onClicked: {\n                                var words = seedtextarea.text.split(' ')\n                                words.pop()\n                                words.push(modelData)\n                                seedtextarea.text = words.join(' ') + ' '\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        SeedKeyboard {\n            id: kbd\n            Layout.fillWidth: true\n            Layout.preferredHeight: kbd.width / 1.75\n            visible: !root.readOnly\n            onKeyEvent: {\n                if (keycode == Qt.Key_Backspace) {\n                    if (seedtextarea.text.length > 0)\n                        seedtextarea.text = seedtextarea.text.substring(0, seedtextarea.text.length-1)\n                } else {\n                    seedtextarea.text = seedtextarea.text + text\n                }\n            }\n        }\n    }\n\n    FontMetrics {\n        id: fontMetrics\n        font: seedtextarea.font\n    }\n\n    Bitcoin {\n        id: bitcoin\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ServerConfig.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\n\nItem {\n    id: root\n\n    property bool showAutoselectServer: true\n    property alias address: address_tf.text\n    property alias serverConnectMode: server_connect_mode_cb.currentValue\n    property alias addressValid: address_tf.valid\n\n    implicitHeight: rootLayout.height\n\n    ColumnLayout {\n        id: rootLayout\n\n        width: parent.width\n        height: parent.height\n        spacing: constants.paddingLarge\n\n\n        RowLayout {\n            Layout.fillWidth: true\n\n            ServerConnectModeComboBox {\n                id: server_connect_mode_cb\n                onCurrentValueChanged: {\n                    if (currentValue == ServerConnectModeComboBox.Mode.Autoconnect) {\n                        address_tf.text = \"\"\n                    }\n                }\n            }\n\n            Item {\n                Layout.fillWidth: true\n                Layout.preferredHeight: 1\n            }\n\n            HelpButton {\n                Layout.alignment: Qt.AlignRight\n                heading: qsTr('Connection mode')+':'\n                helptext: Config.getTranslatedMessage('MSG_CONNECTMODE_SERVER_HELP') + '<br/><br/>' +\n                    Config.getTranslatedMessage('MSG_CONNECTMODE_NODES_HELP') + '<ul>' +\n                    '<li><b>' + Config.getTranslatedMessage('MSG_CONNECTMODE_AUTOCONNECT') +\n                    '</b>: ' + Config.getTranslatedMessage('MSG_CONNECTMODE_AUTOCONNECT_HELP') + '</li>' +\n                    '<li><b>' + Config.getTranslatedMessage('MSG_CONNECTMODE_MANUAL') +\n                    '</b>: ' + Config.getTranslatedMessage('MSG_CONNECTMODE_MANUAL_HELP') + '</li>' +\n                    '<li><b>' + Config.getTranslatedMessage('MSG_CONNECTMODE_ONESERVER') +\n                    '</b>: ' + Config.getTranslatedMessage('MSG_CONNECTMODE_ONESERVER_HELP') + '</li>' +\n                    '</ul>'\n            }\n        }\n\n        Label {\n            text: qsTr(\"Server\")\n            enabled: address_tf.enabled\n        }\n\n        TextHighlightPane {\n            Layout.fillWidth: true\n\n            TextField {\n                id: address_tf\n                enabled: server_connect_mode_cb.currentValue != ServerConnectModeComboBox.Mode.Autoconnect\n                width: parent.width\n                inputMethodHints: Qt.ImhNoPredictiveText\n\n                property bool valid: true\n\n                function validate() {\n                    if (!enabled) {\n                        valid = true\n                        return\n                    }\n                    valid = Network.isValidServerAddress(address_tf.text)\n                }\n\n                onTextChanged: validate()\n                onEnabledChanged: validate()\n\n                Rectangle {\n                    anchors.fill: parent\n                    color: \"red\"\n                    opacity: 0.2\n                    visible: !parent.valid\n                }\n            }\n        }\n\n        ColumnLayout {\n            Heading {\n                text: qsTr('Servers')\n            }\n\n            Frame {\n                background: PaneInsetBackground { baseColor: Material.dialogColor }\n                clip: true\n                verticalPadding: 0\n                horizontalPadding: 0\n                Layout.fillHeight: true\n                Layout.fillWidth: true\n                Layout.bottomMargin: constants.paddingLarge\n\n                ElListView {\n                    id: serversListView\n                    anchors.fill: parent\n                    model: Network.serverListModel\n                    delegate: ServerDelegate {\n                        onClicked: {\n                            address_tf.text = model.name\n                        }\n                    }\n\n                    section.property: 'chain'\n                    section.criteria: ViewSection.FullString\n                    section.delegate: RowLayout {\n                        width: ListView.view.width\n                        required property string section\n                        Label {\n                            text: section\n                                ? serversListView.model.chaintips > 1\n                                    ? qsTr('Connected @%1').arg(section)\n                                    : qsTr('Connected')\n                                : qsTr('Other known servers')\n                            Layout.alignment: Qt.AlignLeft\n                            Layout.topMargin: constants.paddingXSmall\n                            Layout.leftMargin: constants.paddingSmall\n                            font.pixelSize: constants.fontSizeMedium\n                            color: Material.accentColor\n                        }\n                    }\n\n                }\n            }\n        }\n    }\n\n    Component.onCompleted: {\n        root.address = Network.server\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ServerConnectModeComboBox.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nElComboBox {\n    id: control\n\n    enum Mode {\n        Autoconnect,\n        Manual,\n        Single\n    }\n\n    textRole: 'text'\n    valueRole: 'value'\n\n    model: [\n        { text: qsTr('Auto-connect'), value: ServerConnectModeComboBox.Mode.Autoconnect },\n        { text: qsTr('Manual server selection'), value: ServerConnectModeComboBox.Mode.Manual },\n        { text: qsTr('Connect only to a single server'), value: ServerConnectModeComboBox.Mode.Single }\n    ]\n\n    Component.onCompleted: {\n        if (!Network.autoConnectDefined) { // initial setup\n            server_connect_mode_cb.currentIndex = server_connect_mode_cb.indexOfValue(\n                ServerConnectModeComboBox.Mode.Manual)\n        } else {\n            server_connect_mode_cb.currentIndex = server_connect_mode_cb.indexOfValue(\n                Network.autoConnect\n                    ? ServerConnectModeComboBox.Mode.Autoconnect\n                    : Network.oneServer\n                        ? ServerConnectModeComboBox.Mode.Single\n                        : ServerConnectModeComboBox.Mode.Manual\n                )\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ServerDelegate.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Layouts\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nItemDelegate {\n    id: root\n    height: itemLayout.height\n    width: ListView.view.width\n\n    GridLayout {\n        id: itemLayout\n        anchors {\n            left: parent.left\n            right: parent.right\n            leftMargin: constants.paddingXLarge\n            rightMargin: constants.paddingSmall\n        }\n\n        columns: 3\n\n        // topmargin\n        Rectangle {\n            Layout.columnSpan: 3\n            Layout.preferredHeight: constants.paddingSmall\n            color: 'transparent'\n        }\n\n        Item {\n            Layout.preferredWidth: constants.iconSizeMedium\n            Layout.preferredHeight: constants.iconSizeMedium\n            Image {\n                source: '../../../icons/chevron-right.png'\n                width: constants.iconSizeMedium\n                height: constants.iconSizeMedium\n                visible: model.is_primary\n            }\n        }\n        Item {\n            Layout.preferredWidth: constants.iconSizeMedium\n            Layout.preferredHeight: constants.iconSizeMedium\n            Image {\n                source: '../../../icons/status_connected.png'\n                width: constants.iconSizeMedium\n                height: constants.iconSizeMedium\n                visible: model.is_connected\n            }\n        }\n        Label {\n            Layout.fillWidth: true\n            text: model.address\n        }\n\n        // bottommargin\n        Rectangle {\n            Layout.columnSpan: 3\n            Layout.preferredHeight: constants.paddingSmall\n            color: 'transparent'\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/Tag.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nRectangle {\n    id: root\n    radius: height/2\n    implicitWidth: layout.implicitWidth\n    implicitHeight: layout.implicitHeight\n    color: 'transparent'\n    border.color: Material.accentColor\n\n    property alias text: label.text\n    property alias font: label.font\n    property alias labelcolor: label.color\n\n    property string iconSource\n\n    RowLayout {\n        id: layout\n        spacing: 0\n\n        Item {\n            // spacer\n            visible: iconSource\n            Layout.preferredWidth: constants.paddingSmall\n            Layout.preferredHeight: 1\n        }\n\n        Image {\n            visible: iconSource\n            Layout.preferredWidth: constants.iconSizeSmall\n            Layout.preferredHeight: constants.iconSizeSmall\n            source: iconSource\n        }\n\n        Item {\n            // spacer\n            visible: iconSource\n            Layout.preferredWidth: constants.paddingXXSmall\n            Layout.preferredHeight: 1\n        }\n\n        Rectangle {\n            visible: iconSource\n            Layout.preferredHeight: root.height\n            Layout.preferredWidth: 1\n            color: root.color\n            border.color: root.border.color\n        }\n\n        Label {\n            id: label\n            Layout.leftMargin: constants.paddingSmall\n            Layout.rightMargin: constants.paddingSmall\n            Layout.topMargin: constants.paddingXXSmall\n            Layout.bottomMargin: constants.paddingXXSmall\n            font.pixelSize: constants.fontSizeXSmall\n            color: root.border.color\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/TextHighlightPane.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nPane {\n    padding: constants.paddingSmall\n\n    property color backgroundColor: Qt.lighter(Material.background, 1.15)\n    property color borderColor: 'transparent'\n\n    background: Rectangle {\n        color: backgroundColor\n        border.color: borderColor ? borderColor : backgroundColor\n        radius: constants.paddingSmall\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/Toaster.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport \"..\"\n\nItem {\n    id: toaster\n    width: contentItem.implicitWidth\n    height: rect.height\n    visible: false\n\n    property int _y\n    property string _text\n\n    function show(item, text) {\n        _text = text\n        var r = item.mapToItem(parent, item.x, item.y)\n        x = r.x + 0.5*(item.width - toaster.width)\n        y = r.y - toaster.height - constants.paddingLarge\n        toaster._y = y - toaster.height\n        ani.restart()\n    }\n\n    SequentialAnimation {\n        id: ani\n        running: false\n        PropertyAction { target: toaster; property: 'visible'; value: true }\n        PropertyAction { target: toaster; property: 'opacity'; value: 1 }\n        PauseAnimation { duration: 1000}\n        ParallelAnimation {\n            NumberAnimation { target: toaster; property: 'y'; to: toaster._y; duration: 1000; easing.type: Easing.InQuad }\n            NumberAnimation { target: toaster; property: 'opacity'; to: 0; duration: 1000 }\n        }\n        PropertyAction { target: toaster; property: 'visible'; value: false }\n    }\n\n    Rectangle {\n        id: rect\n        width: contentItem.width\n        height: contentItem.height\n        color: constants.colorAlpha(Material.background, 0.90)\n\n        RowLayout {\n            id: contentItem\n            Label {\n                Layout.margins: 10\n                text: toaster._text\n                onTextChanged: {\n                    // hack. ref implicitWidth so it gets recalculated\n                    var _ = contentItem.implicitWidth\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/ToggleLabel.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nLabel {\n    id: root\n    property bool collapsed: true\n    property string labelText\n\n    text: (collapsed ? '▷' : '▽') + ' ' + labelText\n\n    TapHandler {\n        onTapped: {\n            root.collapsed = !root.collapsed\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/TxInput.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nTextHighlightPane {\n    id: root\n\n    property variant model\n    property int idx: -1\n\n    property string _suffix: model.is_mine || model.is_change\n            ? qsTr('mine')\n            : model.is_swap\n                ? qsTr('swap')\n                : model.is_accounting\n                    ? qsTr('accounting')\n                    : \"\"\n\n    ColumnLayout {\n        width: parent.width\n\n        RowLayout {\n            Layout.fillWidth: true\n            Label {\n                Layout.rightMargin: constants.paddingMedium\n                text: '#' + idx\n                font.family: FixedFont\n                font.bold: true\n            }\n            Label {\n                Layout.fillWidth: true\n                text: model.short_id\n                font.family: FixedFont\n            }\n            Label {\n                id: txin_value\n                text: model.value != undefined\n                    ? Config.formatSats(model.value)\n                    : '&lt;' + qsTr('unknown amount') + '&gt;'\n                font.pixelSize: constants.fontSizeMedium\n                font.family: FixedFont\n            }\n            Label {\n                text: Config.baseUnit\n                visible: model.value != undefined\n                font.pixelSize: constants.fontSizeMedium\n                color: Material.accentColor\n            }\n        }\n\n        Rectangle {\n            Layout.fillWidth: true\n            Layout.preferredHeight: 1\n            antialiasing: true\n            color: constants.mutedForeground\n        }\n\n        RowLayout {\n            Layout.fillWidth: true\n            Label {\n                Layout.fillWidth: true\n                text: model.address\n                    ? model.address + (_suffix\n                        ? ' <span style=\"font-size:' + constants.fontSizeXSmall + 'px\">(' + _suffix + ')</span>'\n                        : \"\")\n                    : '&lt;' + qsTr('address unknown') + '&gt;'\n                font.family: FixedFont\n                font.pixelSize: constants.fontSizeMedium\n                textFormat: Text.RichText\n                color: model.is_mine\n                    ? model.is_change\n                        ? constants.colorAddressInternal\n                        : constants.colorAddressExternal\n                    : model.is_swap\n                        ? constants.colorAddressSwap\n                        : model.is_accounting\n                            ? constants.colorAddressAccounting\n                            : Material.foreground\n                wrapMode: Text.WrapAnywhere\n            }\n        }\n\n    }\n}\n\n"
  },
  {
    "path": "electrum/gui/qml/components/controls/TxOutput.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nTextHighlightPane {\n    id: root\n\n    property variant model\n    property bool allowShare: true\n    property bool allowClickAddress: true\n    property int idx: -1\n\n    property string _suffix: model.is_mine || model.is_change || model.is_accounting\n            ? model.is_reserve\n                ? qsTr('reserve')\n                : model.is_accounting\n                    ? qsTr('accounting')\n                    : qsTr('mine')\n            : model.is_swap\n                ? qsTr('swap')\n                : model.is_billing\n                    ? qsTr('billing')\n                    : \"\"\n\n    RowLayout {\n        width: parent.width\n\n        ColumnLayout {\n            Layout.fillWidth: true\n\n            RowLayout {\n                Layout.fillWidth: true\n\n                Label {\n                    Layout.rightMargin: constants.paddingLarge\n                    text: '#' + idx\n                    visible: idx >= 0\n                    font.family: FixedFont\n                    font.pixelSize: constants.fontSizeMedium\n                    font.bold: true\n                }\n                Label {\n                    Layout.fillWidth: true\n                    font.family: FixedFont\n                    text: model.short_id\n                }\n                Label {\n                    text: Config.formatSats(model.value)\n                    font.pixelSize: constants.fontSizeMedium\n                    font.family: FixedFont\n                }\n                Label {\n                    text: Config.baseUnit\n                    font.pixelSize: constants.fontSizeMedium\n                    color: Material.accentColor\n                }\n            }\n\n            Rectangle {\n                Layout.fillWidth: true\n                Layout.preferredHeight: 1\n                antialiasing: true\n                color: constants.mutedForeground\n            }\n\n            RowLayout {\n                Layout.fillWidth: true\n                Label {\n                    text: model.address + (_suffix\n                        ? ' <span style=\"font-size:' + constants.fontSizeXSmall + 'px\">(' + _suffix + ')</span>'\n                        : \"\")\n                    Layout.fillWidth: true\n                    wrapMode: Text.Wrap\n                    font.pixelSize: constants.fontSizeMedium\n                    font.family: FixedFont\n                    textFormat: Text.RichText\n                    color: model.is_mine\n                        ? model.is_change\n                            ? constants.colorAddressInternal\n                            : constants.colorAddressExternal\n                        : model.is_billing\n                            ? constants.colorAddressBilling\n                            : model.is_swap\n                                ? constants.colorAddressSwap\n                                : model.is_accounting\n                                    ? constants.colorAddressAccounting\n                                    : Material.foreground\n                    TapHandler {\n                        enabled: allowClickAddress && model.is_mine\n                        onTapped: {\n                            app.stack.push(Qt.resolvedUrl('../AddressDetails.qml'), {\n                                address: model.address\n                            })\n                        }\n                    }\n                }\n            }\n\n        }\n\n        ToolButton {\n            visible: allowShare\n            icon.source: Qt.resolvedUrl('../../../icons/share.png')\n            icon.color: 'transparent'\n            onClicked: {\n                var dialog = app.genericShareDialog.createObject(app, {\n                    title: qsTr('Tx Output'),\n                    text: model.address\n                })\n                dialog.open()\n            }\n        }\n\n    }\n}\n\n"
  },
  {
    "path": "electrum/gui/qml/components/main.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Basic\nimport QtQuick.Controls.Material\nimport QtQuick.Controls.Material.impl\nimport QtQuick.Window\n\nimport QtQml\nimport QtMultimedia\n\nimport org.electrum 1.0\n\nimport \"controls\"\n\nApplicationWindow\n{\n    id: app\n\n    visible: false // initial value\n\n    readonly property int statusBarHeight: AppController ? AppController.getStatusBarHeight() : 0\n    readonly property int navigationBarHeight: AppController ? AppController.getNavigationBarHeight() : 0\n\n    // dimensions ignored on android\n    width: 480\n    height: 800\n\n    Material.theme: Material.Dark\n    Material.primary: Material.Indigo\n    Material.accent: Material.LightBlue\n    font.pixelSize: constants.fontSizeMedium\n\n    property QtObject constants: appconstants\n    Constants { id: appconstants }\n\n    property alias stack: mainStackView\n    property alias keyboardFreeZone: _keyboardFreeZone\n    property alias infobanner: _infobanner\n\n    property string pendingIntent: \"\"\n\n    property variant activeDialogs: []\n\n    property var _exceptionDialog\n\n    property var pluginobjects: ({})\n\n    property QtObject appMenu: Menu {\n        id: menu\n\n        parent: Overlay.overlay\n        dim: true\n        modal: true\n        Overlay.modal: Rectangle {\n            color: \"#44000000\"\n        }\n\n        property int implicitChildrenWidth: 64\n        width: implicitChildrenWidth + 60 + constants.paddingLarge\n\n        MenuItem {\n            icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor\n            icon.source: '../../icons/network.png'\n            action: Action {\n                text: qsTr('Network')\n                onTriggered: menu.openPage(Qt.resolvedUrl('NetworkOverview.qml'))\n                enabled: stack.currentItem.objectName != 'NetworkOverview'\n            }\n        }\n\n        MenuItem {\n            icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor\n            icon.source: '../../icons/preferences.png'\n            action: Action {\n                text: qsTr('Preferences')\n                onTriggered: menu.openPage(Qt.resolvedUrl('Preferences.qml'))\n                enabled: stack.currentItem.objectName != 'Properties'\n            }\n        }\n\n        MenuItem {\n            icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor\n            icon.source: '../../icons/electrum.png'\n            action: Action {\n                text: qsTr('About');\n                onTriggered: menu.openPage(Qt.resolvedUrl('About.qml'))\n                enabled: stack.currentItem.objectName != 'About'\n            }\n        }\n\n        function openPage(url) {\n            stack.pushOnRoot(url)\n            currentIndex = -1\n        }\n\n        // determine widest element and store in implicitChildrenWidth\n        function updateImplicitWidth() {\n            for (let i = 0; i < menu.count; i++) {\n                var item = menu.itemAt(i)\n                var txt = item.text\n                var txtwidth = fontMetrics.advanceWidth(txt)\n                if (txtwidth > menu.implicitChildrenWidth) {\n                    menu.implicitChildrenWidth = txtwidth\n                }\n            }\n        }\n\n        FontMetrics {\n            id: fontMetrics\n            font: menu.font\n        }\n\n        Component.onCompleted: updateImplicitWidth()\n    }\n\n    function openAppMenu() {\n        appMenu.open()\n        appMenu.x = app.width - appMenu.width\n        appMenu.y = toolbar.height\n    }\n\n    header: ToolBar {\n        id: toolbar\n\n        // Add top margin for status bar on Android when using edge-to-edge\n        topPadding: app.statusBarHeight\n\n        background: Rectangle {\n            implicitHeight: 48\n            color: Material.dialogColor\n\n            layer.enabled: true\n            layer.effect: ElevationEffect {\n                elevation: 4\n                fullWidth: true\n            }\n        }\n\n        ColumnLayout {\n            spacing: 0\n            anchors.left: parent.left\n            anchors.right: parent.right\n            height: toolbar.availableHeight\n\n            RowLayout {\n                id: toolbarTopLayout\n\n                Layout.fillWidth: true\n                Layout.rightMargin: constants.paddingMedium\n                Layout.alignment: Qt.AlignVCenter\n\n                Item {\n                    Layout.fillWidth: true\n                    Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height)\n\n                    MouseArea {\n                        anchors.fill: parent\n                        enabled: Daemon.currentWallet &&\n                            (!stack.currentItem || !stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name)\n                        onClicked: {\n                            stack.getRoot().menu.open()  // open wallet-menu\n                            stack.getRoot().menu.y = toolbar.height\n                        }\n                    }\n\n                    RowLayout {\n                        width: parent.width\n\n                        Item {\n                            Layout.preferredWidth: constants.paddingXLarge\n                            Layout.preferredHeight: 1\n                        }\n\n                        Image {\n                            Layout.preferredWidth: constants.iconSizeSmall\n                            Layout.preferredHeight: constants.iconSizeSmall\n                            visible: Daemon.currentWallet &&\n                                (!stack.currentItem || !stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name)\n                            source: '../../icons/wallet.png'\n                        }\n\n                        Label {\n                            Layout.fillWidth: true\n                            Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height)\n                            text: stack.currentItem && stack.currentItem.title\n                                ? stack.currentItem.title\n                                : Daemon.currentWallet.name\n                            elide: Label.ElideRight\n                            verticalAlignment: Qt.AlignVCenter\n                            font.pixelSize: constants.fontSizeMedium\n                            font.bold: true\n                        }\n                    }\n                }\n\n                Item {\n                    implicitHeight: 48\n                    implicitWidth: statusIconsLayout.width\n\n                    MouseArea {\n                        anchors.fill: parent\n                        onClicked: openAppMenu()  // open global-app-menu\n                    }\n\n                    RowLayout {\n                        id: statusIconsLayout\n                        anchors.verticalCenter: parent.verticalCenter\n                        Item {\n                            Layout.preferredWidth: constants.paddingLarge\n                            Layout.preferredHeight: 1\n                        }\n\n                        Item {\n                            visible: Network.isTestNet\n                            width: column.width\n                            height: column.height\n\n                            ColumnLayout {\n                                id: column\n                                spacing: 0\n                                Image {\n                                    Layout.alignment: Qt.AlignHCenter\n                                    Layout.preferredWidth: constants.iconSizeSmall\n                                    Layout.preferredHeight: constants.iconSizeSmall\n                                    source: \"../../icons/info.png\"\n                                }\n\n                                Label {\n                                    id: networkNameLabel\n                                    text: Network.networkName\n                                    color: Material.accentColor\n                                    font.pixelSize: constants.fontSizeXSmall\n                                }\n                            }\n                        }\n\n                        LightningNetworkStatusIndicator {\n                            id: lnnsi\n                        }\n                        OnchainNetworkStatusIndicator { }\n                    }\n                }\n            }\n\n            // hack to force relayout of toolbar\n            // since qt6 LightningNetworkStatusIndicator.visible doesn't trigger relayout(?)\n            Item {\n                Layout.preferredHeight: 1\n                Layout.topMargin: -1\n                Layout.preferredWidth: lnnsi.visible\n                    ? 1\n                    : 2\n            }\n        }\n    }\n\n    ColumnLayout {\n        width: parent.width\n        height: _keyboardFreeZone.height - header.height\n        spacing: 0\n\n        InfoBanner {\n            id: _infobanner\n            Layout.fillWidth: true\n        }\n\n        StackView {\n            id: mainStackView\n            Layout.fillHeight: true\n            Layout.fillWidth: true\n\n            initialItem: Component {\n                Wallets {}\n            }\n\n            function getRoot() {\n                return mainStackView.get(0)\n            }\n            function pushOnRoot(item) {\n                if (mainStackView.depth > 1) {\n                    mainStackView.replace(mainStackView.get(1), item)\n                } else {\n                    mainStackView.push(item)\n                }\n            }\n            function replaceRoot(item_url) {\n                mainStackView.clear()\n                mainStackView.push(Qt.resolvedUrl(item_url))\n            }\n        }\n\n        // Add bottom padding for navigation bar on Android when UI is edge-to-edge\n        Item {\n            visible: app.navigationBarHeight > 0\n            Layout.fillWidth: true\n            Layout.preferredHeight: app.navigationBarHeight\n        }\n    }\n\n    Timer {\n        id: coverTimer\n        interval: 10\n        onTriggered: {\n            app.visible = true\n            cover.opacity = 0\n        }\n    }\n\n    Rectangle {\n        id: cover\n        parent: Overlay.overlay\n        anchors.fill: parent\n\n        z: 1000\n        color: 'black'\n\n        Behavior on opacity {\n            enabled: AppController ? AppController.isAndroid() : false\n            NumberAnimation {\n                duration: 1000\n                easing.type: Easing.OutQuad;\n            }\n        }\n    }\n\n    Item {\n        id: _keyboardFreeZone\n        // Item as first child in Overlay that adjusts its size to the available\n        // screen space minus the virtual keyboard (e.g. to center dialogs in)\n        // see also ElDialog.resizeWithKeyboard property\n        parent: Overlay.overlay\n        width: parent.width\n        height: parent.height\n\n        states: [\n            State {\n                name: 'visible'\n                when: Qt.inputMethod.keyboardRectangle.y\n                PropertyChanges {\n                    target: _keyboardFreeZone\n                    height: _keyboardFreeZone.parent.height - (Screen.desktopAvailableHeight - (Qt.inputMethod.keyboardRectangle.y/Screen.devicePixelRatio))\n                }\n            }\n        ]\n\n        transitions: [\n            Transition {\n                from: ''\n                to: 'visible'\n                NumberAnimation {\n                    properties: 'height'\n                    duration: 100\n                    easing.type: Easing.OutQuad\n                }\n            },\n            Transition {\n                from: 'visible'\n                to: ''\n                SequentialAnimation {\n                    PauseAnimation {\n                        duration: 200\n                    }\n                    NumberAnimation {\n                        properties: 'height'\n                        duration: 50\n                        easing.type: Easing.OutQuad\n                    }\n                }\n            }\n        ]\n    }\n\n    property alias newWalletWizard: _newWalletWizard\n    Component {\n        id: _newWalletWizard\n        NewWalletWizard {\n            onClosed: destroy()\n        }\n    }\n\n    property alias termsOfUseWizard: _termsOfUseWizard\n    Component {\n        id: _termsOfUseWizard\n        TermsOfUseWizard {\n            onClosed: destroy()\n        }\n    }\n\n    property alias serverConnectWizard: _serverConnectWizard\n    Component {\n        id: _serverConnectWizard\n        ServerConnectWizard {\n            onClosed: destroy()\n        }\n    }\n\n    property alias messageDialog: _messageDialog\n    Component {\n        id: _messageDialog\n        MessageDialog {\n            onClosed: destroy()\n        }\n    }\n\n    property alias helpDialog: _helpDialog\n    Component {\n        id: _helpDialog\n        HelpDialog {\n            onClosed: destroy()\n        }\n    }\n\n    property alias passwordDialog: _passwordDialog\n    Component {\n        id: _passwordDialog\n        PasswordDialog {\n            onClosed: destroy()\n        }\n    }\n\n    property alias genericShareDialog: _genericShareDialog\n    Component {\n        id: _genericShareDialog\n        GenericShareDialog {\n            onClosed: destroy()\n        }\n    }\n\n    property alias openWalletDialog: _openWalletDialog\n    Component {\n        id: _openWalletDialog\n        OpenWalletDialog {\n            onClosed: destroy()\n        }\n    }\n\n    property alias loadingWalletDialog: _loadingWalletDialog\n    Component {\n        id: _loadingWalletDialog\n        LoadingWalletDialog {\n            onClosed: destroy()\n        }\n    }\n\n    property Component scanDialog  // set in Component.onCompleted\n    Component {\n        id: _scanDialog\n        QRScanner {\n            onFinished: destroy()\n        }\n    }\n    Component {\n        id: _qtScanDialog\n        ScanDialog {\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: crashDialog\n        ExceptionDialog {\n            onClosed: destroy()\n        }\n    }\n\n    property alias channelOpenProgressDialog: _channelOpenProgressDialog\n    ChannelOpenProgressDialog {\n        id: _channelOpenProgressDialog\n    }\n\n    property alias signVerifyMessageDialog: _signVerifyMessageDialog\n    Component {\n        id: _signVerifyMessageDialog\n        SignVerifyMessageDialog {\n            onClosed: destroy()\n        }\n    }\n\n    property alias nostrSwapServersDialog: _nostrSwapServersDialog\n    Component {\n        id: _nostrSwapServersDialog\n        NostrSwapServersDialog {\n            onClosed: destroy()\n        }\n    }\n\n    Component {\n        id: swapDialog\n        SwapDialog {\n            id: _swapdialog\n            onClosed: destroy()\n            swaphelper: SwapHelper {\n                id: _swaphelper\n                wallet: Daemon.currentWallet\n                onAuthRequired: (method, authMessage) => {\n                    app.handleAuthRequired(_swaphelper, method, authMessage)\n                }\n                onError: (message) => {\n                    var dialog = app.messageDialog.createObject(app, {\n                        title: qsTr('Error'),\n                        iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                        text: message\n                    })\n                    dialog.open()\n                }\n                onUndefinedNPub: {\n                    var dialog = app.nostrSwapServersDialog.createObject(app, {\n                        swaphelper: _swaphelper,\n                        selectedPubkey: Config.swapServerNPub\n                    })\n                    dialog.accepted.connect(function() {\n                        Config.swapServerNPub = dialog.selectedPubkey\n                        _swaphelper.setReadyState()\n                    })\n                    dialog.rejected.connect(function() {\n                        _swaphelper.npubSelectionCancelled()\n                    })\n                    dialog.open()\n                }\n            }\n        }\n    }\n\n    NotificationPopup {\n        id: notificationPopup\n        width: parent.width\n    }\n\n    Component.onCompleted: {\n        coverTimer.start()\n\n        if (AppController.isAndroid()) {\n            app.scanDialog = _scanDialog\n        } else {\n            app.scanDialog = _qtScanDialog\n        }\n\n        function continueWithServerConnection() {\n            if (!Network.autoConnectDefined) {\n                var dialog = serverConnectWizard.createObject(app)\n                // without completed serverConnectWizard we can't start\n                dialog.rejected.connect(function() {\n                    app.visible = false\n                    AppController.wantClose = true\n                    Qt.callLater(Qt.quit)\n                })\n                dialog.accepted.connect(function() {\n                    Daemon.startNetwork()\n                    var newww = app.newWalletWizard.createObject(app)\n                    newww.walletCreated.connect(function() {\n                        Daemon.availableWallets.reload()\n                        // and load the new wallet\n                        Daemon.loadWallet(newww.path, newww.wizard_data['password'])\n                    })\n                    newww.open()\n                })\n                dialog.open()\n            } else {\n                Daemon.startNetwork()\n                if (Daemon.availableWallets.rowCount() > 0) {\n                    Daemon.loadWallet()\n                } else {\n                    var newww = app.newWalletWizard.createObject(app)\n                    newww.walletCreated.connect(function() {\n                        Daemon.availableWallets.reload()\n                        // and load the new wallet\n                        Daemon.loadWallet(newww.path, newww.wizard_data['password'])\n                    })\n                    newww.open()\n                }\n            }\n        }\n\n        if (!Config.termsOfUseAccepted) {\n            var dialog = termsOfUseWizard.createObject(app)\n\n            dialog.rejected.connect(function() {\n                app.visible = false\n                AppController.wantClose = true\n                Qt.callLater(Qt.quit)\n            })\n            dialog.accepted.connect(function() {\n                Config.termsOfUseAccepted = true\n                continueWithServerConnection()\n            })\n            dialog.open()\n        } else {\n            continueWithServerConnection()\n        }\n    }\n\n    onClosing: (close) => {\n        if (AppController.wantClose) {\n            // destroy most GUI components so that we don't dump so many null reference warnings on exit\n            app.header.visible = false\n            mainStackView.clear()\n            return\n        }\n        if (activeDialogs.length > 0) {\n            var activeDialog = activeDialogs[activeDialogs.length - 1]\n            if (activeDialog.allowClose) {\n                console.log('main: dialog.doClose')\n                activeDialog.doClose()\n            } else {\n                console.log('dialog disallowed close')\n            }\n            close.accepted = false\n            return\n        }\n        if (stack.depth > 1) {\n            close.accepted = false\n            stack.pop()\n        } else {\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Close Electrum?'),\n                yesno: true\n            })\n            dialog.accepted.connect(function() {\n                AppController.wantClose = true\n                app.close()\n            })\n            dialog.open()\n            close.accepted = false\n        }\n    }\n\n    property var _pendingBiometricAuth: null\n    property var _loadingWalletContext: null\n\n    Connections {\n        target: Biometrics\n        function onUnlockSuccess(password) {\n            if (app._pendingBiometricAuth) {\n                if (app._pendingBiometricAuth.action === 'load_wallet') {\n                    app._loadingWalletContext = _pendingBiometricAuth\n                    Daemon.loadWallet(app._pendingBiometricAuth.path, password)\n                    app._pendingBiometricAuth = null\n                    return\n                }\n\n                let qtobject = app._pendingBiometricAuth.qtobject\n                let method = app._pendingBiometricAuth.method\n\n                if (Daemon.currentWallet.verifyPassword(password)) {\n                    qtobject.authProceed()\n                } else {\n                    console.warn(\"Biometric password invalid falling back to manual input\")\n                    // this shouldn't really happen so we better disable biometric auth\n                    Biometrics.disable()\n                    handleManualAuth(qtobject, method, app._pendingBiometricAuth.authMessage)\n                }\n                app._pendingBiometricAuth = null\n            }\n        }\n\n        function onUnlockError(error) {\n            console.log(\"Biometric auth failed: \" + error)\n            // we end up here if QEBiometrics fails to give us the decrypted password. The user might\n            // have cancelled the biometric auth popup or the key got invalidated because a new fingerprint got registered.\n            if (app._pendingBiometricAuth) {\n                if (app._pendingBiometricAuth.action === 'load_wallet') {\n                    // set loadingWalletContext to disable biometric auth until the OpenWalletDialog is closed\n                    app._loadingWalletContext = app._pendingBiometricAuth\n                    showOpenWalletDialog(app._pendingBiometricAuth.name, app._pendingBiometricAuth.path)\n                } else {\n                    console.log('biometric auth failed, not falling back to passwordDialog')\n                    app._pendingBiometricAuth.qtobject.authCancel()  // no fallback to password dialog\n                }\n                app._pendingBiometricAuth = null\n            }\n        }\n\n        function onAuthRequired(method, authMessage) {\n            handleAuthRequired(Biometrics, method, authMessage)\n        }\n    }\n\n    property var _opendialog: null\n    property var _opendialog_startup: true\n\n    function showOpenWalletDialog(name, path) {\n        if (!_opendialog) {\n            _opendialog = openWalletDialog.createObject(app, {\n                name: name,\n                path: path,\n                isStartup: _opendialog_startup,\n            })\n            _opendialog.closed.connect(function() {\n                _opendialog = null\n                app._loadingWalletContext = null  // dialog closed, we can allow trying biometric auth again\n                _opendialog_startup = false\n            })\n            _opendialog.open()\n        }\n    }\n\n    Connections {\n        target: Daemon\n        function onWalletRequiresPassword(name, path) {\n            console.log('wallet requires password')\n            if (Biometrics.isAvailable && Biometrics.isEnabled && !app._loadingWalletContext) {\n                app._pendingBiometricAuth = {\n                    action: 'load_wallet',\n                    name: name,\n                    path: path\n                }\n                Biometrics.unlock()\n            } else {\n                showOpenWalletDialog(name, path)\n            }\n        }\n        function onWalletOpenError(error) {\n            console.log('wallet open error')\n            var dialog = app.messageDialog.createObject(app, {\n                title: qsTr('Error'),\n                iconSource: Qt.resolvedUrl('../../icons/warning.png'),\n                text: error\n            })\n            dialog.open()\n        }\n        function onAuthRequired(method, authMessage) {\n            handleAuthRequired(Daemon, method, authMessage)\n        }\n        function onLoadingChanged() {\n            if (!Daemon.loading)\n                return\n            console.log('wallet loading')\n            var dialog = loadingWalletDialog.createObject(app, { allowClose: false } )\n            dialog.open()\n        }\n        function onWalletLoaded() {\n            app._loadingWalletContext = null  // either biometric auth or manual auth was successful\n        }\n    }\n\n    Connections {\n        target: AppController\n        function onUserNotify(wallet_name, message) {\n            notificationPopup.show(wallet_name, message)\n        }\n        function onShowException(crash_data) {\n            if (app._exceptionDialog)\n                return\n            app._exceptionDialog = crashDialog.createObject(app, {\n                crashData: crash_data\n            })\n            app._exceptionDialog.onClosed.connect(function() {\n                app._exceptionDialog = null\n            })\n            app._exceptionDialog.open()\n        }\n        function onPluginLoaded(name) {\n            console.log('plugin ' + name + ' loaded')\n            var loader = AppController.plugin(name).loader\n            if (loader == undefined)\n                return\n            var url = Qt.resolvedUrl('../../../plugins/' + name + '/qml/' + loader)\n            var comp = Qt.createComponent(url)\n            if (comp.status == Component.Error) {\n                console.log('Could not find/parse PluginLoader for plugin ' + name)\n                console.log(comp.errorString())\n                return\n            }\n            var obj = comp.createObject(app)\n            if (obj != null)\n                app.pluginobjects[name] = obj\n        }\n        function onUriReceived(uri) {\n            console.log('uri received (main): ' + uri)\n            app.pendingIntent = uri\n        }\n    }\n\n    function pluginsComponentsByName(comp_name) {\n        // return named QML components from plugins\n        var plugins = AppController.plugins\n        var result = []\n        for (var i=0; i < plugins.length; i++) {\n            if (!plugins[i].enabled)\n                continue\n            var pluginobject = app.pluginobjects[plugins[i].name]\n            if (!pluginobject)\n                continue\n            if (!(comp_name in pluginobject))\n                continue\n            var comp = pluginobject[comp_name]\n            if (!comp)\n                continue\n\n            result.push(comp)\n        }\n        return result\n    }\n\n    Connections {\n        target: Daemon.currentWallet\n        function onAuthRequired(method, authMessage) {\n            handleAuthRequired(Daemon.currentWallet, method, authMessage)\n        }\n        // TODO: add to notification queue instead of barging through\n        function onPaymentSucceeded(key) {\n            notificationPopup.show(Daemon.currentWallet.name, qsTr('Payment succeeded'))\n        }\n        function onPaymentFailed(key, reason) {\n            notificationPopup.show(Daemon.currentWallet.name, qsTr('Payment failed') + ': ' + reason)\n        }\n    }\n\n    Connections {\n        target: Config\n        function onAuthRequired(method, authMessage) {\n            handleAuthRequired(Config, method, authMessage)\n        }\n    }\n\n    function handleAuthRequired(qtobject, method, authMessage) {\n        console.log('auth using method ' + method)\n\n        if (method === 'payment_auth') {\n            if (Config.paymentAuthentication) {\n                // treat like a wallet auth request\n                method = 'wallet'\n            } else {\n                handleAuthConfirmationOnly(qtobject, authMessage)\n                return\n            }\n        }\n\n        if (Daemon.currentWallet.verifyPassword('')) {\n            // wallet has no password\n            qtobject.authProceed()\n            return\n        }\n\n        if (method !== 'wallet_password_only') {\n            if (Biometrics.isAvailable && Biometrics.isEnabled) {\n                app._pendingBiometricAuth = {\n                    qtobject: qtobject,\n                    method: method,\n                    authMessage: authMessage\n                }\n                Biometrics.unlock(authMessage)\n                return\n            }\n        }\n\n        handleManualAuth(qtobject, method, authMessage)\n    }\n\n    function handleManualAuth(qtobject, method, authMessage) {\n        // 'payment_auth' should have been converted to 'wallet' at this point\n        if (method === 'wallet' || method === 'wallet_password_only') {\n            var dialog = app.passwordDialog.createObject(app, authMessage ? {'title': authMessage} : {})\n            dialog.passwordEntered.connect(function(password) {\n                if (Daemon.currentWallet.verifyPassword(password)) {\n                    dialog.close()\n                    qtobject.authProceed()\n                } else {\n                    dialog.clearPassword()\n                    dialog.errorMessage = qsTr(\"Invalid Password\")\n                }\n            })\n            dialog.rejected.connect(function() {\n                qtobject.authCancel()\n            })\n            dialog.open()\n        } else {\n            console.log('unknown auth method ' + method)\n            qtobject.authCancel()\n        }\n    }\n\n    function handleAuthConfirmationOnly(qtobject, authMessage) {\n        if (!authMessage) {\n            qtobject.authProceed()\n            return\n        }\n        var dialog = app.messageDialog.createObject(app, {\n            title: authMessage,\n            yesno: true\n        })\n        dialog.accepted.connect(function() {\n            qtobject.authProceed()\n        })\n        dialog.rejected.connect(function() {\n            qtobject.authCancel()\n        })\n        dialog.open()\n    }\n\n    function startSwap() {\n        var swapdialog = swapDialog.createObject(app)\n        swapdialog.open()\n    }\n\n    property var _lastActive: 0 // record time of last activity\n    property bool _lockDialogShown: false\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCConfirmExt.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"../controls\"\n\nWizardComponent {\n    id: root\n    securePage: true\n\n    valid: false\n\n    property int cosigner: 0\n\n    function checkValid() {\n        valid = false\n        var input = customwordstext.text\n        if (input == '') {\n            return\n        }\n\n        if (cosigner) {\n            // multisig cosigner\n            if (input != wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_extra_words']) {\n                return\n            }\n        } else {\n            if (input != wizard_data['seed_extra_words']) {\n                return\n            }\n        }\n        valid = true\n    }\n\n    Flickable {\n        anchors.fill: parent\n        contentHeight: mainLayout.height\n        clip: true\n        interactive: height < contentHeight\n\n        ColumnLayout {\n            id: mainLayout\n            width: parent.width\n            spacing: constants.paddingLarge\n\n            Label {\n                Layout.fillWidth: true\n                wrapMode: Text.Wrap\n                text: qsTr('Please enter your custom word(s) a second time:')\n            }\n\n            TextField {\n                id: customwordstext\n                Layout.fillWidth: true\n                Layout.columnSpan: 2\n                placeholderText: qsTr('Enter your custom word(s) here')\n                inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase\n                onTextChanged: checkValid()\n            }\n        }\n    }\n\n    Component.onCompleted: {\n        if (wizard_data['wallet_type'] == 'multisig') {\n            if ('multisig_current_cosigner' in wizard_data)\n                cosigner = wizard_data['multisig_current_cosigner']\n        }\n    }\n}"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCConfirmSeed.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nimport \"..\"\nimport \"../controls\"\n\nWizardComponent {\n    securePage: true\n\n    valid: false\n\n    function checkValid() {\n        var seedvalid = wizard.wiz.isMatchingSeed(wizard_data['seed'], confirm.text)\n        valid = seedvalid\n    }\n\n    Flickable {\n        anchors.fill: parent\n        contentHeight: mainLayout.height\n        clip:true\n        interactive: height < contentHeight\n\n        ColumnLayout {\n            id: mainLayout\n            width: parent.width\n\n            InfoTextArea {\n                Layout.fillWidth: true\n                Layout.bottomMargin: constants.paddingLarge\n                text: qsTr('Your seed is important!') + ' ' +\n                    qsTr('If you lose your seed, your money will be permanently lost.') + ' ' +\n                    qsTr('To make sure that you have properly saved your seed, please retype it here.')\n            }\n\n            Label {\n                text: qsTr('Confirm your seed (re-enter)')\n            }\n\n            SeedTextArea {\n                id: confirm\n                Layout.fillWidth: true\n                placeholderText: qsTr('Enter your seed')\n                onTextChanged: checkValid()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCCosignerKeystore.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"../controls\"\n\nWizardComponent {\n    id: root\n\n    valid: keystoregroup.checkedButton !== null\n\n    property int cosigner: 0\n    property int participants: 0\n    property string multisigMasterPubkey: wizard_data['multisig_master_pubkey']\n\n    function apply() {\n        wizard_data['cosigner_keystore_type'] = keystoregroup.checkedButton.keystoretype\n        wizard_data['multisig_current_cosigner'] = cosigner\n        wizard_data['multisig_cosigner_data'][cosigner.toString()] = {\n            'keystore_type': keystoregroup.checkedButton.keystoretype\n        }\n    }\n\n    ButtonGroup {\n        id: keystoregroup\n    }\n\n    ColumnLayout {\n        width: parent.width\n\n        Label {\n            Layout.fillWidth: true\n\n            visible: cosigner\n            text: qsTr('Here is your master public key. Please share it with your cosigners')\n            wrapMode: Text.Wrap\n        }\n\n        TextHighlightPane {\n            Layout.fillWidth: true\n\n            visible: cosigner\n\n            RowLayout {\n                width: parent.width\n                Label {\n                    Layout.fillWidth: true\n                    text: multisigMasterPubkey\n                    font.pixelSize: constants.fontSizeMedium\n                    font.family: FixedFont\n                    wrapMode: Text.Wrap\n                }\n                ToolButton {\n                    icon.source: '../../../icons/share.png'\n                    icon.color: 'transparent'\n                    onClicked: {\n                        var dialog = app.genericShareDialog.createObject(app,\n                            { title: qsTr('Master public key'), text: multisigMasterPubkey }\n                        )\n                        dialog.open()\n                    }\n                }\n            }\n        }\n\n        Rectangle {\n            Layout.fillWidth: true\n            Layout.preferredHeight: 1\n            Layout.topMargin: constants.paddingLarge\n            Layout.bottomMargin: constants.paddingLarge\n            visible: cosigner\n            color: Material.accentColor\n        }\n\n        Label {\n            Layout.fillWidth: true\n            text: qsTr('Add cosigner #%1 of %2 to your multi-sig wallet').arg(cosigner).arg(participants)\n            wrapMode: Text.Wrap\n        }\n        ElRadioButton {\n            ButtonGroup.group: keystoregroup\n            property string keystoretype: 'masterkey'\n            checked: true\n            text: qsTr('Cosigner key')\n        }\n        ElRadioButton {\n            ButtonGroup.group: keystoregroup\n            property string keystoretype: 'haveseed'\n            text: qsTr('Cosigner seed')\n        }\n    }\n\n    Component.onCompleted: {\n        participants = wizard_data['multisig_participants']\n\n        // cosigner index is determined here and put on the wizard_data dict in apply()\n        // as this page is the start for each additional cosigner\n        cosigner = 2 + Object.keys(wizard_data['multisig_cosigner_data']).length\n    }\n}\n\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCCreateSeed.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nimport \"../controls\"\n\nWizardComponent {\n    securePage: true\n\n    valid: seedtext.text != ''\n\n    function apply() {\n        wizard_data['seed'] = seedtext.text\n        wizard_data['seed_variant'] = 'electrum' // generated seed always electrum variant\n        wizard_data['seed_extend'] = true  // true so we get forwarded to the passphrase page\n    }\n\n    function setWarningText(numwords) {\n        var t = [\n            '<p>',\n            qsTr('Please save these %1 words on paper (order is important).').arg(numwords),\n            qsTr('This seed will allow you to recover your wallet in case of computer failure.'),\n            '</p>',\n            '<b>' + qsTr('WARNING') + ':</b>',\n            '<ul>',\n            '<li>' + qsTr('Never disclose your seed.') + '</li>',\n            '<li>' + qsTr('Never type it on a website.') + '</li>',\n            '<li>' + qsTr('Do not store it electronically.') + '</li>',\n            '</ul>'\n        ]\n        warningtext.text = t.join(' ')\n    }\n\n    Flickable {\n        anchors.fill: parent\n        contentHeight: mainLayout.height\n        clip:true\n        interactive: height < contentHeight\n\n        GridLayout {\n            id: mainLayout\n            width: parent.width\n            columns: 1\n\n            InfoTextArea {\n                id: warningtext\n                Layout.fillWidth: true\n                iconStyle: InfoTextArea.IconStyle.Warn\n            }\n\n            Label {\n                Layout.topMargin: constants.paddingMedium\n                Layout.fillWidth: true\n                wrapMode: Text.Wrap\n                text: qsTr('Your wallet generation seed is:')\n            }\n\n            SeedTextArea {\n                id: seedtext\n                readOnly: true\n                Layout.fillWidth: true\n\n                BusyIndicator {\n                    anchors.centerIn: parent\n                    height: parent.height * 2/3\n                    visible: seedtext.text == ''\n                }\n            }\n\n            Component.onCompleted : {\n                setWarningText(12)\n            }\n        }\n    }\n\n    Component.onCompleted: {\n        bitcoin.generateSeed(wizard_data['seed_type'])\n    }\n\n    Bitcoin {\n        id: bitcoin\n        onGeneratedSeedChanged: {\n            seedtext.text = generatedSeed\n            setWarningText(generatedSeed.split(' ').length)\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCEnterExt.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"../controls\"\n\nWizardComponent {\n    id: root\n    securePage: true\n\n    valid: true\n\n    property int cosigner: 0\n\n    function apply() {\n        var seed_extend = extendcb.checked\n        if (cosigner) {\n            wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_extend'] = seed_extend\n            wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_extra_words'] = seed_extend ? customwordstext.text : ''\n        } else {\n            wizard_data['seed_extend'] = seed_extend\n            wizard_data['seed_extra_words'] = seed_extend ? customwordstext.text : ''\n        }\n    }\n\n    function checkValid() {\n        valid = false\n        validationtext.text = ''\n\n        if (extendcb.checked && customwordstext.text == '') {\n            return\n        } else {\n            // passphrase is either disabled or filled with text\n            apply()\n            if (cosigner && wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_variant'] == 'electrum') {\n                // check if master keys are not duplicated after entering passphrase\n                if (wiz.hasDuplicateMasterKeys(wizard_data)) {\n                    validationtext.text = qsTr('Error: duplicate master public key')\n                    return\n                }\n            }\n        }\n        valid = true\n    }\n\n    Flickable {\n        anchors.fill: parent\n        contentHeight: mainLayout.height\n        clip: true\n        interactive: height < contentHeight\n\n        ColumnLayout {\n            id: mainLayout\n            width: parent.width\n            spacing: constants.paddingLarge\n\n            InfoTextArea {\n                id: validationtext\n                Layout.fillWidth: true\n                Layout.columnSpan: 2\n                visible: text\n                iconStyle: InfoTextArea.IconStyle.Error\n            }\n\n            Label {\n                Layout.fillWidth: true\n                wrapMode: Text.Wrap\n                text: [\n                    qsTr('You may extend your seed with custom words.'),\n                    qsTr('Your seed extension must be saved together with your seed.'),\n                    qsTr('Note that this is NOT your encryption password.'),\n                    '<br/>',\n                    qsTr('Do not enable it unless you know what it does!'),\n                ].join(' ')\n            }\n\n            ElCheckBox {\n                id: extendcb\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n                text: qsTr('Extend seed with custom words')\n                onCheckedChanged: checkValid()\n            }\n\n            TextField {\n                id: customwordstext\n                enabled: extendcb.checked\n                Layout.fillWidth: true\n                Layout.columnSpan: 2\n                placeholderText: qsTr('Enter your custom word(s)')\n                inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase\n                onTextChanged: startValidationTimer()\n            }\n        }\n    }\n\n    function startValidationTimer() {\n        valid = false\n        validationTimer.restart()\n    }\n\n    Timer {\n        id: validationTimer\n        interval: 250\n        repeat: false\n        onTriggered: checkValid()\n    }\n\n    Component.onCompleted: {\n        if (wizard_data['wallet_type'] == 'multisig') {\n            if ('multisig_current_cosigner' in wizard_data)\n                cosigner = wizard_data['multisig_current_cosigner']\n        }\n        checkValid()\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCHaveMasterKey.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"../controls\"\n\nWizardComponent {\n    id: root\n    securePage: true\n\n    valid: false\n\n    property int cosigner: 0\n    property int participants: 0\n    property string multisigMasterPubkey\n\n    function apply() {\n        applyMasterKey(masterkey_ta.text)\n    }\n\n    function applyMasterKey(key) {\n        key = key.trim()\n        if (cosigner) {\n            wizard_data['multisig_cosigner_data'][cosigner.toString()]['master_key'] = key\n        } else {\n            wizard_data['master_key'] = key\n        }\n    }\n\n    function verifyMasterKey(key) {\n        valid = false\n        validationtext.text = ''\n        key = key.trim()\n\n        if (!key) {\n            validationtext.text = ''\n            return false\n        }\n\n        if (!bitcoin.verifyMasterKey(key, wizard_data['wallet_type'])) {\n            validationtext.text = qsTr('Error: invalid master key')\n            return false\n        }\n\n        if (cosigner) {\n            applyMasterKey(key)\n            if (wiz.hasDuplicateMasterKeys(wizard_data)) {\n                validationtext.text = qsTr('Error: duplicate master public key')\n                return false\n            }\n            if (wiz.hasHeterogeneousMasterKeys(wizard_data)) {\n                validationtext.text = qsTr('Error: master public key types do not match')\n                return false\n            }\n        }\n\n        return valid = true\n    }\n\n    ColumnLayout {\n        width: parent.width\n\n        Label {\n            Layout.fillWidth: true\n\n            visible: cosigner\n            text: qsTr('Here is your master public key. Please share it with your cosigners')\n            wrapMode: Text.Wrap\n        }\n\n        TextHighlightPane {\n            Layout.fillWidth: true\n\n            visible: cosigner\n\n            RowLayout {\n                width: parent.width\n                Label {\n                    Layout.fillWidth: true\n                    text: multisigMasterPubkey\n                    font.pixelSize: constants.fontSizeMedium\n                    font.family: FixedFont\n                    wrapMode: Text.Wrap\n                }\n                ToolButton {\n                    icon.source: '../../../icons/share.png'\n                    icon.color: 'transparent'\n                    onClicked: {\n                        var dialog = app.genericShareDialog.createObject(app, {\n                            title: qsTr('Master public key'),\n                            text: multisigMasterPubkey\n                        })\n                        dialog.open()\n                    }\n                }\n            }\n        }\n\n        Rectangle {\n            Layout.fillWidth: true\n            Layout.preferredHeight: 1\n            Layout.topMargin: constants.paddingLarge\n            Layout.bottomMargin: constants.paddingLarge\n            visible: cosigner\n            color: Material.accentColor\n        }\n\n        Label {\n            text: qsTr('Cosigner #%1 of %2').arg(cosigner).arg(participants)\n            visible: cosigner\n        }\n\n        Label {\n            Layout.fillWidth: true\n            text: cosigner\n                    ? [qsTr('Please enter the master public key (xpub) of your cosigner.'),\n                       qsTr('Enter their master private key (xprv) if you want to be able to sign for them.')\n                       ].join('\\n')\n                    : [qsTr('Please enter your master private key (xprv).'),\n                       qsTr('You can also enter a public key (xpub) here, but be aware you will then create a watch-only wallet if all cosigners are added using public keys')\n                       ].join('\\n')\n            wrapMode: Text.Wrap\n        }\n\n        RowLayout {\n            ElTextArea {\n                id: masterkey_ta\n                Layout.fillWidth: true\n                Layout.minimumHeight: 160\n                font.family: FixedFont\n                wrapMode: TextEdit.WrapAnywhere\n                onTextChanged: {\n                    if (anyActiveFocus) {\n                        verifyMasterKey(text)\n                    }\n                }\n                inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase\n                background: PaneInsetBackground {\n                    baseColor: constants.darkerDialogBackground\n                }\n            }\n            ColumnLayout {\n                Layout.alignment: Qt.AlignTop\n                ToolButton {\n                    icon.source: '../../../icons/paste.png'\n                    icon.height: constants.iconSizeMedium\n                    icon.width: constants.iconSizeMedium\n                    onClicked: {\n                        if (verifyMasterKey(AppController.clipboardToText()))\n                            masterkey_ta.text = AppController.clipboardToText()\n                        else\n                            masterkey_ta.text = ''\n                    }\n                }\n                ToolButton {\n                    icon.source: '../../../icons/qrcode.png'\n                    icon.height: constants.iconSizeMedium\n                    icon.width: constants.iconSizeMedium\n                    scale: 1.2\n                    onClicked: {\n                        var dialog = app.scanDialog.createObject(app, {\n                            hint: cosigner\n                                ? qsTr('Scan a cosigner master public key')\n                                : qsTr('Scan a master key')\n                        })\n                        dialog.onFoundText.connect(function(data) {\n                            if (verifyMasterKey(data))\n                                masterkey_ta.text = data\n                            else\n                                masterkey_ta.text = ''\n                            dialog.close()\n                        })\n                        dialog.open()\n                    }\n                }\n            }\n        }\n\n        TextArea {\n            id: validationtext\n            visible: text\n            Layout.fillWidth: true\n            readOnly: true\n            wrapMode: TextInput.WordWrap\n            background: Rectangle {\n                color: 'transparent'\n            }\n        }\n    }\n\n    Bitcoin {\n        id: bitcoin\n        onValidationMessageChanged: {\n            validationtext.text = validationMessage\n        }\n    }\n\n    Component.onCompleted: {\n        if (wizard_data['wallet_type'] == 'multisig') {\n            if ('multisig_current_cosigner' in wizard_data)\n                cosigner = wizard_data['multisig_current_cosigner']\n            participants = wizard_data['multisig_participants']\n\n            if ('multisig_master_pubkey' in wizard_data) {\n                multisigMasterPubkey = wizard_data['multisig_master_pubkey']\n            }\n        }\n        Qt.callLater(masterkey_ta.forceActiveFocus)\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCHaveSeed.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"../controls\"\n\nWizardComponent {\n    id: root\n    securePage: true\n\n    valid: false\n\n    property bool is2fa: false\n    property int cosigner: 0\n    property int participants: 0\n    property string multisigMasterPubkey: wizard_data['multisig_master_pubkey']\n\n    property string _seedType\n    property string _validationMessage\n    property bool _canPassphrase\n    property bool _seedValid\n\n    function apply() {\n        if (cosigner) {\n            wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed'] = seedtext.text\n            wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_variant'] = seed_variant_cb.currentValue\n            wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_type'] = _seedType\n            wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_extend'] = _canPassphrase\n        } else {\n            wizard_data['seed'] = seedtext.text\n            wizard_data['seed_variant'] = seed_variant_cb.currentValue\n            wizard_data['seed_type'] = _seedType\n            wizard_data['seed_extend'] = _canPassphrase\n\n            // determine script type from electrum seed type\n            // (used to limit script type options for bip39 cosigners)\n            if (wizard_data['wallet_type'] == 'multisig' && seed_variant_cb.currentValue == 'electrum') {\n                wizard_data['script_type'] = {\n                    'standard': 'p2sh',\n                    'segwit': 'p2wsh'\n                }[_seedType]\n            }\n        }\n    }\n\n    function setSeedTypeHelpText() {\n        var t = {\n            'electrum': [\n                // not shown as electrum is the default seed type anyways and the name is self-explanatory\n                qsTr('Electrum seeds are the default seed type.'),\n                qsTr('If you are restoring from a seed previously created by Electrum, choose this option')\n            ].join(' '),\n            'bip39': [\n                qsTr('BIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),\n                qsTr('BIP39 seeds do not include a version number, which compromises compatibility with future software.'),\n            ].join(' '),\n            'slip39': [\n                qsTr('SLIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),\n            ].join(' ')\n        }\n        infotext.text = t[seed_variant_cb.currentValue]\n        infotext.visible = !cosigner && !is2fa && seed_variant_cb.currentValue != 'electrum'\n    }\n\n    function checkValid() {\n        valid = false\n        _seedValid = false\n\n        var verifyResult = wiz.verifySeed(seedtext.text, seed_variant_cb.currentValue, wizard_data['wallet_type'])\n\n        _validationMessage = verifyResult.message\n        _seedType = verifyResult.type\n        _canPassphrase = verifyResult.can_passphrase\n\n        if (!cosigner || !verifyResult.valid) {\n            _seedValid = verifyResult.valid\n        } else {\n            // bip39 validate after derivation path is known\n            if (seed_variant_cb.currentValue == 'electrum') {\n                apply()\n                if (wiz.hasDuplicateMasterKeys(wizard_data)) {\n                    _validationMessage = qsTr('Error: duplicate master public key')\n                    return\n                } else if (wiz.hasHeterogeneousMasterKeys(wizard_data)) {\n                    _validationMessage = qsTr('Error: master public key types do not match')\n                    return\n                } else {\n                    _seedValid = true\n                }\n            } else {\n                _seedValid = true\n            }\n        }\n\n        valid = _seedValid\n    }\n\n    Flickable {\n        anchors.fill: parent\n        contentHeight: mainLayout.height\n        clip:true\n        interactive: height < contentHeight\n\n        GridLayout {\n            id: mainLayout\n            width: parent.width\n            columns: 2\n\n            Label {\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n                visible: cosigner\n                text: qsTr('Here is your master public key. Please share it with your cosigners')\n                wrapMode: Text.Wrap\n            }\n\n            TextHighlightPane {\n                Layout.columnSpan: 2\n                Layout.fillWidth: true\n\n                visible: cosigner\n\n                RowLayout {\n                    width: parent.width\n                    Label {\n                        Layout.fillWidth: true\n                        text: multisigMasterPubkey\n                        font.pixelSize: constants.fontSizeMedium\n                        font.family: FixedFont\n                        wrapMode: Text.Wrap\n                    }\n                    ToolButton {\n                        icon.source: '../../../icons/share.png'\n                        icon.color: 'transparent'\n                        onClicked: {\n                            var dialog = app.genericShareDialog.createObject(app,\n                                { title: qsTr('Master public key'), text: multisigMasterPubkey }\n                            )\n                            dialog.open()\n                        }\n                    }\n                }\n            }\n\n            Rectangle {\n                Layout.columnSpan: 2\n                Layout.preferredWidth: parent.width\n                Layout.preferredHeight: 1\n                Layout.topMargin: constants.paddingLarge\n                Layout.bottomMargin: constants.paddingLarge\n                visible: cosigner\n                color: Material.accentColor\n            }\n\n            Label {\n                Layout.columnSpan: 2\n                visible: cosigner\n                text: qsTr('Cosigner #%1 of %2').arg(cosigner).arg(participants)\n            }\n\n            Label {\n                Layout.fillWidth: true\n                visible: !is2fa\n                text: qsTr('Seed Type')\n            }\n\n            ComboBox {\n                id: seed_variant_cb\n                visible: !is2fa\n\n                textRole: 'text'\n                valueRole: 'value'\n                model: [\n                    { text: 'Electrum', value: 'electrum' },\n                    { text: 'BIP39', value: 'bip39' }\n                ]\n                onActivated: {\n                    setSeedTypeHelpText()\n                    checkIsLast()\n                    checkValid()\n                }\n            }\n\n            InfoTextArea {\n                id: infotext\n                Layout.fillWidth: true\n                Layout.columnSpan: 2\n                Layout.bottomMargin: constants.paddingLarge\n            }\n\n            SeedTextArea {\n                id: seedtext\n                Layout.fillWidth: true\n                Layout.columnSpan: 2\n\n                placeholderText: cosigner ? qsTr('Enter cosigner seed') : qsTr('Enter your seed')\n\n                indicatorValid: root._seedValid\n                    ? root._seedType == 'bip39' && root._validationMessage\n                        ? false\n                        : root._seedValid\n                    : root._seedValid\n                indicatorText: root._validationMessage\n                        ? root._validationMessage\n                        : root._seedType\n                onTextChanged: {\n                    startValidationTimer()\n                }\n            }\n        }\n    }\n\n    function startValidationTimer() {\n        valid = false\n        root._seedType = ''\n        root._validationMessage = ''\n        validationTimer.restart()\n    }\n\n    Timer {\n        id: validationTimer\n        interval: 500\n        repeat: false\n        onTriggered: {\n            checkValid()\n            // checkIsLast depends on 'seed_extend'(_canPassphrase) getting set in apply()\n            checkIsLast()\n        }\n    }\n\n    Component.onCompleted: {\n        if (wizard_data['wallet_type'] == '2fa') {\n            is2fa = true\n        } else if (wizard_data['wallet_type'] == 'multisig') {\n            participants = wizard_data['multisig_participants']\n            if ('multisig_current_cosigner' in wizard_data)\n                cosigner = wizard_data['multisig_current_cosigner']\n        }\n        setSeedTypeHelpText()\n        Qt.callLater(seedtext.forceActiveFocus)\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCImport.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nimport \"../controls\"\n\nWizardComponent {\n    id: root\n    securePage: true\n\n    valid: false\n\n    function apply() {\n        if (bitcoin.isAddressList(import_ta.text)) {\n            wizard_data['address_list'] = import_ta.text\n        } else if (bitcoin.isPrivateKeyList(import_ta.text)) {\n            wizard_data['private_key_list'] = import_ta.text\n        }\n    }\n\n    function verify(text) {\n        return bitcoin.isAddressList(text) || bitcoin.isPrivateKeyList(text)\n    }\n\n    ColumnLayout {\n        width: parent.width\n        height: parent.height\n        InfoTextArea {\n            Layout.preferredWidth: parent.width\n            text: qsTr('Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.')\n        }\n\n        RowLayout {\n            Layout.topMargin: constants.paddingMedium\n            Layout.fillHeight: true\n\n            ElTextArea {\n                id: import_ta\n                Layout.fillWidth: true\n                Layout.fillHeight: true\n                font.family: FixedFont\n                wrapMode: TextEdit.WrapAnywhere\n                onTextChanged: valid = verify(text)\n                inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase\n                background: PaneInsetBackground {\n                    baseColor: constants.darkerDialogBackground\n                }\n            }\n\n            ColumnLayout {\n                Layout.alignment: Qt.AlignTop\n                ToolButton {\n                    icon.source: '../../../icons/paste.png'\n                    icon.height: constants.iconSizeMedium\n                    icon.width: constants.iconSizeMedium\n                    onClicked: {\n                        if (verify(AppController.clipboardToText())) {\n                            if (import_ta.text != '')\n                                import_ta.text = import_ta.text + '\\n'\n                            import_ta.text = import_ta.text + AppController.clipboardToText()\n                        }\n                    }\n                }\n                ToolButton {\n                    icon.source: '../../../icons/qrcode.png'\n                    icon.height: constants.iconSizeMedium\n                    icon.width: constants.iconSizeMedium\n                    scale: 1.2\n                    onClicked: {\n                        var dialog = app.scanDialog.createObject(app, {\n                            hint: bitcoin.isAddressList(import_ta.text)\n                                ? qsTr('Scan another address')\n                                : bitcoin.isPrivateKeyList(import_ta.text)\n                                    ? qsTr('Scan another private key')\n                                    : qsTr('Scan a private key or an address')\n                        })\n                        dialog.onFoundText.connect(function(data) {\n                            if (verify(data)) {\n                                if (import_ta.text != '')\n                                    import_ta.text = import_ta.text + '\\n'\n                                import_ta.text = import_ta.text + data\n                            }\n                            dialog.close()\n                        })\n                        dialog.open()\n                    }\n                }\n            }\n        }\n    }\n\n    Bitcoin {\n        id: bitcoin\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCKeystoreType.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport \"../controls\"\n\nWizardComponent {\n    valid: keystoregroup.checkedButton !== null\n\n    function apply() {\n        wizard_data['keystore_type'] = keystoregroup.checkedButton.keystoretype\n    }\n\n    ButtonGroup {\n        id: keystoregroup\n    }\n\n    ColumnLayout {\n        width: parent.width\n\n        Label {\n            Layout.fillWidth: true\n            wrapMode: Text.Wrap\n            text: qsTr('Do you want to create a new seed, restore using an existing seed, or restore from master key?')\n        }\n        ElRadioButton {\n            Layout.fillWidth: true\n            ButtonGroup.group: keystoregroup\n            property string keystoretype: 'createseed'\n            checked: true\n            text: qsTr('Create a new seed')\n        }\n        ElRadioButton {\n            Layout.fillWidth: true\n            ButtonGroup.group: keystoregroup\n            property string keystoretype: 'haveseed'\n            text: qsTr('I already have a seed')\n        }\n        ElRadioButton {\n            Layout.fillWidth: true\n            ButtonGroup.group: keystoregroup\n            property string keystoretype: 'masterkey'\n            text: qsTr('Use a master key')\n        }\n        ElRadioButton {\n            Layout.fillWidth: true\n            enabled: false\n            visible: false\n            ButtonGroup.group: keystoregroup\n            property string keystoretype: 'hardware'\n            text: qsTr('Use a hardware device')\n        }\n    }\n}\n\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCMultisig.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nimport \"../controls\"\n\nWizardComponent {\n    id: root\n\n    valid: true\n\n    property int participants: 2\n    property int signatures: 2\n\n    onParticipantsChanged: {\n        if (participants < signatures)\n            signatures = participants\n        piechart.updateSlices()\n    }\n    onSignaturesChanged: {\n        piechart.updateSlices()\n    }\n\n    function apply() {\n        wizard_data['multisig_participants'] = participants\n        wizard_data['multisig_signatures'] = signatures\n        wizard_data['multisig_cosigner_data'] = {}\n    }\n\n    Flickable {\n        anchors.fill: parent\n        contentHeight: rootLayout.height\n        clip:true\n        interactive: height < contentHeight\n\n        ColumnLayout {\n            id: rootLayout\n            width: parent.width\n\n            InfoTextArea {\n                Layout.preferredWidth: parent.width\n                text: qsTr('Choose the number of participants, and the number of signatures needed to unlock funds in your wallet.')\n            }\n\n            Piechart {\n                id: piechart\n                Layout.preferredWidth: parent.width * 1/2\n                Layout.alignment: Qt.AlignHCenter\n                Layout.preferredHeight: 200 // TODO\n                showLegend: false\n                innerOffset: 3\n                function updateSlices() {\n                    var s = []\n                    for (let i=0; i < participants; i++) {\n                        var item = {\n                            v: (1/participants),\n                            color: i < signatures ? constants.colorPiechartSignature : constants.colorPiechartParticipant\n                        }\n                        s.push(item)\n                    }\n                    piechart.slices = s\n                }\n            }\n\n            Label {\n                text: qsTr('Number of cosigners: %1').arg(participants)\n            }\n\n            Slider {\n                id: participants_slider\n                Layout.preferredWidth: parent.width * 4/5\n                Layout.alignment: Qt.AlignHCenter\n                snapMode: Slider.SnapAlways\n                stepSize: 1\n                from: 2\n                to: 15\n                onValueChanged: {\n                    if (activeFocus)\n                        participants = value\n                }\n            }\n\n            Label {\n                text: qsTr('Number of signatures: %1').arg(signatures)\n            }\n\n            Slider {\n                id: signatures_slider\n                Layout.preferredWidth: parent.width * 4/5\n                Layout.alignment: Qt.AlignHCenter\n                snapMode: Slider.SnapAlways\n                stepSize: 1\n                from: 1\n                to: participants\n                value: signatures\n                onValueChanged: {\n                    if (activeFocus)\n                        signatures = value\n                }\n            }\n        }\n    }\n\n    Component.onCompleted: piechart.updateSlices()\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCProxyConfig.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport \"../controls\"\n\nWizardComponent {\n    valid: true\n    title: qsTr('Proxy')\n\n    function apply() {\n        wizard_data['proxy'] = pc.toProxyDict()\n    }\n\n    ColumnLayout {\n        width: parent.width\n        spacing: constants.paddingLarge\n\n        ProxyConfig {\n            id: pc\n            Layout.fillWidth: true\n            proxy_enabled: false\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCScriptAndDerivation.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport org.electrum 1.0\n\nimport \"..\"\nimport \"../controls\"\n\nWizardComponent {\n    valid: false\n\n    property bool isMultisig: false\n    property int cosigner: 0\n    property int participants: 0\n\n    function apply() {\n        if (cosigner) {\n            wizard_data['multisig_cosigner_data'][cosigner.toString()]['script_type'] = scripttypegroup.checkedButton.scripttype\n            wizard_data['multisig_cosigner_data'][cosigner.toString()]['derivation_path'] = derivationpathtext.text\n        } else {\n            wizard_data['script_type'] = scripttypegroup.checkedButton.scripttype\n            wizard_data['derivation_path'] = derivationpathtext.text\n        }\n    }\n\n    function getScriptTypePurposeDict() {\n        return {\n            'p2pkh': 44,\n            'p2wpkh-p2sh': 49,\n            'p2wpkh': 84\n        }\n    }\n\n    function getMultisigScriptTypePurposeDict() {\n        return {\n            'p2sh': 45,\n            'p2wsh-p2sh': 48,\n            'p2wsh': 48\n        }\n    }\n\n    function validate() {\n        valid = false\n        validationtext.text = ''\n\n        var p = isMultisig ? getMultisigScriptTypePurposeDict() : getScriptTypePurposeDict()\n        if (!scripttypegroup.checkedButton.scripttype in p)\n            return\n        if (!bitcoin.verifyDerivationPath(derivationpathtext.text)) {\n            validationtext.text = qsTr('Invalid derivation path')\n            return\n        }\n\n        if (isMultisig && cosigner) {\n            apply()\n            if (wiz.hasDuplicateMasterKeys(wizard_data)) {\n                validationtext.text = qsTr('Error: duplicate master public key')\n                return\n            } else if (wiz.hasHeterogeneousMasterKeys(wizard_data)) {\n                validationtext.text = qsTr('Error: master public key types do not match')\n                return\n            }\n        }\n        valid = true\n    }\n\n    function setDerivationPath() {\n        var p = isMultisig ? getMultisigScriptTypePurposeDict() : getScriptTypePurposeDict()\n        var scripttype = scripttypegroup.checkedButton.scripttype\n        if (isMultisig) {\n            if (scripttype == 'p2sh')\n                derivationpathtext.text = \"m/\" + p[scripttype] + \"'/0\"\n            else\n                derivationpathtext.text = \"m/\" + p[scripttype] + \"'/\"\n                + (Network.isTestNet ? 1 : 0) + \"'/0'/\"\n                + (scripttype == 'p2wsh' ? 2 : 1) + \"'\"\n        } else {\n            derivationpathtext.text =\n                \"m/\" + p[scripttypegroup.checkedButton.scripttype] + \"'/\"\n                + (Network.isTestNet ? 1 : 0) + \"'/0'\"\n        }\n    }\n\n    ButtonGroup {\n        id: scripttypegroup\n        onCheckedButtonChanged: {\n            setDerivationPath()\n        }\n    }\n\n    Flickable {\n        anchors.fill: parent\n        contentHeight: mainLayout.height\n        clip:true\n        interactive: height < contentHeight\n\n        ColumnLayout {\n            id: mainLayout\n            width: parent.width\n\n            Label {\n                Layout.fillWidth: true\n                text: qsTr('Choose the type of addresses in your wallet.')\n                wrapMode: Text.Wrap\n            }\n\n            // standard\n            ElRadioButton {\n                Layout.fillWidth: true\n                ButtonGroup.group: scripttypegroup\n                property string scripttype: 'p2pkh'\n                text: qsTr('legacy (p2pkh)')\n                visible: !isMultisig\n            }\n            ElRadioButton {\n                Layout.fillWidth: true\n                ButtonGroup.group: scripttypegroup\n                property string scripttype: 'p2wpkh-p2sh'\n                text: qsTr('wrapped segwit (p2wpkh-p2sh)')\n                visible: !isMultisig\n            }\n            ElRadioButton {\n                Layout.fillWidth: true\n                ButtonGroup.group: scripttypegroup\n                property string scripttype: 'p2wpkh'\n                checked: !isMultisig\n                text: qsTr('native segwit (p2wpkh)')\n                visible: !isMultisig\n            }\n\n            // multisig\n            ElRadioButton {\n                Layout.fillWidth: true\n                ButtonGroup.group: scripttypegroup\n                property string scripttype: 'p2sh'\n                text: qsTr('legacy multisig (p2sh)')\n                visible: isMultisig\n                enabled: !cosigner || wizard_data['script_type'] == 'p2sh'\n                checked: cosigner ? wizard_data['script_type'] == 'p2sh' : false\n            }\n            ElRadioButton {\n                Layout.fillWidth: true\n                ButtonGroup.group: scripttypegroup\n                property string scripttype: 'p2wsh-p2sh'\n                text: qsTr('p2sh-segwit multisig (p2wsh-p2sh)')\n                visible: isMultisig\n                enabled: !cosigner || wizard_data['script_type'] == 'p2wsh-p2sh'\n                checked: cosigner ? wizard_data['script_type'] == 'p2wsh-p2sh' : false\n            }\n            ElRadioButton {\n                Layout.fillWidth: true\n                ButtonGroup.group: scripttypegroup\n                property string scripttype: 'p2wsh'\n                text: qsTr('native segwit multisig (p2wsh)')\n                visible: isMultisig\n                enabled: !cosigner || wizard_data['script_type'] == 'p2wsh'\n                checked: cosigner ? wizard_data['script_type'] == 'p2wsh' : isMultisig\n            }\n\n            InfoTextArea {\n                Layout.fillWidth: true\n                text: qsTr('You can override the suggested derivation path.') + ' ' +\n                    qsTr('If you are not sure what this is, leave this field unchanged.')\n            }\n\n            Label {\n                text: qsTr('Derivation path')\n            }\n\n            TextField {\n                id: derivationpathtext\n                Layout.fillWidth: true\n                Layout.leftMargin: constants.paddingMedium\n                inputMethodHints: Qt.ImhNoPredictiveText\n\n                onTextChanged: validate()\n            }\n\n            InfoTextArea {\n                id: validationtext\n                Layout.fillWidth: true\n                visible: text\n                iconStyle: InfoTextArea.IconStyle.Error\n            }\n\n            Pane {\n                Layout.alignment: Qt.AlignHCenter\n                Layout.topMargin: constants.paddingLarge\n                padding: 0\n                visible: !isMultisig\n                background: Rectangle {\n                    color: Qt.lighter(Material.dialogColor, 1.5)\n                }\n\n                FlatButton {\n                    text: qsTr('Detect Existing Accounts')\n                    onClicked: {\n                        var dialog = bip39recoveryDialog.createObject(mainLayout, {\n                            walletType: wizard_data['wallet_type'],\n                            seed: wizard_data['seed'],\n                            seedExtraWords: wizard_data['seed_extra_words']\n                        })\n                        dialog.accepted.connect(function () {\n                            // select matching script type button and set derivation path\n                            for (var i = 0; i < scripttypegroup.buttons.length; i++) {\n                                var btn = scripttypegroup.buttons[i]\n                                if (btn.visible && btn.scripttype == dialog.scriptType) {\n                                    btn.checked = true\n                                    derivationpathtext.text = dialog.derivationPath\n                                    return\n                                }\n                            }\n                        })\n                        dialog.open()\n                    }\n                }\n            }\n\n        }\n    }\n\n    Bitcoin {\n        id: bitcoin\n    }\n\n    Component {\n        id: bip39recoveryDialog\n        BIP39RecoveryDialog { }\n    }\n\n    Component.onCompleted: {\n        isMultisig = wizard_data['wallet_type'] == 'multisig'\n        if (isMultisig) {\n            participants = wizard_data['multisig_participants']\n            if ('multisig_current_cosigner' in wizard_data)\n                cosigner = wizard_data['multisig_current_cosigner']\n            validate()\n        }\n    }\n}\n\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCServerConfig.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport \"../controls\"\n\nWizardComponent {\n    valid: sc.addressValid\n    last: true\n    title: qsTr('Server')\n\n    function apply() {\n        wizard_data['server'] = sc.address\n        wizard_data['autoconnect'] = sc.serverConnectMode == ServerConnectModeComboBox.Mode.Autoconnect\n        wizard_data['one_server'] = sc.serverConnectMode == ServerConnectModeComboBox.Mode.Single\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: constants.paddingLarge\n\n        ServerConfig {\n            id: sc\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCShowMasterPubkey.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nimport \"../controls\"\n\nWizardComponent {\n    valid: true\n\n    property string masterPubkey: wizard_data['multisig_master_pubkey']\n\n    ColumnLayout {\n        width: parent.width\n\n        Label {\n            text: qsTr('Here is your master public key. Please share it with your cosigners')\n            Layout.fillWidth: true\n            wrapMode: Text.Wrap\n        }\n\n        TextHighlightPane {\n            Layout.fillWidth: true\n\n            RowLayout {\n                width: parent.width\n                Label {\n                    Layout.fillWidth: true\n                    text: masterPubkey\n                    font.pixelSize: constants.fontSizeMedium\n                    font.family: FixedFont\n                    wrapMode: Text.Wrap\n                }\n                ToolButton {\n                    icon.source: '../../../icons/share.png'\n                    icon.color: 'transparent'\n                    onClicked: {\n                        var dialog = app.genericShareDialog.createObject(app,\n                            { title: qsTr('Master public key'), text: masterPubkey }\n                        )\n                        dialog.open()\n                    }\n                }\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCTermsOfUseRequest.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport \"../controls\"\n\nWizardComponent {\n    valid: true\n    last: true\n\n    Flickable {\n        anchors.fill: parent\n        contentHeight: mainLayout.height\n        clip: true\n        interactive: height < contentHeight\n\n        ColumnLayout {\n            id: mainLayout\n            width: parent.width\n            spacing: constants.paddingLarge\n\n            Image {\n                Layout.fillWidth: true\n                fillMode: Image.PreserveAspectFit\n                source: Qt.resolvedUrl('../../../icons/electrum_presplash.png')\n                // reduce spacing a bit\n                Layout.topMargin: -100\n                Layout.bottomMargin: -200\n            }\n\n            Label {\n                Layout.fillWidth: true\n                text: qsTr(\"Terms of Use\")\n                font.pixelSize: constants.fontSizeLarge\n                font.bold: true\n                horizontalAlignment: Text.AlignHCenter\n            }\n\n            Label {\n                Layout.fillWidth: true\n                text: wiz.termsOfUseText\n                wrapMode: Text.WordWrap\n                font.pixelSize: constants.fontSizeMedium\n                padding: constants.paddingSmall\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCWalletName.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nWizardComponent {\n    valid: wiz.isValidNewWalletName(wallet_name.text)\n\n    function apply() {\n        wizard_data['wallet_name'] = wallet_name.text\n    }\n\n    ColumnLayout {\n        width: parent.width\n\n        Label {\n            text: qsTr('Wallet name')\n        }\n\n        TextField {\n            id: wallet_name\n            Layout.fillWidth: true\n            focus: true\n            text: Daemon.suggestWalletName()\n            inputMethodHints: Qt.ImhNoPredictiveText\n        }\n    }\n\n    Component.onCompleted: {\n        wallet_name.forceActiveFocus()\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCWalletPassword.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport \"../controls\"\n\n// We will only end up here if Daemon.singlePasswordEnabled == False.\n// If there are existing wallets, the user must reuse the password of one of them.\n// This way they are guided towards password unification.\n// NOTE: This also needs to be enforced when changing a wallets password.\n\nWizardComponent {\n    id: root\n    valid: isInputValid()\n    property bool enforceExistingPassword: Config.walletShouldUseSinglePassword && Daemon.availableWallets.rowCount() > 0\n    property bool passwordMatchesAnyExisting: false\n\n    function apply() {\n        wizard_data['password'] = password1.text\n        wizard_data['encrypt'] = password1.text != ''\n    }\n\n    function isInputValid() {\n        if (password1.text == \"\") {\n            return false\n        }\n        if (enforceExistingPassword) {\n            return passwordMatchesAnyExisting\n        }\n        return password1.text === password2.text && password1.text.length >= 6\n    }\n\n    Timer {\n        id: passwordComparisonTimer\n        interval: 500\n        repeat: false\n        onTriggered: {\n            root.passwordMatchesAnyExisting = Daemon.numWalletsWithPassword(password1.text) > 0\n        }\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n\n        Label {\n            Layout.fillWidth: true\n            text: !enforceExistingPassword ? qsTr('Enter password') : qsTr('Enter existing password')\n            wrapMode: Text.Wrap\n        }\n\n        PasswordField {\n            id: password1\n            onTextChanged: {\n                if (enforceExistingPassword) {\n                    root.passwordMatchesAnyExisting = false\n                    passwordComparisonTimer.restart()\n                }\n            }\n        }\n\n        Label {\n            text: qsTr('Enter password (again)')\n            visible: !enforceExistingPassword\n        }\n\n        PasswordField {\n            id: password2\n            showReveal: false\n            echoMode: password1.echoMode\n            visible: !enforceExistingPassword\n        }\n\n        RowLayout {\n            Layout.fillWidth: true\n            Layout.leftMargin: constants.paddingXLarge\n            Layout.rightMargin: constants.paddingXLarge\n            Layout.topMargin: constants.paddingXLarge\n\n            visible: password1.text != '' && !enforceExistingPassword\n\n            Label {\n                Layout.rightMargin: constants.paddingLarge\n                text: qsTr('Strength')\n            }\n\n            PasswordStrengthIndicator {\n                Layout.fillWidth: true\n                password: password1.text\n            }\n        }\n\n        Item {\n            Layout.preferredWidth: 1\n            Layout.fillHeight: true\n        }\n\n        InfoTextArea {\n            Layout.alignment: Qt.AlignCenter\n            text: qsTr('Passwords don\\'t match')\n            visible: (password1.text != password2.text) && !enforceExistingPassword\n            iconStyle: InfoTextArea.IconStyle.Warn\n        }\n        InfoTextArea {\n            Layout.alignment: Qt.AlignCenter\n            text: qsTr('Password too short')\n            visible: (password1.text == password2.text) && !valid && !enforceExistingPassword\n            iconStyle: InfoTextArea.IconStyle.Warn\n        }\n        InfoTextArea {\n            Layout.alignment: Qt.AlignCenter\n            Layout.fillWidth: true\n            visible: password1.text == \"\" && enforceExistingPassword\n            text: [\n                    qsTr(\"Use the password of any existing wallet.\"),\n                    qsTr(\"Creating new wallets with different passwords is not supported.\")\n                ].join(\"\\n\")\n            iconStyle: InfoTextArea.IconStyle.Info\n        }\n        InfoTextArea {\n            Layout.alignment: Qt.AlignCenter\n            Layout.fillWidth: true\n            visible: password1.text != \"\" && !valid && enforceExistingPassword\n            text: qsTr('Password does not match any existing wallets password.')\n            iconStyle: InfoTextArea.IconStyle.Warn\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCWalletType.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport \"../controls\"\n\nWizardComponent {\n    valid: wallettypegroup.checkedButton !== null\n\n    function apply() {\n        // apply gets called when the page is rendered and implicitly\n        // sets the first radio button or the last selected one when going back\n        wizard_data['wallet_type'] = wallettypegroup.checkedButton.wallettype\n        delete wizard_data['seed_type']\n        if (wizard_data['wallet_type'] == 'standard')\n            wizard_data['seed_type'] = 'segwit'\n        else if (wizard_data['wallet_type'] == '2fa')\n            wizard_data['seed_type'] = '2fa_segwit'\n        else if (wizard_data['wallet_type'] == 'multisig')\n            wizard_data['seed_type'] = 'segwit'\n    }\n\n    ButtonGroup {\n        id: wallettypegroup\n    }\n\n    ColumnLayout {\n        width: parent.width\n\n        Label {\n            Layout.fillWidth: true\n            text: qsTr('What kind of wallet do you want to create?')\n            wrapMode: Text.Wrap\n        }\n        ElRadioButton {\n            Layout.fillWidth: true\n            ButtonGroup.group: wallettypegroup\n            property string wallettype: 'standard'\n            checked: true\n            text: qsTr('Standard Wallet')\n        }\n        ElRadioButton {\n            Layout.fillWidth: true\n            ButtonGroup.group: wallettypegroup\n            property string wallettype: '2fa'\n            text: qsTr('Wallet with two-factor authentication')\n        }\n        ElRadioButton {\n            Layout.fillWidth: true\n            ButtonGroup.group: wallettypegroup\n            property string wallettype: 'multisig'\n            text: qsTr('Multi-signature wallet')\n        }\n        ElRadioButton {\n            Layout.fillWidth: true\n            ButtonGroup.group: wallettypegroup\n            property string wallettype: 'imported'\n            text: qsTr('Import Bitcoin addresses or private keys')\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WCWelcome.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport \"../controls\"\n\nWizardComponent {\n    valid: true\n    wizard_title: qsTr('Network Configuration')\n\n    function apply() {\n        wizard_data['use_defaults'] = !config_proxy.checked && !config_server.checked\n        wizard_data['want_proxy'] = config_proxy.checked\n        wizard_data['autoconnect'] = !config_server.checked\n    }\n\n    ColumnLayout {\n        width: parent.width\n\n        Label {\n            Layout.alignment: Qt.AlignHCenter\n            Layout.preferredWidth: parent.width\n            text: qsTr(\"Optional settings to customize your network connection\") + \":\"\n            wrapMode: Text.WordWrap\n            horizontalAlignment: Text.AlignHLeft\n            font.pixelSize: constants.fontSizeLarge\n        }\n\n        ColumnLayout {\n            Layout.alignment: Qt.AlignHCenter\n            Layout.topMargin: 2*constants.paddingXLarge; Layout.bottomMargin: 2*constants.paddingXLarge\n\n            CheckBox {\n                id: config_proxy\n                text: qsTr('Configure Proxy')\n                checked: false\n                onCheckedChanged: checkIsLast()\n            }\n            CheckBox {\n                id: config_server\n                text: qsTr('Select Server')\n                checked: false\n                onCheckedChanged: checkIsLast()\n            }\n        }\n\n        Label {\n            Layout.alignment: Qt.AlignHCenter\n            Layout.preferredWidth: parent.width\n            text: qsTr(\"If you are unsure what this is, leave them unchecked and Electrum will automatically select servers.\")\n            wrapMode: Text.WordWrap\n            horizontalAlignment: Text.AlignHLeft\n            font.pixelSize: constants.fontSizeMedium\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/Wizard.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\n\nimport org.electrum 1.0\n\nimport \"../controls\"\n\nElDialog {\n    id: wizard\n    focus: true\n\n    width: parent.width\n    height: parent.height\n\n    padding: 0\n\n    title: (pages.currentItem.wizard_title ? pages.currentItem.wizard_title : wizardTitle) +\n        (pages.currentItem.title ? ' - ' + pages.currentItem.title : '')\n    iconSource: '../../../icons/electrum.png'\n\n    // android back button triggers close() on Popups. Disabling close here,\n    // we handle that via Keys.onReleased event handler in the root layout.\n    closePolicy: Popup.NoAutoClose\n\n    property string wizardTitle\n\n    property var wizard_data\n    property alias pages: pages\n    property QtObject wiz\n    property alias finishButtonText: finishButton.text\n\n    function doClose() {\n        if (pages.currentIndex == 0)\n            reject()\n        else\n            pages.prev()\n    }\n\n    function _setWizardData(wdata) {\n        wizard_data = {}\n        Object.assign(wizard_data, wdata) // deep copy\n        // console.log('wizard data is now :' + JSON.stringify(wizard_data))\n    }\n\n    // helper function to dynamically load wizard page components\n    // and add them to the SwipeView\n    // Here we do some manual binding of page.valid -> pages.pagevalid and\n    // page.last -> pages.lastpage to propagate the state without the binding\n    // going stale.\n    function _loadNextComponent(view, wdata={}) {\n        // remove any existing pages after current page\n        while (pages.contentChildren[pages.currentIndex+1]) {\n            pages.takeItem(pages.currentIndex+1).destroy()\n        }\n\n        var url = Qt.resolvedUrl(wiz.viewToComponent(view))\n        var comp = Qt.createComponent(url)\n        if (comp.status == Component.Error) {\n            console.log(comp.errorString())\n            return null\n        }\n\n        // make a deepcopy of wdata and pass it to the component\n        var wdata_copy={}\n        Object.assign(wdata_copy, wdata)\n        var page = comp.createObject(pages, {wizard_data: wdata_copy})\n        page.validChanged.connect(function() {\n            if (page != pages.currentItem)\n                return\n            pages.pagevalid = page.valid\n        })\n        page.lastChanged.connect(function() {\n            if (page != pages.currentItem)\n                return\n            pages.lastpage = page.last\n        })\n        page.next.connect(function() {\n            var newview = wiz.submit(page.wizard_data)\n            if (newview.view) {\n                console.log('next view: ' + newview.view)\n                var newpage = _loadNextComponent(newview.view, newview.wizard_data)\n            } else {\n                console.log('END')\n            }\n        })\n        page.finish.connect(function() {\n            // run wizard.submit() a final time, so that the navmap[view]['accept'] handler can run (if any)\n            var newview = wiz.submit(page.wizard_data)\n            _setWizardData(newview.wizard_data)\n            console.log('wizard finished')\n            // finish wizard\n            wizard.doAccept()\n        })\n        page.prev.connect(function() {\n            var wdata = wiz.prev()\n        })\n\n        pages.pagevalid = page.valid\n        pages.lastpage = page.last\n\n        return page\n    }\n\n    ColumnLayout {\n        anchors.fill: parent\n        spacing: 0\n\n        // root Item in Wizard, capture back button here and delegate to main\n        Keys.onReleased: {\n            if (event.key == Qt.Key_Back) {\n                console.log(\"Back button within wizard\")\n                app.close() // this handles unwind of dialogs/stack\n            }\n        }\n\n        SwipeView {\n            id: pages\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n\n            interactive: false\n\n            clip:true\n\n            function prev() {\n                currentItem.prev()\n                currentIndex = currentIndex - 1\n                _setWizardData(pages.contentChildren[currentIndex].wizard_data)\n                pages.pagevalid = pages.contentChildren[currentIndex].valid\n                pages.lastpage = pages.contentChildren[currentIndex].last\n\n            }\n\n            function next() {\n                currentItem.accept()\n                _setWizardData(pages.contentChildren[currentIndex].wizard_data)\n                currentItem.next()\n                currentIndex = currentIndex + 1\n            }\n\n            function finish() {\n                currentItem.accept()\n                _setWizardData(pages.contentChildren[currentIndex].wizard_data)\n                currentItem.finish()\n            }\n\n            property bool pagevalid: false\n            property bool lastpage: false\n\n            Component.onCompleted: {\n                _setWizardData({})\n            }\n\n            Binding {\n                target: AppController\n                property: 'secureWindow'\n                value: pages.contentChildren[pages.currentIndex].securePage\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                visible: pages.currentIndex == 0\n                text: qsTr(\"Cancel\")\n                onClicked: wizard.doReject()\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                visible: pages.currentIndex > 0\n                text: qsTr('Back')\n                onClicked: pages.prev()\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr(\"Next\")\n                visible: !pages.lastpage\n                enabled: pages.pagevalid\n                onClicked: pages.next()\n            }\n            FlatButton {\n                id: finishButton\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr(\"Finish\")\n                visible: pages.lastpage\n                enabled: pages.pagevalid\n                onClicked: pages.finish()\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/components/wizard/WizardComponent.qml",
    "content": "import QtQuick\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nPane {\n    id: root\n    signal next\n    signal finish\n    signal prev\n    signal accept\n    property var wizard_data : ({})\n    property bool valid\n    property bool last: false\n    property string wizard_title: ''\n    property string title: ''\n    property bool securePage: false\n\n    leftPadding: constants.paddingXLarge\n    rightPadding: constants.paddingXLarge\n\n    background: Rectangle {\n        color: Material.dialogColor\n        TapHandler {\n            onTapped: root.forceActiveFocus()\n        }\n    }\n\n    onAccept: {\n        apply()\n    }\n\n    // override this in descendants to put data from the view in wizard_data\n    function apply() { }\n\n    function checkIsLast() {\n        apply()\n        last = wizard.wiz.isLast(wizard_data)\n    }\n\n    Component.onCompleted: {\n        // NOTE: Use Qt.callLater to execute checkIsLast(), and by extension apply(),\n        // otherwise Component.onCompleted handler in descendants is processed\n        // _after_ apply() is called, which may lead to setting the wrong\n        // wizard_data keys if apply() depends on variables set in descendant\n        // Component.onCompleted handler.\n        Qt.callLater(checkIsLast)\n\n        // move focus to root of WizardComponent, otherwise Android back button\n        // might be missed in Wizard root Item.\n        root.forceActiveFocus()\n    }\n\n}\n"
  },
  {
    "path": "electrum/gui/qml/java_classes/org/electrum/biometry/BiometricActivity.java",
    "content": "package org.electrum.biometry;\n\nimport android.app.Activity;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.os.CancellationSignal;\nimport android.content.Intent;\nimport android.hardware.biometrics.BiometricManager;\nimport android.hardware.biometrics.BiometricPrompt;\nimport android.security.keystore.KeyGenParameterSpec;\nimport android.security.keystore.KeyProperties;\nimport android.util.Base64;\nimport android.util.Log;\nimport android.widget.Toast;\n\nimport java.nio.charset.Charset;\nimport java.security.KeyStore;\nimport java.util.concurrent.Executor;\n\nimport javax.crypto.Cipher;\nimport javax.crypto.KeyGenerator;\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.IvParameterSpec;\n\nimport org.electrum.electrum.res.R;\n\npublic class BiometricActivity extends Activity {\n    private static final String TAG = \"BiometricActivity\";\n    private static final String KEY_NAME = \"electrum_biometric_key\";\n    private static final int RESULT_SETUP_FAILED = 101;\n    private static final int RESULT_POPUP_CANCELLED = 102;\n    private CancellationSignal cancellationSignal;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {\n            Log.e(TAG, \"Biometrics not supported on this Android version (requires API 30+)\");\n            setResult(RESULT_CANCELED);\n            finish();\n            return;\n        }\n\n        handleIntent();\n    }\n\n    private void handleIntent() {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return;\n\n        Intent intent = getIntent();\n        String action = intent.getStringExtra(\"action\");\n        String authMessage = intent.getStringExtra(\"auth_message\");\n\n        Executor executor = getMainExecutor();\n        BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(this)\n                .setTitle(\"Electrum Wallet\")\n                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL)\n                .setSubtitle(authMessage)\n                .build();\n\n        cancellationSignal = new CancellationSignal();\n\n        BiometricPrompt.AuthenticationCallback callback = new BiometricPrompt.AuthenticationCallback() {\n            @Override\n            public void onAuthenticationError(int errorCode, CharSequence errString) {\n                super.onAuthenticationError(errorCode, errString);\n                Log.e(TAG, \"Authentication error: \" + errorCode + \" \" + errString);\n\n                if (\n                        errorCode == BiometricPrompt.BIOMETRIC_ERROR_CANCELED ||\n                        errorCode == BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED ||\n                        errorCode == BiometricPrompt.BIOMETRIC_ERROR_TIMEOUT\n                ) {\n                    setResult(RESULT_POPUP_CANCELLED);\n                } else {\n                    setResult(RESULT_CANCELED);\n                }\n                finish();\n            }\n\n            @Override\n            public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {\n                super.onAuthenticationSucceeded(result);\n                Log.d(TAG, \"Authentication succeeded!\");\n                handleAuthenticationSuccess(result);\n            }\n\n            @Override\n            public void onAuthenticationFailed() {\n                super.onAuthenticationFailed();\n                Log.d(TAG, \"Authentication failed\");\n            }\n        };\n\n        try {\n            if (\"ENCRYPT\".equals(action)) {\n                Cipher cipher = getCipher();\n                SecretKey secretKey = genSecretKey();\n                cipher.init(Cipher.ENCRYPT_MODE, secretKey);\n                biometricPrompt.authenticate(new BiometricPrompt.CryptoObject(cipher), cancellationSignal, executor, callback);\n            } else if (\"DECRYPT\".equals(action)) {\n                String ivStr = intent.getStringExtra(\"iv\");\n                byte[] iv = Base64.decode(ivStr, Base64.NO_WRAP);\n                Cipher cipher = getCipher();\n                SecretKey secretKey = getSecretKey();\n                cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));\n                biometricPrompt.authenticate(new BiometricPrompt.CryptoObject(cipher), cancellationSignal, executor, callback);\n            } else {\n                finish();\n            }\n        } catch (Exception e) {\n            Log.e(TAG, \"Setup error\", e);\n            Toast.makeText(this, \"Biometric setup failed: \" + e.getMessage(), Toast.LENGTH_SHORT).show();\n            setResult(RESULT_SETUP_FAILED);\n            finish();\n        }\n    }\n\n    private void handleAuthenticationSuccess(BiometricPrompt.AuthenticationResult result) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return;\n        try {\n            BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject();\n            Cipher cipher = cryptoObject.getCipher();\n            Intent intent = getIntent();\n            String action = intent.getStringExtra(\"action\");\n            Intent resultIntent = new Intent();\n\n            if (\"ENCRYPT\".equals(action)) {\n                String data = intent.getStringExtra(\"data\"); // wrap_key string to encrypt\n                byte[] encrypted = cipher.doFinal(data.getBytes(Charset.forName(\"UTF-8\")));\n                resultIntent.putExtra(\"data\", Base64.encodeToString(encrypted, Base64.NO_WRAP));\n                resultIntent.putExtra(\"iv\", Base64.encodeToString(cipher.getIV(), Base64.NO_WRAP));\n            } else {\n                String dataStr = intent.getStringExtra(\"data\"); // Encrypted blob\n                byte[] encrypted = Base64.decode(dataStr, Base64.NO_WRAP);\n                byte[] decrypted = cipher.doFinal(encrypted);\n                resultIntent.putExtra(\"data\", new String(decrypted, Charset.forName(\"UTF-8\")));\n            }\n            setResult(RESULT_OK, resultIntent);\n        } catch (Exception e) {\n            Log.e(TAG, \"Crypto error\", e);\n            setResult(RESULT_CANCELED);\n        }\n        finish();\n    }\n\n    private SecretKey getSecretKey() throws Exception {\n        KeyStore keyStore = KeyStore.getInstance(\"AndroidKeyStore\");\n        keyStore.load(null);\n        return (SecretKey) keyStore.getKey(KEY_NAME, null);\n    }\n\n    private SecretKey genSecretKey() throws Exception {\n        // https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder?hl=en\n        KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, \"AndroidKeyStore\");\n        KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(KEY_NAME,\n                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)\n                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)\n                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)\n                .setUserAuthenticationRequired(true)\n                .setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG | KeyProperties.AUTH_DEVICE_CREDENTIAL);\n\n        keyGenerator.init(builder.build());\n        keyGenerator.generateKey();\n\n        return getSecretKey();\n    }\n\n    private Cipher getCipher() throws Exception {\n        return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + \"/\"\n                + KeyProperties.BLOCK_MODE_CBC + \"/\"\n                + KeyProperties.ENCRYPTION_PADDING_PKCS7);\n    }\n}"
  },
  {
    "path": "electrum/gui/qml/java_classes/org/electrum/biometry/BiometricHelper.java",
    "content": "package org.electrum.biometry;\n\nimport android.content.Context;\nimport android.content.pm.PackageManager;\nimport android.hardware.biometrics.BiometricManager;\nimport android.hardware.fingerprint.FingerprintManager;\nimport android.os.Build;\n\npublic class BiometricHelper {\n    public static boolean isAvailable(Context context) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // API 30+\n            BiometricManager biometricManager = context.getSystemService(BiometricManager.class);\n            return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS;\n        }\n        return false;\n    }\n}"
  },
  {
    "path": "electrum/gui/qml/java_classes/org/electrum/qr/SimpleScannerActivity.java",
    "content": "package org.electrum.qr;\n\nimport android.app.Activity;\nimport android.os.Bundle;\nimport android.os.Build;\nimport android.util.Log;\nimport android.content.Intent;\nimport android.Manifest;\nimport android.content.ClipData;\nimport android.content.ClipDescription;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.pm.PackageManager;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.WindowInsets;\nimport android.widget.Button;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport androidx.core.app.ActivityCompat;\n\nimport java.util.Arrays;\n\nimport de.markusfisch.android.barcodescannerview.widget.BarcodeScannerView;\nimport de.markusfisch.android.zxingcpp.ZxingCpp.Result;\nimport de.markusfisch.android.zxingcpp.ZxingCpp.ContentType;\n\n\nimport org.electrum.electrum.res.R; // package set in build.gradle\n\npublic class SimpleScannerActivity extends Activity {\n    private static final int MY_PERMISSIONS_CAMERA = 1002;\n\n    private BarcodeScannerView mScannerView = null;\n    final String TAG = \"org.electrum.qr.SimpleScannerActivity\";\n\n    private boolean mAlreadyRequestedPermissions = false;\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.scanner_layout);\n\n        // change top text\n        Intent intent = getIntent();\n        String text = intent.getStringExtra(intent.EXTRA_TEXT);\n        TextView hintTextView = (TextView) findViewById(R.id.hint);\n        hintTextView.setText(text);\n\n        // bind \"paste\" button\n        Button btn = (Button) findViewById(R.id.paste_btn);\n        btn.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\n                if (clipboard.hasPrimaryClip()\n                        && (clipboard.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)\n                            || clipboard.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML))) {\n                    ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);\n                    String clipboardText = item.getText().toString();\n                    // limit size of content. avoid https://developer.android.com/reference/android/os/TransactionTooLargeException.html\n                    if (clipboardText.length() >  512 * 1024) {\n                        Toast.makeText(SimpleScannerActivity.this, \"Clipboard contents too large.\", Toast.LENGTH_SHORT).show();\n                        return;\n                    }\n                    SimpleScannerActivity.this.setResultAndClose(null, clipboardText);\n                } else {\n                    Toast.makeText(SimpleScannerActivity.this, \"Clipboard is empty.\", Toast.LENGTH_SHORT).show();\n                }\n            }\n        });\n        setupEdgeToEdge();\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        if (this.hasPermission()) {\n            this.startCamera();\n        } else if (!mAlreadyRequestedPermissions) {\n            mAlreadyRequestedPermissions = true;\n            this.requestPermission();\n        }\n    }\n\n    @Override\n    public void onPause() {\n        super.onPause();\n        if (null != mScannerView) {\n            mScannerView.close();  // Stop camera on pause\n        }\n    }\n\n    private void startCamera() {\n        if (mScannerView == null) {\n            mScannerView = new BarcodeScannerView(this);\n            // Set crop ratio to 75% (this defines the square area shown in the scanner view)\n            mScannerView.setCropRatio(0.75f);\n            // allow tap to focus (note: some devices don't support autofocus which is enabled by default)\n            mScannerView.setTapToFocus();\n            // by default only Format.QR_CODE is set\n            ViewGroup contentFrame = (ViewGroup) findViewById(R.id.content_frame);\n            contentFrame.addView(mScannerView);\n            mScannerView.setOnBarcodeListener(result -> {\n                // Handle the scan result\n                this.setResultAndClose(result, null);\n                // Return false to stop scanning after first result\n                return false;\n            });\n        }\n        mScannerView.openAsync();  // Start camera on resume\n    }\n\n    private void setResultAndClose(Result scanResult, String textOnly) {\n        Intent resultIntent = new Intent();\n        if (textOnly != null) {\n            Log.v(TAG, \"clipboard contentType TEXT\");\n            resultIntent.putExtra(\"text\", textOnly);\n        } else if (scanResult != null) {\n            if (scanResult.getContentType() == ContentType.TEXT) {\n                Log.v(TAG, \"scanResult contentType TEXT\");\n                resultIntent.putExtra(\"text\", scanResult.getText());\n            } else if (scanResult.getContentType() == ContentType.BINARY) {\n                Log.v(TAG, \"scanResult contentType BINARY\");\n                resultIntent.putExtra(\"binary\", scanResult.getRawBytes());\n            } else {\n                Log.v(TAG, \"scanresult contenttype unknown\");\n            }\n        }\n        setResult(Activity.RESULT_OK, resultIntent);\n        this.finish();\n    }\n\n    private boolean hasPermission() {\n        return (ActivityCompat.checkSelfPermission(this,\n                                                   Manifest.permission.CAMERA)\n                == PackageManager.PERMISSION_GRANTED);\n    }\n\n    private void requestPermission() {\n        ActivityCompat.requestPermissions(this,\n                    new String[]{Manifest.permission.CAMERA},\n                    MY_PERMISSIONS_CAMERA);\n    }\n\n    @Override\n    public void onRequestPermissionsResult(int requestCode,\n            String permissions[], int[] grantResults) {\n        switch (requestCode) {\n            case MY_PERMISSIONS_CAMERA: {\n                if (grantResults.length > 0\n                    && grantResults[0] == PackageManager.PERMISSION_GRANTED) {\n                    // permission was granted, yay!\n                    this.startCamera();\n                } else {\n                    // permission denied\n                    //this.finish();\n                }\n                return;\n            }\n        }\n    }\n\n    private boolean enforcesEdgeToEdge() {\n        // if true the UI needs to be padded to be e2e compatible\n        return Build.VERSION.SDK_INT >= 35;\n    }\n\n    private void setupEdgeToEdge() {\n        if (!enforcesEdgeToEdge()) {\n            return;\n        }\n\n        // Get the root view and set up insets listener\n        getWindow().getDecorView().setOnApplyWindowInsetsListener((v, insets) -> {\n            android.graphics.Insets systemBars = insets.getInsets(WindowInsets.Type.systemBars());\n\n            // Apply padding to content frame to keep scanner focus area centered\n            ViewGroup contentFrame = findViewById(R.id.content_frame);\n            if (contentFrame != null) {\n                contentFrame.setPadding(\n                    systemBars.left,\n                    systemBars.top,\n                    systemBars.right,\n                    systemBars.bottom\n                );\n            }\n\n            // Apply top padding to hint text for status bar\n            TextView hintTextView = findViewById(R.id.hint);\n            if (hintTextView != null) {\n                hintTextView.setPadding(\n                    hintTextView.getPaddingLeft(),\n                    systemBars.top,\n                    hintTextView.getPaddingRight(),\n                    hintTextView.getPaddingBottom()\n                );\n            }\n\n            // Apply bottom margin to paste button for navigation bar\n            Button pasteButton = findViewById(R.id.paste_btn);\n            if (pasteButton != null) {\n                ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) pasteButton.getLayoutParams();\n                params.bottomMargin = systemBars.bottom;\n                pasteButton.setLayoutParams(params);\n            }\n\n            return insets;\n        });\n    }\n}\n"
  },
  {
    "path": "electrum/gui/qml/qeaddressdetails.py",
    "content": "from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject\n\nfrom electrum.logging import get_logger\nfrom electrum.util import UserFacingException\n\nfrom .auth import auth_protect, AuthMixin\nfrom .qetransactionlistmodel import QETransactionListModel\nfrom .qetypes import QEAmount\nfrom .qewallet import QEWallet\n\n\nclass QEAddressDetails(AuthMixin, QObject):\n    _logger = get_logger(__name__)\n\n    detailsChanged = pyqtSignal()\n    addressDeleteFailed = pyqtSignal([str], arguments=['message'])\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._wallet = None\n        self._address = None\n\n        self._label = None\n        self._frozen = False\n        self._scriptType = None\n        self._status = None\n        self._balance = QEAmount()\n        self._pubkeys = None\n        self._privkey = None\n        self._derivationPath = None\n        self._numtx = 0\n        self._candelete = False\n\n        self._historyModel = None\n\n    walletChanged = pyqtSignal()\n    @pyqtProperty(QEWallet, notify=walletChanged)\n    def wallet(self):\n        return self._wallet\n\n    @wallet.setter\n    def wallet(self, wallet: QEWallet):\n        if self._wallet != wallet:\n            self._wallet = wallet\n            self.walletChanged.emit()\n\n    addressChanged = pyqtSignal()\n    @pyqtProperty(str, notify=addressChanged)\n    def address(self):\n        return self._address\n\n    @address.setter\n    def address(self, address: str):\n        if self._address != address:\n            self._logger.debug('address changed')\n            self._address = address\n            self.addressChanged.emit()\n            self.update()\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def scriptType(self):\n        return self._scriptType\n\n    @pyqtProperty(QEAmount, notify=detailsChanged)\n    def balance(self):\n        return self._balance\n\n    @pyqtProperty('QStringList', notify=detailsChanged)\n    def pubkeys(self):\n        return self._pubkeys\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def privkey(self):\n        return self._privkey\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def derivationPath(self):\n        return self._derivationPath\n\n    @pyqtProperty(int, notify=detailsChanged)\n    def numTx(self):\n        return self._numtx\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def canDelete(self):\n        return self._candelete\n\n    frozenChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=frozenChanged)\n    def isFrozen(self):\n        return self._frozen\n\n    labelChanged = pyqtSignal()\n    @pyqtProperty(str, notify=labelChanged)\n    def label(self):\n        return self._label\n\n    @pyqtSlot(bool)\n    def freeze(self, freeze: bool):\n        if freeze != self._frozen:\n            self._wallet.wallet.set_frozen_state_of_addresses([self._address], freeze=freeze)\n            self._frozen = freeze\n            self.frozenChanged.emit()\n            self._wallet.balanceChanged.emit()\n\n    @pyqtSlot(str)\n    def setLabel(self, label: str):\n        if label != self._label:\n            self._wallet.wallet.set_label(self._address, label)\n            self._label = label\n            self.labelChanged.emit()\n\n    historyModelChanged = pyqtSignal()\n    @pyqtProperty(QETransactionListModel, notify=historyModelChanged)\n    def historyModel(self):\n        if self._historyModel is None:\n            self._historyModel = QETransactionListModel(self._wallet.wallet,\n                                                        onchain_domain=[self._address], include_lightning=False)\n        return self._historyModel\n\n    @pyqtSlot()\n    def requestShowPrivateKey(self):\n        self.retrieve_private_key()\n\n    @auth_protect(method='wallet')\n    def retrieve_private_key(self):\n        try:\n            self._privkey = self._wallet.wallet.export_private_key(self._address, self._wallet.password)\n        except Exception as e:\n            self._logger.error(f'problem retrieving privkey: {str(e)}')\n            self._privkey = ''\n\n        self.detailsChanged.emit()\n\n    @pyqtSlot(result=bool)\n    def deleteAddress(self):\n        assert self.canDelete\n        try:\n            self._wallet.wallet.delete_address(self._address)\n            self._wallet.historyModel.setDirty()\n        except UserFacingException as e:\n            self.addressDeleteFailed.emit(str(e))\n            return False\n        return True\n\n    def update(self):\n        if self._wallet is None:\n            self._logger.error('wallet undefined')\n            return\n\n        self._frozen = self._wallet.wallet.is_frozen_address(self._address)\n        self.frozenChanged.emit()\n\n        self._scriptType = self._wallet.wallet.get_txin_type(self._address)\n        self._label = self._wallet.wallet.get_label_for_address(self._address)\n        c, u, x = self._wallet.wallet.get_addr_balance(self._address)\n        self._balance = QEAmount(amount_sat=c + u + x)\n        self._pubkeys = self._wallet.wallet.get_public_keys(self._address)\n        self._derivationPath = self._wallet.wallet.get_address_path_str(self._address)\n        if self._wallet.derivationPrefix:\n            self._derivationPath = self._derivationPath.replace('m', self._wallet.derivationPrefix)\n        self._numtx = self._wallet.wallet.adb.get_address_history_len(self._address)\n        self._candelete = self.wallet.wallet.can_delete_address()\n        self.detailsChanged.emit()\n"
  },
  {
    "path": "electrum/gui/qml/qeaddresslistmodel.py",
    "content": "from typing import TYPE_CHECKING, List\n\nfrom PyQt6.QtCore import pyqtSlot, QSortFilterProxyModel, pyqtSignal, pyqtProperty\nfrom PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex\n\nfrom electrum.logging import get_logger\nfrom electrum.util import Satoshis\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\n\nfrom .qeconfig import QEConfig\nfrom .qetypes import QEAmount\n\n\nif TYPE_CHECKING:\n    from electrum.wallet import Abstract_Wallet\n    from electrum.transaction import PartialTxInput\n\n\nclass QEAddressCoinFilterProxyModel(QSortFilterProxyModel):\n    _logger = get_logger(__name__)\n\n    def __init__(self, parent_model, parent=None):\n        super().__init__(parent)\n        self._filter_text = None\n        self._show_coins = True\n        self._show_addresses = True\n        self._show_used = False\n        self._parent_model = parent_model\n        self.setSourceModel(parent_model)\n\n    countChanged = pyqtSignal()\n    @pyqtProperty(int, notify=countChanged)\n    def count(self):\n        return self.rowCount(QModelIndex())\n\n    def filterAcceptsRow(self, s_row, s_parent):\n        parent_model = self.sourceModel()\n        addridx = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['addridx'])\n        if addridx is None:  # coin\n            if not self._show_coins:\n                return False\n        else:\n            if not self._show_addresses:\n                return False\n            balance = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['balance'])\n            numtx = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['numtx'])\n            if balance.isEmpty and numtx and not self._show_used:\n                return False\n        if self._filter_text:\n            label = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['label'])\n            address = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['address'])\n            outpoint = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['outpoint'])\n            amount_i = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['amount'])\n            amount = parent_model.wallet.config.format_amount(amount_i.satsInt) if amount_i else None\n            filter_text = self._filter_text.casefold()\n            for item in [label, address, outpoint, amount]:\n                if item is not None and filter_text in str(item).casefold():\n                    return True\n            return False\n        return True\n\n    showAddressesCoinsChanged = pyqtSignal()\n    @pyqtProperty(int, notify=showAddressesCoinsChanged)\n    def showAddressesCoins(self) -> int:\n        result = 0\n        if self._show_addresses:\n            result += 1\n        if self._show_coins:\n            result += 2\n        return result\n\n    @showAddressesCoins.setter\n    def showAddressesCoins(self, show_addresses_coins: int):\n        show_addresses = show_addresses_coins in [1, 3]\n        show_coins = show_addresses_coins in [2, 3]\n\n        if self._show_addresses != show_addresses or self._show_coins != show_coins:\n            self._show_addresses = show_addresses\n            self._show_coins = show_coins\n            self.invalidateFilter()\n            self.showAddressesCoinsChanged.emit()\n\n    showUsedChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=showUsedChanged)\n    def showUsed(self) -> bool:\n        return self._show_used\n\n    @showUsed.setter\n    def showUsed(self, show_used: bool):\n        if self._show_used != show_used:\n            self._show_used = show_used\n            self.invalidateFilter()\n            self.showUsedChanged.emit()\n\n    filterTextChanged = pyqtSignal()\n    @pyqtProperty(str, notify=filterTextChanged)\n    def filterText(self) -> str:\n        return self._filter_text\n\n    @filterText.setter\n    def filterText(self, filter_text: str):\n        if self._filter_text != filter_text:\n            self._filter_text = filter_text\n            self.invalidateFilter()\n            self.filterTextChanged.emit()\n\n\nclass QEAddressCoinListModel(QAbstractListModel, QtEventListener):\n    _logger = get_logger(__name__)\n\n    # define listmodel rolemap\n    _ROLE_NAMES=('type', 'addridx', 'address', 'label', 'balance', 'numtx', 'held', 'height', 'amount', 'outpoint',\n                 'short_outpoint', 'short_id', 'txid')\n    _ROLE_KEYS = range(Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole + len(_ROLE_NAMES))\n    _ROLE_MAP  = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))\n    _ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS))\n\n    def __init__(self, wallet: 'Abstract_Wallet', parent=None):\n        super().__init__(parent)\n        self.wallet = wallet\n        self._items = []\n        self._filterModel = None\n\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n        QEConfig.instance.freezeReusedAddressUtxosChanged.connect(lambda: self.setDirty())\n\n        self._dirty = True\n        self.initModel()\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    @qt_event_listener\n    def on_event_labels_received(self, wallet, labels):\n        if wallet == self.wallet:\n            self.setDirty()\n\n    def rowCount(self, index):\n        return len(self._items)\n\n    def roleNames(self):\n        return self._ROLE_MAP\n\n    def data(self, index, role):\n        address = self._items[index.row()]\n        role_index = role - Qt.ItemDataRole.UserRole\n        try:\n            value = address[self._ROLE_NAMES[role_index]]\n        except KeyError:\n            return None\n        if isinstance(value, (bool, list, int, str, QEAmount)) or value is None:\n            return value\n        if isinstance(value, Satoshis):\n            return value.value\n        return str(value)\n\n    def clear(self):\n        self.beginResetModel()\n        self._items = []\n        self.endResetModel()\n\n    def addr_to_model(self, addrtype: str, addridx: int, address: str):\n        c, u, x = self.wallet.get_addr_balance(address)\n        item = {\n            'type': addrtype,\n            'addridx': addridx,\n            'address': address,\n            'numtx': self.wallet.adb.get_address_history_len(address),\n            'label': self.wallet.get_label_for_address(address),\n            'balance': QEAmount(amount_sat=c + u + x),\n            'held': self.wallet.is_frozen_address(address)\n        }\n        return item\n\n    def coin_to_model(self, addrtype: str, coin: 'PartialTxInput'):\n        txid = coin.prevout.txid.hex()\n        short_id = ''\n        # check below duplicated from TxInput as we cannot get short_id unambiguously\n        if coin.block_txpos is not None and coin.block_txpos >= 0:\n            short_id = str(coin.short_id)\n        item = {\n            'type': addrtype,\n            'amount': QEAmount(amount_sat=coin.value_sats()),\n            'address': coin.address,\n            'height': coin.block_height,\n            'outpoint': coin.prevout.to_str(),\n            'short_outpoint': coin.prevout.short_name(),\n            'short_id': short_id,\n            'txid': txid,\n            'label': self.wallet.get_label_for_txid(txid) or '',\n            'held': self.wallet.is_frozen_coin(coin),\n            'coin': coin\n        }\n        return item\n\n    @pyqtSlot()\n    def setDirty(self):\n        self._dirty = True\n\n    # initial model data\n    @pyqtSlot()\n    @pyqtSlot(bool)\n    def initModel(self, force: bool = False):\n        if not self._dirty and not force:\n            return\n\n        r_addresses = self.wallet.get_receiving_addresses()\n        c_addresses = self.wallet.get_change_addresses() if self.wallet.wallet_type != 'imported' else []\n        n_addresses = len(r_addresses) + len(c_addresses)\n\n        def insert_address(atype, address, addridx):\n            item = self.addr_to_model(atype, addridx, address)\n            self._items.append(item)\n\n            utxos = self.wallet.get_utxos([address])\n            utxos.sort(key=lambda x: x.block_height)\n            for i, coin in enumerate(utxos):\n                self._items.append(self.coin_to_model(atype, coin))\n\n        self.clear()\n        self.beginInsertRows(QModelIndex(), 0, n_addresses - 1)\n        if self.wallet.wallet_type != 'imported':\n            for i, address in enumerate(r_addresses):\n                insert_address('receive', address, i)\n            for i, address in enumerate(c_addresses):\n                insert_address('change', address, i)\n        else:\n            for i, address in enumerate(r_addresses):\n                insert_address('imported', address, i)\n        self.endInsertRows()\n\n        self._dirty = False\n\n        if self._filterModel is not None:\n            self._filterModel.invalidate()\n\n    @pyqtSlot(str)\n    def updateAddress(self, address):\n        for i, a in enumerate(self._items):\n            if a['address'] == address:\n                self.do_update(i, a)\n                return\n\n    @pyqtSlot(str)\n    def deleteAddress(self, address):\n        first = -1\n        last = -1\n        for i, a in enumerate(self._items):\n            if a['address'] == address:\n                if first < 0:\n                    first = i\n                last = i\n        if not first >= 0:\n            return\n        self.beginRemoveRows(QModelIndex(), first, last)\n        self._items = self._items[0:first] + self._items[last+1:]\n        self.endRemoveRows()\n\n    def updateCoin(self, outpoint):\n        for i, a in enumerate(self._items):\n            if a.get('outpoint') == outpoint:\n                self.do_update(i, a)\n                return\n\n    def do_update(self, modelindex, modelitem):\n        mi = self.createIndex(modelindex, 0)\n        self._logger.debug(repr(modelitem))\n        if modelitem.get('outpoint'):\n            modelitem.update(self.coin_to_model(modelitem['type'], modelitem['coin']))\n        else:\n            modelitem.update(self.addr_to_model(modelitem['type'], modelitem['addridx'], modelitem['address']))\n        self._logger.debug(repr(modelitem))\n        self.dataChanged.emit(mi, mi, self._ROLE_KEYS)\n\n    filterModelChanged = pyqtSignal()\n    @pyqtProperty(QEAddressCoinFilterProxyModel, notify=filterModelChanged)\n    def filterModel(self):\n        if self._filterModel is None:\n            self._filterModel = QEAddressCoinFilterProxyModel(self)\n        return self._filterModel\n\n    @pyqtSlot(bool, list)\n    def setFrozenForItems(self, freeze: bool, items: List[str]):\n        self._logger.debug(f'set frozen to {freeze} for {items!r}')\n        coins = list(filter(lambda x: ':' in x, items))\n        if len(coins):\n            self.wallet.set_frozen_state_of_coins(coins, freeze)\n            for coin in coins:\n                self.updateCoin(coin)\n        addresses = list(filter(lambda x: ':' not in x, items))\n        if len(addresses):\n            self.wallet.set_frozen_state_of_addresses(addresses, freeze)\n            for address in addresses:\n                self.updateAddress(address)\n\n"
  },
  {
    "path": "electrum/gui/qml/qeapp.py",
    "content": "import re\nimport queue\nimport time\nimport os\nimport sys\nimport html\nimport threading\nfrom functools import partial\nfrom typing import TYPE_CHECKING, Set, List, Optional, Callable\n\nfrom PyQt6.QtCore import (pyqtSlot, pyqtSignal, pyqtProperty, QObject, QT_VERSION_STR, PYQT_VERSION_STR,\n                          qInstallMessageHandler, QTimer, QSortFilterProxyModel)\nfrom PyQt6.QtGui import QGuiApplication\nfrom PyQt6.QtQml import qmlRegisterType, QQmlApplicationEngine\n\nimport electrum\nfrom electrum import version, constants\nfrom electrum.i18n import _\nfrom electrum.logging import Logger, get_logger\nfrom electrum.bip21 import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME\nfrom electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue\nfrom electrum.network import Network\nfrom electrum.plugin import run_hook\nfrom electrum.gui.common_qt.util import get_font_id\nfrom electrum.util import profiler\n\nfrom .qeconfig import QEConfig\nfrom .qedaemon import QEDaemon\nfrom .qenetwork import QENetwork\nfrom .qewallet import QEWallet\nfrom .qeqr import QEQRParser, QEQRImageProvider, QEQRImageProviderHelper\nfrom .qeqrscanner import QEQRScanner\nfrom .qebitcoin import QEBitcoin\nfrom .qefx import QEFX\nfrom .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller, QETxSweepFinalizer, FeeSlider\nfrom .qeinvoice import QEInvoice, QEInvoiceParser\nfrom .qepiresolver import QEPIResolver\nfrom .qerequestdetails import QERequestDetails\nfrom .qetypes import QEAmount, QEBytes\nfrom .qeaddressdetails import QEAddressDetails\nfrom .qetxdetails import QETxDetails\nfrom .qechannelopener import QEChannelOpener\nfrom .qelnpaymentdetails import QELnPaymentDetails\nfrom .qechanneldetails import QEChannelDetails\nfrom .qeswaphelper import QESwapHelper\nfrom .qewizard import QENewWalletWizard, QEServerConnectWizard, QETermsOfUseWizard\nfrom .qemodelfilter import QEFilterProxyModel\nfrom .qebip39recovery import QEBip39RecoveryListModel\nfrom .qebiometrics import QEBiometrics\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n    from electrum.wallet import Abstract_Wallet\n    from electrum.daemon import Daemon\n    from electrum.plugin import Plugins\n\nif 'ANDROID_DATA' in os.environ:\n    from jnius import autoclass, cast\n    from android import activity, permissions\n\n    jpythonActivity = autoclass('org.kivy.android.PythonActivity').mActivity\n    jHfc = autoclass('android.view.HapticFeedbackConstants')\n    jString = autoclass('java.lang.String')\n    jIntent = autoclass('android.content.Intent')\n    jview = jpythonActivity.getWindow().getDecorView()\n    systemSdkVersion = autoclass('android.os.Build$VERSION').SDK_INT\n\nnotification = None\n\n\nclass QEAppController(BaseCrashReporter, QObject):\n    _dummy = pyqtSignal()\n    userNotify = pyqtSignal(str, str)\n    uriReceived = pyqtSignal(str)\n    showException = pyqtSignal('QVariantMap')\n    sendingBugreport = pyqtSignal()\n    sendingBugreportSuccess = pyqtSignal(str)\n    sendingBugreportFailure = pyqtSignal(str)\n    secureWindowChanged = pyqtSignal()\n    wantCloseChanged = pyqtSignal()\n    pluginLoaded = pyqtSignal(str)\n    startupFinished = pyqtSignal()\n\n    def __init__(self, qeapp: 'ElectrumQmlApplication', plugins: 'Plugins'):\n        BaseCrashReporter.__init__(self, None, None, None)\n        QObject.__init__(self)\n\n        self._app = qeapp\n        self._plugins = plugins\n        self.config = QEConfig.instance.config\n\n        self._crash_user_text = ''\n        self._app_started = False\n        self._intent = ''\n        self._secureWindow = False\n\n        # map of permissions and grant status _after_ asking user\n        self._permissions = {}  # type: dict[str, bool]\n\n        # set up notification queue and notification_timer\n        self.user_notification_queue = queue.Queue()\n        self.user_notification_last_time = 0\n\n        self.notification_timer = QTimer(self)\n        self.notification_timer.setSingleShot(False)\n        self.notification_timer.setInterval(500)  # msec\n        self.notification_timer.timeout.connect(self.on_notification_timer)\n\n        QEDaemon.instance.walletLoaded.connect(self.on_wallet_loaded)\n\n        self.userNotify.connect(self.doNotify)\n\n        if self.isAndroid():\n            self.bindIntent()\n\n        self._want_close = False\n\n    def on_wallet_loaded(self):\n        qewallet = QEDaemon.instance.currentWallet\n        if not qewallet:\n            return\n\n        # register wallet in Exception_Hook\n        Exception_Hook.maybe_setup(wallet=qewallet.wallet)\n\n        # attach to the wallet user notification events\n        # connect only once\n        try:\n            qewallet.userNotify.disconnect(self.on_wallet_usernotify)\n        except Exception:\n            pass\n        qewallet.userNotify.connect(self.on_wallet_usernotify)\n\n    def on_wallet_usernotify(self, wallet, message):\n        self.logger.debug(message)\n        self.user_notification_queue.put((wallet, message))\n        if not self.notification_timer.isActive():\n            self.logger.debug('starting app notification timer')\n            self.notification_timer.start()\n            self.on_notification_timer()\n\n    def on_notification_timer(self):\n        if self.user_notification_queue.qsize() == 0:\n            self.logger.debug('queue empty, stopping app notification timer')\n            self.notification_timer.stop()\n            return\n        now = time.time()\n        rate_limit = 20  # seconds\n        if self.user_notification_last_time + rate_limit > now:\n            return\n        self.user_notification_last_time = now\n        self.logger.info(\"Notifying GUI about new user notifications\")\n        # request permission and defer notify until after permission request callback\n        # note: permission request is only shown to user once, so it is safe to request\n        # multiple times\n        if self.isAndroid() and not self.hasPermission(permissions.Permission.POST_NOTIFICATIONS) \\\n                and self._permissions.get(permissions.Permission.POST_NOTIFICATIONS) is None:\n            self.request_permission(permissions.Permission.POST_NOTIFICATIONS)\n            return\n        try:\n            wallet, message = self.user_notification_queue.get_nowait()\n            self.userNotify.emit(str(wallet), message)\n        except queue.Empty:\n            pass\n\n    def doNotify(self, wallet_name, message):\n        self.logger.debug(f'sending push notification to OS: {message=!r}')\n        if os.name == 'nt':\n            icon = \"\"  # plyer wants image to be in .ico format on Windows\n        else:\n            icon = os.path.join(\n                os.path.dirname(os.path.dirname(os.path.realpath(__file__))), \"icons\", \"electrum.png\",\n            )\n        try:\n            # TODO: lazy load not in UI thread please\n            global notification\n            if not notification:\n                from plyer import notification\n            notification.notify('Electrum', message, app_icon=icon, app_name='Electrum')\n        except ImportError:\n            self.logger.warning('Notification: needs plyer; `python3 -m pip install plyer`')\n        except Exception as e:\n            self.logger.error(repr(e))\n\n    def bindIntent(self):\n        if not self.isAndroid():\n            return\n        try:\n            self.on_new_intent(jpythonActivity.getIntent())\n            activity.bind(on_new_intent=self.on_new_intent)\n        except Exception as e:\n            self.logger.error(f'unable to bind intent: {repr(e)}')\n\n    @pyqtSlot(str, result=bool)\n    def hasPermission(self, permissionFqcn: str) -> bool:\n        if not self.isAndroid():\n            return True\n        result = permissions.check_permission(permissionFqcn)\n        return result\n\n    def request_permission(self, permissionFqcn: str, permission_result_cb: Optional[Callable] = None):\n        if not self.isAndroid():\n            return True\n        self.logger.debug(f'requesting {permissionFqcn=}')\n        permissions.request_permission(\n            permissionFqcn,\n            callback=partial(self.on_request_permissions_result, permissionFqcn, permission_result_cb)\n        )\n\n    def on_request_permissions_result(\n            self,\n            permission: str,\n            permission_result_cb: Optional[Callable[[bool], None]],\n            permissions: List[str],\n            grant_results: List[bool]\n    ):\n        self.logger.debug(f'on_request_permissions_result, len={len(permissions)}, p={repr(permissions)}, g={repr(grant_results)}')\n        grant_result = None\n        try:\n            grant_result = grant_results[permissions.index(permission)]\n        except ValueError:\n            pass\n\n        if grant_result is not None:\n            self._permissions[permission] = grant_result\n\n        if permission_result_cb:\n            permission_result_cb(grant_result)\n\n    def on_new_intent(self, intent):\n        if not self._app_started:\n            self._intent = intent\n            return\n\n        data = str(intent.getDataString())\n        self.logger.debug(f'received intent: {repr(data)}')\n        scheme = str(intent.getScheme()).lower()\n        if scheme == BITCOIN_BIP21_URI_SCHEME or scheme == LIGHTNING_URI_SCHEME:\n            self.uriReceived.emit(data)\n\n    def startup_finished(self):\n        self._app_started = True\n        self.startupFinished.emit()\n        for plugin_name in self._plugins.plugins.keys():\n            self.pluginLoaded.emit(plugin_name)\n        if self._intent:\n            self.on_new_intent(self._intent)\n\n    @pyqtProperty(bool, notify=wantCloseChanged)\n    def wantClose(self):\n        return self._want_close\n\n    @wantClose.setter\n    def wantClose(self, want_close):\n        if want_close != self._want_close:\n            self._want_close = want_close\n            self.wantCloseChanged.emit()\n\n    @pyqtSlot(str, str)\n    def doShare(self, data, title):\n        if not self.isAndroid():\n            return\n\n        sendIntent = jIntent()\n        sendIntent.setAction(jIntent.ACTION_SEND)\n        sendIntent.setType(\"text/plain\")\n        sendIntent.putExtra(jIntent.EXTRA_TEXT, jString(data))\n        it = jIntent.createChooser(sendIntent, cast('java.lang.CharSequence', jString(title)))\n        jpythonActivity.startActivity(it)\n\n    @pyqtSlot(result=bool)\n    def isMaxBrightnessOnQrDisplayEnabled(self):\n        return self.config.GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY\n\n    @pyqtSlot()\n    def setMaxScreenBrightness(self):\n        self._set_screen_brightness(1.0)\n\n    @pyqtSlot()\n    def resetScreenBrightness(self):\n        self._set_screen_brightness(-1.0)\n\n    def _set_screen_brightness(self, br: float) -> None:\n        \"\"\"br is the desired screen brightness, a value in the [0, 1] interval.\n        A negative value, e.g. -1.0, means a \"reset\" back to the system preferred value.\n        \"\"\"\n        if not self.isAndroid():\n            return\n        from android.runnable import run_on_ui_thread\n\n        @run_on_ui_thread\n        def set_br():\n            window = jpythonActivity.getWindow()\n            attrs = window.getAttributes()\n            attrs.screenBrightness = br\n            window.setAttributes(attrs)\n        set_br()\n\n    @pyqtSlot('QString')\n    def textToClipboard(self, text):\n        QGuiApplication.clipboard().setText(text)\n\n    @pyqtSlot(result='QString')\n    def clipboardToText(self):\n        clip = QGuiApplication.clipboard()\n        return clip.text() if clip.mimeData().hasText() else ''\n\n    @pyqtSlot(str, result=QObject)\n    def plugin(self, plugin_name):\n        self.logger.debug(f'now {self._plugins.count()} plugins loaded')\n        plugin = self._plugins.get(plugin_name)\n        self.logger.debug(f'plugin with name {plugin_name} is {str(type(plugin))}')\n        if plugin and hasattr(plugin, 'so'):\n            return plugin.so\n        else:\n            self.logger.debug('None!')\n            return None\n\n    @pyqtProperty('QVariantList', notify=_dummy)\n    def plugins(self):\n        s = []\n        for item in self._plugins.descriptions:\n            s.append({\n                'name': item,\n                'fullname': self._plugins.descriptions[item]['fullname'],\n                'enabled': bool(self._plugins.get(item))\n                })\n\n        return s\n\n    @pyqtSlot(str, bool)\n    def setPluginEnabled(self, plugin: str, enabled: bool):\n        if enabled:\n            self._plugins.enable(plugin)\n            # note: all enabled plugins will receive this hook:\n            run_hook('init_qml', self._app)\n        else:\n            self._plugins.disable(plugin)\n\n    @pyqtSlot(str, result=bool)\n    def isPluginEnabled(self, plugin: str):\n        return bool(self._plugins.get(plugin))\n\n    @pyqtSlot(result=bool)\n    def isAndroid(self):\n        return 'ANDROID_DATA' in os.environ\n\n    @pyqtSlot(result='QVariantMap')\n    def crashData(self):\n        return {\n            'traceback': self.get_traceback_info(*self.exc_args),\n            'extra': self.get_additional_info(),\n            'reportstring': self.get_report_string()\n        }\n\n    @pyqtSlot(object, object, object)\n    def crash(self, e, text, tb):\n        self.exc_args = (e, text, tb)  # for BaseCrashReporter\n        self.showException.emit(self.crashData())\n\n    @pyqtSlot(str)\n    def sendReport(self, user_text: str):\n        self._crash_user_text = user_text\n        network = Network.get_instance()\n        proxy = network.proxy\n\n        def report_task():\n            self.logger.debug('starting report_task')\n            try:\n                response = BaseCrashReporter.send_report(self, network.asyncio_loop, proxy)\n            except Exception as e:\n                self.logger.error('There was a problem with the automatic reporting', exc_info=e)\n                self.sendingBugreportFailure.emit(_('There was a problem with the automatic reporting:') + '<br/>' +\n                                        repr(e)[:120] + '<br/><br/>' +\n                                        _(\"Please report this issue manually\") +\n                                        f' <a href=\"{constants.GIT_REPO_ISSUES_URL}\">on GitHub</a>.')\n            else:\n                text = response.text\n                if response.url:\n                    text += f\" You can track further progress on <a href='{response.url}'>GitHub</a>.\"\n                self.sendingBugreportSuccess.emit(text)\n\n        self.sendingBugreport.emit()\n        threading.Thread(target=report_task, daemon=True).start()\n\n    def _get_traceback_str_to_display(self) -> str:\n        # The msg_box that shows the report uses rich_text=True, so\n        # if traceback contains special HTML characters, e.g. '<',\n        # they need to be escaped to avoid formatting issues.\n        traceback_str = super()._get_traceback_str_to_display()\n        return html.escape(traceback_str).replace('&#x27;', '&apos;')\n\n    def get_user_description(self):\n        return self._crash_user_text\n\n    def get_wallet_type(self):\n        wallet_types = Exception_Hook._INSTANCE.wallet_types_seen\n        return \",\".join(wallet_types)\n\n    @pyqtSlot()\n    def haptic(self):\n        if not self.isAndroid():\n            return\n        jview.performHapticFeedback(jHfc.VIRTUAL_KEY)\n\n    @pyqtProperty(bool, notify=secureWindowChanged)\n    def secureWindow(self):\n        return self._secureWindow\n\n    @secureWindow.setter\n    def secureWindow(self, secure):\n        if not self.isAndroid():\n            return\n        if self.config.GUI_QML_ALWAYS_ALLOW_SCREENSHOTS:\n            return\n        if self._secureWindow != secure:\n            jpythonActivity.setSecureWindow(secure)\n            self._secureWindow = secure\n            self.secureWindowChanged.emit()\n\n    @pyqtSlot(result=bool)\n    def enforcesEdgeToEdge(self) -> bool:\n        if not self.isAndroid():\n            return False\n        return bool(systemSdkVersion >= 35)\n\n    @profiler(min_threshold=0.02)\n    def _getSystemBarHeight(self, bar_type: str) -> int:\n        if not self.enforcesEdgeToEdge():\n            return 0\n        assert systemSdkVersion >= 30, \\\n            f\"Android WindowInsets unavailable on {systemSdkVersion=}\"\n        try:\n            root_insets = jview.getRootWindowInsets()\n            window_insets_type = autoclass('android.view.WindowInsets$Type')\n\n            if bar_type == 'status':\n                ins = root_insets.getInsets(window_insets_type.statusBars())\n            elif bar_type == 'navigation':\n                ins = root_insets.getInsets(window_insets_type.navigationBars())\n            else:\n                raise ValueError(f\"Invalid bar_type: {bar_type}\")\n\n            # Get the display metrics to convert pixels to dp\n            display_metrics = jpythonActivity.getResources().getDisplayMetrics()\n            density = display_metrics.density\n\n            height = int(max(ins.bottom, ins.right, ins.left, ins.top, 0))\n            if not height > 0:\n                return 0\n\n            # Convert from pixels to dp for QML\n            height_dp = int(height / density)\n\n            self.logger.debug(f\"_getSystemBarHeight: {height=}, {height_dp=}, {bar_type=}\")\n            return max(0, height_dp)\n        except Exception as e:\n            self.logger.debug(f\"{bar_type} fallback due to: {e!r}\")\n            return 0\n\n    @pyqtSlot(result=int)\n    def getStatusBarHeight(self) -> int:\n        return self._getSystemBarHeight('status')\n\n    @pyqtSlot(result=int)\n    def getNavigationBarHeight(self) -> int:\n        return self._getSystemBarHeight('navigation')\n\n\nclass ElectrumQmlApplication(QGuiApplication):\n\n    _valid = True\n\n    def __init__(self, args, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'):\n        super().__init__(args)\n\n        self.logger = get_logger(__name__)\n\n        # TODO QT6 order of declaration is important now?\n        qmlRegisterType(QEAmount, 'org.electrum', 1, 0, 'Amount')\n        qmlRegisterType(QEBytes, 'org.electrum', 1, 0, 'Bytes')\n        qmlRegisterType(QENewWalletWizard, 'org.electrum', 1, 0, 'QNewWalletWizard')\n        qmlRegisterType(QETermsOfUseWizard, 'org.electrum', 1, 0, 'QTermsOfUseWizard')\n        qmlRegisterType(QEServerConnectWizard, 'org.electrum', 1, 0, 'QServerConnectWizard')\n        qmlRegisterType(QEFilterProxyModel, 'org.electrum', 1, 0, 'FilterProxyModel')\n        qmlRegisterType(QSortFilterProxyModel, 'org.electrum', 1, 0, 'QSortFilterProxyModel')\n\n        qmlRegisterType(QEWallet, 'org.electrum', 1, 0, 'Wallet')\n        qmlRegisterType(QEBitcoin, 'org.electrum', 1, 0, 'Bitcoin')\n        qmlRegisterType(QEQRParser, 'org.electrum', 1, 0, 'QRParser')\n        qmlRegisterType(QEQRScanner, 'org.electrum', 1, 0, 'QRScanner')\n        qmlRegisterType(QEFX, 'org.electrum', 1, 0, 'FX')\n        qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer')\n        qmlRegisterType(QEPIResolver, 'org.electrum', 1, 0, 'PIResolver')\n        qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice')\n        qmlRegisterType(QEInvoiceParser, 'org.electrum', 1, 0, 'InvoiceParser')\n        qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails')\n        qmlRegisterType(QETxDetails, 'org.electrum', 1, 0, 'TxDetails')\n        qmlRegisterType(QEChannelOpener, 'org.electrum', 1, 0, 'ChannelOpener')\n        qmlRegisterType(QELnPaymentDetails, 'org.electrum', 1, 0, 'LnPaymentDetails')\n        qmlRegisterType(QEChannelDetails, 'org.electrum', 1, 0, 'ChannelDetails')\n        qmlRegisterType(QESwapHelper, 'org.electrum', 1, 0, 'SwapHelper')\n        qmlRegisterType(QERequestDetails, 'org.electrum', 1, 0, 'RequestDetails')\n        qmlRegisterType(QETxRbfFeeBumper, 'org.electrum', 1, 0, 'TxRbfFeeBumper')\n        qmlRegisterType(QETxCpfpFeeBumper, 'org.electrum', 1, 0, 'TxCpfpFeeBumper')\n        qmlRegisterType(QETxCanceller, 'org.electrum', 1, 0, 'TxCanceller')\n        qmlRegisterType(QETxSweepFinalizer, 'org.electrum', 1, 0, 'SweepFinalizer')\n        qmlRegisterType(QEBip39RecoveryListModel, 'org.electrum', 1, 0, 'Bip39RecoveryListModel')\n        qmlRegisterType(FeeSlider, 'org.electrum', 1, 0, 'FeeSlider')\n        # TODO QT6: these were declared as uncreatable, but that doesn't seem to work for pyqt6\n        # qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property')\n        # qmlRegisterUncreatableType(QENewWalletWizard, 'org.electrum', 1, 0, 'QNewWalletWizard', 'QNewWalletWizard can only be used as property')\n        # qmlRegisterUncreatableType(QEServerConnectWizard, 'org.electrum', 1, 0, 'QServerConnectWizard', 'QServerConnectWizard can only be used as property')\n        # qmlRegisterUncreatableType(QEFilterProxyModel, 'org.electrum', 1, 0, 'FilterProxyModel', 'FilterProxyModel can only be used as property')\n        # qmlRegisterUncreatableType(QSortFilterProxyModel, 'org.electrum', 1, 0, 'QSortFilterProxyModel', 'QSortFilterProxyModel can only be used as property')\n\n        self.engine = QQmlApplicationEngine(parent=self)\n\n        screensize = self.primaryScreen().size()\n\n        qr_size = min(screensize.width(), screensize.height()) * 7/8\n        self.qr_ip = QEQRImageProvider(qr_size)\n        self.engine.addImageProvider('qrgen', self.qr_ip)\n        self.qr_ip_h = QEQRImageProviderHelper(qr_size)\n\n        # add a monospace font as we can't rely on device having one\n        self.fixedFont = 'PT Mono'\n        not_loaded = get_font_id('PTMono-Regular.ttf') < 0\n        not_loaded = get_font_id('PTMono-Bold.ttf') < 0 and not_loaded\n        if not_loaded:\n            self.logger.warning('Could not load font PT Mono')\n            self.fixedFont = 'Monospace' # hope for the best\n\n        self.context = self.engine.rootContext()\n        self.plugins = plugins\n        self.config = QEConfig(config)\n        self.network = QENetwork(daemon.network)\n        self.daemon = QEDaemon(daemon, self.plugins)\n        self.appController = QEAppController(self, self.plugins)\n        self.maxAmount = QEAmount(is_max=True)\n        self.biometrics = QEBiometrics(config=config, parent=self)\n        self.context.setContextProperty('AppController', self.appController)\n        self.context.setContextProperty('Config', self.config)\n        self.context.setContextProperty('Network', self.network)\n        self.context.setContextProperty('Daemon', self.daemon)\n        self.context.setContextProperty('FixedFont', self.fixedFont)\n        self.context.setContextProperty('MAX', self.maxAmount)\n        self.context.setContextProperty('QRIP', self.qr_ip_h)\n        self.context.setContextProperty('Biometrics', self.biometrics)\n        self.context.setContextProperty('BUILD', {\n            'electrum_version': version.ELECTRUM_VERSION,\n            'protocol_version': f\"[{version.PROTOCOL_VERSION_MIN}, {version.PROTOCOL_VERSION_MAX}]\",\n            'qt_version': QT_VERSION_STR,\n            'pyqt_version': PYQT_VERSION_STR\n        })\n        self.context.setContextProperty('UI_UNIT_NAME', {\n            \"FEERATE_SAT_PER_VBYTE\": electrum.util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE,\n            \"FEERATE_SAT_PER_VB\":    electrum.util.UI_UNIT_NAME_FEERATE_SAT_PER_VB,\n            \"FIXED_SAT\":             electrum.util.UI_UNIT_NAME_FIXED_SAT,\n            \"TXSIZE_VBYTES\":         electrum.util.UI_UNIT_NAME_TXSIZE_VBYTES,\n            \"MEMPOOL_MB\":            electrum.util.UI_UNIT_NAME_MEMPOOL_MB,\n        })\n\n        self.plugins.load_plugin_by_name('trustedcoin')\n\n        qInstallMessageHandler(self.message_handler)\n\n        # get notified whether root QML document loads or not\n        self.engine.objectCreated.connect(self.objectCreated)\n\n    # slot is called after loading root QML. If object is None, it has failed.\n    @pyqtSlot('QObject*', 'QUrl')\n    def objectCreated(self, object, url):\n        self.engine.objectCreated.disconnect(self.objectCreated)\n        if object is None:\n            self._valid = False\n        else:\n            self.appController.startup_finished()\n\n    def message_handler(self, line, funct, file):\n        # filter out common harmless messages\n        if re.search('file:///.*TypeError: Cannot read property.*null$', file):\n            return\n        self.logger.warning(file)\n\n\nclass Exception_Hook(QObject, Logger):\n    _report_exception = pyqtSignal(object, object, object)\n\n    _INSTANCE = None  # type: Optional[Exception_Hook]  # singleton\n\n    def __init__(self, *, slot):\n        QObject.__init__(self)\n        Logger.__init__(self)\n        assert self._INSTANCE is None, \"Exception_Hook is supposed to be a singleton\"\n        self.wallet_types_seen = set()  # type: Set[str]\n        self.exception_ids_seen = set()  # type: Set[bytes]\n\n        sys.excepthook = self.handler\n        threading.excepthook = self.handler\n\n        if slot:\n            self._report_exception.connect(slot)\n        EarlyExceptionsQueue.set_hook_as_ready()\n\n    @classmethod\n    def maybe_setup(cls, *, wallet: 'Abstract_Wallet' = None, slot=None) -> None:\n        if not cls._INSTANCE:\n            cls._INSTANCE = Exception_Hook(slot=slot)\n        if wallet:\n            cls._INSTANCE.wallet_types_seen.add(wallet.wallet_type)\n\n    def handler(self, *exc_info):\n        self.logger.error('exception caught by crash reporter', exc_info=exc_info)\n        groupid_hash = BaseCrashReporter.get_traceback_groupid_hash(*exc_info)\n        if groupid_hash in self.exception_ids_seen:\n            return  # to avoid annoying the user, only show crash reporter once per exception groupid\n        self.exception_ids_seen.add(groupid_hash)\n        self._report_exception.emit(*exc_info)\n"
  },
  {
    "path": "electrum/gui/qml/qebiometrics.py",
    "content": "import os\nimport secrets\nfrom enum import Enum\nfrom typing import Optional, TYPE_CHECKING\n\nfrom PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty\n\nfrom electrum.i18n import _\nfrom electrum.logging import get_logger\nfrom electrum.base_crash_reporter import send_exception_to_crash_reporter\nfrom electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv\n\nfrom .auth import auth_protect, AuthMixin\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n\n\n_logger = get_logger(__name__)\n\n\njBiometricHelper = None\njBiometricActivity = None\njPythonActivity = None\njIntent = None\njString = None\n\nif 'ANDROID_DATA' in os.environ:\n    from jnius import autoclass, JavaException\n    from android import activity\n    try:\n        jPythonActivity = autoclass('org.kivy.android.PythonActivity').mActivity\n        jIntent = autoclass('android.content.Intent')\n        jString = autoclass('java.lang.String')\n        jBiometricActivity = autoclass('org.electrum.biometry.BiometricActivity')\n        jBiometricHelper = autoclass('org.electrum.biometry.BiometricHelper')\n    except JavaException as e:\n        _logger.error(f\"Could not load Biometric java classes (maybe due to old api version): {e}\")\n\n\nclass BiometricAction(str, Enum):\n    ENCRYPT = \"ENCRYPT\"\n    DECRYPT = \"DECRYPT\"\n\n\nclass QEBiometrics(AuthMixin, QObject):\n    REQUEST_CODE_BIOMETRIC_ACTIVITY = 24553  # random 16 bit int\n    RESULT_CODE_SETUP_FAILED = 101  # codes duplicated from BiometricActivity.java\n    RESULT_CODE_POPUP_CANCELLED = 102\n\n    enablingFailed = pyqtSignal(str, arguments=['error'])\n    unlockSuccess = pyqtSignal(str, arguments=['password'])\n    unlockError = pyqtSignal(str, arguments=['error'])\n\n    def __init__(self, *, config: 'SimpleConfig', parent=None):\n        super().__init__(parent)\n        self.config = config\n        self._current_action: Optional[BiometricAction] = None\n\n    @pyqtProperty(bool, constant=True)\n    def isAvailable(self) -> bool:\n        if 'ANDROID_DATA' not in os.environ or jBiometricHelper is None:\n            return False\n        try:\n            return jBiometricHelper.isAvailable(jPythonActivity)\n        except Exception as e:\n            send_exception_to_crash_reporter(e)\n            return False\n\n    isEnabledChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=isEnabledChanged)\n    def isEnabled(self) -> bool:\n        return self.config.WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION\n\n    @pyqtSlot(str)\n    def enable(self, unified_wallet_password: str):\n        \"\"\"\n        We encrypt (`wrap`) the wallet password with a random key 'wrap_key' and encrypt the random key\n        with the AndroidKeyStore.\n        Both the encrypted wrap_key and the encrypted wallet password are stored in the config.\n        The encryption key for the wrap_key is stored in the AndroidKeyStore.\n        This way the wallet password doesn't have to leave the process.\n        \"\"\"\n        wrap_key, iv = secrets.token_bytes(32), secrets.token_bytes(16)\n        wrapped_wallet_password = aes_encrypt_with_iv(\n            key=wrap_key,\n            iv=iv,\n            data=unified_wallet_password.encode('utf-8'),\n        )\n        encrypted_password_bundle = f\"{iv.hex()}:{wrapped_wallet_password.hex()}\"\n        self.config.WALLET_ANDROID_BIOMETRIC_AUTH_WRAPPED_WALLET_PASSWORD = encrypted_password_bundle\n        self._start_activity(BiometricAction.ENCRYPT, data=wrap_key.hex())\n\n    @pyqtSlot()\n    def disable(self):\n        self.config.WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION = False\n        self.config.WALLET_ANDROID_BIOMETRIC_AUTH_WRAPPED_WALLET_PASSWORD = ''\n        self.config.WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY = ''\n        self.isEnabledChanged.emit()\n        _logger.info(\"Android biometric authentication disabled\")\n\n    @pyqtSlot()\n    @auth_protect(method='wallet_password_only', reject='_disable_protected_failed')\n    def disableProtected(self):\n        \"\"\"\n        Exists to ensure the user knows the wallet password when manually disabling\n        biometric authentication. If they don't remember the password they can still do a seed\n        backup or transactions if biometrics stay enabled. However, note it is still possible for\n        biometrics to get disabled automatically on invalidation or error, so this cannot\n        fully protect the user from forgetting their wallet password either.\n        \"\"\"\n        self.disable()\n\n    def _disable_protected_failed(self):\n        self.isEnabledChanged.emit()\n\n    @pyqtSlot()\n    @pyqtSlot(str)\n    def unlock(self, auth_message: str = None):\n        \"\"\"\n        Called when the user needs to authenticate.\n        Makes the AndroidKeyStore decrypt our encrypted wrap key, we then use the decrypted wrap key\n        to decrypt the encrypted wallet password.\n        auth_message is shown in the system auth popup and defaults to 'Confirm your identity'.\n        \"\"\"\n        encrypted_wrap_key = self.config.WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY\n        assert encrypted_wrap_key, \"shouldn't unlock if biometric auth is disabled\"\n        self._start_activity(BiometricAction.DECRYPT, data=encrypted_wrap_key, auth_message=auth_message)\n\n    def _start_activity(self, action: BiometricAction, data: str, auth_message: str = None):\n        self._current_action = action\n\n        _logger.debug(f\"_start_activity: {action.value}, {len(data)=}\")\n        intent = jIntent(jPythonActivity, jBiometricActivity)\n        intent.putExtra(jString(\"action\"), jString(action.value))\n        intent.putExtra(jString(\"auth_message\"), jString(auth_message or _(\"Confirm your identity\")))\n        if action == BiometricAction.ENCRYPT:\n            intent.putExtra(jString(\"data\"), jString(data))  # wrap_key\n        elif action == BiometricAction.DECRYPT:\n            assert ':' in data, f\"malformed encrypted_bundle: {data=}\"\n            iv, encrypted_wrap_key = data.split(':')\n            intent.putExtra(jString(\"iv\"), jString(iv))\n            intent.putExtra(jString(\"data\"), jString(encrypted_wrap_key))\n        else:\n            raise ValueError(f\"unsupported {action=}\")\n\n        activity.bind(on_activity_result=self._on_activity_result)\n        jPythonActivity.startActivityForResult(intent, self.REQUEST_CODE_BIOMETRIC_ACTIVITY)\n\n    def _on_activity_result(self, requestCode: int, resultCode: int, intent):\n        if requestCode != self.REQUEST_CODE_BIOMETRIC_ACTIVITY:\n            return\n\n        action = self._current_action\n        self._current_action = None\n\n        try:\n            activity.unbind(on_activity_result=self._on_activity_result)\n            if resultCode == -1: # RESULT_OK\n                data = intent.getStringExtra(jString(\"data\"))\n                if action == BiometricAction.ENCRYPT:\n                    iv = intent.getStringExtra(jString(\"iv\"))\n                    encrypted_bundle = f\"{iv}:{data}\"\n                    self._on_wrap_key_encrypted(encrypted_bundle=encrypted_bundle)\n                else:\n                    self._on_wrap_key_decrypted(wrap_key=data)\n                return\n        except Exception as e:  # prevent exc from getting lost\n            send_exception_to_crash_reporter(e)\n\n        # on qml side we act on specific errors, so these error strings shouldn't be changed\n        if resultCode == self.RESULT_CODE_SETUP_FAILED and action == BiometricAction.DECRYPT:\n            # setup failed, we need to delete the biometry data, it cannot be decrypted anymore\n            _logger.debug(f\"biometric decryption failed, probably invalidated key\")\n            error = 'INVALIDATED'\n            self.disable()  # reset\n        elif resultCode == self.RESULT_CODE_POPUP_CANCELLED:  # user clicked cancel on auth popup\n            _logger.debug(f\"biometric auth cancelled by user\")\n            error = 'CANCELLED'\n        else:  # some other error\n            _logger.error(f\"biometric auth failed: {action=}, {resultCode=}\")\n            error = f\"{resultCode=}\"\n\n        if action == BiometricAction.DECRYPT:\n            self.unlockError.emit(error)\n        else:\n            self.disable()  # reset\n            self.enablingFailed.emit(error)\n\n    def _on_wrap_key_decrypted(self, *, wrap_key: str):\n        encrypted_password_bundle = self.config.WALLET_ANDROID_BIOMETRIC_AUTH_WRAPPED_WALLET_PASSWORD\n        assert encrypted_password_bundle and ':' in encrypted_password_bundle\n        iv, encrypted_password = encrypted_password_bundle.split(':')\n        decrypted_password = aes_decrypt_with_iv(\n            key=bytes.fromhex(wrap_key),\n            iv=bytes.fromhex(iv),\n            data=bytes.fromhex(encrypted_password),\n        )\n        self.unlockSuccess.emit(decrypted_password.decode('utf-8'))\n\n    def _on_wrap_key_encrypted(self, *, encrypted_bundle: str):\n        self.config.WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY = encrypted_bundle\n        self.config.WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION = True\n        self.isEnabledChanged.emit()\n"
  },
  {
    "path": "electrum/gui/qml/qebip39recovery.py",
    "content": "import asyncio\nimport concurrent\nfrom enum import IntEnum\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, pyqtEnum\nfrom PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex\n\nfrom electrum import Network, keystore\nfrom electrum.bip32 import BIP32Node\nfrom electrum.bip39_recovery import account_discovery\nfrom electrum.logging import get_logger\nfrom electrum.util import get_asyncio_loop\n\nfrom electrum.gui.common_qt.util import TaskThread\n\n\nclass QEBip39RecoveryListModel(QAbstractListModel):\n    _logger = get_logger(__name__)\n\n    @pyqtEnum\n    class State(IntEnum):\n        Idle = -1\n        Scanning = 0\n        Success = 1\n        Failed = 2\n        Cancelled = 3\n\n    recoveryFailed = pyqtSignal()\n    stateChanged = pyqtSignal()\n\n    # define listmodel rolemap\n    _ROLE_NAMES=('description', 'derivation_path', 'script_type')\n    _ROLE_KEYS = range(Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole + len(_ROLE_NAMES))\n    _ROLE_MAP  = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))\n\n    def __init__(self, config, parent=None):\n        super().__init__(parent)\n        self._accounts = []\n        self._thread = None\n        self._root_seed = None\n        self._state = QEBip39RecoveryListModel.State.Idle\n\n    def rowCount(self, index):\n        return len(self._accounts)\n\n    def roleNames(self):\n        return self._ROLE_MAP\n\n    def data(self, index, role):\n        account = self._accounts[index.row()]\n        role_index = role - Qt.ItemDataRole.UserRole\n        value = account[self._ROLE_NAMES[role_index]]\n        if isinstance(value, (bool, list, int, str)) or value is None:\n            return value\n        return str(value)\n\n    def clear(self):\n        self.beginResetModel()\n        self._accounts = []\n        self.endResetModel()\n\n    @pyqtProperty(int, notify=stateChanged)\n    def state(self):\n        return self._state\n\n    @state.setter\n    def state(self, state: State):\n        if state != self._state:\n            self._state = state\n            self.stateChanged.emit()\n\n    @pyqtSlot(str, str)\n    @pyqtSlot(str, str, str)\n    def startScan(self, wallet_type: str, seed: str, seed_extra_words: str = None):\n        if not seed or not wallet_type:\n            return\n\n        assert wallet_type == 'standard'\n\n        self._root_seed = keystore.bip39_to_seed(seed, passphrase=seed_extra_words)\n\n        self.clear()\n\n        self._thread = TaskThread(self)\n        network = Network.get_instance()\n        coro = account_discovery(network, self.get_account_xpub)\n        self.state = QEBip39RecoveryListModel.State.Scanning\n        fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop())\n        self._thread.add(\n            fut.result,\n            on_success=self.on_recovery_success,\n            on_error=self.on_recovery_error,\n            cancel=fut.cancel,\n        )\n\n    def addAccount(self, account):\n        self._logger.debug(f'addAccount {account!r}')\n        self.beginInsertRows(QModelIndex(), len(self._accounts), len(self._accounts))\n        self._accounts.append(account)\n        self.endInsertRows()\n\n    def on_recovery_success(self, accounts):\n        self.state = QEBip39RecoveryListModel.State.Success\n\n        for account in accounts:\n            self.addAccount(account)\n\n        self._thread.stop()\n\n    def on_recovery_error(self, exc_info):\n        e = exc_info[1]\n        if isinstance(e, concurrent.futures.CancelledError):\n            self.state = QEBip39RecoveryListModel.State.Cancelled\n            return\n        self._logger.error(f'recovery error', exc_info=exc_info)\n        self.state = QEBip39RecoveryListModel.State.Failed\n        self._thread.stop()\n\n    def get_account_xpub(self, account_path):\n        root_node = BIP32Node.from_rootseed(self._root_seed, xtype='standard')\n        account_node = root_node.subkey_at_private_derivation(account_path)\n        account_xpub = account_node.to_xpub()\n        return account_xpub\n"
  },
  {
    "path": "electrum/gui/qml/qebitcoin.py",
    "content": "import asyncio\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject\n\nfrom electrum import mnemonic\nfrom electrum import keystore\nfrom electrum.i18n import _\nfrom electrum.bip32 import is_bip32_derivation, xpub_type\nfrom electrum.logging import get_logger\nfrom electrum.util import get_asyncio_loop\nfrom electrum.transaction import tx_from_any\nfrom electrum.mnemonic import Mnemonic\nfrom electrum.old_mnemonic import wordlist as old_wordlist\nfrom electrum.bitcoin import is_address\n\n\nclass QEBitcoin(QObject):\n    _logger = get_logger(__name__)\n\n    generatedSeedChanged = pyqtSignal()\n    seedTypeChanged = pyqtSignal()\n    validationMessageChanged = pyqtSignal()\n\n    def __init__(self, config, parent=None):\n        super().__init__(parent)\n        self.config = config\n        self._seed_type = ''\n        self._generated_seed = ''\n        self._validationMessage = ''\n        self._words = None\n\n    @pyqtProperty(str, notify=generatedSeedChanged)\n    def generatedSeed(self):\n        return self._generated_seed\n\n    @pyqtProperty(str, notify=seedTypeChanged)\n    def seedType(self):\n        return self._seed_type\n\n    @pyqtProperty(str, notify=validationMessageChanged)\n    def validationMessage(self):\n        return self._validationMessage\n\n    @validationMessage.setter\n    def validationMessage(self, msg):\n        if self._validationMessage != msg:\n            self._validationMessage = msg\n            self.validationMessageChanged.emit()\n\n    @pyqtSlot()\n    @pyqtSlot(str)\n    @pyqtSlot(str, str)\n    def generateSeed(self, seed_type='segwit', language='en'):\n        self._logger.debug('generating seed of type ' + str(seed_type))\n\n        async def co_gen_seed(seed_type, language):\n            self._generated_seed = mnemonic.Mnemonic(language).make_seed(seed_type=seed_type)\n            self._logger.debug('seed generated')\n            self.generatedSeedChanged.emit()\n\n        asyncio.run_coroutine_threadsafe(co_gen_seed(seed_type, language), get_asyncio_loop())\n\n    @pyqtSlot(str, str, result=bool)\n    def verifyMasterKey(self, key, wallet_type='standard'):\n        self.validationMessage = ''\n        if not keystore.is_master_key(key):\n            self.validationMessage = _('Not a master key')\n            return False\n\n        k = keystore.from_master_key(key)\n        if wallet_type == 'standard':\n            if isinstance(k, keystore.Xpub):  # has bip32 xpub\n                t1 = xpub_type(k.xpub)\n                if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:  # disallow Ypub/Zpub\n                    self.validationMessage = '%s: %s' % (_('Wrong key type'), t1)\n                    return False\n            elif isinstance(k, keystore.Old_KeyStore):\n                pass\n            else:\n                self._logger.error(f\"unexpected keystore type: {type(keystore)}\")\n                return False\n        elif wallet_type == 'multisig':\n            if not isinstance(k, keystore.Xpub):  # old mpk?\n                self.validationMessage = '%s: %s' % (_('Wrong key type'), \"not bip32\")\n                return False\n            t1 = xpub_type(k.xpub)\n            if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']:  # disallow ypub/zpub\n                self.validationMessage = '%s: %s' % (_('Wrong key type'), t1)\n                return False\n        else:\n            self.validationMessage = '%s: %s' % (_('Unsupported wallet type'), wallet_type)\n            self._logger.error(f'Unsupported wallet type: {wallet_type}')\n            return False\n        # looks okay\n        return True\n\n    @pyqtSlot(str, result=bool)\n    def verifyDerivationPath(self, path):\n        return is_bip32_derivation(path)\n\n    @pyqtSlot(str, result=bool)\n    def isRawTx(self, rawtx):\n        try:\n            tx_from_any(rawtx)\n            return True\n        except Exception:\n            return False\n\n    @pyqtSlot(str, result=bool)\n    def isAddress(self, addr: str):\n        return is_address(addr)\n\n    @pyqtSlot(str, result=bool)\n    def isAddressList(self, csv: str):\n        return keystore.is_address_list(csv)\n\n    @pyqtSlot(str, result=bool)\n    def isPrivateKeyList(self, csv: str):\n        return keystore.is_private_key_list(csv)\n\n    @pyqtSlot(str, result='QVariantList')\n    def mnemonicsFor(self, fragment):\n        if not fragment:\n            return []\n        if not self._words:\n            self._words = set(Mnemonic('en').wordlist).union(set(old_wordlist))\n        return sorted(filter(lambda x: x.startswith(fragment), self._words))\n"
  },
  {
    "path": "electrum/gui/qml/qechanneldetails.py",
    "content": "import threading\nfrom enum import IntEnum\nfrom typing import Optional, TYPE_CHECKING\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, pyqtEnum\n\nfrom electrum.i18n import _\nfrom electrum.gui import messages\nfrom electrum.logging import get_logger\nfrom electrum.lnutil import LOCAL, REMOTE\nfrom electrum.lnchannel import ChanCloseOption, ChannelState, AbstractChannel, Channel, ChannelBackup\nfrom electrum.util import format_short_id, event_listener\n\nfrom electrum.gui.common_qt.util import QtEventListener\n\nfrom .auth import AuthMixin, auth_protect\nfrom .qewallet import QEWallet\nfrom .qetypes import QEAmount\n\nif TYPE_CHECKING:\n    from electrum.wallet import Abstract_Wallet\n\n\nclass QEChannelDetails(AuthMixin, QObject, QtEventListener):\n    _logger = get_logger(__name__)\n\n    @pyqtEnum\n    class State(IntEnum):  # subset, only ones we currently need in UI\n        Closed = ChannelState.CLOSED\n        Redeemed = ChannelState.REDEEMED\n\n    channelChanged = pyqtSignal()\n    channelCloseSuccess = pyqtSignal()\n    channelCloseFailed = pyqtSignal([str], arguments=['message'])\n    isClosingChanged = pyqtSignal()\n    trampolineFrozenInGossipMode = pyqtSignal()\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._wallet = None  # type: Optional[QEWallet]\n        self._channelid = None  # type: Optional[str]\n        self._channel = None  # type: Optional[AbstractChannel]\n\n        self._capacity = QEAmount()\n        self._local_capacity = QEAmount()\n        self._remote_capacity = QEAmount()\n        self._can_receive = QEAmount()\n        self._can_send = QEAmount()\n        self._is_closing = False\n\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n    @event_listener\n    def on_event_channel(self, wallet: 'Abstract_Wallet', channel: 'AbstractChannel'):\n        if wallet == self._wallet.wallet and self._channelid == channel.channel_id.hex():\n            self.channelChanged.emit()\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    walletChanged = pyqtSignal()\n    @pyqtProperty(QEWallet, notify=walletChanged)\n    def wallet(self) -> QEWallet:\n        return self._wallet\n\n    @wallet.setter\n    def wallet(self, wallet: QEWallet):\n        if self._wallet != wallet:\n            self._wallet = wallet\n            self.walletChanged.emit()\n\n    channelidChanged = pyqtSignal()\n    @pyqtProperty(str, notify=channelidChanged)\n    def channelid(self) -> str:\n        return self._channelid\n\n    @channelid.setter\n    def channelid(self, channelid: str):\n        if self._channelid != channelid:\n            self._channelid = channelid\n            if channelid:\n                self.load()\n            self.channelidChanged.emit()\n\n    def load(self):\n        lnchannels = self._wallet.wallet.lnworker.get_channel_objects()\n        for channel in lnchannels.values():\n            if self._channelid == channel.channel_id.hex():\n                self._channel = channel\n                self.channelChanged.emit()\n\n    @pyqtProperty(str, notify=channelChanged)\n    def name(self) -> str:\n        if not self._channel:\n            return ''\n        return self._wallet.wallet.lnworker.lnpeermgr.get_node_alias(self._channel.node_id) or ''\n\n    @pyqtProperty(str, notify=channelChanged)\n    def pubkey(self) -> str:\n        return self._channel.node_id.hex()\n\n    @pyqtProperty(str, notify=channelChanged)\n    def shortCid(self) -> str:\n        return self._channel.short_id_for_GUI()\n\n    @pyqtProperty(str, notify=channelChanged)\n    def localScidAlias(self) -> str:\n        lsa = self._channel.get_local_scid_alias()\n        return format_short_id(lsa) if lsa else ''\n\n    @pyqtProperty(str, notify=channelChanged)\n    def remoteScidAlias(self) -> str:\n        rsa = self._channel.get_remote_scid_alias()\n        return format_short_id(rsa) if rsa else ''\n\n    @pyqtProperty(str, notify=channelChanged)\n    def currentFeerate(self) -> str:\n        if self._channel.is_backup():\n            return ''\n        assert isinstance(self._channel, Channel)\n        return self._wallet.wallet.config.format_fee_rate(4 * self._channel.get_latest_feerate(LOCAL))\n\n    @pyqtProperty(str, notify=channelChanged)\n    def state(self) -> str:\n        return self._channel.get_state_for_GUI()\n\n    @pyqtProperty(int, notify=channelChanged)\n    def stateCode(self) -> ChannelState:\n        return self._channel.get_state()\n\n    @pyqtProperty(str, notify=channelChanged)\n    def initiator(self) -> str:\n        if self._channel.is_backup():\n            return ''\n        assert isinstance(self._channel, Channel)\n        return 'Local' if self._channel.constraints.is_initiator else 'Remote'\n\n    @pyqtProperty('QVariantMap', notify=channelChanged)\n    def fundingOutpoint(self) -> dict:\n        outpoint = self._channel.funding_outpoint\n        return {\n            'txid': outpoint.txid,\n            'index': outpoint.output_index\n        }\n\n    @pyqtProperty(str, notify=channelChanged)\n    def closingTxid(self) -> str:\n        if not self._channel.is_closed():\n            return ''\n        item = self._channel.get_closing_height()\n        if item:\n            closing_txid, closing_height, timestamp = item\n            return closing_txid\n        else:\n            return ''\n\n    @pyqtProperty(QEAmount, notify=channelChanged)\n    def capacity(self) -> QEAmount:\n        self._capacity.copyFrom(QEAmount(amount_sat=self._channel.get_capacity()))\n        return self._capacity\n\n    @pyqtProperty(QEAmount, notify=channelChanged)\n    def localCapacity(self) -> QEAmount:\n        if not self._channel.is_backup():\n            self._local_capacity.copyFrom(QEAmount(amount_msat=self._channel.balance(LOCAL)))\n        return self._local_capacity\n\n    @pyqtProperty(QEAmount, notify=channelChanged)\n    def remoteCapacity(self) -> QEAmount:\n        if not self._channel.is_backup():\n            self._remote_capacity.copyFrom(QEAmount(amount_msat=self._channel.balance(REMOTE)))\n        return self._remote_capacity\n\n    @pyqtProperty(QEAmount, notify=channelChanged)\n    def canSend(self) -> QEAmount:\n        if not self._channel.is_backup():\n            self._can_send.copyFrom(QEAmount(amount_msat=self._channel.available_to_spend(LOCAL)))\n        return self._can_send\n\n    @pyqtProperty(QEAmount, notify=channelChanged)\n    def canReceive(self) -> QEAmount:\n        if not self._channel.is_backup():\n            self._can_receive.copyFrom(QEAmount(amount_msat=self._channel.available_to_spend(REMOTE)))\n        return self._can_receive\n\n    @pyqtProperty(bool, notify=channelChanged)\n    def frozenForSending(self) -> bool:\n        return self._channel.is_frozen_for_sending()\n\n    @pyqtProperty(bool, notify=channelChanged)\n    def frozenForReceiving(self) -> bool:\n        return self._channel.is_frozen_for_receiving()\n\n    @pyqtProperty(str, notify=channelChanged)\n    def channelType(self) -> str:\n        return self._channel.storage['channel_type'].name_minimal if 'channel_type' in self._channel.storage else 'Channel Backup'\n\n    @pyqtProperty(bool, notify=channelChanged)\n    def isOpen(self) -> bool:\n        return self._channel.is_open()\n\n    @pyqtProperty(bool, notify=channelChanged)\n    def canClose(self) -> bool:\n        return self.canCoopClose or self.canLocalForceClose or self.canRequestForceClose\n\n    @pyqtProperty(bool, notify=channelChanged)\n    def canCoopClose(self) -> bool:\n        return ChanCloseOption.COOP_CLOSE in self._channel.get_close_options()\n\n    @pyqtProperty(bool, notify=channelChanged)\n    def canLocalForceClose(self) -> bool:\n        return ChanCloseOption.LOCAL_FCLOSE in self._channel.get_close_options()\n\n    @pyqtProperty(bool, notify=channelChanged)\n    def canRequestForceClose(self) -> bool:\n        return ChanCloseOption.REQUEST_REMOTE_FCLOSE in self._channel.get_close_options()\n\n    @pyqtProperty(bool, notify=channelChanged)\n    def canDelete(self) -> bool:\n        return self._channel.can_be_deleted()\n\n    @pyqtProperty(str, notify=channelChanged)\n    def messageForceClose(self) -> str:\n        return messages.MSG_REQUEST_FORCE_CLOSE.strip()\n\n    @pyqtProperty(str, notify=channelChanged)\n    def messageForceCloseBackup(self):\n        return ' '.join([\n            _('If you force-close this channel, the funds you have in it will not be available for {} blocks.').format(self.toSelfDelay),\n            _('During that time, funds will not be recoverable from your seed, and may be lost if you lose your device.'),\n            _('To prevent that, please save this channel backup.'),\n            _('It may be imported in another wallet with the same seed.')\n        ])\n\n    @pyqtProperty(bool, notify=channelChanged)\n    def isBackup(self):\n        return self._channel.is_backup()\n\n    @pyqtProperty(str, notify=channelChanged)\n    def backupType(self):\n        if not self.isBackup:\n            return ''\n        assert isinstance(self._channel, ChannelBackup)\n        return 'imported' if self._channel.is_imported else 'on-chain'\n\n    @pyqtProperty(int, notify=channelChanged)\n    def toSelfDelay(self):\n        return self._channel.config[REMOTE].to_self_delay\n\n    @pyqtProperty(bool, notify=isClosingChanged)\n    def isClosing(self):\n        # Note: isClosing only applies to a closing action started by this instance, not\n        # whether the channel is closing\n        return self._is_closing\n\n    @pyqtSlot()\n    def freezeForSending(self):\n        assert isinstance(self._channel, Channel)\n        lnworker = self._channel.lnworker\n        if lnworker.channel_db or lnworker.is_trampoline_peer(self._channel.node_id):\n            self._channel.set_frozen_for_sending(not self.frozenForSending)\n            self.channelChanged.emit()\n        else:\n            self._logger.debug(messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP)\n            self.trampolineFrozenInGossipMode.emit()\n\n    @pyqtSlot()\n    def freezeForReceiving(self):\n        assert isinstance(self._channel, Channel)\n        lnworker = self._channel.lnworker\n        if lnworker.channel_db or lnworker.is_trampoline_peer(self._channel.node_id):\n            self._channel.set_frozen_for_receiving(not self.frozenForReceiving)\n            self.channelChanged.emit()\n        else:\n            self._logger.debug(messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP)\n\n    @pyqtSlot(str)\n    def closeChannel(self, closetype):\n        self.do_close_channel(closetype)\n\n    @auth_protect(message=_('Close Lightning channel?'))\n    def do_close_channel(self, closetype: str):\n        channel_id = self._channel.channel_id\n\n        def handle_result(success: bool, msg: str = ''):\n            try:\n                if success:\n                    self.channelCloseSuccess.emit()\n                else:\n                    self.channelCloseFailed.emit(msg)\n\n                self._is_closing = False\n                self.isClosingChanged.emit()\n            except RuntimeError:  # QEChannelDetails might be deleted at this point if the user closed the dialog.\n                pass\n\n        def do_close():\n            try:\n                self._is_closing = True\n                self.isClosingChanged.emit()\n                if closetype == 'remote_force':\n                    self._wallet.wallet.network.run_from_another_thread(self._wallet.wallet.lnworker.request_force_close(channel_id))\n                elif closetype == 'local_force':\n                    self._wallet.wallet.network.run_from_another_thread(self._wallet.wallet.lnworker.force_close_channel(channel_id))\n                else:\n                    self._wallet.wallet.network.run_from_another_thread(self._wallet.wallet.lnworker.close_channel(channel_id))\n                self._logger.debug('Channel close successful')\n                handle_result(True)\n            except Exception as e:\n                self._logger.exception(\"Could not close channel: \" + repr(e))\n                handle_result(False, _('Could not close channel: ') + repr(e))\n\n        threading.Thread(target=do_close, daemon=True).start()\n\n    @pyqtSlot()\n    def deleteChannel(self):\n        if self.isBackup:\n            self._wallet.wallet.lnworker.remove_channel_backup(self._channel.channel_id)\n        else:\n            self._wallet.wallet.lnworker.remove_channel(self._channel.channel_id)\n\n    @pyqtSlot(result=str)\n    def channelBackup(self):\n        return self._wallet.wallet.lnworker.export_channel_backup(self._channel.channel_id)\n\n    @pyqtSlot(result=str)\n    def channelBackupHelpText(self):\n        return messages.MSG_LN_EXPLAIN_SCB_BACKUPS\n"
  },
  {
    "path": "electrum/gui/qml/qechannellistmodel.py",
    "content": "from PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot\n\nfrom electrum.lnchannel import ChannelState\nfrom electrum.lnutil import LOCAL, REMOTE\nfrom electrum.logging import get_logger\nfrom electrum.util import Satoshis\nfrom electrum.gui import messages\n\nfrom electrum.gui.common_qt.util import qt_event_listener, QtEventListener\n\nfrom .qetypes import QEAmount\nfrom .qemodelfilter import QEFilterProxyModel\n\n\nclass QEChannelListModel(QAbstractListModel, QtEventListener):\n    _logger = get_logger(__name__)\n\n    # define listmodel rolemap\n    _ROLE_NAMES=('cid', 'state', 'state_code', 'initiator', 'capacity', 'can_send',\n                 'can_receive', 'l_csv_delay', 'r_csv_delay', 'send_frozen', 'receive_frozen',\n                 'type', 'node_id', 'node_alias', 'short_cid', 'funding_tx', 'is_trampoline',\n                 'is_backup', 'is_imported', 'local_capacity', 'remote_capacity')\n    _ROLE_KEYS = range(Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole + len(_ROLE_NAMES))\n    _ROLE_MAP  = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))\n    _ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS))\n\n    _network_signal = pyqtSignal(str, object)\n\n    def __init__(self, wallet, parent=None):\n        super().__init__(parent)\n        self.wallet = wallet\n        self._channels = []\n\n        self._fm_backups = None\n        self._fm_nobackups = None\n\n        self.initModel()\n\n        # To avoid leaking references to \"self\" that prevent the\n        # window from being GC-ed when closed, callbacks should be\n        # methods of this class only, and specifically not be\n        # partials, lambdas or methods of subobjects.  Hence...\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n    @qt_event_listener\n    def on_event_channel(self, wallet, channel):\n        if wallet == self.wallet:\n            self.on_channel_updated(channel)\n\n    @qt_event_listener\n    def on_event_channels_updated(self, wallet):\n        if wallet == self.wallet:\n            self.initModel()\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    def rowCount(self, index):\n        return len(self._channels)\n\n    # also expose rowCount as a property\n    countChanged = pyqtSignal()\n    @pyqtProperty(int, notify=countChanged)\n    def count(self):\n        return len(self._channels)\n\n    def roleNames(self):\n        return self._ROLE_MAP\n\n    def data(self, index, role):\n        tx = self._channels[index.row()]\n        role_index = role - Qt.ItemDataRole.UserRole\n        value = tx[self._ROLE_NAMES[role_index]]\n        if isinstance(value, (bool, list, int, str, QEAmount)) or value is None:\n            return value\n        if isinstance(value, Satoshis):\n            return value.value\n        return str(value)\n\n    def clear(self):\n        self.beginResetModel()\n        self._channels = []\n        self.endResetModel()\n\n    def channel_to_model(self, lnc):\n        lnworker = self.wallet.lnworker\n        item = {\n            'cid': lnc.channel_id.hex(),\n            'node_id': lnc.node_id.hex(),\n            'node_alias': lnworker.lnpeermgr.get_node_alias(lnc.node_id) or '',\n            'short_cid': lnc.short_id_for_GUI(),\n            'state': lnc.get_state_for_GUI(),\n            'state_code': int(lnc.get_state()),\n            'is_backup': lnc.is_backup(),\n            'is_trampoline': lnworker.is_trampoline_peer(lnc.node_id),\n            'capacity': QEAmount(amount_sat=lnc.get_capacity())\n        }\n        if lnc.is_backup():\n            item['can_send'] = QEAmount()\n            item['can_receive'] = QEAmount()\n            item['local_capacity'] = QEAmount()\n            item['remote_capacity'] = QEAmount()\n            item['send_frozen'] = True\n            item['receive_frozen'] = True\n            item['is_imported'] = lnc.is_imported\n        else:\n            item['can_send'] = QEAmount(amount_msat=lnc.available_to_spend(LOCAL))\n            item['can_receive'] = QEAmount(amount_msat=lnc.available_to_spend(REMOTE))\n            item['local_capacity'] = QEAmount(amount_msat=lnc.balance(LOCAL))\n            item['remote_capacity'] = QEAmount(amount_msat=lnc.balance(REMOTE))\n            item['send_frozen'] = lnc.is_frozen_for_sending()\n            item['receive_frozen'] = lnc.is_frozen_for_receiving()\n            item['is_imported'] = False\n        return item\n\n    numOpenChannelsChanged = pyqtSignal()\n    @pyqtProperty(int, notify=numOpenChannelsChanged)\n    def numOpenChannels(self):\n        return sum([1 if x['state_code'] == ChannelState.OPEN else 0 for x in self._channels])\n\n    @pyqtSlot()\n    def initModel(self):\n        self._logger.debug('init_model')\n        if not self.wallet.lnworker:\n            self._logger.warning('lnworker should be defined')\n            return\n\n        channels = []\n\n        lnchannels = self.wallet.lnworker.get_channel_objects()\n        for channel in lnchannels.values():\n            item = self.channel_to_model(channel)\n            channels.append(item)\n\n        # sort, for now simply by state\n        def chan_sort_score(c):\n            return c['state_code'] + (10 if c['is_backup'] else 0)\n        channels.sort(key=chan_sort_score)\n\n        self.clear()\n        self.beginInsertRows(QModelIndex(), 0, len(channels) - 1)\n        self._channels = channels\n        self.endInsertRows()\n\n        self.countChanged.emit()\n\n    def on_channel_updated(self, channel):\n        for i, c in enumerate(self._channels):\n            if c['cid'] == channel.channel_id.hex():\n                self.do_update(i, channel)\n                break\n\n    def do_update(self, modelindex, channel):\n        self._logger.debug(f'updating our channel {channel.short_id_for_GUI()}')\n        modelitem = self._channels[modelindex]\n        modelitem.update(self.channel_to_model(channel))\n\n        mi = self.createIndex(modelindex, 0)\n        self.dataChanged.emit(mi, mi, self._ROLE_KEYS)\n        self.numOpenChannelsChanged.emit()\n\n    @pyqtSlot(str)\n    def newChannel(self, cid):\n        self._logger.debug('new channel with cid %s' % cid)\n        lnchannels = self.wallet.lnworker.channels\n        for channel in lnchannels.values():\n            if cid == channel.channel_id.hex():\n                item = self.channel_to_model(channel)\n                self._logger.debug(item)\n                self.beginInsertRows(QModelIndex(), 0, 0)\n                self._channels.insert(0, item)\n                self.endInsertRows()\n                self.countChanged.emit()\n                return\n\n    @pyqtSlot(str)\n    def removeChannel(self, cid):\n        self._logger.debug('remove channel with cid %s' % cid)\n        for i, channel in enumerate(self._channels):\n            if cid == channel['cid']:\n                self._logger.debug(cid)\n                self.beginRemoveRows(QModelIndex(), i, i)\n                self._channels.remove(channel)\n                self.endRemoveRows()\n                self.countChanged.emit()\n                return\n\n    def filterModel(self, role, match):\n        _filterModel = QEFilterProxyModel(self, self)\n        assert role in self._ROLE_RMAP\n        _filterModel.setFilterRole(self._ROLE_RMAP[role])\n        _filterModel.setFilterValue(match)\n        return _filterModel\n\n    @pyqtSlot(result=QEFilterProxyModel)\n    def filterModelBackups(self):\n        self._fm_backups = self.filterModel('is_backup', True)\n        return self._fm_backups\n\n    @pyqtSlot(result=QEFilterProxyModel)\n    def filterModelNoBackups(self):\n        self._fm_nobackups = self.filterModel('is_backup', False)\n        return self._fm_nobackups\n\n    @pyqtSlot(result=str)\n    def lightningWarningMessage(self):\n        return messages.MSG_LIGHTNING_WARNING\n"
  },
  {
    "path": "electrum/gui/qml/qechannelopener.py",
    "content": "import threading\nfrom concurrent.futures import CancelledError\nfrom asyncio.exceptions import TimeoutError\nfrom typing import Optional\nimport electrum_ecc as ecc\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject\n\nfrom electrum.i18n import _\nfrom electrum.gui import messages\nfrom electrum.util import bfh\nfrom electrum.lnutil import MIN_FUNDING_SAT\nfrom electrum.lntransport import extract_nodeid, ConnStringFormatError\nfrom electrum.bitcoin import DummyAddress\nfrom electrum.lnworker import hardcoded_trampoline_nodes\nfrom electrum.logging import get_logger\nfrom electrum.fee_policy import FeePolicy\nfrom electrum.transaction import PartialTransaction\n\nfrom .auth import AuthMixin, auth_protect\nfrom .qetxfinalizer import QETxFinalizer\nfrom .qetxdetails import QETxDetails\nfrom .qetypes import QEAmount\nfrom .qewallet import QEWallet\n\n\nclass QEChannelOpener(QObject, AuthMixin):\n    _logger = get_logger(__name__)\n\n    validationError = pyqtSignal([str, str], arguments=['code', 'message'])\n    conflictingBackup = pyqtSignal([str], arguments=['message'])\n    channelOpening = pyqtSignal([str], arguments=['peer'])\n    channelOpenError = pyqtSignal([str], arguments=['message'])\n    channelOpenSuccess = pyqtSignal([str, bool, int, bool],\n                                    arguments=['cid', 'has_onchain_backup', 'min_depth', 'tx_complete'])\n\n    dataChanged = pyqtSignal()  # generic notify signal\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._wallet = None  # type: Optional[QEWallet]\n        self._connect_str = None\n        self._amount = QEAmount()\n        self._valid = False\n        self._opentx = None\n        self._txdetails = None\n        self._warning = ''\n        self._determine_max_message = None\n\n        self._finalizer = None\n        self._node_pubkey = None\n        self._connect_str_resolved = None\n\n        self._updating_max = False\n\n    walletChanged = pyqtSignal()\n    @pyqtProperty(QEWallet, notify=walletChanged)\n    def wallet(self):\n        return self._wallet\n\n    @wallet.setter\n    def wallet(self, wallet: QEWallet):\n        if self._wallet != wallet:\n            self._wallet = wallet\n            self.walletChanged.emit()\n\n    connectStrChanged = pyqtSignal()\n    @pyqtProperty(str, notify=connectStrChanged)\n    def connectStr(self):\n        return self._connect_str\n\n    @connectStr.setter\n    def connectStr(self, connect_str: str):\n        if self._connect_str != connect_str:\n            self._logger.debug('connectStr set -> %s' % connect_str)\n            self._connect_str = connect_str\n            self.connectStrChanged.emit()\n            self.validate()\n\n    amountChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=amountChanged)\n    def amount(self):\n        return self._amount\n\n    @amount.setter\n    def amount(self, amount: QEAmount):\n        if self._amount != amount:\n            self._amount.copyFrom(amount)\n            self.amountChanged.emit()\n            self.validate()\n\n    validChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=validChanged)\n    def valid(self):\n        return self._valid\n\n    def setValid(self, is_valid):\n        if self._valid != is_valid:\n            self._valid = is_valid\n            self.validChanged.emit()\n\n    warningChanged = pyqtSignal()\n    @pyqtProperty(str, notify=warningChanged)\n    def warning(self):\n        return self._warning\n\n    def setWarning(self, warning):\n        if self._warning != warning:\n            self._warning = warning\n            self.warningChanged.emit()\n\n    finalizerChanged = pyqtSignal()\n    @pyqtProperty(QETxFinalizer, notify=finalizerChanged)\n    def finalizer(self):\n        return self._finalizer\n\n    txDetailsChanged = pyqtSignal()\n    @pyqtProperty(QETxDetails, notify=txDetailsChanged)\n    def txDetails(self):\n        return self._txdetails\n\n    @pyqtProperty(list, notify=dataChanged)\n    def trampolineNodeNames(self):\n        return list(hardcoded_trampoline_nodes().keys())\n\n    # FIXME have requested funding amount\n    def validate(self):\n        \"\"\"side-effects: sets self._node_pubkey, self._connect_str_resolved\"\"\"\n        connect_str_valid = False\n        if self._connect_str:\n            self._logger.debug(f'checking if {self._connect_str=!r} is valid')\n            if not self._wallet.wallet.config.LIGHTNING_USE_GOSSIP:\n                # using trampoline: connect_str is the name of a trampoline node\n                peer_addr = hardcoded_trampoline_nodes()[self._connect_str]\n                self._node_pubkey = peer_addr.pubkey\n                self._connect_str_resolved = str(peer_addr)\n                connect_str_valid = True\n            else:\n                # using gossip: connect_str is anything extract_nodeid() can parse\n                try:\n                    self._node_pubkey, _rest = extract_nodeid(self._connect_str)\n                except ConnStringFormatError:\n                    pass\n                else:\n                    self._connect_str_resolved = self._connect_str\n                    connect_str_valid = True\n\n        self.setWarning('')\n\n        if not connect_str_valid:\n            self.setValid(False)\n            return\n\n        self._logger.debug(f'amount={self._amount}')\n        if not self._amount or not (self._amount.satsInt > 0 or self._amount.isMax):\n            self.setValid(False)\n            return\n\n        # for MAX, estimate is assumed to be calculated and set in self._amount.satsInt\n        if self._amount.satsInt < MIN_FUNDING_SAT:\n            message = _('Minimum required amount: {}').format(\n                self._wallet.wallet.config.format_amount_and_units(MIN_FUNDING_SAT)\n            )\n            if self._amount.isMax and self._determine_max_message:\n                message += '\\n' + self._determine_max_message\n            self.setWarning(message)\n            self.setValid(False)\n            return\n\n        if self._amount.satsInt > self._wallet.wallet.config.LIGHTNING_MAX_FUNDING_SAT:\n            self.setWarning(_('Amount is above maximum channel size: {}').format(\n                self._wallet.wallet.config.format_amount_and_units(self._wallet.wallet.config.LIGHTNING_MAX_FUNDING_SAT)\n            ))\n            self.setValid(False)\n            return\n\n        self.setValid(True)\n\n    @pyqtSlot(str, result=bool)\n    def validateConnectString(self, connect_str):\n        try:\n            extract_nodeid(connect_str)\n        except ConnStringFormatError as e:\n            self._logger.debug(f'invalid connect_str. {e!r}')\n            return False\n        return True\n\n    # FIXME \"max\" button in amount_dialog should enforce LIGHTNING_MAX_FUNDING_SAT\n    @pyqtSlot()\n    @pyqtSlot(bool)\n    def openChannel(self, confirm_backup_conflict=False):\n        if not self.valid:\n            return\n\n        self._logger.debug(f'Connect String: {self._connect_str!r}')\n\n        lnworker = self._wallet.wallet.lnworker\n        if lnworker.has_conflicting_backup_with(self._node_pubkey) and not confirm_backup_conflict:\n            self.conflictingBackup.emit(messages.MSG_CONFLICTING_BACKUP_INSTANCE)\n            return\n\n        amount = '!' if self._amount.isMax else self._amount.satsInt\n        self._logger.debug('amount = %s' % str(amount))\n\n        coins = self._wallet.wallet.get_spendable_coins(None, nonlocal_only=True)\n\n        mktx = lambda amt, fee_policy: lnworker.mktx_for_open_channel(\n            coins=coins,\n            funding_sat=amt,\n            node_id=self._node_pubkey,\n            fee_policy=fee_policy)\n\n        acpt = lambda tx: self.do_open_channel(tx, self._connect_str_resolved, self._wallet.password)\n\n        self._finalizer = QETxFinalizer(self, make_tx=mktx, accept=acpt)\n        self._finalizer.canRbf = False\n        self._finalizer.amount = self._amount\n        self._finalizer.wallet = self._wallet\n        self.finalizerChanged.emit()\n\n    @auth_protect(message=_('Open Lightning channel?'))\n    def do_open_channel(self, funding_tx: PartialTransaction, conn_str, password):\n        \"\"\"\n        conn_str: a connection string that extract_nodeid can parse, i.e. cannot be a trampoline name\n        \"\"\"\n        self._logger.debug('opening channel')\n        # read funding_sat from tx; converts '!' to int value\n        funding_sat = funding_tx.output_value_for_address(DummyAddress.CHANNEL)\n        lnworker = self._wallet.wallet.lnworker\n\n        def open_thread():\n            error = None\n            try:\n                chan, _funding_tx = lnworker.open_channel(\n                    connect_str=conn_str,\n                    funding_tx=funding_tx,\n                    funding_sat=funding_sat,\n                    push_amt_sat=0,\n                    password=password)\n                self._logger.debug('opening channel succeeded')\n                self.channelOpenSuccess.emit(chan.channel_id.hex(), chan.has_onchain_backup(),\n                                             chan.constraints.funding_txn_minimum_depth, funding_tx.is_complete())\n\n                # TODO: handle incomplete TX\n                # if not funding_tx.is_complete():\n                #     self._txdetails = QETxDetails(self)\n                #     self._txdetails.rawTx = funding_tx\n                #     self._txdetails.wallet = self._wallet\n                #     self.txDetailsChanged.emit()\n\n            except (CancelledError, TimeoutError):\n                error = _('Could not connect to channel peer')\n            except Exception as e:\n                error = str(e)\n                if not error:\n                    error = repr(e)\n            finally:\n                if error:\n                    self._logger.exception(\"Problem opening channel: %s\", error)\n                    self.channelOpenError.emit(error)\n\n        self._logger.debug('starting open thread')\n        self.channelOpening.emit(conn_str)\n        threading.Thread(target=open_thread, daemon=True).start()\n\n    @pyqtSlot(str, result=str)\n    def channelBackup(self, cid):\n        return self._wallet.wallet.lnworker.export_channel_backup(bfh(cid))\n\n    @pyqtSlot()\n    def updateMaxAmount(self):\n        if self._updating_max:\n            return\n\n        self._updating_max = True\n\n        def calc_max():\n            try:\n                coins = self._wallet.wallet.get_spendable_coins(None, nonlocal_only=True)\n                dummy_nodeid = ecc.GENERATOR.get_public_key_bytes(compressed=True)\n                make_tx = lambda fee_policy: self._wallet.wallet.lnworker.mktx_for_open_channel(\n                    coins=coins,\n                    funding_sat='!',\n                    node_id=dummy_nodeid,\n                    fee_policy=fee_policy)\n\n                amount, self._determine_max_message = self._wallet.determine_max(mktx=make_tx)\n                self._amount.satsInt = amount if amount else 0\n            finally:\n                self._updating_max = False\n                self.validate()\n\n        threading.Thread(target=calc_max, daemon=True).start()\n"
  },
  {
    "path": "electrum/gui/qml/qeconfig.py",
    "content": "import copy\nfrom decimal import Decimal\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRegularExpression\n\nfrom electrum.bitcoin import TOTAL_COIN_SUPPLY_LIMIT_IN_BTC\nfrom electrum.i18n import set_language, get_gui_lang_names\nfrom electrum.logging import get_logger\nfrom electrum.util import base_unit_name_to_decimal_point\nfrom electrum.gui import messages\n\nfrom .qetypes import QEAmount\nfrom .auth import AuthMixin, auth_protect\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n\n\nclass QEConfig(AuthMixin, QObject):\n    instance = None  # type: Optional[QEConfig]\n    _logger = get_logger(__name__)\n\n    def __init__(self, config: 'SimpleConfig', parent=None):\n        super().__init__(parent)\n        if QEConfig.instance:\n            raise RuntimeError('There should only be one QEConfig instance')\n        QEConfig.instance = self\n        self.config = config\n\n    @pyqtSlot(str, result=str)\n    def shortDescFor(self, key) -> str:\n        cv = getattr(self.config.cv, key)\n        return cv.get_short_desc() if cv else ''\n\n    @pyqtSlot(str, result=str)\n    def longDescFor(self, key) -> str:\n        cv = getattr(self.config.cv, key)\n        if not cv:\n            return \"\"\n        desc = cv.get_long_desc()\n        return messages.to_rtf(desc)\n\n    @pyqtSlot(str, result=str)\n    def getTranslatedMessage(self, key) -> str:\n        return getattr(messages, key)\n\n    languageChanged = pyqtSignal()\n    @pyqtProperty(str, notify=languageChanged)\n    def language(self):\n        return self.config.LOCALIZATION_LANGUAGE\n\n    @language.setter\n    def language(self, language):\n        if language not in get_gui_lang_names():\n            return\n        if self.config.LOCALIZATION_LANGUAGE != language:\n            self.config.LOCALIZATION_LANGUAGE = language\n            set_language(language)\n            self.languageChanged.emit()\n\n    languagesChanged = pyqtSignal()\n    @pyqtProperty('QVariantList', notify=languagesChanged)\n    def languagesAvailable(self):\n        langs = get_gui_lang_names()\n        langs_list = list(map(lambda x: {'value': x[0], 'text': x[1]}, langs.items()))\n        return langs_list\n\n    termsOfUseChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=termsOfUseChanged)\n    def termsOfUseAccepted(self) -> bool:\n        return self.config.TERMS_OF_USE_ACCEPTED >= messages.TERMS_OF_USE_LATEST_VERSION\n\n    @termsOfUseAccepted.setter\n    def termsOfUseAccepted(self, accepted: bool) -> None:\n        if accepted:\n            self.config.TERMS_OF_USE_ACCEPTED = messages.TERMS_OF_USE_LATEST_VERSION\n        else:\n            self.config.TERMS_OF_USE_ACCEPTED = 0\n        self.termsOfUseChanged.emit()\n\n    baseUnitChanged = pyqtSignal()\n    @pyqtProperty(str, notify=baseUnitChanged)\n    def baseUnit(self):\n        return self.config.get_base_unit()\n\n    @baseUnit.setter\n    def baseUnit(self, unit):\n        self.config.set_base_unit(unit)\n        self.baseUnitChanged.emit()\n\n    @pyqtProperty('QRegularExpression', notify=baseUnitChanged)\n    def btcAmountRegex(self):\n        return self._btcAmountRegex()\n\n    @pyqtProperty('QRegularExpression', notify=baseUnitChanged)\n    def btcAmountRegexMsat(self):\n        return self._btcAmountRegex(3)\n\n    def _btcAmountRegex(self, extra_precision: int = 0):\n        decimal_point = base_unit_name_to_decimal_point(self.config.get_base_unit())\n        max_digits_before_dp = (\n            len(str(TOTAL_COIN_SUPPLY_LIMIT_IN_BTC))\n            + (base_unit_name_to_decimal_point(\"BTC\") - decimal_point))\n        exp = '^[0-9]{0,%d}' % max_digits_before_dp\n        decimal_point += extra_precision\n        if decimal_point > 0:\n            exp += '(\\\\.[0-9]{0,%d})?' % decimal_point\n        exp += '$'\n        return QRegularExpression(exp)\n\n    thousandsSeparatorChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=thousandsSeparatorChanged)\n    def thousandsSeparator(self):\n        return self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP\n\n    @thousandsSeparator.setter\n    def thousandsSeparator(self, checked):\n        self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP = checked\n        self.config.amt_add_thousands_sep = checked\n        self.thousandsSeparatorChanged.emit()\n\n    spendUnconfirmedChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=spendUnconfirmedChanged)\n    def spendUnconfirmed(self):\n        return not self.config.WALLET_SPEND_CONFIRMED_ONLY\n\n    @spendUnconfirmed.setter\n    def spendUnconfirmed(self, checked):\n        self.config.WALLET_SPEND_CONFIRMED_ONLY = not checked\n        self.spendUnconfirmedChanged.emit()\n\n    freezeReusedAddressUtxosChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=freezeReusedAddressUtxosChanged)\n    def freezeReusedAddressUtxos(self):\n        return self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS\n\n    @freezeReusedAddressUtxos.setter\n    def freezeReusedAddressUtxos(self, checked):\n        self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS = checked\n        self.freezeReusedAddressUtxosChanged.emit()\n\n    requestExpiryChanged = pyqtSignal()\n    @pyqtProperty(int, notify=requestExpiryChanged)\n    def requestExpiry(self):\n        return self.config.WALLET_PAYREQ_EXPIRY_SECONDS\n\n    @requestExpiry.setter\n    def requestExpiry(self, expiry):\n        self.config.WALLET_PAYREQ_EXPIRY_SECONDS = expiry\n        self.requestExpiryChanged.emit()\n\n    paymentAuthenticationChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=paymentAuthenticationChanged)\n    def paymentAuthentication(self):\n        return self.config.GUI_QML_PAYMENT_AUTHENTICATION\n\n    @paymentAuthentication.setter\n    def paymentAuthentication(self, enabled: bool):\n        if enabled:\n            self.config.GUI_QML_PAYMENT_AUTHENTICATION = True\n            self.paymentAuthenticationChanged.emit()\n        else:\n            self._disable_payment_authentication()\n\n    @auth_protect(method='wallet', reject='_payment_auth_reject')\n    def _disable_payment_authentication(self):\n        self.config.GUI_QML_PAYMENT_AUTHENTICATION = False\n        self.paymentAuthenticationChanged.emit()\n\n    def _payment_auth_reject(self):\n        self.paymentAuthenticationChanged.emit()\n\n    useGossipChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=useGossipChanged)\n    def useGossip(self):\n        return self.config.LIGHTNING_USE_GOSSIP\n\n    @useGossip.setter\n    def useGossip(self, gossip):\n        self.config.LIGHTNING_USE_GOSSIP = gossip\n        self.useGossipChanged.emit()\n\n    enableDebugLogsChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=enableDebugLogsChanged)\n    def enableDebugLogs(self):\n        gui_setting = self.config.GUI_ENABLE_DEBUG_LOGS\n        return gui_setting or bool(self.config.get('verbosity'))\n\n    @pyqtProperty(bool, notify=enableDebugLogsChanged)\n    def canToggleDebugLogs(self):\n        gui_setting = self.config.GUI_ENABLE_DEBUG_LOGS\n        return not self.config.get('verbosity') or gui_setting\n\n    @enableDebugLogs.setter\n    def enableDebugLogs(self, enable):\n        self.config.GUI_ENABLE_DEBUG_LOGS = enable\n        self.enableDebugLogsChanged.emit()\n\n    alwaysAllowScreenshotsChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=alwaysAllowScreenshotsChanged)\n    def alwaysAllowScreenshots(self):\n        return self.config.GUI_QML_ALWAYS_ALLOW_SCREENSHOTS\n\n    @alwaysAllowScreenshots.setter\n    def alwaysAllowScreenshots(self, enable):\n        self.config.GUI_QML_ALWAYS_ALLOW_SCREENSHOTS = enable\n        self.alwaysAllowScreenshotsChanged.emit()\n\n    setMaxBrightnessOnQrDisplayChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=setMaxBrightnessOnQrDisplayChanged)\n    def setMaxBrightnessOnQrDisplay(self):\n        return self.config.GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY\n\n    @setMaxBrightnessOnQrDisplay.setter\n    def setMaxBrightnessOnQrDisplay(self, enable):\n        self.config.GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY = enable\n\n    useRecoverableChannelsChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=useRecoverableChannelsChanged)\n    def useRecoverableChannels(self):\n        return self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS\n\n    @useRecoverableChannels.setter\n    def useRecoverableChannels(self, useRecoverableChannels):\n        self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS = useRecoverableChannels\n        self.useRecoverableChannelsChanged.emit()\n\n    trustedcoinPrepayChanged = pyqtSignal()\n    @pyqtProperty(int, notify=trustedcoinPrepayChanged)\n    def trustedcoinPrepay(self):\n        return self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY\n\n    @trustedcoinPrepay.setter\n    def trustedcoinPrepay(self, num_prepay):\n        if num_prepay != self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY:\n            self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY = num_prepay\n            self.trustedcoinPrepayChanged.emit()\n\n    preferredRequestTypeChanged = pyqtSignal()\n    @pyqtProperty(str, notify=preferredRequestTypeChanged)\n    def preferredRequestType(self):\n        return self.config.GUI_QML_PREFERRED_REQUEST_TYPE\n\n    @preferredRequestType.setter\n    def preferredRequestType(self, preferred_request_type):\n        if preferred_request_type != self.config.GUI_QML_PREFERRED_REQUEST_TYPE:\n            self.config.GUI_QML_PREFERRED_REQUEST_TYPE = preferred_request_type\n            self.preferredRequestTypeChanged.emit()\n\n    userKnowsPressAndHoldChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=userKnowsPressAndHoldChanged)\n    def userKnowsPressAndHold(self):\n        return self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD\n\n    @userKnowsPressAndHold.setter\n    def userKnowsPressAndHold(self, userKnowsPressAndHold):\n        if userKnowsPressAndHold != self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD:\n            self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD = userKnowsPressAndHold\n            self.userKnowsPressAndHoldChanged.emit()\n\n    addresslistShowTypeChanged = pyqtSignal()\n    @pyqtProperty(int, notify=addresslistShowTypeChanged)\n    def addresslistShowType(self):\n        return self.config.GUI_QML_ADDRESS_LIST_SHOW_TYPE\n\n    @addresslistShowType.setter\n    def addresslistShowType(self, addresslistShowType):\n        if addresslistShowType != self.config.GUI_QML_ADDRESS_LIST_SHOW_TYPE:\n            self.config.GUI_QML_ADDRESS_LIST_SHOW_TYPE = addresslistShowType\n            self.addresslistShowTypeChanged.emit()\n\n    addresslistShowUsedChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=addresslistShowUsedChanged)\n    def addresslistShowUsed(self):\n        return self.config.GUI_QML_ADDRESS_LIST_SHOW_USED\n\n    @addresslistShowUsed.setter\n    def addresslistShowUsed(self, addresslistShowUsed):\n        if addresslistShowUsed != self.config.GUI_QML_ADDRESS_LIST_SHOW_USED:\n            self.config.GUI_QML_ADDRESS_LIST_SHOW_USED = addresslistShowUsed\n            self.addresslistShowUsedChanged.emit()\n\n    outputValueRoundingChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=outputValueRoundingChanged)\n    def outputValueRounding(self):\n        return self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING\n\n    @outputValueRounding.setter\n    def outputValueRounding(self, outputValueRounding):\n        if outputValueRounding != self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING:\n            self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = outputValueRounding\n            self.outputValueRoundingChanged.emit()\n\n    lightningPaymentFeeMaxMillionthsChanged = pyqtSignal()\n    @pyqtProperty(int, notify=lightningPaymentFeeMaxMillionthsChanged)\n    def lightningPaymentFeeMaxMillionths(self):\n        return self.config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS\n\n    @lightningPaymentFeeMaxMillionths.setter\n    def lightningPaymentFeeMaxMillionths(self, lightningPaymentFeeMaxMillionths):\n        if lightningPaymentFeeMaxMillionths != self.config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS:\n            self.config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS = lightningPaymentFeeMaxMillionths\n            self.lightningPaymentFeeMaxMillionthsChanged.emit()\n\n    nostrRelaysChanged = pyqtSignal()\n    @pyqtProperty(str, notify=nostrRelaysChanged)\n    def nostrRelays(self):\n        return self.config.NOSTR_RELAYS\n\n    @nostrRelays.setter\n    def nostrRelays(self, nostr_relays):\n        if nostr_relays != self.config.NOSTR_RELAYS:\n            self.config.NOSTR_RELAYS = nostr_relays if nostr_relays else None\n            self.nostrRelaysChanged.emit()\n\n    swapServerNPubChanged = pyqtSignal()\n    @pyqtProperty(str, notify=swapServerNPubChanged)\n    def swapServerNPub(self):\n        return self.config.SWAPSERVER_NPUB\n\n    @swapServerNPub.setter\n    def swapServerNPub(self, swapserver_npub):\n        if swapserver_npub != self.config.SWAPSERVER_NPUB:\n            self.config.SWAPSERVER_NPUB = swapserver_npub\n            self.swapServerNPubChanged.emit()\n\n    lnUtxoReserveChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=lnUtxoReserveChanged)\n    def lnUtxoReserve(self):\n        self._lnutxoreserve = QEAmount(amount_sat=self.config.LN_UTXO_RESERVE)\n        return self._lnutxoreserve\n\n    walletShouldUseSinglePasswordChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=walletShouldUseSinglePasswordChanged)\n    def walletShouldUseSinglePassword(self):\n        \"\"\"\n        NOTE: this only indicates if we even want to use a single password, to check if we\n        actually use a single password the daemon needs to be checked.\n        \"\"\"\n        return self.config.WALLET_SHOULD_USE_SINGLE_PASSWORD\n\n    walletDidUseSinglePasswordChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=walletDidUseSinglePasswordChanged)\n    def walletDidUseSinglePassword(self):\n        \"\"\"\n        Allows to guess if this is a unified password instance without having\n        unlocked any wallet yet. Might be out of sync e.g. if wallet files get copied manually.\n        \"\"\"\n        # TODO: consider removing once encrypted wallet file headers are available\n        return self.config.WALLET_DID_USE_SINGLE_PASSWORD\n\n    @pyqtSlot('qint64', result=str)\n    @pyqtSlot(QEAmount, result=str)\n    def formatSatsForEditing(self, satoshis):\n        if isinstance(satoshis, QEAmount):\n            satoshis = satoshis.satsInt\n        return self.config.format_amount(\n            satoshis,\n            add_thousands_sep=False,\n        )\n\n    @pyqtSlot('qint64', result=str)\n    @pyqtSlot('qint64', bool, result=str)\n    @pyqtSlot(QEAmount, result=str)\n    @pyqtSlot(QEAmount, bool, result=str)\n    def formatSats(self, satoshis, with_unit=False):\n        if isinstance(satoshis, QEAmount):\n            satoshis = satoshis.satsInt\n        if with_unit:\n            return self.config.format_amount_and_units(satoshis)\n        else:\n            return self.config.format_amount(satoshis)\n\n    @pyqtSlot(QEAmount, result=str)\n    @pyqtSlot(QEAmount, bool, result=str)\n    def formatMilliSats(self, amount, with_unit=False):\n        assert isinstance(amount, QEAmount), f\"unexpected type for amount: {type(amount)}\"\n        msats = amount.msatsInt\n        precision = 3  # config.amt_precision_post_satoshi is not exposed in preferences\n        if with_unit:\n            return self.config.format_amount_and_units(msats/1000, precision=precision)\n        else:\n            return self.config.format_amount(msats/1000, precision=precision)\n\n    @pyqtSlot(str, result=QEAmount)\n    def unitsToSats(self, unitAmount):\n        self._amount = QEAmount()\n        try:\n            x = Decimal(unitAmount)\n        except Exception:\n            return self._amount\n\n        sat_max_precision = self.config.BTC_AMOUNTS_DECIMAL_POINT\n        msat_max_precision = self.config.BTC_AMOUNTS_DECIMAL_POINT + 3\n        sat_max_prec_amount = int(pow(10, sat_max_precision) * x)\n        msat_max_prec_amount = int(pow(10, msat_max_precision) * x)\n        self._amount = QEAmount(amount_sat=sat_max_prec_amount, amount_msat=msat_max_prec_amount)\n        return self._amount\n\n    @pyqtSlot('quint64', result=float)\n    def satsToUnits(self, satoshis):\n        return satoshis / pow(10, self.config.BTC_AMOUNTS_DECIMAL_POINT)\n"
  },
  {
    "path": "electrum/gui/qml/qedaemon.py",
    "content": "import base64\nimport os\nimport threading\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject\n\nfrom electrum.i18n import _\nfrom electrum.logging import get_logger\nfrom electrum.util import WalletFileException, standardize_path, InvalidPassword, send_exception_to_crash_reporter\nfrom electrum.plugin import run_hook\nfrom electrum.lnchannel import ChannelState\nfrom electrum.bitcoin import is_address\nfrom electrum.bitcoin import verify_usermessage_with_address\nfrom electrum.storage import StorageReadWriteError\n\nfrom .auth import AuthMixin, auth_protect\nfrom .qefx import QEFX\nfrom .qewallet import QEWallet\nfrom .qewizard import QENewWalletWizard, QEServerConnectWizard, QETermsOfUseWizard\n\nif TYPE_CHECKING:\n    from electrum.daemon import Daemon\n    from electrum.plugin import Plugins\n\n\n# wallet list model. supports both wallet basenames (wallet file basenames)\n# and whole Wallet instances (loaded wallets)\nfrom .util import check_password_strength\n\n\nclass QEWalletListModel(QAbstractListModel):\n    _logger = get_logger(__name__)\n\n    # define listmodel rolemap\n    _ROLE_NAMES= ('name', 'path', 'active')\n    _ROLE_KEYS = range(Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole + len(_ROLE_NAMES))\n    _ROLE_MAP  = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))\n\n    def __init__(self, daemon: 'Daemon', parent=None):\n        QAbstractListModel.__init__(self, parent)\n        self.daemon = daemon\n        self._wallets = []\n        self.reload()\n\n    def rowCount(self, index):\n        return len(self._wallets)\n\n    def roleNames(self):\n        return self._ROLE_MAP\n\n    def data(self, index, role):\n        (wallet_name, wallet_path) = self._wallets[index.row()]\n        role_index = role - Qt.ItemDataRole.UserRole\n        role_name = self._ROLE_NAMES[role_index]\n        if role_name == 'name':\n            return wallet_name\n        if role_name == 'path':\n            return wallet_path\n        if role_name == 'active':\n            return self.daemon.get_wallet(wallet_path) is not None\n\n    @pyqtSlot()\n    def reload(self):\n        self._logger.debug('enumerating available wallets')\n        self.beginResetModel()\n        self._wallets = []\n        self.endResetModel()\n\n        available = []\n        wallet_folder = os.path.dirname(self.daemon.config.get_wallet_path())\n        with os.scandir(wallet_folder) as it:\n            for i in it:\n                if i.is_file() and not i.name.startswith('.'):\n                    available.append(i.path)\n        for path in sorted(available):\n            wallet = self.daemon.get_wallet(path)\n            self.add_wallet(wallet_path=path)\n\n    def add_wallet(self, wallet_path):\n        self.beginInsertRows(QModelIndex(), len(self._wallets), len(self._wallets))\n        wallet_name = os.path.basename(wallet_path)\n        wallet_path = standardize_path(wallet_path)\n        item = (wallet_name, wallet_path)\n        self._wallets.append(item)\n        self.endInsertRows()\n\n    def remove_wallet(self, path):\n        i = 0\n        wallets = []\n        remove = -1\n        for wallet_name, wallet_path in self._wallets:\n            if wallet_path == path:\n                remove = i\n            else:\n                wallets.append((wallet_name, wallet_path))\n            i += 1\n\n        if remove >= 0:\n            self.beginRemoveRows(QModelIndex(), remove, remove)\n            self._wallets = wallets\n            self.endRemoveRows()\n\n    @pyqtSlot(str, result=bool)\n    def wallet_name_exists(self, name):\n        for wallet_name, wallet_path in self._wallets:\n            if name == wallet_name:\n                return True\n        return False\n\n    @pyqtSlot(str)\n    def updateWallet(self, path):\n        i = 0\n        for wallet_name, wallet_path in self._wallets:\n            if wallet_path == path:\n                mi = self.createIndex(i, i)\n                self.dataChanged.emit(mi, mi, self._ROLE_KEYS)\n                return\n            i += 1\n\n\nclass QEDaemon(AuthMixin, QObject):\n    instance = None  # type: Optional[QEDaemon]\n\n    _logger = get_logger(__name__)\n\n    _available_wallets = None\n    _current_wallet = None\n    _new_wallet_wizard = None\n    _terms_of_use_wizard = None\n    _server_connect_wizard = None\n    _path = None\n    _name = None\n    _use_single_password = False\n    _password = None\n    _loading = False\n\n    _backendWalletLoaded = pyqtSignal([str], arguments=['password'])\n\n    availableWalletsChanged = pyqtSignal()\n    fxChanged = pyqtSignal()\n    newWalletWizardChanged = pyqtSignal()\n    termsOfUseWizardChanged = pyqtSignal()\n    serverConnectWizardChanged = pyqtSignal()\n    loadingChanged = pyqtSignal()\n    requestNewPassword = pyqtSignal()\n\n    walletLoaded = pyqtSignal([str, str], arguments=['name', 'path'])\n    walletRequiresPassword = pyqtSignal([str, str], arguments=['name', 'path'])\n    walletOpenError = pyqtSignal([str], arguments=[\"error\"])\n    walletDeleteError = pyqtSignal([str, str], arguments=['code', 'message'])\n\n    def __init__(self, daemon: 'Daemon', plugins: 'Plugins', parent=None):\n        super().__init__(parent)\n        if QEDaemon.instance:\n            raise RuntimeError('There should only be one QEDaemon instance')\n        QEDaemon.instance = self\n        self.daemon = daemon\n        self.plugins = plugins\n        self.qefx = QEFX(daemon.fx, daemon.config)\n\n        self._backendWalletLoaded.connect(self._on_backend_wallet_loaded)\n\n    @pyqtSlot()\n    def passwordValidityCheck(self):\n        if not self._walletdb._validPassword:\n            self.walletRequiresPassword.emit(self._name, self._path)\n\n    @pyqtSlot()\n    @pyqtSlot(str)\n    @pyqtSlot(str, str)\n    def loadWallet(self, path=None, password=None):\n        if self._loading:\n            return\n        self._loading = True\n\n        if path is None:\n            self._path = self.daemon.config.get('wallet_path')  # command line -w option\n            if self._path is None:\n                self._path = self.daemon.config.CURRENT_WALLET\n        else:\n            self._path = path\n        if self._path is None:\n            self._loading = False\n            return\n\n        self.loadingChanged.emit()\n\n        self._path = standardize_path(self._path)\n        self._name = os.path.basename(self._path)\n\n        self._logger.debug('load wallet ' + str(self._path))\n\n        # password unification helper:\n        # - if pw not given (None), try pw of current wallet.\n        # - but \"\" empty str passwords are kept as-is, to open passwordless wallets\n        if password is None:\n            password = self._password\n\n        # map explicit empty str password to None. the backend disallows empty str passwords.\n        if password == '':\n            password = None\n\n        wallet_already_open = self.daemon.get_wallet(self._path)\n        if wallet_already_open is not None:\n            password = QEWallet.getInstanceFor(wallet_already_open).password\n\n        def load_wallet_task():\n            success = False\n            try:\n                local_password = password  # need this in local scope\n                wallet = None\n                try:\n                    wallet = self.daemon.load_wallet(\n                        self._path,\n                        password=local_password,\n                        upgrade=True,\n                        # might have a keystore password, but unencrypted storage. we want to prompt for pw even then:\n                        force_check_password=True,\n                    )\n                except InvalidPassword:\n                    self.walletRequiresPassword.emit(self._name, self._path)\n                except FileNotFoundError:\n                    self.walletOpenError.emit(_('File not found') + f\":\\n{self._path}\")\n                except StorageReadWriteError:\n                    self.walletOpenError.emit(_('Could not read/write file'))\n                except WalletFileException as e:\n                    self.walletOpenError.emit(_('Could not open wallet: {}').format(str(e)))\n                    if e.should_report_crash:\n                        send_exception_to_crash_reporter(e)\n\n                if wallet is None:\n                    return\n\n                if self.daemon.config.WALLET_SHOULD_USE_SINGLE_PASSWORD:\n                    self._use_single_password = self._update_password_for_directory_and_unlock_wallets(old_password=local_password, new_password=local_password)\n                    if not self._use_single_password and self.daemon.config.WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION:\n                        # we need to disable biometric auth if the user creates wallets with different passwords as\n                        # we only store one encrypted password which is not associated to a specific wallet\n                        self._logger.warning(f\"disabling biometric authentication, not in single password mode\")\n                        self.daemon.config.WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION = False\n                        self.daemon.config.WALLET_ANDROID_BIOMETRIC_AUTH_WRAPPED_WALLET_PASSWORD = ''\n                        self.daemon.config.WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY = ''\n                    self._password = local_password\n                    self.singlePasswordChanged.emit()\n                    self._logger.info(f'use single password: {self._use_single_password}')\n                else:\n                    self._logger.info('use single password disabled by config')\n                self.daemon.config.WALLET_DID_USE_SINGLE_PASSWORD = self._use_single_password\n\n                run_hook('load_wallet', wallet)\n\n                success = True\n                self._backendWalletLoaded.emit(local_password)\n            finally:\n                if not success:  # if successful, _loading guard will be reset by _on_backend_wallet_loaded\n                    self._loading = False\n                    self.loadingChanged.emit()\n\n        threading.Thread(target=load_wallet_task, daemon=False).start()\n\n    @pyqtSlot()\n    @pyqtSlot(str)\n    def _on_backend_wallet_loaded(self, password=None):\n        self._logger.debug('_on_backend_wallet_loaded')\n        wallet = self.daemon.get_wallet(self._path)\n        assert wallet is not None\n        self._current_wallet = QEWallet.getInstanceFor(wallet)\n        self.availableWallets.updateWallet(self._path)\n        wallet.unlock(password or None)  # not conditional on wallet.requires_unlock in qml, as\n        # the auth wrapper doesn't pass the entered password, but instead we rely on the password in memory\n        self._loading = False\n        self.loadingChanged.emit()\n        self.walletLoaded.emit(self._name, self._path)\n\n    @pyqtSlot(QEWallet)\n    @pyqtSlot(QEWallet, bool)\n    @pyqtSlot(QEWallet, bool, bool)\n    def checkThenDeleteWallet(self, wallet, confirm_requests=False, confirm_balance=False):\n        if wallet.wallet.lnworker:\n            lnchannels = wallet.wallet.lnworker.get_channel_objects()\n            if any([channel.get_state() != ChannelState.REDEEMED and not channel.is_backup() for channel in lnchannels.values()]):\n                self.walletDeleteError.emit('unclosed_channels', _('There are still channels that are not fully closed'))\n                return\n\n        num_requests = len(wallet.wallet.get_unpaid_requests())\n        if num_requests > 0 and not confirm_requests:\n            self.walletDeleteError.emit('unpaid_requests', _('There are still unpaid requests. Really delete?'))\n            return\n\n        c, u, x = wallet.wallet.get_balance()\n        if c+u+x > 0 and not wallet.wallet.is_watching_only() and not confirm_balance:\n            self.walletDeleteError.emit('balance', _('There are still coins present in this wallet. Really delete?'))\n            return\n\n        self.delete_wallet(wallet)\n\n    @auth_protect(message=_('Really delete this wallet?'))\n    def delete_wallet(self, wallet):\n        path = standardize_path(wallet.wallet.storage.path)\n        self._logger.debug('deleting wallet with path %s' % path)\n        self._current_wallet = None\n        # TODO walletLoaded signal is confusing\n        self.walletLoaded.emit(None, None)\n\n        if not self.daemon.delete_wallet(path):\n            self.walletDeleteError.emit('error', _('Problem deleting wallet'))\n            return\n\n        self.availableWallets.remove_wallet(path)\n\n    @pyqtProperty(bool, notify=loadingChanged)\n    def loading(self):\n        return self._loading\n\n    @pyqtProperty(QEWallet, notify=walletLoaded)\n    def currentWallet(self):\n        return self._current_wallet\n\n    @pyqtProperty(QEWalletListModel, notify=availableWalletsChanged)\n    def availableWallets(self):\n        if not self._available_wallets:\n            self._available_wallets = QEWalletListModel(self.daemon)\n\n        return self._available_wallets\n\n    @pyqtProperty(QEFX, notify=fxChanged)\n    def fx(self):\n        return self.qefx\n\n    @pyqtSlot(str, result=list)\n    def getWalletsUnlockableWithPassword(self, password: str) -> list[str]:\n        \"\"\"\n        Returns any wallet that can be unlocked with the given password.\n        Can be used as fallback to unlock another wallet the user entered a\n        password that doesn't work for the current wallet but might work for another one.\n        \"\"\"\n        wallet_dir = os.path.dirname(self.daemon.config.get_wallet_path())\n        _, _, wallet_paths_can_unlock = self.daemon.check_password_for_directory(\n            old_password=password,\n            new_password=None,\n            wallet_dir=wallet_dir,\n        )\n        if not wallet_paths_can_unlock:\n            return []\n        self._logger.debug(f\"getWalletsUnlockableWithPassword: can unlock {len(wallet_paths_can_unlock)} wallets\")\n        return [str(path) for path in wallet_paths_can_unlock]\n\n    @pyqtSlot(str, result=int)\n    def numWalletsWithPassword(self, password: str) -> int:\n        \"\"\"Returns the number of wallets that can be unlocked with the given password\"\"\"\n        wallet_paths_can_unlock = self.getWalletsUnlockableWithPassword(password)\n        return len(wallet_paths_can_unlock)\n\n    singlePasswordChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=singlePasswordChanged)\n    def singlePasswordEnabled(self):\n        \"\"\"\n        singlePasswordEnabled is False if:\n            a.) the user has no wallet (and password) yet\n            b.) the user has wallets with different passwords (legacy)\n            c.) all wallets are locked, we couldn't check yet if they all use the same password\n            d.) we are on desktop where different passwords are allowed\n        \"\"\"\n        return self._use_single_password\n\n    @pyqtProperty(str, notify=singlePasswordChanged)\n    def singlePassword(self):\n        \"\"\"\n        self._password is also set to the last loaded wallet password if we WANT a single password,\n        but don't actually have a single password yet. So singlePassword being set doesn't strictly\n        mean all wallets use the same password.\n        \"\"\"\n        return self._password\n\n    @singlePassword.setter\n    def singlePassword(self, password: str):\n        assert password\n        assert self.daemon.config.WALLET_SHOULD_USE_SINGLE_PASSWORD\n        if self._password != password:\n            self._password = password\n            self.singlePasswordChanged.emit()\n\n    @pyqtSlot(result=str)\n    def suggestWalletName(self):\n        # FIXME why not use util.get_new_wallet_name ?\n        i = 1\n        while self.availableWallets.wallet_name_exists(f'wallet_{i}'):\n            i = i + 1\n        return f'wallet_{i}'\n\n    @pyqtSlot()\n    @auth_protect(method='wallet_password_only')\n    def startChangePassword(self):\n        if self._use_single_password:\n            self.requestNewPassword.emit()\n        else:\n            self.currentWallet.requestNewPassword.emit()\n\n    @pyqtSlot(str, result=bool)\n    def setPassword(self, password):\n        assert self._use_single_password\n        assert password\n        if not self._update_password_for_directory_and_unlock_wallets(old_password=self._password, new_password=password):\n            return False\n        self._password = password\n        return True\n\n    def _update_password_for_directory_and_unlock_wallets(self, *, old_password, new_password):\n        # note: this assumes all wallet files are in a single directory.\n        # change wallet passwords:\n        ret = self.daemon.update_password_for_directory(old_password=old_password, new_password=new_password)\n        # If some wallets just had their password changed, they got \"locked\" by wallet.update_password().\n        # If the password is not unified yet, other loaded wallets might still be unlocked.\n        # restore the invariant that all loaded wallets in qml must be unlocked:\n        for w in self.daemon.get_wallets().values():\n            if not w.is_unlocked():\n                w.unlock(new_password)\n            assert w.is_unlocked()\n        return ret\n\n    @pyqtProperty(QENewWalletWizard, notify=newWalletWizardChanged)\n    def newWalletWizard(self):\n        if not self._new_wallet_wizard:\n            self._new_wallet_wizard = QENewWalletWizard(self, self.plugins)\n\n        return self._new_wallet_wizard\n\n    @pyqtProperty(QEServerConnectWizard, notify=serverConnectWizardChanged)\n    def serverConnectWizard(self):\n        if not self._server_connect_wizard:\n            self._server_connect_wizard = QEServerConnectWizard(self)\n\n        return self._server_connect_wizard\n\n    @pyqtProperty(QETermsOfUseWizard, notify=termsOfUseWizardChanged)\n    def termsOfUseWizard(self):\n        if not self._terms_of_use_wizard:\n            self._terms_of_use_wizard = QETermsOfUseWizard(self)\n        return self._terms_of_use_wizard\n\n    @pyqtSlot()\n    def startNetwork(self):\n        self.daemon.start_network()\n\n    @pyqtSlot(str, str, str, result=bool)\n    def verifyMessage(self, address, message, signature):\n        address = address.strip()\n        message = message.strip().encode('utf-8')\n        if not is_address(address):\n            return False\n        try:\n            # This can throw on invalid base64\n            sig = base64.b64decode(str(signature.strip()), validate=True)\n            verified = verify_usermessage_with_address(address, sig, message)\n        except Exception as e:\n            verified = False\n        return verified\n\n    @pyqtSlot(str, result=int)\n    def passwordStrength(self, password):\n        if len(password) == 0:\n            return 0\n        return check_password_strength(password)[0]\n"
  },
  {
    "path": "electrum/gui/qml/qefx.py",
    "content": "from datetime import datetime, timedelta\nfrom decimal import Decimal\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRegularExpression\n\nfrom electrum.bitcoin import COIN\nfrom electrum.exchange_rate import FxThread\nfrom electrum.logging import get_logger\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.util import event_listener\n\nfrom electrum.gui.common_qt.util import QtEventListener\n\nfrom .qetypes import QEAmount\n\n\nclass QEFX(QObject, QtEventListener):\n    _logger = get_logger(__name__)\n\n    quotesUpdated = pyqtSignal()\n\n    def __init__(self, fxthread: FxThread, config: SimpleConfig, parent=None):\n        super().__init__(parent)\n        self.fx = fxthread\n        self.config = config\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    @event_listener\n    def on_event_on_quotes(self, *args):\n        self._logger.debug('new quotes')\n        self.quotesUpdated.emit()\n\n    historyUpdated = pyqtSignal()\n    @event_listener\n    def on_event_on_history(self, *args):\n        self._logger.debug('new history')\n        self.historyUpdated.emit()\n\n    currenciesChanged = pyqtSignal()\n    @pyqtProperty('QVariantList', notify=currenciesChanged)\n    def currencies(self):\n        return self.fx.get_currencies(self.historicRates)\n\n    rateSourcesChanged = pyqtSignal()\n    @pyqtProperty('QVariantList', notify=rateSourcesChanged)\n    def rateSources(self):\n        return self.fx.get_exchanges_by_ccy(self.fiatCurrency, self.historicRates)\n\n    fiatCurrencyChanged = pyqtSignal()\n    @pyqtProperty(str, notify=fiatCurrencyChanged)\n    def fiatCurrency(self):\n        return self.fx.get_currency()\n\n    @fiatCurrency.setter\n    def fiatCurrency(self, currency):\n        if currency != self.fiatCurrency:\n            self.fx.set_currency(currency)\n            self.enabled = self.enabled and currency != ''\n            self.fiatCurrencyChanged.emit()\n            self.rateSourcesChanged.emit()\n\n    @pyqtProperty('QRegularExpression', notify=fiatCurrencyChanged)\n    def fiatAmountRegex(self):\n        decimals = self.fx.ccy_precision()\n        exp = '[0-9]*'\n        if decimals:\n            exp += '\\\\.'\n            exp += '[0-9]{0,%d}' % decimals\n        return QRegularExpression(exp)\n\n    historicRatesChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=historicRatesChanged)\n    def historicRates(self):\n        if not self.fx.config.cv.FX_HISTORY_RATES.is_set():\n            self.fx.config.FX_HISTORY_RATES = True  # override default\n        return self.fx.config.FX_HISTORY_RATES\n\n    @historicRates.setter\n    def historicRates(self, checked):\n        if checked != self.historicRates:\n            self.fx.config.FX_HISTORY_RATES = bool(checked)\n            self.historicRatesChanged.emit()\n            self.rateSourcesChanged.emit()\n\n    rateSourceChanged = pyqtSignal()\n    @pyqtProperty(str, notify=rateSourceChanged)\n    def rateSource(self):\n        return self.fx.config_exchange()\n\n    @rateSource.setter\n    def rateSource(self, source):\n        if source != self.rateSource:\n            self.fx.set_exchange(source)\n            self.rateSourceChanged.emit()\n\n    enabledUpdated = pyqtSignal()  # curiously, enabledChanged is clashing, so name it enabledUpdated\n    @pyqtProperty(bool, notify=enabledUpdated)\n    def enabled(self):\n        return self.fx.is_enabled()\n\n    @enabled.setter\n    def enabled(self, enable):\n        if enable != self.enabled:\n            self.fx.set_enabled(enable)\n            self.enabledUpdated.emit()\n\n    @pyqtSlot(str, result=str)\n    @pyqtSlot(str, bool, result=str)\n    @pyqtSlot(QEAmount, result=str)\n    @pyqtSlot(QEAmount, bool, result=str)\n    def fiatValue(self, satoshis, plain=True):\n        rate = self.fx.exchange_rate()\n        if isinstance(satoshis, QEAmount):\n            satoshis = satoshis.msatsInt / 1000 if satoshis.msatsInt != 0 else satoshis.satsInt\n        else:\n            try:\n                sd = Decimal(satoshis)\n            except Exception:\n                return ''\n        if plain:\n            return self.fx.ccy_amount_str(self.fx.fiat_value(satoshis, rate), add_thousands_sep=False)\n        else:\n            return self.fx.value_str(satoshis, rate)\n\n    @pyqtSlot(str, str, result=str)\n    @pyqtSlot(str, str, bool, result=str)\n    @pyqtSlot(QEAmount, str, result=str)\n    @pyqtSlot(QEAmount, str, bool, result=str)\n    def fiatValueHistoric(self, satoshis, timestamp, plain=True):\n        if isinstance(satoshis, QEAmount):\n            satoshis = satoshis.msatsInt / 1000 if satoshis.msatsInt != 0 else satoshis.satsInt\n        else:\n            try:\n                sd = Decimal(satoshis)\n            except Exception:\n                return ''\n\n        try:\n            td = Decimal(timestamp)\n            if td == 0:\n                return ''\n        except Exception:\n            return ''\n        dt = datetime.fromtimestamp(int(td))\n        if plain:\n            return self.fx.ccy_amount_str(self.fx.historical_value(satoshis, dt), add_thousands_sep=False)\n        else:\n            return self.fx.historical_value_str(satoshis, dt)\n\n    @pyqtSlot(str, result=str)\n    @pyqtSlot(str, bool, result=str)\n    def satoshiValue(self, fiat, plain=True):\n        rate = self.fx.exchange_rate()\n        try:\n            fd = Decimal(fiat)\n        except Exception:\n            return ''\n        v = fd / Decimal(rate) * COIN\n        if v.is_nan():\n            return ''\n        if plain:\n            return str(v.to_integral_value())\n        else:\n            return self.config.format_amount(v)\n\n    @pyqtSlot(str, result=bool)\n    def isRecent(self, timestamp):\n        # return True if unknown, e.g. timestamp not known yet, tx in mempool\n        try:\n            td = Decimal(timestamp)\n            if td == 0:\n                return True\n        except Exception:\n            return True\n        dt = datetime.fromtimestamp(int(td))\n        return dt + timedelta(days=1) > datetime.today()\n"
  },
  {
    "path": "electrum/gui/qml/qeinvoice.py",
    "content": "import copy\nimport threading\nfrom enum import IntEnum\nfrom typing import Optional, Dict, Any, Tuple\nfrom urllib.parse import urlparse\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, pyqtEnum, QTimer\n\nfrom electrum.i18n import _\nfrom electrum.logging import get_logger\nfrom electrum.invoices import (\n    Invoice, PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED,\n    PR_BROADCASTING, PR_BROADCAST, LN_EXPIRY_NEVER\n)\nfrom electrum.transaction import PartialTxOutput, TxOutput\nfrom electrum.lnutil import format_short_channel_id\nfrom electrum.lnurl import LNURL6Data\nfrom electrum.bitcoin import COIN, address_to_script\nfrom electrum.paymentrequest import PaymentRequest\nfrom electrum.payment_identifier import PaymentIdentifier, PaymentIdentifierState, PaymentIdentifierType\nfrom electrum.network import Network\nfrom electrum.util import event_listener\n\nfrom electrum.gui.common_qt.util import QtEventListener\n\nfrom .qetypes import QEAmount\nfrom .qewallet import QEWallet\nfrom .util import status_update_timer_interval\nfrom ...util import InvoiceError\n\n\nclass QEInvoice(QObject, QtEventListener):\n    @pyqtEnum\n    class Type(IntEnum):\n        Invalid = -1\n        OnchainInvoice = 0\n        LightningInvoice = 1\n        LNURLPayRequest = 2\n\n    @pyqtEnum\n    class Status(IntEnum):\n        Unpaid = PR_UNPAID\n        Expired = PR_EXPIRED\n        Unknown = PR_UNKNOWN\n        Paid = PR_PAID\n        Inflight = PR_INFLIGHT\n        Failed = PR_FAILED\n        Routing = PR_ROUTING\n        Unconfirmed = PR_UNCONFIRMED\n\n    _logger = get_logger(__name__)\n\n    invoiceChanged = pyqtSignal()\n    invoiceSaved = pyqtSignal([str], arguments=['key'])\n    amountOverrideChanged = pyqtSignal()\n    maxAmountMessage = pyqtSignal([str], arguments=['message'])\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._wallet = None  # type: Optional[QEWallet]\n        self._isSaved = False\n        self._canSave = False\n        self._canPay = False\n        self._key = None\n        self._invoiceType = QEInvoice.Type.Invalid\n        self._effectiveInvoice = None  # type: Optional[Invoice]\n        self._userinfo = ''\n        self._lnprops = {}\n        self._amount = QEAmount()\n        self._amountOverride = QEAmount()\n\n        self._timer = QTimer(self)\n        self._timer.setSingleShot(True)\n        self._timer.timeout.connect(self.updateStatusString)\n\n        self._amountOverride.valueChanged.connect(self._on_amountoverride_value_changed)\n\n        self._updating_max = False\n\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    @event_listener\n    def on_event_payment_succeeded(self, wallet, key):\n        if wallet == self._wallet.wallet and key == self.key:\n            self.statusChanged.emit()\n            self.determine_can_pay()\n            self.userinfo = _('Paid!')\n\n    @event_listener\n    def on_event_payment_failed(self, wallet, key, reason):\n        if wallet == self._wallet.wallet and key == self.key:\n            self.statusChanged.emit()\n            self.determine_can_pay()\n            self.userinfo = _('Payment failed: ') + reason\n\n    @event_listener\n    def on_event_invoice_status(self, wallet, key, status):\n        if self._wallet and wallet == self._wallet.wallet and key == self.key:\n            self.update_userinfo()\n            self.determine_can_pay()\n            self.statusChanged.emit()\n\n    @event_listener\n    def on_event_channel(self, wallet, channel):\n        if self._wallet and wallet == self._wallet.wallet:\n            self.update_userinfo()\n            self.determine_can_pay()\n\n    walletChanged = pyqtSignal()\n    @pyqtProperty(QEWallet, notify=walletChanged)\n    def wallet(self):\n        return self._wallet\n\n    @wallet.setter\n    def wallet(self, wallet: QEWallet):\n        if self._wallet != wallet:\n            self._wallet = wallet\n            self.walletChanged.emit()\n\n    @pyqtProperty(int, notify=invoiceChanged)\n    def invoiceType(self):\n        return self._invoiceType\n\n    # not a qt setter, don't let outside set state\n    def setInvoiceType(self, invoiceType: Type):\n        self._invoiceType = invoiceType\n\n    @pyqtProperty(str, notify=invoiceChanged)\n    def message(self):\n        return self._effectiveInvoice.message if self._effectiveInvoice else ''\n\n    @pyqtProperty('quint64', notify=invoiceChanged)\n    def time(self):\n        return self._effectiveInvoice.time if self._effectiveInvoice else 0\n\n    @pyqtProperty('quint64', notify=invoiceChanged)\n    def expiration(self):\n        return self._effectiveInvoice.exp if self._effectiveInvoice else 0\n\n    @pyqtProperty(str, notify=invoiceChanged)\n    def address(self):\n        return self._effectiveInvoice.get_address() if self._effectiveInvoice else ''\n\n    @pyqtProperty(QEAmount, notify=invoiceChanged)\n    def amount(self):\n        if not self._effectiveInvoice:\n            self._amount.clear()\n            return self._amount\n        self._amount.copyFrom(QEAmount(from_invoice=self._effectiveInvoice))\n        return self._amount\n\n    @pyqtProperty(QEAmount, notify=amountOverrideChanged)\n    def amountOverride(self):\n        return self._amountOverride\n\n    @amountOverride.setter\n    def amountOverride(self, new_amount: QEAmount):\n        self._logger.debug(f'set new override amount {repr(new_amount)}')\n        self._amountOverride.copyFrom(new_amount)\n        self.amountOverrideChanged.emit()\n\n    @pyqtSlot()\n    def _on_amountoverride_value_changed(self):\n        self.update_userinfo()\n        self.determine_can_pay()\n\n    statusChanged = pyqtSignal()\n    @pyqtProperty(int, notify=statusChanged)\n    def status(self):\n        if not self._effectiveInvoice:\n            return PR_UNKNOWN\n        if self.invoiceType == QEInvoice.Type.OnchainInvoice and self._effectiveInvoice.get_amount_sat() == 0:\n            # no amount set, not a final invoice, get_invoice_status would be wrong\n            return PR_UNPAID\n        return self._wallet.wallet.get_invoice_status(self._effectiveInvoice)\n\n    @pyqtProperty(str, notify=statusChanged)\n    def statusString(self):\n        if not self._effectiveInvoice:\n            return ''\n        status = self._wallet.wallet.get_invoice_status(self._effectiveInvoice)\n        return self._effectiveInvoice.get_status_str(status)\n\n    isSavedChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=isSavedChanged)\n    def isSaved(self):\n        return self._isSaved\n\n    canSaveChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=canSaveChanged)\n    def canSave(self):\n        return self._canSave\n\n    @canSave.setter\n    def canSave(self, canSave):\n        if self._canSave != canSave:\n            self._canSave = canSave\n            self.canSaveChanged.emit()\n\n    canPayChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=canPayChanged)\n    def canPay(self):\n        return self._canPay\n\n    @canPay.setter\n    def canPay(self, canPay):\n        if self._canPay != canPay:\n            self._canPay = canPay\n            self.canPayChanged.emit()\n\n    keyChanged = pyqtSignal()\n    @pyqtProperty(str, notify=keyChanged)\n    def key(self):\n        return self._key\n\n    @key.setter\n    def key(self, key):\n        self._key = key\n        invoice = copy.copy(self._wallet.wallet.get_invoice(key))  # copy, so any mutations stay out of wallet invoice list\n        self._logger.debug(f'invoice from key {key}: {repr(invoice)}')\n        self.set_effective_invoice(invoice)\n        self.keyChanged.emit()\n\n    userinfoChanged = pyqtSignal()\n    @pyqtProperty(str, notify=userinfoChanged)\n    def userinfo(self):\n        return self._userinfo\n\n    @userinfo.setter\n    def userinfo(self, userinfo):\n        if self._userinfo != userinfo:\n            self._userinfo = userinfo\n            self.userinfoChanged.emit()\n\n    @pyqtProperty('QVariantMap', notify=invoiceChanged)\n    def lnprops(self):\n        return self._lnprops\n\n    def set_lnprops(self):\n        self._lnprops = {}\n        if not self.invoiceType == QEInvoice.Type.LightningInvoice:\n            return\n\n        lnaddr = self._effectiveInvoice._lnaddr\n        ln_routing_info = lnaddr.get_routing_info('r')\n        self._logger.debug(str(ln_routing_info))\n\n        self._lnprops = {\n            'pubkey': lnaddr.pubkey.serialize().hex(),\n            'payment_hash': lnaddr.paymenthash.hex(),\n            'r': [{\n                'node': self.name_for_node_id(x[-1][0]),\n                'scid': format_short_channel_id(x[-1][1])\n                } for x in ln_routing_info] if ln_routing_info else []\n        }\n\n    def name_for_node_id(self, node_id):\n        lnworker = self._wallet.wallet.lnworker\n        return (lnworker.lnpeermgr.get_node_alias(node_id) if lnworker else None) or node_id.hex()\n\n    def set_effective_invoice(self, invoice: Invoice):\n        self._effectiveInvoice = invoice\n\n        if invoice is None:\n            self.setInvoiceType(QEInvoice.Type.Invalid)\n        else:\n            if invoice.is_lightning():\n                self.setInvoiceType(QEInvoice.Type.LightningInvoice)\n            else:\n                self.setInvoiceType(QEInvoice.Type.OnchainInvoice)\n            self._isSaved = self._wallet.wallet.get_invoice(invoice.get_id()) is not None\n\n        self.set_lnprops()\n\n        self.update_userinfo()\n        self.determine_can_pay()\n\n        self.invoiceChanged.emit()\n        self.statusChanged.emit()\n        self.isSavedChanged.emit()\n\n        self.set_status_timer()\n\n    def set_status_timer(self):\n        if self.status != PR_EXPIRED:\n            if self.expiration > 0 and self.expiration != LN_EXPIRY_NEVER:\n                interval = status_update_timer_interval(self.time + self.expiration)\n                if interval > 0:\n                    self._timer.setInterval(interval)  # msec\n                    self._timer.start()\n        else:\n            self.update_userinfo()\n            self.determine_can_pay()  # status went to PR_EXPIRED\n\n    @pyqtSlot()\n    def updateStatusString(self):\n        self.statusChanged.emit()\n        self.set_status_timer()\n\n    def update_userinfo(self):\n        self.userinfo = ''\n\n        if not self.amountOverride.isEmpty:\n            amount = self.amountOverride\n        else:\n            amount = self.amount\n\n        if self.amount.isEmpty:\n            self.userinfo = _('Enter the amount you want to send')\n\n        status = self.status\n\n        if amount.isEmpty and status == PR_UNPAID:  # unspecified amount\n            return\n\n        def userinfo_for_invoice_status(_status: int) -> str:\n            return {\n                PR_EXPIRED: _('This invoice has expired'),\n                PR_PAID: _('This invoice was already paid'),\n                PR_INFLIGHT: _('Payment in progress...'),\n                PR_ROUTING: _('Payment in progress...'),\n                PR_BROADCASTING: _('Payment in progress...') + ' (' + _('broadcasting') + ')',\n                PR_BROADCAST:  _('Payment in progress...') + ' (' + _('broadcast successfully') + ')',\n                PR_UNCONFIRMED: _('Payment in progress...') + ' (' + _('waiting for confirmation') + ')',\n                PR_UNKNOWN: _('Invoice has unknown status'),\n            }[_status]\n\n        if status in [PR_UNPAID, PR_FAILED]:\n            x, self.userinfo = self.check_can_pay_amount(amount)\n        else:\n            self.userinfo = userinfo_for_invoice_status(status)\n\n    def determine_can_pay(self):\n        self.canPay = False\n        self.canSave = False\n\n        if self.invoiceType not in [QEInvoice.Type.LightningInvoice, QEInvoice.Type.OnchainInvoice]:\n            return\n\n        if not self.amountOverride.isEmpty:\n            amount = self.amountOverride\n        else:\n            amount = self.amount\n\n        self.canSave = not bool(self._wallet.wallet.get_invoice(self._effectiveInvoice.get_id()))\n\n        status = self.status\n\n        if amount.isEmpty and status == PR_UNPAID:  # unspecified amount\n            return\n\n        if status in [PR_UNPAID, PR_FAILED]:\n            self.canPay, x = self.check_can_pay_amount(amount)\n\n    def check_can_pay_amount(self, amount: QEAmount) -> Tuple[bool, Optional[str]]:\n        assert self.status in [PR_UNPAID, PR_FAILED]\n        if self.invoiceType == QEInvoice.Type.LightningInvoice:\n            if self.get_max_spendable_lightning() * 1000 >= amount.msatsInt:\n                lnaddr = self._effectiveInvoice._lnaddr\n                if lnaddr.amount and amount.msatsInt < lnaddr.amount * COIN * 1000:\n                    return False, _('Cannot pay less than the amount specified in the invoice')\n                else:\n                    return True, None\n            elif self.address and self.get_max_spendable_onchain() > amount.satsInt:\n                return True, None\n        elif self.invoiceType == QEInvoice.Type.OnchainInvoice:\n            if (amount.isMax and self.get_max_spendable_onchain() > 0) or (self.get_max_spendable_onchain() >= amount.satsInt):\n                return True, None\n\n        return False, _('Insufficient balance')\n\n    @pyqtSlot()\n    def payLightningInvoice(self):\n        if not self.canPay:\n            raise Exception('can not pay invoice, canPay is false')\n\n        if self.invoiceType != QEInvoice.Type.LightningInvoice:\n            raise Exception('payLightningInvoice can only pay lightning invoices')\n\n        amount_msat = None\n        if self.amount.isEmpty:\n            if self.amountOverride.isEmpty:\n                raise Exception('can not pay 0 amount')\n            amount_msat = self.amountOverride.msatsInt\n\n        self._wallet.pay_lightning_invoice(self._effectiveInvoice, amount_msat)\n\n    def get_max_spendable_onchain(self):\n        return self._wallet.wallet.get_spendable_balance_sat()\n\n    def get_max_spendable_lightning(self):\n        return self._wallet.wallet.lnworker.num_sats_can_send() if self._wallet.wallet.lnworker else 0\n\n    @pyqtSlot()\n    def updateMaxAmount(self):\n        if self._updating_max:\n            return\n\n        assert self.invoiceType == QEInvoice.Type.OnchainInvoice\n\n        # only single address invoice supported\n        invoice_address = self._effectiveInvoice.get_address()\n\n        self._updating_max = True\n\n        def calc_max(address):\n            try:\n                outputs = [PartialTxOutput(scriptpubkey=address_to_script(address), value='!')]\n                make_tx = lambda fee_policy, *, confirmed_only=False: self._wallet.wallet.make_unsigned_transaction(\n                    coins=self._wallet.wallet.get_spendable_coins(None),\n                    outputs=outputs,\n                    fee_policy=fee_policy,\n                    is_sweep=False)\n                amount, message = self._wallet.determine_max(mktx=make_tx)\n                if amount is None:\n                    self._amountOverride.isMax = False\n                else:\n                    self._amountOverride.satsInt = amount\n                if message:\n                    self.maxAmountMessage.emit(message)\n            finally:\n                self._updating_max = False\n\n        threading.Thread(target=calc_max, args=(invoice_address,), daemon=True).start()\n\n\nclass QEInvoiceParser(QEInvoice):\n    _logger = get_logger(__name__)\n\n    validationSuccess = pyqtSignal()\n    validationWarning = pyqtSignal([str, str], arguments=['code', 'message'])\n    validationError = pyqtSignal([str, str], arguments=['code', 'message'])\n\n    invoiceCreateError = pyqtSignal([str, str], arguments=['code', 'message'])\n\n    lnurlRetrieved = pyqtSignal()\n    lnurlError = pyqtSignal([str, str], arguments=['code', 'message'])\n\n    busyChanged = pyqtSignal()\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._pi = None  # type: Optional[PaymentIdentifier]\n        self._lnurlData = None\n        self._busy = False\n\n        self.clear()\n\n    @pyqtSlot(object)\n    def fromResolvedPaymentIdentifier(self, resolved_pi: PaymentIdentifier) -> None:\n        self.clear()\n        self.amountOverride = QEAmount()\n        if resolved_pi:\n            assert not resolved_pi.need_resolve()\n            self.validateRecipient(resolved_pi)\n\n    @pyqtProperty('QVariantMap', notify=lnurlRetrieved)\n    def lnurlData(self):\n        return self._lnurlData\n\n    @pyqtProperty(bool, notify=lnurlRetrieved)\n    def isLnurlPay(self):\n        return self._lnurlData is not None\n\n    @pyqtProperty(bool, notify=busyChanged)\n    def busy(self):\n        return self._busy\n\n    @pyqtSlot()\n    def clear(self):\n        self.setInvoiceType(QEInvoice.Type.Invalid)\n        self._lnurlData = None\n        self.canSave = False\n        self.canPay = False\n        self.userinfo = ''\n        self.invoiceChanged.emit()\n\n    def setValidOnchainInvoice(self, invoice: Invoice):\n        self._logger.debug('setValidOnchainInvoice')\n        if invoice.is_lightning():\n            raise Exception('unexpected LN invoice')\n        self.set_effective_invoice(invoice)\n\n    def setValidLightningInvoice(self, invoice: Invoice):\n        self._logger.debug('setValidLightningInvoice')\n        if not invoice.is_lightning():\n            raise Exception('unexpected Onchain invoice')\n        self._key = invoice.get_id()\n        self.set_effective_invoice(invoice)\n\n    def setValidLNURLPayRequest(self):\n        self._logger.debug('setValidLNURLPayRequest')\n        self.setInvoiceType(QEInvoice.Type.LNURLPayRequest)\n        self._effectiveInvoice = None\n        self.invoiceChanged.emit()\n\n    def create_onchain_invoice(self, outputs, message, payment_request, uri):\n        return self._wallet.wallet.create_invoice(\n            outputs=outputs,\n            message=message,\n            pr=payment_request,\n            URI=uri\n            )\n\n    def _bip70_payment_request_resolved(self, pr: 'PaymentRequest'):\n        self._logger.debug('resolved payment request')\n        if Network.run_from_another_thread(pr.verify()):\n            invoice = Invoice.from_bip70_payreq(pr, height=0)\n            if self._wallet.wallet.get_invoice_status(invoice) == PR_PAID:\n                self.validationError.emit('unknown', _('Invoice already paid'))\n            elif pr.has_expired():\n                self.validationError.emit('unknown', _('Payment request has expired'))\n            else:\n                self.setValidOnchainInvoice(invoice)\n                self.validationSuccess.emit()\n        else:\n            self.validationError.emit('unknown', f'invoice error:\\n{pr.error}')\n\n    def validateRecipient(self, pi: PaymentIdentifier):\n        if not pi:\n            self.setInvoiceType(QEInvoice.Type.Invalid)\n            return\n\n        self._pi = pi\n        if not self._pi.is_valid() or self._pi.type not in [\n            PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21,\n            PaymentIdentifierType.BIP70, PaymentIdentifierType.BOLT11,\n            PaymentIdentifierType.LNADDR, PaymentIdentifierType.LNURLP,\n            PaymentIdentifierType.EMAILLIKE, PaymentIdentifierType.DOMAINLIKE,\n            PaymentIdentifierType.OPENALIAS,\n        ]:\n            self.validationError.emit('unknown', _('Unknown invoice'))\n            return\n\n        if self._pi.type == PaymentIdentifierType.SPK:\n            txo = TxOutput(scriptpubkey=self._pi.spk, value=0)\n            if not txo.address:\n                self.validationError.emit('unknown', _('Unknown invoice'))\n                return\n\n        self._update_from_payment_identifier()\n\n    def _update_from_payment_identifier(self):\n        assert not self._pi.need_resolve(), \"Should have been resolved by QEPIResolver\"\n\n        if self._pi.type in [\n            PaymentIdentifierType.LNURLP,\n            PaymentIdentifierType.LNADDR,\n        ]:\n            self.on_lnurl_pay(self._pi.lnurl_data)\n            return\n\n        if self._pi.type == PaymentIdentifierType.BIP70:\n            self._bip70_payment_request_resolved(self._pi.bip70_data)\n            return\n\n        if self._pi.is_available():\n            if self._pi.type in [PaymentIdentifierType.SPK, PaymentIdentifierType.OPENALIAS]:\n                outputs = [PartialTxOutput(scriptpubkey=self._pi.spk, value=0)]\n                invoice = self.create_onchain_invoice(outputs, None, None, None)\n                self._logger.debug(repr(invoice))\n                self.setValidOnchainInvoice(invoice)\n                self.validationSuccess.emit()\n                return\n            elif self._pi.type == PaymentIdentifierType.BOLT11:\n                lninvoice = self._pi.bolt11\n                if not self._wallet.wallet.has_lightning() and not lninvoice.get_address():\n                    self.validationError.emit('no_lightning',\n                        _('Detected valid Lightning invoice, but Lightning not enabled for wallet and no fallback address found.'))\n                    return\n                if self._wallet.wallet.lnworker and not self._wallet.wallet.lnworker.channels and not lninvoice.get_address():\n                    self.validationWarning.emit('no_channels',\n                        _('Detected valid Lightning invoice, but there are no open channels'))\n                self.setValidLightningInvoice(lninvoice)\n                self.validationSuccess.emit()\n            elif self._pi.type == PaymentIdentifierType.BIP21:\n                if self._wallet.wallet.has_lightning() and self._wallet.wallet.lnworker.channels and self._pi.bolt11:\n                    lninvoice = self._pi.bolt11\n                    self.setValidLightningInvoice(lninvoice)\n                    self.validationSuccess.emit()\n                else:\n                    self._validateRecipient_bip21_onchain(self._pi.bip21)\n\n    def _validateRecipient_bip21_onchain(self, bip21: Dict[str, Any]) -> None:\n        if 'address' not in bip21:\n            self._logger.debug('Neither LN invoice nor address in bip21 uri')\n            self.validationError.emit('unknown', _('Unknown invoice'))\n            return\n\n        amount = bip21.get('amount', 0)\n        outputs = [PartialTxOutput.from_address_and_value(bip21['address'], amount)]\n        self._logger.debug(outputs)\n        message = bip21.get('message', '')\n        invoice = self.create_onchain_invoice(outputs, message, None, bip21)\n        self._logger.debug(repr(invoice))\n        self.setValidOnchainInvoice(invoice)\n        self.validationSuccess.emit()\n\n    def on_lnurl_pay(self, lnurldata: LNURL6Data):\n        assert isinstance(lnurldata, LNURL6Data)\n        self._logger.debug('on_lnurl')\n        self._logger.debug(f'{repr(lnurldata)}')\n\n        self._lnurlData = {\n            'domain': urlparse(lnurldata.callback_url).netloc,\n            'callback_url': lnurldata.callback_url,\n            'min_sendable_sat': lnurldata.min_sendable_sat,\n            'max_sendable_sat': lnurldata.max_sendable_sat,\n            'metadata_plaintext': lnurldata.metadata_plaintext,\n            'comment_allowed': lnurldata.comment_allowed,\n        }\n        self.setValidLNURLPayRequest()\n        self.lnurlRetrieved.emit()\n\n    @pyqtSlot()\n    @pyqtSlot(str)\n    def lnurlGetInvoice(self, comment=None):\n        assert self._lnurlData\n        assert self._pi.need_finalize()\n        assert self.invoiceType == QEInvoice.Type.LNURLPayRequest\n        self._logger.debug(f'{repr(self._lnurlData)}')\n\n        amount = self.amountOverride.satsInt\n\n        if self._lnurlData['comment_allowed'] == 0:\n            comment = None\n\n        def on_finished(pi):\n            self._busy = False\n            self.busyChanged.emit()\n\n            if pi.is_error():\n                if pi.state == PaymentIdentifierState.INVALID_AMOUNT:\n                    self.lnurlError.emit('amount', pi.get_error())\n                else:\n                    self.lnurlError.emit('lnurl', pi.get_error())\n            else:\n                self.on_lnurl_invoice(self.amountOverride.satsInt, pi.bolt11)\n\n        self._busy = True\n        self.busyChanged.emit()\n\n        self._pi.finalize(amount_sat=amount, comment=comment, on_finished=on_finished)\n\n    def on_lnurl_invoice(self, orig_amount, invoice):\n        self._logger.debug('on_lnurl_invoice')\n        self._logger.debug(f'{repr(invoice)}')\n\n        # assure no shenanigans with the bolt11 invoice we get back\n        if orig_amount * 1000 != invoice.amount_msat:  # TODO msat precision can cause trouble here\n            raise Exception('Unexpected amount in invoice, differs from lnurl-pay specified amount')\n\n        self.amountOverride = QEAmount()\n        self.validateRecipient(\n            PaymentIdentifier(self._wallet.wallet, invoice.lightning_invoice)\n        )\n\n    @pyqtSlot(result=bool)\n    def saveInvoice(self) -> bool:\n        if not self._effectiveInvoice:\n            return False\n        if self.isSaved:\n            return False\n\n        try:\n            if not self._effectiveInvoice.amount_msat and not self.amountOverride.isEmpty:\n                if self.invoiceType == QEInvoice.Type.OnchainInvoice and self.amountOverride.isMax:\n                    self._effectiveInvoice.set_amount_msat('!')\n                else:\n                    self._effectiveInvoice.set_amount_msat(self.amountOverride.satsInt * 1000)\n        except InvoiceError as e:\n            self.invoiceCreateError.emit('validation', str(e))\n            return False\n\n        self.canSave = False\n\n        self._wallet.wallet.save_invoice(self._effectiveInvoice)\n        self._key = self._effectiveInvoice.get_id()\n        self._wallet.invoiceModel.addInvoice(self._key)\n        self.invoiceSaved.emit(self._key)\n\n        return True\n"
  },
  {
    "path": "electrum/gui/qml/qeinvoicelistmodel.py",
    "content": "from abc import abstractmethod\nfrom typing import TYPE_CHECKING, List, Dict, Any\n\nfrom PyQt6.QtCore import pyqtSlot, QTimer\nfrom PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex\n\nfrom electrum.logging import get_logger\nfrom electrum.util import Satoshis, format_time\nfrom electrum.invoices import BaseInvoice, PR_EXPIRED, LN_EXPIRY_NEVER, Invoice, Request, PR_PAID\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\n\nfrom .util import status_update_timer_interval\nfrom .qetypes import QEAmount\n\nif TYPE_CHECKING:\n    from electrum.wallet import Abstract_Wallet\n\n\nclass QEAbstractInvoiceListModel(QAbstractListModel):\n    _logger = get_logger(__name__)\n\n    # define listmodel rolemap\n    _ROLE_NAMES=('key', 'is_lightning', 'timestamp', 'date', 'message', 'amount',\n                 'status', 'status_str', 'address', 'expiry', 'type', 'onchain_fallback',\n                 'lightning_invoice')\n    _ROLE_KEYS = range(Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole + len(_ROLE_NAMES))\n    _ROLE_MAP  = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))\n    _ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS))\n\n    def __init__(self, wallet: 'Abstract_Wallet', parent=None):\n        super().__init__(parent)\n        self.wallet = wallet\n        self._invoices = []\n\n        self._timer = QTimer(self)\n        self._timer.setSingleShot(True)\n        self._timer.timeout.connect(self.updateStatusStrings)\n\n        try:\n            self.initModel()\n        except Exception as e:\n            self._logger.error(f'{repr(e)}')\n            raise e\n\n    def rowCount(self, index):\n        return len(self._invoices)\n\n    def roleNames(self):\n        return self._ROLE_MAP\n\n    def data(self, index, role):\n        invoice = self._invoices[index.row()]\n        role_index = role - Qt.ItemDataRole.UserRole\n        value = invoice[self._ROLE_NAMES[role_index]]\n\n        if isinstance(value, (bool, list, int, str, QEAmount)) or value is None:\n            return value\n        if isinstance(value, Satoshis):\n            return value.value\n        return str(value)\n\n    def clear(self):\n        self.beginResetModel()\n        self._invoices = []\n        self.endResetModel()\n\n    @pyqtSlot()\n    def initModel(self):\n        invoices = []\n        for invoice in self.get_invoice_list():\n            item = self.invoice_to_model(invoice)\n            invoices.append(item)\n\n        self.clear()\n        self.beginInsertRows(QModelIndex(), 0, len(invoices) - 1)\n        self._invoices = invoices\n        self.endInsertRows()\n\n        self.set_status_timer()\n\n    def add_invoice(self, invoice: BaseInvoice):\n        # skip if already in list\n        key = invoice.get_id()\n        for x in self._invoices:\n            if x['key'] == key:\n                return\n\n        item = self.invoice_to_model(invoice)\n        self._logger.debug(str(item))\n\n        self.beginInsertRows(QModelIndex(), 0, 0)\n        self._invoices.insert(0, item)\n        self.endInsertRows()\n\n        self.set_status_timer()\n\n    @pyqtSlot(str)\n    def addInvoice(self, key):\n        self.add_invoice(self.get_invoice_for_key(key))\n\n    def delete_invoice(self, key: str):\n        for i, invoice in enumerate(self._invoices):\n            if invoice['key'] == key:\n                self.beginRemoveRows(QModelIndex(), i, i)\n                self._invoices.pop(i)\n                self.endRemoveRows()\n                break\n        self.set_status_timer()\n\n    def get_model_invoice(self, key: str):\n        for invoice in self._invoices:\n            if invoice['key'] == key:\n                return invoice\n        return None\n\n    @pyqtSlot(str, int)\n    def updateInvoice(self, key, status):\n        self._logger.debug(f'updating invoice for {key} to {status}')\n        for i, item in enumerate(self._invoices):\n            if item['key'] == key:\n                invoice = self.get_invoice_for_key(key)\n                item['status'] = status\n                item['status_str'] = invoice.get_status_str(status)\n                index = self.index(i, 0)\n                self.dataChanged.emit(index, index, [self._ROLE_RMAP['status'], self._ROLE_RMAP['status_str']])\n                return\n\n    def invoice_to_model(self, invoice: BaseInvoice):\n        item = self.get_invoice_as_dict(invoice)\n        item['key'] = invoice.get_id()\n        item['is_lightning'] = invoice.is_lightning()\n        if invoice.is_lightning() and 'address' not in item:\n            item['address'] = ''\n        item['date'] = format_time(item['timestamp'])\n        item['amount'] = QEAmount(from_invoice=invoice)\n        item['onchain_fallback'] = invoice.is_lightning() and bool(invoice.get_address())\n\n        return item\n\n    def set_status_timer(self):\n        nearest_interval = LN_EXPIRY_NEVER\n        for invoice in self._invoices:\n            if invoice['status'] != PR_EXPIRED:\n                if invoice['expiry'] > 0 and invoice['expiry'] != LN_EXPIRY_NEVER:\n                    interval = status_update_timer_interval(invoice['timestamp'] + invoice['expiry'])\n                    if interval > 0:\n                        nearest_interval = nearest_interval if nearest_interval < interval else interval\n\n        if nearest_interval != LN_EXPIRY_NEVER:\n            self._timer.setInterval(nearest_interval)  # msec\n            self._timer.start()\n\n    @pyqtSlot()\n    def updateStatusStrings(self):\n        for i, item in enumerate(self._invoices):\n            invoice = self.get_invoice_for_key(item['key'])\n            if invoice is None:  # invoice might be removed from the backend\n                self._logger.debug(f'invoice {item[\"key\"]} not found')\n                continue\n            item['status'] = self.wallet.get_invoice_status(invoice)\n            item['status_str'] = invoice.get_status_str(item['status'])\n            index = self.index(i, 0)\n            self.dataChanged.emit(index, index, [self._ROLE_RMAP['status'], self._ROLE_RMAP['status_str']])\n\n        self.set_status_timer()\n\n    @abstractmethod\n    def get_invoice_for_key(self, key: str):\n        raise Exception('provide impl')\n\n    @abstractmethod\n    def get_invoice_list(self) -> List[BaseInvoice]:\n        raise Exception('provide impl')\n\n    @abstractmethod\n    def get_invoice_as_dict(self, invoice: BaseInvoice) -> Dict[str, Any]:\n        raise Exception('provide impl')\n\n\nclass QEInvoiceListModel(QEAbstractInvoiceListModel, QtEventListener):\n    def __init__(self, wallet, parent=None):\n        super().__init__(wallet, parent)\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n    _logger = get_logger(__name__)\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    @qt_event_listener\n    def on_event_invoice_status(self, wallet, key, status):\n        if wallet == self.wallet:\n            self._logger.debug(f'invoice status update for key {key} to {status}')\n            self.updateInvoice(key, status)\n\n    def invoice_to_model(self, invoice: BaseInvoice):\n        item = super().invoice_to_model(invoice)\n        item['type'] = 'invoice'\n\n        return item\n\n    def get_invoice_list(self):\n        lst = self.wallet.get_unpaid_invoices()\n        lst.reverse()\n        return lst\n\n    def get_invoice_for_key(self, key: str):\n        return self.wallet.get_invoice(key)\n\n    def get_invoice_as_dict(self, invoice: Invoice):\n        return self.wallet.export_invoice(invoice)\n\n\nclass QERequestListModel(QEAbstractInvoiceListModel, QtEventListener):\n    def __init__(self, wallet, parent=None):\n        super().__init__(wallet, parent)\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n    _logger = get_logger(__name__)\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    @qt_event_listener\n    def on_event_request_status(self, wallet, key, status):\n        if wallet == self.wallet:\n            self._logger.debug(f'request status update for key {key} to {status}')\n            self.updateRequest(key, status)\n\n    def invoice_to_model(self, invoice: BaseInvoice):\n        item = super().invoice_to_model(invoice)\n        item['type'] = 'request'\n\n        return item\n\n    def get_invoice_list(self):\n        lst = self.wallet.get_unpaid_requests()\n        lst.reverse()\n        return lst\n\n    def get_invoice_for_key(self, key: str):\n        return self.wallet.get_request(key)\n\n    def get_invoice_as_dict(self, invoice: Request):\n        return self.wallet.export_request(invoice)\n\n    @pyqtSlot(str, int)\n    def updateRequest(self, key, status):\n        if status == PR_PAID:\n            self.delete_invoice(key)\n        else:\n            self.updateInvoice(key, status)\n"
  },
  {
    "path": "electrum/gui/qml/qelnpaymentdetails.py",
    "content": "from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject\n\nfrom electrum.logging import get_logger\nfrom electrum.util import bfh, format_time\n\nfrom .qetypes import QEAmount\nfrom .qewallet import QEWallet\n\n\nclass QELnPaymentDetails(QObject):\n    _logger = get_logger(__name__)\n\n    detailsChanged = pyqtSignal()\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._wallet = None\n        self._key = None\n        self._label = ''\n        self._date = None\n        self._timestamp = 0\n        self._fee = QEAmount()\n        self._amount = QEAmount()\n        self._status = ''\n        self._phash = ''\n        self._preimage = ''\n\n    walletChanged = pyqtSignal()\n    @pyqtProperty(QEWallet, notify=walletChanged)\n    def wallet(self):\n        return self._wallet\n\n    @wallet.setter\n    def wallet(self, wallet: QEWallet):\n        if self._wallet != wallet:\n            self._wallet = wallet\n            self.walletChanged.emit()\n\n    keyChanged = pyqtSignal()\n    @pyqtProperty(str, notify=keyChanged)\n    def key(self):\n        return self._key\n\n    @key.setter\n    def key(self, key: str):\n        if self._key != key:\n            self._logger.debug(f'key set -> {key}')\n            self._key = key\n            self.keyChanged.emit()\n            self.update()\n\n    labelChanged = pyqtSignal()\n    @pyqtProperty(str, notify=labelChanged)\n    def label(self):\n        return self._label\n\n    @pyqtSlot(str)\n    def setLabel(self, label: str):\n        if label != self._label:\n            self._wallet.wallet.set_label(self._key, label)\n            self._label = label\n            self.labelChanged.emit()\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def status(self):\n        return self._status\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def date(self):\n        return self._date\n\n    @pyqtProperty(int, notify=detailsChanged)\n    def timestamp(self):\n        return self._timestamp\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def paymentHash(self):\n        return self._phash\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def preimage(self):\n        return self._preimage\n\n    @pyqtProperty(QEAmount, notify=detailsChanged)\n    def amount(self):\n        return self._amount\n\n    @pyqtProperty(QEAmount, notify=detailsChanged)\n    def fee(self):\n        return self._fee\n\n    def update(self):\n        if self._wallet is None:\n            self._logger.error('wallet undefined')\n            return\n\n        # TODO this is horribly inefficient. need a payment getter/query method\n        tx = self._wallet.wallet.lnworker.get_lightning_history()[self._key]\n        self._logger.debug(str(tx))\n\n        self._fee.msatsInt = 0 if not tx.fee_msat else int(tx.fee_msat)\n        self._amount.msatsInt = int(tx.amount_msat)\n        self._label = tx.label\n        self._date = format_time(tx.timestamp)\n        self._timestamp = tx.timestamp\n        self._status = 'settled'  # TODO: other states? get_lightning_history is deciding the filter for us :(\n        self._phash = tx.payment_hash\n        self._preimage = tx.preimage\n\n        self.detailsChanged.emit()\n"
  },
  {
    "path": "electrum/gui/qml/qemodelfilter.py",
    "content": "from PyQt6.QtCore import pyqtSignal, pyqtProperty, QSortFilterProxyModel, QModelIndex, pyqtSlot\n\nfrom electrum.logging import get_logger\n\n\nclass QEFilterProxyModel(QSortFilterProxyModel):\n    _logger = get_logger(__name__)\n\n    def __init__(self, parent_model, parent=None):\n        super().__init__(parent)\n        self._filter_value = None\n        self.setSourceModel(parent_model)\n\n    countChanged = pyqtSignal()\n    @pyqtProperty(int, notify=countChanged)\n    def count(self):\n        return self.rowCount(QModelIndex())\n\n    def isCustomFilter(self):\n        return self._filter_value is not None\n\n    @pyqtSlot(str)\n    def setFilterValue(self, filter_value):\n        self._filter_value = filter_value\n        self.invalidate()\n\n    def filterAcceptsRow(self, s_row, s_parent):\n        if not self.isCustomFilter:\n            return super().filterAcceptsRow(s_row, s_parent)\n\n        parent_model = self.sourceModel()\n        d = parent_model.data(parent_model.index(s_row, 0, s_parent), self.filterRole())\n        return True if self._filter_value is None else d == self._filter_value\n"
  },
  {
    "path": "electrum/gui/qml/qenetwork.py",
    "content": "from typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot\n\nfrom electrum.logging import get_logger\nfrom electrum import constants\nfrom electrum.network import ProxySettings\nfrom electrum.interface import ServerAddr\nfrom electrum.fee_policy import FEERATE_DEFAULT_RELAY\nfrom electrum.util import event_listener\n\nfrom electrum.gui.common_qt.util import QtEventListener\n\nfrom .qeconfig import QEConfig\nfrom .qeserverlistmodel import QEServerListModel\n\nif TYPE_CHECKING:\n    from electrum.network import Network\n\n\nclass QENetwork(QObject, QtEventListener):\n    _logger = get_logger(__name__)\n\n    networkUpdated = pyqtSignal()\n    blockchainUpdated = pyqtSignal()\n    heightChanged = pyqtSignal([int], arguments=['height'])  # local blockchain height\n    serverHeightChanged = pyqtSignal([int], arguments=['height'])\n    proxySet = pyqtSignal()\n    proxyChanged = pyqtSignal()\n    torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])\n    statusChanged = pyqtSignal()\n    feeHistogramUpdated = pyqtSignal()\n    chaintipsChanged = pyqtSignal()\n    isLaggingChanged = pyqtSignal()\n    gossipUpdated = pyqtSignal()\n\n    # shared signal for static properties\n    dataChanged = pyqtSignal()\n\n    _height = 0\n    _server = \"\"\n    _is_connected = False\n    _server_status = \"\"\n    _network_status = \"\"\n    _chaintips = 1\n    _islagging = False\n    _fee_histogram = []\n    _gossipPeers = 0\n    _gossipUnknownChannels = 0\n    _gossipDbNodes = 0\n    _gossipDbChannels = 0\n    _gossipDbPolicies = 0\n\n    def __init__(self, network: 'Network', parent=None):\n        super().__init__(parent)\n        assert network, \"--offline is not yet implemented for this GUI\"  # TODO\n        self.network = network\n        self._serverListModel = None\n        self._height = network.get_local_height()  # init here, update event can take a while\n        self._server_height = network.get_server_height()  # init here, update event can take a while\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n        QEConfig.instance.useGossipChanged.connect(self.on_gossip_setting_changed)\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    @event_listener\n    def on_event_network_updated(self, *args):\n        self.networkUpdated.emit()\n        self._update_status()\n\n    @event_listener\n    def on_event_blockchain_updated(self):\n        if self._height != self.network.get_local_height():\n            self._height = self.network.get_local_height()\n            self._logger.debug('new height: %d' % self._height)\n            self.heightChanged.emit(self._height)\n        self.blockchainUpdated.emit()\n\n    @event_listener\n    def on_event_default_server_changed(self, *args):\n        self._update_status()\n\n    @event_listener\n    def on_event_proxy_set(self, *args):\n        self._logger.debug('proxy set')\n        self.proxySet.emit()\n        self.proxyTorChanged.emit()\n\n    @event_listener\n    def on_event_tor_probed(self, *args):\n        self.proxyTorChanged.emit()\n\n    def _update_status(self):\n        server = str(self.network.get_parameters().server)\n        if self._server != server:\n            self._server = server\n            self.statusChanged.emit()\n        network_status = self.network.get_status()\n        if self._network_status != network_status:\n            self._logger.debug('network_status updated: %s' % network_status)\n            self._network_status = network_status\n            self.statusChanged.emit()\n        is_connected = self.network.is_connected()\n        if self._is_connected != is_connected:\n            self._is_connected = is_connected\n            self.statusChanged.emit()\n        server_status = self.network.get_connection_status_for_GUI()\n        if self._server_status != server_status:\n            self._logger.debug('server_status updated: %s' % server_status)\n            self._server_status = server_status\n            self.statusChanged.emit()\n        server_height = self.network.get_server_height()\n        if self._server_height != server_height:\n            self._logger.debug(f'server_height updated: {server_height}')\n            self._server_height = server_height\n            self.serverHeightChanged.emit(server_height)\n        chains = len(self.network.get_blockchains())\n        if chains != self._chaintips:\n            self._logger.debug('chain tips # changed: %d', chains)\n            self._chaintips = chains\n            self.chaintipsChanged.emit()\n        server_lag = self.network.get_local_height() - self.network.get_server_height()\n        if self._islagging ^ (server_lag > 1):\n            self._logger.debug('lagging changed: %s', str(server_lag > 1))\n            self._islagging = server_lag > 1\n            self.isLaggingChanged.emit()\n\n    @event_listener\n    def on_event_status(self, *args):\n        self._update_status()\n\n    @event_listener\n    def on_event_fee_histogram(self, histogram):\n        self._logger.debug(f'fee histogram updated')\n        self.update_histogram(histogram)\n\n    def update_histogram(self, histogram):\n        capped_histogram, bytes_current = histogram.get_capped_data()\n        # add clamping attributes for the GUI\n        self._fee_histogram = {\n            'histogram': capped_histogram,\n            'total': bytes_current,\n            'min_fee': capped_histogram[-1][0] if capped_histogram else FEERATE_DEFAULT_RELAY/1000,\n            'max_fee': capped_histogram[0][0] if capped_histogram else FEERATE_DEFAULT_RELAY/1000\n        }\n        self.feeHistogramUpdated.emit()\n\n    @event_listener\n    def on_event_channel_db(self, num_nodes, num_channels, num_policies):\n        changed = False\n        if self._gossipDbNodes != num_nodes:\n            self._gossipDbNodes = num_nodes\n            changed = True\n        if self._gossipDbChannels != num_channels:\n            self._gossipDbChannels = num_channels\n            changed = True\n        if self._gossipDbPolicies != num_policies:\n            self._gossipDbPolicies = num_policies\n            changed = True\n        if changed:\n            self._logger.debug(f'channel_db: {num_nodes} nodes, {num_channels} channels, {num_policies} policies')\n        self.gossipUpdated.emit()\n\n    @event_listener\n    def on_event_gossip_peers(self, num_peers):\n        self._logger.debug(f'gossip peers {num_peers}')\n        self._gossipPeers = num_peers\n        self.gossipUpdated.emit()\n\n    @event_listener\n    def on_event_unknown_channels(self, unknown):\n        if unknown == 0 and self._gossipUnknownChannels == 0:  # TODO: backend sends a lot of unknown=0 events\n            return\n        self._logger.debug(f'unknown channels {unknown}')\n        self._gossipUnknownChannels = unknown\n        self.gossipUpdated.emit()\n\n    def on_gossip_setting_changed(self):\n        if not self.network:\n            return\n        if QEConfig.instance.useGossip:\n            self.network.start_gossip()\n        else:\n            self.network.run_from_another_thread(self.network.stop_gossip())\n\n    @pyqtProperty(int, notify=heightChanged)\n    def height(self):  # local blockchain height\n        return self._height\n\n    @pyqtProperty(int, notify=serverHeightChanged)\n    def serverHeight(self):\n        return self._server_height\n\n    autoConnectChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=autoConnectChanged)\n    def autoConnect(self):\n        return self.network.config.NETWORK_AUTO_CONNECT\n\n    # auto_connect is actually a tri-state, expose the undefined case\n    @pyqtProperty(bool, notify=autoConnectChanged)\n    def autoConnectDefined(self):\n        return self.network.config.cv.NETWORK_AUTO_CONNECT.is_set()\n\n    @pyqtProperty(str, notify=statusChanged)\n    def server(self):\n        return self._server\n\n    @pyqtSlot(str, result=bool)\n    def isValidServerAddress(self, server: str) -> bool:\n        return ServerAddr.from_str_with_inference(server) is not None\n\n    @pyqtSlot(str, bool, bool)\n    def setServerParameters(self, server_str: str, auto_connect: bool, one_server: bool):\n        net_params = self.network.get_parameters()\n        server = ServerAddr.from_str_with_inference(server_str)\n        if server == net_params.server and auto_connect == net_params.auto_connect and one_server == net_params.oneserver:\n            return\n        if server != net_params.server:\n            if server is None:\n                if not auto_connect:\n                    return\n                server = net_params.server\n            self.statusChanged.emit()\n        if auto_connect != net_params.auto_connect:\n            self.network.config.NETWORK_AUTO_CONNECT = auto_connect\n            self.autoConnectChanged.emit()\n        net_params = net_params._replace(server=server, auto_connect=auto_connect, oneserver=one_server)\n        self.network.run_from_another_thread(self.network.set_parameters(net_params))\n\n    @pyqtProperty(str, notify=statusChanged)\n    def serverWithStatus(self):\n        server = self._server\n        if not self.network.is_connected():  # connecting or disconnected\n            return f'{server} (connecting...)'\n        return server\n\n    @pyqtProperty(str, notify=statusChanged)\n    def status(self):\n        return self._network_status\n\n    @pyqtProperty(str, notify=statusChanged)\n    def serverStatus(self):\n        return self.network.get_connection_status_for_GUI()\n\n    @pyqtProperty(bool, notify=statusChanged)\n    def isConnected(self):\n        return self._is_connected\n\n    @pyqtProperty(int, notify=chaintipsChanged)\n    def chaintips(self):\n        return self._chaintips\n\n    @pyqtProperty(bool, notify=isLaggingChanged)\n    def isLagging(self):\n        return self._islagging\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def isTestNet(self):\n        return constants.net.TESTNET\n\n    @pyqtProperty(str, notify=dataChanged)\n    def networkName(self):\n        return constants.net.__name__.replace('Bitcoin', '')\n\n    @pyqtProperty('QVariantMap', notify=proxyChanged)\n    def proxy(self):\n        net_params = self.network.get_parameters()\n        proxy = net_params.proxy\n        return proxy.to_dict()\n\n    @proxy.setter\n    def proxy(self, proxy_dict):\n        net_params = self.network.get_parameters()\n        proxy = ProxySettings.from_dict(proxy_dict)\n        net_params = net_params._replace(proxy=proxy)\n        self.network.run_from_another_thread(self.network.set_parameters(net_params))\n        self.proxyChanged.emit()\n\n    proxyTorChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=proxyTorChanged)\n    def isProxyTor(self):\n        return bool(self.network.is_proxy_tor)\n\n    @pyqtProperty(bool, notify=statusChanged)\n    def oneServer(self):\n        return self.network.oneserver\n\n    @pyqtProperty('QVariant', notify=feeHistogramUpdated)\n    def feeHistogram(self):\n        return self._fee_histogram\n\n    @pyqtProperty('QVariantMap', notify=gossipUpdated)\n    def gossipInfo(self):\n        return {\n            'peers': self._gossipPeers,\n            'unknown_channels': self._gossipUnknownChannels,\n            'db_nodes': self._gossipDbNodes,\n            'db_channels': self._gossipDbChannels,\n            'db_policies': self._gossipDbPolicies\n        }\n\n    serverListModelChanged = pyqtSignal()\n    @pyqtProperty(QEServerListModel, notify=serverListModelChanged)\n    def serverListModel(self):\n        if self._serverListModel is None:\n            self._serverListModel = QEServerListModel(self.network)\n        return self._serverListModel\n\n    @pyqtSlot()\n    def probeTor(self):\n        ProxySettings.probe_tor(self.torProbeFinished.emit)  # via signal\n"
  },
  {
    "path": "electrum/gui/qml/qepiresolver.py",
    "content": "from enum import IntEnum\nfrom typing import Optional\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer\n\nfrom electrum.logging import get_logger\nfrom electrum.i18n import _\nfrom electrum.payment_identifier import PaymentIdentifier, PaymentIdentifierState, PaymentIdentifierType\n\nfrom .qewallet import QEWallet\n\n\nclass QEPIResolver(QObject):\n    \"\"\"Intended to handle a user input Payment Identifier (PI), resolve it if necessary, then\n    allow to distinguish between a Request/voucher/lnurlw and an Invoice (e.g. b11 or lnurlp).\"\"\"\n    _logger = get_logger(__name__)\n\n    busyChanged = pyqtSignal()\n    resolveError = pyqtSignal([str, str], arguments=['code', 'message'])\n    invoiceResolved = pyqtSignal([object], arguments=['pi'])\n    requestResolved = pyqtSignal([object], arguments=['pi'])\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._wallet = None  # type: Optional[QEWallet]\n        self._recipient = None\n        self._pi = None\n        self._busy = False\n\n        self.clear()\n\n    recipientChanged = pyqtSignal()\n    @pyqtProperty(str, notify=recipientChanged)\n    def recipient(self) -> Optional[str]:\n        return self._recipient\n\n    @recipient.setter\n    def recipient(self, recipient: str) -> None:\n        self.clear()\n        if not recipient:\n            return\n        self._recipient = recipient\n        self.recipientChanged.emit()\n        self._pi = PaymentIdentifier(self._wallet.wallet, recipient)\n        if self._pi.need_resolve():\n            self.resolve_pi()\n        else:\n            # assuming if the PI is an invoice if it doesn't need resolving\n            # as there are no request types that do not need resolving currently\n            self.invoiceResolved.emit(self._pi)\n\n    walletChanged = pyqtSignal()\n    @pyqtProperty(QEWallet, notify=walletChanged)\n    def wallet(self) -> Optional[QEWallet]:\n        return self._wallet\n\n    @wallet.setter\n    def wallet(self, wallet: QEWallet) -> None:\n        self._wallet = wallet\n\n    @pyqtProperty(bool, notify=busyChanged)\n    def busy(self):\n        return self._busy\n\n    def resolve_pi(self) -> None:\n        assert self._pi is not None\n        assert self._pi.need_resolve()\n\n        def on_finished(pi: PaymentIdentifier):\n            self._busy = False\n            self.busyChanged.emit()\n\n            if pi.is_error():\n                if pi.type in [PaymentIdentifierType.EMAILLIKE, PaymentIdentifierType.DOMAINLIKE]:\n                    msg = _('Could not resolve address')\n                elif pi.type == PaymentIdentifierType.LNURL:\n                    msg = _('Could not resolve LNURL') + \"\\n\\n\" + pi.get_error()\n                elif pi.type == PaymentIdentifierType.BIP70:\n                    msg = _('Could not resolve BIP70 payment request: {}').format(pi.error)\n                else:\n                    msg = _('Could not resolve')\n                self.resolveError.emit('resolve', msg)\n            else:\n                if pi.type == PaymentIdentifierType.LNURLW:\n                    self.requestResolved.emit(pi)\n                else:\n                    self.invoiceResolved.emit(pi)\n\n        self._busy = True\n        self.busyChanged.emit()\n\n        self._pi.resolve(on_finished=on_finished)\n\n    def clear(self) -> None:\n        self._recipient = None\n        self._pi = None\n        self._busy = False\n        self.busyChanged.emit()\n        self.recipientChanged.emit()\n"
  },
  {
    "path": "electrum/gui/qml/qeqr.py",
    "content": "import asyncio\nimport qrcode\nfrom qrcode.exceptions import DataOverflowError\n\nimport math\nimport urllib\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRect\nfrom PyQt6.QtGui import QImage, QColor\nfrom PyQt6.QtQuick import QQuickImageProvider\ntry:\n    from PyQt6.QtMultimedia import QVideoSink\nexcept ImportError:\n    # stub QVideoSink when not found, as it's not essential on android\n    # and requires many dependencies when unit testing.\n    # Note: missing QtMultimedia will lead to errors when using QR scanner on desktop\n    from PyQt6.QtCore import QObject as QVideoSink\n\nfrom electrum.logging import get_logger\nfrom electrum.qrreader import get_qr_reader\nfrom electrum.i18n import _\nfrom electrum.util import profiler, get_asyncio_loop\nfrom electrum.gui.common_qt.util import draw_qr\n\n\nclass QEQRParser(QObject):\n    _logger = get_logger(__name__)\n\n    busyChanged = pyqtSignal()\n    dataChanged = pyqtSignal()\n    sizeChanged = pyqtSignal()\n    videoSinkChanged = pyqtSignal()\n\n    def __init__(self, text=None, parent=None):\n        super().__init__(parent)\n\n        self._busy = False\n        self._data = None\n        self._video_sink = None\n\n        self._text = text\n        self.qrreader = get_qr_reader()\n        if not self.qrreader:\n            raise Exception(_(\"The platform QR detection library is not available.\"))\n\n    @pyqtProperty(QVideoSink, notify=videoSinkChanged)\n    def videoSink(self):\n        return self._video_sink\n\n    @videoSink.setter\n    def videoSink(self, sink: QVideoSink):\n        if self._video_sink != sink:\n            self._video_sink = sink\n            self._video_sink.videoFrameChanged.connect(self.onVideoFrame)\n\n    def onVideoFrame(self, videoframe):\n        if self._busy or self._data:\n            return\n\n        self._busy = True\n        self.busyChanged.emit()\n\n        if not videoframe.isValid():\n            self._logger.debug('invalid frame')\n            return\n\n        async def co_parse_qr(frame):\n            image = frame.toImage()\n            self._parseQR(image)\n\n        asyncio.run_coroutine_threadsafe(co_parse_qr(videoframe), get_asyncio_loop())\n\n    def _parseQR(self, image: QImage):\n        self._size = min(image.width(), image.height())\n        self.sizeChanged.emit()\n        img_crop_rect = self._get_crop(image, self._size)\n        frame_cropped = image.copy(img_crop_rect)\n\n        # Convert to Y800 / GREY FourCC (single 8-bit channel)\n        frame_y800 = frame_cropped.convertToFormat(QImage.Format.Format_Grayscale8)\n        self.frame_id = 0\n        # Read the QR codes from the frame\n        self.qrreader_res = self.qrreader.read_qr_code(\n            frame_y800.constBits().__int__(),\n            frame_y800.sizeInBytes(),\n            frame_y800.bytesPerLine(),\n            frame_y800.width(),\n            frame_y800.height(),\n            self.frame_id\n            )\n\n        if len(self.qrreader_res) > 0:\n            result = self.qrreader_res[0]\n            self._data = result\n            self.dataChanged.emit()\n\n        self._busy = False\n        self.busyChanged.emit()\n\n    def _get_crop(self, image: QImage, scan_size: int) -> QRect:\n        \"\"\"Returns a QRect that is scan_size x scan_size in the middle of the resolution\"\"\"\n        scan_pos_x = (image.width() - scan_size) // 2\n        scan_pos_y = (image.height() - scan_size) // 2\n        return QRect(scan_pos_x, scan_pos_y, scan_size, scan_size)\n\n    @pyqtProperty(bool, notify=busyChanged)\n    def busy(self):\n        return self._busy\n\n    @pyqtProperty(int, notify=sizeChanged)\n    def size(self):\n        return self._size\n\n    @pyqtProperty(str, notify=dataChanged)\n    def data(self):\n        if not self._data:\n            return ''\n        return self._data.data\n\n    @pyqtSlot()\n    def reset(self):\n        self._data = None\n        self.dataChanged.emit()\n\n\nclass QEQRImageProvider(QQuickImageProvider):\n    MAX_QR_PIXELSIZE = 400\n    ERROR_CORRECT_LEVEL = qrcode.constants.ERROR_CORRECT_M\n    # ^ note: this is higher than for desktop. but on desktop we don't put a logo in the middle.\n    QR_BORDER = 2\n\n    def __init__(self, max_size, parent=None):\n        super().__init__(QQuickImageProvider.ImageType.Image)\n        self._max_size = max_size\n        self.qimg = None\n\n    _logger = get_logger(__name__)\n\n    @profiler\n    def requestImage(self, qstr, size):\n        # Qt does a urldecode before passing the string here\n        # but BIP21 (and likely other uri based specs) requires urlencoding,\n        # so we re-encode percent-quoted if a known 'scheme' is found in the string\n        # (unknown schemes might be found when a colon is in a serialized TX, which\n        # leads to mangling of the tx, so we check for supported schemes.)\n        uri = urllib.parse.urlparse(qstr)\n        if uri.scheme and uri.scheme in ['bitcoin', 'lightning']:\n            # urlencode request parameters\n            query = urllib.parse.parse_qs(uri.query)\n            query = urllib.parse.urlencode(query, doseq=True, quote_via=urllib.parse.quote)\n            uri = uri._replace(query=query)\n            qstr = urllib.parse.urlunparse(uri)\n\n        qr = qrcode.main.QRCode(border=self.QR_BORDER, error_correction=self.ERROR_CORRECT_LEVEL)\n\n        # calculate best box_size\n        pixelsize = min(self._max_size, self.MAX_QR_PIXELSIZE)\n        try:\n            qr.add_data(qstr)\n            modules = len(qr.get_matrix())\n            qr.box_size = math.floor(pixelsize/modules)\n            qr.make(fit=True)\n            self.qimg = QImage(modules * qr.box_size, modules * qr.box_size, QImage.Format.Format_RGB32)\n            draw_qr(qr=qr, paint_device=self.qimg)\n        except (ValueError, qrcode.exceptions.DataOverflowError):\n            # fake it\n            modules = 17 + qr.border * 2\n            box_size = math.floor(pixelsize/modules)\n            self.qimg = QImage(box_size * modules, box_size * modules, QImage.Format.Format_RGB32)\n            self.qimg.fill(QColor('gray'))\n        return self.qimg, self.qimg.size()\n\n\n# helper for placing icon exactly where it should go on the QR code\n# pyqt5 is unwilling to accept slots on QEQRImageProvider, so we need to define\n# a separate class (sigh)\nclass QEQRImageProviderHelper(QObject):\n    def __init__(self, max_size, parent=None):\n        super().__init__(parent)\n        self._max_size = max_size\n\n    @pyqtSlot(str, result='QVariantMap')\n    def getDimensions(self, qstr):\n        qr = qrcode.QRCode(\n            border=QEQRImageProvider.QR_BORDER,\n            error_correction=QEQRImageProvider.ERROR_CORRECT_LEVEL,\n        )\n\n        # calculate best box_size\n        pixelsize = min(self._max_size, QEQRImageProvider.MAX_QR_PIXELSIZE)\n        try:\n            qr.add_data(qstr)\n            modules = len(qr.get_matrix())\n            valid = True\n        except (ValueError, qrcode.exceptions.DataOverflowError):\n            # fake it\n            modules = 17 + qr.border * 2\n            valid = False\n\n        qr.box_size = math.floor(pixelsize/modules)\n        # calculate icon width in modules\n        icon_modules = int(modules / 5)\n        icon_modules += (icon_modules+1) % 2  # force odd\n\n        return {\n            'qr_pixelsize': modules * qr.box_size,\n            'icon_pixelsize': icon_modules * qr.box_size,\n            'valid': valid\n        }\n"
  },
  {
    "path": "electrum/gui/qml/qeqrscanner.py",
    "content": "import os\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Qt\nfrom PyQt6.QtGui import QGuiApplication\n\nfrom electrum.gui.qml.qetypes import QEBytes\nfrom electrum.util import send_exception_to_crash_reporter\nfrom electrum.logging import get_logger\nfrom electrum.i18n import _\n\n\nif 'ANDROID_DATA' in os.environ:\n    from jnius import autoclass\n    from android import activity\n\n    jpythonActivity = autoclass('org.kivy.android.PythonActivity').mActivity\n    jString = autoclass('java.lang.String')\n    jIntent = autoclass('android.content.Intent')\n\n\nclass QEQRScanner(QObject):\n    REQUEST_CODE_SIMPLE_SCANNER_ACTIVITY = 30368  # random 16 bit int\n\n    _logger = get_logger(__name__)\n\n    foundText = pyqtSignal(str)\n    foundBinary = pyqtSignal(QEBytes)\n\n    finished = pyqtSignal()\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self._hint = _(\"Scan a QR code.\")\n        self.finished.connect(self._unbind, Qt.ConnectionType.QueuedConnection)\n\n        self.destroyed.connect(lambda: self.on_destroy())\n\n    def on_destroy(self):\n        self._unbind()\n\n    @pyqtProperty(str)\n    def hint(self):\n        return self._hint\n\n    @hint.setter\n    def hint(self, v: str):\n        self._hint = v\n\n    @pyqtSlot()\n    def open(self):\n        if 'ANDROID_DATA' not in os.environ:\n            self._scan_qr_non_android()\n            return\n        jSimpleScannerActivity = autoclass(\"org.electrum.qr.SimpleScannerActivity\")\n        intent = jIntent(jpythonActivity, jSimpleScannerActivity)\n        intent.putExtra(jIntent.EXTRA_TEXT, jString(self._hint))\n\n        activity.bind(on_activity_result=self.on_qr_activity_result)\n        jpythonActivity.startActivityForResult(intent, self.REQUEST_CODE_SIMPLE_SCANNER_ACTIVITY)\n\n    @pyqtSlot()\n    def close(self):\n        # no-op to prevent qml type error\n        pass\n\n    def on_qr_activity_result(self, requestCode, resultCode, intent):\n        if requestCode != self.REQUEST_CODE_SIMPLE_SCANNER_ACTIVITY:\n            self._logger.warning(f\"got activity result with invalid {requestCode=}\")\n            return\n        try:\n            if resultCode == -1:  # RESULT_OK:\n                if (contents := intent.getStringExtra(jString(\"text\"))) is not None:\n                    self.foundText.emit(contents)\n                if (contents := intent.getByteArrayExtra(jString(\"binary\"))) is not None:\n                    self._binary_content = QEBytes(bytes(contents.tolist()))\n                    self.foundBinary.emit(self._binary_content)\n        except Exception as e:  # exc would otherwise get lost\n            send_exception_to_crash_reporter(e)\n        finally:\n            self.finished.emit()\n\n    @pyqtSlot()\n    def _unbind(self):\n        if 'ANDROID_DATA' in os.environ:\n            activity.unbind(on_activity_result=self.on_qr_activity_result)\n\n    def _scan_qr_non_android(self):\n        data = QGuiApplication.clipboard().text()\n        self.foundText.emit(data)\n        self.finished.emit()\n        return\n"
  },
  {
    "path": "electrum/gui/qml/qerequestdetails.py",
    "content": "from enum import IntEnum\nfrom typing import Optional\nfrom urllib.parse import urlparse\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtEnum\n\nfrom electrum.logging import get_logger\nfrom electrum.invoices import (\n    PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, LN_EXPIRY_NEVER\n)\nfrom electrum.lnutil import MIN_FUNDING_SAT, RECEIVED\nfrom electrum.lnurl import LNURL3Data, request_lnurl_withdraw_callback, LNURLError\nfrom electrum.payment_identifier import PaymentIdentifier, PaymentIdentifierType\nfrom electrum.i18n import _\nfrom electrum.network import Network\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\n\nfrom .qewallet import QEWallet\nfrom .qetypes import QEAmount\nfrom .util import status_update_timer_interval\n\n\nclass QERequestDetails(QObject, QtEventListener):\n\n    @pyqtEnum\n    class Status(IntEnum):\n        Unpaid = PR_UNPAID\n        Expired = PR_EXPIRED\n        Unknown = PR_UNKNOWN\n        Paid = PR_PAID\n        Inflight = PR_INFLIGHT\n        Failed = PR_FAILED\n        Routing = PR_ROUTING\n        Unconfirmed = PR_UNCONFIRMED\n\n    _logger = get_logger(__name__)\n\n    detailsChanged = pyqtSignal()  # generic request properties changed signal\n    statusChanged = pyqtSignal()\n    needsLNURLUserInput = pyqtSignal()\n    lnurlError = pyqtSignal(str, str)  # code, message\n    busyChanged = pyqtSignal()\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._wallet = None  # type: Optional[QEWallet]\n        self._key = None\n        self._req = None\n        self._timer = None\n        self._amount = None\n\n        self._lnurlData = None  # type: Optional[dict]\n        self._busy = False\n\n        self._timer = QTimer(self)\n        self._timer.setSingleShot(True)\n        self._timer.timeout.connect(self.updateStatusString)\n\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n        if self._timer:\n            self._timer.stop()\n            self._timer = None\n\n    @qt_event_listener\n    def on_event_request_status(self, wallet, key, status):\n        if wallet == self._wallet.wallet and key == self._key:\n            self._logger.debug('request status %d for key %s' % (status, key))\n            self.statusChanged.emit()\n\n    walletChanged = pyqtSignal()\n    @pyqtProperty(QEWallet, notify=walletChanged)\n    def wallet(self):\n        return self._wallet\n\n    @wallet.setter\n    def wallet(self, wallet: QEWallet):\n        if self._wallet != wallet:\n            self._wallet = wallet\n            self.walletChanged.emit()\n            self.initRequest()\n\n    keyChanged = pyqtSignal()\n    @pyqtProperty(str, notify=keyChanged)\n    def key(self):\n        return self._key\n\n    @key.setter\n    def key(self, key):\n        if self._key != key:\n            self._key = key\n            self._logger.debug(f'key={key}')\n            self.keyChanged.emit()\n            self.initRequest()\n\n    @pyqtProperty(int, notify=statusChanged)\n    def status(self):\n        return self._wallet.wallet.get_invoice_status(self._req)\n\n    @pyqtProperty(str, notify=statusChanged)\n    def status_str(self):\n        return self._req.get_status_str(self.status) if self._req else ''\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def isLightning(self):\n        return self._req.is_lightning() if self._req else False\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def address(self):\n        addr = self._req.get_address() if self._req else ''\n        return addr if addr else ''\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def message(self):\n        return self._req.get_message() if self._req else ''\n\n    @pyqtProperty(QEAmount, notify=detailsChanged)\n    def amount(self):\n        return self._amount\n\n    @pyqtProperty(int, notify=detailsChanged)\n    def timestamp(self):\n        return self._req.get_time()\n\n    @pyqtProperty(int, notify=detailsChanged)\n    def expiration(self):\n        return self._req.get_expiration_date()\n\n    @pyqtProperty(str, notify=statusChanged)\n    def paidTxid(self):\n        \"\"\"only used when Request status is PR_PAID\"\"\"\n        if not self._req:\n            return ''\n        is_paid, conf_needed, txids = self._wallet.wallet._is_onchain_invoice_paid(self._req)\n        if len(txids) > 0:\n            return txids[0]\n        return ''\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def bolt11(self):\n        wallet = self._wallet.wallet\n        if not wallet.lnworker:\n            return ''\n        amount_sat = self._req.get_amount_sat() or 0 if self._req else 0\n        can_receive = wallet.lnworker.num_sats_can_receive()\n        will_req_zeroconf = wallet.lnworker.receive_requires_jit_channel(amount_msat=amount_sat*1000)\n        if self._req and ((can_receive > 0 and amount_sat <= can_receive)\n                          or (will_req_zeroconf and amount_sat >= MIN_FUNDING_SAT)):\n            bolt11 = wallet.get_bolt11_invoice(self._req)\n        else:\n            return ''\n        # encode lightning invoices as uppercase so QR encoding can use\n        # alphanumeric mode; resulting in smaller QR codes\n        bolt11 = bolt11.upper()\n        return bolt11\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def bip21(self):\n        return self._req.get_bip21_URI() if self._req else ''\n\n    @pyqtProperty('QVariantMap', notify=detailsChanged)\n    def lnurlData(self) -> Optional[dict]:\n        return self._lnurlData\n\n    @pyqtProperty(bool, notify=busyChanged)\n    def busy(self):\n        return self._busy\n\n    def initRequest(self):\n        if self._wallet is None or self._key is None:\n            return\n\n        self._req = self._wallet.wallet.get_request(self._key)\n\n        if self._req is None:\n            self._logger.error(f'payment request key {self._key} unknown in wallet {self._wallet.name}')\n            return\n\n        self._amount = QEAmount(from_invoice=self._req)\n\n        self.detailsChanged.emit()\n        self.statusChanged.emit()\n        self.set_status_timer()\n\n    def set_status_timer(self):\n        if self.status == PR_UNPAID:\n            if self.expiration > 0 and self.expiration != LN_EXPIRY_NEVER:\n                self._logger.debug(f'set_status_timer, expiration={self.expiration}')\n                interval = status_update_timer_interval(self.expiration)\n                if interval > 0:\n                    self._logger.debug(f'setting status update timer to {interval}')\n                    self._timer.setInterval(interval)  # msec\n                    self._timer.start()\n\n    @pyqtSlot()\n    def updateStatusString(self):\n        self.statusChanged.emit()\n        self.set_status_timer()\n\n    @pyqtSlot(object)\n    def fromResolvedPaymentIdentifier(self, resolved_pi: PaymentIdentifier) -> None:\n        \"\"\"\n        Called when a payment identifier is resolved to a request (currently only LNURLW, but\n        could also be used for other \"voucher\" type input like redeeming ecash tokens or\n        some bolt12 thing).\n        \"\"\"\n        if not self._wallet:\n            return\n        if resolved_pi.type == PaymentIdentifierType.LNURLW:\n            lnurldata = resolved_pi.lnurl_data\n            assert isinstance(lnurldata, LNURL3Data), \"Expected LNURL3Data type\"\n            self._lnurlData = {\n                'domain': urlparse(lnurldata.callback_url).netloc,\n                'callback_url': lnurldata.callback_url,\n                'min_withdrawable_sat': lnurldata.min_withdrawable_sat,\n                'max_withdrawable_sat': lnurldata.max_withdrawable_sat,\n                'default_description': lnurldata.default_description,\n                'k1': lnurldata.k1,\n            }\n            self.needsLNURLUserInput.emit()\n        else:\n            raise NotImplementedError(\"Cannot request withdrawal for this payment identifier type\")\n\n    @pyqtSlot(int)\n    def lnurlRequestWithdrawal(self, amount_sat: int) -> None:\n        assert self._lnurlData\n        self._logger.debug(f'requesting lnurlw: {repr(self._lnurlData)}')\n\n        try:\n            key = self._wallet.wallet.create_request(\n                amount_sat=amount_sat,\n                message=self._lnurlData.get('default_description', ''),\n                exp_delay=120,\n                address=None,\n            )\n            req = self._wallet.wallet.get_request(key)\n            info = self._wallet.wallet.lnworker.get_payment_info(req.payment_hash, direction=RECEIVED)\n            _lnaddr, b11_invoice = self._wallet.wallet.lnworker.get_bolt11_invoice(\n                payment_info=info,\n                message=req.get_message(),\n                fallback_address=None,\n            )\n        except Exception as e:\n            self._logger.exception('')\n            self.lnurlError.emit(\n                'lnurl',\n                _(\"Failed to create payment request for withdrawal: {}\").format(str(e))\n            )\n            return\n\n        self._busy = True\n        self.busyChanged.emit()\n\n        coro = request_lnurl_withdraw_callback(\n            callback_url=self._lnurlData['callback_url'],\n            k1=self._lnurlData['k1'],\n            bolt_11=b11_invoice,\n        )\n        try:\n            Network.run_from_another_thread(coro)\n        except LNURLError as e:\n            self.lnurlError.emit('lnurl', str(e))\n        finally:\n            self._busy = False\n            self.busyChanged.emit()\n"
  },
  {
    "path": "electrum/gui/qml/qeserverlistmodel.py",
    "content": "from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot\nfrom PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex\n\nfrom electrum.logging import get_logger\nfrom electrum.util import Satoshis\nfrom electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL\nfrom electrum import blockchain\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\n\n\nclass QEServerListModel(QAbstractListModel, QtEventListener):\n    _logger = get_logger(__name__)\n\n    # define listmodel rolemap\n    _ROLE_NAMES=('name', 'address', 'is_connected', 'is_primary', 'is_tor', 'chain', 'height')\n    _ROLE_KEYS = range(Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole + len(_ROLE_NAMES))\n    _ROLE_MAP  = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))\n    _ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS))\n\n    def __init__(self, network, parent=None):\n        super().__init__(parent)\n\n        self._chaintips = 0\n        self._servers = []\n\n        self.network = network\n        self.initModel()\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.unregister_callbacks())\n\n    @qt_event_listener\n    def on_event_network_updated(self):\n        self._logger.info(f'network updated')\n        self.initModel()\n\n    @qt_event_listener\n    def on_event_blockchain_updated(self):\n        self._logger.info(f'blockchain updated')\n        self.initModel()\n\n    @qt_event_listener\n    def on_event_default_server_changed(self):\n        self._logger.info(f'default server changed')\n        self.initModel()\n\n    def rowCount(self, index):\n        return len(self._servers)\n\n    def roleNames(self):\n        return self._ROLE_MAP\n\n    def data(self, index, role):\n        server = self._servers[index.row()]\n        role_index = role - Qt.ItemDataRole.UserRole\n        value = server[self._ROLE_NAMES[role_index]]\n\n        if isinstance(value, (bool, list, int, str)) or value is None:\n            return value\n        if isinstance(value, Satoshis):\n            return value.value\n        return str(value)\n\n    def clear(self):\n        self.beginResetModel()\n        self._servers = []\n        self.endResetModel()\n\n    chaintipsChanged = pyqtSignal()\n    @pyqtProperty(int, notify=chaintipsChanged)\n    def chaintips(self):\n        return self._chaintips\n\n    def get_chains(self):\n        chains = self.network.get_blockchains()\n        n_chains = len(chains)\n        if n_chains != self._chaintips:\n            self._chaintips = n_chains\n            self.chaintipsChanged.emit()\n        return chains\n\n    @pyqtSlot()\n    def initModel(self):\n        self.clear()\n\n        servers = []\n\n        chains = self.get_chains()\n\n        for chain_id, interfaces in chains.items():\n            self._logger.debug(f'chain {chain_id} has {len(interfaces)} interfaces')\n            b = blockchain.blockchains.get(chain_id)\n            if b is None:\n                continue\n\n            name = b.get_name()\n\n            self._logger.debug(f'chain {chain_id} has name={name}, max_forkpoint=@{b.get_max_forkpoint()}, height={b.height()}')\n\n            for i in interfaces:\n                server = {\n                    'chain': name,\n                    'chain_height': b.height(),\n                    'is_primary': i == self.network.interface,\n                    'is_connected': True,\n                    'name': str(i.server),\n                    'address': i.server.to_friendly_name(),\n                    'height': i.tip\n                }\n\n                servers.append(server)\n\n        # disconnected servers\n        for s in self.network.get_disconnected_server_addrs():\n            server = {\n                'chain': '',\n                'chain_height': 0,\n                'height': 0,\n                'is_primary': False,\n                'is_connected': False,\n                'name': s.to_friendly_name()\n            }\n            server['address'] = server['name']\n\n            servers.append(server)\n\n        self.beginInsertRows(QModelIndex(), 0, len(servers) - 1)\n        self._servers = servers\n        self.endInsertRows()\n"
  },
  {
    "path": "electrum/gui/qml/qeswaphelper.py",
    "content": "import asyncio\nimport bisect\nfrom enum import IntEnum\nfrom typing import Union, Optional, TYPE_CHECKING, Sequence\n\nfrom PyQt6.QtCore import (pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtEnum, QAbstractListModel, Qt,\n                          QModelIndex)\nfrom PyQt6.QtGui import QColor\n\nfrom electrum.i18n import _\nfrom electrum.bitcoin import DummyAddress\nfrom electrum.logging import get_logger\nfrom electrum.transaction import PartialTxOutput, PartialTransaction\nfrom electrum.util import (NotEnoughFunds, NoDynamicFeeEstimates, profiler, get_asyncio_loop, age,\n                           wait_for2, send_exception_to_crash_reporter)\nfrom electrum.submarine_swaps import NostrTransport, SwapServerTransport, pubkey_to_rgb_color\nfrom electrum.fee_policy import FeePolicy\n\nfrom electrum.gui import messages\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\n\nfrom .auth import AuthMixin, auth_protect\nfrom .qetypes import QEAmount\nfrom .qewallet import QEWallet\n\nif TYPE_CHECKING:\n    import concurrent.futures\n    from electrum.submarine_swaps import SwapOffer\n\n\nclass InvalidSwapParameters(Exception): pass\n\n\nclass QESwapServerNPubListModel(QAbstractListModel):\n    _logger = get_logger(__name__)\n\n    # define listmodel rolemap\n    _ROLE_NAMES= ('npub', 'server_pubkey', 'timestamp', 'percentage_fee', 'mining_fee',\n                  'min_amount', 'max_forward_amount', 'max_reverse_amount', 'pow_bits', 'color')\n    _ROLE_KEYS = range(Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole + len(_ROLE_NAMES))\n    _ROLE_MAP  = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))\n\n    def __init__(self, config, parent=None):\n        super().__init__(parent)\n        self.config = config\n        self._services = []\n\n    def rowCount(self, index):\n        return len(self._services)\n\n    # also expose rowCount as a property\n    countChanged = pyqtSignal()\n    @pyqtProperty(int, notify=countChanged)\n    def count(self):\n        return len(self._services)\n\n    def roleNames(self):\n        return self._ROLE_MAP\n\n    def data(self, index, role):\n        service = self._services[index.row()]\n        role_index = role - Qt.ItemDataRole.UserRole\n        value = service[self._ROLE_NAMES[role_index]]\n        if isinstance(value, (bool, list, int, str, QColor)) or value is None:\n            return value\n        return str(value)\n\n    def clear(self):\n        self.beginResetModel()\n        self._services = []\n        self.endResetModel()\n\n    def offer_to_model(self, x: 'SwapOffer'):\n        return {\n            'npub': x.server_npub,\n            'server_pubkey': x.server_pubkey,\n            'percentage_fee': float(x.pairs.percentage),\n            'mining_fee': x.pairs.mining_fee,\n            'min_amount': x.pairs.min_amount,\n            'max_forward_amount': x.pairs.max_forward,\n            'max_reverse_amount': x.pairs.max_reverse,\n            'timestamp': age(x.timestamp),\n            'pow_bits': x.pow_bits,\n            'color': QColor(*pubkey_to_rgb_color(x.server_pubkey)),\n        }\n\n    def updateModel(self, items: Sequence['SwapOffer']):\n        offers = items.copy()\n\n        remove = []\n\n        for i, x in enumerate(self._services):\n            if matches := list(filter(lambda offer: offer.server_npub == x['npub'], offers)):\n                # update\n                self._services[i] = self.offer_to_model(matches[0])\n                index = self.index(i, 0)\n                self.dataChanged.emit(index, index, self._ROLE_KEYS)\n                offers.remove(matches[0])\n            else:\n                # add offer to remove items\n                remove.append(i)\n\n        # # remove offers from model\n        for ri in reversed(remove):\n            self.beginRemoveRows(QModelIndex(), ri, ri)\n            self._services.pop(ri)\n            self.endRemoveRows()\n\n        # add new offers\n        if offers:\n            for offer in offers:\n                # offers are sorted by pow_bits\n                insertion_index = bisect.bisect_left(\n                    self._services,\n                    -offer.pow_bits,  # negate the values to get ascending order\n                    key=lambda service: -service['pow_bits'],\n                )\n                self.beginInsertRows(QModelIndex(), insertion_index, insertion_index)\n                self._services.insert(insertion_index, self.offer_to_model(offer))\n                self.endInsertRows()\n\n        if offers or remove:\n            self.countChanged.emit()\n\n    @pyqtSlot(str, result=int)\n    def indexFor(self, npub: str):\n        for i, item in enumerate(self._services):\n            if npub == item['npub']:\n                return i\n        return -1\n\n\nclass QESwapHelper(AuthMixin, QObject, QtEventListener):\n    _logger = get_logger(__name__)\n\n    MESSAGE_SWAP_HOWTO = ' '.join([\n            _('Move the slider to set the amount and direction of the swap.'),\n            _('Swapping lightning funds for onchain funds will increase your capacity to receive lightning payments.'),\n        ])\n\n    @pyqtEnum\n    class State(IntEnum):\n        Initializing = 0\n        Initialized = 1\n        NoService = 2\n        ServiceReady = 3\n        Started = 4\n        Failed = 5\n        Success = 6\n        Cancelled = 7\n\n    confirm = pyqtSignal([str], arguments=['message'])\n    error = pyqtSignal([str], arguments=['message'])\n    undefinedNPub = pyqtSignal()\n    offersUpdated = pyqtSignal()\n    requestTxUpdate = pyqtSignal()\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._wallet = None  # type: Optional[QEWallet]\n        self._sliderPos = 0\n        self._rangeMin = -1\n        self._rangeMax = 1\n        self._tx = None\n        self._valid = False\n        self._state = QESwapHelper.State.Initialized\n        self._userinfo = QESwapHelper.MESSAGE_SWAP_HOWTO\n        self._tosend = QEAmount()\n        self._toreceive = QEAmount()\n        self._serverfeeperc = ''\n        self._server_miningfee = QEAmount()\n        self._miningfee = QEAmount()\n        self._isReverse = False\n        self._canCancel = False\n        self._swap = None\n        self._fut_htlc_wait = None\n\n        self._service_available = False\n        self._send_amount = 0\n        self._receive_amount = 0\n\n        self._leftVoid = 0\n        self._rightVoid = 0\n\n        self._available_swapservers = None\n\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n        self._fwd_swap_updatetx_timer = QTimer(self)\n        self._fwd_swap_updatetx_timer.setSingleShot(True)\n        self._fwd_swap_updatetx_timer.timeout.connect(self.fwd_swap_updatetx)\n        self.requestTxUpdate.connect(self.tx_update_pushback_timer)\n\n        self.offersUpdated.connect(self.on_offers_updated)\n        self.transport_task: Optional[asyncio.Task] = None\n        self.swap_transport: Optional[SwapServerTransport] = None\n        self.recent_offers = []\n\n    def on_destroy(self):\n        if self.transport_task is not None:\n            self.transport_task.cancel()\n        self.unregister_callbacks()\n\n    walletChanged = pyqtSignal()\n    @pyqtProperty(QEWallet, notify=walletChanged)\n    def wallet(self):\n        return self._wallet\n\n    @wallet.setter\n    def wallet(self, wallet: QEWallet):\n        if self._wallet != wallet:\n            self._wallet = wallet\n            self.run_swap_manager()\n            self.walletChanged.emit()\n\n    sliderPosChanged = pyqtSignal()\n    @pyqtProperty(float, notify=sliderPosChanged)\n    def sliderPos(self):\n        return self._sliderPos\n\n    @sliderPos.setter\n    def sliderPos(self, sliderPos):\n        if self._sliderPos != sliderPos:\n            self._sliderPos = sliderPos\n            self.swap_slider_moved()\n            self.sliderPosChanged.emit()\n\n    rangeMinChanged = pyqtSignal()\n    @pyqtProperty(float, notify=rangeMinChanged)\n    def rangeMin(self):\n        return self._rangeMin\n\n    @rangeMin.setter\n    def rangeMin(self, rangeMin):\n        if self._rangeMin != rangeMin:\n            self._rangeMin = rangeMin\n            self.rangeMinChanged.emit()\n\n    rangeMaxChanged = pyqtSignal()\n    @pyqtProperty(float, notify=rangeMaxChanged)\n    def rangeMax(self):\n        return self._rangeMax\n\n    @rangeMax.setter\n    def rangeMax(self, rangeMax):\n        if self._rangeMax != rangeMax:\n            self._rangeMax = rangeMax\n            self.rangeMaxChanged.emit()\n\n    leftVoidChanged = pyqtSignal()\n    @pyqtProperty(float, notify=leftVoidChanged)\n    def leftVoid(self):\n        return self._leftVoid\n\n    rightVoidChanged = pyqtSignal()\n    @pyqtProperty(float, notify=rightVoidChanged)\n    def rightVoid(self):\n        return self._rightVoid\n\n    validChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=validChanged)\n    def valid(self):\n        return self._valid\n\n    @valid.setter\n    def valid(self, valid):\n        if self._valid != valid:\n            self._valid = valid\n            self.validChanged.emit()\n\n    stateChanged = pyqtSignal()\n    @pyqtProperty(int, notify=stateChanged)\n    def state(self):\n        return self._state\n\n    @state.setter\n    def state(self, state):\n        if self._state != state:\n            self._state = state\n            self.stateChanged.emit()\n\n    userinfoChanged = pyqtSignal()\n    @pyqtProperty(str, notify=userinfoChanged)\n    def userinfo(self):\n        return self._userinfo\n\n    @userinfo.setter\n    def userinfo(self, userinfo):\n        if self._userinfo != userinfo:\n            self._userinfo = userinfo\n            self.userinfoChanged.emit()\n\n    tosendChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=tosendChanged)\n    def tosend(self):\n        return self._tosend\n\n    @tosend.setter\n    def tosend(self, tosend):\n        if self._tosend != tosend:\n            self._tosend = tosend\n            self.tosendChanged.emit()\n\n    toreceiveChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=toreceiveChanged)\n    def toreceive(self):\n        return self._toreceive\n\n    @toreceive.setter\n    def toreceive(self, toreceive):\n        if self._toreceive != toreceive:\n            self._toreceive = toreceive\n            self.toreceiveChanged.emit()\n\n    serverMiningfeeChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=serverMiningfeeChanged)\n    def serverMiningfee(self):\n        return self._server_miningfee\n\n    @serverMiningfee.setter\n    def serverMiningfee(self, server_miningfee):\n        if self._server_miningfee != server_miningfee:\n            self._server_miningfee = server_miningfee\n            self.serverMiningfeeChanged.emit()\n\n    serverfeepercChanged = pyqtSignal()\n    @pyqtProperty(str, notify=serverfeepercChanged)\n    def serverfeeperc(self):\n        return self._serverfeeperc\n\n    @serverfeeperc.setter\n    def serverfeeperc(self, serverfeeperc):\n        if self._serverfeeperc != serverfeeperc:\n            self._serverfeeperc = serverfeeperc\n            self.serverfeepercChanged.emit()\n\n    miningfeeChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=miningfeeChanged)\n    def miningfee(self):\n        return self._miningfee\n\n    @miningfee.setter\n    def miningfee(self, miningfee):\n        if self._miningfee != miningfee:\n            self._miningfee = miningfee\n            self.miningfeeChanged.emit()\n\n    isReverseChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=isReverseChanged)\n    def isReverse(self):\n        return self._isReverse\n\n    @isReverse.setter\n    def isReverse(self, isReverse):\n        if self._isReverse != isReverse:\n            self._isReverse = isReverse\n            self.isReverseChanged.emit()\n\n    canCancelChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=canCancelChanged)\n    def canCancel(self):\n        return self._canCancel\n\n    @canCancel.setter\n    def canCancel(self, canCancel):\n        if self._canCancel != canCancel:\n            self._canCancel = canCancel\n            self.canCancelChanged.emit()\n\n    availableSwapServersChanged = pyqtSignal()\n    @pyqtProperty(QESwapServerNPubListModel, notify=availableSwapServersChanged)\n    def availableSwapServers(self):\n        if not self._available_swapservers:\n            self._available_swapservers = QESwapServerNPubListModel(self._wallet.wallet.config)\n\n        return self._available_swapservers\n\n    def on_offers_updated(self):\n        self.availableSwapServers.updateModel(self.recent_offers)\n\n    @pyqtSlot(result=bool)\n    def isNostr(self):\n        return True  # TODO\n\n    def run_swap_manager(self):\n        self._logger.debug('run_swap_manager')\n        if (lnworker := self._wallet.wallet.lnworker) is None:\n            return\n        swap_manager = lnworker.swap_manager\n\n        assert not swap_manager.is_server, 'running as swap server not supported'\n\n        # if not self._wallet.wallet.config.SWAPSERVER_URL and not self._wallet.wallet.config.SWAPSERVER_NPUB:  # TODO enable nostr\n        #     self._logger.debug('nostr is preferred but swapserver npub still undefined')\n\n        # FIXME: clearing is_initialized, we might be called because the npub was changed\n        swap_manager.is_initialized.clear()\n        self.state = QESwapHelper.State.Initialized if swap_manager.is_initialized.is_set() else QESwapHelper.State.Initializing\n\n        swap_transport = swap_manager.create_transport()\n\n        async def swap_transport_task(transport: SwapServerTransport):\n            async with transport:\n                self.swap_transport = transport\n                if not swap_manager.is_initialized.is_set():\n                    self.userinfo = _('Initializing...')\n                    try:\n                        # is_initialized is set if we receive the event of our configured SWAPSERVER_NPUB\n                        # This will timeout if no server is configured, or our server didn't publish recently.\n                        timeout = transport.connect_timeout + 1\n                        await wait_for2(swap_manager.is_initialized.wait(), timeout=timeout)\n                        self._logger.debug('swapmanager initialized')\n                        self.state = QESwapHelper.State.Initialized\n                    except asyncio.TimeoutError:\n                        # only fail if we didn't get any offers or couldn't connect at all\n                        # otherwise the timeout just means that no offer of the selected npub has\n                        # been found (or that there is no npub selected at all), so the prompt should open\n                        if isinstance(transport, NostrTransport) and not transport.is_connected.is_set():\n                            self.userinfo = _('Error') + ': ' + '\\n'.join([\n                                _('Could not connect to a Nostr relay.'),\n                                _('Please check your relays and network connection')\n                            ])\n                            self.state = QESwapHelper.State.NoService\n                            return\n                        elif not isinstance(transport, NostrTransport) or not transport.get_recent_offers():\n                            self._logger.debug('Could not find a swap provider.')\n                            self.userinfo = _('Could not find a swap provider.')\n                            self.state = QESwapHelper.State.NoService\n                            return\n                    except Exception as e:\n                        try:  # swaphelper might be destroyed at this point\n                            self.userinfo = _('Error') + ': ' + str(e)\n                            self.state = QESwapHelper.State.NoService\n                            self._logger.error(str(e))\n                        except RuntimeError:\n                            pass\n                        return\n\n                if isinstance(transport, NostrTransport) and not swap_manager.is_initialized.is_set():\n                    # not is_initialized.is_set() = configured provider was not found (or no provider configured)\n                    # prompt user to select a swapserver\n                    self.recent_offers = transport.get_recent_offers()\n                    self.offersUpdated.emit()\n                    self.undefinedNPub.emit()\n                elif swap_manager.is_initialized.is_set():\n                    self.setReadyState()\n\n                while True:\n                    # keep fetching new incoming offer events\n                    # the slider range will not get updated continuously as it would irritate the user\n                    if isinstance(transport, NostrTransport):\n                        if (recent_offers := transport.get_recent_offers()) != self.recent_offers:\n                            self._logger.debug(f\"received new swap offer\")\n                            self.recent_offers = recent_offers\n                            self.offersUpdated.emit()\n                    await asyncio.sleep(1)\n\n        def transport_closed_cb(fut: 'concurrent.futures.Future'):\n            self.transport_task = None\n            if fut.cancelled():\n                return\n            exc = fut.exception()\n            if exc:\n                send_exception_to_crash_reporter(exc)\n\n        self.transport_task = asyncio.run_coroutine_threadsafe(\n            swap_transport_task(swap_transport),\n            get_asyncio_loop()\n        )\n        self.transport_task.add_done_callback(transport_closed_cb)\n\n    @pyqtSlot()\n    def npubSelectionCancelled(self):\n        if (self._wallet.wallet.config.SWAPSERVER_NPUB\n                not in [offer.server_npub for offer in self.recent_offers]):\n            self._logger.debug('nostr is preferred but swapserver npub still undefined')\n            if not self._wallet.wallet.config.SWAPSERVER_NPUB:\n                self.userinfo = _('No swap provider selected.')\n            else:\n                self.userinfo = _('Select one of the available swap providers.')\n            self.state = QESwapHelper.State.NoService\n\n    @pyqtSlot()\n    def setReadyState(self):\n        if self._wallet.wallet.config.SWAPSERVER_NPUB \\\n                or not isinstance(self.swap_transport, NostrTransport):\n            self.state = QESwapHelper.State.ServiceReady\n            self.userinfo = QESwapHelper.MESSAGE_SWAP_HOWTO\n            self.initSwapSliderRange()\n\n    def update_swap_manager_pair(self):\n        \"\"\"Updates the swap manager pairs to the recent pairs of the selected server\"\"\"\n        assert self.swap_transport is not None, \"No swap transport\"\n        if isinstance(self.swap_transport, NostrTransport):\n            swap_manager = self._wallet.wallet.lnworker.swap_manager\n            pair = self.swap_transport.get_offer(self._wallet.wallet.config.SWAPSERVER_NPUB)\n            swap_manager.update_pairs(pair.pairs)\n\n    @pyqtSlot()\n    def initSwapSliderRange(self):\n        lnworker = self._wallet.wallet.lnworker\n        swap_manager = lnworker.swap_manager\n        # update the swap_manager pair so the newest available data is used below\n        self.update_swap_manager_pair()\n\n        \"\"\"Sets the minimal and maximal amount that can be swapped for the swap\n        slider.\"\"\"\n        # tx is updated again afterwards with send_amount in case of normal swap\n        # this is just to estimate the maximal spendable onchain amount for HTLC\n        self.update_tx('!')\n        try:\n            max_onchain_spend = self._tx.output_value_for_address(DummyAddress.SWAP)\n        except AttributeError:  # happens if there are no utxos\n            max_onchain_spend = 0\n        reverse = int(min(lnworker.num_sats_can_send(),\n                          swap_manager.get_provider_max_forward_amount()))\n        max_recv_amt_ln = min(swap_manager.get_provider_max_reverse_amount(), int(lnworker.num_sats_can_receive()))\n        max_recv_amt_oc = swap_manager.get_send_amount(max_recv_amt_ln, is_reverse=False) or 0\n        forward = int(min(max_recv_amt_oc,\n                          # maximally supported swap amount by provider\n                          swap_manager.get_provider_max_reverse_amount(),\n                          max_onchain_spend))\n        # we expect range to adjust the value of the swap slider to be in the\n        # correct range, i.e., to correct an overflow when reducing the limits\n        self._logger.debug(f'Slider range {-reverse} - {forward}. Pos {self._sliderPos}')\n        self.rangeMin = -reverse\n        self.rangeMax = forward\n        # percentage of void, right or left\n        if reverse < forward:\n            self._leftVoid = 0.5 * (forward - reverse) / forward\n            self._rightVoid = 0\n        elif reverse > forward:\n            self._leftVoid = 0\n            self._rightVoid = - 0.5 * (forward - reverse) / reverse\n        else:\n            self._leftVoid = 0\n            self._rightVoid = 0\n        self.leftVoidChanged.emit()\n        self.rightVoidChanged.emit()\n\n        if not self.rangeMin <= self._sliderPos <= self.rangeMax:\n            # clamp the slider pos into the given limits\n            if abs(self._sliderPos - self.rangeMin) < abs(self._sliderPos - self.rangeMax):\n                self._sliderPos = self.rangeMin\n            else:\n                self._sliderPos = self.rangeMax\n        self.swap_slider_moved()\n\n    @profiler\n    def update_tx(self, onchain_amount: Union[int, str]):\n        \"\"\"Updates the transaction associated with a forward swap.\"\"\"\n        if onchain_amount is None:\n            self._tx = None\n            self.valid = False\n            return\n        outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]\n        coins = self._wallet.wallet.get_spendable_coins(None)\n        fee_policy = FeePolicy('eta:2')\n        try:\n            self._tx = self._wallet.wallet.make_unsigned_transaction(\n                coins=coins,\n                outputs=outputs,\n                fee_policy=fee_policy)\n        except (NotEnoughFunds, NoDynamicFeeEstimates):\n            self._tx = None\n            self.valid = False\n\n    @qt_event_listener\n    def on_event_fee_histogram(self, *args):\n        self.swap_slider_moved()\n\n    @qt_event_listener\n    def on_event_fee(self, *args):\n        self.swap_slider_moved()\n\n    def swap_slider_moved(self):\n        if self._state in [QESwapHelper.State.Initializing, QESwapHelper.State.Initialized, QESwapHelper.State.NoService]:\n            return\n\n        position = int(self._sliderPos)\n\n        swap_manager = self._wallet.wallet.lnworker.swap_manager\n\n        # pay_amount and receive_amounts are always with fees already included\n        # so they reflect the net balance change after the swap\n        self.isReverse = (position < 0)\n        self._send_amount = abs(position)\n        self.tosend = QEAmount(amount_sat=self._send_amount)\n        self._receive_amount = swap_manager.get_recv_amount(send_amount=self._send_amount, is_reverse=self.isReverse)\n        self.toreceive = QEAmount(amount_sat=self._receive_amount)\n        # fee breakdown\n        self.serverfeeperc = f'{swap_manager.percentage:0.2f}%'\n        server_miningfee = swap_manager.mining_fee\n        self.serverMiningfee = QEAmount(amount_sat=server_miningfee)\n        if self.isReverse:\n            self.miningfee = QEAmount(amount_sat=swap_manager.get_fee_for_txbatcher())\n            self.check_valid(self._send_amount, self._receive_amount)\n        else:\n            # update tx only if slider isn't moved for a while\n            self.valid = False\n            # trigger tx_update_pushback_timer through signal, as this might be called from other thread\n            self.requestTxUpdate.emit()\n\n    def tx_update_pushback_timer(self):\n        self._fwd_swap_updatetx_timer.start(250)\n\n    def check_valid(self, send_amount, receive_amount):\n        if send_amount and receive_amount:\n            self.valid = True\n        else:\n            # add more nuanced error reporting?\n            self.valid = False\n\n    def fwd_swap_updatetx(self):\n        # if slider is on reverse swap side when timer hits, ignore\n        if self.isReverse:\n            return\n        self.update_tx(self._send_amount)\n        # add lockup fees, but the swap amount is position\n        pay_amount = self._send_amount + self._tx.get_fee() if self._tx else 0\n        self.miningfee = QEAmount(amount_sat=self._tx.get_fee()) if self._tx else QEAmount()\n        self.check_valid(pay_amount, self._receive_amount)\n\n    def do_normal_swap(self, lightning_amount, onchain_amount):\n        assert self._tx\n        if lightning_amount is None or onchain_amount is None:\n            return\n\n        async def swap_task():\n            assert self.swap_transport is not None, \"Swap transport not available\"\n            try:\n                dummy_tx = self._create_tx(onchain_amount)\n                self.userinfo = _('Performing swap...')\n                self.state = QESwapHelper.State.Started\n                self._swap, invoice = await self._wallet.wallet.lnworker.swap_manager.request_normal_swap(\n                    transport=self.swap_transport,\n                    lightning_amount_sat=lightning_amount,\n                    expected_onchain_amount_sat=onchain_amount,\n                )\n\n                tx = self._wallet.wallet.lnworker.swap_manager.create_funding_tx(self._swap, dummy_tx, password=self._wallet.password)\n                coro2 = self._wallet.wallet.lnworker.swap_manager.wait_for_htlcs_and_broadcast(\n                    transport=self.swap_transport, swap=self._swap, invoice=invoice, tx=tx)\n                self._fut_htlc_wait = fut = asyncio.create_task(coro2)\n\n                self.canCancel = True\n                txid = await fut\n                try:  # swaphelper might be destroyed at this point\n                    if txid:\n                        self.userinfo = _('Success!')\n                        self.state = QESwapHelper.State.Success\n                    else:\n                        self.userinfo = _('Swap failed!')\n                        self.state = QESwapHelper.State.Failed\n                except RuntimeError:\n                    pass\n            except asyncio.CancelledError:\n                self._wallet.wallet.lnworker.swap_manager.cancel_normal_swap(self._swap)\n                self.userinfo = _('Swap cancelled')\n                self.state = QESwapHelper.State.Cancelled\n            except Exception as e:\n                try:  # swaphelper might be destroyed at this point\n                    self.state = QESwapHelper.State.Failed\n                    self.userinfo = _('Error') + ': ' + str(e)\n                    self._logger.error(str(e))\n                except RuntimeError:\n                    pass\n            finally:\n                try:  # swaphelper might be destroyed at this point\n                    self.canCancel = False\n                    self._swap = None\n                    self._fut_htlc_wait = None\n                except RuntimeError:\n                    pass\n\n        asyncio.run_coroutine_threadsafe(swap_task(), get_asyncio_loop())\n\n    def _create_tx(self, onchain_amount: Union[int, str, None]) -> PartialTransaction:\n        # TODO: func taken from qt GUI, this should be common code\n        assert not self.isReverse\n        if onchain_amount is None:\n            raise InvalidSwapParameters(\"onchain_amount is None\")\n        # coins = self.window.get_coins()\n        coins = self._wallet.wallet.get_spendable_coins()\n        if onchain_amount == '!':\n            max_amount = sum(c.value_sats() for c in coins)\n            max_swap_amount = self._wallet.wallet.lnworker.swap_manager.client_max_amount_forward_swap()\n            if max_swap_amount is None:\n                raise InvalidSwapParameters(\"swap_manager.client_max_amount_forward_swap() is None\")\n            if max_amount > max_swap_amount:\n                onchain_amount = max_swap_amount\n        outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]\n        fee_policy = FeePolicy('eta:2')\n        try:\n            tx = self._wallet.wallet.make_unsigned_transaction(\n                coins=coins,\n                outputs=outputs,\n                send_change_to_lightning=False,\n                fee_policy=fee_policy\n            )\n        except (NotEnoughFunds, NoDynamicFeeEstimates) as e:\n            raise InvalidSwapParameters(str(e)) from e\n        return tx\n\n    def do_reverse_swap(self, lightning_amount, onchain_amount):\n        if lightning_amount is None or onchain_amount is None:\n            return\n\n        async def swap_task():\n            assert self.swap_transport is not None, \"Swap transport not available\"\n            swap_manager = self._wallet.wallet.lnworker.swap_manager\n            try:\n                self.userinfo = _('Performing swap...')\n                self.state = QESwapHelper.State.Started\n                await swap_manager.is_initialized.wait()\n                txid = await swap_manager.reverse_swap(\n                    transport=self.swap_transport,\n                    lightning_amount_sat=lightning_amount,\n                    expected_onchain_amount_sat=onchain_amount + swap_manager.get_fee_for_txbatcher(),\n                    prepayment_sat=2 * self.serverMiningfee.satsInt,\n                )\n                try:  # swaphelper might be destroyed at this point\n                    if txid:\n                        self.userinfo = _('Success!')\n                        self.state = QESwapHelper.State.Success\n                    else:\n                        self.userinfo = _('Swap failed!')\n                        self.state = QESwapHelper.State.Failed\n                except RuntimeError:\n                    pass\n            except Exception as e:\n                try:  # swaphelper might be destroyed at this point\n                    self.state = QESwapHelper.State.Failed\n                    msg = _('Timeout') if isinstance(e, TimeoutError) else str(e)\n                    self.userinfo = _('Error') + ': ' + msg\n                    self._logger.error(str(e))\n                except RuntimeError:\n                    pass\n\n        asyncio.run_coroutine_threadsafe(swap_task(), get_asyncio_loop())\n\n    @pyqtSlot()\n    def executeSwap(self):\n        if not self._wallet.wallet.network:\n            self.error.emit(_(\"You are offline.\"))\n            return\n        self._do_execute_swap()\n\n    @auth_protect(message=_('Confirm Lightning swap?'))\n    def _do_execute_swap(self):\n        if self.isReverse:\n            lightning_amount = self._send_amount\n            onchain_amount = self._receive_amount\n            self.do_reverse_swap(lightning_amount, onchain_amount)\n        else:\n            lightning_amount = self._receive_amount\n            onchain_amount = self._send_amount\n            self.do_normal_swap(lightning_amount, onchain_amount)\n\n    @pyqtSlot()\n    def cancelNormalSwap(self):\n        assert self._swap\n        self.canCancel = False\n        self._fut_htlc_wait.cancel()\n"
  },
  {
    "path": "electrum/gui/qml/qetransactionlistmodel.py",
    "content": "from datetime import datetime, timedelta\nfrom typing import TYPE_CHECKING, Dict, Any\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot\nfrom PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex\n\nfrom electrum.logging import get_logger\nfrom electrum.util import Satoshis, TxMinedInfo\nfrom electrum.address_synchronizer import TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\n\nfrom .qetypes import QEAmount\n\nif TYPE_CHECKING:\n    from electrum.wallet import Abstract_Wallet\n\n\nclass QETransactionListModel(QAbstractListModel, QtEventListener):\n    _logger = get_logger(__name__)\n\n    # define listmodel rolemap\n    _ROLE_NAMES=('txid', 'fee_sat', 'height', 'confirmations', 'timestamp', 'monotonic_timestamp',\n                 'incoming', 'value', 'date', 'label', 'txpos_in_block', 'fee',\n                 'inputs', 'outputs', 'section', 'type', 'lightning', 'payment_hash', 'key', 'complete')\n    _ROLE_KEYS = range(Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole + len(_ROLE_NAMES))\n    _ROLE_MAP  = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))\n    _ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS))\n\n    requestRefresh = pyqtSignal()\n\n    def __init__(self, wallet: 'Abstract_Wallet', parent=None, *, onchain_domain=None, include_lightning=True):\n        super().__init__(parent)\n        self.wallet = wallet\n        self.onchain_domain = onchain_domain\n        self.include_lightning = include_lightning\n\n        self.tx_history = []\n\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n        self.requestRefresh.connect(lambda: self.initModel())\n\n        self._dirty = True\n        self.initModel()\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    @qt_event_listener\n    def on_event_verified(self, wallet, txid, info):\n        if wallet == self.wallet:\n            self._logger.debug('verified event for txid %s' % txid)\n            self.on_tx_verified(txid, info)\n\n    @qt_event_listener\n    def on_event_adb_set_future_tx(self, adb, txid):\n        if adb != self.wallet.adb:\n            return\n        self._logger.debug(f'adb_set_future_tx event for txid {txid}')\n        for i, item in enumerate(self.tx_history):\n            if 'txid' in item and item['txid'] == txid:\n                self._update_future_txitem(i)\n                return\n\n    @qt_event_listener\n    def on_event_fee_histogram(self, histogram):\n        self._logger.debug(f'fee histogram updated')\n        for i, tx_item in enumerate(self.tx_history):\n            if 'height' not in tx_item:  # filter to on-chain\n                continue\n            if tx_item['confirmations'] > 0:  # filter out already mined\n                continue\n            txid = tx_item['txid']\n            tx = self.wallet.db.get_transaction(txid)\n            if not tx:\n                continue\n            txinfo = self.wallet.get_tx_info(tx)\n            status, status_str = self.wallet.get_tx_status(txid, txinfo.tx_mined_status)\n            tx_item['date'] = status_str\n            index = self.index(i, 0)\n            roles = [self._ROLE_RMAP['date']]\n            self.dataChanged.emit(index, index, roles)\n\n    @qt_event_listener\n    def on_event_labels_received(self, wallet, labels):\n        if wallet == self.wallet:\n            self.initModel(True)  # TODO: be less dramatic\n\n    def rowCount(self, index):\n        return len(self.tx_history)\n\n    # also expose rowCount as a property\n    countChanged = pyqtSignal()\n    @pyqtProperty(int, notify=countChanged)\n    def count(self):\n        return len(self.tx_history)\n\n    def roleNames(self):\n        return self._ROLE_MAP\n\n    def data(self, index, role):\n        tx = self.tx_history[index.row()]\n        role_index = role - Qt.ItemDataRole.UserRole\n\n        try:\n            value = tx[self._ROLE_NAMES[role_index]]\n        except KeyError as e:\n            self._logger.error(f'non-existing key \"{self._ROLE_NAMES[role_index]}\" requested')\n            value = None\n\n        if isinstance(value, (bool, list, int, str, QEAmount)) or value is None:\n            return value\n        if isinstance(value, Satoshis):\n            return value.value\n        return str(value)\n\n    @pyqtSlot()\n    def setDirty(self):\n        self._dirty = True\n\n    def clear(self):\n        self.beginResetModel()\n        self.tx_history = []\n        self.endResetModel()\n\n    def tx_to_model(self, tx_item):\n        #self._logger.debug(str(tx_item))\n        item = tx_item\n\n        item['key'] = item.get('txid') or item['payment_hash'] or item['group_id'] # fixme: this is fragile\n\n        if 'lightning' not in item:\n            item['lightning'] = False\n\n        if item['lightning']:\n            item['value'] = QEAmount(amount_sat=item['value'].value, amount_msat=item['amount_msat'])\n            item['incoming'] = True if item['amount_msat'] > 0 else False\n            item['confirmations'] = 0\n        else:\n            item['value'] = QEAmount(amount_sat=item['value'].value)\n\n        if 'txid' in item:\n            tx = self.wallet.db.get_transaction(item['txid'])\n            if tx:\n                item['complete'] = tx.is_complete()\n            else:  # due to races, tx might have already been removed from history\n                item['complete'] = False\n\n        # newly arriving txs, or (partially/fully signed) local txs have no (block) timestamp\n        # FIXME just use wallet.get_tx_status, and change that as needed\n        if not item['timestamp']:  # onchain: local or mempool or unverified txs\n            if not item['lightning']:\n                txid = item['txid']\n                assert txid\n                tx_mined_info = self._tx_mined_info_from_tx_item(tx_item)\n                item['section'] = 'local' if tx_mined_info.is_local_like() else 'mempool'\n                status, status_str = self.wallet.get_tx_status(txid, tx_mined_info=tx_mined_info)\n                item['date'] = status_str\n        else:  # lightning or already mined (and SPV-ed) onchain txs\n            item['section'] = self.get_section_by_timestamp(item['timestamp'])\n            item['date'] = self.format_date_by_section(item['section'], datetime.fromtimestamp(item['timestamp']))\n\n        return item\n\n    @staticmethod\n    def get_section_by_timestamp(timestamp):\n        txts = datetime.fromtimestamp(timestamp)\n        today = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)\n\n        if txts > today:\n            return 'today'\n        elif txts > today - timedelta(days=1):\n            return 'yesterday'\n        elif txts > today - timedelta(days=7):\n            return 'lastweek'\n        elif txts > today - timedelta(days=31):\n            return 'lastmonth'\n        else:\n            return 'older'\n\n    @staticmethod\n    def format_date_by_section(section: str, date: datetime):\n        # TODO: l10n\n        dfmt = {\n            'today': '%H:%M',\n            'yesterday': '%H:%M',\n            'lastweek': '%a, %H:%M',\n            'lastmonth': '%a %d, %H:%M',\n            'older': '%Y-%m-%d %H:%M'\n        }\n        if section not in dfmt:\n            section = 'older'\n        return date.strftime(dfmt[section])\n\n    @staticmethod\n    def _tx_mined_info_from_tx_item(tx_item: Dict[str, Any]) -> TxMinedInfo:\n        # FIXME a bit hackish to have to reconstruct the TxMinedInfo... same thing in qt-gui\n        tx_mined_info = TxMinedInfo(\n            _height=tx_item['height'],\n            conf=tx_item['confirmations'],\n            timestamp=tx_item['timestamp'],\n            wanted_height=tx_item.get('wanted_height', None),\n        )\n        return tx_mined_info\n\n    @pyqtSlot()\n    @pyqtSlot(bool)\n    def initModel(self, force: bool = False):\n        # only (re)construct if dirty or forced\n        if not self._dirty and not force:\n            return\n\n        self._logger.debug('retrieving history')\n        history = self.wallet.get_full_history(\n            onchain_domain=self.onchain_domain,\n            include_lightning=self.include_lightning,\n        )\n        txs = []\n        for key, tx in history.items():\n            txs.append(self.tx_to_model(tx))\n\n        self.clear()\n        self.beginInsertRows(QModelIndex(), 0, len(txs) - 1)\n        self.tx_history = txs\n        self.tx_history.reverse()\n        self.endInsertRows()\n\n        self.countChanged.emit()\n\n        self._dirty = False\n\n    def on_tx_verified(self, txid: str, info: TxMinedInfo):\n        for i, tx in enumerate(self.tx_history):\n            if 'txid' in tx and tx['txid'] == txid:\n                tx['height'] = info.height()\n                tx['confirmations'] = info.conf\n                tx['timestamp'] = info.timestamp\n                tx['section'] = self.get_section_by_timestamp(info.timestamp)\n                tx['date'] = self.format_date_by_section(tx['section'], datetime.fromtimestamp(info.timestamp))\n                index = self.index(i, 0)\n                roles = [self._ROLE_RMAP[x] for x in ['section', 'height', 'confirmations', 'timestamp', 'date']]\n                self.dataChanged.emit(index, index, roles)\n                return\n\n    def _update_future_txitem(self, tx_item_idx: int):\n        tx_item = self.tx_history[tx_item_idx]\n        # note: local txs can transition to future, as \"future\" state is not persisted\n        if tx_item.get('height') not in (TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL):\n            return\n        txid = tx_item['txid']\n        tx = self.wallet.db.get_transaction(txid)\n        if tx is None:\n            return\n        txinfo = self.wallet.get_tx_info(tx)\n        status, status_str = self.wallet.get_tx_status(txid, txinfo.tx_mined_status)\n        tx_item['date'] = status_str\n        # note: if the height changes, that might affect the history order, but we won't re-sort now.\n        tx_item['height'] = self.wallet.adb.get_tx_height(txid).height()\n        index = self.index(tx_item_idx, 0)\n        roles = [self._ROLE_RMAP[x] for x in ['height', 'date']]\n        self.dataChanged.emit(index, index, roles)\n\n    @pyqtSlot(str, str)\n    def updateTxLabel(self, key, label):\n        for i, tx in enumerate(self.tx_history):\n            if tx['key'] == key:\n                tx['label'] = label\n                index = self.index(i, 0)\n                self.dataChanged.emit(index, index, [self._ROLE_RMAP['label']])\n                return\n\n    @pyqtSlot(int)\n    def updateBlockchainHeight(self, height):\n        self._logger.debug('updating height to %d' % height)\n        for i, tx_item in enumerate(self.tx_history):\n            if 'height' in tx_item:\n                if tx_item['height'] > 0:\n                    tx_item['confirmations'] = height - tx_item['height'] + 1\n                    index = self.index(i, 0)\n                    roles = [self._ROLE_RMAP['confirmations']]\n                    self.dataChanged.emit(index, index, roles)\n                elif tx_item['height'] in (TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL):\n                    self._update_future_txitem(i)\n"
  },
  {
    "path": "electrum/gui/qml/qetxdetails.py",
    "content": "from typing import Optional\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject\n\nfrom electrum.i18n import _\nfrom electrum.logging import get_logger\nfrom electrum.bitcoin import DummyAddress\nfrom electrum.util import format_time, TxMinedInfo\nfrom electrum.transaction import tx_from_any, Transaction, PartialTransaction\nfrom electrum.network import Network\nfrom electrum.address_synchronizer import TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE\nfrom electrum.wallet import TxSighashDanger\nfrom electrum.fee_policy import FeePolicy\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\n\nfrom .qewallet import QEWallet\nfrom .qetypes import QEAmount\n\n\nclass QETxDetails(QObject, QtEventListener):\n    _logger = get_logger(__name__)\n\n    confirmRemoveLocalTx = pyqtSignal([str], arguments=['message'])\n    txRemoved = pyqtSignal()\n    saveTxError = pyqtSignal([str, str], arguments=['code', 'message'])\n    saveTxSuccess = pyqtSignal()\n\n    detailsChanged = pyqtSignal()\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n        self._wallet = None  # type: Optional[QEWallet]\n        self._txid = ''\n        self._rawtx = ''\n        self._label = ''\n\n        self._tx = None  # type: Optional[Transaction]\n\n        self._status = ''\n        self._amount = QEAmount()\n        self._lnamount = QEAmount()\n        self._fee = QEAmount()\n        self._feerate_str = ''\n        self._inputs = []\n        self._outputs = []\n\n        self._is_lightning_funding_tx = False\n        self._can_bump = False\n        self._can_dscancel = False\n        self._can_broadcast = False\n        self._can_cpfp = False\n        self._can_save_as_local = False\n        self._can_remove = False\n        self._can_sign = False\n        self._is_unrelated = False\n        self._is_complete = False\n        self._is_mined = False\n        self._is_rbf_enabled = False\n        self._is_removed = False\n        self._lock_delay = 0\n        self._sighash_danger = TxSighashDanger()\n\n        self._mempool_depth = ''\n        self._in_mempool = False\n\n        self._date = ''\n        self._timestamp = 0\n        self._confirmations = 0\n        self._header_hash = ''\n        self._short_id = \"\"\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    @qt_event_listener\n    def on_event_verified(self, wallet, txid, info):\n        if wallet == self._wallet.wallet and txid == self._txid:\n            self._logger.debug(f'verified event for our txid {txid}')\n            self.update()\n\n    @qt_event_listener\n    def on_event_new_transaction(self, wallet, tx):\n        if wallet == self._wallet.wallet and tx.txid() == self._txid:\n            self._logger.debug(f'new_transaction event for our txid {self._txid}')\n            self.update()\n\n    @qt_event_listener\n    def on_event_removed_transaction(self, wallet, tx):\n        if wallet == self._wallet.wallet and tx.txid() == self._txid:\n            self._logger.debug(f'removed my transaction {tx.txid()}')\n            self._is_removed = True\n            self.update()\n            self.txRemoved.emit()\n\n    @qt_event_listener\n    def on_event_fee_histogram(self, histogram):\n        if not self._wallet or not self._tx:\n            return\n        self.update()\n\n    walletChanged = pyqtSignal()\n    @pyqtProperty(QEWallet, notify=walletChanged)\n    def wallet(self):\n        return self._wallet\n\n    @wallet.setter\n    def wallet(self, wallet: QEWallet):\n        if self._wallet != wallet:\n            self._wallet = wallet\n            self.walletChanged.emit()\n\n    txidChanged = pyqtSignal()\n    @pyqtProperty(str, notify=txidChanged)\n    def txid(self):\n        return self._txid\n\n    @txid.setter\n    def txid(self, txid: str):\n        if self._txid != txid:\n            self._logger.debug(f'txid set -> {txid}')\n            self._txid = txid\n            self.txidChanged.emit()\n            self.update(from_txid=True)\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def rawtx(self):\n        return self._rawtx\n\n    @rawtx.setter\n    def rawtx(self, rawtx: str):\n        if self._rawtx != rawtx:\n            self._logger.debug(f'rawtx set -> {rawtx}')\n            self._rawtx = rawtx\n            if not rawtx:\n                return\n            try:\n                self._tx = tx_from_any(rawtx, deserialize=True)\n                self._txid = self._tx.txid()\n                self.txidChanged.emit()\n                self.update()\n            except Exception as e:\n                self._tx = None\n                self._logger.error(repr(e))\n\n    labelChanged = pyqtSignal()\n    @pyqtProperty(str, notify=labelChanged)\n    def label(self):\n        return self._label\n\n    @pyqtSlot(str)\n    def setLabel(self, label: str):\n        if label != self._label:\n            self._wallet.wallet.set_label(self._txid, label)\n            self._label = label\n            self.labelChanged.emit()\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def status(self):\n        return self._status\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def warning(self):\n        return self._sighash_danger.get_long_message()\n\n    @pyqtProperty(QEAmount, notify=detailsChanged)\n    def amount(self):\n        return self._amount\n\n    @pyqtProperty(QEAmount, notify=detailsChanged)\n    def lnAmount(self):\n        return self._lnamount\n\n    @pyqtProperty(QEAmount, notify=detailsChanged)\n    def fee(self):\n        return self._fee\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def feeRateStr(self):\n        return self._feerate_str\n\n    @pyqtProperty('QVariantList', notify=detailsChanged)\n    def inputs(self):\n        return self._inputs\n\n    @pyqtProperty('QVariantList', notify=detailsChanged)\n    def outputs(self):\n        return self._outputs\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def isMined(self):\n        return self._is_mined\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def isRemoved(self):\n        return self._is_removed\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def mempoolDepth(self):\n        return self._mempool_depth\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def inMempool(self):\n        return self._in_mempool\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def date(self):\n        return self._date\n\n    @pyqtProperty(int, notify=detailsChanged)\n    def timestamp(self):\n        return self._timestamp\n\n    @pyqtProperty(int, notify=detailsChanged)\n    def confirmations(self):\n        return self._confirmations\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def shortId(self):\n        return self._short_id\n\n    @pyqtProperty(str, notify=detailsChanged)\n    def headerHash(self):\n        return self._header_hash\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def isLightningFundingTx(self):\n        return self._is_lightning_funding_tx\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def canBump(self):\n        return self._can_bump\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def canCancel(self):\n        return self._can_dscancel\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def canBroadcast(self):\n        return self._can_broadcast\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def canCpfp(self):\n        return self._can_cpfp\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def canSaveAsLocal(self):\n        return self._can_save_as_local\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def canRemove(self):\n        return self._can_remove\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def canSign(self):\n        return self._can_sign\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def isUnrelated(self):\n        return self._is_unrelated\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def isComplete(self):\n        return self._is_complete\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def isRbfEnabled(self):\n        return self._is_rbf_enabled\n\n    @pyqtProperty(int, notify=detailsChanged)\n    def lockDelay(self):\n        return self._lock_delay\n\n    @pyqtProperty(bool, notify=detailsChanged)\n    def shouldConfirm(self):\n        return self._sighash_danger.needs_confirm()\n\n    def update(self, from_txid: bool = False):\n        assert self._wallet\n\n        if self._is_removed:\n            self._logger.debug('tx removed, disable gui options')\n            self._can_broadcast = False\n            self._can_bump = False\n            self._can_dscancel = False\n            self._can_cpfp = False\n            self._can_save_as_local = False\n            self._can_remove = False\n            self._can_sign = False\n            self._mempool_depth = ''\n            self._status = _('removed')\n            self.detailsChanged.emit()\n            return\n\n        if from_txid:\n            self._tx = self._wallet.wallet.db.get_transaction(self._txid)\n            assert self._tx is not None, f'unknown txid \"{self._txid}\"'\n\n        #self._logger.debug(repr(self._tx.to_json()))\n\n        self._logger.debug('adding info from wallet')\n        self._tx.add_info_from_wallet(self._wallet.wallet)\n        if not self._tx.is_complete() and self._tx.is_missing_info_from_network():\n            Network.run_from_another_thread(\n                self._tx.add_info_from_network(self._wallet.wallet.network, timeout=10))  # FIXME is this needed?...\n\n        sm = self._wallet.wallet.lnworker.swap_manager if self._wallet.wallet.lnworker else None\n\n        self._inputs = list(map(lambda x: {\n            'short_id': x.prevout.short_name(),\n            'value': x.value_sats(),\n            'address': x.address,\n            'is_mine': self._wallet.wallet.is_mine(x.address),\n            'is_change': self._wallet.wallet.is_change(x.address),\n            'is_swap': False if not sm else sm.is_lockup_address_for_a_swap(x.address) or x.address == DummyAddress.SWAP,\n            'is_accounting': self._wallet.wallet.is_accounting_address(x.address)\n        }, self._tx.inputs()))\n        self._outputs = list(map(lambda x: {\n            'address': x.get_ui_address_str(),\n            'value': QEAmount(amount_sat=x.value),\n            'short_id': '',  # TODO\n            'is_mine': self._wallet.wallet.is_mine(x.get_ui_address_str()),\n            'is_change': self._wallet.wallet.is_change(x.get_ui_address_str()),\n            'is_billing': self._wallet.wallet.is_billing_address(x.get_ui_address_str()),\n            'is_swap': False if not sm else sm.is_lockup_address_for_a_swap(x.get_ui_address_str()) or x.get_ui_address_str() == DummyAddress.SWAP,\n            'is_accounting': self._wallet.wallet.is_accounting_address(x.get_ui_address_str())\n        }, self._tx.outputs()))\n\n        txinfo = self._wallet.wallet.get_tx_info(self._tx)\n\n        self._logger.debug(repr(txinfo))\n\n        # can be None if outputs unrelated to wallet seed,\n        # e.g. to_local local_force_close commitment CSV-locked p2wsh script\n        if txinfo.amount is None:\n            self._amount.satsInt = 0\n        else:\n            self._amount.satsInt = txinfo.amount\n\n        self._status = txinfo.status\n        self._fee.satsInt = txinfo.fee\n\n        self._feerate_str = \"\"\n        if txinfo.fee is not None:\n            size = self._tx.estimated_size()\n            fee_per_kb = txinfo.fee / size * 1000\n            self._feerate_str = self._wallet.wallet.config.format_fee_rate(fee_per_kb)\n\n        self._sighash_danger = TxSighashDanger()\n\n        self._lock_delay = 0\n        self._in_mempool = False\n        self._is_mined = False if not txinfo.tx_mined_status else txinfo.tx_mined_status.height() > 0\n        if self._is_mined:\n            self.update_mined_status(txinfo.tx_mined_status)\n        else:\n            if txinfo.tx_mined_status.height() in [TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT]:\n                self._mempool_depth = FeePolicy.depth_tooltip(txinfo.mempool_depth_bytes)\n                self._in_mempool = True\n            elif txinfo.tx_mined_status.height() == TX_HEIGHT_FUTURE:\n                self._lock_delay = txinfo.tx_mined_status.wanted_height - self._wallet.wallet.adb.get_local_height()\n            if isinstance(self._tx, PartialTransaction):\n                self._sighash_danger = self._wallet.wallet.check_sighash(self._tx)\n\n        if self._wallet.wallet.lnworker:\n            # Calling wallet.get_full_history here is inefficient.\n            # We should probably pass the tx_item to the constructor.\n            full_history = self._wallet.wallet.get_full_history()\n            item = full_history.get('group:' + self._txid)\n            self._lnamount.satsInt = int(item['ln_value'].value) if item else 0\n        else:\n            self._lnamount.satsInt = 0\n\n        self._is_complete = self._tx.is_complete()\n        self._is_rbf_enabled = self._tx.is_rbf_enabled()\n        self._is_unrelated = txinfo.amount is None and self._lnamount.isEmpty\n        self._is_lightning_funding_tx = txinfo.is_lightning_funding_tx\n        self._can_broadcast = txinfo.can_broadcast\n        self._can_bump = txinfo.can_bump\n        self._can_dscancel = txinfo.can_dscancel\n        self._can_cpfp = txinfo.can_cpfp\n        self._can_save_as_local = txinfo.can_save_as_local\n        self._can_remove = txinfo.can_remove\n        self._can_sign = (\n            not self._is_complete\n            and self._wallet.wallet.can_sign(self._tx)\n            and not self._sighash_danger.needs_reject()\n        )\n\n        self.detailsChanged.emit()\n\n        if self._txid:\n            label = self._wallet.wallet.get_label_for_txid(self._txid)\n            if self._label != label:\n                self._label = label\n                self.labelChanged.emit()\n\n    def update_mined_status(self, tx_mined_info: TxMinedInfo):\n        self._mempool_depth = ''\n        self._date = format_time(tx_mined_info.timestamp)\n        self._timestamp = tx_mined_info.timestamp\n        self._confirmations = tx_mined_info.conf\n        self._header_hash = tx_mined_info.header_hash\n        self._short_id = tx_mined_info.short_id() or \"\"\n\n    @pyqtSlot()\n    def signAndBroadcast(self):\n        self._sign(broadcast=True)\n\n    @pyqtSlot()\n    def sign(self):\n        self._sign(broadcast=False)\n\n    def _sign(self, broadcast):\n        # TODO: connecting/disconnecting signal handlers here is hmm\n        try:\n            if broadcast:\n                self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded)\n                self._wallet.broadcastFailed.disconnect(self.onBroadcastFailed)\n        except Exception:\n            pass\n\n        if broadcast:\n            self._wallet.broadcastSucceeded.connect(self.onBroadcastSucceeded)\n            self._wallet.broadcastFailed.connect(self.onBroadcastFailed)\n            self._wallet.sign_and_broadcast(self._tx, on_success=self.on_signed_tx)\n        else:\n            self._wallet.sign(self._tx, on_success=self.on_signed_tx)\n\n        # side-effect: signing updates self._tx\n        # we rely on this for broadcast\n\n    def on_signed_tx(self, tx: Transaction):\n        self._logger.debug('on_signed_tx')\n        self.update()\n\n    @pyqtSlot()\n    def broadcast(self):\n        assert self._tx.is_complete()\n\n        try:\n            self._wallet.broadcastFailed.disconnect(self.onBroadcastFailed)\n        except Exception:\n            pass\n        self._wallet.broadcastFailed.connect(self.onBroadcastFailed)\n\n        self._can_broadcast = False\n        self.detailsChanged.emit()\n\n        self._wallet.broadcast(self._tx)\n\n    @pyqtSlot(str)\n    def onBroadcastSucceeded(self, txid):\n        if txid != self._txid:\n            return\n\n        self._logger.debug('onBroadcastSucceeded')\n        try:\n            self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded)\n        except Exception:\n            pass\n\n        self._can_broadcast = False\n        self.detailsChanged.emit()\n\n    @pyqtSlot(str, str, str)\n    def onBroadcastFailed(self, txid, code, reason):\n        if txid != self._txid:\n            return\n\n        try:\n            self._wallet.broadcastFailed.disconnect(self.onBroadcastFailed)\n        except Exception:\n            pass\n\n        self._can_broadcast = True\n        self.detailsChanged.emit()\n\n    @pyqtSlot()\n    @pyqtSlot(bool)\n    def removeLocalTx(self, confirm=False):\n        assert self._can_remove, 'cannot remove'\n        txid = self._txid\n        assert txid, 'txid unset'\n\n        if not confirm:\n            num_child_txs = len(self._wallet.wallet.adb.get_depending_transactions(txid))\n            question = _(\"Are you sure you want to remove this transaction?\")\n            if num_child_txs > 0:\n                question = (\n                    _(\"Are you sure you want to remove this transaction and {} child transactions?\")\n                    .format(num_child_txs))\n            self.confirmRemoveLocalTx.emit(question)\n            return\n\n        self._wallet.wallet.adb.remove_transaction(txid)\n        self._wallet.wallet.save_db()\n\n        # NOTE: from here, the tx/txid is unknown and all properties are invalid.\n        # UI should close TxDetails and avoid interacting with this qetxdetails instance.\n        self._tx = None\n\n    @pyqtSlot()\n    def save(self):\n        if not self._tx:\n            return\n\n        if self._wallet.save_tx(self._tx):\n            self._can_save_as_local = False\n            self._can_remove = True\n            self.detailsChanged.emit()\n\n    @pyqtSlot(result='QVariantList')\n    def getSerializedTx(self):\n        txqr = self._tx.to_qr_data()\n        label = \"\"\n        if txid := self._tx.txid():\n            label = self._wallet.wallet.get_label_for_txid(txid)\n        return [str(self._tx), txqr[0], txqr[1], label]\n"
  },
  {
    "path": "electrum/gui/qml/qetxfinalizer.py",
    "content": "import copy\nfrom enum import IntEnum\nimport threading\nfrom decimal import Decimal\nfrom typing import Optional, TYPE_CHECKING, Callable\nfrom functools import partial\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, pyqtEnum\n\nfrom electrum.logging import get_logger\nfrom electrum.i18n import _\nfrom electrum.bitcoin import DummyAddress\nfrom electrum.transaction import PartialTxOutput, PartialTransaction, Transaction, TxOutpoint\nfrom electrum.util import (\n    NotEnoughFunds, profiler, quantize_feerate, UserFacingException, NoDynamicFeeEstimates, event_listener\n)\nfrom electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP, BumpFeeStrategy, sweep_preparations\nfrom electrum import keystore\nfrom electrum.plugin import run_hook\nfrom electrum.fee_policy import FeePolicy, FeeMethod\nfrom electrum.network import NetworkException\n\nfrom electrum.gui import messages\nfrom electrum.gui.common_qt.util import QtEventListener\n\nfrom .qewallet import QEWallet\nfrom .qetypes import QEAmount\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n\n\nclass FeeSlider(QObject):\n\n    @pyqtEnum\n    class FSMethod(IntEnum):\n        FEERATE = 0\n        ETA = 1\n        MEMPOOL = 2\n        MANUAL = 3\n\n        def to_fee_method(self) -> 'FeeMethod':\n            return {\n                self.FEERATE: FeeMethod.FEERATE,\n                self.ETA: FeeMethod.ETA,\n                self.MEMPOOL: FeeMethod.MEMPOOL,\n                self.MANUAL: FeeMethod.FIXED\n            }[self]\n\n        @classmethod\n        def from_fee_method(cls, fm: FeeMethod) -> 'FeeSlider.FSMethod':\n            return {\n                FeeMethod.FEERATE: cls.FEERATE,\n                FeeMethod.ETA: cls.ETA,\n                FeeMethod.MEMPOOL: cls.MEMPOOL,\n                FeeMethod.FIXED: cls.MANUAL\n            }[fm]\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._wallet = None  # type: Optional[QEWallet]\n        self._sliderSteps = 0\n        self._sliderPos = 0\n        self._fee_method = None  # type: Optional[FeeSlider.FSMethod]\n        self._fee_policy = None  # type: Optional[FeePolicy]\n        self._target = ''\n        self._config = None  # type: Optional[SimpleConfig]\n\n    walletChanged = pyqtSignal()\n    @pyqtProperty(QEWallet, notify=walletChanged)\n    def wallet(self):\n        return self._wallet\n\n    @wallet.setter\n    def wallet(self, wallet: QEWallet):\n        if self._wallet != wallet:\n            self._wallet = wallet\n            self._config = self._wallet.wallet.config\n            self.read_config()\n            self.walletChanged.emit()\n\n    sliderStepsChanged = pyqtSignal()\n    @pyqtProperty(int, notify=sliderStepsChanged)\n    def sliderSteps(self):\n        return self._sliderSteps\n\n    sliderPosChanged = pyqtSignal()\n    @pyqtProperty(int, notify=sliderPosChanged)\n    def sliderPos(self):\n        return self._sliderPos\n\n    @sliderPos.setter\n    def sliderPos(self, sliderPos):\n        if self._sliderPos != sliderPos:\n            self._sliderPos = sliderPos\n            self.save_config()\n            self.sliderPosChanged.emit()\n\n    methodChanged = pyqtSignal()\n    @pyqtProperty(int, notify=methodChanged)\n    def method(self) -> int:\n        fsmethod = self.FSMethod.from_fee_method(self._fee_policy.method)\n        return int(fsmethod)\n\n    @method.setter\n    def method(self, method: int):\n        if self._fee_method != FeeSlider.FSMethod(method):\n            self._fee_method = self.FSMethod(method)\n            self._fee_policy.set_method(self._fee_method.to_fee_method())\n            self.update_slider()\n            self.methodChanged.emit()\n            self.save_config()\n\n    targetChanged = pyqtSignal()\n    @pyqtProperty(str, notify=targetChanged)\n    def target(self):\n        return self._target\n\n    @target.setter\n    def target(self, target):\n        if self._target != target:\n            self._target = target\n            self.targetChanged.emit()\n\n    def update_slider(self):\n        if self._fee_method == FeeSlider.FSMethod.MANUAL:\n            return\n        self._sliderSteps = self._fee_policy.get_slider_max()\n        self._sliderPos = self._fee_policy.get_slider_pos()\n        self.sliderStepsChanged.emit()\n        self.sliderPosChanged.emit()\n\n    def update_target(self):\n        self.target = self._fee_policy.get_target_text()\n\n    def read_config(self):\n        self._fee_policy = FeePolicy(self._config.FEE_POLICY)\n        self._fee_method = self.FSMethod.from_fee_method(self._fee_policy.method)\n        self.update_slider()\n        self.methodChanged.emit()\n        self.update()\n\n    def save_config(self):\n        if self._fee_method != FeeSlider.FSMethod.MANUAL:\n            value = int(self._sliderPos)\n            self._fee_policy.set_value_from_slider_pos(value)\n            self._config.FEE_POLICY = self._fee_policy.get_descriptor()\n        self.update()\n\n    def update(self):\n        raise NotImplementedError()\n\n\nclass TxFeeSlider(FeeSlider):\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._fee = QEAmount()\n        self._feeRate = ''\n        self._userFee = ''\n        self._userFeerate = ''\n        self._is_user_feerate_last = True\n        self._rbf = False\n        self._tx = None  # type: Optional[PartialTransaction]\n        self._inputs = []\n        self._outputs = []\n        self._finalized_txid = ''\n        self._valid = False\n        self._warning = ''\n\n    feeChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=feeChanged)\n    def fee(self):\n        return self._fee\n\n    @fee.setter\n    def fee(self, fee):\n        if self._fee != fee:\n            self._fee.copyFrom(fee)\n            self.feeChanged.emit()\n\n    feeRateChanged = pyqtSignal()\n    @pyqtProperty(str, notify=feeRateChanged)\n    def feeRate(self):\n        return self._feeRate\n\n    @feeRate.setter\n    def feeRate(self, feeRate):\n        if self._feeRate != feeRate:\n            self._feeRate = feeRate\n            self.feeRateChanged.emit()\n\n    userFeeChanged = pyqtSignal()\n    @pyqtProperty(str, notify=userFeeChanged)\n    def userFee(self):\n        return self._userFee\n\n    @userFee.setter\n    def userFee(self, userFee):\n        if self._userFee != userFee:\n            self._logger.warn('userFee')\n            self._userFee = userFee\n            user_fee = int(userFee) if userFee else 0\n            self._fee_policy = FeePolicy(f'fixed:{user_fee}')\n            self.userFeeChanged.emit()\n            self.isUserFeerateLast = False\n            self.update()\n\n    userFeerateChanged = pyqtSignal()\n    @pyqtProperty(str, notify=userFeerateChanged)\n    def userFeerate(self):\n        return self._userFeerate\n\n    @userFeerate.setter\n    def userFeerate(self, userFeerate):\n        if self._userFeerate != userFeerate:\n            self._logger.warn('userFeerate')\n            self._userFeerate = userFeerate\n            as_decimal = Decimal(userFeerate) if userFeerate else 0\n            user_feerate = int(as_decimal * 1000)\n            self._fee_policy = FeePolicy(f'feerate:{user_feerate}')\n            self.userFeerateChanged.emit()\n            self.isUserFeerateLast = True\n            self.update()\n\n    isUserFeerateLastChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=isUserFeerateLastChanged)\n    def isUserFeerateLast(self):\n        return self._is_user_feerate_last\n\n    @isUserFeerateLast.setter\n    def isUserFeerateLast(self, isUserFeerateLast):\n        if self._is_user_feerate_last != isUserFeerateLast:\n            self._is_user_feerate_last = isUserFeerateLast\n            self.isUserFeerateLastChanged.emit()\n\n    rbfChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=rbfChanged)\n    def rbf(self):\n        return self._rbf\n\n    @rbf.setter\n    def rbf(self, rbf):\n        if self._rbf != rbf:\n            self._rbf = rbf\n            self.update()\n            self.rbfChanged.emit()\n\n    inputsChanged = pyqtSignal()\n    @pyqtProperty('QVariantList', notify=inputsChanged)\n    def inputs(self):\n        return self._inputs\n\n    @inputs.setter\n    def inputs(self, inputs):\n        if self._inputs != inputs:\n            self._inputs = inputs\n            self.inputsChanged.emit()\n\n    outputsChanged = pyqtSignal()\n    @pyqtProperty('QVariantList', notify=outputsChanged)\n    def outputs(self):\n        return self._outputs\n\n    @outputs.setter\n    def outputs(self, outputs):\n        if self._outputs != outputs:\n            self._outputs = outputs\n            self.outputsChanged.emit()\n\n    finalizedTxidChanged = pyqtSignal()\n    @pyqtProperty(str, notify=finalizedTxidChanged)\n    def finalizedTxid(self):\n        return self._finalized_txid\n\n    @finalizedTxid.setter\n    def finalizedTxid(self, finalized_txid):\n        if self._finalized_txid != finalized_txid:\n            self._finalized_txid = finalized_txid\n            self.finalizedTxidChanged.emit()\n\n    warningChanged = pyqtSignal()\n    @pyqtProperty(str, notify=warningChanged)\n    def warning(self):\n        return self._warning\n\n    @warning.setter\n    def warning(self, warning):\n        if self._warning != warning:\n            self._warning = warning\n            self.warningChanged.emit()\n\n    validChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=validChanged)\n    def valid(self):\n        return self._valid\n\n    @pyqtSlot()\n    def doUpdate(self):\n        self.update()\n\n    def update_from_tx(self, tx: PartialTransaction):\n        tx_size = tx.estimated_size()\n        fee = tx.get_fee()\n        feerate = Decimal(fee) / tx_size  # sat/byte\n\n        self.fee = QEAmount(amount_sat=int(fee))\n        self.feeRate = f'{feerate:.1f}'\n        self.finalizedTxid = tx.txid()\n\n        self.update_inputs_from_tx(tx)\n        self.update_outputs_from_tx(tx)\n        self.update_target()\n        self.update_manual_fields()\n\n    def update_manual_fields(self):\n        if self._fee_method == FeeSlider.FSMethod.MANUAL:\n            if self._fee_policy.method == FeeMethod.FIXED:\n                self._userFeerate = self.feeRate\n                self.userFeerateChanged.emit()\n            else:\n                self._userFee = self.fee.satsStr\n                self.userFeeChanged.emit()\n\n    def update_inputs_from_tx(self, tx: Transaction):\n        inputs = []\n        for inp in tx.inputs():\n            # addr = self.wallet.adb.get_txin_address(txin)\n            addr = inp.address\n            address_str = '<address unknown>' if addr is None else addr\n\n            txin_value = inp.value_sats() if inp.value_sats() else 0\n\n            inputs.append({\n                'address': address_str,\n                'short_id': str(inp.short_id),\n                'value': QEAmount(amount_sat=txin_value),\n                'is_coinbase': inp.is_coinbase_input(),\n                'is_mine': self._wallet.wallet.is_mine(addr),\n                'is_change': self._wallet.wallet.is_change(addr),\n                'prevout_txid': inp.prevout.txid.hex(),\n                'is_swap': False\n            })\n        self.inputs = inputs\n\n    def update_outputs_from_tx(self, tx: PartialTransaction):\n        sm = self._wallet.wallet.lnworker.swap_manager if self._wallet.wallet.lnworker else None\n\n        outputs = []\n        for idx, o in enumerate(tx.outputs()):\n            outputs.append({\n                'address': o.get_ui_address_str(),\n                'value': o.value,\n                'short_id': str(TxOutpoint(bytes.fromhex(tx.txid()), idx).short_name()) if tx.txid() else '',\n                'is_mine': self._wallet.wallet.is_mine(o.get_ui_address_str()),\n                'is_change': self._wallet.wallet.is_change(o.get_ui_address_str()),\n                'is_billing': self._wallet.wallet.is_billing_address(o.get_ui_address_str()),\n                'is_swap': False if not sm else sm.is_lockup_address_for_a_swap(o.get_ui_address_str()) or o.get_ui_address_str() == DummyAddress.SWAP,\n                'is_accounting': self._wallet.wallet.is_accounting_address(o.get_ui_address_str()),\n                'is_reserve': o.is_utxo_reserve\n            })\n        self.outputs = outputs\n\n    def update_fee_warning_from_tx(self, *, tx: PartialTransaction, invoice_amt: Optional[int]):\n        if invoice_amt is None:\n            invoice_amt = sum([txo.value for txo in tx.outputs() if not txo.is_mine])\n            if invoice_amt == 0:\n                invoice_amt = tx.output_value()\n        fee_warning_tuple = self._wallet.wallet.get_tx_fee_warning(\n            invoice_amt=invoice_amt, tx_size=tx.estimated_size(), fee=tx.get_fee(), txid=tx.txid())\n        if fee_warning_tuple:\n            allow_send, long_warning, short_warning = fee_warning_tuple\n            self.warning = _('Warning') + ': ' + long_warning\n        else:\n            self.warning = ''\n\n    def save_config(self):\n        if self._fee_method == FeeSlider.FSMethod.MANUAL:\n            if self.fee:\n                self.userFee = self.fee.satsStr\n            if self.feeRate:\n                self.userFeerate = self.feeRate\n        super().save_config()\n\n\nclass QETxFinalizer(TxFeeSlider):\n    _logger = get_logger(__name__)\n\n    finished = pyqtSignal([bool, bool, bool], arguments=['signed', 'saved', 'complete'])\n    signError = pyqtSignal([str], arguments=['message'])\n\n    def __init__(\n        self,\n        parent=None,\n        *,\n        make_tx: Callable[[int, FeePolicy], PartialTransaction] = None,\n        accept: Callable[[PartialTransaction], None] = None,\n    ):\n        super().__init__(parent)\n        self.f_make_tx = make_tx\n        self.f_accept = accept\n\n        self._address = ''\n        self._amount = QEAmount()\n        self._effectiveAmount = QEAmount()\n        self._extraFee = QEAmount()\n        self._canRbf = False\n\n    addressChanged = pyqtSignal()\n    @pyqtProperty(str, notify=addressChanged)\n    def address(self):\n        return self._address\n\n    @address.setter\n    def address(self, address):\n        if self._address != address:\n            self._address = address\n            self.addressChanged.emit()\n\n    amountChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=amountChanged)\n    def amount(self):\n        return self._amount\n\n    @amount.setter\n    def amount(self, amount):\n        if self._amount != amount:\n            self._logger.debug(str(amount))\n            self._amount.copyFrom(amount)\n            self.amountChanged.emit()\n\n    effectiveAmountChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=effectiveAmountChanged)\n    def effectiveAmount(self):\n        return self._effectiveAmount\n\n    extraFeeChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=extraFeeChanged)\n    def extraFee(self):\n        return self._extraFee\n\n    @extraFee.setter\n    def extraFee(self, extrafee):\n        if self._extraFee != extrafee:\n            self._extraFee.copyFrom(extrafee)\n            self.extraFeeChanged.emit()\n\n    canRbfChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=canRbfChanged)\n    def canRbf(self):\n        return self._canRbf\n\n    @canRbf.setter\n    def canRbf(self, canRbf):\n        if self._canRbf != canRbf:\n            self._canRbf = canRbf\n            self.canRbfChanged.emit()\n        self.rbf = self._canRbf  # if we can RbF, we do RbF\n\n    @profiler\n    def make_tx(self, amount):\n        self._logger.debug(f'make_tx amount={amount}')\n\n        if self.f_make_tx:\n            tx = self.f_make_tx(amount, self._fee_policy)\n        else:\n            # default impl\n            coins = self._wallet.wallet.get_spendable_coins(None)\n            outputs = [PartialTxOutput.from_address_and_value(self.address, amount)]\n            tx = self._wallet.wallet.make_unsigned_transaction(\n                coins=coins,\n                outputs=outputs,\n                fee_policy=self._fee_policy,\n                rbf=self._rbf)\n\n        self._logger.debug('fee: %d, inputs: %d, outputs: %d' % (tx.get_fee(), len(tx.inputs()), len(tx.outputs())))\n\n        return tx\n\n    def update(self):\n        if not self._wallet:\n            self._logger.debug('wallet not set, ignoring update()')\n            return\n\n        try:\n            # make unsigned transaction\n            amount = '!' if self._amount.isMax else self._amount.satsInt\n            tx = self.make_tx(amount=amount)\n        except NotEnoughFunds:\n            self.warning = self._wallet.wallet.get_text_not_enough_funds_mentioning_frozen(for_amount=amount)\n            self._valid = False\n            self.validChanged.emit()\n            return\n        except NoDynamicFeeEstimates:\n            self.warning = _('No dynamic fee estimates available')\n            self._valid = False\n            self.validChanged.emit()\n            return\n        except Exception as e:\n            self._logger.error(str(e))\n            self.warning = repr(e)\n            self._valid = False\n            self.validChanged.emit()\n            return\n\n        self._tx = tx\n\n        amount = self._amount.satsInt if not self._amount.isMax else tx.output_value()\n\n        self._effectiveAmount.satsInt = amount\n        self.effectiveAmountChanged.emit()\n\n        self.update_from_tx(tx)\n\n        x_fee = run_hook('get_tx_extra_fee', self._wallet.wallet, tx)\n        if x_fee:\n            x_fee_address, x_fee_amount = x_fee\n            self.extraFee = QEAmount(amount_sat=x_fee_amount)\n\n        self.update_fee_warning_from_tx(tx=tx, invoice_amt=amount)\n\n        if self._amount.isMax and not self.warning:\n            if reserve_sats := self._wallet.wallet.tx_keeps_ln_utxo_reserve(\n                tx,\n                gui_spend_max=self._amount.isMax\n            ):\n                reserve_str = self._config.format_amount_and_units(reserve_sats)\n                self.warning = ' '.join([\n                    _('Warning') + ':',\n                    _('Could not spend max: a security reserve of {} was kept for your Lightning channels.')\n                    .format(reserve_str)\n                ])\n\n        self._valid = True\n        self.validChanged.emit()\n\n    @pyqtSlot()\n    def saveOrShow(self):\n        if not self._valid or not self._tx:\n            self._logger.debug('no valid tx')\n            return\n\n        saved = False\n        if self._tx.txid():\n            if self._wallet.save_tx(self._tx):\n                saved = True\n\n        self.finished.emit(False, saved, self._tx.is_complete())\n\n    @pyqtSlot()\n    def signAndSend(self):\n        if not self._valid or not self._tx:\n            self._logger.debug('no valid tx')\n            return\n\n        if self.f_accept:\n            self.f_accept(self._tx)\n            return\n\n        self._wallet.sign_and_broadcast(self._tx, on_success=partial(self.on_signed_tx, False), on_failure=self.on_sign_failed)\n\n    @pyqtSlot()\n    def sign(self):\n        if not self._valid or not self._tx:\n            self._logger.error('no valid tx')\n            return\n\n        self._wallet.sign(self._tx, on_success=partial(self.on_signed_tx, True), on_failure=self.on_sign_failed)\n\n    def on_signed_tx(self, save: bool, tx: Transaction):\n        self._logger.debug('on_signed_tx')\n        saved = False\n        if save and self._tx.txid():\n            if self._wallet.save_tx(self._tx):\n                saved = True\n            else:\n                self._logger.error('Could not save tx')\n        self.finished.emit(True, saved, tx.is_complete())\n\n    def on_sign_failed(self, msg: str = None):\n        self._logger.debug('on_sign_failed')\n        self.signError.emit(msg)\n\n    @pyqtSlot(result='QVariantList')\n    def getSerializedTx(self):\n        txqr = self._tx.to_qr_data()\n        label = \"\"\n        if txid := self._tx.txid():\n            label = self._wallet.wallet.get_label_for_txid(txid)\n        return [str(self._tx), txqr[0], txqr[1], label]\n\n\nclass TxMonMixin(QtEventListener):\n    \"\"\" mixin for watching an existing TX based on its txid for verified or removed event.\n        requires self._wallet to contain a QEWallet instance.\n        exposes txid qt property.\n        calls get_tx() once txid is set.\n        calls tx_verified() and emits txMined signal once tx is verified.\n        emits txRemoved signal if tx is removed (e.g. replace-by-fee)\n    \"\"\"\n    txMined = pyqtSignal()\n    txRemoved = pyqtSignal()\n\n    def __init__(self, parent=None):\n        self._logger.debug('TxMonMixin.__init__')\n\n        self._txid = ''\n\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    @event_listener\n    def on_event_verified(self, wallet, txid, info):\n        if wallet == self._wallet.wallet and txid == self._txid:\n            self._logger.debug('verified event for our txid %s' % txid)\n            self.tx_verified()\n            self.txMined.emit()\n\n    @event_listener\n    def on_event_removed_transaction(self, wallet, tx):\n        if wallet == self._wallet.wallet and tx.txid() == self._txid:\n            self._logger.debug('remove tx for our txid %s' % self._txid)\n            self.tx_removed()\n            self.txRemoved.emit()\n\n    txidChanged = pyqtSignal()\n    @pyqtProperty(str, notify=txidChanged)\n    def txid(self):\n        return self._txid\n\n    @txid.setter\n    def txid(self, txid):\n        if self._txid != txid:\n            self._txid = txid\n            self.get_tx()\n            self.txidChanged.emit()\n\n    # override\n    def get_tx(self) -> None:\n        pass\n\n    # override\n    def tx_verified(self) -> None:\n        pass\n\n    # override\n    def tx_removed(self) -> None:\n        pass\n\n\nclass QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):\n    _logger = get_logger(__name__)\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._oldfee = QEAmount()\n        self._oldfee_rate = '0'\n        self._orig_tx = None\n        self._rbf = True\n        self._bump_method = BumpFeeStrategy.PRESERVE_PAYMENT.name\n        self._bump_methods_available = []\n\n    oldfeeChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=oldfeeChanged)\n    def oldfee(self):\n        return self._oldfee\n\n    @oldfee.setter\n    def oldfee(self, oldfee):\n        if self._oldfee != oldfee:\n            self._oldfee.copyFrom(oldfee)\n            self.oldfeeChanged.emit()\n\n    oldfeeRateChanged = pyqtSignal()\n    @pyqtProperty(str, notify=oldfeeRateChanged)\n    def oldfeeRate(self):\n        return self._oldfee_rate\n\n    @oldfeeRate.setter\n    def oldfeeRate(self, oldfeerate):\n        if self._oldfee_rate != oldfeerate:\n            self._oldfee_rate = oldfeerate\n            self.oldfeeRateChanged.emit()\n\n    bumpMethodChanged = pyqtSignal()\n    @pyqtProperty(str, notify=bumpMethodChanged)\n    def bumpMethod(self):\n        return self._bump_method\n\n    @bumpMethod.setter\n    def bumpMethod(self, bumpmethod: str) -> None:\n        if self._bump_method != bumpmethod:\n            self._bump_method = bumpmethod\n            self.bumpMethodChanged.emit()\n            self.update()\n\n    bumpMethodsAvailableChanged = pyqtSignal()\n    @pyqtProperty('QVariantList', notify=bumpMethodsAvailableChanged)\n    def bumpMethodsAvailable(self):\n        return self._bump_methods_available\n\n    def get_tx(self):\n        assert self._txid\n        self._orig_tx = self._wallet.wallet.db.get_transaction(self._txid)\n        assert self._orig_tx\n\n        strategies, def_strat_idx = self._wallet.wallet.get_bumpfee_strategies_for_tx(tx=self._orig_tx)\n        self._bump_methods_available = [{'value': strat.name, 'text': strat.text()} for strat in strategies]\n        self.bumpMethodsAvailableChanged.emit()\n        self.bumpMethod = strategies[def_strat_idx].name\n\n        if not isinstance(self._orig_tx, PartialTransaction):\n            self._orig_tx = PartialTransaction.from_tx(self._orig_tx)\n\n        if not self._orig_tx.add_info_from_wallet_and_network(wallet=self._wallet.wallet, show_error=self._logger.error):\n            return\n\n        self.update_from_tx(self._orig_tx)\n\n        self.oldfee = self.fee\n        self.oldfeeRate = self.feeRate\n        self.update()\n\n    def tx_verified(self):\n        self._valid = False\n        self.validChanged.emit()\n        self.warning = _('Base transaction has been mined')\n\n    def tx_removed(self):\n        self._valid = False\n        self.validChanged.emit()\n        self.warning = _('Base transaction disappeared')\n\n    def update(self):\n        if not self._txid or not self._orig_tx:\n            # not initialized yet\n            return\n\n        if self._fee_policy.method == FeeMethod.FIXED:\n            fee = self._fee_policy.value\n            fee_per_kb = 1000 * fee / self._orig_tx.estimated_size()\n        else:\n            fee_per_kb = self._fee_policy.fee_per_kb(self._wallet.wallet.network)\n            if fee_per_kb is None:\n                # dynamic method and no network\n                self._logger.debug('no fee_per_kb')\n                self.warning = _('Cannot determine dynamic fees, not connected')\n                return\n\n        new_fee_rate = fee_per_kb / 1000\n        if new_fee_rate <= float(self._oldfee_rate):\n            self._valid = False\n            self.validChanged.emit()\n            self.warning = _(\"The new fee rate needs to be higher than the old fee rate.\")\n            return\n\n        if not self._orig_tx.add_info_from_wallet_and_network(wallet=self._wallet.wallet, show_error=self._logger.error):\n            self._valid = False\n            self.validChanged.emit()\n            self.warning = _(\"Transaction is missing info from network\")\n            return\n\n        try:\n            self._tx = self._wallet.wallet.bump_fee(\n                tx=self._orig_tx,\n                new_fee_rate=new_fee_rate,\n                strategy=BumpFeeStrategy[self._bump_method],\n            )\n        except CannotBumpFee as e:\n            self._valid = False\n            self.validChanged.emit()\n            self._logger.error(str(e))\n            self.warning = str(e)\n            return\n        else:\n            self.warning = ''\n\n        self._tx.set_rbf(self.rbf)\n\n        self.update_from_tx(self._tx)\n        self.update_fee_warning_from_tx(tx=self._tx, invoice_amt=None)\n\n        self._valid = True\n        self.validChanged.emit()\n\n    @pyqtSlot(result=str)\n    def getNewTx(self):\n        return str(self._tx)\n\n\nclass QETxCanceller(TxFeeSlider, TxMonMixin):\n    _logger = get_logger(__name__)\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._oldfee = QEAmount()\n        self._oldfee_rate = '0'\n        self._orig_tx = None\n        self._txid = ''\n        self._rbf = True\n\n    oldfeeChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=oldfeeChanged)\n    def oldfee(self):\n        return self._oldfee\n\n    @oldfee.setter\n    def oldfee(self, oldfee):\n        if self._oldfee != oldfee:\n            self._oldfee.copyFrom(oldfee)\n            self.oldfeeChanged.emit()\n\n    oldfeeRateChanged = pyqtSignal()\n    @pyqtProperty(str, notify=oldfeeRateChanged)\n    def oldfeeRate(self):\n        return self._oldfee_rate\n\n    @oldfeeRate.setter\n    def oldfeeRate(self, oldfeerate):\n        if self._oldfee_rate != oldfeerate:\n            self._oldfee_rate = oldfeerate\n            self.oldfeeRateChanged.emit()\n\n    def get_tx(self):\n        assert self._txid\n        self._orig_tx = self._wallet.wallet.db.get_transaction(self._txid)\n        assert self._orig_tx\n\n        if not isinstance(self._orig_tx, PartialTransaction):\n            self._orig_tx = PartialTransaction.from_tx(self._orig_tx)\n\n        if not self._orig_tx.add_info_from_wallet_and_network(wallet=self._wallet.wallet, show_error=self._logger.error):\n            return\n\n        self.update_from_tx(self._orig_tx)\n\n        self.oldfee = self.fee\n        self.oldfeeRate = self.feeRate\n        self.update()\n\n    def tx_verified(self):\n        self._valid = False\n        self.validChanged.emit()\n        self.warning = _('Base transaction has been mined')\n\n    def tx_removed(self):\n        self._valid = False\n        self.validChanged.emit()\n        self.warning = _('Base transaction disappeared')\n\n    def update(self):\n        if not self._txid or not self._orig_tx:\n            # not initialized yet\n            return\n\n        if self._fee_policy.method == FeeMethod.FIXED:\n            fee = self._fee_policy.value\n            fee_per_kb = 1000 * fee / self._orig_tx.estimated_size()\n        else:\n            fee_per_kb = self._fee_policy.fee_per_kb(self._wallet.wallet.network)\n            if fee_per_kb is None:\n                # dynamic method and no network\n                self._logger.debug('no fee_per_kb')\n                self.warning = _('Cannot determine dynamic fees, not connected')\n                return\n\n        new_fee_rate = fee_per_kb / 1000\n        if new_fee_rate <= float(self._oldfee_rate):\n            self._valid = False\n            self.validChanged.emit()\n            self.warning = _(\"The new fee rate needs to be higher than the old fee rate.\")\n            return\n\n        if fee_per_kb < self._wallet.wallet.relayfee():\n            self._valid = False\n            self.validChanged.emit()\n            self._logger.warning('feerate too low for relay')\n            self.warning = messages.MSG_RELAYFEE\n            return\n\n        if not self._orig_tx.add_info_from_wallet_and_network(wallet=self._wallet.wallet, show_error=self._logger.error):\n            self._valid = False\n            self.validChanged.emit()\n            self.warning = _(\"Transaction is missing info from network\")\n            return\n\n        try:\n            self._tx = self._wallet.wallet.dscancel(\n                tx=self._orig_tx,\n                new_fee_rate=new_fee_rate,\n            )\n        except CannotDoubleSpendTx as e:\n            self._valid = False\n            self.validChanged.emit()\n            self._logger.error(str(e))\n            self.warning = str(e)\n            return\n        else:\n            self.warning = ''\n\n        self._tx.set_rbf(self.rbf)\n\n        self.update_from_tx(self._tx)\n        self.update_fee_warning_from_tx(tx=self._tx, invoice_amt=None)\n\n        self._valid = True\n        self.validChanged.emit()\n\n    @pyqtSlot(result=str)\n    def getNewTx(self):\n        return str(self._tx)\n\n\nclass QETxCpfpFeeBumper(TxFeeSlider, TxMonMixin):\n    _logger = get_logger(__name__)\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._input_amount = QEAmount()\n        self._output_amount = QEAmount()\n        self._total_fee = QEAmount()\n        self._total_fee_rate = 0\n        self._total_size = 0\n\n        self._parent_tx = None\n        self._new_tx = None\n        self._parent_tx_size = 0\n        self._parent_fee = 0\n        self._max_fee = 0\n        self._txid = ''\n        self._rbf = True\n\n    totalFeeChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=totalFeeChanged)\n    def totalFee(self):\n        return self._total_fee\n\n    @totalFee.setter\n    def totalFee(self, totalfee):\n        if self._total_fee != totalfee:\n            self._total_fee.copyFrom(totalfee)\n            self.totalFeeChanged.emit()\n\n    totalFeeRateChanged = pyqtSignal()\n    @pyqtProperty(str, notify=totalFeeRateChanged)\n    def totalFeeRate(self):\n        return self._total_fee_rate\n\n    @totalFeeRate.setter\n    def totalFeeRate(self, totalfeerate):\n        if self._total_fee_rate != totalfeerate:\n            self._total_fee_rate = totalfeerate\n            self.totalFeeRateChanged.emit()\n\n    inputAmountChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=inputAmountChanged)\n    def inputAmount(self):\n        return self._input_amount\n\n    outputAmountChanged = pyqtSignal()\n    @pyqtProperty(QEAmount, notify=outputAmountChanged)\n    def outputAmount(self):\n        return self._output_amount\n\n    totalSizeChanged = pyqtSignal()\n    @pyqtProperty(int, notify=totalSizeChanged)\n    def totalSize(self):\n        return self._total_size\n\n    def get_tx(self):\n        assert self._txid\n        self._parent_tx = self._wallet.wallet.db.get_transaction(self._txid)\n        assert self._parent_tx\n\n        if isinstance(self._parent_tx, PartialTransaction):\n            self._logger.error('unexpected PartialTransaction')\n            return\n\n        self._parent_tx_size = self._parent_tx.estimated_size()\n        self._parent_fee = self._wallet.wallet.get_tx_info(self._parent_tx).fee\n\n        if self._parent_fee is None:\n            self._logger.error(_(\"Can't CPFP: unknown fee for parent transaction.\"))\n            self.warning = _(\"Can't CPFP: unknown fee for parent transaction.\")\n            return\n\n        self._new_tx = self._wallet.wallet.cpfp(self._parent_tx, 0)\n        self._total_size = self._parent_tx_size + self._new_tx.estimated_size()\n        self.totalSizeChanged.emit()\n        self._max_fee = self._new_tx.output_value()\n        self._input_amount.satsInt = self._max_fee\n\n        self.update()\n\n    def get_child_fee_from_total_feerate(self, fee_per_kb: Optional[int]) -> Optional[int]:\n        if fee_per_kb is None:\n            return None\n        package_fee = FeePolicy.estimate_fee_for_feerate(fee_per_kb=fee_per_kb, size=self._total_size)\n        return self.get_child_fee_from_total_fee(package_fee)\n\n    def get_child_fee_from_total_fee(self, fee: int) -> int:\n        child_fee = fee - self._parent_fee\n        child_fee = min(self._max_fee, child_fee)\n        return child_fee\n\n    def tx_verified(self):\n        self._valid = False\n        self.validChanged.emit()\n        self.warning = _('Base transaction has been mined')\n\n    def tx_removed(self):\n        self._valid = False\n        self.validChanged.emit()\n        self.warning = _('Base transaction disappeared')\n\n    def update(self):\n        if not self._txid:  # not initialized yet\n            return\n\n        assert self._parent_tx\n\n        self._valid = False\n        self.validChanged.emit()\n        self.warning = ''\n\n        if self._parent_fee is None:\n            self._logger.error(_(\"Can't CPFP: unknown fee for parent transaction.\"))\n            self.warning = _(\"Can't CPFP: unknown fee for parent transaction.\")\n            return\n\n        if self._fee_policy.method == FeeMethod.FIXED:\n            fee = self.get_child_fee_from_total_fee(self._fee_policy.value)\n        else:\n            fee_per_kb = self._fee_policy.fee_per_kb(self._wallet.wallet.network)\n            if fee_per_kb is None:\n                # dynamic method and no network\n                self._logger.debug('no fee_per_kb')\n                self.warning = _('Cannot determine dynamic fees, not connected')\n                return\n            fee = self.get_child_fee_from_total_feerate(fee_per_kb=fee_per_kb)\n\n        if fee is None:\n            self._logger.warning('no fee')\n            self.warning = _('No fee')\n            return\n        if fee > self._max_fee:\n            self._logger.warning('max fee exceeded')\n            self.warning = _('Max fee exceeded')\n            return\n        min_child_fee = FeePolicy.estimate_fee_for_feerate(fee_per_kb=self._wallet.wallet.relayfee(), size=self._total_size)\n        if fee < min_child_fee:\n            self._logger.warning('feerate too low for relay')\n            self.warning = messages.MSG_RELAYFEE\n            return\n\n        comb_fee = fee + self._parent_fee\n        comb_feerate = comb_fee / self._total_size\n\n        if comb_feerate < (self._parent_fee / self._parent_tx_size):\n            self._logger.debug('combined feerate below parent tx feerate')\n            self.warning = _('Combined feerate should be greater than the parent tx feerate')\n            return\n\n        self._fee.satsInt = fee\n        self._output_amount.satsInt = self._max_fee - fee\n        self.outputAmountChanged.emit()\n\n        self._total_fee.satsInt = fee + self._parent_fee\n        self._total_fee_rate = str(quantize_feerate(comb_feerate))\n\n        try:\n            self._new_tx = self._wallet.wallet.cpfp(self._parent_tx, fee)\n        except CannotCPFP as e:\n            self._logger.error(str(e))\n            self.warning = str(e)\n            return\n\n        child_feerate = fee / self._new_tx.estimated_size()\n        self.feeRate = str(quantize_feerate(child_feerate))\n\n        self.update_inputs_from_tx(self._new_tx)\n        self.update_outputs_from_tx(self._new_tx)\n        self.update_target()\n        self.update_manual_fields()\n\n        self._valid = True\n        self.validChanged.emit()\n\n    def update_manual_fields(self):\n        if self._fee_method == FeeSlider.FSMethod.MANUAL:\n            if self._fee_policy.method == FeeMethod.FIXED:\n                self._userFeerate = self._total_fee_rate\n                self.userFeerateChanged.emit()\n            else:\n                self._userFee = self._total_fee.satsStr\n                self.userFeeChanged.emit()\n\n    @pyqtSlot(result=str)\n    def getNewTx(self):\n        return str(self._new_tx)\n\n\nclass QETxSweepFinalizer(QETxFinalizer):\n    _logger = get_logger(__name__)\n\n    txinsRetrieved = pyqtSignal()\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self._private_keys = ''\n        self._txins = None\n        self._amount = QEAmount(is_max=True)\n\n        self.txinsRetrieved.connect(self.update)\n\n    privateKeysChanged = pyqtSignal()\n    @pyqtProperty(str, notify=privateKeysChanged)\n    def privateKeys(self):\n        return self._private_keys\n\n    @privateKeys.setter\n    def privateKeys(self, private_keys):\n        if self._private_keys != private_keys:\n            self._private_keys = private_keys\n            self.update_privkeys()\n            self.privateKeysChanged.emit()\n\n    def make_sweep_tx(self):\n        address = self._wallet.wallet.get_receiving_address()\n        assert self._wallet.wallet.is_mine(address)\n        assert self._txins is not None\n\n        coins, keypairs = copy.deepcopy(self._txins)\n        outputs = [PartialTxOutput.from_address_and_value(address, value='!')]\n\n        tx = self._wallet.wallet.make_unsigned_transaction(\n            coins=coins, outputs=outputs, fee_policy=self._fee_policy, rbf=self._rbf, is_sweep=True)\n        self._logger.debug('fee: %d, inputs: %d, outputs: %d' % (tx.get_fee(), len(tx.inputs()), len(tx.outputs())))\n\n        tx.sign(keypairs)\n        return tx\n\n    def update_privkeys(self):\n        privkeys = keystore.get_private_keys(self._private_keys)\n\n        def fetch_privkeys_info():\n            try:\n                self._txins = self._wallet.wallet.network.run_from_another_thread(sweep_preparations(privkeys, self._wallet.wallet.network))\n                self._logger.debug(f'txins {self._txins!r}')\n            except NetworkException as e:\n                self.warning = _('Network error') + ': ' + str(e)\n                return\n            except UserFacingException as e:\n                self.warning = str(e)\n                return\n            self.txinsRetrieved.emit()\n\n        threading.Thread(target=fetch_privkeys_info, daemon=True).start()\n\n    def update(self):\n        if not self._wallet:\n            self._logger.debug('wallet not set, ignoring update()')\n            return\n        if not self._private_keys:\n            self._logger.debug('private keys not set, ignoring update()')\n            return\n        if self._txins is None:\n            self._logger.debug('txins not set, ignoring update()')\n            return\n\n        try:\n            # make unsigned transaction\n            tx = self.make_sweep_tx()\n        except NoDynamicFeeEstimates:\n            self.warning = _('No dynamic fee estimates available')\n            self._valid = False\n            self.validChanged.emit()\n            return\n        except NotEnoughFunds:\n            self.warning = _('Not enough funds')\n            self._valid = False\n            self.validChanged.emit()\n            return\n\n        self._tx = tx\n\n        amount = tx.output_value()\n\n        self._effectiveAmount.satsInt = amount\n        self.effectiveAmountChanged.emit()\n\n        self.update_from_tx(tx)\n        self.update_fee_warning_from_tx(tx=self._tx, invoice_amt=amount)\n\n        self._valid = True\n        self.validChanged.emit()\n\n        self.on_signed_tx(False, tx)\n\n    @pyqtSlot()\n    def send(self):\n        self._wallet.broadcast(self._tx)\n        self._wallet.wallet.set_label(self._tx.txid(), _('Sweep transaction'))\n"
  },
  {
    "path": "electrum/gui/qml/qetypes.py",
    "content": "from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject\n\nfrom electrum.logging import get_logger\nfrom electrum.i18n import _\n\n\nclass QEAmount(QObject):\n    \"\"\"Container for bitcoin amounts that can be passed around more\n       easily between python, QML-property and QML-javascript contexts.\n       Note: millisat and sat amounts are not synchronized!\n\n       QML type 'int' in property definitions is 32 bit signed, so will overflow easily\n       on (milli)satoshi amounts! 'int' in QML-javascript seems to be larger than 32 bit, and\n       can be used to store q(u)int64 types.\n\n       QML 'quint64' and 'qint64' can be used, but be aware these will in some cases be downcast\n       by QML to 'int' (e.g. when using the property in a property binding, _even_ when a binding\n       is done between two q(u)int64 properties (at least up until Qt6.4))\n    \"\"\"\n\n    _logger = get_logger(__name__)\n\n    def __init__(self, *, amount_sat: int = 0, amount_msat: int = 0, is_max: bool = False, from_invoice=None, parent=None):\n        super().__init__(parent)\n        self._amount_sat = int(amount_sat) if amount_sat is not None else None\n        self._amount_msat = int(amount_msat) if amount_msat is not None else None\n        self._is_max = is_max\n        if from_invoice:\n            inv_amt = from_invoice.get_amount_msat()\n            if inv_amt == '!':\n                self._is_max = True\n            elif inv_amt is not None:\n                self._amount_msat = int(inv_amt)\n                self._amount_sat = int(from_invoice.get_amount_sat())\n\n    valueChanged = pyqtSignal()\n\n    @pyqtProperty('qint64', notify=valueChanged)\n    def satsInt(self):\n        if self._amount_sat is None:  # should normally be defined when accessing this property\n            self._logger.warning('amount_sat is undefined, returning 0')\n            return 0\n        return self._amount_sat\n\n    @satsInt.setter\n    def satsInt(self, sats):\n        if self._amount_sat != sats:\n            self._amount_sat = sats\n            self.valueChanged.emit()\n\n    @pyqtProperty('qint64', notify=valueChanged)\n    def msatsInt(self):\n        if self._amount_msat is None:  # should normally be defined when accessing this property\n            self._logger.warning('amount_msat is undefined, returning 0')\n            return 0\n        return self._amount_msat\n\n    @msatsInt.setter\n    def msatsInt(self, msats):\n        if self._amount_msat != msats:\n            self._amount_msat = msats\n            self.valueChanged.emit()\n\n    @pyqtProperty(str, notify=valueChanged)\n    def satsStr(self):\n        return str(self._amount_sat)\n\n    @pyqtProperty(str, notify=valueChanged)\n    def msatsStr(self):\n        return str(self._amount_msat)\n\n    @pyqtProperty(bool, notify=valueChanged)\n    def isMax(self):\n        return self._is_max\n\n    @isMax.setter\n    def isMax(self, ismax):\n        if self._is_max != ismax:\n            self._is_max = ismax\n            self.valueChanged.emit()\n\n    @pyqtProperty(bool, notify=valueChanged)\n    def isEmpty(self):\n        return not(self._is_max or self._amount_sat or self._amount_msat)\n\n    @pyqtSlot()\n    def clear(self):\n        self._amount_sat = 0\n        self._amount_msat = 0\n        self._is_max = False\n        self.valueChanged.emit()\n\n    @pyqtSlot('QVariant')\n    def copyFrom(self, amount):\n        if not amount:\n            self._logger.warning('copyFrom with None argument. assuming 0')  # TODO\n            amount = QEAmount()\n        self.satsInt = amount.satsInt\n        self.msatsInt = amount.msatsInt\n        self.isMax = amount.isMax\n\n    def __eq__(self, other):\n        if isinstance(other, QEAmount):\n            return self._amount_sat == other._amount_sat and self._amount_msat == other._amount_msat and self._is_max == other._is_max\n        elif isinstance(other, int):\n            return self._amount_sat == other\n        elif isinstance(other, str):\n            return self.satsStr == other\n\n        return False\n\n    def __str__(self):\n        s = _('Amount')\n        if self._is_max:\n            return '%s(MAX)' % s\n        return '%s(sats=%d, msats=%d)' % (s, self._amount_sat, self._amount_msat)\n\n    def __repr__(self):\n        return f\"<QEAmount max={self._is_max} sats={self._amount_sat} msats={self._amount_msat} empty={self.isEmpty}>\"\n\n\nclass QEBytes(QObject):\n    def __init__(self, data: bytes = None, *, parent=None):\n        super().__init__(parent)\n        self.data = data\n\n    @property\n    def data(self):\n        return self._data\n\n    @data.setter\n    def data(self, _data):\n        self._data = _data\n\n    @pyqtProperty(bool)\n    def isEmpty(self):\n        return self._data is None or self._data == bytes()\n\n    def __str__(self):\n        return f'{self._data}'\n\n    def __repr__(self):\n        return f\"<QEBytes data={'None' if self._data is None else self._data.hex()}>\"\n"
  },
  {
    "path": "electrum/gui/qml/qewallet.py",
    "content": "import asyncio\nimport base64\nimport queue\nimport threading\nimport time\nfrom typing import TYPE_CHECKING, Callable, Optional, Any, Tuple\nfrom functools import partial\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer\n\nfrom electrum.i18n import _\nfrom electrum.invoices import InvoiceError, PR_PAID, PR_BROADCASTING, PR_BROADCAST\nfrom electrum.logging import get_logger\nfrom electrum.network import TxBroadcastError, BestEffortRequestFailed\nfrom electrum.transaction import PartialTransaction, Transaction\nfrom electrum.util import (\n    InvalidPassword, event_listener, AddTransactionException, get_asyncio_loop, NotEnoughFunds, NoDynamicFeeEstimates\n)\nfrom electrum.lnutil import MIN_FUNDING_SAT\nfrom electrum.plugin import run_hook\nfrom electrum.wallet import Multisig_Wallet\nfrom electrum.crypto import pw_decode_with_version_and_mac\nfrom electrum.fee_policy import FeePolicy, FixedFeePolicy\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\n\nfrom .auth import AuthMixin, auth_protect\nfrom .qeaddresslistmodel import QEAddressCoinListModel\nfrom .qechannellistmodel import QEChannelListModel\nfrom .qeinvoicelistmodel import QEInvoiceListModel, QERequestListModel\nfrom .qetransactionlistmodel import QETransactionListModel\nfrom .qetypes import QEAmount\n\nif TYPE_CHECKING:\n    from electrum.wallet import Abstract_Wallet\n    from electrum.invoices import Invoice\n\n\nclass QEWallet(AuthMixin, QObject, QtEventListener):\n    __instances = []\n\n    # this factory method should be used to instantiate QEWallet\n    # so we have only one QEWallet for each electrum.wallet\n    @classmethod\n    def getInstanceFor(cls, wallet):\n        for i in cls.__instances:\n            if i.wallet == wallet:\n                return i\n        i = QEWallet(wallet)\n        cls.__instances.append(i)\n        return i\n\n    _logger = get_logger(__name__)\n\n    # emitted when wallet wants to display a user notification\n    # actual presentation should be handled on app or window level\n    userNotify = pyqtSignal(object, object)\n\n    # shared signal for many static wallet properties\n    dataChanged = pyqtSignal()\n\n    balanceChanged = pyqtSignal()\n    requestStatusChanged = pyqtSignal([str, int], arguments=['key', 'status'])\n    requestCreateSuccess = pyqtSignal([str], arguments=['key'])\n    requestCreateError = pyqtSignal([str], arguments=['error'])\n    invoiceStatusChanged = pyqtSignal([str, int], arguments=['key', 'status'])\n    invoiceCreateSuccess = pyqtSignal()\n    invoiceCreateError = pyqtSignal([str, str], arguments=['code', 'error'])\n    paymentAuthRejected = pyqtSignal()\n    paymentSucceeded = pyqtSignal([str], arguments=['key'])\n    paymentFailed = pyqtSignal([str, str], arguments=['key', 'reason'])\n    requestNewPassword = pyqtSignal()\n    broadcastSucceeded = pyqtSignal([str], arguments=['txid'])\n    broadcastFailed = pyqtSignal([str, str, str], arguments=['txid', 'code', 'reason'])\n    saveTxSuccess = pyqtSignal([str], arguments=['txid'])\n    saveTxError = pyqtSignal([str, str, str], arguments=['txid', 'code', 'message'])\n    importChannelBackupFailed = pyqtSignal([str], arguments=['message'])\n    otpRequested = pyqtSignal()\n    otpSuccess = pyqtSignal()\n    otpFailed = pyqtSignal([str, str], arguments=['code', 'message'])\n    peersUpdated = pyqtSignal()\n    seedRetrieved = pyqtSignal()\n    messageSigned = pyqtSignal([str], arguments=['signature'])\n\n    _network_signal = pyqtSignal(str, object)\n\n    def __init__(self, wallet: 'Abstract_Wallet', parent=None):\n        super().__init__(parent)\n        self.wallet = wallet\n\n        self._logger = get_logger(f'{__name__}.[{wallet}]')\n\n        self._synchronizing = False\n        self._synchronizing_progress = ''\n\n        self._historyModel = None\n        self._addressCoinModel = None\n        self._requestModel = None\n        self._invoiceModel = None\n        self._channelModel = None\n\n        self._lightningbalance = QEAmount()\n        self._confirmedbalance = QEAmount()\n        self._unconfirmedbalance = QEAmount()\n        self._frozenbalance = QEAmount()\n        self._totalbalance = QEAmount()\n        self._lightningcanreceive = QEAmount()\n        self._minchannelfunding = QEAmount(amount_sat=int(MIN_FUNDING_SAT))\n        self._lightningcansend = QEAmount()\n        self._lightningbalancefrozen = QEAmount()\n\n        self._seed = ''\n        self._seed_passphrase = ''\n\n        self.tx_notification_queue = queue.Queue()\n        self.tx_notification_last_time = 0\n\n        self.notification_timer = QTimer(self)\n        self.notification_timer.setSingleShot(False)\n        self.notification_timer.setInterval(500)  # msec\n        self.notification_timer.timeout.connect(self.notify_transactions)\n\n        self.sync_progress_timer = QTimer(self)\n        self.sync_progress_timer.setSingleShot(False)\n        self.sync_progress_timer.setInterval(2000)\n        self.sync_progress_timer.timeout.connect(self.update_sync_progress)\n\n        # post-construction init in GUI thread\n        # QMetaObject.invokeMethod(self, 'qt_init', Qt.QueuedConnection)\n\n        # To avoid leaking references to \"self\" that prevent the\n        # window from being GC-ed when closed, callbacks should be\n        # methods of this class only, and specifically not be\n        # partials, lambdas or methods of subobjects.  Hence...\n\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.on_destroy())\n        self.synchronizing = not wallet.is_up_to_date()\n\n    synchronizingChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=synchronizingChanged)\n    def synchronizing(self):\n        return self._synchronizing\n\n    @synchronizing.setter\n    def synchronizing(self, synchronizing):\n        if self._synchronizing != synchronizing:\n            self._logger.debug(f'SYNC {self._synchronizing} -> {synchronizing}')\n            self._synchronizing = synchronizing\n            self.synchronizingChanged.emit()\n            if synchronizing:\n                if not self.sync_progress_timer.isActive():\n                    self.update_sync_progress()\n                    self.sync_progress_timer.start()\n            else:\n                self.sync_progress_timer.stop()\n\n    synchronizingProgressChanged = pyqtSignal()\n    @pyqtProperty(str, notify=synchronizingProgressChanged)\n    def synchronizingProgress(self):\n        return self._synchronizing_progress\n\n    @synchronizingProgress.setter\n    def synchronizingProgress(self, progress):\n        if self._synchronizing_progress != progress:\n            self._synchronizing_progress = progress\n            self._logger.info(progress)\n            self.synchronizingProgressChanged.emit()\n\n    multipleChangeChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=multipleChangeChanged)\n    def multipleChange(self):\n        return self.wallet.multiple_change\n\n    @multipleChange.setter\n    def multipleChange(self, multiple_change):\n        if self.wallet.multiple_change != multiple_change:\n            self.wallet.multiple_change = multiple_change\n            self.wallet.db.put('multiple_change', self.wallet.multiple_change)\n            self.multipleChangeChanged.emit()\n\n    @qt_event_listener\n    def on_event_request_status(self, wallet, key, status):\n        if wallet == self.wallet:\n            self._logger.debug('request status %d for key %s' % (status, key))\n            self.requestStatusChanged.emit(key, status)\n            if status == PR_PAID:\n                # might be new incoming LN payment, update history\n                # TODO: only update if it was paid over lightning,\n                # and even then, we can probably just add the payment instead\n                # of recreating the whole history (expensive)\n                self.historyModel.initModel(True)\n\n    @event_listener\n    def on_event_invoice_status(self, wallet, key, status):\n        if wallet == self.wallet:\n            self._logger.debug(f'invoice status update for key {key} to {status}')\n            self.invoiceStatusChanged.emit(key, status)\n\n    @qt_event_listener\n    def on_event_new_transaction(self, wallet: 'Abstract_Wallet', tx: Transaction):\n        if wallet == self.wallet:\n            self._logger.info(f'new transaction {tx.txid()}')\n            self.add_tx_notification(tx)\n            self.addressCoinModel.setDirty()\n            self.historyModel.setDirty()  # assuming wallet.is_up_to_date triggers after\n            self.balanceChanged.emit()\n\n    @qt_event_listener\n    def on_event_adb_tx_height_changed(self, adb, txid, old_height, new_height):\n        if adb == self.wallet.adb:\n            self._logger.info(f'tx_height_changed {txid}. {old_height} -> {new_height}')\n            self.historyModel.setDirty()  # assuming wallet.is_up_to_date triggers after\n\n    @qt_event_listener\n    def on_event_removed_transaction(self, wallet, tx):\n        # NOTE: this event only triggers once, only for the first deleted tx, when for imported wallets an address\n        # is deleted along with multiple associated txs\n        if wallet == self.wallet:\n            self._logger.info(f'removed transaction {tx.txid()}')\n            self.addressCoinModel.setDirty()\n            self.historyModel.setDirty()\n            self.balanceChanged.emit()\n\n    @qt_event_listener\n    def on_event_wallet_updated(self, wallet):\n        if wallet == self.wallet:\n            self._logger.debug('wallet_updated')\n            self.balanceChanged.emit()\n            self.historyModel.setDirty()\n            self.synchronizing = not wallet.is_up_to_date()\n            if not self.synchronizing:\n                self.historyModel.initModel()  # refresh if dirty\n\n    @event_listener\n    def on_event_channel(self, wallet, channel):\n        if wallet == self.wallet:\n            self.balanceChanged.emit()\n            self.peersUpdated.emit()\n\n    @event_listener\n    def on_event_channels_updated(self, wallet):\n        if wallet == self.wallet:\n            self.balanceChanged.emit()\n            self.peersUpdated.emit()\n\n    @qt_event_listener\n    def on_event_payment_succeeded(self, wallet, key):\n        if wallet == self.wallet:\n            self.paymentSucceeded.emit(key)\n            self.historyModel.initModel(True)  # TODO: be less dramatic\n\n    @event_listener\n    def on_event_payment_failed(self, wallet, key, reason):\n        if wallet == self.wallet:\n            self.paymentFailed.emit(key, reason)\n\n    def on_destroy(self):\n        self.unregister_callbacks()\n\n    def add_tx_notification(self, tx: Transaction):\n        self._logger.debug('new transaction event')\n        self.tx_notification_queue.put(tx)\n        if not self.notification_timer.isActive():\n            self._logger.debug('starting wallet notification timer')\n            self.notification_timer.start()\n\n    def notify_transactions(self):\n        if self.tx_notification_queue.qsize() == 0:\n            self._logger.debug('queue empty, stopping wallet notification timer')\n            self.notification_timer.stop()\n            return\n        if not self.wallet.is_up_to_date():\n            return  # no notifications while syncing\n        now = time.time()\n        rate_limit = 20  # seconds\n        if self.tx_notification_last_time + rate_limit > now:\n            return\n        self.tx_notification_last_time = now\n        self._logger.info(\"Notifying app about new transactions\")\n        txns = []\n        while True:\n            try:\n                txns.append(self.tx_notification_queue.get_nowait())\n            except queue.Empty:\n                break\n\n        for notification in self.wallet.get_user_notifications_for_new_txns(txns):\n            self.userNotify.emit(self.wallet, notification)\n\n    def update_sync_progress(self):\n        if self.wallet.network and self.wallet.network.is_connected():\n            num_sent, num_answered = self.wallet.adb.get_history_sync_state_details()\n            self.synchronizingProgress = \\\n                (\"{} ({}/{})\".format(_(\"Synchronizing...\"), num_answered, num_sent))\n\n    historyModelChanged = pyqtSignal()\n    @pyqtProperty(QETransactionListModel, notify=historyModelChanged)\n    def historyModel(self):\n        if self._historyModel is None:\n            self._historyModel = QETransactionListModel(self.wallet)\n        return self._historyModel\n\n    addressCoinModelChanged = pyqtSignal()\n    @pyqtProperty(QEAddressCoinListModel, notify=addressCoinModelChanged)\n    def addressCoinModel(self):\n        if self._addressCoinModel is None:\n            self._addressCoinModel = QEAddressCoinListModel(self.wallet)\n        return self._addressCoinModel\n\n    requestModelChanged = pyqtSignal()\n    @pyqtProperty(QERequestListModel, notify=requestModelChanged)\n    def requestModel(self):\n        if self._requestModel is None:\n            self._requestModel = QERequestListModel(self.wallet)\n        return self._requestModel\n\n    invoiceModelChanged = pyqtSignal()\n    @pyqtProperty(QEInvoiceListModel, notify=invoiceModelChanged)\n    def invoiceModel(self):\n        if self._invoiceModel is None:\n            self._invoiceModel = QEInvoiceListModel(self.wallet)\n        return self._invoiceModel\n\n    channelModelChanged = pyqtSignal()\n    @pyqtProperty(QEChannelListModel, notify=channelModelChanged)\n    def channelModel(self):\n        if self._channelModel is None:\n            self._channelModel = QEChannelListModel(self.wallet)\n        return self._channelModel\n\n    nameChanged = pyqtSignal()\n    @pyqtProperty(str, notify=nameChanged)\n    def name(self):\n        return self.wallet.basename()\n\n    isLightningChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=isLightningChanged)\n    def isLightning(self):\n        return bool(self.wallet.lnworker)\n\n    billingInfoChanged = pyqtSignal()\n    @pyqtProperty('QVariantMap', notify=billingInfoChanged)\n    def billingInfo(self):\n        if self.wallet.wallet_type != '2fa':\n            return {}\n        return self.wallet.billing_info if self.wallet.billing_info is not None else {}\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def canHaveLightning(self):\n        return self.wallet.can_have_lightning()\n\n    @pyqtProperty(str, notify=dataChanged)\n    def walletType(self):\n        return self.wallet.wallet_type\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def isMultisig(self):\n        return isinstance(self.wallet, Multisig_Wallet)\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def hasSeed(self):\n        return self.wallet.has_seed()\n\n    @pyqtProperty(str, notify=dataChanged)\n    def seed(self):\n        return self._seed\n\n    @pyqtProperty(str, notify=dataChanged)\n    def seedPassphrase(self):\n        return self._seed_passphrase\n\n    @pyqtProperty(str, notify=dataChanged)\n    def txinType(self):\n        if self.wallet.wallet_type == 'imported':\n            return self.wallet.txin_type\n        return self.wallet.get_txin_type(self.wallet.dummy_address())\n\n    @pyqtProperty(str, notify=dataChanged)\n    def seedType(self):\n        return self.wallet.get_seed_type()\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def isWatchOnly(self):\n        return self.wallet.is_watching_only()\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def isDeterministic(self):\n        return self.wallet.is_deterministic()\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def isEncrypted(self):\n        return self.wallet.storage.is_encrypted()\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def isHardware(self):\n        return self.wallet.storage.is_encrypted_with_hw_device()\n\n    @pyqtProperty('QVariantList', notify=dataChanged)\n    def keystores(self):\n        result = []\n        for k in self.wallet.get_keystores():\n            result.append({\n                'keystore_type': k.type,\n                'watch_only': k.is_watching_only(),\n                'derivation_prefix': (k.get_derivation_prefix() if k.is_deterministic() else '') or '',\n                'master_pubkey': (k.get_master_public_key() if k.is_deterministic() else '') or '',\n                'fingerprint': (k.get_root_fingerprint() if k.is_deterministic() else '') or '',\n                'num_imported': len(k.keypairs) if k.can_import() else 0,\n            })\n        return result\n\n    @pyqtProperty(str, notify=dataChanged)\n    def lightningNodePubkey(self):\n        return self.wallet.lnworker.node_keypair.pubkey.hex() if self.wallet.lnworker else ''\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def lightningHasDeterministicNodeId(self):\n        return self.wallet.lnworker.has_deterministic_node_id() if self.wallet.lnworker else False\n\n    @pyqtProperty(str, notify=dataChanged)\n    def derivationPrefix(self):\n        keystores = self.wallet.get_keystores()\n        if len(keystores) > 1:\n            self._logger.debug('multiple keystores not supported yet')\n        if len(keystores) == 0:\n            self._logger.debug('no keystore')\n            return ''\n        if not self.isDeterministic:\n            return ''\n        return keystores[0].get_derivation_prefix()\n\n    @pyqtProperty(str, notify=dataChanged)\n    def masterPubkey(self):\n        return self.wallet.get_master_public_key()\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def canSignWithoutServer(self):\n        return self.wallet.can_sign_without_server() if self.wallet.wallet_type == '2fa' else True\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def canSignWithoutCosigner(self):\n        if isinstance(self.wallet, Multisig_Wallet):\n            if self.wallet.wallet_type == '2fa':  # 2fa is multisig, but it handles cosigning itself\n                return True\n            return self.wallet.m == 1\n        return True\n\n    @pyqtProperty(bool, notify=dataChanged)\n    def canSignMessage(self):\n        return not isinstance(self.wallet, Multisig_Wallet) and not self.wallet.is_watching_only()\n\n    canGetZeroconfChannelChanged = pyqtSignal()\n    @pyqtProperty(bool, notify=canGetZeroconfChannelChanged)\n    def canGetZeroconfChannel(self) -> bool:\n        return self.wallet.lnworker and self.wallet.lnworker.can_get_zeroconf_channel()\n\n    @pyqtProperty(QEAmount, notify=balanceChanged)\n    def frozenBalance(self):\n        c, u, x = self.wallet.get_frozen_balance()\n        self._frozenbalance.satsInt = c+x\n        return self._frozenbalance\n\n    @pyqtProperty(QEAmount, notify=balanceChanged)\n    def unconfirmedBalance(self):\n        self._unconfirmedbalance.satsInt = self.wallet.get_balance()[1]\n        return self._unconfirmedbalance\n\n    @pyqtProperty(QEAmount, notify=balanceChanged)\n    def confirmedBalance(self):\n        c, u, x = self.wallet.get_balance()\n        self._confirmedbalance.satsInt = c+x\n        return self._confirmedbalance\n\n    @pyqtProperty(QEAmount, notify=balanceChanged)\n    def lightningBalance(self):\n        if self.isLightning:\n            self._lightningbalance.satsInt = int(self.wallet.lnworker.get_balance())\n        return self._lightningbalance\n\n    @pyqtProperty(QEAmount, notify=balanceChanged)\n    def lightningBalanceFrozen(self):\n        if self.isLightning:\n            self._lightningbalancefrozen.satsInt = int(self.wallet.lnworker.get_balance(frozen=True))\n        return self._lightningbalancefrozen\n\n    @pyqtProperty(QEAmount, notify=balanceChanged)\n    def totalBalance(self):\n        total = self.confirmedBalance.satsInt + self.lightningBalance.satsInt\n        self._totalbalance.satsInt = total\n        return self._totalbalance\n\n    @pyqtProperty(QEAmount, notify=balanceChanged)\n    def lightningCanSend(self):\n        if self.isLightning:\n            self._lightningcansend.satsInt = int(self.wallet.lnworker.num_sats_can_send())\n        return self._lightningcansend\n\n    @pyqtProperty(QEAmount, notify=balanceChanged)\n    def lightningCanReceive(self):\n        if self.isLightning:\n            self._lightningcanreceive.satsInt = int(self.wallet.lnworker.num_sats_can_receive())\n        return self._lightningcanreceive\n\n    @pyqtProperty(bool, notify=balanceChanged)\n    def isLowReserve(self):\n        return self.wallet.is_low_reserve()\n\n    @pyqtProperty(QEAmount, notify=dataChanged)\n    def minChannelFunding(self):\n        return self._minchannelfunding\n\n    @pyqtProperty(int, notify=peersUpdated)\n    def lightningNumPeers(self):\n        if self.isLightning:\n            return self.wallet.lnworker.lnpeermgr.num_peers()\n        return 0\n\n    @pyqtSlot()\n    def enableLightning(self):\n        self.wallet.init_lightning(password=self.password)\n        self.isLightningChanged.emit()\n        self.dataChanged.emit()\n\n    @auth_protect(message=_('Sign and send on-chain transaction?'))\n    def sign_and_broadcast(self, tx, *,\n                           on_success: Callable[[Transaction], None] = None,\n                           on_failure: Callable[[Optional[Any]], None] = None) -> None:\n        self.do_sign(tx, True, on_success, on_failure)\n\n    @auth_protect(message=_('Sign on-chain transaction?'))\n    def sign(self, tx, *,\n             on_success: Callable[[Transaction], None] = None,\n             on_failure: Callable[[Optional[Any]], None] = None) -> None:\n        self.do_sign(tx, False, on_success, on_failure)\n\n    def do_sign(self, tx, broadcast, on_success: Callable[[Transaction], None] = None, on_failure: Callable[[Optional[Any]], None] = None):\n        # tc_sign_wrapper is only used by 2fa. don't pass on_failure handler, it is handled via otpFailed signal\n        sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx,\n                             partial(self.on_sign_complete, broadcast, on_success),\n                             partial(self.on_sign_failed, None))\n        try:\n            # ignore_warnings=True, because UI checks and asks user confirmation itself\n            tx = self.wallet.sign_transaction(tx, self.password, ignore_warnings=True)\n        except BaseException as e:\n            self._logger.error(f'{e!r}')\n            if on_failure:\n                on_failure(str(e))\n            return\n\n        if tx is None:\n            self._logger.info('did not sign')\n            if on_failure:\n                on_failure()\n            return\n\n        if sign_hook:\n            self._logger.debug('plugin needs to sign tx too')\n            sign_hook(tx)\n            return\n\n        txid = tx.txid()\n        self._logger.debug(f'do_sign(), txid={txid}')\n\n        if not tx.is_complete():\n            self._logger.debug('tx not complete')\n            broadcast = False\n\n        if broadcast:\n            self.broadcast(tx)\n        else:\n            # not broadcasted, so refresh history here\n            self.historyModel.initModel(True)\n\n        if on_success:\n            on_success(tx)\n\n    # this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok\n    def on_sign_complete(self, broadcast, cb: Callable[[Transaction], None] = None, tx: Transaction = None):\n        self.otpSuccess.emit()\n        if cb:\n            cb(tx)\n        if broadcast:\n            self.broadcast(tx)\n\n    # this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok\n    def on_sign_failed(self, cb: Callable[[], None] = None, error: str = None):\n        self.otpFailed.emit('error', error)\n        if cb:\n            cb()\n\n    def request_otp(self, on_submit):\n        self._otp_on_submit = on_submit\n        self.otpRequested.emit()\n\n    @pyqtSlot(str)\n    def submitOtp(self, otp):\n        def submit_otp_task():\n            self._otp_on_submit(otp)\n        threading.Thread(target=submit_otp_task, daemon=True).start()\n\n    def broadcast(self, tx):\n        assert tx.is_complete()\n\n        def broadcast_thread():\n            self.wallet.set_broadcasting(tx, broadcasting_status=PR_BROADCASTING)\n            try:\n                self._logger.info('running broadcast in thread')\n                self.wallet.network.run_from_another_thread(self.wallet.network.broadcast_transaction(tx))\n            except TxBroadcastError as e:\n                self._logger.error(repr(e))\n                self.broadcastFailed.emit(tx.txid(), '', e.get_message_for_gui())\n                self.wallet.set_broadcasting(tx, broadcasting_status=None)\n            except BestEffortRequestFailed as e:\n                self._logger.error(repr(e))\n                self.broadcastFailed.emit(tx.txid(), '', repr(e))\n                self.wallet.set_broadcasting(tx, broadcasting_status=None)\n            else:\n                self._logger.info('broadcast success')\n                self.broadcastSucceeded.emit(tx.txid())\n                self.historyModel.requestRefresh.emit()  # via qt thread\n                self.wallet.set_broadcasting(tx, broadcasting_status=PR_BROADCAST)\n\n        threading.Thread(target=broadcast_thread, daemon=True).start()\n\n        # TODO: properly catch server side errors, e.g. bad-txns-inputs-missingorspent\n\n    def save_tx(self, tx: 'PartialTransaction') -> bool:\n        assert tx\n\n        try:\n            if not self.wallet.adb.add_transaction(tx):\n                self.saveTxError.emit(tx.txid(), 'conflict',\n                            _(\"Transaction could not be saved.\") + \"\\n\" + _(\"It conflicts with current history.\"))\n                return False\n            self.wallet.save_db()\n            self.saveTxSuccess.emit(tx.txid())\n            self.historyModel.initModel(True)\n            return True\n        except AddTransactionException as e:\n            self.saveTxError.emit(tx.txid(), 'error', str(e))\n            return False\n\n    def ln_auth_rejected(self):\n        self.paymentAuthRejected.emit()\n\n    @auth_protect(message=_('Pay lightning invoice?'), reject='ln_auth_rejected')\n    def pay_lightning_invoice(self, invoice: 'Invoice', amount_msat: int = None):\n        # at this point, the user confirmed the payment, potentially with an override amount.\n        # we save the invoice with the override amount if there was no amount defined in the invoice.\n        # (this is similar to what the desktop client does)\n        #\n        # Note: amount_msat can be greater than the invoice-specified amount. This is validated and handled\n        # in lnworker.pay_invoice()\n        if amount_msat is not None:\n            assert type(amount_msat) is int\n            if invoice.get_amount_msat() is None:\n                invoice.set_amount_msat(amount_msat)\n        else:\n            amount_msat = invoice.get_amount_msat()\n\n        self.wallet.save_invoice(invoice)\n        if self._invoiceModel:\n            self._invoiceModel.initModel()\n\n        def pay_thread():\n            try:\n                coro = self.wallet.lnworker.pay_invoice(invoice, amount_msat=amount_msat)\n                fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop())\n                fut.result()\n            except Exception as e:\n                self._logger.error(f'pay_invoice failed! {e!r}')\n                self.paymentFailed.emit(invoice.get_id(), str(e))\n\n        threading.Thread(target=pay_thread, daemon=True).start()\n\n    @pyqtSlot()\n    def deleteExpiredRequests(self):\n        keys = self.wallet.delete_expired_requests()\n        for key in keys:\n            self.requestModel.delete_invoice(key)\n\n    @pyqtSlot(QEAmount, str, int)\n    @pyqtSlot(QEAmount, str, int, bool)\n    @pyqtSlot(QEAmount, str, int, bool, bool)\n    @pyqtSlot(QEAmount, str, int, bool, bool, bool)\n    def createRequest(self, amount: QEAmount, message: str, expiration: int, lightning: bool = False, reuse_address: bool = False):\n        self.deleteExpiredRequests()\n        try:\n            amount = amount.satsInt\n            if not lightning:\n                addr = self.wallet.get_unused_address()\n                if addr is None:\n                    if reuse_address:\n                        addr = self.wallet.get_receiving_address()\n                    else:\n                        msg = [\n                            _('No address available.'),\n                            _('All your addresses are used in pending requests.'),\n                            _('To see the list, press and hold the Receive button.'),\n                        ]\n                        self.requestCreateError.emit(' '.join(msg))\n                        return\n            else:\n                addr = None\n\n            key = self.wallet.create_request(amount, message, expiration, addr)\n        except InvoiceError as e:\n            self.requestCreateError.emit(_('Error creating payment request') + ':\\n' + str(e))\n            return\n\n        assert key is not None\n        self._logger.debug(f'created request with key {key} addr {addr}')\n        self.addressCoinModel.setDirty()\n        self.requestModel.add_invoice(self.wallet.get_request(key))\n        self.requestCreateSuccess.emit(key)\n\n    @pyqtSlot(str)\n    def deleteRequest(self, key: str):\n        self._logger.debug('delete req %s' % key)\n        self.wallet.delete_request(key)\n        self.requestModel.delete_invoice(key)\n\n    @pyqtSlot(str)\n    def deleteInvoice(self, key: str):\n        self._logger.debug('delete inv %s' % key)\n        self.wallet.delete_invoice(key)\n        self.invoiceModel.delete_invoice(key)\n\n    @pyqtSlot(str, result=bool)\n    def verifyPassword(self, password):\n        if not self.wallet.has_password():\n            return not bool(password)\n        try:\n            self.wallet.check_password(password)\n            return True\n        except InvalidPassword as e:\n            return False\n\n    @pyqtSlot(str, result=bool)\n    def setPassword(self, password):\n        if password == '':\n            password = None\n\n        storage = self.wallet.storage\n\n        # HW wallet not supported yet\n        if storage.is_encrypted_with_hw_device():\n            return False\n\n        current_password = self.password if self.password != '' else None\n\n        try:\n            self._logger.info('setting new password')\n            self.wallet.update_password(current_password, password, encrypt_storage=True)\n            # restore the invariant that all loaded wallets in qml must be unlocked:\n            self.wallet.unlock(password)\n            return True\n        except InvalidPassword as e:\n            self._logger.exception(repr(e))\n            return False\n\n    @property\n    def password(self):\n        return self.wallet.get_unlocked_password()\n\n    @pyqtSlot(str)\n    def importAddresses(self, addresslist):\n        self.wallet.import_addresses(addresslist.split())\n        if self._addressCoinModel:\n            self._addressCoinModel.setDirty()\n        self.dataChanged.emit()\n\n    @pyqtSlot(str)\n    def importPrivateKeys(self, keyslist):\n        self.wallet.import_private_keys(keyslist.split(), self.password)\n        if self._addressCoinModel:\n            self._addressCoinModel.setDirty()\n        self.dataChanged.emit()\n\n    @pyqtSlot(str)\n    def importChannelBackup(self, backup_str):\n        try:\n            self.wallet.lnworker.import_channel_backup(backup_str)\n        except Exception as e:\n            self._logger.debug(f'could not import channel backup: {repr(e)}')\n            self.importChannelBackupFailed.emit(f'Failed to import backup:\\n\\n{str(e)}')\n\n    @pyqtSlot(str, result=bool)\n    def isValidChannelBackup(self, backup_str):\n        try:\n            assert backup_str.startswith('channel_backup:')\n            encrypted = backup_str[15:]\n            xpub = self.wallet.get_fingerprint()\n            decrypted = pw_decode_with_version_and_mac(encrypted, xpub)\n            return True\n        except Exception:\n            return False\n\n    @pyqtSlot()\n    def requestShowSeed(self):\n        self.retrieve_seed()\n\n    @auth_protect(method='wallet')\n    def retrieve_seed(self):\n        try:\n            self._seed = self.wallet.get_seed(self.password)\n            self._seed_passphrase = self.wallet.keystore.get_passphrase(self.password)\n            self.seedRetrieved.emit()\n        except Exception:\n            self._seed = ''\n            self._seed_passphrase = ''\n\n        self.dataChanged.emit()\n\n    @pyqtSlot(str, result='QVariantList')\n    def getSerializedTx(self, txid):\n        tx = self.wallet.db.get_transaction(txid)\n        txqr = tx.to_qr_data()\n        return [str(tx), txqr[0], txqr[1]]\n\n    @pyqtSlot(result='QVariantMap')\n    def getBalancesForPiechart(self):\n        p_bal = self.wallet.get_balances_for_piechart()\n        return {\n            'confirmed': p_bal.confirmed,\n            'unconfirmed': p_bal.unconfirmed,\n            'unmatured': p_bal.unmatured,\n            'frozen': p_bal.frozen,\n            'lightning': int(p_bal.lightning),\n            'f_lightning': int(p_bal.lightning_frozen),\n            'total': int(p_bal.total())\n        }\n\n    @pyqtSlot(str, result=bool)\n    def isAddressMine(self, addr):\n        return self.wallet.is_mine(addr)\n\n    @pyqtSlot(str, str)\n    @auth_protect(message=_(\"Sign message?\"))\n    def signMessage(self, address, message):\n        sig = self.wallet.sign_message(address, message, self.password)\n        result = base64.b64encode(sig).decode('ascii')\n        self.messageSigned.emit(result)\n\n    def determine_max(self, *, mktx: Callable[[FeePolicy], PartialTransaction]) -> Tuple[Optional[int], Optional[str]]:\n        # TODO: merge with SendTab.spend_max() and move to backend wallet\n        amount = message = None\n        try:\n            try:\n                fee_policy = FeePolicy(self.wallet.config.FEE_POLICY)\n                tx = mktx(fee_policy)\n            except (NotEnoughFunds, NoDynamicFeeEstimates) as e:\n                # Check if we had enough funds excluding fees,\n                # if so, still provide opportunity to set lower fees.\n                fee_policy = FixedFeePolicy(0)\n                tx = mktx(fee_policy)\n            amount = tx.output_value()\n        except NotEnoughFunds as e:\n            self._logger.debug(str(e))\n            message = self.wallet.get_text_not_enough_funds_mentioning_frozen(for_amount='!')\n\n        return amount, message\n"
  },
  {
    "path": "electrum/gui/qml/qewizard.py",
    "content": "import os\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject\n\nfrom electrum.base_crash_reporter import send_exception_to_crash_reporter\nfrom electrum.logging import get_logger\nfrom electrum import mnemonic\nfrom electrum.wizard import NewWalletWizard, ServerConnectWizard, TermsOfUseWizard\nfrom electrum.storage import WalletStorage, StorageReadWriteError\nfrom electrum.util import WalletFileException, UserFacingException\nfrom electrum.gui import messages\n\nif TYPE_CHECKING:\n    from electrum.gui.qml.qedaemon import QEDaemon\n    from electrum.plugin import Plugins\n\n\nclass QEAbstractWizard(QObject):\n    \"\"\" Concrete subclasses of QEAbstractWizard must also inherit from a concrete AbstractWizard subclass.\n        QEAbstractWizard forms the base for all QML GUI based wizards, while AbstractWizard defines\n        the base for non-gui wizard flow navigation functionality.\n    \"\"\"\n    _logger = get_logger(__name__)\n\n    def __init__(self, parent=None):\n        QObject.__init__(self, parent)\n\n    @pyqtSlot(result=str)\n    def startWizard(self):\n        self.start()\n        return self._current.view\n\n    @pyqtSlot(str, result=str)\n    def viewToComponent(self, view):\n        return self.navmap[view]['gui'] + '.qml'\n\n    @pyqtSlot('QJSValue', result='QVariant')\n    def submit(self, wizard_data):\n        wdata = wizard_data.toVariant()\n        view = self.resolve_next(self._current.view, wdata)\n        return { 'view': view.view, 'wizard_data': view.wizard_data }\n\n    @pyqtSlot(result='QVariant')\n    def prev(self):\n        viewstate = self.resolve_prev()\n        return viewstate.wizard_data\n\n    @pyqtSlot('QJSValue', result=bool)\n    def isLast(self, wizard_data):\n        wdata = wizard_data.toVariant()\n        return self.is_last_view(self._current.view, wdata)\n\n\nclass QENewWalletWizard(NewWalletWizard, QEAbstractWizard):\n    createError = pyqtSignal([str], arguments=[\"error\"])\n    createSuccess = pyqtSignal()\n\n    def __init__(self, daemon: 'QEDaemon', plugins: 'Plugins', parent=None):\n        NewWalletWizard.__init__(self, daemon.daemon, plugins)\n        QEAbstractWizard.__init__(self, parent)\n        self._qedaemon = daemon\n        self._path = None\n        self._password = None\n\n        # attach view names and accept handlers\n        self.navmap_merge({\n            'wallet_name': {'gui': 'WCWalletName'},\n            'wallet_type': {'gui': 'WCWalletType'},\n            'keystore_type': {'gui': 'WCKeystoreType'},\n            'create_seed': {'gui': 'WCCreateSeed'},\n            'create_ext': {'gui': 'WCEnterExt'},\n            'confirm_seed': {'gui': 'WCConfirmSeed'},\n            'confirm_ext': {'gui': 'WCConfirmExt'},\n            'have_seed': {'gui': 'WCHaveSeed'},\n            'have_ext': {'gui': 'WCEnterExt'},\n            'script_and_derivation': {'gui': 'WCScriptAndDerivation'},\n            'have_master_key': {'gui': 'WCHaveMasterKey'},\n            'multisig': {'gui': 'WCMultisig'},\n            'multisig_cosigner_keystore': {'gui': 'WCCosignerKeystore'},\n            'multisig_cosigner_key': {'gui': 'WCHaveMasterKey'},\n            'multisig_cosigner_seed': {'gui': 'WCHaveSeed'},\n            'multisig_cosigner_have_ext': {'gui': 'WCEnterExt'},\n            'multisig_cosigner_script_and_derivation': {'gui': 'WCScriptAndDerivation'},\n            'imported': {'gui': 'WCImport'},\n            'wallet_password': {'gui': 'WCWalletPassword'}\n        })\n\n    pathChanged = pyqtSignal()\n    @pyqtProperty(str, notify=pathChanged)\n    def path(self):\n        return self._path\n\n    @path.setter\n    def path(self, path):\n        self._path = path\n        self.pathChanged.emit()\n\n    def is_single_password(self):\n        return self._qedaemon.singlePasswordEnabled\n\n    @pyqtSlot('QJSValue', result=bool)\n    def hasDuplicateMasterKeys(self, js_data):\n        self._logger.info('Checking for duplicate masterkeys')\n        data = js_data.toVariant()\n        return self.has_duplicate_masterkeys(data)\n\n    @pyqtSlot('QJSValue', result=bool)\n    def hasHeterogeneousMasterKeys(self, js_data):\n        self._logger.info('Checking for heterogeneous masterkeys')\n        data = js_data.toVariant()\n        return self.has_heterogeneous_masterkeys(data)\n\n    @pyqtSlot(str, str, result=bool)\n    def isMatchingSeed(self, seed, seed_again):\n        return mnemonic.is_matching_seed(seed=seed, seed_again=seed_again)\n\n    @pyqtSlot(str, str, str, result='QVariantMap')\n    def verifySeed(self, seed, seed_variant, wallet_type='standard'):\n        seed_valid, seed_type, validation_message, can_passphrase = self.validate_seed(seed, seed_variant, wallet_type)\n        return {\n            'valid': seed_valid,\n            'type': seed_type,\n            'message': validation_message,\n            'can_passphrase': can_passphrase\n        }\n\n    def _wallet_path_from_wallet_name(self, wallet_name: str) -> str:\n        return os.path.join(self._qedaemon.daemon.config.get_datadir_wallet_path(), wallet_name)\n\n    @pyqtSlot(str, result=bool)\n    def isValidNewWalletName(self, wallet_name: str) -> bool:\n        if not wallet_name:\n            return False\n        if self._qedaemon.availableWallets.wallet_name_exists(wallet_name):\n            return False\n        wallet_path = self._wallet_path_from_wallet_name(wallet_name)\n        # note: we should probably restrict wallet names to be alphanumeric (plus underscore, etc)...\n        # try to prevent sketchy path traversals:\n        for forbidden_char in (\"/\", \"\\\\\", ):\n            if forbidden_char in wallet_name:\n                return False\n        if os.path.basename(wallet_name) != wallet_name:\n            return False\n        # validate that the path looks sane to the filesystem:\n        try:\n            temp_storage = WalletStorage(wallet_path)\n        except (StorageReadWriteError, WalletFileException) as e:\n            return False\n        except Exception as e:\n            self._logger.exception(\"\")\n            return False\n        if temp_storage.file_exists():\n            return False\n        return True\n\n    @pyqtSlot('QJSValue', bool, str)\n    def createStorage(self, js_data, single_password_enabled, single_password):\n        self._logger.info('Creating wallet from wizard data')\n        data = js_data.toVariant()\n\n        if single_password_enabled and single_password:\n            data['encrypt'] = True\n            data['password'] = single_password\n\n        path = self._wallet_path_from_wallet_name(data['wallet_name'])\n\n        try:\n            self.create_storage(path, data)\n\n            # minimally populate self after create\n            self._password = data['password']\n            self.path = path\n\n            self.createSuccess.emit()\n        except UserFacingException as e:\n            self._logger.debug(f\"createStorage errored: {e!r}\", exc_info=True)\n            self.createError.emit(str(e))\n        except Exception as e:\n            self._logger.exception(f\"createStorage errored: {e!r}\")\n            send_exception_to_crash_reporter(e)\n\n\nclass QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard):\n    def __init__(self, daemon: 'QEDaemon', parent=None):\n        ServerConnectWizard.__init__(self, daemon.daemon)\n        QEAbstractWizard.__init__(self, parent)\n\n        # attach view names\n        self.navmap_merge({\n            'welcome': {'gui': 'WCWelcome'},\n            'proxy_config': {'gui': 'WCProxyConfig'},\n            'server_config': {'gui': 'WCServerConfig'},\n        })\n\n\nclass QETermsOfUseWizard(TermsOfUseWizard, QEAbstractWizard):\n    def __init__(self, daemon: 'QEDaemon', parent=None):\n        TermsOfUseWizard.__init__(self, daemon.daemon.config)\n        QEAbstractWizard.__init__(self, parent)\n\n        # attach gui classes\n        self.navmap_merge({\n            'terms_of_use': {'gui': 'WCTermsOfUseRequest'},\n        })\n\n    termsOfUseChanged = pyqtSignal()\n    @pyqtProperty(str, notify=termsOfUseChanged)\n    def termsOfUseText(self):\n        return messages.MSG_TERMS_OF_USE\n"
  },
  {
    "path": "electrum/gui/qml/util.py",
    "content": "import math\nimport re\n\nfrom time import time\nfrom typing import Tuple\n\nfrom electrum.i18n import _\n\n\n# return delay in msec when expiry time string should be updated\n# returns 0 when expired or expires > 1 day away (no updates needed)\ndef status_update_timer_interval(exp):\n    # very roughly according to util.age\n    exp_in = int(exp - time())\n    exp_in_min = int(exp_in/60)\n\n    interval = 0\n    if exp_in < 0:\n        interval = 0\n    elif exp_in_min < 2:\n        interval = 1000\n    elif exp_in_min < 90:\n        interval = 1000 * 60\n    elif exp_in_min < 1440:\n        interval = 1000 * 60 * 60\n\n    return interval\n\n\n# TODO: copied from qt password_dialog.py, move to common code\ndef check_password_strength(password: str) -> Tuple[int, str]:\n    \"\"\"Check the strength of the password entered by the user and return back the same\n    :param password: password entered by user in New Password\n    :return: password strength Weak or Medium or Strong\"\"\"\n    password = password\n    n = math.log(len(set(password)))\n    num = re.search(\"[0-9]\", password) is not None and re.match(\"^[0-9]*$\", password) is None\n    caps = password != password.upper() and password != password.lower()\n    extra = re.match(\"^[a-zA-Z0-9]*$\", password) is None\n    score = len(password)*(n + caps + num + extra)/20\n    password_strength = {0: _('Weak'), 1: _('Medium'), 2: _('Strong'), 3: _('Very Strong')}\n    return min(3, int(score)), password_strength[min(3, int(score))]\n"
  },
  {
    "path": "electrum/gui/qt/__init__.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2012 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport os\nimport signal\nimport sys\nimport threading\nfrom typing import Optional, TYPE_CHECKING, List, Sequence, Union\n\ntry:\n    import PyQt6\n    import PyQt6.QtGui\nexcept Exception as e:\n    from electrum import GuiImportError\n    raise GuiImportError(\n        \"Error: Could not import PyQt6. On Linux systems, \"\n        \"you may try 'sudo apt-get install python3-pyqt6'\") from e\n\nfrom PyQt6.QtGui import QGuiApplication, QCursor\nfrom PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QWidget, QMenu, QMessageBox, QDialog, QToolTip\nfrom PyQt6.QtCore import QObject, pyqtSignal, QTimer, Qt\n\nimport PyQt6.QtCore as QtCore\n\nfrom electrum.logging import Logger, get_logger\n_logger = get_logger(__name__)\n\ntry:\n    # Preload QtMultimedia at app start, if available.\n    # We use QtMultimedia on some platforms for camera-handling, and\n    # lazy-loading it later led to some crashes. Maybe due to bugs in PyQt. (see #7725)\n    from PyQt6.QtMultimedia import QMediaDevices; del QMediaDevices\nexcept (ImportError, RuntimeError) as e:\n    _logger.debug(f\"failed to import optional dependency: PyQt6.QtMultimedia. exc={repr(e)}\")\n    pass  # failure is ok; it is an optional dependency.\nelse:\n    _logger.debug(f\"successfully preloaded optional dependency: PyQt6.QtMultimedia\")\n\nif sys.platform == \"linux\" and os.environ.get(\"APPIMAGE\"):\n    # For AppImage, we default to xcb qt backend, for better support of older system.\n    # qt6 normally defaults to QT_QPA_PLATFORM=wayland instead of QT_QPA_PLATFORM=xcb.\n    # However, the wayland QPA plugin requires libwayland-client0>=1.19, which is too new\n    # for debian 11 or ubuntu 20.04. So instead, we default to the X11 integration (and not wayland).\n    # see https://bugreports.qt.io/browse/QTBUG-114635\n    os.environ.setdefault(\"QT_QPA_PLATFORM\", \"xcb\")\n\nfrom electrum.i18n import _, set_language\nfrom electrum.plugin import run_hook\nfrom electrum.util import (UserCancelled, profiler, send_exception_to_crash_reporter,\n                           WalletFileException, get_new_wallet_name, InvalidPassword,\n                           standardize_path, UserFacingException)\nfrom electrum.wallet import Wallet, Abstract_Wallet\nfrom electrum.wallet_db import WalletRequiresSplit, WalletRequiresUpgrade, WalletUnfinished\nfrom electrum.gui import BaseElectrumGui\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.wizard import WizardViewState\nfrom electrum.keystore import load_keystore\nfrom electrum.bip32 import is_xprv\nfrom electrum import constants\n\nfrom electrum.gui.common_qt.i18n import ElectrumTranslator\nfrom electrum.gui.messages import TERMS_OF_USE_LATEST_VERSION\n\nfrom .util import (read_QIcon, ColorScheme, custom_message_box, MessageBoxMixin, WWLabel,\n                   set_windows_os_screenshot_protection_drm_flag)\nfrom .main_window import ElectrumWindow\nfrom .network_dialog import NetworkDialog\nfrom .stylesheet_patcher import patch_qt_stylesheet\nfrom .lightning_dialog import LightningDialog\nfrom .exception_window import Exception_Hook\nfrom .wizard.server_connect import QEServerConnectWizard\nfrom .wizard.wallet import QENewWalletWizard\n\nif TYPE_CHECKING:\n    from electrum.daemon import Daemon\n    from electrum.plugin import Plugins\n\n\nclass OpenFileEventFilter(QObject):\n    def __init__(self, windows: Sequence[ElectrumWindow]):\n        self.windows = windows\n        super(OpenFileEventFilter, self).__init__()\n\n    def eventFilter(self, obj, event):\n        if event.type() == QtCore.QEvent.Type.FileOpen:\n            if len(self.windows) >= 1:\n                self.windows[0].set_payment_identifier(event.url().toString())\n                return True\n        return False\n\n\nclass ScreenshotProtectionEventFilter(QObject):\n    def __init__(self):\n        super().__init__()\n\n    def eventFilter(self, obj, event):\n        if (\n            event.type() == QtCore.QEvent.Type.Show\n            and isinstance(obj, QWidget)\n            and obj.isWindow()\n        ):\n            set_windows_os_screenshot_protection_drm_flag(obj)\n        return False\n\n\nclass QElectrumApplication(QApplication):\n    new_window_signal = pyqtSignal(str, object)\n    quit_signal = pyqtSignal()\n    refresh_tabs_signal = pyqtSignal()\n    refresh_amount_edits_signal = pyqtSignal()\n    update_status_signal = pyqtSignal()\n    update_fiat_signal = pyqtSignal()\n    alias_received_signal = pyqtSignal()\n\n\nclass ElectrumGui(BaseElectrumGui, Logger):\n\n    network_dialog: Optional['NetworkDialog']\n    lightning_dialog: Optional['LightningDialog']\n\n    @profiler\n    def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'):\n        BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins)\n        Logger.__init__(self)\n        self.logger.info(f\"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}\")\n        # Uncomment this call to verify objects are being properly\n        # GC-ed when windows are closed\n        #plugins.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,\n        #                            ElectrumWindow], interval=5)])\n        if hasattr(QtCore.Qt, \"AA_ShareOpenGLContexts\"):\n            QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)\n        if hasattr(QGuiApplication, 'setDesktopFileName'):\n            QGuiApplication.setDesktopFileName('electrum')\n        QGuiApplication.setApplicationName(\"Electrum\")\n        self.gui_thread = threading.current_thread()\n        self.windows = []  # type: List[ElectrumWindow]\n        self.open_file_efilter = OpenFileEventFilter(self.windows)\n        self.app = QElectrumApplication(sys.argv)\n        self.app.installEventFilter(self.open_file_efilter)\n        self.screenshot_protection_efilter = ScreenshotProtectionEventFilter()\n        if sys.platform in ['win32', 'windows'] and self.config.GUI_QT_SCREENSHOT_PROTECTION:\n            self.app.installEventFilter(self.screenshot_protection_efilter)\n        # explicitly set 'AA_DontShowIconsInMenus' False so menu icons are shown on MacOS\n        self.app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus, on=False)\n        self.app.setWindowIcon(read_QIcon(\"electrum.png\"))\n        self.translator = ElectrumTranslator()\n        self.app.installTranslator(self.translator)\n        self._cleaned_up = False\n        self.network_dialog = None\n        self.lightning_dialog = None\n        self._num_wizards_in_progress = 0\n        self._num_wizards_lock = threading.Lock()\n        self.dark_icon = self.config.GUI_QT_DARK_TRAY_ICON\n        self.tray = None  # type: Optional[QSystemTrayIcon]\n        self._init_tray()\n        self.app.new_window_signal.connect(self.start_new_window)\n        self.app.quit_signal.connect(self.app.quit, Qt.ConnectionType.QueuedConnection)\n        # maybe set dark theme\n        self._default_qtstylesheet = self.app.styleSheet()\n        self.reload_app_stylesheet()\n\n    def _init_tray(self):\n        self.tray = QSystemTrayIcon(self.tray_icon(), None)\n        self.tray.setToolTip('Electrum')\n        self.tray.activated.connect(self.tray_activated)\n        self.build_tray_menu()\n        self.tray.show()\n\n    def reload_app_stylesheet(self):\n        \"\"\"Set the Qt stylesheet and custom colors according to the user-selected\n        light/dark theme.\n        TODO this can ~almost be used to change the theme at runtime (without app restart),\n             except for util.ColorScheme... widgets already created with colors set using\n             ColorSchemeItem.as_stylesheet() and similar will not get recolored.\n             See e.g.\n             - in Coins tab, the color for \"frozen\" UTXOs, or\n             - in TxDialog, the receiving/change address colors\n        \"\"\"\n        use_dark_theme = self.config.GUI_QT_COLOR_THEME == 'dark'\n        if use_dark_theme:\n            try:\n                import qdarkstyle\n                self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt6())\n            except BaseException as e:\n                use_dark_theme = False\n                self.logger.warning(f'Error setting dark theme: {repr(e)}')\n        else:\n            self.app.setStyleSheet(self._default_qtstylesheet)\n        # Apply any necessary stylesheet patches\n        patch_qt_stylesheet(use_dark_theme=use_dark_theme)\n        # Even if we ourselves don't set the dark theme,\n        # the OS/window manager/etc might set *a dark theme*.\n        # Hence, try to choose colors accordingly:\n        ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme)\n\n    def build_tray_menu(self):\n        if not self.tray:\n            return\n        # Avoid immediate GC of old menu when window closed via its action\n        if self.tray.contextMenu() is None:\n            m = QMenu()\n            self.tray.setContextMenu(m)\n        else:\n            m = self.tray.contextMenu()\n            m.clear()\n        network = self.daemon.network\n        m.addAction(_(\"Plugins\"), self.show_plugins_dialog)\n        if network:\n            m.addAction(_(\"Network\"), self.show_network_dialog)\n        if network and network.lngossip:\n            m.addAction(_(\"Lightning Network\"), self.show_lightning_dialog)\n        for window in self.windows:\n            name = window.wallet.basename()\n            submenu = m.addMenu(name)\n            submenu.addAction(_(\"Show/Hide\"), window.show_or_hide)\n            submenu.addAction(_(\"Close\"), window.close)\n        m.addAction(_(\"Dark/Light\"), self.toggle_tray_icon)\n        m.addSeparator()\n        m.addAction(_(\"Exit Electrum\"), self.app.quit)\n\n    def tray_icon(self):\n        if self.dark_icon:\n            return read_QIcon('electrum_dark_icon.png')\n        else:\n            return read_QIcon('electrum_light_icon.png')\n\n    def toggle_tray_icon(self):\n        if not self.tray:\n            return\n        self.dark_icon = not self.dark_icon\n        self.config.GUI_QT_DARK_TRAY_ICON = self.dark_icon\n        self.tray.setIcon(self.tray_icon())\n\n    def tray_activated(self, reason):\n        if reason == QSystemTrayIcon.ActivationReason.DoubleClick:\n            if all([w.is_hidden() for w in self.windows]):\n                for w in self.windows:\n                    w.bring_to_top()\n            else:\n                for w in self.windows:\n                    w.hide()\n\n    def _cleanup_before_exit(self):\n        if self._cleaned_up:\n            return\n        self._cleaned_up = True\n        self.app.new_window_signal.disconnect()\n        self.app.removeEventFilter(self.open_file_efilter)\n        self.open_file_efilter = None\n        # it is save to remove the filter, even if it has not been installed\n        self.app.removeEventFilter(self.screenshot_protection_efilter)\n        self.screenshot_protection_efilter = None\n        # If there are still some open windows, try to clean them up.\n        for window in list(self.windows):\n            window.close()\n            window.clean_up()\n        if self.network_dialog:\n            self.network_dialog.close()\n            self.network_dialog = None\n        if self.lightning_dialog:\n            self.lightning_dialog.close()\n            self.lightning_dialog = None\n        # clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html\n        event = QtCore.QEvent(QtCore.QEvent.Type.Clipboard)\n        self.app.sendEvent(self.app.clipboard(), event)\n        if self.tray:\n            self.tray.hide()\n            self.tray.deleteLater()\n            self.tray = None\n\n    def _maybe_quit_if_no_windows_open(self) -> None:\n        \"\"\"Check if there are any open windows and decide whether we should quit.\"\"\"\n        # keep daemon running after close\n        if self.config.get('daemon'):\n            return\n        # check if a wizard is in progress\n        with self._num_wizards_lock:\n            if self._num_wizards_in_progress > 0 or len(self.windows) > 0:\n                return\n        self.app.quit()\n\n    def new_window(self, path, uri=None):\n        # Use a signal as can be called from daemon thread\n        self.app.new_window_signal.emit(path, uri)\n\n    def show_lightning_dialog(self):\n        if not self.daemon.network.has_channel_db():\n            return\n        if not self.lightning_dialog:\n            self.lightning_dialog = LightningDialog(self)\n        self.lightning_dialog.bring_to_top()\n\n    def show_plugins_dialog(self):\n        from .plugins_dialog import PluginsDialog\n        d = PluginsDialog(self.config, self.plugins, gui_object=self)\n        d.exec()\n\n    def show_network_dialog(self, proxy_tab=False):\n        if self.network_dialog:\n            self.network_dialog.show(proxy_tab=proxy_tab)\n            self.network_dialog.raise_()\n            return\n        self.network_dialog = NetworkDialog(network=self.daemon.network)\n        self.network_dialog.show(proxy_tab=proxy_tab)\n\n    def _create_window_for_wallet(self, wallet):\n        w = ElectrumWindow(self, wallet)\n        self.windows.append(w)\n        self.build_tray_menu()\n        w.warn_if_testnet()\n        w.warn_if_watching_only()\n        return w\n\n    def count_wizards_in_progress(func):\n        def wrapper(self: 'ElectrumGui', *args, **kwargs):\n            with self._num_wizards_lock:\n                self._num_wizards_in_progress += 1\n            try:\n                return func(self, *args, **kwargs)\n            finally:\n                with self._num_wizards_lock:\n                    self._num_wizards_in_progress -= 1\n                self._maybe_quit_if_no_windows_open()\n        return wrapper\n\n    def get_window_for_wallet(self, wallet):\n        for window in self.windows:\n            if window.wallet.storage.path == wallet.storage.path:\n                return window\n\n    @count_wizards_in_progress\n    def start_new_window(\n            self,\n            path,\n            uri: Optional[str],\n            *,\n            app_is_starting: bool = False,\n            force_wizard: bool = False,\n    ) -> Optional[ElectrumWindow]:\n        \"\"\"Raises the window for the wallet if it is open.\n        Otherwise, opens the wallet and creates a new window for it.\n        Warning: the returned window might be for a completely different wallet\n                 than the provided path, as we allow user interaction to change the path.\n        \"\"\"\n        if not self.has_accepted_terms_of_use():\n            self.logger.warning(f\"terms of use not accepted, rejecting to start new window\")\n            return None\n\n        wallet = None\n        # Try to open with daemon first. If this succeeds, there won't be a wizard at all\n        # (the wallet main window will appear directly).\n        if not force_wizard:\n            try:\n                wallet = self.daemon.load_wallet(path, None)\n            except FileNotFoundError:\n                pass  # open with wizard below\n            except InvalidPassword:\n                pass  # open with wizard below\n            except WalletRequiresSplit:\n                pass  # open with wizard below\n            except WalletRequiresUpgrade:\n                pass  # open with wizard below\n            except WalletUnfinished:\n                pass  # open with wizard below\n            except Exception as e:\n                self.logger.exception('')\n                err_text = str(e) if isinstance(e, WalletFileException) else repr(e)\n                custom_message_box(icon=QMessageBox.Icon.Warning,\n                                   parent=None,\n                                   title=_('Error'),\n                                   text=_('Cannot load wallet') + ' (1):\\n' + err_text)\n                if isinstance(e, WalletFileException) and e.should_report_crash:\n                    send_exception_to_crash_reporter(e)\n                # if app is starting, still let wizard appear\n                if not app_is_starting:\n                    return\n        # Open a wizard window. This lets the user e.g. enter a password, or select\n        # a different wallet.\n        try:\n            if not wallet:\n                wallet = self._start_wizard_to_select_or_create_wallet(path)\n            if not wallet:\n                return\n            window = self.get_window_for_wallet(wallet)\n            # create or raise window\n            if not window:\n                window = self._create_window_for_wallet(wallet)\n        except UserCancelled:\n            return\n        except Exception as e:\n            self.logger.exception('')\n            if isinstance(e, UserFacingException) \\\n                    or isinstance(e, WalletFileException) and not e.should_report_crash:\n                err_text = str(e) if isinstance(e, WalletFileException) else repr(e)\n                custom_message_box(icon=QMessageBox.Icon.Warning,\n                                   parent=None,\n                                   title=_('Error'),\n                                   text=_('Cannot load wallet') + '(2) :\\n' + err_text)\n            else:\n                send_exception_to_crash_reporter(e)\n            if app_is_starting:\n                # If we raise in this context, there are no more fallbacks, we will shut down.\n                # Worst case scenario, we might have gotten here without user interaction,\n                # in which case, if we raise now without user interaction, the same sequence of\n                # events is likely to repeat when the user restarts the process.\n                # So we play it safe: clear path, clear uri, force a wizard to appear.\n                try:\n                    wallet_dir = os.path.dirname(path)\n                    filename = get_new_wallet_name(wallet_dir)\n                except OSError:\n                    path = self.config.get_fallback_wallet_path()\n                else:\n                    path = os.path.join(wallet_dir, filename)\n                return self.start_new_window(path, uri=None, force_wizard=True)\n            return\n        window.bring_to_top()\n        window.setWindowState(window.windowState() & ~Qt.WindowState.WindowMinimized | Qt.WindowState.WindowActive)\n        window.activateWindow()\n        if uri:\n            window.show_send_tab()\n            window.send_tab.set_payment_identifier(uri)\n        return window\n\n    def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]:\n        wizard = QENewWalletWizard(self.config, self.app, self.plugins, self.daemon, path)\n        result = wizard.exec()\n        # TODO: use dialog.open() instead to avoid new event loop spawn?\n        self.logger.info(f'wizard dialog exec result={result}')\n        if result == QDialog.DialogCode.Rejected:\n            self.logger.info('wizard dialog cancelled by user')\n            return\n\n        d = wizard.get_wizard_data()\n\n        if d['wallet_is_open']:\n            wallet_path = standardize_path(d['wallet_name'])\n            for window in self.windows:\n                if window.wallet.storage.path == wallet_path:\n                    return window.wallet\n            raise Exception('found by wizard but not here?!')\n\n        if not d['wallet_exists']:\n            self.logger.info('about to create wallet')\n            wizard.create_storage()\n            if d['wallet_type'] == '2fa' and 'x3' not in d:\n                return\n            wallet_file = wizard.path\n        else:\n            wallet_file = d['wallet_name']\n\n        password = d.get('password') or None  # convert '' to None\n\n        try:\n            wallet = self.daemon.load_wallet(wallet_file, password, upgrade=True)\n            return wallet\n        except WalletRequiresSplit as e:\n            wizard.run_split(wallet_file, e._split_data)\n            return\n        except WalletUnfinished as e:\n            # wallet creation is not complete, 2fa online phase\n            db = e._wallet_db\n            action = db.get_action()\n            assert action[1] == 'accept_terms_of_use', 'only support for resuming trustedcoin split setup'\n            k1 = load_keystore(db, 'x1')\n            if password is not None:\n                xprv = k1.get_master_private_key(password)\n            else:\n                xprv = db.get('x1')['xprv']\n                if not is_xprv(xprv):\n                    xprv = k1\n            _wiz_data_updates = {\n                'wallet_name': wallet_file,\n                'xprv1': xprv,\n                'xpub1': db.get('x1')['xpub'],\n                'xpub2': db.get('x2')['xpub'],\n            }\n            data = {**d, **_wiz_data_updates}\n            wizard = QENewWalletWizard(self.config, self.app, self.plugins, self.daemon, path,\n                                       start_viewstate=WizardViewState('trustedcoin_tos', data, {}))\n            result = wizard.exec()\n            if result == QDialog.DialogCode.Rejected:\n                self.logger.info('wizard dialog cancelled by user')\n                return\n            db.put('x3', wizard.get_wizard_data()['x3'])\n            db.write_and_force_consolidation()  # TODO API for db is a bit weird: there should be a close method\n\n        wallet = self.daemon.load_wallet(wallet_file, password, upgrade=True)\n        return wallet\n\n    def close_window(self, window: ElectrumWindow):\n        if window in self.windows:\n            self.windows.remove(window)\n        self.build_tray_menu()\n        run_hook('on_close_window', window)\n        if window.should_stop_wallet_on_close:\n            self.daemon.stop_wallet(window.wallet.storage.path)\n\n    def reload_window(self, window):\n        # bump counter so that we do not close the app\n        self._num_wizards_in_progress += 1\n        wallet = window.wallet\n        window.should_stop_wallet_on_close = False\n        window.close()\n        self._create_window_for_wallet(wallet)\n        self._num_wizards_in_progress -= 1\n\n    def reload_windows(self):\n        for window in list(self.windows):\n            self.reload_window(window)\n\n    def has_accepted_terms_of_use(self) -> bool:\n        if self.config.TERMS_OF_USE_ACCEPTED >= TERMS_OF_USE_LATEST_VERSION\\\n                or constants.net.NET_NAME == \"regtest\":\n            return True\n        return False\n\n    def ask_terms_of_use(self):\n        \"\"\"Ask the user to accept the terms of use.\n        This is only shown if the user has not accepted them yet.\n        \"\"\"\n        if self.has_accepted_terms_of_use():\n            return\n        from electrum.gui.qt.wizard.terms_of_use import QETermsOfUseWizard\n        dialog = QETermsOfUseWizard(self.config, self.app)\n        result = dialog.exec()\n        if result == QDialog.DialogCode.Rejected:\n            self.logger.info('terms of use not accepted by user')\n            raise UserCancelled()\n\n    def init_network(self):\n        \"\"\"Start the network, including showing a first-start network dialog if config does not exist.\"\"\"\n        if self.daemon.network:\n            # first-start network-setup\n            if not self.config.cv.NETWORK_AUTO_CONNECT.is_set():\n                dialog = QEServerConnectWizard(self.config, self.app, self.plugins, self.daemon)\n                result = dialog.exec()\n                if result == QDialog.DialogCode.Rejected:\n                    self.logger.info('network wizard dialog cancelled by user')\n                    raise UserCancelled()\n\n            # start network\n            self.daemon.start_network()\n\n    def main(self):\n        # setup Ctrl-C handling and tear-down code first, so that user can easily exit whenever\n        self.app.setQuitOnLastWindowClosed(False)  # so _we_ can decide whether to quit\n        self.app.lastWindowClosed.connect(self._maybe_quit_if_no_windows_open)\n        self.app.aboutToQuit.connect(self._cleanup_before_exit)\n        signal.signal(signal.SIGINT, lambda *args: self.app.quit())\n        # hook for crash reporter\n        Exception_Hook.maybe_setup(config=self.config)\n        # start network, and maybe show first-start network-setup\n        try:\n            self.ask_terms_of_use()\n            self.init_network()\n        except UserCancelled:\n            return\n        except Exception as e:\n            self.logger.exception('')\n            return\n        # start wizard to select/create wallet\n        path = self.config.get_wallet_path()\n        try:\n            if not self.start_new_window(path, self.config.get('url'), app_is_starting=True):\n                return\n        except Exception as e:\n            self.logger.error(\"error loading wallet (or creating window for it)\")\n            send_exception_to_crash_reporter(e)\n            # Let Qt event loop start properly so that crash reporter window can appear.\n            # We will shutdown when the user closes that window, via lastWindowClosed signal.\n        # main loop\n        self.logger.info(\"starting Qt main loop\")\n        self.app.exec()\n        # on some platforms the exec_ call may not return, so use _cleanup_before_exit\n\n    def stop(self):\n        self.logger.info('closing GUI')\n        self.app.quit_signal.emit()\n\n    @classmethod\n    def version_info(cls):\n        ret = {\n            \"qt.version\": QtCore.QT_VERSION_STR,\n            \"pyqt.version\": QtCore.PYQT_VERSION_STR,\n        }\n        if hasattr(PyQt6, \"__path__\"):\n            ret[\"pyqt.path\"] = \", \".join(PyQt6.__path__ or [])\n        return ret\n\n    def do_copy(self, text: str, *, title: str = None) -> None:\n        self.app.clipboard().setText(text)\n        message = _(\"Text copied to Clipboard\") if title is None else _(\"{} copied to Clipboard\").format(title)\n        # tooltip cannot be displayed immediately when called from a menu; wait 200ms\n        QTimer.singleShot(200, lambda: QToolTip.showText(QCursor.pos(), message, None))\n\n\ndef standalone_exception_dialog(exception: Union[str, BaseException]) -> None:\n    app = QApplication.instance()\n    if not app:\n        app = QApplication([])\n\n    msg_box = QMessageBox()\n    msg_box.setWindowTitle(_(\"Error starting Electrum\"))\n    msg_box.setIcon(QMessageBox.Icon.Critical)\n    msg_box.setText(_(\"An error occurred\") + \":\")\n    msg_box.setInformativeText(str(exception))\n\n    # Add detailed traceback if available\n    if hasattr(exception, \"__traceback__\"):\n        import traceback\n        detailed_text = ''.join(traceback.format_exception(\n            type(exception), exception, exception.__traceback__)\n        )\n        msg_box.setDetailedText(detailed_text)\n\n    msg_box.exec()\n"
  },
  {
    "path": "electrum/gui/qt/address_dialog.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2012 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtWidgets import QVBoxLayout, QLabel\n\nfrom electrum.i18n import _\n\nfrom .util import WindowModalDialog, ButtonsLineEdit, ShowQRLineEdit, Buttons, CloseButton\nfrom .history_list import HistoryList, HistoryModel\nfrom .qrtextedit import ShowQRTextEdit\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nclass AddressHistoryModel(HistoryModel):\n    def __init__(self, window: 'ElectrumWindow', address):\n        super().__init__(window)\n        self.address = address\n\n    def get_domain(self):\n        return [self.address]\n\n    def should_include_lightning_payments(self) -> bool:\n        return False\n\n\nclass AddressDialog(WindowModalDialog):\n\n    def __init__(self, window: 'ElectrumWindow', address: str, *, parent=None):\n        if parent is None:\n            parent = window\n        WindowModalDialog.__init__(self, parent, _(\"Address\"))\n        self.address = address\n        self.window = window\n        self.config = window.config\n        self.wallet = window.wallet\n        self.app = window.app\n        self.saved = True\n\n        self.setMinimumWidth(700)\n        vbox = QVBoxLayout()\n        self.setLayout(vbox)\n\n        vbox.addWidget(QLabel(_(\"Address\") + \":\"))\n        self.addr_e = ShowQRLineEdit(self.address, self.config, title=_(\"Address\"))\n        vbox.addWidget(self.addr_e)\n\n        try:\n            pubkeys = self.wallet.get_public_keys(address)\n        except BaseException as e:\n            pubkeys = None\n        if pubkeys:\n            vbox.addWidget(QLabel(_(\"Public keys\") + ':'))\n            for pubkey in pubkeys:\n                pubkey_e = ShowQRLineEdit(pubkey, self.config, title=_(\"Public Key\"))\n                vbox.addWidget(pubkey_e)\n\n        redeem_script = self.wallet.get_redeem_script(address)\n        if redeem_script:\n            vbox.addWidget(QLabel(_(\"Redeem Script\") + ':'))\n            redeem_e = ShowQRTextEdit(text=redeem_script, config=self.config)\n            redeem_e.addCopyButton()\n            vbox.addWidget(redeem_e)\n\n        witness_script = self.wallet.get_witness_script(address)\n        if witness_script:\n            vbox.addWidget(QLabel(_(\"Witness Script\") + ':'))\n            witness_e = ShowQRTextEdit(text=witness_script, config=self.config)\n            witness_e.addCopyButton()\n            vbox.addWidget(witness_e)\n\n        address_path_str = self.wallet.get_address_path_str(address)\n        if address_path_str:\n            vbox.addWidget(QLabel(_(\"Derivation path\") + ':'))\n            der_path_e = ButtonsLineEdit(address_path_str)\n            der_path_e.addCopyButton()\n            der_path_e.setReadOnly(True)\n            vbox.addWidget(der_path_e)\n\n        addr_hist_model = AddressHistoryModel(self.window, self.address)\n        self.hw = HistoryList(self.window, addr_hist_model)\n        self.hw.num_tx_label = QLabel('')\n        addr_hist_model.set_view(self.hw)\n        vbox.addWidget(self.hw.num_tx_label)\n        vbox.addWidget(self.hw)\n\n        vbox.addLayout(Buttons(CloseButton(self)))\n        self.format_amount = self.window.format_amount\n        addr_hist_model.refresh('address dialog constructor')\n\n    def show_qr(self):\n        text = self.address\n        try:\n            self.window.show_qrcode(text, 'Address', parent=self)\n        except Exception as e:\n            self.show_message(repr(e))\n"
  },
  {
    "path": "electrum/gui/qt/address_list.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport enum\nfrom enum import IntEnum\nfrom typing import TYPE_CHECKING, Optional\n\nfrom PyQt6.QtCore import Qt, QPersistentModelIndex, QModelIndex\nfrom PyQt6.QtGui import QStandardItemModel, QStandardItem, QFont\nfrom PyQt6.QtWidgets import QAbstractItemView, QComboBox, QMenu\n\nfrom electrum.i18n import _\nfrom electrum.util import block_explorer_URL, profiler\nfrom electrum.plugin import run_hook\nfrom electrum.bitcoin import is_address\nfrom electrum.wallet import InternalAddressCorruption\nfrom electrum.simple_config import SimpleConfig\n\nfrom .util import MONOSPACE_FONT, ColorScheme, webopen\nfrom .my_treeview import MyTreeView, MySortModel\nfrom ..messages import MSG_FREEZE_ADDRESS\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n    from electrum.wallet import AddressIndexGeneric\n\n\nclass AddressUsageStateFilter(IntEnum):\n    ALL = 0\n    UNUSED = 1\n    FUNDED = 2\n    USED_AND_EMPTY = 3\n    FUNDED_OR_UNUSED = 4\n\n    def ui_text(self) -> str:\n        return {\n            self.ALL: _('All status'),\n            self.UNUSED: _('Unused'),\n            self.FUNDED: _('Funded'),\n            self.USED_AND_EMPTY: _('Used'),\n            self.FUNDED_OR_UNUSED: _('Funded or Unused'),\n        }[self]\n\n\nclass AddressTypeFilter(IntEnum):\n    ALL = 0\n    RECEIVING = 1\n    CHANGE = 2\n\n    def ui_text(self) -> str:\n        return {\n            self.ALL: _('All types'),\n            self.RECEIVING: _('Receiving'),\n            self.CHANGE: _('Change'),\n        }[self]\n\n\nclass AddressList(MyTreeView):\n\n    class Columns(MyTreeView.BaseColumnsEnum):\n        TYPE = enum.auto()\n        ADDRESS = enum.auto()\n        LABEL = enum.auto()\n        COIN_BALANCE = enum.auto()\n        FIAT_BALANCE = enum.auto()\n        NUM_TXS = enum.auto()\n\n    filter_columns = [Columns.TYPE, Columns.ADDRESS, Columns.LABEL, Columns.COIN_BALANCE]\n\n    ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000\n    ROLE_ADDRESS_STR = Qt.ItemDataRole.UserRole + 1001\n    key_role = ROLE_ADDRESS_STR\n\n    def __init__(self, main_window: 'ElectrumWindow'):\n        super().__init__(\n            main_window=main_window,\n            stretch_column=self.Columns.LABEL,\n            editable_columns=[self.Columns.LABEL],\n        )\n        self.wallet = self.main_window.wallet\n        self._address_list_status = 0  # type: int\n        self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)\n        self.setSortingEnabled(True)\n        self.show_change = AddressTypeFilter.ALL  # type: AddressTypeFilter\n        self.show_used = AddressUsageStateFilter.ALL  # type: AddressUsageStateFilter\n        self.change_button = QComboBox(self)\n        self.change_button.currentIndexChanged.connect(self.toggle_change)\n        for addr_type in AddressTypeFilter.__members__.values():  # type: AddressTypeFilter\n            self.change_button.addItem(addr_type.ui_text())\n        self.used_button = QComboBox(self)\n        self.used_button.currentIndexChanged.connect(self.toggle_used)\n        for addr_usage_state in AddressUsageStateFilter.__members__.values():  # type: AddressUsageStateFilter\n            self.used_button.addItem(addr_usage_state.ui_text())\n        self.std_model = QStandardItemModel(self)\n        self.proxy = MySortModel(self, sort_role=self.ROLE_SORT_ORDER)\n        self.proxy.setSourceModel(self.std_model)\n        self.setModel(self.proxy)\n        self.update()\n        self.sortByColumn(self.Columns.TYPE, Qt.SortOrder.AscendingOrder)\n        if self.config:\n            self.configvar_show_toolbar = self.config.cv.GUI_QT_ADDRESSES_TAB_SHOW_TOOLBAR\n\n    def on_double_click(self, idx):\n        addr = self.get_role_data_for_current_item(col=0, role=self.ROLE_ADDRESS_STR)\n        self.main_window.show_address(addr)\n\n    def create_toolbar(self, config: 'SimpleConfig'):\n        toolbar, menu = self.create_toolbar_with_menu('')\n        self.num_addr_label = toolbar.itemAt(0).widget()\n        self._toolbar_checkbox = menu.addToggle(_(\"Show Filter\"), lambda: self.toggle_toolbar())\n        menu.addConfig(config.cv.FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES, callback=self.main_window.app.update_fiat_signal.emit)\n        hbox = self.create_toolbar_buttons()\n        toolbar.insertLayout(1, hbox)\n        return toolbar\n\n    def should_show_fiat(self):\n        return self.main_window.fx and self.main_window.fx.is_enabled() and self.config.FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES\n\n    def get_toolbar_buttons(self):\n        return self.change_button, self.used_button\n\n    def on_hide_toolbar(self):\n        self.show_change = AddressTypeFilter.ALL  # type: AddressTypeFilter\n        self.show_used = AddressUsageStateFilter.ALL  # type: AddressUsageStateFilter\n        self.update()\n\n    def refresh_headers(self):\n        if self.should_show_fiat():\n            ccy = self.main_window.fx.get_currency()\n        else:\n            ccy = _('Fiat')\n        headers = {\n            self.Columns.TYPE: _('Type'),\n            self.Columns.ADDRESS: _('Address'),\n            self.Columns.LABEL: _('Label'),\n            self.Columns.COIN_BALANCE: _('Balance'),\n            self.Columns.FIAT_BALANCE: ccy + ' ' + _('Balance'),\n            self.Columns.NUM_TXS: _('Tx'),\n        }\n        self.update_headers(headers)\n\n    def toggle_change(self, state: int):\n        if state == self.show_change:\n            return\n        self.show_change = AddressTypeFilter(state)\n        self.update()\n\n    def toggle_used(self, state: int):\n        if state == self.show_used:\n            return\n        self.show_used = AddressUsageStateFilter(state)\n        self.update()\n\n    @profiler\n    def update(self):\n        if self.maybe_defer_update():\n            return\n        current_address = self.get_role_data_for_current_item(col=0, role=self.ROLE_ADDRESS_STR)\n        if self.show_change == AddressTypeFilter.RECEIVING:\n            addr_list = self.wallet.get_receiving_addresses()\n        elif self.show_change == AddressTypeFilter.CHANGE:\n            addr_list = self.wallet.get_change_addresses()\n        else:\n            addr_list = self.wallet.get_addresses()\n        self.proxy.setDynamicSortFilter(False)  # temp. disable re-sorting after every change\n        self.std_model.clear()\n        self.refresh_headers()\n        set_address = None\n        num_shown = 0\n        new_address_list_status = 0\n        self.addresses_beyond_gap_limit = self.wallet.get_all_known_addresses_beyond_gap_limit()\n        for address in addr_list:\n            c, u, x = self.wallet.get_addr_balance(address)\n            balance = c + u + x\n            is_used_and_empty = self.wallet.adb.is_used(address) and balance == 0\n            if self.show_used == AddressUsageStateFilter.UNUSED and (balance or is_used_and_empty):\n                continue\n            if self.show_used == AddressUsageStateFilter.FUNDED and balance == 0:\n                continue\n            if self.show_used == AddressUsageStateFilter.USED_AND_EMPTY and not is_used_and_empty:\n                continue\n            if self.show_used == AddressUsageStateFilter.FUNDED_OR_UNUSED and is_used_and_empty:\n                continue\n            num_shown += 1\n            new_address_list_status = hash((new_address_list_status, address, c, u, x, is_used_and_empty))\n            labels = [\"\"] * len(self.Columns)\n            labels[self.Columns.ADDRESS] = address\n            address_item = [QStandardItem(e) for e in labels]\n            # align text and set fonts\n            for i, item in enumerate(address_item):\n                item.setTextAlignment(Qt.AlignmentFlag.AlignVCenter)\n                if i not in (self.Columns.TYPE, self.Columns.LABEL):\n                    item.setFont(QFont(MONOSPACE_FONT))\n            self.set_editability(address_item)\n            address_item[self.Columns.FIAT_BALANCE].setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)\n            # setup column 0\n            if self.wallet.is_change(address):\n                address_item[self.Columns.TYPE].setText(_('change'))\n                address_item[self.Columns.TYPE].setBackground(ColorScheme.YELLOW.as_color(True))\n            else:\n                address_item[self.Columns.TYPE].setText(_('receiving'))\n                address_item[self.Columns.TYPE].setBackground(ColorScheme.GREEN.as_color(True))\n            address_item[self.Columns.TYPE].setData(address, self.ROLE_ADDRESS_STR)\n            address_path = self.wallet.get_address_index(address)\n            address_item[self.Columns.TYPE].setData(self.address_index_as_sortable_key(address_path), self.ROLE_SORT_ORDER)\n            address_path_str = self.wallet.get_address_path_str(address)\n            if address_path_str is not None:\n                address_item[self.Columns.TYPE].setToolTip(address_path_str)\n            # add item\n            count = self.std_model.rowCount()\n            self.std_model.insertRow(count, address_item)\n            self.refresh_row(address, count)\n            address_idx = self.std_model.index(count, self.Columns.LABEL)\n            if address == current_address:\n                set_address = QPersistentModelIndex(address_idx)\n        self.set_current_idx(set_address)\n        # show/hide columns\n        if self.should_show_fiat():\n            self.showColumn(self.Columns.FIAT_BALANCE)\n        else:\n            self.hideColumn(self.Columns.FIAT_BALANCE)\n        if self._address_list_status != new_address_list_status:\n            self._address_list_status = new_address_list_status\n            self.close_menu()\n        self.filter()\n        self.proxy.setDynamicSortFilter(True)\n        # update counter\n        self.num_addr_label.setText(_(\"{} addresses\").format(num_shown))\n\n    @staticmethod\n    def address_index_as_sortable_key(address_index: Optional['AddressIndexGeneric']) -> str:\n        if isinstance(address_index, str):  # pubkey hex\n            return address_index\n        elif address_index is None:\n            return \"\"\n        else:\n            return \"\".join(f\"{i:08x}\" for i in address_index)\n\n    def refresh_row(self, key, row):\n        assert row is not None\n        address = key\n        label = self.wallet.get_label_for_address(address)\n        num = self.wallet.adb.get_address_history_len(address)\n        c, u, x = self.wallet.get_addr_balance(address)\n        balance = c + u + x\n        balance_text = self.main_window.format_amount(balance, whitespaces=True)\n        balance_text_nots = self.main_window.format_amount(balance, whitespaces=False, add_thousands_sep=False)\n        # create item\n        fx = self.main_window.fx\n        if self.should_show_fiat():\n            rate = fx.exchange_rate()\n            fiat_balance_str = fx.value_str(balance, rate, add_thousands_sep=True)\n            fiat_balance_str_nots = fx.value_str(balance, rate, add_thousands_sep=False)\n        else:\n            fiat_balance_str = ''\n            fiat_balance_str_nots = ''\n        address_item = [self.std_model.item(row, col) for col in self.Columns]\n        address_item[self.Columns.LABEL].setText(label)\n        address_item[self.Columns.COIN_BALANCE].setText(balance_text)\n        address_item[self.Columns.COIN_BALANCE].setData(balance, self.ROLE_SORT_ORDER)\n        address_item[self.Columns.COIN_BALANCE].setData(balance_text_nots, self.ROLE_CLIPBOARD_DATA)\n        address_item[self.Columns.FIAT_BALANCE].setText(fiat_balance_str)\n        address_item[self.Columns.FIAT_BALANCE].setData(balance, self.ROLE_SORT_ORDER)\n        address_item[self.Columns.FIAT_BALANCE].setData(fiat_balance_str_nots, self.ROLE_CLIPBOARD_DATA)\n        address_item[self.Columns.NUM_TXS].setText(\"%d\"%num)\n        c = ColorScheme.BLUE.as_color(True) if self.wallet.is_frozen_address(address) else self._default_bg_brush\n        address_item[self.Columns.ADDRESS].setBackground(c)\n        if address in self.addresses_beyond_gap_limit:\n            address_item[self.Columns.ADDRESS].setBackground(ColorScheme.RED.as_color(True))\n\n    def create_menu(self, position):\n        from electrum.wallet import Multisig_Wallet\n        is_multisig = isinstance(self.wallet, Multisig_Wallet)\n        can_delete = self.wallet.can_delete_address()\n        selected = self.selected_in_column(self.Columns.ADDRESS)\n        if not selected:\n            return\n        multi_select = len(selected) > 1\n        addrs = [self.item_from_index(item).text() for item in selected]\n        menu = QMenu()\n        menu.setToolTipsVisible(True)\n        if not multi_select:\n            idx = self.indexAt(position)\n            if not idx.isValid():\n                return\n            item = self.item_from_index(idx)\n            if not item:\n                return\n            addr = addrs[0]\n            menu.addAction(_('Details'), lambda: self.main_window.show_address(addr))\n            addr_column_title = self.std_model.horizontalHeaderItem(self.Columns.LABEL).text()\n            addr_idx = idx.sibling(idx.row(), self.Columns.LABEL)\n            self.add_copy_menu(menu, idx)\n            persistent = QPersistentModelIndex(addr_idx)\n            menu.addAction(_(\"Edit {}\").format(addr_column_title), lambda p=persistent: self.edit(QModelIndex(p)))\n            #menu.addAction(_(\"Request payment\"), lambda: self.main_window.receive_at(addr))\n            if self.wallet.can_export():\n                menu.addAction(_(\"Private key\"), lambda: self.main_window.show_private_key(addr))\n            if not is_multisig and not self.wallet.is_watching_only():\n                menu.addAction(_(\"Sign/verify message\"), lambda: self.main_window.sign_verify_message(addr))\n                menu.addAction(_(\"Encrypt/decrypt message\"), lambda: self.main_window.encrypt_message(addr))\n            if can_delete:\n                menu.addAction(_(\"Remove from wallet\"), lambda: self.main_window.remove_address(addr))\n            addr_URL = block_explorer_URL(self.config, 'addr', addr)\n            if addr_URL:\n                menu.addAction(_(\"View on block explorer\"), lambda: webopen(addr_URL))\n\n            if not self.wallet.is_frozen_address(addr):\n                act = menu.addAction(_(\"Freeze\"), lambda: self.main_window.set_frozen_state_of_addresses([addr], True))\n            else:\n                act = menu.addAction(_(\"Unfreeze\"), lambda: self.main_window.set_frozen_state_of_addresses([addr], False))\n            act.setToolTip(MSG_FREEZE_ADDRESS)\n\n        else:\n            # multiple items selected\n            act = menu.addAction(_(\"Freeze\"), lambda: self.main_window.set_frozen_state_of_addresses(addrs, True))\n            act.setToolTip(MSG_FREEZE_ADDRESS)\n            act = menu.addAction(_(\"Unfreeze\"), lambda: self.main_window.set_frozen_state_of_addresses(addrs, False))\n            act.setToolTip(MSG_FREEZE_ADDRESS)\n\n        coins = self.wallet.get_spendable_coins(addrs)\n        if coins:\n            if self.main_window.utxo_list.are_in_coincontrol(coins):\n                menu.addAction(_(\"Remove from coin control\"), lambda: self.main_window.utxo_list.remove_from_coincontrol(coins))\n            else:\n                menu.addAction(_(\"Add to coin control\"), lambda: self.main_window.utxo_list.add_to_coincontrol(coins))\n\n        run_hook('receive_menu', menu, addrs, self.wallet)\n        self.open_menu(menu, position)\n\n    def place_text_on_clipboard(self, text: str, *, title: str = None) -> None:\n        if is_address(text):\n            try:\n                self.wallet.check_address_for_corruption(text)\n            except InternalAddressCorruption as e:\n                self.main_window.show_error(str(e))\n                raise\n        super().place_text_on_clipboard(text, title=title)\n\n    def get_edit_key_from_coordinate(self, row, col):\n        if col != self.Columns.LABEL:\n            return None\n        return self.get_role_data_from_coordinate(row, 0, role=self.ROLE_ADDRESS_STR)\n\n    def on_edited(self, idx, edit_key, *, text):\n        self.wallet.set_label(edit_key, text)\n        self.main_window.history_model.refresh('address label edited')\n        self.main_window.utxo_list.update()\n        self.main_window.update_completions()\n"
  },
  {
    "path": "electrum/gui/qt/amountedit.py",
    "content": "# -*- coding: utf-8 -*-\n\nfrom decimal import Decimal\nfrom typing import Union\n\nfrom PyQt6.QtCore import pyqtSignal, Qt, QSize\nfrom PyQt6.QtGui import QPainter\nfrom PyQt6.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy)\n\nfrom .util import char_width_in_lineedit, ColorScheme\n\nfrom electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_name,\n                           FEERATE_PRECISION, quantize_feerate, DECIMAL_POINT, UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE)\nfrom electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC\n\n_NOT_GIVEN = object()  # sentinel value\n\n\nclass FreezableLineEdit(QLineEdit):\n    frozen = pyqtSignal()\n\n    def setFrozen(self, b):\n        self.setReadOnly(b)\n        self.setStyleSheet(ColorScheme.LIGHTBLUE.as_stylesheet(True) if b else '')\n        self.frozen.emit()\n\n    def isFrozen(self):\n        return self.isReadOnly()\n\n\nclass SizedFreezableLineEdit(FreezableLineEdit):\n\n    def __init__(self, *, width: int, parent=None):\n        super().__init__(parent)\n        self._width = width\n        self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)\n        self.setMaximumWidth(width)\n\n    def sizeHint(self) -> QSize:\n        sh = super().sizeHint()\n        return QSize(self._width, sh.height())\n\n\nclass AmountEdit(SizedFreezableLineEdit):\n    shortcut = pyqtSignal()\n\n    def __init__(self, base_unit, is_int=False, parent=None, *, max_amount=None):\n        # This seems sufficient for hundred-BTC amounts with 8 decimals\n        width = 16 * char_width_in_lineedit()\n        super().__init__(width=width, parent=parent)\n        self.base_unit = base_unit\n        self.textChanged.connect(self.numbify)\n        self.is_int = is_int\n        self.is_shortcut = False\n        self.extra_precision = 0\n        self.max_amount = max_amount\n\n    def decimal_point(self):\n        return 8\n\n    def max_precision(self):\n        return self.decimal_point() + self.extra_precision\n\n    def numbify(self):\n        text = self.text().strip()\n        if text == '!':\n            self.shortcut.emit()\n            return\n        pos = self.cursorPosition()\n        chars = '0123456789'\n        if not self.is_int: chars += DECIMAL_POINT\n        s = ''.join([i for i in text if i in chars])\n        if not self.is_int:\n            if DECIMAL_POINT in s:\n                p = s.find(DECIMAL_POINT)\n                s = s.replace(DECIMAL_POINT, '')\n                s = s[:p] + DECIMAL_POINT + s[p:p+self.max_precision()]\n        if self.max_amount:\n            if (amt := self._get_amount_from_text(s)) and amt >= self.max_amount:\n                s = self._get_text_from_amount(self.max_amount)\n        self.setText(s)\n        # setText sets Modified to False.  Instead we want to remember\n        # if updates were because of user modification.\n        self.setModified(self.hasFocus())\n        self.setCursorPosition(pos)\n\n    def paintEvent(self, event):\n        QLineEdit.paintEvent(self, event)\n        if self.base_unit:\n            panel = QStyleOptionFrame()\n            self.initStyleOption(panel)\n            textRect = self.style().subElementRect(QStyle.SubElement.SE_LineEditContents, panel, self)\n            textRect.adjust(2, 0, -10, 0)\n            painter = QPainter(self)\n            painter.setPen(ColorScheme.GRAY.as_color())\n            painter.drawText(textRect, int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), self.base_unit())\n\n    def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]:\n        try:\n            text = text.replace(DECIMAL_POINT, '.')\n            return (int if self.is_int else Decimal)(text)\n        except Exception:\n            return None\n\n    def get_amount(self) -> Union[None, Decimal, int]:\n        amt = self._get_amount_from_text(str(self.text()))\n        if self.max_amount and amt and amt >= self.max_amount:\n            return self.max_amount\n        return amt\n\n    def _get_text_from_amount(self, amount) -> str:\n        return \"%d\" % amount\n\n    def setAmount(self, amount):\n        text = self._get_text_from_amount(amount)\n        self.setText(text)\n\n\nclass BTCAmountEdit(AmountEdit):\n\n    def __init__(self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN):\n        if max_amount is _NOT_GIVEN:\n            max_amount = TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN\n        AmountEdit.__init__(self, self._base_unit, is_int, parent, max_amount=max_amount)\n        self.decimal_point = decimal_point\n\n    def _base_unit(self):\n        return decimal_point_to_base_unit_name(self.decimal_point())\n\n    def _get_amount_from_text(self, text):\n        # returns amt in satoshis\n        try:\n            text = text.replace(DECIMAL_POINT, '.')\n            x = Decimal(text)\n        except Exception:\n            return None\n        # scale it to max allowed precision, make it an int\n        power = pow(10, self.max_precision())\n        max_prec_amount = int(power * x)\n        # if the max precision is simply what unit conversion allows, just return\n        if self.max_precision() == self.decimal_point():\n            return max_prec_amount\n        # otherwise, scale it back to the expected unit\n        amount = Decimal(max_prec_amount) / pow(10, self.max_precision()-self.decimal_point())\n        return Decimal(amount) if not self.is_int else int(amount)\n\n    def _get_text_from_amount(self, amount_sat):\n        text = format_satoshis_plain(amount_sat, decimal_point=self.decimal_point())\n        text = text.replace('.', DECIMAL_POINT)\n        return text\n\n    def setAmount(self, amount_sat):\n        if amount_sat is None:\n            self.setText(\" \")  # Space forces repaint in case units changed\n        else:\n            text = self._get_text_from_amount(amount_sat)\n            self.setText(text)\n        self.setFrozen(self.isFrozen()) # re-apply styling, as it is nuked by setText (?)\n        self.repaint()  # macOS hack for #6269\n\n\nclass FeerateEdit(BTCAmountEdit):\n\n    def __init__(self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN):\n        super().__init__(decimal_point, is_int, parent, max_amount=max_amount)\n        self.extra_precision = FEERATE_PRECISION\n\n    def _base_unit(self):\n        return UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE\n\n    def _get_amount_from_text(self, text):\n        sat_per_byte_amount = super()._get_amount_from_text(text)\n        return quantize_feerate(sat_per_byte_amount)\n\n    def _get_text_from_amount(self, amount):\n        amount = quantize_feerate(amount)\n        return super()._get_text_from_amount(amount)\n"
  },
  {
    "path": "electrum/gui/qt/balance_dialog.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2013 ecdsa@github\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QWidget, QGridLayout, QToolButton, QPushButton\nfrom PyQt6.QtCore import QRect, Qt\nfrom PyQt6.QtGui import QPen, QPainter, QPixmap\n\nfrom electrum.i18n import _\nfrom electrum.gui.messages import MSG_LN_UTXO_RESERVE\n\nfrom .util import Buttons, CloseButton, WindowModalDialog, ColorScheme, font_height, AmountLabel, icon_path\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n    from electrum.wallet import Abstract_Wallet\n\n\n# Todo:\n#  show lightning funds that are not usable\n#  pie chart mouse interactive, to prepare a swap\n\nCOLOR_CONFIRMED = Qt.GlobalColor.green\nCOLOR_UNCONFIRMED = Qt.GlobalColor.red\nCOLOR_UNMATURED = Qt.GlobalColor.magenta\nCOLOR_FROZEN = ColorScheme.BLUE.as_color(True)\nCOLOR_LIGHTNING = Qt.GlobalColor.yellow\nCOLOR_FROZEN_LIGHTNING = Qt.GlobalColor.cyan\n\n\nclass PieChartObject:\n\n    def paintEvent(self, event):\n        pen = QPen(Qt.GlobalColor.gray, 1, Qt.PenStyle.SolidLine)\n        qp = QPainter()\n        qp.begin(self)\n        qp.setPen(pen)\n        qp.setRenderHint(QPainter.RenderHint.Antialiasing)\n        qp.setBrush(Qt.GlobalColor.gray)\n        total = sum([x[2] for x in self._list])\n        if total == 0:\n            return\n        alpha = 0\n        s = 0\n        for name, color, amount in self._list:\n            qp.setBrush(color)\n            if amount == 0:\n                continue\n            elif amount == total:\n                qp.drawEllipse(self.R)\n            else:\n                delta = int(16 * 360 * amount/total)\n                qp.drawPie(self.R, alpha, delta)\n                alpha += delta\n        qp.end()\n\n\nclass PieChartWidget(QWidget, PieChartObject):\n\n    def __init__(self, size, l):\n        QWidget.__init__(self)\n        self.size = size\n        self.R = QRect(0, 0, self.size, self.size)\n        self.setGeometry(self.R)\n        self.setMinimumWidth(self.size)\n        self.setMaximumWidth(self.size)\n        self.setMinimumHeight(self.size)\n        self.setMaximumHeight(self.size)\n        self._list = l # list[ (name, color, amount)]\n        self.update()\n\n    def update_list(self, l):\n        self._list = l\n        self.update()\n\n\nclass BalanceToolButton(QToolButton, PieChartObject):\n\n    def __init__(self):\n        QToolButton.__init__(self)\n        self._list = []\n        self._update_size()\n        self._warning = False\n\n    @property\n    def has_warning(self) -> bool:\n        return bool(self._warning)\n\n    def update_list(self, l, warning: bool):\n        self._warning = warning\n        self._list = l\n        self.update()\n\n    def setText(self, text):\n        # this is a hack\n        QToolButton.setText(self, '       ' + text)\n\n    def paintEvent(self, event):\n        QToolButton.paintEvent(self, event)\n        if not self._warning:\n            PieChartObject.paintEvent(self, event)\n        else:\n            pixmap = QPixmap(icon_path(\"warning.png\"))\n            qp = QPainter()\n            qp.begin(self)\n            qp.drawPixmap(self.R, pixmap)\n            qp.end()\n\n    def resizeEvent(self, e):\n        super().resizeEvent(e)\n        self._update_size()\n\n    def _update_size(self):\n        size = round(font_height(self) * 1.1)\n        self.R = QRect(6, 3, size, size)\n\n\nclass LegendWidget(QWidget):\n    size = 20\n\n    def __init__(self, color):\n        QWidget.__init__(self)\n        self.color = color\n        self.R = QRect(0, 0, self.size, int(self.size*0.75))\n        self.setGeometry(self.R)\n        self.setMinimumWidth(self.size)\n        self.setMaximumWidth(self.size)\n        self.setMinimumHeight(self.size)\n        self.setMaximumHeight(self.size)\n\n    def paintEvent(self, event):\n        pen = QPen(Qt.GlobalColor.gray, 1, Qt.PenStyle.SolidLine)\n        qp = QPainter()\n        qp.begin(self)\n        qp.setPen(pen)\n        qp.setRenderHint(QPainter.RenderHint.Antialiasing)\n        qp.setBrush(self.color)\n        qp.drawRect(self.R)\n        qp.end()\n\n\nclass BalanceDialog(WindowModalDialog):\n\n    def __init__(self, parent: 'ElectrumWindow', *, wallet: 'Abstract_Wallet'):\n\n        WindowModalDialog.__init__(self, parent, _(\"Wallet Balance\"))\n        self.wallet = wallet\n        self.window = parent\n        self.config = parent.config\n        self.fx = parent.fx\n\n        p_bal = self.wallet.get_balances_for_piechart()\n        confirmed = p_bal.confirmed\n        unconfirmed = p_bal.unconfirmed\n        unmatured = p_bal.unmatured\n        frozen = p_bal.frozen\n        lightning = p_bal.lightning\n        f_lightning = p_bal.lightning_frozen\n\n        frozen_str =  self.config.format_amount_and_units(frozen)\n        confirmed_str =  self.config.format_amount_and_units(confirmed)\n        unconfirmed_str =  self.config.format_amount_and_units(unconfirmed)\n        unmatured_str =  self.config.format_amount_and_units(unmatured)\n        lightning_str =  self.config.format_amount_and_units(lightning)\n        f_lightning_str =  self.config.format_amount_and_units(f_lightning)\n\n        frozen_fiat_str = self.fx.format_amount_and_units(frozen) if self.fx else ''\n        confirmed_fiat_str = self.fx.format_amount_and_units(confirmed) if self.fx else ''\n        unconfirmed_fiat_str = self.fx.format_amount_and_units(unconfirmed) if self.fx else ''\n        unmatured_fiat_str = self.fx.format_amount_and_units(unmatured) if self.fx else ''\n        lightning_fiat_str = self.fx.format_amount_and_units(lightning) if self.fx else ''\n        f_lightning_fiat_str = self.fx.format_amount_and_units(f_lightning) if self.fx else ''\n\n        piechart = PieChartWidget(\n            max(120, 9 * font_height()),\n            [\n                (_('Frozen'), COLOR_FROZEN, frozen),\n                (_('Unmatured'), COLOR_UNMATURED, unmatured),\n                (_('Unconfirmed'), COLOR_UNCONFIRMED, unconfirmed),\n                (_('On-chain'), COLOR_CONFIRMED, confirmed),\n                (_('Lightning'), COLOR_LIGHTNING, lightning),\n                (_('Lightning frozen'), COLOR_FROZEN_LIGHTNING, f_lightning),\n            ]\n        )\n\n        vbox = QVBoxLayout()\n        if self.wallet.is_low_reserve():\n            reserve_str = self.config.format_amount_and_units(self.config.LN_UTXO_RESERVE)\n            hbox = QHBoxLayout()\n            msg = _('Warning') + ': ' + MSG_LN_UTXO_RESERVE.format(reserve_str)\n            label = QLabel(msg)\n            label.setWordWrap(True)\n            logo = QLabel('')\n            logo.setPixmap(\n                QPixmap(icon_path(\"warning.png\")).scaledToWidth(\n                    25, mode=Qt.TransformationMode.SmoothTransformation)\n            )\n            logo.setMaximumWidth(28)\n            hbox.addWidget(logo)\n            hbox.addWidget(label)\n            vbox.addLayout(hbox)\n\n        vbox.addWidget(piechart)\n        grid = QGridLayout()\n        #grid.addWidget(QLabel(_(\"Onchain\") + ':'), 0, 1)\n        #grid.addWidget(QLabel(onchain_str), 0, 2, alignment=Qt.AlignmentFlag.AlignRight)\n        #grid.addWidget(QLabel(onchain_fiat_str), 0, 3, alignment=Qt.AlignmentFlag.AlignRight)\n\n        if frozen:\n            grid.addWidget(LegendWidget(COLOR_FROZEN), 0, 0)\n            grid.addWidget(QLabel(_(\"Frozen\") + ':'), 0, 1)\n            grid.addWidget(AmountLabel(frozen_str), 0, 2, alignment=Qt.AlignmentFlag.AlignRight)\n            grid.addWidget(AmountLabel(frozen_fiat_str), 0, 3, alignment=Qt.AlignmentFlag.AlignRight)\n        if unconfirmed:\n            grid.addWidget(LegendWidget(COLOR_UNCONFIRMED), 2, 0)\n            grid.addWidget(QLabel(_(\"Unconfirmed\") + ':'), 2, 1)\n            grid.addWidget(AmountLabel(unconfirmed_str), 2, 2, alignment=Qt.AlignmentFlag.AlignRight)\n            grid.addWidget(AmountLabel(unconfirmed_fiat_str), 2, 3, alignment=Qt.AlignmentFlag.AlignRight)\n        if unmatured:\n            grid.addWidget(LegendWidget(COLOR_UNMATURED), 3, 0)\n            grid.addWidget(QLabel(_(\"Unmatured\") + ':'), 3, 1)\n            grid.addWidget(AmountLabel(unmatured_str), 3, 2, alignment=Qt.AlignmentFlag.AlignRight)\n            grid.addWidget(AmountLabel(unmatured_fiat_str), 3, 3, alignment=Qt.AlignmentFlag.AlignRight)\n        if confirmed:\n            grid.addWidget(LegendWidget(COLOR_CONFIRMED), 1, 0)\n            grid.addWidget(QLabel(_(\"On-chain\") + ':'), 1, 1)\n            grid.addWidget(AmountLabel(confirmed_str), 1, 2, alignment=Qt.AlignmentFlag.AlignRight)\n            grid.addWidget(AmountLabel(confirmed_fiat_str), 1, 3, alignment=Qt.AlignmentFlag.AlignRight)\n        if lightning:\n            grid.addWidget(LegendWidget(COLOR_LIGHTNING), 4, 0)\n            grid.addWidget(QLabel(_(\"Lightning\") + ':'), 4, 1)\n            grid.addWidget(AmountLabel(lightning_str), 4, 2, alignment=Qt.AlignmentFlag.AlignRight)\n            grid.addWidget(AmountLabel(lightning_fiat_str), 4, 3, alignment=Qt.AlignmentFlag.AlignRight)\n        if f_lightning:\n            grid.addWidget(LegendWidget(COLOR_FROZEN_LIGHTNING), 5, 0)\n            grid.addWidget(QLabel(_(\"Lightning (frozen)\") + ':'), 5, 1)\n            grid.addWidget(AmountLabel(f_lightning_str), 5, 2, alignment=Qt.AlignmentFlag.AlignRight)\n            grid.addWidget(AmountLabel(f_lightning_fiat_str), 5, 3, alignment=Qt.AlignmentFlag.AlignRight)\n\n        vbox.addLayout(grid)\n        vbox.addStretch(1)\n        buttons = [CloseButton(self)]\n        if self.window.wallet.has_lightning():\n            swap_button = QPushButton(_('Swap'))\n            swap_button.clicked.connect(lambda: self.window.run_swap_dialog())\n            buttons.insert(0, swap_button)\n\n        vbox.addLayout(Buttons(*buttons))\n        self.setLayout(vbox)\n\n    def run(self):\n        self.exec()\n"
  },
  {
    "path": "electrum/gui/qt/bip39_recovery_dialog.py",
    "content": "# Copyright (C) 2020 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nimport asyncio\nimport concurrent.futures\n\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QListWidget, QListWidgetItem\n\nfrom electrum.i18n import _\nfrom electrum.network import Network\nfrom electrum.bip39_recovery import account_discovery\nfrom electrum.logging import get_logger\nfrom electrum.util import get_asyncio_loop, UserFacingException\n\nfrom electrum.gui.common_qt.util import TaskThread\n\nfrom .util import WindowModalDialog, Buttons, CancelButton, OkButton\n\n_logger = get_logger(__name__)\n\n\nclass Bip39RecoveryDialog(WindowModalDialog):\n\n    ROLE_ACCOUNT = Qt.ItemDataRole.UserRole\n\n    def __init__(self, parent: QWidget, get_account_xpub, on_account_select):\n        self.get_account_xpub = get_account_xpub\n        self.on_account_select = on_account_select\n        WindowModalDialog.__init__(self, parent, _('BIP39 Recovery'))\n        self.setMinimumWidth(400)\n        vbox = QVBoxLayout(self)\n        self.content = QVBoxLayout()\n        self.content.addWidget(QLabel(_('Scanning common paths for existing accounts...')))\n        vbox.addLayout(self.content)\n\n        self.thread = TaskThread(self)\n        self.thread.finished.connect(self.deleteLater) # see #3956\n        network = Network.get_instance()\n        coro = account_discovery(network, self.get_account_xpub)\n        fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop())\n        self.thread.add(\n            fut.result,\n            on_success=self.on_recovery_success,\n            on_error=self.on_recovery_error,\n            cancel=fut.cancel,\n        )\n\n        self.ok_button = OkButton(self)\n        self.ok_button.clicked.connect(self.on_ok_button_click)\n        self.ok_button.setEnabled(False)\n        cancel_button = CancelButton(self)\n        cancel_button.clicked.connect(fut.cancel)\n        vbox.addLayout(Buttons(cancel_button, self.ok_button))\n        self.finished.connect(self.on_finished)\n        self.show()\n\n    def on_finished(self):\n        self.thread.stop()\n\n    def on_ok_button_click(self):\n        item = self.list.currentItem()\n        account = item.data(self.ROLE_ACCOUNT)\n        self.on_account_select(account)\n\n    def on_recovery_success(self, accounts):\n        self.clear_content()\n        if len(accounts) == 0:\n            self.content.addWidget(QLabel(_('No existing accounts found.')))\n            return\n        self.content.addWidget(QLabel(_('Choose an account to restore.')))\n        self.list = QListWidget()\n        for account in accounts:\n            item = QListWidgetItem(account['description'])\n            item.setData(self.ROLE_ACCOUNT, account)\n            self.list.addItem(item)\n        self.list.clicked.connect(lambda: self.ok_button.setEnabled(True))\n        self.content.addWidget(self.list)\n\n    def on_recovery_error(self, exc_info):\n        e = exc_info[1]\n        if isinstance(e, concurrent.futures.CancelledError):\n            return\n        self.clear_content()\n        msg = _('Error: Account discovery failed.')\n        if isinstance(e, UserFacingException):\n            msg += f\"\\n{e}\"\n        else:\n            _logger.error(f\"recovery error\", exc_info=exc_info)\n        self.content.addWidget(QLabel(msg))\n\n    def clear_content(self):\n        for i in reversed(range(self.content.count())):\n            self.content.itemAt(i).widget().setParent(None)\n"
  },
  {
    "path": "electrum/gui/qt/channel_details.py",
    "content": "from typing import TYPE_CHECKING, Sequence\n\nimport PyQt6.QtGui as QtGui\nimport PyQt6.QtWidgets as QtWidgets\nimport PyQt6.QtCore as QtCore\nfrom PyQt6.QtWidgets import QLabel, QLineEdit, QHBoxLayout, QGridLayout\n\nfrom electrum.util import EventListener, ShortID\nfrom electrum.i18n import _\nfrom electrum.util import format_time\nfrom electrum.lnutil import format_short_channel_id, LOCAL, REMOTE, UpdateAddHtlc, Direction\nfrom electrum.lnchannel import htlcsum, Channel, AbstractChannel, HTLCWithStatus\nfrom electrum.lnaddr import LnAddr, lndecode\nfrom electrum.bitcoin import COIN\nfrom electrum.wallet import Abstract_Wallet\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\nfrom .util import Buttons, CloseButton, ShowQRLineEdit, MessageBoxMixin, WWLabel, VLine\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nclass HTLCItem(QtGui.QStandardItem):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.setEditable(False)\n\n\nclass SelectableLabel(QtWidgets.QLabel):\n    def __init__(self, text=''):\n        super().__init__(text)\n        self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextSelectableByMouse)\n\n\nclass LinkedLabel(QtWidgets.QLabel):\n    def __init__(self, text, on_clicked):\n        super().__init__(text)\n        self.linkActivated.connect(on_clicked)\n\n\nclass ChannelDetailsDialog(QtWidgets.QDialog, MessageBoxMixin, QtEventListener):\n\n    def __init__(self, window: 'ElectrumWindow', chan: AbstractChannel):\n        super().__init__(window)\n        # initialize instance fields\n        self.window = window\n        self.wallet = window.wallet\n        self.chan = chan\n        self.format_msat = lambda msat: window.format_amount_and_units(msat / 1000)\n        self.format_sat = lambda sat: window.format_amount_and_units(sat)\n        # register callbacks for updating\n        self.register_callbacks()\n        title = _('Lightning Channel') if not self.chan.is_backup() else _('Channel Backup')\n        self.setWindowTitle(title)\n        self.setMinimumSize(800, 400)\n        # activity labels. not used for backups.\n        self.local_balance_label = SelectableLabel()\n        self.remote_balance_label = SelectableLabel()\n        self.can_send_label = SelectableLabel()\n        self.can_receive_label = SelectableLabel()\n        # add widgets\n        vbox = QtWidgets.QVBoxLayout(self)\n        if self.chan.is_backup():\n            vbox.addWidget(QLabel('\\n'.join([\n                _(\"This is a channel backup.\"),\n                _(\"It shows a channel that was opened with another instance of this wallet\"),\n                _(\"A backup does not contain information about your local balance in the channel.\"),\n                _(\"You can use it to request a force close.\")\n            ])))\n\n        form = self.get_common_form(chan)\n        vbox.addLayout(form)\n        if not self.chan.is_closed() and not self.chan.is_backup():\n            hbox_stats = self.get_hbox_stats(chan)\n            form.addRow(QLabel(_('Channel stats')+ ':'), hbox_stats)\n\n        if not self.chan.is_backup():\n            # add htlc tree view to vbox (wouldn't scale correctly in QFormLayout)\n            vbox.addWidget(QLabel(_('Payments (HTLCs):')))\n            w = self.create_htlc_list(chan)\n            vbox.addWidget(w)\n\n        vbox.addLayout(Buttons(CloseButton(self)))\n        # initialize sent/received fields\n        self.update()\n\n    def make_htlc_item(self, i: UpdateAddHtlc, direction: Direction) -> HTLCItem:\n        it = HTLCItem(_('Sent HTLC with ID {}' if Direction.SENT == direction else 'Received HTLC with ID {}').format(i.htlc_id))\n        it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format_msat(i.amount_msat))])\n        it.appendRow([HTLCItem(_('CLTV expiry')), HTLCItem(str(i.cltv_abs))])\n        it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(i.payment_hash.hex())])\n        return it\n\n    def make_model(self, htlcs: Sequence[HTLCWithStatus]) -> QtGui.QStandardItemModel:\n        model = QtGui.QStandardItemModel(0, 2)\n        model.setHorizontalHeaderLabels(['HTLC', 'Property value'])\n        parentItem = model.invisibleRootItem()\n        folder_types = {\n            'settled': _('Fulfilled HTLCs'),\n            'inflight': _('HTLCs in current commitment transaction'),\n            'failed': _('Failed HTLCs'),\n        }\n        self.folders = {}\n        self.keyname_rows = {}\n\n        for keyname, i in folder_types.items():\n            myFont=QtGui.QFont()\n            myFont.setBold(True)\n            folder = HTLCItem(i)\n            folder.setFont(myFont)\n            parentItem.appendRow(folder)\n            self.folders[keyname] = folder\n            mapping = {}\n            num = 0\n            for htlc_with_status in htlcs:\n                if htlc_with_status.status != keyname:\n                    continue\n                htlc = htlc_with_status.htlc\n                it = self.make_htlc_item(htlc, htlc_with_status.direction)\n                self.folders[keyname].appendRow(it)\n                mapping[htlc.payment_hash] = num\n                num += 1\n            self.keyname_rows[keyname] = mapping\n        return model\n\n    def move(self, fro: str, to: str, payment_hash: bytes):\n        assert fro != to\n        row_idx = self.keyname_rows[fro].pop(payment_hash)\n        row = self.folders[fro].takeRow(row_idx)\n        self.folders[to].appendRow(row)\n        dest_mapping = self.keyname_rows[to]\n        dest_mapping[payment_hash] = len(dest_mapping)\n\n    @qt_event_listener\n    def on_event_channel(self, wallet, chan):\n        if chan == self.chan:\n            self.update()\n\n    @qt_event_listener\n    def on_event_htlc_added(self, chan, htlc, direction):\n        if chan != self.chan:\n            return\n        mapping = self.keyname_rows['inflight']\n        mapping[htlc.payment_hash] = len(mapping)\n        self.folders['inflight'].appendRow(self.make_htlc_item(htlc, direction))\n\n    @qt_event_listener\n    def on_event_htlc_fulfilled(self, payment_hash, chan, htlc_id):\n        if chan.channel_id != self.chan.channel_id:\n            return\n        self.move('inflight', 'settled', payment_hash)\n        self.update()\n\n    @qt_event_listener\n    def on_event_htlc_failed(self, payment_hash, chan, htlc_id):\n        if chan.channel_id != self.chan.channel_id:\n            return\n        self.move('inflight', 'failed', payment_hash)\n        self.update()\n\n    def update(self):\n        if self.chan.is_closed() or self.chan.is_backup():\n            return\n        assert isinstance(self.chan, Channel), type(self.chan)\n        self.can_send_label.setText(self.format_msat(self.chan.available_to_spend(LOCAL)))\n        self.can_receive_label.setText(self.format_msat(self.chan.available_to_spend(REMOTE)))\n        self.sent_label.setText(self.format_msat(self.chan.total_msat(Direction.SENT)))\n        self.received_label.setText(self.format_msat(self.chan.total_msat(Direction.RECEIVED)))\n        self.local_balance_label.setText(self.format_msat(self.chan.balance(LOCAL)))\n        self.remote_balance_label.setText(self.format_msat(self.chan.balance(REMOTE)))\n        self.current_feerate.setText(self.window.format_fee_rate(4 * self.chan.get_latest_feerate(LOCAL)))\n\n    @QtCore.pyqtSlot(str)\n    def show_tx(self, link_text: str):\n        tx = self.wallet.adb.get_transaction(link_text)\n        if not tx:\n            self.show_error(_(\"Transaction not found.\"))\n            return\n        self.window.show_transaction(tx)\n\n    def get_common_form(self, chan: AbstractChannel):\n        form = QtWidgets.QFormLayout(None)\n        remote_id_e = ShowQRLineEdit(chan.node_id.hex(), self.window.config, title=_(\"Remote Node ID\"))\n        form.addRow(QLabel(_('Remote Node') + ':'), remote_id_e)\n        channel_id_e = ShowQRLineEdit(chan.channel_id.hex(), self.window.config, title=_(\"Channel ID\"))\n        form.addRow(QLabel(_('Channel ID') + ':'), channel_id_e)\n        form.addRow(QLabel(_('Short Channel ID') + ':'), SelectableLabel(str(chan.short_channel_id)))\n        if local_scid_alias := chan.get_local_scid_alias():\n            form.addRow(QLabel('Local SCID Alias:'), SelectableLabel(str(ShortID(local_scid_alias))))\n        if remote_scid_alias := chan.get_remote_scid_alias():\n            form.addRow(QLabel('Remote SCID Alias:'), SelectableLabel(str(ShortID(remote_scid_alias))))\n        form.addRow(QLabel(_('State') + ':'), SelectableLabel(chan.get_state_for_GUI()))\n        if remote_peer_sent_error := chan.get_remote_peer_sent_error():\n            err_label = WWLabel(remote_peer_sent_error)  # note: text is already truncated to reasonable len\n            err_label.setTextFormat(QtCore.Qt.TextFormat.PlainText)\n            form.addRow(WWLabel(_('Remote peer sent error [DO NOT TRUST]') + ':'), err_label)\n        self.capacity = self.format_sat(chan.get_capacity())\n        form.addRow(QLabel(_('Capacity') + ':'), SelectableLabel(self.capacity))\n        if not chan.is_backup():\n            form.addRow(QLabel(_('Channel type:')), SelectableLabel(chan.storage['channel_type'].name_minimal))\n            initiator = 'Local' if chan.constraints.is_initiator else 'Remote'\n            form.addRow(QLabel(_('Initiator:')), SelectableLabel(initiator))\n        else:\n            form.addRow(QLabel(\"Backup Type\"), QLabel(\"imported\" if self.chan.is_imported else \"on-chain\"))\n        funding_txid = chan.funding_outpoint.txid\n        funding_label_text = f'<a href={funding_txid}>{funding_txid}</a>:{chan.funding_outpoint.output_index}'\n        form.addRow(QLabel(_('Funding Outpoint') + ':'), LinkedLabel(funding_label_text, self.show_tx))\n        if chan.is_closed():\n            item = chan.get_closing_height()\n            if item:\n                closing_txid, closing_height, timestamp = item\n                closing_label_text = f'<a href={closing_txid}>{closing_txid}</a>'\n                form.addRow(QLabel(_('Closing Transaction') + ':'), LinkedLabel(closing_label_text, self.show_tx))\n        return form\n\n    def get_hbox_stats(self, chan: Channel):\n        hbox_stats = QHBoxLayout()\n        form_layout_left = QtWidgets.QFormLayout(None)\n        form_layout_right = QtWidgets.QFormLayout(None)\n        form_layout_left.addRow(_('Local balance') + ':', self.local_balance_label)\n        form_layout_right.addRow(_('Remote balance') + ':', self.remote_balance_label)\n        form_layout_left.addRow(_('Can send') + ':', self.can_send_label)\n        form_layout_right.addRow(_('Can receive') + ':', self.can_receive_label)\n        local_reserve_label = SelectableLabel(\"{}\".format(\n            self.format_sat(chan.config[LOCAL].reserve_sat),\n        ))\n        remote_reserve_label = SelectableLabel(\"{}\".format(\n            self.format_sat(chan.config[REMOTE].reserve_sat),\n        ))\n        form_layout_left.addRow(_('Local reserve') + ':', local_reserve_label)\n        form_layout_right.addRow(_('Remote reserve' + ':'), remote_reserve_label)\n        #self.htlc_minimum_msat = SelectableLabel(str(chan.config[REMOTE].htlc_minimum_msat))\n        #form_layout_left.addRow(_('Minimum HTLC value accepted by peer (mSAT):'), self.htlc_minimum_msat)\n        #self.max_htlcs = SelectableLabel(str(chan.config[REMOTE].max_accepted_htlcs))\n        #form_layout_left.addRow(_('Maximum number of concurrent HTLCs accepted by peer:'), self.max_htlcs)\n        #self.max_htlc_value = SelectableLabel(self.format_sat(chan.config[REMOTE].max_htlc_value_in_flight_msat / 1000))\n        #form_layout_left.addRow(_('Maximum value of in-flight HTLCs accepted by peer:'), self.max_htlc_value)\n        local_dust_limit_label = SelectableLabel(\"{}\".format(\n            self.format_sat(chan.config[LOCAL].dust_limit_sat),\n        ))\n        remote_dust_limit_label = SelectableLabel(\"{}\".format(\n            self.format_sat(chan.config[REMOTE].dust_limit_sat),\n        ))\n        form_layout_left.addRow(_('Local dust limit') + ':', local_dust_limit_label)\n        form_layout_right.addRow(_('Remote dust limit') + ':', remote_dust_limit_label)\n        self.received_label = SelectableLabel()\n        self.sent_label = SelectableLabel()\n        form_layout_left.addRow(_('Total sent') + ':', self.sent_label)\n        form_layout_right.addRow(_('Total received') + ':', self.received_label)\n        # to-self-delay\n        csv_local_header = SelectableLabel(_(\"Remote force-close CSV delay\") + \":\")\n        csv_local_header.setToolTip(_(\"Force-close CSV delay imposed on them\"))\n        csv_remote_header = SelectableLabel(_(\"Local force-close CSV delay\") + \":\")\n        csv_remote_header.setToolTip(_(\"Force-close CSV delay imposed on us\"))\n        csv_local_label  = SelectableLabel(_(\"{} blocks\").format(chan.config[LOCAL].to_self_delay))\n        csv_remote_label = SelectableLabel(_(\"{} blocks\").format(chan.config[REMOTE].to_self_delay))\n        form_layout_left.addRow(csv_local_header, csv_local_label)\n        form_layout_right.addRow(csv_remote_header, csv_remote_label)\n        # onchain feerate\n        self.current_feerate = SelectableLabel()\n        form_layout_left.addRow(_('Current feerate') + ':', self.current_feerate)\n        # channel stats left column\n        hbox_stats.addLayout(form_layout_left, 50)\n        # vertical line separator\n        hbox_stats.addWidget(VLine())\n        # channel stats right column\n        hbox_stats.addLayout(form_layout_right, 50)\n        return hbox_stats\n\n    def create_htlc_list(self, chan):\n        w = QtWidgets.QTreeView(self)\n        htlc_dict = chan.get_payments()\n        htlc_list = []\n        for rhash, plist in htlc_dict.items():\n            for htlc_with_status in plist:\n                htlc_list.append(htlc_with_status)\n        w.setModel(self.make_model(htlc_list))\n        w.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents)\n        return w\n\n    def closeEvent(self, event):\n        self.unregister_callbacks()\n        event.accept()\n"
  },
  {
    "path": "electrum/gui/qt/channels_list.py",
    "content": "# -*- coding: utf-8 -*-\nimport traceback\nimport enum\nfrom typing import Sequence, Optional, Dict, TYPE_CHECKING\nfrom abc import abstractmethod, ABC\n\nfrom PyQt6 import QtCore, QtGui\nfrom PyQt6.QtCore import Qt, QRect, QSize\nfrom PyQt6.QtWidgets import QMenu, QLabel, QVBoxLayout, QGridLayout, QAbstractItemView, QCheckBox, QToolTip\nfrom PyQt6.QtGui import QFont, QStandardItem, QBrush, QPainter, QIcon, QHelpEvent\n\nfrom electrum.i18n import _\nfrom electrum.lnchannel import AbstractChannel, ChannelBackup, Channel, ChanCloseOption\nfrom electrum.wallet import Abstract_Wallet\nfrom electrum.lnutil import LOCAL, REMOTE\nfrom electrum.lnworker import LNWallet\nfrom electrum.gui import messages\n\nfrom .util import WindowModalDialog, Buttons, OkButton, EnterButton, WaitingDialog, MONOSPACE_FONT, ColorScheme\nfrom .util import read_QIcon, font_height\nfrom .my_treeview import MyTreeView\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nROLE_CHANNEL_ID = Qt.ItemDataRole.UserRole\n\n\nclass ChannelsList(MyTreeView):\n    update_rows = QtCore.pyqtSignal(Abstract_Wallet)\n    update_single_row = QtCore.pyqtSignal(Abstract_Wallet, AbstractChannel)\n    gossip_db_loaded = QtCore.pyqtSignal()\n\n    class Columns(MyTreeView.BaseColumnsEnum):\n        FEATURES = enum.auto()\n        SHORT_CHANID = enum.auto()\n        NODE_ALIAS = enum.auto()\n        CAPACITY = enum.auto()\n        LOCAL_BALANCE = enum.auto()\n        REMOTE_BALANCE = enum.auto()\n        CHANNEL_STATUS = enum.auto()\n        LONG_CHANID = enum.auto()\n\n    headers = {\n        Columns.SHORT_CHANID: _('Short Channel ID'),\n        Columns.LONG_CHANID: _('Channel ID'),\n        Columns.NODE_ALIAS: _('Node alias'),\n        Columns.FEATURES: \"\",\n        Columns.CAPACITY: _('Capacity'),\n        Columns.LOCAL_BALANCE: _('Can send'),\n        Columns.REMOTE_BALANCE: _('Can receive'),\n        Columns.CHANNEL_STATUS: _('Status'),\n    }\n\n    filter_columns = [\n        Columns.SHORT_CHANID,\n        Columns.LONG_CHANID,\n        Columns.NODE_ALIAS,\n        Columns.CHANNEL_STATUS,\n    ]\n\n    _default_item_bg_brush = None  # type: Optional[QBrush]\n\n    def __init__(self, main_window: 'ElectrumWindow'):\n        super().__init__(\n            main_window=main_window,\n            stretch_column=self.Columns.NODE_ALIAS,\n        )\n        self.setModel(QtGui.QStandardItemModel(self))\n        self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)\n        self.gossip_db_loaded.connect(self.on_gossip_db)\n        self.update_rows.connect(self.do_update_rows)\n        self.update_single_row.connect(self.do_update_single_row)\n        self.network = self.main_window.network\n        self.wallet = self.main_window.wallet\n        self.setSortingEnabled(True)\n\n    @property\n    # property because lnworker might be initialized at runtime\n    def lnworker(self):\n        return self.wallet.lnworker\n\n    def format_fields(self, chan: AbstractChannel) -> Dict['ChannelsList.Columns', str]:\n        labels = {}\n        for subject in (REMOTE, LOCAL):\n            if isinstance(chan, Channel):\n                can_send = chan.available_to_spend(subject) / 1000\n                label = self.main_window.format_amount(can_send, whitespaces=True)\n                other = subject.inverted()\n                bal_other = chan.balance(other)//1000\n                bal_minus_htlcs_other = chan.balance_minus_outgoing_htlcs(other)//1000\n                if bal_other != bal_minus_htlcs_other:\n                    label += ' (+' + self.main_window.format_amount(bal_other - bal_minus_htlcs_other, whitespaces=False) + ')'\n            else:\n                assert isinstance(chan, ChannelBackup)\n                label = ''\n            labels[subject] = label\n        status = chan.get_state_for_GUI()\n        closed = chan.is_closed()\n        node_alias = self.lnworker.lnpeermgr.get_node_alias(chan.node_id) or chan.node_id.hex()\n        capacity_str = self.main_window.format_amount(chan.get_capacity(), whitespaces=True)\n        return {\n            self.Columns.SHORT_CHANID: chan.short_id_for_GUI(),\n            self.Columns.LONG_CHANID: chan.channel_id.hex(),\n            self.Columns.NODE_ALIAS: node_alias,\n            self.Columns.FEATURES: '',\n            self.Columns.CAPACITY: capacity_str,\n            self.Columns.LOCAL_BALANCE: '' if closed else labels[LOCAL],\n            self.Columns.REMOTE_BALANCE: '' if closed else labels[REMOTE],\n            self.Columns.CHANNEL_STATUS: status,\n        }\n\n    def on_channel_closed(self, txid):\n        self.main_window.show_message('Channel closed' + '\\n' + txid)\n\n    def on_failure(self, exc_info):\n        type_, e, tb = exc_info\n        traceback.print_tb(tb)\n        self.main_window.show_error('Failed to close channel:\\n{}'.format(repr(e)))\n\n    def close_channel(self, channel_id):\n        self.is_force_close = False\n        msg = _('Cooperative close?')\n        msg += '\\n\\n' + messages.MSG_COOPERATIVE_CLOSE\n        if not self.main_window.question(msg):\n            return\n        coro = self.lnworker.close_channel(channel_id)\n        on_success = self.on_channel_closed\n\n        def task():\n            return self.network.run_from_another_thread(coro)\n\n        WaitingDialog(self, _('Please wait...'), task, on_success, self.on_failure)\n\n    def force_close(self, channel_id):\n        self.save_backup = True\n        backup_cb = QCheckBox('Create a backup now', checked=True)\n\n        def on_checked(_x):\n            self.save_backup = backup_cb.isChecked()\n\n        backup_cb.stateChanged.connect(on_checked)\n        chan = self.lnworker.channels[channel_id]\n        to_self_delay = chan.config[REMOTE].to_self_delay\n        msg = '<b>' + _('Force-close channel?') + '</b><br/>'\\\n            + '<p>' + _('If you force-close this channel, the funds you have in it will not be available for {} blocks.').format(to_self_delay) + ' '\\\n            + _('After that delay, funds will be swept to an address derived from your wallet seed.') + '</p>'\\\n            + '<u>' + _('Please create a backup of your wallet file!') + '</u> '\\\n            + '<p>' + _('Funds in this channel will not be recoverable from seed until they are swept back into your wallet, and might be lost if you lose your wallet file.') + ' '\\\n            + _('To prevent that, you should save a backup of your wallet on another device.') + '</p>'\n        if not self.main_window.question(msg, title=_('Force-close channel'), rich_text=True, checkbox=backup_cb):\n            return\n        if self.save_backup:\n            if not self.main_window.backup_wallet():\n                return\n\n        def task():\n            coro = self.lnworker.force_close_channel(channel_id)\n            return self.network.run_from_another_thread(coro)\n\n        WaitingDialog(self, _('Please wait...'), task, self.on_channel_closed, self.on_failure)\n\n    def remove_channel(self, channel_id):\n        if self.main_window.question(_('Are you sure you want to delete this channel? This will purge associated transactions from your wallet history.')):\n            self.lnworker.remove_channel(channel_id)\n\n    def remove_channel_backup(self, channel_id):\n        if self.main_window.question(_('Remove channel backup?')):\n            self.lnworker.remove_channel_backup(channel_id)\n\n    def export_channel_backup(self, channel_id):\n        msg = messages.MSG_LN_EXPLAIN_SCB_BACKUPS\n        data = self.lnworker.export_channel_backup(channel_id)\n        self.main_window.show_qrcode(data, 'channel backup', help_text=msg,\n                                     show_copy_text_btn=True)\n\n    def request_force_close(self, channel_id):\n        msg = _('Request force-close from remote peer?')\n        msg += '\\n\\n' + messages.MSG_REQUEST_FORCE_CLOSE\n        if not self.main_window.question(msg):\n            return\n\n        def task():\n            coro = self.lnworker.request_force_close(channel_id)\n            return self.network.run_from_another_thread(coro)\n\n        def on_done(b):\n            self.main_window.show_message(_('Request scheduled'))\n\n        WaitingDialog(self, _('Please wait...'), task, on_done, self.on_failure)\n\n    def set_frozen(self, chan, *, for_sending, value):\n        if not self.lnworker.uses_trampoline() or self.lnworker.is_trampoline_peer(chan.node_id):\n            if for_sending:\n                chan.set_frozen_for_sending(value)\n            else:\n                chan.set_frozen_for_receiving(value)\n        else:\n            msg = messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP\n            self.main_window.show_warning(msg, title=_('Channel is frozen for sending'))\n\n    def get_rebalance_pair(self):\n        selected = self.selected_in_column(self.Columns.NODE_ALIAS)\n        if len(selected) == 2:\n            idx1 = selected[0]\n            idx2 = selected[1]\n            channel_id1 = idx1.sibling(idx1.row(), self.Columns.NODE_ALIAS).data(ROLE_CHANNEL_ID)\n            channel_id2 = idx2.sibling(idx2.row(), self.Columns.NODE_ALIAS).data(ROLE_CHANNEL_ID)\n            chan1 = self.lnworker.get_channel_by_id(channel_id1)\n            chan2 = self.lnworker.get_channel_by_id(channel_id2)\n            if chan1 and chan2 and (not self.lnworker.uses_trampoline() or chan1.node_id != chan2.node_id):\n                return chan1, chan2\n        return None, None\n\n    def on_rebalance(self):\n        chan1, chan2 = self.get_rebalance_pair()\n        if chan1 is None:\n            self.main_window.show_error(\"Select two active channels to rebalance.\")\n            return\n        self.main_window.rebalance_dialog(chan1, chan2)\n\n    def on_double_click(self, idx):\n        channel_id = idx.sibling(idx.row(), self.Columns.NODE_ALIAS).data(ROLE_CHANNEL_ID)\n        chan = self.lnworker.get_channel_by_id(channel_id) or self.lnworker.channel_backups[channel_id]\n        self.main_window.show_channel_details(chan)\n\n    def create_menu(self, position):\n        menu = QMenu()\n        menu.setSeparatorsCollapsible(True)  # consecutive separators are merged together\n        selected = self.selected_in_column(self.Columns.NODE_ALIAS)\n        if not selected:\n            menu.exec(self.viewport().mapToGlobal(position))\n            return\n        if len(selected) == 2:\n            chan1, chan2 = self.get_rebalance_pair()\n            if chan1 and chan2:\n                menu.addAction(_(\"Rebalance channels\"), lambda: self.main_window.rebalance_dialog(chan1, chan2))\n                menu.exec(self.viewport().mapToGlobal(position))\n            return\n        elif len(selected) > 2:\n            return\n        idx = self.indexAt(position)\n        if not idx.isValid():\n            return\n        item = self.model().itemFromIndex(idx)\n        if not item:\n            return\n        channel_id = idx.sibling(idx.row(), self.Columns.NODE_ALIAS).data(ROLE_CHANNEL_ID)\n        chan = self.lnworker.get_channel_by_id(channel_id) or self.lnworker.channel_backups[channel_id]\n        menu.addAction(_(\"Details\"), lambda: self.main_window.show_channel_details(chan))\n        menu.addSeparator()\n        cc = self.add_copy_menu(menu, idx)\n        cc.addAction(_(\"Node ID\"), lambda: self.place_text_on_clipboard(\n            chan.node_id.hex(), title=_(\"Node ID\")))\n        cc.addAction(_(\"Long Channel ID\"), lambda: self.place_text_on_clipboard(\n            channel_id.hex(), title=_(\"Long Channel ID\")))\n        if not chan.is_backup() and not chan.is_closed():\n            fm = menu.addMenu(_(\"Freeze\"))\n            if not chan.is_frozen_for_sending():\n                fm.addAction(_(\"Freeze for sending\"), lambda: self.set_frozen(chan, for_sending=True, value=True))\n            else:\n                fm.addAction(_(\"Unfreeze for sending\"), lambda: self.set_frozen(chan, for_sending=True, value=False))\n            if not chan.is_frozen_for_receiving():\n                fm.addAction(_(\"Freeze for receiving\"), lambda: self.set_frozen(chan, for_sending=False, value=True))\n            else:\n                fm.addAction(_(\"Unfreeze for receiving\"), lambda: self.set_frozen(chan, for_sending=False, value=False))\n        if close_opts := chan.get_close_options():\n            cm = menu.addMenu(_(\"Close\"))\n            if ChanCloseOption.COOP_CLOSE in close_opts:\n                cm.addAction(_(\"Cooperative close\"), lambda: self.close_channel(channel_id))\n            if ChanCloseOption.LOCAL_FCLOSE in close_opts:\n                cm.addAction(_(\"Force-close\"), lambda: self.force_close(channel_id))\n            if ChanCloseOption.REQUEST_REMOTE_FCLOSE in close_opts:\n                cm.addAction(_(\"Request force-close\"), lambda: self.request_force_close(channel_id))\n        if not chan.is_backup():\n            menu.addAction(_(\"Export backup\"), lambda: self.export_channel_backup(channel_id))\n        if chan.can_be_deleted():\n            menu.addSeparator()\n            if chan.is_backup():\n                menu.addAction(_(\"Delete\"), lambda: self.remove_channel_backup(channel_id))\n            else:\n                menu.addAction(_(\"Delete\"), lambda: self.remove_channel(channel_id))\n        self.open_menu(menu, position)\n\n    @QtCore.pyqtSlot(Abstract_Wallet, AbstractChannel)\n    def do_update_single_row(self, wallet: Abstract_Wallet, chan: AbstractChannel):\n        if wallet != self.wallet:\n            return\n        for row in range(self.model().rowCount()):\n            item = self.model().item(row, self.Columns.NODE_ALIAS)\n            if item.data(ROLE_CHANNEL_ID) != chan.channel_id:\n                continue\n            for column, v in self.format_fields(chan).items():\n                self.model().item(row, column).setData(v, QtCore.Qt.ItemDataRole.DisplayRole)\n            items = [self.model().item(row, column) for column in self.Columns]\n            self._update_chan_frozen_bg(chan=chan, items=items)\n        if wallet.lnworker:\n            self.update_can_send(wallet.lnworker)\n\n    @QtCore.pyqtSlot()\n    def on_gossip_db(self):\n        self.do_update_rows(self.wallet)\n\n    @QtCore.pyqtSlot(Abstract_Wallet)\n    def do_update_rows(self, wallet):\n        if wallet != self.wallet:\n            return\n        self.model().clear()\n        self.update_headers(self.headers)\n        self.set_visibility_of_columns()\n        if not wallet.lnworker:\n            return\n        self.update_can_send(wallet.lnworker)\n        channels = wallet.lnworker.get_channel_objects()\n        for chan in channels.values():\n            field_map = self.format_fields(chan)\n            items = [QtGui.QStandardItem(field_map[col]) for col in sorted(field_map)]\n            self.set_editability(items)\n            if self._default_item_bg_brush is None:\n                self._default_item_bg_brush = items[self.Columns.NODE_ALIAS].background()\n            items[self.Columns.NODE_ALIAS].setData(chan.channel_id, ROLE_CHANNEL_ID)\n            items[self.Columns.NODE_ALIAS].setFont(QFont(MONOSPACE_FONT))\n            items[self.Columns.LOCAL_BALANCE].setFont(QFont(MONOSPACE_FONT))\n            items[self.Columns.REMOTE_BALANCE].setFont(QFont(MONOSPACE_FONT))\n            items[self.Columns.FEATURES].setData(ChannelFeatureIcons.from_channel(chan), self.ROLE_CUSTOM_PAINT)\n            items[self.Columns.CAPACITY].setFont(QFont(MONOSPACE_FONT))\n            self._update_chan_frozen_bg(chan=chan, items=items)\n            self.model().insertRow(0, items)\n\n        # FIXME sorting by SHORT_CHANID should treat values as tuple, not as string ( 50x1x1 > 8x1x1 )\n        self.sortByColumn(self.Columns.SHORT_CHANID, Qt.SortOrder.DescendingOrder)\n\n    def _update_chan_frozen_bg(self, *, chan: AbstractChannel, items: Sequence[QStandardItem]):\n        assert self._default_item_bg_brush is not None\n        # frozen for sending\n        item = items[self.Columns.LOCAL_BALANCE]\n        if chan.is_frozen_for_sending():\n            item.setBackground(ColorScheme.BLUE.as_color(True))\n            item.setToolTip(_(\"This channel is frozen for sending. It will not be used for outgoing payments.\"))\n        else:\n            item.setBackground(self._default_item_bg_brush)\n            item.setToolTip(\"\")\n        # frozen for receiving\n        item = items[self.Columns.REMOTE_BALANCE]\n        if chan.is_frozen_for_receiving():\n            item.setBackground(ColorScheme.BLUE.as_color(True))\n            item.setToolTip(_(\"This channel is frozen for receiving. It will not be included in invoices.\"))\n        else:\n            item.setBackground(self._default_item_bg_brush)\n            item.setToolTip(\"\")\n\n    def update_can_send(self, lnworker: LNWallet):\n        msg = _('Can send') + ' ' + self.main_window.format_amount(lnworker.num_sats_can_send())\\\n              + ' ' + self.main_window.base_unit() + '; '\\\n              + _('can receive') + ' ' + self.main_window.format_amount(lnworker.num_sats_can_receive())\\\n              + ' ' + self.main_window.base_unit()\n        self.can_send_label.setText(msg)\n\n    def create_toolbar(self, config):\n        toolbar, menu = self.create_toolbar_with_menu('')\n        self.can_send_label = toolbar.itemAt(0).widget()\n        menu.addAction(_('Rebalance channels'), lambda: self.on_rebalance())\n        menu.addAction(read_QIcon('update.png'), _('Submarine swap'), lambda: self.main_window.run_swap_dialog())\n        menu.addSeparator()\n        menu.addAction(_(\"Import channel backup\"), lambda: self.main_window.do_process_from_text_channel_backup())\n        # only enable menu if has LN. Or we could selectively enable menu items?\n        #     and maybe add item \"main_window.init_lightning_dialog()\" when applicable\n        menu.setEnabled(self.wallet.has_lightning())\n        self.new_channel_button = EnterButton(_('New Channel'), self.main_window.new_channel_dialog)\n        if not self.wallet.can_have_lightning():\n            self.new_channel_button.setEnabled(False)\n            self.new_channel_button.setToolTip(_(\"Lightning is not available for this wallet.\"))\n        else:\n            self.new_channel_button.setToolTip(_(\"Open a channel to send payments over the Lightning network.\"))\n        toolbar.insertWidget(2, self.new_channel_button)\n        return toolbar\n\n    def statistics_dialog(self):\n        channel_db = self.network.channel_db\n        capacity = self.main_window.format_amount(channel_db.capacity()) + ' '+ self.main_window.base_unit()\n        d = WindowModalDialog(self.main_window, _('Lightning Network Statistics'))\n        d.setMinimumWidth(400)\n        vbox = QVBoxLayout(d)\n        h = QGridLayout()\n        h.addWidget(QLabel(_('Nodes') + ':'), 0, 0)\n        h.addWidget(QLabel('{}'.format(channel_db.num_nodes)), 0, 1)\n        h.addWidget(QLabel(_('Channels') + ':'), 1, 0)\n        h.addWidget(QLabel('{}'.format(channel_db.num_channels)), 1, 1)\n        h.addWidget(QLabel(_('Capacity') + ':'), 2, 0)\n        h.addWidget(QLabel(capacity), 2, 1)\n        vbox.addLayout(h)\n        vbox.addLayout(Buttons(OkButton(d)))\n        d.exec()\n\n    def set_visibility_of_columns(self):\n        def set_visible(col: int, b: bool):\n            self.showColumn(col) if b else self.hideColumn(col)\n        set_visible(self.Columns.LONG_CHANID, False)\n\n\nclass ChannelFeature(ABC):\n    def __init__(self):\n        self.rect = QRect()\n\n    @abstractmethod\n    def tooltip(self) -> str:\n        pass\n\n    @abstractmethod\n    def icon(self) -> QIcon:\n        pass\n\n\nclass ChanFeatChannel(ChannelFeature):\n    def tooltip(self) -> str:\n        return _(\"This is a channel\")\n    def icon(self) -> QIcon:\n        return read_QIcon(\"lightning\")\n\n\nclass ChanFeatBackup(ChannelFeature):\n    def tooltip(self) -> str:\n        return _(\"This is a static channel backup\")\n    def icon(self) -> QIcon:\n        return read_QIcon(\"lightning_disconnected\")\n\n\nclass ChanFeatTrampoline(ChannelFeature):\n    def tooltip(self) -> str:\n        return _(\"The channel peer can route Trampoline payments.\")\n    def icon(self) -> QIcon:\n        return read_QIcon(\"kangaroo\")\n\n\nclass ChanFeatNoOnchainBackup(ChannelFeature):\n    def tooltip(self) -> str:\n        return _(\"This channel cannot be recovered from your seed. You must back it up manually.\")\n    def icon(self) -> QIcon:\n        return read_QIcon(\"cloud_no\")\n\n\n\nclass ChannelFeatureIcons:\n\n    def __init__(self, features: Sequence['ChannelFeature']):\n        size = max(16, font_height())\n        self.icon_size = QSize(size, size)\n        self.features = features\n\n    @classmethod\n    def from_channel(cls, chan: AbstractChannel) -> 'ChannelFeatureIcons':\n        feats = []\n        if chan.is_backup():\n            feats.append(ChanFeatBackup())\n            if chan.is_imported:\n                feats.append(ChanFeatNoOnchainBackup())\n        else:\n            feats.append(ChanFeatChannel())\n            if chan.lnworker.is_trampoline_peer(chan.node_id):\n                feats.append(ChanFeatTrampoline())\n            if not chan.has_onchain_backup():\n                feats.append(ChanFeatNoOnchainBackup())\n        return ChannelFeatureIcons(feats)\n\n    def paint(self, painter: QPainter, rect: QRect) -> None:\n        painter.save()\n        cur_x = rect.x()\n        for feat in self.features:\n            icon_rect = QRect(cur_x, rect.y(), self.icon_size.width(), self.icon_size.height())\n            feat.rect = icon_rect\n            if rect.contains(icon_rect):  # stay inside parent\n                painter.drawPixmap(icon_rect, feat.icon().pixmap(self.icon_size))\n            cur_x += self.icon_size.width() + 1\n        painter.restore()\n\n    def sizeHint(self, default_size: QSize) -> QSize:\n        if not self.features:\n            return default_size\n        width = len(self.features) * (self.icon_size.width() + 1)\n        return QSize(width, default_size.height())\n\n    def show_tooltip(self, evt: QHelpEvent) -> bool:\n        assert isinstance(evt, QHelpEvent)\n        for feat in self.features:\n            if feat.rect.contains(evt.pos()):\n                QToolTip.showText(evt.globalPos(), feat.tooltip())\n                break\n        else:\n            QToolTip.hideText()\n            evt.ignore()\n        return True\n"
  },
  {
    "path": "electrum/gui/qt/completion_text_edit.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2018 The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom PyQt6.QtGui import QTextCursor\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtWidgets import QCompleter, QPlainTextEdit, QApplication\n\nfrom .util import ButtonsTextEdit\n\n\nclass CompletionTextEdit(ButtonsTextEdit):\n\n    def __init__(self):\n        ButtonsTextEdit.__init__(self)\n        self.completer = None\n        self.moveCursor(QTextCursor.MoveOperation.End)\n        self.disable_suggestions()\n\n    def set_completer(self, completer):\n        self.completer = completer\n        self.initialize_completer()\n\n    def initialize_completer(self):\n        self.completer.setWidget(self)\n        self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)\n        self.completer.activated.connect(self.insert_completion)\n        self.enable_suggestions()\n\n    def insert_completion(self, completion):\n        if self.completer.widget() != self:\n            return\n        text_cursor = self.textCursor()\n        extra = len(completion) - len(self.completer.completionPrefix())\n        text_cursor.movePosition(QTextCursor.MoveOperation.Left)\n        text_cursor.movePosition(QTextCursor.MoveOperation.EndOfWord)\n        if extra == 0:\n            text_cursor.insertText(\" \")\n        else:\n            text_cursor.insertText(completion[-extra:] + \" \")\n        self.setTextCursor(text_cursor)\n\n    def text_under_cursor(self):\n        tc = self.textCursor()\n        tc.select(QTextCursor.SelectionType.WordUnderCursor)\n        return tc.selectedText()\n\n    def enable_suggestions(self):\n        self.suggestions_enabled = True\n\n    def disable_suggestions(self):\n        self.suggestions_enabled = False\n\n    def keyPressEvent(self, e):\n        if self.isReadOnly():\n            return\n\n        if self.is_special_key(e):\n            e.ignore()\n            return\n\n        QPlainTextEdit.keyPressEvent(self, e)\n        if self.isReadOnly():  # if field became read-only *after* keyPress, exit now\n            return\n\n        ctrlOrShift = ((Qt.KeyboardModifier.ControlModifier in e.modifiers())\n                       or (Qt.KeyboardModifier.ShiftModifier in e.modifiers()))\n        if self.completer is None or (ctrlOrShift and not e.text()):\n            return\n\n        if not self.suggestions_enabled:\n            return\n\n        eow = \"~!@#$%^&*()_+{}|:\\\"<>?,./;'[]\\\\-=\"\n        hasModifier = (e.modifiers() != Qt.KeyboardModifier.NoModifier) and not ctrlOrShift\n        completionPrefix = self.text_under_cursor()\n\n        if hasModifier or not e.text() or len(completionPrefix) < 1 or eow.find(e.text()[-1]) >= 0:\n            self.completer.popup().hide()\n            return\n\n        if completionPrefix != self.completer.completionPrefix():\n            self.completer.setCompletionPrefix(completionPrefix)\n            self.completer.popup().setCurrentIndex(self.completer.completionModel().index(0, 0))\n\n        cr = self.cursorRect()\n        cr.setWidth(self.completer.popup().sizeHintForColumn(0) + self.completer.popup().verticalScrollBar().sizeHint().width())\n        self.completer.complete(cr)\n\n    def is_special_key(self, e):\n        if self.completer and self.completer.popup().isVisible():\n            if e.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return):\n                return True\n        if e.key() == Qt.Key.Key_Tab:\n            return True\n        return False\n\n\nif __name__ == \"__main__\":\n    app = QApplication([])\n    completer = QCompleter([\"alabama\", \"arkansas\", \"avocado\", \"breakfast\", \"sausage\"])\n    te = CompletionTextEdit()\n    te.set_completer(completer)\n    te.show()\n    app.exec()\n"
  },
  {
    "path": "electrum/gui/qt/confirm_tx_dialog.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (2019) The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport asyncio\nfrom decimal import Decimal\nfrom functools import partial\nfrom typing import TYPE_CHECKING, Optional, Union\nfrom concurrent.futures import Future\nfrom enum import Enum, auto\n\nfrom PyQt6.QtCore import Qt, QTimer, pyqtSlot, pyqtSignal\nfrom PyQt6.QtGui import QIcon\nfrom PyQt6.QtWidgets import (QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QPushButton, QToolButton,\n                             QComboBox, QTabWidget, QWidget, QStackedWidget)\n\nfrom electrum.i18n import _\nfrom electrum.util import (UserCancelled, quantize_feerate, profiler, NotEnoughFunds, NoDynamicFeeEstimates,\n                           get_asyncio_loop, wait_for2, UserFacingException)\nfrom electrum.plugin import run_hook\nfrom electrum.transaction import PartialTransaction, PartialTxOutput\nfrom electrum.wallet import InternalAddressCorruption\nfrom electrum.bitcoin import DummyAddress\nfrom electrum.fee_policy import FeePolicy, FixedFeePolicy, FeeMethod\nfrom electrum.logging import Logger\nfrom electrum.submarine_swaps import NostrTransport, HttpTransport, SwapServerTransport, SwapServerError\nfrom electrum.gui.messages import MSG_SUBMARINE_PAYMENT_HELP_TEXT\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\n\nfrom .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton, WWLabel,\n                   read_QIcon, IconLabel, HelpButton, RunCoroutineDialog)\nfrom .transaction_dialog import TxSizeLabel, TxFiatLabel, TxInOutWidget\nfrom .fee_slider import FeeSlider, FeeComboBox\nfrom .amountedit import FeerateEdit, BTCAmountEdit\nfrom .locktimeedit import LockTimeEdit\nfrom .my_treeview import QMenuWithConfig\nfrom .swap_dialog import SwapProvidersButton\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nclass TxEditorContext(Enum):\n    \"\"\"\n    Context for which the TxEditor gets launched.\n    Allows to enable/disable certain features.\n    \"\"\"\n    PAYMENT = auto()\n    CHANNEL_FUNDING = auto()\n\n\nclass TxEditor(WindowModalDialog, QtEventListener, Logger):\n\n    swap_availability_changed = pyqtSignal()\n\n    def __init__(\n            self, *, title='',\n            window: 'ElectrumWindow',\n            make_tx,\n            output_value: Union[int, str],\n            payee_outputs: Optional[list[PartialTxOutput]] = None,\n            context: TxEditorContext = TxEditorContext.PAYMENT,\n            batching_candidates=None,\n    ):\n\n        WindowModalDialog.__init__(self, window, title=title)\n        Logger.__init__(self)\n        self.main_window = window\n        self.make_tx = make_tx\n        self.output_value = output_value\n        # used only for submarine payments as they construct tx independently of make_tx\n        self.payee_outputs = payee_outputs\n        self.tx = None  # type: Optional[PartialTransaction]\n        self.messages = []\n        self.error = ''   # set by side effect\n\n        self.config = window.config\n        self.network = window.network\n        self.fee_policy = FeePolicy(self.config.FEE_POLICY)\n        self.wallet = window.wallet\n        self.feerounding_sats = 0\n        self.not_enough_funds = False\n        self.no_dynfee_estimates = False\n        self.needs_update = False\n        self.context = context\n        self.is_preview = False\n        self._base_tx = None # for batching\n        self.batching_candidates = batching_candidates\n\n        self.swap_manager = self.wallet.lnworker.swap_manager if self.wallet.has_lightning() else None\n        self.swap_transport = None  # type: Optional[SwapServerTransport]\n        self.swap_availability_changed.connect(self.on_swap_availability_changed, Qt.ConnectionType.QueuedConnection)\n        self.ongoing_swap_transport_connection_attempt = None  # type: Optional[Future]\n        self.did_swap = False  # used to clear the PI on send tab\n\n        self.locktime_e = LockTimeEdit(self)\n        self.locktime_e.valueEdited.connect(self.trigger_update)\n        self.locktime_label = QLabel(_(\"LockTime\") + \": \")\n        self.io_widget = TxInOutWidget(self.main_window, self.wallet)\n        self.create_fee_controls()\n\n        onchain_vbox = QVBoxLayout()\n        onchain_top = self.create_top_bar(self.help_text)\n        onchain_grid = self.create_grid()\n        onchain_vbox.addLayout(onchain_top)\n        onchain_vbox.addLayout(onchain_grid)\n        onchain_vbox.addWidget(self.io_widget)\n        self.message_label = WWLabel('')\n        self.message_label.setMinimumHeight(70)\n        onchain_vbox.addWidget(self.message_label)\n\n        onchain_buttons = self.create_buttons_bar()\n        onchain_vbox.addStretch(1)\n        onchain_vbox.addLayout(onchain_buttons)\n\n        # onchain tab is the main tab and the content is also shown if tabs are disabled\n        self.onchain_tab = QWidget()\n        self.onchain_tab.setContentsMargins(0,0,0,0)\n        self.onchain_tab.setLayout(onchain_vbox)\n\n        # optional submarine payment tab, the tab is only shown if the option is enabled\n        self.submarine_payment_tab = self.create_submarine_payment_tab()\n\n        self.tab_widget = QTabWidget()\n        self.tab_widget.setTabBarAutoHide(True)  # hides the tab bar if there is only one tab\n        self.tab_widget.setContentsMargins(0, 0, 0, 0)\n        self.tab_widget.currentChanged.connect(self.on_tab_changed)\n\n        self.main_layout = QVBoxLayout()\n        self.main_layout.addWidget(self.tab_widget)\n        self.main_layout.setContentsMargins(6, 6, 6, 6)  # reduce outermost margins a bit\n        self.setLayout(self.main_layout)\n\n        self.set_io_visible()\n        self.set_fee_edit_visible()\n        self.set_locktime_visible()\n        self.update_fee_target()\n        self.update_tab_visibility()\n        self.resize_to_fit_content()\n\n        self.timer = QTimer(self)\n        self.timer.setInterval(500)\n        self.timer.setSingleShot(False)\n        self.timer.timeout.connect(self.timer_actions)\n        self.timer.start()\n        self.register_callbacks()\n        # debug_widget_layouts(self)  # enable to show red lines around all elements\n\n    def accept(self):\n        self._cleanup()\n        super().accept()\n\n    def reject(self):\n        self._cleanup()\n        super().reject()\n\n    def closeEvent(self, event):\n        self._cleanup()\n        super().closeEvent(event)\n\n    def _cleanup(self):\n        self.unregister_callbacks()\n        if self.ongoing_swap_transport_connection_attempt:\n            self.ongoing_swap_transport_connection_attempt.cancel()\n        if isinstance(self.swap_transport, NostrTransport):\n            asyncio.run_coroutine_threadsafe(self.swap_transport.stop(), get_asyncio_loop())\n        self.swap_transport = None  # HTTPTransport doesn't need to be closed\n\n    def on_tab_changed(self, index):\n        if self.tab_widget.widget(index) == self.submarine_payment_tab:\n            self.prepare_swap_transport()\n            self.update_submarine_payment_tab()\n        else:\n            self.update()\n\n    def is_batching(self) -> bool:\n        return self._base_tx is not None\n\n    def timer_actions(self):\n        if self.needs_update:\n            self.update()\n            self.needs_update = False\n\n    def update(self):\n        self.update_tx()\n        self.set_locktime()\n        self._update_widgets()\n\n    def stop_editor_updates(self):\n        self.timer.stop()\n\n    def update_tx(self, *, fallback_to_zero_fee: bool = False):\n        # expected to set self.tx, self.message and self.error\n        raise NotImplementedError()\n\n    def create_grid(self) -> QGridLayout:\n        raise NotImplementedError()\n\n    @property\n    def help_text(self) -> str:\n        raise NotImplementedError()\n\n    def update_fee_target(self):\n        if self.fee_slider.is_active():\n            text = self.fee_policy.get_target_text()\n        else:\n            text = \"\"\n        self.fee_target.setText(text)\n\n    def update_feerate_label(self):\n        self.feerate_label.setText(self.feerate_e.text() + ' ' + self.feerate_e.base_unit())\n\n    def create_fee_controls(self):\n\n        self.fee_label = QLabel('')\n        self.fee_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n\n        self.size_label = TxSizeLabel()\n        self.size_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self.size_label.setAmount(0)\n        self.size_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())\n\n        self.feerate_label = QLabel('')\n        self.feerate_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n\n        self.fiat_fee_label = TxFiatLabel()\n        self.fiat_fee_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self.fiat_fee_label.setAmount(0)\n        self.fiat_fee_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())\n\n        self.feerate_e = FeerateEdit(lambda: 0)\n        self.feerate_e.textEdited.connect(partial(self.on_fee_or_feerate, self.feerate_e, False))\n        self.feerate_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.feerate_e, True))\n        self.update_feerate_label()\n\n        self.fee_e = BTCAmountEdit(self.main_window.get_decimal_point)\n        self.fee_e.textEdited.connect(partial(self.on_fee_or_feerate, self.fee_e, False))\n        self.fee_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.fee_e, True))\n\n        self.feerate_e.setFixedWidth(150)\n        self.fee_e.setFixedWidth(150)\n\n        if self.fee_policy.method != FeeMethod.FIXED:\n            self.feerate_e.setAmount(self.fee_policy.fee_per_byte(self.network))\n        else:\n            self.fee_e.setAmount(self.fee_policy.value)\n\n        self.fee_e.textChanged.connect(self.entry_changed)\n        self.feerate_e.textChanged.connect(self.entry_changed)\n\n        self.fee_target = QLabel('')\n        self.fee_slider = FeeSlider(parent=self, network=self.network, fee_policy=self.fee_policy, callback=self.fee_slider_callback)\n        self.fee_combo = FeeComboBox(self.fee_slider)\n        self.fee_combo.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n\n        def feerounding_onclick():\n            text = (self.feerounding_text() + '\\n\\n' +\n                    _('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' +\n                    _('At most 100 satoshis might be lost due to this rounding.') + ' ' +\n                    _(\"You can disable this setting in '{}'.\").format(_('Preferences')) + '\\n' +\n                    _('Also, dust is not kept as change, but added to the fee.')  + '\\n' +\n                    _('Also, when batching RBF transactions, BIP 125 imposes a lower bound on the fee.'))\n            self.show_message(title=_('Fee rounding'), msg=text)\n\n        self.feerounding_icon = QToolButton()\n        self.feerounding_icon.setStyleSheet(\"background-color: rgba(255, 255, 255, 0); \")\n        self.feerounding_icon.setAutoRaise(True)\n        self.feerounding_icon.clicked.connect(feerounding_onclick)\n        self.set_feerounding_visibility(False)\n\n        self.fee_hbox = fee_hbox = QHBoxLayout()\n        fee_hbox.addWidget(self.feerate_e)\n        fee_hbox.addWidget(self.feerate_label)\n        fee_hbox.addWidget(self.size_label)\n        fee_hbox.addWidget(self.fee_e)\n        fee_hbox.addWidget(self.fee_label)\n        fee_hbox.addWidget(self.fiat_fee_label)\n        fee_hbox.addWidget(self.feerounding_icon)\n        fee_hbox.addStretch()\n\n        self.fee_target_hbox = fee_target_hbox = QHBoxLayout()\n        fee_target_hbox.addWidget(self.fee_target)\n        fee_target_hbox.addWidget(self.fee_slider)\n        fee_target_hbox.addWidget(self.fee_combo)\n        fee_target_hbox.addStretch()\n\n        # set feerate_label to same size as feerate_e\n        self.feerate_label.setFixedSize(self.feerate_e.sizeHint())\n        self.fee_label.setFixedSize(self.fee_e.sizeHint())\n        self.fee_slider.setFixedWidth(200)\n        self.fee_target.setFixedSize(self.feerate_e.sizeHint())\n\n    def update_tab_visibility(self):\n        \"\"\"Update self.tab_widget to show all tabs that are enabled.\"\"\"\n        # first remove all tabs\n        while self.tab_widget.count() > 0:\n            self.tab_widget.removeTab(0)\n\n        # always show onchain payment tab\n        self.tab_widget.addTab(self.onchain_tab, _('Onchain Transaction'))\n\n        allow_swaps = self.context == TxEditorContext.PAYMENT and self.payee_outputs and self.swap_manager\n        if self.config.WALLET_ENABLE_SUBMARINE_PAYMENTS and allow_swaps:\n            i = self.tab_widget.addTab(self.submarine_payment_tab, _('Submarine Payment'))\n            tooltip = self.config.cv.WALLET_ENABLE_SUBMARINE_PAYMENTS.get_long_desc()\n            if len(self.payee_outputs) > 1:\n                self.tab_widget.setTabEnabled(i, False)\n                tooltip = _(\"Submarine Payments don't support multiple outputs (Pay-to-many).\")\n            elif self.payee_outputs[0].value == '!':\n                self.tab_widget.setTabEnabled(i, False)\n                self.submarine_payment_tab.setEnabled(False)\n                tooltip = _(\"Submarine Payments don't support 'Max' value spends.\")\n            self.tab_widget.tabBar().setTabToolTip(i, tooltip)\n\n        # enable document mode if there is only one tab to hide the frame\n        self.tab_widget.setDocumentMode(self.tab_widget.count() < 2)\n        self.resize_to_fit_content()\n\n    def trigger_update(self):\n        # set tx to None so that the ok button is disabled while we compute the new tx\n        self.tx = None\n        self.messages = []\n        self.error = ''\n        self._update_widgets()\n        self.needs_update = True\n\n    def fee_slider_callback(self, fee_rate):\n        self.fee_slider.activate()\n        if fee_rate:\n            fee_rate = Decimal(fee_rate)\n            self.feerate_e.setAmount(quantize_feerate(fee_rate / 1000))\n        else:\n            self.feerate_e.setAmount(None)\n        self.fee_e.setModified(False)\n        self.update_fee_target()\n        self.update_feerate_label()\n        self.trigger_update()\n\n    def on_fee_or_feerate(self, edit_changed, editing_finished):\n        edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e\n        if editing_finished:\n            if edit_changed.get_amount() is None:\n                # This is so that when the user blanks the fee and moves on,\n                # we go back to auto-calculate mode and put a fee back.\n                edit_changed.setModified(False)\n        else:\n            # edit_changed was edited just now, so make sure we will\n            # freeze the correct fee setting (this)\n            edit_other.setModified(False)\n            self.fee_slider.deactivate()\n            # do not call trigger_update on editing_finished,\n            # because that event is emitted when we press OK\n            self.trigger_update()\n\n    def is_send_fee_frozen(self) -> bool:\n        return self.fee_e.isVisible() and self.fee_e.isModified() \\\n               and (bool(self.fee_e.text()) or self.fee_e.hasFocus())\n\n    def is_send_feerate_frozen(self) -> bool:\n        return self.feerate_e.isVisible() and self.feerate_e.isModified() \\\n               and (bool(self.feerate_e.text()) or self.feerate_e.hasFocus())\n\n    def feerounding_text(self):\n        return (_('Additional {} satoshis are going to be added.').format(self.feerounding_sats))\n\n    def set_feerounding_visibility(self, b:bool):\n        # we do not use setVisible because it affects the layout\n        self.feerounding_icon.setIcon(read_QIcon('info.png') if b else QIcon())\n        self.feerounding_icon.setEnabled(b)\n\n    def get_fee_policy(self):\n        feerate = self.feerate_e.get_amount()\n        fee_amount = self.fee_e.get_amount()\n        if self.is_send_fee_frozen() and fee_amount is not None:\n            fee_policy = FixedFeePolicy(fee_amount)\n        elif self.is_send_feerate_frozen() and feerate is not None:\n            feerate_per_kb = int(feerate * 1000)\n            fee_policy = FeePolicy(f'feerate:{feerate_per_kb}')\n        else:\n            fee_policy = self.fee_slider.get_policy()\n        return fee_policy\n\n    def entry_changed(self):\n        # blue color denotes auto-filled values\n        text = \"\"\n        fee_color = ColorScheme.DEFAULT\n        feerate_color = ColorScheme.DEFAULT\n        if self.not_enough_funds:\n            fee_color = ColorScheme.RED\n            feerate_color = ColorScheme.RED\n        elif self.fee_e.isModified():\n            feerate_color = ColorScheme.BLUE\n        elif self.feerate_e.isModified():\n            fee_color = ColorScheme.BLUE\n        else:\n            fee_color = ColorScheme.BLUE\n            feerate_color = ColorScheme.BLUE\n        self.fee_e.setStyleSheet(fee_color.as_stylesheet())\n        self.feerate_e.setStyleSheet(feerate_color.as_stylesheet())\n        self.needs_update = True\n\n    def update_fee_fields(self):\n        freeze_fee = self.is_send_fee_frozen()\n        freeze_feerate = self.is_send_feerate_frozen()\n        tx = self.tx\n        if self.no_dynfee_estimates and tx:\n            size = tx.estimated_size()\n            self.size_label.setAmount(size)\n            #self.size_e.setAmount(size)\n        if self.not_enough_funds or self.no_dynfee_estimates:\n            if not freeze_fee:\n                self.fee_e.setAmount(None)\n            if not freeze_feerate:\n                self.feerate_e.setAmount(None)\n            self.set_feerounding_visibility(False)\n            return\n\n        assert tx is not None\n        size = tx.estimated_size()\n        fee = tx.get_fee()\n\n        #self.size_e.setAmount(size)\n        self.size_label.setAmount(size)\n        fiat_fee = self.main_window.format_fiat_and_units(fee)\n        self.fiat_fee_label.setAmount(fiat_fee)\n\n        # Displayed fee/fee_rate values are set according to user input.\n        # Due to rounding or dropping dust in CoinChooser,\n        # actual fees often differ somewhat.\n        if freeze_feerate or self.fee_slider.is_active():\n            displayed_feerate = self.feerate_e.get_amount()\n            if displayed_feerate is not None:\n                displayed_feerate = quantize_feerate(displayed_feerate)\n            elif self.fee_slider.is_active():\n                # fallback to actual fee\n                displayed_feerate = quantize_feerate(fee / size) if fee is not None else None\n                self.feerate_e.setAmount(displayed_feerate)\n            if displayed_feerate is not None:\n                displayed_fee = FeePolicy.estimate_fee_for_feerate(fee_per_kb=displayed_feerate * 1000, size=size)\n            else:\n                displayed_fee = None\n            self.fee_e.setAmount(displayed_fee)\n        else:\n            if freeze_fee:\n                displayed_fee = self.fee_e.get_amount()\n            else:\n                # fallback to actual fee if nothing is frozen\n                displayed_fee = fee\n                self.fee_e.setAmount(displayed_fee)\n            displayed_fee = displayed_fee if displayed_fee else 0\n            displayed_feerate = quantize_feerate(displayed_fee / size) if displayed_fee is not None else None\n            self.feerate_e.setAmount(displayed_feerate)\n\n        # set fee rounding icon to empty if there is no rounding\n        feerounding = (fee - displayed_fee) if (fee and displayed_fee is not None) else 0\n        self.feerounding_sats = int(feerounding)\n        self.feerounding_icon.setToolTip(self.feerounding_text())\n        self.set_feerounding_visibility(abs(feerounding) >= 1)\n        # feerate_label needs to be updated from feerate_e\n        self.update_feerate_label()\n        self.update_fee_target()\n\n    def create_buttons_bar(self):\n        self.change_to_ln_swap_providers_button = SwapProvidersButton(lambda: self.swap_transport, self.config, self.main_window)\n        self.preview_button = QPushButton(_('Preview'))\n        self.preview_button.clicked.connect(self.on_preview)\n        self.preview_button.setVisible(self.context != TxEditorContext.CHANNEL_FUNDING)\n        self.ok_button = QPushButton(_('OK'))\n        self.ok_button.clicked.connect(self.on_send)\n        self.ok_button.setDefault(True)\n        buttons = Buttons(CancelButton(self), self.preview_button, self.ok_button)\n        buttons.insertWidget(0, self.change_to_ln_swap_providers_button)\n\n        if self.batching_candidates is not None and len(self.batching_candidates) > 0:\n            batching_combo = QComboBox()\n            batching_combo.addItems([_('Do not batch')] + [_('Batch with') + ' ' + tx.txid()[0:10] for tx in self.batching_candidates])\n            buttons.insertWidget(0, batching_combo)\n            def on_batching_combo(x):\n                self._base_tx = self.batching_candidates[x - 1] if x > 0 else None\n                self.trigger_update()\n            batching_combo.currentIndexChanged.connect(on_batching_combo)\n        return buttons\n\n    def create_top_bar(self, text):\n        self.pref_menu = QMenuWithConfig(self.config)\n\n        def cb():\n            self.set_io_visible()\n            self.resize_to_fit_content()\n        self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_IO, callback=cb)\n        def cb():\n            self.set_fee_edit_visible()\n            self.resize_to_fit_content()\n        self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS, callback=cb)\n        def cb():\n            self.set_locktime_visible()\n            self.resize_to_fit_content()\n        self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_LOCKTIME, callback=cb)\n        self.pref_menu.addSeparator()\n        can_have_lightning = self.wallet.can_have_lightning()\n        send_ch_to_ln = self.pref_menu.addConfig(\n            self.config.cv.WALLET_SEND_CHANGE_TO_LIGHTNING,\n            callback=lambda: (self.prepare_swap_transport(), self.trigger_update()),  # type: ignore\n            checked=False if not can_have_lightning else None,\n        )\n        sub_payments = self.pref_menu.addConfig(\n            self.config.cv.WALLET_ENABLE_SUBMARINE_PAYMENTS,\n            callback=self.update_tab_visibility,\n            checked=False if not can_have_lightning else None,\n        )\n        if not can_have_lightning:  # disable the buttons and override tooltip\n            ln_unavailable_msg = _(\"Not available for this wallet.\") \\\n                                 + \"\\n\" + _(\"Requires a wallet with Lightning network support.\")\n            for ln_conf in (send_ch_to_ln, sub_payments):\n                ln_conf.setEnabled(False)\n                ln_conf.setToolTip(ln_unavailable_msg)\n        self.pref_menu.addToggle(\n            _('Use change addresses'),\n            self.toggle_use_change,\n            default_state=self.wallet.use_change,\n            tooltip=_('Using change addresses makes it more difficult for other people to track your transactions.'))\n        self.use_multi_change_menu = self.pref_menu.addToggle(\n            _('Use multiple change addresses'),\n            self.toggle_multiple_change,\n            default_state=self.wallet.multiple_change,\n            tooltip='\\n'.join([\n                _('In some cases, use up to 3 change addresses in order to break '\n                  'up large coin amounts and obfuscate the recipient address.'),\n                _('This may result in higher transactions fees.')\n            ]))\n        self.use_multi_change_menu.setEnabled(self.wallet.use_change)\n        # fixme: some of these options (WALLET_SEND_CHANGE_TO_LIGHTNING, WALLET_MERGE_DUPLICATE_OUTPUTS)\n        # only make sense when we create a new tx, and should not be visible/enabled in rbf dialog\n        self.pref_menu.addConfig(self.config.cv.WALLET_MERGE_DUPLICATE_OUTPUTS, callback=self.trigger_update)\n        self.pref_menu.addConfig(self.config.cv.WALLET_SPEND_CONFIRMED_ONLY, callback=self.trigger_update)\n        self.pref_menu.addConfig(self.config.cv.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING, callback=self.trigger_update)\n        self.pref_button = QToolButton()\n        self.pref_button.setIcon(read_QIcon(\"preferences.png\"))\n        self.pref_button.setText(_('Tools'))\n        self.pref_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)\n        self.pref_button.setMenu(self.pref_menu)\n        self.pref_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)\n        self.pref_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n        hbox = QHBoxLayout()\n        hbox.addWidget(QLabel(text))\n        hbox.addStretch()\n        hbox.addWidget(self.pref_button)\n        return hbox\n\n    @profiler(min_threshold=0.02)\n    def resize_to_fit_content(self):\n        # update all geometries so the updated size hints are used for size adjustment\n        for widget in self.findChildren(QWidget):\n            widget.updateGeometry()\n        self.adjustSize()\n\n    def toggle_use_change(self):\n        self.wallet.use_change = not self.wallet.use_change\n        self.wallet.db.put('use_change', self.wallet.use_change)\n        self.use_multi_change_menu.setEnabled(self.wallet.use_change)\n        self.trigger_update()\n\n    def toggle_multiple_change(self):\n        self.wallet.multiple_change = not self.wallet.multiple_change\n        self.wallet.db.put('multiple_change', self.wallet.multiple_change)\n        self.trigger_update()\n\n    def set_io_visible(self):\n        self.io_widget.setVisible(self.config.GUI_QT_TX_EDITOR_SHOW_IO)\n\n    def set_fee_edit_visible(self):\n        b = self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS\n        detailed = [self.feerounding_icon, self.feerate_e, self.fee_e]\n        basic = [self.fee_label, self.feerate_label]\n        # first hide, then show\n        for w in (basic if b else detailed):\n            w.hide()\n        for w in (detailed if b else basic):\n            w.show()\n\n    def set_locktime_visible(self):\n        b = self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME\n        for w in [\n                self.locktime_e,\n                self.locktime_label]:\n            w.setVisible(b)\n\n    def run(self):\n        if self.config.WALLET_SEND_CHANGE_TO_LIGHTNING:\n            # if disabled but submarine payments are enabled we only connect once the other tab gets opened\n            self.prepare_swap_transport()\n        cancelled = not self.exec()\n        self.stop_editor_updates()\n        self.deleteLater()  # see #3956\n        return self.tx if not cancelled else None\n\n    def on_send(self):\n        if self.tx and self.tx.get_dummy_output(DummyAddress.SWAP):\n            if not self.request_forward_swap():\n                return\n        self.accept()\n\n    def on_preview(self):\n        assert not self.tx.get_dummy_output(DummyAddress.SWAP), \"no preview when sending change to ln\"\n        self.is_preview = True\n        self.accept()\n\n    def _update_widgets(self):\n        # side effect: self.error\n        self._update_amount_label()\n        if self.not_enough_funds:\n            self.error = _('Not enough funds.')\n            confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY\n            if confirmed_only and self.can_pay_assuming_zero_fees(confirmed_only=False):\n                self.error += ' ' + _('Change your settings to allow spending unconfirmed coins.')\n            elif self.can_pay_assuming_zero_fees(confirmed_only=confirmed_only):\n                self.error += ' ' + _('You need to set a lower fee.')\n            elif frozen_bal := self.wallet.get_frozen_balance_str():\n                self.error = self.wallet.get_text_not_enough_funds_mentioning_frozen(\n                    for_amount=self.output_value,\n                    hint=_('Can be unfrozen in the Addresses or in the Coins tab')\n                )\n        if not self.tx:\n            if self.not_enough_funds:\n                self.io_widget.update(None)\n            self.set_feerounding_visibility(False)\n            self.messages = [_('Preparing transaction...')]\n        else:\n            self.messages = self.get_messages()\n            self.update_fee_fields()\n            if self.locktime_e.get_locktime() is None:\n                self.locktime_e.set_locktime(self.tx.locktime)\n            self.io_widget.update(self.tx)\n            self.fee_label.setText(self.main_window.config.format_amount_and_units(self.tx.get_fee()))\n            self._update_extra_fees()\n\n        if self.config.WALLET_SEND_CHANGE_TO_LIGHTNING:\n            self.change_to_ln_swap_providers_button.setVisible(True)\n            self.change_to_ln_swap_providers_button.fetching = bool(self.ongoing_swap_transport_connection_attempt)\n            self.change_to_ln_swap_providers_button.update()\n        else:\n            self.change_to_ln_swap_providers_button.setVisible(False)\n\n        self._update_send_button()\n        self._update_message()\n\n    def get_messages(self):\n        # side effect: self.error\n        messages = []\n        fee = self.tx.get_fee()\n        assert fee is not None\n        amount = self.tx.output_value() if self.output_value == '!' else self.output_value\n        tx_size = self.tx.estimated_size()\n        fee_warning_tuple = self.wallet.get_tx_fee_warning(\n            invoice_amt=amount, tx_size=tx_size, fee=fee, txid=self.tx.txid())\n        if fee_warning_tuple:\n            allow_send, long_warning, short_warning = fee_warning_tuple\n            if not allow_send:\n                self.error = long_warning\n            else:\n                messages.append(long_warning)\n        if self.no_dynfee_estimates:\n            self.error = _('Fee estimates not available. Please set a fixed fee or feerate.')\n        if dummy_output := self.tx.get_dummy_output(DummyAddress.SWAP):\n            swap_msg = _('Will send change to lightning')\n            swap_fee_msg = \".\"\n            if self.swap_manager and self.swap_manager.is_initialized.is_set() and isinstance(dummy_output.value, int):\n                ln_amount_we_recv = self.swap_manager.get_recv_amount(send_amount=dummy_output.value, is_reverse=False)\n                if ln_amount_we_recv:\n                    swap_fees = dummy_output.value - ln_amount_we_recv\n                    swap_fee_msg = \" [\" + _(\"Swap fees:\") + \" \" + self.main_window.format_amount_and_units(swap_fees) + \"].\"\n            messages.append(swap_msg + swap_fee_msg)\n        elif self.config.WALLET_SEND_CHANGE_TO_LIGHTNING \\\n                and not self.ongoing_swap_transport_connection_attempt \\\n                and self.tx.has_change():\n            swap_msg = _('Will not send change to Lightning')\n            swap_msg_reason = None\n            change_amount = sum(c.value for c in self.tx.get_change_outputs() if isinstance(c.value, int))\n            if not self.wallet.has_lightning():\n                swap_msg_reason = _('Lightning is not enabled.')\n            elif change_amount > int(self.wallet.lnworker.num_sats_can_receive()):\n                swap_msg_reason = _(\"Your channels cannot receive this amount.\")\n            elif self.wallet.lnworker.swap_manager.is_initialized.is_set():\n                min_amount = self.wallet.lnworker.swap_manager.get_min_amount()\n                max_amount = self.wallet.lnworker.swap_manager.get_provider_max_reverse_amount()\n                if change_amount < min_amount:\n                    swap_msg_reason = _(\"Below the swap providers minimum value of {}.\").format(\n                        self.main_window.format_amount_and_units(min_amount)\n                    )\n                else:\n                    swap_msg_reason = _('Change amount exceeds the swap providers maximum value of {}.').format(\n                        self.main_window.format_amount_and_units(max_amount)\n                    )\n            messages.append(swap_msg + (f\": {swap_msg_reason}\" if swap_msg_reason else '.'))\n        elif self.ongoing_swap_transport_connection_attempt:\n            messages.append(_(\"Fetching submarine swap providers...\"))\n        # warn if spending unconf\n        if any((txin.block_height is not None and txin.block_height<=0) for txin in self.tx.inputs()):\n            messages.append(_('This transaction will spend unconfirmed coins.'))\n        # warn if a reserve utxo was added\n        if reserve_sats := self.wallet.tx_keeps_ln_utxo_reserve(self.tx, gui_spend_max=bool(self.output_value == '!')):\n            reserve_str = self.main_window.config.format_amount_and_units(reserve_sats)\n            messages.append(_('Could not spend max: a security reserve of {} was kept for your Lightning channels.').format(reserve_str))\n        # warn if we merge from mempool\n        if self.is_batching():\n            messages.append(_('This payment will be merged with another existing transaction.'))\n        # warn if we use multiple change outputs\n        num_change = sum(int(o.is_change) for o in self.tx.outputs())\n        num_ismine = sum(int(o.is_mine) for o in self.tx.outputs())\n        if num_change > 1:\n            messages.append(_('This transaction has {} change outputs.'.format(num_change)))\n        # warn if there is no ismine output, as it might be problematic to RBF the tx later.\n        # (though RBF is still possible by adding new inputs, if the wallet has more utxos)\n        if num_ismine == 0:\n            messages.append(_('Make sure you pay enough mining fees; you will not be able to bump the fee later.'))\n\n        # TODO: warn if we send change back to input address\n        return messages\n\n    def set_locktime(self):\n        if not self.tx:\n            return\n        locktime = self.locktime_e.get_locktime()\n        if locktime is not None:\n            self.tx.locktime = locktime\n\n    def _update_amount_label(self):\n        pass\n\n    def _update_extra_fees(self):\n        pass\n\n    def _update_message(self):\n        style = ColorScheme.RED if self.error else ColorScheme.BLUE\n        message_str = '\\n'.join(self.messages) if self.messages else ''\n        self.message_label.setStyleSheet(style.as_stylesheet())\n        self.message_label.setText(self.error or message_str)\n\n    def _update_send_button(self):\n        # disable preview button when sending change to lightning to prevent the user from saving or\n        # exporting the transaction and broadcasting it later somehow.\n        send_change_to_ln = self.tx and self.tx.get_dummy_output(DummyAddress.SWAP)\n        enabled = bool(self.tx) and not self.error\n        self.preview_button.setEnabled(enabled and not send_change_to_ln)\n        self.preview_button.setToolTip(_(\"Can't show preview when sending change to lightning\") if send_change_to_ln else \"\")\n        self.ok_button.setEnabled(enabled)\n\n    def can_pay_assuming_zero_fees(self, confirmed_only: bool) -> bool:\n        raise NotImplementedError\n\n    ### --- Shared functionality for submarine swaps (change to ln and submarine payments) ---\n    def prepare_swap_transport(self):\n        if not self.swap_manager:\n            return  # no swaps possible, lightning disabled\n        if self.swap_transport is not None and self.swap_transport.is_connected.is_set():\n            # we already have a connected transport, no need to create a new one\n            return\n        if self.ongoing_swap_transport_connection_attempt:\n            # another task is currently trying to connect\n            return\n\n        # there should only be a connected transport.\n        # a useless transport should get cleaned up and not stored.\n        assert self.swap_transport is None, \"swap transport wasn't cleaned up properly\"\n\n        new_swap_transport = self.main_window.create_sm_transport()\n        if not new_swap_transport:\n            # user declined to enable Nostr and has no http server configured\n            self.swap_availability_changed.emit()\n            return\n\n        async def _initialize_transport(transport):\n            try:\n                if isinstance(transport, NostrTransport):\n                    asyncio.create_task(transport.main_loop())\n                else:\n                    assert isinstance(transport, HttpTransport)\n                    asyncio.create_task(transport.get_pairs_just_once())\n                if not await self.wait_for_swap_transport(transport):\n                    return\n                self.swap_transport = transport\n            except Exception:\n                self.logger.exception(\"failed to create swap transport\")\n            finally:\n                self.ongoing_swap_transport_connection_attempt = None\n                self.swap_availability_changed.emit()\n\n        # this task will get cancelled if the TxEditor gets closed\n        self.ongoing_swap_transport_connection_attempt = asyncio.run_coroutine_threadsafe(\n            _initialize_transport(new_swap_transport),\n            get_asyncio_loop(),\n        )\n\n    async def wait_for_swap_transport(self, new_swap_transport: Union[HttpTransport, NostrTransport]) -> bool:\n        \"\"\"\n        Wait until we found the announcement event of the configured swap server.\n        If it is not found but the relay connection is established return True anyway,\n        the user will then need to select a different swap server.\n        \"\"\"\n        timeout = new_swap_transport.connect_timeout + 1\n        try:\n            # swap_manager.is_initialized gets set once we got pairs of the configured swap server\n            await wait_for2(self.swap_manager.is_initialized.wait(), timeout)\n        except asyncio.TimeoutError:\n            self.logger.debug(f\"swap transport initialization timed out after {timeout} sec\")\n\n        if self.swap_manager.is_initialized.is_set():\n            return True\n\n        # timed out above\n        if self.config.SWAPSERVER_URL:\n            # http swapserver didn't return pairs\n            self.logger.error(f\"couldn't request pairs from {self.config.SWAPSERVER_URL=}\")\n            return False\n        elif new_swap_transport.is_connected.is_set():\n            assert isinstance(new_swap_transport, NostrTransport)\n            # couldn't find announcement of configured swapserver, maybe it is gone.\n            # update_submarine_payment_tab will tell the user to select a different swap server.\n            return True\n\n        # we couldn't even connect to the relays, this transport is useless. maybe network issues.\n        return False\n\n    @qt_event_listener\n    def on_event_swap_provider_changed(self):\n        self.swap_availability_changed.emit()\n\n    @qt_event_listener\n    def on_event_channel(self, wallet, _channel):\n        # useful e.g. if the user quickly opens the tab after startup before the channels are initialized\n        if wallet == self.wallet and self.swap_manager and self.swap_manager.is_initialized.is_set():\n            self.swap_availability_changed.emit()\n\n    @qt_event_listener\n    def on_event_swap_offers_changed(self, _):\n        self.change_to_ln_swap_providers_button.update()\n        self.submarine_payment_provider_button.update()\n        if self.ongoing_swap_transport_connection_attempt:\n            return\n        self.swap_availability_changed.emit()\n\n    @pyqtSlot()\n    def on_swap_availability_changed(self):\n        # uses a signal/slot to update the gui so we can schedule an update from the asyncio thread\n        if self.tab_widget.currentWidget() == self.submarine_payment_tab:\n            self.update_submarine_payment_tab()\n        else:\n            self.update()\n\n    ### --- Functionality for reverse submarine swaps to external address ---\n    def create_submarine_payment_tab(self) -> QWidget:\n        \"\"\"Returns widget for submarine payment functionality to be added as tab\"\"\"\n        tab_widget = QWidget()\n        vbox = QVBoxLayout(tab_widget)\n\n        # stack two views, a warning view and the regular one. The warning view is shown if\n        # the swap cannot be performed, e.g. due to missing liquidity.\n        self.submarine_stacked_widget = QStackedWidget()\n\n        # Normal layout page\n        normal_page = QWidget()\n        h = QGridLayout(normal_page)\n        help_button = HelpButton(MSG_SUBMARINE_PAYMENT_HELP_TEXT)\n        self.submarine_lightning_send_amount_label = QLabel()\n        self.submarine_onchain_send_amount_label = QLabel()\n        self.submarine_claim_mining_fee_label = QLabel()\n        self.submarine_server_fee_label = QLabel()\n        self.submarine_we_send_label = IconLabel(text=_('You send')+':')\n        self.submarine_we_send_label.setIcon(read_QIcon('lightning.png'))\n        self.submarine_they_receive_label = IconLabel(text=_('They receive')+':')\n        self.submarine_they_receive_label.setIcon(read_QIcon('bitcoin.png'))\n        # column 0 (labels)\n        h.addWidget(self.submarine_we_send_label, 0, 0)\n        h.addWidget(self.submarine_they_receive_label, 1, 0)\n        h.addWidget(QLabel(_('Swap fee')+':'), 2, 0)\n        h.addWidget(QLabel(_('Mining fee')+':'), 3, 0)\n        # column 1 (spacing)\n        h.setColumnStretch(1, 1)\n        # column 2 (amounts)\n        h.addWidget(self.submarine_lightning_send_amount_label, 0, 2)\n        h.addWidget(self.submarine_onchain_send_amount_label, 1, 2)\n        h.addWidget(self.submarine_server_fee_label, 2, 2, 1, 2)\n        h.addWidget(self.submarine_claim_mining_fee_label, 3, 2, 1, 2)\n        # column 3 (spacing)\n        h.setColumnStretch(3, 1)\n        # column 4 (help button)\n        h.addWidget(help_button, 0, 4)\n\n        # Warning layout page\n        warning_page = QWidget()\n        warning_layout = QVBoxLayout(warning_page)\n        self.submarine_warning_label = QLabel('')\n        warning_layout.addWidget(self.submarine_warning_label)\n\n        self.submarine_stacked_widget.addWidget(normal_page)\n        self.submarine_stacked_widget.addWidget(warning_page)\n\n        vbox.addWidget(self.submarine_stacked_widget)\n        vbox.addStretch(1)\n\n        self.submarine_payment_provider_button = SwapProvidersButton(lambda: self.swap_transport, self.config, self.main_window)\n\n        self.submarine_ok_button = QPushButton(_('OK'))\n        self.submarine_ok_button.setDefault(True)\n        self.submarine_ok_button.setEnabled(False)\n        # pay button must not self.accept() as this triggers closing the transport\n        self.submarine_ok_button.clicked.connect(self.start_submarine_payment)\n\n        buttons = Buttons(CancelButton(self), self.submarine_ok_button)\n        buttons.insertWidget(0, self.submarine_payment_provider_button)\n        vbox.addLayout(buttons)\n\n        return tab_widget\n\n    def show_swap_transport_connection_message(self):\n        self.submarine_stacked_widget.setCurrentIndex(1)\n        self.submarine_warning_label.setText(_(\"Connecting, please wait...\"))\n        self.submarine_ok_button.setEnabled(False)\n\n    def start_submarine_payment(self):\n        assert self.payee_outputs and len(self.payee_outputs) == 1\n        payee_output = self.payee_outputs[0]\n\n        assert self.expected_onchain_amount_sat is not None\n        assert self.lightning_send_amount_sat is not None\n        assert self.last_server_mining_fee_sat is not None\n        assert self.swap_transport.is_connected.is_set()\n        assert self.swap_manager.is_initialized.is_set()\n\n        self.tx = None  # prevent broadcasting\n        self.submarine_ok_button.setEnabled(False)\n        coro = self.swap_manager.reverse_swap(\n            transport=self.swap_transport,\n            lightning_amount_sat=self.lightning_send_amount_sat,\n            expected_onchain_amount_sat=self.expected_onchain_amount_sat,\n            prepayment_sat=2 * self.last_server_mining_fee_sat,\n            claim_to_output=payee_output,\n        )\n        try:\n            funding_txid = self.main_window.run_coroutine_dialog(coro, _('Initiating Submarine Payment...'))\n        except Exception as e:\n            self.close()\n            self.main_window.show_error(_(\"Submarine Payment failed:\") + \"\\n\" + str(e))\n            return\n        self.did_swap = True\n        # accepting closes the swap transport, so it needs to happen after the swap\n        self.accept()\n        self.main_window.on_swap_result(funding_txid, is_reverse=True)\n\n    def update_submarine_payment_tab(self):\n        assert self.tab_widget.currentWidget() == self.submarine_payment_tab\n        assert self.payee_outputs, \"Opened submarine payment tab without outputs?\"\n        assert len(self.payee_outputs) == \\\n               len([o for o in self.payee_outputs if not o.is_change and not isinstance(o.value, str)])\n        f = self.main_window.format_amount_and_units\n        self.logger.debug(f\"TxEditor updating submarine payment tab\")\n\n        if not self.swap_manager:\n            self.set_submarine_payment_tab_warning(_(\"Enable Lightning in the 'Channels' tab to use Submarine Swaps.\"))\n            return\n        if not self.swap_manager.is_initialized.is_set() \\\n                and self.ongoing_swap_transport_connection_attempt:\n            self.show_swap_transport_connection_message()\n            return\n        if not self.swap_transport:\n            # couldn't connect to nostr relays or http server didn't respond\n            self.set_submarine_payment_tab_warning(_(\"Submarine swap provider unavailable.\"))\n            return\n\n        # Update the swapserver selection button text\n        self.submarine_payment_provider_button.update()\n\n        if not self.swap_manager.is_initialized.is_set():\n            # connected to nostr relays but couldn't find swapserver announcement\n            assert isinstance(self.swap_transport, NostrTransport), \"HTTPTransport shouldn't get set if it cannot fetch pairs\"\n            assert self.swap_transport.is_connected.is_set(), \"closed transport wasn't cleaned up\"\n            if self.config.SWAPSERVER_NPUB:\n                msg = _(\"Couldn't connect to your swap provider. Please select a different provider.\")\n            else:\n                msg = _('Please select a submarine swap provider.')\n            self.set_submarine_payment_tab_warning(msg)\n            return\n\n        # update values\n        self.lightning_send_amount_sat = self.swap_manager.get_send_amount(\n            self.payee_outputs[0].value,  # claim tx fee reserve gets added in get_send_amount\n            is_reverse=True,\n        )\n        self.last_server_mining_fee_sat = self.swap_manager.mining_fee\n        self.expected_onchain_amount_sat = (\n            self.payee_outputs[0].value + self.swap_manager.get_fee_for_txbatcher()\n        )\n\n        # get warning\n        warning_text = self.get_swap_warning()\n        if warning_text:\n            self.set_submarine_payment_tab_warning(warning_text)\n            return\n\n        # There is no warning, show the normal view (amounts etc.)\n        self.submarine_stacked_widget.setCurrentIndex(0)\n\n        # label showing the payment amount (the amount the user entered in SendTab)\n        self.submarine_onchain_send_amount_label.setText(f(self.payee_outputs[0].value))\n\n        # the fee we pay to claim the funding output to the onchain address, shown as \"Mining Fee\"\n        claim_tx_mining_fee = self.swap_manager.get_fee_for_txbatcher()\n        self.submarine_claim_mining_fee_label.setText(f(claim_tx_mining_fee))\n\n        assert self.lightning_send_amount_sat is not None\n        self.submarine_lightning_send_amount_label.setText(f(self.lightning_send_amount_sat))\n        # complete fee we pay to the server\n        server_fee = self.lightning_send_amount_sat - self.expected_onchain_amount_sat\n        self.submarine_server_fee_label.setText(f(server_fee))\n\n        self.submarine_ok_button.setEnabled(True)\n\n    def get_swap_warning(self) -> Optional[str]:\n        f = self.main_window.format_amount_and_units\n        ln_can_send = int(self.wallet.lnworker.num_sats_can_send())\n\n        if self.expected_onchain_amount_sat < self.swap_manager.get_min_amount():\n            return '\\n'.join([\n                _(\"Payment amount below the minimum possible swap amount.\"),\n                _(\"Minimum amount: {}\").format(f(self.swap_manager.get_min_amount())), \"\",\n                _(\"You need to send a higher amount to be able to do a Submarine Payment.\"),\n            ])\n\n        too_low_outbound_liquidity_msg = ''.join([\n            _(\"You don't have enough outgoing capacity in your lightning channels.\"), '\\n',\n            _(\"Your lightning channels can send: {}\").format(f(ln_can_send)), '\\n',\n            _(\"For this transaction you need: {}\").format(f(self.lightning_send_amount_sat)) if self.lightning_send_amount_sat else '',\n            '\\n\\n' if self.lightning_send_amount_sat else '\\n',\n            _(\"To add outgoing capacity you can open a new lightning channel or do a submarine swap.\"),\n        ])\n\n        # prioritize showing the swap provider liquidity warning before the channel liquidity warning\n        # as it could be annoying for the user to be told to open a new channel just to come back to\n        # notice there is no provider supporting their swap amount\n        if self.lightning_send_amount_sat is None:\n            provider_liquidity = self.swap_manager.get_provider_max_forward_amount()\n            if provider_liquidity < self.swap_manager.get_min_amount():\n                provider_liquidity = 0\n            msg = [\n                _(\"The selected swap provider is unable to offer a forward swap of this value.\"),\n                _(\"Available liquidity\") + f\": {f(provider_liquidity)}\", \"\",\n                _(\"In order to continue select a different provider or try to send a smaller amount.\"),\n            ]\n            # we don't know exactly how much we need to send on ln yet, so we can assume 0 provider fees\n            probably_too_low_outbound_liquidity = self.expected_onchain_amount_sat > ln_can_send\n            if probably_too_low_outbound_liquidity:\n                msg.extend([\n                    \"\",\n                    \"Please also note:\",\n                    too_low_outbound_liquidity_msg,\n                ])\n            return \"\\n\".join(msg)\n\n        # if we have lightning_send_amount_sat our provider has enough liquidity, so we know the exact\n        # amount we need to send including the providers fees\n        too_low_outbound_liquidity = self.lightning_send_amount_sat > ln_can_send\n        if too_low_outbound_liquidity:\n            return too_low_outbound_liquidity_msg\n\n        return None\n\n    def set_submarine_payment_tab_warning(self, warning: str):\n        msg = _('Submarine Payment not possible:') + '\\n' + warning\n        self.submarine_warning_label.setText(msg)\n        self.submarine_stacked_widget.setCurrentIndex(1)\n        self.submarine_ok_button.setEnabled(False)\n\n    # --- send change to lightning swap functionality ---\n    def request_forward_swap(self):\n        swap_dummy_output = self.tx.get_dummy_output(DummyAddress.SWAP)\n        sm, transport = self.swap_manager, self.swap_transport\n        assert sm and transport and swap_dummy_output and isinstance(swap_dummy_output.value, int)\n        coro = sm.request_swap_for_amount(transport=transport, onchain_amount=int(swap_dummy_output.value))\n        coro_dialog = RunCoroutineDialog(self, _('Requesting swap invoice...'), coro)\n        try:\n            swap, swap_invoice = coro_dialog.run()\n        except (SwapServerError, UserFacingException) as e:\n            self.show_error(str(e))\n            return False\n        except UserCancelled:\n            return False\n        self.tx.replace_output_address(DummyAddress.SWAP, swap.lockup_address)\n        assert self.tx.get_dummy_output(DummyAddress.SWAP) is None\n        self.tx.swap_invoice = swap_invoice\n        self.tx.swap_payment_hash = swap.payment_hash\n        return True\n\n\nclass ConfirmTxDialog(TxEditor):\n    help_text = ''  #_('Set the mining fee of your transaction')\n\n    def __init__(\n        self, *,\n        window: 'ElectrumWindow',\n        make_tx,\n        output_value: Union[int, str],\n        payee_outputs: Optional[list[PartialTxOutput]] = None,\n        context: TxEditorContext = TxEditorContext.PAYMENT,\n        batching_candidates=None,\n    ):\n\n        TxEditor.__init__(\n            self,\n            window=window,\n            make_tx=make_tx,\n            output_value=output_value,\n            payee_outputs=payee_outputs,\n            title=_(\"New Transaction\"), # todo: adapt title for channel funding tx, swaps\n            context=context,\n            batching_candidates=batching_candidates,\n        )\n        self.trigger_update()\n\n    def _update_amount_label(self):\n        tx = self.tx\n        if self.output_value == '!':\n            if tx:\n                amount = tx.output_value()\n                amount_str = self.main_window.format_amount_and_units(amount)\n            else:\n                amount_str = \"max\"\n        else:\n            amount = self.output_value\n            amount_str = self.main_window.format_amount_and_units(amount)\n        self.amount_label.setText(amount_str)\n\n    def update_tx(self, *, fallback_to_zero_fee: bool = False):\n        self.fee_policy = fee_policy = self.get_fee_policy()\n        if fee_policy.method != FeeMethod.FIXED:\n            self.config.FEE_POLICY = fee_policy.get_descriptor()\n        confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY\n        base_tx = self._base_tx\n        try:\n            self.tx = self.make_tx(fee_policy, confirmed_only=confirmed_only, base_tx=base_tx)\n            self.not_enough_funds = False\n            self.no_dynfee_estimates = False\n        except NotEnoughFunds:\n            self.not_enough_funds = True\n            self.tx = None\n            if fallback_to_zero_fee:\n                try:\n                    self.tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only, base_tx=base_tx)\n                except BaseException:\n                    return\n            else:\n                return\n        except NoDynamicFeeEstimates:\n            # is this still needed?\n            self.no_dynfee_estimates = True\n            self.tx = None\n            try:\n                self.tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only, base_tx=base_tx)\n            except NotEnoughFunds:\n                self.not_enough_funds = True\n                return\n            except BaseException:\n                return\n        except InternalAddressCorruption as e:\n            self.tx = None\n            self.main_window.show_error(str(e))\n            raise\n        self.tx.set_rbf(True)\n\n    def can_pay_assuming_zero_fees(self, confirmed_only: bool) -> bool:\n        # called in send_tab.py\n        try:\n            tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only, base_tx=None)\n        except NotEnoughFunds:\n            return False\n        else:\n            return True\n\n    def create_grid(self):\n        grid = QGridLayout()\n        msg = (_('The amount to be received by the recipient.') + ' '\n               + _('Fees are paid by the sender.'))\n        self.amount_label = QLabel('')\n        self.amount_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n\n        grid.addWidget(HelpLabel(_(\"Amount to be sent\") + \": \", msg), 0, 0)\n        grid.addWidget(self.amount_label, 0, 1)\n\n        msg = _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\\n\\n'\\\n              + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\\n\\n'\\\n              + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')\n\n        grid.addWidget(HelpLabel(_(\"Mining Fee\") + \": \", msg), 1, 0)\n        grid.addLayout(self.fee_hbox, 1, 1, 1, 3)\n\n        grid.addWidget(HelpLabel(_(\"Fee policy\") + \": \", self.fee_combo.help_msg), 3, 0)\n        grid.addLayout(self.fee_target_hbox, 3, 1, 1, 3)\n\n        grid.setColumnStretch(4, 1)\n\n        # extra fee\n        self.extra_fee_label = QLabel(_(\"Additional fees\") + \": \")\n        self.extra_fee_label.setVisible(False)\n        self.extra_fee_value = QLabel('')\n        self.extra_fee_value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n        self.extra_fee_value.setVisible(False)\n        grid.addWidget(self.extra_fee_label, 5, 0)\n        grid.addWidget(self.extra_fee_value, 5, 1)\n\n        # locktime editor\n        grid.addWidget(self.locktime_label, 6, 0)\n        grid.addWidget(self.locktime_e, 6, 1, 1, 2)\n\n        return grid\n\n    def _update_extra_fees(self):\n        x_fee = run_hook('get_tx_extra_fee', self.wallet, self.tx)\n        if x_fee:\n            x_fee_address, x_fee_amount = x_fee\n            self.extra_fee_label.setVisible(True)\n            self.extra_fee_value.setVisible(True)\n            self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount))\n"
  },
  {
    "path": "electrum/gui/qt/console.py",
    "content": "# source: http://stackoverflow.com/questions/2758159/how-to-embed-a-python-interpreter-in-a-pyqt-widget\n\nimport sys\nimport os\nimport re\nimport traceback\n\nfrom PyQt6 import QtCore, QtGui, QtWidgets\nfrom PyQt6.QtCore import Qt\n\nfrom electrum import util\nfrom electrum.i18n import _\nfrom electrum.base_crash_reporter import taint_reports_by_console_usage\n\nfrom .util import MONOSPACE_FONT, font_height\n\n# sys.ps1 and sys.ps2 are only declared if an interpreter is in interactive mode.\nsys.ps1 = '>>> '\nsys.ps2 = '... '\n\n\nclass OverlayLabel(QtWidgets.QLabel):\n    STYLESHEET = '''\n    QLabel, QLabel link {\n        color: rgb(0, 0, 0);\n        background-color: rgb(248, 240, 200);\n        border: 1px solid;\n        border-color: rgb(255, 114, 47);\n        padding: 2px;\n    }\n    '''\n    def __init__(self, text, parent):\n        super().__init__(text, parent)\n        self.setMinimumHeight(max(150, 10 * font_height()))\n        self.setGeometry(0, 0, self.width(), self.height())\n        self.setStyleSheet(self.STYLESHEET)\n        self.setMargin(0)\n        parent.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)\n        self.setWordWrap(True)\n\n    def mousePressEvent(self, e):\n        self.hide()\n\n    def on_resize(self, w):\n        padding = 2  # px, from the stylesheet above\n        self.setFixedWidth(w - padding)\n\n\nclass Console(QtWidgets.QPlainTextEdit):\n    DEFAULT_FONT_SIZE = 10\n    MIN_FONT_SIZE = 6\n    MAX_FONT_SIZE = 32\n\n    def __init__(self, parent=None):\n        QtWidgets.QPlainTextEdit.__init__(self, parent)\n\n        self.history = []\n        self.namespace = {}\n        self.construct = []\n        self.font_size = self.DEFAULT_FONT_SIZE\n\n        self.setGeometry(50, 75, 600, 400)\n        self.setWordWrapMode(QtGui.QTextOption.WrapMode.WrapAnywhere)\n        self.setUndoRedoEnabled(False)\n        self.setFont(QtGui.QFont(MONOSPACE_FONT, self.font_size, QtGui.QFont.Weight.Normal))\n        self.newPrompt(\"\")  # make sure there is always a prompt, even before first server.banner\n\n        self.updateNamespace({'run':self.run_script})\n        self.set_json(False)\n\n        warning_text = \"<h1>{}</h1><br>{}<br><br>{}\".format(\n            _(\"Warning!\"),\n            _(\"Do not paste code here that you don't understand. Executing the wrong code could lead \"\n              \"to your coins being irreversibly lost.\"),\n            _(\"Click here to hide this message.\")\n        )\n        self.messageOverlay = OverlayLabel(warning_text, self)\n\n    def set_font_size(self, size: int):\n        size = max(self.MIN_FONT_SIZE, min(self.MAX_FONT_SIZE, size))\n        self.font_size = size\n        self.setFont(QtGui.QFont(MONOSPACE_FONT, self.font_size, QtGui.QFont.Weight.Normal))\n\n    def resizeEvent(self, e):\n        super().resizeEvent(e)\n        vertical_scrollbar_width = self.verticalScrollBar().width() * self.verticalScrollBar().isVisible()\n        self.messageOverlay.on_resize(self.width() - vertical_scrollbar_width)\n\n    def set_json(self, b):\n        self.is_json = b\n\n    def run_script(self, filename):\n        with open(filename) as f:\n            script = f.read()\n\n        self._exec_command(script)\n\n    def updateNamespace(self, namespace):\n        self.namespace.update(namespace)\n\n    def showMessage(self, message):\n        curr_line = self.getCommand(strip=False)\n        self.appendPlainText(message)\n        self.newPrompt(curr_line)\n\n    def clear(self):\n        curr_line = self.getCommand()\n        self.setPlainText('')\n        self.newPrompt(curr_line)\n\n    def keyboard_interrupt(self):\n        self.construct = []\n        self.appendPlainText('KeyboardInterrupt')\n        self.newPrompt('')\n\n    def newPrompt(self, curr_line):\n        if self.construct:\n            prompt = sys.ps2 + curr_line\n        else:\n            prompt = sys.ps1 + curr_line\n\n        self.completions_pos = self.textCursor().position()\n        self.completions_visible = False\n\n        self.appendPlainText(prompt)\n        self.moveCursor(QtGui.QTextCursor.MoveOperation.End)\n\n    def getCommand(self, *, strip=True):\n        doc = self.document()\n        curr_line = doc.findBlockByLineNumber(doc.lineCount() - 1).text()\n        if strip:\n            curr_line = curr_line.rstrip()\n        curr_line = curr_line[len(sys.ps1):]\n        return curr_line\n\n    def setCommand(self, command):\n        if self.getCommand() == command:\n            return\n\n        doc = self.document()\n        curr_line = doc.findBlockByLineNumber(doc.lineCount() - 1).text()\n        self.moveCursor(QtGui.QTextCursor.MoveOperation.End)\n        for i in range(len(curr_line) - len(sys.ps1)):\n            self.moveCursor(QtGui.QTextCursor.MoveOperation.Left, QtGui.QTextCursor.MoveMode.KeepAnchor)\n\n        self.textCursor().removeSelectedText()\n        self.textCursor().insertText(command)\n        self.moveCursor(QtGui.QTextCursor.MoveOperation.End)\n\n    def show_completions(self, completions):\n        if self.completions_visible:\n            self.hide_completions()\n\n        c = self.textCursor()\n        c.setPosition(self.completions_pos)\n\n        completions = map(lambda x: x.split('.')[-1], completions)\n        t = '\\n' + ' '.join(completions)\n        if len(t) > 500:\n            t = t[:500] + '...'\n        c.insertText(t)\n        self.completions_end = c.position()\n\n        self.moveCursor(QtGui.QTextCursor.MoveOperation.End)\n        self.completions_visible = True\n\n    def hide_completions(self):\n        if not self.completions_visible:\n            return\n        c = self.textCursor()\n        c.setPosition(self.completions_pos)\n        l = self.completions_end - self.completions_pos\n        for x in range(l): c.deleteChar()\n\n        self.moveCursor(QtGui.QTextCursor.MoveOperation.End)\n        self.completions_visible = False\n\n    def getConstruct(self, command):\n        if self.construct:\n            self.construct.append(command)\n            if not command:\n                ret_val = '\\n'.join(self.construct)\n                self.construct = []\n                return ret_val\n            else:\n                return ''\n        else:\n            if command and command[-1] == (':'):\n                self.construct.append(command)\n                return ''\n            else:\n                return command\n\n    def addToHistory(self, command):\n        if not self.construct and command[0:1] == ' ':\n            return\n\n        if command and (not self.history or self.history[-1] != command):\n            while len(self.history) >= 50:\n                self.history.remove(self.history[0])\n            self.history.append(command)\n        self.history_index = len(self.history)\n\n    def getPrevHistoryEntry(self):\n        if self.history:\n            self.history_index = max(0, self.history_index - 1)\n            return self.history[self.history_index]\n        return ''\n\n    def getNextHistoryEntry(self):\n        if self.history:\n            hist_len = len(self.history)\n            self.history_index = min(hist_len, self.history_index + 1)\n            if self.history_index < hist_len:\n                return self.history[self.history_index]\n        return ''\n\n    def getCursorPosition(self):\n        c = self.textCursor()\n        return c.position() - c.block().position() - len(sys.ps1)\n\n    def setCursorPosition(self, position):\n        self.moveCursor(QtGui.QTextCursor.MoveOperation.StartOfLine)\n        for i in range(len(sys.ps1) + position):\n            self.moveCursor(QtGui.QTextCursor.MoveOperation.Right)\n\n    def run_command(self):\n        command = self.getCommand()\n        self.addToHistory(command)\n\n        command = self.getConstruct(command)\n\n        if command:\n            self._exec_command(command)\n        self.newPrompt('')\n        self.set_json(False)\n\n    def _exec_command(self, command):\n        tmp_stdout = sys.stdout\n        taint_reports_by_console_usage()\n\n        class StdoutProxy:\n            def __init__(self, write_func):\n                self.write_func = write_func\n                self.skip = False\n\n            def flush(self):\n                pass\n\n            def write(self, text):\n                if not self.skip:\n                    stripped_text = text.rstrip('\\n')\n                    self.write_func(stripped_text)\n                    QtCore.QCoreApplication.processEvents()\n                self.skip = not self.skip\n\n        if type(self.namespace.get(command)) == type(lambda: None):\n            self.appendPlainText(\"'{}' is a function. Type '{}()' to use it in the Python console.\"\n                                 .format(command, command))\n            return\n\n        sys.stdout = StdoutProxy(self.appendPlainText)\n        try:\n            try:\n                # eval is generally considered bad practice. use it wisely!\n                result = eval(command, self.namespace, self.namespace)\n                if result is not None:\n                    if self.is_json:\n                        util.print_msg(util.json_encode(result))\n                    else:\n                        self.appendPlainText(repr(result))\n            except SyntaxError:\n                # exec is generally considered bad practice. use it wisely!\n                exec(command, self.namespace, self.namespace)\n        except SystemExit:\n            self.close()\n        except BaseException as e:\n            te = traceback.TracebackException.from_exception(e)\n            # rm part of traceback mentioning this file.\n            # (note: we rm stack items before converting to str, instead of removing lines from the str,\n            #        as this is more reliable. The latter would differ whether the traceback has source text lines,\n            #        which is not always the case.)\n            te.stack = traceback.StackSummary.from_list(te.stack[1:])\n            tb_str = \"\".join(te.format())\n            # rm last linebreak:\n            if tb_str.endswith(\"\\n\"):\n                tb_str = tb_str[:-1]\n            self.appendPlainText(tb_str)\n        sys.stdout = tmp_stdout\n\n    def keyPressEvent(self, event):\n        if event.key() == Qt.Key.Key_Tab:\n            self.completions()\n            return\n\n        self.hide_completions()\n\n        if event.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return):\n            self.run_command()\n            return\n        if event.key() == Qt.Key.Key_Home:\n            self.setCursorPosition(0)\n            return\n        if event.key() == Qt.Key.Key_PageUp:\n            return\n        elif event.key() in (Qt.Key.Key_Left, Qt.Key.Key_Backspace):\n            if self.getCursorPosition() == 0:\n                return\n        elif event.key() == Qt.Key.Key_Up:\n            self.setCommand(self.getPrevHistoryEntry())\n            return\n        elif event.key() == Qt.Key.Key_Down:\n            self.setCommand(self.getNextHistoryEntry())\n            return\n        elif event.key() == Qt.Key.Key_L and event.modifiers() == Qt.KeyboardModifier.ControlModifier:\n            self.clear()\n        elif event.key() == Qt.Key.Key_C and event.modifiers() == Qt.KeyboardModifier.ControlModifier:\n            if not self.textCursor().selectedText():\n                self.keyboard_interrupt()\n        elif event.key() == Qt.Key.Key_Plus and Qt.KeyboardModifier.ControlModifier in event.modifiers():\n            self.set_font_size(self.font_size + 1)\n            return\n        elif event.key() == Qt.Key.Key_Minus and Qt.KeyboardModifier.ControlModifier in event.modifiers():\n            self.set_font_size(self.font_size - 1)\n            return\n\n        super(Console, self).keyPressEvent(event)\n\n    def completions(self):\n        cmd = self.getCommand()\n        # note for regex: new words start after ' ' or '(' or ')'\n        lastword = re.split(r'[ ()]', cmd)[-1]\n        beginning = cmd[0:-len(lastword)]\n\n        path = lastword.split('.')\n        prefix = '.'.join(path[:-1])\n        prefix = (prefix + '.') if prefix else prefix\n        ns = self.namespace.keys()\n\n        if len(path) == 1:\n            ns = ns\n        else:\n            assert len(path) > 1\n            obj = self.namespace.get(path[0])\n            try:\n                for attr in path[1:-1]:\n                    obj = getattr(obj, attr)\n            except AttributeError:\n                ns = []\n            else:\n                ns = dir(obj)\n\n        completions = []\n        for name in ns:\n            if name[0] == '_':continue\n            if name.startswith(path[-1]):\n                completions.append(prefix+name)\n        completions.sort()\n\n        if not completions:\n            self.hide_completions()\n        elif len(completions) == 1:\n            self.hide_completions()\n            self.setCommand(beginning + completions[0])\n        else:\n            # find common prefix\n            p = os.path.commonprefix(completions)\n            if len(p)>len(lastword):\n                self.hide_completions()\n                self.setCommand(beginning + p)\n            else:\n                self.show_completions(completions)\n"
  },
  {
    "path": "electrum/gui/qt/contact_list.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport enum\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtGui import QStandardItemModel, QStandardItem\nfrom PyQt6.QtCore import Qt, QPersistentModelIndex, QModelIndex\nfrom PyQt6.QtWidgets import (QAbstractItemView, QMenu)\n\nfrom electrum.i18n import _\nfrom electrum.bitcoin import is_address\nfrom electrum.util import block_explorer_URL\nfrom electrum.plugin import run_hook\nfrom electrum.gui.qt.util import read_QIcon\n\nfrom .util import webopen\nfrom .my_treeview import MyTreeView\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nclass ContactList(MyTreeView):\n\n    class Columns(MyTreeView.BaseColumnsEnum):\n        NAME = enum.auto()\n        ADDRESS = enum.auto()\n\n    headers = {\n        Columns.NAME: _('Name'),\n        Columns.ADDRESS: _('Address'),\n    }\n    filter_columns = [Columns.NAME, Columns.ADDRESS]\n\n    ROLE_CONTACT_KEY = Qt.ItemDataRole.UserRole + 1000\n    key_role = ROLE_CONTACT_KEY\n\n    def __init__(self, main_window: 'ElectrumWindow'):\n        super().__init__(\n            main_window=main_window,\n            stretch_column=self.Columns.ADDRESS,\n            editable_columns=[self.Columns.NAME],\n        )\n        self.setModel(QStandardItemModel(self))\n        self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)\n        self.setSortingEnabled(True)\n        self.std_model = self.model()\n        self.update()\n\n    def on_edited(self, idx, edit_key, *, text):\n        _type, prior_name = self.main_window.contacts.pop(edit_key)\n        self.main_window.set_contact(text, edit_key)\n        self.update()\n\n    def create_menu(self, position):\n        menu = QMenu()\n        idx = self.indexAt(position)\n        column = idx.column() or self.Columns.NAME\n        selected_keys = []\n        for s_idx in self.selected_in_column(self.Columns.NAME):\n            sel_key = self.model().itemFromIndex(s_idx).data(self.ROLE_CONTACT_KEY)\n            selected_keys.append(sel_key)\n        if selected_keys and idx.isValid():\n            column_title = self.model().horizontalHeaderItem(column).text()\n            column_data = '\\n'.join(self.model().itemFromIndex(s_idx).text()\n                                    for s_idx in self.selected_in_column(column))\n            menu.addAction(_(\"Copy {}\").format(column_title), lambda: self.place_text_on_clipboard(column_data, title=column_title))\n            if column in self.editable_columns:\n                item = self.model().itemFromIndex(idx)\n                if item.isEditable():\n                    # would not be editable if openalias\n                    persistent = QPersistentModelIndex(idx)\n                    menu.addAction(_(\"Edit {}\").format(column_title), lambda p=persistent: self.edit(QModelIndex(p)))\n            menu.addAction(_(\"Pay to\"), lambda: self.main_window.payto_contacts(selected_keys))\n            menu.addAction(_(\"Delete\"), lambda: self.main_window.delete_contacts(selected_keys))\n            URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, selected_keys)]\n            if URLs:\n                menu.addAction(_(\"View on block explorer\"), lambda: [webopen(u) for u in URLs])\n\n        run_hook('create_contact_menu', menu, selected_keys)\n        self.open_menu(menu, position)\n\n    def update(self):\n        if self.maybe_defer_update():\n            return\n        current_key = self.get_role_data_for_current_item(col=self.Columns.NAME, role=self.ROLE_CONTACT_KEY)\n        self.model().clear()\n        self.update_headers(self.__class__.headers)\n        set_current = None\n        for key in sorted(self.main_window.contacts.keys()):\n            contact_type, name = self.main_window.contacts[key]\n            labels = [\"\"] * len(self.Columns)\n            labels[self.Columns.NAME] = name\n            labels[self.Columns.ADDRESS] = key\n            items = [QStandardItem(x) for x in labels]\n            items[self.Columns.NAME].setEditable(contact_type != 'openalias')\n            items[self.Columns.ADDRESS].setEditable(False)\n            items[self.Columns.NAME].setData(key, self.ROLE_CONTACT_KEY)\n            items[self.Columns.NAME].setIcon(\n                read_QIcon(\"lightning\" if contact_type == 'lnaddress' else \"bitcoin\")\n            )\n            row_count = self.model().rowCount()\n            self.model().insertRow(row_count, items)\n            if key == current_key:\n                idx = self.model().index(row_count, self.Columns.NAME)\n                set_current = QPersistentModelIndex(idx)\n        self.set_current_idx(set_current)\n        # FIXME refresh loses sort order; so set \"default\" here:\n        self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)\n        self.filter()\n        run_hook('update_contacts_tab', self)\n\n    def refresh_row(self, key, row):\n        # nothing to update here\n        pass\n\n    def get_edit_key_from_coordinate(self, row, col):\n        if col != self.Columns.NAME:\n            return None\n        return self.get_role_data_from_coordinate(row, col, role=self.ROLE_CONTACT_KEY)\n\n    def create_toolbar(self, config):\n        toolbar, menu = self.create_toolbar_with_menu('')\n        menu.addAction(_(\"&New contact\"), self.main_window.new_contact_dialog)\n        menu.addAction(_(\"Import\"), lambda: self.main_window.import_contacts())\n        menu.addAction(_(\"Export\"), lambda: self.main_window.export_contacts())\n        return toolbar\n"
  },
  {
    "path": "electrum/gui/qt/custom_model.py",
    "content": "# loosely based on\n# http://trevorius.com/scrapbook/uncategorized/pyqt-custom-abstractitemmodel/\n\nfrom PyQt6 import QtCore\n\n\nclass CustomNode:\n\n    def __init__(self, model: 'CustomModel', data):\n        self.model = model\n        self._data = data\n        self._children = []\n        self._parent = None\n        self._row = 0\n\n    def get_data(self):\n        return self._data\n\n    def get_data_for_role(self, index, role):\n        # define in child class\n        raise NotImplementedError()\n\n    def childCount(self):\n        return len(self._children)\n\n    def child(self, row):\n        if row >= 0 and row < self.childCount():\n            return self._children[row]\n\n    def parent(self):\n        return self._parent\n\n    def row(self):\n        return self._row\n\n    def addChild(self, child):\n        child._parent = self\n        child._row = len(self._children)\n        self._children.append(child)\n\n\nclass CustomModel(QtCore.QAbstractItemModel):\n\n    def __init__(self, parent, columncount):\n        QtCore.QAbstractItemModel.__init__(self, parent)\n        self._root = CustomNode(self, None)\n        self._columncount = columncount\n\n    def rowCount(self, index):\n        if index.isValid():\n            return index.internalPointer().childCount()\n        return self._root.childCount()\n\n    def columnCount(self, index):\n        return self._columncount\n\n    def addChild(self, node, _parent):\n        if not _parent or not _parent.isValid():\n            parent = self._root\n        else:\n            parent = _parent.internalPointer()\n        parent.addChild(self, node)\n\n    def index(self, row, column, _parent=None):\n        # Performance-critical function\n\n        if not _parent or not _parent.isValid():\n            parent = self._root\n        else:\n            parent = _parent.internalPointer()\n\n        # Open-coded\n        #   if not QtCore.QAbstractItemModel.hasIndex(self, row, column, _parent):\n        # the implementation is equivalent but it's in C++,\n        # so VM entries take up inordinate amounts of time (up to 25% of refresh()):\n        if row < 0 or column < 0 or row >= self.rowCount(_parent) or column >= self._columncount:\n            return QtCore.QModelIndex()\n\n        child = parent.child(row)\n        if child:\n            return QtCore.QAbstractItemModel.createIndex(self, row, column, child)\n        else:\n            return QtCore.QModelIndex()\n\n    def parent(self, index):\n        if index.isValid():\n            node = index.internalPointer()\n            if node:\n                p = node.parent()\n                if p:\n                    return QtCore.QAbstractItemModel.createIndex(self, p.row(), 0, p)\n            else:\n                return QtCore.QModelIndex()\n        return QtCore.QModelIndex()\n\n    def data(self, index, role):\n        if not index.isValid():\n            return None\n        node = index.internalPointer()\n        return node.get_data_for_role(index, role)\n"
  },
  {
    "path": "electrum/gui/qt/exception_window.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport sys\nimport html\nfrom typing import TYPE_CHECKING, Optional, Set\n\nfrom PyQt6.QtCore import QObject, Qt\nimport PyQt6.QtCore as QtCore\nfrom PyQt6.QtWidgets import (QWidget, QLabel, QPushButton, QTextEdit,\n                             QMessageBox, QHBoxLayout, QVBoxLayout, QDialog, QScrollArea)\n\nfrom electrum.i18n import _\nfrom electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue, CrashReportResponse\nfrom electrum.logging import Logger\nfrom electrum import constants\nfrom electrum.network import Network\n\nfrom .util import MessageBoxMixin, read_QIcon, WaitingDialog, font_height\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n    from electrum.wallet import Abstract_Wallet\n\n\nclass Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin, Logger):\n    _active_window = None\n\n    def __init__(self, config: 'SimpleConfig', exctype, value, tb):\n        BaseCrashReporter.__init__(self, exctype, value, tb)\n        self.network = Network.get_instance()\n        self.config = config\n\n        QWidget.__init__(self)\n        self.setWindowTitle('Electrum - ' + _('An Error Occurred'))\n        self.setMinimumSize(600, 300)\n\n        Logger.__init__(self)\n\n        main_box = QVBoxLayout()\n\n        heading = QLabel('<h2>' + BaseCrashReporter.CRASH_TITLE + '</h2>')\n        main_box.addWidget(heading)\n        main_box.addWidget(QLabel(BaseCrashReporter.CRASH_MESSAGE))\n\n        main_box.addWidget(QLabel(BaseCrashReporter.REQUEST_HELP_MESSAGE))\n\n        self._report_contents_dlg = None  # type: Optional[ReportContentsDialog]\n        collapse_info = QPushButton(_(\"Show report contents\"))\n        collapse_info.clicked.connect(lambda _checked: self.show_report_contents_dlg())\n\n        main_box.addWidget(collapse_info)\n\n        main_box.addWidget(QLabel(BaseCrashReporter.DESCRIBE_ERROR_MESSAGE))\n\n        self.description_textfield = QTextEdit()\n        self.description_textfield.setFixedHeight(4 * font_height())\n        self.description_textfield.setPlaceholderText(self.USER_COMMENT_PLACEHOLDER)\n        main_box.addWidget(self.description_textfield)\n\n        main_box.addWidget(QLabel(BaseCrashReporter.ASK_CONFIRM_SEND))\n\n        buttons = QHBoxLayout()\n\n        report_button = QPushButton(_('Send Bug Report'))\n        report_button.clicked.connect(lambda _checked: self._ask_for_confirm_to_send_report())\n        report_button.setIcon(read_QIcon(\"tab_send.png\"))\n        buttons.addWidget(report_button)\n\n        close_button = QPushButton(_('Not Now'))\n        close_button.clicked.connect(lambda _checked: self.close())\n        buttons.addWidget(close_button)\n\n        main_box.addLayout(buttons)\n\n        # prioritizes the window input over all other windows\n        self.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)\n\n        self.setLayout(main_box)\n        self.show()\n\n    def _ask_for_confirm_to_send_report(self):\n        if self.question(\"Confirm to send bugreport?\"):\n            self.send_report()\n\n    def send_report(self):\n        def on_success(response: CrashReportResponse):\n            text = response.text\n            if response.url:\n                text += f\" You can track further progress on <a href='{response.url}'>GitHub</a>.\"\n            self.show_message(parent=self,\n                              title=_(\"Crash report\"),\n                              msg=text,\n                              rich_text=True)\n            self.close()\n\n        def on_failure(exc_info):\n            e = exc_info[1]\n            self.logger.error('There was a problem with the automatic reporting', exc_info=exc_info)\n            self.show_critical(parent=self,\n                               msg=(_('There was a problem with the automatic reporting:') + '<br/>' +\n                                    repr(e)[:120] + '<br/><br/>' +\n                                    _(\"Please report this issue manually\") +\n                                    f' <a href=\"{constants.GIT_REPO_ISSUES_URL}\">on GitHub</a>.'),\n                               rich_text=True)\n\n        proxy = self.network.proxy\n        task = lambda: BaseCrashReporter.send_report(self, self.network.asyncio_loop, proxy)\n        msg = _('Sending crash report...')\n        WaitingDialog(self, msg, task, on_success, on_failure)\n\n    def on_close(self):\n        Exception_Window._active_window = None\n        self.close()\n\n    def closeEvent(self, event):\n        self.on_close()\n        event.accept()\n\n    def get_user_description(self):\n        return self.description_textfield.toPlainText()\n\n    def get_wallet_type(self):\n        wallet_types = Exception_Hook._INSTANCE.wallet_types_seen\n        return \",\".join(wallet_types)\n\n    def _get_traceback_str_to_display(self) -> str:\n        # The msg_box that shows the report uses rich_text=True, so\n        # if traceback contains special HTML characters, e.g. '<',\n        # they need to be escaped to avoid formatting issues.\n        traceback_str = super()._get_traceback_str_to_display()\n        return html.escape(traceback_str)\n\n    def show_report_contents_dlg(self):\n        if self._report_contents_dlg is None:\n            self._report_contents_dlg = ReportContentsDialog(\n                parent=self,\n                text=self.get_report_string(),\n            )\n        self._report_contents_dlg.show()\n        self._report_contents_dlg.raise_()\n\n\ndef _show_window(*args):\n    if not Exception_Window._active_window:\n        Exception_Window._active_window = Exception_Window(*args)\n\n\nclass Exception_Hook(QObject, Logger):\n    _report_exception = QtCore.pyqtSignal(object, object, object, object)\n\n    _INSTANCE = None  # type: Optional[Exception_Hook]  # singleton\n\n    def __init__(self, *, config: 'SimpleConfig'):\n        QObject.__init__(self)\n        Logger.__init__(self)\n        assert self._INSTANCE is None, \"Exception_Hook is supposed to be a singleton\"\n        self.config = config\n        self.wallet_types_seen = set()  # type: Set[str]\n        self.exception_ids_seen = set()  # type: Set[bytes]\n\n        sys.excepthook = self.handler\n        self._report_exception.connect(_show_window)\n        EarlyExceptionsQueue.set_hook_as_ready()\n\n    @classmethod\n    def maybe_setup(cls, *, config: 'SimpleConfig', wallet: 'Abstract_Wallet' = None) -> None:\n        if not cls._INSTANCE:\n            cls._INSTANCE = Exception_Hook(config=config)\n        if wallet:\n            cls._INSTANCE.wallet_types_seen.add(wallet.wallet_type)\n\n    def handler(self, *exc_info):\n        self.logger.error('exception caught by crash reporter', exc_info=exc_info)\n        groupid_hash = BaseCrashReporter.get_traceback_groupid_hash(*exc_info)\n        if groupid_hash in self.exception_ids_seen:\n            return  # to avoid annoying the user, only show crash reporter once per exception groupid\n        self.exception_ids_seen.add(groupid_hash)\n        self._report_exception.emit(self.config, *exc_info)\n\n\nclass ReportContentsDialog(QDialog):\n\n    def __init__(self, *, parent: QWidget, text: str):\n        QDialog.__init__(self, parent)\n        self.setWindowTitle(_(\"Report contents\"))\n        self.setMinimumSize(800, 500)\n        vbox = QVBoxLayout(self)\n        scroll_area = QScrollArea(self)\n\n        report_text = QLabel(text)\n        report_text.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n        report_text.setTextFormat(Qt.TextFormat.AutoText)  # likely rich text\n\n        scroll_area.setWidget(report_text)\n        vbox.addWidget(scroll_area)\n"
  },
  {
    "path": "electrum/gui/qt/fee_slider.py",
    "content": "import threading\nfrom typing import Callable, Optional\n\nfrom PyQt6.QtGui import QCursor\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtWidgets import QSlider, QToolTip, QComboBox, QWidget\n\nfrom electrum.i18n import _\nfrom electrum.fee_policy import FeeMethod, FeePolicy\nfrom electrum.network import Network\n\n\nclass FeeComboBox(QComboBox):\n\n    def __init__(self, fee_slider: 'FeeSlider'):\n        QComboBox.__init__(self)\n        self.fee_slider = fee_slider\n        self.addItems([x.name_for_GUI() for x in FeeMethod.slider_values()])\n        index = FeeMethod.slider_index_of_method(self.fee_slider.fee_policy.method)\n        self.setCurrentIndex(index)\n        self.currentIndexChanged.connect(self.on_fee_type)\n        self.help_msg = '\\n'.join([\n            _('Feerate: the fee slider uses static feerate values'),\n            _('ETA: fee rate is based on average confirmation time estimates'),\n            _('Mempool based: fee rate is targeting a depth in the memory pool')\n            ]\n        )\n\n    def on_fee_type(self, x):\n        method = FeeMethod.slider_values()[x]\n        self.fee_slider.fee_policy.set_method(method)\n        self.fee_slider.update(is_initialized=True)\n\n\nclass FeeSlider(QSlider):\n\n    def __init__(\n        self,\n        *,\n        parent: Optional[QWidget],\n        network: Network,\n        fee_policy: FeePolicy,\n        callback: Callable[[Optional[int]], None],\n    ):\n        QSlider.__init__(self, Qt.Orientation.Horizontal, parent=parent)\n        self.network = network\n        self.callback = callback\n        self.fee_policy = fee_policy\n        self.lock = threading.RLock()\n        self.update(is_initialized=False)\n        self.valueChanged.connect(self.moved)\n        self._active = True\n\n    @property\n    def dyn(self) -> bool:\n        return self.fee_policy.use_dynamic_estimates\n\n    def get_policy(self) -> FeePolicy:\n        return self.fee_policy\n\n    def moved(self, pos):\n        with self.lock:\n            if self.fee_policy.method == FeeMethod.FIXED:\n                return\n            self.fee_policy.set_value_from_slider_pos(pos)\n            fee_rate = self.fee_policy.fee_per_kb(self.network)\n            tooltip = self.fee_policy.get_tooltip(self.network)\n            QToolTip.showText(QCursor.pos(), tooltip, self)\n            self.setToolTip(tooltip)\n            self.callback(fee_rate)\n\n    def update(self, *, is_initialized: bool = True):\n        with self.lock:\n            if self.fee_policy.method == FeeMethod.FIXED:\n                return\n            pos = self.fee_policy.get_slider_pos()\n            maxp = self.fee_policy.get_slider_max()\n            self.setRange(0, maxp)\n            self.setValue(pos)\n            if is_initialized:\n                self.moved(pos)\n\n    def activate(self):\n        self._active = True\n        self.setStyleSheet('')\n\n    def deactivate(self):\n        self._active = False\n        # TODO it would be nice to find a platform-independent solution\n        # that makes the slider look as if it was disabled\n        self.setStyleSheet(\n            \"\"\"\n            QSlider::groove:horizontal {\n                border: 1px solid #999999;\n                height: 8px;\n                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #B1B1B1, stop:1 #B1B1B1);\n                margin: 2px 0;\n            }\n\n            QSlider::handle:horizontal {\n                background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #b4b4b4, stop:1 #8f8f8f);\n                border: 1px solid #5c5c5c;\n                width: 12px;\n                margin: -2px 0;\n                border-radius: 3px;\n            }\n            \"\"\"\n        )\n\n    def is_active(self):\n        return self._active\n"
  },
  {
    "path": "electrum/gui/qt/history_list.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport os\nimport time\nimport datetime\nfrom datetime import date\nfrom typing import TYPE_CHECKING, Tuple, Dict, Any\nimport threading\nimport enum\nfrom decimal import Decimal\n\nfrom PyQt6.QtGui import QFont, QBrush, QColor\nfrom PyQt6.QtCore import (Qt, QPersistentModelIndex, QModelIndex,\n                          QSortFilterProxyModel, QVariant, QItemSelectionModel, QDate, QPoint)\nfrom PyQt6.QtWidgets import (QMenu, QHeaderView, QLabel, QPushButton, QComboBox, QVBoxLayout, QCalendarWidget,\n                             QGridLayout)\n\nfrom electrum.gui import messages\nfrom electrum.address_synchronizer import TX_HEIGHT_LOCAL\nfrom electrum.i18n import _\nfrom electrum.util import (block_explorer_URL, profiler, TxMinedInfo,\n                           OrderedDictWithIndex, timestamp_to_datetime,\n                           Satoshis, format_time)\nfrom electrum.logging import get_logger, Logger\nfrom electrum.simple_config import SimpleConfig\n\nfrom .custom_model import CustomNode, CustomModel\nfrom .util import (read_QIcon, MONOSPACE_FONT, Buttons, CancelButton, OkButton,\n                   filename_field, AcceptFileDragDrop, WindowModalDialog,\n                   CloseButton, webopen, WWLabel)\nfrom .my_treeview import MyTreeView\n\nif TYPE_CHECKING:\n    from electrum.wallet import Abstract_Wallet\n    from .main_window import ElectrumWindow\n\n\n_logger = get_logger(__name__)\n\n\nTX_ICONS = [\n    \"unconfirmed.png\",\n    \"warning.png\",\n    \"offline_tx.png\",\n    \"offline_tx.png\",\n    \"clock1.png\",\n    \"clock2.png\",\n    \"clock3.png\",\n    \"clock4.png\",\n    \"clock5.png\",\n    \"confirmed.png\",\n]\n\n\nclass HistorySortModel(QSortFilterProxyModel):\n\n    def data_for(self, index: QModelIndex):\n        col = index.column()\n        if col == HistoryColumns.STATUS:\n            # respect sort order of self.transactions (wallet.get_full_history)\n            return index.row()\n        else:\n            node = index.internalPointer()\n            return node.sort_keys[col]\n\n    def lessThan(self, source_left: QModelIndex, source_right: QModelIndex):\n        return self.data_for(source_left) < self.data_for(source_right)\n\n\ndef get_item_key(tx_item):\n    return tx_item.get('txid') or tx_item['payment_hash']\n\ndef flatten_sort_key(v):\n    if v is None or isinstance(v, Decimal) and v.is_nan():\n        return -float(\"inf\")\n    else:\n        return v\n\n\nclass HistoryNode(CustomNode):\n\n    model: 'HistoryModel'\n\n    def __init__(self, model: 'CustomModel', tx_item):\n        super().__init__(model, tx_item)\n\n        if tx_item is None:\n            tx_item = {}\n        is_lightning = tx_item.get('lightning', False)\n        short_id = \"\"\n        if not is_lightning:\n            txpos_in_block = tx_item.get('txpos_in_block') or -1\n            if txpos_in_block >= 0:\n                short_id = f\"{tx_item['height']}x{txpos_in_block}\"\n        self.sort_keys = {\n            HistoryColumns.DESCRIPTION: flatten_sort_key(\n                tx_item.get('label')),\n            HistoryColumns.AMOUNT: flatten_sort_key(\n                (tx_item['bc_value'].value if 'bc_value' in tx_item else 0)\\\n                    + (tx_item['ln_value'].value if 'ln_value' in tx_item else 0)),\n            HistoryColumns.BALANCE: 0,\n            HistoryColumns.FIAT_VALUE: flatten_sort_key(\n                tx_item['fiat_value'].value if 'fiat_value' in tx_item else None),\n            HistoryColumns.FIAT_ACQ_PRICE: flatten_sort_key(\n                tx_item['acquisition_price'].value if 'acquisition_price' in tx_item else None),\n            HistoryColumns.FIAT_CAP_GAINS: flatten_sort_key(\n                tx_item['capital_gain'].value if 'capital_gain' in tx_item else None),\n            HistoryColumns.TXID: flatten_sort_key(\n                tx_item.get('txid') if not is_lightning else None),\n            HistoryColumns.SHORT_ID:\n                short_id,\n        }\n\n    def set_balance(self, balance):\n        self._data['balance'] = Satoshis(balance)\n        self.sort_keys[HistoryColumns.BALANCE] = balance\n\n    def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVariant:\n        assert index.isValid()\n        col = index.column()\n        window = self.model.window\n        tx_item = self.get_data()\n        is_lightning = tx_item.get('lightning', False)\n        if not is_lightning and 'txid' not in tx_item:\n            # this may happen if two lightning tx have the same group id\n            # and the group does not have an onchain tx\n            is_lightning = True\n        timestamp = tx_item['timestamp']\n        if is_lightning:\n            status = 0\n            if timestamp is None:\n                status_str = 'unconfirmed'\n            else:\n                status_str = format_time(int(timestamp))\n        else:\n            tx_hash = tx_item['txid']\n            conf = tx_item['confirmations']\n            try:\n                status, status_str = self.model.tx_status_cache[tx_hash]\n            except KeyError:\n                tx_mined_info = self.model._tx_mined_info_from_tx_item(tx_item)\n                status, status_str = window.wallet.get_tx_status(tx_hash, tx_mined_info)\n\n        if role == MyTreeView.ROLE_EDIT_KEY:\n            return QVariant(get_item_key(tx_item))\n        if role not in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole, MyTreeView.ROLE_CLIPBOARD_DATA):\n            if col == HistoryColumns.STATUS and role == Qt.ItemDataRole.DecorationRole:\n                icon = \"lightning\" if is_lightning else TX_ICONS[status]\n                return QVariant(read_QIcon(icon))\n            elif col == HistoryColumns.STATUS and role == Qt.ItemDataRole.ToolTipRole:\n                if is_lightning:\n                    msg = 'lightning transaction'\n                else:  # on-chain\n                    if tx_item['height'] == TX_HEIGHT_LOCAL:\n                        # note: should we also explain double-spends?\n                        msg = _(\"This transaction is only available on your local machine.\\n\"\n                                \"The currently connected server does not know about it.\\n\"\n                                \"You can either broadcast it now, or simply remove it.\")\n                    else:\n                        msg = str(conf) + _(\" confirmation\" + (\"s\" if conf != 1 else \"\"))\n                return QVariant(msg)\n            elif col > HistoryColumns.DESCRIPTION and role == Qt.ItemDataRole.TextAlignmentRole:\n                return QVariant(int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter))\n            elif col > HistoryColumns.DESCRIPTION and role == Qt.ItemDataRole.FontRole:\n                monospace_font = QFont(MONOSPACE_FONT)\n                return QVariant(monospace_font)\n            #elif col == HistoryColumns.DESCRIPTION and role == Qt.ItemDataRole.DecorationRole and not is_lightning\\\n            #        and self.parent.wallet.invoices.paid.get(tx_hash):\n            #    return QVariant(read_QIcon(\"seal\"))\n            elif col in (HistoryColumns.DESCRIPTION, HistoryColumns.AMOUNT) \\\n                    and role == Qt.ItemDataRole.ForegroundRole and tx_item['value'].value < 0:\n                red_brush = QBrush(QColor(\"#BC1E1E\"))\n                return QVariant(red_brush)\n            elif col == HistoryColumns.FIAT_VALUE and role == Qt.ItemDataRole.ForegroundRole \\\n                    and not tx_item.get('fiat_default') and tx_item.get('fiat_value') is not None:\n                blue_brush = QBrush(QColor(\"#1E1EFF\"))\n                return QVariant(blue_brush)\n            return QVariant()\n\n        add_thousands_sep = None\n        whitespaces = True\n        if role == MyTreeView.ROLE_CLIPBOARD_DATA:\n            add_thousands_sep = False\n            whitespaces = False\n\n        if col == HistoryColumns.STATUS:\n            return QVariant(status_str)\n        elif col == HistoryColumns.DESCRIPTION and 'label' in tx_item:\n            return QVariant(tx_item['label'])\n        elif col == HistoryColumns.AMOUNT:\n            bc_value = tx_item['bc_value'].value if 'bc_value' in tx_item else 0\n            ln_value = tx_item['ln_value'].value if 'ln_value' in tx_item else 0\n            value = bc_value + ln_value\n            v_str = window.format_amount(value, is_diff=True, whitespaces=whitespaces, add_thousands_sep=add_thousands_sep)\n            return QVariant(v_str)\n        elif col == HistoryColumns.BALANCE:\n            balance = tx_item['balance'].value if 'balance' in tx_item else None\n            balance_str = window.format_amount(balance, whitespaces=whitespaces, add_thousands_sep=add_thousands_sep) if balance is not None else ''\n            return QVariant(balance_str)\n        elif col == HistoryColumns.FIAT_VALUE and 'fiat_value' in tx_item:\n            value_str = window.fx.format_fiat(tx_item['fiat_value'].value, add_thousands_sep=add_thousands_sep)\n            return QVariant(value_str)\n        elif col == HistoryColumns.FIAT_ACQ_PRICE and \\\n                tx_item['value'].value < 0 and 'acquisition_price' in tx_item:\n            # fixme: should use is_mine\n            acq = tx_item['acquisition_price'].value\n            return QVariant(window.fx.format_fiat(acq, add_thousands_sep=add_thousands_sep))\n        elif col == HistoryColumns.FIAT_CAP_GAINS and 'capital_gain' in tx_item:\n            cg = tx_item['capital_gain'].value\n            return QVariant(window.fx.format_fiat(cg, add_thousands_sep=add_thousands_sep))\n        elif col == HistoryColumns.TXID:\n            return QVariant(tx_hash) if not is_lightning else QVariant('')\n        elif col == HistoryColumns.SHORT_ID:\n            return QVariant(self.sort_keys[HistoryColumns.SHORT_ID])\n        return QVariant()\n\n\nclass HistoryModel(CustomModel, Logger):\n\n    def __init__(self, window: 'ElectrumWindow'):\n        CustomModel.__init__(self, window, len(HistoryColumns))\n        Logger.__init__(self)\n        self.window = window\n        self.view = None  # type: HistoryList\n        self.transactions = OrderedDictWithIndex()\n        self.tx_status_cache = {}  # type: Dict[str, Tuple[int, str]]\n\n    def set_view(self, history_list: 'HistoryList'):\n        # FIXME HistoryModel and HistoryList mutually depend on each other.\n        # After constructing both, this method needs to be called.\n        self.view = history_list  # type: HistoryList\n        self.set_visibility_of_columns()\n\n    def update_label(self, index):\n        tx_item = index.internalPointer().get_data()\n        tx_item['label'] = self.window.wallet.get_label_for_txid(\n            get_item_key(tx_item))  # FIXME get_item_key might return an RHASH, but we call get_label_for_txid?!\n        topLeft = bottomRight = self.createIndex(index.row(), HistoryColumns.DESCRIPTION)\n        self.dataChanged.emit(topLeft, bottomRight, [Qt.ItemDataRole.DisplayRole])\n        self.window.utxo_list.update()\n\n    def get_domain(self):\n        \"\"\"Overridden in address_dialog.py\"\"\"\n        return None\n\n    def should_include_lightning_payments(self) -> bool:\n        \"\"\"Overridden in address_dialog.py\"\"\"\n        return True\n\n    def should_show_fiat(self):\n        if not self.window.config.FX_HISTORY_RATES:\n            return False\n        fx = self.window.fx\n        if not fx or not fx.is_enabled():\n            return False\n        return fx.has_history()\n\n    def should_show_capital_gains(self):\n        return self.should_show_fiat() and self.window.config.FX_HISTORY_RATES_CAPITAL_GAINS\n\n    @profiler\n    def refresh(self, reason: str):\n        self.logger.info(f\"refreshing... reason: {reason}\")\n        assert self.window.gui_thread == threading.current_thread(), 'must be called from GUI thread'\n        assert self.view, 'view not set'\n        if self.view.maybe_defer_update():\n            return\n        selected = self.view.selectionModel().currentIndex()\n        selected_row = None\n        if selected:\n            selected_row = selected.row()\n        fx = self.window.fx\n        if fx:\n            fx.history_used_spot = False\n        wallet = self.window.wallet\n        self.set_visibility_of_columns()\n        transactions = wallet.get_full_history(\n            fx=self.window.fx if self.should_show_fiat() else None,\n            onchain_domain=self.get_domain(),\n            include_lightning=self.should_include_lightning_payments(),\n        )\n        old_length = self._root.childCount()\n        if old_length != 0:\n            self.beginRemoveRows(QModelIndex(), 0, old_length)\n            self.transactions.clear()\n            self._root = HistoryNode(self, None)\n            self.endRemoveRows()\n        parents = {}\n        for tx_item in transactions.values():\n            node = HistoryNode(self, tx_item)\n            self._root.addChild(node)\n            for child_item in tx_item.get('children', []):\n                child_node = HistoryNode(self, child_item)\n                # add child to parent\n                node.addChild(child_node)\n\n        # compute balance once all children have been added\n        balance = 0\n        for node in self._root._children:\n            balance += node._data['value'].value\n            node.set_balance(balance)\n\n        # update tx_status_cache  (before endInsertRows() triggers get_data_for_role() calls)\n        self.tx_status_cache.clear()\n        for txid, tx_item in transactions.items():\n            if not tx_item.get('lightning', False):\n                tx_mined_info = self._tx_mined_info_from_tx_item(tx_item)\n                self.tx_status_cache[txid] = self.window.wallet.get_tx_status(txid, tx_mined_info)\n\n        new_length = self._root.childCount()\n        self.beginInsertRows(QModelIndex(), 0, new_length-1)\n        self.transactions = transactions\n        self.endInsertRows()\n\n        if selected_row:\n            self.view.selectionModel().select(\n                self.createIndex(selected_row, 0),\n                QItemSelectionModel.SelectionFlag.Rows | QItemSelectionModel.SelectionFlag.SelectCurrent)\n        self.view.filter()\n        # update time filter\n        if not self.view.years and self.transactions:\n            start_date = date.today()\n            end_date = date.today()\n            if len(self.transactions) > 0:\n                start_date = self.transactions.value_from_pos(0).get('date') or start_date\n                end_date = self.transactions.value_from_pos(len(self.transactions) - 1).get('date') or end_date\n            self.view.years = [str(i) for i in range(start_date.year, end_date.year + 1)]\n            self.view.period_combo.insertItems(1, self.view.years)\n        # update counter\n        num_tx = len(self.transactions)\n        if self.view:\n            self.view.num_tx_label.setText(_(\"{} transactions\").format(num_tx))\n\n    def set_visibility_of_columns(self):\n        def set_visible(col: int, b: bool):\n            self.view.showColumn(col) if b else self.view.hideColumn(col)\n\n        # txid\n        set_visible(HistoryColumns.TXID, False)\n        set_visible(HistoryColumns.SHORT_ID, False)\n        # fiat\n        history = self.should_show_fiat()\n        cap_gains = self.should_show_capital_gains()\n        set_visible(HistoryColumns.FIAT_VALUE, history)\n        set_visible(HistoryColumns.FIAT_ACQ_PRICE, history and cap_gains)\n        set_visible(HistoryColumns.FIAT_CAP_GAINS, history and cap_gains)\n\n    def update_fiat(self, idx):\n        tx_item = idx.internalPointer().get_data()\n        txid = tx_item['txid']\n        fee = tx_item.get('fee')\n        value = tx_item['value'].value\n        fiat_fields = self.window.wallet.get_tx_item_fiat(\n            tx_hash=txid, amount_sat=value, fx=self.window.fx, tx_fee=fee.value if fee else None)\n        tx_item.update(fiat_fields)\n        self.dataChanged.emit(idx, idx, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.ForegroundRole])\n\n    def update_tx_mined_status(self, tx_hash: str, tx_mined_info: TxMinedInfo):\n        try:\n            row = self.transactions.pos_from_key(tx_hash)\n            tx_item = self.transactions[tx_hash]\n        except KeyError:\n            return\n        self.tx_status_cache[tx_hash] = self.window.wallet.get_tx_status(tx_hash, tx_mined_info)\n        tx_item.update({\n            'confirmations':  tx_mined_info.conf,\n            'timestamp':      tx_mined_info.timestamp,\n            'txpos_in_block': tx_mined_info.txpos,\n            'date':           timestamp_to_datetime(tx_mined_info.timestamp),\n        })\n        topLeft = self.createIndex(row, 0)\n        bottomRight = self.createIndex(row, len(HistoryColumns) - 1)\n        self.dataChanged.emit(topLeft, bottomRight)\n\n    def on_fee_histogram(self):\n        for tx_hash, tx_item in list(self.transactions.items()):\n            if tx_item.get('lightning'):\n                continue\n            tx_mined_info = self._tx_mined_info_from_tx_item(tx_item)\n            if tx_mined_info.conf > 0:\n                # note: we could actually break here if we wanted to rely on the order of txns in self.transactions\n                continue\n            self.update_tx_mined_status(tx_hash, tx_mined_info)\n\n    def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole):\n        assert orientation == Qt.Orientation.Horizontal\n        if role != Qt.ItemDataRole.DisplayRole:\n            return None\n        fx = self.window.fx\n        fiat_title = 'n/a fiat value'\n        fiat_acq_title = 'n/a fiat acquisition price'\n        fiat_cg_title = 'n/a fiat capital gains'\n        if self.should_show_fiat():\n            fiat_title = '%s ' % fx.ccy + _('Value')\n            fiat_acq_title = '%s ' % fx.ccy + _('Acquisition price')\n            fiat_cg_title = '%s ' % fx.ccy + _('Capital Gains')\n        return {\n            HistoryColumns.STATUS: _('Date'),\n            HistoryColumns.DESCRIPTION: _('Description'),\n            HistoryColumns.AMOUNT: _('Amount'),\n            HistoryColumns.BALANCE: _('Balance'),\n            HistoryColumns.FIAT_VALUE: fiat_title,\n            HistoryColumns.FIAT_ACQ_PRICE: fiat_acq_title,\n            HistoryColumns.FIAT_CAP_GAINS: fiat_cg_title,\n            HistoryColumns.TXID: 'TXID',\n            HistoryColumns.SHORT_ID: 'Short ID',\n        }[section]\n\n    def flags(self, idx: QModelIndex) -> Qt.ItemFlag:\n        extra_flags = Qt.ItemFlag.NoItemFlags  # type: Qt.ItemFlag\n        if idx.column() in self.view.editable_columns:\n            extra_flags |= Qt.ItemFlag.ItemIsEditable\n        return super().flags(idx) | extra_flags\n\n    @staticmethod\n    def _tx_mined_info_from_tx_item(tx_item: Dict[str, Any]) -> TxMinedInfo:\n        # FIXME a bit hackish to have to reconstruct the TxMinedInfo... same thing in qml-gui\n        tx_mined_info = TxMinedInfo(\n            _height=tx_item['height'],\n            conf=tx_item['confirmations'],\n            timestamp=tx_item['timestamp'],\n            wanted_height=tx_item.get('wanted_height', None),\n        )\n        return tx_mined_info\n\n\nclass HistoryList(MyTreeView, AcceptFileDragDrop):\n\n    class Columns(MyTreeView.BaseColumnsEnum):\n        STATUS = enum.auto()\n        DESCRIPTION = enum.auto()\n        AMOUNT = enum.auto()\n        BALANCE = enum.auto()\n        FIAT_VALUE = enum.auto()\n        FIAT_ACQ_PRICE = enum.auto()\n        FIAT_CAP_GAINS = enum.auto()\n        TXID = enum.auto()\n        SHORT_ID = enum.auto()  # ~SCID\n\n    filter_columns = [\n        Columns.STATUS,\n        Columns.DESCRIPTION,\n        Columns.AMOUNT,\n        Columns.TXID,\n        Columns.SHORT_ID,\n    ]\n\n    def tx_item_from_proxy_row(self, proxy_row):\n        hm_idx = self.model().mapToSource(self.model().index(proxy_row, 0))\n        return hm_idx.internalPointer().get_data()\n\n    def should_hide(self, proxy_row):\n        if self.start_date and self.end_date:\n            tx_item = self.tx_item_from_proxy_row(proxy_row)\n            date = tx_item['date']\n            if date:\n                in_interval = self.start_date <= date <= self.end_date\n                if not in_interval:\n                    return True\n            return False\n\n    def __init__(self, main_window: 'ElectrumWindow', model: HistoryModel):\n        super().__init__(\n            main_window=main_window,\n            stretch_column=HistoryColumns.DESCRIPTION,\n            editable_columns=[HistoryColumns.DESCRIPTION, HistoryColumns.FIAT_VALUE],\n        )\n        self.hm = model\n        self.proxy = HistorySortModel(self)\n        self.proxy.setSourceModel(model)\n        self.setModel(self.proxy)\n        AcceptFileDragDrop.__init__(self, \".txn\")\n        self.setSortingEnabled(True)\n        self.start_date = None\n        self.end_date = None\n        self.years = []\n        self.period_combo = QComboBox()\n        self.start_button = QPushButton('-')\n        self.start_button.pressed.connect(self.select_start_date)\n        self.start_button.setEnabled(False)\n        self.end_button = QPushButton('-')\n        self.end_button.pressed.connect(self.select_end_date)\n        self.end_button.setEnabled(False)\n        self.period_combo.addItems([_('All'), _('Custom')])\n        self.period_combo.activated.connect(self.on_combo)\n        self.wallet = self.main_window.wallet  # type: Abstract_Wallet\n        self.sortByColumn(HistoryColumns.STATUS, Qt.SortOrder.DescendingOrder)\n        self.setRootIsDecorated(True)\n        self.header().setStretchLastSection(False)\n        for col in HistoryColumns:\n            sm = QHeaderView.ResizeMode.Stretch if col == self.stretch_column else QHeaderView.ResizeMode.ResizeToContents\n            self.header().setSectionResizeMode(col, sm)\n        if self.config:\n            self.configvar_show_toolbar = self.config.cv.GUI_QT_HISTORY_TAB_SHOW_TOOLBAR\n\n    def update(self):\n        self.hm.refresh('HistoryList.update()')\n\n    def format_date(self, d):\n        return str(datetime.date(d.year, d.month, d.day)) if d else _('None')\n\n    def on_combo(self, x):\n        s = self.period_combo.itemText(x)\n        x = s == _('Custom')\n        self.start_button.setEnabled(x)\n        self.end_button.setEnabled(x)\n        if s == _('All'):\n            self.start_date = None\n            self.end_date = None\n            self.start_button.setText(\"-\")\n            self.end_button.setText(\"-\")\n        else:\n            try:\n                year = int(s)\n            except Exception:\n                return\n            self.start_date = datetime.datetime(year, 1, 1)\n            self.end_date = datetime.datetime(year+1, 1, 1)\n            self.start_button.setText(_('From') + ' ' + self.format_date(self.start_date))\n            self.end_button.setText(_('To') + ' ' + self.format_date(self.end_date))\n        self.hide_rows()\n\n    def create_toolbar(self, config: 'SimpleConfig'):\n        toolbar, menu = self.create_toolbar_with_menu('')\n        self.num_tx_label = toolbar.itemAt(0).widget()\n        self._toolbar_checkbox = menu.addToggle(_(\"Filter by Date\"), lambda: self.toggle_toolbar())\n        self.menu_fiat = menu.addConfig(config.cv.FX_HISTORY_RATES, short_desc=_('Show Fiat Values'), callback=self.main_window.app.update_fiat_signal.emit)\n        self.menu_capgains = menu.addConfig(config.cv.FX_HISTORY_RATES_CAPITAL_GAINS, callback=self.main_window.app.update_fiat_signal.emit)\n        self.menu_summary = menu.addAction(_(\"&Summary\"), self.show_summary)\n        menu.addAction(_(\"&Plot\"), self.plot_history_dialog)\n        menu.addAction(_(\"&Export\"), self.export_history_dialog)\n        hbox = self.create_toolbar_buttons()\n        toolbar.insertLayout(1, hbox)\n        self.update_toolbar_menu()\n        return toolbar\n\n    def update_toolbar_menu(self):\n        fx = self.main_window.fx\n        self.menu_fiat.setEnabled(fx and fx.can_have_history())\n        # setChecked because has_history can be modified through settings dialog\n        self.menu_fiat.setChecked(fx and fx.has_history())\n        self.menu_capgains.setEnabled(fx and fx.has_history())\n        self.menu_summary.setEnabled(fx and fx.has_history())\n\n    def get_toolbar_buttons(self):\n        return self.period_combo, self.start_button, self.end_button\n\n    def on_hide_toolbar(self):\n        self.start_date = None\n        self.end_date = None\n        self.hide_rows()\n\n    def select_start_date(self):\n        self.start_date = self.select_date(self.start_button)\n        self.hide_rows()\n\n    def select_end_date(self):\n        self.end_date = self.select_date(self.end_button)\n        self.hide_rows()\n\n    def select_date(self, button):\n        d = WindowModalDialog(self, _(\"Select date\"))\n        d.setMinimumSize(600, 150)\n        d.date = None\n        vbox = QVBoxLayout()\n\n        def on_date(date):\n            d.date = date\n\n        cal = QCalendarWidget()\n        cal.setGridVisible(True)\n        cal.clicked[QDate].connect(on_date)\n        vbox.addWidget(cal)\n        vbox.addLayout(Buttons(OkButton(d), CancelButton(d)))\n        d.setLayout(vbox)\n        if d.exec():\n            if d.date is None:\n                return None\n            date = d.date.toPyDate()\n            button.setText(self.format_date(date))\n            return datetime.datetime(date.year, date.month, date.day)\n\n    def show_summary(self):\n        if not self.hm.should_show_fiat():\n            self.main_window.show_message(_(\"Enable fiat exchange rate with history.\"))\n            return\n        fx = self.main_window.fx\n        summary = self.wallet.get_onchain_capital_gains(\n            from_timestamp=time.mktime(self.start_date.timetuple()) if self.start_date else None,\n            to_timestamp=time.mktime(self.end_date.timetuple()) if self.end_date else None,\n            fx=fx)\n        if not summary:\n            self.main_window.show_message(_(\"Nothing to summarize.\"))\n            return\n        start = summary['begin']\n        end = summary['end']\n        flow = summary['flow']\n        start_date = start.get('date')\n        end_date = end.get('date')\n        format_amount = lambda x: self.main_window.format_amount(x.value) + ' ' + self.main_window.base_unit()\n        format_fiat = lambda x: str(x) + ' ' + self.main_window.fx.ccy\n\n        d = WindowModalDialog(self, _(\"Summary\"))\n        d.setMinimumSize(600, 150)\n        vbox = QVBoxLayout()\n        msg = messages.to_rtf(messages.MSG_CAPITAL_GAINS)\n        vbox.addWidget(WWLabel(msg))\n        grid = QGridLayout()\n        grid.addWidget(QLabel(_(\"Begin\")), 0, 1)\n        grid.addWidget(QLabel(_(\"End\")), 0, 2)\n        #\n        grid.addWidget(QLabel(_(\"Date\")), 1, 0)\n        grid.addWidget(QLabel(self.format_date(start_date)), 1, 1)\n        grid.addWidget(QLabel(self.format_date(end_date)), 1, 2)\n        #\n        grid.addWidget(QLabel(_(\"BTC balance\")), 2, 0)\n        grid.addWidget(QLabel(format_amount(start['BTC_balance'])), 2, 1)\n        grid.addWidget(QLabel(format_amount(end['BTC_balance'])), 2, 2)\n        #\n        grid.addWidget(QLabel(_(\"BTC Fiat price\")), 3, 0)\n        grid.addWidget(QLabel(format_fiat(start.get('BTC_fiat_price'))), 3, 1)\n        grid.addWidget(QLabel(format_fiat(end.get('BTC_fiat_price'))), 3, 2)\n        #\n        grid.addWidget(QLabel(_(\"Fiat balance\")), 4, 0)\n        grid.addWidget(QLabel(format_fiat(start.get('fiat_balance'))), 4, 1)\n        grid.addWidget(QLabel(format_fiat(end.get('fiat_balance'))), 4, 2)\n        #\n        grid.addWidget(QLabel(_(\"Acquisition price\")), 5, 0)\n        grid.addWidget(QLabel(format_fiat(start.get('acquisition_price', ''))), 5, 1)\n        grid.addWidget(QLabel(format_fiat(end.get('acquisition_price', ''))), 5, 2)\n        #\n        grid.addWidget(QLabel(_(\"Unrealized capital gains\")), 6, 0)\n        grid.addWidget(QLabel(format_fiat(start.get('unrealized_gains', ''))), 6, 1)\n        grid.addWidget(QLabel(format_fiat(end.get('unrealized_gains', ''))), 6, 2)\n        #\n        grid2 = QGridLayout()\n        grid2.addWidget(QLabel(_(\"BTC incoming\")), 0, 0)\n        grid2.addWidget(QLabel(format_amount(flow['BTC_incoming'])), 0, 1)\n        grid2.addWidget(QLabel(_(\"Fiat incoming\")), 1, 0)\n        grid2.addWidget(QLabel(format_fiat(flow.get('fiat_incoming'))), 1, 1)\n        grid2.addWidget(QLabel(_(\"BTC outgoing\")), 2, 0)\n        grid2.addWidget(QLabel(format_amount(flow['BTC_outgoing'])), 2, 1)\n        grid2.addWidget(QLabel(_(\"Fiat outgoing\")), 3, 0)\n        grid2.addWidget(QLabel(format_fiat(flow.get('fiat_outgoing'))), 3, 1)\n        #\n        grid2.addWidget(QLabel(_(\"Realized capital gains\")), 4, 0)\n        grid2.addWidget(QLabel(format_fiat(flow.get('realized_capital_gains'))), 4, 1)\n        vbox.addLayout(grid)\n        vbox.addWidget(QLabel(_('Cash flow')))\n        vbox.addLayout(grid2)\n        vbox.addLayout(Buttons(CloseButton(d)))\n        d.setLayout(vbox)\n        d.exec()\n\n    def plot_history_dialog(self):\n        try:\n            from electrum.plot import plot_history, NothingToPlotException\n        except ImportError as e:\n            _logger.error(f\"could not import electrum.plot. This feature needs matplotlib to be installed. exc={e!r}\")\n            self.main_window.show_message(\"\\n\\n\".join([\n                _(\"This feature requires the 'matplotlib' Python library which is not \"\n                  \"included in Electrum by default.\"),\n                _(\"If you run Electrum from source you can install matplotlib to use this feature.\"),\n                _(\"It is not possible to install matplotlib inside the binary executables \"\n                  \"(e.g. AppImage or Windows installation).\")\n            ]))\n            return\n        try:\n            plt = plot_history(list(self.hm.transactions.values()))\n            plt.show()\n        except NothingToPlotException as e:\n            self.main_window.show_message(str(e))\n\n    def on_edited(self, idx, edit_key, *, text):\n        index = self.model().mapToSource(idx)\n        tx_item = index.internalPointer().get_data()\n        column = index.column()\n        key = get_item_key(tx_item)\n        if column == HistoryColumns.DESCRIPTION:\n            if self.wallet.set_label(key, text):  # changed\n                self.hm.update_label(index)\n                self.main_window.update_completions()\n        elif column == HistoryColumns.FIAT_VALUE:\n            self.wallet.set_fiat_value(key, self.main_window.fx.ccy, text, self.main_window.fx, tx_item['value'].value)\n            value = tx_item['value'].value\n            if value is not None:\n                self.hm.update_fiat(index)\n        else:\n            raise Exception(f\"did not expect {column=!r} to get edited\")\n\n    def on_double_click(self, idx):\n        tx_item = idx.internalPointer().get_data()\n        if tx_item.get('lightning'):\n            if tx_item['type'] == 'payment':\n                self.main_window.show_lightning_transaction(tx_item)\n            return\n        tx_hash = tx_item['txid']\n        tx = self.wallet.adb.get_transaction(tx_hash)\n        if not tx:\n            return\n        self.main_window.show_transaction(tx)\n\n    def add_copy_menu(self, menu, idx):\n        cc = menu.addMenu(_(\"Copy\"))\n        for column in HistoryColumns:\n            if self.isColumnHidden(column):\n                continue\n            column_title = self.hm.headerData(column, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole)\n            idx2 = idx.sibling(idx.row(), column)\n            clipboard_data = self.hm.data(idx2, self.ROLE_CLIPBOARD_DATA).value()\n            if clipboard_data is None:\n                clipboard_data = (self.hm.data(idx2, Qt.ItemDataRole.DisplayRole).value() or '').strip()\n            cc.addAction(\n                column_title,\n                lambda text=clipboard_data, title=column_title:\n                self.place_text_on_clipboard(text, title=title))\n        return cc\n\n    def create_menu(self, position: QPoint):\n        org_idx: QModelIndex = self.indexAt(position)\n        idx = self.proxy.mapToSource(org_idx)\n        if not idx.isValid():\n            # can happen e.g. before list is populated for the first time\n            return\n        tx_item = idx.internalPointer().get_data()\n        if tx_item.get('lightning'):\n            menu = QMenu()\n            menu.addAction(_(\"Details\"), lambda: self.main_window.show_lightning_transaction(tx_item))\n            cc = self.add_copy_menu(menu, idx)\n            cc.addAction(_(\"Payment Hash\"), lambda: self.place_text_on_clipboard(tx_item['payment_hash'], title=\"Payment Hash\"))\n            cc.addAction(_(\"Preimage\"), lambda: self.place_text_on_clipboard(tx_item['preimage'], title=\"Preimage\"))\n            key = tx_item['payment_hash']\n            log = self.wallet.lnworker.logs.get(key)\n            if log:\n                menu.addAction(_(\"View log\"), lambda: self.main_window.send_tab.invoice_list.show_log(key, log))\n            menu.exec(self.viewport().mapToGlobal(position))\n            return\n        tx_hash = tx_item['txid']\n        tx = self.wallet.adb.get_transaction(tx_hash)\n        if not tx:\n            return\n        tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)\n        tx_details = self.wallet.get_tx_info(tx)\n        is_unconfirmed = tx_details.tx_mined_status.height() <= 0\n        menu = QMenu()\n        menu.addAction(_(\"Details\"), lambda: self.main_window.show_transaction(tx))\n        if tx_details.can_remove:\n            menu.addAction(_(\"Remove\"), lambda: self.remove_local_tx(tx_hash))\n        copy_menu = self.add_copy_menu(menu, idx)\n        copy_menu.addAction(_(\"Transaction ID\"), lambda: self.place_text_on_clipboard(tx_hash, title=\"TXID\"))\n        menu_edit = menu.addMenu(_(\"Edit\"))\n        for c in self.editable_columns:\n            if self.isColumnHidden(c):\n                continue\n            label = self.hm.headerData(c, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole)\n            # TODO use siblingAtColumn when min Qt version is >=5.11\n            persistent = QPersistentModelIndex(org_idx.sibling(org_idx.row(), c))\n            menu_edit.addAction(_(\"{}\").format(label), lambda p=persistent: self.edit(QModelIndex(p)))\n        channel_id = tx_item.get('channel_id')\n        if channel_id and self.wallet.lnworker and (chan := self.wallet.lnworker.get_channel_by_id(bytes.fromhex(channel_id))):\n            menu.addAction(_(\"View Channel\"), lambda: self.main_window.show_channel_details(chan))\n        if is_unconfirmed and tx:\n            if tx_details.can_bump:\n                menu.addAction(_(\"Increase fee\"), lambda: self.main_window.bump_fee_dialog(tx))\n            else:\n                if tx_details.can_cpfp:\n                    menu.addAction(_(\"Child pays for parent\"), lambda: self.main_window.cpfp_dialog(tx))\n            if tx_details.can_dscancel:\n                menu.addAction(_(\"Cancel (double-spend)\"), lambda: self.main_window.dscancel_dialog(tx))\n        invoices = self.wallet.get_relevant_invoices_for_tx(tx_hash)\n        if len(invoices) == 1:\n            menu.addAction(_(\"View invoice\"), lambda inv=invoices[0]: self.main_window.show_onchain_invoice(inv))\n        elif len(invoices) > 1:\n            menu_invs = menu.addMenu(_(\"Related invoices\"))\n            for inv in invoices:\n                menu_invs.addAction(_(\"View invoice\"), lambda inv=inv: self.main_window.show_onchain_invoice(inv))\n        if tx_URL:\n            menu.addAction(_(\"View on block explorer\"), lambda: webopen(tx_URL))\n        self.open_menu(menu, position)\n\n    def remove_local_tx(self, tx_hash: str):\n        num_child_txs = len(self.wallet.adb.get_depending_transactions(tx_hash))\n        question = _(\"Are you sure you want to remove this transaction?\")\n        if num_child_txs > 0:\n            question = (_(\"Are you sure you want to remove this transaction and {} child transactions?\")\n                        .format(num_child_txs))\n        if not self.main_window.question(msg=question, title=_(\"Please confirm\")):\n            return\n        self.wallet.adb.remove_transaction(tx_hash)\n        self.wallet.save_db()\n        # need to update at least: history_list, utxo_list, address_list\n        self.main_window.need_update.set()\n\n    def onFileAdded(self, fn):\n        try:\n            with open(fn) as f:\n                tx = self.main_window.tx_from_text(f.read())\n        except IOError as e:\n            self.main_window.show_error(e)\n            return\n        if not tx:\n            return\n        self.main_window.save_transaction_into_wallet(tx)\n\n    def export_history_dialog(self):\n        d = WindowModalDialog(self, _('Export History'))\n        d.setMinimumSize(400, 200)\n        vbox = QVBoxLayout(d)\n        defaultname = f'electrum-history-{self.wallet.basename()}.csv'\n        select_msg = _('Select file to export your wallet transactions to')\n        hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)\n        vbox.addLayout(hbox)\n        vbox.addStretch(1)\n        hbox = Buttons(CancelButton(d), OkButton(d, _('Export')))\n        vbox.addLayout(hbox)\n        #run_hook('export_history_dialog', self, hbox)\n        self.update()\n        if not d.exec():\n            return\n        filename = filename_e.text()\n        if not filename:\n            return\n        try:\n            self.wallet.export_history_to_file(\n                fx=self.main_window.fx if self.hm.should_show_fiat() else None,\n                file_path=filename,\n                is_csv=csv_button.isChecked(),\n            )\n        except (IOError, os.error) as reason:\n            export_error_label = _(\"Electrum was unable to produce a transaction export.\")\n            self.main_window.show_critical(export_error_label + \"\\n\" + str(reason), title=_(\"Unable to export history\"))\n            return\n        self.main_window.show_message(_(\"Your wallet history has been successfully exported.\"))\n\n    def get_text_from_coordinate(self, row, col):\n        return self.get_role_data_from_coordinate(row, col, role=Qt.ItemDataRole.DisplayRole)\n\n    def get_role_data_from_coordinate(self, row, col, *, role):\n        idx = self.model().mapToSource(self.model().index(row, col))\n        return self.hm.data(idx, role).value()\n\n\nHistoryColumns = HistoryList.Columns\n"
  },
  {
    "path": "electrum/gui/qt/invoice_list.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport enum\nfrom typing import Sequence, TYPE_CHECKING\n\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtGui import QStandardItemModel, QStandardItem\nfrom PyQt6.QtWidgets import QAbstractItemView\nfrom PyQt6.QtWidgets import QMenu, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QHeaderView\n\nfrom electrum.i18n import _\nfrom electrum.util import format_time\nfrom electrum.invoices import PR_UNPAID, PR_INFLIGHT, PR_FAILED\nfrom electrum.lnutil import HtlcLog\n\nfrom .util import read_QIcon, pr_icons\nfrom .util import CloseButton, Buttons\nfrom .util import WindowModalDialog\n\nfrom .my_treeview import MyTreeView, MySortModel\n\nif TYPE_CHECKING:\n    from .send_tab import SendTab\n\n\nROLE_REQUEST_TYPE = Qt.ItemDataRole.UserRole\nROLE_REQUEST_ID = Qt.ItemDataRole.UserRole + 1\nROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 2\n\n\nclass InvoiceList(MyTreeView):\n    key_role = ROLE_REQUEST_ID\n\n    class Columns(MyTreeView.BaseColumnsEnum):\n        DATE = enum.auto()\n        DESCRIPTION = enum.auto()\n        AMOUNT = enum.auto()\n        STATUS = enum.auto()\n\n    headers = {\n        Columns.DATE: _('Date'),\n        Columns.DESCRIPTION: _('Description'),\n        Columns.AMOUNT: _('Amount'),\n        Columns.STATUS: _('Status'),\n    }\n    filter_columns = [Columns.DATE, Columns.DESCRIPTION, Columns.AMOUNT]\n\n    def __init__(self, send_tab: 'SendTab'):\n        window = send_tab.window\n        super().__init__(\n            main_window=window,\n            stretch_column=self.Columns.DESCRIPTION,\n        )\n        self.wallet = window.wallet\n        self.send_tab = send_tab\n        self.std_model = QStandardItemModel(self)\n        self.proxy = MySortModel(self, sort_role=ROLE_SORT_ORDER)\n        self.proxy.setSourceModel(self.std_model)\n        self.setModel(self.proxy)\n        self.setSortingEnabled(True)\n        self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)\n\n    def on_double_click(self, idx):\n        key = idx.sibling(idx.row(), self.Columns.DATE).data(ROLE_REQUEST_ID)\n        self.show_invoice(key)\n\n    def refresh_row(self, key, row):\n        assert row is not None\n        invoice = self.wallet.get_invoice(key)\n        if invoice is None:\n            return\n        model = self.std_model\n        status_item = model.item(row, self.Columns.STATUS)\n        status = self.wallet.get_invoice_status(invoice)\n        status_str = invoice.get_status_str(status)\n        if self.wallet.lnworker:\n            log = self.wallet.lnworker.logs.get(key)\n            if log and status == PR_INFLIGHT:\n                status_str += '... (%d)'%len(log)\n        status_item.setText(status_str)\n        status_item.setIcon(read_QIcon(pr_icons.get(status)))\n\n    def update(self):\n        # not calling maybe_defer_update() as it interferes with conditional-visibility\n        self.proxy.setDynamicSortFilter(False)  # temp. disable re-sorting after every change\n        self.std_model.clear()\n        self.update_headers(self.__class__.headers)\n        for idx, item in enumerate(self.wallet.get_unpaid_invoices()):\n            key = item.get_id()\n            if item.is_lightning():\n                icon_name = 'lightning.png'\n            else:\n                icon_name = 'bitcoin.png'\n                if item.bip70:\n                    icon_name = 'seal.png'\n            status = self.wallet.get_invoice_status(item)\n            amount = item.get_amount_sat()\n            amount_str = self.main_window.format_amount(amount, whitespaces=True) if amount else \"\"\n            amount_str_nots = self.main_window.format_amount(amount, whitespaces=True, add_thousands_sep=False) if amount else \"\"\n            timestamp = item.time or 0\n            labels = [\"\"] * len(self.Columns)\n            labels[self.Columns.DATE] = format_time(timestamp) if timestamp else _('Unknown')\n            labels[self.Columns.DESCRIPTION] = item.message\n            labels[self.Columns.AMOUNT] = amount_str\n            labels[self.Columns.STATUS] = item.get_status_str(status)\n            items = [QStandardItem(e) for e in labels]\n            self.set_editability(items)\n            items[self.Columns.DATE].setIcon(read_QIcon(icon_name))\n            items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))\n            items[self.Columns.DATE].setData(key, role=ROLE_REQUEST_ID)\n            #items[self.Columns.DATE].setData(item.type, role=ROLE_REQUEST_TYPE)\n            items[self.Columns.DATE].setData(timestamp, role=ROLE_SORT_ORDER)\n            items[self.Columns.AMOUNT].setData(amount_str_nots.strip(), role=self.ROLE_CLIPBOARD_DATA)\n            self.std_model.insertRow(idx, items)\n        self.filter()\n        self.proxy.setDynamicSortFilter(True)\n        # sort requests by date\n        self.sortByColumn(self.Columns.DATE, Qt.SortOrder.DescendingOrder)\n        self.hide_if_empty()\n\n    def show_invoice(self, key):\n        invoice = self.wallet.get_invoice(key)\n        if not invoice:\n            self.update()\n            return\n        if invoice.is_lightning():\n            self.main_window.show_lightning_invoice(invoice)\n        else:\n            self.main_window.show_onchain_invoice(invoice)\n\n    def hide_if_empty(self):\n        b = self.std_model.rowCount() > 0\n        self.setVisible(b)\n        self.send_tab.invoices_label.setVisible(b)\n\n    def create_menu(self, position):\n        wallet = self.wallet\n        items = self.selected_in_column(0)\n        if len(items) > 1:\n            keys = [item.data(ROLE_REQUEST_ID) for item in items]\n            invoices = [wallet.get_invoice(key) for key in keys]\n            can_batch_pay = all([not i.is_lightning() and wallet.get_invoice_status(i) == PR_UNPAID for i in invoices])\n            menu = QMenu(self)\n            if can_batch_pay:\n                menu.addAction(_(\"Batch pay invoices\") + \"...\", lambda: self.send_tab.pay_multiple_invoices(invoices))\n            menu.addAction(_(\"Delete invoices\"), lambda: self.delete_invoices(keys))\n            menu.exec(self.viewport().mapToGlobal(position))\n            return\n        idx = self.indexAt(position)\n        item = self.item_from_index(idx)\n        item_col0 = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE))\n        if not item or not item_col0:\n            return\n        key = item_col0.data(ROLE_REQUEST_ID)\n        invoice = self.wallet.get_invoice(key)\n        menu = QMenu(self)\n        menu.addAction(_(\"Details\"), lambda: self.show_invoice(key))\n        copy_menu = self.add_copy_menu(menu, idx)\n        address = invoice.get_address()\n        if address:\n            copy_menu.addAction(_(\"Address\"), lambda: self.main_window.do_copy(invoice.get_address(), title='Bitcoin Address'))\n        status = wallet.get_invoice_status(invoice)\n        if status == PR_UNPAID:\n            if bool(invoice.get_amount_sat()):\n                menu.addAction(_(\"Pay\") + \"...\", lambda: self.send_tab.do_pay_invoice(invoice))\n            else:\n                menu.addAction(_(\"Pay\") + \"...\", lambda: self.send_tab.do_edit_invoice(invoice))\n        if status == PR_FAILED:\n            menu.addAction(_(\"Retry\"), lambda: self.send_tab.do_pay_invoice(invoice))\n        if self.wallet.lnworker:\n            log = self.wallet.lnworker.logs.get(key)\n            if log:\n                menu.addAction(_(\"View log\"), lambda: self.show_log(key, log))\n        menu.addAction(_(\"Delete\"), lambda: self.delete_invoices([key]))\n        self.open_menu(menu, position)\n\n    def show_log(self, key, log: Sequence[HtlcLog]):\n        d = WindowModalDialog(self, _(\"Payment log\"))\n        d.setMinimumWidth(600)\n        vbox = QVBoxLayout(d)\n        log_w = QTreeWidget()\n        log_w.setHeaderLabels([_('Hops'), _('Channel ID'), _('Message')])\n        log_w.header().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)\n        log_w.header().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)\n        for payment_attempt_log in log:\n            route_str, chan_str, message = payment_attempt_log.formatted_tuple()\n            x = QTreeWidgetItem([route_str, chan_str, message])\n            log_w.addTopLevelItem(x)\n        vbox.addWidget(log_w)\n        vbox.addLayout(Buttons(CloseButton(d)))\n        d.exec()\n\n    def delete_invoices(self, keys):\n        for key in keys:\n            self.wallet.delete_invoice(key, write_to_disk=False)\n            self.delete_item(key)\n        self.wallet.save_db()\n"
  },
  {
    "path": "electrum/gui/qt/lightning_dialog.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2012 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtWidgets import (QDialog, QLabel, QVBoxLayout, QPushButton)\n\nfrom electrum.i18n import _\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\n\nfrom .util import Buttons\n\nif TYPE_CHECKING:\n    from . import ElectrumGui\n\n\nclass LightningDialog(QDialog, QtEventListener):\n\n    def __init__(self, gui_object: 'ElectrumGui'):\n        QDialog.__init__(self)\n        self.gui_object = gui_object\n        self.config = gui_object.config\n        self.network = gui_object.daemon.network\n        assert self.network\n        self.setWindowTitle(_('Lightning Network'))\n        self.setMinimumWidth(600)\n        vbox = QVBoxLayout(self)\n        self.num_peers = QLabel('')\n        vbox.addWidget(self.num_peers)\n        self.num_nodes = QLabel('')\n        vbox.addWidget(self.num_nodes)\n        self.num_channels = QLabel('')\n        vbox.addWidget(self.num_channels)\n        self.status = QLabel('')\n        vbox.addWidget(self.status)\n        vbox.addStretch(1)\n        b = QPushButton(_('Close'))\n        b.clicked.connect(self.close)\n        vbox.addLayout(Buttons(b))\n        self.register_callbacks()\n        self.network.channel_db.update_counts() # trigger callback\n        if self.network.lngossip:\n            self.on_event_gossip_peers(self.network.lngossip.lnpeermgr.num_peers())\n            self.on_event_unknown_channels(len(self.network.lngossip.unknown_ids))\n        else:\n            self.num_peers.setText(_('Lightning gossip not active.'))\n\n    @qt_event_listener\n    def on_event_channel_db(self, num_nodes, num_channels, num_policies):\n        self.num_nodes.setText(_('{} nodes').format(num_nodes))\n        self.num_channels.setText(_('{} channels').format(num_channels))\n\n    @qt_event_listener\n    def on_event_gossip_peers(self, num_peers):\n        self.num_peers.setText(_('Connected to {} peers').format(num_peers))\n\n    @qt_event_listener\n    def on_event_unknown_channels(self, unknown):\n        self.status.setText(_('Requesting {} channels...').format(unknown) if unknown else '')\n\n    def is_hidden(self):\n        return self.isMinimized() or self.isHidden()\n\n    def show_or_hide(self):\n        if self.is_hidden():\n            self.bring_to_top()\n        else:\n            self.hide()\n\n    def bring_to_top(self):\n        self.show()\n        self.raise_()\n\n    def closeEvent(self, event):\n        self.unregister_callbacks()\n        self.gui_object.lightning_dialog = None\n        event.accept()\n"
  },
  {
    "path": "electrum/gui/qt/lightning_tx_dialog.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2020 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import TYPE_CHECKING\nfrom decimal import Decimal\nimport datetime\n\nfrom PyQt6.QtWidgets import QVBoxLayout, QLabel\n\nfrom electrum.i18n import _\nfrom electrum.lnworker import PaymentDirection\n\nfrom .util import WindowModalDialog, ShowQRLineEdit, Buttons, CloseButton, font_height, ButtonsLineEdit\nfrom .qrtextedit import ShowQRTextEdit\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nclass LightningTxDialog(WindowModalDialog):\n\n    def __init__(self, parent: 'ElectrumWindow', tx_item: dict):\n        WindowModalDialog.__init__(self, parent, _(\"Lightning Payment\"))\n        self.main_window = parent\n        self.config = parent.config\n        self.label = tx_item['label']\n        self.timestamp = tx_item['timestamp']\n        self.amount = Decimal(tx_item['amount_msat']) / 1000\n        self.payment_hash = tx_item['payment_hash']\n        self.preimage = tx_item['preimage']\n        self.invoice = \"\"\n        invoice = self.main_window.wallet.get_invoice(self.payment_hash)  # only check outgoing invoices\n        if invoice:\n            assert invoice.is_lightning(), f\"{self.invoice!r}\"\n            self.invoice = invoice.lightning_invoice\n        self.setMinimumWidth(700)\n        vbox = QVBoxLayout()\n        self.setLayout(vbox)\n        amount_str = self.main_window.format_amount_and_units(self.amount, timestamp=self.timestamp)\n        vbox.addWidget(QLabel(_(\"Amount\") + f\": {amount_str}\"))\n        fee_msat = tx_item.get('fee_msat')\n        if fee_msat is not None:\n            fee_sat = Decimal(fee_msat) / 1000 if fee_msat is not None else None\n            fee_str = self.main_window.format_amount_and_units(fee_sat, timestamp=self.timestamp)\n            vbox.addWidget(QLabel(_(\"Fee: {}\").format(fee_str)))\n        time_str = datetime.datetime.fromtimestamp(self.timestamp).isoformat(' ')[:-3]\n        vbox.addWidget(QLabel(_(\"Date\") + \": \" + time_str))\n        self.tx_desc_label = QLabel(_(\"Description:\"))\n        vbox.addWidget(self.tx_desc_label)\n        self.tx_desc = ButtonsLineEdit(self.label)\n\n        def on_edited():\n            text = self.tx_desc.text()\n            if self.main_window.wallet.set_label(self.payment_hash, text):\n                self.main_window.history_list.update()\n                self.main_window.utxo_list.update()\n                self.main_window.labels_changed_signal.emit()\n        self.tx_desc.editingFinished.connect(on_edited)\n        self.tx_desc.addCopyButton()\n        vbox.addWidget(self.tx_desc)\n        vbox.addWidget(QLabel(_(\"Payment hash\") + \":\"))\n        self.hash_e = ShowQRLineEdit(self.payment_hash, self.config, title=_(\"Payment hash\"))\n        vbox.addWidget(self.hash_e)\n        vbox.addWidget(QLabel(_(\"Preimage\") + \":\"))\n        self.preimage_e = ShowQRLineEdit(self.preimage, self.config, title=_(\"Preimage\"))\n        vbox.addWidget(self.preimage_e)\n        if self.invoice:\n            vbox.addWidget(QLabel(_(\"Lightning Invoice\") + \":\"))\n            self.invoice_e = ShowQRTextEdit(self.invoice, config=self.config)\n            self.invoice_e.setMaximumHeight(max(150, 10 * font_height()))\n            self.invoice_e.addCopyButton()\n            vbox.addWidget(self.invoice_e)\n        self.close_button = CloseButton(self)\n        vbox.addLayout(Buttons(self.close_button))\n        self.close_button.setFocus()\n"
  },
  {
    "path": "electrum/gui/qt/locktimeedit.py",
    "content": "# Copyright (C) 2020 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nimport time\nfrom datetime import datetime\nfrom typing import Optional, Any\n\nfrom PyQt6.QtCore import Qt, QDateTime, pyqtSignal\nfrom PyQt6.QtGui import QPainter\nfrom PyQt6.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox,\n                             QHBoxLayout, QDateTimeEdit)\n\nfrom electrum.i18n import _\nfrom electrum.bitcoin import NLOCKTIME_MIN, NLOCKTIME_MAX, NLOCKTIME_BLOCKHEIGHT_MAX\n\nfrom .util import char_width_in_lineedit, ColorScheme\n\n\nclass LockTimeEdit(QWidget):\n\n    valueEdited = pyqtSignal()\n\n    def __init__(self, parent=None):\n        QWidget.__init__(self, parent)\n\n        hbox = QHBoxLayout()\n        self.setLayout(hbox)\n        hbox.setContentsMargins(0, 0, 0, 0)\n        hbox.setSpacing(0)\n\n        self.locktime_raw_e = LockTimeRawEdit(self)\n        self.locktime_height_e = LockTimeHeightEdit(self)\n        self.locktime_date_e = LockTimeDateEdit(self)\n        self.editors = [self.locktime_raw_e, self.locktime_height_e, self.locktime_date_e]\n\n        self.combo = QComboBox()\n        options = [_(\"Raw\"), _(\"Block height\"), _(\"Date\")]\n        option_index_to_editor_map = {\n            0: self.locktime_raw_e,\n            1: self.locktime_height_e,\n            2: self.locktime_date_e,\n        }\n        default_index = 1\n        self.combo.addItems(options)\n\n        def on_current_index_changed(i):\n            for w in self.editors:\n                w.setVisible(False)\n                w.setEnabled(False)\n            prev_locktime = self.editor.get_locktime()\n            self.editor = option_index_to_editor_map[i]\n            if self.editor.is_acceptable_locktime(prev_locktime):\n                self.editor.set_locktime(prev_locktime)\n            self.editor.setVisible(True)\n            self.editor.setEnabled(True)\n\n        self.editor = option_index_to_editor_map[default_index]\n        self.combo.currentIndexChanged.connect(on_current_index_changed)\n        self.combo.setCurrentIndex(default_index)\n        on_current_index_changed(default_index)\n\n        hbox.addWidget(self.combo)\n        for w in self.editors:\n            hbox.addWidget(w)\n        hbox.addStretch(1)\n\n        self.locktime_height_e.textEdited.connect(self.valueEdited.emit)\n        self.locktime_raw_e.textEdited.connect(self.valueEdited.emit)\n        self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit)\n        self.combo.currentIndexChanged.connect(self.valueEdited.emit)\n\n    def get_locktime(self) -> Optional[int]:\n        return self.editor.get_locktime()\n\n    def set_locktime(self, x: Any) -> None:\n        self.editor.set_locktime(x)\n\n\nclass _LockTimeEditor:\n    min_allowed_value = NLOCKTIME_MIN\n    max_allowed_value = NLOCKTIME_MAX\n\n    def get_locktime(self) -> Optional[int]:\n        raise NotImplementedError()\n\n    def set_locktime(self, x: Any) -> None:\n        raise NotImplementedError()\n\n    @classmethod\n    def is_acceptable_locktime(cls, x: Any) -> bool:\n        if not x:  # e.g. empty string\n            return True\n        try:\n            x = int(x)\n        except Exception:\n            return False\n        return cls.min_allowed_value <= x <= cls.max_allowed_value\n\n\nclass LockTimeRawEdit(QLineEdit, _LockTimeEditor):\n\n    def __init__(self, parent=None):\n        QLineEdit.__init__(self, parent)\n        self.setFixedWidth(14 * char_width_in_lineedit())\n        self.textChanged.connect(self.numbify)\n\n    def numbify(self):\n        text = self.text().strip()\n        chars = '0123456789'\n        pos = self.cursorPosition()\n        pos = len(''.join([i for i in text[:pos] if i in chars]))\n        s = ''.join([i for i in text if i in chars])\n        self.set_locktime(s)\n        # setText sets Modified to False.  Instead we want to remember\n        # if updates were because of user modification.\n        self.setModified(self.hasFocus())\n        self.setCursorPosition(pos)\n\n    def get_locktime(self) -> Optional[int]:\n        try:\n            return int(str(self.text()))\n        except Exception:\n            return None\n\n    def set_locktime(self, x: Any) -> None:\n        try:\n            x = int(x)\n        except Exception:\n            self.setText('')\n            return\n        x = max(x, self.min_allowed_value)\n        x = min(x, self.max_allowed_value)\n        self.setText(str(x))\n\n\nclass LockTimeHeightEdit(LockTimeRawEdit):\n    max_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX\n\n    def __init__(self, parent=None):\n        LockTimeRawEdit.__init__(self, parent)\n        self.setFixedWidth(20 * char_width_in_lineedit())\n\n    def paintEvent(self, event):\n        super().paintEvent(event)\n        panel = QStyleOptionFrame()\n        self.initStyleOption(panel)\n        textRect = self.style().subElementRect(QStyle.SubElement.SE_LineEditContents, panel, self)\n        textRect.adjust(2, 0, -10, 0)\n        painter = QPainter(self)\n        painter.setPen(ColorScheme.GRAY.as_color())\n        painter.drawText(textRect, int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), \"height\")\n\n\ndef get_max_allowed_timestamp() -> int:\n    ts = NLOCKTIME_MAX\n    # Test if this value is within the valid timestamp limits (which is platform-dependent).\n    # see #6170\n    try:\n        datetime.fromtimestamp(ts)\n    except (OSError, OverflowError):\n        ts = 2 ** 31 - 1  # INT32_MAX\n        datetime.fromtimestamp(ts)  # test if raises\n    return ts\n\n\nclass LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):\n    min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1\n    max_allowed_value = get_max_allowed_timestamp()\n\n    def __init__(self, parent=None):\n        QDateTimeEdit.__init__(self, parent)\n        self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value))\n        self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value))\n        self.setDateTime(QDateTime.currentDateTime())\n\n    def get_locktime(self) -> Optional[int]:\n        dt = self.dateTime().toPyDateTime()\n        locktime = int(time.mktime(dt.timetuple()))\n        return locktime\n\n    def set_locktime(self, x: Any) -> None:\n        if not self.is_acceptable_locktime(x):\n            self.setDateTime(QDateTime.currentDateTime())\n            return\n        try:\n            x = int(x)\n        except Exception:\n            self.setDateTime(QDateTime.currentDateTime())\n            return\n        dt = datetime.fromtimestamp(x)\n        self.setDateTime(dt)\n"
  },
  {
    "path": "electrum/gui/qt/main_window.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2012 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport sys\nimport time\nimport threading\nimport os\nimport json\nimport weakref\nimport csv\nfrom decimal import Decimal\nimport base64\nfrom functools import partial\nimport queue\nimport asyncio\nfrom typing import Optional, TYPE_CHECKING, Sequence, Union, Dict, Mapping, Callable, List, Set\nimport concurrent.futures\nimport inspect\n\nfrom PyQt6.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont, QFontMetrics, QAction, QShortcut\nfrom PyQt6.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal, QTimer\nfrom PyQt6.QtWidgets import (QMessageBox, QTabWidget, QMenuBar, QFileDialog, QCheckBox, QLabel,\n                             QVBoxLayout, QGridLayout, QLineEdit, QHBoxLayout, QPushButton, QScrollArea, QTextEdit,\n                             QMainWindow, QInputDialog, QWidget, QSizePolicy, QStatusBar, QToolTip,\n                             QMenu, QToolButton, QDialog)\n\nimport electrum_ecc as ecc\n\nimport electrum\nfrom electrum.gui import messages\nfrom electrum import (keystore, constants, util, bitcoin, commands,\n                      paymentrequest, lnutil)\nfrom electrum.bitcoin import COIN, is_address, DummyAddress\nfrom electrum.plugin import run_hook\nfrom electrum.i18n import _\nfrom electrum.util import (format_time, UserCancelled, profiler, bfh, InvalidPassword,\n                           UserFacingException, get_new_wallet_name,\n                           send_exception_to_crash_reporter,\n                           AddTransactionException, os_chmod, UI_UNIT_NAME_TXSIZE_VBYTES,\n                           is_valid_email, ChoiceItem, event_listener)\nfrom electrum.bip21 import BITCOIN_BIP21_URI_SCHEME\nfrom electrum.payment_identifier import PaymentIdentifier\nfrom electrum.invoices import PR_PAID, Invoice\nfrom electrum.transaction import (Transaction, PartialTxInput, TxOutput,\n                                  PartialTransaction, PartialTxOutput)\nfrom electrum.wallet import (Multisig_Wallet, Abstract_Wallet,\n                             sweep_preparations, InternalAddressCorruption,\n                             CannotCPFP)\nfrom electrum.version import ELECTRUM_VERSION\nfrom electrum.network import Network, UntrustedServerReturnedError\nfrom electrum.exchange_rate import FxThread\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.logging import Logger\nfrom electrum.lntransport import extract_nodeid, ConnStringFormatError\nfrom electrum.lnaddr import lndecode, LnAddr\nfrom electrum.submarine_swaps import SwapServerTransport, NostrTransport\nfrom electrum.fee_policy import FeePolicy\n\nfrom electrum.gui.common_qt.util import TaskThread, QtEventListener, qt_event_listener\n\nfrom .rate_limiter import rate_limited\nfrom .exception_window import Exception_Hook\nfrom .amountedit import BTCAmountEdit\nfrom .qrcodewidget import QRDialog\nfrom .qrtextedit import ShowQRTextEdit, ScanQRTextEdit, ScanShowQRTextEdit\nfrom .transaction_dialog import show_transaction\nfrom .fee_slider import FeeSlider, FeeComboBox\nfrom .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog,\n                   WindowModalDialog, HelpLabel, Buttons,\n                   OkButton, InfoButton, WWLabel, CancelButton,\n                   CloseButton, MessageBoxMixin, EnterButton, import_meta_gui, export_meta_gui,\n                   filename_field, address_field, char_width_in_lineedit, webopen,\n                   TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT,\n                   getOpenFileName, getSaveFileName, ShowQRLineEdit, scan_qr_from_screenshot)\nfrom .wizard.wallet import WIF_HELP_TEXT\nfrom .history_list import HistoryList, HistoryModel\nfrom .update_checker import UpdateCheck, UpdateCheckThread\nfrom .channels_list import ChannelsList\nfrom .confirm_tx_dialog import ConfirmTxDialog, TxEditorContext\nfrom .rbf_dialog import BumpFeeDialog, DSCancelDialog\nfrom .qrreader import scan_qrcode_from_camera\nfrom .swap_dialog import SwapDialog, InvalidSwapParameters\nfrom .balance_dialog import (BalanceToolButton, COLOR_FROZEN, COLOR_UNMATURED, COLOR_UNCONFIRMED, COLOR_CONFIRMED,\n                             COLOR_LIGHTNING, COLOR_FROZEN_LIGHTNING)\n\nif TYPE_CHECKING:\n    from . import ElectrumGui\n    from electrum.submarine_swaps import SwapOffer\n    from electrum.lnchannel import Channel\n\n\nclass StatusBarButton(QToolButton):\n    # note: this class has a custom stylesheet applied in stylesheet_patcher.py\n    def __init__(self, icon, tooltip, func, sb_height):\n        QToolButton.__init__(self)\n        self.setText('')\n        self.setIcon(icon)\n        self.setToolTip(tooltip)\n        self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)\n        self.setAutoRaise(True)\n        size = max(25, round(0.9 * sb_height))\n        self.setMaximumWidth(size)\n        self.clicked.connect(self.onPress)\n        self.func = func\n        self.setIconSize(QSize(size, size))\n        self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))\n\n    def onPress(self, checked=False):\n        '''Drops the unwanted PyQt \"checked\" argument'''\n        self.func()\n\n    def keyPressEvent(self, e):\n        if e.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter]:\n            self.func()\n\n\ndef protected(func):\n    '''Password request wrapper.  The password is passed to the function\n        as the 'password' named argument.  \"None\" indicates either an\n        unencrypted wallet, or the user cancelled the password request.\n        An empty input is passed as the empty string.'''\n    def request_password(self, *args, **kwargs):\n        parent = self.top_level_window()\n        password = None\n        msg = kwargs.get('message')\n        while self._protected_requires_password():\n            password = self.wallet.get_unlocked_password() or self.password_dialog(parent=parent, msg=msg)\n            if password is None:\n                # User cancelled password input\n                return\n            try:\n                self.wallet.check_password(password)\n                break\n            except Exception as e:\n                self.show_error(str(e), parent=parent)\n                continue\n\n        kwargs['password'] = password\n        return func(self, *args, **kwargs)\n    return request_password\n\n\nclass ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):\n\n    computing_privkeys_signal = pyqtSignal()\n    show_privkeys_signal = pyqtSignal()\n    show_error_signal = pyqtSignal(str)\n    show_message_signal = pyqtSignal(str)\n    labels_changed_signal = pyqtSignal()\n\n    def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet):\n        QMainWindow.__init__(self)\n        self.gui_object = gui_object\n        self.should_stop_wallet_on_close = True\n        self.config = config = gui_object.config  # type: SimpleConfig\n        self.gui_thread = gui_object.gui_thread\n        assert wallet, \"no wallet\"\n        self.wallet = wallet\n        self._protected_requires_password = self.wallet.has_keystore_encryption\n        if wallet.has_lightning() and not self.config.cv.GUI_QT_SHOW_TAB_CHANNELS.is_set():\n            self.config.GUI_QT_SHOW_TAB_CHANNELS = True  # override default, but still allow disabling tab manually\n\n        Exception_Hook.maybe_setup(config=self.config, wallet=self.wallet)\n\n        self.network = gui_object.daemon.network  # type: Network\n        self.fx = gui_object.daemon.fx  # type: FxThread\n        self.contacts = wallet.contacts\n        self.tray = gui_object.tray\n        self.app = gui_object.app\n        self._cleaned_up = False\n        self.qr_window = None\n        self.pluginsdialog = None\n        self.showing_cert_mismatch_error = False\n        self.tl_windows = []\n        Logger.__init__(self)\n\n        self._coroutines_scheduled = {}  # type: Dict[concurrent.futures.Future, str]\n        self._coroutines_scheduled_lock = threading.Lock()\n        self.thread = TaskThread(self, self.on_error)\n\n        self.tx_notification_queue = queue.Queue()\n        self.tx_notification_last_time = 0\n\n        self.create_status_bar()\n        self.need_update = threading.Event()\n\n        self.completions = QStringListModel()\n\n        coincontrol_sb = self.create_coincontrol_statusbar()\n\n        self.tabs = tabs = QTabWidget(self)\n        self.send_tab = self.create_send_tab()\n        self.receive_tab = self.create_receive_tab()\n        self.addresses_tab = self.create_addresses_tab()\n        self.utxo_tab = self.create_utxo_tab()\n        self.console_tab = self.create_console_tab()\n        self.notes_tab = self.create_notes_tab()\n        self.contacts_tab = self.create_contacts_tab()\n        self.channels_tab = self.create_channels_tab()\n        tabs.addTab(self.create_history_tab(), read_QIcon(\"tab_history.png\"), _('History'))\n        tabs.addTab(self.send_tab, read_QIcon(\"tab_send.png\"), _('Send'))\n        tabs.addTab(self.receive_tab, read_QIcon(\"tab_receive.png\"), _('Receive'))\n\n        def add_optional_tab(tabs, tab, icon, description):\n            tab.tab_icon = icon\n            tab.tab_description = description\n            tab.tab_pos = len(tabs)\n            if tab.is_shown_cv.get():\n                tabs.addTab(tab, icon, description.replace(\"&\", \"\"))\n\n        add_optional_tab(tabs, self.addresses_tab, read_QIcon(\"tab_addresses.png\"), _(\"&Addresses\"))\n        add_optional_tab(tabs, self.channels_tab, read_QIcon(\"lightning.png\"), _(\"Channels\"))\n        add_optional_tab(tabs, self.utxo_tab, read_QIcon(\"tab_coins.png\"), _(\"Co&ins\"))\n        add_optional_tab(tabs, self.contacts_tab, read_QIcon(\"tab_contacts.png\"), _(\"Con&tacts\"))\n        add_optional_tab(tabs, self.console_tab, read_QIcon(\"tab_console.png\"), _(\"Con&sole\"))\n        add_optional_tab(tabs, self.notes_tab, read_QIcon(\"pen.png\"), _(\"&Notes\"))\n\n        tabs.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)\n\n        central_widget = QScrollArea()\n        vbox = QVBoxLayout(central_widget)\n        vbox.setContentsMargins(0, 0, 0, 0)\n        vbox.addWidget(tabs)\n        vbox.addWidget(coincontrol_sb)\n\n        self.setCentralWidget(central_widget)\n\n        self.setMinimumWidth(640)\n        self.setMinimumHeight(400)\n        if self.config.GUI_QT_WINDOW_IS_MAXIMIZED:\n            self.showMaximized()\n\n        self.setWindowIcon(read_QIcon(\"electrum.png\"))\n        self.init_menubar()\n\n        wrtabs = weakref.proxy(tabs)\n        QShortcut(QKeySequence(\"Ctrl+W\"), self, self.close)\n        QShortcut(QKeySequence(\"Ctrl+Q\"), self, self.close)\n        QShortcut(QKeySequence(\"Ctrl+R\"), self, self.update_wallet)\n        QShortcut(QKeySequence(\"F5\"), self, self.update_wallet)\n        QShortcut(QKeySequence(\"Ctrl+PgUp\"), self, lambda: wrtabs.setCurrentIndex((wrtabs.currentIndex() - 1)%wrtabs.count()))\n        QShortcut(QKeySequence(\"Ctrl+PgDown\"), self, lambda: wrtabs.setCurrentIndex((wrtabs.currentIndex() + 1)%wrtabs.count()))\n\n        for i in range(wrtabs.count()):\n            QShortcut(QKeySequence(\"Alt+\" + str(i + 1)), self, lambda i=i: wrtabs.setCurrentIndex(i))\n\n        self.app.refresh_tabs_signal.connect(self.refresh_tabs)\n        self.app.refresh_amount_edits_signal.connect(self.refresh_amount_edits)\n        self.app.update_status_signal.connect(self.update_status)\n        self.app.update_fiat_signal.connect(self.update_fiat)\n\n        self.show_error_signal.connect(self.show_error)\n        self.show_message_signal.connect(self.show_message)\n        self.history_list.setFocus()\n\n        # network callbacks\n        self.register_callbacks()\n        # wallet closing warning callbacks\n        self.closing_warning_callbacks = []  # type: List[Callable[[], Optional[str]]]\n        self.register_closing_warning_callback(self._check_ongoing_submarine_swaps_callback)\n        self.register_closing_warning_callback(self._check_ongoing_force_closures)\n        # banner may already be there\n        if self.network and self.network.banner:\n            self.console.showMessage(self.network.banner)\n\n        # update fee slider in case we missed the callback\n        #self.fee_slider.update()\n        self.load_wallet(wallet)\n\n        self.timer = QTimer(self)\n        self.timer.setInterval(500)\n        self.timer.setSingleShot(False)\n        self.timer.timeout.connect(self.timer_actions)\n        self.timer.start()\n\n        self.contacts.fetch_openalias(self.config)\n\n        # If the option hasn't been set yet\n        if not config.cv.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS.is_set():\n            choice = self.question(title=\"Electrum - \" + _(\"Enable update check\"),\n                                   msg=_(\"For security reasons we advise that you always use the latest version of Electrum.\") + \" \" +\n                                       _(\"Would you like to be notified when there is a newer version of Electrum available?\"))\n            config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = bool(choice)\n\n        self._update_check_thread = None\n        if config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS:\n            # The references to both the thread and the window need to be stored somewhere\n            # to prevent GC from getting in our way.\n            def on_version_received(v):\n                if UpdateCheck.is_newer(v):\n                    self.update_check_button.setText(_(\"Update to Electrum {} is available\").format(v))\n                    self.update_check_button.clicked.connect(lambda: self.show_update_check(v))\n                    self.update_check_button.show()\n            self._update_check_thread = UpdateCheckThread()\n            self._update_check_thread.checked.connect(on_version_received)\n            self._update_check_thread.start()\n\n    def run_coroutine_dialog(self, coro, text):\n        \"\"\" run coroutine in a waiting dialog, with a Cancel button that cancels the coroutine\"\"\"\n        from .util import RunCoroutineDialog\n        d = RunCoroutineDialog(self, text, coro)\n        return d.run()\n\n    def run_coroutine_from_thread(self, coro, name, on_result=None):\n        if self._cleaned_up:\n            self.logger.warning(f\"stopping or already stopped but run_coroutine_from_thread was called.\")\n            return\n        async def wrapper():\n            try:\n                res = await coro\n            except Exception as e:\n                self.logger.exception(\"exception in coro scheduled via window.wallet\")\n                self.show_error_signal.emit(repr(e))\n            else:\n                if on_result:\n                    on_result(res)\n            finally:\n                with self._coroutines_scheduled_lock:\n                    self._coroutines_scheduled.pop(fut)\n                self.need_update.set()\n\n        fut = asyncio.run_coroutine_threadsafe(wrapper(), self.network.asyncio_loop)\n        with self._coroutines_scheduled_lock:\n            self._coroutines_scheduled[fut] = name\n        self.need_update.set()\n\n    def toggle_lock(self):\n        if self.wallet.get_unlocked_password():\n            self.lock_wallet()\n        else:\n            msg = ' '.join([\n                _('Your wallet is locked.'),\n                _('If you unlock it, its password will not be required to sign transactions.'),\n                _('Enter your password to unlock your wallet:')\n            ])\n            self.unlock_wallet(message=msg)\n\n    def update_lock_menu(self):\n        self.lock_menu.setEnabled(self._protected_requires_password())\n        text = _('Lock') if self.wallet.get_unlocked_password() else _('Unlock')\n        self.lock_menu.setText(text)\n\n    @protected\n    def unlock_wallet(self, password, message=None):\n        self.wallet.unlock(password)\n        self.update_lock_icon()\n        self.update_lock_menu()\n        self.wallet.txbatcher.set_password_future(password)\n        icon = read_QIcon(\"unlock.png\")\n        msg = ' '.join([\n            _('Your wallet is unlocked.'),\n            _('Its password will not be required to sign transactions.'),\n        ])\n        self.show_message(msg, icon=icon.pixmap(30))\n\n    def lock_wallet(self):\n        self.wallet.lock_wallet()\n        self.update_lock_icon()\n        self.update_lock_menu()\n        icon = read_QIcon(\"lock.png\")\n        msg = ' '.join([\n            _('Your wallet is locked.'),\n            _('Its password will be required to sign transactions.'),\n        ])\n        self.show_message(msg, icon=icon.pixmap(30))\n\n    def on_fx_history(self):\n        self.history_model.refresh('fx_history')\n        self.address_list.refresh_all()\n\n    def on_fx_quotes(self):\n        self.update_status()\n        # Refresh edits with the new rate\n        edit = self.send_tab.fiat_send_e if self.send_tab.fiat_send_e.is_last_edited else self.send_tab.amount_e\n        edit.textEdited.emit(edit.text())\n        edit = self.receive_tab.fiat_receive_e if self.receive_tab.fiat_receive_e.is_last_edited else self.receive_tab.receive_amount_e\n        edit.textEdited.emit(edit.text())\n        # History tab needs updating if it used spot\n        if self.fx.history_used_spot:\n            self.history_model.refresh('fx_quotes')\n        self.address_list.refresh_all()\n\n    def toggle_tab(self, tab):\n        show = not tab.is_shown_cv.get()\n        tab.is_shown_cv.set(show)\n        if show:\n            # Find out where to place the tab\n            index = len(self.tabs)\n            for i in range(len(self.tabs)):\n                try:\n                    if tab.tab_pos < self.tabs.widget(i).tab_pos:\n                        index = i\n                        break\n                except AttributeError:\n                    pass\n            self.tabs.insertTab(index, tab, tab.tab_icon, tab.tab_description.replace(\"&\", \"\"))\n        else:\n            i = self.tabs.indexOf(tab)\n            self.tabs.removeTab(i)\n\n    def push_top_level_window(self, window):\n        '''Used for e.g. tx dialog box to ensure new dialogs are appropriately\n        parented.  This used to be done by explicitly providing the parent\n        window, but that isn't something hardware wallet prompts know.'''\n        self.tl_windows.append(window)\n\n    def pop_top_level_window(self, window):\n        self.tl_windows.remove(window)\n\n    def top_level_window(self, test_func=None):\n        '''Do the right thing in the presence of tx dialog windows'''\n        override = self.tl_windows[-1] if self.tl_windows else None\n        if override and test_func and not test_func(override):\n            override = None  # only override if ok for test_func\n        return self.top_level_window_recurse(override, test_func)\n\n    def diagnostic_name(self):\n        #return '{}:{}'.format(self.__class__.__name__, self.wallet.diagnostic_name())\n        return self.wallet.diagnostic_name()\n\n    def is_hidden(self):\n        return self.isMinimized() or self.isHidden()\n\n    def show_or_hide(self):\n        if self.is_hidden():\n            self.bring_to_top()\n        else:\n            self.hide()\n\n    def bring_to_top(self):\n        self.show()\n        self.raise_()\n\n    def on_error(self, exc_info):\n        e = exc_info[1]\n        if isinstance(e, (UserCancelled, concurrent.futures.CancelledError)):\n            pass\n        elif isinstance(e, UserFacingException):\n            self.show_error(str(e))\n        else:\n            # TODO would be nice if we just sent these to the crash reporter...\n            #      anything we don't want to send there, we should explicitly catch\n            # send_exception_to_crash_reporter(e)\n            try:\n                self.logger.error(\"on_error\", exc_info=exc_info)\n            except OSError:\n                pass  # see #4418\n            self.show_error(repr(e))\n\n    @event_listener\n    def on_event_wallet_updated(self, wallet):\n        if wallet == self.wallet:\n            self.need_update.set()\n\n    @event_listener\n    def on_event_new_transaction(self, wallet: Abstract_Wallet, tx: Transaction):\n        if wallet == self.wallet:\n            self.tx_notification_queue.put(tx)\n            self.need_update.set()\n\n    @qt_event_listener\n    def on_event_password_required(self, wallet):\n        if wallet == self.wallet:\n            self.password_required_button.show()\n\n    @qt_event_listener\n    def on_event_password_not_required(self, wallet):\n        if wallet == self.wallet:\n            self.password_required_button.hide()\n\n    def on_password_required_button_clicked(self):\n        if self.wallet.txbatcher.password_future is None:\n            return\n        txids = self.wallet.txbatcher.password_future.txids\n        labels = [ ' - %s ' % (self.wallet.get_label_for_txid(txid) or (txid[0:15] + '...')) for txid in txids ]\n        message = _('Your password is needed to sign the following transactions:') + '\\n' + '\\n'.join(labels)\n        password = self.get_password(message=message)\n        if password:\n            self.wallet.txbatcher.set_password_future(password)\n\n    @qt_event_listener\n    def on_event_status(self):\n        self.update_status()\n\n    @qt_event_listener\n    def on_event_network_updated(self, *args):\n        self.update_status()\n\n    @qt_event_listener\n    def on_event_blockchain_updated(self, *args):\n        # update the number of confirmations in history\n        self.refresh_tabs()\n\n    @qt_event_listener\n    def on_event_on_quotes(self, *args):\n        self.on_fx_quotes()\n\n    @qt_event_listener\n    def on_event_on_history(self, *args):\n        self.on_fx_history()\n\n    @qt_event_listener\n    def on_event_gossip_db_loaded(self, *args):\n        self.channels_list.gossip_db_loaded.emit(*args)\n\n    @qt_event_listener\n    def on_event_channels_updated(self, *args):\n        wallet = args[0]\n        if wallet == self.wallet:\n            self.channels_list.update_rows.emit(*args)\n\n    @qt_event_listener\n    def on_event_channel(self, *args):\n        wallet = args[0]\n        if wallet == self.wallet:\n            self.channels_list.update_single_row.emit(*args)\n            self.update_status()\n\n    @qt_event_listener\n    def on_event_banner(self, *args):\n        self.console.showMessage(args[0])\n\n    @qt_event_listener\n    def on_event_adb_set_future_tx(self, adb, txid):\n        if adb == self.wallet.adb:\n            self.history_model.refresh('set_future_tx')\n            self.utxo_list.refresh_all()  # for coin frozen status\n            self.update_status()  # frozen balance\n\n    @qt_event_listener\n    def on_event_verified(self, *args):\n        wallet, tx_hash, tx_mined_status = args\n        if wallet == self.wallet:\n            self.history_model.update_tx_mined_status(tx_hash, tx_mined_status)\n\n    @qt_event_listener\n    def on_event_fee_histogram(self, *args):\n        self.history_model.on_fee_histogram()\n\n    @qt_event_listener\n    def on_event_ln_gossip_sync_progress(self, *args):\n        self.update_lightning_icon()\n\n    @qt_event_listener\n    def on_event_cert_mismatch(self, *args):\n        self.show_cert_mismatch_error()\n\n    @qt_event_listener\n    def on_event_tor_probed(self, is_tor):\n        self.tor_button.setVisible(is_tor)\n\n    @qt_event_listener\n    def on_event_proxy_set(self, *args):\n        self.tor_button.setVisible(False)\n\n    @qt_event_listener\n    def on_event_recently_opened_wallets_update(self, *args):\n        self.update_recently_opened_menu()\n\n    def close_wallet(self):\n        if self.wallet:\n            self.logger.info(f'close_wallet {self.wallet.storage.path}')\n        run_hook('close_wallet', self.wallet)\n\n    @profiler\n    def load_wallet(self, wallet: Abstract_Wallet):\n        self.update_recently_opened_menu()\n        if wallet.has_lightning():\n            util.trigger_callback('channels_updated', wallet)\n        self.need_update.set()\n        # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized\n        # update menus\n        self.seed_menu.setEnabled(self.wallet.has_seed())\n        self.update_lock_icon()\n        self.update_buttons_on_seed()\n        self.update_console()\n        self.receive_tab.do_clear()\n        self.receive_tab.request_list.update()\n        self.channels_list.update()\n        self.tabs.show()\n        self.init_geometry()\n        if self.config.GUI_QT_HIDE_ON_STARTUP and self.gui_object.tray.isVisible():\n            self.hide()\n        else:\n            self.show()\n        self.watching_only_changed()\n        run_hook('load_wallet', wallet, self)\n        try:\n            wallet.try_detecting_internal_addresses_corruption()\n        except InternalAddressCorruption as e:\n            self.show_error(str(e))\n            send_exception_to_crash_reporter(e)\n\n    def init_geometry(self):\n        # note: does not support multiple monitors well\n        winpos = self.wallet.db.get(\"winpos-qt\")\n        try:\n            winrect = QRect(*winpos)\n        except TypeError:\n            winrect = None\n        screen = self.app.primaryScreen().geometry()\n        if winrect and screen.contains(winrect):\n            self.setGeometry(winrect)\n        else:\n            self.logger.info(\"using default geometry\")\n            self.setGeometry(100, 100, 840, 400)\n\n    @classmethod\n    def get_app_name_and_version_str(cls) -> str:\n        name = \"Electrum\"\n        if constants.net.TESTNET:\n            name += \" \" + constants.net.NET_NAME.capitalize()\n        return f\"{name} {ELECTRUM_VERSION}\"\n\n    def watching_only_changed(self):\n        name_and_version = self.get_app_name_and_version_str()\n        title = f\"{name_and_version}  -  {self.wallet.basename()}\"\n        extra = [self.wallet.db.get('wallet_type', '?')]\n        if self.wallet.is_watching_only():\n            extra.append(_('watching only'))\n        title += '  [%s]'% ', '.join(extra)\n        self.setWindowTitle(title)\n        self.password_menu.setEnabled(self.wallet.may_have_password())\n        self.import_privkey_menu.setVisible(self.wallet.can_import_privkey())\n        self.import_address_menu.setVisible(self.wallet.can_import_address())\n        self.export_menu.setEnabled(self.wallet.can_export())\n\n    def warn_if_watching_only(self):\n        if self.wallet.is_watching_only():\n            msg = ' '.join([\n                _(\"This wallet is watching-only.\"),\n                _(\"This means you will not be able to spend Bitcoins with it.\"),\n                _(\"Make sure you own the seed phrase or the private keys, before you request Bitcoins to be sent to this wallet.\")\n            ])\n            self.show_warning(msg, title=_('Watch-only wallet'))\n\n    def warn_if_testnet(self):\n        if not constants.net.TESTNET:\n            return\n        # user might have opted out already\n        if self.config.DONT_SHOW_TESTNET_WARNING:\n            return\n        # only show once per process lifecycle\n        if getattr(self.gui_object, '_warned_testnet', False):\n            return\n        self.gui_object._warned_testnet = True\n        msg = ''.join([\n            _(\"You are in testnet mode.\"), ' ',\n            _(\"Testnet coins are worthless.\"), '\\n',\n            _(\"Testnet is separate from the main Bitcoin network. It is used for testing.\")\n        ])\n        cb = QCheckBox(_(\"Don't show this again.\"))\n        cb_checked = False\n        def on_cb(_x):\n            nonlocal cb_checked\n            cb_checked = cb.isChecked()\n        cb.stateChanged.connect(on_cb)\n        self.show_warning(msg, title=_('Testnet'), checkbox=cb)\n        if cb_checked:\n            self.config.DONT_SHOW_TESTNET_WARNING = True\n\n    def open_wallet(self):\n        try:\n            wallet_folder = self.get_wallet_folder()\n        except FileNotFoundError as e:\n            self.show_error(str(e))\n            return\n        filename, __ = QFileDialog.getOpenFileName(self, \"Select your wallet file\", wallet_folder)\n        if not filename:\n            return\n        self.gui_object.new_window(filename)\n\n    def select_backup_dir(self, b):\n        name = self.config.WALLET_BACKUP_DIRECTORY or \"\"\n        dirname = QFileDialog.getExistingDirectory(self, \"Select your wallet backup directory\", name)\n        if dirname:\n            self.config.WALLET_BACKUP_DIRECTORY = dirname\n            self.backup_dir_e.setText(dirname)\n\n    def backup_wallet(self):\n        d = WindowModalDialog(self, _(\"File Backup\"))\n        vbox = QVBoxLayout(d)\n        grid = QGridLayout()\n        backup_help = \"\"\n        backup_dir = self.config.WALLET_BACKUP_DIRECTORY\n        backup_dir_label = HelpLabel(_('Backup directory') + ':', backup_help)\n        msg = _('Please select a backup directory')\n        if self.wallet.has_lightning() and self.wallet.lnworker.channels:\n            msg += '\\n\\n' + ' '.join([\n                _(\"Note that lightning channels will be converted to channel backups.\"),\n                _(\"You cannot use channel backups to perform lightning payments.\"),\n                _(\"Channel backups can only be used to request your channels to be closed.\")\n            ])\n        self.backup_dir_e = QPushButton(backup_dir)\n        self.backup_dir_e.clicked.connect(self.select_backup_dir)\n        grid.addWidget(backup_dir_label, 1, 0)\n        grid.addWidget(self.backup_dir_e, 1, 1)\n        vbox.addLayout(grid)\n        vbox.addWidget(WWLabel(msg))\n        vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))\n        if not d.exec():\n            return False\n        backup_dir = self.config.get_backup_dir()\n        if backup_dir is None:\n            self.show_message(_(\"You need to configure a backup directory in your preferences\"), title=_(\"Backup not configured\"))\n            return\n        try:\n            new_path = self.wallet.save_backup(backup_dir)\n        except BaseException as reason:\n            self.show_critical(_(\"Electrum was unable to copy your wallet file to the specified location.\") + \"\\n\" + str(reason), title=_(\"Unable to create backup\"))\n            return\n        msg = _(\"A copy of your wallet file was created in\")+\" '%s'\" % str(new_path)\n        self.show_message(msg, title=_(\"Wallet backup created\"))\n        return True\n\n    def update_recently_opened_menu(self):\n        recent = self.config.RECENTLY_OPEN_WALLET_FILES or []\n        self.recently_visited_menu.clear()\n        for i, k in enumerate(recent):\n            b = os.path.basename(k)\n\n            def loader(k):\n                return lambda: self.gui_object.new_window(k)\n            self.recently_visited_menu.addAction(b, loader(k)).setShortcut(QKeySequence(\"Ctrl+%d\" % (i+1)))\n        self.recently_visited_menu.setEnabled(bool(len(recent)))\n\n    def get_wallet_folder(self):\n        return os.path.abspath(self.config.get_datadir_wallet_path())\n\n    def new_wallet(self):\n        try:\n            wallet_folder = self.get_wallet_folder()\n        except FileNotFoundError as e:\n            self.show_error(str(e))\n            return\n        try:\n            filename = get_new_wallet_name(wallet_folder)\n        except OSError as e:\n            self.logger.exception(\"\")\n            self.show_error(repr(e))\n            path = self.config.get_fallback_wallet_path()\n        else:\n            path = os.path.join(wallet_folder, filename)\n        self.gui_object.start_new_window(path, uri=None, force_wizard=True)\n\n    def init_menubar(self):\n        menubar = QMenuBar()\n\n        self.file_menu = menubar.addMenu(_(\"&File\"))\n        self.recently_visited_menu = self.file_menu.addMenu(_(\"&Recently open\"))\n        self.file_menu.addAction(_(\"&Open\"), self.open_wallet).setShortcut(QKeySequence.StandardKey.Open)\n        self.file_menu.addAction(_(\"&New/Restore\"), self.new_wallet).setShortcut(QKeySequence.StandardKey.New)\n        self.file_menu.addAction(_(\"&Save backup\"), self.backup_wallet).setShortcut(QKeySequence.StandardKey.SaveAs)\n        self.file_menu.addAction(_(\"Delete\"), self.remove_wallet)\n        self.file_menu.addSeparator()\n        self.file_menu.addAction(_(\"&Quit\"), self.close)\n\n        self.wallet_menu = menubar.addMenu(_(\"&Wallet\"))\n        self.wallet_menu.addAction(_(\"&Information\"), self.show_wallet_info)\n        self.wallet_menu.addSeparator()\n\n        self.password_menu = self.wallet_menu.addAction(_(\"&Password\"), self.change_password_dialog)\n        self.lock_menu = self.wallet_menu.addAction(_(\"&Unlock\"), self.toggle_lock)\n        self.update_lock_menu()\n        self.seed_menu = self.wallet_menu.addAction(_(\"&Seed\"), self.show_seed_dialog)\n        self.private_keys_menu = self.wallet_menu.addMenu(_(\"&Private keys\"))\n        self.private_keys_menu.addAction(_(\"&Sweep\"), self.sweep_key_dialog)\n        self.import_privkey_menu = self.private_keys_menu.addAction(_(\"&Import\"), self.do_import_privkey)\n        self.export_menu = self.private_keys_menu.addAction(_(\"&Export\"), self.export_privkeys_dialog)\n        self.import_address_menu = self.wallet_menu.addAction(_(\"Import addresses\"), self.import_addresses)\n\n        self.labels_menu = self.wallet_menu.addMenu(_(\"&Labels\"))\n        self.labels_menu.addAction(_(\"&Import\"), self.do_import_labels)\n        self.labels_menu.addAction(_(\"&Export\"), self.do_export_labels)\n\n        self.wallet_menu.addAction(_(\"Find\"), self.toggle_search).setShortcut(QKeySequence(\"Ctrl+F\"))\n        self.wallet_menu.addSeparator()\n\n        def add_toggle_action(tab):\n            is_shown = tab.is_shown_cv.get()\n            tab.menu_action = self.view_menu.addAction(tab.tab_description, lambda: self.toggle_tab(tab))\n            tab.menu_action.setCheckable(True)\n            tab.menu_action.setChecked(is_shown)\n        self.view_menu = menubar.addMenu(_(\"&View\"))\n        add_toggle_action(self.addresses_tab)\n        add_toggle_action(self.utxo_tab)\n        add_toggle_action(self.channels_tab)\n        add_toggle_action(self.contacts_tab)\n        add_toggle_action(self.console_tab)\n        add_toggle_action(self.notes_tab)\n\n        self.tools_menu = menubar.addMenu(_(\"&Tools\"))  # type: QMenu\n        preferences_action = self.tools_menu.addAction(_(\"Preferences\"), self.settings_dialog)  # type: QAction\n        if sys.platform == 'darwin':\n            # \"Settings\"/\"Preferences\" are all reserved keywords in macOS.\n            # preferences_action will get picked up based on name (and put into a standardized location,\n            # and given a standard reserved hotkey)\n            # Hence, this menu item will be at a \"uniform location re macOS processes\"\n            preferences_action.setMenuRole(QAction.MenuRole.PreferencesRole)  # make sure OS recognizes it as preferences\n            # Add another preferences item, to also have a \"uniform location for Electrum between different OSes\"\n            self.tools_menu.addAction(_(\"Electrum preferences\"), self.settings_dialog)\n\n        self.tools_menu.addAction(_(\"&Network\"), self.gui_object.show_network_dialog).setEnabled(bool(self.network))\n        self.tools_menu.addAction(_(\"&Plugins\"), self.gui_object.show_plugins_dialog)\n        self.tools_menu.addSeparator()\n        self.tools_menu.addAction(_(\"&Sign/verify message\"), self.sign_verify_message)\n        self.tools_menu.addAction(_(\"&Encrypt/decrypt message\"), self.encrypt_message)\n        self.tools_menu.addSeparator()\n\n        raw_transaction_menu = self.tools_menu.addMenu(_(\"&Load transaction\"))\n        raw_transaction_menu.addAction(_(\"&From file\"), self.do_process_from_file)\n        raw_transaction_menu.addAction(_(\"&From text\"), self.do_process_from_text)\n        raw_transaction_menu.addAction(_(\"&From the blockchain\"), self.do_process_from_txid)\n        raw_transaction_menu.addAction(_(\"&From QR code\"), self.read_tx_from_qrcode)\n        self.raw_transaction_menu = raw_transaction_menu\n\n        self.help_menu = menubar.addMenu(_(\"&Help\"))\n        if sys.platform != 'darwin':\n            self.help_menu.addAction(_(\"&About\"), self.show_about)\n        else:\n            # macOS reserves the \"About\" menu item name, similarly to \"Preferences\" (see above).\n            # The \"About\" keyword seems even more strictly locked down:\n            # not allowed as either a prefix or a suffix.\n            about_action = QAction(self)\n            about_action.triggered.connect(self.show_about)\n            about_action.setMenuRole(QAction.MenuRole.AboutRole)  # make sure OS recognizes it as \"About\"\n            self.help_menu.addAction(about_action)\n        self.help_menu.addAction(_(\"&Changelog\"), lambda: webopen(constants.RELEASE_NOTES_URL))\n        self.help_menu.addAction(_(\"&Check for updates\"), self.show_update_check)\n        self.help_menu.addAction(_(\"&Official website\"), lambda: webopen(\"https://electrum.org\"))\n        self.help_menu.addSeparator()\n        self.help_menu.addAction(_(\"&Documentation\"), lambda: webopen(\"http://docs.electrum.org/\")).setShortcut(QKeySequence.StandardKey.HelpContents)\n        if not constants.net.TESTNET:\n            self.help_menu.addAction(_(\"&Bitcoin Paper\"), self.show_bitcoin_paper)\n        self.help_menu.addAction(_(\"&Report Bug\"), self.show_report_bug)\n        self.help_menu.addSeparator()\n        if self.network:\n            self.help_menu.addAction(_(\"&Donate to server\"), self.donate_to_server)\n\n        run_hook('init_menubar', self)\n        self.setMenuBar(menubar)\n\n    def donate_to_server(self):\n        d = self.network.get_donation_address()\n        if d:\n            self.show_send_tab()\n            host = self.network.get_parameters().server.host\n            self.handle_payment_identifier('bitcoin:%s?message=donation for %s' % (d, host))\n        else:\n            self.show_error(_('No donation address for this server'))\n\n    def show_about(self):\n        QMessageBox.about(self, \"Electrum\",\n                          (_(\"Version\")+\" %s\" % ELECTRUM_VERSION + \"\\n\\n\" +\n                           _(\"Electrum's focus is speed, with low resource usage and simplifying Bitcoin.\") + \" \" +\n                           _(\"You do not need to perform regular backups, because your wallet can be \"\n                              \"recovered from a secret phrase that you can memorize or write on paper.\") + \" \" +\n                           _(\"Startup times are instant because it operates in conjunction with high-performance \"\n                              \"servers that handle the most complicated parts of the Bitcoin system.\") + \"\\n\\n\" +\n                           _(\"Uses icons from the Icons8 icon pack (icons8.com).\")))\n\n    def show_bitcoin_paper(self):\n        filename = os.path.join(self.config.path, 'bitcoin.pdf')\n        if not os.path.exists(filename):\n            def fetch_bitcoin_paper():\n                s = self._fetch_tx_from_network(\"54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713\")\n                if not s:\n                    raise concurrent.futures.CancelledError\n                s = s.split(\"0100000000000000\")[1:-1]\n                out = ''.join(x[6:136] + x[138:268] + x[270:400] if len(x) > 136 else x[6:] for x in s)[16:-20]\n                with open(filename, 'wb') as f:\n                    f.write(bytes.fromhex(out))\n            WaitingDialog(\n                self,\n                _(\"Fetching Bitcoin Paper...\"),\n                fetch_bitcoin_paper,\n                on_success=lambda _: webopen('file:///' + filename),\n                on_error=self.on_error,\n            )\n            return\n        webopen('file:///' + filename)\n\n    def show_update_check(self, version=None):\n        self.gui_object._update_check = UpdateCheck(latest_version=version)\n\n    def show_report_bug(self):\n        msg = ' '.join([\n            _(\"Please report any bugs as issues on github:<br/>\"),\n            f'''<a href=\"{constants.GIT_REPO_ISSUES_URL}\">{constants.GIT_REPO_ISSUES_URL}</a><br/><br/>''',\n            _(\"Before reporting a bug, upgrade to the most recent version of Electrum (latest release or git HEAD), and include the version number in your report.\"),\n            _(\"Try to explain not only what the bug is, but how it occurs.\")\n         ])\n        self.show_message(msg, title=\"Electrum - \" + _(\"Reporting Bugs\"), rich_text=True)\n\n    def notify_transactions(self):\n        if self.tx_notification_queue.qsize() == 0:\n            return\n        if not self.wallet.is_up_to_date():\n            return  # no notifications while syncing\n        now = time.time()\n        rate_limit = 20  # seconds\n        if self.tx_notification_last_time + rate_limit > now:\n            return\n        self.tx_notification_last_time = now\n        self.logger.info(\"Notifying GUI about new transactions\")\n        txns = []\n        while True:\n            try:\n                txns.append(self.tx_notification_queue.get_nowait())\n            except queue.Empty:\n                break\n\n        for notification in self.wallet.get_user_notifications_for_new_txns(txns):\n            self.notify(notification)\n\n    def notify(self, message):\n        if self.tray:\n            self.tray.showMessage(\"Electrum\", message, read_QIcon(\"electrum_dark_icon\"), 20000)\n\n    def timer_actions(self):\n        # refresh invoices and requests because they show ETA\n        self.receive_tab.request_list.refresh_all()\n        self.send_tab.invoice_list.refresh_all()\n        # Note this runs in the GUI thread\n        if self.need_update.is_set():\n            self.need_update.clear()\n            self.update_wallet()\n        elif not self.wallet.is_up_to_date():\n            # this updates \"synchronizing\" progress\n            self.update_status()\n        # resolve aliases\n        # FIXME this might do blocking network calls that has a timeout of several seconds\n        # self.send_tab.payto_e.on_timer_check_text()\n        self.notify_transactions()\n\n    def format_amount(\n        self,\n        amount_sat,\n        is_diff=False,\n        whitespaces=False,\n        *,\n        add_thousands_sep: bool = None,\n    ) -> str:\n        \"\"\"Formats amount as string, converting to desired unit.\n        E.g. 500_000 -> '0.005'\n        \"\"\"\n        return self.config.format_amount(\n            amount_sat,\n            is_diff=is_diff,\n            whitespaces=whitespaces,\n            add_thousands_sep=add_thousands_sep,\n        )\n\n    def format_amount_and_units(self, amount_sat, *, timestamp: int = None) -> str:\n        \"\"\"Returns string with both bitcoin and fiat amounts, in desired units.\n        E.g. 500_000 -> '0.005 BTC (191.42 EUR)'\n        \"\"\"\n        text = self.config.format_amount_and_units(amount_sat)\n        fiat = self.fx.format_amount_and_units(amount_sat, timestamp=timestamp) if self.fx else None\n        if text and fiat:\n            text += f' ({fiat})'\n        return text\n\n    def format_fiat_and_units(self, amount_sat) -> str:\n        \"\"\"Returns string of FX fiat amount, in desired units.\n        E.g. 500_000 -> '191.42 EUR'\n        \"\"\"\n        return self.fx.format_amount_and_units(amount_sat) if self.fx else ''\n\n    def format_fee_rate(self, fee_rate) -> str:\n        \"\"\"fee_rate is in sat/kvByte.\"\"\"\n        return self.config.format_fee_rate(fee_rate)\n\n    def get_decimal_point(self):\n        return self.config.BTC_AMOUNTS_DECIMAL_POINT\n\n    def base_unit(self):\n        return self.config.get_base_unit()\n\n    def connect_fields(self, btc_e, fiat_e):\n\n        def edit_changed(edit):\n            if edit.follows:\n                return\n            edit.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())\n            fiat_e.is_last_edited = (edit == fiat_e)\n            amount = edit.get_amount()\n            rate = self.fx.exchange_rate() if self.fx else Decimal('NaN')\n            if rate.is_nan() or amount is None:\n                if edit is fiat_e:\n                    btc_e.setText(\"\")\n                else:\n                    fiat_e.setText(\"\")\n            else:\n                if edit is fiat_e:\n                    btc_e.follows = True\n                    btc_e.setAmount(int(amount / Decimal(rate) * COIN))\n                    btc_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())\n                    btc_e.follows = False\n                else:\n                    fiat_e.follows = True\n                    fiat_e.setText(self.fx.ccy_amount_str(\n                        amount * Decimal(rate) / COIN, add_thousands_sep=False))\n                    fiat_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())\n                    fiat_e.follows = False\n\n        btc_e.follows = False\n        fiat_e.follows = False\n        fiat_e.textChanged.connect(partial(edit_changed, fiat_e))\n        btc_e.textChanged.connect(partial(edit_changed, btc_e))\n        fiat_e.is_last_edited = False\n\n    def update_status(self):\n        if not self.wallet:\n            return\n\n        network_text = \"\"\n        balance_text = \"\"\n\n        if self.tor_button:\n            self.tor_button.setVisible(self.network and bool(self.network.is_proxy_tor))\n\n        if self.network is None:\n            network_text = _(\"Offline\")\n            icon = read_QIcon(\"status_disconnected.png\")\n\n        elif self.network.is_connected():\n            server_height = self.network.get_server_height()\n            server_lag = self.network.get_local_height() - server_height\n            fork_str = \"_fork\" if len(self.network.get_blockchains())>1 else \"\"\n            # Server height can be 0 after switching to a new server\n            # until we get a headers subscription request response.\n            # Display the synchronizing message in that case.\n            if not self.wallet.is_up_to_date() or server_height == 0:\n                num_sent, num_answered = self.wallet.adb.get_history_sync_state_details()\n                network_text = (\"{} ({}/{})\"\n                                .format(_(\"Synchronizing...\"), num_answered, num_sent))\n                icon = read_QIcon(\"status_waiting.png\")\n            elif server_lag > 1:\n                network_text = _(\"Server is lagging ({} blocks)\").format(server_lag)\n                icon = read_QIcon(\"status_lagging%s.png\"%fork_str)\n            else:\n                network_text = _(\"Connected\")\n                p_bal = self.wallet.get_balances_for_piechart()\n                self.balance_label.update_list(\n                    [\n                        (_('Frozen'), COLOR_FROZEN, p_bal.frozen),\n                        (_('Unmatured'), COLOR_UNMATURED, p_bal.unmatured),\n                        (_('Unconfirmed'), COLOR_UNCONFIRMED, p_bal.unconfirmed),\n                        (_('On-chain'), COLOR_CONFIRMED, p_bal.confirmed),\n                        (_('Lightning'), COLOR_LIGHTNING, p_bal.lightning),\n                        (_('Lightning frozen'), COLOR_FROZEN_LIGHTNING, p_bal.lightning_frozen),\n                    ],\n                    warning = self.wallet.is_low_reserve(),\n                )\n                balance = p_bal.total()\n                balance_text =  _(\"Balance\") + \": %s \"%(self.format_amount_and_units(balance))\n                # append fiat balance and price\n                if self.fx.is_enabled():\n                    balance_text += self.fx.get_fiat_status_text(balance,\n                        self.base_unit(), self.get_decimal_point()) or ''\n                if not self.network.proxy or not self.network.proxy.enabled:\n                    icon = read_QIcon(\"status_connected%s.png\"%fork_str)\n                else:\n                    icon = read_QIcon(\"status_connected_proxy%s.png\"%fork_str)\n        else:\n            if self.network.proxy and self.network.proxy.enabled:\n                network_text = \"{} ({})\".format(_(\"Not connected\"), _(\"proxy enabled\"))\n            else:\n                network_text = _(\"Not connected\")\n            icon = read_QIcon(\"status_disconnected.png\")\n\n        if self.tray:\n            # note: don't include balance in systray tooltip, as some OSes persist tooltips,\n            #       hence \"leaking\" the wallet balance (see #5665)\n            name_and_version = self.get_app_name_and_version_str()\n            self.tray.setToolTip(f\"{name_and_version} ({network_text})\")\n        self.balance_label.setText(balance_text or network_text)\n        if self.status_button:\n            self.status_button.setIcon(icon)\n\n        num_tasks = self.num_tasks()\n        if num_tasks == 0:\n            name = ''\n        elif num_tasks == 1:\n            with self._coroutines_scheduled_lock:\n                name = list(self._coroutines_scheduled.values())[0] + '...'\n        else:\n            name = f\"{num_tasks} \" + _('tasks') + '...'\n        self.tasks_label.setText(name)\n        self.tasks_label.setVisible(num_tasks > 0)\n\n    def num_tasks(self):\n        # For the moment, all the coroutines in this set are outgoing LN payments,\n        # so we can use this to disable buttons for rebalance/swap suggestions\n        return len(self._coroutines_scheduled)\n\n    def update_wallet(self):\n        self.update_status()\n        if self.wallet.is_up_to_date() or not self.network or not self.network.is_connected():\n            self.update_tabs()\n\n    def update_tabs(self, wallet=None):\n        if wallet is None:\n            wallet = self.wallet\n        if wallet != self.wallet:\n            return\n        self.history_model.refresh('update_tabs')\n        self.receive_tab.request_list.update()\n        self.receive_tab.update_current_request()\n        self.send_tab.invoice_list.update()\n        self.address_list.update()\n        self.utxo_list.update()\n        self.contact_list.update()\n        self.channels_list.update_rows.emit(wallet)\n        self.update_completions()\n\n    def refresh_tabs(self, wallet=None):\n        self.history_model.refresh('refresh_tabs')\n        self.receive_tab.request_list.refresh_all()\n        self.send_tab.invoice_list.refresh_all()\n        self.address_list.refresh_all()\n        self.utxo_list.refresh_all()\n        self.contact_list.refresh_all()\n        self.channels_list.update_rows.emit(self.wallet)\n\n    def create_channels_tab(self):\n        self.channels_list = ChannelsList(self)\n        tab = self.create_list_tab(self.channels_list)\n        tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_CHANNELS\n        return tab\n\n    def create_history_tab(self):\n        self.history_model = HistoryModel(self)\n        self.history_list = l = HistoryList(self, self.history_model)\n        self.history_model.set_view(self.history_list)\n        l.searchable_list = l\n        tab = self.create_list_tab(self.history_list)\n        return tab\n\n    def show_address(self, addr: str, *, parent: QWidget = None):\n        from . import address_dialog\n        d = address_dialog.AddressDialog(self, addr, parent=parent)\n        d.exec()\n\n    def show_utxo(self, utxo):\n        from . import utxo_dialog\n        d = utxo_dialog.UTXODialog(self, utxo)\n        d.exec()\n\n    def show_channel_details(self, chan):\n        from .channel_details import ChannelDetailsDialog\n        ChannelDetailsDialog(self, chan).show()\n\n    def show_transaction(\n        self,\n        tx: Transaction,\n        *,\n        prompt_if_complete_unsaved: bool = True,\n        external_keypairs: Mapping[bytes, bytes] = None,\n        invoice: Invoice = None,\n        on_closed: Callable[[Optional[Transaction]], None] = None,\n        show_sign_button: bool = True,\n        show_broadcast_button: bool = True,\n    ):\n        show_transaction(\n            tx,\n            parent=self,\n            prompt_if_complete_unsaved=prompt_if_complete_unsaved,\n            external_keypairs=external_keypairs,\n            invoice=invoice,\n            on_closed=on_closed,\n            show_sign_button=show_sign_button,\n            show_broadcast_button=show_broadcast_button,\n        )\n\n    def show_lightning_transaction(self, tx_item):\n        from .lightning_tx_dialog import LightningTxDialog\n        d = LightningTxDialog(self, tx_item)\n        d.show()\n\n    def create_receive_tab(self):\n        from .receive_tab import ReceiveTab\n        return ReceiveTab(self)\n\n    def do_copy(self, text: str, *, title: str = None) -> None:\n        self.gui_object.do_copy(text, title=title)\n\n    def show_tooltip_after_delay(self, message):\n        # tooltip cannot be displayed immediately when called from a menu; wait 200ms\n        QTimer.singleShot(200, lambda: QToolTip.showText(QCursor.pos(), message, self))\n\n    def toggle_qr_window(self):\n        from . import qrwindow\n        if not self.qr_window:\n            self.qr_window = qrwindow.QR_Window(self)\n            self.qr_window.setVisible(True)\n            self.qr_window_geometry = self.qr_window.geometry()\n        else:\n            if not self.qr_window.isVisible():\n                self.qr_window.setVisible(True)\n                self.qr_window.setGeometry(self.qr_window_geometry)\n            else:\n                self.qr_window_geometry = self.qr_window.geometry()\n                self.qr_window.setVisible(False)\n        self.receive_tab.update_receive_qr_window()\n\n    def show_send_tab(self):\n        self.tabs.setCurrentIndex(self.tabs.indexOf(self.send_tab))\n\n    def show_receive_tab(self):\n        self.tabs.setCurrentIndex(self.tabs.indexOf(self.receive_tab))\n\n    def create_send_tab(self):\n        from .send_tab import SendTab\n        return SendTab(self)\n\n    def get_contact_payto(self, key):\n        _type, label = self.contacts.get(key)\n        return label + '  <' + key + '>' if _type == 'address' else key\n\n    def update_completions(self):\n        l = [self.get_contact_payto(key) for key in self.contacts.keys()]\n        self.completions.setStringList(l)\n\n    @protected\n    def protect(self, func, args, password):\n        return func(*args, password)\n\n    def run_swap_dialog(\n        self,\n        is_reverse: Optional[bool] = None,\n        recv_amount_sat_or_max: Optional[Union[int, str]] = None,\n        channels: Optional[Sequence['Channel']] = None,\n    ) -> bool:\n        if not self.network:\n            self.show_error(_(\"You are offline.\"))\n            return False\n        if not self.wallet.lnworker:\n            self.show_error(_('Lightning is disabled'))\n            return False\n        if not self.wallet.lnworker.num_sats_can_send() and not self.wallet.lnworker.num_sats_can_receive():\n            self.show_error(_(\"You do not have liquidity in your active channels.\"))\n            return False\n\n        transport = self.create_sm_transport()\n        if not transport:\n            return False\n\n        with transport:\n            if not self.initialize_swap_manager(transport):\n                return False\n            d = SwapDialog(\n                self,\n                transport,\n                is_reverse=is_reverse,\n                recv_amount_sat_or_max=recv_amount_sat_or_max,\n                channels=channels\n            )\n            try:\n                return d.run(transport)\n            except InvalidSwapParameters as e:\n                self.show_error(str(e))\n                return False\n            except UserCancelled:\n                return False\n\n    def create_sm_transport(self) -> Optional['SwapServerTransport']:\n        sm = self.wallet.lnworker.swap_manager\n        if sm.is_server:\n            self.show_error(_('Swap server is active'))\n            return None\n\n        if self.network is None:\n            return None\n\n        if not self.config.SWAPSERVER_URL and not self.config.SWAPSERVER_NPUB:\n            if not self.question('\\n'.join([\n                    _('Electrum uses Nostr in order to find liquidity providers.'),\n                    _('Do you want to enable Nostr?'),\n            ])):\n                return None\n\n        return sm.create_transport()\n\n    def initialize_swap_manager(self, transport: 'SwapServerTransport'):\n        sm = self.wallet.lnworker.swap_manager\n        if not sm.is_initialized.is_set():\n            async def wait_until_initialized():\n                timeout = transport.connect_timeout + 1\n                try:\n                    await asyncio.wait_for(sm.is_initialized.wait(), timeout=timeout)\n                except asyncio.TimeoutError:\n                    return\n            try:\n                self.run_coroutine_dialog(wait_until_initialized(), _('Please wait...'))\n            except UserCancelled:\n                return False\n            except Exception as e:\n                self.show_error(str(e))\n                return False\n\n        if not sm.is_initialized.is_set():\n            if not self.config.SWAPSERVER_URL:\n                if not self.choose_swapserver_dialog(transport):\n                    return False\n            else:\n                self.show_error(f'Could not contact swap server at {self.config.SWAPSERVER_URL:}')\n                return False\n\n        assert sm.is_initialized.is_set()\n        return True\n\n    def choose_swapserver_dialog(self, transport: NostrTransport) -> bool:\n        assert isinstance(transport, NostrTransport)\n        if not transport.is_connected.is_set():\n            self.show_message(\n                '\\n'.join([\n                    _('Could not connect to a Nostr relay.'),\n                    _('Please check your relays and network connection'),\n                ]))\n            return False\n        recent_offers = transport.get_recent_offers()\n        if not recent_offers:\n            self.show_message(\n                '\\n'.join([\n                    _('Could not find a swap provider.'),\n                ]))\n            return False\n        sm = self.wallet.lnworker.swap_manager\n        from .swap_dialog import SwapServerDialog\n        d = SwapServerDialog(self, recent_offers)\n        choice = d.run()\n        if choice is None:\n            return False\n        self.config.SWAPSERVER_NPUB = choice\n        offer = transport.get_offer(choice)\n        sm.update_pairs(offer.pairs)\n        return True\n\n    @qt_event_listener\n    def on_event_request_status(self, wallet, key, status):\n        if wallet != self.wallet:\n            return\n        req = self.wallet.get_request(key)\n        if req is None:\n            return\n        if status == PR_PAID:\n            # FIXME notification should only be shown if request was not PAID before\n            msg = _('Payment received')\n            amount = req.get_amount_sat()\n            if amount:\n                msg += ': ' + self.format_amount_and_units(amount)\n            msg += '\\n' + req.get_message()\n            self.notify(msg)\n            self.receive_tab.request_list.delete_item(key)\n            self.receive_tab.do_clear()\n            self.need_update.set()\n        else:\n            self.receive_tab.request_list.refresh_item(key)\n\n    @qt_event_listener\n    def on_event_invoice_status(self, wallet, key, status):\n        if wallet != self.wallet:\n            return\n        if status == PR_PAID:\n            self.send_tab.invoice_list.delete_item(key)\n        else:\n            self.send_tab.invoice_list.refresh_item(key)\n\n    @qt_event_listener\n    def on_event_payment_succeeded(self, wallet, key):\n        # sent by lnworker, redundant with invoice_status\n        if wallet != self.wallet:\n            return\n        description = self.wallet.get_label_for_rhash(key)\n        self.notify(_('Payment sent') + '\\n\\n' + description)\n        self.need_update.set()\n\n    @qt_event_listener\n    def on_event_payment_failed(self, wallet, key, reason):\n        if wallet != self.wallet:\n            return\n        description = self.wallet.get_label_for_rhash(key)\n        self.notify(_('Payment failed') + '\\n\\n' + description + '\\n\\n' + reason)\n\n    def get_coins(self, **kwargs) -> Sequence[PartialTxInput]:\n        coins = self.get_manually_selected_coins()\n        if coins is not None:\n            return coins\n        else:\n            return self.wallet.get_spendable_coins(None, **kwargs)\n\n    def get_manually_selected_coins(self) -> Optional[Sequence[PartialTxInput]]:\n        \"\"\"Return a list of selected coins or None.\n        Note: None means selection is not being used,\n              while an empty sequence means the user specifically selected that.\n        \"\"\"\n        return self.utxo_list.get_spend_list()\n\n    def broadcast_or_show(self, tx: Transaction, *, invoice: 'Invoice' = None):\n        if not tx.is_complete():\n            self.show_transaction(tx, invoice=invoice)\n            return\n        if not self.network:\n            self.show_error(_(\"You can't broadcast a transaction without a live network connection.\"))\n            self.show_transaction(tx, invoice=invoice)\n            return\n        self.broadcast_transaction(tx, invoice=invoice)\n\n    def broadcast_transaction(self, tx: Transaction, *, invoice: Invoice = None):\n        self.send_tab.broadcast_transaction(tx, invoice=invoice)\n\n    @protected\n    def sign_tx(\n        self,\n        tx: PartialTransaction,\n        *,\n        callback,\n        external_keypairs: Optional[Mapping[bytes, bytes]],\n        password,\n    ):\n        self.sign_tx_with_password(tx, callback=callback, password=password, external_keypairs=external_keypairs)\n\n    def sign_tx_with_password(\n        self,\n        tx: PartialTransaction,\n        *,\n        callback,\n        password,\n        external_keypairs: Mapping[bytes, bytes] = None,\n    ):\n        '''Sign the transaction in a separate thread.  When done, calls\n        the callback with a success code of True or False.\n        '''\n\n        def on_success(result):\n            callback(True)\n        def on_failure(exc_info):\n            self.on_error(exc_info)\n            callback(False)\n        on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success\n        if external_keypairs:\n            # can sign directly\n            task = partial(tx.sign, external_keypairs)\n        else:\n            # ignore_warnings=True, because UI checks and asks user confirmation itself\n            task = partial(self.wallet.sign_transaction, tx, password, ignore_warnings=True)\n        msg = _('Signing transaction...')\n        WaitingDialog(self, msg, task, on_success, on_failure)\n\n    def mktx_for_open_channel(self, *, funding_sat, node_id):\n        def make_tx(fee_policy, *, confirmed_only=False, base_tx=None):\n            assert base_tx is None\n            return self.wallet.lnworker.mktx_for_open_channel(\n                coins=self.get_coins(nonlocal_only=True, confirmed_only=confirmed_only),\n                funding_sat=funding_sat,\n                node_id=node_id,\n                fee_policy=fee_policy)\n        return make_tx\n\n    def open_channel(self, connect_str, funding_sat, push_amt):\n        try:\n            node_id, rest = extract_nodeid(connect_str)\n        except ConnStringFormatError as e:\n            self.show_error(str(e))\n            return\n        if self.wallet.lnworker.has_conflicting_backup_with(node_id):\n            msg = messages.MSG_CONFLICTING_BACKUP_INSTANCE\n            if not self.question(msg):\n                return\n        # we need to know the fee before we broadcast, because the txid is required\n        make_tx = self.mktx_for_open_channel(funding_sat=funding_sat, node_id=node_id)\n        funding_tx, _, _ = self.confirm_tx_dialog(make_tx, funding_sat, context=TxEditorContext.CHANNEL_FUNDING)\n        if not funding_tx:\n            return\n        self._open_channel(connect_str, funding_sat, push_amt, funding_tx)\n\n    def confirm_tx_dialog(\n        self,\n        make_tx,\n        output_value, *,\n        payee_outputs: Optional[list[TxOutput]] = None,\n        context: TxEditorContext = TxEditorContext.PAYMENT,\n        batching_candidates=None,\n    ) -> tuple[Optional[PartialTransaction], bool, bool]:\n        d = ConfirmTxDialog(\n            window=self,\n            make_tx=make_tx,\n            output_value=output_value,\n            payee_outputs=payee_outputs,\n            context=context,\n            batching_candidates=batching_candidates,\n        )\n        return d.run(), d.is_preview, d.did_swap\n\n    @protected\n    def _open_channel(self, connect_str, funding_sat, push_amt, funding_tx, password):\n        # read funding_sat from tx; converts '!' to int value\n        funding_sat = funding_tx.output_value_for_address(DummyAddress.CHANNEL)\n        def task():\n            return self.wallet.lnworker.open_channel(\n                connect_str=connect_str,\n                funding_tx=funding_tx,\n                funding_sat=funding_sat,\n                push_amt_sat=push_amt,\n                password=password)\n        def on_failure(exc_info):\n            type_, e, traceback = exc_info\n            #self.logger.error(\"Could not open channel\", exc_info=exc_info)\n            self.show_error(_('Could not open channel: {}').format(repr(e)))\n        WaitingDialog(self, _('Opening channel...'), task, self.on_open_channel_success, on_failure)\n\n    def on_open_channel_success(self, args):\n        chan, funding_tx = args\n        lnworker = self.wallet.lnworker\n        if not chan.has_onchain_backup():\n            data = lnworker.export_channel_backup(chan.channel_id)\n            help_text = messages.MSG_CREATED_NON_RECOVERABLE_CHANNEL\n            help_text += '\\n\\n' + _('Alternatively, you can save a backup of your wallet file from the File menu')\n            self.show_qrcode(\n                data, _('Save channel backup'),\n                help_text=help_text,\n                show_copy_text_btn=True)\n        n = chan.constraints.funding_txn_minimum_depth\n        message = '\\n'.join([\n            _('Channel established.'),\n            _('Remote peer ID') + ':' + chan.node_id.hex(),\n            _('This channel will be usable after {} confirmations').format(n)\n        ])\n        if not funding_tx.is_complete():\n            message += '\\n\\n' + _('Please sign and broadcast the funding transaction')\n            self.show_message(message)\n            self.show_transaction(funding_tx)\n        else:\n            self.show_message(message)\n\n    def handle_payment_identifier(self, text: str):\n        pi = PaymentIdentifier(self.wallet, text)\n        if pi.is_valid():\n            self.send_tab.set_payment_identifier(text)\n        else:\n            if pi.error:\n                self.show_error(str(pi.error))\n\n    def set_frozen_state_of_addresses(self, addrs, freeze: bool):\n        self.wallet.set_frozen_state_of_addresses(addrs, freeze)\n        self.address_list.refresh_all()\n        self.utxo_list.refresh_all()\n        self.address_list.selectionModel().clearSelection()\n\n    def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool):\n        utxos_str = {utxo.prevout.to_str() for utxo in utxos}\n        self.wallet.set_frozen_state_of_coins(utxos_str, freeze)\n        self.utxo_list.refresh_all()\n        self.utxo_list.selectionModel().clearSelection()\n\n    def create_list_tab(self, l):\n        w = QWidget()\n        w.searchable_list = l\n        vbox = QVBoxLayout()\n        w.setLayout(vbox)\n        #vbox.setContentsMargins(0, 0, 0, 0)\n        #vbox.setSpacing(0)\n        toolbar = l.create_toolbar(self.config)\n        if toolbar:\n            vbox.addLayout(toolbar)\n        vbox.addWidget(l)\n        if toolbar:\n            l.show_toolbar()\n        return w\n\n    def create_addresses_tab(self):\n        from .address_list import AddressList\n        self.address_list = AddressList(self)\n        tab =  self.create_list_tab(self.address_list)\n        tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_ADDRESSES\n        return tab\n\n    def create_utxo_tab(self):\n        from .utxo_list import UTXOList\n        self.utxo_list = UTXOList(self)\n        tab = self.create_list_tab(self.utxo_list)\n        tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_UTXO\n        return tab\n\n    def create_contacts_tab(self):\n        from .contact_list import ContactList\n        self.contact_list = l = ContactList(self)\n        tab = self.create_list_tab(l)\n        tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_CONTACTS\n        return tab\n\n    def remove_address(self, addr):\n        if not self.question(_(\"Do you want to remove {} from your wallet?\").format(addr)):\n            return\n        try:\n            self.wallet.delete_address(addr)\n        except UserFacingException as e:\n            self.show_error(str(e))\n        else:\n            self.need_update.set()  # history, addresses, coins\n            self.receive_tab.do_clear()\n\n    def payto_contacts(self, labels):\n        self.send_tab.payto_contacts(labels)\n\n    def set_contact(self, label, address):\n        if not (is_address(address) or is_valid_email(address)):  # email = lightning address\n            self.show_error(_('Invalid Address'))\n            self.contact_list.update()  # Displays original unchanged value\n            return False\n        address_type = 'address' if is_address(address) else 'lnaddress'\n        self.contacts[address] = (address_type, label)\n        self.contact_list.update()\n        self.history_list.update()\n        self.update_completions()\n        return True\n\n    def delete_contacts(self, labels):\n        if not self.question(_(\"Remove {} from your list of contacts?\")\n                             .format(\" + \".join(labels))):\n            return\n        for label in labels:\n            self.contacts.pop(label)\n        self.history_list.update()\n        self.contact_list.update()\n        self.update_completions()\n\n    def show_onchain_invoice(self, invoice: Invoice):\n        amount_str = self.format_amount(invoice.get_amount_sat()) + ' ' + self.base_unit()\n        d = WindowModalDialog(self, _(\"Onchain Invoice\"))\n        vbox = QVBoxLayout(d)\n        grid = QGridLayout()\n        grid.addWidget(QLabel(_(\"Amount\") + ':'), 1, 0)\n        grid.addWidget(QLabel(amount_str), 1, 1)\n        if len(invoice.outputs) == 1:\n            grid.addWidget(QLabel(_(\"Address\") + ':'), 2, 0)\n            grid.addWidget(QLabel(invoice.get_address()), 2, 1)\n        else:\n            outputs_str = '\\n'.join(map(lambda x: x.address + ' : ' + self.format_amount(x.value)+ self.base_unit(), invoice.outputs))\n            grid.addWidget(QLabel(_(\"Outputs\") + ':'), 2, 0)\n            grid.addWidget(QLabel(outputs_str), 2, 1)\n        grid.addWidget(QLabel(_(\"Description\") + ':'), 3, 0)\n        grid.addWidget(QLabel(invoice.message), 3, 1)\n        if invoice.exp:\n            grid.addWidget(QLabel(_(\"Expires\") + ':'), 4, 0)\n            grid.addWidget(QLabel(format_time(invoice.exp + invoice.time)), 4, 1)\n        if invoice.bip70:\n            pr = paymentrequest.PaymentRequest(bytes.fromhex(invoice.bip70))\n            Network.run_from_another_thread(pr.verify())\n            grid.addWidget(QLabel(_(\"Requestor\") + ':'), 5, 0)\n            grid.addWidget(QLabel(pr.get_requestor()), 5, 1)\n            grid.addWidget(QLabel(_(\"Signature\") + ':'), 6, 0)\n            grid.addWidget(QLabel(pr.get_verify_status()), 6, 1)\n            def do_export():\n                name = pr.get_name_for_export() or \"payment_request\"\n                name = f\"{name}.bip70\"\n                fn = getSaveFileName(\n                    parent=self,\n                    title=_(\"Save invoice to file\"),\n                    filename=name,\n                    filter=\"*.bip70\",\n                    config=self.config,\n                )\n                if not fn:\n                    return\n                with open(fn, 'wb') as f:\n                    data = f.write(pr.raw)\n                self.show_message(_('BIP70 invoice saved as {}').format(fn))\n            exportButton = EnterButton(_('Export'), do_export)\n            buttons = Buttons(exportButton, CloseButton(d))\n        else:\n            buttons = Buttons(CloseButton(d))\n        vbox.addLayout(grid)\n        vbox.addLayout(buttons)\n        d.exec()\n\n    def show_lightning_invoice(self, invoice: Invoice):\n        from electrum.util import format_short_id\n        lnaddr = lndecode(invoice.lightning_invoice)\n        d = WindowModalDialog(self, _(\"Lightning Invoice\"))\n        vbox = QVBoxLayout(d)\n        grid = QGridLayout()\n        pubkey_e = ShowQRLineEdit(lnaddr.pubkey.serialize().hex(), self.config, title=_(\"Public Key\"))\n        pubkey_e.setMinimumWidth(700)\n        grid.addWidget(QLabel(_(\"Public Key\") + ':'), 0, 0)\n        grid.addWidget(pubkey_e, 0, 1)\n        grid.addWidget(QLabel(_(\"Amount\") + ':'), 1, 0)\n        amount_str = self.format_amount(invoice.get_amount_sat()) + ' ' + self.base_unit()\n        grid.addWidget(QLabel(amount_str), 1, 1)\n        grid.addWidget(QLabel(_(\"Description\") + ':'), 2, 0)\n        grid.addWidget(QLabel(invoice.message), 2, 1)\n        grid.addWidget(QLabel(_(\"Creation time\") + ':'), 3, 0)\n        grid.addWidget(QLabel(format_time(invoice.time)), 3, 1)\n        if invoice.exp:\n            grid.addWidget(QLabel(_(\"Expiration time\") + ':'), 4, 0)\n            grid.addWidget(QLabel(format_time(invoice.time + invoice.exp)), 4, 1)\n        grid.addWidget(QLabel(_('Features') + ':'), 5, 0)\n        grid.addWidget(QLabel(', '.join(lnaddr.get_features().get_names())), 5, 1)\n        payhash_e = ShowQRLineEdit(lnaddr.paymenthash.hex(), self.config, title=_(\"Payment Hash\"))\n        grid.addWidget(QLabel(_(\"Payment Hash\") + ':'), 6, 0)\n        grid.addWidget(payhash_e, 6, 1)\n        fallback = lnaddr.get_fallback_address()\n        if fallback:\n            fallback_e = ShowQRLineEdit(fallback, self.config, title=_(\"Fallback address\"))\n            grid.addWidget(QLabel(_(\"Fallback address\") + ':'), 7, 0)\n            grid.addWidget(fallback_e, 7, 1)\n        invoice_e = ShowQRTextEdit(config=self.config)\n        invoice_e.setFont(QFont(MONOSPACE_FONT))\n        invoice_e.addCopyButton()\n        invoice_e.setText(invoice.lightning_invoice)\n        grid.addWidget(QLabel(_('Text') + ':'), 8, 0)\n        grid.addWidget(invoice_e, 8, 1)\n        r_tags = lnaddr.get_routing_info('r')\n        r_tags = '\\n'.join(repr(r) for r in LnAddr.format_bolt11_routing_info_as_human_readable(r_tags))\n        routing_e = QTextEdit(str(r_tags))\n        routing_e.setReadOnly(True)\n        grid.addWidget(QLabel(_(\"Routing Hints\") + ':'), 9, 0)\n        grid.addWidget(routing_e, 9, 1)\n        vbox.addLayout(grid)\n        vbox.addLayout(Buttons(CloseButton(d),))\n        d.exec()\n\n    def create_console_tab(self):\n        from .console import Console\n        self.console = console = Console()\n        console.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_CONSOLE\n        return console\n\n    def create_notes_tab(self):\n        from PyQt6 import QtGui, QtWidgets\n        notes_tab = QtWidgets.QPlainTextEdit()\n        notes_tab.setWordWrapMode(QtGui.QTextOption.WrapMode.WrapAnywhere)\n        notes_tab.setFont(QtGui.QFont(MONOSPACE_FONT, 10, QtGui.QFont.Weight.Normal))\n        notes_tab.setPlainText(self.wallet.db.get('notes_text', ''))\n        notes_tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_NOTES\n        notes_tab.textChanged.connect(self.maybe_save_notes_text)\n        return notes_tab\n\n    @rate_limited(10, ts_after=True)\n    def maybe_save_notes_text(self):\n        self.save_notes_text()\n\n    def save_notes_text(self):\n        self.logger.info('saving notes')\n        self.wallet.db.put('notes_text', self.notes_tab.toPlainText())\n\n    def update_console(self):\n        console = self.console\n        console.history = self.wallet.db.get_stored_item(\"qt-console-history\", [])\n        console.history_index = len(console.history)\n\n        console.updateNamespace({\n            'wallet': self.wallet,\n            'network': self.network,\n            'plugins': self.gui_object.plugins,\n            'window': self,\n            'config': self.config,\n            'electrum': electrum,\n            'daemon': self.gui_object.daemon,\n            'util': util,\n            'bitcoin': bitcoin,\n            'lnutil': lnutil,\n            'channels': list(self.wallet.lnworker.channels.values()) if self.wallet.lnworker else [],\n            'scan_qr': scan_qr_from_screenshot,\n        })\n\n        c = commands.Commands(\n            config=self.config,\n            daemon=self.gui_object.daemon,\n            network=self.network,\n            callback=lambda: self.console.set_json(True))\n        methods = {}\n        def mkfunc(f, method):\n            return lambda *args, **kwargs: f(method,\n                                             args,\n                                             self.password_dialog,\n                                             **{**kwargs, 'wallet': self.wallet})\n        for m in dir(c):\n            if m[0]=='_' or m in ['network','wallet','config','daemon']: continue\n            methods[m] = mkfunc(c._run, m)\n\n        console.updateNamespace(methods)\n\n    def show_balance_dialog(self):\n        balance = self.wallet.get_balances_for_piechart().total()\n        if balance == 0 and not self.balance_label.has_warning:\n            return\n        from .balance_dialog import BalanceDialog\n        d = BalanceDialog(self, wallet=self.wallet)\n        d.run()\n\n    def create_status_bar(self):\n        sb = QStatusBar()\n        self.balance_label = BalanceToolButton()\n        self.balance_label.setText(\"Loading wallet...\")\n        self.balance_label.setAutoRaise(True)\n        self.balance_label.clicked.connect(self.show_balance_dialog)\n        sb.addWidget(self.balance_label)\n\n        font_height = QFontMetrics(self.balance_label.font()).height()\n        sb_height = max(35, int(2 * font_height))\n        sb.setFixedHeight(sb_height)\n\n        # remove border of all items in status bar\n        self.setStyleSheet(\"QStatusBar::item { border: 0px;} \")\n\n        self.search_box = QLineEdit()\n        self.search_box.textChanged.connect(self.do_search)\n        self.search_box.hide()\n        sb.addPermanentWidget(self.search_box)\n\n        self.update_check_button = QPushButton(\"\")\n        self.update_check_button.setFlat(True)\n        self.update_check_button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))\n        self.update_check_button.setIcon(read_QIcon(\"update.png\"))\n        self.update_check_button.hide()\n        sb.addPermanentWidget(self.update_check_button)\n\n        self.password_required_button = QPushButton(_('Password required'))\n        self.password_required_button.setFlat(True)\n        self.password_required_button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))\n        self.password_required_button.setIcon(read_QIcon(\"warning.png\"))\n        self.password_required_button.setIconSize(self.password_required_button.iconSize() * 1.3)\n        self.password_required_button.clicked.connect(self.on_password_required_button_clicked)\n        self.password_required_button.hide()\n        sb.addPermanentWidget(self.password_required_button)\n\n        self.tasks_label = QLabel('')\n        sb.addPermanentWidget(self.tasks_label)\n\n        self.password_button = StatusBarButton(QIcon(), _(\"Password\"), self.change_password_dialog, sb_height)\n        sb.addPermanentWidget(self.password_button)\n\n        sb.addPermanentWidget(StatusBarButton(read_QIcon(\"preferences.png\"), _(\"Preferences\"), self.settings_dialog, sb_height))\n        self.seed_button = StatusBarButton(read_QIcon(\"seed.png\"), _(\"Seed\"), self.show_seed_dialog, sb_height)\n        sb.addPermanentWidget(self.seed_button)\n        self.lightning_button = StatusBarButton(read_QIcon(\"lightning.png\"), _(\"Lightning Network\"), self.gui_object.show_lightning_dialog, sb_height)\n        sb.addPermanentWidget(self.lightning_button)\n        self.update_lightning_icon()\n        self.status_button = None\n        self.tor_button = None\n        if self.network:\n            self.tor_button = StatusBarButton(\n                read_QIcon(\"tor_logo.png\"),\n                _(\"Tor\"),\n                partial(self.gui_object.show_network_dialog, proxy_tab=True),\n                sb_height,\n            )\n            sb.addPermanentWidget(self.tor_button)\n            self.tor_button.setVisible(False)\n            # add status btn last, to place it at rightmost pos\n            self.status_button = StatusBarButton(\n                read_QIcon(\"status_disconnected.png\"),\n                _(\"Network\"),\n                self.gui_object.show_network_dialog,\n                sb_height,\n            )\n            sb.addPermanentWidget(self.status_button)\n        # add plugins\n        run_hook('create_status_bar', sb)\n        self.setStatusBar(sb)\n\n    def create_coincontrol_statusbar(self):\n        self.coincontrol_sb = sb = QStatusBar()\n        sb.setSizeGripEnabled(False)\n        #sb.setFixedHeight(3 * char_width_in_lineedit())\n        sb.setStyleSheet('QStatusBar::item {border: None;} '\n                         + ColorScheme.GREEN.as_stylesheet(True))\n\n        self.coincontrol_label = QLabel()\n        self.coincontrol_label.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)\n        self.coincontrol_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n        sb.addWidget(self.coincontrol_label)\n\n        clear_cc_button = EnterButton(_('Reset'), lambda: self.utxo_list.clear_coincontrol())\n        clear_cc_button.setStyleSheet(\"margin-right: 5px;\")\n        sb.addPermanentWidget(clear_cc_button)\n\n        sb.setVisible(False)\n        return sb\n\n    def set_coincontrol_msg(self, msg: Optional[str]) -> None:\n        if not msg:\n            self.coincontrol_label.setText(\"\")\n            self.coincontrol_sb.setVisible(False)\n            return\n        self.coincontrol_label.setText(msg)\n        self.coincontrol_sb.setVisible(True)\n\n    def update_lightning_icon(self):\n        if not self.wallet.has_lightning():\n            self.lightning_button.setVisible(False)\n            return\n        if self.network is None or self.network.channel_db is None:\n            self.lightning_button.setVisible(False)\n            return\n        self.lightning_button.setVisible(True)\n\n        cur, total, progress_percent = self.network.lngossip.get_sync_progress_estimate()\n        # self.logger.debug(f\"updating lngossip sync progress estimate: cur={cur}, total={total}\")\n        progress_str = \"??%\"\n        if progress_percent is not None:\n            progress_str = f\"{progress_percent}%\"\n        if progress_percent and progress_percent >= 100:\n            self.lightning_button.setMaximumWidth(25)\n            self.lightning_button.setText('')\n            self.lightning_button.setToolTip(_(\"The Lightning Network graph is fully synced.\"))\n        else:\n            self.lightning_button.setMaximumWidth(25 + 6 * char_width_in_lineedit())\n            self.lightning_button.setText(progress_str)\n            self.lightning_button.setToolTip(_(\"The Lightning Network graph is syncing...\\n\"\n                                               \"Payments are more likely to succeed with a more complete graph.\"))\n\n    def update_lock_icon(self):\n        icon = read_QIcon(\"lock.png\") if self.wallet.has_password() and (self.wallet.get_unlocked_password() is None) else read_QIcon(\"unlock.png\")\n        self.password_button.setIcon(icon)\n\n    def update_buttons_on_seed(self):\n        self.seed_button.setVisible(self.wallet.has_seed())\n        self.password_button.setVisible(self.wallet.may_have_password())\n\n    def change_password_dialog(self):\n        from electrum.storage import StorageEncryptionVersion\n        if StorageEncryptionVersion.XPUB_PASSWORD in self.wallet.get_available_storage_encryption_versions():\n            from .password_dialog import ChangePasswordDialogForHW\n            d = ChangePasswordDialogForHW(self, self.wallet)\n            ok, old_password, new_password, encrypt_with_xpub = d.run()\n            if not ok:\n                return\n            has_xpub_encryption = self.wallet.storage.get_encryption_version() == StorageEncryptionVersion.XPUB_PASSWORD\n            def on_password(hw_dev_pw):\n                self._update_wallet_password(\n                    old_password = hw_dev_pw if has_xpub_encryption else old_password,\n                    new_password = hw_dev_pw if encrypt_with_xpub else new_password,\n                    xpub_encrypt=encrypt_with_xpub,\n                )\n            self.thread.add(\n                self.wallet.keystore.get_password_for_storage_encryption,\n                on_success=on_password)\n        else:\n            from .password_dialog import ChangePasswordDialogForSW\n            d = ChangePasswordDialogForSW(self, self.wallet)\n            ok, old_password, new_password, encrypt_file = d.run()\n            if not ok:\n                return\n            self._update_wallet_password(\n                old_password=old_password, new_password=new_password)\n        self.update_lock_menu()\n\n    def _update_wallet_password(self, *, old_password, new_password, xpub_encrypt=False):\n        try:\n            self.wallet.update_password(old_password, new_password, encrypt_storage=True, xpub_encrypt=xpub_encrypt)\n        except InvalidPassword as e:\n            self.show_error(str(e))\n            return\n        except BaseException:\n            self.logger.exception('Failed to update password')\n            self.show_error(_('Failed to update password'))\n            return\n        msg = _('Password was updated successfully') if self.wallet.has_password() else _('Password is disabled, this wallet is not protected')\n        self.show_message(msg, title=_(\"Success\"))\n        self.update_lock_icon()\n\n    def toggle_search(self):\n        self.search_box.setHidden(not self.search_box.isHidden())\n        if not self.search_box.isHidden():\n            self.search_box.setFocus()\n        else:\n            self.do_search('')\n\n    def do_search(self, t):\n        tab = self.tabs.currentWidget()\n        if hasattr(tab, 'searchable_list'):\n            tab.searchable_list.filter(t)\n\n    def new_channel_dialog(self, *, amount_sat=None, min_amount_sat=None):\n        from electrum.lnutil import MIN_FUNDING_SAT\n        from .new_channel_dialog import NewChannelDialog\n        assert self.wallet.can_have_lightning()\n        confirmed = self.wallet.get_spendable_balance_sat(confirmed_only=True)\n        min_amount_sat = min_amount_sat or MIN_FUNDING_SAT\n        if confirmed < min_amount_sat:\n            msg = _('Not enough funds') + '\\n\\n' + _('You need at least {} to open a channel.').format(self.format_amount_and_units(min_amount_sat))\n            self.show_error(msg)\n            return\n        if not self.wallet.has_lightning() and not self.init_lightning_dialog():\n            return\n        lnworker = self.wallet.lnworker\n        if not lnworker.channels and not lnworker.channel_backups:\n            msg = _('Do you want to create your first channel?') + '\\n\\n' + messages.MSG_LIGHTNING_WARNING\n            if not self.question(msg):\n                return\n        d = NewChannelDialog(self, amount_sat, min_amount_sat)\n        return d.run()\n\n    def new_contact_dialog(self):\n        d = WindowModalDialog(self, _(\"New Contact\"))\n        vbox = QVBoxLayout(d)\n        vbox.addWidget(QLabel(_('New Contact') + ':'))\n        grid = QGridLayout()\n        line1 = QLineEdit()\n        line1.setFixedWidth(32 * char_width_in_lineedit())\n        line2 = QLineEdit()\n        line2.setFixedWidth(32 * char_width_in_lineedit())\n        address_label = QLabel(_(\"Address\"))\n        address_label.setToolTip(_(\"Bitcoin- or Lightning address\"))\n        grid.addWidget(address_label, 1, 0)\n        grid.addWidget(line1, 1, 1)\n        grid.addWidget(QLabel(_(\"Name\")), 2, 0)\n        grid.addWidget(line2, 2, 1)\n        vbox.addLayout(grid)\n        vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))\n        if d.exec():\n            self.set_contact(line2.text(), line1.text())\n\n    def init_lightning_dialog(self, close_dialog: Optional[QDialog] = None) -> bool:\n        assert not self.wallet.has_lightning()\n        if self.wallet.can_have_deterministic_lightning():\n            msg = _(\n                \"Lightning is not enabled because this wallet was created with an old version of Electrum. \"\n                \"Create lightning keys?\")\n        else:\n            msg = _(\n                \"Warning: this wallet type does not support channel recovery from seed. \"\n                \"You will need to backup your wallet every time you create a new channel. \"\n                \"Create lightning keys?\")\n        if self.question(msg):\n            self._init_lightning_dialog(close_dialog=close_dialog)\n        return self.wallet.has_lightning()\n\n    @protected\n    def _init_lightning_dialog(self, *, close_dialog: Optional[QDialog], password):\n        if close_dialog is not None:\n            close_dialog.close()\n        self.wallet.init_lightning(password=password)\n        self.update_lightning_icon()\n        self.show_message(_('Lightning keys have been initialized.'))\n\n    def show_wallet_info(self):\n        from .wallet_info_dialog import WalletInfoDialog\n        d = WalletInfoDialog(self, window=self)\n        d.exec()\n\n    def remove_wallet(self):\n        if self.question('\\n'.join([\n                _('Delete wallet file?'),\n                \"%s\"%self.wallet.storage.path,\n                _('If your wallet contains funds, make sure you have saved its seed.')])):\n            self._delete_wallet()\n\n    @protected\n    def _delete_wallet(self, password):\n        wallet_path = self.wallet.storage.path\n        basename = os.path.basename(wallet_path)\n        r = self.gui_object.daemon.delete_wallet(wallet_path)\n        self.close()\n        if r:\n            self.show_error(_(\"Wallet removed: {}\").format(basename))\n        else:\n            self.show_error(_(\"Wallet file not found: {}\").format(basename))\n\n    @protected\n    def get_password(self, password, message=None):\n        # may be used by plugins to get password\n        return password\n\n    @protected\n    def show_seed_dialog(self, password):\n        if not self.wallet.has_seed():\n            self.show_message(_('This wallet has no seed'))\n            return\n        keystore = self.wallet.get_keystore()\n        try:\n            seed = keystore.get_seed(password)\n            passphrase = keystore.get_passphrase(password)\n        except BaseException as e:\n            self.show_error(repr(e))\n            return\n        from .seed_dialog import SeedDialog\n        d = SeedDialog(self, seed, passphrase, config=self.config)\n        d.exec()\n\n    def show_qrcode(self, data, title=None, parent=None, *,\n                    help_text=None, show_copy_text_btn=False):\n        if not data:\n            return\n        if title is None:\n            title = _(\"QR code\")\n        d = QRDialog(\n            data=data,\n            parent=parent or self,\n            title=title,\n            help_text=help_text,\n            show_copy_text_btn=show_copy_text_btn,\n            config=self.config,\n        )\n        d.exec()\n\n    @protected\n    def show_private_key(self, address, password):\n        if not address:\n            return\n        try:\n            pk = self.wallet.export_private_key(address, password)\n        except Exception as e:\n            self.logger.exception('')\n            self.show_message(repr(e))\n            return\n        xtype = bitcoin.deserialize_privkey(pk)[0]\n        d = WindowModalDialog(self, _(\"Private key\"))\n        d.setMinimumSize(600, 150)\n        vbox = QVBoxLayout()\n        vbox.addWidget(QLabel(_(\"Address\") + ': ' + address))\n        vbox.addWidget(QLabel(_(\"Script type\") + ': ' + xtype))\n        vbox.addWidget(QLabel(_(\"Private key\") + ':'))\n        keys_e = ShowQRTextEdit(text=pk, config=self.config)\n        keys_e.addCopyButton()\n        vbox.addWidget(keys_e)\n        vbox.addLayout(Buttons(CloseButton(d)))\n        d.setLayout(vbox)\n        d.exec()\n\n    msg_sign = _(\"Signing with an address actually means signing with the corresponding \"\n                \"private key, and verifying with the corresponding public key. The \"\n                \"address you have entered does not have a unique public key, so these \"\n                \"operations cannot be performed.\") + '\\n\\n' + \\\n               _('The operation is undefined. Not just in Electrum, but in general.')\n\n    @protected\n    def do_sign(self, address, message, signature, password):\n        address  = address.text().strip()\n        message = message.toPlainText().strip()\n        if not bitcoin.is_address(address):\n            self.show_message(_('Invalid Bitcoin address.'))\n            return\n        if self.wallet.is_watching_only():\n            self.show_message(_('This is a watching-only wallet.'))\n            return\n        if not self.wallet.is_mine(address):\n            self.show_message(_('Address not in wallet.'))\n            return\n        txin_type = self.wallet.get_txin_type(address)\n        if txin_type not in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:\n            self.show_message(_('Cannot sign messages with this type of address:') + \\\n                              ' ' + txin_type + '\\n\\n' + self.msg_sign)\n            return\n        task = partial(self.wallet.sign_message, address, message, password)\n\n        def show_signed_message(sig):\n            try:\n                signature.setText(base64.b64encode(sig).decode('ascii'))\n            except RuntimeError:\n                # (signature) wrapped C/C++ object has been deleted\n                pass\n\n        self.thread.add(task, on_success=show_signed_message)\n\n    def do_verify(self, address, message, signature):\n        address  = address.text().strip()\n        message = message.toPlainText().strip().encode('utf-8')\n        if not bitcoin.is_address(address):\n            self.show_message(_('Invalid Bitcoin address.'))\n            return\n        try:\n            # This can throw on invalid base64\n            sig = base64.b64decode(str(signature.toPlainText()), validate=True)\n            verified = bitcoin.verify_usermessage_with_address(address, sig, message)\n        except Exception as e:\n            verified = False\n        if verified:\n            self.show_message(_(\"Signature verified\"))\n        else:\n            self.show_error(_(\"Wrong signature\"))\n\n    def sign_verify_message(self, address=''):\n        d = WindowModalDialog(self, _('Sign/verify Message'))\n        d.setMinimumSize(610, 290)\n\n        layout = QGridLayout(d)\n\n        message_e = QTextEdit()\n        message_e.setAcceptRichText(False)\n        layout.addWidget(QLabel(_('Message')), 1, 0)\n        layout.addWidget(message_e, 1, 1)\n        layout.setRowStretch(2,3)\n\n        address_e = QLineEdit()\n        address_e.setText(address)\n        layout.addWidget(QLabel(_('Address')), 2, 0)\n        layout.addWidget(address_e, 2, 1)\n\n        signature_e = ScanShowQRTextEdit(config=self.config)\n        layout.addWidget(QLabel(_('Signature')), 3, 0)\n        layout.addWidget(signature_e, 3, 1)\n        layout.setRowStretch(3,1)\n\n        hbox = QHBoxLayout()\n\n        b = QPushButton(_(\"Sign\"))\n        b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))\n        hbox.addWidget(b)\n\n        b = QPushButton(_(\"Verify\"))\n        b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))\n        hbox.addWidget(b)\n\n        b = QPushButton(_(\"Close\"))\n        b.clicked.connect(d.accept)\n        hbox.addWidget(b)\n        layout.addLayout(hbox, 4, 1)\n        d.exec()\n\n    @protected\n    def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):\n        if self.wallet.is_watching_only():\n            self.show_message(_('This is a watching-only wallet.'))\n            return\n        ciphertext = encrypted_e.toPlainText()\n        task = partial(self.wallet.decrypt_message, pubkey_e.text(), ciphertext, password)\n\n        def setText(text):\n            try:\n                message_e.setText(text.decode('utf-8'))\n            except RuntimeError:\n                # (message_e) wrapped C/C++ object has been deleted\n                pass\n\n        self.thread.add(task, on_success=setText)\n\n    def do_encrypt(self, message_e, pubkey_e, encrypted_e):\n        from electrum import crypto\n        message = message_e.toPlainText()\n        message = message.encode('utf-8')\n        try:\n            public_key = ecc.ECPubkey(bfh(pubkey_e.text()))\n        except BaseException as e:\n            self.logger.exception('Invalid Public key')\n            self.show_warning(_('Invalid Public key'))\n            return\n        encrypted = crypto.ecies_encrypt_message(public_key, message)\n        encrypted_e.setText(encrypted.decode('ascii'))\n\n    def encrypt_message(self, address=''):\n        d = WindowModalDialog(self, _('Encrypt/decrypt Message'))\n        d.setMinimumSize(610, 490)\n\n        layout = QGridLayout(d)\n\n        message_e = QTextEdit()\n        message_e.setAcceptRichText(False)\n        layout.addWidget(QLabel(_('Message')), 1, 0)\n        layout.addWidget(message_e, 1, 1)\n        layout.setRowStretch(2,3)\n\n        pubkey_e = QLineEdit()\n        if address:\n            pubkey = self.wallet.get_public_key(address)\n            pubkey_e.setText(pubkey)\n        layout.addWidget(QLabel(_('Public key')), 2, 0)\n        layout.addWidget(pubkey_e, 2, 1)\n\n        encrypted_e = QTextEdit()\n        encrypted_e.setAcceptRichText(False)\n        layout.addWidget(QLabel(_('Encrypted')), 3, 0)\n        layout.addWidget(encrypted_e, 3, 1)\n        layout.setRowStretch(3,1)\n\n        hbox = QHBoxLayout()\n        b = QPushButton(_(\"Encrypt\"))\n        b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))\n        hbox.addWidget(b)\n\n        b = QPushButton(_(\"Decrypt\"))\n        b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))\n        hbox.addWidget(b)\n\n        b = QPushButton(_(\"Close\"))\n        b.clicked.connect(d.accept)\n        hbox.addWidget(b)\n\n        layout.addLayout(hbox, 4, 1)\n        d.exec()\n\n    def tx_from_text(self, data: Union[str, bytes]) -> Union[None, 'PartialTransaction', 'Transaction']:\n        from electrum.transaction import tx_from_any\n        try:\n            return tx_from_any(data)\n        except BaseException as e:\n            self.show_critical(_(\"Electrum was unable to parse your transaction\") + \":\\n\" + repr(e))\n            return\n\n    def import_channel_backup(self, encrypted: str):\n        if not self.question('Import channel backup?'):\n            return\n        if not self.wallet.lnworker:\n            self.show_error(_('Lightning is disabled'))\n            return\n        try:\n            self.wallet.lnworker.import_channel_backup(encrypted)\n        except Exception as e:\n            self.show_error(\"failed to import backup\" + '\\n' + str(e))\n            return\n\n    def read_tx_from_qrcode(self):\n        def cb(success: bool, error: str, data):\n            if not success:\n                if error:\n                    self.show_error(error)\n                return\n            if not data:\n                return\n            # if the user scanned a bitcoin URI\n            if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):\n                self.handle_payment_identifier(data)\n                return\n            if data.lower().startswith('channel_backup:'):\n                self.import_channel_backup(data)\n                return\n            # else if the user scanned an offline signed tx\n            tx = self.tx_from_text(data)\n            if not tx:\n                return\n            self.show_transaction(tx)\n\n        scan_qrcode_from_camera(parent=self.top_level_window(), config=self.config, callback=cb)\n\n    def read_tx_from_file(self) -> Optional[Transaction]:\n        fileName = getOpenFileName(\n            parent=self,\n            title=_(\"Select your transaction file\"),\n            filter=TRANSACTION_FILE_EXTENSION_FILTER_ANY,\n            config=self.config,\n        )\n        if not fileName:\n            return\n        file_content = None  # type: None | str | bytes\n        # 1. try to open file as \"text\"\n        try:\n            with open(fileName, \"r\", encoding=\"ascii\") as f:\n                file_content = f.read()  # type: str\n        except (ValueError, IOError, os.error) as reason:\n            pass\n        else:\n            assert isinstance(file_content, str), f\"expected str, got {type(file_content)}\"\n            file_content = file_content.strip()  # for text, we can safely strip leading/trailing whitespaces\n        # 2. try to open file as \"binary\"\n        if file_content is None:\n            try:\n                with open(fileName, \"rb\") as f:\n                    file_content = f.read()  # type: bytes\n            except (ValueError, IOError, os.error) as reason:\n                self.show_critical(_(\"Electrum was unable to open your transaction file\") + \"\\n\" + str(reason),\n                                   title=_(\"Unable to read file or no transaction found\"))\n        if file_content is None:\n            return None\n        return self.tx_from_text(file_content)\n\n    def do_process_from_text(self):\n        text = text_dialog(\n            parent=self,\n            title=_('Input raw transaction'),\n            header_layout=_(\"Transaction:\"),\n            ok_label=_(\"Load transaction\"),\n            config=self.config,\n        )\n        if not text:\n            return\n        tx = self.tx_from_text(text)\n        if tx:\n            self.show_transaction(tx)\n\n    def do_process_from_text_channel_backup(self):\n        text = text_dialog(\n            parent=self,\n            title=_('Input channel backup'),\n            header_layout=_(\"Channel Backup:\"),\n            ok_label=_(\"Load backup\"),\n            config=self.config,\n        )\n        if not text:\n            return\n        if text.startswith('channel_backup:'):\n            self.import_channel_backup(text)\n\n    def do_process_from_file(self):\n        tx = self.read_tx_from_file()\n        if tx:\n            self.show_transaction(tx)\n\n    def do_process_from_txid(self, *, parent: QWidget = None, txid: str = None):\n        if parent is None:\n            parent = self\n        from electrum import transaction\n        if txid is None:\n            txid, ok = QInputDialog.getText(parent, _('Lookup transaction'), _('Transaction ID') + ':')\n            if not ok:\n                txid = None\n        if not txid:\n            return\n        txid = str(txid).strip()\n        tx = self.wallet.adb.get_transaction(txid)\n        if tx is None:\n            raw_tx = self._fetch_tx_from_network(txid, parent=parent)\n            if not raw_tx:\n                return\n            tx = transaction.Transaction(raw_tx)\n        self.show_transaction(tx)\n\n    def _fetch_tx_from_network(self, txid: str, *, parent: QWidget = None) -> Optional[str]:\n        if not self.network:\n            self.show_message(_(\"You are offline.\"), parent=parent)\n            return\n        try:\n            raw_tx = self.network.run_from_another_thread(\n                self.network.get_transaction(txid, timeout=10))\n        except UntrustedServerReturnedError as e:\n            self.logger.info(f\"Error getting transaction from network: {repr(e)}\")\n            self.show_message(\n                _(\"Error getting transaction from network\") + \":\\n\" + e.get_message_for_gui(),\n                parent=parent,\n            )\n            return\n        except Exception as e:\n            self.show_message(\n                _(\"Error getting transaction from network\") + \":\\n\" + repr(e),\n                parent=parent,\n            )\n            return\n        return raw_tx\n\n    @protected\n    def export_privkeys_dialog(self, password):\n        if self.wallet.is_watching_only():\n            self.show_message(_(\"This is a watching-only wallet\"))\n            return\n\n        if isinstance(self.wallet, Multisig_Wallet):\n            self.show_message(_('WARNING: This is a multi-signature wallet.') + '\\n' +\n                              _('It cannot be \"backed up\" by simply exporting these private keys.'))\n\n        d = WindowModalDialog(self, _('Private keys'))\n        d.setMinimumSize(980, 300)\n        vbox = QVBoxLayout(d)\n\n        msg = \"%s\\n%s\\n%s\" % (_(\"WARNING: ALL your private keys are secret.\"),\n                              _(\"Exposing a single private key can compromise your entire wallet!\"),\n                              _(\"In particular, DO NOT use 'redeem private key' services proposed by third parties.\"))\n        vbox.addWidget(QLabel(msg))\n\n        e = QTextEdit()\n        e.setReadOnly(True)\n        vbox.addWidget(e)\n\n        defaultname = f'electrum-private-keys-{self.wallet.basename()}.csv'\n        select_msg = _('Select file to export your private keys to')\n        hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)\n        vbox.addLayout(hbox)\n\n        b = OkButton(d, _('Export'))\n        b.setEnabled(False)\n        vbox.addLayout(Buttons(CancelButton(d), b))\n\n        private_keys = {}\n        addresses = self.wallet.get_addresses()\n        done = False\n        cancelled = False\n        def privkeys_thread():\n            for addr in addresses:\n                time.sleep(0.1)\n                if done or cancelled:\n                    break\n                privkey = self.wallet.export_private_key(addr, password)\n                private_keys[addr] = privkey\n                self.computing_privkeys_signal.emit()\n            if not cancelled:\n                self.computing_privkeys_signal.disconnect()\n                self.show_privkeys_signal.emit()\n\n        def show_privkeys():\n            s = \"\\n\".join(map(lambda x: x[0] + \"\\t\"+ x[1], private_keys.items()))\n            e.setText(s)\n            b.setEnabled(True)\n            self.show_privkeys_signal.disconnect()\n            nonlocal done\n            done = True\n\n        def on_dialog_closed(*args):\n            nonlocal cancelled\n            if not done:\n                cancelled = True\n                self.computing_privkeys_signal.disconnect()\n                self.show_privkeys_signal.disconnect()\n\n        self.computing_privkeys_signal.connect(lambda: e.setText(\"Please wait... %d/%d\"%(len(private_keys),len(addresses))))\n        self.show_privkeys_signal.connect(show_privkeys)\n        d.finished.connect(on_dialog_closed)\n        threading.Thread(target=privkeys_thread).start()\n\n        if not d.exec():\n            done = True\n            return\n\n        filename = filename_e.text()\n        if not filename:\n            return\n\n        try:\n            self.do_export_privkeys(filename, private_keys, csv_button.isChecked())\n        except (IOError, os.error) as reason:\n            txt = \"\\n\".join([\n                _(\"Electrum was unable to produce a private key-export.\"),\n                str(reason)\n            ])\n            self.show_critical(txt, title=_(\"Unable to create csv\"))\n\n        except Exception as e:\n            self.show_message(repr(e))\n            return\n\n        self.show_message(_(\"Private keys exported.\"))\n\n    def do_export_privkeys(self, fileName, pklist, is_csv):\n        with open(fileName, \"w+\") as f:\n            os_chmod(fileName, 0o600)  # set restrictive perms *before* we write data\n            if is_csv:\n                transaction = csv.writer(f)\n                transaction.writerow([\"address\", \"private_key\"])\n                for addr, pk in pklist.items():\n                    transaction.writerow([\"%34s\"%addr,pk])\n            else:\n                f.write(json.dumps(pklist, indent = 4))\n\n    def do_import_labels(self):\n        def on_import():\n            self.need_update.set()\n        import_meta_gui(self, _('labels'), self.wallet.import_labels, on_import)\n\n    def do_export_labels(self):\n        export_meta_gui(self, _('labels'), self.wallet.export_labels)\n\n    def import_invoices(self):\n        import_meta_gui(self, _('invoices'), self.wallet.import_invoices, self.send_tab.invoice_list.update)\n\n    def export_invoices(self):\n        export_meta_gui(self, _('invoices'), self.wallet.export_invoices)\n\n    def import_requests(self):\n        import_meta_gui(self, _('requests'), self.wallet.import_requests, self.receive_tab.request_list.update)\n\n    def export_requests(self):\n        export_meta_gui(self, _('requests'), self.wallet.export_requests)\n\n    def import_contacts(self):\n        import_meta_gui(self, _('contacts'), self.contacts.import_file, self.contact_list.update)\n\n    def export_contacts(self):\n        export_meta_gui(self, _('contacts'), self.contacts.export_file)\n\n\n    def sweep_key_dialog(self):\n        if not self.network:\n            self.show_error(_(\"You are offline.\"))\n            return\n        d = WindowModalDialog(self, title=_('Sweep private keys'))\n        d.setMinimumSize(600, 300)\n        vbox = QVBoxLayout(d)\n        hbox_top = QHBoxLayout()\n        hbox_top.addWidget(QLabel(_(\"Enter private keys to sweep coins from:\")))\n        hbox_top.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignmentFlag.AlignRight)\n        vbox.addLayout(hbox_top)\n        keys_e = ScanQRTextEdit(allow_multi=True, config=self.config)\n        keys_e.setTabChangesFocus(True)\n        vbox.addWidget(keys_e)\n        vbox.addWidget(QLabel(_(\"Send to address\") + \":\"))\n\n        addresses = self.wallet.get_unused_addresses()\n        if not addresses:\n            addresses = self.wallet.get_receiving_addresses()\n        h, address_e = address_field(addresses)\n        vbox.addLayout(h)\n\n        vbox.addStretch(1)\n        button = OkButton(d, _('Sweep'))\n        vbox.addLayout(Buttons(CancelButton(d), button))\n        button.setEnabled(False)\n\n        def get_address():\n            addr = str(address_e.text()).strip()\n            if bitcoin.is_address(addr):\n                return addr\n\n        def get_pk(*, raise_on_error=False) -> Sequence[str]:\n            text = str(keys_e.toPlainText())\n            return keystore.get_private_keys(text, raise_on_error=raise_on_error)\n\n        def on_edit():\n            valid_privkeys = False\n            try:\n                valid_privkeys = bool(get_pk(raise_on_error=True))\n            except Exception as e:\n                button.setToolTip(f'{_(\"Error\")}: {repr(e)}')\n            else:\n                button.setToolTip('')\n            button.setEnabled(get_address() is not None and valid_privkeys)\n        on_address = lambda text: address_e.setStyleSheet((ColorScheme.DEFAULT if get_address() else ColorScheme.RED).as_stylesheet())\n        keys_e.textChanged.connect(on_edit)\n        address_e.textChanged.connect(on_edit)\n        address_e.textChanged.connect(on_address)\n        on_address(str(address_e.text()))\n        if not d.exec():\n            return\n        # user pressed \"sweep\"\n        addr = get_address()\n        try:\n            self.wallet.check_address_for_corruption(addr)\n        except InternalAddressCorruption as e:\n            self.show_error(str(e))\n            raise\n        privkeys = get_pk()\n\n        def on_success(result):\n            coins, keypairs = result\n            outputs = [PartialTxOutput.from_address_and_value(addr, value='!')]\n            self.warn_if_watching_only()\n            self.send_tab.pay_onchain_dialog(\n                outputs, external_keypairs=keypairs, get_coins=lambda *args, **kwargs: coins)\n        def on_failure(exc_info):\n            self.on_error(exc_info)\n        msg = _('Preparing sweep transaction...')\n        task = lambda: self.network.run_from_another_thread(\n            sweep_preparations(privkeys, self.network))\n        WaitingDialog(self, msg, task, on_success, on_failure)\n\n    def _do_import(self, title, header_layout, func):\n        text = text_dialog(\n            parent=self,\n            title=title,\n            header_layout=header_layout,\n            ok_label=_('Import'),\n            allow_multi=True,\n            config=self.config,\n        )\n        if not text:\n            return\n        keys = str(text).split()\n        good_inputs, bad_inputs = func(keys)\n        if good_inputs:\n            msg = '\\n'.join(good_inputs[:10])\n            if len(good_inputs) > 10: msg += '\\n...'\n            self.show_message(_(\"The following addresses were added\")\n                              + f' ({len(good_inputs)}):\\n' + msg)\n        if bad_inputs:\n            msg = \"\\n\".join(f\"{key[:10]}... ({msg})\" for key, msg in bad_inputs[:10])\n            if len(bad_inputs) > 10: msg += '\\n...'\n            self.show_error(_(\"The following inputs could not be imported\")\n                            + f' ({len(bad_inputs)}):\\n' + msg)\n        self.address_list.update()\n        self.history_list.update()\n\n    def import_addresses(self):\n        if not self.wallet.can_import_address():\n            return\n        title, msg = _('Import addresses'), _(\"Enter addresses\")+':'\n        self._do_import(title, msg, self.wallet.import_addresses)\n\n    @protected\n    def do_import_privkey(self, password):\n        if not self.wallet.can_import_privkey():\n            return\n        title = _('Import private keys')\n        header_layout = QHBoxLayout()\n        header_layout.addWidget(QLabel(_(\"Enter private keys\")+':'))\n        header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignmentFlag.AlignRight)\n        self._do_import(title, header_layout, lambda x: self.wallet.import_private_keys(x, password))\n\n    def refresh_amount_edits(self):\n        edits = self.send_tab.amount_e, self.receive_tab.receive_amount_e\n        amounts = [edit.get_amount() for edit in edits]\n        for edit, amount in zip(edits, amounts):\n            edit.setAmount(amount)\n\n    def update_fiat(self):\n        b = self.fx and self.fx.is_enabled()\n        self.send_tab.fiat_send_e.setVisible(b)\n        self.receive_tab.fiat_receive_e.setVisible(b)\n        self.history_model.refresh('update_fiat')\n        self.history_list.update_toolbar_menu()\n        self.address_list.refresh_headers()\n        self.address_list.update()\n        self.update_status()\n\n    def settings_dialog(self):\n        from .settings_dialog import SettingsDialog\n        d = SettingsDialog(self, self.config)\n        d.exec()\n        if self.fx:\n            self.fx.trigger_update()\n        run_hook('close_settings_dialog')\n        if d.need_restart:\n            self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success'))\n        else:\n            # Some values might need to be updated if settings have changed.\n            # For example 'Can send' in the lightning tab will change if the fees config is changed.\n            self.refresh_tabs()\n\n    def _show_closing_warnings(self) -> bool:\n        \"\"\"Show any closing warnings and return True if the user chose to quit anyway.\"\"\"\n\n        warnings: Set[str] = set()\n        for cb in self.closing_warning_callbacks:\n            if warning := cb():\n                warnings.add(warning)\n\n        for warning in list(warnings)[:3]:\n            warning = ''.join([\n                _(\"Are you sure you want to close Electrum?\"),\n                '\\n\\n',\n                _(\"An ongoing operation requires you to stay online.\"),\n                '\\n',\n                warning\n            ])\n            result = self.question(\n                msg=warning,\n                icon=QMessageBox.Icon.Warning,\n                title=_(\"Warning\"),\n            )\n            if not result:\n                break\n        else:\n            # user chose to cancel all warnings or there were no warnings\n            return True\n        return False\n\n    def register_closing_warning_callback(self, callback: Callable[[], Optional[str]]) -> None:\n        \"\"\"\n        Registers a callback that will be called when the wallet is closed. If the callback\n        returns a string it will be shown to the user as a warning to prevent them closing the wallet.\n        \"\"\"\n        assert not inspect.iscoroutinefunction(callback)\n        def warning_callback() -> Optional[str]:\n            try:\n                return callback()\n            except Exception:\n                self.logger.exception(\"Error in closing warning callback\")\n                return None\n        self.logger.debug(f\"registering wallet closing warning callback\")\n        self.closing_warning_callbacks.append(warning_callback)\n\n    def _check_ongoing_force_closures(self) -> Optional[str]:\n        from electrum.lnutil import MIN_FINAL_CLTV_DELTA_ACCEPTED\n        if not self.wallet.has_lightning():\n            return None\n        if not self.network:\n            return None\n        force_closes = self.wallet.lnworker.lnwatcher.get_pending_force_closes()\n        if not force_closes:\n            return\n        # fixme: this is inaccurate, we need local_height - cltv_of_htlc\n        cltv_delta = MIN_FINAL_CLTV_DELTA_ACCEPTED\n        msg = '\\n\\n'.join([\n            _(\"Pending channel force-close\"),\n            messages.MSG_FORCE_CLOSE_WARNING.format(cltv_delta),\n        ])\n        return msg\n\n    def _check_ongoing_submarine_swaps_callback(self) -> Optional[str]:\n        \"\"\"Callback that will return a warning string if there are unconfirmed swap funding txs.\"\"\"\n        from electrum.submarine_swaps import MIN_FINAL_CLTV_DELTA_FOR_CLIENT, LOCKTIME_DELTA_REFUND\n        if not (self.wallet.has_lightning() and self.wallet.lnworker.swap_manager):\n            return None\n        if not self.network:\n            return None\n        ongoing_swaps = self.wallet.lnworker.swap_manager.get_pending_swaps()\n        if not ongoing_swaps:\n            return None\n        is_forward = any(not swap.is_reverse for swap in ongoing_swaps)\n        if is_forward:\n            # fixme: this is inaccurate, we need local_height - cltv_of_htlc\n            delta = MIN_FINAL_CLTV_DELTA_FOR_CLIENT\n            warning = messages.MSG_FORWARD_SWAP_WARNING.format(delta)\n        else:\n            locktime = min(swap.locktime for swap in ongoing_swaps)\n            delta = locktime - self.wallet.adb.get_local_height()\n            warning = messages.MSG_REVERSE_SWAP_WARNING.format(delta)\n        return \"\\n\\n\".join((\n            _(\"Pending submarine swap\"),\n            warning,\n        ))\n\n    def closeEvent(self, event):\n        # note that closeEvent is NOT called if the user quits with Ctrl-C\n        if not self._show_closing_warnings():\n            event.ignore()\n            return\n        self.clean_up()\n        event.accept()\n\n    def clean_up(self):\n        if self._cleaned_up:\n            return\n        self._cleaned_up = True\n        if self.thread:\n            self.thread.stop()\n            self.thread = None\n        with self._coroutines_scheduled_lock:\n            coro_keys = list(self._coroutines_scheduled.keys())\n        for fut in coro_keys:\n            fut.cancel()\n        self.wallet.txbatcher.set_password_future(None)\n        self.unregister_callbacks()\n        self.config.GUI_QT_WINDOW_IS_MAXIMIZED = self.isMaximized()\n        self.save_notes_text()\n        if not self.isMaximized():\n            g = self.geometry()\n            self.wallet.db.put(\n                \"winpos-qt\", [g.left(),g.top(), g.width(),g.height()])\n        if self.qr_window:\n            self.qr_window.close()\n        self.close_wallet()\n\n        if self._update_check_thread:\n            self._update_check_thread.stop()\n        if self.tray:\n            self.tray = None\n        self.timer.stop()\n        self.gui_object.close_window(self)\n\n    def cpfp_dialog(self, parent_tx: Transaction) -> None:\n        new_tx = self.wallet.cpfp(parent_tx, 0)\n        total_size = parent_tx.estimated_size() + new_tx.estimated_size()\n        parent_txid = parent_tx.txid()\n        assert parent_txid\n        parent_fee = self.wallet.get_tx_info(parent_tx).fee\n        if parent_fee is None:\n            self.show_error(_(\"Can't CPFP: unknown fee for parent transaction.\"))\n            return\n        d = WindowModalDialog(self, _('Child Pays for Parent'))\n        vbox = QVBoxLayout(d)\n        msg = _(\n            \"A CPFP is a transaction that sends an unconfirmed output back to \"\n            \"yourself, with a high fee. The goal is to have miners confirm \"\n            \"the parent transaction in order to get the fee attached to the \"\n            \"child transaction.\")\n        vbox.addWidget(WWLabel(msg))\n        msg2 = _(\"The proposed fee is computed using your \"\n            \"fee/kB settings, applied to the total size of both child and \"\n            \"parent transactions. After you broadcast a CPFP transaction, \"\n            \"it is normal to see a new unconfirmed transaction in your history.\")\n        vbox.addWidget(WWLabel(msg2))\n        grid = QGridLayout()\n        grid.addWidget(QLabel(_('Total size') + ':'), 0, 0)\n        grid.addWidget(QLabel(f\"{total_size} {UI_UNIT_NAME_TXSIZE_VBYTES}\"), 0, 1)\n        max_fee = new_tx.output_value()\n        grid.addWidget(QLabel(_('Input amount') + ':'), 1, 0)\n        grid.addWidget(QLabel(self.format_amount(max_fee) + ' ' + self.base_unit()), 1, 1)\n        output_amount = QLabel('')\n        grid.addWidget(QLabel(_('Output amount') + ':'), 2, 0)\n        grid.addWidget(output_amount, 2, 1)\n        fee_e = BTCAmountEdit(self.get_decimal_point)\n        combined_fee = QLabel('')\n        combined_feerate = QLabel('')\n        def on_fee_edit(x):\n            fee_for_child = fee_e.get_amount()\n            if fee_for_child is None:\n                return\n            out_amt = max_fee - fee_for_child\n            out_amt_str = (self.format_amount(out_amt) + ' ' + self.base_unit()) if out_amt else ''\n            output_amount.setText(out_amt_str)\n            comb_fee = parent_fee + fee_for_child\n            comb_fee_str = (self.format_amount(comb_fee) + ' ' + self.base_unit()) if comb_fee else ''\n            combined_fee.setText(comb_fee_str)\n            comb_feerate = comb_fee / total_size * 1000\n            comb_feerate_str = self.format_fee_rate(comb_feerate) if comb_feerate else ''\n            combined_feerate.setText(comb_feerate_str)\n        fee_e.textChanged.connect(on_fee_edit)\n        def get_child_fee_from_total_feerate(fee_per_kb: Optional[int]) -> Optional[int]:\n            if fee_per_kb is None:\n                return None\n            package_fee = FeePolicy.estimate_fee_for_feerate(fee_per_kb=fee_per_kb, size=total_size)\n            child_fee = package_fee - parent_fee\n            child_fee = min(max_fee, child_fee)\n            # pay at least minrelayfee for combined size:\n            min_child_fee = FeePolicy.estimate_fee_for_feerate(fee_per_kb=self.wallet.relayfee(), size=total_size)\n            child_fee = max(min_child_fee, child_fee)\n            return child_fee\n        fee_policy = FeePolicy(self.config.FEE_POLICY)\n        suggested_feerate = fee_policy.fee_per_kb(self.network)\n        fee = get_child_fee_from_total_feerate(suggested_feerate)\n        fee_e.setAmount(fee)\n        grid.addWidget(QLabel(_('Fee for child') + ':'), 3, 0)\n        grid.addWidget(fee_e, 3, 1)\n        def on_rate(fee_rate):\n            fee = get_child_fee_from_total_feerate(fee_rate)\n            fee_e.setAmount(fee)\n        fee_slider = FeeSlider(parent=self, network=self.network, fee_policy=fee_policy, callback=on_rate)\n        fee_combo = FeeComboBox(fee_slider)\n        fee_slider.update()\n        grid.addWidget(fee_slider, 4, 1)\n        grid.addWidget(fee_combo, 4, 2)\n        grid.addWidget(QLabel(_('Total fee') + ':'), 5, 0)\n        grid.addWidget(combined_fee, 5, 1)\n        grid.addWidget(QLabel(_('Total feerate') + ':'), 6, 0)\n        grid.addWidget(combined_feerate, 6, 1)\n        vbox.addLayout(grid)\n        vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))\n        if not d.exec():\n            return\n        fee = fee_e.get_amount()\n        if fee is None:\n            return  # fee left empty, treat it as \"cancel\"\n        if fee > max_fee:\n            self.show_error(_('Max fee exceeded'))\n            return\n        try:\n            new_tx = self.wallet.cpfp(parent_tx, fee)\n        except CannotCPFP as e:\n            self.show_error(str(e))\n            return\n        self.show_transaction(new_tx)\n\n    def bump_fee_dialog(self, tx: Transaction):\n        if not isinstance(tx, PartialTransaction):\n            tx = PartialTransaction.from_tx(tx)\n        if not tx.add_info_from_wallet_and_network(wallet=self.wallet, show_error=self.show_error):\n            return\n        d = BumpFeeDialog(main_window=self, tx=tx)\n        d.run()\n\n    def dscancel_dialog(self, tx: Transaction):\n        if not isinstance(tx, PartialTransaction):\n            tx = PartialTransaction.from_tx(tx)\n        if not tx.add_info_from_wallet_and_network(wallet=self.wallet, show_error=self.show_error):\n            return\n        d = DSCancelDialog(main_window=self, tx=tx)\n        d.run()\n\n    def save_transaction_into_wallet(self, tx: Transaction):\n        win = self.top_level_window()\n        try:\n            if not self.wallet.adb.add_transaction(tx):\n                win.show_error(_(\"Transaction could not be saved.\") + \"\\n\" +\n                               _(\"It conflicts with current history.\"))\n                return False\n        except AddTransactionException as e:\n            win.show_error(e)\n            return False\n        else:\n            self.wallet.save_db()\n            # need to update at least: history_list, utxo_list, address_list\n            self.need_update.set()\n            msg = (_(\"Transaction added to wallet history.\") + '\\n\\n' +\n                   _(\"Note: this is an offline transaction, if you want the network \"\n                     \"to see it, you need to broadcast it.\"))\n            win.msg_box(QPixmap(icon_path(\"offline_tx.png\")), None, _('Success'), msg)\n            return True\n\n    def show_cert_mismatch_error(self):\n        if self.showing_cert_mismatch_error:\n            return\n        self.showing_cert_mismatch_error = True\n        self.show_critical(title=_(\"Certificate mismatch\"),\n                           msg=_(\"The SSL certificate provided by the main server did not match the fingerprint passed in with the --serverfingerprint option.\") + \"\\n\\n\" +\n                               _(\"Electrum will now exit.\"))\n        self.showing_cert_mismatch_error = False\n        self.close()\n\n    def rebalance_dialog(self, chan1, chan2, amount_sat=None):\n        from .rebalance_dialog import RebalanceDialog\n        if chan1 is None or chan2 is None:\n            return\n        d = RebalanceDialog(self, chan1, chan2, amount_sat)\n        d.run()\n\n    def on_swap_result(self, txid: Optional[str], *, is_reverse: bool):\n        msg = _(\"Submarine swap\") + ': ' + (_(\"Success\") if txid else _(\"Expired\")) + '\\n\\n'\n        if txid:\n            msg += _(\"Funding transaction\") + ': ' + txid + '\\n\\n'\n            if is_reverse:\n                msg += messages.MSG_REVERSE_SWAP_FUNDING_MEMPOOL\n            else:\n                msg += messages.MSG_FORWARD_SWAP_FUNDING_MEMPOOL\n            self.show_message_signal.emit(msg)\n        else:\n            msg += _(\"Lightning funds were not received.\")  # FIXME should this not depend on is_reverse?\n            self.show_error_signal.emit(msg)\n\n    def set_payment_identifier(self, pi: str):\n        # delegate to send_tab\n        self.send_tab.set_payment_identifier(pi)\n"
  },
  {
    "path": "electrum/gui/qt/my_treeview.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2023 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport enum\nfrom decimal import Decimal\nfrom typing import (Optional, TYPE_CHECKING, Union, List, Dict, Any,\n                    Sequence, Iterable, Type, Callable)\n\nfrom PyQt6.QtGui import (QStandardItem, QStandardItemModel,\n                         QShowEvent, QPainter, QHelpEvent, QMouseEvent, QAction)\nfrom PyQt6.QtCore import (Qt, QPersistentModelIndex, QModelIndex, QItemSelectionModel,\n                          QSortFilterProxyModel, QSize, QAbstractItemModel, QEvent, QPoint)\nfrom PyQt6.QtWidgets import (QLabel, QHBoxLayout, QAbstractItemView, QLineEdit,\n                             QWidget, QToolButton, QTreeView, QHeaderView, QStyledItemDelegate,\n                             QMenu, QStyleOptionViewItem)\n\nfrom electrum.i18n import _\nfrom electrum.simple_config import ConfigVarWithConfig\n\nfrom electrum.gui import messages\n\nfrom .util import read_QIcon\n\nif TYPE_CHECKING:\n    from electrum import SimpleConfig\n    from .main_window import ElectrumWindow\n\n\nclass QMenuWithConfig(QMenu):\n\n    def __init__(self, config: 'SimpleConfig'):\n        QMenu.__init__(self)\n        self.setToolTipsVisible(True)\n        self.config = config\n\n    def addToggle(\n        self,\n        text: str,\n        callback: Callable[[], None],\n        *,\n        tooltip: Optional[str] = None,\n        default_state: bool = False,\n    ) -> QAction:\n        m = self.addAction(text, callback)\n        m.setCheckable(True)\n        m.setChecked(default_state)\n        tooltip = tooltip or \"\"\n        m.setToolTip(tooltip)\n        return m\n\n    def addConfig(\n        self,\n        configvar: 'ConfigVarWithConfig',\n        *,\n        callback: Optional[Callable[[], None]] = None,\n        checked: Optional[bool] = None,  # to override initial state of checkbox\n        short_desc: Optional[str] = None,\n    ) -> QAction:\n        assert isinstance(configvar, ConfigVarWithConfig), configvar\n        if short_desc is None:\n            short_desc = configvar.get_short_desc()\n            assert short_desc is not None, f\"short_desc missing for {configvar}\"\n        if checked is None:\n            checked = bool(configvar.get())\n        tooltip = None\n        if (long_desc := configvar.get_long_desc()) is not None:\n            tooltip = messages.to_rtf(long_desc)\n        return self.addToggle(\n            short_desc,\n            lambda: self._do_toggle_config(configvar, callback=callback),\n            tooltip=tooltip,\n            default_state=checked,\n        )\n\n    def _do_toggle_config(\n        self,\n        configvar: 'ConfigVarWithConfig',\n        *,\n        callback: Optional[Callable[[], None]] = None,\n    ):\n        b = configvar.get()\n        configvar.set(not b)\n        # call cb after configvar state is updated:\n        if callback:\n            callback()\n\n\ndef create_toolbar_with_menu(config: 'SimpleConfig', title):\n    menu = QMenuWithConfig(config)\n    toolbar_button = QToolButton()\n    toolbar_button.setText(_('Tools'))\n    toolbar_button.setIcon(read_QIcon(\"preferences.png\"))\n    toolbar_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)\n    toolbar_button.setMenu(menu)\n    toolbar_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)\n    toolbar_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n    toolbar = QHBoxLayout()\n    toolbar.addWidget(QLabel(title))\n    toolbar.addStretch()\n    toolbar.addWidget(toolbar_button)\n    return toolbar, menu\n\n\nclass MySortModel(QSortFilterProxyModel):\n    def __init__(self, parent, *, sort_role):\n        super().__init__(parent)\n        self._sort_role = sort_role\n\n    def lessThan(self, source_left: QModelIndex, source_right: QModelIndex):\n        parent_model = self.sourceModel()  # type: QStandardItemModel\n        item1 = parent_model.itemFromIndex(source_left)\n        item2 = parent_model.itemFromIndex(source_right)\n        data1 = item1.data(self._sort_role)\n        data2 = item2.data(self._sort_role)\n        if data1 is not None and data2 is not None:\n            return data1 < data2\n        v1 = item1.text()\n        v2 = item2.text()\n        try:\n            return Decimal(v1) < Decimal(v2)\n        except Exception:\n            return v1 < v2\n\n\nclass ElectrumItemDelegate(QStyledItemDelegate):\n    def __init__(self, tv: 'MyTreeView'):\n        super().__init__(tv)\n        self.tv = tv\n        self.opened = None\n\n        def on_closeEditor(editor: QLineEdit, hint):\n            self.opened = None\n            self.tv.is_editor_open = False\n            if self.tv._pending_update:\n                self.tv.update()\n\n        def on_commitData(editor: QLineEdit):\n            new_text = editor.text()\n            idx = QModelIndex(self.opened)\n            row, col = idx.row(), idx.column()\n            edit_key = self.tv.get_edit_key_from_coordinate(row, col)\n            assert edit_key is not None, (idx.row(), idx.column())\n            self.tv.on_edited(idx, edit_key=edit_key, text=new_text)\n\n        self.closeEditor.connect(on_closeEditor)\n        self.commitData.connect(on_commitData)\n\n    def createEditor(self, parent, option, idx):\n        self.opened = QPersistentModelIndex(idx)\n        self.tv.is_editor_open = True\n        return super().createEditor(parent, option, idx)\n\n    def paint(self, painter: QPainter, option: QStyleOptionViewItem, idx: QModelIndex) -> None:\n        custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT)\n        if custom_data is None:\n            return super().paint(painter, option, idx)\n        else:\n            # let's call the default paint method first; to paint the background (e.g. selection)\n            super().paint(painter, option, idx)\n            # and now paint on top of that\n            custom_data.paint(painter, option.rect)\n\n    def helpEvent(self, evt: QHelpEvent, view: QAbstractItemView, option: QStyleOptionViewItem, idx: QModelIndex) -> bool:\n        custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT)\n        if custom_data is None:\n            return super().helpEvent(evt, view, option, idx)\n        else:\n            if evt.type() == QEvent.Type.ToolTip:\n                if custom_data.show_tooltip(evt):\n                    return True\n        return super().helpEvent(evt, view, option, idx)\n\n    def sizeHint(self, option: QStyleOptionViewItem, idx: QModelIndex) -> QSize:\n        custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT)\n        if custom_data is None:\n            return super().sizeHint(option, idx)\n        else:\n            default_size = super().sizeHint(option, idx)\n            return custom_data.sizeHint(default_size)\n\n\nclass MyTreeView(QTreeView):\n\n    ROLE_CLIPBOARD_DATA = Qt.ItemDataRole.UserRole + 100\n    ROLE_CUSTOM_PAINT   = Qt.ItemDataRole.UserRole + 101\n    ROLE_EDIT_KEY       = Qt.ItemDataRole.UserRole + 102\n    ROLE_FILTER_DATA    = Qt.ItemDataRole.UserRole + 103\n\n    filter_columns: Iterable[int]\n\n    class BaseColumnsEnum(enum.IntEnum):\n        @staticmethod\n        def _generate_next_value_(name: str, start: int, count: int, last_values):\n            # this is overridden to get a 0-based counter\n            return count\n\n    Columns: Type[BaseColumnsEnum]\n\n    def __init__(\n        self,\n        *,\n        parent: Optional[QWidget] = None,\n        main_window: Optional['ElectrumWindow'] = None,\n        stretch_column: Optional[int] = None,\n        editable_columns: Optional[Sequence[int]] = None,\n    ):\n        parent = parent or main_window\n        super().__init__(parent)\n        self.main_window = main_window\n        self.config = self.main_window.config if self.main_window else None\n        self.stretch_column = stretch_column\n        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)\n        self.customContextMenuRequested.connect(self.create_menu)\n        self.setUniformRowHeights(True)\n\n        # Control which columns are editable\n        if editable_columns is None:\n            editable_columns = []\n        self.editable_columns = set(editable_columns)\n        self.setItemDelegate(ElectrumItemDelegate(self))\n        self.current_filter = \"\"\n        self.is_editor_open = False\n\n        self.setRootIsDecorated(False)  # remove left margin\n        self.toolbar_shown = False\n\n        # When figuring out the size of columns, Qt by default looks at\n        # the first 1000 rows (at least if resize mode is QHeaderView.ResizeToContents).\n        # This would be REALLY SLOW, and it's not perfect anyway.\n        # So to speed the UI up considerably, set it to\n        # only look at as many rows as currently visible.\n        self.header().setResizeContentsPrecision(0)\n\n        self._pending_update = False\n        self._forced_update = False\n\n        self._currently_open_menu = None  # type: Optional[QMenu]\n\n        self._default_bg_brush = QStandardItem().background()\n        self.proxy = None # history, and address tabs use a proxy\n\n    def create_menu(self, position: QPoint) -> None:\n        pass\n\n    def open_menu(self, menu: QMenu, position) -> None:\n        try:\n            self._currently_open_menu = menu\n            menu.exec(self.viewport().mapToGlobal(position))\n        finally:\n            self._currently_open_menu = None\n\n    def close_menu(self):\n        if self._currently_open_menu:\n            self._currently_open_menu.close()\n            self._currently_open_menu = None\n\n    def set_editability(self, items):\n        for idx, i in enumerate(items):\n            i.setEditable(idx in self.editable_columns)\n\n    def selected_in_column(self, column: int):\n        items = self.selectionModel().selectedIndexes()\n        return list(x for x in items if x.column() == column)\n\n    def get_role_data_for_current_item(self, *, col, role) -> Any:\n        idx = self.selectionModel().currentIndex()\n        idx = idx.sibling(idx.row(), col)\n        item = self.item_from_index(idx)\n        if item:\n            return item.data(role)\n\n    def item_from_index(self, idx: QModelIndex) -> Optional[QStandardItem]:\n        model = self.model()\n        if isinstance(model, QSortFilterProxyModel):\n            idx = model.mapToSource(idx)\n            return model.sourceModel().itemFromIndex(idx)\n        else:\n            return model.itemFromIndex(idx)\n\n    def original_model(self) -> QAbstractItemModel:\n        model = self.model()\n        if isinstance(model, QSortFilterProxyModel):\n            return model.sourceModel()\n        else:\n            return model\n\n    def set_current_idx(self, set_current: QPersistentModelIndex):\n        if set_current:\n            assert isinstance(set_current, QPersistentModelIndex)\n            assert set_current.isValid()\n            self.selectionModel().select(QModelIndex(set_current), QItemSelectionModel.SelectionFlag.SelectCurrent)\n\n    def update_headers(self, headers: Union[List[str], Dict[int, str]]):\n        # headers is either a list of column names, or a dict: (col_idx->col_name)\n        if not isinstance(headers, dict):  # convert to dict\n            headers = dict(enumerate(headers))\n        col_names = [headers[col_idx] for col_idx in sorted(headers.keys())]\n        self.original_model().setHorizontalHeaderLabels(col_names)\n        self.header().setStretchLastSection(False)\n        for col_idx in headers:\n            sm = QHeaderView.ResizeMode.Stretch if col_idx == self.stretch_column else QHeaderView.ResizeMode.ResizeToContents\n            self.header().setSectionResizeMode(col_idx, sm)\n\n    def keyPressEvent(self, event):\n        if self.itemDelegate().opened:\n            return\n        if event.key() in [Qt.Key.Key_F2, Qt.Key.Key_Return, Qt.Key.Key_Enter]:\n            self.on_activated(self.selectionModel().currentIndex())\n            return\n        super().keyPressEvent(event)\n\n    def mouseDoubleClickEvent(self, event: QMouseEvent):\n        idx: QModelIndex = self.indexAt(event.pos())\n        if self.proxy:\n            idx = self.proxy.mapToSource(idx)\n        if not idx.isValid():\n            # can happen e.g. before list is populated for the first time\n            return\n        self.on_double_click(idx)\n\n    def on_double_click(self, idx):\n        pass\n\n    def on_activated(self, idx):\n        # on 'enter' we show the menu\n        pt = self.visualRect(idx).bottomLeft()\n        pt.setX(50)\n        self.customContextMenuRequested.emit(pt)\n\n    def edit(self, idx, trigger=QAbstractItemView.EditTrigger.AllEditTriggers, event=None):\n        \"\"\"\n        this is to prevent:\n           edit: editing failed\n        from inside qt\n        \"\"\"\n        return super().edit(idx, trigger, event)\n\n    def on_edited(self, idx: QModelIndex, edit_key, *, text: str) -> None:\n        raise NotImplementedError()\n\n    def should_hide(self, row):\n        \"\"\"\n        row_num is for self.model(). So if there is a proxy, it is the row number\n        in that!\n        \"\"\"\n        return False\n\n    def get_text_from_coordinate(self, row, col) -> str:\n        idx = self.model().index(row, col)\n        item = self.item_from_index(idx)\n        return item.text()\n\n    def get_role_data_from_coordinate(self, row, col, *, role) -> Any:\n        idx = self.model().index(row, col)\n        item = self.item_from_index(idx)\n        role_data = item.data(role)\n        return role_data\n\n    def get_edit_key_from_coordinate(self, row, col) -> Any:\n        # overriding this might allow avoiding storing duplicate data\n        return self.get_role_data_from_coordinate(row, col, role=self.ROLE_EDIT_KEY)\n\n    def get_filter_data_from_coordinate(self, row, col) -> str:\n        filter_data = self.get_role_data_from_coordinate(row, col, role=self.ROLE_FILTER_DATA)\n        if filter_data:\n            return filter_data\n        txt = self.get_text_from_coordinate(row, col)\n        txt = txt.lower()\n        return txt\n\n    def hide_row(self, row_num):\n        \"\"\"\n        row_num is for self.model(). So if there is a proxy, it is the row number\n        in that!\n        \"\"\"\n        should_hide = self.should_hide(row_num)\n        if not self.current_filter and should_hide is None:\n            # no filters at all, neither date nor search\n            self.setRowHidden(row_num, QModelIndex(), False)\n            return\n        for column in self.filter_columns:\n            filter_data = self.get_filter_data_from_coordinate(row_num, column)\n            if self.current_filter in filter_data:\n                # the filter matched, but the date filter might apply\n                self.setRowHidden(row_num, QModelIndex(), bool(should_hide))\n                break\n        else:\n            # we did not find the filter in any columns, hide the item\n            self.setRowHidden(row_num, QModelIndex(), True)\n\n    def filter(self, p=None):\n        if p is not None:\n            p = p.lower()\n            self.current_filter = p\n        self.hide_rows()\n\n    def hide_rows(self):\n        for row in range(self.model().rowCount()):\n            self.hide_row(row)\n\n    def create_toolbar(self, config: 'SimpleConfig'):\n        return\n\n    def create_toolbar_buttons(self):\n        hbox = QHBoxLayout()\n        buttons = self.get_toolbar_buttons()\n        for b in buttons:\n            b.setVisible(False)\n            hbox.addWidget(b)\n        self.toolbar_buttons = buttons\n        return hbox\n\n    def create_toolbar_with_menu(self, title):\n        return create_toolbar_with_menu(self.config, title)\n\n    configvar_show_toolbar = None  # type: Optional[ConfigVarWithConfig]\n    _toolbar_checkbox = None  # type: Optional[QAction]\n    def show_toolbar(self, state: bool = None):\n        if state is None:  # get value from config\n            if self.configvar_show_toolbar:\n                state = self.configvar_show_toolbar.get()\n            else:\n                return\n        assert isinstance(state, bool), state\n        if state == self.toolbar_shown:\n            return\n        self.toolbar_shown = state\n        for b in self.toolbar_buttons:\n            b.setVisible(state)\n        if not state:\n            self.on_hide_toolbar()\n        if self._toolbar_checkbox is not None:\n            # update the cb state now, in case the checkbox was not what triggered us\n            self._toolbar_checkbox.setChecked(state)\n\n    def on_hide_toolbar(self):\n        pass\n\n    def toggle_toolbar(self):\n        new_state = not self.toolbar_shown\n        self.show_toolbar(new_state)\n        if self.configvar_show_toolbar:\n            self.configvar_show_toolbar.set(new_state)\n\n    def add_copy_menu(self, menu: QMenu, idx) -> QMenu:\n        cc = menu.addMenu(_(\"Copy\"))\n        for column in self.Columns:\n            if self.isColumnHidden(column):\n                continue\n            column_title = self.original_model().horizontalHeaderItem(column).text()\n            if not column_title:\n                continue\n            item_col = self.item_from_index(idx.sibling(idx.row(), column))\n            clipboard_data = item_col.data(self.ROLE_CLIPBOARD_DATA)\n            if clipboard_data is None:\n                clipboard_data = item_col.text().strip()\n            cc.addAction(column_title,\n                         lambda text=clipboard_data, title=column_title:\n                         self.place_text_on_clipboard(text, title=title))\n        return cc\n\n    def place_text_on_clipboard(self, text: str, *, title: str = None) -> None:\n        self.main_window.do_copy(text, title=title)\n\n    def showEvent(self, e: 'QShowEvent'):\n        super().showEvent(e)\n        if e.isAccepted() and self._pending_update:\n            self._forced_update = True\n            self.update()\n            self._forced_update = False\n\n    def maybe_defer_update(self) -> bool:\n        \"\"\"Returns whether we should defer an update/refresh.\"\"\"\n        defer = (not self._forced_update\n                 and (not self.isVisible() or self.is_editor_open))\n        # side-effect: if we decide to defer update, the state will become stale:\n        self._pending_update = defer\n        return defer\n\n    def find_row_by_key(self, key) -> Optional[int]:\n        for row in range(0, self.std_model.rowCount()):\n            item = self.std_model.item(row, 0)\n            if item.data(self.key_role) == key:\n                return row\n\n    def refresh_all(self):\n        if self.maybe_defer_update():\n            return\n        for row in range(0, self.std_model.rowCount()):\n            item = self.std_model.item(row, 0)\n            key = item.data(self.key_role)\n            self.refresh_row(key, row)\n\n    def refresh_row(self, key: str, row: int) -> None:\n        pass\n\n    def refresh_item(self, key):\n        row = self.find_row_by_key(key)\n        if row is not None:\n            self.refresh_row(key, row)\n\n    def delete_item(self, key):\n        row = self.find_row_by_key(key)\n        if row is not None:\n            self.std_model.takeRow(row)\n        self.hide_if_empty()\n\n\n"
  },
  {
    "path": "electrum/gui/qt/network_dialog.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2012 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom enum import IntEnum\n\nfrom PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot\nfrom PyQt6.QtWidgets import (\n    QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox, QLineEdit, QDialog, QVBoxLayout, QHeaderView,\n    QCheckBox, QTabWidget, QWidget, QLabel, QPushButton, QHBoxLayout,\n    QListWidget, QListWidgetItem,\n)\nfrom PyQt6.QtGui import QIntValidator\n\nfrom electrum.i18n import _\nfrom electrum import blockchain\nfrom electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL\nfrom electrum.network import Network, ProxySettings, is_valid_host, is_valid_port\nfrom electrum.logging import get_logger\nfrom electrum.util import is_valid_websocket_url\nfrom electrum.gui import messages\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\nfrom .util import (\n    Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit, PasswordLineEdit, Spinner, HelpLabel\n)\n\n_logger = get_logger(__name__)\n\nprotocol_names = ['TCP', 'SSL']\nprotocol_letters = 'ts'\n\n\nclass NetworkDialog(QDialog, QtEventListener):\n    def __init__(self, *, network: Network):\n        QDialog.__init__(self)\n        self.setWindowTitle(_('Network'))\n        self.setMinimumSize(500, 500)\n        self.tabs = tabs = QTabWidget()\n        self._blockchain_tab = ServerWidget(network)\n        self._proxy_tab = ProxyWidget(network)\n        self._nostr_tab = NostrWidget(network)\n        tabs.addTab(self._blockchain_tab, _('Server'))\n        tabs.addTab(self._nostr_tab, _('Nostr'))\n        tabs.addTab(self._proxy_tab, _('Proxy'))\n        vbox = QVBoxLayout(self)\n        vbox.addWidget(self.tabs)\n        vbox.addLayout(Buttons(CloseButton(self)))\n\n    def show(self, *, proxy_tab: bool = False):\n        super().show()\n        self.tabs.setCurrentWidget(self._proxy_tab if proxy_tab else self._blockchain_tab)\n\n\nclass NodesListWidget(QTreeWidget):\n    \"\"\"List of connected servers.\"\"\"\n\n    SERVER_ADDR_ROLE = Qt.ItemDataRole.UserRole + 100\n    CHAIN_ID_ROLE = Qt.ItemDataRole.UserRole + 101\n    ITEMTYPE_ROLE = Qt.ItemDataRole.UserRole + 102\n\n    class ItemType(IntEnum):\n        CHAIN = 0\n        CONNECTED_SERVER = 1\n        DISCONNECTED_SERVER = 2\n        TOPLEVEL = 3\n\n    followServer = pyqtSignal([ServerAddr], arguments=['server'])\n    followChain = pyqtSignal([str], arguments=['chain_id'])\n    setServer = pyqtSignal([str], arguments=['server'])\n\n    def __init__(self, *, network: Network):\n        QTreeWidget.__init__(self)\n        self.setHeaderLabels([_('Server'), _('Height')])\n        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)\n        self.customContextMenuRequested.connect(self.create_menu)\n        self.network = network\n\n    def create_menu(self, position):\n        item = self.currentItem()\n        if not item:\n            return\n        item_type = item.data(0, self.ITEMTYPE_ROLE)\n        menu = QMenu()\n        if item_type in [self.ItemType.CONNECTED_SERVER, self.ItemType.DISCONNECTED_SERVER]:\n            server = item.data(0, self.SERVER_ADDR_ROLE)  # type: ServerAddr\n            if item_type == self.ItemType.CONNECTED_SERVER:\n                def do_follow_server():\n                    self.followServer.emit(server)\n                menu.addAction(read_QIcon(\"chevron-right.png\"), _(\"Use as server\"), do_follow_server)\n            elif item_type == self.ItemType.DISCONNECTED_SERVER:\n                def do_set_server():\n                    self.setServer.emit(str(server))\n                menu.addAction(read_QIcon(\"chevron-right.png\"), _(\"Use as server\"), do_set_server)\n\n            def set_bookmark(*, add: bool):\n                self.network.set_server_bookmark(server, add=add)\n                self.update()\n\n            if self.network.is_server_bookmarked(server):\n                menu.addAction(read_QIcon(\"bookmark_remove.png\"), _(\"Remove from bookmarks\"), lambda: set_bookmark(add=False))\n            else:\n                menu.addAction(read_QIcon(\"bookmark_add.png\"), _(\"Bookmark this server\"), lambda: set_bookmark(add=True))\n        elif item_type == self.ItemType.CHAIN:\n            chain_id = item.data(0, self.CHAIN_ID_ROLE)\n\n            def do_follow_chain():\n                self.followChain.emit(chain_id)\n\n            menu.addAction(_(\"Follow this branch\"), do_follow_chain)\n        else:\n            return\n        menu.exec(self.viewport().mapToGlobal(position))\n\n    def keyPressEvent(self, event):\n        if event.key() in [Qt.Key.Key_F2, Qt.Key.Key_Return, Qt.Key.Key_Enter]:\n            self.on_activated(self.currentItem(), self.currentColumn())\n        else:\n            QTreeWidget.keyPressEvent(self, event)\n\n    def on_activated(self, item, column):\n        # on 'enter' we show the menu\n        pt = self.visualItemRect(item).bottomLeft()\n        pt.setX(50)\n        self.customContextMenuRequested.emit(pt)\n\n    def update(self):\n        self.clear()\n        network = self.network\n\n        # connected servers\n        connected_servers_item = QTreeWidgetItem([_(\"Connected nodes\"), ''])\n        connected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)\n        chains = network.get_blockchains()\n        n_chains = len(chains)\n        for chain_id, interfaces in chains.items():\n            b = blockchain.blockchains.get(chain_id)\n            if b is None:\n                continue\n            name = b.get_name()\n            if n_chains > 1:\n                x = QTreeWidgetItem([name + '@%d'%b.get_max_forkpoint(), '%d'%b.height()])\n                x.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CHAIN)\n                x.setData(0, self.CHAIN_ID_ROLE, b.get_id())\n            else:\n                x = connected_servers_item\n            for i in interfaces:\n                item = QTreeWidgetItem([f\"{i.server.to_friendly_name()}\", '%d'%i.tip])\n                item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CONNECTED_SERVER)\n                item.setData(0, self.SERVER_ADDR_ROLE, i.server)\n                item.setToolTip(0, str(i.server))\n                if i == network.interface:\n                    item.setIcon(0, read_QIcon(\"chevron-right.png\"))\n                elif network.is_server_bookmarked(i.server):\n                    item.setIcon(0, read_QIcon(\"bookmark.png\"))\n                x.addChild(item)\n            if n_chains > 1:\n                connected_servers_item.addChild(x)\n\n        # disconnected servers\n        disconnected_servers_item = QTreeWidgetItem([_(\"Other known servers\"), \"\"])\n        disconnected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)\n        for server in network.get_disconnected_server_addrs():\n            item = QTreeWidgetItem([server.to_friendly_name(), \"\"])\n            item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.DISCONNECTED_SERVER)\n            item.setData(0, self.SERVER_ADDR_ROLE, server)\n            if network.is_server_bookmarked(server):\n                item.setIcon(0, read_QIcon(\"bookmark.png\"))\n            disconnected_servers_item.addChild(item)\n\n        self.addTopLevelItem(connected_servers_item)\n        self.addTopLevelItem(disconnected_servers_item)\n\n        connected_servers_item.setExpanded(True)\n        for i in range(connected_servers_item.childCount()):\n            connected_servers_item.child(i).setExpanded(True)\n        disconnected_servers_item.setExpanded(True)\n\n        # headers\n        h = self.header()\n        h.setStretchLastSection(False)\n        h.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)\n        h.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)\n\n        super().update()\n\n\nclass ProxyWidget(QWidget):\n    PROXY_MODES = {\n        'socks4': 'SOCKS4',\n        'socks5': 'SOCKS5/TOR'\n    }\n\n    torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])\n\n    def __init__(self, network: Network, parent=None):\n        super().__init__(parent)\n        self.network = network\n        self.config = network.config\n\n        fixed_width_port = 6 * char_width_in_lineedit()\n\n        # proxy setting.\n        self.proxy_cb = QCheckBox(_('Use proxy'))\n        self.proxy_mode = QComboBox()\n        for k, v in self.PROXY_MODES.items():\n            self.proxy_mode.addItem(v, k)\n        self.proxy_mode.setCurrentIndex(1)\n        self.proxy_host = QLineEdit()\n        self.proxy_port = QLineEdit()\n        self.proxy_port.setFixedWidth(fixed_width_port)\n        self.proxy_port_validator = QIntValidator(1, 65535)\n        self.proxy_port.setValidator(self.proxy_port_validator)\n\n        self.proxy_user = QLineEdit()\n        self.proxy_user.setPlaceholderText(_(\"Proxy username\"))\n        self.proxy_password = PasswordLineEdit()\n        self.proxy_password.setPlaceholderText(_(\"Proxy password\"))\n\n        grid = QGridLayout(self)\n        grid.setSpacing(8)\n\n        grid.addWidget(self.proxy_cb, 0, 0, 1, 4)\n        proxy_helpbutton = HelpButton(\n            _('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))\n        grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)\n        grid.addWidget(self.proxy_mode, 1, 0, 1, 1)\n        grid.addWidget(self.proxy_host, 1, 1, 1, 3)\n        grid.addWidget(self.proxy_port, 1, 4, 1, 1)\n        grid.addWidget(self.proxy_user, 2, 1, 1, 2)\n        grid.addWidget(self.proxy_password, 2, 3, 1, 2)\n\n        detect_l = QHBoxLayout()\n        self.detect_button = QPushButton(_('Detect Tor proxy'))\n        self.spinner = Spinner()\n        self.spinner.setMargin(5)\n        detect_l.addWidget(self.detect_button)\n        detect_l.addWidget(self.spinner)\n\n        grid.addLayout(detect_l, 3, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignLeft)\n\n        spacer = QVBoxLayout()\n        spacer.addStretch(1)\n        grid.addLayout(spacer, 4, 0, 1, 5)\n\n        self.update_from_config()\n        self.update()\n\n        # connect signal handlers after init from config\n        self.proxy_cb.stateChanged.connect(self.on_proxy_enable_toggle)\n        self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)\n        self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)\n        self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)\n        self.proxy_user.editingFinished.connect(self.on_proxy_settings_changed)\n        self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)\n        self.detect_button.clicked.connect(self.detect_tor)\n\n        self.torProbeFinished.connect(self.on_tor_probe_finished)\n\n    def update(self):\n        enabled = self.proxy_cb.isChecked() and self.config.cv.NETWORK_PROXY.is_modifiable()\n        for item in [\n                self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password,\n                self.detect_button\n        ]:\n            item.setEnabled(enabled)\n\n        if not self.proxy_port.hasAcceptableInput() and not is_valid_port(self.proxy_port.text()):\n            return\n\n        if not is_valid_host(self.proxy_host.text()):\n            return\n\n        net_params = self.network.get_parameters()\n        proxy = self.get_proxy_settings()\n        net_params = net_params._replace(proxy=proxy)\n        self.network.run_from_another_thread(self.network.set_parameters(net_params))\n\n    def update_from_config(self):\n        proxy = ProxySettings.from_config(self.config)\n        self.proxy_cb.setChecked(proxy.enabled)\n        self.proxy_mode.setCurrentText(self.PROXY_MODES.get(proxy.mode))\n        self.proxy_host.setText(proxy.host)\n        self.proxy_port.setText(proxy.port)\n        self.proxy_user.setText(proxy.user)\n        self.proxy_password.setText(proxy.password)\n\n        if not self.config.cv.NETWORK_PROXY.is_modifiable():\n            for w in [\n                    self.proxy_cb, self.proxy_mode, self.proxy_host, self.proxy_port,\n                    self.proxy_user, self.proxy_password, self.detect_button\n            ]:\n                w.setEnabled(False)\n\n    def on_proxy_enable_toggle(self):\n        # probe if enabled and no pre-existing settings\n        # if self.proxy_cb.isChecked() and (not self.proxy_host.text() or not self.proxy_port.text()):\n        #     self.detect_tor()\n        self.update()\n\n    def on_proxy_settings_changed(self):\n        self.update()\n\n    def get_proxy_settings(self) -> ProxySettings:\n        proxy = ProxySettings()\n        proxy.enabled = self.proxy_cb.isChecked()\n        proxy.mode = self.proxy_mode.currentData()\n        proxy.host = self.proxy_host.text()\n        proxy.port = self.proxy_port.text()\n        proxy.user = self.proxy_user.text()\n        proxy.password = self.proxy_password.text()\n        return proxy\n\n    def detect_tor(self):\n        self.detect_button.setEnabled(False)\n        self.spinner.setVisible(True)\n        ProxySettings.probe_tor(self.torProbeFinished.emit)  # via signal\n\n    @pyqtSlot(str, int)\n    def on_tor_probe_finished(self, host: str, port: int):\n        self.detect_button.setEnabled(True)\n        self.spinner.setVisible(False)\n        if host:\n            self.proxy_mode.setCurrentIndex(1)\n            self.proxy_host.setText(host)\n            self.proxy_port.setText(str(port))\n            self.update()\n\n\nclass ConnectMode(IntEnum):\n    AUTOCONNECT = 0\n    MANUAL      = 1\n    ONESERVER   = 2\n\nclass ServerWidget(QWidget, QtEventListener):\n    CONNECT_MODES = {\n        ConnectMode.AUTOCONNECT: messages.MSG_CONNECTMODE_AUTOCONNECT,\n        ConnectMode.MANUAL: messages.MSG_CONNECTMODE_MANUAL,\n        ConnectMode.ONESERVER: messages.MSG_CONNECTMODE_ONESERVER,\n    }\n\n    server_e_valid = pyqtSignal(bool)\n\n    def __init__(self, network: Network, parent=None):\n        super().__init__(parent)\n        self.network = network\n        self.config = network.config\n\n        self.setLayout(QVBoxLayout())\n\n        grid = QGridLayout()\n\n        self.connect_combo = QComboBox()\n        for i, v in sorted(self.CONNECT_MODES.items()):\n            self.connect_combo.addItem(v, i)\n        self.connect_combo.currentIndexChanged.connect(self.on_server_settings_changed)\n        grid.addWidget(QLabel(_('Connection mode') + ':'), 0, 0)\n        msg = (\n            f\"\"\"\n            {messages.MSG_CONNECTMODE_SERVER_HELP}<br/><br/>\n            {messages.MSG_CONNECTMODE_NODES_HELP}\n            <ul>\n            <li><b>{messages.MSG_CONNECTMODE_AUTOCONNECT}</b>: {messages.MSG_CONNECTMODE_AUTOCONNECT_HELP}</li>\n            <li><b>{messages.MSG_CONNECTMODE_MANUAL}</b>: {messages.MSG_CONNECTMODE_MANUAL_HELP}</li>\n            <li><b>{messages.MSG_CONNECTMODE_ONESERVER}</b>: {messages.MSG_CONNECTMODE_ONESERVER_HELP}</li>\n            </ul>\n            \"\"\"\n        )\n        grid.addWidget(HelpButton(msg), 0, 4)\n        grid.addWidget(self.connect_combo, 0, 1, 1, 3)\n\n        self.server_e = QLineEdit()\n        self.server_e.textChanged.connect(self.validate_server_e)\n        self.server_e.editingFinished.connect(self.on_server_settings_changed)\n        grid.addWidget(QLabel(_('Server') + ':'), 1, 0)\n        grid.addWidget(self.server_e, 1, 1, 1, 3)\n        grid.addWidget(HelpButton(messages.MSG_CONNECTMODE_SERVER_HELP), 1, 4)\n\n        self.status_label_header = QLabel(_('Status') + ':')\n        self.status_label = QLabel('')\n        self.status_label_helpbutton = HelpButton(messages.MSG_CONNECTMODE_NODES_HELP)\n        grid.addWidget(self.status_label_header, 2, 0)\n        grid.addWidget(self.status_label, 2, 1, 1, 3)\n        grid.addWidget(self.status_label_helpbutton, 2, 4)\n\n        msg = _('This is the height of your local copy of the blockchain.')\n        self.height_label_header = QLabel(_('Blockchain') + ':')\n        self.height_label = QLabel('')\n        self.height_label_helpbutton = HelpButton(msg)\n        grid.addWidget(self.height_label_header, 3, 0)\n        grid.addWidget(self.height_label, 3, 1)\n        grid.addWidget(self.height_label_helpbutton, 3, 4)\n\n        self.split_label = QLabel('')\n        grid.addWidget(self.split_label, 4, 1, 1, 3)\n\n        self.layout().addLayout(grid)\n\n        self.nodes_list_widget = NodesListWidget(network=self.network)\n        self.nodes_list_widget.followServer.connect(self.follow_server)\n        self.nodes_list_widget.followChain.connect(self.follow_branch)\n\n        def do_set_server(server):\n            self.server_e.setText(server)\n            if self.is_auto_connect():\n                # switch to manual mode as the user manually selected a server\n                self.set_connect_mode(ConnectMode.MANUAL, block_signals=True)\n            self.on_server_settings_changed()\n        self.nodes_list_widget.setServer.connect(do_set_server)\n\n        self.layout().addWidget(self.nodes_list_widget)\n        self.nodes_list_widget.update()\n\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.unregister_callbacks())\n\n    def showEvent(self, event):\n        # gets called every time the ServerWidget is shown, when opening it and when\n        # switching between the tabs.\n        super().showEvent(event)\n        _logger.debug(f\"showing ServerWidget\")\n        # If the user entered garbage the previous time the ServerWidget was open this will restore\n        # it back to the current config\n        self.update_from_config()\n        self.update()\n\n    @qt_event_listener\n    def on_event_network_updated(self):\n        self.nodes_list_widget.update()  # NOTE: move event handling to widget itself?\n        self.update()\n\n    def is_auto_connect(self):\n        return self.connect_combo.currentIndex() == ConnectMode.AUTOCONNECT\n\n    def is_one_server(self):\n        return self.connect_combo.currentIndex() == ConnectMode.ONESERVER\n\n    def set_connect_mode(self, connect_mode: ConnectMode, *, block_signals = False):\n        # if block_signals = True the on_server_settings_changed won't get called when changing the index\n        assert isinstance(connect_mode, ConnectMode), connect_mode\n        self.connect_combo.blockSignals(block_signals)\n        self.connect_combo.setCurrentIndex(connect_mode)\n        self.connect_combo.blockSignals(False)\n\n    def on_server_settings_changed(self):\n        if not self.network._was_started:\n            self.update()\n            return\n\n        current_net_params = self.network.get_parameters()\n        new_server = ServerAddr.from_str_with_inference(self.server_e.text().strip())\n        new_server = new_server or current_net_params.server  # keep existing server while input is invalid\n\n        settings_changed = False\n        if new_server != current_net_params.server:\n            settings_changed = True\n        if self.is_auto_connect() != current_net_params.auto_connect:\n            settings_changed = True\n        if self.is_one_server() != current_net_params.oneserver:\n            settings_changed = True\n\n        if settings_changed:\n            _logger.debug(\n                f\"ServerWidget.on_server_settings_changed:\\n\"\n                f\"[server: {current_net_params.server} -> {new_server}]\\n\"\n                f\"[auto_connect: {current_net_params.auto_connect} -> {self.is_auto_connect()}]\\n\"\n                f\"[oneserver: {current_net_params.oneserver} -> {self.is_one_server()}]\"\n            )\n            self.set_server(\n                new_server,\n                auto_connect=self.is_auto_connect(),\n                one_server=self.is_one_server(),\n            )\n            self.update()\n\n    def update(self):\n        self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not self.is_auto_connect())\n        if self.is_auto_connect():\n            self.server_e.clear()\n        elif not self.server_e.text():\n            self.server_e.setText(self.config.NETWORK_SERVER or \"\")\n        for item in [\n                self.status_label_header, self.status_label, self.status_label_helpbutton,\n                self.height_label_header, self.height_label, self.height_label_helpbutton]:\n            item.setVisible(self.network._was_started)\n        self.validate_server_e()\n        msg = _('Fork detection disabled') if self.is_one_server() else ''\n        if self.network._was_started:\n            # Network was started, so we don't run in initial setup wizard.\n            # behavior in this case is to apply changes immediately.\n            # Also, we show block height and potential chain tips\n            height_str = _('{} blocks').format(self.network.get_local_height())\n            self.height_label.setText(height_str)\n            self.status_label.setText(self.network.get_status())\n            chains = self.network.get_blockchains()\n            if len(chains) > 1:\n                chain = self.network.blockchain()\n                forkpoint = chain.get_max_forkpoint()\n                name = chain.get_name()\n                msg = _('Fork detected at block {0}').format(forkpoint) + '\\n'\n                if self.is_auto_connect():\n                    msg += _('You are following branch {}').format(name)\n                else:\n                    msg += _('Your server is on branch {0} ({1} blocks)').format(name, chain.get_branch_size())\n        self.split_label.setText(msg)\n\n    def validate_server_e(self):\n        if not self.server_e.isEnabled():\n            self.server_e.setStyleSheet(\"\")\n            self.server_e_valid.emit(True)\n            return\n        server = ServerAddr.from_str_with_inference(self.server_e.text())\n        self.server_e.setStyleSheet(\"background-color: rgba(255, 0, 0, 0.2);\" if not server else \"\")\n        self.server_e_valid.emit(server is not None)\n\n    def update_from_config(self):\n        auto_connect = self.config.NETWORK_AUTO_CONNECT\n        one_server = self.config.NETWORK_ONESERVER\n        v = ConnectMode.AUTOCONNECT if auto_connect else ConnectMode.ONESERVER if one_server else ConnectMode.MANUAL\n        self.set_connect_mode(v)\n\n        server = self.config.NETWORK_SERVER\n        self.server_e.setText(server)\n\n        self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not auto_connect)\n        self.nodes_list_widget.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable())\n        _logger.debug(f\"update from config: done\")\n\n    def follow_branch(self, chain_id):\n        self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))\n        # follow_chain_given_id connects to random interface, so set connect_mode back to AUTOCONNECT\n        self.set_connect_mode(ConnectMode.AUTOCONNECT, block_signals=True)\n        self.update()\n\n    def follow_server(self, server: ServerAddr):\n        try:\n            self.network.follow_chain_given_server(server)\n        except KeyError:\n            _logger.debug(f\"follow_server: cannot follow, not connected to {server.net_addr_str()}.\")\n            return\n\n        self.server_e.setText(str(server))\n        if self.is_auto_connect():\n            # the user manually selected a server, so the ConnectMode gets set to MANUAL\n            self.set_connect_mode(ConnectMode.MANUAL, block_signals=True)\n\n        self.set_server(\n            server=server,\n            auto_connect=False,\n            one_server=self.is_one_server(),\n        )\n        self.update()\n\n    def set_server(self, server: ServerAddr, *, auto_connect: bool, one_server: bool):\n        current_net_params = self.network.get_parameters()\n        new_net_params = current_net_params._replace(\n            server=server,\n            auto_connect=auto_connect,\n            oneserver=one_server,\n        )\n        _logger.debug(f\"set_server: {new_net_params=}\")\n        self.network.run_from_another_thread(self.network.set_parameters(new_net_params))\n\n\nclass NostrWidget(QWidget, QtEventListener):\n\n    def __init__(self, network: Network, parent=None):\n        super().__init__(parent)\n        self.network = network\n        self.config = network.config\n        vbox = QVBoxLayout()\n        self.setLayout(vbox)\n        grid = QGridLayout()\n        nostr_relays_label = QLabel(self.config.cv.NOSTR_RELAYS.get_short_desc())\n        nostr_helpbutton = HelpButton(self.config.cv.NOSTR_RELAYS.get_long_desc())\n        grid.addWidget(nostr_relays_label, 0, 0)\n        grid.addWidget(nostr_helpbutton, 0, 1)\n        vbox.addLayout(grid)\n\n        self.relays_list = QListWidget()\n        self.relay_edit = QLineEdit()\n        self.relay_edit.textChanged.connect(self.on_relay_edited)\n        vbox.addWidget(self.relays_list)\n        vbox.addStretch()\n        self.add_button = QPushButton(_('Add'))\n        self.add_button.clicked.connect(self.add_relay)\n        self.add_button.setEnabled(False)\n        remove_button = QPushButton(_('Remove'))\n        remove_button.clicked.connect(self.remove_relay)\n        reset_button = QPushButton(_('Reset'))\n        reset_button.clicked.connect(self.reset_relays)\n        buttons = Buttons(self.relay_edit, self.add_button, remove_button, reset_button)\n        vbox.addLayout(buttons)\n        self.update_list()\n\n    def on_relay_edited(self, text):\n        self.add_button.setEnabled(is_valid_websocket_url(text))\n\n    def update_list(self):\n        self.relays_list.clear()\n        for relay in self.config.get_nostr_relays():\n            item = QListWidgetItem(relay)\n            self.relays_list.addItem(item)\n\n    def add_relay(self):\n        relay = self.relay_edit.text()\n        self.config.add_nostr_relay(relay)\n        self.update_list()\n\n    def remove_relay(self):\n        item = self.relays_list.currentItem()\n        if item is None:\n            return\n        self.config.remove_nostr_relay(item.text())\n        self.update_list()\n\n    def reset_relays(self):\n        self.config.NOSTR_RELAYS = None\n        self.update_list()\n"
  },
  {
    "path": "electrum/gui/qt/new_channel_dialog.py",
    "content": "from typing import TYPE_CHECKING, Optional\nfrom PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton, QComboBox, QLineEdit, QHBoxLayout\n\nimport electrum_ecc as ecc\n\nfrom electrum.i18n import _\nfrom electrum.lnutil import MIN_FUNDING_SAT\nfrom electrum.lnworker import hardcoded_trampoline_nodes\nfrom electrum.util import NotEnoughFunds, NoDynamicFeeEstimates\nfrom electrum.fee_policy import FeePolicy\nfrom electrum.lntransport import extract_nodeid, ConnStringFormatError\n\nfrom .util import (WindowModalDialog, Buttons, OkButton, CancelButton,\n                   EnterButton, WWLabel, char_width_in_lineedit)\nfrom .amountedit import BTCAmountEdit\nfrom .my_treeview import create_toolbar_with_menu\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nclass NewChannelDialog(WindowModalDialog):\n\n    def __init__(self, window: 'ElectrumWindow', amount_sat: Optional[int] = None, min_amount_sat: Optional[int] = None):\n        WindowModalDialog.__init__(self, window, _('Open Channel'))\n        self.window = window\n        self.network = window.network\n        self.config = window.config\n        self.lnworker = self.window.wallet.lnworker\n        self.trampolines = hardcoded_trampoline_nodes()\n        self.trampoline_names = list(self.trampolines.keys())\n        self.min_amount_sat = min_amount_sat or MIN_FUNDING_SAT\n        vbox = QVBoxLayout(self)\n        toolbar, menu = create_toolbar_with_menu(self.config, '')\n        menu.addConfig(\n            self.config.cv.LIGHTNING_USE_RECOVERABLE_CHANNELS,\n            checked=self.lnworker.has_recoverable_channels(),\n        ).setEnabled(self.lnworker.can_have_recoverable_channels())\n        vbox.addLayout(toolbar)\n        msg = _('Choose a remote node and an amount to fund the channel.')\n        msg += '\\n' + _('Minimum required amount: {}').format(self.window.format_amount_and_units(self.min_amount_sat))\n        vbox.addWidget(WWLabel(msg))\n        if self.network.channel_db:\n            vbox.addWidget(QLabel(_('Enter Remote Node ID or connection string or invoice')))\n            self.remote_nodeid = QLineEdit()\n            self.remote_nodeid.setMinimumWidth(700)\n            self.remote_nodeid.textChanged.connect(self.maybe_enable_ok_button)\n            self.suggest_button = QPushButton(self, text=_('Suggest Peer'))\n            self.suggest_button.clicked.connect(self.on_suggest)\n        else:\n            self.trampoline_combo = QComboBox()\n            self.trampoline_combo.addItems(self.trampoline_names)\n            # index 1 is \"Electrum trampoline\" on mainnet, this defaults to -1 if 1 is not available\n            self.trampoline_combo.setCurrentIndex(1)\n            self.trampoline_combo.currentIndexChanged.connect(self.maybe_enable_ok_button)\n        self.amount_e = BTCAmountEdit(self.window.get_decimal_point)\n        self.amount_e.setAmount(amount_sat)\n        self.amount_e.textChanged.connect(self.maybe_enable_ok_button)\n\n        btn_width = 10 * char_width_in_lineedit()\n        self.min_button = EnterButton(_(\"Min\"), self.spend_min)\n        self.min_button.setEnabled(bool(self.min_amount_sat))\n        self.min_button.setFixedWidth(btn_width)\n        self.max_button = EnterButton(_(\"Max\"), self.spend_max)\n        self.max_button.setFixedWidth(btn_width)\n        self.max_button.setCheckable(True)\n        self.clear_button = QPushButton(self, text=_('Clear'))\n        self.clear_button.clicked.connect(self.on_clear)\n        self.clear_button.setFixedWidth(btn_width)\n        h = QGridLayout()\n        if self.network.channel_db:\n            h.addWidget(QLabel(_('Remote Node ID')), 0, 0)\n            h.addWidget(self.remote_nodeid, 0, 1, 1, 4)\n            h.addWidget(self.suggest_button, 0, 5)\n        else:\n            h.addWidget(QLabel(_('Remote Node')), 0, 0)\n            h.addWidget(self.trampoline_combo, 0, 1, 1, 4)\n        h.addWidget(QLabel('Amount'), 2, 0)\n\n        amt_hbox = QHBoxLayout()\n        amt_hbox.setContentsMargins(0, 0, 0, 0)\n        amt_hbox.addWidget(self.amount_e)\n        amt_hbox.addWidget(self.min_button)\n        amt_hbox.addWidget(self.max_button)\n        amt_hbox.addWidget(self.clear_button)\n        amt_hbox.addStretch()\n        h.addLayout(amt_hbox, 2, 1, 1, 4)\n\n        vbox.addLayout(h)\n        vbox.addStretch()\n        self.ok_button = OkButton(self)\n        self.ok_button.setDefault(True)\n        self.maybe_enable_ok_button()\n        vbox.addLayout(Buttons(CancelButton(self), self.ok_button))\n\n    def maybe_enable_ok_button(self):\n        enable = True\n        if self.network.channel_db:\n            try:\n                extract_nodeid(str(self.remote_nodeid.text()).strip())\n            except ConnStringFormatError:\n                enable = False\n        else:\n            try:\n                self.trampoline_names[self.trampoline_combo.currentIndex()]\n            except IndexError:\n                enable = False\n        if not self.amount_e.get_amount():\n            enable = False\n        self.ok_button.setEnabled(enable)\n\n    def on_suggest(self):\n        self.network.start_gossip()\n        nodeid = (self.lnworker.suggest_peer() or b\"\").hex()\n        if not nodeid:\n            self.remote_nodeid.setText(\"\")\n            self.remote_nodeid.setPlaceholderText(\n                _(\"Couldn't find suitable peer yet, try again later.\")\n            )\n        else:\n            self.remote_nodeid.setText(nodeid)\n        self.remote_nodeid.repaint()  # macOS hack for #6269\n\n    def on_clear(self):\n        self.amount_e.setText('')\n        self.amount_e.setFrozen(False)\n        self.amount_e.repaint()  # macOS hack for #6269\n        if self.network.channel_db:\n            self.remote_nodeid.setText('')\n            self.remote_nodeid.repaint()  # macOS hack for #6269\n        self.max_button.setChecked(False)\n        self.max_button.repaint()  # macOS hack for #6269\n\n    def spend_min(self):\n        self.max_button.setChecked(False)\n        self.amount_e.setFrozen(False)\n        self.amount_e.setAmount(self.min_amount_sat)\n\n    def spend_max(self):\n        self.amount_e.setFrozen(self.max_button.isChecked())\n        if not self.max_button.isChecked():\n            return\n        dummy_nodeid = ecc.GENERATOR.get_public_key_bytes(compressed=True)\n        make_tx = self.window.mktx_for_open_channel(funding_sat='!', node_id=dummy_nodeid)\n        try:\n            tx = make_tx(FeePolicy(self.config.FEE_POLICY))\n        except (NotEnoughFunds, NoDynamicFeeEstimates) as e:\n            self.max_button.setChecked(False)\n            self.amount_e.setFrozen(False)\n            self.window.show_error(str(e))\n            return\n        amount = tx.output_value()\n        amount = min(amount, self.config.LIGHTNING_MAX_FUNDING_SAT)\n        self.amount_e.setAmount(amount)\n\n    def run(self):\n        if not self.exec():\n            return\n        if self.max_button.isChecked() and self.amount_e.get_amount() < self.config.LIGHTNING_MAX_FUNDING_SAT:\n            # if 'max' enabled and amount is strictly less than max allowed,\n            # that means we have fewer coins than max allowed, and hence we can\n            # spend all coins\n            funding_sat = '!'\n        else:\n            funding_sat = self.amount_e.get_amount()\n        if not funding_sat:\n            return\n        if funding_sat != '!':\n            if self.min_amount_sat and funding_sat < self.min_amount_sat:\n                self.window.show_error(_('Amount too low'))\n                return\n        if self.network.channel_db:\n            connect_str = str(self.remote_nodeid.text()).strip()\n        else:\n            name = self.trampoline_names[self.trampoline_combo.currentIndex()]\n            connect_str = str(self.trampolines[name])\n        if not connect_str:\n            return\n        self.window.open_channel(connect_str, funding_sat, 0)\n        return True\n"
  },
  {
    "path": "electrum/gui/qt/password_dialog.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2013 ecdsa@github\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport re\nimport math\n\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtGui import QPixmap\nfrom PyQt6.QtWidgets import QLabel, QGridLayout, QVBoxLayout, QCheckBox\n\nfrom electrum.i18n import _\nfrom electrum.plugin import run_hook\n\nfrom .util import icon_path, WindowModalDialog, OkButton, CancelButton, Buttons, PasswordLineEdit\n\n\ndef check_password_strength(password):\n\n    '''\n    Check the strength of the password entered by the user and return back the same\n    :param password: password entered by user in New Password\n    :return: password strength Weak or Medium or Strong\n    '''\n    password = password\n    n = math.log(len(set(password)))\n    num = re.search(\"[0-9]\", password) is not None and re.match(\"^[0-9]*$\", password) is None\n    caps = password != password.upper() and password != password.lower()\n    extra = re.match(\"^[a-zA-Z0-9]*$\", password) is None\n    score = len(password)*(n + caps + num + extra)/20\n    password_strength = {0:\"Weak\",1:\"Medium\",2:\"Strong\",3:\"Very Strong\"}\n    return password_strength[min(3, int(score))]\n\n\nPW_NEW, PW_CHANGE, PW_PASSPHRASE = range(0, 3)\n\nMSG_ENTER_PASSWORD = _(\"Choose a password to encrypt your wallet keys.\") + '\\n'\\\n                     + _(\"Leave this field empty if you want to disable encryption.\")\n\n\nclass PasswordLayout(object):\n\n    titles = [_(\"Enter Password\"), _(\"Change Password\"), _(\"Enter Passphrase\")]\n\n    def __init__(self, msg, kind, OK_button, wallet=None):\n        self.wallet = wallet\n\n        self.pw = PasswordLineEdit()\n        self.new_pw = PasswordLineEdit()\n        self.conf_pw = PasswordLineEdit()\n        self.kind = kind\n        self.OK_button = OK_button\n\n        vbox = QVBoxLayout()\n        label = QLabel(msg + \"\\n\")\n        label.setWordWrap(True)\n\n        self.grid = grid = QGridLayout()\n        grid.setSpacing(8)\n        grid.setColumnMinimumWidth(0, 150)\n        grid.setColumnMinimumWidth(1, 100)\n        grid.setColumnStretch(1,1)\n\n        if kind == PW_PASSPHRASE:\n            vbox.addWidget(label)\n            msgs = [_('Passphrase:'), _('Confirm Passphrase:')]\n        else:\n            logo_grid = QGridLayout()\n            logo_grid.setSpacing(8)\n            logo_grid.setColumnMinimumWidth(0, 70)\n            logo_grid.setColumnStretch(1,1)\n\n            logo = QLabel()\n            logo.setAlignment(Qt.AlignmentFlag.AlignCenter)\n\n            logo_grid.addWidget(logo,  0, 0)\n            logo_grid.addWidget(label, 0, 1, 1, 2)\n            vbox.addLayout(logo_grid)\n\n            m1 = _('New Password:') if kind == PW_CHANGE else _('Password:')\n            msgs = [m1, _('Confirm Password:')]\n            if wallet and wallet.has_password() and not wallet.storage.is_encrypted_with_hw_device():\n                grid.addWidget(QLabel(_('Current Password:')), 0, 0)\n                grid.addWidget(self.pw, 0, 1)\n                lockfile = \"lock.png\"\n            else:\n                lockfile = \"unlock.png\"\n            logo.setPixmap(QPixmap(icon_path(lockfile))\n                           .scaledToWidth(36, mode=Qt.TransformationMode.SmoothTransformation))\n\n        self.new_password_label = QLabel(msgs[0])\n        grid.addWidget(self.new_password_label, 1, 0)\n        grid.addWidget(self.new_pw, 1, 1)\n\n        self.confirm_password_label = QLabel(msgs[1])\n        grid.addWidget(self.confirm_password_label, 2, 0)\n        grid.addWidget(self.conf_pw, 2, 1)\n        vbox.addLayout(grid)\n\n        # Password Strength Label\n        if kind != PW_PASSPHRASE:\n            self.pw_strength = QLabel()\n            grid.addWidget(self.pw_strength, 3, 0, 1, 2)\n            self.new_pw.textChanged.connect(self.pw_changed)\n\n        def enable_OK():\n            ok = self.new_pw.text() == self.conf_pw.text()\n            OK_button.setEnabled(ok)\n        self.new_pw.textChanged.connect(enable_OK)\n        self.conf_pw.textChanged.connect(enable_OK)\n        enable_OK()\n\n        self.vbox = vbox\n\n    def title(self):\n        return self.titles[self.kind]\n\n    def layout(self):\n        return self.vbox\n\n    def pw_changed(self):\n        password = self.new_pw.text()\n        if password:\n            colors = {\"Weak\":\"Red\", \"Medium\":\"Blue\", \"Strong\":\"Green\",\n                      \"Very Strong\":\"Green\"}\n            strength = check_password_strength(password)\n            label = (_(\"Password Strength\") + \": \" + \"<font color=\"\n                     + colors[strength] + \">\" + strength + \"</font>\")\n        else:\n            label = \"\"\n        self.pw_strength.setText(label)\n\n    def old_password(self):\n        if self.kind == PW_CHANGE:\n            return self.pw.text() or None\n        return None\n\n    def new_password(self):\n        pw = self.new_pw.text()\n        # Empty passphrases are fine and returned empty.\n        if pw == \"\" and self.kind != PW_PASSPHRASE:\n            pw = None\n        return pw\n\n    def clear_password_fields(self):\n        for field in [self.pw, self.new_pw, self.conf_pw]:\n            field.clear()\n\n\nclass PasswordLayoutForHW(PasswordLayout):\n\n    def __init__(self, msg, kind, OK_button, wallet=None):\n        PasswordLayout.__init__(self, msg, kind, OK_button, wallet=wallet)\n        self.encrypt_cb = QCheckBox(_('Encrypt wallet file using hardware wallet device'))\n        self.encrypt_cb.setToolTip(_('If you enable this setting, you will need your hardware device to open your wallet.'))\n        self.encrypt_cb.stateChanged.connect(self.on_encrypt_cb)\n        self.grid.addWidget(self.encrypt_cb, 4, 0, 1, 2)\n        self.encrypt_cb.setChecked(wallet.storage.is_encrypted_with_hw_device() if wallet else True)\n\n    def on_encrypt_cb(self, checked):\n        checked = bool(checked)\n        self.new_pw.setVisible(not checked)\n        self.conf_pw.setVisible(not checked)\n        self.new_password_label.setVisible(not checked)\n        self.confirm_password_label.setVisible(not checked)\n\n    def should_encrypt_storage_with_xpub(self):\n        return self.encrypt_cb.isChecked()\n\n\n\nclass ChangePasswordDialogBase(WindowModalDialog):\n\n    def __init__(self, parent, wallet):\n        WindowModalDialog.__init__(self, parent)\n        is_encrypted = wallet.has_storage_encryption()\n        OK_button = OkButton(self)\n\n        self.create_password_layout(wallet, is_encrypted, OK_button)\n\n        self.setWindowTitle(self.playout.title())\n        vbox = QVBoxLayout(self)\n        vbox.addLayout(self.playout.layout())\n        vbox.addStretch(1)\n        vbox.addLayout(Buttons(CancelButton(self), OK_button))\n\n    def create_password_layout(self, wallet, is_encrypted, OK_button):\n        raise NotImplementedError()\n\n\nclass NewPasswordDialog(WindowModalDialog):\n\n    def __init__(self, parent, msg):\n        self.msg = msg\n        WindowModalDialog.__init__(self, parent)\n        OK_button = OkButton(self)\n        self.playout = PasswordLayout(\n            msg=self.msg,\n            kind=PW_CHANGE,\n            OK_button=OK_button,\n            wallet=None)\n        self.setWindowTitle(self.playout.title())\n        vbox = QVBoxLayout(self)\n        vbox.addLayout(self.playout.layout())\n        vbox.addStretch(1)\n        vbox.addLayout(Buttons(CancelButton(self), OK_button))\n\n    def run(self):\n        try:\n            if not self.exec():\n                return None\n            return self.playout.new_password()\n        finally:\n            self.playout.clear_password_fields()\n\n\nclass ChangePasswordDialogForSW(ChangePasswordDialogBase):\n\n    def create_password_layout(self, wallet, is_encrypted, OK_button):\n        if not wallet.has_password():\n            msg = _('Your wallet is not protected.')\n            msg += ' ' + _('Use this dialog to add a password to your wallet.')\n        else:\n            if not is_encrypted:\n                msg = _('Your bitcoins are password protected. However, your wallet file is not encrypted.')\n            else:\n                msg = _('Your wallet is password protected and encrypted.')\n            msg += ' ' + _('Use this dialog to change your password.')\n        self.playout = PasswordLayout(\n            msg=msg,\n            kind=PW_CHANGE,\n            OK_button=OK_button,\n            wallet=wallet)\n\n    def run(self):\n        try:\n            if not self.exec():\n                return False, None, None, None\n            return True, self.playout.old_password(), self.playout.new_password(), True\n        finally:\n            self.playout.clear_password_fields()\n\n\nclass ChangePasswordDialogForHW(ChangePasswordDialogBase):\n\n    def __init__(self, parent, wallet):\n        ChangePasswordDialogBase.__init__(self, parent, wallet)\n\n    def create_password_layout(self, wallet, is_encrypted, OK_button):\n        if not is_encrypted:\n            msg = _('Your wallet file is NOT encrypted.')\n        else:\n            if wallet.storage.is_encrypted_with_hw_device():\n                msg = _('Your wallet file is encrypted with your hardware device.')\n            else:\n                msg = _('Your wallet file is password-encrypted.')\n        self.playout = PasswordLayoutForHW(\n            msg=msg,\n            kind=PW_CHANGE,\n            OK_button=OK_button,\n            wallet=wallet)\n\n    def run(self):\n        if not self.exec():\n            return False, None, None, None\n        return True, self.playout.old_password(), self.playout.new_password(), self.playout.should_encrypt_storage_with_xpub()\n\n\nclass PasswordDialog(WindowModalDialog):\n\n    def __init__(self, parent=None, msg=None):\n        msg = msg or _('Please enter your password')\n        WindowModalDialog.__init__(self, parent, _(\"Enter Password\"))\n        self.pw = pw = PasswordLineEdit()\n        label = QLabel(msg)\n        label.setWordWrap(True)\n        vbox = QVBoxLayout()\n        vbox.addWidget(label)\n        grid = QGridLayout()\n        grid.setSpacing(8)\n        grid.addWidget(QLabel(_('Password')), 1, 0)\n        grid.addWidget(pw, 1, 1)\n        vbox.addLayout(grid)\n        vbox.addLayout(Buttons(CancelButton(self), OkButton(self)))\n        self.setLayout(vbox)\n        run_hook('password_dialog', pw, grid, 1)\n\n    def run(self):\n        try:\n            if not self.exec():\n                return\n            return self.pw.text()\n        finally:\n            self.pw.clear()\n"
  },
  {
    "path": "electrum/gui/qt/paytoedit.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2012 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom functools import partial\nfrom typing import Optional, TYPE_CHECKING, Union\n\nfrom PyQt6.QtCore import Qt, QTimer, QSize, QStringListModel\nfrom PyQt6.QtCore import pyqtSignal\nfrom PyQt6.QtGui import QFontMetrics, QFont, QContextMenuEvent\nfrom PyQt6.QtWidgets import QTextEdit, QWidget, QLineEdit, QStackedLayout, QCompleter\n\nfrom electrum.payment_identifier import PaymentIdentifier\nfrom electrum.logging import Logger\nfrom electrum.util import EventListener, event_listener\n\nfrom . import util\nfrom .util import MONOSPACE_FONT, GenericInputHandler, ColorScheme, add_input_actions_to_context_menu\n\nif TYPE_CHECKING:\n    from .send_tab import SendTab\n\n\nfrozen_style = \"QWidget {border:none;}\"\nnormal_style = \"QPlainTextEdit { }\"\n\n\nclass InvalidPaymentIdentifier(Exception):\n    pass\n\n\nclass ResizingTextEdit(QTextEdit):\n\n    textReallyChanged = pyqtSignal()\n    resized = pyqtSignal()\n\n    def __init__(self):\n        QTextEdit.__init__(self)\n        self._text = ''\n        self.setAcceptRichText(False)\n        self.textChanged.connect(self.on_text_changed)\n        document = self.document()\n        fontMetrics = QFontMetrics(document.defaultFont())\n        self.fontSpacing = fontMetrics.lineSpacing()\n        margins = self.contentsMargins()\n        documentMargin = document.documentMargin()\n        self.verticalMargins = margins.top() + margins.bottom()\n        self.verticalMargins += self.frameWidth() * 2\n        self.verticalMargins += documentMargin * 2\n        self.heightMin = self.fontSpacing + self.verticalMargins\n        self.heightMax = (self.fontSpacing * 10) + self.verticalMargins\n        self.update_size()\n\n    def on_text_changed(self):\n        # QTextEdit emits spurious textChanged events\n        if self.toPlainText() != self._text:\n            self._text = self.toPlainText()\n            self.textReallyChanged.emit()\n            self.update_size()\n\n    def update_size(self):\n        docLineCount = self.document().lineCount()\n        docHeight = max(3, docLineCount) * self.fontSpacing\n        h = docHeight + self.verticalMargins\n        h = min(max(h, self.heightMin), self.heightMax)\n        self.setMinimumHeight(int(h))\n        self.setMaximumHeight(int(h))\n        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)\n        self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax)\n        self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)\n        self.resized.emit()\n\n    def sizeHint(self) -> QSize:\n        return QSize(0, self.minimumHeight())\n\n\nclass PayToEdit(QWidget, Logger, GenericInputHandler, EventListener):\n    paymentIdentifierChanged = pyqtSignal()\n    textChanged = pyqtSignal()\n\n    def __init__(self, send_tab: 'SendTab'):\n        QWidget.__init__(self, parent=send_tab)\n        Logger.__init__(self)\n        GenericInputHandler.__init__(self)\n\n        self._text = ''\n        self._layout = QStackedLayout()\n        self.setLayout(self._layout)\n\n        self.send_tab = send_tab\n\n        def text_edit_changed():\n            text = self.text_edit.toPlainText()\n            if self._text != text:\n                # sync and emit\n                self._text = text\n                self.line_edit.setText(text)\n                self.textChanged.emit()\n\n        def text_edit_resized():\n            self.update_height()\n\n        def line_edit_changed():\n            text = self.line_edit.text()\n            if self._text != text:\n                # sync and emit\n                self._text = text\n                self.text_edit.setPlainText(text)\n                self.textChanged.emit()\n\n        self.line_edit = QLineEdit()\n        self.line_edit.textChanged.connect(line_edit_changed)\n        self.text_edit = ResizingTextEdit()\n        self.text_edit.setTabChangesFocus(True)\n        self.text_edit.textReallyChanged.connect(text_edit_changed)\n        self.text_edit.resized.connect(text_edit_resized)\n\n        def on_completed(item: str):\n            text = self._completer_contacts[1][self._completer_contacts[0].index(item)]\n            self.try_payment_identifier(text)\n            self.completer.popup().hide()\n\n        self.completer = QCompleter()\n        self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)\n        self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)\n        self.completer.setFilterMode(Qt.MatchFlag.MatchContains)\n        self.completer.activated.connect(on_completed)\n\n        self.update_completer()\n\n        self.line_edit.setCompleter(self.completer)\n\n        self.textChanged.connect(self._handle_text_change)\n\n        self._layout.addWidget(self.line_edit)\n        self._layout.addWidget(self.text_edit)\n\n        self.multiline = False\n\n        self._is_paytomany = False\n        self.line_edit.setFont(QFont(MONOSPACE_FONT))\n        self.text_edit.setFont(QFont(MONOSPACE_FONT))\n        self.send_tab = send_tab\n        self.config = send_tab.config\n\n        # button handlers\n        self.on_qr_from_camera_input_btn = partial(\n            self.input_qr_from_camera,\n            config=self.config,\n            allow_multi=False,\n            show_error=self.send_tab.show_error,\n            setText=self.try_payment_identifier,\n            parent=self.send_tab.window,\n        )\n        self.on_qr_from_screenshot_input_btn = partial(\n            self.input_qr_from_screenshot,\n            allow_multi=False,\n            show_error=self.send_tab.show_error,\n            setText=self.try_payment_identifier,\n        )\n        self.on_qr_from_file_input_btn = partial(\n            self.input_qr_from_file,\n            allow_multi=False,\n            config=self.config,\n            show_error=self.send_tab.show_error,\n            setText=self.try_payment_identifier,\n        )\n        self.on_input_file = partial(\n            self.input_file,\n            config=self.config,\n            show_error=self.send_tab.show_error,\n            setText=self.try_payment_identifier,\n        )\n\n        self.text_edit.contextMenuEvent = partial(self.custom_context_menu_event, tl_edit=self.text_edit)\n        self.line_edit.contextMenuEvent = partial(self.custom_context_menu_event, tl_edit=self.line_edit)\n\n        self.edit_timer = QTimer(self)\n        self.edit_timer.setSingleShot(True)\n        self.edit_timer.setInterval(1000)\n        self.edit_timer.timeout.connect(self._on_edit_timer)\n\n        self.payment_identifier = None  # type: Optional[PaymentIdentifier]\n\n        self.register_callbacks()\n        self.destroyed.connect(lambda: self.unregister_callbacks())\n\n    def custom_context_menu_event(self, e: 'QContextMenuEvent', *, tl_edit: Union[QTextEdit, QLineEdit]) -> None:\n        m = tl_edit.createStandardContextMenu()\n        m.addSeparator()\n        add_input_actions_to_context_menu(self, m)\n        m.exec(e.globalPos())\n\n    @event_listener\n    def on_event_contacts_updated(self):\n        self.update_completer()\n\n    def update_completer(self):\n        self._completer_contacts = [], []\n        for k, v in self.send_tab.wallet.contacts.items():\n            self._completer_contacts[0].append(f'{v[1]} <{k}>')\n            self._completer_contacts[1].append(k)\n\n        self.completer.setModel(QStringListModel(self._completer_contacts[0]))\n\n    @property\n    def multiline(self):\n        return self._multiline\n\n    @multiline.setter\n    def multiline(self, b: bool) -> None:\n        if b is None:\n            return\n        self._multiline = b\n        self._layout.setCurrentWidget(self.text_edit if b else self.line_edit)\n        self.update_height()\n\n    def update_height(self) -> None:\n        h = self._layout.currentWidget().sizeHint().height()\n        self.setMaximumHeight(h)\n\n    def setText(self, text: str) -> None:\n        if self._text != text:\n            self.line_edit.setText(text)\n            self.text_edit.setText(text)\n\n    def setFocus(self, reason=Qt.FocusReason.OtherFocusReason) -> None:\n        if self.multiline:\n            self.text_edit.setFocus(reason)\n        else:\n            self.line_edit.setFocus(reason)\n\n    def setToolTip(self, tt: str) -> None:\n        self.line_edit.setToolTip(tt)\n        self.text_edit.setToolTip(tt)\n\n    def try_payment_identifier(self, text) -> None:\n        '''set payment identifier only if valid, else exception'''\n        pi = PaymentIdentifier(self.send_tab.wallet, text)\n        if not pi.is_valid():\n            raise InvalidPaymentIdentifier('Invalid payment identifier')\n        self.set_payment_identifier(text)\n\n    def set_payment_identifier(self, text) -> None:\n        if self.payment_identifier and self.payment_identifier.text == text.strip():\n            # no change.\n            return\n\n        self.payment_identifier = PaymentIdentifier(self.send_tab.wallet, text)\n\n        # toggle to multiline if payment identifier is a multiline\n        if self.payment_identifier.is_multiline() and not self._is_paytomany:\n            self.set_paytomany(True)\n\n        # if payment identifier gets set externally, we want to update the edit control\n        # Note: this triggers the change handler, but we shortcut if it's the same payment identifier\n        self.setText(text)\n\n        self.paymentIdentifierChanged.emit()\n\n    def set_paytomany(self, b):\n        self._is_paytomany = b\n        self.multiline = b\n        self.send_tab.paytomany_menu.setChecked(b)\n\n    def toggle_paytomany(self) -> None:\n        self.set_paytomany(not self._is_paytomany)\n\n    def is_paytomany(self):\n        return self._is_paytomany\n\n    def setReadOnly(self, b: bool) -> None:\n        self.line_edit.setReadOnly(b)\n        self.text_edit.setReadOnly(b)\n\n    def isReadOnly(self):\n        return self.line_edit.isReadOnly()\n\n    def setStyleSheet(self, stylesheet: str) -> None:\n        self.line_edit.setStyleSheet(stylesheet)\n        self.text_edit.setStyleSheet(stylesheet)\n\n    def setFrozen(self, b) -> None:\n        self.setReadOnly(b)\n        self.setStyleSheet(ColorScheme.LIGHTBLUE.as_stylesheet(True) if b else '')\n\n    def isFrozen(self):\n        return self.isReadOnly()\n\n    def do_clear(self) -> None:\n        self.set_paytomany(False)\n        self.setText('')\n        self.setToolTip('')\n        self.payment_identifier = None\n\n    def setGreen(self) -> None:\n        self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True))\n\n    def setExpired(self) -> None:\n        self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True))\n\n    def _handle_text_change(self) -> None:\n        if self.isFrozen():\n            # if editor is frozen, we ignore text changes as they might not be a payment identifier\n            # but a user friendly representation.\n            return\n\n        # pushback timer if timer active or PI needs resolving\n        pi = PaymentIdentifier(self.send_tab.wallet, self._text)\n        if not pi.is_valid() or pi.need_resolve() or self.edit_timer.isActive():\n            self.edit_timer.start()\n        else:\n            self.set_payment_identifier(self._text)\n\n    def _on_edit_timer(self) -> None:\n        if not self.isFrozen():\n            self.set_payment_identifier(self._text)\n"
  },
  {
    "path": "electrum/gui/qt/plugins_dialog.py",
    "content": "from typing import TYPE_CHECKING, Optional\nfrom functools import partial\nimport shutil\nimport os\n\nfrom PyQt6.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QWidget, QScrollArea, \\\n    QFormLayout, QFileDialog, QMenu, QApplication, QMessageBox\nfrom PyQt6.QtCore import QTimer\n\nfrom electrum.i18n import _\nfrom electrum.gui import messages\nfrom electrum.logging import get_logger\n\nfrom .util import (WindowModalDialog, Buttons, CloseButton, WWLabel, insert_spaces, MessageBoxMixin,\n                   EnterButton, read_QIcon_from_bytes, IconLabel, RunCoroutineDialog, read_QIcon,\n                   webopen)\n\n\nif TYPE_CHECKING:\n    from . import ElectrumGui\n    from electrum_ecc import ECPrivkey\n    from electrum.simple_config import SimpleConfig\n    from electrum.plugin import Plugins\n\n\nclass PluginDialog(WindowModalDialog):\n\n    def __init__(self, name, metadata, status_button: Optional['PluginStatusButton'], window: 'PluginsDialog'):\n        display_name = metadata.get('fullname', '')\n        author = metadata.get('author', '')\n        description = metadata.get('description', '')\n        requires = metadata.get('requires')\n        version = metadata.get('version')\n        zip_hash = metadata.get('zip_hash_sha256', None)\n        icon_path = metadata.get('icon')\n\n        WindowModalDialog.__init__(self, window, 'Plugin')\n        self.setMinimumSize(400, 250)\n        self.window = window\n        self.metadata = metadata\n        self.plugins = self.window.plugins\n        self.name = name\n        self.status_button = status_button\n        p = self.plugins.get(name)  # is enabled\n        vbox = QVBoxLayout(self)\n        name_label = IconLabel(text=display_name, reverse=True)\n        if icon_path:\n            name_label.icon_size = 64\n            icon = read_QIcon_from_bytes(self.plugins.read_file(name, icon_path))\n            name_label.setIcon(icon)\n        vbox.addWidget(name_label)\n        vbox.addStretch()\n        vbox.addWidget(WWLabel(description))\n        vbox.addStretch()\n        form = QFormLayout(None)\n        if author:\n            form.addRow(QLabel(_('Author') + ':'), QLabel(author))\n        if version:\n            form.addRow(QLabel(_('Version') + ':'), QLabel(version))\n        if zip_hash:\n            form.addRow(QLabel('Hash [sha256]:'), WWLabel(insert_spaces(zip_hash, 8)))\n        if requires:\n            msg = '\\n'.join(map(lambda x: x[1], requires))\n            form.addRow(QLabel(_('Requires') + ':'), WWLabel(msg))\n        vbox.addLayout(form)\n        vbox.addStretch()\n        close_button = CloseButton(self)\n        close_button.setText(_('Close'))\n        buttons = [close_button]\n        p = self.plugins.get(name)\n        is_enabled = p and p.is_enabled()\n        is_external = self.plugins.is_external(name)\n        if is_external:\n            is_authorized = self.plugins.is_authorized(name)\n            if status_button is not None:\n                # status_button is None when called from add_external_plugin\n                remove_button = QPushButton('')\n                remove_button.clicked.connect(self.do_remove)\n                remove_button.setText(_('Remove'))\n                buttons.insert(0, remove_button)\n            if not is_authorized:\n                auth_button = QPushButton('Install')\n                auth_button.clicked.connect(self.do_authorize)\n                buttons.insert(0, auth_button)\n        else:\n            toggle_button = QPushButton('')\n            toggle_button.setText(_('Disable') if is_enabled else _('Enable'))\n            toggle_button.clicked.connect(self.do_toggle)\n            buttons.insert(0, toggle_button)\n        # add settings button\n        if p and p.requires_settings() and p.is_enabled():\n            settings_button = EnterButton(\n                _('Settings'),\n                partial(p.settings_dialog, self))\n            buttons.insert(1, settings_button)\n        # add buttons\n        vbox.addLayout(Buttons(*buttons))\n\n    def do_toggle(self):\n        if not self.plugins.is_available(self.name):\n            msg = \"\\n\".join([\n                _('This plugin requires installation of additional dependencies.'),\n                _('For Electrum to recognize external packages, you need to run it from source.')\n            ])\n            self.window.show_message(msg)\n            return\n\n        self.close()\n        self.window.do_toggle(self.name, self.status_button)\n\n    def do_remove(self):\n        self.window.uninstall_plugin(self.name)\n        self.close()\n\n    def do_authorize(self):\n        assert not self.plugins.is_authorized(self.name)\n        privkey = self.window.get_plugins_privkey()\n        if not privkey:\n            return\n        filename = self.plugins.zip_plugin_path(self.name)\n        self.window.plugins.authorize_plugin(self.name, filename, privkey)\n        self.window.plugins.enable(self.name)\n        d = self.plugins.get_metadata(self.name)\n        if details := d.get('registers_keystore'):\n            self.plugins.register_keystore(self.name, details)\n        if self.status_button:\n            self.status_button.update()\n        self.accept()\n\n\nclass PluginStatusButton(QPushButton):\n\n    def __init__(self, window: 'PluginsDialog', name: str):\n        QPushButton.__init__(self, '')\n        self.window = window\n        self.plugins = window.plugins\n        self.name = name\n        self.clicked.connect(self.show_plugin_dialog)\n        self.update()\n\n    def show_plugin_dialog(self):\n        metadata = self.plugins.descriptions[self.name]\n        d = PluginDialog(self.name, metadata, self, self.window)\n        d.exec()\n\n    def update(self):\n        from .util import ColorScheme\n        p = self.plugins.get(self.name)\n        plugin_is_loaded = p is not None\n        enabled = not plugin_is_loaded or (plugin_is_loaded and p.can_user_disable())\n        self.setEnabled(enabled)\n        if p is not None and p.is_enabled():\n            text, color = _('Enabled'), ColorScheme.BLUE\n        else:\n            text, color = _('Disabled'), ColorScheme.RED\n        self.setStyleSheet(color.as_stylesheet())\n        self.setText(text)\n\n\nclass PluginsDialog(WindowModalDialog, MessageBoxMixin):\n    _logger = get_logger(__name__)\n\n    def __init__(self, config: 'SimpleConfig', plugins: 'Plugins', *, gui_object: Optional['ElectrumGui'] = None):\n        WindowModalDialog.__init__(self, None, _('Electrum Plugins'))\n        self.gui_object = gui_object\n        self.config = config\n        self.plugins = plugins\n        vbox = QVBoxLayout(self)\n        scroll = QScrollArea()\n        scroll.setEnabled(True)\n        scroll.setWidgetResizable(True)\n        scroll.setMinimumSize(400, 250)\n        scroll_w = QWidget()\n        scroll.setWidget(scroll_w)\n        self.grid = QGridLayout()\n        self.grid.setColumnStretch(0, 1)\n        scroll_w.setLayout(self.grid)\n        vbox.addWidget(scroll)\n        add_button = QPushButton(_('Add'))\n        add_button.setMinimumWidth(40)  # looks better on windows, no difference on linux\n        add_button.clicked.connect(self.add_plugin_dialog)\n        website_button = QPushButton(read_QIcon('globe.png'), _('Help'))\n        website_button.setToolTip(_('Visit plugins website'))\n        website_button.clicked.connect(lambda: webopen('https://plugins.electrum.org/'))\n        hbox = QHBoxLayout()\n        hbox.addWidget(website_button)\n        hbox.addStretch(1)\n        hbox.addWidget(add_button)\n        hbox.addWidget(CloseButton(self))\n        vbox.addLayout(hbox)\n        self.show_list()\n\n    def get_plugins_privkey(self) -> Optional['ECPrivkey']:\n        pubkey, salt = self.plugins.get_pubkey_bytes()\n        if not pubkey:\n            self.init_plugins_password()\n            return None\n        # ask for url and password, same window\n        pw = self.password_dialog(msg=messages.MSG_THIRD_PARTY_PLUGIN_WARNING)\n        if not pw:\n            return None\n        privkey = self.plugins.derive_privkey(pw, salt)\n        if pubkey != privkey.get_public_key_bytes():\n            keyfile_path, _keyfile_help = self.plugins.get_keyfile_path(None)\n\n            while True:\n                exit_dialog = True\n                auto_reset_btn = QPushButton(_('Try Auto-Reset'))\n                def on_try_auto_reset_clicked():\n                    nonlocal exit_dialog\n                    if not self.plugins.try_auto_key_reset():\n                        self.show_error(_(\"Auto-Reset not possible. Delete the file manually.\"))\n                        exit_dialog = False\n                    else:\n                        self.show_message(_(\"Auto-Reset successful. You can now setup a new password.\"))\n                auto_reset_btn.clicked.connect(on_try_auto_reset_clicked)\n\n                buttons = [\n                    QMessageBox.StandardButton.Ok,\n                    (auto_reset_btn, QMessageBox.ButtonRole.ActionRole, 0),\n                ]\n                if self.show_error(\n                    ''.join([\n                        _('Incorrect password.'), '\\n\\n',\n                        _('Your plugin authorization password is required to install plugins.'), ' ',\n                        _('If you need to reset it, remove the following file:'), '\\n\\n',\n                        keyfile_path\n                    ]),\n                    buttons=buttons\n                ) or exit_dialog:\n                    break\n\n            return None\n        return privkey\n\n    def init_plugins_password(self):\n        from .password_dialog import NewPasswordDialog\n        msg = ' '.join([\n            _('In order to install third-party plugins, you need to choose a plugin authorization password.'),\n            _('Its purpose is to prevent unauthorized users (or malware) from installing plugins.'),\n        ])\n        d = NewPasswordDialog(self, msg=msg)\n        pw = d.run()\n        if not pw:\n            return\n        key_hex = self.plugins.create_new_key(pw)\n        keyfile_path, keyfile_help = self.plugins.get_keyfile_path(key_hex)\n        msg = '\\n\\n'.join([\n            _('Your plugins key is:'), key_hex,\n            _('This key has been copied to your clipboard. Please save it in:'),\n            keyfile_path,\n            keyfile_help,\n            '',\n        ])\n        clipboard = QApplication.clipboard()\n        clipboard.setText(key_hex)\n\n        while True:\n            exit_dialog = True\n            # the button has to be recreated inside the loop, as qt destroys it when the dialog is closed\n            auto_setup_btn = QPushButton(_('Try Auto-Setup'))\n            def on_auto_setup_clicked():\n                nonlocal exit_dialog\n                if not self.plugins.try_auto_key_setup(key_hex):\n                    self.show_error(_(\"Auto-Setup not possible. Try the manual setup.\"))\n                    exit_dialog = False\n                else:\n                    self.show_message(_(\"Auto-Setup successful. You can now install plugins.\"))\n            auto_setup_btn.clicked.connect(on_auto_setup_clicked)\n\n            # on windows, the auto-setup button is shown right of the ok button,\n            # apparently due to OS conventions\n            buttons = [\n                (auto_setup_btn, QMessageBox.ButtonRole.ActionRole, 0),\n                QMessageBox.StandardButton.Ok,\n            ]\n            if self.show_message(msg, buttons=buttons) or exit_dialog:\n                break\n\n    def add_plugin_dialog(self):\n        pubkey, salt = self.plugins.get_pubkey_bytes()\n        if not pubkey:\n            self.init_plugins_password()\n            return\n        filename, __ = QFileDialog.getOpenFileName(self, _(\"Select your plugin zipfile\"), \"\", \"*.zip\")\n        if not filename:\n            return\n        plugins_dir = self.plugins.get_external_plugin_dir()\n        path = os.path.join(plugins_dir, os.path.basename(filename))\n        if os.path.exists(path):\n            self.show_warning(_('Plugin already installed.'))\n            return\n        try:\n            shutil.copyfile(filename, path)\n        except OSError as e:\n            self.show_error(_(\"Could not copy plugin file {} into directory {}:\\n\\n{}\").format(\n                filename,\n                path,\n                str(e)\n            ))\n            return\n        self._try_add_external_plugin_from_path(path)\n\n    def _try_add_external_plugin_from_path(self, path: str):\n        try:\n            success = self.add_external_plugin(path)\n        except Exception as e:\n            self._logger.exception(\"\")\n            self.show_error(f\"{e}\")\n            success = False\n        if not success:\n            try:\n                os.unlink(path)\n            except FileNotFoundError:\n                self._logger.debug(\"\", exc_info=True)\n\n    def add_external_plugin(self, path):\n        manifest = self.plugins.read_manifest(path)\n        name = manifest['name']\n        self.plugins.external_plugin_metadata[name] = manifest\n        d = PluginDialog(name, manifest, None, self)\n        if not d.exec():\n            self.plugins.external_plugin_metadata.pop(name)\n            return False\n        if self.gui_object:\n            self.gui_object.reload_windows()\n        self.show_list()\n        return True\n\n    def show_list(self):\n        descriptions = self.plugins.descriptions\n        descriptions = sorted(descriptions.items())\n        grid = self.grid\n        # clear existing items\n        for i in reversed(range(grid.count())):\n            grid.itemAt(i).widget().setParent(None)\n        # populate\n        i = 0\n        for name, metadata in descriptions:\n            i += 1\n            if self.plugins.is_internal(name) and self.plugins.is_auto_loaded(name):\n                continue\n            display_name = metadata.get('fullname')\n            if not display_name:\n                continue\n            label = IconLabel(text=display_name, reverse=True)\n            icon_path = metadata.get('icon')\n            if icon_path:\n                icon = read_QIcon_from_bytes(self.plugins.read_file(name, icon_path))\n                label.setIcon(icon)\n            label.status_button = PluginStatusButton(self, name)\n            grid.addWidget(label, i, 0)\n            grid.addWidget(label.status_button, i, 1)\n        # add stretch\n        grid.setRowStretch(i + 1, 1)\n\n    def do_toggle(self, name, status_button):\n        p = self.plugins.get(name)\n        is_enabled = p and p.is_enabled()\n        if is_enabled:\n            self.plugins.disable(name)\n        else:\n            self.plugins.enable(name)\n        if status_button:\n            status_button.update()\n        if self.gui_object:\n            self.gui_object.reload_windows()\n        self.bring_to_front()\n\n    def uninstall_plugin(self, name):\n        if not self.question(_('Remove plugin \\'{}\\'?').format(name)):\n            return\n        self.plugins.uninstall(name)\n        if self.gui_object:\n            self.gui_object.reload_windows()\n        self.show_list()\n        self.bring_to_front()\n\n    def bring_to_front(self):\n        def _bring_self_to_front():\n            self.activateWindow()\n            self.setFocus()\n        QTimer.singleShot(100, _bring_self_to_front)\n"
  },
  {
    "path": "electrum/gui/qt/qrcodewidget.py",
    "content": "from typing import Optional\n\nimport qrcode\nimport qrcode.exceptions\n\nimport PyQt6.QtGui as QtGui\nfrom PyQt6.QtCore import QRect\nfrom PyQt6.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QPushButton, QWidget\n\nfrom electrum.i18n import _\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.gui.common_qt.util import draw_qr\n\nfrom .util import WindowModalDialog, WWLabel, getSaveFileName\n\n\nclass QrCodeDataOverflow(qrcode.exceptions.DataOverflowError):\n    pass\n\n\nclass QRCodeWidget(QWidget):\n\n    MIN_BOXSIZE = 2  # min size in pixels of single black/white unit box of the qr code\n\n    def __init__(self, data=None, *, manual_size: bool = False):\n        QWidget.__init__(self)\n        self.data = None\n        self.qr = None\n        self._framesize = None  # type: Optional[int]\n        self._manual_size = manual_size\n        self.setData(data)\n\n    def setData(self, data):\n        if data:\n            qr = qrcode.QRCode(\n                error_correction=qrcode.constants.ERROR_CORRECT_L,\n                border=1,\n            )\n            try:\n                qr.add_data(data)\n                qr_matrix = qr.get_matrix()  # test that data fits in QR code\n            except (ValueError, qrcode.exceptions.DataOverflowError) as e:\n                raise QrCodeDataOverflow() from e\n            self.qr = qr\n            self.data = data\n            if not self._manual_size:\n                k = len(qr_matrix)\n                size = min(k * 5, 150 + k * self.MIN_BOXSIZE)\n                self.setMinimumSize(size, size)\n        else:\n            self.qr = None\n            self.data = None\n\n        self.update()\n\n    def paintEvent(self, e):\n        if not self.data:\n            return\n        draw_qr(\n            qr=self.qr,\n            paint_device=self,\n            is_enabled=self.isEnabled(),\n            min_boxsize=self.MIN_BOXSIZE,\n        )\n\n    def grab(self) -> QtGui.QPixmap:\n        \"\"\"Overrides QWidget.grab to only include the QR code itself,\n        excluding horizontal/vertical stretch.\n        \"\"\"\n        fsize = self._framesize\n        if fsize is None:\n            fsize = -1\n        rect = QRect(0, 0, fsize, fsize)\n        return QWidget.grab(self, rect)\n\n\nclass QRDialog(WindowModalDialog):\n\n    def __init__(\n            self,\n            *,\n            data,\n            parent=None,\n            title=\"\",\n            show_text=False,\n            help_text=None,\n            show_copy_text_btn=False,\n            config: SimpleConfig,\n    ):\n        WindowModalDialog.__init__(self, parent, title)\n        self.config = config\n\n        vbox = QVBoxLayout()\n\n        qrw = QRCodeWidget(data, manual_size=False)\n        vbox.addWidget(qrw, 1)\n\n        help_text = data if show_text else help_text\n        if help_text:\n            text_label = WWLabel()\n            text_label.setText(help_text)\n            vbox.addWidget(text_label)\n        hbox = QHBoxLayout()\n        hbox.addStretch(1)\n\n        def print_qr():\n            filename = getSaveFileName(\n                parent=self,\n                title=_(\"Select where to save file\"),\n                filename=\"qrcode.png\",\n                config=self.config,\n            )\n            if not filename:\n                return\n            p = qrw.grab()\n            p.save(filename, 'png')\n            self.show_message(_(\"QR code saved to file\") + \" \" + filename)\n\n        def copy_image_to_clipboard():\n            p = qrw.grab()\n            QApplication.clipboard().setPixmap(p)\n            self.show_message(_(\"QR code copied to clipboard\"))\n\n        def copy_text_to_clipboard():\n            QApplication.clipboard().setText(data)\n            self.show_message(_(\"Text copied to clipboard\"))\n\n        b = QPushButton(_(\"Copy Image\"))\n        hbox.addWidget(b)\n        b.clicked.connect(copy_image_to_clipboard)\n\n        if show_copy_text_btn:\n            b = QPushButton(_(\"Copy Text\"))\n            hbox.addWidget(b)\n            b.clicked.connect(copy_text_to_clipboard)\n\n        b = QPushButton(_(\"Save\"))\n        hbox.addWidget(b)\n        b.clicked.connect(print_qr)\n\n        b = QPushButton(_(\"Close\"))\n        hbox.addWidget(b)\n        b.clicked.connect(self.accept)\n        b.setDefault(True)\n\n        vbox.addLayout(hbox)\n        self.setLayout(vbox)\n\n        # note: the word-wrap on the text_label is causing layout sizing issues.\n        #       see https://stackoverflow.com/a/25661985 and https://bugreports.qt.io/browse/QTBUG-37673\n        #       workaround:\n        self.setMinimumSize(self.sizeHint())\n"
  },
  {
    "path": "electrum/gui/qt/qrreader/__init__.py",
    "content": "# Copyright (C) 2021 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n#\n# We have two toolchains to scan qr codes:\n# 1. access camera via QtMultimedia, take picture, feed picture to zbar\n# 2. let zbar handle whole flow (including accessing the camera)\n#\n# notes:\n# - zbar needs to be compiled with platform-dependent extra config options to be able\n#   to access the camera\n# - zbar fails to access the camera on macOS\n# - qtmultimedia seems to support more cameras on Windows than zbar\n# - qtmultimedia is often not packaged with PyQt\n#   in particular, on debian, you need both \"python3-pyqt6\" and \"python3-pyqt6.qtmultimedia\"\n# - older versions of qtmultimedia don't seem to work reliably\n#\n# Considering the above, we use QtMultimedia for Windows and macOS, as there\n# most users run our binaries where we can make sure the packaged versions work well.\n# On Linux where many people run from source, we use zbar.\n#\n# Note: this module is safe to import on all platforms.\n\nimport sys\nfrom typing import Callable, Optional, TYPE_CHECKING, Mapping, Sequence\n\nfrom PyQt6.QtWidgets import QMessageBox, QWidget\nfrom PyQt6.QtGui import QImage, QPainter, QColor\nfrom PyQt6.QtCore import QRect, QCoreApplication\nfrom PyQt6 import QtCore\n\nfrom electrum.i18n import _\nfrom electrum.util import UserFacingException\nfrom electrum.logging import get_logger\nfrom electrum.qrreader import get_qr_reader, QrCodeResult, MissingQrDetectionLib\n\nfrom electrum.gui.qt.util import MessageBoxMixin, custom_message_box\n\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n\n\n_logger = get_logger(__name__)\n\n\ndef scan_qrcode_from_camera(\n        *,\n        parent: Optional[QWidget],\n        config: 'SimpleConfig',\n        callback: Callable[[bool, str, Optional[str]], None],\n) -> None:\n    \"\"\"Scans QR code using camera. It handles requesting camera access permission from the OS if needed.\"\"\"\n    assert parent is None or isinstance(parent, QWidget), f\"parent should be a QWidget, not {parent!r}\"\n    def do_scan():\n        _scan_qrcode_from_camera(parent=parent, config=config, callback=callback)\n\n    if _has_camera_permission():\n        do_scan()\n    else:\n        # Request permission now. This is only a thing on macOS atm.\n        # Note: this assumes we are running on the main thread. Permissions can only be requested from the main thread.\n        app = QCoreApplication.instance()\n        app.requestPermission(QtCore.QCameraPermission(), lambda _x: do_scan())\n\n\ndef scan_qr_from_image(image: QImage) -> Sequence[QrCodeResult]:\n    \"\"\"Might raise exception: MissingQrDetectionLib.\"\"\"\n    qr_reader = get_qr_reader()\n\n    for attempt in range(4):\n        image_y800 = image.convertToFormat(QImage.Format.Format_Grayscale8)\n        res = qr_reader.read_qr_code(\n            image_y800.constBits().__int__(),\n            image_y800.sizeInBytes(),\n            image_y800.bytesPerLine(),\n            image_y800.width(),\n            image_y800.height(),\n        )\n        if res:\n            break\n        # zbar doesn't like qr codes that are too large in relation to the whole image\n        image = _reduce_qr_code_density(image)\n    return res\n\ndef _reduce_qr_code_density(image: QImage) -> QImage:\n    \"\"\" Reduces the size of the qr code relative to the whole image. \"\"\"\n    new_image = QImage(image.width(), image.height(), QImage.Format.Format_RGB32)\n    new_image.fill(QColor(255, 255, 255))  # Fill white\n\n    painter = QPainter(new_image)\n    source_rect = QRect(0, 0, image.width(), image.height())\n    target_rect = QRect(0, 0, int(image.width() * 0.75), int(image.height() * 0.75))\n    painter.drawImage(target_rect, image, source_rect)\n    painter.end()\n\n    return new_image\n\ndef find_system_cameras() -> Mapping[str, str]:\n    \"\"\"Returns a camera_description -> camera_path map.\"\"\"\n    if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'):\n        try:\n            from .qtmultimedia import find_system_cameras\n        except (ImportError, RuntimeError) as e:\n            _logger.exception('error importing .qtmultimedia')\n            return {}\n        else:\n            return find_system_cameras()\n    else:  # desktop Linux and similar\n        from electrum import qrscanner\n        return qrscanner.find_system_cameras()\n\n\n# --- Internals below (not part of external API)\n\ndef _scan_qrcode_using_zbar(\n        *,\n        parent: Optional[QWidget],\n        config: 'SimpleConfig',\n        callback: Callable[[bool, str, Optional[str]], None],\n) -> None:\n    from electrum import qrscanner\n    data = None\n    try:\n        data = qrscanner.scan_barcode(config.get_video_device())\n    except UserFacingException as e:\n        success = False\n        error = str(e)\n    except BaseException as e:\n        _logger.exception('camera error')\n        success = False\n        error = repr(e)\n    else:\n        success = True\n        error = \"\"\n    if data is None:\n        # probably user cancelled\n        success = False\n    callback(success, error, data)\n\n\n# Use a global to prevent multiple QR dialogs created simultaneously\n_qr_dialog = None\n\n\ndef _scan_qrcode_using_qtmultimedia(\n        *,\n        parent: Optional[QWidget],\n        config: 'SimpleConfig',\n        callback: Callable[[bool, str, Optional[str]], None],\n) -> None:\n    try:\n        from .qtmultimedia import QrReaderCameraDialog, CameraError\n    except (ImportError, RuntimeError) as e:\n        icon = QMessageBox.Icon.Warning\n        title = _(\"QR Reader Error\")\n        message = _(\"QR reader failed to load. This may happen if \"\n                    \"you are using an older version of PyQt.\") + \"\\n\\n\" + str(e)\n        _logger.exception(message)\n        if isinstance(parent, MessageBoxMixin):\n            parent.msg_box(title=title, text=message, icon=icon, parent=None)\n        else:\n            custom_message_box(title=title, text=message, icon=icon, parent=parent)\n        return\n\n    global _qr_dialog\n    if _qr_dialog:\n        _logger.warning(\"QR dialog is already presented, ignoring.\")\n        return\n    _qr_dialog = None\n    try:\n        _qr_dialog = QrReaderCameraDialog(parent=parent, config=config)\n\n        def _on_qr_reader_finished(success: bool, error: str, data):\n            global _qr_dialog\n            if _qr_dialog:\n                _qr_dialog.deleteLater()\n                _qr_dialog = None\n            callback(success, error, data)\n\n        _qr_dialog.qr_finished.connect(_on_qr_reader_finished)\n        _qr_dialog.start_scan(config.get_video_device())\n    except (MissingQrDetectionLib, CameraError) as e:\n        _qr_dialog = None\n        callback(False, str(e), None)\n    except Exception as e:\n        _logger.exception('camera error')\n        _qr_dialog = None\n        callback(False, repr(e), None)\n\n\ndef _scan_qrcode_from_camera(\n        *,\n        parent: Optional[QWidget],\n        config: 'SimpleConfig',\n        callback: Callable[[bool, str, Optional[str]], None],\n) -> None:\n    \"\"\"Scans QR code using camera.\"\"\"\n    assert parent is None or isinstance(parent, QWidget), f\"parent should be a QWidget, not {parent!r}\"\n    if not _has_camera_permission():\n        callback(False, _(\"Missing camera permission.\"), None)\n        return\n    if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'):\n        _scan_qrcode_using_qtmultimedia(parent=parent, config=config, callback=callback)\n    else:  # desktop Linux and similar\n        _scan_qrcode_using_zbar(parent=parent, config=config, callback=callback)\n\n\ndef _has_camera_permission() -> bool:\n    if not hasattr(QtCore, \"QCameraPermission\"):  # requires Qt 6.5+\n        _logger.info(f\"QtCore does not support QCameraPermission. This requires Qt 6.5+\")\n        return True  # hope for the best\n    app = QCoreApplication.instance()\n    permission_status = app.checkPermission(QtCore.QCameraPermission())\n    return permission_status == QtCore.Qt.PermissionStatus.Granted\n\n"
  },
  {
    "path": "electrum/gui/qt/qrreader/qtmultimedia/__init__.py",
    "content": "#!/usr/bin/env python3\n#\n# Copyright (C) 2019 Axel Gembe <derago@gmail.com>\n# Copyright (c) 2024 The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n#\n# -----\n#\n# Note: This module is risky to import. At the very least, ImportError and\n#       RuntimeError needs to be handled at import time!\n\nfrom typing import Mapping\n\nfrom .camera_dialog import (QrReaderCameraDialog, CameraError, NoCamerasFound,\n                            get_camera_path)\nfrom .validator import (QrReaderValidatorResult, AbstractQrReaderValidator,\n                        QrReaderValidatorCounting, QrReaderValidatorColorizing,\n                        QrReaderValidatorStrong, QrReaderValidatorCounted)\n\n\ndef find_system_cameras() -> Mapping[str, str]:\n    \"\"\"Returns a camera_description -> camera_path map.\"\"\"\n    from PyQt6.QtMultimedia import QMediaDevices\n    system_cameras = QMediaDevices.videoInputs()\n    return {cam.description(): get_camera_path(cam) for cam in system_cameras}\n"
  },
  {
    "path": "electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py",
    "content": "#!/usr/bin/env python3\n#\n# Copyright (C) 2019 Axel Gembe <derago@gmail.com>\n# Copyright (c) 2024 The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport time\nimport math\nimport sys\nimport os\nfrom typing import List, Optional\n\nfrom PyQt6.QtMultimedia import QMediaDevices, QCamera, QMediaCaptureSession, QCameraDevice\nfrom PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QCheckBox, QPushButton, QLabel, QWidget\nfrom PyQt6.QtGui import QImage, QPixmap\nfrom PyQt6.QtCore import QSize, QRect, Qt, pyqtSignal, PYQT_VERSION\n\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.i18n import _\nfrom electrum.qrreader import get_qr_reader, QrCodeResult, MissingQrDetectionLib\nfrom electrum.logging import Logger\n\nfrom electrum.gui.qt.util import MessageBoxMixin, FixedAspectRatioLayout, ImageGraphicsEffect\n\nfrom .video_widget import QrReaderVideoWidget\nfrom .video_overlay import QrReaderVideoOverlay\nfrom .video_surface import QrReaderVideoSurface\nfrom .crop_blur_effect import QrReaderCropBlurEffect\nfrom .validator import AbstractQrReaderValidator, QrReaderValidatorCounted, QrReaderValidatorResult\n\n\nclass CameraError(RuntimeError):\n    ''' Base class of the camera-related error conditions. '''\n\nclass NoCamerasFound(CameraError):\n    ''' Raised by start_scan if no usable cameras were found. Interested\n    code can catch this specific exception.'''\n\n\ndef get_camera_path(cam: 'QCameraDevice') -> str:\n    return bytes(cam.id()).decode('ascii')\n\n\nclass QrReaderCameraDialog(Logger, MessageBoxMixin, QDialog):\n    \"\"\"\n    Dialog for reading QR codes from a camera\n    \"\"\"\n\n    # Try to crop so we have minimum 512 dimensions\n    SCAN_SIZE: int = 512\n\n    qr_finished = pyqtSignal(bool, str, object)\n\n    def __init__(self, parent: Optional[QWidget], *, config: SimpleConfig):\n        ''' Note: make sure parent is a \"top_level_window()\" as per\n        MessageBoxMixin API else bad things can happen on macOS. '''\n        QDialog.__init__(self, parent=parent)\n        Logger.__init__(self)\n\n        self.validator: AbstractQrReaderValidator = None\n        self.frame_id: int = 0\n        self.qr_crop: QRect = None\n        self.qrreader_res: List[QrCodeResult] = []\n        self.validator_res: QrReaderValidatorResult = None\n        self.last_stats_time: float = 0.0\n        self.frame_counter: int = 0\n        self.qr_frame_counter: int = 0\n        self.last_qr_scan_ts: float = 0.0\n        self.camera: QCamera = None\n        self.media_capture_session: QMediaCaptureSession = None\n        self._error_message: str = None\n        self._ok_done: bool = False\n        self.camera_sc_conn = None\n        self.resolution: QSize = None\n\n        self.config = config\n\n        # Try to get the QR reader for this system\n        self.qrreader = get_qr_reader()\n\n        # Set up the window, add the maximize button\n        flags = self.windowFlags()\n        flags = flags | Qt.WindowType.WindowMaximizeButtonHint\n        self.setWindowFlags(flags)\n        self.setWindowTitle(_(\"Scan QR Code\"))\n        self.setWindowModality(Qt.WindowModality.WindowModal if parent else Qt.WindowModality.ApplicationModal)\n\n        # Create video widget and fixed aspect ratio layout to contain it\n        self.video_widget = QrReaderVideoWidget()\n        self.video_overlay = QrReaderVideoOverlay()\n        self.video_layout = FixedAspectRatioLayout()\n        self.video_layout.addWidget(self.video_widget)\n        self.video_layout.addWidget(self.video_overlay)\n\n        # Create root layout and add the video widget layout to it\n        vbox = QVBoxLayout()\n        self.setLayout(vbox)\n        vbox.setContentsMargins(0, 0, 0, 0)\n        vbox.addLayout(self.video_layout)\n\n        # Create a layout for the controls\n        controls_layout = QHBoxLayout()\n        controls_layout.addStretch(2)\n        controls_layout.setContentsMargins(10, 10, 10, 10)\n        controls_layout.setSpacing(10)\n        vbox.addLayout(controls_layout)\n\n        # Flip horizontally checkbox with default coming from global config\n        self.flip_x = QCheckBox()\n        self.flip_x.setText(_(\"&Flip horizontally\"))\n        self.flip_x.setChecked(self.config.QR_READER_FLIP_X)\n        self.flip_x.stateChanged.connect(self._on_flip_x_changed)\n        controls_layout.addWidget(self.flip_x)\n\n        close_but = QPushButton(_(\"&Close\"))\n        close_but.clicked.connect(self.reject)\n        controls_layout.addWidget(close_but)\n\n        # Create the video surface and receive events when new frames arrive\n        self.video_surface = QrReaderVideoSurface(self)\n        self.video_surface.frame_available.connect(self._on_frame_available)\n\n        # Create the crop blur effect\n        self.crop_blur_effect = QrReaderCropBlurEffect(self)\n        self.image_effect = ImageGraphicsEffect(self, self.crop_blur_effect)\n\n\n        # Note these should stay as queued connections because we use the idiom\n        # self.reject() and self.accept() in this class to kill the scan --\n        # and we do it from within callback functions. If you don't use\n        # queued connections here, bad things can happen.\n        self.finished.connect(self._boilerplate_cleanup, Qt.ConnectionType.QueuedConnection)\n        self.finished.connect(self._on_finished, Qt.ConnectionType.QueuedConnection)\n\n    def _on_flip_x_changed(self, _state: int):\n        self.config.QR_READER_FLIP_X = self.flip_x.isChecked()\n\n    @staticmethod\n    def _get_crop(resolution: QSize, scan_size: int) -> QRect:\n        \"\"\"\n        Returns a QRect that is scan_size x scan_size in the middle of the resolution\n        \"\"\"\n        scan_pos_x = (resolution.width() - scan_size) // 2\n        scan_pos_y = (resolution.height() - scan_size) // 2\n        return QRect(scan_pos_x, scan_pos_y, scan_size, scan_size)\n\n    def start_scan(self, device: str = ''):\n        \"\"\"\n        Scans a QR code from the given camera device.\n        If no QR code is found the returned string will be empty.\n        If the camera is not found or can't be opened NoCamerasFound will be raised.\n        \"\"\"\n\n        self.validator = QrReaderValidatorCounted()\n\n        device_info = None\n\n        for camera in QMediaDevices.videoInputs():\n            if get_camera_path(camera) == device:\n                device_info = camera\n                break\n\n        if not device_info:\n            self.logger.info('Failed to open selected camera, trying to use default camera')\n            device_info = QMediaDevices.defaultVideoInput()\n\n        if not device_info or device_info.isNull():\n            raise NoCamerasFound(_(\"Cannot start QR scanner, no usable camera found.\"))\n\n        self._init_stats()\n        self.qrreader_res = []\n        self.validator_res = None\n        self._ok_done = False\n        self._error_message = None\n\n        if self.camera:\n            self.logger.info(\"Warning: start_scan already called for this instance.\")\n\n        self.camera = QCamera(device_info)\n        self.camera.start()\n        self.camera.errorOccurred.connect(self._on_camera_error)  # log the errors we get, if any, for debugging\n\n        self.media_capture_session = QMediaCaptureSession()\n        self.media_capture_session.setCamera(self.camera)\n        self.media_capture_session.setVideoSink(self.video_surface)\n\n        self.open()\n\n    def _set_resolution(self, resolution: QSize):\n        self.resolution = resolution\n        self.qr_crop = self._get_crop(resolution, self.SCAN_SIZE)\n\n        # Initialize the video widget\n        #self.video_widget.setMinimumSize(resolution)  # <-- on macOS this makes it fixed size for some reason.\n        self.resize(720, 540)\n        self.video_overlay.set_crop(self.qr_crop)\n        self.video_overlay.set_resolution(resolution)\n        self.video_layout.set_aspect_ratio(resolution.width() / resolution.height())\n\n        # Set up the crop blur effect\n        self.crop_blur_effect.setCrop(self.qr_crop)\n\n    def _on_camera_error(self, error: QCamera.Error, error_str: str):\n        self.logger.info(f\"QCamera error: {error}. {error_str}\")\n\n    def accept(self):\n        self._ok_done = True  # immediately blocks further processing\n        super().accept()\n\n    def reject(self):\n        self._ok_done = True  # immediately blocks further processing\n        super().reject()\n\n    def _boilerplate_cleanup(self):\n        self._close_camera()\n        if self.isVisible():\n            self.close()\n\n    def _close_camera(self):\n        if self.camera:\n            self.camera.stop()\n            self.camera = None\n\n    def _on_finished(self, code):\n        res = ( (code == QDialog.DialogCode.Accepted\n                    and self.validator_res and self.validator_res.accepted\n                    and self.validator_res.simple_result)\n                or '' )\n\n        self.validator = None\n\n        self.logger.info(f'closed {res}')\n\n        self.qr_finished.emit(code == QDialog.DialogCode.Accepted, self._error_message, res)\n\n    def _on_frame_available(self, frame: QImage):\n        if self._ok_done:\n            return\n\n        self.frame_id += 1\n\n        self._set_resolution(frame.size())\n\n        flip_x = self.flip_x.isChecked()\n\n        # Only QR scan every QR_SCAN_PERIOD secs\n        qr_scanned = time.time() - self.last_qr_scan_ts >= self.qrreader.interval()\n        if qr_scanned:\n            self.last_qr_scan_ts = time.time()\n            # Crop the frame so we only scan a SCAN_SIZE rect\n            frame_cropped = frame.copy(self.qr_crop)\n\n            # Convert to Y800 / GREY FourCC (single 8-bit channel)\n            # This creates a copy, so we don't need to keep the frame around anymore\n            frame_y800 = frame_cropped.convertToFormat(QImage.Format.Format_Grayscale8)\n\n            # Read the QR codes from the frame\n            self.qrreader_res = self.qrreader.read_qr_code(\n                frame_y800.constBits().__int__(),\n                frame_y800.sizeInBytes(),\n                frame_y800.bytesPerLine(),\n                frame_y800.width(),\n                frame_y800.height(),\n                self.frame_id,\n                )\n\n            # Call the validator to see if the scanned results are acceptable\n            self.validator_res = self.validator.validate_results(self.qrreader_res)\n\n            # Update the video overlay with the results\n            self.video_overlay.set_results(self.qrreader_res, flip_x, self.validator_res)\n\n            # Close the dialog if the validator accepted the result\n            if self.validator_res.accepted:\n                self.accept()\n                return\n\n        # Apply the crop blur effect\n        if self.image_effect:\n            frame = self.image_effect.apply(frame)\n\n        # If horizontal flipping is enabled, only flip the display\n        if flip_x:\n            frame = frame.mirrored(True, False)\n\n        # Display the frame in the widget\n        self.video_widget.setPixmap(QPixmap.fromImage(frame))\n\n        self._update_stats(qr_scanned)\n\n    def _init_stats(self):\n        self.last_stats_time = time.perf_counter()\n        self.frame_counter = 0\n        self.qr_frame_counter = 0\n\n    def _update_stats(self, qr_scanned):\n        self.frame_counter += 1\n        if qr_scanned:\n            self.qr_frame_counter += 1\n        now = time.perf_counter()\n        last_stats_delta = now - self.last_stats_time\n        if last_stats_delta > 1.0:  # stats every 1.0 seconds\n            fps = self.frame_counter / last_stats_delta\n            qr_fps = self.qr_frame_counter / last_stats_delta\n            #if self.validator is not None:\n            #    self.validator.strong_count = math.ceil(qr_fps / 3)  # 1/3 of a second's worth of qr frames determines strong_count\n            stats_format = 'running at {} FPS, scanner at {} FPS'\n            self.logger.info(stats_format.format(fps, qr_fps))\n            self.frame_counter = 0\n            self.qr_frame_counter = 0\n            self.last_stats_time = now\n"
  },
  {
    "path": "electrum/gui/qt/qrreader/qtmultimedia/crop_blur_effect.py",
    "content": "#!/usr/bin/env python3\n#\n# Electron Cash - lightweight Bitcoin client\n# Copyright (C) 2019 Axel Gembe <derago@gmail.com>\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom PyQt6.QtWidgets import QGraphicsBlurEffect, QGraphicsEffect\nfrom PyQt6.QtGui import QPainter, QTransform, QRegion\nfrom PyQt6.QtCore import QObject, QRect, QPoint, Qt\n\n\nclass QrReaderCropBlurEffect(QGraphicsBlurEffect):\n    CROP_OFFSET_ENABLED = False\n    CROP_OFFSET = QPoint(5, 5)\n\n    BLUR_DARKEN = 0.25\n    BLUR_RADIUS = 8\n\n    def __init__(self, parent: QObject, crop: QRect = None):\n        super().__init__(parent)\n        self.crop = crop\n        self.setBlurRadius(self.BLUR_RADIUS)\n\n    def setCrop(self, crop: QRect = None):\n        self.crop = crop\n\n    def draw(self, painter: QPainter):\n        assert self.crop, 'crop must be set'\n\n        # Compute painter regions for the crop and the blur\n        all_region = QRegion(painter.viewport())\n        crop_region = QRegion(self.crop)\n        blur_region = all_region.subtracted(crop_region)\n\n        # Let the QGraphicsBlurEffect only paint in blur_region\n        painter.setClipRegion(blur_region)\n\n        # Fill with black and set opacity so that the blurred region is drawn darker\n        if self.BLUR_DARKEN > 0.0:\n            painter.fillRect(painter.viewport(), Qt.GlobalColor.black)\n            painter.setOpacity(1 - self.BLUR_DARKEN)\n\n        # Draw the blur effect\n        super().draw(painter)\n\n        # Restore clipping and opacity\n        painter.setClipping(False)\n        painter.setOpacity(1.0)\n\n        # Get the source pixmap\n        pixmap, offset = self.sourcePixmap(Qt.CoordinateSystem.DeviceCoordinates, QGraphicsEffect.PixmapPadMode.NoPad)\n        painter.setWorldTransform(QTransform())\n\n        # Get the source by adding the offset to the crop location\n        source = self.crop\n        if self.CROP_OFFSET_ENABLED:\n            source = source.translated(self.CROP_OFFSET)\n        painter.drawPixmap(self.crop.topLeft() + offset, pixmap, source)\n"
  },
  {
    "path": "electrum/gui/qt/qrreader/qtmultimedia/validator.py",
    "content": "#!/usr/bin/env python3\n#\n# Electron Cash - lightweight Bitcoin client\n# Copyright (C) 2019 Axel Gembe <derago@gmail.com>\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import List, Dict, Callable, Any\nfrom abc import ABC, abstractmethod\n\nfrom PyQt6.QtGui import QColor\nfrom PyQt6.QtCore import Qt\n\nfrom electrum.i18n import _\nfrom electrum.qrreader import QrCodeResult\n\nfrom electrum.gui.qt.util import ColorScheme, QColorLerp\n\n\nclass QrReaderValidatorResult():\n    \"\"\"\n    Result of a QR code validator\n    \"\"\"\n\n    def __init__(self):\n        self.accepted: bool = False\n\n        self.message: str = None\n        self.message_color: QColor = None\n\n        self.simple_result : str = None\n\n        self.result_usable: Dict[QrCodeResult, bool] = {}\n        self.result_colors: Dict[QrCodeResult, QColor] = {}\n        self.result_messages: Dict[QrCodeResult, str] = {}\n\n        self.selected_results: List[QrCodeResult] = []\n\n\nclass AbstractQrReaderValidator(ABC):\n    \"\"\"\n    Abstract base class for QR code result validators.\n    \"\"\"\n\n    @abstractmethod\n    def validate_results(self, results: List[QrCodeResult]) -> QrReaderValidatorResult:\n        \"\"\"\n        Checks a list of QR code results for usable codes.\n        \"\"\"\n\nclass QrReaderValidatorCounting(AbstractQrReaderValidator):\n    \"\"\"\n    This QR code result validator doesn't directly accept any results but maintains a dictionary\n    of detection counts in `result_counts`.\n    \"\"\"\n\n    result_counts: Dict[QrCodeResult, int] = {}\n\n    def validate_results(self, results: List[QrCodeResult]) -> QrReaderValidatorResult:\n        res = QrReaderValidatorResult()\n\n        for result in results:\n            # Increment the detection count\n            if result not in self.result_counts:\n                self.result_counts[result] = 0\n            self.result_counts[result] += 1\n\n        # Search for missing results, iterate over a copy because the loop might modify the dict\n        for result in self.result_counts.copy():\n            # Count down missing results\n            if result in results:\n                continue\n            self.result_counts[result] -= 2\n            # When the count goes to zero, remove\n            if self.result_counts[result] < 1:\n                del self.result_counts[result]\n\n        return res\n\nclass QrReaderValidatorColorizing(QrReaderValidatorCounting):\n    \"\"\"\n    This QR code result validator doesn't directly accept any results but colorizes the results\n    based on the counts maintained by `QrReaderValidatorCounting`.\n    \"\"\"\n\n    WEAK_COLOR: QColor = QColor(Qt.GlobalColor.red)\n    STRONG_COLOR: QColor = QColor(Qt.GlobalColor.green)\n\n    strong_count: int = 2  # FIXME: make this time based rather than framect based\n    # note: we set a low strong_count to ~disable this mechanism and make QR codes\n    #       much easier to scan (but potentially with some false positives)\n\n    def validate_results(self, results: List[QrCodeResult]) -> QrReaderValidatorResult:\n        res = super().validate_results(results)\n\n        # Colorize the QR code results by their detection counts\n        for result in results:\n            # Enforce strong_count as upper limit\n            self.result_counts[result] = min(self.result_counts[result], self.strong_count)\n\n            # Interpolate between WEAK_COLOR and STRONG_COLOR based on count / strong_count\n            lerp_factor = (self.result_counts[result] - 1) / self.strong_count\n            lerped_color = QColorLerp(self.WEAK_COLOR, self.STRONG_COLOR, lerp_factor)\n            res.result_colors[result] = lerped_color\n\n        return res\n\nclass QrReaderValidatorStrong(QrReaderValidatorColorizing):\n    \"\"\"\n    This QR code result validator doesn't directly accept any results but passes every strong\n    detection in the return values `selected_results`.\n    \"\"\"\n\n    def validate_results(self, results: List[QrCodeResult]) -> QrReaderValidatorResult:\n        res = super().validate_results(results)\n\n        for result in results:\n            if self.result_counts[result] >= self.strong_count:\n                res.selected_results.append(result)\n                break\n\n        return res\n\nclass QrReaderValidatorCounted(QrReaderValidatorStrong):\n    \"\"\"\n    This QR code result validator accepts a result as soon as there is at least `minimum` and at\n    most `maximum` QR code(s) with strong detection.\n    \"\"\"\n\n    def __init__(self, minimum: int = 1, maximum: int = 1):\n        super().__init__()\n        self.minimum = minimum\n        self.maximum = maximum\n\n    def validate_results(self, results: List[QrCodeResult]) -> QrReaderValidatorResult:\n        res = super().validate_results(results)\n\n        num_results = len(res.selected_results)\n        if num_results < self.minimum:\n            if num_results > 0:\n                res.message = _('Too few QR codes detected.')\n                res.message_color = ColorScheme.RED.as_color()\n        elif num_results > self.maximum:\n            res.message = _('Too many QR codes detected.')\n            res.message_color = ColorScheme.RED.as_color()\n        else:\n            res.accepted = True\n            res.simple_result = (results and results[0].data) or ''  # hack added by calin just to take the first one\n\n        return res\n"
  },
  {
    "path": "electrum/gui/qt/qrreader/qtmultimedia/video_overlay.py",
    "content": "#!/usr/bin/env python3\n#\n# Electron Cash - lightweight Bitcoin client\n# Copyright (C) 2019 Axel Gembe <derago@gmail.com>\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import List\n\nfrom PyQt6.QtWidgets import QWidget\nfrom PyQt6.QtGui import QPainter, QPaintEvent, QPen, QPainterPath, QColor, QTransform\nfrom PyQt6.QtCore import QPoint, QSize, QRect, QRectF, Qt\n\nfrom electrum.qrreader import QrCodeResult\n\nfrom .validator import QrReaderValidatorResult\n\n\nclass QrReaderVideoOverlay(QWidget):\n    \"\"\"\n    Overlays the QR scanner results over the video\n    \"\"\"\n\n    BG_RECT_PADDING = 10\n    BG_RECT_CORNER_RADIUS = 10.0\n    BG_RECT_OPACITY = 0.75\n\n    def __init__(self, parent: QWidget = None):\n        super().__init__(parent)\n\n        self.results = []\n        self.flip_x = False\n        self.validator_results = None\n        self.crop = None\n        self.resolution = None\n\n        self.qr_outline_pen = QPen()\n        self.qr_outline_pen.setColor(Qt.GlobalColor.red)\n        self.qr_outline_pen.setWidth(3)\n        self.qr_outline_pen.setStyle(Qt.PenStyle.DotLine)\n\n        self.text_pen = QPen()\n        self.text_pen.setColor(Qt.GlobalColor.black)\n\n        self.bg_rect_pen = QPen()\n        self.bg_rect_pen.setColor(Qt.GlobalColor.black)\n        self.bg_rect_pen.setStyle(Qt.PenStyle.DotLine)\n        self.bg_rect_fill = QColor(255, 255, 255, int(255 * self.BG_RECT_OPACITY))\n\n    def set_results(self, results: List[QrCodeResult], flip_x: bool,\n                    validator_results: QrReaderValidatorResult):\n        self.results = results\n        self.flip_x = flip_x\n        self.validator_results = validator_results\n        self.update()\n\n    def set_crop(self, crop: QRect):\n        self.crop = crop\n\n    def set_resolution(self, resolution: QSize):\n        self.resolution = resolution\n\n    def paintEvent(self, _event: QPaintEvent):\n        if not self.crop or not self.resolution:\n            return\n\n        painter = QPainter(self)\n\n        # Keep a backup of the transform and create a new one\n        transform = painter.worldTransform()\n\n        # Set scaling transform\n        transform = transform.scale(self.width() / self.resolution.width(),\n                                    self.height() / self.resolution.height())\n\n        # Compute the transform to flip the coordinate system on the x axis\n        transform_flip = QTransform()\n        if self.flip_x:\n            transform_flip = transform_flip.translate(self.resolution.width(), 0.0)\n            transform_flip = transform_flip.scale(-1.0, 1.0)\n\n        # Small helper for tuple to QPoint\n        def toqp(point):\n            return QPoint(point[0], point[1])\n\n        # Starting from here we care about AA\n        painter.setRenderHint(QPainter.RenderHint.Antialiasing)\n\n        # Draw all the QR code results\n        for res in self.results:\n            painter.setWorldTransform(transform_flip * transform, False)\n\n            # Draw lines between all of the QR code points\n            pen = QPen(self.qr_outline_pen)\n            if res in self.validator_results.result_colors:\n                pen.setColor(self.validator_results.result_colors[res])\n            painter.setPen(pen)\n            num_points = len(res.points)\n            for i in range(0, num_points):\n                i_n = i + 1\n\n                line_from = toqp(res.points[i])\n                line_from += self.crop.topLeft()\n\n                line_to = toqp(res.points[i_n] if i_n < num_points else res.points[0])\n                line_to += self.crop.topLeft()\n\n                painter.drawLine(line_from, line_to)\n\n            # Draw the QR code data\n            # Note that we reset the world transform to only the scaled transform\n            # because otherwise the text could be flipped. We only use transform_flip\n            # to map the center point of the result.\n            painter.setWorldTransform(transform, False)\n            font_metrics = painter.fontMetrics()\n            data_metrics = QSize(font_metrics.horizontalAdvance(res.data), font_metrics.capHeight())\n\n            center_pos = toqp(res.center)\n            center_pos += self.crop.topLeft()\n            center_pos = transform_flip.map(center_pos)\n\n            text_offset = QPoint(data_metrics.width(), data_metrics.height())\n            text_offset = text_offset / 2\n            text_offset.setX(-text_offset.x())\n            center_pos += text_offset\n\n            padding = self.BG_RECT_PADDING\n            bg_rect_pos = center_pos - QPoint(padding, data_metrics.height() + padding)\n            bg_rect_size = data_metrics + (QSize(padding, padding) * 2)\n            bg_rect = QRect(bg_rect_pos, bg_rect_size)\n            bg_rect_path = QPainterPath()\n            radius = self.BG_RECT_CORNER_RADIUS\n            bg_rect_path.addRoundedRect(QRectF(bg_rect), radius, radius, Qt.SizeMode.AbsoluteSize)\n            painter.setPen(self.bg_rect_pen)\n            painter.fillPath(bg_rect_path, self.bg_rect_fill)\n            painter.drawPath(bg_rect_path)\n\n            painter.setPen(self.text_pen)\n            painter.drawText(center_pos, res.data)\n"
  },
  {
    "path": "electrum/gui/qt/qrreader/qtmultimedia/video_surface.py",
    "content": "#!/usr/bin/env python3\n#\n# Copyright (C) 2019 Axel Gembe <derago@gmail.com>\n# Copyright (c) 2024 The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import List\n\nfrom PyQt6.QtMultimedia import (QVideoFrame, QVideoFrameFormat, QVideoSink)\nfrom PyQt6.QtGui import QImage\nfrom PyQt6.QtCore import QObject, pyqtSignal\n\nfrom electrum.i18n import _\nfrom electrum.logging import get_logger\n\n\n_logger = get_logger(__name__)\n\n\nclass QrReaderVideoSurface(QVideoSink):\n    \"\"\"\n    Receives QVideoFrames from QCamera, converts them into a QImage, flips the X and Y axis if\n    necessary and sends them to listeners via the frame_available event.\n    \"\"\"\n\n    def __init__(self, parent: QObject = None):\n        super().__init__(parent)\n        self.videoFrameChanged.connect(self._on_new_frame)\n\n    def _on_new_frame(self, frame: QVideoFrame) -> None:\n        if not frame.isValid():\n            return\n\n        image_format = QVideoFrameFormat.imageFormatFromPixelFormat(frame.pixelFormat())\n        if image_format == QVideoFrameFormat.PixelFormat.Format_Invalid:\n            _logger.info(_('QR code scanner for video frame with invalid pixel format'))\n            return\n\n        if not frame.map(QVideoFrame.MapMode.ReadOnly):\n            _logger.info(_('QR code scanner failed to map video frame'))\n            return\n\n        try:\n            img = frame.toImage()\n\n            # Check whether we need to flip the image on any axis\n            surface_format = frame.surfaceFormat()\n            flip_x = surface_format.isMirrored()\n            flip_y = surface_format.scanLineDirection() == QVideoFrameFormat.Direction.BottomToTop\n\n            # Mirror the image if needed\n            if flip_x or flip_y:\n                img = img.mirrored(flip_x, flip_y)\n\n            # Create a copy of the image so the original frame data can be freed\n            img = img.copy()\n        finally:\n            frame.unmap()\n\n        self.frame_available.emit(img)\n\n    frame_available = pyqtSignal(QImage)\n"
  },
  {
    "path": "electrum/gui/qt/qrreader/qtmultimedia/video_widget.py",
    "content": "#!/usr/bin/env python3\n#\n# Electron Cash - lightweight Bitcoin client\n# Copyright (C) 2019 Axel Gembe <derago@gmail.com>\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom PyQt6.QtWidgets import QWidget\nfrom PyQt6.QtGui import QPixmap, QPainter, QPaintEvent\n\n\nclass QrReaderVideoWidget(QWidget):\n    \"\"\"\n    Simple widget for drawing a pixmap\n    \"\"\"\n\n    USE_BILINEAR_FILTER = True\n\n    def __init__(self, parent: QWidget = None):\n        super().__init__(parent)\n\n        self.pixmap = None\n\n    def paintEvent(self, _event: QPaintEvent):\n        if not self.pixmap:\n            return\n        painter = QPainter(self)\n        if self.USE_BILINEAR_FILTER:\n            painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)\n        painter.drawPixmap(self.rect(), self.pixmap, self.pixmap.rect())\n\n    def setPixmap(self, pixmap: QPixmap):\n        self.pixmap = pixmap\n        self.update()\n"
  },
  {
    "path": "electrum/gui/qt/qrtextedit.py",
    "content": "from functools import partial\nfrom typing import Callable\n\nfrom electrum.i18n import _\nfrom electrum.plugin import run_hook\nfrom electrum.simple_config import SimpleConfig\n\nfrom .util import ButtonsTextEdit, MessageBoxMixin, ColorScheme, read_QIcon\nfrom .util import get_icon_camera, get_icon_qrcode, add_input_actions_to_context_menu\n\n\nclass ShowQRTextEdit(ButtonsTextEdit):\n\n    def __init__(self, text=None, *, config: SimpleConfig):\n        ButtonsTextEdit.__init__(self, text)\n        self.setReadOnly(True)\n        self.add_qr_show_button(config=config)\n        run_hook('show_text_edit', self)\n\n    def contextMenuEvent(self, e):\n        m = self.createStandardContextMenu()\n        m.addAction(get_icon_qrcode(), _(\"Show as QR code\"), self.on_qr_show_btn)\n        m.exec(e.globalPos())\n\n\nclass ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):\n\n    def __init__(\n            self, text=\"\", allow_multi: bool = False,\n            *,\n            config: SimpleConfig,\n            setText: Callable[[str], None] = None,\n            is_payto = False,\n    ):\n        ButtonsTextEdit.__init__(self, text)\n        self.setReadOnly(False)\n        self.on_qr_from_camera_input_btn = partial(\n            self.input_qr_from_camera,\n            config=config,\n            allow_multi=allow_multi,\n            show_error=self.show_error,\n            setText=setText,\n        )\n        self.on_qr_from_screenshot_input_btn = partial(\n            self.input_qr_from_screenshot,\n            allow_multi=allow_multi,\n            show_error=self.show_error,\n            setText=setText,\n        )\n        self.on_qr_from_file_input_btn = partial(\n            self.input_qr_from_file,\n            allow_multi=allow_multi,\n            config=config,\n            show_error=self.show_error,\n            setText=setText,\n        )\n        self.on_input_file = partial(\n            self.input_file,\n            config=config,\n            show_error=self.show_error,\n            setText=setText,\n        )\n        # for send tab, buttons are available in the toolbar\n        if not is_payto:\n            self.add_input_buttons(config, allow_multi, setText)\n        run_hook('scan_text_edit', self)\n\n    def add_input_buttons(self, config, allow_multi, setText):\n        self.add_menu_button(\n            options=[\n                (\"picture_in_picture.png\", _(\"Read QR code from screen\"), self.on_qr_from_screenshot_input_btn),\n                (\"qr_file.png\",            _(\"Read QR code from file\"),   self.on_qr_from_file_input_btn),\n                (\"file.png\",               _(\"Read text from file\"),      self.on_input_file),\n            ],\n        )\n        self.add_qr_input_from_camera_button(config=config, show_error=self.show_error, allow_multi=allow_multi, setText=setText)\n\n    def contextMenuEvent(self, e):\n        m = self.createStandardContextMenu()\n        m.addSeparator()\n        add_input_actions_to_context_menu(self, m)\n        m.exec(e.globalPos())\n\n\nclass ScanShowQRTextEdit(ScanQRTextEdit):\n\n    def __init__(self, *args, config: SimpleConfig, **kwargs):\n        ScanQRTextEdit.__init__(self, *args, **kwargs, config=config)\n        self.add_qr_show_button(config=config)\n        run_hook('show_text_edit', self)\n\n    def contextMenuEvent(self, e):\n        m = self.createStandardContextMenu()\n        m.addSeparator()\n        add_input_actions_to_context_menu(self, m)\n        m.addAction(get_icon_qrcode(), _(\"Show as QR code\"), self.on_qr_show_btn)\n        m.exec(e.globalPos())\n"
  },
  {
    "path": "electrum/gui/qt/qrwindow.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2014 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtWidgets import QHBoxLayout, QWidget\n\nfrom .qrcodewidget import QRCodeWidget\n\nfrom electrum.i18n import _\n\n\nclass QR_Window(QWidget):\n\n    def __init__(self, win):\n        QWidget.__init__(self)\n        self.main_window = win\n        self.setWindowTitle('Electrum - '+_('Payment Request'))\n        self.setMinimumSize(800, 800)\n        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n        main_box = QHBoxLayout()\n        self.qrw = QRCodeWidget()\n        main_box.addWidget(self.qrw, 1)\n        self.setLayout(main_box)\n\n    def closeEvent(self, event):\n        self.main_window.receive_tab.qr_menu_action.setChecked(False)\n"
  },
  {
    "path": "electrum/gui/qt/rate_limiter.py",
    "content": "# Copyright (c) 2019 Calin Culianu <calin.culianu@gmail.com>\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nfrom functools import wraps\nimport threading\nimport time\nimport weakref\n\nfrom PyQt6.QtCore import QObject, QTimer\n\nfrom electrum.logging import Logger, get_logger\n\n\n_logger = get_logger(__name__)\n\n\nclass RateLimiter(Logger):\n    ''' Manages the state of a @rate_limited decorated function, collating\n    multiple invocations. This class is not intended to be used directly. Instead,\n    use the @rate_limited decorator (for instance methods).\n    This state instance gets inserted into the instance attributes of the target\n    object wherever a @rate_limited decorator appears.\n    The inserted attribute is named \"__FUNCNAME__RateLimiter\". '''\n    # some defaults\n    last_ts = 0.0\n    timer = None\n    saved_args = (tuple(),dict())\n    ctr = 0\n\n    def __init__(self, rate, ts_after, obj, func):\n        self.n = func.__name__\n        self.qn = func.__qualname__\n        self.rate = rate\n        self.ts_after = ts_after\n        self.obj = weakref.ref(obj) # keep a weak reference to the object to prevent cycles\n        self.func = func\n        Logger.__init__(self)\n        #self.logger.debug(f\"*** Created: {func=},{obj=},{rate=}\")\n\n    def diagnostic_name(self):\n        return \"{}:{}\".format(\"rate_limited\",self.qn)\n\n    def kill_timer(self):\n        if self.timer:\n            #self.logger.debug(\"deleting timer\")\n            try:\n                self.timer.stop()\n                self.timer.deleteLater()\n            except RuntimeError as e:\n                if 'c++ object' in str(e).lower():\n                    # This can happen if the attached object which actually owns\n                    # QTimer is deleted by Qt before this call path executes.\n                    # This call path may be executed from a queued connection in\n                    # some circumstances, hence the crazyness (I think).\n                    self.logger.debug(\"advisory: QTimer was already deleted by Qt, ignoring...\")\n                else:\n                    raise\n            finally:\n                self.timer = None\n\n    @classmethod\n    def attr_name(cls, func): return \"__{}__{}\".format(func.__name__, cls.__name__)\n\n    @classmethod\n    def invoke(cls, rate, ts_after, func, args, kwargs):\n        ''' Calls _invoke() on an existing RateLimiter object (or creates a new\n        one for the given function on first run per target object instance). '''\n        assert args and isinstance(args[0], object), \"@rate_limited decorator may only be used with object instance methods\"\n        assert threading.current_thread() is threading.main_thread(), \"@rate_limited decorator may only be used with functions called in the main thread\"\n        obj = args[0]\n        a_name = cls.attr_name(func)\n        #_logger.debug(f\"*** {a_name=}, {obj=}\")\n        rl = getattr(obj, a_name, None) # we hide the RateLimiter state object in an attribute (name based on the wrapped function name) in the target object\n        if rl is None:\n            # must be the first invocation, create a new RateLimiter state instance.\n            rl = cls(rate, ts_after, obj, func)\n            setattr(obj, a_name, rl)\n        return rl._invoke(args, kwargs)\n\n    def _invoke(self, args, kwargs):\n        self._push_args(args, kwargs)  # since we're collating, save latest invocation's args unconditionally. any future invocation will use the latest saved args.\n        self.ctr += 1 # increment call counter\n        #self.logger.debug(f\"args_saved={args}, kwarg_saved={kwargs}\")\n        if not self.timer: # check if there's a pending invocation already\n            now = time.time()\n            diff = float(self.rate) - (now - self.last_ts)\n            if diff <= 0:\n                # Time since last invocation was greater than self.rate, so call the function directly now.\n                #self.logger.debug(\"calling directly\")\n                return self._doIt()\n            else:\n                # Time since last invocation was less than self.rate, so defer to the future with a timer.\n                self.timer = QTimer(self.obj() if isinstance(self.obj(), QObject) else None)\n                self.timer.timeout.connect(self._doIt)\n                #self.timer.destroyed.connect(lambda x=None,qn=self.qn: print(qn,\"Timer deallocated\"))\n                self.timer.setSingleShot(True)\n                self.timer.start(int(diff*1e3))\n                #self.logger.debug(\"deferring\")\n        else:\n            # We had a timer active, which means as future call will occur. So return early and let that call happen in the future.\n            # Note that a side-effect of this aborted invocation was to update self.saved_args.\n            pass\n            #self.logger.debug(\"ignoring (already scheduled)\")\n\n    def _pop_args(self):\n        args, kwargs = self.saved_args # grab the latest collated invocation's args. this attribute is always defined.\n        self.saved_args = (tuple(),dict()) # clear saved args immediately\n        return args, kwargs\n\n    def _push_args(self, args, kwargs):\n        self.saved_args = (args, kwargs)\n\n    def _doIt(self):\n        #self.logger.debug(\"called!\")\n        t0 = time.time()\n        args, kwargs = self._pop_args()\n        #self.logger.debug(f\"args_actually_used={args}, kwarg_actually_used={kwargs}\")\n        ctr0 = self.ctr # read back current call counter to compare later for reentrancy detection\n        retval = self.func(*args, **kwargs) # and.. call the function. use latest invocation's args\n        was_reentrant = self.ctr != ctr0 # if ctr is not the same, func() led to a call this function!\n        del args, kwargs # deref args right away (allow them to get gc'd)\n        tf = time.time()\n        time_taken = tf-t0\n        if self.ts_after:\n            self.last_ts = tf\n        else:\n            if time_taken > float(self.rate):\n                self.logger.debug(f\"method took too long: {time_taken} > {self.rate}. Fudging timestamps to compensate.\")\n                self.last_ts = tf # Hmm. This function takes longer than its rate to complete. so mark its last run time as 'now'. This breaks the rate but at least prevents this function from starving the CPU (benforces a delay).\n            else:\n                self.last_ts = t0 # Function takes less than rate to complete, so mark its t0 as when we entered to keep the rate constant.\n\n        if self.timer: # timer is not None if and only if we were a delayed (collated) invocation.\n            if was_reentrant:\n                # we got a reentrant call to this function as a result of calling func() above! re-schedule the timer.\n                self.logger.debug(\"*** detected a re-entrant call, re-starting timer\")\n                time_left = float(self.rate) - (tf - self.last_ts)\n                self.timer.start(time_left*1e3)\n            else:\n                # We did not get a reentrant call, so kill the timer so subsequent calls can schedule the timer and/or call func() immediately.\n                self.kill_timer()\n        elif was_reentrant:\n            self.logger.debug(\"*** detected a re-entrant call\")\n\n        return retval\n\n\nclass RateLimiterClassLvl(RateLimiter):\n    ''' This RateLimiter object is used if classlevel=True is specified to the\n    @rate_limited decorator.  It inserts the __RateLimiterClassLvl state object\n    on the class level and collates calls for all instances to not exceed rate.\n    Each instance is guaranteed to receive at least 1 call and to have multiple\n    calls updated with the latest args for the final call. So for instance:\n    a.foo(1)\n    a.foo(2)\n    b.foo(10)\n    b.foo(3)\n    Would collate to a single 'class-level' call using 'rate':\n    a.foo(2) # latest arg taken, collapsed to 1 call\n    b.foo(3) # latest arg taken, collapsed to 1 call\n    '''\n\n    @classmethod\n    def invoke(cls, rate, ts_after, func, args, kwargs):\n        assert args and not isinstance(args[0], type), \"@rate_limited decorator may not be used with static or class methods\"\n        obj = args[0]\n        objcls = obj.__class__\n        args = list(args)\n        args.insert(0, objcls) # prepend obj class to trick super.invoke() into making this state object be class-level.\n        return super(RateLimiterClassLvl, cls).invoke(rate, ts_after, func, args, kwargs)\n\n    def _push_args(self, args, kwargs):\n        objcls, obj = args[0:2]\n        args = args[2:]\n        self.saved_args[obj] = (args, kwargs)\n\n    def _pop_args(self):\n        weak_dict = self.saved_args\n        self.saved_args = weakref.WeakKeyDictionary()\n        return (weak_dict,),dict()\n\n    def _call_func_for_all(self, weak_dict):\n        for ref in weak_dict.keyrefs():\n            obj = ref()\n            if obj:\n                args,kwargs = weak_dict[obj]\n                obj_name = obj.diagnostic_name() if hasattr(obj, \"diagnostic_name\") else obj\n                #self.logger.debug(f\"calling for {obj_name}, timer={bool(self.timer)}\")\n                self.func_target(obj, *args, **kwargs)\n\n    def __init__(self, rate, ts_after, obj, func):\n        # note: obj here is really the __class__ of the obj because we prepended the class in our custom invoke() above.\n        super().__init__(rate, ts_after, obj, func)\n        self.func_target = func\n        self.func = self._call_func_for_all\n        self.saved_args = weakref.WeakKeyDictionary() # we don't use a simple arg tuple, but instead an instance -> args,kwargs dictionary to store collated calls, per instance collated\n\n\ndef rate_limited(rate, *, classlevel=False, ts_after=False):\n    \"\"\" A Function decorator for rate-limiting GUI event callbacks. Argument\n        rate in seconds is the minimum allowed time between subsequent calls of\n        this instance of the function. Calls that arrive more frequently than\n        rate seconds will be collated into a single call that is deferred onto\n        a QTimer. It is preferable to use this decorator on QObject subclass\n        instance methods. This decorator is particularly useful in limiting\n        frequent calls to GUI update functions.\n        params:\n            rate - calls are collated to not exceed rate (in seconds)\n            classlevel - if True, specify that the calls should be collated at\n                1 per `rate` secs. for *all* instances of a class, otherwise\n                calls will be collated on a per-instance basis.\n            ts_after - if True, mark the timestamp of the 'last call' AFTER the\n                target method completes.  That is, the collation of calls will\n                ensure at least `rate` seconds will always elapse between\n                subsequent calls. If False, the timestamp is taken right before\n                the collated calls execute (thus ensuring a fixed period for\n                collated calls).\n                TL;DR: ts_after=True : `rate` defines the time interval you want\n                                        from last call's exit to entry into next\n                                        call.\n                       ts_adter=False: `rate` defines the time between each\n                                        call's entry.\n        (See on_fx_quotes & on_fx_history in main_window.py for example usages\n        of this decorator). \"\"\"\n    def wrapper0(func):\n        @wraps(func)\n        def wrapper(*args, **kwargs):\n            if classlevel:\n                return RateLimiterClassLvl.invoke(rate, ts_after, func, args, kwargs)\n            return RateLimiter.invoke(rate, ts_after, func, args, kwargs)\n        return wrapper\n    return wrapper0\n\n"
  },
  {
    "path": "electrum/gui/qt/rbf_dialog.py",
    "content": "# Copyright (C) 2021 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtWidgets import QLabel, QGridLayout, QHBoxLayout, QComboBox\n\nfrom .util import ColorScheme\n\nfrom electrum.i18n import _\nfrom electrum.transaction import PartialTransaction\nfrom electrum.wallet import CannotRBFTx, BumpFeeStrategy\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nfrom .confirm_tx_dialog import TxEditor, TxSizeLabel, HelpLabel\n\n\nclass _BaseRBFDialog(TxEditor):\n\n    def __init__(\n            self,\n            *,\n            main_window: 'ElectrumWindow',\n            tx: PartialTransaction,\n            title: str):\n\n        self.wallet = main_window.wallet\n        self.old_tx = tx\n        self.message = ''\n\n        self.old_fee = self.old_tx.get_fee()\n        self.old_tx_size = tx.estimated_size()\n        self.old_fee_rate = old_fee_rate = self.old_fee / self.old_tx_size  # sat/vbyte\n\n        output_value = sum([txo.value for txo in tx.outputs() if not txo.is_mine])\n        if output_value == 0:\n            output_value = tx.output_value()\n\n        TxEditor.__init__(\n            self,\n            window=main_window,\n            title=title,\n            make_tx=self.rbf_func,\n            output_value=output_value,\n        )\n\n        self.fee_e.setFrozen(True)  # disallow setting absolute fee for now, as wallet.bump_fee can only target feerate\n        new_fee_rate = self.old_fee_rate + max(1, self.old_fee_rate // 20)\n        self.feerate_e.setAmount(new_fee_rate)\n        self.update()\n        self.fee_slider.deactivate()\n\n    def create_grid(self):\n        self.method_label = QLabel(_('Method') + ':')\n        self.method_combo = QComboBox()\n        self._strategies, def_strat_idx = self.wallet.get_bumpfee_strategies_for_tx(tx=self.old_tx)\n        self.method_combo.addItems([strat.text() for strat in self._strategies])\n        self.method_combo.setCurrentIndex(def_strat_idx)\n        self.method_combo.currentIndexChanged.connect(self.trigger_update)\n        self.method_combo.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n        old_size_label = TxSizeLabel()\n        old_size_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        old_size_label.setAmount(self.old_tx_size)\n        old_size_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())\n        current_fee_hbox = QHBoxLayout()\n        current_fee_hbox.addWidget(QLabel(self.main_window.format_fee_rate(1000 * self.old_fee_rate)))\n        current_fee_hbox.addWidget(old_size_label)\n        current_fee_hbox.addWidget(QLabel(self.main_window.format_amount_and_units(self.old_fee)))\n        current_fee_hbox.addStretch()\n        grid = QGridLayout()\n        grid.addWidget(self.method_label, 0, 0)\n        grid.addWidget(self.method_combo, 0, 1)\n        grid.addWidget(QLabel(_('Current fee') + ':'), 1, 0)\n        grid.addLayout(current_fee_hbox, 1, 1, 1, 3)\n        grid.addWidget(QLabel(_('New fee') + ':'), 2, 0)\n        grid.addLayout(self.fee_hbox, 2, 1, 1, 3)\n        grid.addWidget(HelpLabel(_(\"Fee target\") + \": \", self.fee_combo.help_msg), 4, 0)\n        grid.addLayout(self.fee_target_hbox, 4, 1, 1, 3)\n        grid.setColumnStretch(4, 1)\n        # locktime\n        grid.addWidget(self.locktime_label, 5, 0)\n        grid.addWidget(self.locktime_e, 5, 1, 1, 2)\n        return grid\n\n    def run(self) -> None:\n        if not self.exec():\n            return\n        if self.is_preview:\n            self.main_window.show_transaction(self.tx)\n            return\n\n        def sign_done(success):\n            if success:\n                self.main_window.broadcast_or_show(self.tx)\n\n        self.main_window.sign_tx(\n            self.tx,\n            callback=sign_done,\n            external_keypairs={})\n\n    def update_tx(self):\n        fee_rate = self.feerate_e.get_amount()\n        if fee_rate is None:\n            self.tx = None\n            self.error = _('No fee rate')\n        elif fee_rate <= self.old_fee_rate:\n            self.tx = None\n            self.error = _(\"The new fee rate needs to be higher than the old fee rate.\")\n        else:\n            try:\n                self.tx = self.make_tx(fee_rate)\n            except CannotRBFTx as e:\n                self.tx = None\n                self.error = str(e)\n\n    def get_messages(self):\n        messages = super().get_messages()\n        if not self.tx:\n            return\n        delta = self.tx.get_fee() - self.old_tx.get_fee()\n        if self._strategies[self.method_combo.currentIndex()] == BumpFeeStrategy.PRESERVE_PAYMENT:\n            msg = _(\"You will pay {} more.\").format(self.main_window.format_amount_and_units(delta))\n        elif self._strategies[self.method_combo.currentIndex()] == BumpFeeStrategy.DECREASE_PAYMENT:\n            msg = _(\"The recipient will receive {} less.\").format(self.main_window.format_amount_and_units(delta))\n        else:\n            raise Exception(f\"unknown strategy: {self=}\")\n        messages.insert(0, msg)\n        return messages\n\n\nclass BumpFeeDialog(_BaseRBFDialog):\n\n    help_text = _(\"Increase your transaction's fee to improve its position in mempool.\")\n\n    def __init__(\n            self,\n            *,\n            main_window: 'ElectrumWindow',\n            tx: PartialTransaction,\n    ):\n        _BaseRBFDialog.__init__(\n            self,\n            main_window=main_window,\n            tx=tx,\n            title=_('Bump Fee'))\n\n    def rbf_func(self, fee_rate, *, confirmed_only=False):\n        return self.wallet.bump_fee(\n            tx=self.old_tx,\n            new_fee_rate=fee_rate,\n            coins=self.main_window.get_coins(nonlocal_only=True, confirmed_only=confirmed_only),\n            strategy=self._strategies[self.method_combo.currentIndex()],\n        )\n\n\nclass DSCancelDialog(_BaseRBFDialog):\n\n    help_text = _(\n        \"Cancel an unconfirmed transaction by replacing it with \"\n        \"a higher-fee transaction that spends back to your wallet.\")\n\n    def __init__(\n            self,\n            *,\n            main_window: 'ElectrumWindow',\n            tx: PartialTransaction,\n    ):\n        _BaseRBFDialog.__init__(\n            self,\n            main_window=main_window,\n            tx=tx,\n            title=_('Cancel transaction'))\n        self.method_label.setVisible(False)\n        self.method_combo.setVisible(False)\n\n    def rbf_func(self, fee_rate, *, confirmed_only=False):\n        return self.wallet.dscancel(tx=self.old_tx, new_fee_rate=fee_rate)\n"
  },
  {
    "path": "electrum/gui/qt/rebalance_dialog.py",
    "content": "from typing import TYPE_CHECKING\n\nfrom PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton\n\nfrom electrum.i18n import _\nfrom electrum.lnchannel import Channel\n\nfrom .util import WindowModalDialog, Buttons, OkButton, CancelButton, WWLabel\nfrom .amountedit import BTCAmountEdit\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nclass RebalanceDialog(WindowModalDialog):\n\n    def __init__(self, window: 'ElectrumWindow', chan1: Channel, chan2: Channel, amount_sat):\n        WindowModalDialog.__init__(self, window, _(\"Rebalance channels\"))\n        self.window = window\n        self.wallet = window.wallet\n        self.chan1 = chan1\n        self.chan2 = chan2\n        vbox = QVBoxLayout(self)\n        vbox.addWidget(WWLabel(_('Rebalance your channels in order to increase your sending or receiving capacity') + ':'))\n        grid = QGridLayout()\n        self.amount_e = BTCAmountEdit(self.window.get_decimal_point)\n        self.amount_e.setAmount(amount_sat)\n        self.amount_e.textChanged.connect(self.on_amount)\n        self.rev_button = QPushButton(u'\\U000021c4')\n        self.rev_button.clicked.connect(self.on_reverse)\n        self.max_button = QPushButton('Max')\n        self.max_button.clicked.connect(self.on_max)\n        self.label1 = QLabel('')\n        self.label2 = QLabel('')\n        self.ok_button = OkButton(self)\n        self.ok_button.setEnabled(False)\n        grid.addWidget(QLabel(_(\"From channel\")), 0, 0)\n        grid.addWidget(self.label1, 0, 1)\n        grid.addWidget(QLabel(_(\"To channel\")), 1, 0)\n        grid.addWidget(self.label2, 1, 1)\n        grid.addWidget(QLabel(_(\"Amount\")), 2, 0)\n        grid.addWidget(self.amount_e, 2, 1)\n        grid.addWidget(self.max_button, 2, 2)\n        grid.addWidget(self.rev_button, 0, 2)\n        vbox.addLayout(grid)\n        vbox.addLayout(Buttons(CancelButton(self), self.ok_button))\n        self.update()\n\n    def on_reverse(self, x):\n        a, b = self.chan1, self.chan2\n        self.chan1, self.chan2 = b, a\n        self.amount_e.setAmount(None)\n        self.update()\n\n    def on_amount(self, x):\n        self.update()\n\n    def on_max(self, x):\n        n_sat = self.wallet.lnworker.num_sats_can_rebalance(self.chan1, self.chan2)\n        self.amount_e.setAmount(n_sat)\n\n    def update(self):\n        self.label1.setText(self.chan1.short_id_for_GUI())\n        self.label2.setText(self.chan2.short_id_for_GUI())\n        amount_sat = self.amount_e.get_amount()\n        b = bool(amount_sat) and self.wallet.lnworker.num_sats_can_rebalance(self.chan1, self.chan2) >= amount_sat\n        self.ok_button.setEnabled(b)\n\n    def run(self):\n        if not self.exec():\n            return\n        amount_msat = self.amount_e.get_amount() * 1000\n        coro = self.wallet.lnworker.rebalance_channels(self.chan1, self.chan2, amount_msat=amount_msat)\n        self.window.run_coroutine_from_thread(coro, _('Rebalancing channels'))\n        self.window.receive_tab.update_current_request()  # this will gray out the button\n"
  },
  {
    "path": "electrum/gui/qt/receive_tab.py",
    "content": "# Copyright (C) 2022 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nfrom typing import Optional, TYPE_CHECKING\n\nfrom PyQt6.QtGui import QFont, QCursor, QMouseEvent\nfrom PyQt6.QtCore import Qt, QSize\nfrom PyQt6.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QTextEdit,\n                             QHBoxLayout, QPushButton, QWidget, QSizePolicy, QFrame)\n\nfrom electrum.i18n import _\nfrom electrum.util import InvoiceError, ChoiceItem\nfrom electrum.invoices import pr_expiration_values\nfrom electrum.logging import Logger\n\nfrom .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit\nfrom .qrcodewidget import QRCodeWidget\nfrom .util import read_QIcon, WWLabel, MessageBoxMixin, MONOSPACE_FONT, get_icon_qrcode\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nclass ReceiveTab(QWidget, MessageBoxMixin, Logger):\n\n    # strings updated by update_current_request\n    addr = ''\n    lnaddr = ''\n    URI = ''\n    address_help = ''\n    URI_help = ''\n    ln_help = ''\n\n    def __init__(self, window: 'ElectrumWindow'):\n        QWidget.__init__(self, window)\n        Logger.__init__(self)\n\n        self.window = window\n        self.wallet = window.wallet\n        self.fx = window.fx\n        self.config = window.config\n\n        # A 4-column grid layout.  All the stretch is in the last column.\n        # The exchange rate plugin adds a fiat widget in column 2\n        self.receive_grid = grid = QGridLayout()\n        grid.setSpacing(8)\n        grid.setColumnStretch(3, 1)\n\n        self.receive_message_e = SizedFreezableLineEdit(width=400)\n        grid.addWidget(QLabel(_('Description')), 0, 0)\n        grid.addWidget(self.receive_message_e, 0, 1, 1, 4)\n\n        self.receive_amount_e = BTCAmountEdit(self.window.get_decimal_point)\n        grid.addWidget(QLabel(_('Requested amount')), 1, 0)\n        grid.addWidget(self.receive_amount_e, 1, 1)\n\n        self.fiat_receive_e = AmountEdit(self.fx.get_currency if self.fx else '')\n        if not self.fx or not self.fx.is_enabled():\n            self.fiat_receive_e.setVisible(False)\n        grid.addWidget(self.fiat_receive_e, 1, 2, Qt.AlignmentFlag.AlignLeft)\n\n        self.window.connect_fields(self.receive_amount_e, self.fiat_receive_e)\n\n        self.expiry_button = QPushButton('')\n        self.expiry_button.clicked.connect(self.expiry_dialog)\n        grid.addWidget(QLabel(_('Expiry')), 2, 0)\n        grid.addWidget(self.expiry_button, 2, 1)\n\n        self.clear_invoice_button = QPushButton(_('Clear'))\n        self.clear_invoice_button.clicked.connect(self.do_clear)\n        text = _('Onchain') if self.wallet.has_lightning() else _('Request')\n        self.create_onchain_invoice_button = QPushButton(text)\n        self.create_onchain_invoice_button.setIcon(read_QIcon(\"bitcoin.png\"))\n        self.create_onchain_invoice_button.clicked.connect(lambda: self.create_invoice(False))\n        self.create_lightning_invoice_button = QPushButton(_('Lightning'))\n        self.create_lightning_invoice_button.setIcon(read_QIcon(\"lightning.png\"))\n        self.create_lightning_invoice_button.clicked.connect(lambda: self.create_invoice(True))\n        self.create_lightning_invoice_button.setVisible(self.wallet.has_lightning())\n\n        self.receive_buttons = buttons = QHBoxLayout()\n        buttons.addWidget(self.clear_invoice_button)\n        buttons.addStretch(1)\n        buttons.addWidget(self.create_onchain_invoice_button)\n        buttons.addWidget(self.create_lightning_invoice_button)\n        grid.addLayout(buttons, 4, 1, 1, -1)\n\n        self.receive_e = QTextEdit()\n        self.receive_e.setFont(QFont(MONOSPACE_FONT))\n        self.receive_e.setReadOnly(True)\n        self.receive_e.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)\n        self.receive_e.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)\n        self.receive_e.textChanged.connect(self.update_receive_widgets)\n\n        self.receive_qr = QRCodeWidget(manual_size=True)\n\n        self.receive_help_text = WWLabel('')\n        self.receive_help_text.setLayout(QHBoxLayout())\n        self.receive_rebalance_button = QPushButton('Rebalance')\n        self.receive_rebalance_button.suggestion = None\n        self.receive_zeroconf_button = QPushButton(_('Accept'))\n        self.receive_zeroconf_button.clicked.connect(self.on_accept_zeroconf)\n\n        def on_receive_rebalance():\n            if self.receive_rebalance_button.suggestion:\n                chan1, chan2, delta = self.receive_rebalance_button.suggestion\n                self.window.rebalance_dialog(chan1, chan2, amount_sat=delta)\n        self.receive_rebalance_button.clicked.connect(on_receive_rebalance)\n        self.receive_swap_button = QPushButton('Swap')\n        self.receive_swap_button.suggestion = None\n\n        def on_receive_swap():\n            if self.receive_swap_button.suggestion:\n                chan, swap_recv_amount_sat = self.receive_swap_button.suggestion\n                self.window.run_swap_dialog(is_reverse=True, recv_amount_sat_or_max=swap_recv_amount_sat, channels=[chan])\n        self.receive_swap_button.clicked.connect(on_receive_swap)\n        buttons = QHBoxLayout()\n        buttons.addWidget(self.receive_rebalance_button)\n        buttons.addWidget(self.receive_swap_button)\n        buttons.addWidget(self.receive_zeroconf_button)\n        vbox = QVBoxLayout()\n        vbox.addWidget(self.receive_help_text)\n        vbox.addLayout(buttons)\n        self.receive_help_widget = FramedWidget()\n        self.receive_help_widget.setVisible(False)\n        self.receive_help_widget.setLayout(vbox)\n\n        self.receive_widget = ReceiveWidget(\n            self, self.receive_e, self.receive_qr, self.receive_help_widget)\n        #self.receive_widget.mouseReleaseEvent = lambda x: self.toggle_receive_qr()\n\n        receive_widget_sp = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding)\n        receive_widget_sp.setRetainSizeWhenHidden(True)\n        self.receive_widget.setSizePolicy(receive_widget_sp)\n        self.receive_widget.setVisible(False)\n\n        self.receive_requests_label = QLabel(_('Requests'))\n        # with QDarkStyle, this label may partially cover the qrcode widget.\n        # setMaximumWidth prevents that\n        self.receive_requests_label.setMaximumWidth(400)\n        from .request_list import RequestList\n        self.request_list = RequestList(self)\n        # toolbar\n        self.toolbar, menu = self.request_list.create_toolbar_with_menu('')\n\n        self.toggle_qr_button = QPushButton('')\n        self.toggle_qr_button.setIcon(get_icon_qrcode())\n        self.toggle_qr_button.setToolTip(_('Switch between text and QR code view'))\n        self.toggle_qr_button.clicked.connect(self.toggle_receive_qr)\n        self.toggle_qr_button.setEnabled(False)\n        self.toolbar.insertWidget(2, self.toggle_qr_button)\n\n        # menu\n        self.qr_menu_action = menu.addToggle(_(\"Show detached QR code window\"), self.window.toggle_qr_window)\n        menu.addAction(_(\"Import requests\"), self.window.import_requests)\n        menu.addAction(_(\"Export requests\"), self.window.export_requests)\n        menu.addAction(_(\"Delete expired requests\"), self.request_list.delete_expired_requests)\n        self.toolbar_menu = menu\n\n        # layout\n        vbox_g = QVBoxLayout()\n        vbox_g.addLayout(grid)\n        vbox_g.addStretch()\n        hbox = QHBoxLayout()\n        hbox.addLayout(vbox_g)\n        hbox.addStretch()\n        hbox.addWidget(self.receive_widget, 1)\n\n        self.searchable_list = self.request_list\n        vbox = QVBoxLayout(self)\n        vbox.addLayout(self.toolbar)\n        vbox.addLayout(hbox)\n        vbox.addStretch()\n        vbox.addWidget(self.receive_requests_label)\n        vbox.addWidget(self.request_list)\n        vbox.setStretchFactor(hbox, 40)\n        vbox.setStretchFactor(self.request_list, 60)\n        self.request_list.update()  # after parented and put into a layout, can update without flickering\n        self.update_expiry_text()\n\n    def update_expiry_text(self):\n        expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS\n        text = pr_expiration_values()[expiry]\n        self.expiry_button.setText(text)\n\n    def expiry_dialog(self):\n        msg = ''.join([\n            _('Expiration period of your request.'), ' ',\n            _('This information is seen by the recipient if you send them a signed payment request.'),\n            '\\n\\n',\n            _('For on-chain requests, the address gets reserved until expiration. After that, it might get reused.'), ' ',\n            _('The bitcoin address never expires and will always be part of this electrum wallet.'), ' ',\n            _('You can reuse a bitcoin address any number of times but it is not good for your privacy.'),\n            '\\n\\n',\n            _('For Lightning requests, payments will not be accepted after the expiration.'),\n        ])\n        expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS\n        choices = [ChoiceItem(key=exptime, label=label)\n                   for (exptime, label) in pr_expiration_values().items()]\n        v = self.window.query_choice(msg, choices, title=_('Expiry'), default_key=expiry)\n        if v is None:\n            return\n        self.config.WALLET_PAYREQ_EXPIRY_SECONDS = v\n        self.update_expiry_text()\n\n    def on_tab_changed(self):\n        text, data, help_text, title = self.get_tab_data()\n        self.window.do_copy(text, title=title)\n        self.update_receive_qr_window()\n\n    def do_copy(self, e: 'QMouseEvent'):\n        if e.button() != Qt.MouseButton.LeftButton:\n            return\n        text, data, help_text, title = self.get_tab_data()\n        self.window.do_copy(text, title=title)\n\n    def toggle_receive_qr(self):\n        b = not self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE\n        self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE = b\n        self.update_receive_widgets()\n\n    def update_receive_widgets(self):\n        b = self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE\n        self.receive_widget.update_visibility(b)\n\n    def update_current_request(self):\n        if len(self.request_list.selectionModel().selectedRows(0)) > 1:\n            key = None\n        else:\n            key = self.request_list.get_current_key()\n        req = self.wallet.get_request(key) if key else None\n        if req is None:\n            self.receive_e.setText('')\n            self.addr = self.URI = self.lnaddr = ''\n            self.address_help = self.URI_help = self.ln_help = ''\n            return\n        help_texts = self.wallet.get_help_texts_for_receive_request(req)\n        self.addr = (req.get_address() or '') if not help_texts.address_is_error else ''\n        self.URI = (self.wallet.get_request_URI(req) or '') if not help_texts.URI_is_error else ''\n        self.lnaddr = self.wallet.get_bolt11_invoice(req) if not help_texts.ln_is_error else ''\n        self.address_help = help_texts.address_help\n        self.URI_help = help_texts.URI_help\n        self.ln_help = help_texts.ln_help\n        can_rebalance = help_texts.can_rebalance()\n        can_swap = help_texts.can_swap()\n        can_zeroconf = help_texts.can_zeroconf()\n        self.receive_rebalance_button.suggestion = help_texts.ln_rebalance_suggestion\n        self.receive_swap_button.suggestion = help_texts.ln_swap_suggestion\n        self.receive_rebalance_button.setVisible(can_rebalance)\n        self.receive_swap_button.setVisible(can_swap)\n        self.receive_rebalance_button.setEnabled(can_rebalance and self.window.num_tasks() == 0)\n        self.receive_swap_button.setEnabled(can_swap and self.window.num_tasks() == 0)\n        self.receive_zeroconf_button.setVisible(can_zeroconf)\n        self.receive_zeroconf_button.setEnabled(can_zeroconf)\n        text, data, help_text, title = self.get_tab_data()\n        self.receive_e.setText(text)\n        self.receive_qr.setData(data)\n        self.receive_help_text.setText(help_text)\n        for w in [self.receive_e, self.receive_qr]:\n            w.setEnabled(bool(text) and (not help_text or can_zeroconf))\n            w.setToolTip(help_text)\n        # macOS hack (similar to #4777)\n        self.receive_e.repaint()\n        # always show\n        if can_zeroconf:\n            # show the help message if zeroconf so user can first accept it and still sees the invoice\n            # after accepting\n            self.receive_widget.show_help()\n        self.receive_widget.setVisible(True)\n        self.toggle_qr_button.setEnabled(True)\n        self.update_receive_qr_window()\n\n    def on_accept_zeroconf(self):\n        self.receive_zeroconf_button.setVisible(False)\n        self.update_receive_widgets()\n\n    def get_tab_data(self):\n        if self.URI:\n            out = self.URI, self.URI, self.URI_help, _('Bitcoin URI')\n        elif self.addr:\n            out = self.addr, self.addr, self.address_help, _('Address')\n        else:\n            # encode lightning invoices as uppercase so QR encoding can use\n            # alphanumeric mode; resulting in smaller QR codes\n            out = self.lnaddr, self.lnaddr.upper(), self.ln_help, _('Lightning Request')\n        return out\n\n    def update_receive_qr_window(self):\n        if self.window.qr_window and self.window.qr_window.isVisible():\n            text, data, help_text, title = self.get_tab_data()\n            self.window.qr_window.qrw.setData(data)\n\n    def create_invoice(self, is_lightning: bool):\n        amount_sat = self.receive_amount_e.get_amount()\n        message = self.receive_message_e.text()\n        expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS\n        if is_lightning:\n            address = None\n        else:\n            if amount_sat and amount_sat < self.wallet.dust_threshold():\n                self.show_error(_('Amount too small to be received onchain'))\n                return\n            address = self.get_bitcoin_address_for_request(amount_sat)\n            if not address:\n                return\n            self.window.address_list.update()\n\n        # generate even if we cannot receive\n        try:\n            key = self.wallet.create_request(amount_sat, message, expiry, address)\n        except InvoiceError as e:\n            self.show_error(_('Error creating payment request') + ':\\n' + str(e))\n            return\n        except Exception as e:\n            self.logger.exception('Error adding payment request')\n            self.show_error(_('Error adding payment request') + ':\\n' + repr(e))\n            return\n        assert key is not None\n        self.window.address_list.refresh_all()\n        self.request_list.update()\n        self.request_list.set_current_key(key)\n        # clear request fields\n        self.receive_amount_e.setText('')\n        self.receive_message_e.setText('')\n        # copy current tab to clipboard\n        self.on_tab_changed()\n\n    def get_bitcoin_address_for_request(self, amount) -> Optional[str]:\n        addr = self.wallet.get_unused_address()\n        if addr is None:\n            if not self.wallet.is_deterministic():  # imported wallet\n                msg = [\n                    _('No more addresses in your wallet.'), ' ',\n                    _('You are using a non-deterministic wallet, which cannot create new addresses.'), ' ',\n                    _('If you want to create new addresses, use a deterministic wallet instead.'), '\\n\\n',\n                    _('Creating a new payment request will reuse one of your addresses and overwrite an existing request. Continue anyway?'),\n                   ]\n                if not self.question(''.join(msg)):\n                    return\n                addr = self.wallet.get_receiving_address()\n            else:  # deterministic wallet\n                if not self.question(_(\"Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\\n\\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\\n\\nCreate anyway?\")):\n                    return\n                addr = self.wallet.create_new_address(False)\n        return addr\n\n    def do_clear(self):\n        self.receive_e.setText('')\n        self.addr = self.URI = self.lnaddr = ''\n        self.address_help = self.URI_help = self.ln_help = ''\n        self.receive_widget.setVisible(False)\n        self.toggle_qr_button.setEnabled(False)\n        self.receive_message_e.setText('')\n        self.receive_amount_e.setAmount(None)\n        self.request_list.clearSelection()\n\n\nclass ReceiveWidget(QWidget):\n    min_size = QSize(200, 200)\n\n    def __init__(self, receive_tab: 'ReceiveTab', textedit: QWidget, qr: QWidget, help_widget: QWidget):\n        QWidget.__init__(self)\n        self.textedit = textedit\n        self.qr = qr\n        self.help_widget = help_widget\n        self.setMinimumSize(self.min_size)\n\n        for w in [textedit, qr]:\n            w.mousePressEvent = receive_tab.do_copy\n            w.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))\n\n        textedit.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n        if isinstance(help_widget, QLabel):\n            help_widget.setFrameStyle(QFrame.Shape.StyledPanel)\n            help_widget.setStyleSheet(\"QLabel {border:1px solid gray; border-radius:2px; }\")\n\n        hbox = QHBoxLayout()\n        hbox.addStretch()\n        hbox.addWidget(textedit)\n        hbox.addWidget(help_widget)\n        hbox.addWidget(qr)\n\n        vbox = QVBoxLayout()\n        vbox.addLayout(hbox)\n        vbox.addStretch()\n\n        self.setLayout(vbox)\n\n    def update_visibility(self, is_qr):\n        if str(self.textedit.toPlainText()):\n            self.help_widget.setVisible(False)\n            self.textedit.setVisible(not is_qr)\n            self.qr.setVisible(is_qr)\n        else:\n            self.show_help()\n\n    def show_help(self):\n        self.help_widget.setVisible(True)\n        self.textedit.setVisible(False)\n        self.qr.setVisible(False)\n\n    def resizeEvent(self, e):\n        # keep square aspect ratio when resized\n        size = e.size()\n        margin = 10\n        x = min(size.height(), size.width()) - margin\n        for w in [self.textedit, self.qr, self.help_widget]:\n            w.setFixedWidth(x)\n            w.setFixedHeight(x)\n        return super().resizeEvent(e)\n\n\nclass FramedWidget(QFrame):\n    def __init__(self):\n        QFrame.__init__(self)\n        self.setFrameStyle(QFrame.Shape.StyledPanel)\n        self.setStyleSheet(\"FramedWidget {border:1px solid gray; border-radius:2px; }\")\n"
  },
  {
    "path": "electrum/gui/qt/request_list.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport enum\nfrom typing import Optional, TYPE_CHECKING\n\nfrom PyQt6.QtGui import QStandardItemModel, QStandardItem\nfrom PyQt6.QtWidgets import QMenu, QAbstractItemView\nfrom PyQt6.QtCore import Qt, QItemSelectionModel, QModelIndex\n\nfrom electrum.i18n import _\nfrom electrum.util import format_time\nfrom electrum.plugin import run_hook\n\nfrom .util import pr_icons, read_QIcon\nfrom .my_treeview import MyTreeView, MySortModel\n\nif TYPE_CHECKING:\n    from .receive_tab import ReceiveTab\n\n\nROLE_REQUEST_TYPE = Qt.ItemDataRole.UserRole\nROLE_KEY = Qt.ItemDataRole.UserRole + 1\nROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 2\n\n\nclass RequestList(MyTreeView):\n    key_role = ROLE_KEY\n\n    class Columns(MyTreeView.BaseColumnsEnum):\n        DATE = enum.auto()\n        DESCRIPTION = enum.auto()\n        AMOUNT = enum.auto()\n        STATUS = enum.auto()\n        ADDRESS = enum.auto()\n        LN_RHASH = enum.auto()\n\n    headers = {\n        Columns.DATE: _('Date'),\n        Columns.DESCRIPTION: _('Description'),\n        Columns.AMOUNT: _('Amount'),\n        Columns.STATUS: _('Status'),\n        Columns.ADDRESS: _('Address'),\n        Columns.LN_RHASH: 'LN RHASH',\n    }\n    filter_columns = [\n        Columns.DATE, Columns.DESCRIPTION, Columns.AMOUNT,\n        Columns.ADDRESS, Columns.LN_RHASH,\n    ]\n\n    def __init__(self, receive_tab: 'ReceiveTab'):\n        window = receive_tab.window\n        super().__init__(\n            main_window=window,\n            stretch_column=self.Columns.DESCRIPTION,\n        )\n        self.wallet = window.wallet\n        self.receive_tab = receive_tab\n        self.std_model = QStandardItemModel(self)\n        self.proxy = MySortModel(self, sort_role=ROLE_SORT_ORDER)\n        self.proxy.setSourceModel(self.std_model)\n        self.setModel(self.proxy)\n        self.setSortingEnabled(True)\n        self.selectionModel().currentRowChanged.connect(self.item_changed)\n        self.selectionModel().selectionChanged.connect(self.selection_changed)\n        self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)\n\n    def set_current_key(self, key):\n        for i in range(self.model().rowCount()):\n            item = self.model().index(i, self.Columns.DATE)\n            row_key = item.data(ROLE_KEY)\n            if key == row_key:\n                self.selectionModel().setCurrentIndex(\n                    item, QItemSelectionModel.SelectionFlag.SelectCurrent | QItemSelectionModel.SelectionFlag.Rows)\n                break\n\n    def get_current_key(self):\n        return self.get_role_data_for_current_item(col=self.Columns.DATE, role=ROLE_KEY)\n\n    def selection_changed(self, selected, deselected):\n        self.receive_tab.update_current_request()\n\n    def item_changed(self, idx: Optional[QModelIndex]):\n        if idx is None:\n            self.receive_tab.update_current_request()\n            return\n        if not idx.isValid():\n            return\n        item = self.item_from_index(idx.siblingAtColumn(self.Columns.DATE))\n        key = item.data(ROLE_KEY)\n        req = self.wallet.get_request(key)\n        if req is None:\n            self.update()\n        self.receive_tab.update_current_request()\n\n    def clearSelection(self):\n        super().clearSelection()\n        self.selectionModel().clearCurrentIndex()\n\n    def refresh_row(self, key, row):\n        assert row is not None\n        model = self.std_model\n        request = self.wallet.get_request(key)\n        if request is None:\n            return\n        status_item = model.item(row, self.Columns.STATUS)\n        status = self.wallet.get_invoice_status(request)\n        status_str = request.get_status_str(status)\n        status_item.setText(status_str)\n        status_item.setIcon(read_QIcon(pr_icons.get(status)))\n\n    def update(self):\n        current_key = self.get_current_key()\n        # not calling maybe_defer_update() as it interferes with conditional-visibility\n        self.proxy.setDynamicSortFilter(False)  # temp. disable re-sorting after every change\n        self.std_model.clear()\n        self.update_headers(self.__class__.headers)\n        self.set_visibility_of_columns()\n        for req in self.wallet.get_unpaid_requests():\n            key = req.get_id()\n            status = self.wallet.get_invoice_status(req)\n            status_str = req.get_status_str(status)\n            timestamp = req.get_time()\n            amount = req.get_amount_sat()\n            message = req.get_message()\n            date = format_time(timestamp)\n            amount_str = self.main_window.format_amount(amount) if amount else \"\"\n            amount_str_nots = self.main_window.format_amount(amount, add_thousands_sep=False) if amount else \"\"\n            labels = [\"\"] * len(self.Columns)\n            labels[self.Columns.DATE] = date\n            labels[self.Columns.DESCRIPTION] = message\n            labels[self.Columns.AMOUNT] = amount_str\n            labels[self.Columns.STATUS] = status_str\n            labels[self.Columns.ADDRESS] = req.get_address() or \"\"\n            labels[self.Columns.LN_RHASH] = req.rhash if req.is_lightning() else \"\"\n            items = [QStandardItem(e) for e in labels]\n            self.set_editability(items)\n            #items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)\n            items[self.Columns.DATE].setData(key, ROLE_KEY)\n            items[self.Columns.DATE].setData(timestamp, ROLE_SORT_ORDER)\n            items[self.Columns.DATE].setIcon(read_QIcon(\"lightning\" if req.is_lightning() else \"bitcoin\"))\n            items[self.Columns.AMOUNT].setData(amount_str_nots.strip(), self.ROLE_CLIPBOARD_DATA)\n            items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))\n            self.std_model.insertRow(self.std_model.rowCount(), items)\n        self.filter()\n        self.proxy.setDynamicSortFilter(True)\n        # sort requests by date\n        self.sortByColumn(self.Columns.DATE, Qt.SortOrder.DescendingOrder)\n        self.hide_if_empty()\n        if current_key is not None:\n            self.set_current_key(current_key)\n\n    def hide_if_empty(self):\n        b = self.std_model.rowCount() > 0\n        self.setVisible(b)\n        self.receive_tab.receive_requests_label.setVisible(b)\n        if not b:\n            # list got hidden, so selected item should also be cleared:\n            self.item_changed(None)\n\n    def create_menu(self, position):\n        items = self.selected_in_column(0)\n        if len(items) > 1:\n            keys = [item.data(ROLE_KEY) for item in items]\n            menu = QMenu(self)\n            menu.addAction(_(\"Delete requests\"), lambda: self.delete_requests(keys))\n            menu.exec(self.viewport().mapToGlobal(position))\n            return\n        idx = self.indexAt(position)\n        item = self.item_from_index(idx.siblingAtColumn(self.Columns.DATE))\n        if not item:\n            return\n        key = item.data(ROLE_KEY)\n        req = self.wallet.get_request(key)\n        if req is None:\n            self.update()\n            return\n        menu = QMenu(self)\n        copy_menu = self.add_copy_menu(menu, idx)\n        if req.get_address():\n            copy_menu.addAction(_(\"Address\"), lambda: self.main_window.do_copy(req.get_address(), title='Bitcoin Address'))\n        if URI := self.wallet.get_request_URI(req):\n            copy_menu.addAction(_(\"Bitcoin URI\"), lambda: self.main_window.do_copy(URI, title='Bitcoin URI'))\n        if req.is_lightning():\n            copy_menu.addAction(_(\"Lightning Request\"), lambda: self.main_window.do_copy(self.wallet.get_bolt11_invoice(req), title='Lightning Request'))\n        #if 'view_url' in req:\n        #    menu.addAction(_(\"View in web browser\"), lambda: webopen(req['view_url']))\n        menu.addAction(_(\"Delete\"), lambda: self.delete_requests([key]))\n        run_hook('receive_list_menu', self.main_window, menu, key)\n        self.open_menu(menu, position)\n\n    def delete_requests(self, keys):\n        self.wallet.delete_requests(keys)\n        self.update()\n        self.receive_tab.do_clear()\n\n    def delete_expired_requests(self):\n        keys = self.wallet.delete_expired_requests()\n        self.update()\n        self.receive_tab.do_clear()\n\n    def set_visibility_of_columns(self):\n        def set_visible(col: int, b: bool):\n            self.showColumn(col) if b else self.hideColumn(col)\n        set_visible(self.Columns.ADDRESS, False)\n        set_visible(self.Columns.LN_RHASH, False)\n"
  },
  {
    "path": "electrum/gui/qt/seed_dialog.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2013 ecdsa@github\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import Qt, pyqtSignal\nfrom PyQt6.QtGui import QPixmap\nfrom PyQt6.QtWidgets import (QVBoxLayout, QCheckBox, QHBoxLayout, QLineEdit,\n                             QLabel, QCompleter, QDialog, QStyledItemDelegate,\n                             QWidget, QPushButton)\n\nfrom electrum.i18n import _\nfrom electrum.mnemonic import Mnemonic, calc_seed_type, is_any_2fa_seed_type\nfrom electrum import old_mnemonic\nfrom electrum import slip39\nfrom electrum.util import ChoiceItem\n\nfrom .util import (\n    Buttons, OkButton, WWLabel, ButtonsTextEdit, icon_path, EnterButton,\n    CloseButton, WindowModalDialog, ColorScheme, font_height, ChoiceWidget,\n)\nfrom .qrtextedit import ShowQRTextEdit, ScanQRTextEdit\nfrom .completion_text_edit import CompletionTextEdit\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n\n\nMSG_PASSPHRASE_WARN_ISSUE4566 = _(\"Warning\") + \": \"\\\n                              + _(\"You have multiple consecutive whitespaces or leading/trailing \"\n                                  \"whitespaces in your passphrase.\") + \" \" \\\n                              + _(\"This is discouraged.\") + \" \" \\\n                              + _(\"Due to a bug, old versions of Electrum will NOT be creating the \"\n                                  \"same wallet as newer versions or other software.\")\n\n\ndef seed_warning_msg(seed):\n    return ''.join([\n        \"<p>\",\n        _(\"Please save these {0} words on paper (order is important). \"),\n        _(\"This seed will allow you to recover your wallet in case \"\n          \"of computer failure.\"),\n        \"</p>\",\n        \"<b>\" + _(\"WARNING\") + \":</b>\",\n        \"<ul>\",\n        \"<li>\" + _(\"Never disclose your seed.\") + \"</li>\",\n        \"<li>\" + _(\"Never type it on a website.\") + \"</li>\",\n        \"<li>\" + _(\"Do not store it electronically.\") + \"</li>\",\n        \"</ul>\"\n    ]).format(len(seed.split()))\n\n\nclass SeedWidget(QWidget):\n\n    updated = pyqtSignal()\n    validChanged = pyqtSignal([bool], arguments=['valid'])\n\n    def __init__(\n            self,\n            seed=None,\n            title=None,\n            icon=True,\n            msg=None,\n            options=None,\n            is_seed=None,  # only used for electrum seeds\n            passphrase=None,\n            parent=None,\n            for_seed_words=True,\n            *,\n            config: 'SimpleConfig',\n    ):\n        QWidget.__init__(self, parent)\n        vbox = QVBoxLayout()\n        self.setLayout(vbox)\n\n        self.options = options\n        self.config = config\n        self.msg = msg\n\n        if options:\n            self.seed_types = [\n                ChoiceItem(key=stype, label=label) for stype, label in (\n                    ('electrum', 'Electrum'),\n                    ('bip39', _('BIP39 seed')),\n                    ('slip39', _('SLIP39 seed')),\n                )\n                if stype in self.options\n            ]\n            assert len(self.seed_types)\n            self.seed_type = self.seed_types[0].key\n        else:\n            self.seed_type = 'electrum'\n\n        self.is_seed = is_seed\n\n        if title:\n            vbox.addWidget(WWLabel(title))\n        if seed:  # \"read only\", we already have the text\n            if for_seed_words:\n                self.seed_e = ButtonsTextEdit()\n            else:  # e.g. xpub\n                self.seed_e = ShowQRTextEdit(config=self.config)\n                self.seed_e.addCopyButton()\n            self.seed_e.setReadOnly(True)\n            self.seed_e.setText(seed)\n        else:  # we expect user to enter text\n            assert for_seed_words\n            self.seed_e = CompletionTextEdit()\n            self.seed_e.setTabChangesFocus(False)  # so that tab auto-completes\n            self.seed_e.textChanged.connect(self.on_edit)\n            self.initialize_completer()\n\n        self.seed_e.setMaximumHeight(max(75, 5 * font_height()))\n        hbox = QHBoxLayout()\n        if icon:\n            logo = QLabel()\n            logo.setPixmap(QPixmap(icon_path(\"seed.png\"))\n                           .scaledToWidth(64, mode=Qt.TransformationMode.SmoothTransformation))\n            logo.setMaximumWidth(60)\n            hbox.addWidget(logo)\n        hbox.addWidget(self.seed_e)\n        vbox.addLayout(hbox)\n        hbox = QHBoxLayout()\n        hbox.addStretch(1)\n        self.seed_type_label = QLabel('')\n        hbox.addWidget(self.seed_type_label)\n\n        # options\n        self.is_ext = False\n        if options:\n            opt_button = EnterButton(_('Options'), self.seed_options)\n            hbox.addWidget(opt_button)\n            vbox.addLayout(hbox)\n        if passphrase:\n            hbox = QHBoxLayout()\n            passphrase_e = QLineEdit()\n            passphrase_e.setText(passphrase)\n            passphrase_e.setReadOnly(True)\n            hbox.addWidget(QLabel(_(\"Your seed extension is\") + ':'))\n            hbox.addWidget(passphrase_e)\n            vbox.addLayout(hbox)\n\n        # slip39 shares\n        self.slip39_mnemonic_index = 0\n        self.slip39_mnemonics = [\"\"]\n        self.slip39_seed = None\n        self.slip39_current_mnemonic_invalid = None\n        hbox = QHBoxLayout()\n        hbox.addStretch(1)\n        self.prev_share_btn = QPushButton(_(\"Previous share\"))\n        self.prev_share_btn.clicked.connect(self.on_prev_share)\n        hbox.addWidget(self.prev_share_btn)\n        self.next_share_btn = QPushButton(_(\"Next share\"))\n        self.next_share_btn.clicked.connect(self.on_next_share)\n        hbox.addWidget(self.next_share_btn)\n        self.update_share_buttons()\n        vbox.addLayout(hbox)\n\n        vbox.addStretch(1)\n        self.seed_status = WWLabel('')\n        vbox.addWidget(self.seed_status)\n        self.seed_warning = WWLabel('')\n        if msg:\n            self.seed_warning.setText(seed_warning_msg(seed))\n        else:\n            self.update_seed_warning()\n\n        vbox.addWidget(self.seed_warning)\n\n    def seed_options(self):\n        dialog = QDialog()\n        dialog.setWindowTitle(_(\"Seed Options\"))\n        vbox = QVBoxLayout(dialog)\n\n        if 'ext' in self.options:\n            cb_ext = QCheckBox(_('Extend this seed with custom words'))\n            cb_ext.setChecked(self.is_ext)\n            vbox.addWidget(cb_ext)\n\n        def on_selected(idx):\n            self.seed_type = seed_type_choice.selected_key\n            self.slip39_current_mnemonic_invalid = None\n            self.seed_status.setText('')\n            self.update_seed_warning()\n            self.on_edit()\n            self.update_share_buttons()\n            self.initialize_completer()\n\n        if len(self.seed_types) > 1:\n            seed_type_choice = ChoiceWidget(message=_('Seed type'), choices=self.seed_types, default_key=self.seed_type)\n            seed_type_choice.itemSelected.connect(on_selected)\n            vbox.addWidget(seed_type_choice)\n\n        vbox.addLayout(Buttons(OkButton(dialog)))\n\n        if not dialog.exec():\n            return None\n\n        if 'ext' in self.options:\n            self.is_ext = cb_ext.isChecked()\n        if len(self.seed_types) > 1:\n            self.seed_type = seed_type_choice.selected_key\n\n        self.update_seed_warning()\n        self.updated.emit()\n\n    def update_seed_warning(self):\n        if self.msg:\n            return\n\n        if self.seed_type == 'bip39':\n            message = ' '.join([\n                '<b>' + _('Warning') + ':</b>  ',\n                _('BIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),\n                _('However, we do not generate BIP39 seeds, because they do not meet our safety standard.'),\n                _('BIP39 seeds do not include a version number, which compromises compatibility with future software.'),\n                _('We do not guarantee that BIP39 imports will always be supported in Electrum.'),\n            ])\n        elif self.seed_type == 'slip39':\n            message = ' '.join([\n                '<b>' + _('Warning') + ':</b>  ',\n                _('SLIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),\n                _('However, we do not generate SLIP39 seeds.'),\n            ])\n        else:\n            message = ''\n\n        self.seed_warning.setText(message)\n\n    def initialize_completer(self):\n        if self.seed_type != 'slip39':\n            bip39_english_list = Mnemonic('en').wordlist\n            old_list = old_mnemonic.wordlist\n            only_old_list = set(old_list) - set(bip39_english_list)\n            self.wordlist = list(bip39_english_list) + list(only_old_list)  # concat both lists\n            self.wordlist.sort()\n\n            class CompleterDelegate(QStyledItemDelegate):\n                def initStyleOption(self, option, index):\n                    super().initStyleOption(option, index)\n                    # Some people complained that due to merging the two word lists,\n                    # it is difficult to restore from a metal backup, as they planned\n                    # to rely on the \"4 letter prefixes are unique in bip39 word list\" property.\n                    # So we color words that are only in old list.\n                    if option.text in only_old_list:\n                        # yellow bg looks ~ok on both light/dark theme, regardless if (un)selected\n                        option.backgroundBrush = ColorScheme.YELLOW.as_color(background=True)\n\n            delegate = CompleterDelegate(self.seed_e)\n        else:\n            self.wordlist = list(slip39.get_wordlist())\n            delegate = None\n\n        self.completer = QCompleter(self.wordlist)\n        if delegate:\n            self.completer.popup().setItemDelegate(delegate)\n        self.seed_e.set_completer(self.completer)\n\n    def get_seed_words(self):\n        return self.seed_e.text().split()\n\n    def get_seed(self):\n        if self.seed_type != 'slip39':\n            return ' '.join(self.get_seed_words())\n        else:\n            return self.slip39_seed\n\n    def on_edit(self):\n        s = ' '.join(self.get_seed_words())\n        if self.seed_type == 'bip39':\n            from electrum.keystore import bip39_is_checksum_valid\n            is_checksum, is_wordlist = bip39_is_checksum_valid(s)\n            label = ''\n            valid = bool(s)\n            if valid:\n                label = ('' if is_checksum else _('BIP39 checksum failed')) if is_wordlist else _('Unknown BIP39 wordlist')\n        elif self.seed_type == 'slip39':\n            self.slip39_mnemonics[self.slip39_mnemonic_index] = s\n            try:\n                slip39.decode_mnemonic(s)\n            except slip39.Slip39Error as e:\n                share_status = str(e)\n                current_mnemonic_invalid = True\n            else:\n                share_status = _('Valid.')\n                current_mnemonic_invalid = False\n\n            label = _('SLIP39 share') + ' #%d: %s' % (self.slip39_mnemonic_index + 1, share_status)\n\n            # No need to process mnemonics if the current mnemonic remains invalid after editing.\n            if not (self.slip39_current_mnemonic_invalid and current_mnemonic_invalid):\n                self.slip39_seed, seed_status = slip39.process_mnemonics(self.slip39_mnemonics)\n                self.seed_status.setText(seed_status)\n            self.slip39_current_mnemonic_invalid = current_mnemonic_invalid\n\n            valid = self.slip39_seed is not None\n            self.update_share_buttons()\n        else:\n            valid = self.is_seed(s)\n            t = calc_seed_type(s)\n            label = _('Seed Type') + ': ' + t if t else ''\n            if t and not valid:  # electrum seed, but does not conform to dialog rules\n                wiztype_fullname = _('Wallet with two-factor authentication') if is_any_2fa_seed_type(t) else _(\"Standard wallet\")\n                msg = ' '.join([\n                    '<b>' + _('Warning') + ':</b>  ',\n                    _(\"Looks like you have entered a valid seed of type '{}' but this dialog does not support such seeds.\").format(t),\n                    _(\"If unsure, try restoring as '{}'.\").format(wiztype_fullname),\n                ])\n                self.seed_warning.setText(msg)\n            else:\n                self.seed_warning.setText(\"\")\n\n        self.seed_type_label.setText(label)\n        self.validChanged.emit(valid)\n\n        # disable suggestions if user already typed an unknown word\n        for word in self.get_seed_words()[:-1]:\n            if word not in self.wordlist:\n                self.seed_e.disable_suggestions()\n                return\n        self.seed_e.enable_suggestions()\n\n    def update_share_buttons(self):\n        if self.seed_type != 'slip39':\n            self.prev_share_btn.hide()\n            self.next_share_btn.hide()\n            return\n\n        finished = self.slip39_seed is not None\n        self.prev_share_btn.show()\n        self.next_share_btn.show()\n        self.prev_share_btn.setEnabled(self.slip39_mnemonic_index != 0)\n        self.next_share_btn.setEnabled(\n            # already pressed \"prev\" and undoing that:\n            self.slip39_mnemonic_index < len(self.slip39_mnemonics) - 1\n            # finished entering latest share and starting new one:\n            or (bool(self.seed_e.text().strip()) and not self.slip39_current_mnemonic_invalid and not finished)\n        )\n\n    def on_prev_share(self):\n        if not self.slip39_mnemonics[self.slip39_mnemonic_index]:\n            del self.slip39_mnemonics[self.slip39_mnemonic_index]\n\n        self.slip39_mnemonic_index -= 1\n        self.seed_e.setText(self.slip39_mnemonics[self.slip39_mnemonic_index])\n        self.slip39_current_mnemonic_invalid = None\n\n    def on_next_share(self):\n        if not self.slip39_mnemonics[self.slip39_mnemonic_index]:\n            del self.slip39_mnemonics[self.slip39_mnemonic_index]\n        else:\n            self.slip39_mnemonic_index += 1\n\n        if len(self.slip39_mnemonics) <= self.slip39_mnemonic_index:\n            self.slip39_mnemonics.append(\"\")\n            self.seed_e.setFocus()\n        self.seed_e.setText(self.slip39_mnemonics[self.slip39_mnemonic_index])\n        self.slip39_current_mnemonic_invalid = None\n\n\nclass KeysWidget(QWidget):\n\n    validChanged = pyqtSignal([bool], arguments=['valid'])\n\n    def __init__(\n            self,\n            parent=None,\n            header_layout=None,\n            is_valid=None,\n            allow_multi=False,\n            *,\n            config: 'SimpleConfig',\n    ):\n        QWidget.__init__(self, parent)\n        vbox = QVBoxLayout()\n        self.setLayout(vbox)\n\n        self.is_valid = is_valid\n        self.text_e = ScanQRTextEdit(allow_multi=allow_multi, config=config)\n        self.text_e.textChanged.connect(self.on_edit)\n        if isinstance(header_layout, str):\n            vbox.addWidget(WWLabel(header_layout))\n        else:\n            vbox.addLayout(header_layout)\n        vbox.addWidget(self.text_e)\n\n    def get_text(self):\n        return self.text_e.text()\n\n    def on_edit(self):\n        try:\n            valid = self.is_valid(self.get_text())\n        except Exception as e:\n            valid = False\n        self.validChanged.emit(valid)\n\n\nclass SeedDialog(WindowModalDialog):\n\n    def __init__(self, parent, seed, passphrase, *, config: 'SimpleConfig'):\n        WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed')))\n        self.setMinimumWidth(400)\n        vbox = QVBoxLayout(self)\n        title = _(\"Your wallet generation seed is:\")\n        seed_widget = SeedWidget(title=title, seed=seed, msg=True, passphrase=passphrase, config=config)\n        vbox.addWidget(seed_widget)\n        vbox.addLayout(Buttons(CloseButton(self)))\n"
  },
  {
    "path": "electrum/gui/qt/send_tab.py",
    "content": "# Copyright (C) 2022 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nfrom decimal import Decimal\nfrom typing import Optional, TYPE_CHECKING, Sequence, List, Callable, Union, Mapping\nimport urllib.parse\n\nfrom PyQt6.QtCore import pyqtSignal, QPoint, Qt\nfrom PyQt6.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QHBoxLayout,\n                             QWidget, QToolTip, QPushButton, QApplication)\n\nfrom electrum.i18n import _\nfrom electrum.logging import Logger\nfrom electrum.bitcoin import DummyAddress\nfrom electrum.plugin import run_hook\nfrom electrum.util import (\n    NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend, UserCancelled, ChoiceItem,\n    UserFacingException,\n)\nfrom electrum.lnutil import RECEIVED\nfrom electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST\nfrom electrum.transaction import Transaction, PartialTxInput, PartialTxOutput\nfrom electrum.network import TxBroadcastError, BestEffortRequestFailed\nfrom electrum.payment_identifier import (PaymentIdentifierType, PaymentIdentifier,\n                                         invoice_from_payment_identifier,\n                                         payment_identifier_from_invoice, PaymentIdentifierState)\nfrom electrum.submarine_swaps import SwapServerError\nfrom electrum.fee_policy import FeePolicy, FixedFeePolicy\nfrom electrum.lnurl import LNURL3Data, request_lnurl_withdraw_callback, LNURLError\n\nfrom .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit\nfrom .paytoedit import InvalidPaymentIdentifier\nfrom .util import (WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit,\n                   get_icon_camera, read_QIcon, ColorScheme, IconLabel, Spinner, Buttons, WWLabel,\n                   add_input_actions_to_context_menu, WindowModalDialog, OkButton, CancelButton)\nfrom .invoice_list import InvoiceList\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nclass SendTab(QWidget, MessageBoxMixin, Logger):\n\n    resolve_done_signal = pyqtSignal(object)\n    finalize_done_signal = pyqtSignal(object)\n    notify_merchant_done_signal = pyqtSignal(object)\n\n    def __init__(self, window: 'ElectrumWindow'):\n        QWidget.__init__(self, window)\n        Logger.__init__(self)\n        self.app = QApplication.instance()\n        self.window = window\n        self.wallet = window.wallet\n        self.fx = window.fx\n        self.config = window.config\n        self.network = window.network\n\n        self.format_amount_and_units = window.format_amount_and_units\n        self.format_amount = window.format_amount\n        self.base_unit = window.base_unit\n\n        self.pending_invoice = None\n\n        # A 4-column grid layout.  All the stretch is in the last column.\n        # The exchange rate plugin adds a fiat widget in column 2\n        self.send_grid = grid = QGridLayout()\n        grid.setSpacing(8)\n        grid.setColumnStretch(3, 1)\n\n        from .paytoedit import PayToEdit\n        self.amount_e = BTCAmountEdit(self.window.get_decimal_point)\n        self.payto_e = PayToEdit(self)\n        msg = (_(\"Recipient of the funds.\")\n               + \"\\n\\n\"\n               + _(\"This field can contain:\") + \"\\n\"\n               + _(\"- a Bitcoin address or BIP21 URI\") + \"\\n\"\n               + _(\"- a Lightning invoice\") + \"\\n\"\n               + _(\"- a label from your list of contacts\") + \"\\n\"\n               + _(\"- an openalias\") + \"\\n\"\n               + _(\"- an arbitrary on-chain script, e.g.:\") + \" script(OP_RETURN deadbeef)\" + \"\\n\"\n               + \"\\n\"\n               + _(\"You can also pay to many outputs in a single transaction, \"\n                   \"specifying one output per line.\") + \"\\n\" + _(\"Format: address, amount\") + \"\\n\"\n               + _(\"To set the amount to 'max', use the '!' special character.\") + \"\\n\"\n               + _(\"Integers weights can also be used in conjunction with '!', \"\n                   \"e.g. set one amount to '2!' and another to '3!' to split your coins 40-60.\"))\n        self.payto_label = HelpLabel(_('Pay to'), msg)\n        grid.addWidget(self.payto_label, 0, 0, Qt.AlignmentFlag.AlignLeft)\n        grid.addWidget(self.payto_e, 0, 1, 1, 4)\n\n        #completer = QCompleter()\n        #completer.setCaseSensitivity(False)\n        #self.payto_e.set_completer(completer)\n        #completer.setModel(self.window.completions)\n\n        msg = _('Description of the transaction (not mandatory).') + '\\n\\n' \\\n              + _(\n            'The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \\'History\\' tab.')\n        description_label = HelpLabel(_('Description'), msg)\n        grid.addWidget(description_label, 1, 0)\n        self.message_e = SizedFreezableLineEdit(width=600)\n        grid.addWidget(self.message_e, 1, 1, 1, 4)\n\n        msg = _('Comment for recipient')\n        self.comment_label = HelpLabel(_('Comment'), msg)\n        grid.addWidget(self.comment_label, 2, 0)\n        self.comment_e = SizedFreezableLineEdit(width=600)\n        grid.addWidget(self.comment_e, 2, 1, 1, 4)\n        self.comment_label.hide()\n        self.comment_e.hide()\n\n        msg = (_('The amount to be received by the recipient.') + ' '\n               + _('Fees are paid by the sender.') + '\\n\\n'\n               + _('Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') + '\\n\\n'\n               + _('Keyboard shortcut: type \"!\" to send all your coins.'))\n        amount_label = HelpLabel(_('Amount'), msg)\n        grid.addWidget(amount_label, 3, 0)\n\n        amount_widgets = QHBoxLayout()\n        amount_widgets.addWidget(self.amount_e)\n\n        self.fiat_send_e = AmountEdit(self.fx.get_currency if self.fx else '')\n        if not self.fx or not self.fx.is_enabled():\n            self.fiat_send_e.setVisible(False)\n        amount_widgets.addWidget(self.fiat_send_e)\n        self.amount_e.frozen.connect(\n            lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly()))\n\n        self.window.connect_fields(self.amount_e, self.fiat_send_e)\n\n        self.max_button = EnterButton(_(\"Max\"), self.spend_max)\n        btn_width = 10 * char_width_in_lineedit()\n        self.max_button.setFixedWidth(btn_width)\n        self.max_button.setCheckable(True)\n        self.max_button.setEnabled(False)\n        amount_widgets.addWidget(self.max_button)\n        amount_widgets.addStretch(1)\n        grid.addLayout(amount_widgets, 3, 1, 1, -1)\n\n        invoice_error_icon = read_QIcon(\"warning.png\")\n        self.invoice_error = IconLabel(reverse=True, hide_if_empty=True)\n        self.invoice_error.setIcon(invoice_error_icon)\n        grid.addWidget(self.invoice_error, 3, 4, Qt.AlignmentFlag.AlignRight)\n\n        self.paste_button = QPushButton(_('Paste'))\n        self.paste_button.clicked.connect(self.do_paste)\n        self.paste_button.setIcon(read_QIcon('copy.png'))\n        self.paste_button.setToolTip(_('Paste invoice from clipboard'))\n        self.paste_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n\n        self.spinner = Spinner()\n        grid.addWidget(self.spinner, 0, 1, 1, 4, Qt.AlignmentFlag.AlignRight)\n\n        self.save_button = EnterButton(_(\"Save\"), self.do_save_invoice)\n        self.save_button.setEnabled(False)\n        self.send_button = EnterButton(_(\"Pay\") + \"...\", self.do_pay_or_get_invoice)\n        self.send_button.setEnabled(False)\n        self.clear_button = EnterButton(_(\"Clear\"), self.do_clear)\n\n        #buttons1 = QHBoxLayout()\n        #buttons1.addWidget(self.paste_button)\n        #buttons1.addWidget(self.clear_button)\n        #buttons1.addStretch(1)\n        #grid.addLayout(buttons1, 0, 1, 1, 4)\n\n        buttons = QHBoxLayout()\n        buttons.addWidget(self.paste_button)\n        buttons.addWidget(self.clear_button)\n        buttons.addStretch(1)\n        buttons.addWidget(self.save_button)\n        buttons.addWidget(self.send_button)\n        grid.addLayout(buttons, 6, 1, 1, 4)\n\n        self.amount_e.shortcut.connect(self.spend_max)\n\n        def reset_max(text):\n            self.max_button.setChecked(False)\n\n        self.amount_e.textChanged.connect(self.on_amount_changed)\n        self.amount_e.textEdited.connect(reset_max)\n        self.fiat_send_e.textEdited.connect(reset_max)\n\n        self.invoices_label = QLabel(_('Invoices'))\n        self.invoice_list = InvoiceList(self)\n        self.toolbar, menu = self.invoice_list.create_toolbar_with_menu('')\n\n        add_input_actions_to_context_menu(self.payto_e, menu)\n        self.paytomany_menu = menu.addToggle(_(\"&Pay to many\"), self.toggle_paytomany)\n        menu.addSeparator()\n        menu.addAction(_(\"Import invoices\"), self.window.import_invoices)\n        menu.addAction(_(\"Export invoices\"), self.window.export_invoices)\n\n        vbox0 = QVBoxLayout()\n        vbox0.addLayout(grid)\n        hbox = QHBoxLayout()\n        hbox.addLayout(vbox0)\n        hbox.addStretch(1)\n\n        vbox = QVBoxLayout(self)\n        vbox.addLayout(self.toolbar)\n        vbox.addLayout(hbox)\n        vbox.addStretch(1)\n        vbox.addWidget(self.invoices_label)\n        vbox.addWidget(self.invoice_list)\n        vbox.setStretchFactor(self.invoice_list, 1000)\n        self.searchable_list = self.invoice_list\n        self.invoice_list.update()  # after parented and put into a layout, can update without flickering\n        run_hook('create_send_tab', grid)\n\n        self.resolve_done_signal.connect(self.on_resolve_done)\n        self.finalize_done_signal.connect(self.on_finalize_done)\n        self.notify_merchant_done_signal.connect(self.on_notify_merchant_done)\n        self.payto_e.paymentIdentifierChanged.connect(self._handle_payment_identifier)\n\n        self.setTabOrder(self.send_button, self.invoice_list)\n\n    def on_amount_changed(self, text):\n        # FIXME: implement full valid amount check to enable/disable Pay button\n        pi = self.payto_e.payment_identifier\n        if not pi:\n            self.send_button.setEnabled(False)\n            return\n        pi_error = pi.is_error() if pi.is_valid() else False\n        is_spk_script = pi.type == PaymentIdentifierType.SPK and not pi.spk_is_address\n        valid_amount = is_spk_script or bool(self.amount_e.get_amount())\n        ready_to_finalize = not pi.need_resolve()\n        self.send_button.setEnabled(pi.is_valid() and not pi_error and valid_amount and ready_to_finalize)\n\n    def do_paste(self):\n        self.logger.debug('do_paste')\n        try:\n            self.payto_e.try_payment_identifier(self.app.clipboard().text())\n        except InvalidPaymentIdentifier as e:\n            self.show_error(_('Invalid payment identifier on clipboard'))\n\n    def set_payment_identifier(self, text):\n        self.logger.debug('set_payment_identifier')\n        try:\n            self.payto_e.try_payment_identifier(text)\n        except InvalidPaymentIdentifier as e:\n            self.show_error(_('Invalid payment identifier'))\n\n    def spend_max(self):\n        pi = self.payto_e.payment_identifier\n\n        if pi is None or pi.type == PaymentIdentifierType.UNKNOWN:\n            return\n        elif pi.type not in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE,\n                           PaymentIdentifierType.BIP21, PaymentIdentifierType.OPENALIAS]:\n            # clear the amount field once it is clear this PI is not eligible for '!'\n            self.amount_e.clear()\n            return\n\n        if pi.type == PaymentIdentifierType.BIP21:\n            assert 'amount' not in pi.bip21\n\n        if run_hook('abort_send', self):\n            return\n        outputs = pi.get_onchain_outputs('!')\n        if not outputs:\n            return\n        make_tx = lambda fee_policy, *, confirmed_only=False: self.wallet.make_unsigned_transaction(\n            fee_policy=fee_policy,\n            coins=self.window.get_coins(),\n            outputs=outputs,\n            is_sweep=False)\n        try:\n            try:\n                tx = make_tx(FeePolicy(self.config.FEE_POLICY))\n            except (NotEnoughFunds, NoDynamicFeeEstimates) as e:\n                # Check if we had enough funds excluding fees,\n                # if so, still provide opportunity to set lower fees.\n                tx = make_tx(FixedFeePolicy(0))\n        except NotEnoughFunds as e:\n            self.max_button.setChecked(False)\n            text = self.wallet.get_text_not_enough_funds_mentioning_frozen(for_amount='!')\n            self.show_error(text)\n            return\n\n        self.max_button.setChecked(True)\n        amount = tx.output_value()\n        __, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)\n        amount_after_all_fees = amount - x_fee_amount\n        self.amount_e.setAmount(amount_after_all_fees)\n        # show tooltip explaining max amount\n        mining_fee = tx.get_fee()\n        mining_fee_str = self.format_amount_and_units(mining_fee)\n        msg = _(\"Mining fee: {} (can be adjusted on next screen)\").format(mining_fee_str)\n        if x_fee_amount:\n            twofactor_fee_str = self.format_amount_and_units(x_fee_amount)\n            msg += \"\\n\" + _(\"2fa fee: {} (for the next batch of transactions)\").format(twofactor_fee_str)\n        frozen_bal = self.wallet.get_frozen_balance_str()\n        if frozen_bal:\n            msg += \"\\n\" + _(\"Some coins are frozen: {} (can be unfrozen in the Addresses or in the Coins tab)\").format(frozen_bal)\n        QToolTip.showText(self.max_button.mapToGlobal(QPoint(0, 0)), msg)\n\n    # TODO: instead of passing outputs, use an invoice instead (like pay_lightning_invoice)\n    # so we have more context (we cannot rely on send_tab field contents or payment identifier\n    # as this method is called from other places as well).\n    def pay_onchain_dialog(\n            self,\n            outputs: List[PartialTxOutput],\n            *,\n            nonlocal_only=False,\n            external_keypairs: Mapping[bytes, bytes] = None,\n            get_coins: Callable[..., Sequence[PartialTxInput]] = None,\n            invoice: Optional[Invoice] = None\n    ) -> None:\n        # trustedcoin requires this\n        if run_hook('abort_send', self):\n            return\n\n        is_sweep = bool(external_keypairs)\n        # we call get_coins inside make_tx, so that inputs can be changed dynamically\n        if get_coins is None:\n            get_coins = self.window.get_coins\n\n        def make_tx(fee_policy, *, confirmed_only=False, base_tx=None):\n            coins = get_coins(nonlocal_only=nonlocal_only, confirmed_only=confirmed_only)\n            return self.wallet.make_unsigned_transaction(\n                fee_policy=fee_policy,\n                coins=coins,\n                outputs=outputs,\n                base_tx=base_tx,\n                is_sweep=is_sweep,\n                send_change_to_lightning=self.config.WALLET_SEND_CHANGE_TO_LIGHTNING,\n                merge_duplicate_outputs=self.config.WALLET_MERGE_DUPLICATE_OUTPUTS,\n            )\n        output_values = [x.value for x in outputs]\n        is_max = any(parse_max_spend(outval) for outval in output_values)\n        output_value = '!' if is_max else sum(output_values)\n\n        # To find batching candidates, we need to know our available UTXOs.\n        # Ideally should use same set of coins make_tx() will use.\n        # note: - prone to races: coins set might change due to new txs between now and make_tx() call\n        #       - make_tx() might pass different params to get_coins()\n        #         - to mitigate, we prefer to be more restrictive. hence confirmed_only=True\n        coins_conservative = get_coins(nonlocal_only=True, confirmed_only=True)\n        candidates = self.wallet.get_candidates_for_batching(outputs, coins=coins_conservative)\n\n        tx, is_preview, paid_with_swap = self.window.confirm_tx_dialog(\n            make_tx,\n            output_value,\n            payee_outputs=[o for o in outputs if not o.is_change],\n            batching_candidates=candidates,\n        )\n        if tx is None:\n            if paid_with_swap:\n                self.do_clear()\n            # user cancelled or paid with swap\n            return\n\n        if is_preview:\n            self.window.show_transaction(\n                tx,\n                external_keypairs=external_keypairs,\n                invoice=invoice,\n                show_sign_button=self.wallet.wallet_type != '2fa',\n                show_broadcast_button=self.wallet.wallet_type != '2fa',\n            )\n            return\n        self.save_pending_invoice()\n        def sign_done(success):\n            if success:\n                self.window.broadcast_or_show(tx, invoice=invoice)\n        self.window.sign_tx(\n            tx,\n            callback=sign_done,\n            external_keypairs=external_keypairs)\n\n    def do_clear(self):\n        self.logger.debug('do_clear')\n        self.lock_fields(lock_recipient=False, lock_amount=False, lock_max=True, lock_description=False)\n        self.max_button.setChecked(False)\n        self.payto_e.do_clear()\n        for w in [self.comment_e, self.comment_label]:\n            w.setVisible(False)\n        for w in [self.message_e, self.amount_e, self.fiat_send_e, self.comment_e]:\n            w.setText('')\n            w.setToolTip('')\n        for w in [self.save_button, self.send_button]:\n            w.setEnabled(False)\n        self.window.update_status()\n        self.paytomany_menu.setChecked(self.payto_e.multiline)\n        self.invoice_error.setText('')\n\n        run_hook('do_clear', self)\n\n    def prepare_for_send_tab_network_lookup(self):\n        for btn in [self.save_button, self.send_button, self.clear_button]:\n            btn.setEnabled(False)\n        self.spinner.setVisible(True)\n\n    def payment_request_error(self, error):\n        self.show_message(error)\n        self.do_clear()\n\n    def set_field_validated(self, w, *, validated: Optional[bool] = None):\n        if validated is not None:\n            w.setStyleSheet(ColorScheme.GREEN.as_stylesheet(True) if validated else ColorScheme.RED.as_stylesheet(True))\n\n    def lock_fields(\n            self, *,\n            lock_recipient: Optional[bool] = None,\n            lock_amount: Optional[bool] = None,\n            lock_max: Optional[bool] = None,\n            lock_description: Optional[bool] = None\n    ) -> None:\n        self.logger.debug(f'locking fields, r={lock_recipient}, a={lock_amount}, m={lock_max}, d={lock_description}')\n        if lock_recipient is not None:\n            self.payto_e.setFrozen(lock_recipient)\n        if lock_amount is not None:\n            self.amount_e.setFrozen(lock_amount)\n        if lock_max is not None:\n            self.max_button.setEnabled(not lock_max)\n            if lock_max is True:\n                self.max_button.setChecked(False)\n        if lock_description is not None:\n            self.message_e.setFrozen(lock_description)\n\n    def update_fields(self):\n        self.logger.debug('update_fields')\n        pi = self.payto_e.payment_identifier\n\n        self.clear_button.setEnabled(True)\n\n        if pi.is_multiline():\n            self.lock_fields(lock_recipient=False, lock_amount=True, lock_max=True, lock_description=False)\n            self.set_field_validated(self.payto_e, validated=pi.is_valid())  # TODO: validated used differently here than openalias\n            self.save_button.setEnabled(pi.is_valid())\n            self.send_button.setEnabled(pi.is_valid())\n            self.payto_e.setToolTip(pi.get_error() if not pi.is_valid() else '')\n            if pi.is_valid():\n                self.handle_multiline(pi.multiline_outputs)\n            return\n\n        if not pi.is_valid():\n            self.lock_fields(lock_recipient=False, lock_amount=False, lock_max=True, lock_description=False)\n            self.save_button.setEnabled(False)\n            self.send_button.setEnabled(False)\n            return\n\n        lock_recipient = pi.type in [PaymentIdentifierType.LNURL, PaymentIdentifierType.LNURLW,\n                                     PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR,\n                                     PaymentIdentifierType.OPENALIAS, PaymentIdentifierType.BIP70,\n                                     PaymentIdentifierType.BIP21, PaymentIdentifierType.BOLT11] and not pi.need_resolve()\n        lock_amount = pi.is_amount_locked()\n        lock_max = lock_amount or pi.type not in [PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21]\n\n        self.lock_fields(lock_recipient=lock_recipient,\n                         lock_amount=lock_amount,\n                         lock_max=lock_max,\n                         lock_description=False)\n        if lock_recipient:\n            fields = pi.get_fields_for_GUI()\n            if fields.recipient:\n                self.payto_e.setText(fields.recipient)\n            if fields.description:\n                self.message_e.setText(fields.description)\n                self.lock_fields(lock_description=True)\n            if fields.amount:\n                self.amount_e.setAmount(fields.amount)\n            for w in [self.comment_e, self.comment_label]:\n                w.setVisible(bool(fields.comment))\n            if fields.comment:\n                self.comment_e.setToolTip(_('Max comment length: {} characters').format(fields.comment))\n            self.set_field_validated(self.payto_e, validated=fields.validated)\n\n            # LNURLp amount range\n            if fields.amount_range:\n                amin, amax = fields.amount_range\n                self.amount_e.setToolTip(_('Amount must be between {} and {} sat.').format(amin, amax))\n            else:\n                self.amount_e.setToolTip('')\n\n        # resolve '!' in amount editor if it was set before PI\n        if not lock_max and self.amount_e.text() == '!':\n            self.spend_max()\n        elif lock_max and self.amount_e.text() == '!':\n            self.amount_e.clear()\n\n        pi_unusable = pi.is_error() or (not self.wallet.has_lightning() and not pi.is_onchain())\n        is_spk_script = pi.type == PaymentIdentifierType.SPK and not pi.spk_is_address\n\n        amount_valid = is_spk_script or bool(self.amount_e.get_amount())\n\n        self.send_button.setEnabled(not pi_unusable and amount_valid and not pi.has_expired())\n        self.save_button.setEnabled(not pi_unusable and not is_spk_script and not pi.has_expired() and \\\n                                    pi.type not in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR])\n\n        self.invoice_error.setText(_('Expired') if pi.has_expired() else '')\n\n    def _handle_payment_identifier(self):\n        self.update_fields()\n\n        if not self.payto_e.payment_identifier.is_valid():\n            self.logger.debug(f'PI error: {self.payto_e.payment_identifier.error}')\n            return\n\n        if self.payto_e.payment_identifier.need_resolve():\n            self.prepare_for_send_tab_network_lookup()\n            self.payto_e.payment_identifier.resolve(on_finished=self.resolve_done_signal.emit)\n\n    def on_resolve_done(self, pi: 'PaymentIdentifier'):\n        # TODO: resolve can happen while typing, we don't want message dialogs to pop up\n        # currently we don't set error for emaillike recipients to avoid just that\n        self.logger.debug('payment identifier resolve done')\n        self.spinner.setVisible(False)\n        if pi.error:\n            self.show_error(pi.error)\n            self.do_clear()\n            return\n        if pi.type == PaymentIdentifierType.LNURLW:\n            assert pi.state == PaymentIdentifierState.LNURLW_FINALIZE, \\\n                f\"Detected LNURLW but not ready to finalize? {pi=}\"\n            self.do_clear()\n            self.request_lnurl_withdraw_dialog(pi.lnurl_data)\n            return\n\n        # if openalias add openalias to contacts\n        if pi.type == PaymentIdentifierType.OPENALIAS:\n            key = pi.emaillike if pi.emaillike else pi.domainlike\n            pi.contacts[key] = ('openalias', pi.openalias_data.get('name'))\n\n        self.update_fields()\n\n    def get_message(self):\n        return self.message_e.text()\n\n    def read_invoice(self) -> Optional[Invoice]:\n        if self.check_payto_line_and_show_errors():\n            return\n\n        amount_sat = self.read_amount()\n        invoice = invoice_from_payment_identifier(\n            self.payto_e.payment_identifier, self.wallet, amount_sat, self.get_message())\n        if not invoice:\n            self.show_error('error getting invoice' + self.payto_e.payment_identifier.error)\n            return\n\n        if not self.wallet.has_lightning() and not invoice.can_be_paid_onchain():\n            self.show_error(_('Lightning is disabled'))\n        if self.wallet.get_invoice_status(invoice) == PR_PAID:\n            # fixme: this is only for bip70 and lightning\n            self.show_error(_('Invoice already paid'))\n            return\n        #if not invoice.is_lightning():\n        #    if self.check_onchain_outputs_and_show_errors(outputs):\n        #        return\n        return invoice\n\n    def do_save_invoice(self):\n        self.pending_invoice = self.read_invoice()\n        if not self.pending_invoice:\n            return\n        self.save_pending_invoice()\n\n    def save_pending_invoice(self):\n        if not self.pending_invoice:\n            return\n        self.do_clear()\n        self.wallet.save_invoice(self.pending_invoice)\n        self.invoice_list.update()\n        self.pending_invoice = None\n\n    def get_amount(self) -> int:\n        # must not be None\n        return self.amount_e.get_amount() or 0\n\n    def on_finalize_done(self, pi: PaymentIdentifier):\n        self.spinner.setVisible(False)\n        self.update_fields()\n        if pi.error:\n            self.show_error(pi.error)\n            return\n        invoice = pi.bolt11\n        self.pending_invoice = invoice\n        self.logger.debug(f'after finalize invoice: {invoice!r}')\n        self.do_pay_invoice(invoice)\n\n    def do_pay_or_get_invoice(self):\n        pi = self.payto_e.payment_identifier\n        if pi.need_finalize():\n            self.prepare_for_send_tab_network_lookup()\n            pi.finalize(amount_sat=self.get_amount(), comment=self.comment_e.text(),\n                        on_finished=self.finalize_done_signal.emit)\n            return\n        self.pending_invoice = self.read_invoice()\n        if not self.pending_invoice:\n            return\n        self.do_pay_invoice(self.pending_invoice)\n\n    def pay_multiple_invoices(self, invoices):\n        outputs = []\n        for invoice in invoices:\n            outputs += invoice.outputs\n        self.pay_onchain_dialog(outputs)\n\n    def do_edit_invoice(self, invoice: 'Invoice'):  # FIXME broken\n        assert not bool(invoice.get_amount_sat())\n        text = invoice.lightning_invoice if invoice.is_lightning() else invoice.get_address()\n        self.set_payment_identifier(text)\n        self.amount_e.setFocus()\n        # disable save button, because it would create a new invoice\n        self.save_button.setEnabled(False)\n\n    def do_pay_invoice(self, invoice: 'Invoice'):\n        if not bool(invoice.get_amount_sat()):\n            pi = self.payto_e.payment_identifier\n            if pi.type == PaymentIdentifierType.SPK and not pi.spk_is_address:\n                pass\n            else:\n                self.show_error(_('No amount'))\n                return\n        if invoice.is_lightning():\n            self.pay_lightning_invoice(invoice)\n        else:\n            self.pay_onchain_dialog(invoice.outputs, invoice=invoice)\n\n    def read_amount(self) -> Union[int, str]:\n        amount = '!' if self.max_button.isChecked() else self.get_amount()\n        return amount\n\n    def check_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool:\n        \"\"\"Returns whether there are errors with outputs.\n        Also shows error dialog to user if so.\n        \"\"\"\n        if not outputs:\n            self.show_error(_('No outputs'))\n            return True\n\n        for o in outputs:\n            if o.scriptpubkey is None:\n                self.show_error(_('Bitcoin Address is None'))\n                return True\n            if o.value is None:\n                self.show_error(_('Invalid Amount'))\n                return True\n\n        return False  # no errors\n\n    def check_payto_line_and_show_errors(self) -> bool:\n        \"\"\"Returns whether there are errors.\n        Also shows error dialog to user if so.\n        \"\"\"\n        error = self.payto_e.payment_identifier.get_error()\n        if error:\n            if not self.payto_e.payment_identifier.is_multiline():\n                err = error\n                self.show_warning(\n                    _(\"Failed to parse 'Pay to' line\") + \":\\n\" +\n                    f\"{err.line_content[:40]}...\\n\\n\"\n                    f\"{err.exc!r}\")\n            else:\n                self.show_warning(\n                    _(\"Invalid Lines found:\") + \"\\n\\n\" + error)\n                #'\\n'.join([_(\"Line #\") +\n                #               f\"{err.idx+1}: {err.line_content[:40]}... ({err.exc!r})\"\n                #               for err in errors]))\n            return True\n\n        warning = self.payto_e.payment_identifier.warning\n        if warning:\n            warning += '\\n' + _('Do you wish to continue?')\n            if not self.question(warning):\n                return True\n\n        if self.payto_e.payment_identifier.has_expired():\n            self.show_error(_('Payment request has expired'))\n            return True\n\n        return False  # no errors\n\n    def pay_lightning_invoice(self, invoice: Invoice):\n        amount_sat = invoice.get_amount_sat()\n        if amount_sat is None:\n            raise Exception(\"missing amount for LN invoice\")\n        # note: lnworker might be None if LN is disabled,\n        #       in which case we should still offer the user to pay onchain.\n        lnworker = self.wallet.lnworker\n        if lnworker is None or not lnworker.can_pay_invoice(invoice):\n            coins = self.window.get_coins(nonlocal_only=True)\n            can_pay_with_new_channel = False\n            can_pay_with_swap = False\n            can_rebalance = False\n            if lnworker:\n                can_pay_with_new_channel = lnworker.suggest_funding_amount(amount_sat, coins=coins)\n                can_pay_with_swap = lnworker.suggest_swap_to_send(amount_sat, coins=coins)\n                rebalance_suggestion = lnworker.suggest_rebalance_to_send(amount_sat)\n                can_rebalance = bool(rebalance_suggestion) and self.window.num_tasks() == 0\n            choices = []  # type: List[ChoiceItem]\n            if can_rebalance:\n                msg = ''.join([\n                    _('Rebalance existing channels'), '\\n',\n                    _('Move funds between your channels in order to increase your sending capacity.')\n                ])\n                choices.append(ChoiceItem(key='rebalance', label=msg))\n            if can_pay_with_new_channel:\n                msg = ''.join([\n                    _('Open a new channel'), '\\n',\n                    _('You will be able to pay once the channel is open.')\n                ])\n                choices.append(ChoiceItem(key='new_channel', label=msg))\n            if can_pay_with_swap:\n                msg = ''.join([\n                    _('Swap onchain funds for lightning funds'), '\\n',\n                    _('You will be able to pay once the swap is confirmed.')\n                ])\n                choices.append(ChoiceItem(key='swap', label=msg))\n            msg = _('You cannot pay that invoice using Lightning.')\n            if lnworker and lnworker.channels:\n                num_sats_can_send = int(lnworker.num_sats_can_send())\n                msg += '\\n' + _('Your channels can send {}.').format(self.format_amount(num_sats_can_send) + ' ' + self.base_unit())\n            if not choices:\n                self.window.show_error(msg)\n                return\n            r = self.window.query_choice(msg, choices)\n            if r is not None:\n                self.save_pending_invoice()\n                if r == 'rebalance':\n                    chan1, chan2, delta = rebalance_suggestion\n                    self.window.rebalance_dialog(chan1, chan2, amount_sat=delta)\n                elif r == 'new_channel':\n                    amount_sat, min_amount_sat = can_pay_with_new_channel\n                    self.window.new_channel_dialog(amount_sat=amount_sat, min_amount_sat=min_amount_sat)\n                elif r == 'swap':\n                    chan, swap_recv_amount_sat = can_pay_with_swap\n                    self.window.run_swap_dialog(is_reverse=False, recv_amount_sat_or_max=swap_recv_amount_sat, channels=[chan])\n                elif r == 'onchain':\n                    self.pay_onchain_dialog(invoice.get_outputs(), nonlocal_only=True, invoice=invoice)\n            return\n\n        assert lnworker is not None\n        # FIXME this is currently lying to user as we truncate to satoshis\n        amount_msat = invoice.get_amount_msat()\n        msg = _(\"Pay lightning invoice?\") + '\\n\\n' + _(\"This will send {}?\").format(self.format_amount_and_units(Decimal(amount_msat)/1000))\n        if not self.question(msg):\n            return\n        self.save_pending_invoice()\n        coro = lnworker.pay_invoice(invoice, amount_msat=amount_msat)\n        self.window.run_coroutine_from_thread(coro, _('Sending payment'))\n\n    def broadcast_transaction(self, tx: Transaction, *, invoice: Invoice = None):\n        if hasattr(tx, 'swap_payment_hash'):\n            sm = self.wallet.lnworker.swap_manager\n            swap = sm.get_swap(tx.swap_payment_hash)\n            with sm.create_transport() as transport:\n                coro = sm.wait_for_htlcs_and_broadcast(\n                    transport=transport, swap=swap, invoice=tx.swap_invoice, tx=tx)\n                try:\n                    funding_txid = self.window.run_coroutine_dialog(coro, _('Awaiting lightning payment...'))\n                except UserCancelled:\n                    sm.cancel_normal_swap(swap)\n                    return\n                self.window.on_swap_result(funding_txid, is_reverse=False)\n\n        def broadcast_thread():\n            # non-GUI thread\n            if invoice and invoice.has_expired():\n                return False, _(\"Invoice has expired\")\n            try:\n                self.network.run_from_another_thread(self.network.broadcast_transaction(tx))\n            except TxBroadcastError as e:\n                return False, e.get_message_for_gui()\n            except BestEffortRequestFailed as e:\n                return False, repr(e)\n            # success\n            if invoice and invoice.bip70:\n                payment_identifier = payment_identifier_from_invoice(invoice)\n                # FIXME: this should move to backend\n                if payment_identifier and payment_identifier.need_merchant_notify():\n                    refund_address = self.wallet.get_receiving_address()\n                    payment_identifier.notify_merchant(\n                        tx=tx,\n                        refund_address=refund_address,\n                        on_finished=self.notify_merchant_done_signal.emit\n                    )\n            return True, tx.txid()\n\n        # Capture current TL window; override might be removed on return\n        parent = self.window.top_level_window(lambda win: isinstance(win, MessageBoxMixin))\n\n        # FIXME: move to backend and let Abstract_Wallet set broadcasting state, not gui\n        self.wallet.set_broadcasting(tx, broadcasting_status=PR_BROADCASTING)\n\n        def broadcast_done(result):\n            # GUI thread\n            if result:\n                success, msg = result\n                if success:\n                    parent.show_message(_('Payment sent.') + '\\n' + msg)\n                    self.invoice_list.update()\n                    self.wallet.set_broadcasting(tx, broadcasting_status=PR_BROADCAST)\n                else:\n                    msg = msg or ''\n                    parent.show_error(msg)\n                    self.wallet.set_broadcasting(tx, broadcasting_status=None)\n\n        WaitingDialog(self, _('Broadcasting transaction...'),\n                      broadcast_thread, broadcast_done, self.window.on_error)\n\n    def on_notify_merchant_done(self, pi: PaymentIdentifier):\n        if pi.is_error():\n            self.logger.debug(f'merchant notify error: {pi.get_error()}')\n        else:\n            self.logger.debug(f'merchant notify result: {pi.merchant_ack_status}: {pi.merchant_ack_message}')\n        # TODO: show user? if we broadcasted the tx successfully, do we care?\n        # BitPay complains with a NAK if tx is RbF\n\n    def toggle_paytomany(self):\n        self.payto_e.toggle_paytomany()\n        if self.payto_e.is_paytomany():\n            message = '\\n'.join([\n                _('Enter a list of outputs in the \\'Pay to\\' field.'),\n                _('One output per line.'),\n                _('Format: address, amount'),\n                _('You may load a CSV file using the file icon.')\n            ])\n            self.window.show_tooltip_after_delay(message)\n            self.payto_label.setAlignment(Qt.AlignmentFlag.AlignTop)\n            self.payto_label.setText(_('Pay to many'))\n        else:\n            self.payto_label.setAlignment(Qt.AlignmentFlag.AlignLeft)\n            self.payto_label.setText(_('Pay to'))\n\n    def payto_contacts(self, labels):\n        paytos = [self.window.get_contact_payto(label) for label in labels]\n        self.window.show_send_tab()\n        self.do_clear()\n        if len(paytos) == 1:\n            self.logger.debug('payto_e setText 1')\n            self.payto_e.setText(paytos[0])\n            self.amount_e.setFocus()\n        else:\n            self.payto_e.setFocus()\n            text = \"\\n\".join([payto + \", 0\" for payto in paytos])\n            self.logger.debug('payto_e setText n')\n            self.payto_e.setText(text)\n            self.payto_e.setFocus()\n\n    def handle_multiline(self, outputs):\n        total = 0\n        for output in outputs:\n            if parse_max_spend(output.value):\n                self.max_button.setChecked(True)  # TODO: remove and let spend_max set this?\n                self.spend_max()\n                return\n            else:\n                total += output.value\n        self.amount_e.setAmount(total if outputs else None)\n\n    def request_lnurl_withdraw_dialog(self, lnurl_data: LNURL3Data):\n        if not self.wallet.has_lightning():\n            self.show_error(\n                _(\"Cannot request lightning withdrawal, wallet has no lightning channels.\")\n            )\n            return\n\n        dialog = WindowModalDialog(self, _(\"Lightning Withdrawal\"))\n        dialog.setMinimumWidth(400)\n\n        vbox = QVBoxLayout()\n        dialog.setLayout(vbox)\n        grid = QGridLayout()\n        grid.setSpacing(8)\n        grid.setColumnStretch(3, 1)  # Make the last column stretch\n\n        row = 0\n\n        # provider url\n        domain_label = QLabel(_(\"Provider\") + \":\")\n        domain_text = WWLabel(urllib.parse.urlparse(lnurl_data.callback_url).netloc)\n        grid.addWidget(domain_label, row, 0)\n        grid.addWidget(domain_text, row, 1, 1, 3)\n        row += 1\n\n        if lnurl_data.default_description:\n            desc_label = QLabel(_(\"Description\") + \":\")\n            desc_text = WWLabel(lnurl_data.default_description)\n            grid.addWidget(desc_label, row, 0)\n            grid.addWidget(desc_text, row, 1, 1, 3)\n            row += 1\n\n        min_amount = max(lnurl_data.min_withdrawable_sat, 1)\n        max_amount = min(\n            lnurl_data.max_withdrawable_sat,\n            int(self.wallet.lnworker.num_sats_can_receive())\n        )\n        min_text = self.format_amount_and_units(lnurl_data.min_withdrawable_sat)\n        if min_amount > int(self.wallet.lnworker.num_sats_can_receive()):\n            self.show_error(\"\".join([\n                _(\"Too little incoming liquidity to satisfy this withdrawal request.\"), \"\\n\\n\",\n                _(\"Can receive: {}\").format(\n                    self.format_amount_and_units(self.wallet.lnworker.num_sats_can_receive()),\n                ), \"\\n\",\n                _(\"Minimum withdrawal amount: {}\").format(min_text), \"\\n\\n\",\n                _(\"Do a submarine swap in the 'Channels' tab to get more incoming liquidity.\")\n            ]))\n            return\n\n        is_fixed_amount = lnurl_data.min_withdrawable_sat == lnurl_data.max_withdrawable_sat\n\n        # Range information (only for non-fixed amounts)\n        if not is_fixed_amount:\n            range_label_text = QLabel(_(\"Range\") + \":\")\n            range_value = QLabel(\"{} - {}\".format(\n                min_text,\n                self.format_amount_and_units(lnurl_data.max_withdrawable_sat)\n            ))\n            grid.addWidget(range_label_text, row, 0)\n            grid.addWidget(range_value, row, 1, 1, 2)\n            row += 1\n\n        # Amount section\n        amount_label = QLabel(_(\"Amount\") + \":\")\n        amount_edit = BTCAmountEdit(self.window.get_decimal_point, max_amount=max_amount)\n        amount_edit.setAmount(max_amount)\n        grid.addWidget(amount_label, row, 0)\n        grid.addWidget(amount_edit, row, 1)\n\n        if is_fixed_amount:\n            # Fixed amount, just show the amount\n            amount_edit.setDisabled(True)\n        else:\n            # Range, show max button\n            max_button = EnterButton(_(\"Max\"), lambda: amount_edit.setAmount(max_amount))\n            btn_width = 10 * char_width_in_lineedit()\n            max_button.setFixedWidth(btn_width)\n            grid.addWidget(max_button, row, 2)\n\n        row += 1\n\n        # Warning for insufficient liquidity\n        if lnurl_data.max_withdrawable_sat > int(self.wallet.lnworker.num_sats_can_receive()):\n            warning_text = WWLabel(\n                _(\"The maximum withdrawable amount is larger than what your channels can receive. \"\n                  \"You may need to do a submarine swap to increase your incoming liquidity.\")\n            )\n            warning_text.setStyleSheet(\"color: orange;\")\n            grid.addWidget(warning_text, row, 0, 1, 4)\n            row += 1\n\n        vbox.addLayout(grid)\n\n        # Buttons\n        request_button = OkButton(dialog, _(\"Request Withdrawal\"))\n        cancel_button = CancelButton(dialog)\n        vbox.addLayout(Buttons(cancel_button, request_button))\n\n        # Show dialog and handle result\n        if dialog.exec():\n            if is_fixed_amount:\n                amount_sat = lnurl_data.max_withdrawable_sat\n            else:\n                amount_sat = amount_edit.get_amount()\n                if not amount_sat or not (min_amount <= int(amount_sat) <= max_amount):\n                    self.show_error(_(\"Enter a valid amount. You entered: {}\").format(amount_sat))\n                    return\n        else:\n            return\n\n        try:\n            key = self.wallet.create_request(\n                amount_sat=amount_sat,\n                message=lnurl_data.default_description,\n                exp_delay=120,\n                address=None,\n            )\n            req = self.wallet.get_request(key)\n            info = self.wallet.lnworker.get_payment_info(req.payment_hash, direction=RECEIVED)\n            _lnaddr, b11_invoice = self.wallet.lnworker.get_bolt11_invoice(\n                payment_info=info,\n                message=req.get_message(),\n                fallback_address=None,\n            )\n        except Exception as e:\n            self.logger.exception('')\n            self.show_error(\n                f\"{_('Failed to create payment request for withdrawal')}: {str(e)}\"\n            )\n            return\n\n        coro = request_lnurl_withdraw_callback(\n            callback_url=lnurl_data.callback_url,\n            k1=lnurl_data.k1,\n            bolt_11=b11_invoice\n        )\n        try:\n            self.window.run_coroutine_dialog(coro, _(\"Requesting lightning withdrawal...\"))\n        except LNURLError as e:\n            self.show_error(f\"{_('Failed to request withdrawal')}:\\n{str(e)}\")\n        except UserCancelled:\n            pass\n"
  },
  {
    "path": "electrum/gui/qt/settings_dialog.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2012 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport ast\nimport sys\nfrom typing import TYPE_CHECKING, Dict\n\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtWidgets import (QComboBox,  QTabWidget, QDialog, QSpinBox,  QCheckBox, QLabel,\n                             QVBoxLayout, QGridLayout, QLineEdit, QWidget, QHBoxLayout, QSlider)\n\nfrom electrum.i18n import _, get_gui_lang_names\nfrom electrum import util\nfrom electrum.util import base_units_list, event_listener\n\nfrom electrum.gui.common_qt.util import QtEventListener\nfrom electrum.gui import messages\n\nfrom .util import ColorScheme, HelpLabel, Buttons, CloseButton\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig, ConfigVarWithConfig\n    from .main_window import ElectrumWindow\n\n\ndef checkbox_from_configvar(cv: 'ConfigVarWithConfig') -> QCheckBox:\n    short_desc = cv.get_short_desc()\n    assert short_desc is not None, f\"short_desc missing for {cv}\"\n    cb = QCheckBox(short_desc)\n    if (long_desc := cv.get_long_desc()) is not None:\n        cb.setToolTip(messages.to_rtf(long_desc))\n    return cb\n\n\nclass SettingsDialog(QDialog, QtEventListener):\n\n    def __init__(self, window: 'ElectrumWindow', config: 'SimpleConfig'):\n        QDialog.__init__(self)\n        self.setWindowTitle(_('Preferences'))\n        self.setMinimumWidth(500)\n        self.config = config\n        self.network = window.network\n        self.app = window.app\n        self.need_restart = False\n        self.fx = window.fx\n        self.wallet = window.wallet\n\n        self.register_callbacks()\n        self.app.alias_received_signal.connect(self.set_alias_color)\n\n        vbox = QVBoxLayout()\n        tabs = QTabWidget()\n\n        # language\n        lang_label = HelpLabel.from_configvar(self.config.cv.LOCALIZATION_LANGUAGE)\n        lang_combo = QComboBox()\n        _languages = get_gui_lang_names()\n        lang_combo.addItems(list(_languages.values()))\n        lang_keys = list(_languages.keys())\n        lang_cur_setting = self.config.LOCALIZATION_LANGUAGE\n        try:\n            index = lang_keys.index(lang_cur_setting)\n        except ValueError:  # not in list\n            index = 0\n        lang_combo.setCurrentIndex(index)\n        if not self.config.cv.LOCALIZATION_LANGUAGE.is_modifiable():\n            for w in [lang_combo, lang_label]: w.setEnabled(False)\n\n        def on_lang(x):\n            lang_request = list(_languages.keys())[lang_combo.currentIndex()]\n            if lang_request != self.config.LOCALIZATION_LANGUAGE:\n                self.config.LOCALIZATION_LANGUAGE = lang_request\n                self.need_restart = True\n        lang_combo.currentIndexChanged.connect(on_lang)\n\n        nz_label = HelpLabel.from_configvar(self.config.cv.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT)\n        nz = QSpinBox()\n        nz.setMinimum(0)\n        nz.setMaximum(self.config.BTC_AMOUNTS_DECIMAL_POINT)\n        nz.setValue(self.config.num_zeros)\n        if not self.config.cv.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT.is_modifiable():\n            for w in [nz, nz_label]: w.setEnabled(False)\n\n        def on_nz():\n            value = nz.value()\n            if self.config.num_zeros != value:\n                self.config.num_zeros = value\n                self.config.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = value\n                self.app.refresh_tabs_signal.emit()\n                self.app.update_status_signal.emit()\n        nz.valueChanged.connect(on_nz)\n\n        # lightning\n        trampoline_cb = checkbox_from_configvar(self.config.cv.LIGHTNING_USE_GOSSIP)\n        trampoline_cb.setChecked(not self.config.LIGHTNING_USE_GOSSIP)\n\n        def on_trampoline_checked(_x):\n            use_trampoline = trampoline_cb.isChecked()\n            if not use_trampoline:\n                if not window.question('\\n'.join([\n                        _(\"Are you sure you want to disable trampoline?\"),\n                        _(\"Without this option, Electrum will need to sync with the Lightning network on every start.\"),\n                        _(\"This may impact the reliability of your payments.\"),\n                ]), parent=self):\n                    trampoline_cb.setCheckState(Qt.CheckState.Checked)\n                    return\n            self.config.LIGHTNING_USE_GOSSIP = not use_trampoline\n            if not use_trampoline:\n                self.network.start_gossip()\n            else:\n                self.network.run_from_another_thread(\n                    self.network.stop_gossip())\n            util.trigger_callback('ln_gossip_sync_progress')\n            # FIXME: update all wallet windows\n            util.trigger_callback('channels_updated', self.wallet)\n        trampoline_cb.stateChanged.connect(on_trampoline_checked)\n\n        lnfee_hlabel = HelpLabel.from_configvar(self.config.cv.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS)\n        lnfee_map = [500, 1_000, 3_000, 5_000, 10_000, 20_000, 30_000, 50_000]\n\n        def lnfee_update_vlabel(fee_val: int):\n            lnfee_vlabel.setText(_(\"{}% of payment\").format(f\"{fee_val / 10 ** 4:.2f}\"))\n\n        def lnfee_slider_moved():\n            pos = lnfee_slider.sliderPosition()\n            fee_val = lnfee_map[pos]\n            lnfee_update_vlabel(fee_val)\n            self.config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS = fee_val\n\n        lnfee_slider = QSlider(Qt.Orientation.Horizontal)\n        lnfee_slider.setRange(0, len(lnfee_map)-1)\n        lnfee_slider.setTracking(True)\n        try:\n            lnfee_spos = lnfee_map.index(self.config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS)\n        except ValueError:\n            lnfee_spos = 0\n        lnfee_slider.setSliderPosition(lnfee_spos)\n        lnfee_vlabel = QLabel(\"\")\n        lnfee_update_vlabel(self.config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS)\n        lnfee_slider.valueChanged.connect(lnfee_slider_moved)\n        lnfee_hbox = QHBoxLayout()\n        lnfee_hbox.setContentsMargins(0, 0, 0, 0)\n        lnfee_hbox.addWidget(lnfee_vlabel)\n        lnfee_hbox.addWidget(lnfee_slider)\n        lnfee_hbox_w = QWidget()\n        lnfee_hbox_w.setLayout(lnfee_hbox)\n\n        alias_label = HelpLabel.from_configvar(self.config.cv.OPENALIAS_ID)\n        alias = self.config.OPENALIAS_ID\n        self.alias_e = QLineEdit(alias)\n        self.set_alias_color()\n        self.alias_e.editingFinished.connect(self.on_alias_edit)\n\n\n        msat_cb = checkbox_from_configvar(self.config.cv.BTC_AMOUNTS_PREC_POST_SAT)\n        msat_cb.setChecked(self.config.BTC_AMOUNTS_PREC_POST_SAT > 0)\n\n        def on_msat_checked(_x):\n            prec = 3 if msat_cb.isChecked() else 0\n            if self.config.amt_precision_post_satoshi != prec:\n                self.config.amt_precision_post_satoshi = prec\n                self.config.BTC_AMOUNTS_PREC_POST_SAT = prec\n                self.app.refresh_tabs_signal.emit()\n\n        msat_cb.stateChanged.connect(on_msat_checked)\n\n        # units\n        units = base_units_list\n        msg = (_('Base unit of your wallet.')\n               + '\\n1 BTC = 1000 mBTC. 1 mBTC = 1000 bits. 1 bit = 100 sat.\\n'\n               + _('This setting affects the Send tab, and all balance related fields.'))\n        unit_label = HelpLabel(_('Base unit') + ':', msg)\n        unit_combo = QComboBox()\n        unit_combo.addItems(units)\n        unit_combo.setCurrentIndex(units.index(self.config.get_base_unit()))\n\n        def on_unit(x, nz):\n            unit_result = units[unit_combo.currentIndex()]\n            if self.config.get_base_unit() == unit_result:\n                return\n            self.config.set_base_unit(unit_result)\n            nz.setMaximum(self.config.BTC_AMOUNTS_DECIMAL_POINT)\n            self.app.refresh_tabs_signal.emit()\n            self.app.update_status_signal.emit()\n            self.app.refresh_amount_edits_signal.emit()\n        unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz))\n\n        thousandsep_cb = checkbox_from_configvar(self.config.cv.BTC_AMOUNTS_ADD_THOUSANDS_SEP)\n        thousandsep_cb.setChecked(self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP)\n\n        def on_set_thousandsep(_x):\n            checked = thousandsep_cb.isChecked()\n            if self.config.amt_add_thousands_sep != checked:\n                self.config.amt_add_thousands_sep = checked\n                self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP = checked\n                self.app.refresh_tabs_signal.emit()\n        thousandsep_cb.stateChanged.connect(on_set_thousandsep)\n\n        qr_combo = QComboBox()\n        qr_combo.addItem(\"Default\", \"default\")\n        qr_label = HelpLabel.from_configvar(self.config.cv.VIDEO_DEVICE_PATH)\n        from .qrreader import find_system_cameras\n        system_cameras = find_system_cameras()\n        for cam_desc, cam_path in system_cameras.items():\n            qr_combo.addItem(cam_desc, cam_path)\n        index = qr_combo.findData(self.config.VIDEO_DEVICE_PATH)\n        qr_combo.setCurrentIndex(index)\n\n        def on_video_device(x):\n            self.config.VIDEO_DEVICE_PATH = qr_combo.itemData(x)\n        qr_combo.currentIndexChanged.connect(on_video_device)\n\n        colortheme_combo = QComboBox()\n        colortheme_combo.addItem(_('Light'), 'default')\n        colortheme_combo.addItem(_('Dark'), 'dark')\n        index = colortheme_combo.findData(self.config.GUI_QT_COLOR_THEME)\n        colortheme_combo.setCurrentIndex(index)\n        colortheme_label = QLabel(self.config.cv.GUI_QT_COLOR_THEME.get_short_desc() + ':')\n\n        def on_colortheme(x):\n            self.config.GUI_QT_COLOR_THEME = colortheme_combo.itemData(x)\n            self.need_restart = True\n        colortheme_combo.currentIndexChanged.connect(on_colortheme)\n\n        updatecheck_cb = checkbox_from_configvar(self.config.cv.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS)\n        updatecheck_cb.setChecked(self.config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS)\n\n        def on_set_updatecheck(_x):\n            self.config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = updatecheck_cb.isChecked()\n        updatecheck_cb.stateChanged.connect(on_set_updatecheck)\n\n        filelogging_cb = checkbox_from_configvar(self.config.cv.WRITE_LOGS_TO_DISK)\n        filelogging_cb.setChecked(self.config.WRITE_LOGS_TO_DISK)\n\n        def on_set_filelogging(_x):\n            self.config.WRITE_LOGS_TO_DISK = filelogging_cb.isChecked()\n            self.need_restart = True\n        filelogging_cb.stateChanged.connect(on_set_filelogging)\n\n        screenshot_protection_cb = checkbox_from_configvar(\n            self.config.cv.GUI_QT_SCREENSHOT_PROTECTION\n        )\n        screenshot_protection_cb.setChecked(self.config.GUI_QT_SCREENSHOT_PROTECTION)\n        if sys.platform not in ['windows', 'win32']:\n            screenshot_protection_cb.setChecked(False)\n            screenshot_protection_cb.setDisabled(True)\n            screenshot_protection_cb.setToolTip(_(\"This option is only available on Windows\"))\n\n        def on_set_screenshot_protection(_x):\n            self.config.GUI_QT_SCREENSHOT_PROTECTION = screenshot_protection_cb.isChecked()\n            self.need_restart = True\n        screenshot_protection_cb.stateChanged.connect(on_set_screenshot_protection)\n\n        block_explorers = sorted(util.block_explorer_info().keys())\n        BLOCK_EX_CUSTOM_ITEM = _(\"Custom URL\")\n        if BLOCK_EX_CUSTOM_ITEM in block_explorers:  # malicious translation?\n            block_explorers.remove(BLOCK_EX_CUSTOM_ITEM)\n        block_explorers.append(BLOCK_EX_CUSTOM_ITEM)\n        block_ex_label = HelpLabel.from_configvar(self.config.cv.BLOCK_EXPLORER)\n        block_ex_combo = QComboBox()\n        block_ex_custom_e = QLineEdit(str(self.config.BLOCK_EXPLORER_CUSTOM or ''))\n        block_ex_combo.addItems(block_explorers)\n        block_ex_combo.setCurrentIndex(\n            block_ex_combo.findText(util.block_explorer(self.config) or BLOCK_EX_CUSTOM_ITEM))\n\n        def showhide_block_ex_custom_e():\n            block_ex_custom_e.setVisible(block_ex_combo.currentText() == BLOCK_EX_CUSTOM_ITEM)\n        showhide_block_ex_custom_e()\n\n        def on_be_combo(x):\n            if block_ex_combo.currentText() == BLOCK_EX_CUSTOM_ITEM:\n                on_be_edit()\n            else:\n                be_result = block_explorers[block_ex_combo.currentIndex()]\n                self.config.BLOCK_EXPLORER_CUSTOM = None\n                self.config.BLOCK_EXPLORER = be_result\n            showhide_block_ex_custom_e()\n\n        block_ex_combo.currentIndexChanged.connect(on_be_combo)\n\n        def on_be_edit():\n            val = block_ex_custom_e.text()\n            try:\n                val = ast.literal_eval(val)  # to also accept tuples\n            except Exception:\n                pass\n            self.config.BLOCK_EXPLORER_CUSTOM = val\n\n        block_ex_custom_e.editingFinished.connect(on_be_edit)\n        block_ex_hbox = QHBoxLayout()\n        block_ex_hbox.setContentsMargins(0, 0, 0, 0)\n        block_ex_hbox.setSpacing(0)\n        block_ex_hbox.addWidget(block_ex_combo)\n        block_ex_hbox.addWidget(block_ex_custom_e)\n        block_ex_hbox_w = QWidget()\n        block_ex_hbox_w.setLayout(block_ex_hbox)\n\n        # Fiat Currency\n        self.history_rates_cb = checkbox_from_configvar(self.config.cv.FX_HISTORY_RATES)\n        ccy_combo = QComboBox()\n        ex_combo = QComboBox()\n\n        def update_currencies():\n            if not self.fx:\n                return\n            h = self.config.FX_HISTORY_RATES\n            currencies = sorted(self.fx.get_currencies(h))\n            ccy_combo.clear()\n            ccy_combo.addItems([_('None')] + currencies)\n            if self.fx.is_enabled():\n                ccy_combo.setCurrentIndex(ccy_combo.findText(self.fx.get_currency()))\n\n        def update_exchanges():\n            if not self.fx: return\n            b = self.fx.is_enabled()\n            ex_combo.setEnabled(b)\n            if b:\n                h = self.config.FX_HISTORY_RATES\n                c = self.fx.get_currency()\n                exchanges = self.fx.get_exchanges_by_ccy(c, h)\n            else:\n                exchanges = self.fx.get_exchanges_by_ccy('USD', False)\n            ex_combo.blockSignals(True)\n            ex_combo.clear()\n            ex_combo.addItems(sorted(exchanges))\n            ex_combo.setCurrentIndex(ex_combo.findText(self.fx.config_exchange()))\n            ex_combo.blockSignals(False)\n\n        def on_currency(hh):\n            if not self.fx: return\n            b = bool(ccy_combo.currentIndex())\n            ccy = str(ccy_combo.currentText()) if b else None\n            self.fx.set_enabled(b)\n            if b and ccy != self.fx.ccy:\n                self.fx.set_currency(ccy)\n            update_exchanges()\n            self.app.update_fiat_signal.emit()\n\n        def on_exchange(idx):\n            exchange = str(ex_combo.currentText())\n            if self.fx and self.fx.is_enabled() and exchange and exchange != self.fx.exchange.name():\n                self.fx.set_exchange(exchange)\n            self.app.update_fiat_signal.emit()\n\n        def on_history_rates(_x):\n            self.config.FX_HISTORY_RATES = self.history_rates_cb.isChecked()\n            if not self.fx:\n                return\n            update_exchanges()\n            window.app.update_fiat_signal.emit()\n\n        update_currencies()\n        update_exchanges()\n        ccy_combo.currentIndexChanged.connect(on_currency)\n        self.history_rates_cb.setChecked(self.config.FX_HISTORY_RATES)\n        self.history_rates_cb.stateChanged.connect(on_history_rates)\n        ex_combo.currentIndexChanged.connect(on_exchange)\n\n        gui_widgets = []\n        gui_widgets.append((lang_label, lang_combo))\n        gui_widgets.append((colortheme_label, colortheme_combo))\n        gui_widgets.append((block_ex_label, block_ex_hbox_w))\n        units_widgets = []\n        units_widgets.append((unit_label, unit_combo))\n        units_widgets.append((nz_label, nz))\n        units_widgets.append((msat_cb, None))\n        units_widgets.append((thousandsep_cb, None))\n        lightning_widgets = []\n        lightning_widgets.append((trampoline_cb, None))\n        lightning_widgets.append((lnfee_hlabel, lnfee_hbox_w))\n        fiat_widgets = []\n        fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo))\n        fiat_widgets.append((QLabel(_('Source')), ex_combo))\n        fiat_widgets.append((self.history_rates_cb, None))\n        misc_widgets = []\n        misc_widgets.append((updatecheck_cb, None))\n        misc_widgets.append((filelogging_cb, None))\n        misc_widgets.append((screenshot_protection_cb, None))\n        misc_widgets.append((alias_label, self.alias_e))\n        misc_widgets.append((qr_label, qr_combo))\n\n        tabs_info = [\n            (gui_widgets, _('Appearance')),\n            (units_widgets, _('Units')),\n            (fiat_widgets, _('Fiat')),\n            (lightning_widgets, _('Lightning')),\n            (misc_widgets, _('Misc')),\n        ]\n        for widgets, name in tabs_info:\n            tab = QWidget()\n            tab_vbox = QVBoxLayout(tab)\n            grid = QGridLayout()\n            for a,b in widgets:\n                i = grid.rowCount()\n                if b:\n                    if a:\n                        grid.addWidget(a, i, 0)\n                    grid.addWidget(b, i, 1)\n                else:\n                    grid.addWidget(a, i, 0, 1, 2)\n            tab_vbox.addLayout(grid)\n            tab_vbox.addStretch(1)\n            tabs.addTab(tab, name)\n\n        vbox.addWidget(tabs)\n        vbox.addStretch(1)\n        vbox.addLayout(Buttons(CloseButton(self)))\n        self.setLayout(vbox)\n\n    @event_listener\n    def on_event_alias_received(self):\n        self.app.alias_received_signal.emit()\n\n    def set_alias_color(self):\n        if not self.config.OPENALIAS_ID:\n            self.alias_e.setStyleSheet(\"\")\n            return\n        if self.wallet.contacts.alias_info:\n            self.alias_e.setStyleSheet(ColorScheme.GREEN.as_stylesheet(True))\n        else:\n            self.alias_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))\n\n    def on_alias_edit(self):\n        self.alias_e.setStyleSheet(\"\")\n        alias = str(self.alias_e.text())\n        self.config.OPENALIAS_ID = alias\n        if alias:\n            self.wallet.contacts.fetch_openalias(self.config)\n\n    def closeEvent(self, event):\n        self.unregister_callbacks()\n        try:\n            self.app.alias_received_signal.disconnect(self.set_alias_color)\n        except TypeError:\n            pass  # 'method' object is not connected\n        event.accept()\n"
  },
  {
    "path": "electrum/gui/qt/stylesheet_patcher.py",
    "content": "\"\"\"This is used to patch the QApplication style sheet.\nIt reads the current stylesheet, appends our modifications and sets the new stylesheet.\n\"\"\"\n\nimport sys\n\nfrom PyQt6 import QtWidgets\n\n\nCUSTOM_PATCH_FOR_DARK_THEME = '''\n/* PayToEdit text was being clipped */\nQAbstractScrollArea {\n    padding: 0px;\n}\n/* In History tab, labels while edited were being clipped (Windows) */\nQAbstractItemView QLineEdit {\n    padding: 0px;\n    show-decoration-selected: 1;\n}\n/* Checked item in dropdowns have way too much height...\n   see #6281 and https://github.com/ColinDuquesnoy/QDarkStyleSheet/issues/200\n   */\nQComboBox::item:checked {\n    font-weight: bold;\n    max-height: 30px;\n}\n'''\n\nCUSTOM_PATCH_FOR_DEFAULT_THEME_MACOS = '''\n/* On macOS, main window status bar icons have ugly frame (see #6300) */\nStatusBarButton {\n    background-color: transparent;\n    border: 1px solid transparent;\n    border-radius: 4px;\n    margin: 0px;\n    padding: 2px;\n}\nStatusBarButton:checked {\n  background-color: transparent;\n  border: 1px solid #1464A0;\n}\nStatusBarButton:checked:disabled {\n  border: 1px solid #14506E;\n}\nStatusBarButton:pressed {\n  margin: 1px;\n  background-color: transparent;\n  border: 1px solid #1464A0;\n}\nStatusBarButton:disabled {\n  border: none;\n}\nStatusBarButton:hover {\n  border: 1px solid #148CD2;\n}\n'''\n\n\ndef patch_qt_stylesheet(use_dark_theme: bool) -> None:\n    custom_patch = \"\"\n    if use_dark_theme:\n        custom_patch = CUSTOM_PATCH_FOR_DARK_THEME\n    else:  # default theme (typically light)\n        if sys.platform == 'darwin':\n            custom_patch = CUSTOM_PATCH_FOR_DEFAULT_THEME_MACOS\n\n    app = QtWidgets.QApplication.instance()\n    style_sheet = app.styleSheet() + custom_patch\n    app.setStyleSheet(style_sheet)\n"
  },
  {
    "path": "electrum/gui/qt/swap_dialog.py",
    "content": "import enum\nfrom typing import TYPE_CHECKING, Optional, Union, Tuple, Sequence, Callable\n\nfrom PyQt6.QtCore import pyqtSignal, Qt, QTimer\nfrom PyQt6.QtGui import QIcon, QPixmap, QColor\nfrom PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton\nfrom PyQt6.QtWidgets import QTreeWidget, QTreeWidgetItem, QHeaderView\n\nfrom electrum_aionostr.util import from_nip19\n\nfrom electrum.i18n import _\nfrom electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, UserCancelled, trigger_callback\nfrom electrum.bitcoin import DummyAddress\nfrom electrum.transaction import PartialTxOutput, PartialTransaction\nfrom electrum.fee_policy import FeePolicy\nfrom electrum.submarine_swaps import NostrTransport\n\nfrom electrum.gui.common_qt.util import QtEventListener, qt_event_listener\nfrom electrum.gui import messages\n\nfrom . import util\nfrom .util import (WindowModalDialog, Buttons, OkButton, CancelButton,\n                   EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel, char_width_in_lineedit,\n                   pubkey_to_q_icon)\nfrom .amountedit import BTCAmountEdit\nfrom .fee_slider import FeeSlider, FeeComboBox\nfrom .my_treeview import create_toolbar_with_menu, MyTreeView\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n    from electrum.submarine_swaps import SwapServerTransport, SwapOffer\n    from electrum.lnchannel import Channel\n    from electrum.simple_config import SimpleConfig\n\nCANNOT_RECEIVE_WARNING = _(\n\"\"\"The requested amount is higher than what you can receive in your currently open channels.\nIf you continue, your funds will be locked until the remote server can find a path to pay you.\nIf the swap cannot be performed after 24h, you will be refunded.\nDo you want to continue?\"\"\"\n)\n\n\nROLE_NPUB = Qt.ItemDataRole.UserRole + 1000\n\nclass InvalidSwapParameters(Exception): pass\n\n\nclass SwapProvidersButton(QPushButton):\n\n    def __init__(\n        self,\n        transport_getter: Callable[[], Optional['SwapServerTransport']],\n        config: 'SimpleConfig',\n        main_window: 'ElectrumWindow',\n    ):\n        \"\"\"parent must have a transport() method\"\"\"\n        QPushButton.__init__(self)\n        self.config = config\n        self.transport_getter = transport_getter\n        self.main_window = main_window\n        self.clicked.connect(self.choose_swap_server)\n        self.fetching = False\n        self.update()\n\n    def update(self):\n        if self.fetching:\n            self.setEnabled(False)\n            self.setText(_(\"Fetching...\"))\n            self.setVisible(True)\n            return\n\n        transport = self.transport_getter()\n        if not isinstance(transport, NostrTransport):\n            # HTTPTransport or no Network, not showing server selection button\n            self.setEnabled(False)\n            self.setVisible(False)\n            return\n        self.setEnabled(True)\n        self.setVisible(True)\n        offer_count = len(transport.get_recent_offers())\n        button_text = f' {offer_count} ' + (_('swap providers') if offer_count != 1 else _('swap provider'))\n        self.setText(button_text)\n        # update icon\n        if self.config.SWAPSERVER_NPUB:\n            pubkey = from_nip19(self.config.SWAPSERVER_NPUB)['object'].hex()\n            self.setIcon(pubkey_to_q_icon(pubkey))\n\n    def choose_swap_server(self) -> None:\n        transport = self.transport_getter()\n        assert isinstance(transport, NostrTransport), transport\n        self.main_window.choose_swapserver_dialog(transport)  # type: ignore\n        self.update()\n        trigger_callback('swap_provider_changed')\n\n\n\nclass SwapDialog(WindowModalDialog, QtEventListener):\n\n    def __init__(\n        self,\n        window: 'ElectrumWindow',\n        transport: 'SwapServerTransport',\n        is_reverse: Optional[bool] = None,\n        recv_amount_sat_or_max: Optional[Union[int, str]] = None,  # sat or '!'\n        channels: Optional[Sequence['Channel']] = None,\n    ):\n        WindowModalDialog.__init__(self, window, _('Submarine Swap'))\n        self.window = window\n        self.config = window.config\n        self.lnworker = self.window.wallet.lnworker\n        self.swap_manager = self.lnworker.swap_manager\n        self.network = window.network\n        self.channels = channels\n        self.is_reverse = is_reverse if is_reverse is not None else True\n        vbox = QVBoxLayout(self)\n\n        self.transport = transport\n        self.server_button = SwapProvidersButton(lambda: self.transport, self.config, self.window)\n        self.description_label = WWLabel(self.get_description())\n        self.send_amount_e = BTCAmountEdit(self.window.get_decimal_point)\n        self.recv_amount_e = BTCAmountEdit(self.window.get_decimal_point)\n        self.max_button = EnterButton(_(\"Max\"), self.spend_max)\n        btn_width = 10 * char_width_in_lineedit()\n        self.max_button.setFixedWidth(btn_width)\n        self.max_button.setCheckable(True)\n        self.toggle_button = QPushButton('  \\U000021c4  ')  # whitespace to force larger min width\n        self.toggle_button.setEnabled(is_reverse is None)\n        # send_follows is used to know whether the send amount field / receive\n        # amount field should be adjusted after the fee slider was moved\n        self.send_follows = False\n        self.send_amount_e.follows = False\n        self.recv_amount_e.follows = False\n        self.toggle_button.clicked.connect(self.toggle_direction)\n        # textChanged is triggered for both user and automatic action\n        self.send_amount_e.textChanged.connect(self.on_send_edited)\n        self.recv_amount_e.textChanged.connect(self.on_recv_edited)\n        # textEdited is triggered only for user editing of the fields\n        self.send_amount_e.textEdited.connect(self.uncheck_max)\n        self.recv_amount_e.textEdited.connect(self.uncheck_max)\n\n        self.fee_policy = FeePolicy(self.config.FEE_POLICY)\n        self.fee_slider = FeeSlider(parent=self, network=self.network, fee_policy=self.fee_policy, callback=self.fee_slider_callback)\n        self.fee_combo = FeeComboBox(self.fee_slider)\n        self.fee_target_label = QLabel()\n        self._set_fee_slider_visibility(is_visible=not self.is_reverse)\n\n        self.swap_limits_label = QLabel()\n        self.fee_label = QLabel()\n        self.server_fee_label = QLabel()\n        self.last_server_mining_fee_sat = None\n        h = QGridLayout()\n        h.addWidget(self.description_label, 0, 0, 1, 3)\n        h.addWidget(self.toggle_button, 0, 3)\n        self.send_label = IconLabel(text=_('You send')+':')\n        self.recv_label = IconLabel(text=_('You receive')+':')\n        h.addWidget(self.send_label, 1, 0)\n        h.addWidget(self.send_amount_e, 1, 1)\n        h.addWidget(self.max_button, 1, 2)\n        h.addWidget(self.recv_label, 2, 0)\n        h.addWidget(self.recv_amount_e, 2, 1)\n        h.addWidget(QLabel(_('Swap limits')+':'), 4, 0)\n        h.addWidget(self.swap_limits_label, 4, 1, 1, 2)\n        h.addWidget(QLabel(_('Server fee')+':'), 5, 0)\n        h.addWidget(self.server_fee_label, 5, 1, 1, 2)\n        h.addWidget(QLabel(_('Mining fee')+':'), 6, 0)\n        h.addWidget(self.fee_label, 6, 1, 1, 2)\n        h.addWidget(self.fee_slider, 7, 1)\n        h.addWidget(self.fee_combo, 7, 2)\n        h.addWidget(self.fee_target_label, 7, 0)\n        h.addWidget(QLabel(''), 8, 0)\n        vbox.addLayout(h)\n        vbox.addStretch()\n        self.ok_button = OkButton(self)\n        self.ok_button.setDefault(True)\n        self.ok_button.setEnabled(False)\n        buttons = Buttons(CancelButton(self), self.ok_button)\n        vbox.addLayout(buttons)\n        buttons.insertWidget(0, self.server_button)\n        if recv_amount_sat_or_max:\n            assert isinstance(recv_amount_sat_or_max, (int, str)), f\"invalid {type(recv_amount_sat_or_max)=}\"\n            self.init_recv_amount(recv_amount_sat_or_max)\n        self.update()\n        self.needs_tx_update = True\n\n        self.timer = QTimer(self)\n        self.timer.setInterval(500)\n        self.timer.setSingleShot(False)\n        self.timer.timeout.connect(self.timer_actions)\n        self.timer.start()\n\n        self.fee_slider.update()\n        self.register_callbacks()\n\n    def closeEvent(self, event):\n        self.unregister_callbacks()\n        event.accept()\n\n    @qt_event_listener\n    def on_event_fee_histogram(self, *args):\n        self.update_send_receive()\n\n    @qt_event_listener\n    def on_event_fee(self, *args):\n        self.update_send_receive()\n\n    @qt_event_listener\n    def on_event_swap_offers_changed(self, recent_offers: Sequence['SwapOffer']):\n        self.server_button.update()\n        if not self.ok_button.isEnabled():\n            # only update the dialog with the new offer if the user hasn't entered an amount yet.\n            # if the user has already entered an amount we prefer the swap to fail due to outdated\n            # fees than the possibility of a swap happening with fees the user hasn't seen\n            # due to an update happening just before the user initiated the swap\n            self.update()\n\n    @qt_event_listener\n    def on_event_swap_provider_changed(self):\n        self.update()\n        self.update_send_receive()\n\n    def timer_actions(self):\n        if self.needs_tx_update:\n            self.update_tx()\n            self.update_ok_button()\n            self.needs_tx_update = False\n\n    def init_recv_amount(self, recv_amount_sat):\n        if recv_amount_sat == '!':\n            self.max_button.setChecked(True)\n            self.spend_max()\n        else:\n            recv_amount_sat = max(recv_amount_sat, self.swap_manager.get_min_amount())\n            self.recv_amount_e.setAmount(recv_amount_sat)\n\n    def fee_slider_callback(self, fee_rate):\n        self.config.FEE_POLICY = self.fee_policy.get_descriptor()\n        if not self.is_reverse:\n            self.fee_target_label.setText(self.fee_policy.get_target_text())\n        self.update_send_receive()\n        self.update()\n\n    def _set_fee_slider_visibility(self, *, is_visible: bool):\n        if is_visible:\n            self.fee_slider.setEnabled(True)\n            self.fee_combo.setEnabled(True)\n            self.fee_target_label.setText(self.fee_policy.get_target_text())\n        else:\n            self.fee_slider.setEnabled(False)\n            self.fee_combo.setEnabled(False)\n            # show the eta of the swap claim\n            self.fee_target_label.setText(FeePolicy(self.config.FEE_POLICY_SWAPS).get_target_text())\n\n    def toggle_direction(self):\n        self.is_reverse = not self.is_reverse\n        self._set_fee_slider_visibility(is_visible=not self.is_reverse)\n        self.send_amount_e.setAmount(None)\n        self.recv_amount_e.setAmount(None)\n        self.max_button.setChecked(False)\n        self.update()\n\n    def spend_max(self):\n        if self.max_button.isChecked():\n            if self.is_reverse:\n                self._spend_max_reverse_swap()\n            else:\n                # spend_max_forward_swap will be called in update_tx\n                pass\n        else:\n            self.send_amount_e.setAmount(None)\n        self.needs_tx_update = True\n\n    def uncheck_max(self):\n        self.max_button.setChecked(False)\n        self.update()\n\n    def _spend_max_forward_swap(self, tx: Optional[PartialTransaction]) -> None:\n        if tx:\n            amount = tx.output_value_for_address(DummyAddress.SWAP)\n            self.send_amount_e.setAmount(amount)\n        else:\n            self.send_amount_e.setAmount(None)\n            self.max_button.setChecked(False)\n\n    def _spend_max_reverse_swap(self) -> None:\n        amount = min(self.lnworker.num_sats_can_send(), self.swap_manager.get_provider_max_forward_amount())\n        amount = int(amount)  # round down msats\n        self.send_amount_e.setAmount(amount)\n\n    def on_send_edited(self):\n        if self.send_amount_e.follows:\n            return\n        self.send_amount_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())\n        send_amount = self.send_amount_e.get_amount()\n        recv_amount = self.swap_manager.get_recv_amount(send_amount, is_reverse=self.is_reverse)\n        if self.is_reverse and send_amount and send_amount > self.lnworker.num_sats_can_send():\n            # cannot send this much on lightning\n            recv_amount = None\n        if (not self.is_reverse) and recv_amount and recv_amount > self.lnworker.num_sats_can_receive():\n            # cannot receive this much on lightning\n            recv_amount = None\n        self.recv_amount_e.follows = True\n        self.recv_amount_e.setAmount(recv_amount)\n        self.recv_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())\n        self.recv_amount_e.follows = False\n        self.send_follows = False\n        self.needs_tx_update = True\n\n    def on_recv_edited(self):\n        if self.recv_amount_e.follows:\n            return\n        self.recv_amount_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())\n        recv_amount = self.recv_amount_e.get_amount()\n        send_amount = self.swap_manager.get_send_amount(recv_amount, is_reverse=self.is_reverse)\n        if self.is_reverse and send_amount and send_amount > self.lnworker.num_sats_can_send():\n            send_amount = None\n        self.send_amount_e.follows = True\n        self.send_amount_e.setAmount(send_amount)\n        self.send_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())\n        self.send_amount_e.follows = False\n        self.send_follows = True\n        self.needs_tx_update = True\n\n    def update_send_receive(self):\n        self.on_recv_edited() if self.send_follows else self.on_send_edited()\n\n    def update(self):\n        sm = self.swap_manager\n        w_base_unit = self.window.base_unit()\n        send_icon = read_QIcon(\"lightning.png\" if self.is_reverse else \"bitcoin.png\")\n        self.send_label.setIcon(send_icon)\n        recv_icon = read_QIcon(\"lightning.png\" if not self.is_reverse else \"bitcoin.png\")\n        self.recv_label.setIcon(recv_icon)\n        self.description_label.setText(self.get_description())\n        self.description_label.repaint()  # macOS hack for #6269\n        min_swap_limit, max_swap_limit = self.get_client_swap_limits_sat()\n        if max_swap_limit == 0:\n            swap_name = _(\"reverse\") if self.is_reverse else _(\"forward\")\n            swap_limit_str = _(\"No {} swap possible with this provider\").format(swap_name)\n        else:\n            swap_limit_str = (f\"{self.window.format_amount(min_swap_limit)} - \"\n                              f\"{self.window.format_amount(max_swap_limit)} {w_base_unit}\")\n        self.swap_limits_label.setText(swap_limit_str)\n        self.swap_limits_label.repaint()  # macOS hack for #6269\n        self.last_server_mining_fee_sat = sm.mining_fee\n        server_fee_str = '%.2f'%sm.percentage + '%  +  '  + self.window.format_amount(sm.mining_fee) + ' ' + w_base_unit\n        self.server_fee_label.setText(server_fee_str)\n        self.server_fee_label.repaint()  # macOS hack for #6269\n        self.needs_tx_update = True\n\n    def get_client_swap_limits_sat(self) -> Tuple[int, int]:\n        \"\"\"Returns the (min, max) client swap limits in sat.\"\"\"\n        sm = self.swap_manager\n\n        if self.is_reverse:\n            lower_limit = sm.get_min_amount()\n            upper_limit = sm.client_max_amount_reverse_swap() or 0\n        else:\n            lower_limit = sm.get_send_amount(sm.get_min_amount(), is_reverse=False) or sm.get_min_amount()\n            upper_limit = sm.client_max_amount_forward_swap() or 0\n\n        if lower_limit > upper_limit:\n            # if the max possible amount is below the lower limit no swap is possible\n            lower_limit, upper_limit = 0, 0\n        return lower_limit, upper_limit\n\n    def update_fee(self, tx: Optional[PartialTransaction]) -> None:\n        \"\"\"Updates self.fee_label. No other side-effects.\"\"\"\n        if self.is_reverse:\n            sm = self.swap_manager\n            fee = sm.get_fee_for_txbatcher()\n        else:\n            fee = tx.get_fee() if tx else None\n        fee_text = self.window.format_amount(fee) + ' ' + self.window.base_unit() if fee else _(\"no input\")\n        self.fee_label.setText(fee_text)\n        self.fee_label.repaint()  # macOS hack for #6269\n\n    def run(self, transport: 'SwapServerTransport') -> bool:\n        \"\"\"Can raise InvalidSwapParameters.\"\"\"\n        if not self.exec():\n            return False\n        if self.is_reverse:\n            lightning_amount = self.send_amount_e.get_amount()\n            onchain_amount = self.recv_amount_e.get_amount()\n            if lightning_amount is None or onchain_amount is None:\n                return False\n            sm = self.swap_manager\n            coro = sm.reverse_swap(\n                transport=transport,\n                lightning_amount_sat=lightning_amount,\n                expected_onchain_amount_sat=onchain_amount + self.swap_manager.get_fee_for_txbatcher(),\n                prepayment_sat=2 * self.last_server_mining_fee_sat,\n            )\n            try:\n                # we must not leave the context, so we use run_couroutine_dialog\n                funding_txid = self.window.run_coroutine_dialog(coro, _('Initiating swap...'))\n            except Exception as e:\n                self.window.show_error(f\"Reverse swap failed: {str(e)}\")\n                return False\n            self.window.on_swap_result(funding_txid, is_reverse=True)\n            return True\n        else:\n            lightning_amount = self.recv_amount_e.get_amount()\n            onchain_amount = self.send_amount_e.get_amount()\n            if lightning_amount is None or onchain_amount is None:\n                return False\n            if lightning_amount > self.lnworker.num_sats_can_receive():\n                if not self.window.question(CANNOT_RECEIVE_WARNING):\n                    return False\n            self.window.protect(self.do_normal_swap, (transport, lightning_amount, onchain_amount))\n            return True\n\n    def update_tx(self) -> None:\n        if self.is_reverse:\n            self.update_fee(None)\n            return\n        is_max = self.max_button.isChecked()\n        if is_max:\n            tx = self._create_tx_safe('!')\n            self._spend_max_forward_swap(tx)\n        else:\n            onchain_amount = self.send_amount_e.get_amount()\n            tx = self._create_tx_safe(onchain_amount)\n        self.update_fee(tx)\n\n    def _create_tx(self, onchain_amount: Union[int, str, None]) -> PartialTransaction:\n        assert not self.is_reverse\n        if onchain_amount is None:\n            raise InvalidSwapParameters(\"onchain_amount is None\")\n        coins = self.window.get_coins()\n        if onchain_amount == '!':\n            max_amount = sum(c.value_sats() for c in coins)\n            max_swap_amount = self.swap_manager.client_max_amount_forward_swap()\n            if max_swap_amount is None:\n                raise InvalidSwapParameters(\"swap_manager.client_max_amount_forward_swap() is None\")\n            if max_amount > max_swap_amount:\n                onchain_amount = max_swap_amount\n        outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]\n        try:\n            tx = self.window.wallet.make_unsigned_transaction(\n                fee_policy=self.fee_policy,\n                coins=coins,\n                outputs=outputs,\n                send_change_to_lightning=False,\n            )\n        except (NotEnoughFunds, NoDynamicFeeEstimates) as e:\n            raise InvalidSwapParameters(str(e)) from e\n        return tx\n\n    def _create_tx_safe(self, onchain_amount: Union[int, str, None]) -> Optional[PartialTransaction]:\n        try:\n            return self._create_tx(onchain_amount=onchain_amount)\n        except InvalidSwapParameters:\n            return None\n\n    def update_ok_button(self):\n        \"\"\"Updates self.ok_button. No other side-effects.\"\"\"\n        send_amount = self.send_amount_e.get_amount()\n        recv_amount = self.recv_amount_e.get_amount()\n        self.ok_button.setEnabled(bool(send_amount) and bool(recv_amount))\n\n    async def _do_normal_swap(self, transport, lightning_amount, onchain_amount, password):\n        dummy_tx = self._create_tx(onchain_amount)\n        assert dummy_tx\n        sm = self.swap_manager\n        swap, invoice = await sm.request_normal_swap(\n            transport=transport,\n            lightning_amount_sat=lightning_amount,\n            expected_onchain_amount_sat=onchain_amount,\n            channels=self.channels,\n        )\n        self._current_swap = swap\n        tx = sm.create_funding_tx(swap, dummy_tx, password=password)\n        txid = await sm.wait_for_htlcs_and_broadcast(transport=transport, swap=swap, invoice=invoice, tx=tx)\n        return txid\n\n    def do_normal_swap(self, transport, lightning_amount, onchain_amount, password):\n        self._current_swap = None\n        coro = self._do_normal_swap(transport, lightning_amount, onchain_amount, password)\n        try:\n            funding_txid = self.window.run_coroutine_dialog(coro, _('Awaiting swap payment...'))\n        except UserCancelled:\n            self.swap_manager.cancel_normal_swap(self._current_swap)\n            self.window.show_message(_('Swap cancelled'))\n            return\n        except Exception as e:\n            self.window.show_error(str(e))\n            return\n        self.window.on_swap_result(funding_txid, is_reverse=False)\n\n    def get_description(self):\n        onchain_funds = \"onchain\"\n        lightning_funds = \"lightning\"\n\n        return \"Send {fromType}, receive {toType}.\\nThis will increase your lightning {capacityType} capacity.\\n\".format(\n            fromType=lightning_funds if self.is_reverse else onchain_funds,\n            toType=onchain_funds if self.is_reverse else lightning_funds,\n            capacityType=\"receiving\" if self.is_reverse else \"sending\",\n        )\n\n\nclass SwapServerDialog(WindowModalDialog, QtEventListener):\n\n    class Columns(MyTreeView.BaseColumnsEnum):\n        PUBKEY = enum.auto()\n        FEE = enum.auto()\n        MAX_FORWARD = enum.auto()\n        MAX_REVERSE = enum.auto()\n        LAST_SEEN = enum.auto()\n\n    headers = {\n        Columns.PUBKEY: _(\"Pubkey\"),\n        Columns.FEE: _(\"Fee\"),\n        Columns.MAX_FORWARD: _('Max Forward'),\n        Columns.MAX_REVERSE: _('Max Reverse'),\n        Columns.LAST_SEEN: _(\"Last seen\"),\n    }\n\n    def __init__(self, window: 'ElectrumWindow', servers: Sequence['SwapOffer']):\n        WindowModalDialog.__init__(self, window, _('Choose Swap Provider'))\n        self.window = window\n        self.config = window.config\n        msg = '\\n'.join([\n            _(\"Please choose a provider from this list.\"),\n            _(\"Note that fees and liquidity may be updated frequently.\")\n        ])\n        self.servers_list = QTreeWidget()\n        col_names = [self.headers[col_idx] for col_idx in sorted(self.headers.keys())]\n        self.servers_list.setHeaderLabels(col_names)\n        self.servers_list.header().setStretchLastSection(False)\n        for col_idx in range(len(self.Columns)):\n            sm = QHeaderView.ResizeMode.Stretch if col_idx == self.Columns.PUBKEY else QHeaderView.ResizeMode.ResizeToContents\n            self.servers_list.header().setSectionResizeMode(col_idx, sm)\n        self.update_servers_list(servers)\n        vbox = QVBoxLayout()\n        self.setLayout(vbox)\n        vbox.addWidget(WWLabel(msg))\n        vbox.addWidget(self.servers_list, stretch=1)\n        vbox.addSpacing(10)\n        self.ok_button = OkButton(self)\n        vbox.addLayout(Buttons(CancelButton(self), self.ok_button))\n        self.setMinimumWidth(650)\n        self.register_callbacks()\n\n    def run(self):\n        if self.exec() != 1:\n            return None\n        if item := self.servers_list.currentItem():\n            return item.data(self.Columns.PUBKEY, ROLE_NPUB)\n        return None\n\n    def closeEvent(self, event):\n        self.unregister_callbacks()\n        event.accept()\n\n    @qt_event_listener\n    def on_event_swap_offers_changed(self, recent_offers: Sequence['SwapOffer']):\n        self.update_servers_list(recent_offers)\n\n    def update_servers_list(self, servers: Sequence['SwapOffer']):\n        self.servers_list.clear()\n        from electrum.util import age\n        items = []\n        for x in servers:\n            labels = [\"\"] * len(self.Columns)\n            labels[self.Columns.PUBKEY] = x.server_pubkey\n            labels[self.Columns.FEE] = f\"{x.pairs.percentage}% + {x.pairs.mining_fee} sats\"\n            labels[self.Columns.MAX_FORWARD] = self.window.format_amount(x.pairs.max_forward) + ' ' + self.window.base_unit()\n            labels[self.Columns.MAX_REVERSE] = self.window.format_amount(x.pairs.max_reverse) + ' ' + self.window.base_unit()\n            labels[self.Columns.LAST_SEEN] = age(x.timestamp)\n            item = QTreeWidgetItem(labels)\n            item.setData(self.Columns.PUBKEY, ROLE_NPUB, x.server_npub)\n            item.setIcon(self.Columns.PUBKEY, pubkey_to_q_icon(x.server_pubkey))\n            items.append(item)\n        self.servers_list.insertTopLevelItems(0, items)\n\n"
  },
  {
    "path": "electrum/gui/qt/transaction_dialog.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2012 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport asyncio\nimport concurrent.futures\nimport copy\nimport datetime\nimport time\nfrom typing import TYPE_CHECKING, Optional, List, Union, Mapping, Callable\nfrom functools import partial\nfrom decimal import Decimal\n\nfrom PyQt6.QtCore import QSize, Qt, QUrl, QPoint, pyqtSignal\nfrom PyQt6.QtGui import QTextCharFormat, QBrush, QFont, QPixmap, QTextCursor, QAction\nfrom PyQt6.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QWidget,\n                             QToolButton, QMenu, QTextBrowser,\n                             QSizePolicy)\nimport qrcode\nfrom qrcode import exceptions\n\nfrom electrum import bitcoin\n\nfrom electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX, DummyAddress\nfrom electrum.i18n import _\nfrom electrum.plugin import run_hook\nfrom electrum.transaction import SerializationError, Transaction, PartialTransaction, TxOutpoint, TxinDataFetchProgress\nfrom electrum.logging import get_logger\nfrom electrum.util import (ShortID, get_asyncio_loop, UI_UNIT_NAME_TXSIZE_VBYTES, delta_time_str,\n                           UserCancelled)\nfrom electrum.network import Network\nfrom electrum.wallet import TxSighashRiskLevel, TxSighashDanger\n\nfrom .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,\n                   MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, ShowQRLineEdit, text_dialog,\n                   char_width_in_lineedit, TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE,\n                   TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX,\n                   TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX,\n                   getSaveFileName, ColorSchemeItem,\n                   get_icon_qrcode, VLine, WaitingDialog)\nfrom .rate_limiter import rate_limited\nfrom .my_treeview import create_toolbar_with_menu, QMenuWithConfig\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n    from electrum.wallet import Abstract_Wallet\n    from electrum.invoices import Invoice\n\n\n_logger = get_logger(__name__)\ndialogs = []  # Otherwise python randomly garbage collects the dialogs...\n\n\nclass TxSizeLabel(QLabel):\n    def setAmount(self, byte_size):\n        text = \"\"\n        if byte_size:\n            text = f\"x   {byte_size} {UI_UNIT_NAME_TXSIZE_VBYTES}   =\"\n        self.setText(text)\n\n\nclass TxFiatLabel(QLabel):\n    def setAmount(self, fiat_fee):\n        self.setText(('≈  %s' % fiat_fee) if fiat_fee else '')\n\n\nclass QTextBrowserWithDefaultSize(QTextBrowser):\n    def __init__(self, width: int = 0, height: int = 0):\n        self._width = width\n        self._height = height\n        QTextBrowser.__init__(self)\n        self.setLineWrapMode(QTextBrowser.LineWrapMode.NoWrap)\n\n    def sizeHint(self):\n        return QSize(self._width, self._height)\n\n\nclass TxInOutWidget(QWidget):\n    def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'):\n        QWidget.__init__(self)\n\n        self.wallet = wallet\n        self.main_window = main_window\n        self.tx = None  # type: Optional[Transaction]\n        self.inputs_header = QLabel()\n        self.inputs_textedit = QTextBrowserWithDefaultSize(750, 100)\n        self.inputs_textedit.setOpenLinks(False)  # disable automatic link opening\n        self.inputs_textedit.anchorClicked.connect(self._open_internal_link)  # send links to our handler\n        self.inputs_textedit.setTextInteractionFlags(\n            self.inputs_textedit.textInteractionFlags() | Qt.TextInteractionFlag.LinksAccessibleByMouse | Qt.TextInteractionFlag.LinksAccessibleByKeyboard)\n        self.inputs_textedit.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)\n        self.inputs_textedit.customContextMenuRequested.connect(self.on_context_menu_for_inputs)\n\n        self.sighash_label = QLabel()\n        self.sighash_label.setStyleSheet('font-weight: bold')\n        self.sighash_danger = TxSighashDanger()\n        self.inputs_warning_icon = QLabel()\n        pixmap = QPixmap(icon_path(\"warning\"))\n        pixmap_size = round(2 * char_width_in_lineedit())\n        pixmap = pixmap.scaled(pixmap_size, pixmap_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)\n        self.inputs_warning_icon.setPixmap(pixmap)\n        self.inputs_warning_icon.setVisible(False)\n\n        self.inheader_hbox = QHBoxLayout()\n        self.inheader_hbox.setContentsMargins(0, 0, 0, 0)\n        self.inheader_hbox.addWidget(self.inputs_header)\n        self.inheader_hbox.addStretch(2)\n        self.inheader_hbox.addWidget(self.sighash_label)\n        self.inheader_hbox.addWidget(self.inputs_warning_icon)\n\n        self.txo_color_recv = TxOutputColoring(\n            legend=_(\"Wallet Address\"), color=ColorScheme.GREEN, tooltip=_(\"Wallet receiving address\"))\n        self.txo_color_change = TxOutputColoring(\n            legend=_(\"Change Address\"), color=ColorScheme.YELLOW, tooltip=_(\"Wallet change address\"))\n        self.txo_color_accounting = TxOutputColoring(\n            legend=_(\"Accounting Address\"), color=ColorScheme.ORANGE, tooltip=_(\"Address from which funds were swept to your wallet.\"))\n        self.txo_color_2fa = TxOutputColoring(\n            legend=_(\"TrustedCoin (2FA) batch fee\"), color=ColorScheme.BLUE, tooltip=_(\"TrustedCoin (2FA) fee for the next batch of transactions\"))\n        self.txo_color_swap = TxOutputColoring(\n            legend=_(\"Submarine swap address\"), color=ColorScheme.BLUE, tooltip=_(\"Submarine swap address\"))\n        self.outputs_header = QLabel()\n        self.outputs_textedit = QTextBrowserWithDefaultSize(750, 100)\n        self.outputs_textedit.setOpenLinks(False)  # disable automatic link opening\n        self.outputs_textedit.anchorClicked.connect(self._open_internal_link)  # send links to our handler\n        self.outputs_textedit.setTextInteractionFlags(\n            self.outputs_textedit.textInteractionFlags() | Qt.TextInteractionFlag.LinksAccessibleByMouse | Qt.TextInteractionFlag.LinksAccessibleByKeyboard)\n        self.outputs_textedit.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)\n        self.outputs_textedit.customContextMenuRequested.connect(self.on_context_menu_for_outputs)\n\n        outheader_hbox = QHBoxLayout()\n        outheader_hbox.setContentsMargins(0, 0, 0, 0)\n        outheader_hbox.addWidget(self.outputs_header)\n        outheader_hbox.addStretch(2)\n        outheader_hbox.addWidget(self.txo_color_recv.legend_label)\n        outheader_hbox.addWidget(self.txo_color_change.legend_label)\n        outheader_hbox.addWidget(self.txo_color_2fa.legend_label)\n        outheader_hbox.addWidget(self.txo_color_swap.legend_label)\n        outheader_hbox.addWidget(self.txo_color_accounting.legend_label)\n\n        vbox = QVBoxLayout()\n        vbox.addLayout(self.inheader_hbox)\n        vbox.addWidget(self.inputs_textedit)\n        vbox.addLayout(outheader_hbox)\n        vbox.addWidget(self.outputs_textedit)\n        self.setLayout(vbox)\n        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)\n\n    def update(self, tx: Optional[Transaction]):\n        self.tx = tx\n        if tx is None:\n            self.inputs_header.setText('')\n            self.inputs_textedit.setText('')\n            self.outputs_header.setText('')\n            self.outputs_textedit.setText('')\n            return\n\n        inputs_header_text = _(\"Inputs\") + ' (%d)'%len(self.tx.inputs())\n        self.inputs_header.setText(inputs_header_text)\n        ext = QTextCharFormat()  # \"external\"\n        lnk = QTextCharFormat()\n        lnk.setToolTip(_('Click to open, right-click for menu'))\n        lnk.setAnchor(True)\n        lnk.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SingleUnderline)\n        tf_used_recv, tf_used_change, tf_used_2fa, tf_used_swap = False, False, False, False\n        tf_used_accounting = False\n\n        def addr_text_format(addr: str) -> QTextCharFormat:\n            nonlocal tf_used_recv, tf_used_change, tf_used_2fa, tf_used_swap, tf_used_accounting\n            sm = self.wallet.lnworker.swap_manager if self.wallet.lnworker else None\n            if self.wallet.is_mine(addr):\n                if self.wallet.is_change(addr):\n                    tf_used_change = True\n                    fmt = QTextCharFormat(self.txo_color_change.text_char_format)\n                else:\n                    tf_used_recv = True\n                    fmt = QTextCharFormat(self.txo_color_recv.text_char_format)\n                fmt.setAnchorHref(addr)\n                fmt.setToolTip(_('Click to open, right-click for menu'))\n                fmt.setAnchor(True)\n                fmt.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SingleUnderline)\n                return fmt\n            elif sm and sm.is_lockup_address_for_a_swap(addr) or addr == DummyAddress.SWAP:\n                tf_used_swap = True\n                return self.txo_color_swap.text_char_format\n            elif self.wallet.is_billing_address(addr):\n                tf_used_2fa = True\n                return self.txo_color_2fa.text_char_format\n            elif self.wallet.is_accounting_address(addr):\n                tf_used_accounting = True\n                return self.txo_color_accounting.text_char_format\n            return ext\n\n        def insert_tx_io(\n            *,\n            cursor: QTextCursor,\n            txio_idx: int,\n            is_coinbase: bool,\n            tcf_shortid: QTextCharFormat = None,\n            short_id: str,\n            addr: Optional[str],\n            value: Optional[int],\n        ):\n            tcf_ext = QTextCharFormat(ext)\n            tcf_addr = addr_text_format(addr)\n            if tcf_shortid is None:\n                tcf_shortid = tcf_ext\n            a_name = f\"txio_idx {txio_idx}\"\n            for tcf in (tcf_ext, tcf_shortid, tcf_addr):  # used by context menu creation\n                tcf.setAnchorNames([a_name])\n            if is_coinbase:\n                cursor.insertText('coinbase', tcf_ext)\n            else:\n                # short_id\n                cursor.insertText(short_id, tcf_shortid)\n                cursor.insertText(\" \" * max(0, 15 - len(short_id)), tcf_ext)  # padding\n                cursor.insertText('\\t', tcf_ext)\n                # addr\n                if addr is None:\n                    address_str = '<address unknown>'\n                elif len(addr) <= 42:\n                    address_str = addr\n                else:\n                    address_str = addr[0:30] + '…' + addr[-11:]\n                cursor.insertText(address_str, tcf_addr)\n                cursor.insertText(\" \" * max(0, 42 - len(address_str)), tcf_ext)  # padding\n                cursor.insertText('\\t', tcf_ext)\n                # value\n                value_str = self.main_window.format_amount(value, whitespaces=True)\n                cursor.insertText(value_str, tcf_ext)\n            cursor.insertBlock()\n\n        i_text = self.inputs_textedit\n        i_text.clear()\n        i_text.setFont(QFont(MONOSPACE_FONT))\n        i_text.setReadOnly(True)\n        cursor = i_text.textCursor()\n        for txin_idx, txin in enumerate(self.tx.inputs()):\n            addr = self.wallet.adb.get_txin_address(txin)\n            txin_value = self.wallet.adb.get_txin_value(txin)\n            tcf_shortid = QTextCharFormat(lnk)\n            tcf_shortid.setAnchorHref(txin.prevout.txid.hex())\n            insert_tx_io(\n                cursor=cursor, is_coinbase=txin.is_coinbase_input(), txio_idx=txin_idx,\n                tcf_shortid=tcf_shortid,\n                short_id=str(txin.short_id), addr=addr, value=txin_value,\n            )\n\n        if isinstance(self.tx, PartialTransaction):\n            self.sighash_danger = self.wallet.check_sighash(self.tx)\n            if self.sighash_danger.risk_level >= TxSighashRiskLevel.WEIRD_SIGHASH:\n                self.sighash_label.setText(self.sighash_danger.short_message)\n                self.inputs_warning_icon.setVisible(True)\n                self.inputs_warning_icon.setToolTip(self.sighash_danger.get_long_message())\n\n        self.outputs_header.setText(_(\"Outputs\") + ' (%d)'%len(self.tx.outputs()))\n        o_text = self.outputs_textedit\n        o_text.clear()\n        o_text.setFont(QFont(MONOSPACE_FONT))\n        o_text.setReadOnly(True)\n        tx_height, tx_pos = None, None\n        tx_hash = self.tx.txid()\n        if tx_hash:\n            tx_mined_info = self.wallet.adb.get_tx_height(tx_hash)\n            tx_height = tx_mined_info.height()\n            tx_pos = tx_mined_info.txpos\n        cursor = o_text.textCursor()\n        for txout_idx, o in enumerate(self.tx.outputs()):\n            if tx_height is not None and tx_pos is not None and tx_pos >= 0:\n                short_id = ShortID.from_components(tx_height, tx_pos, txout_idx)\n            elif tx_hash:\n                short_id = TxOutpoint(bytes.fromhex(tx_hash), txout_idx).short_name()\n            else:\n                short_id = f\"unknown:{txout_idx}\"\n            addr = o.get_ui_address_str()\n            spender_txid = None  # type: Optional[str]\n            if tx_hash:\n                spender_txid = self.wallet.db.get_spent_outpoint(tx_hash, txout_idx)\n            tcf_shortid = None\n            if spender_txid:\n                tcf_shortid = QTextCharFormat(lnk)\n                tcf_shortid.setAnchorHref(spender_txid)\n            insert_tx_io(\n                cursor=cursor, is_coinbase=False, txio_idx=txout_idx,\n                tcf_shortid=tcf_shortid,\n                short_id=str(short_id), addr=addr, value=o.value,\n            )\n\n        self.txo_color_recv.legend_label.setVisible(tf_used_recv)\n        self.txo_color_change.legend_label.setVisible(tf_used_change)\n        self.txo_color_2fa.legend_label.setVisible(tf_used_2fa)\n        self.txo_color_swap.legend_label.setVisible(tf_used_swap)\n        self.txo_color_accounting.legend_label.setVisible(tf_used_accounting)\n\n    def _open_internal_link(self, target):\n        \"\"\"Accepts either a str txid, str address, or a QUrl which should be\n        of the bare form \"txid\" and/or \"address\" -- used by the clickable\n        links in the inputs/outputs QTextBrowsers\"\"\"\n        if isinstance(target, QUrl):\n            target = target.toString(QUrl.UrlFormattingOption.None_)\n        assert target\n        if bitcoin.is_address(target):\n            # target was an address, open address dialog\n            self.main_window.show_address(target, parent=self)\n        else:\n            # target was a txid, open new tx dialog\n            self.main_window.do_process_from_txid(txid=target, parent=self)\n\n    def on_context_menu_for_inputs(self, pos: QPoint):\n        i_text = self.inputs_textedit\n        global_pos = i_text.viewport().mapToGlobal(pos)\n\n        cursor = i_text.cursorForPosition(pos)\n        charFormat = cursor.charFormat()\n        name = charFormat.anchorNames() and charFormat.anchorNames()[0]\n        if not name:\n            menu = i_text.createStandardContextMenu()\n            menu.exec(global_pos)\n            return\n\n        menu = QMenu()\n        show_list = []\n        copy_list = []\n        # figure out which input they right-clicked on. input lines have an anchor named \"txio_idx N\"\n        txin_idx = int(name.split()[1])  # split \"txio_idx N\", translate N -> int\n        txin = self.tx.inputs()[txin_idx]\n\n        menu.addAction(_(\"Tx Input #{}\").format(txin_idx)).setDisabled(True)\n        menu.addSeparator()\n        if txin.is_coinbase_input():\n            menu.addAction(_(\"Coinbase Input\")).setDisabled(True)\n        else:\n            show_list += [(_(\"Show Prev Tx\"), lambda: self._open_internal_link(txin.prevout.txid.hex()))]\n            copy_list += [(_(\"Copy Outpoint\"), lambda: self.main_window.do_copy(txin.prevout.to_str()))]\n            addr = self.wallet.adb.get_txin_address(txin)\n            if addr:\n                if self.wallet.is_mine(addr):\n                    show_list += [(_(\"Address Details\"), lambda: self.main_window.show_address(addr, parent=self))]\n                copy_list += [(_(\"Copy Address\"), lambda: self.main_window.do_copy(addr))]\n            txin_value = self.wallet.adb.get_txin_value(txin)\n            if txin_value:\n                value_str = self.main_window.format_amount(txin_value, add_thousands_sep=False)\n                copy_list += [(_(\"Copy Amount\"), lambda: self.main_window.do_copy(value_str))]\n\n        for item in show_list:\n            menu.addAction(*item)\n        if show_list and copy_list:\n            menu.addSeparator()\n        for item in copy_list:\n            menu.addAction(*item)\n\n        menu.addSeparator()\n        std_menu = i_text.createStandardContextMenu()\n        menu.addActions(std_menu.actions())\n        menu.exec(global_pos)\n\n    def on_context_menu_for_outputs(self, pos: QPoint):\n        o_text = self.outputs_textedit\n        global_pos = o_text.viewport().mapToGlobal(pos)\n\n        cursor = o_text.cursorForPosition(pos)\n        charFormat = cursor.charFormat()\n        name = charFormat.anchorNames() and charFormat.anchorNames()[0]\n        if not name:\n            menu = o_text.createStandardContextMenu()\n            menu.exec(global_pos)\n            return\n\n        menu = QMenu()\n        show_list = []\n        copy_list = []\n        # figure out which output they right-clicked on. output lines have an anchor named \"txio_idx N\"\n        txout_idx = int(name.split()[1])  # split \"txio_idx N\", translate N -> int\n        menu.addAction(_(\"Tx Output #{}\").format(txout_idx)).setDisabled(True)\n        menu.addSeparator()\n        if tx_hash := self.tx.txid():\n            outpoint = TxOutpoint(bytes.fromhex(tx_hash), txout_idx)\n            copy_list += [(_(\"Copy Outpoint\"), lambda: self.main_window.do_copy(outpoint.to_str()))]\n        if addr := self.tx.outputs()[txout_idx].address:\n            if self.wallet.is_mine(addr):\n                show_list += [(_(\"Address Details\"), lambda: self.main_window.show_address(addr, parent=self))]\n            copy_list += [(_(\"Copy Address\"), lambda: self.main_window.do_copy(addr))]\n        else:\n            spk = self.tx.outputs()[txout_idx].scriptpubkey\n            copy_list += [(_(\"Copy scriptPubKey\"), lambda: self.main_window.do_copy(spk.hex()))]\n        txout_value = self.tx.outputs()[txout_idx].value\n        value_str = self.main_window.format_amount(txout_value, add_thousands_sep=False)\n        copy_list += [(_(\"Copy Amount\"), lambda: self.main_window.do_copy(value_str))]\n\n        for item in show_list:\n            menu.addAction(*item)\n        if show_list and copy_list:\n            menu.addSeparator()\n        for item in copy_list:\n            menu.addAction(*item)\n\n        run_hook('transaction_dialog_address_menu', menu, addr, self.wallet)\n        menu.addSeparator()\n        std_menu = o_text.createStandardContextMenu()\n        menu.addActions(std_menu.actions())\n        menu.exec(global_pos)\n\n\ndef show_transaction(\n    tx: Transaction,\n    *,\n    parent: 'ElectrumWindow',\n    prompt_if_unsaved: bool = False,\n    prompt_if_complete_unsaved: bool = True,\n    external_keypairs: Mapping[bytes, bytes] = None,\n    invoice: 'Invoice' = None,\n    on_closed: Callable[[Optional[Transaction]], None] = None,\n    show_sign_button: bool = True,\n    show_broadcast_button: bool = True,\n):\n    try:\n        d = TxDialog(\n            tx,\n            parent=parent,\n            prompt_if_unsaved=prompt_if_unsaved,\n            prompt_if_complete_unsaved=prompt_if_complete_unsaved,\n            external_keypairs=external_keypairs,\n            invoice=invoice,\n            on_closed=on_closed,\n        )\n        if not show_sign_button:\n            d.sign_button.setVisible(False)\n        if not show_broadcast_button:\n            d.broadcast_button.setVisible(False)\n    except SerializationError as e:\n        _logger.exception('unable to deserialize the transaction')\n        parent.show_critical(_(\"Electrum was unable to deserialize the transaction:\") + \"\\n\" + str(e))\n    except UserCancelled:\n        return\n    else:\n        d.show()\n\n\nclass TxDialog(QDialog, MessageBoxMixin):\n\n    throttled_update_sig = pyqtSignal()  # emit from thread to do update in main thread\n\n    def __init__(\n        self,\n        tx: Transaction,\n        *,\n        parent: 'ElectrumWindow',\n        prompt_if_unsaved: bool,\n        prompt_if_complete_unsaved: bool = True,\n        external_keypairs: Mapping[bytes, bytes] = None,\n        invoice: 'Invoice' = None,\n        on_closed: Callable[[Optional[Transaction]], None] = None,\n    ):\n        '''Transactions in the wallet will show their description.\n        Pass desc to give a description for txs not yet in the wallet.\n        '''\n        # We want to be a top-level window\n        QDialog.__init__(self, parent=None)\n        self.tx = None  # type: Optional[Transaction]\n        self.external_keypairs = external_keypairs\n        self.main_window = parent\n        self.config = parent.config\n        self.wallet = parent.wallet\n        self.invoice = invoice\n        self.prompt_if_unsaved = prompt_if_unsaved\n        self.prompt_if_complete_unsaved = prompt_if_complete_unsaved\n        self.on_closed = on_closed\n        self.saved = False\n        self.desc = None\n        if txid := tx.txid():\n            self.desc = self.wallet.get_label_for_txid(txid) or None\n        if not self.desc and self.invoice:\n            self.desc = self.invoice.get_message()\n        self.setMinimumWidth(640)\n\n        self.psbt_only_widgets = []  # type: List[Union[QWidget, QAction]]\n\n        vbox = QVBoxLayout()\n        self.setLayout(vbox)\n        toolbar, menu = create_toolbar_with_menu(self.config, '')\n        menu.addConfig(\n            self.config.cv.GUI_QT_TX_DIALOG_FETCH_TXIN_DATA,\n            callback=self.maybe_fetch_txin_data)\n        vbox.addLayout(toolbar)\n\n        vbox.addWidget(QLabel(_(\"Transaction ID:\")))\n        self.tx_hash_e = ShowQRLineEdit('', self.config, title=_('Transaction ID'))\n        vbox.addWidget(self.tx_hash_e)\n        self.tx_desc_label = QLabel(_(\"Description:\"))\n        vbox.addWidget(self.tx_desc_label)\n        self.tx_desc = ButtonsLineEdit('')\n\n        self.tx_desc.editingFinished.connect(self.store_tx_label)\n        self.tx_desc.addCopyButton()\n        vbox.addWidget(self.tx_desc)\n\n        self.add_tx_stats(vbox)\n\n        vbox.addSpacing(10)\n\n        self.io_widget = TxInOutWidget(self.main_window, self.wallet)\n        vbox.addWidget(self.io_widget)\n\n        self.sign_button = b = QPushButton(_(\"Sign\"))\n        b.clicked.connect(self.sign)\n\n        self.broadcast_button = b = QPushButton(_(\"Broadcast\"))\n        b.clicked.connect(self.do_broadcast)\n\n        self.save_button = b = QPushButton(_(\"Add to History\"))\n        b.clicked.connect(self.save)\n\n        self.cancel_button = b = QPushButton(_(\"Close\"))\n        b.clicked.connect(self.close)\n        b.setDefault(True)\n\n        self.export_actions_menu = export_actions_menu = QMenuWithConfig(config=self.config)\n        self.add_export_actions_to_menu(export_actions_menu)\n        export_actions_menu.addSeparator()\n        export_option = export_actions_menu.addConfig(\n            self.config.cv.GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA)\n        self.psbt_only_widgets.append(export_option)\n        export_option = export_actions_menu.addConfig(\n            self.config.cv.GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS)\n        self.psbt_only_widgets.append(export_option)\n        if self.wallet.has_support_for_slip_19_ownership_proofs():\n            export_option = export_actions_menu.addAction(\n                _('Include SLIP-19 ownership proofs'),\n                self._add_slip_19_ownership_proofs_to_tx)\n            export_option.setToolTip(_(\"Some cosigners (e.g. Trezor) might require this for coinjoins.\"))\n            self._export_option_slip19 = export_option\n            export_option.setCheckable(True)\n            export_option.setChecked(False)\n            self.psbt_only_widgets.append(export_option)\n\n        self.export_actions_button = QToolButton()\n        self.export_actions_button.setText(_(\"Share\"))\n        self.export_actions_button.setMenu(export_actions_menu)\n        self.export_actions_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)\n\n        partial_tx_actions_menu = QMenu()\n        ptx_merge_sigs_action = QAction(_(\"Merge signatures from\"), self)\n        ptx_merge_sigs_action.triggered.connect(self.merge_sigs)\n        partial_tx_actions_menu.addAction(ptx_merge_sigs_action)\n        self._ptx_join_txs_action = QAction(_(\"Join inputs/outputs\"), self)\n        self._ptx_join_txs_action.triggered.connect(self.join_tx_with_another)\n        partial_tx_actions_menu.addAction(self._ptx_join_txs_action)\n        self.partial_tx_actions_button = QToolButton()\n        self.partial_tx_actions_button.setText(_(\"Combine\"))\n        self.partial_tx_actions_button.setMenu(partial_tx_actions_menu)\n        self.partial_tx_actions_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)\n        self.psbt_only_widgets.append(self.partial_tx_actions_button)\n\n        # Action buttons\n        self.buttons = [self.partial_tx_actions_button, self.sign_button, self.broadcast_button, self.cancel_button]\n        # Transaction sharing buttons\n        self.sharing_buttons = [self.export_actions_button, self.save_button]\n        run_hook('transaction_dialog', self)\n        self.hbox = hbox = QHBoxLayout()\n        hbox.addLayout(Buttons(*self.sharing_buttons))\n        hbox.addStretch(1)\n        hbox.addLayout(Buttons(*self.buttons))\n        vbox.addLayout(hbox)\n        dialogs.append(self)\n\n        self._fetch_txin_data_fut = None  # type: Optional[concurrent.futures.Future]\n        self._fetch_txin_data_progress = None  # type: Optional[TxinDataFetchProgress]\n        self.throttled_update_sig.connect(self._throttled_update, Qt.ConnectionType.QueuedConnection)\n\n        self.set_tx(tx)\n        self.update()\n        self.set_title()\n\n    def store_tx_label(self):\n        text = self.tx_desc.text()\n        if self.wallet.set_label(self.tx.txid(), text):\n            self.main_window.history_list.update()\n            self.main_window.utxo_list.update()\n            self.main_window.labels_changed_signal.emit()\n\n    def set_tx(self, tx: 'Transaction'):\n        # Take a copy; it might get updated in the main window by\n        # e.g. the FX plugin.  If this happens during or after a long\n        # sign operation the signatures are lost.\n        self.tx = tx = copy.deepcopy(tx)\n        try:\n            self.tx.deserialize()\n        except BaseException as e:\n            raise SerializationError(e)\n        # If the wallet can populate the inputs with more info, do it now.\n        # As a result, e.g. we might learn an imported address tx is segwit,\n        # or that a beyond-gap-limit address is is_mine.\n        # note: this might fetch prev txs over the network.\n        tx.add_info_from_wallet(self.wallet)\n        # FIXME for PSBTs, we do a blocking fetch, as the missing data might be needed for e.g. signing\n        # - otherwise, the missing data is for display-completeness only, e.g. fee, input addresses (we do it async)\n        if not tx.is_complete() and tx.is_missing_info_from_network():\n            self.main_window.run_coroutine_dialog(\n                tx.add_info_from_network(self.wallet.network, timeout=10),\n                _(\"Adding info to tx, from network...\"),\n            )\n        else:\n            self.maybe_fetch_txin_data()\n\n    def do_broadcast(self):\n        self.main_window.push_top_level_window(self)\n        self.main_window.send_tab.save_pending_invoice()\n        try:\n            self.main_window.broadcast_transaction(self.tx, invoice=self.invoice)\n        finally:\n            self.main_window.pop_top_level_window(self)\n        self.saved = True\n        self.update()\n\n    def closeEvent(self, event):\n        if (self.prompt_if_unsaved and not self.saved\n                and not self.question(_('This transaction is not saved. Close anyway?'), title=_(\"Warning\"))):\n            event.ignore()\n        else:\n            event.accept()\n            try:\n                dialogs.remove(self)\n            except ValueError:\n                pass  # was not in list already\n        if self._fetch_txin_data_fut:\n            self._fetch_txin_data_fut.cancel()\n            self._fetch_txin_data_fut = None\n\n        if self.on_closed:\n            self.on_closed(self.tx)\n\n    def reject(self):\n        # Override escape-key to close normally (and invoke closeEvent)\n        self.close()\n\n    def add_export_actions_to_menu(self, menu: QMenu) -> None:\n        def gettx() -> Transaction:\n            if not isinstance(self.tx, PartialTransaction):\n                return self.tx\n            tx = copy.deepcopy(self.tx)\n            if self.config.GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS:\n                Network.run_from_another_thread(\n                    tx.prepare_for_export_for_hardware_device(self.wallet))\n            if self.config.GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA:\n                tx.prepare_for_export_for_coinjoin()\n            return tx\n\n        action = QAction(_(\"Copy to clipboard\"), self)\n        action.triggered.connect(lambda: self.copy_to_clipboard(tx=gettx()))\n        menu.addAction(action)\n\n        action = QAction(get_icon_qrcode(), _(\"Show as QR code\"), self)\n        action.triggered.connect(lambda: self.show_qr(tx=gettx()))\n        menu.addAction(action)\n\n        action = QAction(_(\"Save to file\"), self)\n        action.triggered.connect(lambda: self.export_to_file(tx=gettx()))\n        menu.addAction(action)\n\n    def _add_slip_19_ownership_proofs_to_tx(self):\n        assert isinstance(self.tx, PartialTransaction)\n\n        def on_success(result):\n            self._export_option_slip19.setEnabled(False)\n            self.main_window.pop_top_level_window(self)\n\n        def on_failure(exc_info):\n            self._export_option_slip19.setChecked(False)\n            self.main_window.on_error(exc_info)\n            self.main_window.pop_top_level_window(self)\n        task = partial(self.wallet.add_slip_19_ownership_proofs_to_tx, self.tx)\n        msg = _('Adding SLIP-19 ownership proofs to transaction...')\n        self.main_window.push_top_level_window(self)\n        WaitingDialog(self, msg, task, on_success, on_failure)\n\n    def copy_to_clipboard(self, *, tx: Transaction = None):\n        if tx is None:\n            tx = self.tx\n        self.main_window.do_copy(str(tx), title=_(\"Transaction\"))\n\n    def show_qr(self, *, tx: Transaction = None):\n        if tx is None:\n            tx = self.tx\n        qr_data, is_complete = tx.to_qr_data()\n        help_text = None\n        if not is_complete:\n            help_text = _(\n                \"\"\"Warning: Some data (prev txs / \"full utxos\") was left \"\"\"\n                \"\"\"out of the QR code as it would not fit. This might cause issues if signing offline. \"\"\"\n                \"\"\"As a workaround, try exporting the tx as file or text instead.\"\"\")\n        try:\n            self.main_window.show_qrcode(qr_data, _(\"Transaction\"), parent=self, help_text=help_text)\n        except qrcode.exceptions.DataOverflowError:\n            self.show_error(_('Failed to display QR code.') + '\\n' +\n                            _('Transaction is too large in size.'))\n        except Exception as e:\n            self.show_error(_('Failed to display QR code.') + '\\n' + repr(e))\n\n    def sign(self):\n        def sign_done(success):\n            if self.tx.is_complete() and self.prompt_if_complete_unsaved:\n                self.prompt_if_unsaved = True\n                self.saved = False\n            self.update()\n            self.main_window.pop_top_level_window(self)\n\n        if self.io_widget.sighash_danger.needs_confirm():\n            if not self.question(\n                msg='\\n'.join([\n                    self.io_widget.sighash_danger.get_long_message(),\n                    '',\n                    _('Are you sure you want to sign this transaction?')\n                ]),\n                title=self.io_widget.sighash_danger.short_message,\n            ):\n                return\n        self.sign_button.setDisabled(True)\n        self.main_window.push_top_level_window(self)\n        self.main_window.sign_tx(self.tx, callback=sign_done, external_keypairs=self.external_keypairs)\n\n    def save(self):\n        self.main_window.push_top_level_window(self)\n        if self.main_window.save_transaction_into_wallet(self.tx):\n            self.store_tx_label()\n            self.save_button.setDisabled(True)\n            self.saved = True\n        self.main_window.pop_top_level_window(self)\n\n    def export_to_file(self, *, tx: Transaction = None):\n        if tx is None:\n            tx = self.tx\n        if isinstance(tx, PartialTransaction):\n            tx.finalize_psbt()\n        txid = tx.txid()\n        suffix = txid[0:8] if txid is not None else time.strftime('%Y%m%d-%H%M')\n        if tx.is_complete():\n            extension = 'txn'\n            default_filter = TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX\n        else:\n            extension = 'psbt'\n            default_filter = TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX\n        name = f'{self.wallet.basename()}-{suffix}.{extension}'\n        fileName = getSaveFileName(\n            parent=self,\n            title=_(\"Select where to save your transaction\"),\n            filename=name,\n            filter=TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE,\n            default_extension=extension,\n            default_filter=default_filter,\n            config=self.config,\n        )\n        if not fileName:\n            return\n        if tx.is_complete():  # network tx hex\n            with open(fileName, \"w+\") as f:\n                network_tx_hex = tx.serialize_to_network()\n                f.write(network_tx_hex + '\\n')\n        else:  # if partial: PSBT bytes\n            assert isinstance(tx, PartialTransaction)\n            with open(fileName, \"wb+\") as f:\n                f.write(tx.serialize_as_bytes())\n\n        self.show_message(_(\"Transaction exported successfully\"))\n        self.saved = True\n\n    def merge_sigs(self):\n        if not isinstance(self.tx, PartialTransaction):\n            return\n        text = text_dialog(\n            parent=self,\n            title=_('Input raw transaction'),\n            header_layout=_(\"Transaction to merge signatures from\") + \":\",\n            ok_label=_(\"Load transaction\"),\n            config=self.config,\n        )\n        if not text:\n            return\n        tx = self.main_window.tx_from_text(text)\n        if not tx:\n            return\n        try:\n            self.tx.combine_with_other_psbt(tx)\n        except Exception as e:\n            self.show_error(_(\"Error combining partial transactions\") + \":\\n\" + repr(e))\n            return\n        self.update()\n\n    def join_tx_with_another(self):\n        if not isinstance(self.tx, PartialTransaction):\n            return\n        text = text_dialog(\n            parent=self,\n            title=_('Input raw transaction'),\n            header_layout=_(\"Transaction to join with\") + \" (\" + _(\"add inputs and outputs\") + \"):\",\n            ok_label=_(\"Load transaction\"),\n            config=self.config,\n        )\n        if not text:\n            return\n        tx = self.main_window.tx_from_text(text)\n        if not tx:\n            return\n        try:\n            self.tx.join_with_other_psbt(tx, config=self.config)\n        except Exception as e:\n            self.show_error(_(\"Error joining partial transactions\") + \":\\n\" + repr(e))\n            return\n        self.update()\n\n    @rate_limited(0.5, ts_after=True)\n    def _throttled_update(self):\n        self.update()\n\n    def update(self):\n        if self.tx is None:\n            return\n        self.io_widget.update(self.tx)\n        desc = self.desc\n        base_unit = self.main_window.base_unit()\n        format_amount = self.main_window.format_amount\n        format_fiat_and_units = self.main_window.format_fiat_and_units\n        tx_details = self.wallet.get_tx_info(self.tx)\n        tx_mined_status = tx_details.tx_mined_status\n        exp_n = tx_details.mempool_depth_bytes\n        amount, fee = tx_details.amount, tx_details.fee\n        size = self.tx.estimated_size()\n        txid = self.tx.txid()\n        fx = self.main_window.fx\n        tx_item_fiat = None\n        if txid is not None and fx.is_enabled() and amount is not None:\n            tx_item_fiat = self.wallet.get_tx_item_fiat(\n                tx_hash=txid, amount_sat=abs(amount), fx=fx, tx_fee=fee)\n\n        if self.wallet.lnworker and txid:\n            # if it is a group, collect ln amount\n            full_history = self.wallet.get_full_history()\n            item = full_history.get('group:' + txid)\n            ln_amount = item['ln_value'].value if item else None\n        else:\n            ln_amount = None\n\n        self.broadcast_button.setEnabled(tx_details.can_broadcast)\n        can_sign = not self.tx.is_complete() and \\\n            (self.wallet.can_sign(self.tx) or bool(self.external_keypairs))\n        self.sign_button.setEnabled(can_sign and not self.io_widget.sighash_danger.needs_reject())\n        if sh_danger_msg := self.io_widget.sighash_danger.get_long_message():\n            self.sign_button.setToolTip(sh_danger_msg)\n        if tx_details.txid:\n            self.tx_hash_e.setText(tx_details.txid)\n        else:\n            # note: when not finalized, RBF and locktime changes do not trigger\n            #       a make_tx, so the txid is unreliable, hence:\n            self.tx_hash_e.setText(_('Unknown'))\n        tx_in_db = bool(self.wallet.adb.get_transaction(txid))\n        if not desc and not tx_in_db:\n            self.tx_desc.hide()\n            self.tx_desc_label.hide()\n        else:\n            self.tx_desc.setText(desc)\n            self.tx_desc.show()\n            self.tx_desc_label.show()\n        self.status_label.setText(_('Status: {}').format(tx_details.status))\n\n        if tx_mined_status.timestamp:\n            time_str = datetime.datetime.fromtimestamp(tx_mined_status.timestamp).isoformat(' ')[:-3]\n            self.date_label.setText(_(\"Date: {}\").format(time_str))\n            self.date_label.show()\n        elif exp_n is not None:\n            from electrum.fee_policy import FeePolicy\n            self.date_label.setText(_('Position in mempool: {}').format(FeePolicy.depth_tooltip(exp_n)))\n            self.date_label.show()\n        else:\n            self.date_label.hide()\n        if self.tx.locktime <= NLOCKTIME_BLOCKHEIGHT_MAX:\n            locktime_str = _('height')\n        else:\n            locktime_str = datetime.datetime.fromtimestamp(self.tx.locktime)\n        locktime_final_str = _(\"LockTime: {} ({})\").format(self.tx.locktime, locktime_str)\n        self.locktime_final_label.setText(locktime_final_str)\n\n        nsequence_time = self.tx.get_time_based_relative_locktime()\n        nsequence_blocks = self.tx.get_block_based_relative_locktime()\n        if nsequence_time or nsequence_blocks:\n            if nsequence_time:\n                seconds = nsequence_time * 512\n                time_str = delta_time_str(datetime.timedelta(seconds=seconds))\n            else:\n                time_str = '{} blocks'.format(nsequence_blocks)\n            nsequence_str = _(\"Relative locktime: {}\").format(time_str)\n            self.nsequence_label.setText(nsequence_str)\n        else:\n            self.nsequence_label.hide()\n\n        # TODO: 'Yes'/'No' might be better translatable than 'True'/'False'?\n        self.rbf_label.setText(_('Replace by fee: {}').format(_('True') if self.tx.is_rbf_enabled() else _('False')))\n\n        if tx_mined_status.header_hash:\n            self.block_height_label.setText(_(\"At block height: {}\").format(tx_mined_status.height()))\n        else:\n            self.block_height_label.hide()\n        if amount is None and ln_amount is None:\n            amount_str = _(\"Transaction unrelated to your wallet\")\n        elif amount is None:\n            amount_str = ''\n        else:\n            amount_str = ''\n            if fx.is_enabled():\n                if tx_item_fiat:  # historical tx -> using historical price\n                    amount_str += ' ({})'.format(tx_item_fiat['fiat_value'].to_ui_string())\n                elif tx_details.is_related_to_wallet:  # probably \"tx preview\" -> using current price\n                    amount_str += ' ({})'.format(format_fiat_and_units(abs(amount)))\n            amount_str = format_amount(abs(amount)) + ' ' + base_unit + amount_str\n            if amount > 0:\n                amount_str = _(\"Amount received: {}\").format(amount_str)\n            else:\n                amount_str = _(\"Amount sent: {}\").format(amount_str)\n        if amount_str:\n            self.amount_label.setText(amount_str)\n        else:\n            self.amount_label.hide()\n        size_str = _(\"Size: {} {}\").format(size, UI_UNIT_NAME_TXSIZE_VBYTES)\n        if fee is None:\n            if prog := self._fetch_txin_data_progress:\n                if not prog.has_errored:\n                    fee_str = _(\"Downloading input data... {}\").format(f\"({prog.num_tasks_done}/{prog.num_tasks_total})\")\n                else:\n                    fee_str = _(\"Downloading input data... {}\").format(_(\"error\"))\n            else:\n                fee_str = _(\"Fee: {}\").format(_(\"unknown\"))\n        else:\n            fee_str = _(\"Fee: {}\").format(f'{format_amount(fee)} {base_unit}')\n            if fx.is_enabled():\n                if tx_item_fiat:  # historical tx -> using historical price\n                    fee_str += ' ({})'.format(tx_item_fiat['fiat_fee'].to_ui_string())\n                elif tx_details.is_related_to_wallet:  # probably \"tx preview\" -> using current price\n                    fee_str += ' ({})'.format(format_fiat_and_units(fee))\n\n            fee_rate = Decimal(fee) / size  # sat/byte\n            fee_str += '  ( %s ) ' % self.main_window.format_fee_rate(fee_rate * 1000)\n            if isinstance(self.tx, PartialTransaction):\n                # 'amount' is zero for self-payments, so in that case we use sum-of-outputs\n                invoice_amt = abs(amount) if amount else self.tx.output_value()\n                fee_warning_tuple = self.wallet.get_tx_fee_warning(\n                    invoice_amt=invoice_amt, tx_size=size, fee=fee, txid=self.tx.txid())\n                if fee_warning_tuple:\n                    allow_send, long_warning, short_warning = fee_warning_tuple\n                    fee_str += \" - <font color={color}>{header}: {body}</font>\".format(\n                        header=_('Warning'),\n                        body=short_warning,\n                        color=ColorScheme.RED.as_color().name(),\n                    )\n        if isinstance(self.tx, PartialTransaction):\n            sh_warning = self.io_widget.sighash_danger.get_long_message()\n            self.fee_warning_icon.setToolTip(str(sh_warning))\n            self.fee_warning_icon.setVisible(can_sign and bool(sh_warning))\n        self.fee_label.setText(fee_str)\n        self.size_label.setText(size_str)\n        if ln_amount is None or ln_amount == 0:\n            ln_amount_str = ''\n        elif ln_amount > 0:\n            ln_amount_str = _('Amount received in channels: {}').format(format_amount(ln_amount) + ' ' + base_unit)\n        else:\n            assert ln_amount < 0, f\"{ln_amount!r}\"\n            ln_amount_str = _('Amount withdrawn from channels: {}').format(format_amount(-ln_amount) + ' ' + base_unit)\n        if ln_amount_str:\n            self.ln_amount_label.setText(ln_amount_str)\n        else:\n            self.ln_amount_label.hide()\n        show_psbt_only_widgets = isinstance(self.tx, PartialTransaction)\n        for widget in self.psbt_only_widgets:\n            if isinstance(widget, QMenu):\n                widget.menuAction().setVisible(show_psbt_only_widgets)\n            else:\n                widget.setVisible(show_psbt_only_widgets)\n        if tx_details.is_lightning_funding_tx:\n            self._ptx_join_txs_action.setEnabled(False)  # would change txid\n\n        self.save_button.setEnabled(tx_details.can_save_as_local)\n        if tx_details.can_save_as_local:\n            self.save_button.setToolTip(_(\"Add transaction to history, without broadcasting it\"))\n        else:\n            self.save_button.setToolTip(_(\"Transaction already in history or not yet signed.\"))\n\n        run_hook('transaction_dialog_update', self)\n\n    def add_tx_stats(self, vbox):\n        hbox_stats = QHBoxLayout()\n        hbox_stats.setContentsMargins(0, 0, 0, 0)\n        hbox_stats_w = QWidget()\n        hbox_stats_w.setLayout(hbox_stats)\n        hbox_stats_w.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)\n\n        # left column\n        vbox_left = QVBoxLayout()\n        self.status_label = TxDetailLabel()\n        vbox_left.addWidget(self.status_label)\n        self.date_label = TxDetailLabel()\n        vbox_left.addWidget(self.date_label)\n        self.amount_label = TxDetailLabel()\n        vbox_left.addWidget(self.amount_label)\n        self.ln_amount_label = TxDetailLabel()\n        vbox_left.addWidget(self.ln_amount_label)\n\n        fee_hbox = QHBoxLayout()\n        self.fee_label = TxDetailLabel()\n        fee_hbox.addWidget(self.fee_label)\n        self.fee_warning_icon = QLabel()\n        pixmap = QPixmap(icon_path(\"warning\"))\n        pixmap_size = round(2 * char_width_in_lineedit())\n        pixmap = pixmap.scaled(pixmap_size, pixmap_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)\n        self.fee_warning_icon.setPixmap(pixmap)\n        self.fee_warning_icon.setVisible(False)\n        fee_hbox.addWidget(self.fee_warning_icon)\n        fee_hbox.addStretch(1)\n        vbox_left.addLayout(fee_hbox)\n\n        vbox_left.addStretch(1)\n        hbox_stats.addLayout(vbox_left, 50)\n\n        # vertical line separator\n        hbox_stats.addWidget(VLine())\n\n        # right column\n        vbox_right = QVBoxLayout()\n        self.size_label = TxDetailLabel()\n        vbox_right.addWidget(self.size_label)\n        self.rbf_label = TxDetailLabel()\n        vbox_right.addWidget(self.rbf_label)\n\n        self.locktime_final_label = TxDetailLabel()\n        vbox_right.addWidget(self.locktime_final_label)\n\n        self.nsequence_label = TxDetailLabel()\n        vbox_right.addWidget(self.nsequence_label)\n\n        self.block_height_label = TxDetailLabel()\n        vbox_right.addWidget(self.block_height_label)\n        vbox_right.addStretch(1)\n        hbox_stats.addLayout(vbox_right, 50)\n\n        vbox.addWidget(hbox_stats_w)\n\n        # set visibility after parenting can be determined by Qt\n        self.rbf_label.setVisible(True)\n        self.locktime_final_label.setVisible(True)\n\n    def set_title(self):\n        txid = self.tx.txid() or \"<no txid yet>\"\n        self.setWindowTitle(_(\"Transaction\") + ' ' + txid)\n\n    def maybe_fetch_txin_data(self):\n        \"\"\"Download missing input data from the network, asynchronously.\n        Note: we fetch the prev txs, which allows calculating the fee and showing \"input addresses\".\n              We could also SPV-verify the tx, to fill in missing tx_mined_status (block height, blockhash, timestamp),\n              but this is not done currently.\n        \"\"\"\n        if not self.config.GUI_QT_TX_DIALOG_FETCH_TXIN_DATA:\n            return\n        tx = self.tx\n        if not tx:\n            return\n        if self._fetch_txin_data_fut is not None:\n            return\n        network = self.wallet.network\n\n        def progress_cb(prog: TxinDataFetchProgress):\n            self._fetch_txin_data_progress = prog\n            self.throttled_update_sig.emit()\n\n        async def wrapper():\n            try:\n                await tx.add_info_from_network(network, progress_cb=progress_cb)\n            finally:\n                self._fetch_txin_data_fut = None\n\n        self._fetch_txin_data_progress = None\n        self._fetch_txin_data_fut = asyncio.run_coroutine_threadsafe(wrapper(), get_asyncio_loop())\n\n\nclass TxDetailLabel(QLabel):\n    def __init__(self, *, word_wrap=None):\n        super().__init__()\n        self.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n        if word_wrap is not None:\n            self.setWordWrap(word_wrap)\n\n\nclass TxOutputColoring:\n    # used for both inputs and outputs\n\n    def __init__(\n            self,\n            *,\n            legend: str,\n            color: ColorSchemeItem,\n            tooltip: str,\n    ):\n        self.color = color.as_color(background=True)\n        self.legend_label = QLabel(\"<font color={color}>{box_char}</font> = {label}\".format(\n            color=self.color.name(),\n            box_char=\"█\",\n            label=legend,\n        ))\n        font = self.legend_label.font()\n        font.setPointSize(font.pointSize() - 1)\n        self.legend_label.setFont(font)\n        self.legend_label.setVisible(False)\n        self.text_char_format = QTextCharFormat()\n        self.text_char_format.setBackground(QBrush(self.color))\n        self.text_char_format.setToolTip(tooltip)\n\n"
  },
  {
    "path": "electrum/gui/qt/update_checker.py",
    "content": "# Copyright (C) 2019 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nimport asyncio\nimport base64\nfrom typing import Optional\n\nfrom PyQt6.QtCore import Qt, QThread, pyqtSignal\nfrom PyQt6.QtWidgets import QVBoxLayout, QLabel, QProgressBar, QHBoxLayout, QPushButton, QDialog\n\nfrom electrum import version\nfrom electrum import constants\nfrom electrum.bitcoin import verify_usermessage_with_address\nfrom electrum.i18n import _\nfrom electrum.util import make_aiohttp_session\nfrom electrum.logging import Logger\nfrom electrum.network import Network\nfrom electrum._vendor.distutils.version import StrictVersion\n\n\nclass UpdateCheck(QDialog, Logger):\n    url = \"https://electrum.org/version\"\n    download_url = \"https://electrum.org/#download\"\n\n    VERSION_ANNOUNCEMENT_SIGNING_KEYS = (\n        \"13xjmVAB1EATPP8RshTE8S8sNwwSUM9p1P\",  # ThomasV (since 3.3.4)\n        \"1Nxgk6NTooV4qZsX5fdqQwrLjYcsQZAfTg\",  # ghost43 (since 4.1.2)\n    )\n\n    def __init__(self, *, latest_version=None):\n        QDialog.__init__(self)\n        self.setWindowTitle('Electrum - ' + _('Update Check'))\n        self.content = QVBoxLayout()\n        self.content.setContentsMargins(*[10]*4)\n\n        self.heading_label = QLabel()\n        self.content.addWidget(self.heading_label)\n\n        self.detail_label = QLabel()\n        self.detail_label.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse)\n        self.detail_label.setOpenExternalLinks(True)\n        self.content.addWidget(self.detail_label)\n\n        self.pb = QProgressBar()\n        self.pb.setMaximum(0)\n        self.pb.setMinimum(0)\n        self.content.addWidget(self.pb)\n\n        versions = QHBoxLayout()\n        versions.addWidget(QLabel(_(\"Current version: {}\").format(version.ELECTRUM_VERSION)))\n        self.latest_version_label = QLabel(_(\"Latest version: {}\").format(\" \"))\n        versions.addWidget(self.latest_version_label)\n        self.content.addLayout(versions)\n\n        self.update_view(latest_version)\n\n        self.update_check_thread = UpdateCheckThread()\n        self.update_check_thread.checked.connect(self.on_version_retrieved)\n        self.update_check_thread.failed.connect(self.on_retrieval_failed)\n        self.update_check_thread.start()\n\n        close_button = QPushButton(_(\"Close\"))\n        close_button.clicked.connect(self.close)\n        self.content.addWidget(close_button)\n        self.setLayout(self.content)\n        self.show()\n\n    def on_version_retrieved(self, version):\n        self.update_view(version)\n\n    def on_retrieval_failed(self):\n        self.heading_label.setText('<h2>' + _(\"Update check failed\") + '</h2>')\n        self.detail_label.setText(_(\"Sorry, but we were unable to check for updates. Please try again later.\"))\n        self.pb.hide()\n\n    @staticmethod\n    def is_newer(latest_version):\n        return latest_version > StrictVersion(version.ELECTRUM_VERSION)\n\n    def update_view(self, latest_version=None):\n        if latest_version:\n            self.pb.hide()\n            self.latest_version_label.setText(_(\"Latest version: {}\").format(latest_version))\n            if self.is_newer(latest_version):\n                self.heading_label.setText('<h2>' + _(\"There is a new update available\") + '</h2>')\n                url = \"<a href='{u}'>{u}</a>\".format(u=UpdateCheck.download_url)\n                self.detail_label.setText(_(\"You can download the new version from {}.\").format(url))\n            else:\n                self.heading_label.setText('<h2>' + _(\"Already up to date\") + '</h2>')\n                self.detail_label.setText(_(\"You are already on the latest version of Electrum.\"))\n        else:\n            self.heading_label.setText('<h2>' + _(\"Checking for updates...\") + '</h2>')\n            self.detail_label.setText(_(\"Please wait while Electrum checks for available updates.\"))\n\n\nclass UpdateCheckThread(QThread, Logger):\n    checked = pyqtSignal(object)\n    failed = pyqtSignal()\n\n    def __init__(self):\n        QThread.__init__(self)\n        Logger.__init__(self)\n        self.network = Network.get_instance()\n        self._fut = None  # type: Optional[asyncio.Future]\n\n    async def get_update_info(self):\n        # note: Use long timeout here as it is not critical that we get a response fast,\n        #       and it's bad not to get an update notification just because we did not wait enough.\n        async with make_aiohttp_session(proxy=self.network.proxy, timeout=120) as session:\n            async with session.get(UpdateCheck.url) as result:\n                signed_version_dict = await result.json(content_type=None)\n                # example signed_version_dict:\n                # {\n                #     \"version\": \"3.9.9\",\n                #     \"signatures\": {\n                #         \"1Lqm1HphuhxKZQEawzPse8gJtgjm9kUKT4\": \"IA+2QG3xPRn4HAIFdpu9eeaCYC7S5wS/sDxn54LJx6BdUTBpse3ibtfq8C43M7M1VfpGkD5tsdwl5C6IfpZD/gQ=\"\n                #     }\n                # }\n                version_num = signed_version_dict['version']\n                sigs = signed_version_dict['signatures']\n                for address, sig in sigs.items():\n                    if address not in UpdateCheck.VERSION_ANNOUNCEMENT_SIGNING_KEYS:\n                        continue\n                    sig = base64.b64decode(sig, validate=True)\n                    msg = version_num.encode('utf-8')\n                    if verify_usermessage_with_address(\n                        address=address, sig65=sig, message=msg,\n                        net=constants.BitcoinMainnet\n                    ):\n                        self.logger.info(f\"valid sig for version announcement '{version_num}' from address '{address}'\")\n                        break\n                else:\n                    raise Exception('no valid signature for version announcement')\n                return StrictVersion(version_num.strip())\n\n    def run(self):\n        if not self.network:\n            self.failed.emit()\n            return\n        self._fut = asyncio.run_coroutine_threadsafe(self.get_update_info(), self.network.asyncio_loop)\n        try:\n            update_info = self._fut.result()\n        except Exception as e:\n            self.logger.info(f\"got exception: '{repr(e)}'\")\n            self.failed.emit()\n        else:\n            self.checked.emit(update_info)\n\n    def stop(self):\n        if self._fut:\n            self._fut.cancel()\n        self.exit()\n        self.wait()\n"
  },
  {
    "path": "electrum/gui/qt/util.py",
    "content": "from abc import ABC, ABCMeta\nimport os.path\nimport time\nimport sys\nimport platform\nimport queue\nimport os\nimport webbrowser\nimport ctypes\nfrom functools import partial, lru_cache\nfrom typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, List, Any, Sequence, Tuple, Union)\n\nfrom PyQt6 import QtCore\nfrom PyQt6.QtGui import (QFont, QColor, QCursor, QPixmap, QImage,\n                         QPalette, QIcon, QFontMetrics, QPainter, QContextMenuEvent, QMovie)\nfrom PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QSize, QRect, QPoint, QObject)\nfrom PyQt6.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout, QVBoxLayout, QLineEdit,\n                             QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton,\n                             QFileDialog, QWidget, QToolButton, QPlainTextEdit, QApplication, QToolTip,\n                             QGraphicsEffect, QGraphicsScene, QGraphicsPixmapItem, QLayoutItem, QLayout, QMenu,\n                             QFrame, QAbstractButton)\n\nfrom electrum.i18n import _\nfrom electrum.util import (FileImportFailed, FileExportFailed, resource_path, EventListener,\n                           get_logger, UserCancelled, UserFacingException, ChoiceItem)\nfrom electrum.invoices import (PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING,\n                               PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST)\nfrom electrum.qrreader import MissingQrDetectionLib, QrCodeResult\nfrom electrum.submarine_swaps import pubkey_to_rgb_color\n\nfrom electrum.gui.common_qt.util import TaskThread\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n    from .paytoedit import PayToEdit\n\n    from electrum.simple_config import SimpleConfig\n    from electrum.simple_config import ConfigVarWithConfig\n\n\nif platform.system() == 'Windows':\n    MONOSPACE_FONT = 'Lucida Console'\nelif platform.system() == 'Darwin':\n    MONOSPACE_FONT = 'Monaco'\nelse:\n    MONOSPACE_FONT = 'monospace'\n\n\n_logger = get_logger(__name__)\n\ndialogs = []\n\npr_icons = {\n    PR_UNKNOWN: \"warning.png\",\n    PR_UNPAID: \"unpaid.png\",\n    PR_PAID: \"confirmed.png\",\n    PR_EXPIRED: \"expired.png\",\n    PR_INFLIGHT: \"unconfirmed.png\",\n    PR_FAILED: \"warning.png\",\n    PR_ROUTING: \"unconfirmed.png\",\n    PR_UNCONFIRMED: \"unconfirmed.png\",\n    PR_BROADCASTING: \"unconfirmed.png\",\n    PR_BROADCAST: \"unconfirmed.png\",\n}\n\n\n# filter tx files in QFileDialog:\nTRANSACTION_FILE_EXTENSION_FILTER_ANY = \"Transaction (*.txn *.psbt);;All files (*)\"\nTRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX = \"Partial Transaction (*.psbt)\"\nTRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX = \"Complete Transaction (*.txn)\"\nTRANSACTION_FILE_EXTENSION_FILTER_SEPARATE = (f\"{TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX};;\"\n                                              f\"{TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX};;\"\n                                              f\"All files (*)\")\n\n\nclass EnterButton(QPushButton):\n    def __init__(self, text, func):\n        QPushButton.__init__(self, text)\n        self.func = func\n        self.clicked.connect(func)\n        self._orig_text = text\n\n    def keyPressEvent(self, e):\n        if e.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter]:\n            self.func()\n\n    def restore_original_text(self):\n        self.setText(self._orig_text)\n\n\nclass ThreadedButton(QPushButton):\n    def __init__(self, text, task, on_success=None, on_error=None):\n        QPushButton.__init__(self, text)\n        self.task = task\n        self.on_success = on_success\n        self.on_error = on_error\n        self.clicked.connect(self.run_task)\n\n    def run_task(self):\n        self.setEnabled(False)\n        self.thread = TaskThread(self)\n        self.thread.add(self.task, self.on_success, self.done, self.on_error)\n\n    def done(self):\n        self.setEnabled(True)\n        self.thread.stop()\n\n\nclass WWLabel(QLabel):\n    \"\"\"Word-wrapping label\"\"\"\n    def __init__(self, text=\"\", parent=None):\n        QLabel.__init__(self, text, parent)\n        self.setWordWrap(True)\n        self.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n\n\nclass RichLabel(WWLabel):\n    \"\"\"Word-wrapping label with link activation\"\"\"\n    def __init__(self, text='', parent=None):\n        WWLabel.__init__(self, text, parent)\n        self.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)\n        self.setOpenExternalLinks(True)\n\n\nclass AmountLabel(QLabel):\n    def __init__(self, *args, **kwargs):\n        QLabel.__init__(self, *args, **kwargs)\n        self.setFont(QFont(MONOSPACE_FONT))\n        self.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n\n\nclass Spinner(QLabel):\n    def __init__(self, *args, **kwargs):\n        QLabel.__init__(self, *args, **kwargs)\n        self.spinner = QMovie(icon_path('spinner.gif'))\n        self.spinner.setScaledSize(QSize(20, 20))\n        self.spinner.frameChanged.connect(lambda: self.setPixmap(self.spinner.currentPixmap()))\n        self.setVisible(False)\n\n    def setVisible(self, visible):\n        if visible:\n            self.spinner.start()\n        else:\n            self.spinner.stop()\n        super().setVisible(visible)\n\n\nclass HelpMixin:\n    def __init__(self, help_text: str, *, help_title: str = None):\n        assert isinstance(self, QWidget), \"HelpMixin must be a QWidget instance!\"\n        self.help_text = help_text\n        self._help_title = help_title or _('Help')\n        if isinstance(self, QLabel):\n            self.setTextInteractionFlags(\n                (self.textInteractionFlags() | Qt.TextInteractionFlag.TextSelectableByMouse)\n                & ~Qt.TextInteractionFlag.TextSelectableByKeyboard)\n\n    def show_help(self):\n        custom_message_box(\n            icon=QMessageBox.Icon.Information,\n            parent=self,\n            title=self._help_title,\n            text=self.help_text,\n            rich_text=True,\n        )\n\n\nclass HelpLabel(HelpMixin, QLabel):\n\n    def __init__(self, text: str, help_text: str):\n        QLabel.__init__(self, text)\n        HelpMixin.__init__(self, help_text)\n        self.app = QCoreApplication.instance()\n        self.font = self.font()\n\n    @classmethod\n    def from_configvar(cls, cv: 'ConfigVarWithConfig') -> 'HelpLabel':\n        return HelpLabel(cv.get_short_desc() + ':', cv.get_long_desc())\n\n    def mouseReleaseEvent(self, x):\n        self.show_help()\n\n    def enterEvent(self, event):\n        self.font.setUnderline(True)\n        self.setFont(self.font)\n        self.app.setOverrideCursor(QCursor(Qt.CursorShape.PointingHandCursor))\n        return QLabel.enterEvent(self, event)\n\n    def leaveEvent(self, event):\n        self.font.setUnderline(False)\n        self.setFont(self.font)\n        self.app.setOverrideCursor(QCursor(Qt.CursorShape.ArrowCursor))\n        return QLabel.leaveEvent(self, event)\n\n\nclass HelpButton(HelpMixin, QToolButton):\n    def __init__(self, text: str):\n        QToolButton.__init__(self)\n        HelpMixin.__init__(self, text)\n        self.setText('?')\n        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n        self.setFixedWidth(round(2.2 * char_width_in_lineedit()))\n        self.clicked.connect(self.show_help)\n\n\nclass InfoButton(HelpMixin, QPushButton):\n    def __init__(self, text: str):\n        QPushButton.__init__(self, _('Info'))\n        HelpMixin.__init__(self, text, help_title=_('Info'))\n        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n        self.setFixedWidth(6 * char_width_in_lineedit())\n        self.clicked.connect(self.show_help)\n\n\nclass Buttons(QHBoxLayout):\n    def __init__(self, *buttons):\n        QHBoxLayout.__init__(self)\n        self.addStretch(1)\n        for b in buttons:\n            if b is None:\n                continue\n            self.addWidget(b)\n\n\nclass CloseButton(QPushButton):\n    def __init__(self, dialog):\n        QPushButton.__init__(self, _(\"Close\"))\n        self.clicked.connect(dialog.close)\n        self.setDefault(True)\n\n\nclass CopyButton(QPushButton):\n    def __init__(self, text_getter, app):\n        QPushButton.__init__(self, _(\"Copy\"))\n        self.clicked.connect(lambda: app.clipboard().setText(text_getter()))\n\n\nclass CopyCloseButton(QPushButton):\n    def __init__(self, text_getter, app, dialog):\n        QPushButton.__init__(self, _(\"Copy and Close\"))\n        self.clicked.connect(lambda: app.clipboard().setText(text_getter()))\n        self.clicked.connect(dialog.close)\n        self.setDefault(True)\n\n\nclass OkButton(QPushButton):\n    def __init__(self, dialog, label=None):\n        QPushButton.__init__(self, label or _(\"OK\"))\n        self.clicked.connect(dialog.accept)\n        self.setDefault(True)\n\n\nclass CancelButton(QPushButton):\n    def __init__(self, dialog, label=None):\n        QPushButton.__init__(self, label or _(\"Cancel\"))\n        self.clicked.connect(dialog.reject)\n\n\nclass MessageBoxMixin(object):\n    def top_level_window_recurse(self, window=None, test_func=None):\n        window = window or self\n        classes = (WindowModalDialog, QMessageBox)\n        if test_func is None:\n            test_func = lambda x: True\n        for n, child in enumerate(window.children()):\n            # Test for visibility as old closed dialogs may not be GC-ed.\n            # Only accept children that confirm to test_func.\n            if isinstance(child, classes) and child.isVisible() \\\n                    and test_func(child):\n                return self.top_level_window_recurse(child, test_func=test_func)\n        return window\n\n    def top_level_window(self, test_func=None):\n        return self.top_level_window_recurse(test_func)\n\n    def question(self, msg, parent=None, title=None, icon=None, **kwargs) -> bool:\n        yes, no = QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No\n        return yes == self.msg_box(icon=icon or QMessageBox.Icon.Question,\n                                   parent=parent,\n                                   title=title or '',\n                                   text=msg,\n                                   buttons=yes | no,\n                                   defaultButton=no,\n                                   **kwargs)\n\n    def show_warning(self, msg, parent=None, title=None, **kwargs):\n        return self.msg_box(QMessageBox.Icon.Warning, parent,\n                            title or _('Warning'), msg, **kwargs)\n\n    def show_error(self, msg, parent=None, **kwargs):\n        return self.msg_box(QMessageBox.Icon.Warning, parent,\n                            _('Error'), msg, **kwargs)\n\n    def show_critical(self, msg, parent=None, title=None, **kwargs):\n        return self.msg_box(QMessageBox.Icon.Critical, parent,\n                            title or _('Critical Error'), msg, **kwargs)\n\n    def show_message(self, msg, parent=None, title=None, icon=QMessageBox.Icon.Information, **kwargs):\n        return self.msg_box(icon, parent, title or _('Information'), msg, **kwargs)\n\n    def msg_box(\n            self,\n            icon: Union[QMessageBox.Icon, QPixmap],\n            parent: QWidget,\n            title: str,\n            text: str,\n            *,\n            buttons: Union[QMessageBox.StandardButton,\n                           List[Union[QMessageBox.StandardButton, Tuple[QAbstractButton, QMessageBox.ButtonRole, int]]]] = QMessageBox.StandardButton.Ok,\n            defaultButton: QMessageBox.StandardButton = QMessageBox.StandardButton.NoButton,\n            rich_text: bool = False,\n            checkbox: Optional[bool] = None\n    ):\n        parent = parent or self.top_level_window()\n        return custom_message_box(\n            icon=icon, parent=parent, title=title, text=text, buttons=buttons, defaultButton=defaultButton,\n            rich_text=rich_text, checkbox=checkbox\n        )\n\n    def query_choice(\n        self,\n        msg: Optional[str],\n        choices: Sequence['ChoiceItem'],\n        *,\n        title: Optional[str] = None,\n        default_key: Optional[Any] = None,\n    ) -> Optional[Any]:\n        \"\"\"Returns ChoiceItem.key (for selected item), or None if the user cancels the dialog.\n\n        Needed by QtHandler for hardware wallets.\n        \"\"\"\n        if title is None:\n            title = _('Question')\n        dialog = WindowModalDialog(self.top_level_window(), title=title)\n        dialog.setMinimumWidth(400)\n        choice_widget = ChoiceWidget(message=msg, choices=choices, default_key=default_key)\n        vbox = QVBoxLayout(dialog)\n        vbox.addWidget(choice_widget)\n        cancel_button = CancelButton(dialog)\n        vbox.addLayout(Buttons(cancel_button, OkButton(dialog)))\n        cancel_button.setFocus()\n        if not dialog.exec():\n            return None\n        return choice_widget.selected_key\n\n    def password_dialog(self, msg=None, parent=None):\n        from .password_dialog import PasswordDialog\n        parent = parent or self\n        d = PasswordDialog(parent, msg)\n        return d.run()\n\n\ndef custom_message_box(\n        *,\n        icon: Union[QMessageBox.Icon, QPixmap],\n        parent: QWidget,\n        title: str,\n        text: str,\n        buttons: Union[QMessageBox.StandardButton,\n                       List[Union[QMessageBox.StandardButton, Tuple[QAbstractButton, QMessageBox.ButtonRole, int]]]] = QMessageBox.StandardButton.Ok,\n        defaultButton: QMessageBox.StandardButton = QMessageBox.StandardButton.NoButton,\n        rich_text: bool = False,\n        checkbox: Optional[bool] = None\n) -> int:\n    custom_buttons = []\n    standard_buttons = QMessageBox.StandardButton.NoButton\n    if buttons:\n        if not isinstance(buttons, list):\n            buttons = [buttons]\n        for button in buttons:\n            if isinstance(button, QMessageBox.StandardButton):\n                standard_buttons |= button\n            else:\n                custom_buttons.append(button)\n    if type(icon) is QPixmap:\n        d = QMessageBox(QMessageBox.Icon.Information, title, str(text), standard_buttons, parent)\n        d.setIconPixmap(icon)\n    else:\n        d = QMessageBox(icon, title, str(text), standard_buttons, parent)\n    for button, role, _ in custom_buttons:\n        d.addButton(button, role)\n    d.setWindowModality(Qt.WindowModality.WindowModal)\n    d.setDefaultButton(defaultButton)\n    if rich_text:\n        d.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.LinksAccessibleByMouse)\n        # set AutoText instead of RichText\n        # AutoText lets Qt figure out whether to render as rich text.\n        # e.g. if text is actually plain text and uses \"\\n\" newlines;\n        #      and we set RichText here, newlines would be swallowed\n        d.setTextFormat(Qt.TextFormat.AutoText)\n    else:\n        d.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n        d.setTextFormat(Qt.TextFormat.PlainText)\n    if checkbox is not None:\n        d.setCheckBox(checkbox)\n    result = d.exec()\n    for button, _, value in custom_buttons:\n        if button == d.clickedButton():\n            return value\n    return result\n\n\nclass WindowModalDialog(QDialog, MessageBoxMixin):\n    '''Handy wrapper; window modal dialogs are better for our multi-window\n    daemon model as other wallet windows can still be accessed.'''\n    def __init__(self, parent, title=None):\n        QDialog.__init__(self, parent)\n        self.setWindowModality(Qt.WindowModality.WindowModal)\n        if title:\n            self.setWindowTitle(title)\n\n\nclass WaitingDialog(WindowModalDialog):\n    '''Shows a please wait dialog whilst running a task.  It is not\n    necessary to maintain a reference to this dialog.'''\n    def __init__(self, parent: QWidget, message: str, task, on_success=None, on_error=None, on_cancel=None):\n        assert parent\n        if isinstance(parent, MessageBoxMixin):\n            parent = parent.top_level_window()\n        WindowModalDialog.__init__(self, parent, _(\"Please wait\"))\n        self.message_label = QLabel(message)\n        vbox = QVBoxLayout(self)\n        vbox.addWidget(self.message_label)\n        if on_cancel:\n            self.cancel_button = CancelButton(self)\n            self.cancel_button.clicked.connect(on_cancel)\n            vbox.addLayout(Buttons(self.cancel_button))\n        self.accepted.connect(self.on_accepted)\n        self.show()\n        self.thread = TaskThread(self)\n        self.thread.finished.connect(self.deleteLater)  # see #3956\n        self.thread.add(task, on_success, self.accept, on_error)\n\n    def wait(self):\n        self.thread.wait()\n\n    def on_accepted(self):\n        self.thread.stop()\n\n    def update(self, msg):\n        print(msg)\n        self.message_label.setText(msg)\n\n\nclass RunCoroutineDialog(WaitingDialog):\n\n    def __init__(self, parent: QWidget, message: str, coroutine):\n        from electrum import util\n        import asyncio\n        import concurrent.futures\n        loop = util.get_asyncio_loop()\n        assert util.get_running_loop() != loop, 'must not be called from asyncio thread'\n        self._exception = None\n        self._result = None\n        self._future = asyncio.run_coroutine_threadsafe(coroutine, loop)\n        def task():\n            try:\n                self._result = self._future.result()\n            except concurrent.futures.CancelledError:\n                self._exception = UserCancelled\n            except Exception as e:\n                self._exception = e\n        WaitingDialog.__init__(self, parent, message, task, on_cancel=self._future.cancel)\n\n    def run(self):\n        self.exec()\n        if self._exception:\n            raise self._exception\n        else:\n            return self._result\n\n\ndef line_dialog(parent, title, label, ok_label, default=None):\n    dialog = WindowModalDialog(parent, title)\n    dialog.setMinimumWidth(500)\n    l = QVBoxLayout()\n    dialog.setLayout(l)\n    l.addWidget(QLabel(label))\n    txt = QLineEdit()\n    if default:\n        txt.setText(default)\n    l.addWidget(txt)\n    l.addLayout(Buttons(CancelButton(dialog), OkButton(dialog, ok_label)))\n    if dialog.exec():\n        return txt.text()\n\n\ndef text_dialog(\n        *,\n        parent,\n        title,\n        header_layout,\n        ok_label,\n        default=None,\n        allow_multi=False,\n        config: 'SimpleConfig',\n):\n    from .qrtextedit import ScanQRTextEdit\n    dialog = WindowModalDialog(parent, title)\n    dialog.setMinimumWidth(600)\n    l = QVBoxLayout()\n    dialog.setLayout(l)\n    if isinstance(header_layout, str):\n        l.addWidget(QLabel(header_layout))\n    else:\n        l.addLayout(header_layout)\n    txt = ScanQRTextEdit(allow_multi=allow_multi, config=config)\n    if default:\n        txt.setText(default)\n    l.addWidget(txt)\n    l.addLayout(Buttons(CancelButton(dialog), OkButton(dialog, ok_label)))\n    if dialog.exec():\n        return txt.toPlainText()\n\n\nclass ChoiceWidget(QWidget):\n    \"\"\"Renders a list of ChoiceItems as a radiobuttons group.\n    Callers can pre-select an item by key, through the 'default_key' parameter.\n    The selected item is made available by index (selected_index),\n    by key (selected_key) and by Choice (selected_item).\n    \"\"\"\n\n    itemSelected = pyqtSignal([int], arguments=['index'])\n\n    def __init__(\n        self,\n        *,\n        message: Optional[str] = None,\n        choices: Sequence[ChoiceItem] = None,\n        default_key: Optional[Any] = None,\n    ):\n        QWidget.__init__(self)\n        vbox = QVBoxLayout()\n        self.setLayout(vbox)\n\n        if choices is None:\n            choices = []\n\n        self.selected_index = -1   # type: int\n        self.selected_item = None  # type: Optional[ChoiceItem]\n        self.selected_key = None   # type: Optional[Any]\n        self.choices = choices     # type: Sequence[ChoiceItem]\n\n        if message and len(message) > 50:\n            vbox.addWidget(WWLabel(message))\n            message = \"\"\n        gb2 = QGroupBox(message)\n        vbox.addWidget(gb2)\n        vbox2 = QVBoxLayout()\n        gb2.setLayout(vbox2)\n        self.group = group = QButtonGroup()\n        assert isinstance(choices, list)\n        for i, c in enumerate(choices):\n            assert isinstance(c, ChoiceItem), f\"{c=!r}\"\n            button = QRadioButton(gb2)\n            button.setText(c.label)\n            vbox2.addWidget(button)\n            group.addButton(button)\n            group.setId(button, i)\n            if (i == 0 and default_key is None) or c.key == default_key:\n                self.selected_index = i\n                self.selected_item = c\n                self.selected_key = c.key\n                button.setChecked(True)\n        group.buttonClicked.connect(self.on_selected)\n\n    def on_selected(self, button):\n        self.selected_index = self.group.id(button)\n        self.selected_item = self.choices[self.selected_index]\n        self.selected_key = self.choices[self.selected_index].key\n        self.itemSelected.emit(self.selected_index)\n\n    def select(self, key):\n        for i, c in enumerate(self.choices):\n            if key == c.key:\n                self.group.button(i).click()\n\n\nclass ResizableStackedWidget(QWidget):\n    \"\"\"Simple alternative to QStackedWidget, as QStackedWidget always resizes to the largest\n       widget in the stack, leaving ugly scrollbars where they're not needed.\"\"\"\n    def __init__(self, parent):\n        super().__init__(parent)\n        self.setLayout(QVBoxLayout())\n        self.widgets = []\n        self.current_index = -1\n\n    def sizeHint(self) -> QSize:\n        if not self.count() or not self.currentWidget():\n            return super().sizeHint()\n        return self.currentWidget().sizeHint()\n\n    def addWidget(self, widget: QWidget) -> int:\n        self.widgets.append(widget)\n        self.layout().addWidget(widget)\n        if len(self.widgets) == 1:  # first widget?\n            self.current_index = 0\n        self.showCurrentWidget()\n        return len(self.widgets) - 1\n\n    def removeWidget(self, widget: QWidget):\n        i = self.widgets.index(widget)\n        self.widgets.remove(widget)\n        self.layout().removeWidget(widget)\n        if self.current_index >= i:\n            self.current_index -= 1\n            if self.current_index == self.count() - 1:\n                self.showCurrentWidget()\n\n    def setCurrentIndex(self, index: int):\n        assert isinstance(index, int)\n        assert 0 <= index < len(self.widgets), f'invalid widget index {index}'\n        self.current_index = index\n        self.showCurrentWidget()\n\n    def currentWidget(self) -> Optional[QWidget]:\n        if self.current_index < 0:\n            return None\n        return self.widgets[self.current_index]\n\n    def showCurrentWidget(self):\n        if not self.widgets:\n            return\n\n        for i, k in enumerate(self.widgets):\n            if i == self.current_index:\n                k.show()\n            else:\n                k.hide()\n\n    def count(self) -> int:\n        return len(self.widgets)\n\n\nclass VLine(QFrame):\n    \"\"\"Vertical line separator\"\"\"\n    def __init__(self):\n        super(VLine, self).__init__()\n        self.setFrameShape(QFrame.Shape.VLine)\n        self.setFrameShadow(QFrame.Shadow.Sunken)\n        self.setLineWidth(1)\n\n\ndef address_field(addresses, *, btn_text: str = None):\n    if btn_text is None:\n        btn_text = _('Get wallet address')\n    hbox = QHBoxLayout()\n    address_e = QLineEdit()\n    if addresses and len(addresses) > 0:\n        address_e.setText(addresses[0])\n    else:\n        addresses = []\n\n    def func():\n        try:\n            i = addresses.index(str(address_e.text())) + 1\n            i = i % len(addresses)\n            address_e.setText(addresses[i])\n        except ValueError:\n            # the user might have changed address_e to an\n            # address not in the wallet (or to something that isn't an address)\n            if addresses and len(addresses) > 0:\n                address_e.setText(addresses[0])\n    button = QPushButton(btn_text)\n    button.clicked.connect(func)\n    hbox.addWidget(button)\n    hbox.addWidget(address_e)\n    return hbox, address_e\n\n\ndef filename_field(parent, config, defaultname, select_msg):\n    vbox = QVBoxLayout()\n    vbox.addWidget(QLabel(_(\"Format\")))\n    gb = QGroupBox(\"format\", parent)\n    b1 = QRadioButton(gb)\n    b1.setText(_(\"CSV\"))\n    b1.setChecked(True)\n    b2 = QRadioButton(gb)\n    b2.setText(_(\"json\"))\n    vbox.addWidget(b1)\n    vbox.addWidget(b2)\n\n    hbox = QHBoxLayout()\n\n    directory = config.IO_DIRECTORY\n    path = os.path.join(directory, defaultname)\n    filename_e = QLineEdit()\n    filename_e.setText(path)\n\n    def func():\n        text = filename_e.text()\n        _filter = \"*.csv\" if defaultname.endswith(\".csv\") else \"*.json\" if defaultname.endswith(\".json\") else None\n        p = getSaveFileName(\n            parent=None,\n            title=select_msg,\n            filename=text,\n            filter=_filter,\n            config=config,\n        )\n        if p:\n            filename_e.setText(p)\n\n    button = QPushButton(_('File'))\n    button.clicked.connect(func)\n    hbox.addWidget(button)\n    hbox.addWidget(filename_e)\n    vbox.addLayout(hbox)\n\n    def set_csv(v):\n        text = filename_e.text()\n        text = text.replace(\".json\",\".csv\") if v else text.replace(\".csv\",\".json\")\n        filename_e.setText(text)\n\n    b1.clicked.connect(lambda: set_csv(True))\n    b2.clicked.connect(lambda: set_csv(False))\n\n    return vbox, filename_e, b1\n\n\ndef get_icon_qrcode() -> QIcon:\n    name = \"qrcode_white.png\" if ColorScheme.dark_scheme else \"qrcode.png\"\n    return read_QIcon(name)\n\n\ndef get_icon_camera() -> QIcon:\n    name = \"camera_white.png\" if ColorScheme.dark_scheme else \"camera_dark.png\"\n    return read_QIcon(name)\n\n\ndef pubkey_to_q_icon(server_pubkey: str) -> QIcon:\n    color = QColor(*pubkey_to_rgb_color(server_pubkey))\n    color_pixmap = QPixmap(100, 100)\n    color_pixmap.fill(color)\n    return QIcon(color_pixmap)\n\n\ndef add_input_actions_to_context_menu(gih: 'GenericInputHandler', m: QMenu) -> None:\n    if gih.on_qr_from_camera_input_btn:\n        m.addAction(get_icon_camera(), _(\"Read QR code with camera\"), gih.on_qr_from_camera_input_btn)\n    if gih.on_qr_from_screenshot_input_btn:\n        m.addAction(read_QIcon(\"picture_in_picture.png\"), _(\"Read QR code from screen\"), gih.on_qr_from_screenshot_input_btn)\n    if gih.on_qr_from_file_input_btn:\n        m.addAction(read_QIcon(\"qr_file.png\"), _(\"Read QR code from file\"), gih.on_qr_from_file_input_btn)\n    if gih.on_input_file:\n        m.addAction(read_QIcon(\"file.png\"), _(\"Read text from file\"), gih.on_input_file)\n\n\ndef scan_qr_from_screenshot() -> QrCodeResult:\n    from .qrreader import scan_qr_from_image\n    screenshots = [screen.grabWindow(0).toImage()\n                   for screen in QApplication.instance().screens()]\n    if all(screen.allGray() for screen in screenshots):\n        raise UserFacingException(_(\"Failed to take screenshot.\"))\n    scanned_qr = None\n    for screenshot in screenshots:\n        try:\n            scan_result = scan_qr_from_image(screenshot)\n        except MissingQrDetectionLib as e:\n            raise UserFacingException(_(\"Unable to scan image.\") + \"\\n\" + repr(e))\n        if len(scan_result) > 0:\n            if (scanned_qr is not None) or len(scan_result) > 1:\n                raise UserFacingException(_(\"More than one QR code was found on the screen.\"))\n            scanned_qr = scan_result\n    if scanned_qr is None:\n        raise UserFacingException(_(\"No QR code was found on the screen.\"))\n    assert len(scanned_qr) == 1, f\"{len(scanned_qr)=}, expected 1\"\n    return scanned_qr[0]\n\n\nclass GenericInputHandler:\n    on_qr_from_camera_input_btn: Callable[[], None] = None\n    on_qr_from_screenshot_input_btn: Callable[[], None] = None\n    on_qr_from_file_input_btn: Callable[[], None] = None\n    on_input_file: Callable[[], None] = None\n\n    def input_qr_from_camera(\n            self,\n            *,\n            config: 'SimpleConfig',\n            allow_multi: bool = False,\n            show_error: Callable[[str], None],\n            setText: Callable[[str], None] = None,\n            parent: QWidget = None,\n    ) -> None:\n        if setText is None:\n            setText = self.setText\n        def cb(success: bool, error: str, data: Optional[str]):\n            if not success:\n                if error:\n                    show_error(error)\n                return\n            if not data:\n                data = ''\n            try:\n                if allow_multi:\n                    text = self.text()\n                    if data in text:\n                        return\n                    if text and not text.endswith('\\n'):\n                        text += '\\n'\n                    text += data\n                    text += '\\n'\n                    setText(text)\n                else:\n                    new_text = data\n                    setText(new_text)\n            except Exception as e:\n                show_error(_('Invalid payment identifier in QR') + ':\\n' + repr(e))\n\n        from .qrreader import scan_qrcode_from_camera\n        if parent is None:\n            parent = self if isinstance(self, QWidget) else None\n        scan_qrcode_from_camera(parent=parent, config=config, callback=cb)\n\n    def input_qr_from_screenshot(\n            self,\n            *,\n            allow_multi: bool = False,\n            show_error: Callable[[str], None],\n            setText: Callable[[str], None] = None,\n    ) -> None:\n        if setText is None:\n            setText = self.setText\n        try:\n            scanned_qr = scan_qr_from_screenshot()\n        except UserFacingException as e:\n            show_error(str(e))\n            return\n        data = scanned_qr.data\n        try:\n            if allow_multi:\n                text = self.text()\n                if data in text:\n                    return\n                if text and not text.endswith('\\n'):\n                    text += '\\n'\n                text += data\n                text += '\\n'\n                setText(text)\n            else:\n                new_text = data\n                setText(new_text)\n        except Exception as e:\n            show_error(_('Invalid payment identifier in QR') + ':\\n' + repr(e))\n\n    def input_file(\n            self,\n            *,\n            config: 'SimpleConfig',\n            show_error: Callable[[str], None],\n            setText: Callable[[str], None] = None,\n    ) -> None:\n        if setText is None:\n            setText = self.setText\n        fileName = getOpenFileName(\n            parent=None,\n            title='select file',\n            # trying to open non-text things like pdfs makes electrum freeze\n            filter=\"Text files (*.txt *.csv);;All files (*)\",\n            config=config,\n        )\n        if not fileName:\n            return\n        try:\n            try:\n                with open(fileName, \"r\") as f:\n                    data = f.read()\n            except UnicodeError as e:\n                with open(fileName, \"rb\") as f:\n                    data = f.read()\n                data = data.hex()\n        except BaseException as e:\n            show_error(_('Error opening file') + ':\\n' + repr(e))\n        else:\n            try:\n                setText(data)\n            except Exception as e:\n                show_error(_('Invalid payment identifier in file') + ':\\n' + repr(e))\n\n    def input_qr_from_file(\n        self,\n        *,\n        allow_multi: bool = False,\n        config: 'SimpleConfig',\n        show_error: Callable[[str], None],\n        setText: Callable[[str], None] = None,\n    ):\n        from .qrreader import scan_qr_from_image\n        if setText is None:\n            setText = self.setText\n\n        file_name = getOpenFileName(\n            parent=None,\n            title=_(\"Select image file\"),\n            config=config,\n            filter=\"Image files (*.png *.jpg *.jpeg *.bmp);;\",\n        )\n        if not file_name:\n            return\n        image = QImage(file_name)\n        if image.isNull():\n            show_error(_(\"Failed to open image file.\"))\n            return\n        try:\n            scan_result: Sequence[QrCodeResult] = scan_qr_from_image(image)\n        except MissingQrDetectionLib as e:\n            show_error(_(\"Unable to scan image.\") + \"\\n\" + repr(e))\n            return\n        if len(scan_result) < 1:\n            show_error(_(\"No QR code was found in the image.\"))\n            return\n        if len(scan_result) > 1 and not allow_multi:\n            show_error(_(\"More than one QR code was found in the image.\"))\n            return\n\n        if len(scan_result) > 1:\n            result_text = \"\\n\".join([r.data for r in scan_result])\n        else:\n            result_text = scan_result[0].data\n\n        try:\n            setText(result_text)\n        except Exception as e:\n            show_error(_(\"Couldn't set result\") + ':\\n' + repr(e))\n\n    def input_paste_from_clipboard(\n            self,\n            *,\n            setText: Callable[[str], None] = None,\n    ) -> None:\n        if setText is None:\n            setText = self.setText\n        app = QApplication.instance()\n        setText(app.clipboard().text())\n\n\nclass OverlayControlMixin(GenericInputHandler):\n    STYLE_SHEET_COMMON = '''\n    QPushButton { border-width: 1px; padding: 0px; margin: 0px; }\n    '''\n\n    STYLE_SHEET_LIGHT = '''\n    QPushButton { border: 1px solid transparent; }\n    QPushButton:hover { border: 1px solid #3daee9; }\n    '''\n\n    def __init__(self, middle: bool = False):\n        GenericInputHandler.__init__(self)\n        assert isinstance(self, QWidget)\n        assert isinstance(self, OverlayControlMixin)  # only here for type-hints in IDE\n        self.middle = middle\n        self.overlay_widget = QWidget(self)\n        style_sheet = self.STYLE_SHEET_COMMON\n        if not ColorScheme.dark_scheme:\n            style_sheet = style_sheet + self.STYLE_SHEET_LIGHT\n        self.overlay_widget.setStyleSheet(style_sheet)\n        self.overlay_layout = QHBoxLayout(self.overlay_widget)\n        self.overlay_layout.setContentsMargins(0, 0, 0, 0)\n        self.overlay_layout.setSpacing(1)\n        self._updateOverlayPos()\n\n    def resizeEvent(self, e):\n        super().resizeEvent(e)\n        self._updateOverlayPos()\n\n    def _updateOverlayPos(self):\n        frame_width = self.style().pixelMetric(QStyle.PixelMetric.PM_DefaultFrameWidth)\n        overlay_size = self.overlay_widget.sizeHint()\n        x = self.rect().right() - frame_width - overlay_size.width()\n        y = self.rect().bottom() - overlay_size.height()\n        middle = self.middle\n        if hasattr(self, 'document'):\n            # Keep the buttons centered if we have less than 2 lines in the editor\n            line_spacing = QFontMetrics(self.document().defaultFont()).lineSpacing()\n            if self.rect().height() < (line_spacing * 2):\n                middle = True\n        y = (y / 2) + frame_width if middle else y - frame_width\n        if hasattr(self, 'verticalScrollBar') and self.verticalScrollBar().isVisible():\n            scrollbar_width = self.style().pixelMetric(QStyle.PixelMetric.PM_ScrollBarExtent)\n            x -= scrollbar_width\n        self.overlay_widget.move(int(x), int(y))\n\n    def addWidget(self, widget: QWidget):\n        # The old code positioned the items the other way around, so we just insert at position 0 instead\n        self.overlay_layout.insertWidget(0, widget)\n\n    def addButton(self, icon: QIcon, on_click, tooltip: str) -> QPushButton:\n        button = QPushButton(self.overlay_widget)\n        button.setToolTip(tooltip)\n        button.setIcon(icon)\n        button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))\n        button.clicked.connect(on_click)\n        self.addWidget(button)\n        return button\n\n    def addCopyButton(self):\n        def on_copy():\n            app = QApplication.instance()\n            app.clipboard().setText(self.text())\n            QToolTip.showText(QCursor.pos(), _(\"Text copied to clipboard\"), self)\n        self.addButton(read_QIcon(\"copy.png\"), on_copy, _(\"Copy to clipboard\"))\n\n    def addPasteButton(\n            self,\n            *,\n            setText: Callable[[str], None] = None,\n    ):\n        input_paste_from_clipboard = partial(\n            self.input_paste_from_clipboard,\n            setText=setText,\n        )\n        self.addButton(read_QIcon(\"copy.png\"), input_paste_from_clipboard, _(\"Paste from clipboard\"))\n\n    def add_qr_show_button(self, *, config: 'SimpleConfig', title: Optional[str] = None):\n        if title is None:\n            title = _(\"QR code\")\n\n        def qr_show():\n            from .qrcodewidget import QRDialog\n            try:\n                s = str(self.text())\n            except Exception:\n                s = self.text()\n            if not s:\n                return\n            QRDialog(\n                data=s,\n                parent=self,\n                title=title,\n                config=config,\n            ).exec()\n\n        self.addButton(get_icon_qrcode(), qr_show, _(\"Show as QR code\"))\n        # side-effect: we export this method:\n        self.on_qr_show_btn = qr_show\n\n    def add_qr_input_from_camera_button(\n            self,\n            *,\n            config: 'SimpleConfig',\n            allow_multi: bool = False,\n            show_error: Callable[[str], None],\n            setText: Callable[[str], None] = None,\n    ):\n        input_qr_from_camera = partial(\n            self.input_qr_from_camera,\n            config=config,\n            allow_multi=allow_multi,\n            show_error=show_error,\n            setText=setText,\n        )\n        self.addButton(get_icon_camera(), input_qr_from_camera, _(\"Read QR code with camera\"))\n        # side-effect: we export these methods:\n        self.on_qr_from_camera_input_btn = input_qr_from_camera\n\n    def add_file_input_button(\n            self,\n            *,\n            config: 'SimpleConfig',\n            show_error: Callable[[str], None],\n            setText: Callable[[str], None] = None,\n    ) -> None:\n        input_file = partial(\n            self.input_file,\n            config=config,\n            show_error=show_error,\n            setText=setText,\n        )\n        self.addButton(read_QIcon(\"file.png\"), input_file, _(\"Read file\"))\n\n    def add_menu_button(\n            self,\n            *,\n            options: Sequence[Tuple[Optional[Union[str, QIcon]], str, Callable[[], None]]],  # list of (icon, text, cb)\n            icon: Optional[QIcon] = None,\n            tooltip: Optional[str] = None,\n    ):\n        if icon is None:\n            icon_name = \"menu_vertical_white.png\" if ColorScheme.dark_scheme else \"menu_vertical.png\"\n            icon = read_QIcon(icon_name)\n        if tooltip is None:\n            tooltip = _(\"Other options\")\n        btn = self.addButton(icon, lambda: None, tooltip)\n        menu = QMenu()\n        for opt_icon, opt_text, opt_cb in options:\n            if opt_icon is None:\n                menu.addAction(opt_text, opt_cb)\n            else:\n                opt_icon = read_QIcon(opt_icon) if isinstance(opt_icon, str) else opt_icon\n                menu.addAction(opt_icon, opt_text, opt_cb)\n        btn.setMenu(menu)\n\n\nclass ButtonsLineEdit(OverlayControlMixin, QLineEdit):\n    def __init__(self, text=None):\n        QLineEdit.__init__(self, text)\n        OverlayControlMixin.__init__(self, middle=True)\n\n\nclass ShowQRLineEdit(ButtonsLineEdit):\n    \"\"\" read-only line with qr and copy buttons \"\"\"\n    def __init__(self, text: str, config, title=None):\n        ButtonsLineEdit.__init__(self, text)\n        self.setReadOnly(True)\n        self.setFont(QFont(MONOSPACE_FONT))\n        self.add_qr_show_button(config=config, title=title)\n        self.addCopyButton()\n\n\nclass ButtonsTextEdit(OverlayControlMixin, QPlainTextEdit):\n    def __init__(self, text=None):\n        QPlainTextEdit.__init__(self, text)\n        OverlayControlMixin.__init__(self)\n        self.setText = self.setPlainText\n        self.text = self.toPlainText\n\n\nclass PasswordLineEdit(QLineEdit):\n    def __init__(self, *args, **kwargs):\n        QLineEdit.__init__(self, *args, **kwargs)\n        self.setEchoMode(QLineEdit.EchoMode.Password)\n\n    def clear(self):\n        # Try to actually overwrite the memory.\n        # This is really just a best-effort thing...\n        self.setText(len(self.text()) * \" \")\n        super().clear()\n\n\nclass ColorSchemeItem:\n    def __init__(self, fg_color, bg_color):\n        self.colors = (fg_color, bg_color)\n\n    def _get_color(self, background):\n        return self.colors[(int(background) + int(ColorScheme.dark_scheme)) % 2]\n\n    def as_stylesheet(self, background=False):\n        css_prefix = \"background-\" if background else \"\"\n        color = self._get_color(background)\n        return \"QWidget {{ {}color:{}; }}\".format(css_prefix, color)\n\n    def as_color(self, background=False):\n        color = self._get_color(background)\n        return QColor(color)\n\n\nclass ColorScheme:\n    dark_scheme = False\n\n    GREEN = ColorSchemeItem(\"#117c11\", \"#8af296\")\n    YELLOW = ColorSchemeItem(\"#897b2a\", \"#ffff00\")\n    RED = ColorSchemeItem(\"#7c1111\", \"#f18c8c\")\n    BLUE = ColorSchemeItem(\"#123b7c\", \"#8cb3f2\")\n    LIGHTBLUE = ColorSchemeItem(\"black\", \"#d0f0ff\")\n    DEFAULT = ColorSchemeItem(\"black\", \"white\")\n    GRAY = ColorSchemeItem(\"gray\", \"gray\")\n    ORANGE = ColorSchemeItem(\"#ff9b45\", \"#ff9b45\")\n\n    @staticmethod\n    def has_dark_background(widget):\n        brightness = sum(widget.palette().color(QPalette.ColorRole.Window).getRgb()[0:3])\n        return brightness < (255*3/2)\n\n    @staticmethod\n    def update_from_widget(widget, force_dark=False):\n        ColorScheme.dark_scheme = bool(force_dark or ColorScheme.has_dark_background(widget))\n\n\nclass AcceptFileDragDrop:\n    def __init__(self, file_type=\"\"):\n        assert isinstance(self, QWidget)\n        self.setAcceptDrops(True)\n        self.file_type = file_type\n\n    def validateEvent(self, event):\n        if not event.mimeData().hasUrls():\n            event.ignore()\n            return False\n        for url in event.mimeData().urls():\n            if not url.toLocalFile().endswith(self.file_type):\n                event.ignore()\n                return False\n        event.accept()\n        return True\n\n    def dragEnterEvent(self, event):\n        self.validateEvent(event)\n\n    def dragMoveEvent(self, event):\n        if self.validateEvent(event):\n            event.setDropAction(Qt.DropAction.CopyAction)\n\n    def dropEvent(self, event):\n        if self.validateEvent(event):\n            for url in event.mimeData().urls():\n                self.onFileAdded(url.toLocalFile())\n\n    def onFileAdded(self, fn):\n        raise NotImplementedError()\n\n\ndef import_meta_gui(electrum_window: 'ElectrumWindow', title, importer, on_success):\n    filter_ = \"JSON (*.json);;All files (*)\"\n    filename = getOpenFileName(\n        parent=electrum_window,\n        title=_(\"Open {} file\").format(title),\n        filter=filter_,\n        config=electrum_window.config,\n    )\n    if not filename:\n        return\n    try:\n        importer(filename)\n    except FileImportFailed as e:\n        electrum_window.show_critical(str(e))\n    else:\n        electrum_window.show_message(_(\"Your {} were successfully imported\").format(title))\n        on_success()\n\n\ndef export_meta_gui(electrum_window: 'ElectrumWindow', title, exporter):\n    filter_ = \"JSON (*.json);;All files (*)\"\n    filename = getSaveFileName(\n        parent=electrum_window,\n        title=_(\"Select file to save your {}\").format(title),\n        filename='electrum_{}.json'.format(title),\n        filter=filter_,\n        config=electrum_window.config,\n    )\n    if not filename:\n        return\n    try:\n        exporter(filename)\n    except FileExportFailed as e:\n        electrum_window.show_critical(str(e))\n    else:\n        electrum_window.show_message(_(\"Your {0} were exported to '{1}'\")\n                                     .format(title, str(filename)))\n\n\ndef getOpenFileName(*, parent, title, filter=\"\", config: 'SimpleConfig') -> Optional[str]:\n    \"\"\"Custom wrapper for getOpenFileName that remembers the path selected by the user.\"\"\"\n    directory = config.IO_DIRECTORY\n    fileName, __ = QFileDialog.getOpenFileName(parent, title, directory, filter)\n    if fileName and directory != os.path.dirname(fileName):\n        config.IO_DIRECTORY = os.path.dirname(fileName)\n    return fileName\n\n\ndef getSaveFileName(\n        *,\n        parent,\n        title,\n        filename,\n        filter=\"\",\n        default_extension: str = None,\n        default_filter: str = None,\n        config: 'SimpleConfig',\n) -> Optional[str]:\n    \"\"\"Custom wrapper for getSaveFileName that remembers the path selected by the user.\"\"\"\n    directory = config.IO_DIRECTORY\n    path = os.path.join(directory, filename)\n\n    file_dialog = QFileDialog(parent, title, path, filter)\n    file_dialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)\n    if default_extension:\n        # note: on MacOS, the selected filter's first extension seems to have priority over this...\n        file_dialog.setDefaultSuffix(default_extension)\n    if default_filter:\n        assert default_filter in filter, f\"default_filter={default_filter!r} does not appear in filter={filter!r}\"\n        file_dialog.selectNameFilter(default_filter)\n    if file_dialog.exec() != QDialog.DialogCode.Accepted:\n        return None\n\n    selected_path = file_dialog.selectedFiles()[0]\n    if selected_path and directory != os.path.dirname(selected_path):\n        config.IO_DIRECTORY = os.path.dirname(selected_path)\n    return selected_path\n\n\ndef icon_path(icon_basename: str):\n    return resource_path('gui', 'icons', icon_basename)\n\n\ndef internal_plugin_icon_path(plugin_name, icon_basename: str):\n    return resource_path('plugins', plugin_name, icon_basename)\n\n\n@lru_cache(maxsize=1000)\ndef read_QIcon(icon_basename: str) -> QIcon:\n    return QIcon(icon_path(icon_basename))\n\n\ndef read_QPixmap_from_bytes(b: bytes) -> QPixmap:\n    qp = QPixmap()\n    qp.loadFromData(b)\n    return qp\n\n\ndef read_QIcon_from_bytes(b: bytes) -> QIcon:\n    qp = read_QPixmap_from_bytes(b)\n    return QIcon(qp)\n\n\nclass IconLabel(QWidget):\n    HorizontalSpacing = 2\n    def __init__(self, *, text='', final_stretch=True, reverse=False, hide_if_empty=False):\n        super(QWidget, self).__init__()\n        self.hide_if_empty = hide_if_empty\n        size = max(16, font_height())\n        self.icon_size = QSize(size, size)\n        layout = QHBoxLayout()\n        layout.setContentsMargins(0, 0, 0, 0)\n        self.setLayout(layout)\n        self.icon = QLabel()\n        self.label = QLabel(text)\n        self.label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n        layout.addWidget(self.icon if reverse else self.label)\n        layout.addSpacing(self.HorizontalSpacing)\n        layout.addWidget(self.label if reverse else self.icon)\n        if final_stretch:\n            layout.addStretch()\n        self.setText(text)\n\n    def setText(self, text):\n        self.label.setText(text)\n        if self.hide_if_empty:\n            self.setVisible(bool(text))\n\n    def setIcon(self, icon):\n        self.icon.setPixmap(icon.pixmap(self.icon_size))\n        self.icon.repaint()  # macOS hack for #6269\n\n\ndef char_width_in_lineedit() -> int:\n    char_width = QFontMetrics(QLineEdit().font()).averageCharWidth()\n    # 'averageCharWidth' seems to underestimate on Windows, hence 'max()'\n    return max(9, char_width)\n\n\ndef font_height(widget: QWidget = None) -> int:\n    if widget is None:\n        widget = QLabel()\n    return QFontMetrics(widget.font()).height()\n\n\ndef webopen(url: str):\n    if sys.platform == 'linux' and os.environ.get('APPIMAGE'):\n        # When on Linux webbrowser.open can fail in AppImage because it can't find the correct libdbus.\n        # We just fork the process and unset LD_LIBRARY_PATH before opening the URL.\n        # See #5425\n        if os.fork() == 0:\n            del os.environ['LD_LIBRARY_PATH']\n            webbrowser.open(url)\n            os._exit(0)\n    else:\n        webbrowser.open(url)\n\n\nclass FixedAspectRatioLayout(QLayout):\n    def __init__(self, parent: QWidget = None, aspect_ratio: float = 1.0):\n        super().__init__(parent)\n        self.aspect_ratio = aspect_ratio\n        self.items: List[QLayoutItem] = []\n\n    def set_aspect_ratio(self, aspect_ratio: float = 1.0):\n        self.aspect_ratio = aspect_ratio\n        self.update()\n\n    def addItem(self, item: QLayoutItem):\n        self.items.append(item)\n\n    def count(self) -> int:\n        return len(self.items)\n\n    def itemAt(self, index: int) -> QLayoutItem:\n        if index >= len(self.items):\n            return None\n        return self.items[index]\n\n    def takeAt(self, index: int) -> QLayoutItem:\n        if index >= len(self.items):\n            return None\n        return self.items.pop(index)\n\n    def _get_contents_margins_size(self) -> QSize:\n        margins = self.contentsMargins()\n        return QSize(margins.left() + margins.right(), margins.top() + margins.bottom())\n\n    def setGeometry(self, rect: QRect):\n        super().setGeometry(rect)\n        if not self.items:\n            return\n\n        contents = self.contentsRect()\n        if contents.height() > 0:\n            c_aratio = contents.width() / contents.height()\n        else:\n            c_aratio = 1\n        s_aratio = self.aspect_ratio\n        item_rect = QRect(QPoint(0, 0), QSize(\n            contents.width() if c_aratio < s_aratio else int(contents.height() * s_aratio),\n            contents.height() if c_aratio > s_aratio else int(contents.width() / s_aratio)\n        ))\n\n        content_margins = self.contentsMargins()\n        free_space = contents.size() - item_rect.size()\n\n        for item in self.items:\n            if free_space.width() > 0 and not item.alignment() & Qt.AlignmentFlag.AlignLeft:\n                if item.alignment() & Qt.AlignmentFlag.AlignRight:\n                    item_rect.moveRight(contents.width() + content_margins.right())\n                else:\n                    item_rect.moveLeft(content_margins.left() + (free_space.width() // 2))\n            else:\n                item_rect.moveLeft(content_margins.left())\n\n            if free_space.height() > 0 and not item.alignment() & Qt.AlignmentFlag.AlignTop:\n                if item.alignment() & Qt.AlignmentFlag.AlignBottom:\n                    item_rect.moveBottom(contents.height() + content_margins.bottom())\n                else:\n                    item_rect.moveTop(content_margins.top() + (free_space.height() // 2))\n            else:\n                item_rect.moveTop(content_margins.top())\n\n            item.widget().setGeometry(item_rect)\n\n    def sizeHint(self) -> QSize:\n        result = QSize()\n        for item in self.items:\n            result = result.expandedTo(item.sizeHint())\n        return self._get_contents_margins_size() + result\n\n    def minimumSize(self) -> QSize:\n        result = QSize()\n        for item in self.items:\n            result = result.expandedTo(item.minimumSize())\n        return self._get_contents_margins_size() + result\n\n    def expandingDirections(self) -> Qt.Orientation:\n        return Qt.Orientation.Horizontal | Qt.Orientation.Vertical\n\n\ndef QColorLerp(a: QColor, b: QColor, t: float):\n    \"\"\"\n    Blends two QColors. t=0 returns a. t=1 returns b. t=0.5 returns evenly mixed.\n    \"\"\"\n    t = max(min(t, 1.0), 0.0)\n    i_t = 1.0 - t\n    return QColor(\n        int((a.red()   * i_t) + (b.red()   * t)),\n        int((a.green() * i_t) + (b.green() * t)),\n        int((a.blue()  * i_t) + (b.blue()  * t)),\n        int((a.alpha() * i_t) + (b.alpha() * t)),\n    )\n\n\nclass ImageGraphicsEffect(QObject):\n    \"\"\"\n    Applies a QGraphicsEffect to a QImage\n    \"\"\"\n\n    def __init__(self, parent: QObject, effect: QGraphicsEffect):\n        super().__init__(parent)\n        assert effect, 'effect must be set'\n        self.effect = effect\n        self.graphics_scene = QGraphicsScene()\n        self.graphics_item = QGraphicsPixmapItem()\n        self.graphics_item.setGraphicsEffect(effect)\n        self.graphics_scene.addItem(self.graphics_item)\n\n    def apply(self, image: QImage):\n        assert image, 'image must be set'\n        result = QImage(image.size(), QImage.Format.Format_ARGB32)\n        result.fill(Qt.GlobalColor.transparent)\n        painter = QPainter(result)\n        self.graphics_item.setPixmap(QPixmap.fromImage(image))\n        self.graphics_scene.render(painter)\n        self.graphics_item.setPixmap(QPixmap())\n        return result\n\n\ndef insert_spaces(text: str, every_chars: int) -> str:\n    '''Insert spaces at every Nth character to allow for WordWrap'''\n    return ' '.join(text[i:i+every_chars] for i in range(0, len(text), every_chars))\n\n\ndef set_windows_os_screenshot_protection_drm_flag(window: QWidget) -> None:\n    \"\"\"\n    sets the windows WDA_MONITOR flag on the window so windows prevents capturing\n    screenshots and microsoft recall will not be able to record the window\n    https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowdisplayaffinity\n    \"\"\"\n    if sys.platform not in ('win32', 'windows'):\n        return\n    try:\n        window_id = int(window.winId())\n        WDA_MONITOR = 0x01\n        ctypes.windll.user32.SetWindowDisplayAffinity(window_id, WDA_MONITOR)\n    except Exception:\n        _logger.exception(f\"failed to set windows screenshot protection flag\")\n\n\ndef debug_widget_layouts(gui_element: QObject):\n    \"\"\"Draw red borders around all widgets of given QObject for debugging.\n    E.g. add util.debug_widget_layouts(self) at the end of TxEditor.__init__\n    \"\"\"\n    assert isinstance(gui_element, QObject) and hasattr(gui_element, 'findChildren')\n    def set_border(widget):\n        if widget is not None:\n            widget.setStyleSheet(widget.styleSheet() + \" * { border: 1px solid red; }\")\n\n    # Apply to all child widgets recursively\n    for widget in gui_element.findChildren(QWidget):\n        set_border(widget)\n\n\nclass _ABCQObjectMeta(type(QObject), ABCMeta): pass\nclass _ABCQWidgetMeta(type(QWidget), ABCMeta): pass\nclass AbstractQObject(QObject, ABC, metaclass=_ABCQObjectMeta): pass\nclass AbstractQWidget(QWidget, ABC, metaclass=_ABCQWidgetMeta): pass\n\n\nif __name__ == \"__main__\":\n    app = QApplication([])\n    t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', \"done\"))\n    t.start()\n    app.exec()\n"
  },
  {
    "path": "electrum/gui/qt/utxo_dialog.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2023 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import TYPE_CHECKING\nimport copy\n\nfrom PyQt6.QtCore import Qt, QUrl\nfrom PyQt6.QtGui import QTextCharFormat, QFont\nfrom PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel\n\nfrom electrum.i18n import _\n\nfrom .util import WindowModalDialog, ColorScheme, Buttons, CloseButton, MONOSPACE_FONT, WWLabel\nfrom .transaction_dialog import TxOutputColoring, QTextBrowserWithDefaultSize\n\nif TYPE_CHECKING:\n    from electrum.transaction import PartialTxInput\n    from .main_window import ElectrumWindow\n\n\nclass UTXODialog(WindowModalDialog):\n\n    def __init__(self, window: 'ElectrumWindow', utxo: 'PartialTxInput'):\n        WindowModalDialog.__init__(self, window, _(\"Coin Privacy Analysis\"))\n        self.main_window = window\n        self.config = window.config\n        self.wallet = window.wallet\n        self.utxo = utxo\n\n        self.parents_list = QTextBrowserWithDefaultSize(800, 400)\n        self.parents_list.setOpenLinks(False)  # disable automatic link opening\n        self.parents_list.anchorClicked.connect(self.open_tx)  # send links to our handler\n        self.parents_list.setFont(QFont(MONOSPACE_FONT))\n        self.parents_list.setReadOnly(True)\n        self.parents_list.setTextInteractionFlags(\n            self.parents_list.textInteractionFlags() |\n            Qt.TextInteractionFlag.LinksAccessibleByMouse |\n            Qt.TextInteractionFlag.LinksAccessibleByKeyboard\n        )\n        self.txo_color_parent = TxOutputColoring(\n            legend=_(\"Direct parent\"), color=ColorScheme.BLUE, tooltip=_(\"Direct parent\"))\n        self.txo_color_uncle = TxOutputColoring(\n            legend=_(\"Address reuse\"), color=ColorScheme.RED, tooltip=_(\"Address reuse\"))\n\n        vbox = QVBoxLayout()\n        vbox.addWidget(QLabel(_(\"Output point\") + \": \" + str(self.utxo.short_id)))\n        vbox.addWidget(QLabel(_(\"Amount\") + \": \" + self.main_window.format_amount_and_units(self.utxo.value_sats())))\n        self.stats_label = WWLabel()\n        vbox.addWidget(self.stats_label)\n        vbox.addWidget(self.parents_list)\n        legend_hbox = QHBoxLayout()\n        legend_hbox.setContentsMargins(0, 0, 0, 0)\n        legend_hbox.addStretch(2)\n        legend_hbox.addWidget(self.txo_color_parent.legend_label)\n        legend_hbox.addWidget(self.txo_color_uncle.legend_label)\n        vbox.addLayout(legend_hbox)\n        vbox.addLayout(Buttons(CloseButton(self)))\n        self.setLayout(vbox)\n        self.update()\n        self.main_window.labels_changed_signal.connect(self.update)\n\n    def update(self):\n\n        txid = self.utxo.prevout.txid.hex()\n        parents = self.wallet.get_tx_parents(txid)\n        num_parents = len(parents)\n        parents_copy = copy.deepcopy(parents)\n        cursor = self.parents_list.textCursor()\n        ext = QTextCharFormat()\n\n        if num_parents < 200:\n            ASCII_EDGE   = '└─'\n            ASCII_BRANCH = '├─'\n            ASCII_PIPE   = '│ '\n            ASCII_SPACE  = '  '\n        else:\n            ASCII_EDGE   = '└'\n            ASCII_BRANCH = '├'\n            ASCII_PIPE   = '│'\n            ASCII_SPACE  = ' '\n\n        self.parents_list.clear()\n        self.num_reuse = 0\n\n        def print_ascii_tree(_txid, prefix, is_last, is_uncle):\n            if _txid not in parents:\n                return\n            tx_mined_info = self.wallet.adb.get_tx_height(_txid)\n            tx_height = tx_mined_info.height()\n            tx_pos = tx_mined_info.txpos\n            key = \"%dx%d\"%(tx_height, tx_pos) if tx_pos is not None else _txid[0:8]\n            label = self.wallet.get_label_for_txid(_txid) or \"\"\n            if _txid not in parents_copy:\n                label = '[duplicate]'\n            c = '' if _txid == txid else (ASCII_EDGE if is_last else ASCII_BRANCH)\n            cursor.insertText(prefix + c, ext)\n            if is_uncle:\n                self.num_reuse += 1\n                lnk = QTextCharFormat(self.txo_color_uncle.text_char_format)\n            else:\n                lnk = QTextCharFormat(self.txo_color_parent.text_char_format)\n            lnk.setToolTip(_('Click to open, right-click for menu'))\n            lnk.setAnchorHref(_txid)\n            #lnk.setAnchorNames([a_name])\n            lnk.setAnchor(True)\n            lnk.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SingleUnderline)\n            cursor.insertText(key, lnk)\n            cursor.insertText(\" \", ext)\n            cursor.insertText(label, ext)\n            cursor.insertBlock()\n            next_prefix = '' if txid == _txid else prefix + (ASCII_SPACE if is_last else ASCII_PIPE)\n            parents_list, uncle_list = parents_copy.pop(_txid, ([],[]))\n            for i, p in enumerate(parents_list + uncle_list):\n                is_last = (i == len(parents_list) + len(uncle_list)- 1)\n                is_uncle = (i > len(parents_list) - 1)\n                print_ascii_tree(p, next_prefix, is_last, is_uncle)\n\n        # recursively build the tree\n        print_ascii_tree(txid, '', False, False)\n        msg = _(\"This UTXO has {} parent transactions in your wallet.\").format(num_parents)\n        if self.num_reuse:\n            msg += '\\n' + _('This does not include transactions that are downstream of address reuse.')\n        self.stats_label.setText(msg)\n        self.txo_color_parent.legend_label.setVisible(True)\n        self.txo_color_uncle.legend_label.setVisible(bool(self.num_reuse))\n        # set cursor to top\n        cursor.setPosition(0)\n        self.parents_list.setTextCursor(cursor)\n\n    def open_tx(self, txid):\n        if isinstance(txid, QUrl):\n            txid = txid.toString(QUrl.UrlFormattingOption.None_)\n        tx = self.wallet.adb.get_transaction(txid)\n        if not tx:\n            return\n        self.main_window.show_transaction(tx)\n"
  },
  {
    "path": "electrum/gui/qt/utxo_list.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import Optional, List, Dict, Sequence, Set, TYPE_CHECKING\nimport enum\nimport copy\n\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtGui import QStandardItemModel, QStandardItem, QFont\nfrom PyQt6.QtWidgets import QAbstractItemView, QMenu\n\nfrom electrum.i18n import _\nfrom electrum.bitcoin import is_address\nfrom electrum.transaction import PartialTxInput, PartialTxOutput\nfrom electrum.lnutil import MIN_FUNDING_SAT\nfrom electrum.util import profiler\nfrom electrum.plugin import run_hook\n\nfrom .util import ColorScheme, MONOSPACE_FONT\nfrom .my_treeview import MyTreeView, MySortModel\nfrom .new_channel_dialog import NewChannelDialog\nfrom ..messages import MSG_FREEZE_ADDRESS, MSG_FREEZE_COIN\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nclass UTXOList(MyTreeView):\n    _spend_set: Set[str]  # coins selected by the user to spend from\n    _utxo_dict: Dict[str, PartialTxInput]  # coin name -> coin\n\n    class Columns(MyTreeView.BaseColumnsEnum):\n        OUTPOINT = enum.auto()\n        ADDRESS = enum.auto()\n        LABEL = enum.auto()\n        AMOUNT = enum.auto()\n        PARENTS = enum.auto()\n\n    headers = {\n        Columns.OUTPOINT: _('Output point'),\n        Columns.ADDRESS: _('Address'),\n        Columns.PARENTS: _('Parents'),\n        Columns.LABEL: _('Label'),\n        Columns.AMOUNT: _('Amount'),\n    }\n    filter_columns = [Columns.ADDRESS, Columns.LABEL, Columns.OUTPOINT]\n    stretch_column = Columns.LABEL\n\n    ROLE_PREVOUT_STR = Qt.ItemDataRole.UserRole + 1000\n    ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1001\n    key_role = ROLE_PREVOUT_STR\n\n    def __init__(self, main_window: 'ElectrumWindow'):\n        super().__init__(\n            main_window=main_window,\n            stretch_column=self.stretch_column,\n        )\n        self._spend_set = set()\n        self._utxo_dict = {}\n        self.wallet = self.main_window.wallet\n        self.std_model = QStandardItemModel(self)\n        self.proxy = MySortModel(self, sort_role=self.ROLE_SORT_ORDER)\n        self.proxy.setSourceModel(self.std_model)\n        self.setModel(self.proxy)\n        self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)\n        self.setSortingEnabled(True)\n\n    def create_toolbar(self, config):\n        toolbar, menu = self.create_toolbar_with_menu('')\n        self.num_coins_label = toolbar.itemAt(0).widget()\n        menu.addAction(_('Coin control'), lambda: self.add_selection_to_coincontrol())\n\n        def cb():\n            self.main_window.utxo_list.refresh_all()  # for coin frozen status\n            self.main_window.update_status()  # frozen balance\n        menu.addConfig(config.cv.WALLET_FREEZE_REUSED_ADDRESS_UTXOS, callback=cb)\n        return toolbar\n\n    @profiler(min_threshold=0.05)\n    def update(self):\n        # not calling maybe_defer_update() as it interferes with coincontrol status bar\n        self.proxy.setDynamicSortFilter(False)  # temp. disable re-sorting after every change\n        utxos = self.wallet.get_utxos()\n        self._maybe_reset_coincontrol(utxos)\n        self._utxo_dict = dict([(utxo.prevout.to_str(), utxo) for utxo in utxos])\n        self.std_model.clear()\n        self.update_headers(self.__class__.headers)\n        for idx, utxo in enumerate(utxos):\n            name = utxo.prevout.to_str()\n            labels = [\"\"] * len(self.Columns)\n            amount_str = self.main_window.format_amount(\n                utxo.value_sats(), whitespaces=True)\n            amount_str_nots = self.main_window.format_amount(\n                utxo.value_sats(), whitespaces=False, add_thousands_sep=False)\n            labels[self.Columns.OUTPOINT] = str(utxo.short_id)\n            labels[self.Columns.ADDRESS] = utxo.address\n            labels[self.Columns.AMOUNT] = amount_str\n            utxo_item = [QStandardItem(x) for x in labels]\n            self.set_editability(utxo_item)\n            utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_PREVOUT_STR)\n            utxo_item[self.Columns.AMOUNT].setData(amount_str_nots, self.ROLE_CLIPBOARD_DATA)\n            utxo_item[self.Columns.ADDRESS].setFont(QFont(MONOSPACE_FONT))\n            utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT))\n            utxo_item[self.Columns.PARENTS].setFont(QFont(MONOSPACE_FONT))\n            utxo_item[self.Columns.OUTPOINT].setFont(QFont(MONOSPACE_FONT))\n            self.std_model.insertRow(idx, utxo_item)\n            self.refresh_row(name, idx)\n        self.filter()\n        self.proxy.setDynamicSortFilter(True)\n        self.sortByColumn(self.Columns.OUTPOINT, Qt.SortOrder.DescendingOrder)\n        self.update_coincontrol_bar()\n        self.num_coins_label.setText(_('{} unspent transaction outputs').format(len(utxos)))\n\n    def update_coincontrol_bar(self):\n        # update coincontrol status bar\n        if bool(self._spend_set):\n            coins = [self._utxo_dict[x] for x in self._spend_set]\n            coins = self._filter_frozen_coins(coins)\n            amount = sum(x.value_sats() for x in coins)\n            amount_str = self.main_window.format_amount_and_units(amount)\n            num_outputs_str = _(\"{} outputs available ({} total)\").format(len(coins), len(self._utxo_dict))\n            self.main_window.set_coincontrol_msg(_(\"Coin control active\") + f': {num_outputs_str}, {amount_str}')\n        else:\n            self.main_window.set_coincontrol_msg(None)\n\n    def refresh_row(self, key, row):\n        assert row is not None\n        utxo = self._utxo_dict[key]\n        utxo_item = [self.std_model.item(row, col) for col in self.Columns]\n        txid = utxo.prevout.txid.hex()\n        num_parents = self.wallet.get_num_parents(txid)\n        utxo_item[self.Columns.PARENTS].setText('%6s'%num_parents if num_parents else '-')\n        label = self.wallet.get_label_for_txid(txid) or ''\n        utxo_item[self.Columns.LABEL].setText(label)\n        sort_key = (\n            self.wallet.adb.tx_height_to_sort_height(utxo.block_height),  # sort by block height\n            str(utxo.short_id),                                           # order inside block (if mined), or just txid\n        )\n        utxo_item[self.Columns.OUTPOINT].setData(sort_key, self.ROLE_SORT_ORDER)\n        SELECTED_TO_SPEND_TOOLTIP = _('Coin selected to be spent')\n        if key in self._spend_set:\n            tooltip = key + \"\\n\" + SELECTED_TO_SPEND_TOOLTIP\n            color = ColorScheme.GREEN.as_color(True)\n        else:\n            tooltip = key\n            color = self._default_bg_brush\n        for col in utxo_item:\n            col.setBackground(color)\n            col.setToolTip(tooltip)\n        if self.wallet.is_frozen_address(utxo.address):\n            utxo_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True))\n            utxo_item[self.Columns.ADDRESS].setToolTip(_('Address is frozen'))\n        if self.wallet.is_frozen_coin(utxo):\n            utxo_item[self.Columns.OUTPOINT].setBackground(ColorScheme.BLUE.as_color(True))\n            utxo_item[self.Columns.OUTPOINT].setToolTip(f\"{key}\\n{_('Coin is frozen')}\")\n\n    def get_selected_outpoints(self) -> List[str]:\n        if not self.model():\n            return []\n        items = self.selected_in_column(self.Columns.OUTPOINT)\n        return [x.data(self.ROLE_PREVOUT_STR) for x in items]\n\n    def _filter_frozen_coins(self, coins: List[PartialTxInput]) -> List[PartialTxInput]:\n        coins = [utxo for utxo in coins\n                 if (not self.wallet.is_frozen_address(utxo.address) and\n                     not self.wallet.is_frozen_coin(utxo))]\n        return coins\n\n    def are_in_coincontrol(self, coins: List[PartialTxInput]) -> bool:\n        return all([utxo.prevout.to_str() in self._spend_set for utxo in coins])\n\n    def add_to_coincontrol(self, coins: List[PartialTxInput]):\n        assert all(utxo.prevout.to_str() in self._utxo_dict for utxo in coins) # see issue 10206\n        coins = self._filter_frozen_coins(coins)\n        for utxo in coins:\n            self._spend_set.add(utxo.prevout.to_str())\n        self._refresh_coincontrol()\n\n    def remove_from_coincontrol(self, coins: List[PartialTxInput]):\n        for utxo in coins:\n            self._spend_set.remove(utxo.prevout.to_str())\n        self._refresh_coincontrol()\n\n    def clear_coincontrol(self):\n        self._spend_set.clear()\n        self._refresh_coincontrol()\n\n    def add_selection_to_coincontrol(self):\n        if bool(self._spend_set):\n            self.clear_coincontrol()\n            return\n        selected = self.get_selected_outpoints()\n        coins = [self._utxo_dict[name] for name in selected]\n        if not coins:\n            self.main_window.show_error(_('You need to select coins from the list first.\\nUse ctrl+left mouse button to select multiple items'))\n            return\n        self.add_to_coincontrol(coins)\n\n    def _refresh_coincontrol(self):\n        self.refresh_all()\n        self.update_coincontrol_bar()\n        self.selectionModel().clearSelection()\n\n    def get_spend_list(self) -> Optional[Sequence[PartialTxInput]]:\n        if not bool(self._spend_set):\n            return None\n        utxos = [self._utxo_dict[x] for x in self._spend_set]\n        return copy.deepcopy(utxos)  # copy so that side-effects don't affect utxo_dict\n\n    def _maybe_reset_coincontrol(self, current_wallet_utxos: Sequence[PartialTxInput]) -> None:\n        if not self._spend_set and not self._currently_open_menu:\n            return\n        utxo_set = {utxo.prevout.to_str() for utxo in current_wallet_utxos}\n        if self._currently_open_menu:\n            # if we spent one of the qt-highlighted UTXOs, close context-menu\n            if not all(prevout_str in utxo_set for prevout_str in self.get_selected_outpoints()):\n                self.close_menu()\n        if self._spend_set:\n            # if we spent one of the green-marked UTXOs, just reset selection\n            if not all([prevout_str in utxo_set for prevout_str in self._spend_set]):\n                self._spend_set.clear()\n\n    def can_swap_coins(self, coins):\n        # fixme: min and max_amounts are known only after first request\n        if self.wallet.lnworker is None:\n            return False\n        value = sum(x.value_sats() for x in coins)\n        min_amount = self.wallet.lnworker.swap_manager.get_min_amount()\n        max_amount = self.wallet.lnworker.swap_manager.client_max_amount_forward_swap()\n        if min_amount is None or max_amount is None:\n            # we need to fetch data from swap server\n            return True\n        if value < min_amount:\n            return False\n        if max_amount is None or value > max_amount:\n            return False\n        return True\n\n    def swap_coins(self, coins: list[PartialTxInput]) -> None:\n        assert coins, \"no coins selected?\"\n        #self.clear_coincontrol()\n        self.add_to_coincontrol(coins)\n        self.main_window.run_swap_dialog(is_reverse=False, recv_amount_sat_or_max='!')\n        self.clear_coincontrol()\n\n    def can_open_channel(self, coins):\n        if self.wallet.lnworker is None:\n            return False\n        value = sum(x.value_sats() for x in coins)\n        return value >= MIN_FUNDING_SAT and value <= self.config.LIGHTNING_MAX_FUNDING_SAT\n\n    def open_channel_with_coins(self, coins: list[PartialTxInput]) -> None:\n        assert coins, \"no coins selected?\"\n        # todo : use a single dialog in new flow\n        #self.clear_coincontrol()\n        self.add_to_coincontrol(coins)\n        d = NewChannelDialog(self.main_window)\n        d.max_button.setChecked(True)\n        d.max_button.setEnabled(False)\n        d.min_button.setEnabled(False)\n        d.clear_button.setEnabled(False)\n        d.amount_e.setFrozen(True)\n        d.spend_max()\n        d.run()\n        self.clear_coincontrol()\n\n    def clipboard_contains_address(self) -> bool:\n        text = self.main_window.app.clipboard().text()\n        return is_address(text)\n\n    def pay_to_clipboard_address(self, coins: list[PartialTxInput]) -> None:\n        assert coins, \"no coins selected?\"\n        if not self.clipboard_contains_address():\n            self.main_window.show_error(_('Clipboard doesn\\'t contain a valid address'))\n            return\n        addr = self.main_window.app.clipboard().text()\n        outputs = [PartialTxOutput.from_address_and_value(addr, '!')]\n        #self.clear_coincontrol()\n        self.add_to_coincontrol(coins)\n        self.main_window.send_tab.pay_onchain_dialog(outputs)\n        self.clear_coincontrol()\n\n    def on_double_click(self, idx):\n        outpoint = idx.sibling(idx.row(), self.Columns.OUTPOINT).data(self.ROLE_PREVOUT_STR)\n        utxo = self._utxo_dict[outpoint]\n        self.main_window.show_utxo(utxo)\n\n    def create_menu(self, position):\n        selected = self.get_selected_outpoints()\n        coins = [self._utxo_dict[name] for name in selected]\n\n        if not coins:\n            return\n\n        unfrozen_coins = self._filter_frozen_coins(coins)\n        menu = QMenu()\n        menu.setSeparatorsCollapsible(True)  # consecutive separators are merged together\n\n        if len(coins) == 1:\n            idx = self.indexAt(position)\n            if not idx.isValid():\n                return\n            utxo = coins[0]\n            txid = utxo.prevout.txid.hex()\n            # \"Details\"\n            tx = self.wallet.adb.get_transaction(txid)\n            if tx:\n                label = self.wallet.get_label_for_txid(txid)\n                menu.addAction(_(\"Privacy analysis\"), lambda: self.main_window.show_utxo(utxo))\n            cc = self.add_copy_menu(menu, idx)\n            cc.addAction(_(\"Long Output point\"), lambda: self.place_text_on_clipboard(utxo.prevout.to_str(), title=\"Long Output point\"))\n        # fully spend\n        m = menu_spend = menu.addMenu(_(\"Fully spend\") + '…')\n        m.setEnabled(bool(unfrozen_coins))\n        m = menu_spend.addAction(_(\"send to address in clipboard\"), lambda: self.pay_to_clipboard_address(unfrozen_coins))\n        m.setEnabled(self.clipboard_contains_address())\n        m = menu_spend.addAction(_(\"in new channel\"), lambda: self.open_channel_with_coins(unfrozen_coins))\n        m.setEnabled(self.can_open_channel(unfrozen_coins))\n        m = menu_spend.addAction(_(\"in submarine swap\"), lambda: self.swap_coins(unfrozen_coins))\n        m.setEnabled(self.can_swap_coins(unfrozen_coins))\n        # coin control\n        if self.are_in_coincontrol(coins):\n            menu.addAction(_(\"Remove from coin control\"), lambda: self.remove_from_coincontrol(coins))\n        else:\n            m = menu.addAction(_(\"Add to coin control\"), lambda: self.add_to_coincontrol(coins))\n            m.setEnabled(bool(unfrozen_coins))\n        # Freeze menu\n        if len(coins) == 1:\n            utxo = coins[0]\n            addr = utxo.address\n            menu_freeze = menu.addMenu(_(\"Freeze\"))\n            menu_freeze.setToolTipsVisible(True)\n            if not self.wallet.is_frozen_coin(utxo):\n                act = menu_freeze.addAction(_(\"Freeze Coin\"), lambda: self.main_window.set_frozen_state_of_coins([utxo], True))\n            else:\n                act = menu_freeze.addAction(_(\"Unfreeze Coin\"), lambda: self.main_window.set_frozen_state_of_coins([utxo], False))\n            act.setToolTip(MSG_FREEZE_COIN)\n            if not self.wallet.is_frozen_address(addr):\n                act = menu_freeze.addAction(_(\"Freeze Address\"), lambda: self.main_window.set_frozen_state_of_addresses([addr], True))\n            else:\n                act = menu_freeze.addAction(_(\"Unfreeze Address\"), lambda: self.main_window.set_frozen_state_of_addresses([addr], False))\n            act.setToolTip(MSG_FREEZE_ADDRESS)\n        elif len(coins) > 1:  # multiple items selected\n            menu.addSeparator()\n            addrs = [utxo.address for utxo in coins]\n            is_coin_frozen = [self.wallet.is_frozen_coin(utxo) for utxo in coins]\n            is_addr_frozen = [self.wallet.is_frozen_address(utxo.address) for utxo in coins]\n            menu_freeze = menu.addMenu(_(\"Freeze\"))\n            menu_freeze.setToolTipsVisible(True)\n            if not all(is_coin_frozen):\n                act = menu_freeze.addAction(_(\"Freeze Coins\"), lambda: self.main_window.set_frozen_state_of_coins(coins, True))\n                act.setToolTip(MSG_FREEZE_COIN)\n            if any(is_coin_frozen):\n                act = menu_freeze.addAction(_(\"Unfreeze Coins\"), lambda: self.main_window.set_frozen_state_of_coins(coins, False))\n                act.setToolTip(MSG_FREEZE_COIN)\n            if not all(is_addr_frozen):\n                act = menu_freeze.addAction(_(\"Freeze Addresses\"), lambda: self.main_window.set_frozen_state_of_addresses(addrs, True))\n                act.setToolTip(MSG_FREEZE_ADDRESS)\n            if any(is_addr_frozen):\n                act = menu_freeze.addAction(_(\"Unfreeze Addresses\"), lambda: self.main_window.set_frozen_state_of_addresses(addrs, False))\n                act.setToolTip(MSG_FREEZE_ADDRESS)\n\n        run_hook('qt_utxo_menu', menu, coins, self.wallet)\n        self.open_menu(menu, position)\n\n    def get_filter_data_from_coordinate(self, row, col):\n        if col == self.Columns.OUTPOINT:\n            return self.get_role_data_from_coordinate(row, col, role=self.ROLE_PREVOUT_STR)\n        return super().get_filter_data_from_coordinate(row, col)\n"
  },
  {
    "path": "electrum/gui/qt/wallet_info_dialog.py",
    "content": "# Copyright (C) 2023 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nimport os\nfrom typing import TYPE_CHECKING\nfrom functools import partial\n\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtWidgets import (\n    QLabel, QVBoxLayout, QGridLayout,\n    QHBoxLayout, QPushButton, QWidget, QTabWidget)\n\nfrom electrum.plugin import run_hook\nfrom electrum.i18n import _\nfrom electrum.wallet import Multisig_Wallet\nfrom electrum.wizard import WizardViewState\n\nfrom .main_window import protected\nfrom electrum.gui.qt.wizard.wallet import QEKeystoreWizard\nfrom .qrtextedit import ShowQRTextEdit\nfrom .util import (\n    read_QIcon, WindowModalDialog, Buttons,\n    WWLabel, CloseButton, HelpButton, font_height, ShowQRLineEdit\n)\n\nif TYPE_CHECKING:\n    from .main_window import ElectrumWindow\n\n\nclass WalletInfoDialog(WindowModalDialog):\n\n    def __init__(self, parent: QWidget, *, window: 'ElectrumWindow'):\n        WindowModalDialog.__init__(self, parent, _(\"Wallet Information\"))\n        self.setMinimumSize(800, 100)\n        self.window = window\n        self.wallet = wallet = window.wallet\n        # required for @protected decorator\n        self._protected_requires_password = lambda: self.wallet.has_keystore_encryption() or self.wallet.storage.is_encrypted_with_user_pw()\n        config = window.config\n        vbox = QVBoxLayout()\n        wallet_type = wallet.db.get('wallet_type', '')\n        if wallet.is_watching_only():\n            wallet_type += ' [{}]'.format(_('watching-only'))\n        seed_available = _('False')\n        if wallet.has_seed():\n            seed_available = _('True')\n            seed_available += f\" ({wallet.get_seed_type()})\"\n        keystore_types = [k.get_type_text() for k in wallet.get_keystores()]\n        grid = QGridLayout()\n        basename = os.path.basename(wallet.storage.path)\n        cur_row = 0\n        grid.addWidget(WWLabel(_(\"Wallet name\")+ ':'), cur_row, 0)\n        grid.addWidget(WWLabel(basename), cur_row, 1)\n        cur_row += 1\n        if db_metadata := wallet.db.get_db_metadata():\n            grid.addWidget(WWLabel(_(\"File created\") + ':'), cur_row, 0)\n            grid.addWidget(WWLabel(db_metadata.to_str()), cur_row, 1)\n            cur_row += 1\n        grid.addWidget(WWLabel(_(\"Wallet type\")+ ':'), cur_row, 0)\n        grid.addWidget(WWLabel(wallet_type), cur_row, 1)\n        cur_row += 1\n        grid.addWidget(WWLabel(_(\"Script type\")+ ':'), cur_row, 0)\n        grid.addWidget(WWLabel(wallet.txin_type), cur_row, 1)\n        cur_row += 1\n        grid.addWidget(WWLabel(_(\"Seed available\") + ':'), cur_row, 0)\n        grid.addWidget(WWLabel(str(seed_available)), cur_row, 1)\n        cur_row += 1\n        if len(keystore_types) <= 1:\n            grid.addWidget(WWLabel(_(\"Keystore type\") + ':'), cur_row, 0)\n            ks_type = str(keystore_types[0]) if keystore_types else _('No keystore')\n            grid.addWidget(WWLabel(ks_type), cur_row, 1)\n            cur_row += 1\n        # lightning\n        grid.addWidget(WWLabel(_('Lightning') + ':'), cur_row, 0)\n        from .util import IconLabel\n        if wallet.has_lightning():\n            if wallet.lnworker.has_deterministic_node_id():\n                grid.addWidget(WWLabel(_('Enabled')), cur_row, 1)\n            else:\n                label = IconLabel(text='Enabled, non-recoverable channels')\n                label.setIcon(read_QIcon('cloud_no'))\n                grid.addWidget(label, cur_row, 1)\n                if wallet.get_seed_type() == 'segwit':\n                    msg = _(\"Your channels cannot be recovered from seed, because they were created with an old version of Electrum. \"\n                            \"This means that you must save a backup of your wallet every time you create a new channel.\\n\\n\"\n                            \"If you want this wallet to have recoverable channels, you must close your existing channels and restore this wallet from seed\")\n                else:\n                    msg = _(\"Your channels cannot be recovered from seed. \"\n                            \"This means that you must save a backup of your wallet every time you create a new channel.\\n\\n\"\n                            \"If you want to have recoverable channels, you must create a new wallet with an Electrum seed\")\n                grid.addWidget(HelpButton(msg), cur_row, 3)\n            cur_row += 1\n            grid.addWidget(WWLabel(_('Lightning Node ID:')), cur_row, 0)\n            cur_row += 1\n            nodeid_text = wallet.lnworker.node_keypair.pubkey.hex()\n            nodeid_e = ShowQRLineEdit(nodeid_text, config, title=_(\"Node ID\"))\n            grid.addWidget(nodeid_e, cur_row, 0, 1, 4)\n            cur_row += 1\n        else:\n            if wallet.can_have_lightning():\n                grid.addWidget(WWLabel('Not enabled'), cur_row, 1)\n                button = QPushButton(_(\"Enable\"))\n                button.pressed.connect(lambda: window.init_lightning_dialog(self))\n                grid.addWidget(button, cur_row, 3)\n            else:\n                grid.addWidget(WWLabel(_(\"Not available for this wallet.\")), cur_row, 1)\n                grid.addWidget(HelpButton(_(\"Lightning is currently restricted to HD wallets with p2wpkh addresses.\")), cur_row, 2)\n            cur_row += 1\n        vbox.addLayout(grid)\n\n        labels_clayout = None\n\n        if wallet.is_deterministic():\n            keystores = sorted(wallet.get_keystores(), key=lambda _ks: _ks.get_root_fingerprint() or '')\n\n            self.keystore_tabs = QTabWidget()\n\n            for idx, ks in enumerate(keystores):\n                ks_w = QWidget()\n                ks_vbox = QVBoxLayout()\n                ks_w.setLayout(ks_vbox)\n\n                status_label = _('This keystore is watching-only (disabled)') if ks.is_watching_only() else _('This keystore is active (enabled)')\n                ks_vbox.addWidget(QLabel(status_label))\n                label = f'{ks.label}' if hasattr(ks, 'label') and ks.label else ''\n                ks_vbox.addWidget(QLabel(_('Type') + ': ' + f'{ks.get_type_text()}' + ' ' + label))\n\n                mpk_text = ShowQRTextEdit(ks.get_master_public_key(), config=config)\n                mpk_text.setMaximumHeight(max(150, 10 * font_height()))\n                mpk_text.addCopyButton()\n                run_hook('show_xpub_button', mpk_text, ks)\n                ks_vbox.addWidget(WWLabel(_(\"Master Public Key\")))\n                ks_vbox.addWidget(mpk_text)\n\n                der_path_hbox = QHBoxLayout()\n                der_path_hbox.setContentsMargins(0, 0, 0, 0)\n                der_path_hbox.addWidget(WWLabel(_(\"Derivation path\") + ':'))\n                der_path_text = WWLabel(ks.get_derivation_prefix() or _(\"unknown\"))\n                der_path_text.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n                der_path_hbox.addWidget(der_path_text)\n                der_path_hbox.addStretch()\n                ks_vbox.addLayout(der_path_hbox)\n\n                bip32fp_hbox = QHBoxLayout()\n                bip32fp_hbox.setContentsMargins(0, 0, 0, 0)\n                bip32fp_hbox.addWidget(QLabel(\"BIP32 root fingerprint:\"))\n                bip32fp_text = WWLabel(ks.get_root_fingerprint() or _(\"unknown\"))\n                bip32fp_text.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n                bip32fp_hbox.addWidget(bip32fp_text)\n                bip32fp_hbox.addStretch()\n                ks_vbox.addLayout(bip32fp_hbox)\n                if wallet.can_enable_disable_keystore(ks):\n                    ks_buttons = []\n                    if not ks.is_watching_only():\n                        rm_keystore_button = QPushButton('Disable keystore')\n                        rm_keystore_button.clicked.connect(partial(self.disable_keystore, ks))\n                        ks_buttons.insert(0, rm_keystore_button)\n                    else:\n                        add_keystore_button = QPushButton('Enable Keystore')\n                        add_keystore_button.clicked.connect(self.enable_keystore)\n                        ks_buttons.insert(0, add_keystore_button)\n                    ks_vbox.addLayout(Buttons(*ks_buttons))\n                tab_label = _(\"Cosigner\") + f' {idx+1}' if len(keystores) > 1 else _(\"Keystore\")\n                index = self.keystore_tabs.addTab(ks_w, tab_label)\n                if not ks.is_watching_only():\n                    self.keystore_tabs.setTabIcon(index, read_QIcon('confirmed.svg'))\n            vbox.addWidget(self.keystore_tabs)\n\n        vbox.addStretch(1)\n\n        buttons = [CloseButton(self)]\n        btn_export_info = run_hook('wallet_info_buttons', window, self)\n        if btn_export_info is None:\n            btn_export_info = []\n        buttons = btn_export_info + buttons\n\n        btns = Buttons(*buttons)\n        vbox.addLayout(btns)\n        self.setLayout(vbox)\n\n    def disable_keystore(self, keystore):\n        if self.wallet.has_channels():\n            self.window.show_message(_('Cannot disable keystore: You have active lightning channels'))\n            return\n\n        msg = _('Disable keystore? This will make the keystore watching-only.')\n        if self.wallet.storage.is_encrypted_with_hw_device():\n            msg += '\\n\\n' + _('Note that this will disable wallet file encryption, because it uses your hardware wallet device.')\n        if not self.window.question(msg):\n            return\n        self.accept()\n        self.wallet.disable_keystore(keystore)\n        self.window.gui_object.reload_windows()\n\n    def enable_keystore(self, b: bool):\n        v = WizardViewState('keystore_type', {'wallet_type': self.window.wallet.wallet_type}, {})\n        dialog = QEKeystoreWizard(config=self.window.config, app=self.window.gui_object.app,\n                                  plugins=self.window.gui_object.plugins, start_viewstate=v)\n        result = dialog.run()\n        if not result:\n            return\n        keystore, is_hardware = result\n        for k in self.wallet.get_keystores():\n            if k.get_master_public_key() == keystore.get_master_public_key():\n                break\n        else:\n            self.window.show_error(_('Keystore not found in this wallet'))\n            return\n        self._enable_keystore(keystore, is_hardware)\n\n    @protected\n    def _enable_keystore(self, keystore, is_hardware, password):\n        self.accept()\n        self.wallet.enable_keystore(keystore, is_hardware, password)\n        self.window.gui_object.reload_windows()\n"
  },
  {
    "path": "electrum/gui/qt/wizard/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/gui/qt/wizard/server_connect.py",
    "content": "from typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtGui import QPixmap\nfrom PyQt6.QtWidgets import QCheckBox, QLabel, QHBoxLayout, QVBoxLayout, QWidget\n\nfrom electrum.i18n import _\nfrom electrum.wizard import ServerConnectWizard\nfrom electrum.gui.qt.network_dialog import ProxyWidget, ServerWidget\nfrom electrum.gui.qt.util import icon_path\nfrom .wizard import QEAbstractWizard, WizardComponent\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n    from electrum.plugin import Plugins\n    from electrum.daemon import Daemon\n    from electrum.gui.qt import QElectrumApplication\n\n\nclass QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard):\n\n    def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', plugins: 'Plugins', daemon: 'Daemon', parent=None):\n        ServerConnectWizard.__init__(self, daemon)\n        QEAbstractWizard.__init__(self, config, app)\n        self.window_title = _('Network and server configuration')\n        self.finish_label = _('Next')\n\n        # attach gui classes\n        self.navmap_merge({\n            'welcome': {'gui': WCWelcome},\n            'proxy_config': {'gui': WCProxyConfig},\n            'server_config': {'gui': WCServerConfig},\n        })\n\n\nclass WCWelcome(WizardComponent):\n    def __init__(self, parent, wizard):\n        WizardComponent.__init__(self, parent, wizard, title='Network Configuration')\n        self.wizard_title = _('Electrum Bitcoin Wallet')\n\n        self.first_help_label = QLabel()\n        self.first_help_label.setText(_(\"Optional settings to customize your network connection\") + \":\")\n        self.first_help_label.setWordWrap(True)\n\n        self.config_proxy_w = QCheckBox(_('Use Proxy'))\n        self.config_proxy_w.setChecked(False)\n        self.config_proxy_w.stateChanged.connect(self.on_updated)\n        self.config_server_w = QCheckBox(_('Select Electrum Server'))\n        self.config_server_w.setChecked(False)\n        self.config_server_w.stateChanged.connect(self.on_updated)\n        options_w = QWidget()\n        vbox = QVBoxLayout()\n        vbox.addWidget(self.config_proxy_w)\n        vbox.addWidget(self.config_server_w)\n        options_w.setLayout(vbox)\n\n        self.second_help_label = QLabel()\n        self.second_help_label.setText(\n            _(\"If you are unsure what these options are, leave them unchecked.\")\n        )\n        self.second_help_label.setWordWrap(True)\n\n        self.layout().addWidget(self.first_help_label)\n        self.layout().addWidget(options_w)\n        self.layout().addWidget(self.second_help_label)\n        self.layout().addStretch(1)\n        self._valid = True\n\n    def apply(self):\n        self.wizard_data['use_defaults'] = not (self.config_server_w.isChecked() or self.config_proxy_w.isChecked())\n        self.wizard_data['want_proxy'] = self.config_proxy_w.isChecked()\n        self.wizard_data['autoconnect'] = not self.config_server_w.isChecked()\n\n\nclass WCProxyConfig(WizardComponent):\n    def __init__(self, parent, wizard):\n        WizardComponent.__init__(self, parent, wizard, title=_('Proxy'))\n        self.pw = ProxyWidget(wizard._daemon.network, self)\n        self.pw.proxy_cb.setChecked(True)\n        self.pw.proxy_host.setText('localhost')\n        self.pw.proxy_port.setText('9050')\n        self.layout().addWidget(self.pw)\n        self._valid = True\n\n    def apply(self):\n        self.wizard_data['proxy'] = self.pw.get_proxy_settings().to_dict()\n\n\nclass WCServerConfig(WizardComponent):\n    def __init__(self, parent, wizard):\n        WizardComponent.__init__(self, parent, wizard, title=_('Server'))\n        self.sw = ServerWidget(wizard._daemon.network, self)\n        self.layout().addWidget(self.sw)\n        self.sw.server_e_valid.connect(self.on_server_e_valid)\n\n    def on_server_e_valid(self, valid):\n        self.valid = valid\n\n    def apply(self):\n        self.wizard_data['autoconnect'] = self.sw.server_e.text().strip() == ''\n        self.wizard_data['server'] = self.sw.server_e.text()\n        self.wizard_data['one_server'] = self.wizard.config.NETWORK_ONESERVER\n"
  },
  {
    "path": "electrum/gui/qt/wizard/terms_of_use.py",
    "content": "from typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import QTimer, QEvent\nfrom PyQt6.QtGui import QPixmap\nfrom PyQt6.QtWidgets import QLabel, QHBoxLayout, QScrollArea\n\nfrom electrum.i18n import _\nfrom electrum.wizard import TermsOfUseWizard\nfrom electrum.gui.qt.util import icon_path, WWLabel\nfrom electrum.gui import messages\nfrom .wizard import QEAbstractWizard, WizardComponent\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n    from electrum.gui.qt import QElectrumApplication\n\n\nclass QETermsOfUseWizard(TermsOfUseWizard, QEAbstractWizard):\n    def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication'):\n        TermsOfUseWizard.__init__(self, config)\n        QEAbstractWizard.__init__(self, config, app)\n        self.window_title = _('Terms of Use')\n        self.finish_label = _('I Accept')\n        self.title.setVisible(False)\n        # self.window().setMinimumHeight(565)  # Enough to show the whole text without scrolling\n        self.next_button.setToolTip(\"You accept the Terms of Use by clicking this button.\")\n\n        # attach gui classes\n        self.navmap_merge({\n            'terms_of_use': {'gui': WCTermsOfUseScreen, 'params': {'icon': ''}},\n        })\n\nclass WCTermsOfUseScreen(WizardComponent):\n    def __init__(self, parent, wizard):\n        WizardComponent.__init__(self, parent, wizard, title='')\n        self.wizard_title = _('Electrum Terms of Use')\n        self.img_label = QLabel()\n        pixmap = QPixmap(icon_path('electrum_darkblue_1.png'))\n        self.img_label.setPixmap(pixmap)\n        self.img_label2 = QLabel()\n        pixmap = QPixmap(icon_path('electrum_text.png'))\n        self.img_label2.setPixmap(pixmap)\n        hbox_img = QHBoxLayout()\n        hbox_img.addStretch(1)\n        hbox_img.addWidget(self.img_label)\n        hbox_img.addWidget(self.img_label2)\n        hbox_img.addStretch(1)\n\n        self.layout().addLayout(hbox_img)\n        self.layout().addSpacing(15)\n\n        self.tos_label = WWLabel()\n        self.tos_label.setText(messages.MSG_TERMS_OF_USE)\n        self.layout().addWidget(self.tos_label)\n        self._valid = True\n\n    def apply(self):\n        pass\n"
  },
  {
    "path": "electrum/gui/qt/wizard/wallet.py",
    "content": "from abc import ABC\nimport os\nimport sys\nimport threading\n\nfrom typing import TYPE_CHECKING, Optional, List, Tuple\n\nfrom PyQt6.QtCore import Qt, QTimer, QRect, pyqtSignal\nfrom PyQt6.QtGui import QPen, QPainter, QPalette, QPixmap\nfrom PyQt6.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QWidget,\n                             QFileDialog, QSlider, QGridLayout, QDialog, QApplication)\n\nfrom electrum.bip32 import is_bip32_derivation, BIP32Node, normalize_bip32_derivation, xpub_type\nfrom electrum.daemon import Daemon\nfrom electrum.i18n import _\nfrom electrum.keystore import bip44_derivation, bip39_to_seed, purpose48_derivation, ScriptTypeNotSupported\nfrom electrum.plugin import run_hook, HardwarePluginLibraryUnavailable\nfrom electrum.storage import StorageReadWriteError\nfrom electrum.util import WalletFileException, get_new_wallet_name, UserFacingException, InvalidPassword\nfrom electrum.util import is_subpath, ChoiceItem, multisig_type, UserCancelled, standardize_path\nfrom electrum.wallet import wallet_types\nfrom .wizard import QEAbstractWizard, WizardComponent\nfrom electrum.logging import get_logger, Logger\nfrom electrum import WalletStorage, mnemonic, keystore\nfrom electrum.wallet_db import WalletDB\nfrom electrum.wizard import NewWalletWizard, KeystoreWizard, WizardViewState\n\nfrom electrum.gui.qt.bip39_recovery_dialog import Bip39RecoveryDialog\nfrom electrum.gui.qt.password_dialog import PasswordLayout, PW_NEW, MSG_ENTER_PASSWORD, PasswordLayoutForHW\nfrom electrum.gui.qt.seed_dialog import SeedWidget, MSG_PASSPHRASE_WARN_ISSUE4566, KeysWidget\nfrom electrum.gui.qt.util import (PasswordLineEdit, char_width_in_lineedit, WWLabel, InfoButton, font_height,\n                                  ChoiceWidget, MessageBoxMixin, icon_path, IconLabel, read_QIcon)\nfrom electrum.gui.qt.plugins_dialog import PluginsDialog\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n    from electrum.plugin import Plugins, DeviceInfo\n    from electrum.gui.qt import QElectrumApplication\n\nWIF_HELP_TEXT = (_('WIF keys are typed in Electrum, based on script type.') + '\\n\\n' +\n                 _('A few examples') + ':\\n' +\n                 'p2pkh:KxZcY47uGp9a...       \\t-> 1DckmggQM...\\n' +\n                 'p2wpkh-p2sh:KxZcY47uGp9a... \\t-> 3NhNeZQXF...\\n' +\n                 'p2wpkh:KxZcY47uGp9a...      \\t-> bc1q3fjfk...')\n\nMSG_HW_STORAGE_ENCRYPTION = _(\"Set wallet file encryption.\") + '\\n'\\\n                          + _(\"Your wallet file does not contain secrets, mostly just metadata. \") \\\n                          + _(\"It also contains your master public key that allows watching your addresses.\")\n\n\nclass QEKeystoreWizard(KeystoreWizard, QEAbstractWizard, MessageBoxMixin):\n    _logger = get_logger(__name__)\n\n    def __init__(\n            self,\n            *,\n            config: 'SimpleConfig',\n            app: 'QElectrumApplication',\n            plugins: 'Plugins',\n            start_viewstate: WizardViewState = None,\n    ):\n        assert 'wallet_type' in start_viewstate.wizard_data, 'wallet_type required'\n\n        QEAbstractWizard.__init__(self, config, app, start_viewstate=start_viewstate)\n        KeystoreWizard.__init__(self, plugins)\n        self.window_title = _('Extend wallet keystore')\n        # attach gui classes to views\n        self.navmap_merge({\n            'keystore_type': {'gui': WCExtendKeystore},\n            'enter_seed': {'gui': WCHaveSeed},\n            'enter_ext': {'gui': WCEnterExt},\n            'choose_hardware_device': {'gui': WCChooseHWDevice},\n            'script_and_derivation': {'gui': WCScriptAndDerivation},\n            'wallet_password': {'gui': WCWalletPassword},\n            'wallet_password_hardware': {'gui': WCWalletPasswordHardware},\n        })\n\n    def is_single_password(self):\n        return True\n\n    def run(self):\n        if self.exec() == QDialog.DialogCode.Rejected:\n            return\n        return self._result\n\n\nclass QENewWalletWizard(NewWalletWizard, QEAbstractWizard, MessageBoxMixin):\n    _logger = get_logger(__name__)\n\n    def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', plugins: 'Plugins', daemon: Daemon, path, *, start_viewstate=None):\n        NewWalletWizard.__init__(self, daemon, plugins)\n        QEAbstractWizard.__init__(self, config, app, start_viewstate=start_viewstate)\n        self.window_title = _('Create/Restore wallet')\n\n        self._path = standardize_path(path)\n        self._password = None\n\n        # attach gui classes to views\n        self.navmap_merge({\n            'wallet_name': {'gui': WCWalletName},\n            'hw_unlock': {'gui': WCChooseHWDevice},\n            'wallet_type': {'gui': WCWalletType},\n            'keystore_type': {'gui': WCKeystoreType},\n            'create_seed': {'gui': WCCreateSeed},\n            'create_ext': {'gui': WCEnterExt},\n            'confirm_seed': {'gui': WCConfirmSeed},\n            'confirm_ext': {'gui': WCConfirmExt},\n            'have_seed': {'gui': WCHaveSeed},\n            'have_ext': {'gui': WCEnterExt},\n            'choose_hardware_device': {'gui': WCChooseHWDevice},\n            'script_and_derivation': {'gui': WCScriptAndDerivation},\n            'have_master_key': {'gui': WCHaveMasterKey},\n            'multisig': {'gui': WCMultisig},\n            'multisig_cosigner_keystore': {'gui': WCCosignerKeystore},\n            'multisig_cosigner_key': {'gui': WCHaveMasterKey},\n            'multisig_cosigner_seed': {'gui': WCHaveSeed},\n            'multisig_cosigner_have_ext': {'gui': WCEnterExt},\n            'multisig_cosigner_hardware': {'gui': WCChooseHWDevice},\n            'multisig_cosigner_script_and_derivation': {'gui': WCScriptAndDerivation},\n            'imported': {'gui': WCImport},\n            'wallet_password': {'gui': WCWalletPassword},\n            'wallet_password_hardware': {'gui': WCWalletPasswordHardware}\n        })\n\n        # add open existing wallet from wizard\n        self.navmap_merge({\n            'wallet_name': {\n                'next': lambda d: 'hw_unlock' if d['wallet_needs_hw_unlock'] else 'wallet_type',\n                'last': lambda d: d['wallet_exists'] and not d['wallet_needs_hw_unlock']\n            },\n        })\n\n        run_hook('init_wallet_wizard', self)\n\n    @property\n    def path(self):\n        return self._path\n\n    @path.setter\n    def path(self, path):\n        self._path = path\n\n    def is_single_password(self):\n        # not supported on desktop\n        return False\n\n    def create_storage(self, single_password: str = None):\n        self._logger.info('Creating wallet from wizard data')\n        data = self.get_wizard_data()\n\n        path = os.path.join(os.path.dirname(self._daemon.config.get_wallet_path()), data['wallet_name'])\n\n        super().create_storage(path, data)\n\n        # minimally populate self after create\n        self._password = data['password']\n        self.path = path\n\n    def run_split(self, wallet_path, split_data) -> None:\n        msg = _(\n            \"The wallet '{}' contains multiple accounts, which are no longer supported since Electrum 2.7.\\n\\n\"\n            \"Do you want to split your wallet into multiple files?\").format(wallet_path)\n        if self.question(msg):\n            file_list = WalletDB.split_accounts(wallet_path, split_data)\n            msg = _('Your accounts have been moved to') + ':\\n' + '\\n'.join(file_list) + '\\n\\n' + _(\n                'Do you want to delete the old file') + ':\\n' + wallet_path\n            if self.question(msg):\n                os.remove(wallet_path)\n                self.show_warning(_('The file was removed'))\n\n    def is_finalized(self, wizard_data: dict) -> bool:\n        # check decryption of existing wallet and keep wizard open if incorrect.\n\n        if not wizard_data['wallet_exists'] or wizard_data['wallet_is_open']:\n            return True\n\n        wallet_file = wizard_data['wallet_name']\n\n        storage = WalletStorage(wallet_file)\n        assert storage.file_exists(), f\"file {wallet_file!r} does not exist\"\n        if not storage.is_encrypted_with_user_pw() and not storage.is_encrypted_with_hw_device():\n            return True\n\n        try:\n            storage.decrypt(wizard_data['password'])\n        except InvalidPassword:\n            if storage.is_encrypted_with_hw_device():\n                self.show_message('This hardware device could not decrypt this wallet. Is it the correct one?')\n            else:\n                self.show_message('Invalid password')\n            return False\n\n        return True\n\n    def waiting_dialog(self, task, msg, on_finished=None):\n        dialog = QDialog()\n        label = WWLabel(msg)\n        vbox = QVBoxLayout()\n        vbox.addSpacing(100)\n        label.setMinimumWidth(300)\n        label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        vbox.addWidget(label)\n        vbox.addSpacing(100)\n        dialog.setLayout(vbox)\n        dialog.setModal(True)\n\n        exc = None\n\n        def task_wrap(_task):\n            nonlocal exc\n            try:\n                _task()\n            except Exception as e:\n                exc = e\n\n        t = threading.Thread(target=task_wrap, args=(task,))\n        t.start()\n\n        dialog.show()\n\n        while True:\n            QApplication.processEvents()\n            t.join(1.0/60)\n            if not t.is_alive():\n                break\n\n        dialog.close()\n\n        if exc:\n            raise exc\n\n        if on_finished:\n            on_finished()\n\n\nclass WalletWizardComponent(WizardComponent, ABC):\n    # ^ this class only exists to help with typing\n    wizard: QENewWalletWizard\n\n    def __init__(self, parent: QWidget, wizard: QENewWalletWizard, **kwargs):\n        WizardComponent.__init__(self, parent, wizard, **kwargs)\n\n\nclass WCWalletName(WalletWizardComponent, Logger):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Electrum wallet'))\n        Logger.__init__(self)\n\n        path = wizard._path\n\n        if os.path.isdir(path):\n            raise Exception(\"wallet path cannot point to a directory\")\n\n        self.wallet_exists = False\n        self.wallet_is_open = False\n        self.wallet_needs_hw_unlock = False\n\n        hbox = QHBoxLayout()\n        hbox.addWidget(QLabel(_('Wallet') + ':'))\n        self.name_e = QLineEdit()\n        hbox.addWidget(self.name_e)\n        button = QPushButton(_('Choose...'))\n        button_create_new = QPushButton(_('New'))\n        hbox.addWidget(button)\n        hbox.addWidget(button_create_new)\n        self.layout().addLayout(hbox)\n        outside_label = WWLabel('')\n        self.layout().addWidget(outside_label)\n\n        self.layout().addSpacing(50)\n        msg_label = WWLabel('')\n        self.layout().addWidget(msg_label)\n        hbox2 = QHBoxLayout()\n        self.pw_e = PasswordLineEdit('', self)\n        self.pw_e.setFixedWidth(17 * char_width_in_lineedit())\n        pw_label = QLabel(_('Password') + ':')\n        hbox2.addWidget(pw_label)\n        hbox2.addWidget(self.pw_e)\n        hbox2.addStretch()\n        self.layout().addLayout(hbox2)\n        self.layout().addStretch(1)\n\n        temp_storage = None  # type: Optional[WalletStorage]\n        datadir_wallet_folder = self.wizard.config.get_datadir_wallet_path()\n\n        def relative_path(path):\n            new_path = path\n            try:\n                if is_subpath(path, datadir_wallet_folder):\n                    # below datadir_wallet_path, make relative\n                    commonpath = os.path.commonpath([path, datadir_wallet_folder])\n                    new_path = os.path.relpath(path, commonpath)\n            except ValueError:\n                pass\n            return new_path\n\n        def on_choose():\n            _path, __ = QFileDialog.getOpenFileName(self, \"Select your wallet file\", datadir_wallet_folder)\n            if _path:\n                self.name_e.setText(relative_path(_path))\n\n        def on_filename(filename_or_path):\n            # Note: \"filename\" might contain \"..\" (etc) and hence sketchy path traversals are possible\n            nonlocal temp_storage\n            temp_storage = None\n            msg = None\n            self.wallet_exists = False\n            self.wallet_is_open = False\n            self.wallet_needs_hw_unlock = False\n            if filename_or_path:\n                # Note: if filename_or_path is a path, os.path.join will leave it unchanged\n                _path = os.path.join(datadir_wallet_folder, filename_or_path)\n                wallet_from_memory = self.wizard._daemon.get_wallet(_path)\n                try:\n                    if wallet_from_memory:\n                        temp_storage = wallet_from_memory.storage  # type: Optional[WalletStorage]\n                        self.wallet_is_open = True\n                    else:\n                        temp_storage = WalletStorage(_path)\n                    self.wallet_exists = temp_storage.file_exists()\n                except (StorageReadWriteError, WalletFileException) as e:\n                    msg = _('Cannot read file') + f'\\n{repr(e)}'\n                except Exception as e:\n                    self.logger.exception('')\n                    msg = _('Cannot read file') + f'\\n{repr(e)}'\n            else:\n                msg = \"\"\n            self.valid = temp_storage is not None\n            user_needs_to_enter_password = False\n            if temp_storage:\n                if not temp_storage.file_exists():\n                    msg = _(\"This file does not exist.\") + '\\n' \\\n                          + _(\"Press 'Next' to create this wallet, or choose another file.\")\n                elif not wallet_from_memory:\n                    if temp_storage.is_encrypted_with_user_pw():\n                        msg = _(\"This file is encrypted with a password.\")\n                        user_needs_to_enter_password = True\n                    elif temp_storage.is_encrypted_with_hw_device():\n                        msg = _(\"This file is encrypted using a hardware device.\") + '\\n' \\\n                              + _(\"Press 'Next' to choose device to decrypt.\")\n                        self.wallet_needs_hw_unlock = True\n                    else:\n                        msg = _(\"Press 'Finish' to open this wallet.\")\n                else:\n                    msg = _(\"This file is already open in memory.\") + \"\\n\" \\\n                          + _(\"Press 'Finish' to create/focus window.\")\n            if msg is None:\n                msg = _('Cannot read file')\n            if filename_or_path and os.path.isabs(relative_path(_path)):\n                outside_text = _('Note: this wallet file is outside the default wallets folder.')\n            else:\n                outside_text = ''\n            outside_label.setText(outside_text)\n            msg_label.setText(msg)\n            if user_needs_to_enter_password:\n                pw_label.show()\n                self.pw_e.show()\n                if not self.name_e.hasFocus():\n                    self.pw_e.setFocus()\n            else:\n                pw_label.hide()\n                self.pw_e.hide()\n            self.on_updated()\n\n        button.clicked.connect(on_choose)\n        button_create_new.clicked.connect(\n            lambda: self.name_e.setText(get_new_wallet_name(datadir_wallet_folder)))  # FIXME get_new_wallet_name might raise\n        self.name_e.textChanged.connect(on_filename)\n        self.name_e.setText(relative_path(path))\n\n    def initialFocus(self) -> Optional[QWidget]:\n        return self.pw_e\n\n    def apply(self):\n        if self.wallet_exists:\n            # use full path\n            wallet_folder = self.wizard.config.get_datadir_wallet_path()\n            self.wizard_data['wallet_name'] = os.path.join(wallet_folder, self.name_e.text())\n        else:\n            # FIXME: wizard_data['wallet_name'] is sometimes a full path, sometimes a basename\n            self.wizard_data['wallet_name'] = self.name_e.text()\n        self.wizard_data['wallet_exists'] = self.wallet_exists\n        self.wizard_data['wallet_is_open'] = self.wallet_is_open\n        self.wizard_data['password'] = self.pw_e.text()\n        self.wizard_data['wallet_needs_hw_unlock'] = self.wallet_needs_hw_unlock\n\n\nclass WCWalletType(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Create new wallet'))\n        message = _('What kind of wallet do you want to create?')\n        wallet_kinds = [\n            ChoiceItem(key='standard', label=_('Standard wallet')),\n            ChoiceItem(key='2fa', label=_('Wallet with two-factor authentication')),\n            ChoiceItem(key='multisig', label=_('Multi-signature wallet')),\n            ChoiceItem(key='imported', label=_('Import Bitcoin addresses or private keys')),\n        ]\n        choices = [c for c in wallet_kinds if c.key in wallet_types]\n\n        self.choice_w = ChoiceWidget(message=message, choices=choices, default_key='standard')\n        self.layout().addWidget(self.choice_w)\n        self.layout().addStretch(1)\n        self._valid = True\n\n    def apply(self):\n        self.wizard_data['wallet_type'] = self.choice_w.selected_key\n\n\nclass WCKeystoreType(WalletWizardComponent):\n\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Keystore'))\n        message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')\n        choices = [\n            ChoiceItem(key='createseed', label=_('Create a new seed')),\n            ChoiceItem(key='haveseed', label=_('I already have a seed')),\n            ChoiceItem(key='masterkey', label=_('Use a master key')),\n            ChoiceItem(key='hardware', label=_('Use a hardware device')),\n        ]\n        self.choice_w = ChoiceWidget(message=message, choices=choices)\n        self.layout().addWidget(self.choice_w)\n        self.layout().addStretch(1)\n        self._valid = True\n\n    def apply(self):\n        self.wizard_data['keystore_type'] = self.choice_w.selected_key\n\n\nclass WCExtendKeystore(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Keystore'))\n        message = _('What type of signing method do you want to add?')\n        choices = [\n            ChoiceItem(key='haveseed', label=_('Enter seed')),\n            ChoiceItem(key='hardware', label=_('Use a hardware device')),\n        ]\n        self.choice_w = ChoiceWidget(message=message, choices=choices)\n        self.layout().addWidget(self.choice_w)\n        self.layout().addStretch(1)\n        self._valid = True\n\n    def apply(self):\n        self.wizard_data['keystore_type'] = self.choice_w.selected_key\n\n\nclass WCCreateSeed(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Wallet Seed'))\n        self._busy = True\n        self.seed_type = 'standard' if self.wizard.config.WIZARD_DONT_CREATE_SEGWIT else 'segwit'\n        self.seed_widget = None\n        self.seed = None\n\n    def on_ready(self):\n        if self.wizard_data['wallet_type'] == '2fa':\n            self.seed_type = '2fa_segwit'\n        QTimer.singleShot(1, self.create_seed)\n\n    def apply(self):\n        if self.seed_widget:\n            self.wizard_data['seed'] = self.seed\n            self.wizard_data['seed_type'] = self.seed_type\n            self.wizard_data['seed_extend'] = self.seed_widget.is_ext\n            self.wizard_data['seed_variant'] = 'electrum'\n\n    def create_seed(self):\n        self.busy = True\n        self.seed = mnemonic.Mnemonic('en').make_seed(seed_type=self.seed_type)\n\n        self.seed_widget = SeedWidget(\n            title=_('Your wallet generation seed is:'),\n            seed=self.seed,\n            options=['ext', 'electrum'],\n            msg=True,\n            parent=self,\n            config=self.wizard.config,\n        )\n        self.layout().addWidget(self.seed_widget)\n        self.layout().addStretch(1)\n        self.busy = False\n        self.valid = True\n\n\nclass WCConfirmSeed(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Confirm Seed'))\n        message = ' '.join([\n            _('Your seed is important!'),\n            _('If you lose your seed, your money will be permanently lost.'),\n            _('To make sure that you have properly saved your seed, please retype it here.')\n        ])\n\n        self.layout().addWidget(WWLabel(message))\n\n        self.seed_widget = SeedWidget(\n            is_seed=lambda x: x == self.wizard_data['seed'],\n            config=self.wizard.config,\n        )\n\n        def seed_valid_changed(valid):\n            self.valid = valid\n\n        self.seed_widget.validChanged.connect(seed_valid_changed)\n        self.layout().addWidget(self.seed_widget)\n\n        wizard.app.clipboard().clear()\n\n    def apply(self):\n        pass\n\n\nclass WCEnterExt(WalletWizardComponent, Logger):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Seed Extension'))\n        Logger.__init__(self)\n\n        message = '\\n'.join([\n            _('You may extend your seed with custom words.'),\n            _('Your seed extension must be saved together with your seed.'),\n        ])\n        warning = '\\n'.join([\n            _('Note that this is NOT your encryption password.'),\n            _('If you do not know what this is, leave this field empty.'),\n        ])\n\n        self.ext_edit = SeedExtensionEdit(self, message=message, warning=warning)\n        self.ext_edit.textEdited.connect(self.on_text_edited)\n        self.layout().addWidget(self.ext_edit)\n        self.layout().addStretch(1)\n        self.warn_label = IconLabel(reverse=True, hide_if_empty=True)\n        self.warn_label.setIcon(read_QIcon('warning.png'))\n        self.layout().addWidget(self.warn_label)\n\n    def on_ready(self):\n        self.validate()\n\n    def on_text_edited(self, text):\n        # TODO also for cosigners?\n        self.ext_edit.warn_issue4566 = self.wizard_data['keystore_type'] == 'haveseed' and \\\n                                       self.wizard_data['seed_type'] == 'bip39'\n        self.validate()\n\n    def validate(self):\n        self.apply()\n\n        musig_valid, errortext = self.wizard.check_multisig_constraints(self.wizard_data)\n        self.valid = musig_valid\n        self.warn_label.setText(errortext)\n\n    def apply(self):\n        cosigner_data = self.wizard.current_cosigner(self.wizard_data)\n        cosigner_data['seed_extra_words'] = self.ext_edit.text()\n\n\nclass WCConfirmExt(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Confirm Seed Extension'))\n        message = '\\n'.join([\n            _('Your seed extension must be saved together with your seed.'),\n            _('Please type it here.'),\n        ])\n        self.ext_edit = SeedExtensionEdit(self, message=message)\n        self.ext_edit.textEdited.connect(self.on_text_edited)\n        self.layout().addWidget(self.ext_edit)\n        self.layout().addStretch(1)\n\n    def on_ready(self):\n        self.validate()\n\n    def on_text_edited(self, *args):\n        self.validate()\n\n    def validate(self):\n        self.valid = self.ext_edit.text() == self.wizard_data['seed_extra_words']\n\n    def apply(self):\n        pass\n\n\nclass WCHaveSeed(WalletWizardComponent, Logger):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Enter Seed'))\n        Logger.__init__(self)\n\n        self.layout().addWidget(WWLabel(_('Please enter your seed phrase in order to restore your wallet.')))\n        self.warn_label = IconLabel(reverse=True, hide_if_empty=True)\n        self.warn_label.setIcon(read_QIcon('warning.png'))\n\n        self.seed_widget = None\n        self.can_passphrase = True\n\n    def on_ready(self):\n        options = ['ext', 'electrum', 'bip39', 'slip39']\n        if self.wizard_data['wallet_type'] == '2fa':\n            options = ['ext', 'electrum']\n        else:\n            if self.params and 'seed_options' in self.params:\n                options = self.params['seed_options']\n\n        self.seed_widget = SeedWidget(\n            is_seed=self.is_seed,\n            options=options,\n            config=self.wizard.config,\n        )\n\n        def seed_valid_changed(valid):\n            if not valid:\n                self.valid = valid\n            else:\n                self.validate()\n\n        self.seed_widget.validChanged.connect(seed_valid_changed)\n        self.seed_widget.updated.connect(self.validate)\n\n        self.layout().addWidget(self.seed_widget)\n        self.layout().addStretch(1)\n\n        self.layout().addWidget(self.warn_label)\n\n    def is_seed(self, x):\n        # really only used for electrum seeds. bip39 and slip39 are validated in SeedWidget\n        t = mnemonic.calc_seed_type(x)\n        if self.wizard_data['wallet_type'] == 'standard':\n            return mnemonic.is_seed(x) and not mnemonic.is_any_2fa_seed_type(t)\n        elif self.wizard_data['wallet_type'] == '2fa':\n            return mnemonic.is_any_2fa_seed_type(t)\n        else:\n            # multisig?  by default, only accept modern non-2fa electrum seeds\n            return t in ['standard', 'segwit']\n\n    def validate(self):\n        # precond: only call when SeedWidget deems seed a valid seed\n        seed = self.seed_widget.get_seed()\n        seed_variant = self.seed_widget.seed_type\n        wallet_type = self.wizard_data['wallet_type']\n        seed_valid, seed_type, validation_message, self.can_passphrase = self.wizard.validate_seed(seed, seed_variant, wallet_type)\n\n        is_cosigner = self.wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in self.wizard_data\n\n        if not is_cosigner or not seed_valid:\n            self.valid = seed_valid\n            return\n\n        self.apply()\n        musig_valid, errortext = self.wizard.check_multisig_constraints(self.wizard_data)\n        if not musig_valid:\n            seed_valid = False\n\n        self.warn_label.setText(errortext)\n        self.valid = seed_valid\n\n    def apply(self):\n        cosigner_data = self.wizard.current_cosigner(self.wizard_data)\n\n        cosigner_data['seed'] = self.seed_widget.get_seed()\n        cosigner_data['seed_variant'] = self.seed_widget.seed_type\n        if self.seed_widget.seed_type == 'electrum':\n            cosigner_data['seed_type'] = mnemonic.calc_seed_type(self.seed_widget.get_seed())\n        else:\n            cosigner_data['seed_type'] = self.seed_widget.seed_type\n        cosigner_data['seed_extend'] = self.seed_widget.is_ext if self.can_passphrase else False\n\n\nclass WCScriptAndDerivation(WalletWizardComponent, Logger):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Script type and Derivation path'))\n        Logger.__init__(self)\n\n        self.choice_w = None  # type: ChoiceWidget\n        self.derivation_path_edit = None\n\n        self.warn_label = IconLabel(reverse=True, hide_if_empty=True)\n        self.warn_label.setIcon(read_QIcon('warning.png'))\n\n    def on_ready(self):\n        message1 = _('Choose the type of addresses in your wallet.')\n        message2 = ' '.join([\n            _('You can override the suggested derivation path.'),\n            _('If you are not sure what this is, leave this field unchanged.')\n        ])\n        hide_choices = False\n\n        if self.wizard_data['wallet_type'] == 'multisig':\n            choices = [\n                # TODO: nicer to refactor 'standard' to 'p2sh', but backend wallet still uses 'standard'\n                ChoiceItem(key='standard', label='legacy multisig (p2sh)',\n                           extra_data=normalize_bip32_derivation(\"m/45'/0\")),\n                ChoiceItem(key='p2wsh-p2sh', label='p2sh-segwit multisig (p2wsh-p2sh)',\n                           extra_data=purpose48_derivation(0, xtype='p2wsh-p2sh')),\n                ChoiceItem(key='p2wsh', label='native segwit multisig (p2wsh)',\n                           extra_data=purpose48_derivation(0, xtype='p2wsh')),\n            ]\n            if 'multisig_current_cosigner' in self.wizard_data:\n                # get script type of first cosigner\n                ks = self.wizard.keystore_from_data(self.wizard_data['wallet_type'], self.wizard_data)\n                default_choice = xpub_type(ks.get_master_public_key())\n                hide_choices = True\n            else:\n                default_choice = 'p2wsh'\n        else:\n            default_choice = 'p2wpkh'\n            choices = [\n                # TODO: nicer to refactor 'standard' to 'p2pkh', but backend wallet still uses 'standard'\n                ChoiceItem(key='standard', label='legacy (p2pkh)',\n                           extra_data=bip44_derivation(0, bip43_purpose=44)),\n                ChoiceItem(key='p2wpkh-p2sh', label='p2sh-segwit (p2wpkh-p2sh)',\n                           extra_data=bip44_derivation(0, bip43_purpose=49)),\n                ChoiceItem(key='p2wpkh', label='native segwit (p2wpkh)',\n                           extra_data=bip44_derivation(0, bip43_purpose=84)),\n            ]\n\n        if self.wizard_data['wallet_type'] == 'standard' and not self.wizard_data['keystore_type'] == 'hardware':\n            button = QPushButton(_(\"Detect Existing Accounts\"))\n\n            passphrase = self.wizard_data['seed_extra_words'] if self.wizard_data['seed_extend'] else ''\n            if self.wizard_data['seed_variant'] == 'bip39':\n                root_seed = bip39_to_seed(self.wizard_data['seed'], passphrase=passphrase)\n            elif self.wizard_data['seed_variant'] == 'slip39':\n                root_seed = self.wizard_data['seed'].decrypt(passphrase)\n\n            def get_account_xpub(account_path):\n                root_node = BIP32Node.from_rootseed(root_seed, xtype=\"standard\")\n                account_node = root_node.subkey_at_private_derivation(account_path)\n                account_xpub = account_node.to_xpub()\n                return account_xpub\n\n            def on_account_select(account):\n                script_type = account[\"script_type\"]\n                if script_type == \"p2pkh\":\n                    script_type = \"standard\"\n                self.choice_w.select(script_type)\n                self.derivation_path_edit.setText(account[\"derivation_path\"])\n\n            button.clicked.connect(lambda: Bip39RecoveryDialog(self, get_account_xpub, on_account_select))\n            self.layout().addWidget(button, alignment=Qt.AlignmentFlag.AlignLeft)\n            self.layout().addWidget(QLabel(_(\"Or\")))\n\n        def on_choice_click(index):\n            self.derivation_path_edit.setText(self.choice_w.selected_item.extra_data)\n        self.choice_w = ChoiceWidget(message=message1, choices=choices, default_key=default_choice)\n        self.choice_w.itemSelected.connect(on_choice_click)\n\n        if not hide_choices:\n            self.layout().addWidget(self.choice_w)\n\n        self.layout().addWidget(WWLabel(message2))\n\n        self.derivation_path_edit = QLineEdit()\n        self.derivation_path_edit.textChanged.connect(self.validate)\n        self.layout().addWidget(self.derivation_path_edit)\n\n        on_choice_click(self.choice_w.selected_index)  # set default value for derivation path\n\n        self.layout().addStretch(1)\n        self.layout().addWidget(self.warn_label)\n\n    def validate(self):\n        self.apply()\n\n        cosigner_data = self.wizard.current_cosigner(self.wizard_data)\n        valid = is_bip32_derivation(cosigner_data['derivation_path'])\n\n        if valid:\n            valid, errortext = self.wizard.check_multisig_constraints(self.wizard_data)\n            if not valid:\n                self.logger.error(errortext)\n            self.warn_label.setText(errortext)\n        else:\n            self.warn_label.setText(_('Invalid derivation path'))\n\n        self.valid = valid\n\n    def apply(self):\n        cosigner_data = self.wizard.current_cosigner(self.wizard_data)\n        cosigner_data['script_type'] = self.choice_w.selected_key\n        cosigner_data['derivation_path'] = str(self.derivation_path_edit.text())\n\n\nclass WCCosignerKeystore(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard)\n\n        message = _('Add a cosigner to your multi-sig wallet')\n        choices = [\n            ChoiceItem(key='masterkey', label=_('Enter cosigner key')),\n            ChoiceItem(key='haveseed', label=_('Enter cosigner seed')),\n            ChoiceItem(key='hardware', label=_('Cosign with hardware device')),\n        ]\n\n        self.choice_w = ChoiceWidget(message=message, choices=choices)\n        self.layout().addWidget(self.choice_w)\n\n        self.cosigner = 0\n        self.participants = 0\n\n        self._valid = True\n\n    def on_ready(self):\n        self.participants = self.wizard_data['multisig_participants']\n        # cosigner index is determined here and put on the wizard_data dict in apply()\n        # as this page is the start for each additional cosigner\n        self.cosigner = 2 + len(self.wizard_data['multisig_cosigner_data'])\n\n        self.wizard_data['multisig_current_cosigner'] = self.cosigner\n        self.title = _(\"Add Cosigner {}\").format(self.wizard_data['multisig_current_cosigner'])\n\n        # different from old wizard: master public key for sharing is now shown on this page\n        self.layout().addSpacing(20)\n        self.layout().addWidget(WWLabel(_('Below is your master public key. Please share it with your cosigners')))\n        seed_widget = SeedWidget(\n            self.wizard_data['multisig_master_pubkey'],\n            icon=False,\n            for_seed_words=False,\n            config=self.wizard.config,\n        )\n        self.layout().addWidget(seed_widget)\n        self.layout().addStretch(1)\n\n    def apply(self):\n        self.wizard_data['cosigner_keystore_type'] = self.choice_w.selected_key\n        self.wizard_data['multisig_current_cosigner'] = self.cosigner\n        self.wizard_data['multisig_cosigner_data'][str(self.cosigner)] = {\n            'keystore_type': self.choice_w.selected_key\n        }\n\n\nclass WCHaveMasterKey(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Create keystore from a master key'))\n\n        self.keys_widget = None\n\n        self.message_create = ' '.join([\n            _(\"To create a watching-only wallet, please enter your master public key (xpub/ypub/zpub).\"),\n            _(\"To create a spending wallet, please enter a master private key (xprv/yprv/zprv).\")\n        ])\n        self.message_multisig = ' '.join([\n            _('Please enter your master private key (xprv).'),\n            _('You can also enter a public key (xpub) here, but be aware you will then create a watch-only wallet if all cosigners are added using public keys'),\n        ])\n        self.message_cosign = ' '.join([\n            _('Please enter the master public key (xpub) of your cosigner.'),\n            _('Enter their master private key (xprv) if you want to be able to sign for them.')\n        ])\n\n        self.header_layout = QHBoxLayout()\n        self.label = WWLabel()\n        self.label.setMinimumWidth(400)\n        self.header_layout.addWidget(self.label)\n\n        self.warn_label = IconLabel(reverse=True, hide_if_empty=True)\n        self.warn_label.setIcon(read_QIcon('warning.png'))\n\n    def on_ready(self):\n        if self.wizard_data['wallet_type'] == 'standard':\n            self.label.setText(self.message_create)\n\n            def is_valid(x) -> bool:\n                self.apply()\n                key_valid, message = self.wizard.validate_master_key(x, self.wizard_data['wallet_type'])\n                self.warn_label.setText(message)\n                return key_valid\n        elif self.wizard_data['wallet_type'] == 'multisig':\n            if 'multisig_current_cosigner' in self.wizard_data:\n                self.title = _(\"Add Cosigner {}\").format(self.wizard_data['multisig_current_cosigner'])\n                self.label.setText(self.message_cosign)\n            else:\n                self.label.setText(self.message_multisig)\n\n            def is_valid(x) -> bool:\n                self.apply()\n                key_valid, message = self.wizard.validate_master_key(x, self.wizard_data['wallet_type'])\n                if not key_valid:\n                    self.warn_label.setText(message)\n                    return False\n                musig_valid, errortext = self.wizard.check_multisig_constraints(self.wizard_data)\n                self.warn_label.setText(errortext)\n                if not musig_valid:\n                    return False\n                return True\n        else:\n            raise Exception(f\"unexpected wallet type: {self.wizard_data['wallet_type']}\")\n\n        self.keys_widget = KeysWidget(parent=self, header_layout=self.header_layout, is_valid=is_valid,\n                                      allow_multi=False, config=self.wizard.config)\n\n        def key_valid_changed(valid):\n            self.valid = valid\n\n        self.keys_widget.validChanged.connect(key_valid_changed)\n\n        self.layout().addWidget(self.keys_widget)\n        self.layout().addStretch()\n        self.layout().addWidget(self.warn_label)\n\n    def apply(self):\n        text = self.keys_widget.get_text()\n        cosigner_data = self.wizard.current_cosigner(self.wizard_data)\n        cosigner_data['master_key'] = text\n\n\nclass WCMultisig(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Multi-Signature Wallet'))\n\n        def on_m(m):\n            m_label.setText(_('Require {0} signatures').format(m))\n            cw.set_m(m)\n            backup_warning_label.setVisible(cw.m != cw.n)\n\n        def on_n(n):\n            n_label.setText(_('From {0} cosigners').format(n))\n            cw.set_n(n)\n            m_edit.setMaximum(n)\n            backup_warning_label.setVisible(cw.m != cw.n)\n\n        backup_warning_label = WWLabel(_('Warning: to be able to restore a multisig wallet, '\n                                         'you should include the master public key for each cosigner '\n                                         'in all of your backups.'))\n\n        cw = CosignWidget(2, 2)\n        m_label = QLabel()\n        n_label = QLabel()\n\n        m_edit = QSlider(Qt.Orientation.Horizontal, self)\n        m_edit.setMinimum(1)\n        m_edit.setMaximum(2)\n        m_edit.setValue(2)\n        m_edit.valueChanged.connect(on_m)\n        on_m(m_edit.value())\n\n        n_edit = QSlider(Qt.Orientation.Horizontal, self)\n        n_edit.setMinimum(2)\n        n_edit.setMaximum(15)\n        n_edit.setValue(2)\n        n_edit.valueChanged.connect(on_n)\n        on_n(n_edit.value())\n\n        grid = QGridLayout()\n        grid.addWidget(n_label, 0, 0)\n        grid.addWidget(n_edit, 0, 1)\n        grid.addWidget(m_label, 1, 0)\n        grid.addWidget(m_edit, 1, 1)\n\n        self.layout().addWidget(cw)\n        self.layout().addWidget(WWLabel(_('Choose the number of signatures needed to unlock funds in your wallet:')))\n        self.layout().addLayout(grid)\n        self.layout().addSpacing(2 * char_width_in_lineedit())\n        self.layout().addWidget(backup_warning_label)\n        self.layout().addStretch(1)\n\n        self.n_edit = n_edit\n        self.m_edit = m_edit\n\n        self._valid = True\n\n    def apply(self):\n        self.wizard_data['multisig_participants'] = int(self.n_edit.value())\n        self.wizard_data['multisig_signatures'] = int(self.m_edit.value())\n        self.wizard_data['multisig_cosigner_data'] = {}\n\n\nclass WCImport(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Addresses or Private Keys'))\n        message = _(\n            'Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.')\n        header_layout = QHBoxLayout()\n        label = WWLabel(message)\n        label.setMinimumWidth(400)\n        header_layout.addWidget(label)\n        header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignmentFlag.AlignRight)\n\n        def is_valid(x) -> bool:\n            return keystore.is_address_list(x) or keystore.is_private_key_list(x, raise_on_error=True)\n\n        self.keys_widget = KeysWidget(header_layout=header_layout, is_valid=is_valid,\n                                      allow_multi=True, config=self.wizard.config)\n\n        def key_valid_changed(valid):\n            self.valid = valid\n\n        self.keys_widget.validChanged.connect(key_valid_changed)\n        self.layout().addWidget(self.keys_widget)\n\n    def apply(self):\n        text = self.keys_widget.get_text()\n        if keystore.is_address_list(text):\n            self.wizard_data['address_list'] = text\n        elif keystore.is_private_key_list(text):\n            self.wizard_data['private_key_list'] = text\n\n\nclass WCWalletPassword(WalletWizardComponent):\n\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Wallet Password'))\n        # TODO: PasswordLayout assumes a button, refactor PasswordLayout\n        # for now, fake next_button.setEnabled\n        class Hack:\n            def setEnabled(self2, b):\n                self.valid = b\n        self.next_button = Hack()\n        self.pw_layout = PasswordLayout(\n            msg=MSG_ENTER_PASSWORD,\n            kind=PW_NEW,\n            OK_button=self.next_button,\n        )\n        self.layout().addLayout(self.pw_layout.layout())\n        self.layout().addStretch(1)\n\n    def initialFocus(self) -> Optional[QWidget]:\n        return self.pw_layout.new_pw\n\n    def apply(self):\n        self.wizard_data['password'] = self.pw_layout.new_password()\n        self.wizard_data['encrypt'] = True\n\n\nclass SeedExtensionEdit(QWidget):\n    def __init__(self, parent, *, message: str = None, warning: str = None, warn_issue4566: bool = False):\n        super().__init__(parent)\n\n        self.warn_issue4566 = warn_issue4566\n\n        layout = QVBoxLayout()\n        self.setLayout(layout)\n\n        if message:\n            layout.addWidget(WWLabel(message))\n\n        self.line = QLineEdit()\n        layout.addWidget(self.line)\n\n        def f(text):\n            if self.warn_issue4566:\n                text_whitespace_normalised = ' '.join(text.split())\n                warn_issue4566_label.setVisible(text != text_whitespace_normalised)\n        self.line.textEdited.connect(f)\n\n        if warning:\n            layout.addWidget(WWLabel(warning))\n\n        warn_issue4566_label = WWLabel(MSG_PASSPHRASE_WARN_ISSUE4566)\n        warn_issue4566_label.setVisible(False)\n        layout.addWidget(warn_issue4566_label)\n\n        # expose textEdited signal and text() func to widget\n        self.textEdited = self.line.textEdited\n        self.text = self.line.text\n\n\nclass CosignWidget(QWidget):\n    def __init__(self, m, n):\n        QWidget.__init__(self)\n        self.size = max(120, 9 * font_height())\n        self.R = QRect(0, 0, self.size, self.size)\n        self.setGeometry(self.R)\n        self.setMinimumHeight(self.size)\n        self.setMaximumHeight(self.size)\n        self.m = m\n        self.n = n\n\n    def set_n(self, n):\n        self.n = n\n        self.update()\n\n    def set_m(self, m):\n        self.m = m\n        self.update()\n\n    def paintEvent(self, event):\n        bgcolor = self.palette().color(QPalette.ColorRole.Window)\n        pen = QPen(bgcolor, 7, Qt.PenStyle.SolidLine)\n        qp = QPainter()\n        qp.begin(self)\n        qp.setPen(pen)\n        qp.setRenderHint(QPainter.RenderHint.Antialiasing)\n        qp.setBrush(Qt.GlobalColor.gray)\n        for i in range(self.n):\n            alpha = int(16 * 360 * i/self.n)\n            alpha2 = int(16 * 360 * 1/self.n)\n            qp.setBrush(Qt.GlobalColor.green if i < self.m else Qt.GlobalColor.gray)\n            qp.drawPie(self.R, alpha, alpha2)\n        qp.end()\n\n\nclass WCChooseHWDevice(WalletWizardComponent, Logger):\n    scanFailed = pyqtSignal([str, str], arguments=['code', 'message'])\n    scanComplete = pyqtSignal()\n\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Choose Hardware Device'))\n        Logger.__init__(self)\n        self.scanFailed.connect(self.on_scan_failed)\n        self.scanComplete.connect(self.on_scan_complete)\n        self.plugins = wizard.plugins\n        self.config = wizard.config\n\n        self.error_l = WWLabel()\n        self.error_l.setVisible(False)\n\n        self.device_list = QWidget()\n        self.device_list_layout = QVBoxLayout()\n        self.device_list.setLayout(self.device_list_layout)\n        self.choice_w = None  # type: ChoiceWidget\n\n        self.rescan_button = QPushButton(_('Rescan devices'))\n        self.rescan_button.clicked.connect(self.on_rescan)\n\n        self.add_plugin_button = QPushButton(_('Add plugin'))\n        self.add_plugin_button.clicked.connect(self.on_add_plugin)\n\n        hbox = QHBoxLayout()\n        hbox.addStretch(1)\n        hbox.addWidget(self.rescan_button)\n        hbox.addWidget(self.add_plugin_button)\n        hbox.addStretch(1)\n\n        self.layout().addWidget(self.error_l)\n        self.layout().addWidget(self.device_list)\n        self.layout().addStretch(1)\n        self.layout().addLayout(hbox)\n        self.layout().addStretch(1)\n\n    def on_ready(self):\n        self.scan_devices()\n\n    def on_rescan(self):\n        self.scan_devices()\n\n    def on_add_plugin(self):\n        d = PluginsDialog(self.config, self.plugins)\n        d.exec()\n        self.scan_devices()\n\n    def on_scan_failed(self, code, message):\n        self.error_l.setText(message)\n        self.error_l.setVisible(True)\n        self.device_list.setVisible(False)\n\n        self.valid = False\n\n    def on_scan_complete(self):\n        self.error_l.setVisible(False)\n        self.device_list.setVisible(True)\n\n        choices = []  # type: List[ChoiceItem]\n        for name, info in self.devices:\n            state = _(\"initialized\") if info.initialized else _(\"wiped\")\n            label = info.label or _(\"An unnamed {}\").format(name)\n            try:\n                transport_str = info.device.transport_ui_string[:20]\n            except Exception:\n                transport_str = 'unknown transport'\n            descr = f\"{label} [{info.model_name or name}, {state}, {transport_str}]\"\n            choices.append(ChoiceItem(key=(name, info), label=descr))\n        msg = _('Select a device') + ':'\n\n        if self.choice_w:\n            self.device_list_layout.removeWidget(self.choice_w)\n\n        self.choice_w = ChoiceWidget(message=msg, choices=choices)\n        self.device_list_layout.addWidget(self.choice_w)\n\n        self.valid = True\n\n        if self.valid:\n            self.wizard.next_button.setFocus()\n        else:\n            self.rescan_button.setFocus()\n\n    def scan_devices(self):\n        self.valid = False\n        self.busy_msg = _('Scanning devices...')\n        self.busy = True\n\n        def scan_task():\n            # check available plugins\n            supported_plugins = self.plugins.get_hardware_support()\n            devices = []  # type: List[Tuple[str, DeviceInfo]]\n            devmgr = self.plugins.device_manager\n            debug_msg = ''\n\n            def failed_getting_device_infos(name, e):\n                nonlocal debug_msg\n                err_str_oneline = ' // '.join(str(e).splitlines())\n                self.logger.warning(f'error getting device infos for {name}: {err_str_oneline}')\n                _indented_error_msg = '    '.join([''] + str(e).splitlines(keepends=True))\n                debug_msg += f'  {name}: (error getting device infos)\\n{_indented_error_msg}\\n'\n\n            # scan devices\n            try:\n                # scanned_devices = self.run_task_without_blocking_gui(task=devmgr.scan_devices,\n                #                                                      msg=_(\"Scanning devices...\"))\n                scanned_devices = devmgr.scan_devices()\n            except BaseException as e:\n                self.logger.info('error scanning devices: {}'.format(repr(e)))\n                debug_msg = '  {}:\\n    {}'.format(_('Error scanning devices'), e)\n            else:\n                for splugin in supported_plugins:\n                    name, plugin = splugin.name, splugin.plugin\n                    # plugin init errored?\n                    if not plugin:\n                        e = splugin.exception\n                        indented_error_msg = '    '.join([''] + str(e).splitlines(keepends=True))\n                        debug_msg += f'  {name}: (error during plugin init)\\n'\n                        debug_msg += '    {}\\n'.format(_('You might have an incompatible library.'))\n                        debug_msg += f'{indented_error_msg}\\n'\n                        continue\n                    # see if plugin recognizes 'scanned_devices'\n                    try:\n                        # FIXME: side-effect: this sets client.handler\n                        device_infos = devmgr.list_pairable_device_infos(\n                            handler=None, plugin=plugin, devices=scanned_devices, include_failing_clients=True)\n                    except HardwarePluginLibraryUnavailable as e:\n                        failed_getting_device_infos(name, e)\n                        continue\n                    except BaseException as e:\n                        self.logger.exception('')\n                        failed_getting_device_infos(name, e)\n                        continue\n                    device_infos_failing = list(filter(lambda di: di.exception is not None, device_infos))\n                    for di in device_infos_failing:\n                        failed_getting_device_infos(name, di.exception)\n                    device_infos_working = list(filter(lambda di: di.exception is None, device_infos))\n                    devices += list(map(lambda x: (name, x), device_infos_working))\n            if not debug_msg:\n                debug_msg = '  {}'.format(_('No exceptions encountered.'))\n            if not devices:\n                msg = (_('No hardware device detected.') + '\\n\\n')\n                if sys.platform == 'win32':\n                    msg += _('If your device is not detected on Windows, go to \"Settings\", \"Devices\", \"Connected devices\", '\n                             'and do \"Remove device\". Then, plug your device again.') + '\\n'\n                    msg += _('While this is less than ideal, it might help if you run Electrum as Administrator.') + '\\n'\n                else:\n                    msg += _('On Linux, you might have to add a new permission to your udev rules.') + '\\n'\n                msg += '\\n\\n'\n                msg += _('Debug message') + '\\n' + debug_msg\n\n                self.scanFailed.emit('no_devices', msg)\n                self.busy = False\n                return\n\n            # select device\n            self.devices = devices\n            self.scanComplete.emit()\n            self.busy = False\n\n        t = threading.Thread(target=scan_task, daemon=True)\n        t.start()\n\n    def apply(self):\n        if self.choice_w:\n            cosigner_data = self.wizard.current_cosigner(self.wizard_data)\n            cosigner_data['hardware_device'] = self.choice_w.selected_key\n\n\nclass WCWalletPasswordHardware(WalletWizardComponent):\n\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Encrypt using hardware'))\n        self.plugins = wizard.plugins\n        # TODO: PasswordLayout assumes a button, refactor PasswordLayout\n        # for now, fake next_button.setEnabled\n        class Hack:\n            def setEnabled(self2, b):\n                self.valid = b\n        self.next_button = Hack()\n        self.playout = PasswordLayoutForHW(\n            MSG_HW_STORAGE_ENCRYPTION,\n            kind=PW_NEW,\n            OK_button=self.next_button,\n        )\n        self.layout().addLayout(self.playout.layout())\n        self.layout().addStretch(1)\n\n        self._hw_password = None  # type: Optional[str]\n        self._valid = False\n\n    def on_ready(self):\n        _name, info = self.wizard_data['hardware_device']\n        device_id = info.device.id_\n        client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)\n        if client is None:\n            self.valid = False\n            self.error = _(\"Client for hardware device was unpaired.\")\n            return\n\n        def retrieve_password_task():\n            try:\n                self._hw_password = client.get_password_for_storage_encryption()\n                self.valid = True\n            except UserFacingException as e:\n                self.error = str(e)\n                self.valid = False\n            finally:\n                self.busy = False\n\n        self.busy = True\n        t = threading.Thread(target=retrieve_password_task, daemon=True)\n        t.start()\n\n    def apply(self):\n        if not self.valid:\n            return\n        self.wizard_data['encrypt'] = True\n        if self.playout.should_encrypt_storage_with_xpub():\n            self.wizard_data['xpub_encrypt'] = True\n            assert self._hw_password\n            self.wizard_data['password'] = self._hw_password\n        else:\n            self.wizard_data['xpub_encrypt'] = False\n            self.wizard_data['password'] = self.playout.new_password()\n\n\nclass WCHWUnlock(WalletWizardComponent, Logger):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Unlocking hardware'))\n        Logger.__init__(self)\n        self.plugins = wizard.plugins\n        self.plugin = None\n        self._busy = True\n        self.password = None\n\n        ok_icon = QLabel()\n        ok_icon.setPixmap(QPixmap(icon_path('confirmed.png')).scaledToWidth(48, mode=Qt.TransformationMode.SmoothTransformation))\n        ok_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self.ok_l = WWLabel(_('Hardware successfully unlocked'))\n        self.ok_l.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self.layout().addStretch(1)\n        self.layout().addWidget(ok_icon)\n        self.layout().addWidget(self.ok_l)\n        self.layout().addStretch(1)\n\n    def on_ready(self):\n        _name, _info = self.wizard_data['hardware_device']\n        self.plugin = self.plugins.get_plugin(_info.plugin_name)\n        self.title = _('Unlocking {} ({})').format(_info.model_name, _info.label)\n\n        device_id = _info.device.id_\n        client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)\n        if client is None:\n            self.error = _(\"Client for hardware device was unpaired.\")\n            self.busy = False\n            self.validate()\n            return\n        client.handler = self.plugin.create_handler(self.wizard)\n\n        def unlock_task(client):\n            try:\n                self.password = client.get_password_for_storage_encryption()\n            except UserCancelled as e:\n                self.error = repr(e)\n            except Exception as e:\n                self.error = repr(e)  # TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully\n                self.logger.exception(repr(e))\n            self.busy = False\n            self.validate()\n\n        t = threading.Thread(target=unlock_task, args=(client,), daemon=True)\n        t.start()\n\n    def validate(self):\n        self.valid = False\n        if self.password and not self.error:\n            if not self.check_hw_decrypt():\n                self.error = _('This hardware device could not decrypt this wallet. Is it the correct one?')\n            else:\n                self.apply()\n                self.valid = True\n\n        if self.valid:\n            self.wizard.requestNext.emit()  # via signal, so it triggers Next/Finish on GUI thread after on_updated()\n\n    def check_hw_decrypt(self):\n        wallet_file = self.wizard_data['wallet_name']\n\n        storage = WalletStorage(wallet_file)\n        if not storage.is_encrypted_with_hw_device():\n            return True\n\n        try:\n            storage.decrypt(self.password)\n        except InvalidPassword:\n            return False\n        return True\n\n    def apply(self):\n        if self.valid:\n            self.wizard_data['password'] = self.password\n\n\nclass WCHWXPub(WalletWizardComponent, Logger):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Retrieving extended public key from hardware'))\n        Logger.__init__(self)\n        self.plugins = wizard.plugins\n        self.plugin = None\n        self._busy = True\n\n        self.xpub = None\n        self.root_fingerprint = None\n        self.label = None\n        self.soft_device_id = None\n\n        ok_icon = QLabel()\n        ok_icon.setPixmap(QPixmap(icon_path('confirmed.png')).scaledToWidth(48, mode=Qt.TransformationMode.SmoothTransformation))\n        ok_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self.ok_l = WWLabel(_('Hardware keystore added to wallet'))\n        self.ok_l.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self.layout().addStretch(1)\n        self.layout().addWidget(ok_icon)\n        self.layout().addWidget(self.ok_l)\n        self.layout().addStretch(1)\n\n    def on_ready(self):\n        cosigner_data = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = cosigner_data['hardware_device']\n        self.plugin = self.plugins.get_plugin(_info.plugin_name)\n        self.title = _('Retrieving extended public key from {} ({})').format(_info.model_name, _info.label)\n\n        device_id = _info.device.id_\n        client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)\n        if client is None:\n            self.error = _(\"Client for hardware device was unpaired.\")\n            self.busy = False\n            self.validate()\n            return\n        if not client.handler:\n            client.handler = self.plugin.create_handler(self.wizard)\n\n        xtype = cosigner_data['script_type']\n        derivation = cosigner_data['derivation_path']\n\n        def get_xpub_task(_client, _derivation, _xtype):\n            try:\n                self.xpub = self.get_xpub_from_client(_client, _derivation, _xtype)\n                self.root_fingerprint = _client.request_root_fingerprint_from_device()\n                self.label = _client.label()\n                self.soft_device_id = _client.get_soft_device_id()\n            except UserFacingException as e:\n                self.error = str(e)\n                self.logger.error(repr(e))\n            except Exception as e:\n                self.error = repr(e)  # TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully\n                self.logger.exception(repr(e))\n            if self.xpub:\n                self.logger.debug(f'Done retrieve xpub: {self.xpub[:10]}...{self.xpub[-5:]}')\n            self.busy = False\n            self.validate()\n\n        t = threading.Thread(target=get_xpub_task, args=(client, derivation, xtype), daemon=True)\n        t.start()\n\n    def get_xpub_from_client(self, client, derivation, xtype):  # override for HWW specific client if needed\n        cosigner_data = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = cosigner_data['hardware_device']\n        if xtype not in self.plugin.SUPPORTED_XTYPES:\n            raise ScriptTypeNotSupported(_('This type of script is not supported with {}').format(_info.model_name))\n        return client.get_xpub(derivation, xtype)\n\n    def validate(self):\n        if self.xpub and not self.error:\n            self.apply()\n            valid, error = self.wizard.check_multisig_constraints(self.wizard_data)\n            if not valid:\n                self.error = '\\n'.join([\n                    _('Could not add hardware keystore to wallet'),\n                    error\n                ])\n            self.valid = valid\n        else:\n            self.valid = False\n\n        if self.valid:\n            self.wizard.requestNext.emit()  # via signal, so it triggers Next/Finish on GUI thread after on_updated()\n\n    def apply(self):\n        cosigner_data = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = cosigner_data['hardware_device']\n        cosigner_data['hw_type'] = _info.plugin_name\n        cosigner_data['master_key'] = self.xpub\n        cosigner_data['root_fingerprint'] = self.root_fingerprint\n        cosigner_data['label'] = self.label\n        cosigner_data['soft_device_id'] = self.soft_device_id\n\n\nclass WCHWUninitialized(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Hardware not initialized'))\n\n    def on_ready(self):\n        cosigner_data = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = cosigner_data['hardware_device']\n        w_icon = QLabel()\n        w_icon.setPixmap(QPixmap(icon_path('warning.png')).scaledToWidth(48, mode=Qt.TransformationMode.SmoothTransformation))\n        w_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        label = WWLabel(_('This {} is not initialized. Use manufacturer tooling to initialize the device.').format(_info.model_name))\n        label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self.layout().addStretch(1)\n        self.layout().addWidget(w_icon)\n        self.layout().addWidget(label)\n        self.layout().addStretch(1)\n\n    def apply(self):\n        pass\n"
  },
  {
    "path": "electrum/gui/qt/wizard/wizard.py",
    "content": "import copy\nimport threading\nfrom abc import abstractmethod\nfrom typing import TYPE_CHECKING, Optional\n\nfrom PyQt6.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QSize, QMetaObject\nfrom PyQt6.QtGui import QPixmap\nfrom PyQt6.QtWidgets import (QDialog, QPushButton, QWidget, QLabel, QVBoxLayout, QScrollArea,\n                             QHBoxLayout, QLayout)\n\nfrom electrum.i18n import _\nfrom electrum.logging import get_logger\nfrom electrum.gui.qt.util import Buttons, icon_path, MessageBoxMixin, WWLabel, ResizableStackedWidget, AbstractQWidget\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n    from electrum.gui.qt import QElectrumApplication\n    from electrum.wizard import WizardViewState\n\n\nclass QEAbstractWizard(QDialog, MessageBoxMixin):\n    \"\"\" Concrete subclasses of QEAbstractWizard must also inherit from a concrete AbstractWizard subclass.\n        QEAbstractWizard forms the base for all QtWidgets GUI based wizards, while AbstractWizard defines\n        the base for non-gui wizard flow navigation functionality.\n    \"\"\"\n    _logger = get_logger(__name__)\n\n    requestNext = pyqtSignal()\n    requestPrev = pyqtSignal()\n\n    def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', *, start_viewstate: 'WizardViewState' = None):\n        QDialog.__init__(self, None)\n        self.app = app\n        self.config = config\n\n        # compat\n        self.gui_thread = threading.current_thread()\n\n        self.setMinimumSize(600, 400)\n\n        self.title = QLabel()\n        self.window_title = ''\n        self.finish_label = _('Finish')\n\n        self.main_widget = ResizableStackedWidget(self)\n\n        self.back_button = QPushButton(_(\"Back\"), self)\n        self.back_button.clicked.connect(self.on_back_button_clicked)\n        self.back_button.setEnabled(False)\n        self.back_button.setDefault(False)\n        self.back_button.setAutoDefault(False)\n        self.next_button = QPushButton(_(\"Next\"), self)\n        self.next_button.clicked.connect(self.on_next_button_clicked)\n        self.next_button.setEnabled(False)\n        self.next_button.setDefault(True)\n        self.next_button.setAutoDefault(True)\n        self.requestPrev.connect(self.on_back_button_clicked)\n        self.requestNext.connect(self.on_next_button_clicked)\n        self.logo = QLabel()\n\n        please_wait_layout = QVBoxLayout()\n        please_wait_layout.addStretch(1)\n        self.please_wait_l = QLabel(_(\"Please wait...\"))\n        self.please_wait_l.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        please_wait_layout.addWidget(self.please_wait_l)\n        please_wait_layout.addStretch(1)\n        self.please_wait = QWidget()\n        self.please_wait.setVisible(False)\n        self.please_wait.setLayout(please_wait_layout)\n\n        error_layout = QVBoxLayout()\n        error_layout.addStretch(1)\n        error_icon = QLabel()\n        error_icon.setPixmap(QPixmap(icon_path('warning.png')).scaledToWidth(48, mode=Qt.TransformationMode.SmoothTransformation))\n        error_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        error_layout.addWidget(error_icon)\n        self.error_msg = WWLabel()\n        self.error_msg.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        error_layout.addWidget(self.error_msg)\n        error_layout.addStretch(1)\n        self.error = QWidget()\n        self.error.setVisible(False)\n        self.error.setLayout(error_layout)\n\n        outer_vbox = QVBoxLayout(self)\n        inner_vbox = QVBoxLayout()\n        inner_vbox.addWidget(self.title)\n        inner_vbox.addWidget(self.main_widget)\n        inner_vbox.addWidget(self.please_wait)\n        inner_vbox.addWidget(self.error)\n\n        scroll_widget = QWidget()\n        scroll_widget.setLayout(inner_vbox)\n        scroll = QScrollArea()\n        scroll.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n        scroll.setWidget(scroll_widget)\n        scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)\n        scroll.setWidgetResizable(True)\n        icon_vbox = QVBoxLayout()\n        icon_vbox.addWidget(self.logo)\n        icon_vbox.addStretch(1)\n        hbox = QHBoxLayout()\n        hbox.addLayout(icon_vbox)\n        hbox.addSpacing(5)\n        hbox.addWidget(scroll)\n        hbox.setStretchFactor(scroll, 1)\n        outer_vbox.addLayout(hbox)\n        outer_vbox.addLayout(Buttons(self.back_button, self.next_button))\n\n        self.setTabOrder(self.back_button, self.next_button)\n\n        self.icon_filename = None\n        self.set_icon('electrum.png')\n\n        self.start_viewstate = start_viewstate\n\n        self.show()\n        self.raise_()\n\n        QMetaObject.invokeMethod(self, 'strt', Qt.ConnectionType.QueuedConnection)  # call strt after subclass constructor(s)\n\n    def sizeHint(self) -> QSize:\n        return QSize(600, 400)\n\n    @pyqtSlot()\n    def strt(self):\n        viewstate = self.start_wizard(start_viewstate=self.start_viewstate)\n        self.load_next_component(viewstate.view, viewstate.wizard_data, viewstate.params)\n        self.set_default_focus()\n\n        # TODO: re-test if needed on macOS\n        self.refresh_gui()  # Need for QT on MacOSX.  Lame.\n\n    def refresh_gui(self):\n        # For some reason, to refresh the GUI this needs to be called twice\n        self.app.processEvents()\n        self.app.processEvents()\n\n    def load_next_component(self, view, wdata=None, params=None):\n        if wdata is None:\n            wdata = {}\n        if params is None:\n            params = {}\n\n        comp = self.view_to_component(view)\n        try:\n            self._logger.debug(f'load_next_component: {comp!r}')\n            page = comp(self.main_widget, self)\n        except Exception as e:\n            self._logger.error(f'not a class: {comp!r}')\n            raise e\n        page.wizard_data = copy.deepcopy(wdata)\n        page.params = params\n        page.on_ready()  # call before component emits any signals\n\n        page.updated.connect(self.on_page_updated)\n\n        # add to stack and update wizard\n        page.apply()\n        self.main_widget.setCurrentIndex(self.main_widget.addWidget(page))\n        self.update()\n\n    @pyqtSlot(object)\n    def on_page_updated(self, page):\n        page.apply()\n        if page == self.main_widget.currentWidget():\n            self.update()\n\n    def set_icon(self, filename):\n        prior_filename, self.icon_filename = self.icon_filename, filename\n        self.logo.setPixmap(QPixmap(icon_path(filename))\n                            .scaledToWidth(60, mode=Qt.TransformationMode.SmoothTransformation))\n        return prior_filename\n\n    def set_default_focus(self):\n        page = self.main_widget.currentWidget()\n        control = page.initialFocus()\n        if control and control.isVisible() and control.isEnabled():\n            control.setFocus()\n        else:\n            self.next_button.setFocus()\n\n    def can_go_back(self) -> bool:\n        return len(self._stack) > 0\n\n    def update(self):\n        page = self.main_widget.currentWidget()\n        self.setWindowTitle(page.wizard_title if page.wizard_title else self.window_title)\n        self.title.setText(f'<b>{page.title}</b>' if page.title else '')\n        self.back_button.setText(_('Back') if self.can_go_back() else _('Cancel'))\n        self.back_button.setEnabled(not page.busy)\n        self.next_button.setText(_('Next') if not self.is_last(page.wizard_data) else self.finish_label)\n        self.next_button.setEnabled(not page.busy and page.valid)\n        self.main_widget.setVisible(not page.busy and not bool(page.error))\n        self.please_wait.setVisible(page.busy)\n        self.please_wait_l.setText(page.busy_msg if page.busy_msg else _(\"Please wait...\"))\n        self.error_msg.setText(str(page.error))\n        self.error.setVisible(not page.busy and bool(page.error))\n        icon = page.params.get('icon', icon_path('electrum.png'))\n        if icon:\n            if icon != self.icon_filename:\n                self.set_icon(icon)\n            self.logo.setVisible(True)\n        else:\n            self.logo.setVisible(False)\n\n    def on_back_button_clicked(self):\n        if self.can_go_back():\n            self.prev()\n            widget = self.main_widget.currentWidget()\n            self.main_widget.removeWidget(widget)\n            widget.deleteLater()\n            self.update()\n        else:\n            self.close()\n\n    def on_next_button_clicked(self):\n        page = self.main_widget.currentWidget()\n        page.apply()\n        wd = page.wizard_data.copy()\n        if self.is_last(wd):\n            self.submit(wd)\n            if self.is_finalized(wd):\n                self.accept()\n            else:\n                self.prev()  # rollback the submit above\n        else:\n            view = self.submit(wd)\n            try:\n                self.load_next_component(view.view, view.wizard_data, view.params)\n                self.set_default_focus()\n            except Exception as e:\n                self.prev()  # rollback the submit above\n                raise e\n\n    def start_wizard(self, *, start_viewstate: Optional['WizardViewState'] = None) -> 'WizardViewState':\n        self.start(start_viewstate=start_viewstate)\n        return self._current\n\n    def view_to_component(self, view) -> QWidget:\n        return self.navmap[view]['gui']\n\n    def submit(self, wizard_data) -> 'WizardViewState':\n        wdata = wizard_data.copy()\n        view = self.resolve_next(self._current.view, wdata)\n        return view\n\n    def prev(self) -> dict:\n        viewstate = self.resolve_prev()\n        return viewstate.wizard_data\n\n    def is_last(self, wizard_data: dict) -> bool:\n        wdata = wizard_data.copy()\n        return self.is_last_view(self._current.view, wdata)\n\n    def is_finalized(self, wizard_data: dict) -> bool:\n        ''' Final check before closing the wizard. '''\n        return True\n\n\nclass WizardComponent(AbstractQWidget):\n    updated = pyqtSignal(object)\n\n    def __init__(self, parent: QWidget, wizard: QEAbstractWizard, *, title: str = None, layout: QLayout = None):\n        super().__init__(parent)\n        self.setLayout(layout if layout else QVBoxLayout(self))\n        self.wizard_data = {}\n        self.title = title if title is not None else 'No title'\n        self.wizard_title = None\n        self.busy_msg = ''\n        self.wizard = wizard\n        self._error = ''\n        self._valid = False\n        self._busy = False\n\n    @property\n    def valid(self):\n        return self._valid\n\n    @valid.setter\n    def valid(self, is_valid):\n        if self._valid != is_valid:\n            self._valid = is_valid\n            self.on_updated()\n\n    @property\n    def busy(self):\n        return self._busy\n\n    @busy.setter\n    def busy(self, is_busy):\n        if self._busy != is_busy:\n            self._busy = is_busy\n            self.on_updated()\n\n    @property\n    def error(self):\n        return self._error\n\n    @error.setter\n    def error(self, error):\n        if self._error != error:\n            self._error = error\n            self.on_updated()\n\n    @abstractmethod\n    def apply(self):\n        # called to apply UI component values to wizard_data\n        pass\n\n    def on_ready(self):\n        # called when wizard_data is available\n        pass\n\n    @pyqtSlot()\n    def on_updated(self, *args):\n        try:\n            self.updated.emit(self)\n        except RuntimeError:\n            pass\n\n    def initialFocus(self) -> Optional[QWidget]:\n        \"\"\"Override to specify a control that should receive initial focus\"\"\"\n        return None\n"
  },
  {
    "path": "electrum/gui/stdio.py",
    "content": "from decimal import Decimal\nimport getpass\nimport datetime\nimport logging\nfrom typing import Optional\n\nfrom electrum.gui import BaseElectrumGui\nfrom electrum import util\nfrom electrum import WalletStorage, Wallet\nfrom electrum.wallet import Abstract_Wallet\nfrom electrum.wallet_db import WalletDB\nfrom electrum.util import format_satoshis, EventListener, event_listener\nfrom electrum.bitcoin import is_address, COIN\nfrom electrum.transaction import PartialTxOutput\nfrom electrum.network import TxBroadcastError, BestEffortRequestFailed\nfrom electrum.fee_policy import FixedFeePolicy\n\n_ = lambda x:x  # i18n\n\n# minimal fdisk like gui for console usage\n# written by rofl0r, with some bits stolen from the text gui (ncurses)\n\n\nclass ElectrumGui(BaseElectrumGui, EventListener):\n\n    def __init__(self, *, config, daemon, plugins):\n        BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins)\n        self.network = daemon.network\n        storage = WalletStorage(config.get_wallet_path())\n        password = None\n        if not storage.file_exists():\n            print(\"Wallet not found. try 'electrum create'\")\n            exit()\n        if storage.is_encrypted():\n            password = getpass.getpass('Password:', stream=None)\n            storage.decrypt(password)\n        del storage\n        self.wallet = self.daemon.load_wallet(config.get_wallet_path(), password)\n        self.contacts = self.wallet.contacts\n\n        self.done = 0\n        self.last_balance = \"\"\n\n        self.str_recipient = \"\"\n        self.str_description = \"\"\n        self.str_amount = \"\"\n        self.str_fee = \"\"\n\n        self.register_callbacks()\n        self.commands = [_(\"[h] - displays this help text\"), \\\n                         _(\"[i] - display transaction history\"), \\\n                         _(\"[o] - enter payment order\"), \\\n                         _(\"[p] - print stored payment order\"), \\\n                         _(\"[s] - send stored payment order\"), \\\n                         _(\"[r] - show own receipt addresses\"), \\\n                         _(\"[c] - display contacts\"), \\\n                         _(\"[b] - print server banner\"), \\\n                         _(\"[q] - quit\")]\n        self.num_commands = len(self.commands)\n\n    @event_listener\n    def on_event_wallet_updated(self, wallet):\n        self.updated()\n\n    @event_listener\n    def on_event_network_updated(self):\n        self.updated()\n\n    @event_listener\n    def on_event_banner(self, *args):\n        self.print_banner()\n\n    def main_command(self):\n        self.print_balance()\n        c = input(\"enter command: \")\n        if c == \"h\" : self.print_commands()\n        elif c == \"i\" : self.print_history()\n        elif c == \"o\" : self.enter_order()\n        elif c == \"p\" : self.print_order()\n        elif c == \"s\" : self.send_order()\n        elif c == \"r\" : self.print_addresses()\n        elif c == \"c\" : self.print_contacts()\n        elif c == \"b\" : self.print_banner()\n        elif c == \"n\" : self.network_dialog()\n        elif c == \"e\" : self.settings_dialog()\n        elif c == \"q\" : self.done = 1\n        else: self.print_commands()\n\n    def updated(self):\n        s = self.get_balance()\n        if s != self.last_balance:\n            print(s)\n        self.last_balance = s\n        return True\n\n    def print_commands(self):\n        self.print_list(self.commands, \"Available commands\")\n\n    def print_history(self):\n        width = [20, 40, 14, 14]\n        delta = (80 - sum(width) - 4)/3\n        format_str = \"%\"+\"%d\"%width[0]+\"s\"+\"%\"+\"%d\"%(width[1]+delta)+\"s\"+\"%\" \\\n        + \"%d\"%(width[2]+delta)+\"s\"+\"%\"+\"%d\"%(width[3]+delta)+\"s\"\n        messages = []\n        domain = self.wallet.get_addresses()\n        for hist_item in reversed(self.wallet.adb.get_history(domain)):\n            if hist_item.tx_mined_status.conf:\n                timestamp = hist_item.tx_mined_status.timestamp\n                try:\n                    time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]\n                except Exception:\n                    time_str = \"unknown\"\n            else:\n                time_str = 'unconfirmed'\n\n            label = self.wallet.get_label_for_txid(hist_item.txid)\n            messages.append(format_str % (\n                time_str, label,\n                format_satoshis(hist_item.delta, whitespaces=True),\n                format_satoshis(hist_item.balance, whitespaces=True)))\n\n        self.print_list(messages[::-1], format_str%(_(\"Date\"), _(\"Description\"), _(\"Amount\"), _(\"Balance\")))\n\n\n    def print_balance(self):\n        print(self.get_balance())\n\n    def get_balance(self):\n        network = self.wallet.network\n        if network and network.is_connected():\n            if not self.wallet.is_up_to_date():\n                msg = _(\"Synchronizing...\")\n            else:\n                c, u, x =  self.wallet.get_balance()\n                msg = _(\"Balance\")+\": {}  \".format(Decimal(c) / COIN)\n                if u:\n                    msg += \"  [{} unconfirmed]\".format(Decimal(u) / COIN)\n                if x:\n                    msg += \"  [{} unmatured]\".format(Decimal(x) / COIN)\n        else:\n                msg = _(\"Not connected\")\n\n        return msg\n\n\n    def print_contacts(self):\n        messages = map(lambda x: \"%20s   %45s \"%(x[0], x[1][1]), self.contacts.items())\n        self.print_list(messages, \"%19s  %25s \"%(\"Key\", \"Value\"))\n\n    def print_addresses(self):\n        messages = map(lambda addr: \"%30s    %30s       \"%(addr, self.wallet.get_label_for_address(addr)), self.wallet.get_addresses())\n        self.print_list(messages, \"%19s  %25s \"%(\"Address\", \"Label\"))\n\n    def print_order(self):\n        print(\"send order to \" + self.str_recipient + \", amount: \" + self.str_amount \\\n              + \"\\nfee: \" + self.str_fee + \", desc: \" + self.str_description)\n\n    def enter_order(self):\n        self.str_recipient = input(\"Pay to: \")\n        self.str_description = input(\"Description : \")\n        self.str_amount = input(\"Amount: \")\n        self.str_fee = input(\"Fee: \")\n\n    def send_order(self):\n        self.do_send()\n\n    def print_banner(self):\n        for i, x in enumerate(self.wallet.network.banner.split('\\n')):\n            print(x)\n\n    def print_list(self, lst, firstline):\n        lst = list(lst)\n        self.maxpos = len(lst)\n        if not self.maxpos: return\n        print(firstline)\n        for i in range(self.maxpos):\n            msg = lst[i] if i < len(lst) else \"\"\n            print(msg)\n\n\n    def main(self):\n        self.daemon.start_network()\n        while self.done == 0:\n            self.main_command()\n\n    def do_send(self):\n        if not is_address(self.str_recipient):\n            print(_('Invalid Bitcoin address'))\n            return\n        try:\n            amount = int(Decimal(self.str_amount) * COIN)\n        except Exception:\n            print(_('Invalid Amount'))\n            return\n        try:\n            fee = int(Decimal(self.str_fee) * COIN)\n        except Exception:\n            print(_('Invalid Fee'))\n            return\n\n        if self.wallet.has_password():\n            password = self.password_dialog()\n            if not password:\n                return\n        else:\n            password = None\n\n        c = \"\"\n        while c != \"y\":\n            c = input(\"ok to send (y/n)?\")\n            if c == \"n\": return\n\n        try:\n            tx = self.wallet.make_unsigned_transaction(\n                outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)],\n                fee_policy=FixedFeePolicy(fee),\n            )\n            self.wallet.sign_transaction(tx, password)\n        except Exception as e:\n            print(repr(e))\n            return\n\n        if self.str_description:\n            self.wallet.set_label(tx.txid(), self.str_description)\n\n        print(_(\"Please wait...\"))\n        try:\n            self.network.run_from_another_thread(self.network.broadcast_transaction(tx))\n        except TxBroadcastError as e:\n            msg = e.get_message_for_gui()\n            print(msg)\n        except BestEffortRequestFailed as e:\n            msg = repr(e)\n            print(msg)\n        else:\n            print(_('Payment sent.'))\n            #self.do_clear()\n            #self.update_contacts_tab()\n\n    def network_dialog(self):\n        print(\"use 'electrum setconfig server/proxy' to change your network settings\")\n        return True\n\n\n    def settings_dialog(self):\n        print(\"use 'electrum setconfig' to change your settings\")\n        return True\n\n    def password_dialog(self):\n        return getpass.getpass()\n\n\n#   XXX unused\n\n    def run_receive_tab(self, c):\n        #if c == 10:\n        #    out = self.run_popup('Address', [\"Edit label\", \"Freeze\", \"Prioritize\"])\n        return\n\n    def run_contacts_tab(self, c):\n        pass\n"
  },
  {
    "path": "electrum/gui/text.py",
    "content": "import tty\nimport sys\nimport curses\nimport datetime\nimport locale\nfrom decimal import Decimal\nimport getpass\nfrom typing import TYPE_CHECKING, Optional\n\n# 3rd-party dependency:\ntry:\n    import pyperclip\nexcept ImportError:  # only use vendored lib as fallback, to allow Linux distros to bring their own\n    from electrum._vendor import pyperclip\n\nfrom electrum.gui import BaseElectrumGui\nfrom electrum.bip21 import parse_bip21_URI\nfrom electrum.util import format_time\nfrom electrum.util import EventListener, event_listener\nfrom electrum.bitcoin import is_address, address_to_script\nfrom electrum.transaction import PartialTxOutput\nfrom electrum.wallet import Wallet, Abstract_Wallet\nfrom electrum.wallet_db import WalletDB\nfrom electrum.storage import WalletStorage\nfrom electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed, ProxySettings\nfrom electrum.interface import ServerAddr\nfrom electrum.invoices import Invoice\nfrom electrum.fee_policy import FeePolicy\n\nif TYPE_CHECKING:\n    from electrum.daemon import Daemon\n    from electrum.simple_config import SimpleConfig\n    from electrum.plugin import Plugins\n\n\n_ = lambda x:x  # i18n\n\n\n# ascii key codes\nKEY_BACKSPACE = 8\nKEY_ESC = 27\nKEY_DELETE = 127\n\n\ndef parse_bip21(text):\n    try:\n        return parse_bip21_URI(text)\n    except Exception:\n        return\n\n\ndef parse_bolt11(text):\n    from electrum.lnaddr import lndecode\n    try:\n        return lndecode(text)\n    except Exception:\n        return\n\n\nclass ElectrumGui(BaseElectrumGui, EventListener):\n\n    def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'):\n        BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins)\n        self.network = daemon.network\n        storage = WalletStorage(config.get_wallet_path())\n        password = None\n        if not storage.file_exists():\n            print(\"Wallet not found. try 'electrum create'\")\n            exit()\n        if storage.is_encrypted():\n            password = getpass.getpass('Password:', stream=None)\n        del storage\n        self.wallet = self.daemon.load_wallet(config.get_wallet_path(), password)\n        self.contacts = self.wallet.contacts\n\n        locale.setlocale(locale.LC_ALL, '')\n        self.encoding = locale.getpreferredencoding()\n\n        self.stdscr = curses.initscr()\n        curses.noecho()\n        curses.cbreak()\n        curses.start_color()\n        curses.use_default_colors()\n        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)\n        curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_CYAN)\n        curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE)\n        curses.halfdelay(1)\n        self.stdscr.keypad(True)\n        self.stdscr.border(0)\n        self.maxy, self.maxx = self.stdscr.getmaxyx()\n        self.set_cursor(0)\n        self.w = curses.newwin(10, 50, 5, 5)\n\n        self.lightning_invoice = None\n        self.tab = 0\n        self.pos = 0\n        self.popup_pos = 0\n\n        self.str_recipient = \"\"\n        self.str_description = \"\"\n        self.str_amount = \"\"\n        self.history = None\n        self.txid = []\n        self.str_recv_description = \"\"\n        self.str_recv_amount = \"\"\n        self.str_recv_expiry = \"\"\n        self.channel_ids = []\n        self.requests = []\n\n        self.register_callbacks()\n        self.tab_names = [_(\"History\"), _(\"Send\"), _(\"Receive\"), _(\"Addresses\"), _(\"Coins\"), _(\"Channels\"), _(\"Contacts\"), _(\"Banner\")]\n        self.num_tabs = len(self.tab_names)\n        self.need_update = False\n\n    def stop(self):\n        self.tab = -1\n\n    @event_listener\n    def on_event_wallet_updated(self, wallet):\n        self.need_update = True\n\n    @event_listener\n    def on_event_network_updated(self):\n        self.need_update = True\n\n    def set_cursor(self, x):\n        try:\n            curses.curs_set(x)\n        except Exception:\n            pass\n\n    def restore_or_create(self):\n        pass\n\n    def verify_seed(self):\n        pass\n\n    def get_string(self, y, x) -> str:\n        self.set_cursor(1)\n        curses.echo()\n        self.stdscr.addstr(y, x, \" \"*20, curses.A_REVERSE)\n        s = self.stdscr.getstr(y,x).decode()\n        curses.noecho()\n        self.set_cursor(0)\n        return s\n\n    def update(self):\n        self.update_history()\n        if self.tab == 0:\n            self.print_history()\n        self.refresh()\n        self.need_update = False\n\n    def print_button(self, x, y, text, pos):\n        self.stdscr.addstr(x, y, text, curses.A_REVERSE if self.pos%self.max_pos==pos else curses.color_pair(2))\n\n    def print_edit_line(self, y, x, label, text, index, size):\n        text += \" \"*(size - len(text))\n        self.stdscr.addstr(y, x, label)\n        self.stdscr.addstr(y, x + 13, text, curses.A_REVERSE if self.pos%self.max_pos==index else curses.color_pair(1))\n\n    def print_history(self):\n        x = 2\n        self.history_format_str = self.format_column_width(x, [-20, '*', 15, 15])\n        if self.history is None:\n            self.update_history()\n        self.print_list(2, x, self.history[::-1], headers=self.history_format_str%(_(\"Date\"), _(\"Description\"), _(\"Amount\"), _(\"Balance\")))\n\n    def update_history(self):\n        width = [20, 40, 14, 14]\n        delta = (self.maxx - sum(width) - 4)/3\n        domain = self.wallet.get_addresses()\n        self.history = []\n        self.txid = []\n        balance_sat = 0\n        for item in self.wallet.get_full_history().values():\n            amount_sat = item['value'].value\n            balance_sat += amount_sat\n            if item.get('lightning'):\n                timestamp = item['timestamp']\n                label = self.wallet.get_label_for_rhash(item['payment_hash'])\n                self.txid.insert(0, item['payment_hash'])\n            else:\n                conf = item['confirmations']\n                timestamp = item['timestamp'] if conf > 0 else 0\n                label = self.wallet.get_label_for_txid(item['txid'])\n                self.txid.insert(0, item['txid'])\n            if timestamp:\n                time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]\n            else:\n                time_str = 'unconfirmed'\n\n            if len(label) > 40:\n                label = label[0:37] + '...'\n            self.history.append(self.history_format_str % (\n                time_str, label,\n                self.config.format_amount(amount_sat, whitespaces=True),\n                self.config.format_amount(balance_sat, whitespaces=True)))\n\n    def print_clipboard(self):\n        return\n        c = pyperclip.paste()\n        if c:\n            if len(c) > 20:\n                c = c[0:20] + '...'\n            self.stdscr.addstr(self.maxy -1, self.maxx // 3, ' ' + _('Clipboard') + ': ' + c + ' ')\n\n    def print_balance(self):\n        if not self.network:\n            msg = _(\"Offline\")\n        elif self.network.is_connected():\n            if not self.wallet.is_up_to_date():\n                msg = _(\"Synchronizing...\")\n            else:\n                balance = self.wallet.get_balances_for_piechart().total()\n                msg = _(\"Balance\") + ': ' + self.config.format_amount_and_units(balance)\n        else:\n            msg = _(\"Not connected\")\n        msg = ' ' + msg + ' '\n        self.stdscr.addstr(self.maxy -1, 3, msg)\n        for i in range(self.num_tabs):\n            self.stdscr.addstr(0, 2 + 2*i + len(''.join(self.tab_names[0:i])), ' '+self.tab_names[i]+' ', curses.A_REVERSE if self.tab == i else 0)\n        self.stdscr.addstr(self.maxy -1, self.maxx-30, ' ' + ' '.join([_(\"Settings\"), _(\"Network\"), _(\"Quit\")]) + ' ')\n\n    def print_receive_tab(self):\n        self.stdscr.clear()\n        self.buttons = {}\n        self.max_pos = 6 + len(list(self.wallet.get_unpaid_requests()))\n        self.index = 0\n        self.add_edit_line(3, 2, _(\"Description\"), self.str_recv_description, 40)\n        self.add_edit_line(5, 2, _(\"Amount\"), self.str_recv_amount, 15)\n        self.stdscr.addstr(5, 31, self.config.get_base_unit())\n        self.add_edit_line(7, 2, _(\"Expiry\"), self.str_recv_expiry, 15)\n        self.add_button(9, 15, _(\"[Clear]\"), self.do_clear_request)\n        self.add_button(9, 25, _(\"[Onchain]\"), lambda: self.do_create_request(lightning=False))\n        self.add_button(9, 35, _(\"[Lightning]\"), lambda: self.do_create_request(lightning=True))\n        self.print_requests_list(13, 2, offset_pos=6)\n        return\n\n    def run_receive_tab(self, c):\n        if self.pos == 0:\n            self.str_recv_description = self.edit_str(self.str_recv_description, c)\n        elif self.pos == 1:\n            self.str_recv_amount = self.edit_str(self.str_recv_amount, c)\n        elif self.pos in self.buttons and c == ord(\"\\n\"):\n            self.buttons[self.pos]()\n        elif self.pos >= 6 and c == ord(\"\\n\"):\n            key = self.requests[self.pos - 6]\n            self.show_request(key)\n\n    def question(self, msg):\n        out = self.run_popup(msg, [\"No\", \"Yes\"]).get('button')\n        return out == \"Yes\"\n\n    def show_invoice_menu(self):\n        key = self.invoices[self.pos - 7]\n        invoice = self.wallet.get_invoice(key)\n        out = self.run_popup('Invoice', [\"Pay\", \"Delete\"]).get('button')\n        if out == \"Pay\":\n            self.do_pay_invoice(invoice)\n        elif out == \"Delete\":\n            self.wallet.delete_invoice(key)\n            self.max_pos -= 1\n\n    def format_column_width(self, offset, width):\n        delta = self.maxx -2 -offset - sum([abs(x) for x in width if x != '*'])\n        fmt = ''\n        for w in width:\n            if w == '*':\n                fmt += \"%-\" + \"%d\"%delta + \"s\"\n            else:\n                fmt += \"%\" + \"%d\"%w + \"s\"\n        return fmt\n\n    def print_invoices_list(self, y, x, offset_pos):\n        messages = []\n        invoices = []\n        fmt = self.format_column_width(x, [-20, '*', 15, 25])\n        headers = fmt % (\"Date\", \"Description\", \"Amount\", \"Status\")\n        for req in self.wallet.get_unpaid_invoices():\n            key = req.get_id()\n            status = self.wallet.get_invoice_status(req)\n            status_str = req.get_status_str(status)\n            timestamp = req.get_time()\n            date = format_time(timestamp)\n            amount = req.get_amount_sat()\n            message = req.get_message()\n            amount_str = self.config.format_amount(amount) if amount else \"\"\n            labels = []\n            messages.append(fmt % (date, message, amount_str, status_str))\n            invoices.append(key)\n        self.invoices = invoices\n        self.print_list(y, x, messages, headers=headers, offset_pos=offset_pos)\n\n    def print_requests_list(self, y, x, offset_pos):\n        messages = []\n        requests = []\n        fmt = self.format_column_width(x, [-20, '*', 15, 25])\n        headers = fmt % (\"Date\", \"Description\", \"Amount\", \"Status\")\n        for req in self.wallet.get_unpaid_requests():\n            key = req.get_id()\n            status = self.wallet.get_invoice_status(req)\n            status_str = req.get_status_str(status)\n            timestamp = req.get_time()\n            date = format_time(timestamp)\n            amount = req.get_amount_sat()\n            message = req.get_message()\n            amount_str = self.config.format_amount(amount) if amount else \"\"\n            labels = []\n            messages.append(fmt % (date, message, amount_str, status_str))\n            requests.append(key)\n        self.requests = requests\n        self.print_list(y, x, messages, headers=headers, offset_pos=offset_pos)\n\n    def print_contacts(self):\n        messages = list(map(lambda x: \"%20s   %45s \"%(x[0], x[1][1]), self.contacts.items()))\n        self.print_list(2, 1, messages, \"%19s  %15s \"%(\"Key\", \"Value\"))\n\n    def print_addresses(self):\n        x = 2\n        fmt = self.format_column_width(x, [-50, '*', 15])\n        messages = [ fmt % (\n            addr,\n            self.wallet.get_label_for_address(addr),\n            self.config.format_amount(sum(self.wallet.get_addr_balance(addr)), whitespaces=True)\n        ) for addr in self.wallet.get_addresses() ]\n        self.print_list(2, x, messages, fmt % (\"Address\", \"Description\", \"Balance\"))\n\n    def print_utxos(self):\n        x = 2\n        fmt = self.format_column_width(x, [-70, '*', 15])\n        utxos = self.wallet.get_utxos()\n        messages = [ fmt % (\n            utxo.prevout.to_str(),\n            self.wallet.get_label_for_txid(utxo.prevout.txid.hex()),\n            self.config.format_amount(utxo.value_sats(), whitespaces=True)\n        ) for utxo in utxos]\n        self.print_list(2, x, sorted(messages), fmt % (\"Outpoint\", \"Description\", \"Balance\"))\n\n    def print_channels(self):\n        if not self.wallet.lnworker:\n            return\n        fmt = \"%-35s  %-10s  %-30s\"\n        channels = self.wallet.lnworker.get_channel_objects()\n        messages = []\n        channel_ids = []\n        for chan in channels.values():\n            channel_ids.append(chan.short_id_for_GUI())\n            messages.append(fmt % (chan.short_id_for_GUI(), self.config.format_amount(chan.get_capacity()), chan.get_state().name))\n        self.channel_ids = channel_ids\n        self.print_list(2, 1, messages, fmt % (\"Scid\", \"Capacity\", \"State\"))\n\n    def print_send_tab(self):\n        self.stdscr.clear()\n        self.buttons = {}\n        self.max_pos = 7 + len(list(self.wallet.get_unpaid_invoices()))\n        self.index = 0\n        self.add_edit_line(3, 2, _(\"Pay to\"), self.str_recipient, 40)\n        self.add_edit_line(5, 2, _(\"Description\"), self.str_description, 40)\n        self.add_edit_line(7, 2, _(\"Amount\"), self.str_amount, 15)\n        self.stdscr.addstr(7, 31, self.config.get_base_unit())\n        self.add_button(9, 15, _(\"[Paste]\"), self.do_paste)\n        self.add_button(9, 25, _(\"[Clear]\"), self.do_clear)\n        self.add_button(9, 35, _(\"[Save]\"), self.do_save_invoice)\n        self.add_button(9, 44, _(\"[Pay]\"), self.do_pay)\n        #\n        self.print_invoices_list(13, 2, offset_pos=7)\n\n    def add_edit_line(self, y, x, title, data, length):\n        self.print_edit_line(y, x, title, data, self.index, length)\n        self.index += 1\n\n    def add_button(self, y, x, title, action):\n        self.print_button(y, x, title, self.index)\n        self.buttons[self.index] = action\n        self.index += 1\n\n    def print_banner(self):\n        if self.network and self.network.banner:\n            banner = self.network.banner\n            banner = banner.replace('\\r', '')\n            self.print_list(2, 1, banner.split('\\n'))\n\n    def get_qr(self, data):\n        import qrcode\n        try:\n            from StringIO import StringIO\n        except ImportError:\n            from io import StringIO\n        s = StringIO()\n        self.qr = qrcode.QRCode()\n        self.qr.add_data(data)\n        self.qr.print_ascii(out=s, invert=False)\n        msg = s.getvalue()\n        lines = msg.split('\\n')\n        return lines\n\n    def print_qr(self, w, y, x, lines):\n        try:\n            for i, l in enumerate(lines):\n                l = l.encode(\"utf-8\")\n                w.addstr(y + i, x, l, curses.color_pair(3))\n        except curses.error:\n            m = 'error. screen too small?'\n            m = m.encode(self.encoding)\n            w.addstr(y, x, m, 0)\n\n    def print_list(self, y, x, lst, headers=None, offset_pos=0):\n        self.list_length = len(lst)\n        if not self.list_length:\n            return\n        if headers:\n            headers += \" \"*(self.maxx -2 - len(headers))\n            self.stdscr.addstr(y, x, headers, curses.A_BOLD)\n        for i in range(self.maxy - 2 - y):\n            msg = lst[i] if i < self.list_length else \"\"\n            msg += \" \"*(self.maxx - 2 - len(msg))\n            m = msg[0:self.maxx - 2]\n            m = m.encode(self.encoding)\n            selected = self.pos >= offset_pos and (i == ((self.pos - offset_pos) % self.list_length))\n            self.stdscr.addstr(i+y+1, x, m, curses.A_REVERSE if selected else 0)\n\n        self.max_pos = self.list_length + offset_pos\n\n    def refresh(self):\n        if self.tab == -1:\n            return\n        self.stdscr.border(0)\n        self.print_balance()\n        self.print_clipboard()\n        self.stdscr.refresh()\n\n    def increase_cursor(self, delta):\n        self.pos += delta\n        self.pos = max(0, self.pos)\n        self.pos = min(self.pos, self.max_pos - 1)\n\n    def getch(self, redraw=False):\n        while True:\n            c = self.stdscr.getch()\n            if c != -1:\n                return c\n            if self.need_update and redraw:\n                self.update()\n            if self.tab == -1:\n                return KEY_ESC\n\n    def main_command(self):\n        c = self.getch(redraw=True)\n        cc = curses.unctrl(c).decode()\n        if   c == curses.KEY_RIGHT:\n            self.tab = (self.tab + 1)%self.num_tabs\n        elif c == curses.KEY_LEFT:\n            self.tab = (self.tab - 1)%self.num_tabs\n        elif c in [curses.KEY_DOWN, ord(\"\\t\")]:\n            self.increase_cursor(1)\n        elif c == curses.KEY_UP:\n            self.increase_cursor(-1)\n        elif cc in ['^W', '^C', '^X', '^Q']:\n            self.tab = -1\n        elif cc in ['^N']:\n            self.network_dialog()\n        elif cc == '^S':\n            self.settings_dialog()\n        else:\n            return c\n\n    def run_tab(self, i, print_func, exec_func):\n        while self.tab == i:\n            self.stdscr.clear()\n            print_func()\n            self.refresh()\n            c = self.main_command()\n            if c: exec_func(c)\n\n    def run_history_tab(self, c):\n        # Get txid from cursor position\n        if c == ord(\"\\n\"):\n            out = self.run_popup('', ['Transaction ID:', self.txid[self.pos]])\n\n    def edit_str(self, target, c, is_num=False):\n        if target is None:\n            target = ''\n        # detect backspace\n        cc = curses.unctrl(c).decode()\n        if c in [KEY_BACKSPACE, KEY_DELETE, curses.KEY_BACKSPACE] and target:\n            target = target[:-1]\n        elif not is_num or cc in '0123456789.':\n            target += cc\n        return target\n\n    def run_send_tab(self, c):\n        self.pos = self.pos % self.max_pos\n        if self.pos == 0:\n            self.str_recipient = self.edit_str(self.str_recipient, c)\n        elif self.pos == 1:\n            self.str_description = self.edit_str(self.str_description, c)\n        elif self.pos == 2:\n            self.str_amount = self.edit_str(self.str_amount, c, True)\n        elif self.pos in self.buttons and c == ord(\"\\n\"):\n            self.buttons[self.pos]()\n        elif self.pos >= 7 and c == ord(\"\\n\"):\n            self.show_invoice_menu()\n\n    def run_contacts_tab(self, c):\n        if c == ord(\"\\n\") and self.contacts:\n            out = self.run_popup('Address', [\"Copy\", \"Pay to\", \"Edit label\", \"Delete\"]).get('button')\n            key = list(self.contacts.keys())[self.pos%len(self.contacts.keys())]\n            if out == \"Pay to\":\n                self.tab = 1\n                self.str_recipient = key\n                self.pos = 2\n            elif out == \"Edit label\":\n                s = self.get_string(6 + self.pos, 18)\n                if s:\n                    self.contacts[key] = ('address', s)\n            elif out == \"Delete\":\n                self.contacts.pop(key)\n                self.pos = 0\n\n    def run_addresses_tab(self, c):\n        pass\n\n    def run_utxos_tab(self, c):\n        pass\n\n    def run_channels_tab(self, c):\n        if c == ord(\"\\n\") and self.channel_ids:\n            out = self.run_popup('Channel Details', ['Short channel ID:', self.channel_ids[self.pos]])\n\n    def run_banner_tab(self, c):\n        self.show_message(repr(c))\n        pass\n\n    def main(self):\n        self.daemon.start_network()\n        tty.setraw(sys.stdin)\n        try:\n            while self.tab != -1:\n                self.run_tab(0, self.print_history,       self.run_history_tab)\n                self.run_tab(1, self.print_send_tab,      self.run_send_tab)\n                self.run_tab(2, self.print_receive_tab,   self.run_receive_tab)\n                self.run_tab(3, self.print_addresses,     self.run_addresses_tab)\n                self.run_tab(4, self.print_utxos,         self.run_utxos_tab)\n                self.run_tab(5, self.print_channels,      self.run_channels_tab)\n                self.run_tab(6, self.print_contacts,      self.run_contacts_tab)\n                self.run_tab(7, self.print_banner,        self.run_banner_tab)\n        except curses.error as e:\n            raise Exception(\"Error with curses. Is your screen too small?\") from e\n        finally:\n            tty.setcbreak(sys.stdin)\n            curses.nocbreak()\n            self.stdscr.keypad(False)\n            curses.echo()\n            curses.endwin()\n\n    def do_clear(self):\n        self.str_amount = ''\n        self.str_recipient = ''\n        self.str_fee = ''\n        self.str_description = ''\n\n    def do_create_request(self, lightning: bool):\n        amount_sat = self.parse_amount(self.str_recv_amount) or 0\n        if not lightning:\n            if amount_sat and amount_sat < self.wallet.dust_threshold():\n                self.show_message(_('Amount too low'))\n                return\n            address = self.wallet.get_unused_address()\n            if not address:\n                self.show_message(_('No more unused address'))\n                return\n        else:\n            if not self.wallet.has_lightning():\n                self.show_message(_('Lightning is disabled on this wallet'))\n                return\n            address = None\n\n        message = self.str_recv_description\n        expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS\n        key = self.wallet.create_request(amount_sat, message, expiry, address)\n        self.do_clear_request()\n        self.pos = self.max_pos\n        self.show_request(key)\n\n    def do_clear_request(self):\n        self.str_recv_amount = \"\"\n        self.str_recv_description = \"\"\n\n    def do_paste(self):\n        text = pyperclip.paste()\n        text = text.strip()\n        if not text:\n            return\n        if is_address(text):\n            self.str_recipient = text\n            self.lightning_invoice = None\n        elif out := parse_bip21(text):\n            amount_sat = out.get('amount')\n            self.str_amount = self.config.format_amount(amount_sat) if amount_sat is not None else ''\n            self.str_recipient = out.get('address') or ''\n            self.str_description = out.get('message') or ''\n            self.lightning_invoice = None\n        elif lnaddr := parse_bolt11(text):\n            amount_sat = lnaddr.get_amount_sat()\n            self.str_recipient = lnaddr.pubkey.serialize().hex()\n            self.str_description = lnaddr.get_description()\n            self.str_amount = self.config.format_amount(amount_sat) if amount_sat is not None else ''\n            self.lightning_invoice = text\n        else:\n            self.show_message(_('Could not parse clipboard text') + '\\n\\n' + text[0:20] + '...')\n\n    def parse_amount(self, text):\n        try:\n            x = Decimal(text)\n        except Exception:\n            return None\n        power = pow(10, self.config.BTC_AMOUNTS_DECIMAL_POINT)\n        return int(power * x)\n\n    def read_invoice(self):\n        if self.lightning_invoice:\n            invoice = Invoice.from_bech32(self.lightning_invoice)\n            if invoice.amount_msat is None:\n                amount_sat = self.parse_amount(self.str_amount)\n                if amount_sat:\n                    invoice.set_amount_msat(int(amount_sat * 1000))\n                else:\n                    self.show_message(_('No amount'))\n                    return None\n        elif is_address(self.str_recipient):\n            amount_sat = self.parse_amount(self.str_amount)\n            if not amount_sat:\n                self.show_message(_('No amount'))\n                return None\n            scriptpubkey = address_to_script(self.str_recipient)\n            outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount_sat)]\n            invoice = self.wallet.create_invoice(\n                outputs=outputs,\n                message=self.str_description,\n                pr=None,\n                URI=None)\n        else:\n            self.show_message(_('Invalid Bitcoin address'))\n            return None\n        return invoice\n\n    def do_save_invoice(self):\n        invoice = self.read_invoice()\n        if not invoice:\n            return\n        self.save_pending_invoice(invoice)\n\n    def save_pending_invoice(self, invoice):\n        self.do_clear()\n        self.wallet.save_invoice(invoice)\n        self.pending_invoice = None\n\n    def do_pay(self):\n        invoice = self.read_invoice()\n        if not invoice:\n            return\n        self.do_pay_invoice(invoice)\n\n    def do_pay_invoice(self, invoice):\n        if invoice.is_lightning():\n            self.pay_lightning_invoice(invoice)\n        else:\n            self.pay_onchain_dialog(invoice)\n\n    def pay_lightning_invoice(self, invoice):\n        amount_msat = invoice.get_amount_msat()\n        msg = _(\"Pay lightning invoice?\")\n        #+ '\\n\\n' + _(\"This will send {}?\").format(self.format_amount_and_units(Decimal(amount_msat)/1000))\n        if not self.question(msg):\n            return\n        self.save_pending_invoice(invoice)\n        coro = self.wallet.lnworker.pay_invoice(invoice, amount_msat=amount_msat)\n\n        #self.window.run_coroutine_from_thread(coro, _('Sending payment'))\n        self.show_message(_(\"Please wait...\"), getchar=False)\n        try:\n            self.network.run_from_another_thread(coro)\n        except Exception as e:\n            self.show_message(str(e))\n        else:\n            self.show_message(_('Payment sent.'))\n\n    def pay_onchain_dialog(self, invoice):\n        if self.wallet.has_password():\n            password = self.password_dialog()\n            if not password:\n                return\n        else:\n            password = None\n        fee_policy = FeePolicy(self.config.FEE_POLICY)\n        try:\n            tx = self.wallet.make_unsigned_transaction(\n                outputs=invoice.outputs,\n                fee_policy=fee_policy,\n            )\n            self.wallet.sign_transaction(tx, password)\n        except Exception as e:\n            self.show_message(repr(e))\n            return\n        if self.str_description:\n            self.wallet.set_label(tx.txid(), self.str_description)\n\n        self.save_pending_invoice(invoice)\n        self.show_message(_(\"Please wait...\"), getchar=False)\n        try:\n            self.network.run_from_another_thread(self.network.broadcast_transaction(tx))\n        except TxBroadcastError as e:\n            msg = e.get_message_for_gui()\n            self.show_message(msg)\n        except BestEffortRequestFailed as e:\n            msg = repr(e)\n            self.show_message(msg)\n        else:\n            self.show_message(_('Payment sent.'))\n            self.do_clear()\n            #self.update_contacts_tab()\n\n    def show_message(self, message, getchar = True):\n        w = self.w\n        w.clear()\n        w.border(0)\n        for i, line in enumerate(message.split('\\n')):\n            w.addstr(2+i,2,line)\n        w.refresh()\n        if getchar:\n            c = self.getch()\n\n    def run_popup(self, title, items):\n        return self.run_dialog(title, list(map(lambda x: {'type':'button','label':x}, items)), interval=1, y_pos = self.pos+3)\n\n    def network_dialog(self):\n        if not self.network:\n            return\n        net_params = self.network.get_parameters()\n        server_addr = net_params.server\n        proxy_config, auto_connect = net_params.proxy, net_params.auto_connect\n        srv = 'auto-connect' if auto_connect else str(self.network.default_server)\n        out = self.run_dialog('Network', [\n            {'label': 'server', 'type': 'str', 'value': srv},\n            {'label': 'proxy', 'type': 'str', 'value': self.config.NETWORK_PROXY},\n            {'label': 'proxy user', 'type': 'str', 'value': self.config.NETWORK_PROXY_USER},\n            {'label': 'proxy pass', 'type': 'str', 'value': self.config.NETWORK_PROXY_PASSWORD},\n            ], buttons=1)\n        if out:\n            self.show_message(repr(proxy_config))\n            if out.get('server'):\n                server_str = out.get('server')\n                auto_connect = server_str == 'auto-connect'\n                if not auto_connect:\n                    try:\n                        server_addr = ServerAddr.from_str(server_str)\n                    except Exception:\n                        self.show_message(\"Error:\" + server_str + \"\\nIn doubt, type \\\"auto-connect\\\"\")\n                        return False\n            if out.get('server') or out.get('proxy') or out.get('proxy user') or out.get('proxy pass'):\n                if out.get('proxy'):\n                    new_proxy_config = ProxySettings()\n                    new_proxy_config.deserialize_proxy_cfgstr(out.get('proxy'))\n                    new_proxy_config.user = out.get('proxy user', proxy_config.user)\n                    new_proxy_config.password = out.get('proxy pass', proxy_config.password)\n                    new_proxy_config.enabled = True\n                else:\n                    new_proxy_config = proxy_config\n                net_params = NetworkParameters(\n                    server=server_addr,\n                    proxy=new_proxy_config,\n                    auto_connect=auto_connect)\n                self.network.run_from_another_thread(self.network.set_parameters(net_params))\n\n    def settings_dialog(self):\n        from electrum.fee_policy import FeePolicy\n        out = self.run_dialog('Settings', [\n            {'label':'Fee policy', 'type':'str', 'value': self.config.FEE_POLICY}\n        ], buttons = 1)\n        if out:\n            if descr := out.get('Fee policy'):\n                fee_policy = FeePolicy(descr)\n                self.config.FEE_POLICY = fee_policy.get_descriptor()\n\n    def password_dialog(self):\n        out = self.run_dialog('Password', [\n            {'label':'Password', 'type':'password', 'value':''}\n            ], buttons = 1)\n        return out.get('Password')\n\n    def run_dialog(self, title, items, interval=2, buttons=None, y_pos=3):\n        self.popup_pos = 0\n\n        self.w = curses.newwin(5 + len(list(items))*interval + (2 if buttons else 0), 68, y_pos, 5)\n        w = self.w\n        out = {}\n        while True:\n            w.clear()\n            w.border(0)\n            w.addstr(0, 2, title)\n            num = len(list(items))\n            numpos = num\n            if buttons:\n                numpos += 2\n            for i in range(num):\n                item = items[i]\n                label = item.get('label')\n                if item.get('type') == 'list':\n                    value = item.get('value','')\n                elif item.get('type') == 'satoshis':\n                    value = item.get('value','')\n                elif item.get('type') == 'str':\n                    value = item.get('value','')\n                elif item.get('type') == 'password':\n                    value = '*'*len(item.get('value',''))\n                else:\n                    value = ''\n                if value is None:\n                    value = ''\n                if len(value)<20:\n                    value += ' '*(20-len(value))\n\n                if 'value' in item:\n                    w.addstr(2+interval*i, 2, label)\n                    w.addstr(2+interval*i, 15, value, curses.A_REVERSE if self.popup_pos%numpos==i else curses.color_pair(1))\n                else:\n                    w.addstr(2+interval*i, 2, label, curses.A_REVERSE if self.popup_pos%numpos==i else 0)\n\n            if buttons:\n                w.addstr(5+interval*i, 10, \"[  ok  ]\", curses.A_REVERSE if self.popup_pos%numpos==(numpos-2) else curses.color_pair(2))\n                w.addstr(5+interval*i, 25, \"[cancel]\", curses.A_REVERSE if self.popup_pos%numpos==(numpos-1) else curses.color_pair(2))\n\n            w.refresh()\n\n            c = self.getch()\n            if c in [ord('q'), KEY_ESC]:\n                break\n            elif c in [curses.KEY_LEFT, curses.KEY_UP]:\n                self.popup_pos -= 1\n            elif c in [curses.KEY_RIGHT, curses.KEY_DOWN]:\n                self.popup_pos +=1\n            else:\n                i = self.popup_pos%numpos\n                if buttons and c == ord(\"\\n\"):\n                    if i == numpos-2:\n                        return out\n                    elif i == numpos -1:\n                        return {}\n\n                item = items[i]\n                _type = item.get('type')\n\n                if _type == 'str':\n                    item['value'] = self.edit_str(item['value'], c)\n                    out[item.get('label')] = item.get('value')\n\n                elif _type == 'password':\n                    item['value'] = self.edit_str(item['value'], c)\n                    out[item.get('label')] = item ['value']\n\n                elif _type == 'satoshis':\n                    item['value'] = self.edit_str(item['value'], c, True)\n                    out[item.get('label')] = item.get('value')\n\n                elif _type == 'list':\n                    choices = item.get('choices')\n                    try:\n                        j = choices.index(item.get('value'))\n                    except Exception:\n                        j = 0\n                    new_choice = choices[(j + 1)% len(choices)]\n                    item['value'] = new_choice\n                    out[item.get('label')] = item.get('value')\n\n                elif _type == 'button':\n                    out['button'] = item.get('label')\n                    break\n        return out\n\n    def print_textbox(self, w, y, x, _text, highlighted):\n        width = 60\n        for i in range(len(_text)//width + 1):\n            s = _text[i*width:(i+1)*width]\n            w.addstr(y+i, x, s, curses.A_REVERSE if highlighted else curses.A_NORMAL)\n        return i\n\n    def show_request(self, key):\n        req = self.wallet.get_request(key)\n        addr = req.get_address() or ''\n        URI = self.wallet.get_request_URI(req) or ''\n        lnaddr = self.wallet.get_bolt11_invoice(req) or ''\n        w = curses.newwin(self.maxy - 2, self.maxx - 2, 1, 1)\n        pos = 2\n        text = URI or addr or lnaddr\n        data = URI or addr or lnaddr.upper()\n        while True:\n            w.clear()\n            w.border(0)\n            w.addstr(0, 2, ' ' + _('Payment Request') + ' ')\n            y = 2\n            if URI:\n                w.addstr(y, 2, \"URI\")\n                h = self.print_textbox(w, y, 13, URI, False)\n            elif addr:\n                w.addstr(y, 2, \"Address\")\n                h = self.print_textbox(w, y, 13, addr, False)\n            elif lnaddr:\n                w.addstr(y, 2, \"Lightning\")\n                h = self.print_textbox(w, y, 13, lnaddr, False)\n            else:\n                return\n            y += h + 2\n            lines = self.get_qr(data)\n            qr_width = len(lines) * 2\n            x = self.maxx - qr_width\n            if x > 60:\n                self.print_qr(w, 1, x, lines)\n            else:\n                w.addstr(y, 35, \"(Window too small for QR code)\")\n            w.addstr(y, 13, \"[Copy]\",   curses.A_REVERSE if pos==0 else curses.color_pair(2))\n            w.addstr(y, 23, \"[Delete]\", curses.A_REVERSE if pos==1 else curses.color_pair(2))\n            w.addstr(y, 35, \"[Close]\",  curses.A_REVERSE if pos==2 else curses.color_pair(2))\n            w.refresh()\n            c = self.getch()\n            if c in [curses.KEY_UP, curses.KEY_LEFT]:\n                pos -= 1\n            elif c in [curses.KEY_DOWN, curses.KEY_RIGHT, ord(\"\\t\")]:\n                pos += 1\n            elif c == ord(\"\\n\"):\n                if pos == 0:\n                    pyperclip.copy(text)\n                    self.show_message('Text copied to clipboard')\n                elif pos == 1:\n                    if self.question(\"Delete Request?\"):\n                        self.wallet.delete_request(key)\n                        self.max_pos -= 1\n                        break\n                elif pos == 2:\n                    break\n            else:\n                break\n            pos = pos % 3\n        self.stdscr.refresh()\n        return\n"
  },
  {
    "path": "electrum/harden_memory_linux.py",
    "content": "# Copyright (C) 2020 cptpcrd\n# Copyright (C) 2025 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n#\n# based on https://github.com/cptpcrd/pyprctl/blob/578ed3e81066a8a61dede912454d5eeaef37eeea/pyprctl/ffi.py#L28\n#\n# This module tries to restrict the ability of other processes to access the memory of our process.\n# Traditionally, on Linux, one process can access the memory of another arbitrary process\n# if both are running as the same user (uid). (Root can ofc access the memory of ~any process)\n# Programs can opt-out from this by setting prctl(PR_SET_DUMPABLE, 0);\n#\n# Besides PR_SET_DUMPABLE, there are ways to globally restrict this for all processes:\n# 1. The Yama (Linux Security Module) ptrace scope can be used to reduce these permissions\n#    This runtime kernel parameter can be set to the following options:\n#      0 - Default attach security permissions.\n#      1 - Restricted attach. Only child processes plus normal permissions.\n#      2 - Admin-only attach. Only executables with CAP_SYS_PTRACE.\n#      3 - No attach. No process may call ptrace at all. Irrevocable.\n#    # Note: The default value of kernel.yama.ptrace_scope is distro-specific.\n#    #       See `$ cat /proc/sys/kernel/yama/ptrace_scope`.\n#    #       - ubuntu 22.04 sets it to 1 (see /etc/sysctl.d/10-ptrace.conf),\n#    #       - debian 12 sets it to 0\n#    #       - manjaro sets it to 1\n# 2. SELinux: ptrace can be restricted by setting the selinux deny_ptrace boolean.\n#\n# For a quick test on your system, try:\n#   $ cat /proc/$$/mem > /dev/null\n#   cat: /proc/4907/mem: Permission denied\n# Getting \"Permission denied\" means access failed, \"Input/output error\" means access succeeded.\n\nimport ctypes\nimport ctypes.util\nimport os\nimport sys\nfrom typing import Optional\n\nfrom .logging import get_logger\n\n\n_logger = get_logger(__name__)\n\nPR_GET_DUMPABLE = 3\nPR_SET_DUMPABLE = 4\n\n\n_libc = None  # type: Optional[ctypes.CDLL]\ndef _load_libc():\n    global _libc\n    if _libc is not None:\n        return\n    #assert sys.platform == \"linux\", sys.platform\n    # note: find_library can raise FileNotFoundError(OSError), see https://github.com/python/cpython/issues/93094\n    _libc_path = ctypes.util.find_library(\"c\")\n    _libc = ctypes.CDLL(_libc_path, use_errno=True)\n    _libc.prctl.argtypes = (ctypes.c_int, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong)\n    _libc.prctl.restype = ctypes.c_int\n\n\ndef set_dumpable(flag: bool) -> None:\n    \"\"\"Set the \"dumpable\" attribute on the current process.\n    This controls whether a core dump will be produced if the process receives a signal whose\n    default behavior is to produce a core dump.\n    In addition, processes that are not dumpable cannot be attached with ptrace() PTRACE_ATTACH.\n\n    In effect, another process running as the same user as us can read our memory if we are dumpable.\n    \"\"\"\n    _load_libc()\n    res = _libc.prctl(PR_SET_DUMPABLE, int(bool(flag)), 0, 0, 0)\n    if res < 0:\n        eno = ctypes.get_errno()\n        raise OSError(eno, os.strerror(eno), None, None, None)\n\n\ndef set_dumpable_safe(flag: bool) -> None:\n    try:\n        _load_libc()\n    except Exception as e:\n        _logger.exception(\"error loading libc\")\n        return\n    assert _libc is not None\n    try:\n        set_dumpable(flag)\n    except OSError as e:\n        _logger.error(f\"libc.prctl(PR_SET_DUMPABLE, {flag}) errored: {e}\")\n\n\ndef get_dumpable() -> bool:\n    _load_libc()\n    res = _libc.prctl(PR_GET_DUMPABLE, 0, 0, 0, 0)\n    if res < 0:\n        eno = ctypes.get_errno()\n        raise OSError(eno, os.strerror(eno), None, None, None)\n    return res != 0\n"
  },
  {
    "path": "electrum/hw_wallet/__init__.py",
    "content": "from .plugin import HW_PluginBase, HardwareClientBase, HardwareHandlerBase\nfrom .cmdline import CmdLineHandler\n"
  },
  {
    "path": "electrum/hw_wallet/cmdline.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nfrom electrum.util import print_stderr, raw_input\nfrom electrum.logging import get_logger\n\nfrom .plugin import HardwareHandlerBase\n\n\n_logger = get_logger(__name__)\n\n\nclass CmdLineHandler(HardwareHandlerBase):\n\n    def get_passphrase(self, msg, confirm):\n        import getpass\n        print_stderr(msg)\n        return getpass.getpass('')\n\n    def get_pin(self, msg, *, show_strength=True):\n        t = {'a': '7', 'b': '8', 'c': '9', 'd': '4', 'e': '5', 'f': '6', 'g': '1', 'h': '2', 'i': '3'}\n        t.update({str(i): str(i) for i in range(1, 10)})  # sneakily also support numpad-conversion\n        print_stderr(msg)\n        print_stderr(\"a b c\\nd e f\\ng h i\\n-----\")\n        o = raw_input()\n        try:\n            return ''.join(map(lambda x: t[x], o))\n        except KeyError as e:\n            raise Exception(\"Character {} not in matrix!\".format(e)) from e\n\n    def prompt_auth(self, msg):\n        import getpass\n        print_stderr(msg)\n        response = getpass.getpass('')\n        if len(response) == 0:\n            return None\n        return response\n\n    def yes_no_question(self, msg):\n        print_stderr(msg)\n        return raw_input() in 'yY'\n\n    def stop(self):\n        pass\n\n    def show_message(self, msg, on_cancel=None):\n        print_stderr(msg)\n\n    def show_error(self, msg, blocking=False):\n        print_stderr(msg)\n\n    def update_status(self, b):\n        _logger.info(f'hw device status {b}')\n\n    def finished(self):\n        pass\n"
  },
  {
    "path": "electrum/hw_wallet/plugin.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nfrom abc import abstractmethod, ABC\nfrom typing import TYPE_CHECKING, Sequence, Optional, Type, Iterable, Any\n\nfrom electrum.plugin import (BasePlugin, hook, Device, DeviceMgr,\n                             assert_runs_in_hwd_thread, runs_in_hwd_thread)\nfrom electrum.i18n import _\nfrom electrum.bitcoin import is_address, opcodes\nfrom electrum.util import versiontuple, UserFacingException, ChoiceItem\nfrom electrum.transaction import TxOutput, PartialTransaction\nfrom electrum.bip32 import BIP32Node\nfrom electrum.storage import get_derivation_used_for_hw_device_encryption\nfrom electrum.keystore import Xpub, Hardware_KeyStore\n\nif TYPE_CHECKING:\n    import threading\n    from electrum.plugin import DeviceInfo\n    from electrum.wallet import Abstract_Wallet\n    from electrum.wizard import AbstractWizard\n\n\nclass HW_PluginBase(BasePlugin, ABC):\n    keystore_class: Type['Hardware_KeyStore']\n    libraries_available: bool\n    SUPPORTED_XTYPES = ()\n\n    # define supported library versions:  minimum_library <= x < maximum_library\n    minimum_library = (0,)\n    maximum_library = (float('inf'),)\n\n    DEVICE_IDS: Iterable[Any]\n\n    def __init__(self, parent, config, name):\n        BasePlugin.__init__(self, parent, config, name)\n        self.device = self.keystore_class.device\n        self.keystore_class.plugin = self\n        self._ignore_outdated_fw = False\n\n    def is_enabled(self):\n        return True\n\n    def device_manager(self) -> 'DeviceMgr':\n        return self.parent.device_manager\n\n    def create_device_from_hid_enumeration(self, d: dict, *, product_key) -> Optional['Device']:\n        # note: id_ needs to be unique between simultaneously connected devices,\n        #       and ideally unchanged while a device is connected.\n        # Older versions of hid don't provide interface_number\n        interface_number = d.get('interface_number', -1)\n        usage_page = d['usage_page']\n        # id_=str(d['path']) in itself might be sufficient, but this had to be touched\n        # a number of times already, so let's just go for the overkill approach:\n        id_ = f\"{d['path']},{d['serial_number']},{interface_number},{usage_page}\"\n        device = Device(path=d['path'],\n                        interface_number=interface_number,\n                        id_=id_,\n                        product_key=product_key,\n                        usage_page=usage_page,\n                        transport_ui_string='hid')\n        return device\n\n    @hook\n    def close_wallet(self, wallet: 'Abstract_Wallet'):\n        for keystore in wallet.get_keystores():\n            if isinstance(keystore, self.keystore_class):\n                self.device_manager().unpair_pairing_code(keystore.pairing_code())\n                if keystore.thread:\n                    keystore.thread.stop()\n\n    def get_client(self, keystore: 'Hardware_KeyStore', force_pair: bool = True, *,\n                   devices: Sequence['Device'] = None,\n                   allow_user_interaction: bool = True) -> Optional['HardwareClientBase']:\n        devmgr = self.device_manager()\n        handler = keystore.handler\n        client = devmgr.client_for_keystore(self, handler, keystore, force_pair,\n                                            devices=devices,\n                                            allow_user_interaction=allow_user_interaction)\n        return client\n\n    def show_address(self, wallet: 'Abstract_Wallet', address, keystore: 'Hardware_KeyStore' = None):\n        pass  # implemented in child classes\n\n    def show_address_helper(self, wallet, address, keystore=None):\n        if keystore is None:\n            keystore = wallet.get_keystore()\n        if not is_address(address):\n            keystore.handler.show_error(_('Invalid Bitcoin Address'))\n            return False\n        if not wallet.is_mine(address):\n            keystore.handler.show_error(_('Address not in wallet.'))\n            return False\n        if type(keystore) != self.keystore_class:\n            return False\n        return True\n\n    def get_library_version(self) -> str:\n        \"\"\"Returns the version of the 3rd party python library\n        for the hw wallet. For example '0.9.0'\n\n        Returns 'unknown' if library is found but cannot determine version.\n        Raises 'ImportError' if library is not found.\n        Raises 'LibraryFoundButUnusable' if found but there was some problem (includes version num).\n        \"\"\"\n        raise NotImplementedError()\n\n    def check_libraries_available(self) -> bool:\n        def version_str(t):\n            return \".\".join(str(i) for i in t)\n\n        try:\n            # this might raise ImportError or LibraryFoundButUnusable\n            library_version = self.get_library_version()\n            # if no exception so far, we might still raise LibraryFoundButUnusable\n            if (library_version == 'unknown'\n                    or versiontuple(library_version) < self.minimum_library\n                    or versiontuple(library_version) >= self.maximum_library):\n                raise LibraryFoundButUnusable(library_version=library_version)\n        except ImportError as e:\n            self.libraries_available_message = (\n                _(\"Missing libraries for {}.\").format(self.name)\n                + f\"\\n    {e!r}\"\n            )\n            return False\n        except LibraryFoundButUnusable as e:\n            library_version = e.library_version\n            self.libraries_available_message = (\n                    _(\"Library version for '{}' is incompatible.\").format(self.name)\n                    + '\\nInstalled: {}, Needed: {} <= x < {}'\n                    .format(library_version, version_str(self.minimum_library), version_str(self.maximum_library)))\n            self.logger.warning(self.libraries_available_message)\n            return False\n\n        return True\n\n    def get_library_not_available_message(self) -> str:\n        if hasattr(self, 'libraries_available_message'):\n            message = self.libraries_available_message\n        else:\n            message = _(\"Missing libraries for {}.\").format(self.name)\n        message += '\\n' + _(\"Make sure you install it with python3\")\n        return message\n\n    def set_ignore_outdated_fw(self):\n        self._ignore_outdated_fw = True\n\n    def is_outdated_fw_ignored(self) -> bool:\n        return self._ignore_outdated_fw\n\n    def create_client(self, device: 'Device',\n                      handler: Optional['HardwareHandlerBase']) -> Optional['HardwareClientBase']:\n        raise NotImplementedError()\n\n    def create_handler(self, window) -> 'HardwareHandlerBase':\n        # note: in Qt GUI, 'window' is either an ElectrumWindow or an QENewWalletWizard\n        raise NotImplementedError()\n\n    def can_recognize_device(self, device: Device) -> bool:\n        \"\"\"Whether the plugin thinks it can handle the given device.\n        Used for filtering all connected hardware devices to only those by this vendor.\n        \"\"\"\n        return device.product_key in self.DEVICE_IDS\n\n    @abstractmethod\n    def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet: bool) -> str:\n        \"\"\"Return view name for device\n        \"\"\"\n        pass\n\n    @hook\n    def init_wallet_wizard(self, wizard: 'AbstractWizard') -> None:\n        self.extend_wizard(wizard)\n\n    @abstractmethod\n    def extend_wizard(self, wizard: 'AbstractWizard') -> None:\n        pass\n\n\nclass HardwareClientBase(ABC):\n    handler = None  # type: Optional['HardwareHandlerBase']\n\n    def __init__(self, *, plugin: 'HW_PluginBase'):\n        assert_runs_in_hwd_thread()\n        self.plugin = plugin\n\n    def device_manager(self) -> 'DeviceMgr':\n        return self.plugin.device_manager()\n\n    @abstractmethod\n    def is_pairable(self) -> bool:\n        pass\n\n    @abstractmethod\n    def close(self):\n        pass\n\n    def timeout(self, cutoff) -> None:  # noqa: B027\n        pass\n\n    @abstractmethod\n    def is_initialized(self) -> bool:\n        \"\"\"True if initialized, False if wiped.\"\"\"\n        pass\n\n    def label(self) -> Optional[str]:\n        \"\"\"The name given by the user to the device.\n\n        Note: labels are shown to the user to help distinguish their devices,\n        and they are also used as a fallback to distinguish devices programmatically.\n        So ideally, different devices would have different labels.\n        \"\"\"\n        # When returning a constant here (i.e. not implementing the method in the way\n        # it is supposed to work), make sure the return value is in electrum.plugin.PLACEHOLDER_HW_CLIENT_LABELS\n        return \" \"\n\n    def get_soft_device_id(self) -> Optional[str]:\n        \"\"\"An id-like string that is used to distinguish devices programmatically.\n        This is a long term id for the device, that does not change between reconnects.\n        This method should not prompt the user, i.e. no user interaction, as it is used\n        during USB device enumeration (called for each unpaired device).\n        Stored in the wallet file.\n        \"\"\"\n        root_fp = self.request_root_fingerprint_from_device()\n        return root_fp\n\n    @abstractmethod\n    def has_usable_connection_with_device(self) -> bool:\n        pass\n\n    @abstractmethod\n    def get_xpub(self, bip32_path: str, xtype) -> str:\n        pass\n\n    @runs_in_hwd_thread\n    def request_root_fingerprint_from_device(self) -> str:\n        # digitalbitbox (at least) does not reveal xpubs corresponding to unhardened paths\n        # so ask for a direct child, and read out fingerprint from that:\n        child_of_root_xpub = self.get_xpub(\"m/0'\", xtype='standard')\n        root_fingerprint = BIP32Node.from_xkey(child_of_root_xpub).fingerprint.hex().lower()\n        return root_fingerprint\n\n    @runs_in_hwd_thread\n    def get_password_for_storage_encryption(self) -> str:\n        # note: using a different password based on hw device type is highly undesirable! see #5993\n        derivation = get_derivation_used_for_hw_device_encryption()\n        xpub = self.get_xpub(derivation, \"standard\")\n        password = Xpub.get_pubkey_from_xpub(xpub, ()).hex()\n        return password\n\n    def device_model_name(self) -> Optional[str]:\n        \"\"\"Return the name of the model of this device, which might be displayed in the UI.\n        E.g. for Trezor, \"Trezor One\" or \"Trezor T\".\n        If this method is not defined for a plugin, the plugin name is used as default\n        \"\"\"\n        return self.plugin.name\n\n\nclass HardwareClientDummy(HardwareClientBase):\n    \"\"\"Hw device we recognize but do not support.\n    E.g. for Ledger HW.1 devices that we used to support in the past, but no longer do.\n    This allows showing an error message to the user.\n    \"\"\"\n    def __init__(self, *, plugin: 'HW_PluginBase', error_text: str):\n        HardwareClientBase.__init__(self, plugin=plugin)\n        self.error_text = error_text\n\n    def get_xpub(self, bip32_path: str, xtype) -> str:\n        raise Exception(self.error_text)\n\n    def is_pairable(self) -> bool:\n        return False\n\n    def close(self):\n        pass\n\n    def is_initialized(self) -> bool:\n        \"\"\"True if initialized, False if wiped.\"\"\"\n        return True\n\n    def label(self) -> Optional[str]:\n        return \"dummy_client\"\n\n    def has_usable_connection_with_device(self) -> bool:\n        return True\n\n\nclass HardwareHandlerBase:\n    \"\"\"An interface between the GUI and the device handling logic for handling I/O.\"\"\"\n    win = None\n    device: str\n\n    def get_wallet(self) -> Optional['Abstract_Wallet']:\n        if self.win is not None:\n            if hasattr(self.win, 'wallet'):\n                return self.win.wallet\n\n    def get_gui_thread(self) -> Optional['threading.Thread']:\n        if self.win is not None:\n            if hasattr(self.win, 'gui_thread'):\n                return self.win.gui_thread\n\n    def update_status(self, paired: bool) -> None:\n        pass\n\n    def query_choice(self, msg: str, choices: Sequence[ChoiceItem]) -> Optional[Any]:\n        \"\"\"Returns ChoiceItem.key (for selected item), or None if the user cancels the dialog.\"\"\"\n        raise NotImplementedError()\n\n    def yes_no_question(self, msg: str) -> bool:\n        raise NotImplementedError()\n\n    def show_message(self, msg: str, on_cancel=None) -> None:\n        raise NotImplementedError()\n\n    def show_error(self, msg: str, blocking: bool = False) -> None:\n        raise NotImplementedError()\n\n    def finished(self) -> None:\n        pass\n\n    def get_word(self, msg: str) -> str:\n        raise NotImplementedError()\n\n    def get_passphrase(self, msg: str, confirm: bool) -> Optional[str]:\n        raise NotImplementedError()\n\n    def get_pin(self, msg: str, *, show_strength: bool = True) -> str:\n        raise NotImplementedError()\n\n\ndef is_any_tx_output_on_change_branch(tx: PartialTransaction) -> bool:\n    return any([txout.is_change for txout in tx.outputs()])\n\n\ndef trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes:\n    validate_op_return_output(output)\n    script = output.scriptpubkey\n    if not (script[0] == opcodes.OP_RETURN and\n            script[1] == len(script) - 2 and script[1] <= 75):\n        raise UserFacingException(_(\"Only OP_RETURN scripts, with one constant push, are supported.\"))\n    return script[2:]\n\n\ndef validate_op_return_output(output: TxOutput, *, max_size: int = None) -> None:\n    script = output.scriptpubkey\n    if script[0] != opcodes.OP_RETURN:\n        raise UserFacingException(_(\"Only OP_RETURN scripts are supported.\"))\n    if max_size is not None and len(script) > max_size:\n        raise UserFacingException(_(\"OP_RETURN payload too large.\" + \"\\n\"\n                                  + f\"(scriptpubkey size {len(script)} > {max_size})\"))\n    if output.value != 0:\n        raise UserFacingException(_(\"Amount for OP_RETURN output must be zero.\"))\n\n\ndef only_hook_if_libraries_available(func):\n    # note: this decorator must wrap @hook, not the other way around,\n    # as 'hook' uses the name of the function it wraps\n    def wrapper(self: 'HW_PluginBase', *args, **kwargs):\n        if not self.libraries_available: return None\n        return func(self, *args, **kwargs)\n    return wrapper\n\n\nclass LibraryFoundButUnusable(Exception):\n    def __init__(self, library_version='unknown'):\n        self.library_version = library_version\n\n\nclass OutdatedHwFirmwareException(UserFacingException):\n\n    def text_ignore_old_fw_and_continue(self) -> str:\n        suffix = (_(\"The firmware of your hardware device is too old. \"\n                    \"If possible, you should upgrade it. \"\n                    \"You can ignore this error and try to continue, however things are likely to break.\") + \"\\n\\n\" +\n                  _(\"Ignore and continue?\"))\n        if str(self):\n            return str(self) + \"\\n\\n\" + suffix\n        else:\n            return suffix\n\n\nclass OperationCancelled(UserFacingException):\n    \"\"\"Emitted when an operation is cancelled by user on a HW device\n    \"\"\"\n    pass\n"
  },
  {
    "path": "electrum/hw_wallet/qt.py",
    "content": "#!/usr/bin/env python3\n# -*- mode: python -*-\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2016  The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport threading\nfrom functools import partial\nfrom typing import TYPE_CHECKING, Union, Optional, Sequence\n\nfrom PyQt6.QtCore import QObject, pyqtSignal, Qt\nfrom PyQt6.QtWidgets import QVBoxLayout, QLineEdit, QHBoxLayout, QLabel, QMenu\n\nfrom electrum.i18n import _\nfrom electrum.logging import Logger\nfrom electrum.util import UserCancelled, UserFacingException, ChoiceItem\nfrom electrum.plugin import hook\n\nfrom electrum.gui.common_qt.util import TaskThread\nfrom electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE\nfrom electrum.gui.qt.util import (\n    read_QIcon, WWLabel, OkButton, WindowModalDialog, Buttons, CancelButton, char_width_in_lineedit, PasswordLineEdit,\n    read_QIcon_from_bytes\n)\nfrom electrum.gui.qt.main_window import StatusBarButton\n\n\nfrom .plugin import OutdatedHwFirmwareException, HW_PluginBase, HardwareHandlerBase\n\nif TYPE_CHECKING:\n    from electrum.wallet import Abstract_Wallet\n    from electrum.keystore import Hardware_KeyStore\n    from electrum.gui.qt import ElectrumWindow\n    from electrum.gui.qt.wizard.wallet import QENewWalletWizard\n\n\n# The trickiest thing about this handler was getting windows properly\n# parented on macOS.\nclass QtHandlerBase(HardwareHandlerBase, QObject, Logger):\n    \"\"\"An interface between the GUI (here, QT) and the device handling\n    logic for handling I/O.\"\"\"\n\n    passphrase_signal = pyqtSignal(object, object)\n    message_signal = pyqtSignal(object, object)\n    error_signal = pyqtSignal(object, object)\n    word_signal = pyqtSignal(object)\n    clear_signal = pyqtSignal()\n    query_signal = pyqtSignal(object, object)\n    yes_no_signal = pyqtSignal(object)\n    status_signal = pyqtSignal(object)\n\n    def __init__(self, win: Union['ElectrumWindow', 'QENewWalletWizard'], device: str):\n        QObject.__init__(self)\n        Logger.__init__(self)\n        assert win.gui_thread == threading.current_thread(), 'must be called from GUI thread'\n        self.clear_signal.connect(self.clear_dialog)\n        self.error_signal.connect(self.error_dialog)\n        self.message_signal.connect(self.message_dialog)\n        self.passphrase_signal.connect(self.passphrase_dialog)\n        self.word_signal.connect(self.word_dialog)\n        self.query_signal.connect(self.win_query_choice)\n        self.yes_no_signal.connect(self.win_yes_no_question)\n        self.status_signal.connect(self._update_status)\n        self.win = win\n        self.device = device\n        self.dialog = None\n        self.done = threading.Event()\n\n    def top_level_window(self):\n        return self.win.top_level_window()\n\n    def update_status(self, paired):\n        self.status_signal.emit(paired)\n\n    def _update_status(self, paired):\n        if hasattr(self, 'button'):\n            button = self.button\n            icon_bytes = button.icon_paired if paired else button.icon_unpaired\n            icon = read_QIcon_from_bytes(icon_bytes)\n            button.setIcon(icon)\n\n    def query_choice(self, msg: str, choices: Sequence[ChoiceItem]):\n        self.done.clear()\n        self.query_signal.emit(msg, choices)\n        self.done.wait()\n        return self.choice\n\n    def yes_no_question(self, msg):\n        self.done.clear()\n        self.yes_no_signal.emit(msg)\n        self.done.wait()\n        return self.ok\n\n    def show_message(self, msg, on_cancel=None):\n        self.message_signal.emit(msg, on_cancel)\n\n    def show_error(self, msg, blocking=False):\n        self.done.clear()\n        self.error_signal.emit(msg, blocking)\n        if blocking:\n            self.done.wait()\n\n    def finished(self):\n        self.clear_signal.emit()\n\n    def get_word(self, msg):\n        self.done.clear()\n        self.word_signal.emit(msg)\n        self.done.wait()\n        return self.word\n\n    def get_passphrase(self, msg, confirm):\n        self.done.clear()\n        self.passphrase_signal.emit(msg, confirm)\n        self.done.wait()\n        return self.passphrase\n\n    def passphrase_dialog(self, msg, confirm):\n        # If confirm is true, require the user to enter the passphrase twice\n        parent = self.top_level_window()\n        d = WindowModalDialog(parent, _(\"Enter Passphrase\"))\n        if confirm:\n            OK_button = OkButton(d)\n            playout = PasswordLayout(msg=msg, kind=PW_PASSPHRASE, OK_button=OK_button)\n            vbox = QVBoxLayout()\n            vbox.addLayout(playout.layout())\n            vbox.addLayout(Buttons(CancelButton(d), OK_button))\n            d.setLayout(vbox)\n            passphrase = playout.new_password() if d.exec() else None\n        else:\n            pw = PasswordLineEdit()\n            pw.setMinimumWidth(200)\n            vbox = QVBoxLayout()\n            vbox.addWidget(WWLabel(msg))\n            vbox.addWidget(pw)\n            vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))\n            d.setLayout(vbox)\n            passphrase = pw.text() if d.exec() else None\n        self.passphrase = passphrase\n        self.done.set()\n\n    def word_dialog(self, msg):\n        dialog = WindowModalDialog(self.top_level_window(), \"\")\n        hbox = QHBoxLayout(dialog)\n        hbox.addWidget(QLabel(msg))\n        text = QLineEdit()\n        text.setMaximumWidth(12 * char_width_in_lineedit())\n        text.returnPressed.connect(dialog.accept)\n        hbox.addWidget(text)\n        hbox.addStretch(1)\n        dialog.exec()  # Firmware cannot handle cancellation\n        self.word = text.text()\n        self.done.set()\n\n    MESSAGE_DIALOG_TITLE = None  # type: Optional[str]\n    def message_dialog(self, msg, on_cancel=None):\n        self.clear_dialog()\n        title = self.MESSAGE_DIALOG_TITLE\n        if title is None:\n            title = _('Please check your {} device').format(self.device)\n        self.dialog = dialog = WindowModalDialog(self.top_level_window(), title)\n        label = QLabel(msg)\n        label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n        vbox = QVBoxLayout(dialog)\n        vbox.addWidget(label)\n        if on_cancel:\n            dialog.rejected.connect(on_cancel)\n            vbox.addLayout(Buttons(CancelButton(dialog)))\n        dialog.show()\n\n    def error_dialog(self, msg, blocking):\n        self.win.show_error(msg, parent=self.top_level_window())\n        if blocking:\n            self.done.set()\n\n    def clear_dialog(self):\n        if self.dialog:\n            self.dialog.accept()\n            self.dialog = None\n\n    def win_query_choice(self, msg: str, choices: Sequence[ChoiceItem]):\n        try:\n            self.choice = self.win.query_choice(msg, choices)\n        except UserCancelled:\n            self.choice = None\n        self.done.set()\n\n    def win_yes_no_question(self, msg):\n        self.ok = self.win.question(msg)\n        self.done.set()\n\n\nclass QtPluginBase(object):\n\n    @hook\n    def load_wallet(self: Union['QtPluginBase', HW_PluginBase], wallet: 'Abstract_Wallet', window: 'ElectrumWindow'):\n        relevant_keystores = [keystore for keystore in wallet.get_keystores()\n                              if isinstance(keystore, self.keystore_class)]\n        if not relevant_keystores:\n            return\n        for keystore in relevant_keystores:\n            if not self.libraries_available:\n                message = keystore.plugin.get_library_not_available_message()\n                window.show_error(message)\n                return\n            tooltip = self.device + '\\n' + (keystore.label or 'unnamed')\n            cb = partial(self._on_status_bar_button_click, window=window, keystore=keystore)\n            sb = window.statusBar()\n            icon = read_QIcon_from_bytes(self.read_file(self.icon_unpaired))\n            button = StatusBarButton(icon, tooltip, cb, sb.height())\n            button.icon_paired = self.read_file(self.icon_paired)\n            button.icon_unpaired = self.read_file(self.icon_unpaired)\n            sb.addPermanentWidget(button)\n            handler = self.create_handler(window)\n            handler.button = button\n            keystore.handler = handler\n            keystore.thread = TaskThread(window, on_error=partial(self.on_task_thread_error, window, keystore))\n            self.add_show_address_on_hw_device_button_for_receive_addr(wallet, keystore, window)\n        # Trigger pairings\n        devmgr = self.device_manager()\n        trigger_pairings = partial(devmgr.trigger_pairings, relevant_keystores, allow_user_interaction=True)\n        some_keystore = relevant_keystores[0]\n        some_keystore.thread.add(trigger_pairings)\n\n    def _on_status_bar_button_click(self, *, window: 'ElectrumWindow', keystore: 'Hardware_KeyStore'):\n        try:\n            self.show_settings_dialog(window=window, keystore=keystore)\n        except (UserFacingException, UserCancelled) as e:\n            exc_info = (type(e), e, e.__traceback__)\n            self.on_task_thread_error(window=window, keystore=keystore, exc_info=exc_info)\n\n    def on_task_thread_error(self: Union['QtPluginBase', HW_PluginBase], window: 'ElectrumWindow',\n                             keystore: 'Hardware_KeyStore', exc_info):\n        e = exc_info[1]\n        if isinstance(e, OutdatedHwFirmwareException):\n            if window.question(e.text_ignore_old_fw_and_continue(), title=_(\"Outdated device firmware\")):\n                self.set_ignore_outdated_fw()\n                # will need to re-pair\n                devmgr = self.device_manager()\n\n                def re_pair_device():\n                    device_id = self.choose_device(window, keystore)\n                    devmgr.unpair_id(device_id)\n                    self.get_client(keystore)\n\n                keystore.thread.add(re_pair_device)\n            return\n        else:\n            window.on_error(exc_info)\n\n    def choose_device(self: Union['QtPluginBase', HW_PluginBase], window: 'ElectrumWindow',\n                      keystore: 'Hardware_KeyStore') -> Optional[str]:\n        \"\"\"This dialog box should be usable even if the user has\n        forgotten their PIN or it is in bootloader mode.\"\"\"\n        assert window.gui_thread != threading.current_thread(), 'must not be called from GUI thread'\n        device_id = self.device_manager().id_by_pairing_code(keystore.pairing_code())\n        if not device_id:\n            try:\n                info = self.device_manager().select_device(self, keystore.handler, keystore)\n            except UserCancelled:\n                return\n            device_id = info.device.id_\n        return device_id\n\n    def show_settings_dialog(self, window: 'ElectrumWindow', keystore: 'Hardware_KeyStore') -> None:\n        # default implementation (if no dialog): just try to connect to device\n        def connect():\n            device_id = self.choose_device(window, keystore)\n\n        keystore.thread.add(connect)\n\n    def add_show_address_on_hw_device_button_for_receive_addr(\n            self,\n            wallet: 'Abstract_Wallet',\n            keystore: 'Hardware_KeyStore',\n            main_window: 'ElectrumWindow'\n    ):\n        plugin = keystore.plugin\n        receive_tab = main_window.receive_tab\n\n        def show_address():\n            addr = str(receive_tab.addr)\n            keystore.thread.add(partial(plugin.show_address, wallet, addr, keystore))\n\n        dev_name = f\"{plugin.device} ({keystore.label})\"\n        receive_tab.toolbar_menu.addAction(read_QIcon(\"eye1.png\"), _(\"Show address on {}\").format(dev_name), show_address)\n\n    def create_handler(self, window: Union['ElectrumWindow', 'QENewWalletWizard']) -> 'QtHandlerBase':\n        raise NotImplementedError()\n\n    def _add_menu_action(self, menu: QMenu, address: str, wallet: 'Abstract_Wallet'):\n        if not wallet.is_mine(address):\n            return\n        for keystore in wallet.get_keystores():\n            if type(keystore) == self.keystore_class:\n\n                def show_address(keystore=keystore):\n                    keystore.thread.add(partial(self.show_address, wallet, address, keystore=keystore))\n\n                device_name = \"{} ({})\".format(self.device, keystore.label)\n                menu.addAction(read_QIcon(\"eye1.png\"), _(\"Show address on {}\").format(device_name), show_address)\n"
  },
  {
    "path": "electrum/hw_wallet/trezor_qt_pinmatrix.py",
    "content": "# from https://github.com/trezor/trezor-firmware/blob/3f1d2059ca140788dab8726778f05cedbea20bc4/python/src/trezorlib/qt/pinmatrix.py\n#\n# This file is part of the Trezor project.\n#\n# Copyright (C) 2012-2022 SatoshiLabs and contributors\n#\n# This library is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Lesser General Public License version 3\n# as published by the Free Software Foundation.\n#\n# This library is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU Lesser General Public License for more details.\n#\n# You should have received a copy of the License along with this library.\n# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.\n\nimport math\nfrom typing import Any\n\nfrom PyQt6.QtCore import QRegularExpression, Qt\nfrom PyQt6.QtGui import QRegularExpressionValidator\nfrom PyQt6.QtWidgets import (\n    QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, QVBoxLayout, QWidget\n)\n\n\nclass PinButton(QPushButton):\n    def __init__(self, password: QLineEdit, encoded_value: int) -> None:\n        super(PinButton, self).__init__(\"?\")\n        self.password = password\n        self.encoded_value = encoded_value\n\n        self.clicked.connect(self._pressed)\n\n    def _pressed(self) -> None:\n        self.password.setText(self.password.text() + str(self.encoded_value))\n        self.password.setFocus()\n\n\nclass PinMatrixWidget(QWidget):\n    \"\"\"\n    Displays widget with nine blank buttons and password box.\n    Encodes button clicks into sequence of numbers for passing\n    into PinAck messages of Trezor.\n\n    show_strength=True may be useful for entering new PIN\n    \"\"\"\n\n    def __init__(self, show_strength: bool = True, parent: Any = None) -> None:\n        super(PinMatrixWidget, self).__init__(parent)\n\n        self.password = QLineEdit()\n        self.password.setValidator(QRegularExpressionValidator(QRegularExpression(\"[1-9]+\"), None))\n        self.password.setEchoMode(QLineEdit.EchoMode.Password)\n\n        self.password.textChanged.connect(self._password_changed)\n\n        self.strength = QLabel()\n        self.strength.setMinimumWidth(75)\n        self.strength.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self._set_strength(0)\n\n        grid = QGridLayout()\n        grid.setSpacing(0)\n        for y in range(3)[::-1]:\n            for x in range(3):\n                button = PinButton(self.password, x + y * 3 + 1)\n                button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)\n                button.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n                grid.addWidget(button, 3 - y, x)\n\n        hbox = QHBoxLayout()\n        hbox.addWidget(self.password)\n        if show_strength:\n            hbox.addWidget(self.strength)\n\n        vbox = QVBoxLayout()\n        vbox.addLayout(grid)\n        vbox.addLayout(hbox)\n        self.setLayout(vbox)\n\n    def _set_strength(self, strength: float) -> None:\n        if strength < 3000:\n            self.strength.setText(\"weak\")\n            self.strength.setStyleSheet(\"QLabel { color : #d00; }\")\n        elif strength < 60000:\n            self.strength.setText(\"fine\")\n            self.strength.setStyleSheet(\"QLabel { color : #db0; }\")\n        elif strength < 360000:\n            self.strength.setText(\"strong\")\n            self.strength.setStyleSheet(\"QLabel { color : #0a0; }\")\n        else:\n            self.strength.setText(\"ULTIMATE\")\n            self.strength.setStyleSheet(\"QLabel { color : #000; font-weight: bold;}\")\n\n    def _password_changed(self, password: Any) -> None:\n        self._set_strength(self.get_strength())\n\n    def get_strength(self) -> float:\n        digits = len(set(str(self.password.text())))\n        strength = math.factorial(9) / math.factorial(9 - digits)\n        return strength\n\n    def get_value(self) -> str:\n        return self.password.text()\n"
  },
  {
    "path": "electrum/i18n.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2012 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport functools\nimport json\nimport os\nimport string\nfrom typing import Optional\n\nimport gettext\n\nfrom .logging import get_logger\n\n\n_logger = get_logger(__name__)\nLOCALE_DIR = os.path.join(os.path.dirname(__file__), 'locale', 'locale')\n\n\ndef _get_null_translations():\n    \"\"\"Returns a gettext Translations obj with translations explicitly disabled.\"\"\"\n    return gettext.translation('electrum', fallback=True, class_=gettext.NullTranslations)\n\n\n# Set initial default language to None. i.e. translations explicitly disabled.\n# The main script or GUIs can call set_language to enable translations.\n_language = _get_null_translations()\n\n\ndef _ensure_translation_keeps_format_string_syntax_similar(translator):\n    \"\"\"This checks that the source string is syntactically similar to the translated string.\n    If not, translations are rejected by falling back to the source string.\n    \"\"\"\n    sf = string.Formatter()\n    @functools.wraps(translator)\n    def safe_translator(msg: str, **kwargs):\n        translation = translator(msg, **kwargs)\n        parsed1 = list(sf.parse(msg))  # iterable of tuples (literal_text, field_name, format_spec, conversion)\n        try:\n            parsed2 = list(sf.parse(translation))\n        except ValueError:  # malformed format string in translation\n            _logger.warning(\n                f\"rejected translation string: failed to parse. original={msg!r}. {translation=!r}\",\n                only_once=True)\n            return msg\n        # num of replacement fields must match:\n        if len(parsed1) != len(parsed2):\n            _logger.warning(\n                f\"rejected translation string: num replacement fields mismatch. original={msg!r}. {translation=!r}\",\n                only_once=True)\n            return msg\n        # set of \"field_name\"s must not change. (re-ordering is explicitly allowed):\n        field_names1 = set(tupl[1] for tupl in parsed1)\n        field_names2 = set(tupl[1] for tupl in parsed2)\n        if field_names1 != field_names2:\n            _logger.warning(\n                f\"rejected translation string: set of field_names mismatch. original={msg!r}. {translation=!r}\",\n                only_once=True)\n            return msg\n        # checks done.\n        return translation\n    return safe_translator\n\n\n# note: do not use old-style (%) formatting inside translations,\n#       as syntactically incorrectly translated strings often raise exceptions (see #3237).\n#       e.g. consider  _(\"Connected to %d nodes.\") % n            # <- raises. do NOT use\n#                      >>> \"Connecté aux noeuds\" % n\n#                      TypeError: not all arguments converted during string formatting\n# note: f-strings cannot be translated! see https://stackoverflow.com/q/49797658\n#       So this does NOT work:   _(f\"My name: {name}\")            # <- cannot be translated. do NOT use\n#       instead use .format:     _(\"My name: {}\").format(name)    # <- works. prefer this way.\n# note: positional and keyword-based substitution also works with str.format().\n#       These give more flexibility to translators: it allows reordering the substituted values.\n#       However, only if the translators understand and use it correctly!\n#          _(\"time left: {0} minutes, {1} seconds\").format(t//60, t%60)                   # <- works. ok to use\n#          _(\"time left: {mins} minutes, {secs} seconds\").format(mins=t//60, secs=t%60)   # <- works, but too complex\n@_ensure_translation_keeps_format_string_syntax_similar\ndef _(msg: str, *, context=None) -> str:\n    if msg == \"\":\n        return \"\"  # empty string must not be translated. see #7158\n    if context:\n        contexts = [context]\n        if context[-1] != \"|\":  # try with both \"|\" suffix and without\n            contexts.append(context + \"|\")\n        else:\n            contexts.append(context[:-1])\n        for ctx in contexts:\n            out = _language.pgettext(ctx, msg)\n            if out != msg:  # found non-trivial translation\n                return out\n        # else try without context\n    return _language.gettext(msg)\n\n\ndef set_language(x: Optional[str]) -> None:\n    _logger.info(f\"setting language to {x!r}\")\n    global _language\n    if not x:\n        return\n    if x.startswith(\"en_\"):\n        # Setting the language to \"English\" is a protected special-case:\n        # we disable all translations and use the source strings.\n        _language = _get_null_translations()\n    else:\n        _language = gettext.translation('electrum', LOCALE_DIR, fallback=True, languages=[x])\n\n\n# note: The values (human-visible lang names) should be either in English or in their own lang,\n#       but NOT translated to the currently selected lang.\n#       e.g. \"fr_FR\" we could show as either \"French\" or \"Francais\", or even as \"French - Francais\",\n#       but it is evil to show it as \"Franzosisch\". How am I supposed to switch back to English from Korean??? :)\nlanguages = {\n    '': _('Default'),\n    'ar_SA': 'Arabic',\n    'bg_BG': 'Bulgarian',\n    'cs_CZ': 'Czech',\n    'da_DK': 'Danish',\n    'de_DE': 'German',\n    'el_GR': 'Greek',\n    'eo_UY': 'Esperanto',\n    'en_UK': 'English',  # selecting this guarantees seeing the untranslated source strings\n    'es_ES': 'Spanish',\n    'fa_IR': 'Persian',\n    'fr_FR': 'French',\n    'hu_HU': 'Hungarian',\n    'hy_AM': 'Armenian',\n    'id_ID': 'Indonesian',\n    'it_IT': 'Italian',\n    'ja_JP': 'Japanese',\n    'ky_KG': 'Kyrgyz',\n    'lv_LV': 'Latvian',\n    'nb_NO': 'Norwegian Bokmal',\n    'nl_NL': 'Dutch',\n    'pl_PL': 'Polish',\n    'pt_BR': 'Portuguese (Brazil)',\n    'pt_PT': 'Portuguese',\n    'ro_RO': 'Romanian',\n    'ru_RU': 'Russian',\n    'sk_SK': 'Slovak',\n    'sl_SI': 'Slovenian',\n    'sv_SE': 'Swedish',\n    'ta_IN': 'Tamil',\n    'th_TH': 'Thai',\n    'tr_TR': 'Turkish',\n    'uk_UA': 'Ukrainian',\n    'vi_VN': 'Vietnamese',\n    'zh_CN': 'Chinese Simplified',\n    'zh_TW': 'Chinese Traditional',\n}\nassert '' in languages\n\n\ndef get_gui_lang_names(*, show_completion_percent: bool = True) -> dict[str, str]:\n    \"\"\"Returns a  lang_code -> lang_name  mapping, sorted.\n\n    If show_completion_percent is True, lang_name includes a % estimate for translation completeness.\n    \"\"\"\n    # calc catalog sizes\n    if show_completion_percent:\n        stats = _get_stats()\n    # sort (\"Default\" first, then \"English\", then lexicographically sorted names)\n    languages_copy = languages.copy()\n    lang_pair_default = (\"\", languages_copy.pop(\"\")) # pop \"Default\"\n    lang_pair_english = (\"en_UK\", languages_copy.pop(\"en_UK\")) # pop \"English\"\n    lang_pairs_sorted = sorted(languages_copy.items(), key=lambda x: x[1])\n    # fancy names\n    gui_lang_names = {}  # type: dict[str, str]\n    gui_lang_names[lang_pair_default[0]] = lang_pair_default[1]\n    gui_lang_names[lang_pair_english[0]] = lang_pair_english[1]\n    for lang_code, lang_name in lang_pairs_sorted:\n        if show_completion_percent and stats:\n            source_str_cnt = max(stats[\"source_string_count\"], 1)  # avoid div-by-zero\n            try:\n                lang_data = stats[\"translations\"][lang_code]\n            except KeyError as e:\n                _logger.warning(f\"missing language from stats.json: {e!r}\")\n                catalog_percent = \"??\"\n            else:\n                translated_str_cnt = lang_data[\"string_count\"]\n                catalog_percent = round(100 * translated_str_cnt / source_str_cnt)\n            gui_lang_names[lang_code] = f\"{lang_name} ({catalog_percent}%)\"\n        else:\n            gui_lang_names[lang_code] = lang_name\n    return gui_lang_names\n\n\n_stats = None\ndef _get_stats() -> dict:\n    global _stats\n    if _stats is None:\n        fname = f\"{LOCALE_DIR}/stats.json\"\n        try:\n            with open(fname, \"r\", encoding=\"utf-8\") as f:\n                text = f.read()\n        except OSError as e:  # we tolerate the file missing\n            # This can happen e.g. when running from git clone if user did not run build_locale.sh.\n            _logger.info(f\"failed to open stats file {fname!r} - built locale (translations) missing??: {e!r}\")\n            _stats = {}\n        else:  # found file. if it is there, it MUST parse correctly\n            _stats = json.loads(text)\n    return _stats\n"
  },
  {
    "path": "electrum/interface.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2011 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport os\nimport re\nimport ssl\nimport sys\nimport time\nimport traceback\nimport asyncio\nimport socket\nfrom typing import Tuple, Union, List, TYPE_CHECKING, Optional, Set, NamedTuple, Any, Sequence, Dict\nfrom collections import defaultdict\nfrom ipaddress import IPv4Network, IPv6Network, ip_address, IPv6Address, IPv4Address\nimport itertools\nimport logging\nimport hashlib\nimport functools\nimport random\nimport enum\n\nimport aiorpcx\nfrom aiorpcx import RPCSession, Notification, NetAddress, NewlineFramer\nfrom aiorpcx.curio import timeout_after, TaskTimeout\nfrom aiorpcx.jsonrpc import JSONRPC, CodeMessageError\nfrom aiorpcx.rawsocket import RSClient, RSTransport\nimport certifi\n\nfrom .util import (ignore_exceptions, log_exceptions, bfh, ESocksProxy,\n                   is_integer, is_non_negative_integer, is_hash256_str, is_hex_str,\n                   is_int_or_float, is_non_negative_int_or_float, OldTaskGroup,\n                   send_exception_to_crash_reporter, error_text_str_to_safe_str, versiontuple)\nfrom . import util\nfrom . import x509\nfrom . import pem\nfrom . import version\nfrom . import blockchain\nfrom .blockchain import Blockchain, HEADER_SIZE, CHUNK_SIZE\nfrom . import bitcoin\nfrom .bitcoin import DummyAddress, DummyAddressUsedInTxException\nfrom . import constants\nfrom .i18n import _\nfrom .logging import Logger\nfrom .transaction import Transaction\nfrom .fee_policy import FEE_ETA_TARGETS\nfrom .lrucache import LRUCache\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .simple_config import SimpleConfig\n\n\nca_path = certifi.where()\n\nBUCKET_NAME_OF_ONION_SERVERS = 'onion'\n\nKNOWN_ELEC_PROTOCOL_TRANSPORTS = {'t', 's'}\nPREFERRED_NETWORK_PROTOCOL = 's'\nassert PREFERRED_NETWORK_PROTOCOL in KNOWN_ELEC_PROTOCOL_TRANSPORTS\n\nMAX_NUM_HEADERS_PER_REQUEST = 2016\nassert MAX_NUM_HEADERS_PER_REQUEST >= CHUNK_SIZE\n\n\nclass NetworkTimeout:\n    # seconds\n    class Generic:\n        NORMAL = 30\n        RELAXED = 45\n        MOST_RELAXED = 600\n\n    class Urgent(Generic):\n        NORMAL = 10\n        RELAXED = 20\n        MOST_RELAXED = 60\n\n\ndef assert_non_negative_integer(val: Any) -> None:\n    if not is_non_negative_integer(val):\n        raise RequestCorrupted(f'{val!r} should be a non-negative integer')\n\n\ndef assert_integer(val: Any) -> None:\n    if not is_integer(val):\n        raise RequestCorrupted(f'{val!r} should be an integer')\n\n\ndef assert_int_or_float(val: Any) -> None:\n    if not is_int_or_float(val):\n        raise RequestCorrupted(f'{val!r} should be int or float')\n\n\ndef assert_non_negative_int_or_float(val: Any) -> None:\n    if not is_non_negative_int_or_float(val):\n        raise RequestCorrupted(f'{val!r} should be a non-negative int or float')\n\n\ndef assert_hash256_str(val: Any) -> None:\n    if not is_hash256_str(val):\n        raise RequestCorrupted(f'{val!r} should be a hash256 str')\n\n\ndef assert_hex_str(val: Any) -> None:\n    if not is_hex_str(val):\n        raise RequestCorrupted(f'{val!r} should be a hex str')\n\n\ndef assert_dict_contains_field(d: Any, *, field_name: str) -> Any:\n    if not isinstance(d, dict):\n        raise RequestCorrupted(f'{d!r} should be a dict')\n    if field_name not in d:\n        raise RequestCorrupted(f'required field {field_name!r} missing from dict')\n    return d[field_name]\n\n\ndef assert_list_or_tuple(val: Any) -> None:\n    if not isinstance(val, (list, tuple)):\n        raise RequestCorrupted(f'{val!r} should be a list or tuple')\n\n\ndef protocol_tuple(s: Any) -> tuple[int, ...]:\n    \"\"\"Converts a protocol version number, such as \"1.0\" to a tuple (1, 0).\n\n    If the version number is bad, (0, ) indicating version 0 is returned.\n    \"\"\"\n    try:\n        assert isinstance(s, str)\n        return versiontuple(s)\n    except Exception:\n        return (0, )\n\n\nclass ChainResolutionMode(enum.Enum):\n    CATCHUP = enum.auto()\n    BACKWARD = enum.auto()\n    BINARY = enum.auto()\n    FORK = enum.auto()\n    NO_FORK = enum.auto()\n\n\nclass NotificationSession(RPCSession):\n\n    def __init__(self, *args, interface: 'Interface', **kwargs):\n        super(NotificationSession, self).__init__(*args, **kwargs)\n        self.subscriptions = defaultdict(list)\n        self.cache = {}\n        self._msg_counter = itertools.count(start=1)\n        self.interface = interface\n        self.taskgroup = interface.taskgroup\n        self.cost_hard_limit = 0  # disable aiorpcx resource limits\n\n    async def handle_request(self, request):\n        self.maybe_log(f\"--> {request}\")\n        try:\n            if isinstance(request, Notification):\n                params, result = request.args[:-1], request.args[-1]\n                key = self.get_hashable_key_for_rpc_call(request.method, params)\n                if key in self.subscriptions:\n                    self.cache[key] = result\n                    for queue in self.subscriptions[key]:\n                        await queue.put(request.args)\n                else:\n                    raise Exception(f'unexpected notification')\n            else:\n                raise Exception(f'unexpected request. not a notification')\n        except Exception as e:\n            self.interface.logger.info(f\"error handling request {request}. exc: {repr(e)}\")\n            await self.close()\n\n    async def send_request(self, *args, timeout=None, **kwargs):\n        # note: semaphores/timeouts/backpressure etc are handled by\n        # aiorpcx. the timeout arg here in most cases should not be set\n        msg_id = next(self._msg_counter)\n        self.maybe_log(f\"<-- {args} {kwargs} (id: {msg_id})\")\n        try:\n            # note: RPCSession.send_request raises TaskTimeout in case of a timeout.\n            # TaskTimeout is a subclass of CancelledError, which is *suppressed* in TaskGroups\n            response = await util.wait_for2(\n                super().send_request(*args, **kwargs),\n                timeout)\n        except (TaskTimeout, asyncio.TimeoutError) as e:\n            self.maybe_log(f\"--> request timed out: {args} (id: {msg_id})\")\n            raise RequestTimedOut(f'request timed out: {args} (id: {msg_id})') from e\n        except CodeMessageError as e:\n            self.maybe_log(f\"--> {repr(e)} (id: {msg_id})\")\n            raise\n        except BaseException as e:  # cancellations, etc. are useful for debugging\n            self.maybe_log(f\"--> {repr(e)} (id: {msg_id})\")\n            raise\n        else:\n            self.maybe_log(f\"--> {response} (id: {msg_id})\")\n            return response\n\n    def set_default_timeout(self, timeout):\n        assert hasattr(self, \"sent_request_timeout\")  # in base class\n        self.sent_request_timeout = timeout\n        assert hasattr(self, \"max_send_delay\")        # in base class\n        self.max_send_delay = timeout\n\n    async def subscribe(self, method: str, params: List, queue: asyncio.Queue):\n        # note: until the cache is written for the first time,\n        # each 'subscribe' call might make a request on the network.\n        key = self.get_hashable_key_for_rpc_call(method, params)\n        self.subscriptions[key].append(queue)\n        if key in self.cache:\n            result = self.cache[key]\n        else:\n            result = await self.send_request(method, params)\n            self.cache[key] = result\n        await queue.put(params + [result])\n\n    def unsubscribe(self, queue):\n        \"\"\"Unsubscribe a callback to free object references to enable GC.\"\"\"\n        # note: we can't unsubscribe from the server, so we keep receiving\n        # subsequent notifications\n        for v in self.subscriptions.values():\n            if queue in v:\n                v.remove(queue)\n\n    @classmethod\n    def get_hashable_key_for_rpc_call(cls, method, params):\n        \"\"\"Hashable index for subscriptions and cache\"\"\"\n        return str(method) + repr(params)\n\n    def maybe_log(self, msg: str) -> None:\n        if not self.interface: return\n        if self.interface.debug or self.interface.network.debug:\n            self.interface.logger.debug(msg)\n\n    def default_framer(self):\n        # overridden so that max_size can be customized\n        max_size = self.interface.network.config.NETWORK_MAX_INCOMING_MSG_SIZE\n        assert max_size > 500_000, f\"{max_size=} (< 500_000) is too small\"\n        return NewlineFramer(max_size=max_size)\n\n    async def close(self, *, force_after: int = None):\n        \"\"\"Closes the connection and waits for it to be closed.\n        We try to flush buffered data to the wire, which can take some time.\n        \"\"\"\n        if force_after is None:\n            # We give up after a while and just abort the connection.\n            # Note: specifically if the server is running Fulcrum, waiting seems hopeless,\n            #       the connection must be aborted (see https://github.com/cculianu/Fulcrum/issues/76)\n            # Note: if the ethernet cable was pulled or wifi disconnected, that too might\n            #       wait until this timeout is triggered\n            force_after = 1  # seconds\n        await super().close(force_after=force_after)\n\n\nclass NetworkException(Exception): pass\n\n\nclass GracefulDisconnect(NetworkException):\n    log_level = logging.INFO\n\n    def __init__(self, *args, log_level=None, **kwargs):\n        Exception.__init__(self, *args, **kwargs)\n        if log_level is not None:\n            self.log_level = log_level\n\n\nclass RequestTimedOut(GracefulDisconnect):\n    def __str__(self):\n        return _(\"Network request timed out.\")\n\n\nclass RequestCorrupted(Exception): pass\n\nclass ErrorParsingSSLCert(Exception): pass\nclass ErrorGettingSSLCertFromServer(Exception): pass\nclass ErrorSSLCertFingerprintMismatch(Exception): pass\nclass InvalidOptionCombination(Exception): pass\nclass ConnectError(NetworkException): pass\n\n\nclass TxBroadcastError(NetworkException):\n    def get_message_for_gui(self):\n        raise NotImplementedError()\n\n\nclass TxBroadcastHashMismatch(TxBroadcastError):\n    def get_message_for_gui(self):\n        return \"{}\\n{}\\n\\n{}\" \\\n            .format(_(\"The server returned an unexpected transaction ID when broadcasting the transaction.\"),\n                    _(\"Consider trying to connect to a different server, or updating Electrum.\"),\n                    str(self))\n\n\nclass TxBroadcastServerReturnedError(TxBroadcastError):\n    def get_message_for_gui(self):\n        return \"{}\\n{}\\n\\n{}\" \\\n            .format(_(\"The server returned an error when broadcasting the transaction.\"),\n                    _(\"Consider trying to connect to a different server, or updating Electrum.\"),\n                    str(self))\n\n\nclass TxBroadcastUnknownError(TxBroadcastError):\n    def get_message_for_gui(self):\n        return \"{}\\n{}\" \\\n            .format(_(\"Unknown error when broadcasting the transaction.\"),\n                    _(\"Consider trying to connect to a different server, or updating Electrum.\"))\n\n\nclass _RSClient(RSClient):\n    async def create_connection(self):\n        try:\n            return await super().create_connection()\n        except OSError as e:\n            # note: using \"from e\" here will set __cause__ of ConnectError\n            raise ConnectError(e) from e\n\n\nclass PaddedRSTransport(RSTransport):\n    \"\"\"A raw socket transport that provides basic countermeasures against traffic analysis\n    by padding the jsonrpc payload with whitespaces to have ~uniform-size TCP packets.\n    (it is assumed that a network observer does not see plaintext transport contents,\n    due to it being wrapped e.g. in TLS)\n    \"\"\"\n\n    MIN_PACKET_SIZE = 1024\n    WAIT_FOR_BUFFER_GROWTH_SECONDS = 1.0\n    # (unpadded) amount of bytes sent instantly before beginning with polling.\n    # This makes the initial handshake where a few small messages are exchanged faster.\n    WARMUP_BUDGET_SIZE = 1024\n\n    session: Optional['RPCSession']\n\n    def __init__(self, *args, **kwargs):\n        RSTransport.__init__(self, *args, **kwargs)\n        self._sbuffer = bytearray()  # \"send buffer\"\n        self._sbuffer_task = None  # type: Optional[asyncio.Task]\n        self._sbuffer_has_data_evt = asyncio.Event()\n        self._last_send = time.monotonic()\n        self._force_send = False  # type: bool\n\n    # note: this does not call super().write() but is a complete reimplementation\n    async def write(self, message):\n        await self._can_send.wait()\n        if self.is_closing():\n            return\n        framed_message = self._framer.frame(message)\n        self._sbuffer += framed_message\n        self._sbuffer_has_data_evt.set()\n        self._maybe_consume_sbuffer()\n\n    def _maybe_consume_sbuffer(self) -> None:\n        \"\"\"Maybe take some data from sbuffer and send it on the wire.\"\"\"\n        if not self._can_send.is_set() or self.is_closing():\n            return\n        buf = self._sbuffer\n        if not buf:\n            return\n        # if there is enough data in the buffer, or if we haven't sent in a while, send now:\n        if not (\n            self._force_send\n            or len(buf) >= self.MIN_PACKET_SIZE\n            or self._last_send + self.WAIT_FOR_BUFFER_GROWTH_SECONDS < time.monotonic()\n            or self.session.send_size < self.WARMUP_BUDGET_SIZE\n        ):\n            return\n        assert buf[-2:] in (b\"}\\n\", b\"]\\n\"), f\"unexpected json-rpc terminator: {buf[-2:]=!r}\"\n        # either (1) pad length to next power of two, to create \"lsize\" packet:\n        payload_lsize = len(buf)\n        total_lsize = max(self.MIN_PACKET_SIZE, 2 ** (payload_lsize.bit_length()))\n        npad_lsize = total_lsize - payload_lsize\n        # or if that wasted a lot of bandwidth with padding, (2) defer sending some messages\n        # and create a packet with half that size (\"ssize\", s for small)\n        total_ssize = max(self.MIN_PACKET_SIZE, total_lsize // 2)\n        payload_ssize = buf.rfind(b\"\\n\", 0, total_ssize)\n        if payload_ssize != -1:\n            payload_ssize += 1  # for \"\\n\" char\n            npad_ssize = total_ssize - payload_ssize\n        else:\n            npad_ssize = float(\"inf\")\n        # decide between (1) and (2):\n        if self._force_send or npad_lsize <= npad_ssize:\n            # (1) create \"lsize\" packet: consume full buffer\n            npad = npad_lsize\n            p_idx = payload_lsize\n        else:\n            # (2) create \"ssize\" packet: consume some, but defer some for later\n            npad = npad_ssize\n            p_idx = payload_ssize\n        # pad by adding spaces near end\n        # self.session.maybe_log(\n        #     f\"PaddedRSTransport. calling low-level write(). \"\n        #     f\"chose between (lsize:{payload_lsize}+{npad_lsize}, ssize:{payload_ssize}+{npad_ssize}). \"\n        #     f\"won: {'tie' if npad_lsize == npad_ssize else 'lsize' if npad_lsize < npad_ssize else 'ssize'}.\"\n        # )\n        json_rpc_terminator = buf[p_idx-2:p_idx]\n        assert json_rpc_terminator in (b\"}\\n\", b\"]\\n\"), f\"unexpected {json_rpc_terminator=!r}\"\n        buf2 = buf[:p_idx-2] + (npad * b\" \") + json_rpc_terminator\n        self._asyncio_transport.write(buf2)\n        self._last_send = time.monotonic()\n        del self._sbuffer[:p_idx]\n        if not self._sbuffer:\n            self._sbuffer_has_data_evt.clear()\n\n    async def _poll_sbuffer(self):\n        while not self.is_closing():\n            await self._can_send.wait()\n            await self._sbuffer_has_data_evt.wait()  # to avoid busy-waiting\n            self._maybe_consume_sbuffer()\n            # If there is still data in the buffer, sleep until it would time out.\n            # note: If the transport is ~idle, when we wake up, we will send the current buf data,\n            #       but if busy, we might wake up to completely new buffer contents. Either is fine.\n            if len(self._sbuffer) > 0:\n                timeout_abs = self._last_send + self.WAIT_FOR_BUFFER_GROWTH_SECONDS\n                timeout_rel = max(0.0, timeout_abs - time.monotonic())\n                await asyncio.sleep(timeout_rel)\n\n    def connection_made(self, transport: asyncio.BaseTransport):\n        super().connection_made(transport)\n        if isinstance(self.session, NotificationSession):\n            coro = self.session.taskgroup.spawn(self._poll_sbuffer())\n            self._sbuffer_task = self.loop.create_task(coro)\n        else:\n            # This a short-lived \"fetch_certificate\"-type session.\n            # No polling here, we always force-empty the buffer.\n            self._force_send = True\n\n    async def close(self, *args, **kwargs):\n        '''Close the connection and return when closed.'''\n        # Flush buffer before disconnecting. This makes ReplyAndDisconnect work:\n        self._force_send = True\n        self._maybe_consume_sbuffer()\n        await super().close(*args, **kwargs)\n\n\nclass ServerAddr:\n\n    def __init__(self, host: str, port: Union[int, str], *, protocol: str = None):\n        assert isinstance(host, str), repr(host)\n        if protocol is None:\n            protocol = 's'\n        if not host:\n            raise ValueError('host must not be empty')\n        if host[0] == '[' and host[-1] == ']':  # IPv6\n            host = host[1:-1]\n        try:\n            net_addr = NetAddress(host, port)  # this validates host and port\n        except Exception as e:\n            raise ValueError(f\"cannot construct ServerAddr: invalid host or port (host={host}, port={port})\") from e\n        if protocol not in KNOWN_ELEC_PROTOCOL_TRANSPORTS:\n            raise ValueError(f\"invalid network protocol: {protocol}\")\n        self.host = str(net_addr.host)  # canonical form (if e.g. IPv6 address)\n        self.port = int(net_addr.port)\n        self.protocol = protocol\n        self._net_addr_str = str(net_addr)\n\n    @classmethod\n    def from_str(cls, s: str) -> 'ServerAddr':\n        \"\"\"Constructs a ServerAddr or raises ValueError.\"\"\"\n        # host might be IPv6 address, hence do rsplit:\n        host, port, protocol = str(s).rsplit(':', 2)\n        return ServerAddr(host=host, port=port, protocol=protocol)\n\n    @classmethod\n    def from_str_with_inference(cls, s: str) -> Optional['ServerAddr']:\n        \"\"\"Construct ServerAddr from str, guessing missing details.\n        Does not raise - just returns None if guessing failed.\n        Ongoing compatibility not guaranteed.\n        \"\"\"\n        if not s:\n            return None\n        host = \"\"\n        if s[0] == \"[\" and \"]\" in s:  # IPv6 address\n            host_end = s.index(\"]\")\n            host = s[1:host_end]\n            s = s[host_end+1:]\n        items = str(s).rsplit(':', 2)\n        if len(items) < 2:\n            return None  # although maybe we could guess the port too?\n        host = host or items[0]\n        port = items[1]\n        if len(items) >= 3:\n            protocol = items[2]\n        else:\n            protocol = PREFERRED_NETWORK_PROTOCOL\n        try:\n            return ServerAddr(host=host, port=port, protocol=protocol)\n        except ValueError:\n            return None\n\n    def to_friendly_name(self) -> str:\n        # note: this method is closely linked to from_str_with_inference\n        if self.protocol == 's':  # hide trailing \":s\"\n            return self.net_addr_str()\n        return str(self)\n\n    def __str__(self):\n        return '{}:{}'.format(self.net_addr_str(), self.protocol)\n\n    def to_json(self) -> str:\n        return str(self)\n\n    def __repr__(self):\n        return f'<ServerAddr host={self.host} port={self.port} protocol={self.protocol}>'\n\n    def net_addr_str(self) -> str:\n        return self._net_addr_str\n\n    def __eq__(self, other):\n        if not isinstance(other, ServerAddr):\n            return False\n        return (self.host == other.host\n                and self.port == other.port\n                and self.protocol == other.protocol)\n\n    def __ne__(self, other):\n        return not (self == other)\n\n    def __hash__(self):\n        return hash((self.host, self.port, self.protocol))\n\n\ndef _get_cert_path_for_host(*, config: 'SimpleConfig', host: str) -> str:\n    filename = host\n    try:\n        ip = ip_address(host)\n    except ValueError:\n        pass\n    else:\n        if isinstance(ip, IPv6Address):\n            filename = f\"ipv6_{ip.packed.hex()}\"\n    return os.path.join(config.path, 'certs', filename)\n\n\nclass Interface(Logger):\n\n    def __init__(self, *, network: 'Network', server: ServerAddr):\n        assert isinstance(server, ServerAddr), f\"expected ServerAddr, got {type(server)}\"\n        self.ready = network.asyncio_loop.create_future()\n        self.got_disconnected = asyncio.Event()\n        self._blockchain_updated = asyncio.Event()\n        self.server = server\n        Logger.__init__(self)\n        assert network.config.path\n        self.cert_path = _get_cert_path_for_host(config=network.config, host=self.host)\n        self.blockchain = None  # type: Optional[Blockchain]\n        self._requested_chunks = set()  # type: Set[int]\n        self.network = network\n        self.session = None  # type: Optional[NotificationSession]\n        self._ipaddr_bucket = None\n        # Set up proxy.\n        # - for servers running on localhost, the proxy is not used. If user runs their own server\n        #   on same machine, this lets them enable the proxy (which is used for e.g. FX rates).\n        #   note: we could maybe relax this further and bypass the proxy for all private\n        #         addresses...? e.g. 192.168.x.x\n        if util.is_localhost(server.host):\n            self.logger.info(f\"looks like localhost: not using proxy for this server\")\n            self.proxy = None\n        else:\n            self.proxy = ESocksProxy.from_network_settings(network)\n\n        # Latest block header and corresponding height, as claimed by the server.\n        # Note that these values are updated before they are verified.\n        # Especially during initial header sync, verification can take a long time.\n        # Failing verification will get the interface closed.\n        self.tip_header = None  # type: Optional[dict]\n        self.tip = 0\n\n        self._headers_cache = {}  # type: Dict[int, bytes]\n        self._rawtx_cache = LRUCache(maxsize=20)  # type: LRUCache[str, bytes]  # txid->rawtx\n\n        self.fee_estimates_eta = {}  # type: Dict[int, int]\n\n        self.active_protocol_tuple = (0,)  # type: Optional[tuple[int, ...]]\n\n        # Dump network messages (only for this interface).  Set at runtime from the console.\n        self.debug = False\n\n        self.taskgroup = OldTaskGroup()\n\n        async def spawn_task():\n            task = await self.network.taskgroup.spawn(self.run())\n            task.set_name(f\"interface::{str(server)}\")\n        asyncio.run_coroutine_threadsafe(spawn_task(), self.network.asyncio_loop)\n\n    @property\n    def host(self):\n        return self.server.host\n\n    @property\n    def port(self):\n        return self.server.port\n\n    @property\n    def protocol(self):\n        return self.server.protocol\n\n    def diagnostic_name(self):\n        return self.server.net_addr_str()\n\n    def __str__(self):\n        return f\"<Interface {self.diagnostic_name()}>\"\n\n    async def is_server_ca_signed(self, ca_ssl_context: ssl.SSLContext) -> bool:\n        \"\"\"Given a CA enforcing SSL context, returns True if the connection\n        can be established. Returns False if the server has a self-signed\n        certificate but otherwise is okay. Any other failures raise.\n        \"\"\"\n        try:\n            await self.open_session(ssl_context=ca_ssl_context, exit_early=True)\n        except ConnectError as e:\n            cause = e.__cause__\n            if (isinstance(cause, ssl.SSLCertVerificationError)\n                    and cause.reason == 'CERTIFICATE_VERIFY_FAILED'\n                    and cause.verify_code == 18):  # \"self signed certificate\"\n                # Good. We will use this server as self-signed.\n                return False\n            # Not good. Cannot use this server.\n            raise\n        # Good. We will use this server as CA-signed.\n        return True\n\n    async def _try_saving_ssl_cert_for_first_time(self, ca_ssl_context: ssl.SSLContext) -> None:\n        ca_signed = await self.is_server_ca_signed(ca_ssl_context)\n        if ca_signed:\n            if self._get_expected_fingerprint():\n                raise InvalidOptionCombination(\"cannot use --serverfingerprint with CA signed servers\")\n            with open(self.cert_path, 'w') as f:\n                # empty file means this is CA signed, not self-signed\n                f.write('')\n        else:\n            await self._save_certificate()\n\n    def _is_saved_ssl_cert_available(self):\n        if not os.path.exists(self.cert_path):\n            return False\n        with open(self.cert_path, 'r') as f:\n            contents = f.read()\n        if contents == '':  # CA signed\n            if self._get_expected_fingerprint():\n                raise InvalidOptionCombination(\"cannot use --serverfingerprint with CA signed servers\")\n            return True\n        # pinned self-signed cert\n        try:\n            b = pem.dePem(contents, 'CERTIFICATE')\n        except SyntaxError as e:\n            self.logger.info(f\"error parsing already saved cert: {e}\")\n            raise ErrorParsingSSLCert(e) from e\n        try:\n            x = x509.X509(b)\n        except Exception as e:\n            self.logger.info(f\"error parsing already saved cert: {e}\")\n            raise ErrorParsingSSLCert(e) from e\n        try:\n            x.check_date()\n        except x509.CertificateError as e:\n            self.logger.info(f\"certificate has expired: {e}\")\n            os.unlink(self.cert_path)  # delete pinned cert only in this case\n            return False\n        self._verify_certificate_fingerprint(bytes(b))\n        return True\n\n    async def _get_ssl_context(self) -> Optional[ssl.SSLContext]:\n        if self.protocol != 's':\n            # using plaintext TCP\n            return None\n\n        # see if we already have cert for this server; or get it for the first time\n        ca_sslc = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path)\n        if not self._is_saved_ssl_cert_available():\n            try:\n                await self._try_saving_ssl_cert_for_first_time(ca_sslc)\n            except (OSError, ConnectError, aiorpcx.socks.SOCKSError) as e:\n                raise ErrorGettingSSLCertFromServer(e) from e\n        # now we have a file saved in our certificate store\n        siz = os.stat(self.cert_path).st_size\n        if siz == 0:\n            # CA signed cert\n            sslc = ca_sslc\n        else:\n            # pinned self-signed cert\n            sslc = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=self.cert_path)\n            # note: Flag \"ssl.VERIFY_X509_STRICT\" is enabled by default in python 3.13+ (disabled in older versions).\n            #       We explicitly disable it as it breaks lots of servers.\n            sslc.verify_flags &= ~ssl.VERIFY_X509_STRICT\n            sslc.check_hostname = False\n        return sslc\n\n    def handle_disconnect(func):\n        @functools.wraps(func)\n        async def wrapper_func(self: 'Interface', *args, **kwargs):\n            try:\n                return await func(self, *args, **kwargs)\n            except GracefulDisconnect as e:\n                self.logger.log(e.log_level, f\"disconnecting due to {repr(e)}\")\n            except aiorpcx.jsonrpc.RPCError as e:\n                self.logger.warning(f\"disconnecting due to {repr(e)}\")\n                self.logger.debug(f\"(disconnect) trace for {repr(e)}\", exc_info=True)\n            finally:\n                self.got_disconnected.set()\n                # Make sure taskgroup gets cleaned-up. This explicit clean-up is needed here\n                # in case the \"with taskgroup\" ctx mgr never got a chance to run:\n                await self.taskgroup.cancel_remaining()\n                await self.network.connection_down(self)\n                # if was not 'ready' yet, schedule waiting coroutines:\n                self.ready.cancel()\n        return wrapper_func\n\n    @ignore_exceptions  # do not kill network.taskgroup\n    @log_exceptions\n    @handle_disconnect\n    async def run(self):\n        try:\n            ssl_context = await self._get_ssl_context()\n        except (ErrorParsingSSLCert, ErrorGettingSSLCertFromServer) as e:\n            self.logger.info(f'disconnecting due to: {repr(e)}')\n            return\n        try:\n            await self.open_session(ssl_context=ssl_context)\n        except (asyncio.CancelledError, ConnectError, aiorpcx.socks.SOCKSError) as e:\n            # make SSL errors for main interface more visible (to help servers ops debug cert pinning issues)\n            if (isinstance(e, ConnectError) and isinstance(e.__cause__, ssl.SSLError)\n                    and self.is_main_server() and not self.network.auto_connect):\n                self.logger.warning(f'Cannot connect to main server due to SSL error '\n                                    f'(maybe cert changed compared to \"{self.cert_path}\"). Exc: {repr(e)}')\n            else:\n                self.logger.info(f'disconnecting due to: {repr(e)}')\n            return\n\n    def _mark_ready(self) -> None:\n        if self.ready.cancelled():\n            raise GracefulDisconnect('conn establishment was too slow; *ready* future was cancelled')\n        if self.ready.done():\n            return\n\n        assert self.tip_header\n        chain = blockchain.check_header(self.tip_header)\n        if not chain:\n            self.blockchain = blockchain.get_best_chain()\n        else:\n            self.blockchain = chain\n        assert self.blockchain is not None\n\n        self.logger.info(f\"set blockchain with height {self.blockchain.height()}\")\n\n        self.ready.set_result(1)\n\n    def is_connected_and_ready(self) -> bool:\n        return self.ready.done() and not self.got_disconnected.is_set()\n\n    async def _save_certificate(self) -> None:\n        if not os.path.exists(self.cert_path):\n            # we may need to retry this a few times, in case the handshake hasn't completed\n            for _ in range(10):\n                dercert = await self._fetch_certificate()\n                if dercert:\n                    self.logger.info(\"succeeded in getting cert\")\n                    self._verify_certificate_fingerprint(dercert)\n                    with open(self.cert_path, 'w') as f:\n                        cert = ssl.DER_cert_to_PEM_cert(dercert)\n                        # workaround android bug\n                        cert = re.sub(\"([^\\n])-----END CERTIFICATE-----\",\"\\\\1\\n-----END CERTIFICATE-----\",cert)\n                        f.write(cert)\n                        # even though close flushes, we can't fsync when closed.\n                        # and we must flush before fsyncing, cause flush flushes to OS buffer\n                        # fsync writes to OS buffer to disk\n                        f.flush()\n                        os.fsync(f.fileno())\n                    break\n                await asyncio.sleep(1)\n            else:\n                raise GracefulDisconnect(\"could not get certificate after 10 tries\")\n\n    async def _fetch_certificate(self) -> bytes:\n        sslc = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)\n        sslc.check_hostname = False\n        sslc.verify_mode = ssl.CERT_NONE\n        async with _RSClient(\n            session_factory=RPCSession,\n            host=self.host, port=self.port,\n            ssl=sslc,\n            proxy=self.proxy,\n            transport=PaddedRSTransport,\n        ) as session:\n            asyncio_transport = session.transport._asyncio_transport  # type: asyncio.BaseTransport\n            ssl_object = asyncio_transport.get_extra_info(\"ssl_object\")  # type: ssl.SSLObject\n            return ssl_object.getpeercert(binary_form=True)\n\n    def _get_expected_fingerprint(self) -> Optional[str]:\n        if self.is_main_server():\n            return self.network.config.NETWORK_SERVERFINGERPRINT\n        return None\n\n    def _verify_certificate_fingerprint(self, certificate: bytes) -> None:\n        expected_fingerprint = self._get_expected_fingerprint()\n        if not expected_fingerprint:\n            return\n        fingerprint = hashlib.sha256(certificate).hexdigest()\n        fingerprints_match = fingerprint.lower() == expected_fingerprint.lower()\n        if not fingerprints_match:\n            util.trigger_callback('cert_mismatch')\n            raise ErrorSSLCertFingerprintMismatch('Refusing to connect to server due to cert fingerprint mismatch')\n        self.logger.info(\"cert fingerprint verification passed\")\n\n    async def _maybe_warm_headers_cache(self, *, from_height: int, to_height: int, mode: ChainResolutionMode) -> None:\n        \"\"\"Populate header cache for block heights in range [from_height, to_height].\"\"\"\n        assert from_height <= to_height, (from_height, to_height)\n        assert to_height - from_height < MAX_NUM_HEADERS_PER_REQUEST\n        if all(height in self._headers_cache for height in range(from_height, to_height+1)):\n            # cache already has all requested headers\n            return\n        # use lower timeout as we usually have network.bhi_lock here\n        timeout = self.network.get_network_timeout_seconds(NetworkTimeout.Urgent)\n        count = to_height - from_height + 1\n        headers = await self.get_block_headers(start_height=from_height, count=count, timeout=timeout, mode=mode)\n        for idx, raw_header in enumerate(headers):\n            header_height = from_height + idx\n            self._headers_cache[header_height] = raw_header\n\n    async def get_block_header(self, height: int, *, mode: ChainResolutionMode) -> dict:\n        if not is_non_negative_integer(height):\n            raise Exception(f\"{repr(height)} is not a block height\")\n        #self.logger.debug(f'get_block_header() {height} in {mode=}')\n        # use lower timeout as we usually have network.bhi_lock here\n        timeout = self.network.get_network_timeout_seconds(NetworkTimeout.Urgent)\n        if raw_header := self._headers_cache.get(height):\n            return blockchain.deserialize_header(raw_header, height)\n        self.logger.info(f'requesting block header {height} in {mode=}')\n        res = await self.session.send_request('blockchain.block.header', [height], timeout=timeout)\n        return blockchain.deserialize_header(bytes.fromhex(res), height)\n\n    async def get_block_headers(\n        self,\n        *,\n        start_height: int,\n        count: int,\n        timeout=None,\n        mode: Optional[ChainResolutionMode] = None,\n    ) -> Sequence[bytes]:\n        \"\"\"Request a number of consecutive block headers, starting at `start_height`.\n        `count` is the num of requested headers, BUT note the server might return fewer than this\n        (if range would extend beyond its tip).\n        note: the returned headers are not verified or parsed at all.\n        \"\"\"\n        if not is_non_negative_integer(start_height):\n            raise Exception(f\"{repr(start_height)} is not a block height\")\n        if not is_non_negative_integer(count) or not (0 < count <= MAX_NUM_HEADERS_PER_REQUEST):\n            raise Exception(f\"{repr(count)} not an int in range ]0, {MAX_NUM_HEADERS_PER_REQUEST}]\")\n        self.logger.info(\n            f\"requesting block headers: [{start_height}, {start_height+count-1}], {count=}\"\n            + (f\" (in {mode=})\" if mode is not None else \"\")\n        )\n        res = await self.session.send_request('blockchain.block.headers', [start_height, count], timeout=timeout)\n        # check response\n        assert_dict_contains_field(res, field_name='count')\n        assert_dict_contains_field(res, field_name='max')\n        assert_non_negative_integer(res['count'])\n        assert_non_negative_integer(res['max'])\n        if self.active_protocol_tuple >= (1, 6):\n            hex_headers_list = assert_dict_contains_field(res, field_name='headers')\n            assert_list_or_tuple(hex_headers_list)\n            for item in hex_headers_list:\n                assert_hex_str(item)\n                if len(item) != HEADER_SIZE * 2:\n                    raise RequestCorrupted(f\"invalid header size. got {len(item)//2}, expected {HEADER_SIZE}\")\n            if len(hex_headers_list) != res['count']:\n                raise RequestCorrupted(f\"{len(hex_headers_list)=} != {res['count']=}\")\n            headers = list(bfh(hex_header) for hex_header in hex_headers_list)\n        else: # proto 1.4\n            hex_headers_concat = assert_dict_contains_field(res, field_name='hex')\n            assert_hex_str(hex_headers_concat)\n            if len(hex_headers_concat) != HEADER_SIZE * 2 * res['count']:\n                raise RequestCorrupted('inconsistent chunk hex and count')\n            headers = list(util.chunks(bfh(hex_headers_concat), size=HEADER_SIZE))\n        # we never request more than MAX_NUM_HEADERS_IN_REQUEST headers, but we enforce those fit in a single response\n        if res['max'] < MAX_NUM_HEADERS_PER_REQUEST:\n            raise RequestCorrupted(f\"server uses too low 'max' count for block.headers: {res['max']} < {MAX_NUM_HEADERS_PER_REQUEST}\")\n        if res['count'] > count:\n            raise RequestCorrupted(f\"asked for {count} headers but got more: {res['count']}\")\n        elif res['count'] < count:\n            # we only tolerate getting fewer headers if it is due to reaching the tip\n            end_height = start_height + res['count'] - 1\n            if end_height < self.tip:  # still below tip. why did server not send more?!\n                raise RequestCorrupted(\n                    f\"asked for {count} headers but got fewer: {res['count']}. ({start_height=}, {self.tip=})\")\n        # checks done.\n        return headers\n\n    async def request_chunk_below_max_checkpoint(\n        self,\n        *,\n        height: int,\n    ) -> None:\n        if not is_non_negative_integer(height):\n            raise Exception(f\"{repr(height)} is not a block height\")\n        assert height <= constants.net.max_checkpoint(), f\"{height=} must be <= cp={constants.net.max_checkpoint()}\"\n        index = height // CHUNK_SIZE\n        if index in self._requested_chunks:\n            return None\n        self.logger.debug(f\"requesting chunk from height {height}\")\n        try:\n            self._requested_chunks.add(index)\n            headers = await self.get_block_headers(start_height=index * CHUNK_SIZE, count=CHUNK_SIZE)\n        finally:\n            self._requested_chunks.discard(index)\n        conn = self.blockchain.connect_chunk(index, data=b\"\".join(headers))\n        if not conn:\n            raise RequestCorrupted(f\"chunk ({index=}, for {height=}) does not connect to blockchain\")\n        return None\n\n    async def _fast_forward_chain(\n        self,\n        *,\n        height: int,  # usually local chain tip + 1\n        tip: int,  # server tip. we should not request past this.\n    ) -> int:\n        \"\"\"Request some headers starting at `height` to grow the blockchain of this interface.\n        Returns number of headers we managed to connect, starting at `height`.\n        \"\"\"\n        if not is_non_negative_integer(height):\n            raise Exception(f\"{repr(height)} is not a block height\")\n        if not is_non_negative_integer(tip):\n            raise Exception(f\"{repr(tip)} is not a block height\")\n        if not (height > constants.net.max_checkpoint()\n                or height == 0 == constants.net.max_checkpoint()):\n            raise Exception(f\"{height=} must be > cp={constants.net.max_checkpoint()}\")\n        assert height <= tip, f\"{height=} must be <= {tip=}\"\n        # Request a few chunks of headers concurrently.\n        # tradeoffs:\n        # - more chunks: higher memory requirements\n        # - more chunks: higher concurrency => syncing needs fewer network round-trips\n        # - if a chunk does not connect, bandwidth for all later chunks is wasted\n        async with OldTaskGroup() as group:\n            tasks = []  # type: List[Tuple[int, asyncio.Task[Sequence[bytes]]]]\n            index0 = height // CHUNK_SIZE\n            for chunk_cnt in range(10):\n                index = index0 + chunk_cnt\n                start_height = index * CHUNK_SIZE\n                if start_height > tip:\n                    break\n                end_height = min(start_height + CHUNK_SIZE - 1, tip)\n                size = end_height - start_height + 1\n                tasks.append((index, await group.spawn(self.get_block_headers(start_height=start_height, count=size))))\n        # try to connect chunks\n        num_headers = 0\n        for index, task in tasks:\n            headers = task.result()\n            conn = self.blockchain.connect_chunk(index, data=b\"\".join(headers))\n            if not conn:\n                break\n            num_headers += len(headers)\n        # We started at a chunk boundary, instead of requested `height`. Need to correct for that.\n        offset = height - index0 * CHUNK_SIZE\n        return max(0, num_headers - offset)\n\n    def is_main_server(self) -> bool:\n        return (self.network.interface == self or\n                self.network.interface is None and self.network.default_server == self.server)\n\n    async def open_session(\n        self,\n        *,\n        ssl_context: Optional[ssl.SSLContext],\n        exit_early: bool = False,\n    ):\n        session_factory = lambda *args, iface=self, **kwargs: NotificationSession(*args, **kwargs, interface=iface)\n        async with _RSClient(\n            session_factory=session_factory,\n            host=self.host, port=self.port,\n            ssl=ssl_context,\n            proxy=self.proxy,\n            transport=PaddedRSTransport,\n        ) as session:\n            start = time.perf_counter()\n            self.session = session  # type: NotificationSession\n            self.session.set_default_timeout(self.network.get_network_timeout_seconds(NetworkTimeout.Generic))\n            client_prange = [version.PROTOCOL_VERSION_MIN, version.PROTOCOL_VERSION_MAX]\n            try:\n                ver = await session.send_request('server.version', [self.client_name(), client_prange])\n            except aiorpcx.jsonrpc.RPCError as e:\n                raise GracefulDisconnect(e)  # probably 'unsupported protocol version'\n            if exit_early:\n                return\n            self.active_protocol_tuple = protocol_tuple(ver[1])\n            client_pmin = protocol_tuple(client_prange[0])\n            client_pmax = protocol_tuple(client_prange[1])\n            if not (client_pmin <= self.active_protocol_tuple <= client_pmax):\n                raise GracefulDisconnect(f'server violated protocol-version-negotiation. '\n                                         f'we asked for {client_prange!r}, they sent {ver[1]!r}')\n            if not self.network.check_interface_against_healthy_spread_of_connected_servers(self):\n                raise GracefulDisconnect(f'too many connected servers already '\n                                         f'in bucket {self.bucket_based_on_ipaddress()}')\n\n            try:\n                features = await session.send_request('server.features')\n                server_genesis_hash = assert_dict_contains_field(features, field_name='genesis_hash')\n            except (aiorpcx.jsonrpc.RPCError, RequestCorrupted) as e:\n                raise GracefulDisconnect(e)\n            if server_genesis_hash != constants.net.GENESIS:\n                raise GracefulDisconnect(f'server on different chain: {server_genesis_hash=}. ours: {constants.net.GENESIS}')\n            self.logger.info(f\"connection established. version: {ver}, handshake duration: {(time.perf_counter() - start) * 1000:.2f} ms\")\n\n            try:\n                async with self.taskgroup as group:\n                    await group.spawn(self.ping)\n                    await group.spawn(self.request_fee_estimates)\n                    await group.spawn(self.run_fetch_blocks)\n                    await group.spawn(self.monitor_connection)\n            except aiorpcx.jsonrpc.RPCError as e:\n                if e.code in (\n                    JSONRPC.EXCESSIVE_RESOURCE_USAGE,\n                    JSONRPC.SERVER_BUSY,\n                    JSONRPC.METHOD_NOT_FOUND,\n                    JSONRPC.INTERNAL_ERROR,\n                ):\n                    log_level = logging.WARNING if self.is_main_server() else logging.INFO\n                    raise GracefulDisconnect(e, log_level=log_level) from e\n                raise\n            finally:\n                self.got_disconnected.set()  # set this ASAP, ideally before any awaits\n\n    async def monitor_connection(self):\n        while True:\n            await asyncio.sleep(1)\n            # If the session/transport is no longer open, we disconnect.\n            # e.g. if the remote cleanly sends EOF, we would handle that here.\n            # note: If the user pulls the ethernet cable or disconnects wifi,\n            #       ideally we would detect that here, so that the GUI/etc can reflect that.\n            #       - On Android, this seems to work reliably , where asyncio.BaseProtocol.connection_lost()\n            #         gets called with e.g. ConnectionAbortedError(103, 'Software caused connection abort').\n            #       - On desktop Linux/Win, it seems BaseProtocol.connection_lost() is not called in such cases.\n            #         Hence, in practice the connection issue will only be detected the next time we try\n            #         to send a message (plus timeout), which can take minutes...\n            if not self.session or self.session.is_closing():\n                raise GracefulDisconnect('session was closed')\n\n    async def ping(self):\n        # We periodically send a \"ping\" msg to make sure the server knows we are still here.\n        # Adding a bit of randomness generates some noise against traffic analysis.\n        while True:\n            await asyncio.sleep(random.random() * 300)\n            await self.session.send_request('server.ping')\n            await self._maybe_send_noise()\n\n    async def _maybe_send_noise(self):\n        while random.random() < 0.2:\n            await asyncio.sleep(random.random())\n            await self.session.send_request('server.ping')\n\n    async def request_fee_estimates(self):\n        while True:\n            async with OldTaskGroup() as group:\n                fee_tasks = []\n                for i in FEE_ETA_TARGETS[0:-1]:\n                    fee_tasks.append((i, await group.spawn(self.get_estimatefee(i))))\n            for nblock_target, task in fee_tasks:\n                fee = task.result()\n                if fee < 0: continue\n                assert isinstance(fee, int)\n                self.fee_estimates_eta[nblock_target] = fee\n            self.network.update_fee_estimates()\n            await asyncio.sleep(60)\n\n    async def close(self, *, force_after: int = None):\n        \"\"\"Closes the connection and waits for it to be closed.\n        We try to flush buffered data to the wire, which can take some time.\n        \"\"\"\n        if self.session:\n            await self.session.close(force_after=force_after)\n        # monitor_connection will cancel tasks\n\n    async def run_fetch_blocks(self):\n        header_queue = asyncio.Queue()\n        await self.session.subscribe('blockchain.headers.subscribe', [], header_queue)\n        while True:\n            item = await header_queue.get()\n            raw_header = item[0]\n            height = raw_header['height']\n            header_bytes = bfh(raw_header['hex'])\n            header_dict = blockchain.deserialize_header(header_bytes, height)\n            self.tip_header = header_dict\n            self.tip = height\n            if self.tip < constants.net.max_checkpoint():\n                raise GracefulDisconnect(\n                    f\"server tip below max checkpoint. ({self.tip} < {constants.net.max_checkpoint()})\")\n            self._mark_ready()\n            self._headers_cache.clear()  # tip changed, so assume anything could have happened with chain\n            self._headers_cache[height] = header_bytes\n            try:\n                blockchain_updated = await self._process_header_at_tip()\n            finally:\n                self._headers_cache.clear()  # to reduce memory usage\n            # header processing done\n            if self.is_main_server() or blockchain_updated:\n                self.logger.info(f\"new chain tip. {height=}\")\n            if blockchain_updated:\n                util.trigger_callback('blockchain_updated')\n                self._blockchain_updated.set()\n                self._blockchain_updated.clear()\n            util.trigger_callback('network_updated')\n            await self.network.switch_unwanted_fork_interface()\n            await self.network.switch_lagging_interface()\n            await self.taskgroup.spawn(self._maybe_send_noise())\n\n    async def _process_header_at_tip(self) -> bool:\n        \"\"\"Returns:\n        False - boring fast-forward: we already have this header as part of this blockchain from another interface,\n        True - new header we didn't have, or reorg\n        \"\"\"\n        height, header = self.tip, self.tip_header\n        async with self.network.bhi_lock:\n            if self.blockchain.height() >= height and self.blockchain.check_header(header):\n                # another interface amended the blockchain\n                return False\n            await self.sync_until(height)\n            return True\n\n    async def sync_until(\n        self,\n        height: int,\n        *,\n        next_height: Optional[int] = None,  # sync target. typically the tip, except in unit tests\n    ) -> Tuple[ChainResolutionMode, int]:\n        if next_height is None:\n            next_height = self.tip\n        last = None  # type: Optional[ChainResolutionMode]\n        while last is None or height <= next_height:\n            prev_last, prev_height = last, height\n            if next_height > height + 144:\n                # We are far from the tip.\n                # It is more efficient to process headers in large batches (CPU/disk_usage/logging).\n                # (but this wastes a little bandwidth, if we are not on a chunk boundary)\n                num_headers = await self._fast_forward_chain(\n                    height=height, tip=next_height)\n                if num_headers == 0:\n                    if height <= constants.net.max_checkpoint():\n                        raise GracefulDisconnect('server chain conflicts with checkpoints or genesis')\n                    last, height = await self.step(height)\n                    continue\n                # report progress to gui/etc\n                util.trigger_callback('blockchain_updated')\n                self._blockchain_updated.set()\n                self._blockchain_updated.clear()\n                util.trigger_callback('network_updated')\n                height += num_headers\n                assert height <= next_height+1, (height, self.tip)\n                last = ChainResolutionMode.CATCHUP\n            else:\n                # We are close to the tip, so process headers one-by-one.\n                # (note: due to headers_cache, to save network latency, this can still batch-request headers)\n                last, height = await self.step(height)\n            assert (prev_last, prev_height) != (last, height), 'had to prevent infinite loop in interface.sync_until'\n        return last, height\n\n    async def step(\n        self,\n        height: int,\n    ) -> Tuple[ChainResolutionMode, int]:\n        assert 0 <= height <= self.tip, (height, self.tip)\n        await self._maybe_warm_headers_cache(\n            from_height=height,\n            to_height=min(self.tip, height+MAX_NUM_HEADERS_PER_REQUEST-1),\n            mode=ChainResolutionMode.CATCHUP,\n        )\n        header = await self.get_block_header(height, mode=ChainResolutionMode.CATCHUP)\n\n        chain = blockchain.check_header(header)\n        if chain:\n            self.blockchain = chain\n            # note: there is an edge case here that is not handled.\n            # we might know the blockhash (enough for check_header) but\n            # not have the header itself. e.g. regtest chain with only genesis.\n            # this situation resolves itself on the next block\n            return ChainResolutionMode.CATCHUP, height+1\n\n        can_connect = blockchain.can_connect(header)\n        if not can_connect:\n            self.logger.info(f\"can't connect new block: {height=}\")\n            height, header, bad, bad_header = await self._search_headers_backwards(height, header=header)\n            chain = blockchain.check_header(header)\n            can_connect = blockchain.can_connect(header)\n            assert chain or can_connect\n        if can_connect:\n            height += 1\n            self.blockchain = can_connect\n            self.blockchain.save_header(header)\n            return ChainResolutionMode.CATCHUP, height\n\n        good, bad, bad_header = await self._search_headers_binary(height, bad, bad_header, chain)\n        return await self._resolve_potential_chain_fork_given_forkpoint(good, bad, bad_header)\n\n    async def _search_headers_binary(\n        self,\n        height: int,\n        bad: int,\n        bad_header: dict,\n        chain: Optional[Blockchain],\n    ) -> Tuple[int, int, dict]:\n        assert bad == bad_header['block_height']\n        _assert_header_does_not_check_against_any_chain(bad_header)\n\n        self.blockchain = chain\n        good = height\n        while True:\n            assert 0 <= good < bad, (good, bad)\n            height = (good + bad) // 2\n            self.logger.info(f\"binary step. good {good}, bad {bad}, height {height}\")\n            if bad - good + 1 <= MAX_NUM_HEADERS_PER_REQUEST:  # if interval is small, trade some bandwidth for lower latency\n                await self._maybe_warm_headers_cache(\n                    from_height=good, to_height=bad, mode=ChainResolutionMode.BINARY)\n            header = await self.get_block_header(height, mode=ChainResolutionMode.BINARY)\n            chain = blockchain.check_header(header)\n            if chain:\n                self.blockchain = chain\n                good = height\n            else:\n                bad = height\n                bad_header = header\n            if good + 1 == bad:\n                break\n\n        if not self.blockchain.can_connect(bad_header, check_height=False):\n            raise Exception('unexpected bad header during binary: {}'.format(bad_header))\n        _assert_header_does_not_check_against_any_chain(bad_header)\n\n        self.logger.info(f\"binary search exited. good {good}, bad {bad}. {chain=}\")\n        return good, bad, bad_header\n\n    async def _resolve_potential_chain_fork_given_forkpoint(\n        self,\n        good: int,\n        bad: int,\n        bad_header: dict,\n    ) -> Tuple[ChainResolutionMode, int]:\n        assert good + 1 == bad\n        assert bad == bad_header['block_height']\n        _assert_header_does_not_check_against_any_chain(bad_header)\n        # 'good' is the height of a block 'good_header', somewhere in self.blockchain.\n        # bad_header connects to good_header; bad_header itself is NOT in self.blockchain.\n\n        bh = self.blockchain.height()\n        assert bh >= good, (bh, good)\n        if bh == good:\n            height = good + 1\n            self.logger.info(f\"catching up from {height}\")\n            return ChainResolutionMode.NO_FORK, height\n\n        # this is a new fork we don't yet have\n        height = bad + 1\n        self.logger.info(f\"new fork at bad height {bad}\")\n        b = self.blockchain.fork(bad_header)  # type: Blockchain\n        self.blockchain = b\n        assert b.forkpoint == bad\n        return ChainResolutionMode.FORK, height\n\n    async def _search_headers_backwards(\n        self,\n        height: int,\n        *,\n        header: dict,\n    ) -> Tuple[int, dict, int, dict]:\n        async def iterate():\n            nonlocal height, header\n            checkp = False\n            if height <= constants.net.max_checkpoint():\n                height = constants.net.max_checkpoint()\n                checkp = True\n            header = await self.get_block_header(height, mode=ChainResolutionMode.BACKWARD)\n            chain = blockchain.check_header(header)\n            can_connect = blockchain.can_connect(header)\n            if chain or can_connect:\n                return False\n            if checkp:\n                raise GracefulDisconnect(\"server chain conflicts with checkpoints\")\n            return True\n\n        bad, bad_header = height, header\n        _assert_header_does_not_check_against_any_chain(bad_header)\n        with blockchain.blockchains_lock: chains = list(blockchain.blockchains.values())\n        local_max = max([0] + [x.height() for x in chains])\n        height = min(local_max + 1, height - 1)\n        assert height >= 0\n\n        await self._maybe_warm_headers_cache(\n            from_height=max(0, height-10), to_height=height, mode=ChainResolutionMode.BACKWARD)\n\n        delta = 2\n        while await iterate():\n            bad, bad_header = height, header\n            height -= delta\n            delta *= 2\n\n        _assert_header_does_not_check_against_any_chain(bad_header)\n        self.logger.info(f\"exiting backward mode at {height}\")\n        return height, header, bad, bad_header\n\n    @classmethod\n    def client_name(cls) -> str:\n        return f'electrum/{version.ELECTRUM_VERSION}'\n\n    def is_tor(self):\n        return self.host.endswith('.onion')\n\n    def ip_addr(self) -> Optional[str]:\n        session = self.session\n        if not session: return None\n        peer_addr = session.remote_address()\n        if not peer_addr: return None\n        return str(peer_addr.host)\n\n    def bucket_based_on_ipaddress(self) -> str:\n        def do_bucket():\n            if self.is_tor():\n                return BUCKET_NAME_OF_ONION_SERVERS\n            try:\n                ip_addr = ip_address(self.ip_addr())  # type: Union[IPv4Address, IPv6Address]\n            except ValueError:\n                return ''\n            if not ip_addr:\n                return ''\n            if ip_addr.is_loopback:  # localhost is exempt\n                return ''\n            if ip_addr.version == 4:\n                slash16 = IPv4Network(ip_addr).supernet(prefixlen_diff=32-16)\n                return str(slash16)\n            elif ip_addr.version == 6:\n                slash48 = IPv6Network(ip_addr).supernet(prefixlen_diff=128-48)\n                return str(slash48)\n            return ''\n\n        if not self._ipaddr_bucket:\n            self._ipaddr_bucket = do_bucket()\n        return self._ipaddr_bucket\n\n    async def get_merkle_for_transaction(self, tx_hash: str, tx_height: int) -> dict:\n        if not is_hash256_str(tx_hash):\n            raise Exception(f\"{repr(tx_hash)} is not a txid\")\n        if not is_non_negative_integer(tx_height):\n            raise Exception(f\"{repr(tx_height)} is not a block height\")\n        # do request\n        res = await self.session.send_request('blockchain.transaction.get_merkle', [tx_hash, tx_height])\n        # check response\n        block_height = assert_dict_contains_field(res, field_name='block_height')\n        merkle = assert_dict_contains_field(res, field_name='merkle')\n        pos = assert_dict_contains_field(res, field_name='pos')\n        # note: tx_height was just a hint to the server, don't enforce the response to match it\n        assert_non_negative_integer(block_height)\n        assert_non_negative_integer(pos)\n        assert_list_or_tuple(merkle)\n        for item in merkle:\n            assert_hash256_str(item)\n        return res\n\n    async def get_transaction(self, tx_hash: str, *, timeout=None) -> str:\n        if not is_hash256_str(tx_hash):\n            raise Exception(f\"{repr(tx_hash)} is not a txid\")\n        if rawtx_bytes := self._rawtx_cache.get(tx_hash):\n            return rawtx_bytes.hex()\n        raw = await self.session.send_request('blockchain.transaction.get', [tx_hash], timeout=timeout)\n        # validate response\n        if not is_hex_str(raw):\n            raise RequestCorrupted(f\"received garbage (non-hex) as tx data (txid {tx_hash}): {raw!r}\")\n        tx = Transaction(raw)\n        try:\n            tx.deserialize()  # see if raises\n        except Exception as e:\n            raise RequestCorrupted(f\"cannot deserialize received transaction (txid {tx_hash})\") from e\n        if tx.txid() != tx_hash:\n            raise RequestCorrupted(f\"received tx does not match expected txid {tx_hash} (got {tx.txid()})\")\n        self._rawtx_cache[tx_hash] = bytes.fromhex(raw)\n        return raw\n\n    async def broadcast_transaction(self, tx: 'Transaction', *, timeout=None) -> None:\n        \"\"\"caller should handle TxBroadcastError and RequestTimedOut\"\"\"\n        txid_calc = tx.txid()\n        assert txid_calc is not None\n        rawtx = tx.serialize()\n        assert is_hex_str(rawtx)\n        if timeout is None:\n            timeout = self.network.get_network_timeout_seconds(NetworkTimeout.Urgent)\n        if any(DummyAddress.is_dummy_address(txout.address) for txout in tx.outputs()):\n            raise DummyAddressUsedInTxException(\"tried to broadcast tx with dummy address!\")\n        try:\n            out = await self.session.send_request('blockchain.transaction.broadcast', [rawtx], timeout=timeout)\n            # note: both 'out' and exception messages are untrusted input from the server\n        except (RequestTimedOut, asyncio.CancelledError, asyncio.TimeoutError):\n            raise  # pass-through\n        except aiorpcx.jsonrpc.CodeMessageError as e:\n            self.logger.info(f\"broadcast_transaction error [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(repr(e))}. tx={str(tx)}\")\n            raise TxBroadcastServerReturnedError(sanitize_tx_broadcast_response(e.message)) from e\n        except BaseException as e:  # intentional BaseException for sanity!\n            self.logger.info(f\"broadcast_transaction error2 [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(repr(e))}. tx={str(tx)}\")\n            send_exception_to_crash_reporter(e)\n            raise TxBroadcastUnknownError() from e\n        if out != txid_calc:\n            self.logger.info(f\"unexpected txid for broadcast_transaction [DO NOT TRUST THIS MESSAGE]: \"\n                             f\"{error_text_str_to_safe_str(out)} != {txid_calc}. tx={str(tx)}\")\n            raise TxBroadcastHashMismatch(_(\"Server returned unexpected transaction ID.\"))\n        # broadcast succeeded.\n        # We now cache the rawtx, for *this interface only*. The tx likely touches some ismine addresses, affecting\n        # the status of a scripthash we are subscribed to. Caching here will save a future get_transaction RPC.\n        self._rawtx_cache[txid_calc] = bytes.fromhex(rawtx)\n\n    async def broadcast_txpackage(self, txs: Sequence['Transaction']) -> bool:\n        assert self.active_protocol_tuple >= (1, 6), f\"server using old protocol: {self.active_protocol_tuple}\"\n        rawtxs = [tx.serialize() for tx in txs]\n        assert all(is_hex_str(rawtx) for rawtx in rawtxs)\n        assert all(tx.txid() is not None for tx in txs)\n        timeout = self.network.get_network_timeout_seconds(NetworkTimeout.Urgent)\n        for tx in txs:\n            if any(DummyAddress.is_dummy_address(txout.address) for txout in tx.outputs()):\n                raise DummyAddressUsedInTxException(\"tried to broadcast tx with dummy address!\")\n        try:\n            res = await self.session.send_request('blockchain.transaction.broadcast_package', [rawtxs], timeout=timeout)\n        except aiorpcx.jsonrpc.CodeMessageError as e:\n            self.logger.info(f\"broadcast_txpackage error [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(repr(e))}. {rawtxs=}\")\n            return False\n        success = assert_dict_contains_field(res, field_name='success')\n        if not success:\n            errors = assert_dict_contains_field(res, field_name='errors')\n            self.logger.info(f\"broadcast_txpackage error [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(repr(errors))}. {rawtxs=}\")\n            return False\n        assert success\n        # broadcast succeeded.\n        # We now cache the rawtx, for *this interface only*. The tx likely touches some ismine addresses, affecting\n        # the status of a scripthash we are subscribed to. Caching here will save a future get_transaction RPC.\n        for tx, rawtx in zip(txs, rawtxs):\n            self._rawtx_cache[tx.txid()] = bytes.fromhex(rawtx)\n        return True\n\n    async def get_history_for_scripthash(self, sh: str) -> List[dict]:\n        if not is_hash256_str(sh):\n            raise Exception(f\"{repr(sh)} is not a scripthash\")\n        # do request\n        res = await self.session.send_request('blockchain.scripthash.get_history', [sh])\n        # check response\n        assert_list_or_tuple(res)\n        prev_height = 1\n        for tx_item in res:\n            height = assert_dict_contains_field(tx_item, field_name='height')\n            assert_dict_contains_field(tx_item, field_name='tx_hash')\n            assert_integer(height)\n            if height < -1:\n                raise RequestCorrupted(f'{height!r} is not a valid block height')\n            assert_hash256_str(tx_item['tx_hash'])\n            if height in (-1, 0):\n                assert_dict_contains_field(tx_item, field_name='fee')\n                assert_non_negative_integer(tx_item['fee'])\n                prev_height = float(\"inf\")  # this ensures confirmed txs can't follow mempool txs\n            else:\n                # check monotonicity of heights\n                if height < prev_height:\n                    raise RequestCorrupted(f'heights of confirmed txs must be in increasing order')\n                prev_height = height\n        if self.active_protocol_tuple >= (1, 6):\n            # enforce order of mempool txs\n            mempool_txs = [tx_item for tx_item in res if tx_item['height'] <= 0]\n            if mempool_txs != sorted(mempool_txs, key=lambda x: (-x['height'], bytes.fromhex(x['tx_hash']))):\n                raise RequestCorrupted(f'mempool txs not in canonical order')\n        hashes = set(map(lambda item: item['tx_hash'], res))\n        if len(hashes) != len(res):\n            # Either server is sending garbage... or maybe if server is race-prone\n            # a recently mined tx could be included in both last block and mempool?\n            # Still, it's simplest to just disregard the response.\n            raise RequestCorrupted(f\"server history has non-unique txids for sh={sh}\")\n        return res\n\n    async def listunspent_for_scripthash(self, sh: str) -> List[dict]:\n        if not is_hash256_str(sh):\n            raise Exception(f\"{repr(sh)} is not a scripthash\")\n        # do request\n        res = await self.session.send_request('blockchain.scripthash.listunspent', [sh])\n        # check response\n        assert_list_or_tuple(res)\n        for utxo_item in res:\n            assert_dict_contains_field(utxo_item, field_name='tx_pos')\n            assert_dict_contains_field(utxo_item, field_name='value')\n            assert_dict_contains_field(utxo_item, field_name='tx_hash')\n            assert_dict_contains_field(utxo_item, field_name='height')\n            assert_non_negative_integer(utxo_item['tx_pos'])\n            assert_non_negative_integer(utxo_item['value'])\n            assert_non_negative_integer(utxo_item['height'])\n            assert_hash256_str(utxo_item['tx_hash'])\n        return res\n\n    async def get_balance_for_scripthash(self, sh: str) -> dict:\n        if not is_hash256_str(sh):\n            raise Exception(f\"{repr(sh)} is not a scripthash\")\n        # do request\n        res = await self.session.send_request('blockchain.scripthash.get_balance', [sh])\n        # check response\n        assert_dict_contains_field(res, field_name='confirmed')\n        assert_dict_contains_field(res, field_name='unconfirmed')\n        assert_non_negative_integer(res['confirmed'])\n        assert_integer(res['unconfirmed'])\n        return res\n\n    async def get_txid_from_txpos(self, tx_height: int, tx_pos: int, merkle: bool):\n        if not is_non_negative_integer(tx_height):\n            raise Exception(f\"{repr(tx_height)} is not a block height\")\n        if not is_non_negative_integer(tx_pos):\n            raise Exception(f\"{repr(tx_pos)} should be non-negative integer\")\n        # do request\n        res = await self.session.send_request(\n            'blockchain.transaction.id_from_pos',\n            [tx_height, tx_pos, merkle],\n        )\n        # check response\n        if merkle:\n            assert_dict_contains_field(res, field_name='tx_hash')\n            assert_dict_contains_field(res, field_name='merkle')\n            assert_hash256_str(res['tx_hash'])\n            assert_list_or_tuple(res['merkle'])\n            for node_hash in res['merkle']:\n                assert_hash256_str(node_hash)\n        else:\n            assert_hash256_str(res)\n        return res\n\n    async def get_fee_histogram(self) -> Sequence[Tuple[Union[float, int], int]]:\n        # do request\n        res = await self.session.send_request('mempool.get_fee_histogram')\n        # check response\n        assert_list_or_tuple(res)\n        prev_fee = float('inf')\n        for fee, s in res:\n            assert_non_negative_int_or_float(fee)\n            assert_non_negative_integer(s)\n            if fee >= prev_fee:  # check monotonicity\n                raise RequestCorrupted(f'fees must be in decreasing order')\n            prev_fee = fee\n        return res\n\n    async def get_server_banner(self) -> str:\n        # do request\n        res = await self.session.send_request('server.banner')\n        # check response\n        if not isinstance(res, str):\n            raise RequestCorrupted(f'{res!r} should be a str')\n        return res\n\n    async def get_donation_address(self) -> str:\n        # do request\n        res = await self.session.send_request('server.donation_address')\n        # check response\n        if not res:  # ignore empty string\n            return ''\n        if not isinstance(res, str):\n            raise RequestCorrupted(f'{res!r} should be a str')\n        address = res.removeprefix('bitcoin:')\n        if not bitcoin.is_address(address):\n            # note: do not hard-fail -- allow server to use future-type\n            #       bitcoin address we do not recognize\n            self.logger.info(f\"invalid donation address from server: {repr(res)}\")\n            return ''\n        return address\n\n    async def get_relay_fee(self) -> int:\n        \"\"\"Returns the min relay feerate in sat/kbyte.\"\"\"\n        # do request\n        if self.active_protocol_tuple >= (1, 6):\n            res = await self.session.send_request('mempool.get_info')\n            minrelaytxfee = assert_dict_contains_field(res, field_name='minrelaytxfee')\n        else:\n            minrelaytxfee = await self.session.send_request('blockchain.relayfee')\n        # check response\n        assert_non_negative_int_or_float(minrelaytxfee)\n        relayfee = int(minrelaytxfee * bitcoin.COIN)\n        relayfee = max(0, relayfee)\n        return relayfee\n\n    async def get_estimatefee(self, num_blocks: int) -> int:\n        \"\"\"Returns a feerate estimate for getting confirmed within\n        num_blocks blocks, in sat/kbyte.\n        Returns -1 if the server could not provide an estimate.\n        \"\"\"\n        if not is_non_negative_integer(num_blocks):\n            raise Exception(f\"{repr(num_blocks)} is not a num_blocks\")\n        # do request\n        try:\n            res = await self.session.send_request('blockchain.estimatefee', [num_blocks])\n        except aiorpcx.jsonrpc.ProtocolError as e:\n            # The protocol spec says the server itself should already have returned -1\n            # if it cannot provide an estimate, however apparently \"electrs\" does not conform\n            # and sends an error instead. Convert it here:\n            if \"cannot estimate fee\" in e.message:\n                res = -1\n            else:\n                raise\n        except aiorpcx.jsonrpc.RPCError as e:\n            # The protocol spec says the server itself should already have returned -1\n            # if it cannot provide an estimate. \"Fulcrum\" often sends:\n            #   aiorpcx.jsonrpc.RPCError: (-32603, 'internal error: bitcoind request timed out')\n            if e.code == JSONRPC.INTERNAL_ERROR:\n                res = -1\n            else:\n                raise\n        # check response\n        if res != -1:\n            assert_non_negative_int_or_float(res)\n            res = int(res * bitcoin.COIN)\n        return res\n\n\ndef _assert_header_does_not_check_against_any_chain(header: dict) -> None:\n    chain_bad = blockchain.check_header(header)\n    if chain_bad:\n        raise Exception('bad_header must not check!')\n\n\ndef sanitize_tx_broadcast_response(server_msg) -> str:\n    # Unfortunately, bitcoind and hence the Electrum protocol doesn't return a useful error code.\n    # So, we use substring matching to grok the error message.\n    # server_msg is untrusted input so it should not be shown to the user. see #4968\n    server_msg = str(server_msg)\n    server_msg = server_msg.replace(\"\\n\", r\"\\n\")\n\n    # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/script/script_error.cpp\n    script_error_messages = {\n        r\"Script evaluated without error but finished with a false/empty top stack element\",\n        r\"Script failed an OP_VERIFY operation\",\n        r\"Script failed an OP_EQUALVERIFY operation\",\n        r\"Script failed an OP_CHECKMULTISIGVERIFY operation\",\n        r\"Script failed an OP_CHECKSIGVERIFY operation\",\n        r\"Script failed an OP_NUMEQUALVERIFY operation\",\n        r\"Script is too big\",\n        r\"Push value size limit exceeded\",\n        r\"Operation limit exceeded\",\n        r\"Stack size limit exceeded\",\n        r\"Signature count negative or greater than pubkey count\",\n        r\"Pubkey count negative or limit exceeded\",\n        r\"Opcode missing or not understood\",\n        r\"Attempted to use a disabled opcode\",\n        r\"Operation not valid with the current stack size\",\n        r\"Operation not valid with the current altstack size\",\n        r\"OP_RETURN was encountered\",\n        r\"Invalid OP_IF construction\",\n        r\"Negative locktime\",\n        r\"Locktime requirement not satisfied\",\n        r\"Signature hash type missing or not understood\",\n        r\"Non-canonical DER signature\",\n        r\"Data push larger than necessary\",\n        r\"Only push operators allowed in signatures\",\n        r\"Non-canonical signature: S value is unnecessarily high\",\n        r\"Dummy CHECKMULTISIG argument must be zero\",\n        r\"OP_IF/NOTIF argument must be minimal\",\n        r\"Signature must be zero for failed CHECK(MULTI)SIG operation\",\n        r\"NOPx reserved for soft-fork upgrades\",\n        r\"Witness version reserved for soft-fork upgrades\",\n        r\"Taproot version reserved for soft-fork upgrades\",\n        r\"OP_SUCCESSx reserved for soft-fork upgrades\",\n        r\"Public key version reserved for soft-fork upgrades\",\n        r\"Public key is neither compressed or uncompressed\",\n        r\"Stack size must be exactly one after execution\",\n        r\"Extra items left on stack after execution\",\n        r\"Witness program has incorrect length\",\n        r\"Witness program was passed an empty witness\",\n        r\"Witness program hash mismatch\",\n        r\"Witness requires empty scriptSig\",\n        r\"Witness requires only-redeemscript scriptSig\",\n        r\"Witness provided for non-witness script\",\n        r\"Using non-compressed keys in segwit\",\n        r\"Invalid Schnorr signature size\",\n        r\"Invalid Schnorr signature hash type\",\n        r\"Invalid Schnorr signature\",\n        r\"Invalid Taproot control block size\",\n        r\"Too much signature validation relative to witness weight\",\n        r\"OP_CHECKMULTISIG(VERIFY) is not available in tapscript\",\n        r\"OP_IF/NOTIF argument must be minimal in tapscript\",\n        r\"Using OP_CODESEPARATOR in non-witness script\",\n        r\"Signature is found in scriptCode\",\n    }\n    for substring in script_error_messages:\n        if substring in server_msg:\n            return substring\n    # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/validation.cpp\n    # grep \"REJECT_\"\n    # grep \"TxValidationResult\"\n    # should come after script_error.cpp (due to e.g. \"non-mandatory-script-verify-flag\")\n    validation_error_messages = {\n        r\"coinbase\": None,\n        r\"tx-size-small\": None,\n        r\"non-final\": None,\n        r\"txn-already-in-mempool\": None,\n        r\"txn-mempool-conflict\": None,\n        r\"txn-already-known\": None,\n        r\"non-BIP68-final\": None,\n        r\"bad-txns-nonstandard-inputs\": None,\n        r\"bad-witness-nonstandard\": None,\n        r\"bad-txns-too-many-sigops\": None,\n        r\"mempool min fee not met\":\n            (\"mempool min fee not met\\n\" +\n             _(\"Your transaction is paying a fee that is so low that the bitcoin node cannot \"\n               \"fit it into its mempool. The mempool is already full of hundreds of megabytes \"\n               \"of transactions that all pay higher fees. Try to increase the fee.\")),\n        r\"min relay fee not met\": None,\n        r\"absurdly-high-fee\": None,\n        r\"max-fee-exceeded\": None,\n        r\"too-long-mempool-chain\": None,\n        r\"bad-txns-spends-conflicting-tx\": None,\n        r\"insufficient fee\": (\"insufficient fee\\n\" +\n             _(\"Your transaction is trying to replace another one in the mempool but it \"\n               \"does not meet the rules to do so. Try to increase the fee.\")),\n        r\"too many potential replacements\": None,\n        r\"replacement-adds-unconfirmed\": None,\n        r\"mempool full\": None,\n        r\"non-mandatory-script-verify-flag\": None,\n        r\"mandatory-script-verify-flag-failed\": None,\n        r\"Transaction check failed\": None,\n    }\n    for substring in validation_error_messages:\n        if substring in server_msg:\n            msg = validation_error_messages[substring]\n            return msg if msg else substring\n    # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/rpc/rawtransaction.cpp\n    # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/util/error.cpp\n    # https://github.com/bitcoin/bitcoin/blob/3f83c744ac28b700090e15b5dda2260724a56f49/src/common/messages.cpp#L126\n    # grep \"RPC_TRANSACTION\"\n    # grep \"RPC_DESERIALIZATION_ERROR\"\n    # grep \"TransactionError\"\n    rawtransaction_error_messages = {\n        r\"Missing inputs\": None,\n        r\"Inputs missing or spent\": None,\n        r\"transaction already in block chain\": None,\n        r\"Transaction already in block chain\": None,\n        r\"Transaction outputs already in utxo set\": None,\n        r\"TX decode failed\": None,\n        r\"Peer-to-peer functionality missing or disabled\": None,\n        r\"Transaction rejected by AcceptToMemoryPool\": None,\n        r\"AcceptToMemoryPool failed\": None,\n        r\"Transaction rejected by mempool\": None,\n        r\"Mempool internal error\": None,\n        r\"Fee exceeds maximum configured by user\": None,\n        r\"Unspendable output exceeds maximum configured by user\": None,\n        r\"Transaction rejected due to invalid package\": None,\n    }\n    for substring in rawtransaction_error_messages:\n        if substring in server_msg:\n            msg = rawtransaction_error_messages[substring]\n            return msg if msg else substring\n    # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/consensus/tx_verify.cpp\n    # https://github.com/bitcoin/bitcoin/blob/c7ad94428ab6f54661d7a5441e1fdd0ebf034903/src/consensus/tx_check.cpp\n    # grep \"REJECT_\"\n    # grep \"TxValidationResult\"\n    tx_verify_error_messages = {\n        r\"bad-txns-vin-empty\": None,\n        r\"bad-txns-vout-empty\": None,\n        r\"bad-txns-oversize\": None,\n        r\"bad-txns-vout-negative\": None,\n        r\"bad-txns-vout-toolarge\": None,\n        r\"bad-txns-txouttotal-toolarge\": None,\n        r\"bad-txns-inputs-duplicate\": None,\n        r\"bad-cb-length\": None,\n        r\"bad-txns-prevout-null\": None,\n        r\"bad-txns-inputs-missingorspent\":\n            (\"bad-txns-inputs-missingorspent\\n\" +\n             _(\"You might have a local transaction in your wallet that this transaction \"\n               \"builds on top. You need to either broadcast or remove the local tx.\")),\n        r\"bad-txns-premature-spend-of-coinbase\": None,\n        r\"bad-txns-inputvalues-outofrange\": None,\n        r\"bad-txns-in-belowout\": None,\n        r\"bad-txns-fee-outofrange\": None,\n    }\n    for substring in tx_verify_error_messages:\n        if substring in server_msg:\n            msg = tx_verify_error_messages[substring]\n            return msg if msg else substring\n    # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/policy/policy.cpp\n    # grep \"reason =\"\n    # should come after validation.cpp (due to \"tx-size\" vs \"tx-size-small\")\n    # should come after script_error.cpp (due to e.g. \"version\")\n    policy_error_messages = {\n        r\"version\": _(\"Transaction uses non-standard version.\"),\n        r\"tx-size\": _(\"The transaction was rejected because it is too large (in bytes).\"),\n        r\"scriptsig-size\": None,\n        r\"scriptsig-not-pushonly\": None,\n        r\"scriptpubkey\":\n            (\"scriptpubkey\\n\" +\n             _(\"Some of the outputs pay to a non-standard script.\")),\n        r\"bare-multisig\": None,\n        r\"dust\":\n            (_(\"Transaction could not be broadcast due to dust outputs.\\n\"\n               \"Some of the outputs are too small in value, probably lower than 1000 satoshis.\\n\"\n               \"Check the units, make sure you haven't confused e.g. mBTC and BTC.\")),\n        r\"multi-op-return\": _(\"The transaction was rejected because it contains multiple OP_RETURN outputs.\"),\n    }\n    for substring in policy_error_messages:\n        if substring in server_msg:\n            msg = policy_error_messages[substring]\n            return msg if msg else substring\n    # otherwise:\n    return _(\"Unknown error\")\n\n\ndef check_cert(host, cert):\n    try:\n        b = pem.dePem(cert, 'CERTIFICATE')\n        x = x509.X509(b)\n    except Exception:\n        traceback.print_exc(file=sys.stdout)\n        return\n\n    try:\n        x.check_date()\n        expired = False\n    except Exception:\n        expired = True\n\n    m = \"host: %s\\n\"%host\n    m += \"has_expired: %s\\n\"% expired\n    util.print_msg(m)\n\n\n# Used by tests\ndef _match_hostname(name, val):\n    if val == name:\n        return True\n\n    return val.startswith('*.') and name.endswith(val[1:])\n\n\ndef test_certificates():\n    from .simple_config import SimpleConfig\n    config = SimpleConfig()\n    mydir = os.path.join(config.path, \"certs\")\n    certs = os.listdir(mydir)\n    for c in certs:\n        p = os.path.join(mydir,c)\n        with open(p, encoding='utf-8') as f:\n            cert = f.read()\n        check_cert(c, cert)\n\nif __name__ == \"__main__\":\n    test_certificates()\n"
  },
  {
    "path": "electrum/invoices.py",
    "content": "import time\nfrom typing import TYPE_CHECKING, List, Optional, Union, Dict, Any, Sequence\nfrom decimal import Decimal\n\nimport attr\n\nfrom .json_db import StoredObject, stored_in\nfrom .i18n import _\nfrom .util import age, InvoiceError, format_satoshis\nfrom .bip21 import create_bip21_uri\nfrom .lnutil import hex_to_bytes\nfrom .lnaddr import lndecode, LnAddr\nfrom . import constants\nfrom .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC\nfrom .bitcoin import address_to_script\nfrom .transaction import PartialTxOutput\nfrom .crypto import sha256d\n\nif TYPE_CHECKING:\n    from .paymentrequest import PaymentRequest\n\n# convention: 'invoices' = outgoing , 'request' = incoming\n\n# status of payment requests\nPR_UNPAID   = 0     # if onchain: invoice amt not reached by txs in mempool+chain. if LN: invoice not paid.\nPR_EXPIRED  = 1     # invoice is unpaid and expiry time reached\nPR_UNKNOWN  = 2     # e.g. invoice not found\nPR_PAID     = 3     # if onchain: paid and mined (1 conf). if LN: invoice is paid.\nPR_INFLIGHT = 4     # only for LN. payment attempt in progress\nPR_FAILED   = 5     # only for LN. we attempted to pay it, but all attempts failed\nPR_ROUTING  = 6     # only for LN. *unused* atm.\nPR_UNCONFIRMED = 7  # only onchain. invoice is satisfied but tx is not mined yet.\nPR_BROADCASTING = 8    # onchain, tx is being broadcast\nPR_BROADCAST    = 9    # onchain, tx was broadcast, is not yet in our history\n\npr_color = {\n    PR_UNPAID:   (.7, .7, .7, 1),\n    PR_PAID:     (.2, .9, .2, 1),\n    PR_UNKNOWN:  (.7, .7, .7, 1),\n    PR_EXPIRED:  (.9, .2, .2, 1),\n    PR_INFLIGHT: (.9, .6, .3, 1),\n    PR_FAILED:   (.9, .2, .2, 1),\n    PR_ROUTING:  (.9, .6, .3, 1),\n    PR_BROADCASTING:  (.9, .6, .3, 1),\n    PR_BROADCAST:  (.9, .6, .3, 1),\n    PR_UNCONFIRMED: (.9, .6, .3, 1),\n}\n\n\ndef pr_tooltips():\n    return {\n        PR_UNPAID: _('Unpaid'),\n        PR_PAID: _('Paid'),\n        PR_UNKNOWN: _('Unknown'),\n        PR_EXPIRED: _('Expired'),\n        PR_INFLIGHT: _('In progress'),\n        PR_BROADCASTING: _('Broadcasting'),\n        PR_BROADCAST: _('Broadcast successfully'),\n        PR_FAILED: _('Failed'),\n        PR_ROUTING: _('Computing route...'),\n        PR_UNCONFIRMED: _('Unconfirmed'),\n    }\n\n\ndef pr_expiration_values():\n    return {\n        0: _('Never'),\n        10*60: _('10 minutes'),\n        60*60: _('1 hour'),\n        24*60*60: _('1 day'),\n        7*24*60*60: _('1 week'),\n    }\n\n\nPR_DEFAULT_EXPIRATION_WHEN_CREATING = 24*60*60  # 1 day\nassert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values()\n\n\ndef _decode_outputs(outputs) -> Optional[List[PartialTxOutput]]:\n    if outputs is None:\n        return None\n    ret = []\n    for output in outputs:\n        if not isinstance(output, PartialTxOutput):\n            output = PartialTxOutput.from_legacy_tuple(*output)\n        ret.append(output)\n    return ret\n\n\n# hack: BOLT-11 is not really clear on what an expiry of 0 means.\n# It probably interprets it as 0 seconds, so already expired...\n# Our higher level invoices code however uses 0 for \"never\".\n# Hence set some high expiration here\nLN_EXPIRY_NEVER = 100 * 365 * 24 * 60 * 60  # 100 years\n\n\n@attr.s\nclass BaseInvoice(StoredObject):\n    \"\"\"\n    Base class for Invoice and Request\n    In the code, we use 'invoice' for outgoing payments, and 'request' for incoming payments.\n\n    TODO this class is getting too complicated for \"attrs\"... maybe we should rewrite it without.\n    \"\"\"\n\n    # mandatory fields\n    amount_msat = attr.ib(  # can be '!' or None\n        kw_only=True, on_setattr=attr.setters.validate)  # type: Optional[Union[int, str]]\n    message = attr.ib(type=str, kw_only=True)\n    time = attr.ib(  # timestamp of the invoice\n        type=int, kw_only=True, validator=attr.validators.instance_of(int), on_setattr=attr.setters.validate)\n    exp = attr.ib(  # expiration delay (relative). 0 means never\n        type=int, kw_only=True, validator=attr.validators.instance_of(int), on_setattr=attr.setters.validate)\n\n    # optional fields.\n    # an request (incoming) can be satisfied onchain, using lightning or using a swap\n    # an invoice (outgoing) is constructed from a source: bip21, bip70, lnaddr\n\n    # onchain only\n    outputs = attr.ib(kw_only=True, converter=_decode_outputs)  # type: Optional[List[PartialTxOutput]]\n    height = attr.ib(  # only for receiving\n        type=int, kw_only=True, validator=attr.validators.instance_of(int), on_setattr=attr.setters.validate)\n    bip70 = attr.ib(type=str, kw_only=True)  # type: Optional[str]\n    #bip70_requestor = attr.ib(type=str, kw_only=True)  # type: Optional[str]\n\n    def is_lightning(self) -> bool:\n        raise NotImplementedError()\n\n    def get_address(self) -> Optional[str]:\n        \"\"\"returns the first address, to be displayed in GUI\"\"\"\n        raise NotImplementedError()\n\n    @property\n    def rhash(self) -> str:\n        raise NotImplementedError()\n\n    def get_status_str(self, status):\n        status_str = pr_tooltips()[status]\n        if status == PR_UNPAID:\n            if self.exp > 0 and self.exp != LN_EXPIRY_NEVER:\n                expiration = self.get_expiration_date()\n                status_str = _('Expires') + ' ' + age(expiration, include_seconds=True)\n        return status_str\n\n    def get_outputs(self) -> Sequence[PartialTxOutput]:\n        outputs = self.outputs or []\n        if not outputs:\n            address = self.get_address()\n            amount = self.get_amount_sat()\n            if address and amount is not None:\n                outputs = [PartialTxOutput.from_address_and_value(address, int(amount))]\n        return outputs\n\n    def get_expiration_date(self):\n        # 0 means never\n        return self.exp + self.time if self.exp else 0\n\n    @staticmethod\n    def _get_cur_time():  # for unit tests\n        return time.time()\n\n    def has_expired(self) -> bool:\n        exp = self.get_expiration_date()\n        return bool(exp) and exp < self._get_cur_time()\n\n    def get_amount_msat(self) -> Union[int, str, None]:\n        return self.amount_msat\n\n    def get_time(self):\n        return self.time\n\n    def get_message(self):\n        return self.message\n\n    def get_amount_sat(self) -> Union[int, str, None]:\n        \"\"\"\n        Returns an integer satoshi amount, or '!' or None.\n        Callers who need msat precision should call get_amount_msat()\n        \"\"\"\n        amount_msat = self.amount_msat\n        if amount_msat in [None, \"!\"]:\n            return amount_msat\n        return int(amount_msat // 1000)\n\n    def set_amount_msat(self, amount_msat: Union[int, str]) -> None:\n        \"\"\"The GUI uses this to fill the amount for a zero-amount invoice.\"\"\"\n        if amount_msat == \"!\":\n            amount_sat = amount_msat\n        else:\n            assert isinstance(amount_msat, int), f\"{amount_msat=!r}\"\n            assert amount_msat >= 0, amount_msat\n            amount_sat = (amount_msat // 1000) + int(amount_msat % 1000 > 0)  # round up\n        if outputs := self.outputs:\n            assert len(self.outputs) == 1, len(self.outputs)\n            self.outputs = [PartialTxOutput(scriptpubkey=outputs[0].scriptpubkey, value=amount_sat)]\n        self.amount_msat = amount_msat\n\n    @amount_msat.validator\n    def _validate_amount(self, attribute, value):\n        if value is None:\n            return\n        if isinstance(value, int):\n            if not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN * 1000):\n                raise InvoiceError(f\"amount is out-of-bounds: {value!r} msat\")\n        elif isinstance(value, str):\n            if value != '!':\n                raise InvoiceError(f\"unexpected amount: {value!r}\")\n        else:\n            raise InvoiceError(f\"unexpected amount: {value!r}\")\n\n    @classmethod\n    def from_bech32(cls, invoice: str) -> 'Invoice':\n        \"\"\"Constructs Invoice object from BOLT-11 string.\n        Might raise InvoiceError.\n        \"\"\"\n        try:\n            lnaddr = lndecode(invoice)\n        except Exception as e:\n            raise InvoiceError(e) from e\n        amount_msat = lnaddr.get_amount_msat()\n        timestamp = lnaddr.date\n        exp_delay = lnaddr.get_expiry()\n        message = lnaddr.get_description()\n        return Invoice(\n            message=message,\n            amount_msat=amount_msat,\n            time=timestamp,\n            exp=exp_delay,\n            outputs=None,\n            bip70=None,\n            height=0,\n            lightning_invoice=invoice,\n        )\n\n    @classmethod\n    def from_bip70_payreq(cls, pr: 'PaymentRequest', *, height: int = 0) -> 'Invoice':\n        return Invoice(\n            amount_msat=pr.get_amount()*1000,\n            message=pr.get_memo(),\n            time=pr.get_time(),\n            exp=pr.get_expiration_date() - pr.get_time(),\n            outputs=pr.get_outputs(),\n            bip70=pr.raw.hex(),\n            height=height,\n            lightning_invoice=None,\n        )\n\n    def get_id(self) -> str:\n        if self.is_lightning():\n            return self.rhash\n        else:  # on-chain\n            return get_id_from_onchain_outputs(outputs=self.get_outputs(), timestamp=self.time)\n\n    def as_dict(self, status):\n        d = {\n            'is_lightning': self.is_lightning(),\n            'amount_BTC': format_satoshis(self.get_amount_sat()),\n            'message': self.message,\n            'timestamp': self.get_time(),\n            'expiry': self.exp,\n            'status': status,\n            'status_str': self.get_status_str(status),\n            'id': self.get_id(),\n            'amount_sat': self.get_amount_sat(),\n        }\n        if self.is_lightning():\n            d['amount_msat'] = self.get_amount_msat()\n        return d\n\n\n@stored_in('invoices')\n@attr.s\nclass Invoice(BaseInvoice):\n    lightning_invoice = attr.ib(type=str, kw_only=True)  # type: Optional[str]\n    __lnaddr = None\n    _broadcasting_status = None # can be None or PR_BROADCASTING or PR_BROADCAST\n\n    def is_lightning(self):\n        return self.lightning_invoice is not None\n\n    def get_broadcasting_status(self):\n        return self._broadcasting_status\n\n    def get_address(self) -> Optional[str]:\n        address = None\n        if self.outputs:\n            address = self.outputs[0].address if len(self.outputs) > 0 else None\n        if not address and self.is_lightning():\n            address = self._lnaddr.get_fallback_address() or None\n        return address\n\n    @property\n    def _lnaddr(self) -> LnAddr:\n        if self.__lnaddr is None:\n            self.__lnaddr = lndecode(self.lightning_invoice)\n        return self.__lnaddr\n\n    @property\n    def rhash(self) -> str:\n        assert self.is_lightning()\n        return self._lnaddr.paymenthash.hex()\n\n    @lightning_invoice.validator\n    def _validate_invoice_str(self, attribute, value):\n        if value is not None:\n            lnaddr = lndecode(value)  # this checks the str can be decoded\n            self.__lnaddr = lnaddr    # save it, just to avoid having to recompute later\n\n    def can_be_paid_onchain(self) -> bool:\n        if self.is_lightning():\n            return bool(self._lnaddr.get_fallback_address()) or (bool(self.outputs))\n        else:\n            return True\n\n    def to_debug_json(self) -> Dict[str, Any]:\n        d = self.to_json()\n        d[\"lnaddr\"] = self._lnaddr.to_debug_json()\n        return d\n\n\n@stored_in('payment_requests')\n@attr.s\nclass Request(BaseInvoice):\n    payment_hash = attr.ib(type=bytes, kw_only=True, converter=hex_to_bytes)  # type: Optional[bytes]\n\n    def is_lightning(self):\n        return self.payment_hash is not None\n\n    def get_address(self) -> Optional[str]:\n        address = None\n        if self.outputs:\n            address = self.outputs[0].address if len(self.outputs) > 0 else None\n        return address\n\n    @property\n    def rhash(self) -> str:\n        assert self.is_lightning()\n        return self.payment_hash.hex()\n\n    def get_bip21_URI(\n        self,\n        *,\n        lightning_invoice: Optional[str] = None,\n    ) -> Optional[str]:\n        addr = self.get_address()\n        amount = self.get_amount_sat()\n        message = self.message\n        if amount is None and not message:\n            return\n        if amount:\n            amount = int(amount)\n        extra = {}\n        if self.time and self.exp:\n            extra['time'] = str(int(self.time))\n            extra['exp'] = str(int(self.exp))\n        if lightning_invoice:\n            extra['lightning'] = lightning_invoice\n        if not addr and lightning_invoice:\n            return \"bitcoin:?lightning=\"+lightning_invoice\n        if not addr and not lightning_invoice:\n            return None\n        uri = create_bip21_uri(addr, amount, message, extra_query_params=extra)\n        return str(uri)\n\n\ndef get_id_from_onchain_outputs(outputs: Sequence[PartialTxOutput], *, timestamp: int) -> str:\n    outputs_str = \"\\n\".join(f\"{txout.scriptpubkey.hex()}, {txout.value}\" for txout in outputs)\n    return sha256d(outputs_str + \"%d\" % timestamp).hex()[0:10]\n"
  },
  {
    "path": "electrum/json_db.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2019 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport threading\nimport copy\nimport json\nfrom typing import TYPE_CHECKING, Optional, Sequence, List, Union, Any\n\nimport jsonpatch\nimport jsonpointer\n\nfrom . import util\nfrom .util import WalletFileException, profiler, sticky_property\nfrom .logging import Logger\n\nif TYPE_CHECKING:\n    from .storage import WalletStorage\n\n\n# We monkeypatch exceptions in the jsonpatch package to ensure they do not contain secrets from the DB.\n# We often log exceptions and offer to send them to the crash reporter, so they must not contain secrets.\njsonpointer.JsonPointerException.__str__ = lambda self: \"\"\"(JPE) 'redacted'\"\"\"\njsonpointer.JsonPointerException.__repr__ = lambda self: \"\"\"<JsonPointerException 'redacted'>\"\"\"\nsetattr(jsonpointer.JsonPointerException, '__cause__', sticky_property(None))\nsetattr(jsonpointer.JsonPointerException, '__context__', sticky_property(None))\nsetattr(jsonpointer.JsonPointerException, '__suppress_context__', sticky_property(True))\njsonpatch.JsonPatchException.__str__ = lambda self: \"\"\"(JPE) 'redacted'\"\"\"\njsonpatch.JsonPatchException.__repr__ = lambda self: \"\"\"<JsonPatchException 'redacted'>\"\"\"\nsetattr(jsonpatch.JsonPatchException, '__cause__', sticky_property(None))\nsetattr(jsonpatch.JsonPatchException, '__context__', sticky_property(None))\nsetattr(jsonpatch.JsonPatchException, '__suppress_context__', sticky_property(True))\n\n\ndef modifier(func):\n    def wrapper(self, *args, **kwargs):\n        with self.lock:\n            self._modified = True\n            return func(self, *args, **kwargs)\n    return wrapper\n\ndef locked(func):\n    def wrapper(self, *args, **kwargs):\n        with self.lock:\n            return func(self, *args, **kwargs)\n    return wrapper\n\n\nregistered_names = {}\nregistered_dicts = {}\nregistered_dict_keys = {}\nregistered_parent_keys = {}\n\ndef register_dict(name, method, _type):\n    registered_dicts[name] = method, _type\n\ndef register_name(name, method, _type):\n    registered_names[name] = method, _type\n\ndef register_dict_key(name, method):\n    registered_dict_keys[name] = method\n\ndef register_parent_key(name, method):\n    registered_parent_keys[name] = method\n\ndef stored_as(name, _type=dict):\n    \"\"\" decorator that indicates the storage key of a stored object\"\"\"\n    def decorator(func):\n        registered_names[name] = func, _type\n        return func\n    return decorator\n\ndef stored_in(name, _type=dict):\n    \"\"\" decorator that indicates the storage key of an element in a StoredDict\"\"\"\n    def decorator(func):\n        registered_dicts[name] = func, _type\n        return func\n    return decorator\n\n_FLEX_KEY = str | int | None\n\ndef key_path(path: Sequence[_FLEX_KEY], key: _FLEX_KEY) -> str:\n    def to_str(x: _FLEX_KEY) -> str:\n        assert isinstance(x, _FLEX_KEY), repr(x)\n        assert x is not None\n        if isinstance(x, int):\n            return str(int(x))\n        else:\n            assert isinstance(x, str), f\"unexpected key type for: {x!r}\"\n            return x\n    items = [to_str(x) for x in path]\n    if key is not None:\n        items.append(to_str(key))\n    return '/'.join(items)\n\nclass BaseStoredObject:\n\n    _db: 'JsonDB' = None\n    _key: _FLEX_KEY = None\n    _parent: Optional['BaseStoredObject'] = None\n    _lock: threading.RLock = None\n\n    def set_db(self, db):\n        self._db = db\n        self._lock = self._db.lock if self._db else threading.RLock()\n\n    def set_parent(self, *, key: _FLEX_KEY, parent: Optional['BaseStoredObject']) -> None:\n        assert (key == \"\") == (parent is None), f\"{key=!r}, {parent=!r}\"\n        assert isinstance(key, _FLEX_KEY), repr(key)\n        self._key = key\n        self._parent = parent\n\n    @property\n    def lock(self):\n        return self._lock\n\n    @property\n    def path(self) -> Sequence[_FLEX_KEY] | None:\n        # return None iff we are pruned from root\n        x = self\n        s = [x._key]\n        while x._parent is not None:\n            x = x._parent\n            s = [x._key] + s\n        if x._key != '':\n            return None\n        assert self._db is not None\n        return s\n\n    def db_add(self, key: _FLEX_KEY, value) -> None:\n        assert isinstance(key, _FLEX_KEY), repr(key)\n        if self.path:\n            self._db.add(self.path, key, value)\n\n    def db_replace(self, key: _FLEX_KEY, value) -> None:\n        assert isinstance(key, _FLEX_KEY), repr(key)\n        if self.path:\n            self._db.replace(self.path, key, value)\n\n    def db_remove(self, key: _FLEX_KEY) -> None:\n        assert isinstance(key, _FLEX_KEY), repr(key)\n        if self.path:\n            self._db.remove(self.path, key)\n\n\nclass StoredObject(BaseStoredObject):\n    \"\"\"for attr.s objects \"\"\"\n\n    def __setattr__(self, key: str, value):\n        assert isinstance(key, str), repr(key)\n        if self.path and not key.startswith('_'):\n            if value != getattr(self, key):\n                self.db_replace(key, value)\n        object.__setattr__(self, key, value)\n\n    def to_json(self):\n        d = dict(vars(self))\n        # don't expose/store private stuff\n        d = {k: v for k, v in d.items()\n             if not k.startswith('_')}\n        return d\n\n\n\n_RaiseKeyError = object() # singleton for no-default behavior\n\n\nclass StoredDict(dict, BaseStoredObject):\n\n    def __init__(self, data: dict, db: 'JsonDB'):\n        self.set_db(db)\n        # recursively convert dicts to StoredDict\n        for k, v in list(data.items()):\n            self.__setitem__(k, v)\n\n    @locked\n    def __setitem__(self, key: _FLEX_KEY, v) -> None:\n        assert isinstance(key, _FLEX_KEY), repr(key)\n        is_new = key not in self\n        # early return to prevent unnecessary disk writes\n        if not is_new and self._db and json.dumps(v, cls=self._db.encoder) == json.dumps(self[key], cls=self._db.encoder):\n            return\n        # convert dict to StoredDict.\n        if type(v) == dict and (self._db is None or self._db._should_convert_to_stored_dict(key)):\n            v = StoredDict(v, self._db)\n        # convert list to StoredList\n        elif type(v) == list:\n            v = StoredList(v, self._db)\n        # reject sets. they do not work well with jsonpatch\n        elif isinstance(v, set):\n            raise Exception(f\"Do not store sets inside jsondb. path={self.path!r}\")\n        # set db for StoredObject, because it is not set in the constructor\n        if isinstance(v, StoredObject):\n            v.set_db(self._db)\n        # set parent\n        if isinstance(v, BaseStoredObject):\n            v.set_parent(key=key, parent=self)\n        # set item\n        dict.__setitem__(self, key, v)\n        self.db_add(key, v) if is_new else self.db_replace(key, v)\n\n    @locked\n    def __delitem__(self, key: _FLEX_KEY) -> None:\n        assert isinstance(key, _FLEX_KEY), repr(key)\n        r  = self.get(key, None)\n        dict.__delitem__(self, key)\n        self.db_remove(key)\n        if isinstance(r, BaseStoredObject):\n            r._parent = None\n\n    @locked\n    def pop(self, key: _FLEX_KEY, v=_RaiseKeyError) -> Any:\n        assert isinstance(key, _FLEX_KEY), repr(key)\n        if key not in self:\n            if v is _RaiseKeyError:\n                raise KeyError(key)\n            else:\n                return v\n        r = dict.pop(self, key)\n        self.db_remove(key)\n        if isinstance(r, BaseStoredObject):\n            r._parent = None\n        return r\n\n    def setdefault(self, key: _FLEX_KEY, default = None, /):\n        assert isinstance(key, _FLEX_KEY), repr(key)\n        if key not in self:\n            self.__setitem__(key, default)\n        return self[key]\n\n\nclass StoredList(list, BaseStoredObject):\n\n    def __init__(self, data, db: 'JsonDB'):\n        list.__init__(self, data)\n        self.set_db(db)\n\n    @locked\n    def append(self, item):\n        n = len(self)\n        list.append(self, item)\n        self.db_add('%d'%n, item)\n\n    @locked\n    def remove(self, item):\n        n = self.index(item)\n        list.remove(self, item)\n        self.db_remove('%d'%n)\n\n    @locked\n    def clear(self):\n        list.clear(self)\n        self.db_replace(None, [])\n\n\n\nclass JsonDB(Logger):\n\n    def __init__(\n        self,\n        s: str,\n        *,\n        storage: Optional['WalletStorage'] = None,\n        encoder=None,\n        upgrader=None,\n    ):\n        Logger.__init__(self)\n        self.lock = threading.RLock()\n        self.storage = storage\n        self.encoder = encoder\n        self.pending_changes = []  # type: List[str]\n        self._modified = False\n        # load data\n        data = self.load_data(s)\n        if upgrader:\n            data, was_upgraded = upgrader(data)\n            self._modified |= was_upgraded\n        # convert json to python objects\n        data = self._convert_dict([], data)\n        # convert dict to StoredDict\n        self.data = StoredDict(data, self)\n        self.data.set_parent(key='', parent=None)\n        # write file in case there was a db upgrade\n        if self.storage and self.storage.file_exists():\n            self.write_and_force_consolidation()\n\n    def load_data(self, s: str) -> dict:\n        if s == '':\n            return {}\n        try:\n            data = json.loads('[' + s + ']')\n            data, patches = data[0], data[1:]\n        except Exception:\n            if r := self.maybe_load_ast_data(s):\n                data, patches = r, []\n            elif r := self.maybe_load_incomplete_data(s):\n                data, patches = r, []\n            else:\n                raise WalletFileException(\"Cannot read wallet file. (parsing failed)\")\n        if not isinstance(data, dict):\n            raise WalletFileException(\"Malformed wallet file (not dict)\")\n        if patches:\n            # apply patches\n            self.logger.info('found %d patches'%len(patches))\n            patch = jsonpatch.JsonPatch(patches)\n            data = patch.apply(data)\n            self.set_modified(True)\n        return data\n\n    def maybe_load_ast_data(self, s):\n        \"\"\" for old wallets \"\"\"\n        try:\n            import ast\n            d = ast.literal_eval(s)\n            labels = d.get('labels', {})\n        except Exception as e:\n            return\n        data = {}\n        for key, value in d.items():\n            try:\n                json.dumps(key)\n                json.dumps(value)\n            except Exception:\n                self.logger.info(f'Failed to convert label to json format: {key}')\n                continue\n            data[key] = value\n        return data\n\n    def maybe_load_incomplete_data(self, s):\n        n = s.count('{') - s.count('}')\n        i = len(s)\n        while n > 0 and i > 0:\n            i = i - 1\n            if s[i] == '{':\n                n = n - 1\n            if s[i] == '}':\n                n = n + 1\n            if n == 0:\n                s = s[0:i]\n                assert s[-2:] == ',\\n'\n                self.logger.info('found incomplete data {s[i:]}')\n                return self.load_data(s[0:-2])\n\n    def set_modified(self, b):\n        with self.lock:\n            self._modified = b\n\n    def modified(self):\n        return self._modified\n\n    @locked\n    def add_patch(self, patch):\n        self.pending_changes.append(json.dumps(patch, cls=self.encoder))\n        self.set_modified(True)\n\n    def add(self, path, key: _FLEX_KEY, value) -> None:\n        assert isinstance(key, _FLEX_KEY), repr(key)\n        self.add_patch({'op': 'add', 'path': key_path(path, key), 'value': value})\n\n    def replace(self, path, key: _FLEX_KEY, value) -> None:\n        assert isinstance(key, _FLEX_KEY), repr(key)\n        self.add_patch({'op': 'replace', 'path': key_path(path, key), 'value': value})\n\n    def remove(self, path, key: _FLEX_KEY) -> None:\n        assert isinstance(key, _FLEX_KEY), repr(key)\n        self.add_patch({'op': 'remove', 'path': key_path(path, key)})\n\n    @locked\n    def get(self, key, default=None):\n        v = self.data.get(key)\n        if v is None:\n            v = default\n        return v\n\n    @modifier\n    def put(self, key, value):\n        try:\n            json.dumps(key, cls=self.encoder)\n            json.dumps(value, cls=self.encoder)\n        except Exception:\n            self.logger.info(f\"json error: cannot save {repr(key)} ({repr(value)})\")\n            return False\n        if value is not None:\n            if self.data.get(key) != value:\n                self.data[key] = copy.deepcopy(value)\n                return True\n        elif key in self.data:\n            self.data.pop(key)\n            return True\n        return False\n\n    @locked\n    def get_dict(self, name) -> dict:\n        # Warning: interacts un-intuitively with 'put': certain parts\n        # of 'data' will have pointers saved as separate variables.\n        if name not in self.data:\n            self.data[name] = {}\n        return self.data[name]\n\n    @locked\n    def get_stored_item(self, key, default) -> dict:\n        if key not in self.data:\n            self.data[key] = default\n        return self.data[key]\n\n    @locked\n    def dump(self, *, human_readable: bool = True) -> str:\n        \"\"\"Serializes the DB as a string.\n        'human_readable': makes the json indented and sorted, but this is ~2x slower\n        \"\"\"\n        return json.dumps(\n            self.data,\n            indent=4 if human_readable else None,\n            sort_keys=bool(human_readable),\n            cls=self.encoder,\n        )\n\n    def _should_convert_to_stored_dict(self, key) -> bool:\n        return True\n\n    def _convert_dict_key(self, path: List[str]) -> _FLEX_KEY:\n        \"\"\"Maybe convert key from str to python type (typically int or IntEnum)\"\"\"\n        assert all(isinstance(x, str) for x in path), repr(path)\n        key = path[-1]\n        parent_key = path[-2] if len(path) > 1 else None\n        gp_key = path[-3] if len(path) > 2 else None\n        if parent_key and parent_key in registered_dict_keys:\n            convert_key = registered_dict_keys[parent_key]\n        elif gp_key and gp_key in registered_parent_keys:\n            convert_key = registered_parent_keys.get(gp_key)\n        else:\n            convert_key = None\n        if convert_key:\n            key = convert_key(key)\n        assert isinstance(key, _FLEX_KEY), f\"unexpected type for {key=!r} at {path=}\"\n        return key\n\n    def _convert_dict_value(self, path: List[str], v) -> Any:\n        assert all(isinstance(x, str) for x in path), repr(path)\n        key = path[-1]\n        if key in registered_dicts:\n            constructor, _type = registered_dicts[key]\n            if _type == dict:\n                v = dict((k, constructor(**x)) for k, x in v.items())\n            elif _type == tuple:\n                v = dict((k, constructor(*x)) for k, x in v.items())\n            else:\n                v = dict((k, constructor(x)) for k, x in v.items())\n        elif key in registered_names:\n            constructor, _type = registered_names[key]\n            if _type == dict:\n                v = constructor(**v)\n            else:\n                v = constructor(v)\n        if isinstance(v, dict):\n            v = self._convert_dict(path, v)\n        return v\n\n    def _convert_dict(self, path: List[str], data: dict):\n        # recursively convert json dict to StoredDict\n        assert all(isinstance(x, str) for x in path), repr(path)\n        d = {}\n        for k, v in list(data.items()):\n            child_path = path + [k]\n            k = self._convert_dict_key(child_path)\n            v = self._convert_dict_value(child_path, v)\n            d[k] = v\n        return d\n\n    @locked\n    def write(self):\n        if self.storage.should_do_full_write_next():\n            self.write_and_force_consolidation()\n        else:\n            self._append_pending_changes()\n\n    @locked\n    def _append_pending_changes(self):\n        if threading.current_thread().daemon:\n            raise Exception('daemon thread cannot write db')\n        if not self.pending_changes:\n            self.logger.info('no pending changes')\n            return\n        self.logger.info(f'appending {len(self.pending_changes)} pending changes')\n        s = ''.join([',\\n' + x for x in self.pending_changes])\n        self.storage.append(s)\n        self.pending_changes = []\n\n    @locked\n    @profiler\n    def write_and_force_consolidation(self):\n        if threading.current_thread().daemon:\n            raise Exception('daemon thread cannot write db')\n        if not self.modified():\n            return\n        json_str = self.dump(human_readable=not self.storage.is_encrypted())\n        self.storage.write(json_str)\n        self.pending_changes = []\n        self.set_modified(False)\n"
  },
  {
    "path": "electrum/keystore.py",
    "content": "#!/usr/bin/env python2\n# -*- mode: python -*-\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2016  The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom unicodedata import normalize\nimport hashlib\nimport re\nimport copy\nfrom typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List, NamedTuple, Any, Type\nfrom functools import wraps\nfrom abc import ABC, abstractmethod\n\nimport electrum_ecc as ecc\nfrom electrum_ecc import string_to_number\n\nfrom . import bitcoin, constants, bip32\nfrom .bitcoin import deserialize_privkey, serialize_privkey, BaseDecodeError\nfrom .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, TxInput\nfrom .bip32 import (convert_bip32_strpath_to_intpath, BIP32_PRIME,\n                    is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation,\n                    convert_bip32_intpath_to_strpath, is_xkey_consistent_with_key_origin_info,\n                    KeyOriginInfo)\nfrom .descriptor import PubkeyProvider\nfrom . import crypto\nfrom .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST,\n                     SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160,\n                     CiphertextFormatError)\nfrom .util import (InvalidPassword, WalletFileException,\n                   BitcoinException, bfh, inv_dict, is_hex_str)\nfrom .mnemonic import Mnemonic, Wordlist, calc_seed_type, is_seed\nfrom .plugin import run_hook\nfrom .logging import Logger\nfrom .lrucache import LRUCache\n\nif TYPE_CHECKING:\n    from .gui.common_qt.util import TaskThread\n    from .hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase\n    from .wallet_db import WalletDB\n    from .plugin import Device\n\n\nclass CannotDerivePubkey(Exception): pass\nclass ScriptTypeNotSupported(Exception): pass\n\n\ndef also_test_none_password(check_password_fn):\n    \"\"\"Decorator for check_password, simply to give a friendlier exception if\n    check_password(x) is called on a keystore that does not have a password set.\n    \"\"\"\n    @wraps(check_password_fn)\n    def wrapper(self: 'Software_KeyStore', *args):\n        password = args[0]\n        try:\n            return check_password_fn(self, password)\n        except (CiphertextFormatError, InvalidPassword) as e:\n            if password is not None:\n                try:\n                    check_password_fn(self, None)\n                except Exception:\n                    pass\n                else:\n                    raise InvalidPassword(\"password given but keystore has no password\") from e\n            raise\n    return wrapper\n\n\nclass KeyStore(Logger, ABC):\n    type: str\n\n    def __init__(self):\n        Logger.__init__(self)\n        self.is_requesting_to_be_rewritten_to_wallet_file = False  # type: bool\n\n    def has_seed(self) -> bool:\n        return False\n\n    def is_watching_only(self) -> bool:\n        return False\n\n    def can_import(self) -> bool:\n        return False\n\n    def get_type_text(self) -> str:\n        return f'{self.type}'\n\n    @abstractmethod\n    def may_have_password(self) -> bool:\n        \"\"\"Returns whether the keystore can be encrypted with a password.\"\"\"\n        pass\n\n    def _get_tx_derivations(self, tx: 'PartialTransaction') -> Dict[bytes, Union[Sequence[int], str]]:\n        keypairs = {}\n        for txin in tx.inputs():\n            keypairs.update(self._get_txin_derivations(txin))\n        return keypairs\n\n    def _get_txin_derivations(self, txin: 'PartialTxInput') -> Dict[bytes, Union[Sequence[int], str]]:\n        if txin.is_complete():\n            return {}\n        keypairs = {}\n        for pubkey in txin.pubkeys:\n            if pubkey in txin.sigs_ecdsa:\n                # this pubkey already signed\n                continue\n            derivation = self.get_pubkey_derivation(pubkey, txin)\n            if not derivation:\n                continue\n            keypairs[pubkey] = derivation\n        return keypairs\n\n    def can_sign(self, tx: 'Transaction', *, ignore_watching_only: bool = False) -> bool:\n        \"\"\"Returns whether this keystore could sign *something* in this tx.\"\"\"\n        if not ignore_watching_only and self.is_watching_only():\n            return False\n        if not isinstance(tx, PartialTransaction):\n            return False\n        return bool(self._get_tx_derivations(tx))\n\n    def can_sign_txin(self, txin: 'TxInput', *, ignore_watching_only: bool = False) -> bool:\n        \"\"\"Returns whether this keystore could sign this txin.\"\"\"\n        if not ignore_watching_only and self.is_watching_only():\n            return False\n        if not isinstance(txin, PartialTxInput):\n            return False\n        return bool(self._get_txin_derivations(txin))\n\n    def ready_to_sign(self) -> bool:\n        return not self.is_watching_only()\n\n    @abstractmethod\n    def dump(self) -> dict[str, Any]:\n        pass\n\n    @abstractmethod\n    def is_deterministic(self) -> bool:\n        pass\n\n    @abstractmethod\n    def sign_message(\n            self,\n            sequence: 'AddressIndexGeneric',\n            message: str,\n            password,\n            *,\n            script_type: Optional[str] = None,\n    ) -> bytes:\n        pass\n\n    @abstractmethod\n    def decrypt_message(self, sequence: 'AddressIndexGeneric', message, password) -> bytes:\n        pass\n\n    @abstractmethod\n    def sign_transaction(self, tx: 'PartialTransaction', password) -> None:\n        pass\n\n    @abstractmethod\n    def get_pubkey_derivation(self, pubkey: bytes,\n                              txinout: Union['PartialTxInput', 'PartialTxOutput'],\n                              *, only_der_suffix=True) \\\n            -> Union[Sequence[int], str, None]:\n        \"\"\"Returns either a derivation int-list if the pubkey can be HD derived from this keystore,\n        the pubkey itself (hex) if the pubkey belongs to the keystore but not HD derived,\n        or None if the pubkey is unrelated.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]:\n        pass\n\n    def find_my_pubkey_in_txinout(\n            self, txinout: Union['PartialTxInput', 'PartialTxOutput'],\n            *, only_der_suffix: bool = False\n    ) -> Tuple[Optional[bytes], Optional[List[int]]]:\n        # note: we assume that this cosigner only has one pubkey in this txin/txout\n        for pubkey in txinout.bip32_paths:\n            path = self.get_pubkey_derivation(pubkey, txinout, only_der_suffix=only_der_suffix)\n            if path and not isinstance(path, (str, bytes)):\n                return pubkey, list(path)\n        return None, None\n\n    def can_have_deterministic_lightning_xprv(self) -> bool:\n        return False\n\n    def has_support_for_slip_19_ownership_proofs(self) -> bool:\n        return False\n\n    def add_slip_19_ownership_proofs_to_tx(self, tx: 'PartialTransaction', *, password) -> None:\n        raise NotImplementedError()\n\n\nclass Software_KeyStore(KeyStore):\n\n    def __init__(self, d: dict):\n        KeyStore.__init__(self)\n        self.pw_hash_version = d.get('pw_hash_version', 1)\n        if self.pw_hash_version not in SUPPORTED_PW_HASH_VERSIONS:\n            raise UnsupportedPasswordHashVersion(self.pw_hash_version)\n\n    def may_have_password(self):\n        return not self.is_watching_only()\n\n    def sign_message(self, sequence, message, password, *, script_type=None) -> bytes:\n        privkey, compressed = self.get_private_key(sequence, password)\n        key = ecc.ECPrivkey(privkey)\n        return bitcoin.ecdsa_sign_usermessage(key, message, is_compressed=compressed)\n\n    def decrypt_message(self, sequence, message, password) -> bytes:\n        privkey, compressed = self.get_private_key(sequence, password)\n        ec = ecc.ECPrivkey(privkey)\n        decrypted = crypto.ecies_decrypt_message(ec, message)\n        return decrypted\n\n    def sign_transaction(self, tx, password):\n        if self.is_watching_only():\n            return\n        # Raise if password is not correct.\n        self.check_password(password)\n        # Add private keys\n        keypairs = {}\n        pubkey_to_deriv_map = self._get_tx_derivations(tx)\n        for pubkey, deriv in pubkey_to_deriv_map.items():\n            privkey, is_compressed = self.get_private_key(deriv, password)\n            keypairs[pubkey] = privkey\n        # Sign\n        if keypairs:\n            tx.sign(keypairs)\n\n    @abstractmethod\n    def update_password(self, old_password, new_password) -> None:\n        pass\n\n    @abstractmethod\n    def check_password(self, password: Optional[str]) -> None:\n        \"\"\"Raises InvalidPassword if password is not correct\"\"\"\n        pass\n\n    @abstractmethod\n    def get_private_key(self, sequence: 'AddressIndexGeneric', password) -> Tuple[bytes, bool]:\n        \"\"\"Returns (privkey, is_compressed)\"\"\"\n        pass\n\n\nclass Imported_KeyStore(Software_KeyStore):\n    # keystore for imported private keys\n\n    type = 'imported'\n\n    def __init__(self, d: dict):\n        Software_KeyStore.__init__(self, d)\n        self.keypairs = d.get('keypairs', {})  # type: Dict[str, str]\n\n    def is_deterministic(self):\n        return False\n\n    def dump(self):\n        return {\n            'type': self.type,\n            'keypairs': self.keypairs,\n            'pw_hash_version': self.pw_hash_version,\n        }\n\n    def can_import(self):\n        return True\n\n    @also_test_none_password\n    def check_password(self, password):\n        pubkey = list(self.keypairs.keys())[0]\n        self.get_private_key(pubkey, password)\n\n    def import_privkey(self, sec: str, password) -> Tuple[str, str]:\n        txin_type, privkey, compressed = deserialize_privkey(sec)\n        pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)\n        # re-serialize the key so the internal storage format is consistent\n        serialized_privkey = serialize_privkey(\n            privkey, compressed, txin_type, internal_use=True)\n        # NOTE: if the same pubkey is reused for multiple addresses (script types),\n        # there will only be one pubkey-privkey pair for it in self.keypairs,\n        # and the privkey will encode a txin_type but that txin_type cannot be trusted.\n        # Removing keys complicates this further.\n        self.keypairs[pubkey] = pw_encode(serialized_privkey, password, version=self.pw_hash_version)\n        return txin_type, pubkey\n\n    def delete_imported_key(self, key: str) -> None:\n        self.keypairs.pop(key)\n\n    def get_private_key(self, pubkey: str, password):\n        sec = pw_decode(self.keypairs[pubkey], password, version=self.pw_hash_version)\n        try:\n            txin_type, privkey, compressed = deserialize_privkey(sec)\n        except BaseDecodeError as e:\n            raise InvalidPassword() from e\n        if pubkey != ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed):\n            raise InvalidPassword()\n        return privkey, compressed\n\n    def get_pubkey_derivation(self, pubkey, txin, *, only_der_suffix=True):\n        if pubkey.hex() in self.keypairs:\n            return pubkey.hex()\n        return None\n\n    def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]:\n        if sequence in self.keypairs:\n            return PubkeyProvider(\n                origin=None,\n                pubkey=sequence,\n                deriv_path=None,\n            )\n        return None\n\n    def update_password(self, old_password, new_password):\n        self.check_password(old_password)\n        if new_password == '':\n            new_password = None\n        for k, v in self.keypairs.items():\n            b = pw_decode(v, old_password, version=self.pw_hash_version)\n            c = pw_encode(b, new_password, version=PW_HASH_VERSION_LATEST)\n            self.keypairs[k] = c\n        self.pw_hash_version = PW_HASH_VERSION_LATEST\n\n\nclass Deterministic_KeyStore(Software_KeyStore):\n\n    def __init__(self, d: dict):\n        Software_KeyStore.__init__(self, d)\n        self.seed = d.get('seed', '')  # only electrum seeds\n        self.passphrase = d.get('passphrase', '')\n        self._seed_type = d.get('seed_type', None)  # only electrum seeds\n\n    def is_deterministic(self):\n        return True\n\n    def dump(self):\n        d = {\n            'type': self.type,\n            'pw_hash_version': self.pw_hash_version,\n        }\n        if self.seed:\n            d['seed'] = self.seed\n        if self.passphrase:\n            d['passphrase'] = self.passphrase\n        if self._seed_type:\n            d['seed_type'] = self._seed_type\n        return d\n\n    def has_seed(self):\n        return bool(self.seed)\n\n    def get_seed_type(self) -> Optional[str]:\n        return self._seed_type\n\n    def is_watching_only(self):\n        return not self.has_seed()\n\n    @abstractmethod\n    def format_seed(self, seed: str) -> str:\n        pass\n\n    def add_seed(self, seed: str) -> None:\n        if self.seed:\n            raise Exception(\"a seed exists\")\n        self.seed = self.format_seed(seed)\n        self._seed_type = calc_seed_type(seed) or None\n\n    def get_seed(self, password) -> str:\n        if not self.has_seed():\n            raise Exception(\"This wallet has no seed words\")\n        return pw_decode(self.seed, password, version=self.pw_hash_version)\n\n    def get_passphrase(self, password) -> str:\n        if self.passphrase:\n            return pw_decode(self.passphrase, password, version=self.pw_hash_version)\n        else:\n            return ''\n\n\nclass MasterPublicKeyMixin(ABC):\n\n    def __init__(self):\n        self._pubkey_cache = LRUCache(maxsize=10**4)  # type: LRUCache[Sequence[int], bytes]  # path->pubkey\n\n    @abstractmethod\n    def get_master_public_key(self) -> str:\n        pass\n\n    @abstractmethod\n    def get_derivation_prefix(self) -> Optional[str]:\n        \"\"\"Returns to bip32 path from some root node to self.xpub\n        Note that the return value might be None; if it is unknown.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_root_fingerprint(self) -> Optional[str]:\n        \"\"\"Returns the bip32 fingerprint of the top level node.\n        This top level node is the node at the beginning of the derivation prefix,\n        i.e. applying the derivation prefix to it will result self.xpub\n        Note that the return value might be None; if it is unknown.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_fp_and_derivation_to_be_used_in_partial_tx(\n            self,\n            der_suffix: Sequence[int],\n            *,\n            only_der_suffix: bool,\n    ) -> Tuple[bytes, Sequence[int]]:\n        \"\"\"Returns fingerprint and derivation path corresponding to a derivation suffix.\n        The fingerprint is either the root fp or the intermediate fp, depending on what is available\n        and 'only_der_suffix', and the derivation path is adjusted accordingly.\n        \"\"\"\n        pass\n\n    def get_key_origin_info(self) -> Optional[KeyOriginInfo]:\n        return None\n\n    def derive_pubkey(self, for_change: int, n: int) -> bytes:\n        key = (for_change, n)\n        if key not in self._pubkey_cache:\n            self._pubkey_cache[key] = self._derive_pubkey(*key)\n        return self._pubkey_cache[key]\n\n    @abstractmethod\n    def _derive_pubkey(self, for_change: int, n: int) -> bytes:\n        \"\"\"Returns pubkey at given path.\n        May raise CannotDerivePubkey.\n        \"\"\"\n        pass\n\n    def get_pubkey_derivation(\n            self,\n            pubkey: bytes,\n            txinout: Union['PartialTxInput', 'PartialTxOutput'],\n            *,\n            only_der_suffix=True,\n    ) -> Union[Sequence[int], str, None]:\n        EXPECTED_DER_SUFFIX_LEN = 2\n        def test_der_suffix_against_pubkey(der_suffix: Sequence[int], pubkey: bytes) -> bool:\n            if len(der_suffix) != EXPECTED_DER_SUFFIX_LEN:\n                return False\n            try:\n                if pubkey != self.derive_pubkey(*der_suffix):\n                    return False\n            except CannotDerivePubkey:\n                return False\n            return True\n\n        if pubkey not in txinout.bip32_paths:\n            return None\n        fp_found, path_found = txinout.bip32_paths[pubkey]\n        der_suffix = None\n        full_path = None\n        # 1. try fp against our root\n        ks_root_fingerprint_hex = self.get_root_fingerprint()\n        ks_der_prefix_str = self.get_derivation_prefix()\n        ks_der_prefix = convert_bip32_strpath_to_intpath(ks_der_prefix_str) if ks_der_prefix_str else None\n        if (ks_root_fingerprint_hex is not None and ks_der_prefix is not None and\n                fp_found.hex() == ks_root_fingerprint_hex):\n            if path_found[:len(ks_der_prefix)] == ks_der_prefix:\n                der_suffix = path_found[len(ks_der_prefix):]\n                if not test_der_suffix_against_pubkey(der_suffix, pubkey):\n                    der_suffix = None\n        # 2. try fp against our intermediate fingerprint\n        if (der_suffix is None and isinstance(self, Xpub) and\n                fp_found == self.get_bip32_node_for_xpub().calc_fingerprint_of_this_node()):\n            der_suffix = path_found\n            if not test_der_suffix_against_pubkey(der_suffix, pubkey):\n                der_suffix = None\n        # 3. hack/bruteforce: ignore fp and check pubkey anyway\n        #    This is only to resolve the following scenario/problem:\n        #    problem: if we don't know our root fp, but tx contains root fp and full path,\n        #             we will miss the pubkey (false negative match). Though it might still work\n        #             within gap limit due to tx.add_info_from_wallet overwriting the fields.\n        #             Example: keystore has intermediate xprv without root fp; tx contains root fp and full path.\n        if der_suffix is None:\n            der_suffix = path_found[-EXPECTED_DER_SUFFIX_LEN:]\n            if not test_der_suffix_against_pubkey(der_suffix, pubkey):\n                der_suffix = None\n        # if all attempts/methods failed, we give up now:\n        if der_suffix is None:\n            return None\n        if ks_der_prefix is not None:\n            full_path = ks_der_prefix + list(der_suffix)\n        return der_suffix if only_der_suffix else full_path\n\n\nclass Xpub(MasterPublicKeyMixin):\n\n    def __init__(self, *, derivation_prefix: str = None, root_fingerprint: str = None):\n        MasterPublicKeyMixin.__init__(self)\n        self.xpub = None\n        self.xpub_receive = None\n        self.xpub_change = None\n        self._xpub_bip32_node = None  # type: Optional[BIP32Node]\n\n        # \"key origin\" info (subclass should persist these):\n        self._derivation_prefix = derivation_prefix  # type: Optional[str]\n        self._root_fingerprint = root_fingerprint  # type: Optional[str]\n\n    def get_master_public_key(self):\n        return self.xpub\n\n    def get_bip32_node_for_xpub(self) -> Optional[BIP32Node]:\n        if self._xpub_bip32_node is None:\n            if self.xpub is None:\n                return None\n            self._xpub_bip32_node = BIP32Node.from_xkey(self.xpub)\n        return self._xpub_bip32_node\n\n    def get_derivation_prefix(self) -> Optional[str]:\n        if self._derivation_prefix is None:\n            return None\n        return normalize_bip32_derivation(self._derivation_prefix)\n\n    def get_root_fingerprint(self) -> Optional[str]:\n        return self._root_fingerprint\n\n    def get_fp_and_derivation_to_be_used_in_partial_tx(\n            self,\n            der_suffix: Sequence[int],\n            *,\n            only_der_suffix: bool,\n    ) -> Tuple[bytes, Sequence[int]]:\n        fingerprint_hex = self.get_root_fingerprint()\n        der_prefix_str = self.get_derivation_prefix()\n        if not only_der_suffix and fingerprint_hex is not None and der_prefix_str is not None:\n            # use root fp, and true full path\n            fingerprint_bytes = bfh(fingerprint_hex)\n            der_prefix_ints = convert_bip32_strpath_to_intpath(der_prefix_str)\n        else:\n            # use intermediate fp, and claim der suffix is the full path\n            fingerprint_bytes = self.get_bip32_node_for_xpub().calc_fingerprint_of_this_node()\n            der_prefix_ints = convert_bip32_strpath_to_intpath('m')\n        der_full = der_prefix_ints + list(der_suffix)\n        return fingerprint_bytes, der_full\n\n    def get_xpub_to_be_used_in_partial_tx(self, *, only_der_suffix: bool) -> str:\n        assert self.xpub\n        fp_bytes, der_full = self.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[],\n                                                                                 only_der_suffix=only_der_suffix)\n        bip32node = self.get_bip32_node_for_xpub()\n        depth = len(der_full)\n        child_number_int = der_full[-1] if len(der_full) >= 1 else 0\n        child_number_bytes = child_number_int.to_bytes(length=4, byteorder=\"big\")\n        fingerprint = bytes(4) if depth == 0 else bip32node.fingerprint\n        bip32node = bip32node._replace(\n            depth=depth,\n            fingerprint=fingerprint,\n            child_number=child_number_bytes,\n            # only put plain xpubs (not ypub/zpub) in PSBTs:\n            xtype=\"standard\",\n        )\n        return bip32node.to_xpub()\n\n    def get_key_origin_info(self) -> Optional[KeyOriginInfo]:\n        fp_bytes, der_full = self.get_fp_and_derivation_to_be_used_in_partial_tx(\n            der_suffix=[], only_der_suffix=False)\n        origin = KeyOriginInfo(fingerprint=fp_bytes, path=der_full)\n        return origin\n\n    def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]:\n        strpath = convert_bip32_intpath_to_strpath(sequence)\n        strpath = strpath[1:]  # cut leading \"m\"\n        bip32node = self.get_bip32_node_for_xpub()\n        return PubkeyProvider(\n            origin=self.get_key_origin_info(),\n            pubkey=bip32node._replace(xtype=\"standard\").to_xkey(),\n            deriv_path=strpath,\n        )\n\n    def add_key_origin_from_root_node(self, *, derivation_prefix: str, root_node: BIP32Node) -> None:\n        assert self.xpub\n        # try to derive ourselves from what we were given\n        child_node1 = root_node.subkey_at_private_derivation(derivation_prefix)\n        child_pubkey_bytes1 = child_node1.eckey.get_public_key_bytes(compressed=True)\n        child_node2 = self.get_bip32_node_for_xpub()\n        child_pubkey_bytes2 = child_node2.eckey.get_public_key_bytes(compressed=True)\n        if child_pubkey_bytes1 != child_pubkey_bytes2:\n            raise Exception(\"(xpub, derivation_prefix, root_node) inconsistency\")\n        self.add_key_origin(derivation_prefix=derivation_prefix,\n                            root_fingerprint=root_node.calc_fingerprint_of_this_node().hex().lower())\n\n    def add_key_origin(self, *, derivation_prefix: str = None, root_fingerprint: str = None) -> None:\n        assert self.xpub\n        if not (root_fingerprint is None or (is_hex_str(root_fingerprint) and len(root_fingerprint) == 8)):\n            raise Exception(\"root fp must be 8 hex characters\")\n        derivation_prefix = normalize_bip32_derivation(derivation_prefix)\n        if not is_xkey_consistent_with_key_origin_info(self.xpub,\n                                                       derivation_prefix=derivation_prefix,\n                                                       root_fingerprint=root_fingerprint):\n            raise Exception(\"xpub inconsistent with provided key origin info\")\n        if root_fingerprint is not None:\n            self._root_fingerprint = root_fingerprint\n        if derivation_prefix is not None:\n            self._derivation_prefix = derivation_prefix\n        self.is_requesting_to_be_rewritten_to_wallet_file = True\n\n    def _derive_pubkey(self, for_change: int, n: int) -> bytes:\n        for_change = int(for_change)\n        if for_change not in (0, 1):\n            raise CannotDerivePubkey(\"forbidden path\")\n        xpub = self.xpub_change if for_change else self.xpub_receive\n        if xpub is None:\n            rootnode = self.get_bip32_node_for_xpub()\n            xpub = rootnode.subkey_at_public_derivation((for_change,)).to_xpub()\n            if for_change:\n                self.xpub_change = xpub\n            else:\n                self.xpub_receive = xpub\n        return self.get_pubkey_from_xpub(xpub, (n,))\n\n    @classmethod\n    def get_pubkey_from_xpub(cls, xpub: str, sequence) -> bytes:\n        node = BIP32Node.from_xkey(xpub).subkey_at_public_derivation(sequence)\n        return node.eckey.get_public_key_bytes(compressed=True)\n\n\nclass BIP32_KeyStore(Xpub, Deterministic_KeyStore):\n\n    type = 'bip32'\n\n    def __init__(self, d: dict):\n        Xpub.__init__(self, derivation_prefix=d.get('derivation'), root_fingerprint=d.get('root_fingerprint'))\n        Deterministic_KeyStore.__init__(self, d)\n        self.xpub = d.get('xpub')\n        self.xprv = d.get('xprv')\n\n    def watching_only_keystore(self):\n        return BIP32_KeyStore({\n            'xpub': self.xpub,\n            'root_fingerprint': self.get_root_fingerprint(),\n            'derivation': self.get_derivation_prefix(),\n        })\n\n    def format_seed(self, seed):\n        return ' '.join(seed.split())\n\n    def dump(self):\n        d = Deterministic_KeyStore.dump(self)\n        d['xpub'] = self.xpub\n        d['xprv'] = self.xprv\n        d['derivation'] = self.get_derivation_prefix()\n        d['root_fingerprint'] = self.get_root_fingerprint()\n        return d\n\n    def get_master_private_key(self, password) -> str:\n        return pw_decode(self.xprv, password, version=self.pw_hash_version)\n\n    @also_test_none_password\n    def check_password(self, password):\n        xprv = pw_decode(self.xprv, password, version=self.pw_hash_version)\n        try:\n            bip32node = BIP32Node.from_xkey(xprv)\n        except BaseDecodeError as e:\n            raise InvalidPassword() from e\n        if bip32node.chaincode != self.get_bip32_node_for_xpub().chaincode:\n            raise InvalidPassword()\n\n    def update_password(self, old_password, new_password):\n        self.check_password(old_password)\n        if new_password == '':\n            new_password = None\n        if self.has_seed():\n            decoded = self.get_seed(old_password)\n            self.seed = pw_encode(decoded, new_password, version=PW_HASH_VERSION_LATEST)\n        if self.passphrase:\n            decoded = self.get_passphrase(old_password)\n            self.passphrase = pw_encode(decoded, new_password, version=PW_HASH_VERSION_LATEST)\n        if self.xprv is not None:\n            b = pw_decode(self.xprv, old_password, version=self.pw_hash_version)\n            self.xprv = pw_encode(b, new_password, version=PW_HASH_VERSION_LATEST)\n        self.pw_hash_version = PW_HASH_VERSION_LATEST\n\n    def is_watching_only(self):\n        return self.xprv is None\n\n    def add_xpub(self, xpub: str) -> None:\n        assert is_xpub(xpub)\n        self.xpub = xpub\n        root_fingerprint, derivation_prefix = bip32.root_fp_and_der_prefix_from_xkey(xpub)\n        self.add_key_origin(derivation_prefix=derivation_prefix, root_fingerprint=root_fingerprint)\n\n    def add_xprv(self, xprv: str) -> None:\n        assert is_xprv(xprv)\n        self.xprv = xprv\n        self.add_xpub(bip32.xpub_from_xprv(xprv))\n\n    def add_xprv_from_seed(self, bip32_seed: bytes, *, xtype: str, derivation: str) -> None:\n        rootnode = BIP32Node.from_rootseed(bip32_seed, xtype=xtype)\n        node = rootnode.subkey_at_private_derivation(derivation)\n        self.add_xprv(node.to_xprv())\n        self.add_key_origin_from_root_node(derivation_prefix=derivation, root_node=rootnode)\n\n    def get_private_key(self, sequence: Sequence[int], password):\n        xprv = self.get_master_private_key(password)\n        node = BIP32Node.from_xkey(xprv).subkey_at_private_derivation(sequence)\n        pk = node.eckey.get_secret_bytes()\n        return pk, True\n\n    def can_have_deterministic_lightning_xprv(self):\n        if (self.get_seed_type() == 'segwit'\n                and self.get_bip32_node_for_xpub().xtype == 'p2wpkh'):\n            return True\n        return False\n\n    def get_lightning_xprv(self, password) -> str:\n        assert self.can_have_deterministic_lightning_xprv()\n        xprv = self.get_master_private_key(password)\n        rootnode = BIP32Node.from_xkey(xprv)\n        node = rootnode.subkey_at_private_derivation(\"m/67'/\")\n        return node.to_xprv()\n\nclass Old_KeyStore(MasterPublicKeyMixin, Deterministic_KeyStore):\n\n    type = 'old'\n\n    def __init__(self, d: dict):\n        MasterPublicKeyMixin.__init__(self)\n        Deterministic_KeyStore.__init__(self, d)\n        self.mpk = d.get('mpk')  # type: Optional[str]\n        self._root_fingerprint = None\n\n    def watching_only_keystore(self):\n        return Old_KeyStore({'mpk': self.mpk})\n\n    def _get_hex_seed(self, password) -> str:\n        if not is_hex_str(self.seed) and password is None:\n            raise InvalidPassword()\n        hex_str = pw_decode(self.seed, password, version=self.pw_hash_version)\n        assert is_hex_str(hex_str), f\"expected hex str, got {type(hex_str)} with {len(hex_str)=}\"\n        return hex_str\n\n    def dump(self):\n        d = Deterministic_KeyStore.dump(self)\n        d['mpk'] = self.mpk\n        return d\n\n    def add_seed(self, seed):\n        Deterministic_KeyStore.add_seed(self, seed)\n        hex_seed = self._get_hex_seed(None)\n        self.mpk = self.mpk_from_seed(hex_seed)\n\n    def add_master_public_key(self, mpk: str) -> None:\n        self.mpk = mpk\n\n    def format_seed(self, seed):\n        \"\"\"Returns seed in hex format.\n\n        seed: either in hex or as mnemonic words\n        \"\"\"\n        from . import old_mnemonic, mnemonic\n        seed = mnemonic.normalize_text(seed)\n        # see if seed was entered as hex\n        if seed:\n            try:\n                bfh(seed)\n                return str(seed)\n            except Exception:\n                pass\n        words = seed.split()\n        seed = old_mnemonic.mn_decode(words)\n        if not seed:\n            raise Exception(\"Invalid seed\")\n        return seed\n\n    def get_seed(self, password):\n        from . import old_mnemonic\n        hex_seed = self._get_hex_seed(password)\n        return ' '.join(old_mnemonic.mn_encode(hex_seed))\n\n    @classmethod\n    def mpk_from_seed(cls, hex_seed: str) -> str:\n        secexp = cls.stretch_key(hex_seed)\n        privkey = ecc.ECPrivkey.from_secret_scalar(secexp)\n        return privkey.get_public_key_hex(compressed=False)[2:]\n\n    @classmethod\n    def stretch_key(cls, hex_seed: str) -> int:\n        assert is_hex_str(hex_seed), f\"expected hex str, got {type(hex_seed)} with {len(hex_seed)=}\"\n        encoded_hex_seed = hex_seed.encode('ascii')\n        x = encoded_hex_seed\n        for i in range(100000):\n            x = hashlib.sha256(x + encoded_hex_seed).digest()\n        return string_to_number(x)\n\n    @classmethod\n    def get_sequence(cls, mpk: str, for_change: int, n: int) -> int:\n        return string_to_number(sha256d((\"%d:%d:\"%(n, for_change)).encode('ascii') + bfh(mpk)))\n\n    @classmethod\n    def get_pubkey_from_mpk(cls, mpk: str, for_change: int, n: int) -> bytes:\n        z = cls.get_sequence(mpk, for_change, n)\n        master_public_key = ecc.ECPubkey(bfh('04'+mpk))\n        public_key = master_public_key + z*ecc.GENERATOR\n        return public_key.get_public_key_bytes(compressed=False)\n\n    def _derive_pubkey(self, for_change, n) -> bytes:\n        for_change = int(for_change)\n        if for_change not in (0, 1):\n            raise CannotDerivePubkey(\"forbidden path\")\n        return self.get_pubkey_from_mpk(self.mpk, for_change, n)\n\n    def _get_private_key_from_stretched_exponent(self, for_change: int, n: int, secexp: int) -> bytes:\n        secexp = (secexp + self.get_sequence(self.mpk, for_change, n)) % ecc.CURVE_ORDER\n        pk = int.to_bytes(secexp, length=32, byteorder='big', signed=False)\n        return pk\n\n    def get_private_key(self, sequence: Sequence[int], password):\n        hex_seed = self._get_hex_seed(password)\n        secexp = self.stretch_key(hex_seed)\n        self._check_seed(hex_seed, secexp=secexp)\n        for_change, n = sequence\n        assert isinstance(for_change, int), type(for_change)\n        assert isinstance(n, int), type(n)\n        pk = self._get_private_key_from_stretched_exponent(for_change, n, secexp)\n        return pk, False\n\n    def _check_seed(self, hex_seed: str, *, secexp: int = None) -> None:\n        if secexp is None:\n            secexp = self.stretch_key(hex_seed)\n        master_private_key = ecc.ECPrivkey.from_secret_scalar(secexp)\n        master_public_key = master_private_key.get_public_key_bytes(compressed=False)[1:]\n        if master_public_key != bfh(self.mpk):\n            raise InvalidPassword()\n\n    @also_test_none_password\n    def check_password(self, password):\n        hex_seed = self._get_hex_seed(password)\n        self._check_seed(hex_seed)\n\n    def get_master_public_key(self):\n        return self.mpk\n\n    def get_derivation_prefix(self) -> str:\n        return 'm'\n\n    def get_root_fingerprint(self) -> str:\n        if self._root_fingerprint is None:\n            master_public_key = ecc.ECPubkey(bfh('04'+self.mpk))\n            xfp = hash_160(master_public_key.get_public_key_bytes(compressed=True))[0:4]\n            self._root_fingerprint = xfp.hex().lower()\n        return self._root_fingerprint\n\n    def get_fp_and_derivation_to_be_used_in_partial_tx(\n            self,\n            der_suffix: Sequence[int],\n            *,\n            only_der_suffix: bool,\n    ) -> Tuple[bytes, Sequence[int]]:\n        fingerprint_hex = self.get_root_fingerprint()\n        der_prefix_str = self.get_derivation_prefix()\n        fingerprint_bytes = bfh(fingerprint_hex)\n        der_prefix_ints = convert_bip32_strpath_to_intpath(der_prefix_str)\n        der_full = der_prefix_ints + list(der_suffix)\n        return fingerprint_bytes, der_full\n\n    def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]:\n        return PubkeyProvider(\n            origin=None,\n            pubkey=self.derive_pubkey(*sequence).hex(),\n            deriv_path=None,\n        )\n\n    def update_password(self, old_password, new_password):\n        self.check_password(old_password)\n        if new_password == '':\n            new_password = None\n        if self.has_seed():\n            decoded = pw_decode(self.seed, old_password, version=self.pw_hash_version)\n            self.seed = pw_encode(decoded, new_password, version=PW_HASH_VERSION_LATEST)\n        self.pw_hash_version = PW_HASH_VERSION_LATEST\n\n\nclass Hardware_KeyStore(Xpub, KeyStore):\n    hw_type: str\n    device: str\n    plugin: 'HW_PluginBase'\n    thread: Optional['TaskThread'] = None\n\n    type = 'hardware'\n\n    def __init__(self, d):\n        Xpub.__init__(self, derivation_prefix=d.get('derivation'), root_fingerprint=d.get('root_fingerprint'))\n        KeyStore.__init__(self)\n        # Errors and other user interaction is done through the wallet's\n        # handler.  The handler is per-window and preserved across\n        # device reconnects\n        self.xpub = d.get('xpub')\n        self.label = d.get('label')  # type: Optional[str]\n        self.soft_device_id = d.get('soft_device_id')  # type: Optional[str]\n        self.handler = None  # type: Optional[HardwareHandlerBase]\n        run_hook('init_keystore', self)\n\n    def watching_only_keystore(self):\n        return BIP32_KeyStore({\n            'xpub': self.xpub,\n            'root_fingerprint': self.get_root_fingerprint(),\n            'derivation': self.get_derivation_prefix(),\n        })\n\n    def set_label(self, label: Optional[str]) -> None:\n        self.label = label\n\n    def may_have_password(self):\n        return False\n\n    def is_deterministic(self):\n        return True\n\n    def get_type_text(self) -> str:\n        return f'hw[{self.hw_type}]'\n\n    def dump(self):\n        return {\n            'type': self.type,\n            'hw_type': self.hw_type,\n            'xpub': self.xpub,\n            'derivation': self.get_derivation_prefix(),\n            'root_fingerprint': self.get_root_fingerprint(),\n            'label': self.label,\n            'soft_device_id': self.soft_device_id,\n        }\n\n    def is_watching_only(self):\n        \"\"\"The wallet is not watching-only; the user will be prompted for\n        pin and passphrase as appropriate when needed.\"\"\"\n        assert not self.has_seed()\n        return False\n\n    def get_client(\n            self,\n            force_pair: bool = True,\n            *,\n            devices: Sequence['Device'] = None,\n            allow_user_interaction: bool = True,\n    ) -> Optional['HardwareClientBase']:\n        return self.plugin.get_client(\n            self,\n            force_pair=force_pair,\n            devices=devices,\n            allow_user_interaction=allow_user_interaction,\n        )\n\n    def get_password_for_storage_encryption(self) -> str:\n        client = self.get_client()\n        return client.get_password_for_storage_encryption()\n\n    def has_usable_connection_with_device(self) -> bool:\n        # we try to create a client even if there isn't one already,\n        # but do not prompt the user if auto-select fails:\n        client = self.get_client(\n            force_pair=True,\n            allow_user_interaction=False,\n        )\n        if client is None:\n            return False\n        return client.has_usable_connection_with_device()\n\n    def ready_to_sign(self):\n        return super().ready_to_sign() and self.has_usable_connection_with_device()\n\n    def opportunistically_fill_in_missing_info_from_device(self, client: 'HardwareClientBase'):\n        assert client is not None\n        if self._root_fingerprint is None:\n            self._root_fingerprint = client.request_root_fingerprint_from_device()\n            self.is_requesting_to_be_rewritten_to_wallet_file = True\n        if self.label != client.label():\n            self.label = client.label()\n            self.is_requesting_to_be_rewritten_to_wallet_file = True\n        if self.soft_device_id != client.get_soft_device_id():\n            self.soft_device_id = client.get_soft_device_id()\n            self.is_requesting_to_be_rewritten_to_wallet_file = True\n\n    def pairing_code(self) -> Optional[str]:\n        \"\"\"Used by the DeviceMgr to keep track of paired hw devices.\"\"\"\n        if not self.soft_device_id:\n            return None\n        return f\"{self.plugin.name}/{self.soft_device_id}\"\n\n\nKeyStoreWithMPK = Union[KeyStore, MasterPublicKeyMixin]  # intersection really...\nAddressIndexGeneric = Union[Sequence[int], str]  # can be hex pubkey str\n\n\ndef bip39_normalize_passphrase(passphrase: str):\n    return normalize('NFKD', passphrase or '')\n\n\ndef bip39_to_seed(mnemonic: str, *, passphrase: Optional[str]) -> bytes:\n    import hashlib\n    passphrase = passphrase or \"\"\n    PBKDF2_ROUNDS = 2048\n    mnemonic = normalize('NFKD', ' '.join(mnemonic.split()))\n    passphrase = bip39_normalize_passphrase(passphrase)\n    return hashlib.pbkdf2_hmac('sha512', mnemonic.encode('utf-8'),\n        b'mnemonic' + passphrase.encode('utf-8'), iterations = PBKDF2_ROUNDS)\n\n\ndef bip39_is_checksum_valid(\n        mnemonic: str,\n        *,\n        wordlist: Wordlist = None,\n) -> Tuple[bool, bool]:\n    \"\"\"Test checksum of bip39 mnemonic assuming English wordlist.\n    Returns tuple (is_checksum_valid, is_wordlist_valid)\n    \"\"\"\n    words = [normalize('NFKD', word) for word in mnemonic.split()]\n    words_len = len(words)\n    if wordlist is None:\n        wordlist = Wordlist.from_file(\"english.txt\")\n    n = len(wordlist)\n    i = 0\n    words.reverse()\n    while words:\n        w = words.pop()\n        try:\n            k = wordlist.index(w)\n        except ValueError:\n            return False, False\n        i = i*n + k\n    if words_len not in [12, 15, 18, 21, 24]:\n        return False, True\n    checksum_length = 11 * words_len // 33  # num bits\n    entropy_length = 32 * checksum_length  # num bits\n    entropy = i >> checksum_length\n    checksum = i % 2**checksum_length\n    entropy_bytes = int.to_bytes(entropy, length=entropy_length//8, byteorder=\"big\")\n    hashed = int.from_bytes(sha256(entropy_bytes), byteorder=\"big\")\n    calculated_checksum = hashed >> (256 - checksum_length)\n    return checksum == calculated_checksum, True\n\n\ndef from_bip43_rootseed(\n    root_seed: bytes,\n    *,\n    derivation: str,\n    xtype: Optional[str] = None,\n):\n    k = BIP32_KeyStore({})\n    if xtype is None:\n        xtype = xtype_from_derivation(derivation)\n    k.add_xprv_from_seed(root_seed, xtype=xtype, derivation=derivation)\n    return k\n\n\nPURPOSE48_SCRIPT_TYPES = {\n    'p2wsh-p2sh': 1,  # specifically multisig\n    'p2wsh': 2,       # specifically multisig\n}\nPURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES)\n\n\ndef xtype_from_derivation(derivation: str) -> str:\n    \"\"\"Returns the script type to be used for this derivation.\"\"\"\n    bip32_indices = convert_bip32_strpath_to_intpath(derivation)\n    if len(bip32_indices) >= 1:\n        if bip32_indices[0] == 84 + BIP32_PRIME:\n            return 'p2wpkh'\n        elif bip32_indices[0] == 49 + BIP32_PRIME:\n            return 'p2wpkh-p2sh'\n        elif bip32_indices[0] == 44 + BIP32_PRIME:\n            return 'standard'\n        elif bip32_indices[0] == 45 + BIP32_PRIME:\n            return 'standard'\n\n    if len(bip32_indices) >= 4:\n        if bip32_indices[0] == 48 + BIP32_PRIME:\n            # m / purpose' / coin_type' / account' / script_type' / change / address_index\n            script_type_int = bip32_indices[3] - BIP32_PRIME\n            script_type = PURPOSE48_SCRIPT_TYPES_INV.get(script_type_int)\n            if script_type is not None:\n                return script_type\n    return 'standard'\n\n\nhw_keystores = {}  # type: Dict[str, Type[Hardware_KeyStore]]\n\ndef register_keystore(hw_type: str, constructor: Type[Hardware_KeyStore]) -> None:\n    hw_keystores[hw_type] = constructor\n\ndef hardware_keystore(d) -> Hardware_KeyStore:\n    hw_type = d['hw_type']\n    if hw_type in hw_keystores:\n        constructor = hw_keystores[hw_type]\n        return constructor(d)\n    raise WalletFileException(f'unknown hardware type: {hw_type}. '\n                              f'hw_keystores: {list(hw_keystores)}')\n\ndef load_keystore(db: 'WalletDB', name: str) -> KeyStore:\n    # deepcopy object to avoid keeping a pointer to db.data\n    # note: this is needed as type(wallet.db.get(\"keystore\")) != StoredDict\n    d = copy.deepcopy(db.get(name, {}))\n    t = d.get('type')\n    if not t:\n        raise WalletFileException(\n            'Wallet format requires update.\\n'\n            'Cannot find keystore for name {}'.format(name))\n    keystore_constructors = {ks.type: ks for ks in [Old_KeyStore, Imported_KeyStore, BIP32_KeyStore]}\n    keystore_constructors['hardware'] = hardware_keystore\n    try:\n        ks_constructor = keystore_constructors[t]\n    except KeyError:\n        raise WalletFileException(f'Unknown type {t} for keystore named {name}')\n    k = ks_constructor(d)\n    return k\n\n\ndef is_old_mpk(mpk: str) -> bool:\n    try:\n        int(mpk, 16)  # test if hex string\n    except Exception:\n        return False\n    if len(mpk) != 128:\n        return False\n    try:\n        ecc.ECPubkey(bfh('04' + mpk))\n    except Exception:\n        return False\n    return True\n\n\ndef is_address_list(text: str) -> bool:\n    parts = text.split()\n    return bool(parts) and all(bitcoin.is_address(x) for x in parts)\n\n\ndef get_private_keys(text: str, *, allow_spaces_inside_key=True, raise_on_error=False) -> Sequence[str]:\n    if allow_spaces_inside_key:  # see #1612\n        parts = text.split('\\n')\n        parts = map(lambda x: ''.join(x.split()), parts)\n        parts = list(filter(bool, parts))\n    else:\n        parts = text.split()\n    if bool(parts) and all(bitcoin.is_private_key(x, raise_on_error=raise_on_error) for x in parts):\n        return parts\n    return []\n\n\ndef is_private_key_list(text: str, *, allow_spaces_inside_key: bool = True, raise_on_error: bool = False) -> bool:\n    return bool(get_private_keys(text,\n                                 allow_spaces_inside_key=allow_spaces_inside_key,\n                                 raise_on_error=raise_on_error))\n\n\ndef is_master_key(x: str) -> bool:\n    return is_old_mpk(x) or is_bip32_key(x)\n\n\ndef is_bip32_key(x: str) -> bool:\n    return is_xprv(x) or is_xpub(x)\n\n\ndef bip44_derivation(account_id: int, bip43_purpose: int = 44) -> str:\n    coin = constants.net.BIP44_COIN_TYPE\n    der = \"m/%d'/%d'/%d'\" % (bip43_purpose, coin, int(account_id))\n    return normalize_bip32_derivation(der)\n\n\ndef purpose48_derivation(account_id: int, xtype: str) -> str:\n    # m / purpose' / coin_type' / account' / script_type' / change / address_index\n    bip43_purpose = 48\n    coin = constants.net.BIP44_COIN_TYPE\n    account_id = int(account_id)\n    script_type_int = PURPOSE48_SCRIPT_TYPES.get(xtype)\n    if script_type_int is None:\n        raise Exception('unknown xtype: {}'.format(xtype))\n    der = \"m/%d'/%d'/%d'/%d'\" % (bip43_purpose, coin, account_id, script_type_int)\n    return normalize_bip32_derivation(der)\n\n\ndef from_seed(seed: str, *, passphrase: Optional[str], for_multisig: bool = False) -> Union[BIP32_KeyStore, Old_KeyStore]:\n    passphrase = passphrase or \"\"\n    t = calc_seed_type(seed)\n    if t == 'old':\n        if passphrase:\n            raise Exception(\"'old'-type electrum seed cannot have passphrase\")\n        keystore = Old_KeyStore({})\n        keystore.add_seed(seed)\n    elif t in ['standard', 'segwit']:\n        keystore = BIP32_KeyStore({})\n        keystore.add_seed(seed)\n        keystore.passphrase = passphrase\n        bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase=passphrase)\n        if t == 'standard':\n            der = \"m/\"\n            xtype = 'standard'\n        else:\n            der = \"m/1'/\" if for_multisig else \"m/0'/\"\n            xtype = 'p2wsh' if for_multisig else 'p2wpkh'\n        keystore.add_xprv_from_seed(bip32_seed, xtype=xtype, derivation=der)\n    else:\n        raise BitcoinException('Unexpected seed type {}'.format(repr(t)))\n    return keystore\n\ndef from_private_key_list(text: str) -> Imported_KeyStore:\n    keystore = Imported_KeyStore({})\n    for x in get_private_keys(text):\n        keystore.import_privkey(x, None)\n    return keystore\n\ndef from_old_mpk(mpk: str) -> Old_KeyStore:\n    keystore = Old_KeyStore({})\n    keystore.add_master_public_key(mpk)\n    return keystore\n\ndef from_xpub(xpub: str) -> BIP32_KeyStore:\n    k = BIP32_KeyStore({})\n    k.add_xpub(xpub)\n    return k\n\ndef from_xprv(xprv: str) -> BIP32_KeyStore:\n    k = BIP32_KeyStore({})\n    k.add_xprv(xprv)\n    return k\n\ndef from_master_key(text: str) -> Union[BIP32_KeyStore, Old_KeyStore]:\n    if is_xprv(text):\n        k = from_xprv(text)\n    elif is_old_mpk(text):\n        k = from_old_mpk(text)\n    elif is_xpub(text):\n        k = from_xpub(text)\n    else:\n        raise BitcoinException('Invalid master key')\n    return k\n"
  },
  {
    "path": "electrum/lnaddr.py",
    "content": "#! /usr/bin/env python3\n# This was forked from https://github.com/rustyrussell/lightning-payencode/tree/acc16ec13a3fa1dc16c07af6ec67c261bd8aff23\n\nimport io\nimport re\nimport time\nfrom hashlib import sha256\nfrom binascii import hexlify\nfrom decimal import Decimal\nfrom typing import Optional, TYPE_CHECKING, Type, Dict, Any, Sequence, Tuple\nimport random\n\nimport electrum_ecc as ecc\n\nfrom .bitcoin import hash160_to_b58_address, b58_address_to_hash160, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC\nfrom .segwit_addr import bech32_encode, bech32_decode, CHARSET, CHARSET_INVERSE, convertbits\nfrom . import segwit_addr\nfrom . import constants\nfrom .constants import AbstractNet\nfrom .bitcoin import COIN\n\nif TYPE_CHECKING:\n    from .lnutil import LnFeatures\n\n\nclass LnInvoiceException(Exception): pass\nclass LnDecodeException(LnInvoiceException): pass\nclass LnEncodeException(LnInvoiceException): pass\n\n\n# BOLT #11:\n#\n# A writer MUST encode `amount` as a positive decimal integer with no\n# leading zeroes, SHOULD use the shortest representation possible.\ndef shorten_amount(amount):\n    \"\"\" Given an amount in bitcoin, shorten it\n    \"\"\"\n    # Convert to pico initially\n    amount = int(amount * 10**12)\n    units = ['p', 'n', 'u', 'm']\n    for unit in units:\n        if amount % 1000 == 0:\n            amount //= 1000\n        else:\n            break\n    else:\n        unit = ''\n    return str(amount) + unit\n\ndef unshorten_amount(amount) -> Decimal:\n    \"\"\" Given a shortened amount, convert it into a decimal\n    \"\"\"\n    # BOLT #11:\n    # The following `multiplier` letters are defined:\n    #\n    #* `m` (milli): multiply by 0.001\n    #* `u` (micro): multiply by 0.000001\n    #* `n` (nano): multiply by 0.000000001\n    #* `p` (pico): multiply by 0.000000000001\n    units = {\n        'p': 10**12,\n        'n': 10**9,\n        'u': 10**6,\n        'm': 10**3,\n    }\n    unit = str(amount)[-1]\n    # BOLT #11:\n    # A reader SHOULD fail if `amount` contains a non-digit, or is followed by\n    # anything except a `multiplier` in the table above.\n    if not re.fullmatch(\"\\\\d+[pnum]?\", str(amount)):\n        raise LnDecodeException(\"Invalid amount '{}'\".format(amount))\n\n    if unit in units.keys():\n        return Decimal(amount[:-1]) / units[unit]\n    else:\n        return Decimal(amount)\n\n\ndef encode_fallback_addr(fallback: str, net: Type[AbstractNet]) -> Sequence[int]:\n    \"\"\"Encode all supported fallback addresses.\"\"\"\n    wver, wprog_ints = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, fallback)\n    if wver is not None:\n        wprog = bytes(wprog_ints)\n    else:\n        addrtype, addr = b58_address_to_hash160(fallback)\n        if addrtype == net.ADDRTYPE_P2PKH:\n            wver = 17\n        elif addrtype == net.ADDRTYPE_P2SH:\n            wver = 18\n        else:\n            raise LnEncodeException(f\"Unknown address type {addrtype} for {net}\")\n        wprog = addr\n    data5 = convertbits(wprog, 8, 5)\n    assert data5 is not None\n    return tagged5('f', [wver] + list(data5))\n\n\ndef parse_fallback_addr(data5: Sequence[int], net: Type[AbstractNet]) -> Optional[str]:\n    wver = data5[0]\n    data8 = bytes(convertbits(data5[1:], 5, 8, False))\n    if wver == 17:\n        addr = hash160_to_b58_address(data8, net.ADDRTYPE_P2PKH)\n    elif wver == 18:\n        addr = hash160_to_b58_address(data8, net.ADDRTYPE_P2SH)\n    elif wver <= 16:\n        addr = segwit_addr.encode_segwit_address(net.SEGWIT_HRP, wver, data8)\n    else:\n        return None\n    return addr\n\n\ndef tagged5(char: str, data5: Sequence[int]) -> Sequence[int]:\n    assert len(data5) < (1 << 10)\n    return [CHARSET_INVERSE[char], len(data5) >> 5, len(data5) & 31] + data5\n\n\ndef tagged8(char: str, data8: Sequence[int]) -> Sequence[int]:\n    return tagged5(char, convertbits(data8, 8, 5))\n\n\ndef int_to_data5(val: int, *, bit_len: int = None) -> Sequence[int]:\n    \"\"\"Represent big-endian number with as many 0-31 values as it takes.\n    If `bit_len` is set, use exactly bit_len//5 values (left-padded with zeroes).\n    \"\"\"\n    if bit_len is not None:\n        assert bit_len % 5 == 0, bit_len\n        if val.bit_length() > bit_len:\n            raise ValueError(f\"{val=} too big for {bit_len=!r}\")\n    ret = []\n    while val != 0:\n        ret.append(val % 32)\n        val //= 32\n    if bit_len is not None:\n        ret.extend([0] * (len(ret) - bit_len // 5))\n    ret.reverse()\n    return ret\n\n\ndef int_from_data5(data5: Sequence[int]) -> int:\n    total = 0\n    for v in data5:\n        total = 32 * total + v\n    return total\n\n\ndef pull_tagged(data5: bytearray) -> Tuple[str, Sequence[int]]:\n    \"\"\"Try to pull out tagged data: returns tag, tagged data. Mutates data in-place.\"\"\"\n    if len(data5) < 3:\n        raise ValueError(\"Truncated field\")\n    length = data5[1] * 32 + data5[2]\n    if length > len(data5) - 3:\n        raise ValueError(\n            \"Truncated {} field: expected {} values\".format(CHARSET[data5[0]], length))\n    ret = (CHARSET[data5[0]], data5[3:3+length])\n    del data5[:3 + length]    # much faster than: data5=data5[offset:]\n    return ret\n\n\ndef lnencode(addr: 'LnAddr', privkey) -> str:\n    if addr.amount:\n        amount = addr.net.BOLT11_HRP + shorten_amount(addr.amount)\n    else:\n        amount = addr.net.BOLT11_HRP if addr.net else ''\n\n    hrp = 'ln' + amount\n\n    # Start with the timestamp\n    data5 = int_to_data5(addr.date, bit_len=35)\n\n    tags_set = set()\n\n    # Payment hash\n    assert addr.paymenthash is not None\n    data5 += tagged8('p', addr.paymenthash)\n    tags_set.add('p')\n\n    if addr.payment_secret is not None:\n        data5 += tagged8('s', addr.payment_secret)\n        tags_set.add('s')\n\n    for k, v in addr.tags:\n\n        # BOLT #11:\n        #\n        # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,\n        if k in ('d', 'h', 'n', 'x', 'p', 's', '9'):\n            if k in tags_set:\n                raise LnEncodeException(\"Duplicate '{}' tag\".format(k))\n\n        if k == 'r':\n            route = bytearray()\n            for step in v:\n                pubkey, scid, feebase, feerate, cltv = step\n                route += pubkey\n                route += scid\n                route += int.to_bytes(feebase, length=4, byteorder=\"big\", signed=False)\n                route += int.to_bytes(feerate, length=4, byteorder=\"big\", signed=False)\n                route += int.to_bytes(cltv, length=2, byteorder=\"big\", signed=False)\n            data5 += tagged8('r', route)\n        elif k == 't':\n            pubkey, feebase, feerate, cltv = v\n            route = bytearray()\n            route += pubkey\n            route += int.to_bytes(feebase, length=4, byteorder=\"big\", signed=False)\n            route += int.to_bytes(feerate, length=4, byteorder=\"big\", signed=False)\n            route += int.to_bytes(cltv, length=2, byteorder=\"big\", signed=False)\n            data5 += tagged8('t', route)\n        elif k == 'f':\n            if v is not None:\n                data5 += encode_fallback_addr(v, addr.net)\n        elif k == 'd':\n            # truncate to max length: 1024*5 bits = 639 bytes\n            data5 += tagged8('d', v.encode()[0:639])\n        elif k == 'x':\n            expirybits = int_to_data5(v)\n            data5 += tagged5('x', expirybits)\n        elif k == 'h':\n            data5 += tagged8('h', sha256(v.encode('utf-8')).digest())\n        elif k == 'n':\n            data5 += tagged8('n', v)\n        elif k == 'c':\n            finalcltvbits = int_to_data5(v)\n            data5 += tagged5('c', finalcltvbits)\n        elif k == '9':\n            if v == 0:\n                continue\n            feature_bits = int_to_data5(v)\n            data5 += tagged5('9', feature_bits)\n        else:\n            # FIXME: Support unknown tags?\n            raise LnEncodeException(\"Unknown tag {}\".format(k))\n\n        tags_set.add(k)\n\n    # BOLT #11:\n    #\n    # A writer MUST include either a `d` or `h` field, and MUST NOT include\n    # both.\n    if 'd' in tags_set and 'h' in tags_set:\n        raise ValueError(\"Cannot include both 'd' and 'h'\")\n    if 'd' not in tags_set and 'h' not in tags_set:\n        raise ValueError(\"Must include either 'd' or 'h'\")\n\n    # We actually sign the hrp, then data (padded to 8 bits with zeroes).\n    msg = hrp.encode(\"ascii\") + bytes(convertbits(data5, 5, 8))\n    msg32 = sha256(msg).digest()\n    privkey = ecc.ECPrivkey(privkey)\n    sig = privkey.ecdsa_sign_recoverable(msg32, is_compressed=False)\n    recovery_flag = bytes([sig[0] - 27])\n    sig = bytes(sig[1:]) + recovery_flag\n    sig = bytes(convertbits(sig, 8, 5, False))\n    data5 += sig\n\n    return bech32_encode(segwit_addr.Encoding.BECH32, hrp, data5)\n\n\nclass LnAddr(object):\n    def __init__(self, *, paymenthash: bytes = None, amount=None, net: Type[AbstractNet] = None, tags=None, date=None,\n                 payment_secret: bytes = None):\n        self.date = int(time.time()) if not date else int(date)\n        self.tags = [] if not tags else tags\n        self.unknown_tags = []\n        self.paymenthash = paymenthash\n        self.payment_secret = payment_secret\n        self.signature = None\n        self.pubkey = None\n        self.net = constants.net if net is None else net  # type: Type[AbstractNet]\n        self._amount = amount  # type: Optional[Decimal]  # in bitcoins\n\n    @property\n    def amount(self) -> Optional[Decimal]:\n        return self._amount\n\n    @amount.setter\n    def amount(self, value):\n        if not (isinstance(value, Decimal) or value is None):\n            raise LnInvoiceException(f\"amount must be Decimal or None, not {value!r}\")\n        if value is None:\n            self._amount = None\n            return\n        assert isinstance(value, Decimal)\n        if value.is_nan() or not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC):\n            raise LnInvoiceException(f\"amount is out-of-bounds: {value!r} BTC\")\n        if value * 10**12 % 10:\n            # max resolution is millisatoshi\n            raise LnInvoiceException(f\"Cannot encode {value!r}: too many decimal places\")\n        self._amount = value\n\n    def get_amount_sat(self) -> Optional[Decimal]:\n        # note that this has msat resolution potentially\n        if self.amount is None:\n            return None\n        return self.amount * COIN\n\n    def get_routing_info(self, tag):\n        # note: tag will be 't' for trampoline\n        r_tags = list(filter(lambda x: x[0] == tag, self.tags))\n        # strip the tag type, it's implicitly 'r' now\n        r_tags = list(map(lambda x: x[1], r_tags))\n        # if there are multiple hints, we will use the first one that works,\n        # from a random permutation\n        random.shuffle(r_tags)\n        return r_tags\n\n    @staticmethod\n    def format_bolt11_routing_info_as_human_readable(r_tags, *, has_explicit_r_tagtype: bool = False):\n        \"\"\"Converts the node-id bytes->hex, and the SCID bytes->\"AAAxBBBxCC\", e.g. for logging.\"\"\"\n        from .util import format_short_id\n        r_tags2 = []\n        for r_tag in r_tags:\n            if has_explicit_r_tagtype:\n                (tagtype, path) = r_tag\n                assert tagtype == \"r\", f\"found unexpected {tagtype=}\"\n            else:\n                path = r_tag\n            path2 = [\n                (edge[0].hex(), format_short_id(edge[1]), edge[2], edge[3], edge[4])\n                for edge in path]\n            r_tag2 = (tagtype, path2) if has_explicit_r_tagtype else path2\n            r_tags2.append(r_tag2)\n        return r_tags2\n\n    def get_amount_msat(self) -> Optional[int]:\n        if self.amount is None:\n            return None\n        return int(self.amount * COIN * 1000)\n\n    def get_features(self) -> 'LnFeatures':\n        from .lnutil import LnFeatures\n        return LnFeatures(self.get_tag('9') or 0)\n\n    def validate_and_compare_features(self, myfeatures: 'LnFeatures') -> None:\n        \"\"\"Raises IncompatibleOrInsaneFeatures.\n\n        note: these checks are not done by the parser (in lndecode), as then when we started requiring a new feature,\n              old saved already paid invoices could no longer be parsed.\n        \"\"\"\n        from .lnutil import validate_features, ln_compare_features\n        invoice_features = self.get_features()\n        validate_features(invoice_features)\n        ln_compare_features(myfeatures.for_invoice(), invoice_features)\n\n    def __str__(self):\n        return \"LnAddr[{}, amount={}{} tags=[{}]]\".format(\n            hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None,\n            self.amount, self.net.BOLT11_HRP,\n            \", \".join([k + '=' + str(v) for k, v in self.tags])\n        )\n\n    def get_min_final_cltv_delta(self) -> int:\n        cltv = self.get_tag('c')\n        if cltv is None:\n            return 18\n        return int(cltv)\n\n    def get_tag(self, tag):\n        for k, v in self.tags:\n            if k == tag:\n                return v\n        return None\n\n    def get_description(self) -> str:\n        return self.get_tag('d') or ''\n\n    def get_fallback_address(self) -> str:\n        return self.get_tag('f') or ''\n\n    def get_expiry(self) -> int:\n        exp = self.get_tag('x')\n        if exp is None:\n            exp = 3600\n        return int(exp)\n\n    def is_expired(self) -> bool:\n        now = time.time()\n        # BOLT-11 does not specify what expiration of '0' means.\n        # we treat it as 0 seconds here (instead of never)\n        return now > self.get_expiry() + self.date\n\n    def to_debug_json(self) -> Dict[str, Any]:\n        d = {\n            'pubkey': self.pubkey.serialize().hex(),\n            'amount_BTC': str(self.amount),\n            'rhash': self.paymenthash.hex(),\n            'payment_secret': self.payment_secret.hex() if self.payment_secret else None,\n            'description': self.get_description(),\n            'exp': self.get_expiry(),\n            'time': self.date,\n            'min_final_cltv_delta': self.get_min_final_cltv_delta(),\n            'features': self.get_features().get_names(),\n            'tags': self.tags,\n            'unknown_tags': self.unknown_tags,\n        }\n        if ln_routing_info := self.get_routing_info('r'):\n            d['r_tags'] = self.format_bolt11_routing_info_as_human_readable(ln_routing_info)\n        return d\n\n\nclass SerializableKey:\n    def __init__(self, pubkey):\n        self.pubkey = pubkey\n    def serialize(self):\n        return self.pubkey.get_public_key_bytes(True)\n\n\ndef lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr:\n    \"\"\"Parses a string into an LnAddr object.\n    Can raise LnDecodeException or IncompatibleOrInsaneFeatures.\n    \"\"\"\n    if net is None:\n        net = constants.net\n    decoded_bech32 = bech32_decode(invoice, ignore_long_length=True)\n    hrp = decoded_bech32.hrp\n    data5 = decoded_bech32.data  # \"5\" as in list of 5-bit integers\n    if decoded_bech32.encoding is None:\n        raise LnDecodeException(\"Bad bech32 checksum\")\n    if decoded_bech32.encoding != segwit_addr.Encoding.BECH32:\n        raise LnDecodeException(\"Bad bech32 encoding: must be using vanilla BECH32\")\n\n    # BOLT #11:\n    #\n    # A reader MUST fail if it does not understand the `prefix`.\n    if not hrp.startswith('ln'):\n        raise LnDecodeException(\"Does not start with ln\")\n\n    if not hrp[2:].startswith(net.BOLT11_HRP):\n        raise LnDecodeException(f\"Wrong Lightning invoice HRP {hrp[2:]}, should be {net.BOLT11_HRP}\")\n\n    # Final signature 65 bytes, split it off.\n    if len(data5) < 65*8//5:\n        raise LnDecodeException(\"Too short to contain signature\")\n    sigdecoded = bytes(convertbits(data5[-65*8//5:], 5, 8, False))\n    data5 = data5[:-65*8//5]\n    data5_remaining = bytearray(data5)  # note: bytearray is faster than list of ints\n\n    addr = LnAddr()\n    addr.pubkey = None\n    addr.net = net\n\n    amountstr = hrp[2+len(net.BOLT11_HRP):]\n    # BOLT #11:\n    #\n    # A reader SHOULD indicate if amount is unspecified, otherwise it MUST\n    # multiply `amount` by the `multiplier` value (if any) to derive the\n    # amount required for payment.\n    if amountstr != '':\n        addr.amount = unshorten_amount(amountstr)\n\n    addr.date = int_from_data5(data5_remaining[:7])\n    data5_remaining = data5_remaining[7:]\n\n    while data5_remaining:\n        tag, tagdata = pull_tagged(data5_remaining)  # mutates arg\n\n        # BOLT #11:\n        #\n        # A reader MUST skip over unknown fields, an `f` field with unknown\n        # `version`, or a `p`, `h`, or `n` field which does not have\n        # `data_length` 52, 52, or 53 respectively.\n        data_length = len(tagdata)\n\n        if tag == 'r':\n            # BOLT #11:\n            #\n            # * `r` (3): `data_length` variable.  One or more entries\n            # containing extra routing information for a private route;\n            # there may be more than one `r` field, too.\n            #    * `pubkey` (264 bits)\n            #    * `short_channel_id` (64 bits)\n            #    * `feebase` (32 bits, big-endian)\n            #    * `feerate` (32 bits, big-endian)\n            #    * `cltv_expiry_delta` (16 bits, big-endian)\n            tagdata = convertbits(tagdata, 5, 8, False)\n            if not tagdata:\n                continue\n            route = []\n            with io.BytesIO(bytes(tagdata)) as s:\n                while True:\n                    pubkey = s.read(33)\n                    scid = s.read(8)\n                    feebase = s.read(4)\n                    feerate = s.read(4)\n                    cltv = s.read(2)\n                    if len(cltv) != 2:\n                        break  # EOF\n                    feebase = int.from_bytes(feebase, byteorder=\"big\")\n                    feerate = int.from_bytes(feerate, byteorder=\"big\")\n                    cltv = int.from_bytes(cltv, byteorder=\"big\")\n                    route.append((pubkey, scid, feebase, feerate, cltv))\n            if route:\n                addr.tags.append(('r',route))\n        elif tag == 't':\n            tagdata = convertbits(tagdata, 5, 8, False)\n            if not tagdata:\n                continue\n            route = []\n            with io.BytesIO(bytes(tagdata)) as s:\n                pubkey = s.read(33)\n                feebase = s.read(4)\n                feerate = s.read(4)\n                cltv = s.read(2)\n                if len(cltv) == 2:  # no EOF\n                    feebase = int.from_bytes(feebase, byteorder=\"big\")\n                    feerate = int.from_bytes(feerate, byteorder=\"big\")\n                    cltv = int.from_bytes(cltv, byteorder=\"big\")\n                    route.append((pubkey, feebase, feerate, cltv))\n            addr.tags.append(('t', route))\n        elif tag == 'f':\n            fallback = parse_fallback_addr(tagdata, addr.net)\n            if fallback:\n                addr.tags.append(('f', fallback))\n            else:\n                # Incorrect version.\n                addr.unknown_tags.append((tag, tagdata))\n                continue\n\n        elif tag == 'd':\n            addr.tags.append(('d', bytes(convertbits(tagdata, 5, 8, False)).decode('utf-8')))\n\n        elif tag == 'h':\n            if data_length != 52:\n                addr.unknown_tags.append((tag, tagdata))\n                continue\n            addr.tags.append(('h', bytes(convertbits(tagdata, 5, 8, False))))\n\n        elif tag == 'x':\n            addr.tags.append(('x', int_from_data5(tagdata)))\n\n        elif tag == 'p':\n            if data_length != 52:\n                addr.unknown_tags.append((tag, tagdata))\n                continue\n            addr.paymenthash = bytes(convertbits(tagdata, 5, 8, False))\n\n        elif tag == 's':\n            if data_length != 52:\n                addr.unknown_tags.append((tag, tagdata))\n                continue\n            addr.payment_secret = bytes(convertbits(tagdata, 5, 8, False))\n\n        elif tag == 'n':\n            if data_length != 53:\n                addr.unknown_tags.append((tag, tagdata))\n                continue\n            pubkeybytes = bytes(convertbits(tagdata, 5, 8, False))\n            addr.pubkey = pubkeybytes\n\n        elif tag == 'c':\n            addr.tags.append(('c', int_from_data5(tagdata)))\n\n        elif tag == '9':\n            features = int_from_data5(tagdata)\n            addr.tags.append(('9', features))\n            # note: The features are not validated here in the parser,\n            #       instead, validation is done just before we try paying the invoice (in lnworker._check_bolt11_invoice).\n            #       Context: invoice parsing happens when opening a wallet. If there was a backwards-incompatible\n            #       change to a feature, and we raised, some existing wallets could not be opened. Such a change\n            #       can happen to features not-yet-merged-to-BOLTs (e.g. trampoline feature bit was moved and reused).\n        else:\n            addr.unknown_tags.append((tag, tagdata))\n\n    if verbose:\n        print('hex of signature data (32 byte r, 32 byte s): {}'\n              .format(hexlify(sigdecoded[0:64])))\n        print('recovery flag: {}'.format(sigdecoded[64]))\n        data8 = bytes(convertbits(data5, 5, 8, True))\n        print('hex of data for signing: {}'\n              .format(hexlify(hrp.encode(\"ascii\") + data8)))\n        print('SHA256 of above: {}'.format(sha256(hrp.encode(\"ascii\") + data8).hexdigest()))\n\n    # BOLT #11:\n    #\n    # A reader MUST check that the `signature` is valid (see the `n` tagged\n    # field specified below).\n    addr.signature = sigdecoded[:65]\n    hrp_hash = sha256(hrp.encode(\"ascii\") + bytes(convertbits(data5, 5, 8, True))).digest()\n    if addr.pubkey:  # Specified by `n`\n        # BOLT #11:\n        #\n        # A reader MUST use the `n` field to validate the signature instead of\n        # performing signature recovery if a valid `n` field is provided.\n        if not ecc.ECPubkey(addr.pubkey).ecdsa_verify(sigdecoded[:64], hrp_hash):\n            raise LnDecodeException(\"bad signature\")\n        pubkey_copy = addr.pubkey\n\n        class WrappedBytesKey:\n            serialize = lambda: pubkey_copy\n\n        addr.pubkey = WrappedBytesKey\n    else: # Recover pubkey from signature.\n        addr.pubkey = SerializableKey(ecc.ECPubkey.from_ecdsa_sig64(sigdecoded[:64], sigdecoded[64], hrp_hash))\n\n    return addr\n"
  },
  {
    "path": "electrum/lnchannel.py",
    "content": "# Copyright (C) 2018 The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\nimport dataclasses\nimport enum\nfrom collections import defaultdict\nfrom enum import IntEnum, Enum\nfrom typing import (\n    Optional, Dict, List, Tuple, NamedTuple,\n    Iterable, Sequence, TYPE_CHECKING, Iterator, Union, Mapping)\nimport time\nimport threading\nfrom abc import ABC, abstractmethod\nimport itertools\n\nfrom aiorpcx import NetAddress\nimport attr\n\nimport electrum_ecc as ecc\nfrom electrum_ecc import ECPubkey\n\nfrom . import constants, util\nfrom .util import bfh, chunks, TxMinedInfo, error_text_bytes_to_safe_str\nfrom .bitcoin import redeem_script_to_address\nfrom .crypto import sha256, sha256d\nfrom .transaction import Transaction, PartialTransaction, TxInput, Sighash\nfrom .logging import Logger\nfrom .lntransport import LNPeerAddr\nfrom .lnonion import OnionRoutingFailure\nfrom . import lnutil\nfrom .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints,\n                     get_per_commitment_secret_from_seed, secret_to_pubkey, derive_privkey, make_closing_tx,\n                     sign_and_get_sig_string, RevocationStore, derive_blinded_pubkey, Direction, derive_pubkey,\n                     make_htlc_tx_with_open_channel, make_commitment, UpdateAddHtlc,\n                     funding_output_script, SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, make_commitment_outputs,\n                     ScriptHtlc, PaymentFailure, calc_fees_for_commitment_tx, RemoteMisbehaving, make_htlc_output_witness_script,\n                     ShortChannelID, map_htlcs_to_ctx_output_idxs,\n                     fee_for_htlc_output, offered_htlc_trim_threshold_sat,\n                     received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT,\n                     ChannelType, LNProtocolWarning, ZEROCONF_TIMEOUT)\nfrom .lnsweep import sweep_our_ctx, sweep_their_ctx\nfrom .lnsweep import sweep_their_htlctx_justice, sweep_our_htlctx, SweepInfo, MaybeSweepInfo\nfrom .lnsweep import sweep_their_ctx_to_remote_backup\nfrom .lnhtlc import HTLCManager\nfrom .lnmsg import encode_msg, decode_msg\nfrom .address_synchronizer import TX_HEIGHT_LOCAL\nfrom .lnutil import CHANNEL_OPENING_TIMEOUT_BLOCKS, CHANNEL_OPENING_TIMEOUT_SEC\nfrom .lnutil import ChannelBackupStorage, ImportedChannelBackupStorage, OnchainChannelBackupStorage\nfrom .lnutil import format_short_channel_id\nfrom .fee_policy import FEERATE_PER_KW_MIN_RELAY_LIGHTNING\n\nif TYPE_CHECKING:\n    from .lnworker import LNWallet\n    from .json_db import StoredDict\n\n\n# channel flags\nCF_ANNOUNCE_CHANNEL = 0x01\n\n# lightning channel states\n# Note: these states are persisted by name (for a given channel) in the wallet file,\n#       so consider doing a wallet db upgrade when changing them.\nclass ChannelState(IntEnum):\n    PREOPENING      = 0  # Initial negotiation. Channel will not be reestablished\n    OPENING         = 1  # Channel will be reestablished. (per BOLT2)\n                         #  - Funding node: has received funding_signed (can broadcast the funding tx)\n                         #  - Non-funding node: has sent the funding_signed message.\n    FUNDED          = 2  # Funding tx was mined (requires min_depth and tx verification)\n    OPEN            = 3  # both parties have sent funding_locked\n    SHUTDOWN        = 4  # shutdown has been sent.\n    CLOSING         = 5  # closing negotiation done. we have a fully signed tx.\n    FORCE_CLOSING   = 6  # *we* force-closed, and closing tx is unconfirmed. Note that if the\n                         # remote force-closes then we remain OPEN until it gets mined -\n                         # the server could be lying to us with a fake tx.\n    REQUESTED_FCLOSE = 7   # Chan is open, but we have tried to request the *remote* to force-close\n    WE_ARE_TOXIC     = 8   # Chan is open, but we have lost state and the remote proved this.\n                           # The remote must force-close, it is *not* safe for us to do so.\n    CLOSED           = 9   # closing tx has been mined\n    REDEEMED         = 10  # we can stop watching\n\n\nclass PeerState(IntEnum):\n    DISCONNECTED   = 0\n    REESTABLISHING = 1\n    GOOD           = 2\n    BAD            = 3\n\n\ncs = ChannelState\nstate_transitions = [\n    (cs.PREOPENING, cs.OPENING),\n    (cs.OPENING, cs.FUNDED),\n    (cs.FUNDED, cs.OPEN),\n    (cs.OPENING, cs.SHUTDOWN),\n    (cs.FUNDED, cs.SHUTDOWN),\n    (cs.OPEN, cs.SHUTDOWN),\n    (cs.SHUTDOWN, cs.SHUTDOWN),  # if we reestablish\n    (cs.SHUTDOWN, cs.CLOSING),\n    (cs.CLOSING, cs.CLOSING),\n    # we can force close almost any time\n    (cs.OPENING,  cs.FORCE_CLOSING),\n    (cs.FUNDED,   cs.FORCE_CLOSING),\n    (cs.OPEN,     cs.FORCE_CLOSING),\n    (cs.SHUTDOWN, cs.FORCE_CLOSING),\n    (cs.CLOSING,  cs.FORCE_CLOSING),\n    (cs.REQUESTED_FCLOSE, cs.FORCE_CLOSING),\n    # we can request a force-close almost any time\n    (cs.OPENING,  cs.REQUESTED_FCLOSE),\n    (cs.FUNDED,   cs.REQUESTED_FCLOSE),\n    (cs.OPEN,     cs.REQUESTED_FCLOSE),\n    (cs.SHUTDOWN, cs.REQUESTED_FCLOSE),\n    (cs.CLOSING,  cs.REQUESTED_FCLOSE),\n    (cs.REQUESTED_FCLOSE,  cs.REQUESTED_FCLOSE),\n    # we can get force closed almost any time\n    (cs.OPENING,  cs.CLOSED),\n    (cs.FUNDED,   cs.CLOSED),\n    (cs.OPEN,     cs.CLOSED),\n    (cs.SHUTDOWN, cs.CLOSED),\n    (cs.CLOSING,  cs.CLOSED),\n    (cs.REQUESTED_FCLOSE, cs.CLOSED),\n    (cs.WE_ARE_TOXIC,          cs.CLOSED),\n    # during channel_reestablish, we might realise we have lost state\n    (cs.OPENING,  cs.WE_ARE_TOXIC),\n    (cs.FUNDED,   cs.WE_ARE_TOXIC),\n    (cs.OPEN,     cs.WE_ARE_TOXIC),\n    (cs.SHUTDOWN, cs.WE_ARE_TOXIC),\n    (cs.REQUESTED_FCLOSE, cs.WE_ARE_TOXIC),\n    (cs.WE_ARE_TOXIC, cs.WE_ARE_TOXIC),\n    #\n    (cs.FORCE_CLOSING, cs.FORCE_CLOSING),  # allow multiple attempts\n    (cs.FORCE_CLOSING, cs.CLOSED),\n    (cs.FORCE_CLOSING, cs.REDEEMED),\n    (cs.CLOSED, cs.REDEEMED),\n    (cs.OPENING, cs.REDEEMED),  # channel never funded (dropped from mempool)\n    (cs.PREOPENING, cs.REDEEMED),  # channel never funded\n]\ndel cs  # delete as name is ambiguous without context\n\n\nclass ChanCloseOption(Enum):\n    COOP_CLOSE = enum.auto()\n    LOCAL_FCLOSE = enum.auto()\n    REQUEST_REMOTE_FCLOSE = enum.auto()\n\n\nclass RevokeAndAck(NamedTuple):\n    per_commitment_secret: bytes\n    next_per_commitment_point: bytes\n\n\nclass RemoteCtnTooFarInFuture(Exception): pass\n\n\ndef htlcsum(htlcs: Iterable[UpdateAddHtlc]):\n    return sum([x.amount_msat for x in htlcs])\n\ndef now():\n    return int(time.time())\n\nclass HTLCWithStatus(NamedTuple):\n    channel_id: bytes\n    htlc: UpdateAddHtlc\n    direction: Direction\n    status: str\n\n\nclass AbstractChannel(Logger, ABC):\n    storage: Union['StoredDict', dict]\n    config: Dict[HTLCOwner, Union[LocalConfig, RemoteConfig]]\n    lnworker: 'LNWallet'\n    channel_id: bytes\n    short_channel_id: Optional[ShortChannelID] = None\n    funding_outpoint: Outpoint\n    node_id: bytes  # note that it might not be the full 33 bytes; for OCB it is only the prefix\n    should_request_force_close: bool = False\n    _state: ChannelState\n    _who_closed: Optional[int] = None  # HTLCOwner (1 or -1).  0 means \"unknown\"\n\n    def set_short_channel_id(self, short_id: ShortChannelID) -> None:\n        self.short_channel_id = short_id\n        self.storage[\"short_channel_id\"] = short_id\n\n    def get_id_for_log(self) -> str:\n        scid = self.short_channel_id\n        if scid:\n            return str(scid)\n        return self.channel_id.hex()\n\n    def short_id_for_GUI(self) -> str:\n        return format_short_channel_id(self.short_channel_id)\n\n    def diagnostic_name(self):\n        return self.get_id_for_log()\n\n    def set_state(self, state: ChannelState, *, force: bool = False) -> None:\n        \"\"\"Set on-chain state.\n        `force` can be set while debugging from the console to allow illegal transitions.\n        \"\"\"\n        old_state = self._state\n        if not force and (old_state, state) not in state_transitions:\n            raise Exception(f\"Transition not allowed: {old_state.name} -> {state.name}\")\n        self.logger.debug(f'Setting channel state: {old_state.name} -> {state.name}')\n        self._state = state\n        self.storage['state'] = self._state.name\n        self.lnworker.channel_state_changed(self)\n\n    def get_state(self) -> ChannelState:\n        return self._state\n\n    def is_funded(self) -> bool:\n        return self.get_state() >= ChannelState.FUNDED\n\n    def is_open(self) -> bool:\n        return self.get_state() == ChannelState.OPEN\n\n    def is_closed(self) -> bool:\n        # the closing txid has been saved\n        return self.get_state() >= ChannelState.CLOSING\n\n    def is_closed_or_closing(self):\n        # related: self.get_state_for_GUI\n        return self.is_closed() or self.unconfirmed_closing_txid is not None\n\n    def is_redeemed(self) -> bool:\n        return self.get_state() == ChannelState.REDEEMED\n\n    def need_to_subscribe(self) -> bool:\n        \"\"\"Whether lnwatcher/synchronizer need to be watching this channel.\"\"\"\n        if not self.is_redeemed():\n            return True\n        # Chan already deeply closed. Still, if some txs are missing, we should sub.\n        # check we have funding tx\n        # note: tx might not be directly related to the wallet, e.g. chan opened by remote\n        if (funding_item := self.get_funding_height()) is None:\n            return True\n        funding_txid, funding_height, funding_timestamp = funding_item\n        if self.lnworker.wallet.adb.get_transaction(funding_txid) is None:\n            return True\n        # check we have closing tx\n        # note: tx might not be directly related to the wallet, e.g. local-fclose\n        if (closing_item := self.get_closing_height()) is None:\n            return True\n        closing_txid, closing_height, closing_timestamp = closing_item\n        if self.lnworker.wallet.adb.get_transaction(closing_txid) is None:\n            return True\n        return False\n\n    @abstractmethod\n    def get_close_options(self) -> Sequence[ChanCloseOption]:\n        pass\n\n    def save_funding_height(self, *, txid: str, height: int, timestamp: Optional[int]) -> None:\n        self.storage['funding_height'] = txid, height, timestamp\n\n    def get_funding_height(self) -> Optional[Tuple[str, int, Optional[int]]]:\n        return self.storage.get('funding_height')\n\n    def delete_funding_height(self):\n        self.storage.pop('funding_height', None)\n\n    def save_closing_height(self, *, txid: str, height: int, timestamp: Optional[int]) -> None:\n        self.storage['closing_height'] = txid, height, timestamp\n\n    def get_closing_height(self) -> Optional[Tuple[str, int, Optional[int]]]:\n        return self.storage.get('closing_height')\n\n    def delete_closing_height(self):\n        self.storage.pop('closing_height', None)\n\n    def create_sweeptxs_for_our_ctx(self, ctx: Transaction) -> Dict[str, MaybeSweepInfo]:\n        return sweep_our_ctx(chan=self, ctx=ctx)\n\n    def create_sweeptxs_for_their_ctx(self, ctx: Transaction) -> Dict[str, MaybeSweepInfo]:\n        return sweep_their_ctx(chan=self, ctx=ctx)\n\n    def is_backup(self) -> bool:\n        return False\n\n    def get_local_scid_alias(self, *, create_new_if_needed: bool = False) -> Optional[bytes]:\n        return None\n\n    def get_remote_scid_alias(self) -> Optional[bytes]:\n        return None\n\n    def get_remote_peer_sent_error(self) -> Optional[str]:\n        return None\n\n    def get_ctx_sweep_info(self, ctx: Transaction) -> Tuple[bool, Dict[str, MaybeSweepInfo]]:\n        our_sweep_info = self.create_sweeptxs_for_our_ctx(ctx)\n        their_sweep_info = self.create_sweeptxs_for_their_ctx(ctx)\n        if our_sweep_info:\n            sweep_info = our_sweep_info\n            who_closed = LOCAL\n        elif their_sweep_info:\n            sweep_info = their_sweep_info\n            who_closed = REMOTE\n        else:\n            sweep_info = {}\n            who_closed = 0\n        if self._who_closed != who_closed:  # mostly here to limit log spam\n            self._who_closed = who_closed\n            if who_closed == LOCAL:\n                self.logger.info(f'we (local) force closed')\n            elif who_closed == REMOTE:\n                self.logger.info(f'they (remote) force closed.')\n            else:\n                self.logger.info(f'not sure who closed. maybe co-op close?')\n        is_local_ctx = who_closed == LOCAL\n        return is_local_ctx, sweep_info\n\n    def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, MaybeSweepInfo]:\n        return {}\n\n    def extract_preimage_from_htlc_txin(self, txin: TxInput, *, is_deeply_mined: bool) -> None:\n        return\n\n    def update_onchain_state(self, *, funding_txid: str, funding_height: TxMinedInfo,\n                             closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None:\n        # note: state transitions are irreversible, but\n        # save_funding_height, save_closing_height are reversible\n        if funding_height.height() == TX_HEIGHT_LOCAL:\n            self.update_unfunded_state()\n        elif closing_height.height() == TX_HEIGHT_LOCAL:\n            self.update_funded_state(\n                funding_txid=funding_txid,\n                funding_height=funding_height)\n        else:\n            self.update_closed_state(\n                funding_txid=funding_txid,\n                funding_height=funding_height,\n                closing_txid=closing_txid,\n                closing_height=closing_height,\n                keep_watching=keep_watching)\n\n    def update_unfunded_state(self) -> None:\n        self.delete_funding_height()\n        self.delete_closing_height()\n        state = self.get_state()\n        if state in [ChannelState.PREOPENING, ChannelState.OPENING, ChannelState.FORCE_CLOSING]:\n            if self.is_initiator():\n                # set channel state to REDEEMED so that it can be removed manually\n                # to protect ourselves against a server lying by omission,\n                # we check that funding_inputs have been double spent and deeply mined\n                inputs = self.storage.get('funding_inputs', [])\n                if not inputs:\n                    self.logger.info(f'channel funding inputs are not provided')\n                    self.set_state(ChannelState.REDEEMED)\n                for i in inputs:\n                    spender_txid = self.lnworker.wallet.db.get_spent_outpoint(*i)\n                    if spender_txid is None:\n                        continue\n                    if spender_txid != self.funding_outpoint.txid:\n                        tx_mined_height = self.lnworker.wallet.adb.get_tx_height(spender_txid)\n                        if tx_mined_height.conf > lnutil.REDEEM_AFTER_DOUBLE_SPENT_DELAY:\n                            self.logger.info(f'channel is double spent {inputs}')\n                            self.set_state(ChannelState.REDEEMED)\n                            break\n            elif self.has_funding_timed_out():\n                self.logger.warning(f\"dropping incoming channel, funding tx not found in mempool\")\n                self.lnworker.remove_channel(self.channel_id)\n        elif self.is_zeroconf() and state in [ChannelState.OPEN, ChannelState.CLOSING, ChannelState.FORCE_CLOSING]:\n            chan_age = now() - self.storage['init_timestamp']\n            # handling zeroconf channels with no funding tx, can happen if broadcasting fails on LSP side\n            # or if the LSP did double spent the funding tx/never published it intentionally\n            # only remove a timed out OPEN channel if we are connected to the network to prevent removing it if we went\n            # offline before seeing the funding tx\n            if state != ChannelState.OPEN or chan_age > ZEROCONF_TIMEOUT and self.lnworker.network.is_connected():\n                # we delete the channel if its in closing state (either initiated manually by client or by LSP on failure)\n                # or if the channel is not seeing any funding tx after 10 minutes to prevent further usage (limit damage)\n                self.set_state(ChannelState.REDEEMED, force=True)\n                local_balance_sat = int(self.balance(LOCAL) // 1000)\n                if local_balance_sat > 0:\n                    self.logger.warning(\n                        f\"we may have been scammed out of {local_balance_sat} sat by our \"\n                        f\"JIT provider: {self.lnworker.config.ZEROCONF_TRUSTED_NODE} or he didn't use our preimage\")\n                    self.lnworker.config.ZEROCONF_TRUSTED_NODE = ''\n                # FIXME this is broken: lnwatcher.unwatch_channel does not exist\n                self.lnworker.lnwatcher.unwatch_channel(self.get_funding_address(), self.funding_outpoint.to_str())\n                # remove remaining local transactions from the wallet, this will also remove child transactions (closing tx)\n                self.lnworker.lnwatcher.adb.remove_transaction(self.funding_outpoint.txid)\n                self.lnworker.remove_channel(self.channel_id)\n\n    def update_funded_state(self, *, funding_txid: str, funding_height: TxMinedInfo) -> None:\n        self.save_funding_height(txid=funding_txid, height=funding_height.height(), timestamp=funding_height.timestamp)\n        self.delete_closing_height()\n        if funding_height.conf>0:\n            self.set_short_channel_id(ShortChannelID.from_components(\n                funding_height.height(), funding_height.txpos, self.funding_outpoint.output_index))\n        elif self.has_funding_timed_out():\n            self.logger.warning(\"dropping incoming channel, funding tx took too long to confirm\")\n            self.lnworker.remove_channel(self.channel_id)\n            return\n        if self.get_state() == ChannelState.OPENING:\n            if self.is_funding_tx_mined(funding_height):\n                self.set_state(ChannelState.FUNDED)\n        elif self.is_zeroconf() and funding_height.conf >= 3 and not self.should_request_force_close:\n            if not self.is_funding_tx_mined(funding_height):\n                # funding tx is invalid (invalid amount or address) we need to get rid of the channel again\n                self.should_request_force_close = True\n                if peer := self.lnworker.lnpeermgr.get_peer_by_pubkey(self.node_id):\n                    # reconnect to trigger force close request\n                    peer.close_and_cleanup()\n            else:\n                # remove zeroconf flag as we are now confirmed, this is to prevent an electrum server causing\n                # us to remove a channel later in update_unfunded_state by omitting its funding tx\n                self.remove_zeroconf_flag()\n\n    def update_closed_state(self, *, funding_txid: str, funding_height: TxMinedInfo,\n                            closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None:\n        self.save_funding_height(txid=funding_txid, height=funding_height.height(), timestamp=funding_height.timestamp)\n        self.save_closing_height(txid=closing_txid, height=closing_height.height(), timestamp=closing_height.timestamp)\n        if funding_height.conf>0:\n            self.set_short_channel_id(ShortChannelID.from_components(\n                funding_height.height(), funding_height.txpos, self.funding_outpoint.output_index))\n        if self.get_state() < ChannelState.CLOSED:\n            conf = closing_height.conf\n            if conf > 0:\n                self.set_state(ChannelState.CLOSED)\n                self.lnworker.wallet.txbatcher.set_password_future(None)\n            else:\n                # we must not trust the server with unconfirmed transactions,\n                # because the state transition is irreversible. if the remote\n                # force closed, we remain OPEN until the closing tx is confirmed\n                self.unconfirmed_closing_txid = closing_txid\n                util.trigger_callback('channel', self.lnworker.wallet, self)\n\n        if self.get_state() == ChannelState.CLOSED and not keep_watching:\n            self.set_state(ChannelState.REDEEMED)\n            if self.is_backup():\n                # auto-remove redeemed backups\n                self.lnworker.remove_channel_backup(self.channel_id)\n\n    @abstractmethod\n    def is_initiator(self) -> bool:\n        pass\n\n    @abstractmethod\n    def is_public(self) -> bool:\n        pass\n\n    @abstractmethod\n    def is_zeroconf(self) -> bool:\n        pass\n\n    @abstractmethod\n    def remove_zeroconf_flag(self) -> None:\n        pass\n\n    @abstractmethod\n    def is_funding_tx_mined(self, funding_height: TxMinedInfo) -> bool:\n        pass\n\n    @abstractmethod\n    def get_funding_address(self) -> str:\n        pass\n\n    def get_funding_tx(self) -> Optional[Transaction]:\n        funding_txid = self.funding_outpoint.txid\n        return self.lnworker.lnwatcher.adb.get_transaction(funding_txid)\n\n    @abstractmethod\n    def get_sweep_address(self) -> str:\n        \"\"\"Returns a wallet address we can use to sweep coins to.\n        It could be something static to the channel (fixed for its lifecycle),\n        or it might just ask the wallet now for an unused address.\n        \"\"\"\n        pass\n\n    def get_state_for_GUI(self) -> str:\n        cs = self.get_state()\n        if cs <= ChannelState.OPEN and self.unconfirmed_closing_txid:\n            return 'FORCE-CLOSING'\n        return cs.name\n\n    @abstractmethod\n    def get_oldest_unrevoked_ctn(self, subject: HTLCOwner) -> int:\n        pass\n\n    @abstractmethod\n    def included_htlcs(self, subject: HTLCOwner, direction: Direction, ctn: int = None) -> Sequence[UpdateAddHtlc]:\n        pass\n\n    @abstractmethod\n    def funding_txn_minimum_depth(self) -> int:\n        pass\n\n    @abstractmethod\n    def balance(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None) -> int:\n        \"\"\"This balance (in msat) only considers HTLCs that have been settled by ctn.\n        It disregards reserve, fees, and pending HTLCs (in both directions).\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def balance_minus_outgoing_htlcs(self, whose: HTLCOwner, *,\n                                     ctx_owner: HTLCOwner = HTLCOwner.LOCAL,\n                                     ctn: int = None) -> int:\n        \"\"\"This balance (in msat), which includes the value of\n        pending outgoing HTLCs, is used in the UI.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def is_frozen_for_sending(self) -> bool:\n        \"\"\"Whether the user has marked this channel as frozen for sending.\n        Frozen channels are not supposed to be used for new outgoing payments.\n        (note that payment-forwarding ignores this option)\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def is_frozen_for_receiving(self) -> bool:\n        \"\"\"Whether the user has marked this channel as frozen for receiving.\n        Frozen channels are not supposed to be used for new incoming payments.\n        (note that payment-forwarding ignores this option)\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_local_pubkey(self) -> bytes:\n        \"\"\"Returns our node ID.\"\"\"\n        pass\n\n    @abstractmethod\n    def get_capacity(self) -> Optional[int]:\n        \"\"\"Returns channel capacity in satoshis, or None if unknown.\"\"\"\n        pass\n\n    @abstractmethod\n    def can_be_deleted(self) -> bool:\n        pass\n\n    @abstractmethod\n    def has_funding_timed_out(self) -> bool:\n        pass\n\n    @abstractmethod\n    def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:\n        \"\"\"Returns a list of addrs that the wallet should not use, to avoid address-reuse.\n        Typically, these addresses are wallet.is_mine, but that is not guaranteed,\n        in which case the wallet can just ignore those.\n        \"\"\"\n        pass\n\n    def has_anchors(self) -> bool:\n        pass\n\n\nclass ChannelBackup(AbstractChannel):\n    \"\"\"\n    current capabilities:\n      - detect force close\n      - request force close\n      - sweep my ctx to_local\n    future:\n      - will need to sweep their ctx to_remote\n    \"\"\"\n\n    def __init__(self, cb: ChannelBackupStorage, *, lnworker: 'LNWallet'):\n        self.name = None\n        self.cb = cb\n        self.is_imported = isinstance(self.cb, ImportedChannelBackupStorage)\n        self.storage = {} # dummy storage\n        self._state = ChannelState.OPENING\n        self.node_id = cb.node_id if self.is_imported else cb.node_id_prefix\n        self.channel_id = cb.channel_id()\n        self.funding_outpoint = cb.funding_outpoint()\n        self.lnworker = lnworker\n        self.short_channel_id = None\n        Logger.__init__(self)\n        self.config = {}\n        if self.is_imported:\n            assert isinstance(cb, ImportedChannelBackupStorage)\n            self.init_config(cb)\n        self.unconfirmed_closing_txid = None # not a state, only for GUI\n\n    def init_config(self, cb: ImportedChannelBackupStorage):\n        local_payment_pubkey = cb.local_payment_pubkey\n        if local_payment_pubkey is None:\n            self.logger.warning(\n                f\"local_payment_pubkey missing from (old-type) channel backup. \"\n                f\"You should export and re-import a newer backup.\")\n        multisig_funding_keypair = None\n        if multisig_funding_secret := cb.multisig_funding_privkey:\n            multisig_funding_keypair = Keypair(\n                privkey=multisig_funding_secret,\n                pubkey=ecc.ECPrivkey(multisig_funding_secret).get_public_key_bytes(),\n            )\n        self.config[LOCAL] = LocalConfig.from_seed(\n            channel_seed=cb.channel_seed,\n            to_self_delay=cb.local_delay,\n            # there are three cases of backups:\n            # 1. legacy: payment_basepoint will be derived\n            # 2. static_remotekey: to_remote sweep not necessary due to wallet address\n            # 3. anchor outputs: sweep to_remote by deriving the key from the funding pubkeys\n            static_remotekey=local_payment_pubkey,\n            multisig_key=multisig_funding_keypair,\n            # dummy values\n            static_payment_key=None,\n            dust_limit_sat=None,\n            max_htlc_value_in_flight_msat=None,\n            max_accepted_htlcs=None,\n            initial_msat=None,\n            reserve_sat=None,\n            funding_locked_received=False,\n            current_commitment_signature=None,\n            current_htlc_signatures=b'',\n            htlc_minimum_msat=1,\n            upfront_shutdown_script='',\n            announcement_node_sig=b'',\n            announcement_bitcoin_sig=b'',\n        )\n        self.config[REMOTE] = RemoteConfig(\n            # payment_basepoint needed to deobfuscate ctn in our_ctx\n            payment_basepoint=OnlyPubkeyKeypair(cb.remote_payment_pubkey),\n            # revocation_basepoint is used to claim to_local in our ctx\n            revocation_basepoint=OnlyPubkeyKeypair(cb.remote_revocation_pubkey),\n            to_self_delay=cb.remote_delay,\n            # dummy values\n            multisig_key=OnlyPubkeyKeypair(None),\n            htlc_basepoint=OnlyPubkeyKeypair(None),\n            delayed_basepoint=OnlyPubkeyKeypair(None),\n            dust_limit_sat=None,\n            max_htlc_value_in_flight_msat=None,\n            max_accepted_htlcs=None,\n            initial_msat = None,\n            reserve_sat = None,\n            htlc_minimum_msat=None,\n            next_per_commitment_point=None,\n            current_per_commitment_point=None,\n            upfront_shutdown_script='',\n            announcement_node_sig=b'',\n            announcement_bitcoin_sig=b'',\n        )\n\n    def can_be_deleted(self):\n        return self.is_imported or self.is_redeemed()\n\n    def has_funding_timed_out(self):\n        return False\n\n    def get_capacity(self):\n        lnwatcher = self.lnworker.lnwatcher\n        if lnwatcher:\n            # fixme: we should probably not call that method here\n            return lnwatcher.adb.get_tx_delta(self.funding_outpoint.txid, self.cb.funding_address)\n        return None\n\n    def is_backup(self):\n        return True\n\n    def create_sweeptxs_for_their_ctx(self, ctx):\n        funding_tx = self.get_funding_tx()\n        assert funding_tx\n        return sweep_their_ctx_to_remote_backup(chan=self, ctx=ctx, funding_tx=funding_tx)\n\n    def create_sweeptxs_for_our_ctx(self, ctx):\n        if self.is_imported:\n            return sweep_our_ctx(chan=self, ctx=ctx)\n        else:\n            return {}\n\n    def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, MaybeSweepInfo]:\n        return {}\n\n    def extract_preimage_from_htlc_txin(self, txin: TxInput, *, is_deeply_mined: bool) -> None:\n        return None\n\n    def get_funding_address(self):\n        return self.cb.funding_address\n\n    def is_initiator(self):\n        return self.cb.is_initiator\n\n    def is_public(self):\n        return False\n\n    def get_oldest_unrevoked_ctn(self, who):\n        return -1\n\n    def included_htlcs(self, subject, direction, ctn=None):\n        return []\n\n    def funding_txn_minimum_depth(self):\n        return 1\n\n    def is_funding_tx_mined(self, funding_height):\n        return funding_height.conf > 1\n\n    def balance_minus_outgoing_htlcs(self, whose: HTLCOwner, *, ctx_owner: HTLCOwner = HTLCOwner.LOCAL, ctn: int = None):\n        return 0\n\n    def balance(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None) -> int:\n        return 0\n\n    def is_frozen_for_sending(self) -> bool:\n        return False\n\n    def is_frozen_for_receiving(self) -> bool:\n        return False\n\n    def get_sweep_address(self) -> str:\n        return self.lnworker.wallet.get_new_sweep_address_for_channel()\n\n    def has_anchors(self) -> Optional[bool]:\n        return None\n\n    def is_zeroconf(self) -> bool:\n        return False\n\n    def remove_zeroconf_flag(self) -> None:\n        pass\n\n    def get_local_pubkey(self) -> bytes:\n        cb = self.cb\n        assert isinstance(cb, ChannelBackupStorage)\n        if isinstance(cb, ImportedChannelBackupStorage):\n            return ecc.ECPrivkey(cb.privkey).get_public_key_bytes(compressed=True)\n        if isinstance(cb, OnchainChannelBackupStorage):\n            return self.lnworker.node_keypair.pubkey\n        raise NotImplementedError(f\"unexpected cb type: {type(cb)}\")\n\n    def get_close_options(self) -> Sequence[ChanCloseOption]:\n        ret = []\n        if self.get_state() == ChannelState.FUNDED:\n            ret.append(ChanCloseOption.REQUEST_REMOTE_FCLOSE)\n        return ret\n\n    def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:\n        if self.is_imported:\n            # For v1 imported cbs, we have the local_payment_pubkey, which is\n            # directly used as p2wpkh() of static_remotekey channels.\n            # (for v0 imported cbs, the correct local_payment_pubkey is missing, and so\n            #  we might calculate a different address here, which might not be wallet.is_mine,\n            #  but that should be harmless)\n            our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey\n            to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=self.has_anchors())\n            return [to_remote_address]\n        else:  # on-chain backup\n            return []\n\n\nclass Channel(AbstractChannel):\n    # note: try to avoid naming ctns/ctxs/etc as \"current\" and \"pending\".\n    #       they are ambiguous. Use \"oldest_unrevoked\" or \"latest\" or \"next\".\n    #       TODO enforce this ^\n\n    # our forwarding parameters for forwarding HTLCs through this channel\n    forwarding_cltv_delta = 144\n    forwarding_fee_base_msat = 1000\n    forwarding_fee_proportional_millionths = 1\n\n    def __repr__(self):\n        return \"Channel(%s)\"%self.get_id_for_log()\n\n    def __init__(\n        self,\n        state: 'StoredDict', *,\n        name=None,\n        lnworker: 'LNWallet',\n        initial_feerate=None,\n        jit_opening_fee: Optional[int] = None,\n    ):\n        self.jit_opening_fee = jit_opening_fee\n        self.name = name\n        self.channel_id = bfh(state[\"channel_id\"])\n        self.short_channel_id = ShortChannelID.normalize(state[\"short_channel_id\"])\n        Logger.__init__(self)  # should be after short_channel_id is set\n        self.lnworker = lnworker\n        self.storage = state\n        self.db_lock = self.storage.lock\n        self.config = {}\n        self.config[LOCAL] = state[\"local_config\"]\n        self.config[REMOTE] = state[\"remote_config\"]\n        self.constraints = state[\"constraints\"]  # type: ChannelConstraints\n        self.funding_outpoint = state[\"funding_outpoint\"]\n        self.node_id = bfh(state[\"node_id\"])\n        self.onion_keys = state['onion_keys']  # type: Dict[int, bytes]\n        self.data_loss_protect_remote_pcp = state['data_loss_protect_remote_pcp']\n        self.hm = HTLCManager(log=state['log'], initiator = LOCAL if self.constraints.is_initiator else REMOTE, initial_feerate=initial_feerate)\n        self.unfulfilled_htlcs = state[\"unfulfilled_htlcs\"]  # type: Dict[int, Optional[str]]\n        # ^ htlc_id -> onion_packet_hex\n        self._state = ChannelState[state['state']]\n        self.peer_state = PeerState.DISCONNECTED\n        self._outgoing_channel_update = None  # type: Optional[bytes]\n        self.revocation_store = RevocationStore(state[\"revocation_store\"])\n        self._can_send_ctx_updates = True  # type: bool\n        self._receive_fail_reasons = {}  # type: Dict[int, (bytes, OnionRoutingFailure)]\n        self.unconfirmed_closing_txid = None # not a state, only for GUI\n        self.sent_channel_ready = False # no need to persist this, because channel_ready is re-sent in channel_reestablish\n        self.sent_announcement_signatures = False\n        self.htlc_settle_time = {}\n\n    def get_local_scid_alias(self, *, create_new_if_needed: bool = False) -> Optional[bytes]:\n        \"\"\"Get scid_alias to be used for *outgoing* HTLCs.\n        (called local as we choose the value)\n        \"\"\"\n        if alias := self.storage.get('local_scid_alias'):\n            return bytes.fromhex(alias)\n        elif create_new_if_needed:\n            # deterministic, same secrecy level as wallet master pubkey\n            wallet_fingerprint = bytes(self.lnworker.wallet.get_fingerprint(), \"utf8\")\n            alias = sha256(wallet_fingerprint + self.channel_id)[0:8]\n            self.storage['local_scid_alias'] = alias.hex()\n            return alias\n        return None\n\n    def save_remote_scid_alias(self, alias: bytes):\n        self.storage['alias'] = alias.hex()\n\n    def get_remote_scid_alias(self) -> Optional[bytes]:\n        \"\"\"Get scid_alias to be used for *incoming* HTLCs.\n        (called remote as the remote chooses the value)\n        \"\"\"\n        alias = self.storage.get('alias')\n        return bytes.fromhex(alias) if alias else None\n\n    def get_scid_or_local_alias(self):\n        return self.short_channel_id or self.get_local_scid_alias()\n\n    def has_onchain_backup(self):\n        return self.storage.get('has_onchain_backup', False)\n\n    def can_be_deleted(self) -> bool:\n        if self.has_funding_timed_out():\n            return True\n        return self.is_redeemed()\n\n    def has_funding_timed_out(self):\n        if self.is_initiator() or self.is_funded():\n            return False\n        if self.lnworker.network.blockchain().is_tip_stale() or not self.lnworker.wallet.is_up_to_date():\n            return False\n        init_height = self.storage.get('init_height', 0)\n        init_timestamp = self.storage.get('init_timestamp', 0)\n        age_blocks = self.lnworker.network.get_local_height() - init_height\n        age_sec = now() - init_timestamp\n        # some channels might not have init_height set so we check both time and block based timeouts\n        return age_blocks > CHANNEL_OPENING_TIMEOUT_BLOCKS and age_sec > CHANNEL_OPENING_TIMEOUT_SEC\n\n    def get_capacity(self):\n        return self.constraints.capacity\n\n    def is_public(self):\n        return bool(self.constraints.flags & CF_ANNOUNCE_CHANNEL)\n\n    def is_initiator(self):\n        return self.constraints.is_initiator\n\n    def is_active(self):\n        return self.get_state() == ChannelState.OPEN and self.peer_state == PeerState.GOOD\n\n    def funding_txn_minimum_depth(self):\n        return self.constraints.funding_txn_minimum_depth\n\n    def diagnostic_name(self):\n        if self.name:\n            return str(self.name)\n        return super().diagnostic_name()\n\n    def set_onion_key(self, key: int, value: bytes):\n        self.onion_keys[key] = value\n\n    def pop_onion_key(self, key: int) -> bytes:\n        return self.onion_keys.pop(key)\n\n    def set_data_loss_protect_remote_pcp(self, key, value):\n        self.data_loss_protect_remote_pcp[key] = value\n\n    def get_data_loss_protect_remote_pcp(self, key):\n        return self.data_loss_protect_remote_pcp.get(key)\n\n    def get_local_pubkey(self) -> bytes:\n        return self.lnworker.node_keypair.pubkey\n\n    def set_remote_update(self, payload: dict) -> None:\n        \"\"\"Save the ChannelUpdate message for the incoming direction of this channel.\n        This message contains info we need to populate private route hints when\n        creating invoices.\n        \"\"\"\n        assert payload['short_channel_id'] in [self.short_channel_id, self.get_local_scid_alias()]\n        from .channel_db import ChannelDB\n        ChannelDB.verify_channel_update(payload, start_node=self.node_id)\n        raw = payload['raw']\n        self.storage['remote_update'] = raw.hex()\n\n    def get_remote_update(self) -> Optional[bytes]:\n        return bfh(self.storage.get('remote_update')) if self.storage.get('remote_update') else None\n\n    def add_or_update_peer_addr(self, peer: LNPeerAddr) -> None:\n        if 'peer_network_addresses' not in self.storage:\n            self.storage['peer_network_addresses'] = {}\n        self.storage['peer_network_addresses'][peer.net_addr_str()] = now()\n\n    def get_peer_addresses(self) -> Iterator[LNPeerAddr]:\n        # sort by timestamp: most recent first\n        addrs = sorted(self.storage.get('peer_network_addresses', {}).items(),\n                       key=lambda x: x[1], reverse=True)\n        for net_addr_str, ts in addrs:\n            net_addr = NetAddress.from_string(net_addr_str)\n            yield LNPeerAddr(host=str(net_addr.host), port=net_addr.port, pubkey=self.node_id)\n\n    def save_remote_peer_sent_error(self, original_error: bytes):\n        # We save the original arbitrary text(/bytes) error, as received.\n        # The length is only implicitly limited by the BOLT-08 max msg size.\n        # Receiving an error usually results in the channel getting closed, so\n        # there is likely no need to store multiple errors. We only store one, and overwrite.\n        self.storage['remote_peer_sent_error'] = original_error.hex()\n\n    def get_remote_peer_sent_error(self) -> Optional[str]:\n        original_error = self.storage.get('remote_peer_sent_error')\n        if not original_error:\n            return None\n        err_bytes = bytes.fromhex(original_error)\n        safe_str = error_text_bytes_to_safe_str(err_bytes)   # note: truncates\n        return safe_str\n\n    def get_outgoing_gossip_channel_update(self, *, scid: ShortChannelID = None) -> bytes:\n        \"\"\"\n        scid: to be put into the channel_update message instead of the real scid, as this might be an scid alias\n        \"\"\"\n        if self._outgoing_channel_update is not None and scid is None:\n            return self._outgoing_channel_update\n        if scid is None:\n            scid = self.short_channel_id\n        sorted_node_ids = list(sorted([self.node_id, self.get_local_pubkey()]))\n        channel_flags = b'\\x00' if sorted_node_ids[0] == self.get_local_pubkey() else b'\\x01'\n        htlc_maximum_msat = min(self.config[REMOTE].max_htlc_value_in_flight_msat, 1000 * self.constraints.capacity)\n\n        chan_upd = encode_msg(\n            \"channel_update\",\n            short_channel_id=scid,\n            channel_flags=channel_flags,\n            message_flags=b'\\x01',\n            cltv_expiry_delta=self.forwarding_cltv_delta,\n            htlc_minimum_msat=self.config[REMOTE].htlc_minimum_msat,\n            htlc_maximum_msat=htlc_maximum_msat,\n            fee_base_msat=self.forwarding_fee_base_msat,\n            fee_proportional_millionths=self.forwarding_fee_proportional_millionths,\n            chain_hash=constants.net.rev_genesis_bytes(),\n            timestamp=now(),\n        )\n        sighash = sha256d(chan_upd[2 + 64:])\n        sig = ecc.ECPrivkey(self.lnworker.node_keypair.privkey).ecdsa_sign(sighash, sigencode=ecc.ecdsa_sig64_from_r_and_s)\n        message_type, payload = decode_msg(chan_upd)\n        payload['signature'] = sig\n        chan_upd = encode_msg(message_type, **payload)\n\n        self._outgoing_channel_update = chan_upd\n        return chan_upd\n\n    def construct_channel_announcement_without_sigs(self) -> Tuple[bytes, bool]:\n        bitcoin_keys = [\n            self.config[REMOTE].multisig_key.pubkey,\n            self.config[LOCAL].multisig_key.pubkey]\n        node_ids = [self.node_id, self.get_local_pubkey()]\n        is_reverse = node_ids[0] > node_ids[1]\n        if is_reverse:\n            node_ids.reverse()\n            bitcoin_keys.reverse()\n        chan_ann = encode_msg(\n            \"channel_announcement\",\n            len=0,\n            features=b'',\n            chain_hash=constants.net.rev_genesis_bytes(),\n            short_channel_id=self.short_channel_id,\n            node_id_1=node_ids[0],\n            node_id_2=node_ids[1],\n            bitcoin_key_1=bitcoin_keys[0],\n            bitcoin_key_2=bitcoin_keys[1],\n        )\n        return chan_ann, is_reverse\n\n    def get_channel_announcement_hash(self):\n        chan_ann, _ = self.construct_channel_announcement_without_sigs()\n        return sha256d(chan_ann[256+2:])\n\n    def is_static_remotekey_enabled(self) -> bool:\n        channel_type = ChannelType(self.storage.get('channel_type'))\n        return bool(channel_type & ChannelType.OPTION_STATIC_REMOTEKEY)\n\n    def is_zeroconf(self) -> bool:\n        channel_type = ChannelType(self.storage.get('channel_type'))\n        return bool(channel_type & ChannelType.OPTION_ZEROCONF)\n\n    def remove_zeroconf_flag(self) -> None:\n        if not self.is_zeroconf():\n            return\n        channel_type = ChannelType(self.storage.get('channel_type'))\n        self.storage['channel_type'] = channel_type & ~ChannelType.OPTION_ZEROCONF\n\n    def get_sweep_address(self) -> str:\n        # TODO: in case of unilateral close with pending HTLCs, this address will be reused\n        if self.has_anchors():\n            addr = self.lnworker.wallet.get_new_sweep_address_for_channel()\n        elif self.is_static_remotekey_enabled():\n            our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey\n            addr = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=self.has_anchors())\n        assert self.lnworker.wallet.is_mine(addr)\n        return addr\n\n    def has_anchors(self) -> bool:\n        channel_type = ChannelType(self.storage.get('channel_type'))\n        return bool(channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX)\n\n    def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:\n        assert self.is_static_remotekey_enabled()\n        our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey\n        to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=self.has_anchors())\n        return [to_remote_address]\n\n    def get_feerate(self, subject: HTLCOwner, *, ctn: int) -> int:\n        # returns feerate in sat/kw\n        return self.hm.get_feerate(subject, ctn)\n\n    def get_oldest_unrevoked_feerate(self, subject: HTLCOwner) -> int:\n        return self.hm.get_feerate_in_oldest_unrevoked_ctx(subject)\n\n    def get_latest_feerate(self, subject: HTLCOwner) -> int:\n        return self.hm.get_feerate_in_latest_ctx(subject)\n\n    def get_next_feerate(self, subject: HTLCOwner) -> int:\n        return self.hm.get_feerate_in_next_ctx(subject)\n\n    def get_payments(self, status=None) -> Mapping[bytes, List[HTLCWithStatus]]:\n        out = defaultdict(list)\n        for direction, htlc in self.hm.all_htlcs_ever():\n            htlc_proposer = LOCAL if direction is SENT else REMOTE\n            if self.hm.was_htlc_failed(htlc_id=htlc.htlc_id, htlc_proposer=htlc_proposer):\n                _status = 'failed'\n            elif self.hm.was_htlc_preimage_released(htlc_id=htlc.htlc_id, htlc_proposer=htlc_proposer):\n                _status = 'settled'\n            else:\n                _status = 'inflight'\n            if status and status != _status:\n                continue\n            htlc_with_status = HTLCWithStatus(\n                channel_id=self.channel_id, htlc=htlc, direction=direction, status=_status)\n            out[htlc.payment_hash].append(htlc_with_status)\n        return out\n\n    def open_with_first_pcp(self, remote_pcp: bytes, remote_sig: bytes) -> None:\n        with self.db_lock:\n            self.config[REMOTE].current_per_commitment_point = remote_pcp\n            self.config[REMOTE].next_per_commitment_point = None\n            self.config[LOCAL].current_commitment_signature = remote_sig\n            self.hm.channel_open_finished()\n            self.peer_state = PeerState.GOOD\n\n    def get_state_for_GUI(self):\n        cs_name = super().get_state_for_GUI()\n        if self.is_closed() or self.unconfirmed_closing_txid:\n            return cs_name\n        ps = self.peer_state\n        if ps != PeerState.GOOD:\n            return ps.name\n        return cs_name\n\n    def set_can_send_ctx_updates(self, b: bool) -> None:\n        self._can_send_ctx_updates = b\n\n    def can_update_ctx(self, *, proposer: HTLCOwner) -> bool:\n        \"\"\"Whether proposer is allowed to send commitment_signed, revoke_and_ack,\n        and update_* messages.\n        \"\"\"\n        if self.get_state() not in (ChannelState.OPEN, ChannelState.SHUTDOWN):\n            return False\n        if self.peer_state != PeerState.GOOD:\n            return False\n        if proposer == LOCAL:\n            if not self._can_send_ctx_updates:\n                return False\n        return True\n\n    def can_send_update_add_htlc(self) -> bool:\n        return self.can_update_ctx(proposer=LOCAL) and self.is_open()\n\n    def is_frozen_for_sending(self) -> bool:\n        if self.lnworker.uses_trampoline() and not self.lnworker.is_trampoline_peer(self.node_id):\n            return True\n        return self.storage.get('frozen_for_sending', False)\n\n    def set_frozen_for_sending(self, b: bool) -> None:\n        self.storage['frozen_for_sending'] = bool(b)\n        util.trigger_callback('channel', self.lnworker.wallet, self)\n\n    def is_frozen_for_receiving(self) -> bool:\n        if self.lnworker.uses_trampoline() and not self.lnworker.is_trampoline_peer(self.node_id):\n            return True\n        return self.storage.get('frozen_for_receiving', False)\n\n    def set_frozen_for_receiving(self, b: bool) -> None:\n        self.storage['frozen_for_receiving'] = bool(b)\n        util.trigger_callback('channel', self.lnworker.wallet, self)\n\n    def _assert_can_add_htlc(self, *, htlc_proposer: HTLCOwner, amount_msat: int,\n                             ignore_min_htlc_value: bool = False) -> None:\n        \"\"\"Raises PaymentFailure if the htlc_proposer cannot add this new HTLC.\n        (this is relevant both for forwarding and endpoint)\n        \"\"\"\n        htlc_receiver = htlc_proposer.inverted()\n        # note: all these tests are about the *receiver's* *next* commitment transaction,\n        #       and the constraints are the ones imposed by their config\n        ctn = self.get_next_ctn(htlc_receiver)\n        chan_config = self.config[htlc_receiver]\n        if self.get_state() != ChannelState.OPEN:\n            raise PaymentFailure(f\"Channel not open. {self.get_state()!r}\")\n        if not self.can_update_ctx(proposer=htlc_proposer):\n            raise PaymentFailure(f\"cannot update channel. {self.get_state()!r} {self.peer_state!r}\")\n        if htlc_proposer == LOCAL:\n            if not self.can_send_update_add_htlc():\n                raise PaymentFailure('Channel cannot add htlc')\n\n        # check htlc raw value\n        if not ignore_min_htlc_value:\n            if amount_msat <= 0:\n                raise PaymentFailure(\"HTLC value must be positive\")\n            if amount_msat < chan_config.htlc_minimum_msat:\n                # todo: for incoming htlcs this could be handled more gracefully with `amount_below_minimum`\n                raise PaymentFailure(f'HTLC value too small: {amount_msat} msat')\n\n        if self.htlc_slots_left(htlc_proposer) == 0:\n            raise PaymentFailure('Too many HTLCs already in channel')\n\n        if amount_msat > self.remaining_max_inflight(htlc_receiver, strict=False):\n            raise PaymentFailure(\n                f'HTLC value sum (sum of pending htlcs plus new htlc) '\n                f'would exceed max allowed: {chan_config.max_htlc_value_in_flight_msat/1000} sat')\n\n        # check proposer can afford htlc\n        max_can_send_msat = self.available_to_spend(htlc_proposer)\n        if max_can_send_msat < amount_msat:\n            raise PaymentFailure(f'Not enough balance. can send: {max_can_send_msat}, tried: {amount_msat}')\n\n    def htlc_slots_left(self, htlc_proposer: HTLCOwner) -> int:\n        # check \"max_accepted_htlcs\"\n        htlc_receiver = htlc_proposer.inverted()\n        ctn = self.get_next_ctn(htlc_receiver)\n        chan_config = self.config[htlc_receiver]\n        # If proposer is LOCAL we apply stricter checks as that is behaviour we can control.\n        # This should lead to fewer disagreements (i.e. channels failing).\n        strict = (htlc_proposer == LOCAL)\n        if not strict:\n            # this is the loose check BOLT-02 specifies:\n            return chan_config.max_accepted_htlcs - len(self.hm.htlcs_by_direction(htlc_receiver, direction=RECEIVED, ctn=ctn))\n        else:\n            # however, c-lightning is a lot stricter, so extra checks:\n            # https://github.com/ElementsProject/lightning/blob/4dcd4ca1556b13b6964a10040ba1d5ef82de4788/channeld/full_channel.c#L581\n            max_concurrent_htlcs = min(\n                self.config[htlc_proposer].max_accepted_htlcs,\n                self.config[htlc_receiver].max_accepted_htlcs)\n            return max_concurrent_htlcs - len(self.hm.htlcs(htlc_receiver, ctn=ctn))\n\n    def remaining_max_inflight(self, htlc_receiver: HTLCOwner, *, strict: bool) -> int:\n        \"\"\"\n        Checks max_htlc_value_in_flight_msat\n        strict = False -> how much we can accept according to BOLT2\n        strict = True -> how much the remote will accept to send to us (Eclair has stricter rules)\n        \"\"\"\n        ctn = self.get_next_ctn(htlc_receiver)\n        current_htlc_sum = htlcsum(self.hm.htlcs_by_direction(htlc_receiver, direction=RECEIVED, ctn=ctn).values())\n        max_inflight = self.config[htlc_receiver].max_htlc_value_in_flight_msat\n        if strict and htlc_receiver == LOCAL:\n            # in order to send, eclair applies both local and remote max values\n            # https://github.com/ACINQ/eclair/blob/9b0c00a2a28d3ba6c7f3d01fbd2d8704ebbdc75d/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala#L503\n            max_inflight = min(\n                self.config[LOCAL].max_htlc_value_in_flight_msat,\n                self.config[REMOTE].max_htlc_value_in_flight_msat\n            )\n        return max_inflight - current_htlc_sum\n\n    def can_pay(self, amount_msat: int, *, check_frozen=False) -> bool:\n        \"\"\"Returns whether we can add an HTLC of given value.\"\"\"\n        if check_frozen and self.is_frozen_for_sending():\n            return False\n        try:\n            self._assert_can_add_htlc(htlc_proposer=LOCAL, amount_msat=amount_msat)\n        except PaymentFailure:\n            return False\n        return True\n\n    def can_receive(self, amount_msat: int, *, check_frozen=False,\n                    ignore_min_htlc_value: bool = False) -> bool:\n        \"\"\"Returns whether the remote can add an HTLC of given value.\"\"\"\n        if check_frozen and self.is_frozen_for_receiving():\n            return False\n        try:\n            self._assert_can_add_htlc(\n                htlc_proposer=REMOTE,\n                amount_msat=amount_msat,\n                ignore_min_htlc_value=ignore_min_htlc_value)\n        except PaymentFailure:\n            return False\n        return True\n\n    def should_try_to_reestablish_peer(self) -> bool:\n        if self.peer_state != PeerState.DISCONNECTED:\n            return False\n        if self.should_request_force_close:\n            return True\n        return ChannelState.PREOPENING < self._state < ChannelState.CLOSING\n\n    def get_funding_address(self):\n        script = funding_output_script(self.config[LOCAL], self.config[REMOTE])\n        return redeem_script_to_address('p2wsh', script)\n\n    def add_htlc(self, htlc: UpdateAddHtlc) -> UpdateAddHtlc:\n        \"\"\"Adds a new LOCAL HTLC to the channel.\n        Action must be initiated by LOCAL.\n        \"\"\"\n        assert isinstance(htlc, UpdateAddHtlc)\n        self._assert_can_add_htlc(htlc_proposer=LOCAL, amount_msat=htlc.amount_msat)\n        if htlc.htlc_id is None:\n            htlc = dataclasses.replace(htlc, htlc_id=self.hm.get_next_htlc_id(LOCAL))\n        with self.db_lock:\n            self.hm.send_htlc(htlc)\n        self.logger.info(\"add_htlc\")\n        return htlc\n\n    def receive_htlc(self, htlc: UpdateAddHtlc, onion_packet:bytes = None) -> UpdateAddHtlc:\n        \"\"\"Adds a new REMOTE HTLC to the channel.\n        Action must be initiated by REMOTE.\n        \"\"\"\n        assert isinstance(htlc, UpdateAddHtlc)\n        try:\n            self._assert_can_add_htlc(htlc_proposer=REMOTE, amount_msat=htlc.amount_msat)\n        except PaymentFailure as e:\n            raise RemoteMisbehaving(e) from e\n        if htlc.htlc_id is None:  # used in unit tests\n            htlc = dataclasses.replace(htlc, htlc_id=self.hm.get_next_htlc_id(REMOTE))\n        with self.db_lock:\n            self.hm.recv_htlc(htlc)\n            if onion_packet:\n                self.unfulfilled_htlcs[htlc.htlc_id] = onion_packet.hex()\n\n        self.logger.info(\"receive_htlc\")\n        return htlc\n\n    def sign_next_commitment(self) -> Tuple[bytes, Sequence[bytes]]:\n        \"\"\"Returns signatures for our next remote commitment tx.\n        Action must be initiated by LOCAL.\n        Finally, the next remote ctx becomes the latest remote ctx.\n        \"\"\"\n        # TODO: when more channel types are supported, this method should depend on channel type\n        next_remote_ctn = self.get_next_ctn(REMOTE)\n        self.logger.info(f\"sign_next_commitment. ctn={next_remote_ctn}\")\n        assert not self.is_closed(), self.get_state()\n\n        pending_remote_commitment = self.get_next_commitment(REMOTE)\n        sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.config[LOCAL], self.config[REMOTE])\n        self.logger.debug(f\"sign_next_commitment. {pending_remote_commitment.serialize()=}. {sig_64.hex()=}\")\n\n        their_remote_htlc_privkey_number = derive_privkey(\n            int.from_bytes(self.config[LOCAL].htlc_basepoint.privkey, 'big'),\n            self.config[REMOTE].next_per_commitment_point)\n        their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big')\n\n        htlcsigs = []\n        htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(chan=self,\n                                                                  ctx=pending_remote_commitment,\n                                                                  pcp=self.config[REMOTE].next_per_commitment_point,\n                                                                  subject=REMOTE,\n                                                                  ctn=next_remote_ctn)\n        for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():\n            _script, htlc_tx = make_htlc_tx_with_open_channel(chan=self,\n                                                              pcp=self.config[REMOTE].next_per_commitment_point,\n                                                              subject=REMOTE,\n                                                              ctn=next_remote_ctn,\n                                                              htlc_direction=direction,\n                                                              commit=pending_remote_commitment,\n                                                              ctx_output_idx=ctx_output_idx,\n                                                              htlc=htlc)\n            if self.has_anchors():\n                # we send a signature with the following sighash flags\n                # for the peer to be able to replace inputs and outputs\n                htlc_tx.inputs()[0].sighash = Sighash.ANYONECANPAY | Sighash.SINGLE\n            sig = htlc_tx.sign_txin(0, their_remote_htlc_privkey)\n            htlc_sig = ecc.ecdsa_sig64_from_der_sig(sig[:-1])\n            htlcsigs.append((ctx_output_idx, htlc_sig))\n        htlcsigs.sort()\n        htlcsigs = [x[1] for x in htlcsigs]\n        with self.db_lock:\n            self.hm.send_ctx()\n        return sig_64, htlcsigs\n\n    def receive_new_commitment(self, sig: bytes, htlc_sigs: Sequence[bytes]) -> None:\n        \"\"\"Processes signatures for our next local commitment tx, sent by the REMOTE.\n        Action must be initiated by REMOTE.\n        If all checks pass, the next local ctx becomes the latest local ctx.\n        \"\"\"\n        # TODO in many failure cases below, we should \"fail\" the channel (force-close)\n        # TODO: when more channel types are supported, this method should depend on channel type\n        next_local_ctn = self.get_next_ctn(LOCAL)\n        self.logger.info(f\"receive_new_commitment. ctn={next_local_ctn}, len(htlc_sigs)={len(htlc_sigs)}\")\n        assert not self.is_closed(), self.get_state()\n\n        assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes\n\n        pending_local_commitment = self.get_next_commitment(LOCAL)\n        pre_hash = pending_local_commitment.serialize_preimage(0)\n        msg_hash = sha256d(pre_hash)\n        if not ECPubkey(self.config[REMOTE].multisig_key.pubkey).ecdsa_verify(sig, msg_hash):\n            raise LNProtocolWarning(\n                f'failed verifying signature for our updated commitment transaction. '\n                f'sig={sig.hex()}. '\n                f'msg_hash={msg_hash.hex()}. '\n                f'pubkey={self.config[REMOTE].multisig_key.pubkey}. '\n                f'ctx={pending_local_commitment.serialize()} '\n            )\n\n        htlc_sigs_string = b''.join(htlc_sigs)\n\n        _secret, pcp = self.get_secret_and_point(subject=LOCAL, ctn=next_local_ctn)\n\n        htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(chan=self,\n                                                                  ctx=pending_local_commitment,\n                                                                  pcp=pcp,\n                                                                  subject=LOCAL,\n                                                                  ctn=next_local_ctn)\n        if len(htlc_to_ctx_output_idx_map) != len(htlc_sigs):\n            raise LNProtocolWarning(f'htlc sigs failure. recv {len(htlc_sigs)} sigs, expected {len(htlc_to_ctx_output_idx_map)}')\n        for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():\n            htlc_sig = htlc_sigs[htlc_relative_idx]\n            self._verify_htlc_sig(htlc=htlc,\n                                  htlc_sig=htlc_sig,\n                                  htlc_direction=direction,\n                                  pcp=pcp,\n                                  ctx=pending_local_commitment,\n                                  ctx_output_idx=ctx_output_idx,\n                                  ctn=next_local_ctn)\n        with self.db_lock:\n            self.hm.recv_ctx()\n            self.config[LOCAL].current_commitment_signature=sig\n            self.config[LOCAL].current_htlc_signatures=htlc_sigs_string\n\n    def _verify_htlc_sig(self, *, htlc: UpdateAddHtlc, htlc_sig: bytes, htlc_direction: Direction,\n                         pcp: bytes, ctx: Transaction, ctx_output_idx: int, ctn: int) -> None:\n        _script, htlc_tx = make_htlc_tx_with_open_channel(chan=self,\n                                                          pcp=pcp,\n                                                          subject=LOCAL,\n                                                          ctn=ctn,\n                                                          htlc_direction=htlc_direction,\n                                                          commit=ctx,\n                                                          ctx_output_idx=ctx_output_idx,\n                                                          htlc=htlc)\n        if self.has_anchors():\n            # peer sent us a signature for our ctx using anchor sighash flags\n            htlc_tx.inputs()[0].sighash = Sighash.ANYONECANPAY | Sighash.SINGLE\n        pre_hash = htlc_tx.serialize_preimage(0)\n        msg_hash = sha256d(pre_hash)\n        remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, pcp)\n        if not ECPubkey(remote_htlc_pubkey).ecdsa_verify(htlc_sig, msg_hash):\n            raise LNProtocolWarning(\n                f'failed verifying HTLC signatures: {htlc=}, {htlc_direction=}. '\n                f'htlc_tx={htlc_tx.serialize()}. '\n                f'htlc_sig={htlc_sig.hex()}. '\n                f'remote_htlc_pubkey={remote_htlc_pubkey.hex()}. '\n                f'msg_hash={msg_hash.hex()}. '\n                f'ctx={ctx.serialize()}. '\n                f'ctx_output_idx={ctx_output_idx}. '\n                f'ctn={ctn}. '\n            )\n\n    def get_remote_htlc_sig_for_htlc(self, *, htlc_relative_idx: int) -> bytes:\n        data = self.config[LOCAL].current_htlc_signatures\n        htlc_sigs = list(chunks(data, 64))\n        htlc_sig = htlc_sigs[htlc_relative_idx]\n        remote_sighash = Sighash.ALL if not self.has_anchors() else Sighash.ANYONECANPAY | Sighash.SINGLE\n        remote_htlc_sig = ecc.ecdsa_der_sig_from_ecdsa_sig64(htlc_sig) + Sighash.to_sigbytes(remote_sighash)\n        return remote_htlc_sig\n\n    def revoke_current_commitment(self):\n        self.logger.info(\"revoke_current_commitment\")\n        assert not self.is_closed(), self.get_state()\n        new_ctn = self.get_latest_ctn(LOCAL)\n        new_ctx = self.get_latest_commitment(LOCAL)\n        if not self.signature_fits(new_ctx):\n            # this should never fail; as receive_new_commitment already did this test\n            raise Exception(\"refusing to revoke as remote sig does not fit\")\n        with self.db_lock:\n            self.hm.send_rev()\n        last_secret, last_point = self.get_secret_and_point(LOCAL, new_ctn - 1)\n        next_secret, next_point = self.get_secret_and_point(LOCAL, new_ctn + 1)\n        return RevokeAndAck(last_secret, next_point)\n\n    def receive_revocation(self, revocation: RevokeAndAck):\n        self.logger.info(\"receive_revocation\")\n        assert not self.is_closed(), self.get_state()\n        new_ctn = self.get_latest_ctn(REMOTE)\n        cur_point = self.config[REMOTE].current_per_commitment_point\n        derived_point = ecc.ECPrivkey(revocation.per_commitment_secret).get_public_key_bytes(compressed=True)\n        if cur_point != derived_point:\n            raise Exception('revoked secret not for current point')\n        with self.db_lock:\n            self.revocation_store.add_next_entry(revocation.per_commitment_secret)\n            ##### start applying fee/htlc changes\n            self.hm.recv_rev()\n            self.config[REMOTE].current_per_commitment_point=self.config[REMOTE].next_per_commitment_point\n            self.config[REMOTE].next_per_commitment_point=revocation.next_per_commitment_point\n        assert new_ctn == self.get_oldest_unrevoked_ctn(REMOTE)\n        # lnworker callbacks\n        sent = self.hm.sent_in_ctn(new_ctn)\n        for htlc in sent:\n            self.lnworker.htlc_fulfilled(self, htlc.payment_hash, htlc.htlc_id)\n        failed = self.hm.failed_in_ctn(new_ctn)\n        for htlc in failed:\n            try:\n                error_bytes, failure_message = self._receive_fail_reasons.pop(htlc.htlc_id)\n            except KeyError:\n                error_bytes, failure_message = None, None\n            self.lnworker.htlc_failed(self, htlc.payment_hash, htlc.htlc_id, error_bytes, failure_message)\n\n    def extract_preimage_from_htlc_txin(self, txin: TxInput, *, is_deeply_mined: bool) -> None:\n        from . import lnutil\n        from .crypto import ripemd\n        from .transaction import match_script_against_template, script_GetOp\n        from .lnonion import OnionRoutingFailure, OnionFailureCode\n        witness = txin.witness_elements()\n        witness_script = witness[-1]\n        script_ops = [x for x in script_GetOp(witness_script)]\n        if match_script_against_template(witness_script, lnutil.WITNESS_TEMPLATE_OFFERED_HTLC, debug=False) \\\n           or match_script_against_template(witness_script, lnutil.WITNESS_TEMPLATE_OFFERED_HTLC_ANCHORS, debug=False):\n            ripemd_payment_hash = script_ops[21][1]\n        elif match_script_against_template(witness_script, lnutil.WITNESS_TEMPLATE_RECEIVED_HTLC, debug=False) \\\n           or match_script_against_template(witness_script, lnutil.WITNESS_TEMPLATE_RECEIVED_HTLC_ANCHORS, debug=False):\n            ripemd_payment_hash = script_ops[14][1]\n        else:\n            return\n        found = {}\n        for direction, htlc in itertools.chain(\n                self.hm.get_htlcs_in_oldest_unrevoked_ctx(REMOTE),\n                self.hm.get_htlcs_in_latest_ctx(REMOTE)):\n            if ripemd(htlc.payment_hash) == ripemd_payment_hash:\n                is_sent = direction == RECEIVED\n                found[htlc.htlc_id] = (htlc, is_sent)\n        for direction, htlc in itertools.chain(\n                self.hm.get_htlcs_in_oldest_unrevoked_ctx(LOCAL),\n                self.hm.get_htlcs_in_latest_ctx(LOCAL)):\n            if ripemd(htlc.payment_hash) == ripemd_payment_hash:\n                is_sent = direction == SENT\n                found[htlc.htlc_id] = (htlc, is_sent)\n        if not found:\n            return\n        if len(witness) == 5:    # HTLC success tx\n            preimage = witness[3]\n        elif len(witness) == 3:  # spending offered HTLC directly from ctx\n            preimage = witness[1]\n        else:\n            preimage = None      # HTLC timeout tx\n        if preimage:\n            assert ripemd(sha256(preimage)) == ripemd_payment_hash\n            payment_hash = sha256(preimage)\n            if self.lnworker.get_preimage(payment_hash) is not None:\n                return\n            # ^ note: log message text grepped for in regtests\n            self.logger.info(f\"found preimage in witness of length {len(witness)}, for {payment_hash.hex()}\")\n\n        # Mark the htlc as fulfilled or failed.\n        # If we forwarded this, this ensures that the success/failure is propagated back on the incoming channel.\n        # FIXME we only look at outgoing htlcs that have a corresponding output in the commitment tx,\n        #       however we should also look at those that do not. E.g. a small value htlc might not create an output\n        #       but we should still propagate back success or failure on the incoming link. And it is not just about\n        #       small value htlcs: even a large htlc might not appear in the outgoing channel's ctx, e.g. maybe it was\n        #       not committed yet - we should still make sure it gets removed on the incoming channel. (see #9631)\n        if preimage:\n            self.lnworker.save_preimage(payment_hash, preimage, mark_as_public=True)\n            for htlc, is_sent in found.values():\n                if is_sent:\n                    self.lnworker.htlc_fulfilled(self, payment_hash, htlc.htlc_id)\n        else:\n            # htlc timeout tx\n            if not is_deeply_mined:\n                return\n            failure = OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')\n            for htlc, is_sent in found.values():\n                if is_sent:\n                    self.logger.info(f'htlc timeout tx: failing htlc {is_sent}')\n                    self.lnworker.htlc_failed(\n                        self,\n                        payment_hash=htlc.payment_hash,\n                        htlc_id=htlc.htlc_id,\n                        error_bytes=None,\n                        failure_message=failure)\n\n    def balance(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None) -> int:\n        assert type(whose) is HTLCOwner\n        initial = self.config[whose].initial_msat\n        return self.hm.get_balance_msat(whose=whose,\n                                        ctx_owner=ctx_owner,\n                                        ctn=ctn,\n                                        initial_balance_msat=initial)\n\n    def balance_minus_outgoing_htlcs(self, whose: HTLCOwner, *, ctx_owner: HTLCOwner = HTLCOwner.LOCAL,\n                                     ctn: int = None) -> int:\n        assert type(whose) is HTLCOwner\n        if ctn is None:\n            ctn = self.get_next_ctn(ctx_owner)\n        committed_balance = self.balance(whose, ctx_owner=ctx_owner, ctn=ctn)\n        direction = RECEIVED if whose != ctx_owner else SENT\n        balance_in_htlcs = self.balance_tied_up_in_htlcs_by_direction(ctx_owner, ctn=ctn, direction=direction)\n        return committed_balance - balance_in_htlcs\n\n    def balance_tied_up_in_htlcs_by_direction(self, ctx_owner: HTLCOwner = LOCAL, *, ctn: int = None,\n                                              direction: Direction):\n        # in msat\n        if ctn is None:\n            ctn = self.get_next_ctn(ctx_owner)\n        return htlcsum(self.hm.htlcs_by_direction(ctx_owner, direction, ctn).values())\n\n    def has_unsettled_htlcs(self) -> bool:\n        return len(self.hm.htlcs(LOCAL)) + len(self.hm.htlcs(REMOTE)) > 0\n\n    def available_to_spend(self, subject: HTLCOwner) -> int:\n        \"\"\"The usable balance of 'subject' in msat, after taking reserve and fees (and anchors) into\n        consideration. Note that fees (and hence the result) fluctuate even without user interaction.\n        \"\"\"\n        assert type(subject) is HTLCOwner\n        sender = subject\n        receiver = subject.inverted()\n        initiator = LOCAL if self.constraints.is_initiator else REMOTE  # the initiator/funder pays on-chain fees\n\n        def consider_ctx(*, ctx_owner: HTLCOwner, is_htlc_dust: bool) -> int:\n            ctn = self.get_next_ctn(ctx_owner)\n            sender_balance_msat = self.balance_minus_outgoing_htlcs(whose=sender, ctx_owner=ctx_owner, ctn=ctn)\n            receiver_balance_msat = self.balance_minus_outgoing_htlcs(whose=receiver, ctx_owner=ctx_owner, ctn=ctn)\n            sender_reserve_msat = self.config[receiver].reserve_sat * 1000\n            receiver_reserve_msat = self.config[sender].reserve_sat * 1000\n            num_htlcs_in_ctx = len(self.included_htlcs(ctx_owner, SENT, ctn=ctn) + self.included_htlcs(ctx_owner, RECEIVED, ctn=ctn))\n            feerate = self.get_feerate(ctx_owner, ctn=ctn)\n            ctx_fees_msat = calc_fees_for_commitment_tx(\n                num_htlcs=num_htlcs_in_ctx,\n                feerate=feerate,\n                is_local_initiator=self.constraints.is_initiator,\n                round_to_sat=False,\n                has_anchors=self.has_anchors()\n            )\n            htlc_fee_msat = fee_for_htlc_output(feerate=feerate)\n            htlc_trim_func = received_htlc_trim_threshold_sat if ctx_owner == receiver else offered_htlc_trim_threshold_sat\n            htlc_trim_threshold_msat = htlc_trim_func(dust_limit_sat=self.config[ctx_owner].dust_limit_sat, feerate=feerate, has_anchors=self.has_anchors()) * 1000\n\n            # the sender cannot spend below its reserve\n            max_send_msat = sender_balance_msat - sender_reserve_msat\n\n            # reserve a fee spike buffer\n            # see https://github.com/lightningnetwork/lightning-rfc/pull/740\n            if sender == initiator == LOCAL:\n                fee_spike_buffer = calc_fees_for_commitment_tx(\n                    num_htlcs=num_htlcs_in_ctx + int(not is_htlc_dust) + 1,\n                    feerate=2 * feerate,\n                    is_local_initiator=self.constraints.is_initiator,\n                    round_to_sat=False,\n                    has_anchors=self.has_anchors())[sender]\n                max_send_msat -= fee_spike_buffer\n            # we can't enforce the fee spike buffer on the remote party\n            elif sender == initiator == REMOTE:\n                max_send_msat -= ctx_fees_msat[sender]\n\n            # initiator pays for anchor outputs\n            if sender == initiator and self.has_anchors():\n                max_send_msat -= 2 * FIXED_ANCHOR_SAT * 1000\n\n            # handle the transaction fees for the HTLC transaction\n            if is_htlc_dust:\n                # nobody pays additional HTLC transaction fees\n                return min(max_send_msat, htlc_trim_threshold_msat - 1)\n            else:\n                # somebody has to pay for the additional HTLC transaction fees\n                if sender == initiator:\n                    return max_send_msat - htlc_fee_msat\n                else:\n                    # check if the receiver can afford to pay for the HTLC transaction fees\n                    new_receiver_balance = receiver_balance_msat - receiver_reserve_msat - ctx_fees_msat[receiver] - htlc_fee_msat\n                    if self.has_anchors():\n                        new_receiver_balance -= 2 * FIXED_ANCHOR_SAT * 1000\n                    if new_receiver_balance < 0:\n                        return 0\n                    return max_send_msat\n\n        max_send_msat = min(\n            max(\n                consider_ctx(ctx_owner=receiver, is_htlc_dust=True),\n                consider_ctx(ctx_owner=receiver, is_htlc_dust=False),\n            ),\n            max(\n                consider_ctx(ctx_owner=sender, is_htlc_dust=True),\n                consider_ctx(ctx_owner=sender, is_htlc_dust=False),\n            ),\n        )\n\n        max_send_msat = min(max_send_msat, self.remaining_max_inflight(receiver, strict=True))\n        if self.htlc_slots_left(sender) == 0:\n            max_send_msat = 0\n\n        max_send_msat = max(max_send_msat, 0)\n        return max_send_msat\n\n\n    def included_htlcs(self, subject: HTLCOwner, direction: Direction, ctn: int = None, *,\n                       feerate: int = None) -> List[UpdateAddHtlc]:\n        \"\"\"Returns list of non-dust HTLCs for subject's commitment tx at ctn,\n        filtered by direction (of HTLCs).\n        \"\"\"\n        assert type(subject) is HTLCOwner\n        assert type(direction) is Direction\n        if ctn is None:\n            ctn = self.get_oldest_unrevoked_ctn(subject)\n        if feerate is None:\n            feerate = self.get_feerate(subject, ctn=ctn)\n        conf = self.config[subject]\n        if direction == RECEIVED:\n            threshold_sat = received_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate, has_anchors=self.has_anchors())\n        else:\n            threshold_sat = offered_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate, has_anchors=self.has_anchors())\n        htlcs = self.hm.htlcs_by_direction(subject, direction, ctn=ctn).values()\n        return list(filter(lambda htlc: htlc.amount_msat // 1000 >= threshold_sat, htlcs))\n\n    def get_secret_and_point(self, subject: HTLCOwner, ctn: int) -> Tuple[Optional[bytes], bytes]:\n        assert type(subject) is HTLCOwner\n        assert ctn >= 0, ctn\n        offset = ctn - self.get_oldest_unrevoked_ctn(subject)\n        if subject == REMOTE:\n            if offset > 1:\n                raise RemoteCtnTooFarInFuture(f\"offset: {offset}\")\n            conf = self.config[REMOTE]\n            if offset == 1:\n                secret = None\n                point = conf.next_per_commitment_point\n            elif offset == 0:\n                secret = None\n                point = conf.current_per_commitment_point\n            else:\n                secret = self.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)\n                point = secret_to_pubkey(int.from_bytes(secret, 'big'))\n        else:\n            secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)\n            point = secret_to_pubkey(int.from_bytes(secret, 'big'))\n        return secret, point\n\n    def get_secret_and_commitment(self, subject: HTLCOwner, *, ctn: int) -> Tuple[Optional[bytes], PartialTransaction]:\n        secret, point = self.get_secret_and_point(subject, ctn)\n        ctx = self.make_commitment(subject, point, ctn)\n        return secret, ctx\n\n    def get_commitment(self, subject: HTLCOwner, *, ctn: int) -> PartialTransaction:\n        secret, ctx = self.get_secret_and_commitment(subject, ctn=ctn)\n        return ctx\n\n    def get_next_commitment(self, subject: HTLCOwner) -> PartialTransaction:\n        ctn = self.get_next_ctn(subject)\n        return self.get_commitment(subject, ctn=ctn)\n\n    def get_latest_commitment(self, subject: HTLCOwner) -> PartialTransaction:\n        ctn = self.get_latest_ctn(subject)\n        return self.get_commitment(subject, ctn=ctn)\n\n    def get_oldest_unrevoked_commitment(self, subject: HTLCOwner) -> PartialTransaction:\n        ctn = self.get_oldest_unrevoked_ctn(subject)\n        return self.get_commitment(subject, ctn=ctn)\n\n    def create_sweeptxs_for_watchtower(self, ctn: int) -> List[Transaction]:\n        from .lnsweep import sweep_their_ctx_watchtower\n        from .fee_policy import FeePolicy\n        from .transaction import PartialTxOutput, PartialTransaction\n        secret, ctx = self.get_secret_and_commitment(REMOTE, ctn=ctn)\n        txs = []\n        txins = sweep_their_ctx_watchtower(self, ctx, secret)\n        fee_policy = FeePolicy('eta:2')\n        for txin in txins:\n            output_idx = txin.prevout.out_idx\n            value = ctx.outputs()[output_idx].value\n            tx_size_bytes = 121\n            fee = fee_policy.estimate_fee(tx_size_bytes, network=self.lnworker.network, allow_fallback_to_static_rates=True)\n            outvalue = value - fee\n            sweep_outputs = [PartialTxOutput.from_address_and_value(self.get_sweep_address(), outvalue)]\n            sweep_tx = PartialTransaction.from_io([txin], sweep_outputs, version=2)\n            sig = sweep_tx.sign_txin(0, txin.privkey)\n            txin.witness = txin.make_witness(sig)\n            txs.append(sweep_tx)\n        return txs\n\n    def get_oldest_unrevoked_ctn(self, subject: HTLCOwner) -> int:\n        return self.hm.ctn_oldest_unrevoked(subject)\n\n    def get_latest_ctn(self, subject: HTLCOwner) -> int:\n        return self.hm.ctn_latest(subject)\n\n    def get_next_ctn(self, subject: HTLCOwner) -> int:\n        return self.hm.ctn_latest(subject) + 1\n\n    def total_msat(self, direction: Direction) -> int:\n        \"\"\"Return the cumulative total msat amount received/sent so far.\"\"\"\n        assert type(direction) is Direction\n        return htlcsum(self.hm.all_settled_htlcs_ever_by_direction(LOCAL, direction))\n\n    def settle_htlc(self, preimage: bytes, htlc_id: int) -> None:\n        \"\"\"Settle/fulfill a pending received HTLC.\n        Action must be initiated by LOCAL.\n        \"\"\"\n        self.logger.info(\"settle_htlc\")\n        assert self.can_update_ctx(proposer=LOCAL), f\"cannot update channel. {self.get_state()!r} {self.peer_state!r}\"\n        htlc = self.hm.get_htlc_by_id(REMOTE, htlc_id)\n        if htlc.payment_hash != sha256(preimage):\n            raise Exception(\"incorrect preimage for HTLC\")\n        assert htlc_id not in self.hm.log[REMOTE]['settles']\n        self.hm.send_settle(htlc_id)\n        self.htlc_settle_time[htlc_id] = now()\n        self.lnworker.save_preimage(htlc.payment_hash, preimage, mark_as_public=True)\n\n    def get_payment_hash(self, htlc_id: int) -> bytes:\n        htlc = self.hm.get_htlc_by_id(LOCAL, htlc_id)\n        return htlc.payment_hash\n\n    def receive_htlc_settle(self, preimage: bytes, htlc_id: int) -> None:\n        \"\"\"Settle/fulfill a pending offered HTLC.\n        Action must be initiated by REMOTE.\n        \"\"\"\n        self.logger.info(\"receive_htlc_settle\")\n        assert self.can_update_ctx(proposer=REMOTE), f\"cannot update channel. {self.get_state()!r} {self.peer_state!r}\"\n        htlc = self.hm.get_htlc_by_id(LOCAL, htlc_id)\n        if htlc.payment_hash != sha256(preimage):\n            raise RemoteMisbehaving(\"received incorrect preimage for HTLC\")\n        assert htlc_id not in self.hm.log[LOCAL]['settles']\n        with self.db_lock:\n            self.hm.recv_settle(htlc_id)\n        self.lnworker.save_preimage(htlc.payment_hash, preimage, mark_as_public=True)\n\n    def fail_htlc(self, htlc_id: int) -> None:\n        \"\"\"Fail a pending received HTLC.\n        Action must be initiated by LOCAL.\n        \"\"\"\n        self.logger.info(\"fail_htlc\")\n        assert self.can_update_ctx(proposer=LOCAL), f\"cannot update channel. {self.get_state()!r} {self.peer_state!r}\"\n        with self.db_lock:\n            self.hm.send_fail(htlc_id)\n\n    def receive_fail_htlc(self, htlc_id: int, *,\n                          error_bytes: Optional[bytes],\n                          reason: Optional[OnionRoutingFailure] = None) -> None:\n        \"\"\"Fail a pending offered HTLC.\n        Action must be initiated by REMOTE.\n        \"\"\"\n        self.logger.info(\"receive_fail_htlc\")\n        assert self.can_update_ctx(proposer=REMOTE), f\"cannot update channel. {self.get_state()!r} {self.peer_state!r}\"\n        with self.db_lock:\n            self.hm.recv_fail(htlc_id)\n        self._receive_fail_reasons[htlc_id] = (error_bytes, reason)\n\n    def get_next_fee(self, subject: HTLCOwner) -> int:\n        return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(subject).outputs())\n\n    def get_latest_fee(self, subject: HTLCOwner) -> int:\n        return self.constraints.capacity - sum(x.value for x in self.get_latest_commitment(subject).outputs())\n\n    def update_fee(self, feerate: int, from_us: bool) -> None:\n        # feerate uses sat/kw\n        if self.constraints.is_initiator != from_us:\n            raise Exception(f\"Cannot update_fee: wrong initiator. us: {from_us}\")\n        if feerate < FEERATE_PER_KW_MIN_RELAY_LIGHTNING:\n            raise Exception(f\"Cannot update_fee: feerate lower than min relay fee. {feerate} sat/kw. us: {from_us}\")\n        sender = LOCAL if from_us else REMOTE\n        ctx_owner = -sender\n        ctn = self.get_next_ctn(ctx_owner)\n        sender_balance_msat = self.balance_minus_outgoing_htlcs(whose=sender, ctx_owner=ctx_owner, ctn=ctn)\n        sender_reserve_msat = self.config[-sender].reserve_sat * 1000\n        num_htlcs_in_ctx = len(self.included_htlcs(ctx_owner, SENT, ctn=ctn, feerate=feerate) +\n                               self.included_htlcs(ctx_owner, RECEIVED, ctn=ctn, feerate=feerate))\n        ctx_fees_msat = calc_fees_for_commitment_tx(\n            num_htlcs=num_htlcs_in_ctx,\n            feerate=feerate,\n            is_local_initiator=self.constraints.is_initiator,\n            has_anchors=self.has_anchors()\n        )\n        remainder = sender_balance_msat - sender_reserve_msat - ctx_fees_msat[sender]\n        if remainder < 0:\n            raise Exception(f\"Cannot update_fee. {sender} tried to update fee but they cannot afford it. \"\n                            f\"Their balance would go below reserve: {remainder} msat missing.\")\n        assert self.can_update_ctx(proposer=LOCAL if from_us else REMOTE), f\"cannot update channel. {self.get_state()!r} {self.peer_state!r}. {from_us=}\"\n        with self.db_lock:\n            if from_us:\n                self.hm.send_update_fee(feerate)\n            else:\n                self.hm.recv_update_fee(feerate)\n\n    def make_commitment(self, subject: HTLCOwner, this_point: bytes, ctn: int) -> PartialTransaction:\n        assert type(subject) is HTLCOwner\n        feerate = self.get_feerate(subject, ctn=ctn)\n        other = subject.inverted()\n        local_msat = self.balance(subject, ctx_owner=subject, ctn=ctn)\n        remote_msat = self.balance(other, ctx_owner=subject, ctn=ctn)\n        received_htlcs = self.hm.htlcs_by_direction(subject, RECEIVED, ctn).values()\n        sent_htlcs = self.hm.htlcs_by_direction(subject, SENT, ctn).values()\n        remote_msat -= htlcsum(received_htlcs)\n        local_msat -= htlcsum(sent_htlcs)\n        assert remote_msat >= 0\n        assert local_msat >= 0\n        # same htlcs as before, but now without dust.\n        received_htlcs = self.included_htlcs(subject, RECEIVED, ctn)\n        sent_htlcs = self.included_htlcs(subject, SENT, ctn)\n\n        this_config = self.config[subject]\n        other_config = self.config[-subject]\n        other_htlc_pubkey = derive_pubkey(other_config.htlc_basepoint.pubkey, this_point)\n        this_htlc_pubkey = derive_pubkey(this_config.htlc_basepoint.pubkey, this_point)\n        other_revocation_pubkey = derive_blinded_pubkey(other_config.revocation_basepoint.pubkey, this_point)\n        htlcs = []  # type: List[ScriptHtlc]\n        for is_received_htlc, htlc_list in zip((True, False), (received_htlcs, sent_htlcs)):\n            for htlc in htlc_list:\n                htlcs.append(ScriptHtlc(make_htlc_output_witness_script(\n                    is_received_htlc=is_received_htlc,\n                    remote_revocation_pubkey=other_revocation_pubkey,\n                    remote_htlc_pubkey=other_htlc_pubkey,\n                    local_htlc_pubkey=this_htlc_pubkey,\n                    payment_hash=htlc.payment_hash,\n                    cltv_abs=htlc.cltv_abs,\n                    has_anchors=self.has_anchors()), htlc))\n        # note: maybe flip initiator here for fee purposes, we want LOCAL and REMOTE\n        #       in the resulting dict to correspond to the to_local and to_remote *outputs* of the ctx\n        onchain_fees = calc_fees_for_commitment_tx(\n            num_htlcs=len(htlcs),\n            feerate=feerate,\n            is_local_initiator=self.constraints.is_initiator == (subject == LOCAL),\n            has_anchors=self.has_anchors(),\n        )\n        assert self.is_static_remotekey_enabled()\n        payment_pubkey = other_config.payment_basepoint.pubkey\n        return make_commitment(\n            ctn=ctn,\n            local_funding_pubkey=this_config.multisig_key.pubkey,\n            remote_funding_pubkey=other_config.multisig_key.pubkey,\n            remote_payment_pubkey=payment_pubkey,\n            funder_payment_basepoint=self.config[LOCAL if     self.constraints.is_initiator else REMOTE].payment_basepoint.pubkey,\n            fundee_payment_basepoint=self.config[LOCAL if not self.constraints.is_initiator else REMOTE].payment_basepoint.pubkey,\n            revocation_pubkey=other_revocation_pubkey,\n            delayed_pubkey=derive_pubkey(this_config.delayed_basepoint.pubkey, this_point),\n            to_self_delay=other_config.to_self_delay,\n            funding_txid=self.funding_outpoint.txid,\n            funding_pos=self.funding_outpoint.output_index,\n            funding_sat=self.constraints.capacity,\n            local_amount=local_msat,\n            remote_amount=remote_msat,\n            dust_limit_sat=this_config.dust_limit_sat,\n            fees_per_participant=onchain_fees,\n            htlcs=htlcs,\n            has_anchors=self.has_anchors()\n        )\n\n    def make_closing_tx(self, local_script: bytes, remote_script: bytes,\n                        fee_sat: int, *, drop_remote = False) -> Tuple[bytes, PartialTransaction]:\n        \"\"\" cooperative close \"\"\"\n        _, outputs = make_commitment_outputs(\n            fees_per_participant={\n                LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0,\n                REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0,\n            },\n            local_amount_msat=self.balance(LOCAL),\n            remote_amount_msat=self.balance(REMOTE) if not drop_remote else 0,\n            local_script=local_script,\n            remote_script=remote_script,\n            htlcs=[],\n            dust_limit_sat=self.config[LOCAL].dust_limit_sat,\n            has_anchors=self.has_anchors(),\n            local_anchor_script=None,\n            remote_anchor_script=None,\n        )\n\n        closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey,\n                                     self.config[REMOTE].multisig_key.pubkey,\n                                     funding_txid=self.funding_outpoint.txid,\n                                     funding_pos=self.funding_outpoint.output_index,\n                                     funding_sat=self.constraints.capacity,\n                                     outputs=outputs)\n\n        der_sig = closing_tx.sign_txin(0, self.config[LOCAL].multisig_key.privkey)\n        sig = ecc.ecdsa_sig64_from_der_sig(der_sig[:-1])\n        return sig, closing_tx\n\n    def signature_fits(self, tx: PartialTransaction) -> bool:\n        remote_sig = self.config[LOCAL].current_commitment_signature\n        pre_hash = tx.serialize_preimage(0)\n        msg_hash = sha256d(pre_hash)\n        assert remote_sig\n        res = ECPubkey(self.config[REMOTE].multisig_key.pubkey).ecdsa_verify(remote_sig, msg_hash)\n        return res\n\n    def force_close_tx(self) -> PartialTransaction:\n        tx = self.get_latest_commitment(LOCAL)\n        assert self.signature_fits(tx)\n        tx.sign({self.config[LOCAL].multisig_key.pubkey: self.config[LOCAL].multisig_key.privkey})\n        remote_sig = self.config[LOCAL].current_commitment_signature\n        remote_sig = ecc.ecdsa_der_sig_from_ecdsa_sig64(remote_sig) + Sighash.to_sigbytes(Sighash.ALL)\n        tx.add_signature_to_txin(txin_idx=0,\n                                 signing_pubkey=self.config[REMOTE].multisig_key.pubkey,\n                                 sig=remote_sig)\n        assert tx.is_complete()\n        return tx\n\n    def get_close_options(self) -> Sequence[ChanCloseOption]:\n        # This method is used both in the GUI, and in lnpeer.schedule_force_closing\n        # in the latter case, the result does not depend on peer_state\n        ret = []\n        if not self.is_closed() and self.peer_state == PeerState.GOOD:\n            # If there are unsettled HTLCs, although is possible to cooperatively close,\n            # we choose not to expose that option in the GUI, because it is very likely\n            # that HTLCs will take a long time to settle (submarine swap, or stuck payment),\n            # and the close dialog would be taking a very long time to finish\n            if not self.has_unsettled_htlcs():\n                ret.append(ChanCloseOption.COOP_CLOSE)\n                ret.append(ChanCloseOption.REQUEST_REMOTE_FCLOSE)\n        if self.get_state() == ChannelState.WE_ARE_TOXIC:\n            ret.append(ChanCloseOption.REQUEST_REMOTE_FCLOSE)\n        if not self.is_closed() or self.get_state() == ChannelState.REQUESTED_FCLOSE:\n            ret.append(ChanCloseOption.LOCAL_FCLOSE)\n        assert not (self.get_state() == ChannelState.WE_ARE_TOXIC and ChanCloseOption.LOCAL_FCLOSE in ret), \"local force-close unsafe if we are toxic\"\n        return ret\n\n    def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, MaybeSweepInfo]:\n        # look at the output address, check if it matches\n        d = sweep_their_htlctx_justice(self, ctx, htlc_tx)\n        d2 = sweep_our_htlctx(self, ctx, htlc_tx)\n        d.update(d2)\n        return d\n\n    def has_pending_changes(self, subject: HTLCOwner) -> bool:\n        next_htlcs = self.hm.get_htlcs_in_next_ctx(subject)\n        latest_htlcs = self.hm.get_htlcs_in_latest_ctx(subject)\n        return not (next_htlcs == latest_htlcs and self.get_next_feerate(subject) == self.get_latest_feerate(subject))\n\n    def should_be_closed_due_to_expiring_htlcs(self, local_height: int) -> bool:\n        htlcs_we_could_reclaim = {}  # type: Dict[Tuple[Direction, int], UpdateAddHtlc]\n        # If there is a received HTLC for which we already released the preimage\n        # but the remote did not revoke yet, and the CLTV of this HTLC is dangerously close\n        # to the present, then unilaterally close channel\n        recv_htlc_deadline_delta = lnutil.NBLOCK_DEADLINE_DELTA_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS\n        for sub, dir, ctn in ((LOCAL, RECEIVED, self.get_latest_ctn(LOCAL)),\n                              (REMOTE, SENT, self.get_oldest_unrevoked_ctn(REMOTE)),\n                              (REMOTE, SENT, self.get_latest_ctn(REMOTE)),):\n            for htlc_id, htlc in self.hm.htlcs_by_direction(subject=sub, direction=dir, ctn=ctn).items():\n                if not self.hm.was_htlc_preimage_released(htlc_id=htlc_id, htlc_proposer=REMOTE):\n                    continue\n                if htlc.cltv_abs - recv_htlc_deadline_delta > local_height:\n                    continue\n                # Do not force-close if we just sent fulfill_htlc and have not received revack yet\n                if htlc_id in self.htlc_settle_time and now() - self.htlc_settle_time[htlc_id] < 30:\n                    continue\n                htlcs_we_could_reclaim[(RECEIVED, htlc_id)] = htlc\n        # If there is an offered HTLC which has already expired (+ some grace period after), we\n        # will unilaterally close the channel and time out the HTLC\n        offered_htlc_deadline_delta = lnutil.NBLOCK_DEADLINE_DELTA_AFTER_EXPIRY_FOR_OFFERED_HTLCS\n        for sub, dir, ctn in ((LOCAL, SENT, self.get_latest_ctn(LOCAL)),\n                              (REMOTE, RECEIVED, self.get_oldest_unrevoked_ctn(REMOTE)),\n                              (REMOTE, RECEIVED, self.get_latest_ctn(REMOTE)),):\n            for htlc_id, htlc in self.hm.htlcs_by_direction(subject=sub, direction=dir, ctn=ctn).items():\n                if htlc.cltv_abs + offered_htlc_deadline_delta > local_height:\n                    continue\n                htlcs_we_could_reclaim[(SENT, htlc_id)] = htlc\n        # Note: previously we used a threshold concept, \"min_value_worth_closing_channel_over_sat\", and\n        #       only force-closed the channel if the total value of these expiring htlcs was large enough.\n        #       However, if we are forwarding, and an outgoing htlc expires, we should always close\n        #       the outgoing channel (regardless of htlc value), so that we can propagate back the\n        #       removal of the htlc in the incoming channel.\n        return len(htlcs_we_could_reclaim) > 0\n\n    def is_funding_tx_mined(self, funding_height):\n        funding_txid = self.funding_outpoint.txid\n        funding_idx = self.funding_outpoint.output_index\n        conf = funding_height.conf\n        if conf < self.funding_txn_minimum_depth():\n            #self.logger.info(f\"funding tx is still not at sufficient depth. actual depth: {conf}\")\n            return False\n        assert conf > 0 or self.is_zeroconf()\n        # check funding_tx amount and script\n        funding_tx = self.lnworker.lnwatcher.adb.get_transaction(funding_txid)\n        if not funding_tx:\n            self.logger.info(f\"no funding_tx {funding_txid}\")\n            return False\n        outp = funding_tx.outputs()[funding_idx]\n        redeem_script = funding_output_script(self.config[REMOTE], self.config[LOCAL])\n        funding_address = redeem_script_to_address('p2wsh', redeem_script)\n        funding_sat = self.constraints.capacity\n        if not (outp.address == funding_address and outp.value == funding_sat):\n            self.logger.info('funding outpoint mismatch')\n            return False\n        return True\n"
  },
  {
    "path": "electrum/lnhtlc.py",
    "content": "from copy import deepcopy\nfrom typing import Sequence, Tuple, Dict, TYPE_CHECKING, Set\n\nfrom .lnutil import SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, UpdateAddHtlc, Direction, FeeUpdate\nfrom .util import bfh, with_lock\n\nif TYPE_CHECKING:\n    from .json_db import StoredDict\n\nLOG_TEMPLATE = {\n    'adds': {},              # \"side who offered htlc\" -> htlc_id -> htlc\n    'locked_in': {},         # \"side who offered htlc\" -> action -> htlc_id -> whose ctx -> ctn\n    'settles': {},           # \"side who offered htlc\" -> action -> htlc_id -> whose ctx -> ctn\n    'fails': {},             # \"side who offered htlc\" -> action -> htlc_id -> whose ctx -> ctn\n    'fee_updates': {},       # \"side who initiated fee update\" -> index -> list of FeeUpdates\n    'revack_pending': False,\n    'next_htlc_id': 0,\n    'ctn': -1,               # oldest unrevoked ctx of sub\n}\n\n\nclass HTLCManager:\n\n    def __init__(self, log: 'StoredDict', *, initiator=None, initial_feerate=None):\n\n        if len(log) == 0:\n            # note: \"htlc_id\" keys in dict are str! but due to json_db magic they can *almost* be treated as int...\n            log[LOCAL] = deepcopy(LOG_TEMPLATE)\n            log[REMOTE] = deepcopy(LOG_TEMPLATE)\n            log[LOCAL]['unacked_updates'] = {}\n            log[LOCAL]['was_revoke_last'] = False\n\n        # maybe bootstrap fee_updates if initial_feerate was provided\n        if initial_feerate is not None:\n            assert type(initial_feerate) is int\n            assert initiator in [LOCAL, REMOTE]\n            log[initiator]['fee_updates'][0] = FeeUpdate(rate=initial_feerate, ctn_local=0, ctn_remote=0)\n        self.log = log\n\n        # We need a lock as many methods of HTLCManager are accessed by both the asyncio thread and the GUI.\n        # lnchannel sometimes calls us with Channel.db_lock (== log.lock) already taken,\n        # and we ourselves often take log.lock (via StoredDict.__getitem__).\n        # Hence, to avoid deadlocks, we reuse this same lock.\n        self.lock = log.lock\n\n        self._init_maybe_active_htlc_ids()\n\n    @with_lock\n    def ctn_latest(self, sub: HTLCOwner) -> int:\n        \"\"\"Return the ctn for the latest (newest that has a valid sig) ctx of sub\"\"\"\n        return self.ctn_oldest_unrevoked(sub) + int(self.is_revack_pending(sub))\n\n    def ctn_oldest_unrevoked(self, sub: HTLCOwner) -> int:\n        \"\"\"Return the ctn for the oldest unrevoked ctx of sub\"\"\"\n        return self.log[sub]['ctn']\n\n    def is_revack_pending(self, sub: HTLCOwner) -> bool:\n        \"\"\"Returns True iff sub was sent commitment_signed but they did not\n        send revoke_and_ack yet (sub has multiple unrevoked ctxs)\n        \"\"\"\n        return self.log[sub]['revack_pending']\n\n    def _set_revack_pending(self, sub: HTLCOwner, pending: bool) -> None:\n        self.log[sub]['revack_pending'] = pending\n\n    def get_next_htlc_id(self, sub: HTLCOwner) -> int:\n        return self.log[sub]['next_htlc_id']\n\n    ##### Actions on channel:\n\n    @with_lock\n    def channel_open_finished(self):\n        self.log[LOCAL]['ctn'] = 0\n        self.log[REMOTE]['ctn'] = 0\n        self._set_revack_pending(LOCAL, False)\n        self._set_revack_pending(REMOTE, False)\n\n    @with_lock\n    def send_htlc(self, htlc: UpdateAddHtlc) -> UpdateAddHtlc:\n        htlc_id = htlc.htlc_id\n        if htlc_id != self.get_next_htlc_id(LOCAL):\n            raise Exception(f\"unexpected local htlc_id. next should be \"\n                            f\"{self.get_next_htlc_id(LOCAL)} but got {htlc_id}\")\n        self.log[LOCAL]['adds'][htlc_id] = htlc\n        self.log[LOCAL]['locked_in'][htlc_id] = {LOCAL: None, REMOTE: self.ctn_latest(REMOTE)+1}\n        self.log[LOCAL]['next_htlc_id'] += 1\n        self._maybe_active_htlc_ids[LOCAL].add(htlc_id)\n        return htlc\n\n    @with_lock\n    def recv_htlc(self, htlc: UpdateAddHtlc) -> None:\n        htlc_id = htlc.htlc_id\n        if htlc_id != self.get_next_htlc_id(REMOTE):\n            raise Exception(f\"unexpected remote htlc_id. next should be \"\n                            f\"{self.get_next_htlc_id(REMOTE)} but got {htlc_id}\")\n        self.log[REMOTE]['adds'][htlc_id] = htlc\n        self.log[REMOTE]['locked_in'][htlc_id] = {LOCAL: self.ctn_latest(LOCAL)+1, REMOTE: None}\n        self.log[REMOTE]['next_htlc_id'] += 1\n        self._maybe_active_htlc_ids[REMOTE].add(htlc_id)\n\n    @with_lock\n    def send_settle(self, htlc_id: int) -> None:\n        next_ctn = self.ctn_latest(REMOTE) + 1\n        if not self.is_htlc_active_at_ctn(ctx_owner=REMOTE, ctn=next_ctn, htlc_proposer=REMOTE, htlc_id=htlc_id):\n            raise Exception(f\"(local) cannot remove htlc that is not there...\")\n        self.log[REMOTE]['settles'][htlc_id] = {LOCAL: None, REMOTE: next_ctn}\n\n    @with_lock\n    def recv_settle(self, htlc_id: int) -> None:\n        next_ctn = self.ctn_latest(LOCAL) + 1\n        if not self.is_htlc_active_at_ctn(ctx_owner=LOCAL, ctn=next_ctn, htlc_proposer=LOCAL, htlc_id=htlc_id):\n            raise Exception(f\"(remote) cannot remove htlc that is not there...\")\n        self.log[LOCAL]['settles'][htlc_id] = {LOCAL: next_ctn, REMOTE: None}\n\n    @with_lock\n    def send_fail(self, htlc_id: int) -> None:\n        next_ctn = self.ctn_latest(REMOTE) + 1\n        if not self.is_htlc_active_at_ctn(ctx_owner=REMOTE, ctn=next_ctn, htlc_proposer=REMOTE, htlc_id=htlc_id):\n            raise Exception(f\"(local) cannot remove htlc that is not there...\")\n        self.log[REMOTE]['fails'][htlc_id] = {LOCAL: None, REMOTE: next_ctn}\n\n    @with_lock\n    def recv_fail(self, htlc_id: int) -> None:\n        next_ctn = self.ctn_latest(LOCAL) + 1\n        if not self.is_htlc_active_at_ctn(ctx_owner=LOCAL, ctn=next_ctn, htlc_proposer=LOCAL, htlc_id=htlc_id):\n            raise Exception(f\"(remote) cannot remove htlc that is not there...\")\n        self.log[LOCAL]['fails'][htlc_id] = {LOCAL: next_ctn, REMOTE: None}\n\n    @with_lock\n    def send_update_fee(self, feerate: int) -> None:\n        fee_update = FeeUpdate(rate=feerate,\n                               ctn_local=None, ctn_remote=self.ctn_latest(REMOTE) + 1)\n        self._new_feeupdate(fee_update, subject=LOCAL)\n\n    @with_lock\n    def recv_update_fee(self, feerate: int) -> None:\n        fee_update = FeeUpdate(rate=feerate,\n                               ctn_local=self.ctn_latest(LOCAL) + 1, ctn_remote=None)\n        self._new_feeupdate(fee_update, subject=REMOTE)\n\n    @with_lock\n    def _new_feeupdate(self, fee_update: FeeUpdate, subject: HTLCOwner) -> None:\n        # overwrite last fee update if not yet committed to by anyone; otherwise append\n        d = self.log[subject]['fee_updates']\n        #assert type(d) is StoredDict\n        n = len(d)\n        last_fee_update = d[n-1]\n        if (last_fee_update.ctn_local is None or last_fee_update.ctn_local > self.ctn_latest(LOCAL)) \\\n                and (last_fee_update.ctn_remote is None or last_fee_update.ctn_remote > self.ctn_latest(REMOTE)):\n            d[n-1] = fee_update\n        else:\n            d[n] = fee_update\n\n    @with_lock\n    def send_ctx(self) -> None:\n        assert self.ctn_latest(REMOTE) == self.ctn_oldest_unrevoked(REMOTE), (self.ctn_latest(REMOTE), self.ctn_oldest_unrevoked(REMOTE))\n        self._set_revack_pending(REMOTE, True)\n        self.log[LOCAL]['was_revoke_last'] = False\n\n    @with_lock\n    def recv_ctx(self) -> None:\n        assert self.ctn_latest(LOCAL) == self.ctn_oldest_unrevoked(LOCAL), (self.ctn_latest(LOCAL), self.ctn_oldest_unrevoked(LOCAL))\n        self._set_revack_pending(LOCAL, True)\n\n    @with_lock\n    def send_rev(self) -> None:\n        self.log[LOCAL]['ctn'] += 1\n        self._set_revack_pending(LOCAL, False)\n        self.log[LOCAL]['was_revoke_last'] = True\n        # htlcs\n        for htlc_id in self._maybe_active_htlc_ids[REMOTE]:\n            ctns = self.log[REMOTE]['locked_in'][htlc_id]\n            if ctns[REMOTE] is None and ctns[LOCAL] <= self.ctn_latest(LOCAL):\n                ctns[REMOTE] = self.ctn_latest(REMOTE) + 1\n        for log_action in ('settles', 'fails'):\n            for htlc_id in self._maybe_active_htlc_ids[LOCAL]:\n                ctns = self.log[LOCAL][log_action].get(htlc_id, None)\n                if ctns is None: continue\n                if ctns[REMOTE] is None and ctns[LOCAL] <= self.ctn_latest(LOCAL):\n                    ctns[REMOTE] = self.ctn_latest(REMOTE) + 1\n        self._update_maybe_active_htlc_ids()\n        # fee updates\n        for k, fee_update in list(self.log[REMOTE]['fee_updates'].items()):\n            if fee_update.ctn_remote is None and fee_update.ctn_local <= self.ctn_latest(LOCAL):\n                fee_update.ctn_remote = self.ctn_latest(REMOTE) + 1\n\n    @with_lock\n    def recv_rev(self) -> None:\n        self.log[REMOTE]['ctn'] += 1\n        self._set_revack_pending(REMOTE, False)\n        # htlcs\n        for htlc_id in self._maybe_active_htlc_ids[LOCAL]:\n            ctns = self.log[LOCAL]['locked_in'][htlc_id]\n            if ctns[LOCAL] is None and ctns[REMOTE] <= self.ctn_latest(REMOTE):\n                ctns[LOCAL] = self.ctn_latest(LOCAL) + 1\n        for log_action in ('settles', 'fails'):\n            for htlc_id in self._maybe_active_htlc_ids[REMOTE]:\n                ctns = self.log[REMOTE][log_action].get(htlc_id, None)\n                if ctns is None: continue\n                if ctns[LOCAL] is None and ctns[REMOTE] <= self.ctn_latest(REMOTE):\n                    ctns[LOCAL] = self.ctn_latest(LOCAL) + 1\n        self._update_maybe_active_htlc_ids()\n        # fee updates\n        for k, fee_update in list(self.log[LOCAL]['fee_updates'].items()):\n            if fee_update.ctn_local is None and fee_update.ctn_remote <= self.ctn_latest(REMOTE):\n                fee_update.ctn_local = self.ctn_latest(LOCAL) + 1\n\n        # no need to keep local update raw msgs anymore, they have just been ACKed.\n        self.log[LOCAL]['unacked_updates'].pop(self.log[REMOTE]['ctn'], None)\n\n    @with_lock\n    def _update_maybe_active_htlc_ids(self) -> None:\n        # - Loosely, we want a set that contains the htlcs that are\n        #   not \"removed and revoked from all ctxs of both parties\". (self._maybe_active_htlc_ids)\n        #   It is guaranteed that those htlcs are in the set, but older htlcs might be there too:\n        #   there is a sanity margin of 1 ctn -- this relaxes the care needed re order of method calls.\n        # - balance_delta is in sync with maybe_active_htlc_ids. When htlcs are removed from the latter,\n        #   balance_delta is updated to reflect that htlc.\n        sanity_margin = 1\n        for htlc_proposer in (LOCAL, REMOTE):\n            for log_action in ('settles', 'fails'):\n                for htlc_id in list(self._maybe_active_htlc_ids[htlc_proposer]):\n                    ctns = self.log[htlc_proposer][log_action].get(htlc_id, None)\n                    if ctns is None: continue\n                    if (ctns[LOCAL] is not None\n                            and ctns[LOCAL] <= self.ctn_oldest_unrevoked(LOCAL) - sanity_margin\n                            and ctns[REMOTE] is not None\n                            and ctns[REMOTE] <= self.ctn_oldest_unrevoked(REMOTE) - sanity_margin):\n                        self._maybe_active_htlc_ids[htlc_proposer].remove(htlc_id)\n                        if log_action == 'settles':\n                            htlc = self.log[htlc_proposer]['adds'][htlc_id]  # type: UpdateAddHtlc\n                            self._balance_delta -= htlc.amount_msat * htlc_proposer\n\n    @with_lock\n    def _init_maybe_active_htlc_ids(self):\n        # first idx is \"side who offered htlc\":\n        self._maybe_active_htlc_ids = {LOCAL: set(), REMOTE: set()}  # type: Dict[HTLCOwner, Set[int]]\n        # add all htlcs\n        self._balance_delta = 0  # the balance delta of LOCAL since channel open\n        for htlc_proposer in (LOCAL, REMOTE):\n            for htlc_id in self.log[htlc_proposer]['adds']:\n                self._maybe_active_htlc_ids[htlc_proposer].add(htlc_id)\n        # remove old htlcs\n        self._update_maybe_active_htlc_ids()\n\n    @with_lock\n    def discard_unsigned_remote_updates(self):\n        \"\"\"Discard updates sent by the remote, that the remote itself\n        did not yet sign (i.e. there was no corresponding commitment_signed msg)\n        \"\"\"\n        # htlcs added\n        for htlc_id, ctns in list(self.log[REMOTE]['locked_in'].items()):\n            if ctns[LOCAL] > self.ctn_latest(LOCAL):\n                del self.log[REMOTE]['locked_in'][htlc_id]\n                del self.log[REMOTE]['adds'][htlc_id]\n                self._maybe_active_htlc_ids[REMOTE].discard(htlc_id)\n        if self.log[REMOTE]['locked_in']:\n            self.log[REMOTE]['next_htlc_id'] = max([int(x) for x in self.log[REMOTE]['locked_in'].keys()]) + 1\n        else:\n            self.log[REMOTE]['next_htlc_id'] = 0\n        # htlcs removed\n        for log_action in ('settles', 'fails'):\n            for htlc_id, ctns in list(self.log[LOCAL][log_action].items()):\n                if ctns[LOCAL] > self.ctn_latest(LOCAL):\n                    del self.log[LOCAL][log_action][htlc_id]\n        # fee updates\n        for k, fee_update in list(self.log[REMOTE]['fee_updates'].items()):\n            if fee_update.ctn_local > self.ctn_latest(LOCAL):\n                self.log[REMOTE]['fee_updates'].pop(k)\n\n    @with_lock\n    def store_local_update_raw_msg(self, raw_update_msg: bytes, *, is_commitment_signed: bool) -> None:\n        \"\"\"We need to be able to replay unacknowledged updates we sent to the remote\n        in case of disconnections. Hence, raw update and commitment_signed messages\n        are stored temporarily (until they are acked).\"\"\"\n        # self.log[LOCAL]['unacked_updates'][ctn_idx] is a list of raw messages\n        # containing some number of updates and then a single commitment_signed\n        if is_commitment_signed:\n            ctn_idx = self.ctn_latest(REMOTE)\n        else:\n            ctn_idx = self.ctn_latest(REMOTE) + 1\n        l = self.log[LOCAL]['unacked_updates'].get(ctn_idx, [])\n        l.append(raw_update_msg.hex())\n        self.log[LOCAL]['unacked_updates'][ctn_idx] = l\n\n    @with_lock\n    def get_unacked_local_updates(self) -> Dict[int, Sequence[bytes]]:\n        #return self.log[LOCAL]['unacked_updates']\n        return {ctn: [bfh(msg) for msg in messages]\n                for ctn, messages in self.log[LOCAL]['unacked_updates'].items()}\n\n    @with_lock\n    def was_revoke_last(self) -> bool:\n        \"\"\"Whether we sent a revoke_and_ack after the last commitment_signed we sent.\"\"\"\n        return self.log[LOCAL].get('was_revoke_last') or False\n\n    ##### Queries re HTLCs:\n\n    def get_htlc_by_id(self, htlc_proposer: HTLCOwner, htlc_id: int) -> UpdateAddHtlc:\n        return self.log[htlc_proposer]['adds'][htlc_id]\n\n    @with_lock\n    def is_htlc_active_at_ctn(self, *, ctx_owner: HTLCOwner, ctn: int,\n                              htlc_proposer: HTLCOwner, htlc_id: int) -> bool:\n        htlc_id = int(htlc_id)\n        if htlc_id >= self.get_next_htlc_id(htlc_proposer):\n            return False\n        settles = self.log[htlc_proposer]['settles']\n        fails = self.log[htlc_proposer]['fails']\n        ctns = self.log[htlc_proposer]['locked_in'][htlc_id]\n        if ctns[ctx_owner] is not None and ctns[ctx_owner] <= ctn:\n            not_settled = htlc_id not in settles or settles[htlc_id][ctx_owner] is None or settles[htlc_id][ctx_owner] > ctn\n            not_failed = htlc_id not in fails or fails[htlc_id][ctx_owner] is None or fails[htlc_id][ctx_owner] > ctn\n            if not_settled and not_failed:\n                return True\n        return False\n\n    @with_lock\n    def is_htlc_irrevocably_added_yet(\n            self,\n            *,\n            ctx_owner: HTLCOwner = None,\n            htlc_proposer: HTLCOwner,\n            htlc_id: int,\n    ) -> bool:\n        \"\"\"Returns whether `add_htlc` was irrevocably committed to `ctx_owner's` ctx.\n        If `ctx_owner` is None, both parties' ctxs are checked.\n        \"\"\"\n        in_local = self._is_htlc_irrevocably_added_yet(\n            ctx_owner=LOCAL, htlc_proposer=htlc_proposer, htlc_id=htlc_id)\n        in_remote = self._is_htlc_irrevocably_added_yet(\n            ctx_owner=REMOTE, htlc_proposer=htlc_proposer, htlc_id=htlc_id)\n        if ctx_owner is None:\n            return in_local and in_remote\n        elif ctx_owner == LOCAL:\n            return in_local\n        elif ctx_owner == REMOTE:\n            return in_remote\n        else:\n            raise Exception(f\"unexpected ctx_owner: {ctx_owner!r}\")\n\n    @with_lock\n    def _is_htlc_irrevocably_added_yet(\n            self,\n            *,\n            ctx_owner: HTLCOwner,\n            htlc_proposer: HTLCOwner,\n            htlc_id: int,\n    ) -> bool:\n        if htlc_id >= self.get_next_htlc_id(htlc_proposer):\n            return False\n        ctns = self.log[htlc_proposer]['locked_in'][htlc_id]\n        if ctns[ctx_owner] is None:\n            return False\n        return ctns[ctx_owner] <= self.ctn_oldest_unrevoked(ctx_owner)\n\n    @with_lock\n    def is_htlc_irrevocably_removed_yet(\n            self,\n            *,\n            ctx_owner: HTLCOwner = None,\n            htlc_proposer: HTLCOwner,\n            htlc_id: int,\n    ) -> bool:\n        \"\"\"Returns whether the removal of an htlc was irrevocably committed to `ctx_owner's` ctx.\n        The removal can either be a fulfill/settle or a fail; they are not distinguished.\n        If `ctx_owner` is None, both parties' ctxs are checked.\n        \"\"\"\n        in_local = self._is_htlc_irrevocably_removed_yet(\n            ctx_owner=LOCAL, htlc_proposer=htlc_proposer, htlc_id=htlc_id)\n        in_remote = self._is_htlc_irrevocably_removed_yet(\n            ctx_owner=REMOTE, htlc_proposer=htlc_proposer, htlc_id=htlc_id)\n        if ctx_owner is None:\n            return in_local and in_remote\n        elif ctx_owner == LOCAL:\n            return in_local\n        elif ctx_owner == REMOTE:\n            return in_remote\n        else:\n            raise Exception(f\"unexpected ctx_owner: {ctx_owner!r}\")\n\n    @with_lock\n    def _is_htlc_irrevocably_removed_yet(\n            self,\n            *,\n            ctx_owner: HTLCOwner,\n            htlc_proposer: HTLCOwner,\n            htlc_id: int,\n    ) -> bool:\n        if htlc_id >= self.get_next_htlc_id(htlc_proposer):\n            return False\n        if htlc_id in self.log[htlc_proposer]['settles']:\n            ctn_of_settle = self.log[htlc_proposer]['settles'][htlc_id][ctx_owner]\n        else:\n            ctn_of_settle = None\n        if htlc_id in self.log[htlc_proposer]['fails']:\n            ctn_of_fail = self.log[htlc_proposer]['fails'][htlc_id][ctx_owner]\n        else:\n            ctn_of_fail = None\n        ctn_of_rm = ctn_of_settle or ctn_of_fail or None\n        if ctn_of_rm is None:\n            return False\n        return ctn_of_rm <= self.ctn_oldest_unrevoked(ctx_owner)\n\n    @with_lock\n    def htlcs_by_direction(self, subject: HTLCOwner, direction: Direction,\n                           ctn: int = None) -> Dict[int, UpdateAddHtlc]:\n        \"\"\"Return the dict of received or sent (depending on direction) HTLCs\n        in subject's ctx at ctn, keyed by htlc_id.\n\n        direction is relative to subject!\n        \"\"\"\n        assert type(subject) is HTLCOwner\n        assert type(direction) is Direction\n        if ctn is None:\n            ctn = self.ctn_oldest_unrevoked(subject)\n        d = {}\n        # subject's ctx\n        # party is the proposer of the HTLCs\n        party = subject if direction == SENT else subject.inverted()\n        if ctn >= self.ctn_oldest_unrevoked(subject):\n            considered_htlc_ids = self._maybe_active_htlc_ids[party]\n        else:  # ctn is too old; need to consider full log (slow...)\n            considered_htlc_ids = self.log[party]['locked_in']\n        for htlc_id in considered_htlc_ids:\n            htlc_id = int(htlc_id)\n            if self.is_htlc_active_at_ctn(ctx_owner=subject, ctn=ctn, htlc_proposer=party, htlc_id=htlc_id):\n                d[htlc_id] = self.log[party]['adds'][htlc_id]\n        return d\n\n    @with_lock\n    def htlcs(self, subject: HTLCOwner, ctn: int = None) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:\n        \"\"\"Return the list of HTLCs in subject's ctx at ctn.\"\"\"\n        assert type(subject) is HTLCOwner\n        if ctn is None:\n            ctn = self.ctn_oldest_unrevoked(subject)\n        l = []\n        l += [(SENT, x) for x in self.htlcs_by_direction(subject, SENT, ctn).values()]\n        l += [(RECEIVED, x) for x in self.htlcs_by_direction(subject, RECEIVED, ctn).values()]\n        return l\n\n    @with_lock\n    def get_htlcs_in_oldest_unrevoked_ctx(self, subject: HTLCOwner) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:\n        assert type(subject) is HTLCOwner\n        ctn = self.ctn_oldest_unrevoked(subject)\n        return self.htlcs(subject, ctn)\n\n    @with_lock\n    def get_htlcs_in_latest_ctx(self, subject: HTLCOwner) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:\n        assert type(subject) is HTLCOwner\n        ctn = self.ctn_latest(subject)\n        return self.htlcs(subject, ctn)\n\n    @with_lock\n    def get_htlcs_in_next_ctx(self, subject: HTLCOwner) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:\n        assert type(subject) is HTLCOwner\n        ctn = self.ctn_latest(subject) + 1\n        return self.htlcs(subject, ctn)\n\n    def was_htlc_preimage_released(self, *, htlc_id: int, htlc_proposer: HTLCOwner) -> bool:\n        settles = self.log[htlc_proposer]['settles']\n        if htlc_id not in settles:\n            return False\n        return settles[htlc_id][htlc_proposer] is not None\n\n    def was_htlc_failed(self, *, htlc_id: int, htlc_proposer: HTLCOwner) -> bool:\n        \"\"\"Returns whether an HTLC has been (or will be if we already know) failed.\"\"\"\n        fails = self.log[htlc_proposer]['fails']\n        if htlc_id not in fails:\n            return False\n        return fails[htlc_id][htlc_proposer] is not None\n\n    @with_lock\n    def all_settled_htlcs_ever_by_direction(self, subject: HTLCOwner, direction: Direction,\n                                            ctn: int = None) -> Sequence[UpdateAddHtlc]:\n        \"\"\"Return the list of all HTLCs that have been ever settled in subject's\n        ctx up to ctn, filtered to only \"direction\".\n        \"\"\"\n        assert type(subject) is HTLCOwner\n        if ctn is None:\n            ctn = self.ctn_oldest_unrevoked(subject)\n        # subject's ctx\n        # party is the proposer of the HTLCs\n        party = subject if direction == SENT else subject.inverted()\n        d = []\n        for htlc_id, ctns in self.log[party]['settles'].items():\n            if ctns[subject] is not None and ctns[subject] <= ctn:\n                d.append(self.log[party]['adds'][htlc_id])\n        return d\n\n    @with_lock\n    def all_settled_htlcs_ever(self, subject: HTLCOwner, ctn: int = None) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:\n        \"\"\"Return the list of all HTLCs that have been ever settled in subject's\n        ctx up to ctn.\n        \"\"\"\n        assert type(subject) is HTLCOwner\n        if ctn is None:\n            ctn = self.ctn_oldest_unrevoked(subject)\n        sent = [(SENT, x) for x in self.all_settled_htlcs_ever_by_direction(subject, SENT, ctn)]\n        received = [(RECEIVED, x) for x in self.all_settled_htlcs_ever_by_direction(subject, RECEIVED, ctn)]\n        return sent + received\n\n    @with_lock\n    def all_htlcs_ever(self) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:\n        sent = [(SENT, htlc) for htlc in self.log[LOCAL]['adds'].values()]\n        received = [(RECEIVED, htlc) for htlc in self.log[REMOTE]['adds'].values()]\n        return sent + received\n\n    @with_lock\n    def get_balance_msat(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None,\n                         initial_balance_msat: int) -> int:\n        \"\"\"Returns the balance of 'whose' in 'ctx' at 'ctn'.\n        Only HTLCs that have been settled by that ctn are counted.\n        \"\"\"\n        if ctn is None:\n            ctn = self.ctn_oldest_unrevoked(ctx_owner)\n        balance = initial_balance_msat\n        if ctn >= self.ctn_oldest_unrevoked(ctx_owner):\n            balance += self._balance_delta * whose\n            considered_sent_htlc_ids = self._maybe_active_htlc_ids[whose]\n            considered_recv_htlc_ids = self._maybe_active_htlc_ids[-whose]\n        else:  # ctn is too old; need to consider full log (slow...)\n            considered_sent_htlc_ids = self.log[whose]['settles']\n            considered_recv_htlc_ids = self.log[-whose]['settles']\n        # sent htlcs\n        for htlc_id in considered_sent_htlc_ids:\n            ctns = self.log[whose]['settles'].get(htlc_id, None)\n            if ctns is None:\n                continue\n            if ctns[ctx_owner] is not None and ctns[ctx_owner] <= ctn:\n                htlc = self.log[whose]['adds'][htlc_id]\n                balance -= htlc.amount_msat\n        # recv htlcs\n        for htlc_id in considered_recv_htlc_ids:\n            ctns = self.log[-whose]['settles'].get(htlc_id, None)\n            if ctns is None:\n                continue\n            if ctns[ctx_owner] is not None and ctns[ctx_owner] <= ctn:\n                htlc = self.log[-whose]['adds'][htlc_id]\n                balance += htlc.amount_msat\n        return balance\n\n    @with_lock\n    def _get_htlcs_that_got_removed_exactly_at_ctn(\n            self, ctn: int, *, ctx_owner: HTLCOwner, htlc_proposer: HTLCOwner, log_action: str,\n    ) -> Sequence[UpdateAddHtlc]:\n        if ctn >= self.ctn_oldest_unrevoked(ctx_owner):\n            considered_htlc_ids = self._maybe_active_htlc_ids[htlc_proposer]\n        else:  # ctn is too old; need to consider full log (slow...)\n            considered_htlc_ids = self.log[htlc_proposer][log_action]\n        htlcs = []\n        for htlc_id in considered_htlc_ids:\n            ctns = self.log[htlc_proposer][log_action].get(htlc_id, None)\n            if ctns is None:\n                continue\n            if ctns[ctx_owner] == ctn:\n                htlcs.append(self.log[htlc_proposer]['adds'][htlc_id])\n        return htlcs\n\n    def received_in_ctn(self, local_ctn: int) -> Sequence[UpdateAddHtlc]:\n        \"\"\"\n        received htlcs that became fulfilled when we send a revocation.\n        we check only local, because they are committed in the remote ctx first.\n        \"\"\"\n        return self._get_htlcs_that_got_removed_exactly_at_ctn(local_ctn,\n                                                               ctx_owner=LOCAL,\n                                                               htlc_proposer=REMOTE,\n                                                               log_action='settles')\n\n    def sent_in_ctn(self, remote_ctn: int) -> Sequence[UpdateAddHtlc]:\n        \"\"\"\n        sent htlcs that became fulfilled when we received a revocation\n        we check only remote, because they are committed in the local ctx first.\n        \"\"\"\n        return self._get_htlcs_that_got_removed_exactly_at_ctn(remote_ctn,\n                                                               ctx_owner=REMOTE,\n                                                               htlc_proposer=LOCAL,\n                                                               log_action='settles')\n\n    def failed_in_ctn(self, remote_ctn: int) -> Sequence[UpdateAddHtlc]:\n        \"\"\"\n        sent htlcs that became failed when we received a revocation\n        we check only remote, because they are committed in the local ctx first.\n        \"\"\"\n        return self._get_htlcs_that_got_removed_exactly_at_ctn(remote_ctn,\n                                                               ctx_owner=REMOTE,\n                                                               htlc_proposer=LOCAL,\n                                                               log_action='fails')\n\n    ##### Queries re Fees:\n    # note: feerates are in sat/kw everywhere in this file\n\n    @with_lock\n    def get_feerate(self, subject: HTLCOwner, ctn: int) -> int:\n        \"\"\"Return feerate (sat/kw) used in subject's commitment txn at ctn.\"\"\"\n        ctn = max(0, ctn)  # FIXME rm this\n        # only one party can update fees; use length of logs to figure out which:\n        assert not (len(self.log[LOCAL]['fee_updates']) > 0 and len(self.log[REMOTE]['fee_updates']) > 0)\n        fee_log = self.log[LOCAL]['fee_updates']  # type: Sequence[FeeUpdate]\n        if len(self.log[REMOTE]['fee_updates']) > 0:\n            fee_log = self.log[REMOTE]['fee_updates']\n        # binary search\n        left = 0\n        right = len(fee_log)\n        while True:\n            i = (left + right) // 2\n            ctn_at_i = fee_log[i].ctn_local if subject == LOCAL else fee_log[i].ctn_remote\n            if right - left <= 1:\n                break\n            if ctn_at_i is None:  # Nones can only be on the right end\n                right = i\n                continue\n            if ctn_at_i <= ctn:  # among equals, we want the rightmost\n                left = i\n            else:\n                right = i\n        assert ctn_at_i <= ctn\n        return fee_log[i].rate\n\n    def get_feerate_in_oldest_unrevoked_ctx(self, subject: HTLCOwner) -> int:\n        return self.get_feerate(subject=subject, ctn=self.ctn_oldest_unrevoked(subject))\n\n    def get_feerate_in_latest_ctx(self, subject: HTLCOwner) -> int:\n        return self.get_feerate(subject=subject, ctn=self.ctn_latest(subject))\n\n    def get_feerate_in_next_ctx(self, subject: HTLCOwner) -> int:\n        return self.get_feerate(subject=subject, ctn=self.ctn_latest(subject) + 1)\n"
  },
  {
    "path": "electrum/lnmsg.py",
    "content": "import os\nimport csv\nimport io\nfrom typing import Callable, Tuple, Any, Dict, List, Sequence, Union, Optional, Mapping\nfrom types import MappingProxyType\nfrom collections import OrderedDict\n\nfrom .lnutil import OnionFailureCodeMetaFlag\n\n\nclass FailedToParseMsg(Exception):\n    msg_type_int: Optional[int] = None\n    msg_type_name: Optional[str] = None\n\nclass UnknownMsgType(FailedToParseMsg): pass\nclass UnknownOptionalMsgType(UnknownMsgType): pass\nclass UnknownMandatoryMsgType(UnknownMsgType): pass\n\nclass MalformedMsg(FailedToParseMsg): pass\nclass UnknownMsgFieldType(MalformedMsg): pass\nclass UnexpectedEndOfStream(MalformedMsg): pass\nclass FieldEncodingNotMinimal(MalformedMsg): pass\nclass UnknownMandatoryTLVRecordType(MalformedMsg): pass\nclass MsgTrailingGarbage(MalformedMsg): pass\nclass MsgInvalidFieldOrder(MalformedMsg): pass\nclass UnexpectedFieldSizeForEncoder(MalformedMsg): pass\n\n\ndef _num_remaining_bytes_to_read(fd: io.BytesIO) -> int:\n    cur_pos = fd.tell()\n    end_pos = fd.seek(0, io.SEEK_END)\n    fd.seek(cur_pos)\n    return end_pos - cur_pos\n\n\ndef _assert_can_read_at_least_n_bytes(fd: io.BytesIO, n: int) -> None:\n    # note: it's faster to read n bytes and then check if we read n, than\n    #       to assert we can read at least n and then read n bytes.\n    nremaining = _num_remaining_bytes_to_read(fd)\n    if nremaining < n:\n        raise UnexpectedEndOfStream(f\"wants to read {n} bytes but only {nremaining} bytes left\")\n\n\ndef write_bigsize_int(i: int) -> bytes:\n    assert i >= 0, i\n    if i < 0xfd:\n        return int.to_bytes(i, length=1, byteorder=\"big\", signed=False)\n    elif i < 0x1_0000:\n        return b\"\\xfd\" + int.to_bytes(i, length=2, byteorder=\"big\", signed=False)\n    elif i < 0x1_0000_0000:\n        return b\"\\xfe\" + int.to_bytes(i, length=4, byteorder=\"big\", signed=False)\n    else:\n        return b\"\\xff\" + int.to_bytes(i, length=8, byteorder=\"big\", signed=False)\n\n\ndef read_bigsize_int(fd: io.BytesIO) -> Optional[int]:\n    try:\n        first = fd.read(1)[0]\n    except IndexError:\n        return None  # end of file\n    if first < 0xfd:\n        return first\n    elif first == 0xfd:\n        buf = fd.read(2)\n        if len(buf) != 2:\n            raise UnexpectedEndOfStream()\n        val = int.from_bytes(buf, byteorder=\"big\", signed=False)\n        if not (0xfd <= val < 0x1_0000):\n            raise FieldEncodingNotMinimal()\n        return val\n    elif first == 0xfe:\n        buf = fd.read(4)\n        if len(buf) != 4:\n            raise UnexpectedEndOfStream()\n        val = int.from_bytes(buf, byteorder=\"big\", signed=False)\n        if not (0x1_0000 <= val < 0x1_0000_0000):\n            raise FieldEncodingNotMinimal()\n        return val\n    elif first == 0xff:\n        buf = fd.read(8)\n        if len(buf) != 8:\n            raise UnexpectedEndOfStream()\n        val = int.from_bytes(buf, byteorder=\"big\", signed=False)\n        if not (0x1_0000_0000 <= val):\n            raise FieldEncodingNotMinimal()\n        return val\n    raise Exception()\n\n\n# TODO: maybe if field_type is not \"byte\", we could return a list of type_len sized chunks?\n#       if field_type is a numeric, we could return a list of ints?\ndef _read_primitive_field(\n        *,\n        fd: io.BytesIO,\n        field_type: str,\n        count: Union[int, str]\n) -> Union[bytes, int]:\n    if not fd:\n        raise Exception()\n    if isinstance(count, int):\n        assert count >= 0, f\"{count!r} must be non-neg int\"\n    elif count == \"...\":\n        pass\n    else:\n        raise Exception(f\"unexpected field count: {count!r}\")\n    if count == 0:\n        return b\"\"\n    type_len = None\n    if field_type == 'byte':\n        type_len = 1\n    elif field_type in ('u8', 'u16', 'u32', 'u64'):\n        if field_type == 'u8':\n            type_len = 1\n        elif field_type == 'u16':\n            type_len = 2\n        elif field_type == 'u32':\n            type_len = 4\n        else:\n            assert field_type == 'u64'\n            type_len = 8\n        assert count == 1, count\n        buf = fd.read(type_len)\n        if len(buf) != type_len:\n            raise UnexpectedEndOfStream()\n        return int.from_bytes(buf, byteorder=\"big\", signed=False)\n    elif field_type in ('tu16', 'tu32', 'tu64'):\n        if field_type == 'tu16':\n            type_len = 2\n        elif field_type == 'tu32':\n            type_len = 4\n        else:\n            assert field_type == 'tu64'\n            type_len = 8\n        assert count == 1, count\n        raw = fd.read(type_len)\n        if len(raw) > 0 and raw[0] == 0x00:\n            raise FieldEncodingNotMinimal()\n        return int.from_bytes(raw, byteorder=\"big\", signed=False)\n    elif field_type == 'bigsize':\n        assert count == 1, count\n        val = read_bigsize_int(fd)\n        if val is None:\n            raise UnexpectedEndOfStream()\n        return val\n    elif field_type == 'chain_hash':\n        type_len = 32\n    elif field_type == 'channel_id':\n        type_len = 32\n    elif field_type == 'sha256':\n        type_len = 32\n    elif field_type == 'signature':\n        type_len = 64\n    elif field_type == 'point':\n        type_len = 33\n    elif field_type == 'short_channel_id':\n        type_len = 8\n    elif field_type == 'sciddir_or_pubkey':\n        buf = fd.read(1)\n        if buf[0] in [0, 1]:\n            type_len = 9\n        elif buf[0] in [2, 3]:\n            type_len = 33\n        else:\n            raise Exception(f\"invalid sciddir_or_pubkey, prefix byte not in range 0-3\")\n        buf += fd.read(type_len - 1)\n        if len(buf) != type_len:\n            raise UnexpectedEndOfStream()\n        return buf\n\n    if count == \"...\":\n        total_len = -1  # read all\n    else:\n        if type_len is None:\n            raise UnknownMsgFieldType(f\"unknown field type: {field_type!r}\")\n        total_len = count * type_len\n\n    buf = fd.read(total_len)\n    if total_len >= 0 and len(buf) != total_len:\n        raise UnexpectedEndOfStream()\n    return buf\n\n\n# TODO: maybe for \"value\" we could accept a list with len \"count\" of appropriate items\ndef _write_primitive_field(\n        *,\n        fd: io.BytesIO,\n        field_type: str,\n        count: Union[int, str],\n        value: Union[bytes, int]\n) -> None:\n    if not fd:\n        raise Exception()\n    if isinstance(count, int):\n        assert count >= 0, f\"{count!r} must be non-neg int\"\n    elif count == \"...\":\n        pass\n    else:\n        raise Exception(f\"unexpected field count: {count!r}\")\n    if count == 0:\n        return\n    type_len = None\n    if field_type == 'byte':\n        type_len = 1\n    elif field_type == 'u8':\n        type_len = 1\n    elif field_type == 'u16':\n        type_len = 2\n    elif field_type == 'u32':\n        type_len = 4\n    elif field_type == 'u64':\n        type_len = 8\n    elif field_type in ('tu16', 'tu32', 'tu64'):\n        if field_type == 'tu16':\n            type_len = 2\n        elif field_type == 'tu32':\n            type_len = 4\n        else:\n            assert field_type == 'tu64'\n            type_len = 8\n        assert count == 1, count\n        if isinstance(value, int):\n            value = int.to_bytes(value, length=type_len, byteorder=\"big\", signed=False)\n        if not isinstance(value, (bytes, bytearray)):\n            raise Exception(f\"can only write bytes into fd. got: {value!r}\")\n        while len(value) > 0 and value[0] == 0x00:\n            value = value[1:]\n        nbytes_written = fd.write(value)\n        if nbytes_written != len(value):\n            raise Exception(f\"tried to write {len(value)} bytes, but only wrote {nbytes_written}!?\")\n        return\n    elif field_type == 'bigsize':\n        assert count == 1, count\n        if isinstance(value, int):\n            value = write_bigsize_int(value)\n        if not isinstance(value, (bytes, bytearray)):\n            raise Exception(f\"can only write bytes into fd. got: {value!r}\")\n        nbytes_written = fd.write(value)\n        if nbytes_written != len(value):\n            raise Exception(f\"tried to write {len(value)} bytes, but only wrote {nbytes_written}!?\")\n        return\n    elif field_type == 'chain_hash':\n        type_len = 32\n    elif field_type == 'channel_id':\n        type_len = 32\n    elif field_type == 'sha256':\n        type_len = 32\n    elif field_type == 'signature':\n        type_len = 64\n    elif field_type == 'point':\n        type_len = 33\n    elif field_type == 'short_channel_id':\n        type_len = 8\n    elif field_type == 'sciddir_or_pubkey':\n        assert isinstance(value, bytes)\n        if value[0] in [0, 1]:\n            type_len = 9  # short_channel_id\n        elif value[0] in [2, 3]:\n            type_len = 33  # point\n        else:\n            raise Exception(f\"invalid sciddir_or_pubkey, prefix byte not in range 0-3\")\n    total_len = -1\n    if count != \"...\":\n        if type_len is None:\n            raise UnknownMsgFieldType(f\"unknown field type: {field_type!r}\")\n        total_len = count * type_len\n        if isinstance(value, int) and (count == 1 or field_type == 'byte'):\n            value = int.to_bytes(value, length=total_len, byteorder=\"big\", signed=False)\n    if not isinstance(value, (bytes, bytearray)):\n        raise Exception(f\"can only write bytes into fd. got: {value!r}\")\n    if count != \"...\" and total_len != len(value):\n        raise UnexpectedFieldSizeForEncoder(f\"expected: {total_len}, got {len(value)}\")\n    nbytes_written = fd.write(value)\n    if nbytes_written != len(value):\n        raise Exception(f\"tried to write {len(value)} bytes, but only wrote {nbytes_written}!?\")\n\n\ndef _read_tlv_record(*, fd: io.BytesIO) -> Tuple[int, bytes]:\n    if not fd: raise Exception()\n    tlv_type = _read_primitive_field(fd=fd, field_type=\"bigsize\", count=1)\n    tlv_len = _read_primitive_field(fd=fd, field_type=\"bigsize\", count=1)\n    tlv_val = _read_primitive_field(fd=fd, field_type=\"byte\", count=tlv_len)\n    return tlv_type, tlv_val\n\n\ndef _write_tlv_record(*, fd: io.BytesIO, tlv_type: int, tlv_val: bytes) -> None:\n    if not fd: raise Exception()\n    tlv_len = len(tlv_val)\n    _write_primitive_field(fd=fd, field_type=\"bigsize\", count=1, value=tlv_type)\n    _write_primitive_field(fd=fd, field_type=\"bigsize\", count=1, value=tlv_len)\n    _write_primitive_field(fd=fd, field_type=\"byte\", count=tlv_len, value=tlv_val)\n\n\ndef _resolve_field_count(field_count_str: str, *, vars_dict: Mapping, allow_any=False) -> Union[int, str]:\n    \"\"\"Returns an evaluated field count, typically an int.\n    If allow_any is True, the return value can be a str with value==\"...\".\n    \"\"\"\n    if field_count_str == \"\":\n        field_count = 1\n    elif field_count_str == \"...\":\n        if not allow_any:\n            raise Exception(\"field count is '...' but allow_any is False\")\n        return field_count_str\n    else:\n        try:\n            field_count = int(field_count_str)\n        except ValueError:\n            field_count = vars_dict[field_count_str]\n            if isinstance(field_count, (bytes, bytearray)):\n                field_count = int.from_bytes(field_count, byteorder=\"big\")\n    assert isinstance(field_count, int)\n    return field_count\n\n\ndef _parse_msgtype_intvalue_for_onion_wire(value: str) -> int:\n    msg_type_int = 0\n    for component in value.split(\"|\"):\n        try:\n            msg_type_int |= int(component)\n        except ValueError:\n            msg_type_int |= OnionFailureCodeMetaFlag[component]\n    return msg_type_int\n\n\nclass LNSerializer:\n\n    def __init__(self, *, name: str = 'peer_wire'):\n        # TODO msg_type could be 'int' everywhere...\n        self.msg_scheme_from_type = {}  # type: Dict[bytes, List[Sequence[str]]]\n        self.msg_type_from_name = {}  # type: Dict[str, bytes]\n\n        self.in_tlv_stream_get_tlv_record_scheme_from_type = {}  # type: Dict[str, Dict[int, List[Sequence[str]]]]\n        self.in_tlv_stream_get_record_type_from_name = {}  # type: Dict[str, Dict[str, int]]\n        self.in_tlv_stream_get_record_name_from_type = {}  # type: Dict[str, Dict[int, str]]\n\n        self.subtypes = {}  # type: Dict[str, Dict[str, Sequence[str]]]\n\n        path = os.path.join(os.path.dirname(__file__), \"lnwire\", name + \".csv\")\n        with open(path, newline='') as f:\n            csvreader = csv.reader(f)\n            for row in csvreader:\n                #print(f\">>> {row!r}\")\n                if row[0] == \"msgtype\":\n                    # msgtype,<msgname>,<value>[,<option>]\n                    msg_type_name = row[1]\n                    if name == 'onion_wire':\n                        msg_type_int = _parse_msgtype_intvalue_for_onion_wire(str(row[2]))\n                    else:\n                        msg_type_int = int(row[2])\n                    msg_type_bytes = msg_type_int.to_bytes(2, 'big')\n                    assert msg_type_bytes not in self.msg_scheme_from_type, f\"type collision? for {msg_type_name}\"\n                    assert msg_type_name not in self.msg_type_from_name, f\"type collision? for {msg_type_name}\"\n                    row[2] = msg_type_int\n                    self.msg_scheme_from_type[msg_type_bytes] = [tuple(row)]\n                    self.msg_type_from_name[msg_type_name] = msg_type_bytes\n                elif row[0] == \"msgdata\":\n                    # msgdata,<msgname>,<fieldname>,<typename>,[<count>][,<option>]\n                    assert msg_type_name == row[1]\n                    self.msg_scheme_from_type[msg_type_bytes].append(tuple(row))\n                elif row[0] == \"tlvtype\":\n                    # tlvtype,<tlvstreamname>,<tlvname>,<value>[,<option>]\n                    tlv_stream_name = row[1]\n                    tlv_record_name = row[2]\n                    tlv_record_type = int(row[3])\n                    row[3] = tlv_record_type\n                    if tlv_stream_name not in self.in_tlv_stream_get_tlv_record_scheme_from_type:\n                        self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name] = OrderedDict()\n                        self.in_tlv_stream_get_record_type_from_name[tlv_stream_name] = {}\n                        self.in_tlv_stream_get_record_name_from_type[tlv_stream_name] = {}\n                    assert tlv_record_type not in self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name], f\"type collision? for {tlv_stream_name}/{tlv_record_name}\"\n                    assert tlv_record_name not in self.in_tlv_stream_get_record_type_from_name[tlv_stream_name], f\"type collision? for {tlv_stream_name}/{tlv_record_name}\"\n                    assert tlv_record_type not in self.in_tlv_stream_get_record_type_from_name[tlv_stream_name], f\"type collision? for {tlv_stream_name}/{tlv_record_name}\"\n                    self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name][tlv_record_type] = [tuple(row)]\n                    self.in_tlv_stream_get_record_type_from_name[tlv_stream_name][tlv_record_name] = tlv_record_type\n                    self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type] = tlv_record_name\n                    if max(self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name].keys()) > tlv_record_type:\n                        raise Exception(f\"tlv record types must be listed in monotonically increasing order for stream. \"\n                                        f\"stream={tlv_stream_name}\")\n                elif row[0] == \"tlvdata\":\n                    # tlvdata,<tlvstreamname>,<tlvname>,<fieldname>,<typename>,[<count>][,<option>]\n                    assert tlv_stream_name == row[1]\n                    assert tlv_record_name == row[2]\n                    self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name][tlv_record_type].append(tuple(row))\n                elif row[0] == \"subtype\":\n                    # subtype,<subtypename>\n                    subtypename = row[1]\n                    assert subtypename not in self.subtypes, f\"duplicate declaration of subtype {subtypename}\"\n                    self.subtypes[subtypename] = {}\n                elif row[0] == \"subtypedata\":\n                    # subtypedata,<subtypename>,<fieldname>,<typename>,[<count>]\n                    subtypename = row[1]\n                    fieldname = row[2]\n                    assert subtypename in self.subtypes, f\"subtypedata definition for subtype {subtypename} declared before subtype\"\n                    assert fieldname not in self.subtypes[subtypename], f\"duplicate field definition for {fieldname} for subtype {subtypename}\"\n                    self.subtypes[subtypename][fieldname] = tuple(row)\n                else:\n                    pass  # TODO\n\n    def write_field(\n            self,\n            *,\n            fd: io.BytesIO,\n            field_type: str,\n            count: Union[int, str],\n            value: Union[Sequence[Mapping[str, Any]], Mapping[str, Any]],\n    ) -> None:\n        assert fd\n\n        if field_type not in self.subtypes:\n            _write_primitive_field(fd=fd, field_type=field_type, count=count, value=value)\n            return\n\n        if isinstance(count, int):\n            assert count >= 0, f\"{count!r} must be non-neg int\"\n        elif count == \"...\":\n            pass\n        else:\n            raise Exception(f\"unexpected field count: {count!r}\")\n        if count == 0:\n            return\n\n        if count == 1:\n            assert isinstance(value, (MappingProxyType, dict)) or isinstance(value, (list, tuple)), type(value)\n            values = [value] if isinstance(value, (MappingProxyType, dict)) else value\n        else:\n            assert isinstance(value, (tuple, list)), f'{field_type=}, expected value of type list/tuple for {count=}'\n            values = value\n\n        if count == '...':\n            count = len(values)\n        else:\n            assert count == len(values), f'{field_type=}, expected {count} but got {len(values)}'\n        if count == 0:\n            return\n\n        for record in values:\n            for subtypename, row in self.subtypes[field_type].items():\n                # subtypedata,<subtypename>,<fieldname>,<typename>,[<count>]\n                subtype_field_name = row[2]\n                subtype_field_type = row[3]\n                subtype_field_count_str = row[4]\n\n                subtype_field_count = _resolve_field_count(\n                    subtype_field_count_str,\n                    vars_dict=record,\n                    allow_any=True)\n\n                if subtype_field_name not in record:\n                    raise Exception(f'complex field type {field_type} missing element {subtype_field_name}')\n\n                self.write_field(\n                    fd=fd,\n                    field_type=subtype_field_type,\n                    count=subtype_field_count,\n                    value=record[subtype_field_name])\n\n    def read_field(\n            self,\n            *,\n            fd: io.BytesIO,\n            field_type: str,\n            count: Union[int, str]\n    ) -> Union[bytes, List[Dict[str, Any]], Dict[str, Any]]:\n        assert fd\n\n        if field_type not in self.subtypes:\n            return _read_primitive_field(fd=fd, field_type=field_type, count=count)\n\n        if isinstance(count, int):\n            assert count >= 0, f\"{count!r} must be non-neg int\"\n        elif count == \"...\":\n            pass\n        else:\n            raise Exception(f\"unexpected field count: {count!r}\")\n        if count == 0:\n            return b\"\"\n\n        parsedlist = []\n\n        while _num_remaining_bytes_to_read(fd):\n            parsed = {}\n            for subtypename, row in self.subtypes[field_type].items():\n                # subtypedata,<subtypename>,<fieldname>,<typename>,[<count>]\n                subtype_field_name = row[2]\n                subtype_field_type = row[3]\n                subtype_field_count_str = row[4]\n\n                subtype_field_count = _resolve_field_count(\n                    subtype_field_count_str,\n                    vars_dict=parsed,\n                    allow_any=True)\n\n                parsed[subtype_field_name] = self.read_field(\n                    fd=fd,\n                    field_type=subtype_field_type,\n                    count=subtype_field_count)\n            parsedlist.append(parsed)\n\n        return parsedlist if count == '...' or count > 1 else parsedlist[0]\n\n    def write_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str, **kwargs) -> None:\n        scheme_map = self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name]\n        for tlv_record_type, scheme in scheme_map.items():  # note: tlv_record_type is monotonically increasing\n            tlv_record_name = self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type]\n            if tlv_record_name not in kwargs:\n                continue\n            with io.BytesIO() as tlv_record_fd:\n                for row in scheme:\n                    if row[0] == \"tlvtype\":\n                        pass\n                    elif row[0] == \"tlvdata\":\n                        # tlvdata,<tlvstreamname>,<tlvname>,<fieldname>,<typename>,[<count>][,<option>]\n                        assert tlv_stream_name == row[1]\n                        assert tlv_record_name == row[2]\n                        field_name = row[3]\n                        field_type = row[4]\n                        field_count_str = row[5]\n                        field_count = _resolve_field_count(field_count_str,\n                                                           vars_dict=kwargs[tlv_record_name],\n                                                           allow_any=True)\n                        field_value = kwargs[tlv_record_name][field_name]\n                        self.write_field(\n                            fd=tlv_record_fd,\n                            field_type=field_type,\n                            count=field_count,\n                            value=field_value)\n                    else:\n                        raise Exception(f\"unexpected row in scheme: {row!r}\")\n                _write_tlv_record(fd=fd, tlv_type=tlv_record_type, tlv_val=tlv_record_fd.getvalue())\n\n    def read_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str) -> Dict[str, Dict[str, Any]]:\n        parsed = {}  # type: Dict[str, Dict[str, Any]]\n        scheme_map = self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name]\n        last_seen_tlv_record_type = -1  # type: int\n        while _num_remaining_bytes_to_read(fd) > 0:\n            tlv_record_type, tlv_record_val = _read_tlv_record(fd=fd)\n            if not (tlv_record_type > last_seen_tlv_record_type):\n                raise MsgInvalidFieldOrder(f\"TLV records must be monotonically increasing by type. \"\n                                           f\"cur: {tlv_record_type}. prev: {last_seen_tlv_record_type}\")\n            last_seen_tlv_record_type = tlv_record_type\n            try:\n                scheme = scheme_map[tlv_record_type]\n            except KeyError:\n                if tlv_record_type % 2 == 0:\n                    # unknown \"even\" type: hard fail\n                    raise UnknownMandatoryTLVRecordType(f\"{tlv_stream_name}/{tlv_record_type}\") from None\n                else:\n                    # unknown \"odd\" type: skip it\n                    continue\n            tlv_record_name = self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type]\n            parsed[tlv_record_name] = {}\n            with io.BytesIO(tlv_record_val) as tlv_record_fd:\n                for row in scheme:\n                    #print(f\"row: {row!r}\")\n                    if row[0] == \"tlvtype\":\n                        pass\n                    elif row[0] == \"tlvdata\":\n                        # tlvdata,<tlvstreamname>,<tlvname>,<fieldname>,<typename>,[<count>][,<option>]\n                        assert tlv_stream_name == row[1]\n                        assert tlv_record_name == row[2]\n                        field_name = row[3]\n                        field_type = row[4]\n                        field_count_str = row[5]\n                        field_count = _resolve_field_count(\n                            field_count_str,\n                            vars_dict=parsed[tlv_record_name],\n                            allow_any=True)\n                        #print(f\">> count={field_count}. parsed={parsed}\")\n                        parsed[tlv_record_name][field_name] = self.read_field(\n                            fd=tlv_record_fd,\n                            field_type=field_type,\n                            count=field_count)\n                    else:\n                        raise Exception(f\"unexpected row in scheme: {row!r}\")\n                if _num_remaining_bytes_to_read(tlv_record_fd) > 0:\n                    raise MsgTrailingGarbage(f\"TLV record ({tlv_stream_name}/{tlv_record_name}) has extra trailing garbage\")\n        return parsed\n\n    def encode_msg(self, msg_type: str, **kwargs) -> bytes:\n        \"\"\"\n        Encode kwargs into a Lightning message (bytes)\n        of the type given in the msg_type string\n        \"\"\"\n        #print(f\">>> encode_msg. msg_type={msg_type}, payload={kwargs!r}\")\n        msg_type_bytes = self.msg_type_from_name[msg_type]\n        scheme = self.msg_scheme_from_type[msg_type_bytes]\n        with io.BytesIO() as fd:\n            fd.write(msg_type_bytes)\n            for row in scheme:\n                if row[0] == \"msgtype\":\n                    pass\n                elif row[0] == \"msgdata\":\n                    # msgdata,<msgname>,<fieldname>,<typename>,[<count>][,<option>]\n                    field_name = row[2]\n                    field_type = row[3]\n                    field_count_str = row[4]\n                    #print(f\">>> encode_msg. msgdata. field_name={field_name!r}. field_type={field_type!r}. field_count_str={field_count_str!r}\")\n                    field_count = _resolve_field_count(field_count_str, vars_dict=kwargs)\n                    if field_name == \"tlvs\":\n                        tlv_stream_name = field_type\n                        if tlv_stream_name in kwargs:\n                            self.write_tlv_stream(fd=fd, tlv_stream_name=tlv_stream_name, **(kwargs[tlv_stream_name]))\n                        continue\n                    try:\n                        field_value = kwargs[field_name]\n                    except KeyError:\n                        field_value = 0  # default mandatory fields to zero\n                    #print(f\">>> encode_msg. writing field: {field_name}. value={field_value!r}. field_type={field_type!r}. count={field_count!r}\")\n                    _write_primitive_field(fd=fd, field_type=field_type, count=field_count, value=field_value)\n                    #print(f\">>> encode_msg. so far: {fd.getvalue().hex()}\")\n                else:\n                    raise Exception(f\"unexpected row in scheme: {row!r}\")\n            return fd.getvalue()\n\n    def decode_msg(self, data: bytes) -> Tuple[str, dict]:\n        \"\"\"\n        Decode Lightning message by reading the first\n        two bytes to determine message type.\n\n        Returns message type string and parsed message contents dict,\n        or raises FailedToParseMsg.\n        \"\"\"\n        #print(f\"decode_msg >>> {data.hex()}\")\n        assert len(data) >= 2\n        msg_type_bytes = data[:2]\n        msg_type_int = int.from_bytes(msg_type_bytes, byteorder=\"big\", signed=False)\n        try:\n            scheme = self.msg_scheme_from_type[msg_type_bytes]\n        except KeyError:\n            if msg_type_int % 2 == 0:  # even types must be understood: \"mandatory\"\n                raise UnknownMandatoryMsgType(f\"msg_type={msg_type_int}\")\n            else:  # odd types are ok not to understand: \"optional\"\n                raise UnknownOptionalMsgType(f\"msg_type={msg_type_int}\")\n        assert scheme[0][2] == msg_type_int\n        msg_type_name = scheme[0][1]\n        parsed = {}\n        try:\n            with io.BytesIO(data[2:]) as fd:\n                for row in scheme:\n                    #print(f\"row: {row!r}\")\n                    if row[0] == \"msgtype\":\n                        pass\n                    elif row[0] == \"msgdata\":\n                        field_name = row[2]\n                        field_type = row[3]\n                        field_count_str = row[4]\n                        field_count = _resolve_field_count(field_count_str, vars_dict=parsed)\n                        if field_name == \"tlvs\":\n                            tlv_stream_name = field_type\n                            d = self.read_tlv_stream(fd=fd, tlv_stream_name=tlv_stream_name)\n                            parsed[tlv_stream_name] = d\n                            continue\n                        #print(f\">> count={field_count}. parsed={parsed}\")\n                        parsed[field_name] = _read_primitive_field(fd=fd, field_type=field_type, count=field_count)\n                    else:\n                        raise Exception(f\"unexpected row in scheme: {row!r}\")\n        except FailedToParseMsg as e:\n            e.msg_type_int = msg_type_int\n            e.msg_type_name = msg_type_name\n            raise\n        return msg_type_name, parsed\n\n\n_inst = LNSerializer()\nencode_msg = _inst.encode_msg\ndecode_msg = _inst.decode_msg\n\n\nOnionWireSerializer = LNSerializer(name='onion_wire')\n"
  },
  {
    "path": "electrum/lnonion.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2018 The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport io\nimport hashlib\nfrom functools import cached_property\nfrom typing import (Sequence, List, Tuple, NamedTuple, TYPE_CHECKING, Dict, Any, Optional, Union,\n                    Mapping, Iterator)\nfrom enum import IntEnum\nfrom dataclasses import dataclass, field, replace\nfrom types import MappingProxyType\n\nimport electrum_ecc as ecc\n\nfrom .crypto import sha256, hmac_oneshot, chacha20_encrypt, get_ecdh, chacha20_poly1305_encrypt, chacha20_poly1305_decrypt\nfrom .util import profiler, xor_bytes, bfh\nfrom .lnutil import (PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH,\n                     NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, OnionFailureCodeMetaFlag)\nfrom .lnmsg import OnionWireSerializer, read_bigsize_int, write_bigsize_int\nfrom . import lnmsg\nfrom . import util\n\nif TYPE_CHECKING:\n    from .lnrouter import LNPaymentRoute\n\n\nHOPS_DATA_SIZE = 1300      # also sometimes called routingInfoSize in bolt-04\nTRAMPOLINE_HOPS_DATA_SIZE = 400\nPER_HOP_HMAC_SIZE = 32\nONION_MESSAGE_LARGE_SIZE = 32768\n\nclass UnsupportedOnionPacketVersion(Exception): pass\nclass InvalidOnionMac(Exception): pass\nclass InvalidOnionPubkey(Exception): pass\nclass InvalidPayloadSize(Exception): pass\n\n\n@dataclass(frozen=True, kw_only=True)\nclass OnionHopsDataSingle:\n    payload: Mapping = field(default_factory=lambda: MappingProxyType({}))\n    hmac: Optional[bytes] = None\n    tlv_stream_name: str = 'payload'\n    blind_fields: Mapping = field(default_factory=lambda: MappingProxyType({}))\n    _raw_bytes_payload: Optional[bytes] = None\n\n    def __post_init__(self):\n        # make all fields immutable recursively\n        object.__setattr__(self, 'payload', util.make_object_immutable(self.payload))\n        object.__setattr__(self, 'blind_fields', util.make_object_immutable(self.blind_fields))\n        assert isinstance(self.payload, MappingProxyType)\n        assert isinstance(self.blind_fields, MappingProxyType)\n        assert isinstance(self.tlv_stream_name, str)\n        assert (isinstance(self.hmac, bytes) and len(self.hmac) == PER_HOP_HMAC_SIZE) or self.hmac is None\n\n    def to_bytes(self) -> bytes:\n        hmac_ = self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE)\n        if self._raw_bytes_payload is not None:\n            ret = self._raw_bytes_payload\n            ret += hmac_\n            return ret\n        # adding TLV payload. note: legacy hop data format no longer supported.\n        payload_fd = io.BytesIO()\n        OnionWireSerializer.write_tlv_stream(fd=payload_fd,\n                                             tlv_stream_name=self.tlv_stream_name,\n                                             **self.payload)\n        payload_bytes = payload_fd.getvalue()\n        with io.BytesIO() as fd:\n            fd.write(write_bigsize_int(len(payload_bytes)))\n            fd.write(payload_bytes)\n            fd.write(hmac_)\n            return fd.getvalue()\n\n    @classmethod\n    def from_fd(cls, fd: io.BytesIO, *, tlv_stream_name: str = 'payload') -> 'OnionHopsDataSingle':\n        first_byte = fd.read(1)\n        if len(first_byte) == 0:\n            raise Exception(f\"unexpected EOF\")\n        fd.seek(-1, io.SEEK_CUR)  # undo read\n        if first_byte == b'\\x00':\n            # legacy hop data format\n            raise Exception(\"legacy hop data format no longer supported\")\n        elif first_byte == b'\\x01':\n            # reserved for future use\n            raise Exception(\"unsupported hop payload: length==1\")\n        else:  # tlv format\n            hop_payload_length = read_bigsize_int(fd)\n            hop_payload = fd.read(hop_payload_length)\n            if hop_payload_length != len(hop_payload):\n                raise Exception(f\"unexpected EOF\")\n            payload = OnionWireSerializer.read_tlv_stream(fd=io.BytesIO(hop_payload),\n                                                          tlv_stream_name=tlv_stream_name)\n            ret = OnionHopsDataSingle(\n                tlv_stream_name=tlv_stream_name,\n                payload=payload,\n                hmac=fd.read(PER_HOP_HMAC_SIZE)\n            )\n            return ret\n\n    def __repr__(self):\n        return f\"<OnionHopsDataSingle. {self.payload=}. {self.hmac=}>\"\n\n\n@dataclass(frozen=True, kw_only=True)\nclass OnionPacket:\n    public_key: bytes\n    hops_data: bytes  # also called RoutingInfo in bolt-04\n    hmac: bytes\n    version: int = 0\n    # for debugging our own onions:\n    _debug_hops_data: Optional[Sequence[OnionHopsDataSingle]] = None\n    _debug_route: Optional['LNPaymentRoute'] = None\n\n    def __post_init__(self):\n        assert len(self.public_key) == 33\n        assert len(self.hops_data) in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE]\n        assert len(self.hmac) == PER_HOP_HMAC_SIZE\n        if not ecc.ECPubkey.is_pubkey_bytes(self.public_key):\n            raise InvalidOnionPubkey()\n\n    def to_bytes(self) -> bytes:\n        ret = bytes([self.version])\n        ret += self.public_key\n        ret += self.hops_data\n        ret += self.hmac\n        if len(ret) - 66 not in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE]:\n            raise Exception('unexpected length {}'.format(len(ret)))\n        return ret\n\n    @classmethod\n    def from_bytes(cls, b: bytes) -> 'OnionPacket':\n        if len(b) - 66 not in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE]:\n            raise Exception('unexpected length {}'.format(len(b)))\n        return OnionPacket(\n            public_key=b[1:34],\n            hops_data=b[34:-32],\n            hmac=b[-32:],\n            version=b[0],\n        )\n\n    @cached_property\n    def onion_hash(self) -> bytes:\n        return sha256(self.to_bytes())\n\n\ndef get_bolt04_onion_key(key_type: bytes, secret: bytes) -> bytes:\n    if key_type not in (b'rho', b'mu', b'um', b'ammag', b'pad', b'blinded_node_id'):\n        raise Exception('invalid key_type {}'.format(key_type))\n    key = hmac_oneshot(key_type, msg=secret, digest=hashlib.sha256)\n    return key\n\n\ndef get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes],\n                                   session_key: bytes) -> Tuple[Sequence[bytes], Sequence[bytes]]:\n    num_hops = len(payment_path_pubkeys)\n    hop_shared_secrets = num_hops * [b'']\n    hop_blinded_node_ids = num_hops * [b'']\n    ephemeral_key = session_key\n    # compute shared key for each hop\n    for i in range(0, num_hops):\n        hop_shared_secrets[i] = get_ecdh(ephemeral_key, payment_path_pubkeys[i])\n        hop_blinded_node_ids[i] = get_blinded_node_id(payment_path_pubkeys[i], hop_shared_secrets[i])\n        ephemeral_pubkey = ecc.ECPrivkey(ephemeral_key).get_public_key_bytes()\n        blinding_factor = sha256(ephemeral_pubkey + hop_shared_secrets[i])\n        blinding_factor_int = int.from_bytes(blinding_factor, byteorder=\"big\")\n        ephemeral_key_int = int.from_bytes(ephemeral_key, byteorder=\"big\")\n        ephemeral_key_int = ephemeral_key_int * blinding_factor_int % ecc.CURVE_ORDER\n        ephemeral_key = ephemeral_key_int.to_bytes(32, byteorder=\"big\")\n    return hop_shared_secrets, hop_blinded_node_ids\n\n\ndef get_blinded_node_id(node_id: bytes, shared_secret: bytes):\n    # blinded node id\n    # B(i) = HMAC256(\"blinded_node_id\", ss(i)) * N(i)\n    ss_bni_hmac = get_bolt04_onion_key(b'blinded_node_id', shared_secret)\n    ss_bni_hmac_int = int.from_bytes(ss_bni_hmac, byteorder=\"big\")\n    blinded_node_id = ecc.ECPubkey(node_id) * ss_bni_hmac_int\n    return blinded_node_id.get_public_key_bytes()\n\n\ndef new_onion_packet(\n    payment_path_pubkeys: Sequence[bytes],\n    session_key: bytes,\n    hops_data: List[OnionHopsDataSingle],\n    *,\n    associated_data: bytes = b'',\n    trampoline: bool = False,\n    onion_message: bool = False\n) -> OnionPacket:\n    num_hops = len(payment_path_pubkeys)\n    assert num_hops == len(hops_data)\n    hop_shared_secrets, _ = get_shared_secrets_along_route(payment_path_pubkeys, session_key)\n\n    payload_size = 0\n    for i in range(num_hops):\n        # FIXME: serializing here and again below. cache bytes in OnionHopsDataSingle? _raw_bytes_payload?\n        payload_size += PER_HOP_HMAC_SIZE + len(hops_data[i].to_bytes())\n    if trampoline:\n        data_size = TRAMPOLINE_HOPS_DATA_SIZE\n    elif onion_message:\n        if payload_size <= HOPS_DATA_SIZE:\n            data_size = HOPS_DATA_SIZE\n        else:\n            data_size = ONION_MESSAGE_LARGE_SIZE\n    else:\n        data_size = HOPS_DATA_SIZE\n\n    if payload_size > data_size:\n        raise InvalidPayloadSize(f'payload too big for onion packet (max={data_size}, required={payload_size})')\n\n    filler = _generate_filler(b'rho', hops_data, hop_shared_secrets, data_size)\n    next_hmac = bytes(PER_HOP_HMAC_SIZE)\n\n    # Our starting packet needs to be filled out with random bytes, we\n    # generate some deterministically using the session private key.\n    pad_key = get_bolt04_onion_key(b'pad', session_key)\n    mix_header = generate_cipher_stream(pad_key, data_size)\n\n    # compute routing info and MAC for each hop\n    for i in range(num_hops-1, -1, -1):\n        rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i])\n        mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i])\n        hops_data[i] = replace(hops_data[i], hmac=next_hmac)\n        stream_bytes = generate_cipher_stream(rho_key, data_size)\n        hop_data_bytes = hops_data[i].to_bytes()\n        mix_header = mix_header[:-len(hop_data_bytes)]\n        mix_header = hop_data_bytes + mix_header\n        mix_header = xor_bytes(mix_header, stream_bytes)\n        if i == num_hops - 1 and len(filler) != 0:\n            mix_header = mix_header[:-len(filler)] + filler\n        packet = mix_header + associated_data\n        next_hmac = hmac_oneshot(mu_key, msg=packet, digest=hashlib.sha256)\n\n    return OnionPacket(\n        public_key=ecc.ECPrivkey(session_key).get_public_key_bytes(),\n        hops_data=mix_header,\n        hmac=next_hmac)\n\n\ndef encrypt_onionmsg_data_tlv(*, shared_secret, **kwargs):\n    rho_key = get_bolt04_onion_key(b'rho', shared_secret)\n    with io.BytesIO() as encrypted_data_tlv_fd:\n        OnionWireSerializer.write_tlv_stream(\n            fd=encrypted_data_tlv_fd,\n            tlv_stream_name='encrypted_data_tlv',\n            **kwargs)\n        encrypted_data_tlv_bytes = encrypted_data_tlv_fd.getvalue()\n        encrypted_recipient_data = chacha20_poly1305_encrypt(\n            key=rho_key, nonce=bytes(12),\n            data=encrypted_data_tlv_bytes)\n        return encrypted_recipient_data\n\n\ndef decrypt_onionmsg_data_tlv(*, shared_secret: bytes, encrypted_recipient_data: bytes) -> dict:\n    rho_key = get_bolt04_onion_key(b'rho', shared_secret)\n    recipient_data_bytes = chacha20_poly1305_decrypt(key=rho_key, nonce=bytes(12), data=encrypted_recipient_data)\n\n    with io.BytesIO(recipient_data_bytes) as fd:\n        recipient_data = OnionWireSerializer.read_tlv_stream(fd=fd, tlv_stream_name='encrypted_data_tlv')\n\n    return recipient_data\n\n\ndef encrypt_hops_recipient_data(\n        tlv_stream_name: str,\n        hops_data: List[OnionHopsDataSingle],\n        hop_shared_secrets: Sequence[bytes]\n) -> None:\n    \"\"\"encrypt unencrypted encrypted_recipient_data for hops with blind_fields.\n\n       NOTE: contents of payload.encrypted_recipient_data is slightly different for 'payload'\n       vs 'oniomsg_tlv' tlv_stream_names, so we map to the correct key here based on tlv_stream_name.\n       We can also change onion_wire.csv to use the same key, but as we import that from specs it might\n       regress in the future, so I rather make it explicit in code here.\n    \"\"\"\n    # key naming payload TLV vs onionmsg_tlv TLV\n    erd_key = 'encrypted_recipient_data' if tlv_stream_name == 'onionmsg_tlv' else 'encrypted_data'\n\n    num_hops = len(hops_data)\n    for i in range(num_hops):\n        if hops_data[i].tlv_stream_name == tlv_stream_name and 'encrypted_recipient_data' not in hops_data[i].payload:\n            # construct encrypted_recipient_data from blind_fields\n            encrypted_recipient_data = encrypt_onionmsg_data_tlv(shared_secret=hop_shared_secrets[i], **hops_data[i].blind_fields)\n            # work around immutablility of OnionHopsDataSingle\n            hop_payload = {'encrypted_recipient_data': {erd_key: encrypted_recipient_data}}\n            hop_payload.update(hops_data[i].payload)\n            hops_data[i] = OnionHopsDataSingle(tlv_stream_name=hops_data[i].tlv_stream_name, payload=hop_payload, blind_fields=hops_data[i].blind_fields)\n\n\ndef calc_hops_data_for_payment(\n        route: 'LNPaymentRoute',\n        amount_msat: int,  # that final recipient receives\n        *,\n        final_cltv_abs: int,\n        total_msat: int,\n        payment_secret: bytes,\n) -> Tuple[List[OnionHopsDataSingle], int, int]:\n    \"\"\"Returns the hops_data to be used for constructing an onion packet,\n    and the amount_msat and cltv_abs to be used on our immediate channel.\n    \"\"\"\n    if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:\n        raise PaymentFailure(f\"too long route ({len(route)} edges)\")\n    amt = amount_msat\n    cltv_abs = final_cltv_abs\n    # payload that will be seen by the last hop:\n    # for multipart payments we need to tell the receiver about the total and\n    # partial amounts\n    hop_payload = {\n        \"amt_to_forward\": {\"amt_to_forward\": amt},\n        \"outgoing_cltv_value\": {\"outgoing_cltv_value\": cltv_abs},\n        \"payment_data\": {\n            \"payment_secret\": payment_secret,\n            \"total_msat\": total_msat,\n            \"amount_msat\": amt,\n        }}\n    hops_data = [OnionHopsDataSingle(payload=hop_payload)]\n    # payloads, backwards from last hop (but excluding the first edge):\n    for edge_index in range(len(route) - 1, 0, -1):\n        route_edge = route[edge_index]\n        hop_payload = {\n            \"amt_to_forward\": {\"amt_to_forward\": amt},\n            \"outgoing_cltv_value\": {\"outgoing_cltv_value\": cltv_abs},\n            \"short_channel_id\": {\"short_channel_id\": route_edge.short_channel_id},\n        }\n        hops_data.append(\n            OnionHopsDataSingle(payload=hop_payload))\n        amt += route_edge.fee_for_edge(amt)\n        cltv_abs += route_edge.cltv_delta\n    hops_data.reverse()\n    return hops_data, amt, cltv_abs\n\n\ndef _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle],\n                     shared_secrets: Sequence[bytes], data_size:int) -> bytes:\n    num_hops = len(hops_data)\n\n    # generate filler that matches all but the last hop (no HMAC for last hop)\n    filler_size = 0\n    for hop_data in hops_data[:-1]:\n        filler_size += len(hop_data.to_bytes())\n    filler = bytearray(filler_size)\n\n    for i in range(0, num_hops-1):  # -1, as last hop does not obfuscate\n        # Sum up how many frames were used by prior hops.\n        filler_start = data_size\n        for hop_data in hops_data[:i]:\n            filler_start -= len(hop_data.to_bytes())\n        # The filler is the part dangling off of the end of the\n        # routingInfo, so offset it from there, and use the current\n        # hop's frame count as its size.\n        filler_end = data_size + len(hops_data[i].to_bytes())\n\n        stream_key = get_bolt04_onion_key(key_type, shared_secrets[i])\n        stream_bytes = generate_cipher_stream(stream_key, 2 * data_size)\n        filler = xor_bytes(filler, stream_bytes[filler_start:filler_end])\n        filler += bytes(filler_size - len(filler))  # right pad with zeroes\n\n    return filler\n\n\ndef generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes:\n    return chacha20_encrypt(key=stream_key,\n                            nonce=bytes(8),\n                            data=bytes(num_bytes))\n\n\nclass ProcessedOnionPacket(NamedTuple):\n    are_we_final: bool\n    hop_data: OnionHopsDataSingle\n    next_packet: OnionPacket\n    trampoline_onion_packet: OnionPacket\n\n    @property\n    def amt_to_forward(self) -> Optional[int]:\n        k1 = k2 = 'amt_to_forward'\n        return self._get_from_payload(k1, k2, int)\n\n    @property\n    def outgoing_cltv_value(self) -> Optional[int]:\n        k1 = k2 = 'outgoing_cltv_value'\n        return self._get_from_payload(k1, k2, int)\n\n    @property\n    def next_chan_scid(self) -> Optional[ShortChannelID]:\n        k1 = k2 = 'short_channel_id'\n        return self._get_from_payload(k1, k2, ShortChannelID)\n\n    @property\n    def total_msat(self) -> Optional[int]:\n        return self._get_from_payload('payment_data', 'total_msat', int)\n\n    @property\n    def payment_secret(self) -> Optional[bytes]:\n        return self._get_from_payload('payment_data', 'payment_secret', bytes)\n\n    def _get_from_payload(self, k1: str, k2: str, res_type: type):\n        try:\n            result = self.hop_data.payload[k1][k2]\n            return res_type(result)\n        except Exception:\n            return None\n\n\n# TODO replay protection\ndef process_onion_packet(\n        onion_packet: OnionPacket,\n        our_onion_private_key: bytes,\n        *,\n        associated_data: bytes = b'',\n        is_trampoline=False,\n        is_onion_message=False,\n        tlv_stream_name='payload') -> ProcessedOnionPacket:\n    # TODO: check Onion features ( PERM|NODE|3 (required_node_feature_missing )\n    if onion_packet.version != 0:\n        raise UnsupportedOnionPacketVersion()\n    if not ecc.ECPubkey.is_pubkey_bytes(onion_packet.public_key):\n        raise InvalidOnionPubkey()\n    shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key)\n    # check message integrity\n    mu_key = get_bolt04_onion_key(b'mu', shared_secret)\n    calculated_mac = hmac_oneshot(\n        mu_key, msg=onion_packet.hops_data+associated_data,\n        digest=hashlib.sha256)\n    if not util.constant_time_compare(onion_packet.hmac, calculated_mac):\n        raise InvalidOnionMac()\n    # peel an onion layer off\n    rho_key = get_bolt04_onion_key(b'rho', shared_secret)\n    data_size = TRAMPOLINE_HOPS_DATA_SIZE if is_trampoline else HOPS_DATA_SIZE\n    if is_onion_message and len(onion_packet.hops_data) > HOPS_DATA_SIZE:\n        data_size = ONION_MESSAGE_LARGE_SIZE\n    stream_bytes = generate_cipher_stream(rho_key, 2 * data_size)\n    padded_header = onion_packet.hops_data + bytes(data_size)\n    next_hops_data = xor_bytes(padded_header, stream_bytes)\n    next_hops_data_fd = io.BytesIO(next_hops_data)\n    hop_data = OnionHopsDataSingle.from_fd(next_hops_data_fd, tlv_stream_name=tlv_stream_name)\n    # trampoline\n    trampoline_onion_packet = hop_data.payload.get('trampoline_onion_packet')\n    if trampoline_onion_packet:\n        if is_trampoline:\n            raise Exception(\"found nested trampoline inside trampoline\")\n        top_version = trampoline_onion_packet.get('version')\n        top_public_key = trampoline_onion_packet.get('public_key')\n        top_hops_data = trampoline_onion_packet.get('hops_data')\n        top_hops_data_fd = io.BytesIO(top_hops_data)\n        top_hmac = trampoline_onion_packet.get('hmac')\n        trampoline_onion_packet = OnionPacket(\n            public_key=top_public_key,\n            hops_data=top_hops_data_fd.read(TRAMPOLINE_HOPS_DATA_SIZE),\n            hmac=top_hmac)\n    # calc next ephemeral key\n    blinding_factor = sha256(onion_packet.public_key + shared_secret)\n    blinding_factor_int = int.from_bytes(blinding_factor, byteorder=\"big\")\n    next_public_key_int = ecc.ECPubkey(onion_packet.public_key) * blinding_factor_int\n    next_public_key = next_public_key_int.get_public_key_bytes()\n    next_onion_packet = OnionPacket(\n        public_key=next_public_key,\n        hops_data=next_hops_data_fd.read(data_size),\n        hmac=hop_data.hmac)\n    if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE):\n        # we are the destination / exit node\n        are_we_final = True\n    else:\n        # we are an intermediate node; forwarding\n        are_we_final = False\n    return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet, trampoline_onion_packet)\n\n\ndef compare_trampoline_onions(\n    trampoline_onions: Iterator[Optional[ProcessedOnionPacket]],\n    *,\n    exclude_amt_to_fwd: bool = False,\n) -> bool:\n    \"\"\"\n    compare values of trampoline onions payloads and are_we_final.\n    If we are receiver of a multi trampoline payment amt_to_fwd can differ between the trampoline\n    parts of the payment, so it needs to be excluded from the comparison when comparing all trampoline\n    onions of the whole payment (however it can be compared between the onions in a single trampoline part).\n    \"\"\"\n    try:\n        first_onion = next(trampoline_onions)\n    except StopIteration:\n        raise ValueError(\"nothing to compare\")\n\n    if first_onion is None:\n        # we don't support mixed mpp sets of htlcs with trampoline onions and regular non-trampoline htlcs.\n        # In theory this could happen if a sender e.g. uses trampoline as fallback to deliver\n        # outstanding mpp parts if local pathfinding wasn't successful for the whole payment,\n        # resulting in a mixed payment. However, it's not even clear if the spec allows for such a constellation.\n        return all(onion is None for onion in trampoline_onions)\n    assert isinstance(first_onion, ProcessedOnionPacket), f\"{first_onion=}\"\n\n    are_we_final = first_onion.are_we_final\n    payload = first_onion.hop_data.payload\n    total_msat = first_onion.total_msat\n    outgoing_cltv = first_onion.outgoing_cltv_value\n    payment_secret = first_onion.payment_secret\n    for onion in trampoline_onions:\n        if onion is None:\n            return False\n        assert isinstance(onion, ProcessedOnionPacket), f\"{onion=}\"\n        assert onion.trampoline_onion_packet is None, f\"{onion=} cannot have trampoline_onion_packet\"\n        if onion.are_we_final != are_we_final:\n            return False\n        if not exclude_amt_to_fwd:\n            if onion.hop_data.payload != payload:\n                return False\n        else:\n            if onion.total_msat != total_msat:\n                return False\n            if onion.outgoing_cltv_value != outgoing_cltv:\n                return False\n            if onion.payment_secret != payment_secret:\n                return False\n    return True\n\n\nclass FailedToDecodeOnionError(Exception): pass\n\n\nclass OnionRoutingFailure(Exception):\n\n    def __init__(self, code: Union[int, 'OnionFailureCode'], data: bytes):\n        self.code = code\n        self.data = data\n\n    def __repr__(self):\n        return repr((self.code, self.data))\n\n    def to_bytes(self) -> bytes:\n        ret = self.code.to_bytes(2, byteorder=\"big\")\n        ret += self.data\n        return ret\n\n    @classmethod\n    def from_bytes(cls, failure_msg: bytes):\n        failure_code = int.from_bytes(failure_msg[:2], byteorder='big')\n        failure_code = OnionFailureCode.from_int(failure_code)  # convert to enum, if known code\n        failure_data = failure_msg[2:]\n        return OnionRoutingFailure(failure_code, failure_data)\n\n    def code_name(self) -> str:\n        if isinstance(self.code, OnionFailureCode):\n            return str(self.code.name)\n        return f\"Unknown error ({self.code!r})\"\n\n    def decode_data(self) -> Optional[Dict[str, Any]]:\n        try:\n            message_type, payload = OnionWireSerializer.decode_msg(self.to_bytes())\n        except lnmsg.FailedToParseMsg:\n            payload = None\n        return payload\n\n    def to_wire_msg(self, onion_packet: OnionPacket, privkey: bytes, local_height: int) -> bytes:\n        onion_error = construct_onion_error(self, onion_packet.public_key, privkey, local_height)\n        error_bytes = obfuscate_onion_error(onion_error, onion_packet.public_key, privkey)\n        return error_bytes\n\n\nclass OnionParsingError(OnionRoutingFailure):\n    \"\"\"\n    Onion parsing error will cause a htlc to get failed with update_fail_malformed_htlc.\n    Using INVALID_ONION_VERSION as there is no unspecific BADONION failure code defined in the spec\n    for the case we just cannot parse the onion.\n    \"\"\"\n    def __init__(self, data: bytes):\n        OnionRoutingFailure.__init__(self, code=OnionFailureCode.INVALID_ONION_VERSION, data=data)\n\n\ndef construct_onion_error(\n        error: OnionRoutingFailure,\n        their_public_key: bytes,\n        our_onion_private_key: bytes,\n        local_height: int\n) -> bytes:\n    # add local height\n    if error.code == OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS:\n        error.data += local_height.to_bytes(4, byteorder=\"big\")\n    # create payload\n    failure_msg = error.to_bytes()\n    failure_len = len(failure_msg)\n    pad_len = 256 - failure_len\n    assert pad_len >= 0\n    error_packet =  failure_len.to_bytes(2, byteorder=\"big\")\n    error_packet += failure_msg\n    error_packet += pad_len.to_bytes(2, byteorder=\"big\")\n    error_packet += bytes(pad_len)\n    # add hmac\n    shared_secret = get_ecdh(our_onion_private_key, their_public_key)\n    um_key = get_bolt04_onion_key(b'um', shared_secret)\n    hmac_ = hmac_oneshot(um_key, msg=error_packet, digest=hashlib.sha256)\n    error_packet = hmac_ + error_packet\n    return error_packet\n\ndef obfuscate_onion_error(error_packet, their_public_key, our_onion_private_key):\n    shared_secret = get_ecdh(our_onion_private_key, their_public_key)\n    ammag_key = get_bolt04_onion_key(b'ammag', shared_secret)\n    stream_bytes = generate_cipher_stream(ammag_key, len(error_packet))\n    error_packet = xor_bytes(error_packet, stream_bytes)\n    return error_packet\n\n\ndef _decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],\n                        session_key: bytes) -> Tuple[bytes, int]:\n    \"\"\"\n    Returns the decoded error bytes, and the index of the sender of the error.\n    https://github.com/lightning/bolts/blob/14272b1bd9361750cfdb3e5d35740889a6b510b5/04-onion-routing.md?plain=1#L1096\n    \"\"\"\n    num_hops = len(payment_path_pubkeys)\n    hop_shared_secrets, _ = get_shared_secrets_along_route(payment_path_pubkeys, session_key)\n    result = None\n    dummy_secret = bytes(32)\n    # SHOULD continue decrypting, until the loop has been repeated 27 times\n    for i in range(27):\n        if i < num_hops:\n            ammag_key = get_bolt04_onion_key(b'ammag', hop_shared_secrets[i])\n            um_key = get_bolt04_onion_key(b'um', hop_shared_secrets[i])\n        else:\n            # SHOULD use constant `ammag` and `um` keys to obfuscate the route length.\n            ammag_key = get_bolt04_onion_key(b'ammag', dummy_secret)\n            um_key = get_bolt04_onion_key(b'um', dummy_secret)\n\n        stream_bytes = generate_cipher_stream(ammag_key, len(error_packet))\n        error_packet = xor_bytes(error_packet, stream_bytes)\n        hmac_computed = hmac_oneshot(um_key, msg=error_packet[32:], digest=hashlib.sha256)\n        hmac_found = error_packet[:32]\n        if util.constant_time_compare(hmac_found, hmac_computed) and i < num_hops:\n            result = error_packet, i\n\n    if result is not None:\n        return result\n    raise FailedToDecodeOnionError()\n\n\ndef decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],\n                       session_key: bytes) -> Tuple[OnionRoutingFailure, int]:\n    \"\"\"Returns the failure message, and the index of the sender of the error.\"\"\"\n    decrypted_error, sender_index = _decode_onion_error(error_packet, payment_path_pubkeys, session_key)\n    failure_msg = get_failure_msg_from_onion_error(decrypted_error)\n    return failure_msg, sender_index\n\n\ndef get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRoutingFailure:\n    # get failure_msg bytes from error packet\n    failure_len = int.from_bytes(decrypted_error_packet[32:34], byteorder='big')\n    failure_msg = decrypted_error_packet[34:34+failure_len]\n    # create failure message object\n    return OnionRoutingFailure.from_bytes(failure_msg)\n\n\n\n# TODO maybe we should rm this and just use OnionWireSerializer and onion_wire.csv\nBADONION = OnionFailureCodeMetaFlag.BADONION\nPERM     = OnionFailureCodeMetaFlag.PERM\nNODE     = OnionFailureCodeMetaFlag.NODE\nUPDATE   = OnionFailureCodeMetaFlag.UPDATE\nclass OnionFailureCode(IntEnum):\n    INVALID_REALM =                           PERM | 1\n    TEMPORARY_NODE_FAILURE =                  NODE | 2\n    PERMANENT_NODE_FAILURE =                  PERM | NODE | 2\n    REQUIRED_NODE_FEATURE_MISSING =           PERM | NODE | 3\n    INVALID_ONION_VERSION =                   BADONION | PERM | 4\n    INVALID_ONION_HMAC =                      BADONION | PERM | 5\n    INVALID_ONION_KEY =                       BADONION | PERM | 6\n    TEMPORARY_CHANNEL_FAILURE =               UPDATE | 7\n    PERMANENT_CHANNEL_FAILURE =               PERM | 8\n    REQUIRED_CHANNEL_FEATURE_MISSING =        PERM | 9\n    UNKNOWN_NEXT_PEER =                       PERM | 10\n    AMOUNT_BELOW_MINIMUM =                    UPDATE | 11\n    FEE_INSUFFICIENT =                        UPDATE | 12\n    INCORRECT_CLTV_EXPIRY =                   UPDATE | 13\n    EXPIRY_TOO_SOON =                         UPDATE | 14\n    INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS =    PERM | 15\n    _LEGACY_INCORRECT_PAYMENT_AMOUNT =        PERM | 16\n    FINAL_EXPIRY_TOO_SOON =                   17\n    FINAL_INCORRECT_CLTV_EXPIRY =             18\n    FINAL_INCORRECT_HTLC_AMOUNT =             19\n    CHANNEL_DISABLED =                        UPDATE | 20\n    EXPIRY_TOO_FAR =                          21\n    INVALID_ONION_PAYLOAD =                   PERM | 22\n    MPP_TIMEOUT =                             23\n    TRAMPOLINE_FEE_INSUFFICIENT =             NODE | 51\n    TRAMPOLINE_EXPIRY_TOO_SOON =              NODE | 52\n\n    @classmethod\n    def from_int(cls, code: int) -> Union[int, 'OnionFailureCode']:\n        try:\n            code = OnionFailureCode(code)\n        except ValueError:\n            pass  # unknown failure code\n        return code\n\n\n# don't use these elsewhere, the names are ambiguous without context\ndel BADONION; del PERM; del NODE; del UPDATE\n"
  },
  {
    "path": "electrum/lnpeer.py",
    "content": "#!/usr/bin/env python3\n#\n# Copyright (C) 2018 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nfrom collections import OrderedDict, defaultdict\nimport asyncio\nimport os\nimport time\nfrom typing import Tuple, Dict, TYPE_CHECKING, Optional, Union, Set, Callable, Coroutine, List, Any\nfrom datetime import datetime\nimport functools\nfrom functools import partial\nimport inspect\n\nimport electrum_ecc as ecc\nfrom electrum_ecc import ecdsa_sig64_from_r_and_s, ecdsa_der_sig_from_ecdsa_sig64, ECPubkey\n\nimport aiorpcx\nfrom aiorpcx import ignore_after\n\nfrom .lrucache import LRUCache\nfrom .crypto import sha256, sha256d, privkey_to_pubkey\nfrom . import bitcoin, util\nfrom . import constants\nfrom .util import (log_exceptions, ignore_exceptions, chunks, OldTaskGroup,\n                   UnrelatedTransactionException, error_text_bytes_to_safe_str, AsyncHangDetector,\n                   NoDynamicFeeEstimates, event_listener, EventListener)\nfrom . import transaction\nfrom .bitcoin import make_op_return, DummyAddress\nfrom .transaction import PartialTxOutput, match_script_against_template, Sighash\nfrom .logging import Logger\nfrom . import lnonion\nfrom .lnonion import (OnionFailureCode, OnionPacket, obfuscate_onion_error,\n                      OnionRoutingFailure, ProcessedOnionPacket, UnsupportedOnionPacketVersion,\n                      InvalidOnionMac, InvalidOnionPubkey, OnionFailureCodeMetaFlag,\n                      OnionParsingError)\nfrom .lnchannel import Channel, RevokeAndAck, ChannelState, PeerState, ChanCloseOption, CF_ANNOUNCE_CHANNEL\nfrom . import lnutil\nfrom .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, ChannelConfig,\n                     RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,\n                     funding_output_script, get_per_commitment_secret_from_seed,\n                     secret_to_pubkey, PaymentFailure, LnFeatures,\n                     LOCAL, REMOTE, HTLCOwner,\n                     ln_compare_features, MIN_FINAL_CLTV_DELTA_ACCEPTED,\n                     RemoteMisbehaving, ShortChannelID,\n                     IncompatibleLightningFeatures, ChannelType, LNProtocolWarning, validate_features,\n                     IncompatibleOrInsaneFeatures, ReceivedMPPStatus, ReceivedMPPHtlc,\n                     GossipForwardingMessage, GossipTimestampFilter, channel_id_from_funding_tx,\n                     serialize_htlc_key, Keypair, RecvMPPResolution)\nfrom .lntransport import LNTransport, LNTransportBase, LightningPeerConnectionClosed, HandshakeFailed\nfrom .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType, FailedToParseMsg\nfrom .interface import GracefulDisconnect\nfrom .json_db import StoredDict\nfrom .invoices import PR_PAID\nfrom .fee_policy import FEE_LN_ETA_TARGET, FEERATE_PER_KW_MIN_RELAY_LIGHTNING\nfrom .channel_db import FLAG_DIRECTION\n\nif TYPE_CHECKING:\n    from .lnworker import LNGossip, LNWallet\n    from .lnrouter import LNPaymentRoute\n    from .transaction import PartialTransaction\n\n\nLN_P2P_NETWORK_TIMEOUT = 20\n\n\nclass Peer(Logger, EventListener):\n    # note: in general this class is NOT thread-safe. Most methods are assumed to be running on asyncio thread.\n\n    ORDERED_MESSAGES = (\n        'accept_channel', 'funding_signed', 'funding_created', 'accept_channel', 'closing_signed')\n    SPAMMY_MESSAGES = (\n        'ping', 'pong', 'channel_announcement', 'node_announcement', 'channel_update',\n        'gossip_timestamp_filter', 'reply_channel_range', 'query_channel_range',\n        'query_short_channel_ids', 'reply_short_channel_ids', 'reply_short_channel_ids_end')\n\n    DELAY_INC_MSG_PROCESSING_SLEEP = 0.01\n    MIN_TIME_BETWEEN_SENDING_COMMITSIGS = 0.05\n    RECV_GOSSIP_QUEUE_SOFT_MAXSIZE = 2000\n    RECV_GOSSIP_QUEUE_HARD_MAXSIZE = 5000\n\n    def __init__(\n            self,\n            lnworker: Union['LNWallet', 'LNGossip'],\n            pubkey: bytes,\n            transport: LNTransportBase,\n            *, is_channel_backup= False):\n\n        self.lnworker = lnworker\n        self.network = lnworker.network\n        self.asyncio_loop = self.network.asyncio_loop\n        self.is_channel_backup = is_channel_backup\n        self._sent_init = False  # type: bool\n        self._received_init = False  # type: bool\n        self.initialized = self.asyncio_loop.create_future()\n        self.got_disconnected = asyncio.Event()\n        self.querying = asyncio.Event()\n        self.transport = transport\n        self.pubkey = pubkey  # remote pubkey\n        self.privkey = self.transport.privkey  # local privkey\n        self.features = self.lnworker.features  # type: LnFeatures\n        self.their_features = LnFeatures(0)  # type: LnFeatures\n        self.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)]\n        assert self.node_ids[0] != self.node_ids[1]\n        self.last_message_time = 0\n        self.pong_event = asyncio.Event()\n        self.reply_channel_range = None  # type: Optional[asyncio.Queue]\n        # gossip uses a single queue to preserve message order\n        self.recv_gossip_queue = asyncio.Queue(maxsize=self.RECV_GOSSIP_QUEUE_HARD_MAXSIZE)\n        self.our_gossip_timestamp_filter = None  # type: Optional[GossipTimestampFilter]\n        self.their_gossip_timestamp_filter = None  # type: Optional[GossipTimestampFilter]\n        self.outgoing_gossip_reply = False # type: bool\n        self.ordered_message_queues = defaultdict(partial(asyncio.Queue, maxsize=10))  # type: Dict[bytes, asyncio.Queue] # for messages that are ordered\n        self.temp_id_to_id = {}  # type: Dict[bytes, Optional[bytes]]   # to forward error messages\n        self.funding_created_sent = set() # for channels in PREOPENING\n        self.funding_signed_sent = set()  # for channels in PREOPENING\n        self.shutdown_received = {} # chan_id -> asyncio.Future()\n        self.channel_reestablish_msg = defaultdict(self.asyncio_loop.create_future)  # type: Dict[bytes, asyncio.Future]\n        self._chan_reest_finished = defaultdict(asyncio.Event)  # type: Dict[bytes, asyncio.Event]\n        self.orphan_channel_updates = OrderedDict()  # type: OrderedDict[ShortChannelID, dict]\n        Logger.__init__(self)\n        self.taskgroup = OldTaskGroup()\n        # HTLCs offered by REMOTE, that we started removing but are still active:\n        self.received_htlcs_pending_removal = set()  # type: Set[Tuple[Channel, int]]\n        self.received_htlc_removed_event = asyncio.Event()\n        self._htlc_switch_iterstart_event = asyncio.Event()\n        self._htlc_switch_iterdone_event = asyncio.Event()\n        self._received_revack_event = asyncio.Event()\n        self.received_commitsig_event = asyncio.Event()\n        self.downstream_htlc_resolved_event = asyncio.Event()\n        self.register_callbacks()\n        self._num_gossip_messages_forwarded = 0\n        self._processed_onion_cache = LRUCache(maxsize=100)  # type: LRUCache[bytes, ProcessedOnionPacket]\n        self._last_commitsig_sent_time = time.monotonic()\n\n    def send_message(self, message_name: str, **kwargs):\n        assert util.get_running_loop() == util.get_asyncio_loop(), f\"this must be run on the asyncio thread!\"\n        assert type(message_name) is str\n        if message_name not in self.SPAMMY_MESSAGES:\n            self.logger.debug(f\"Sending {message_name.upper()}\")\n        if message_name.upper() != \"INIT\" and not self.is_initialized():\n            raise Exception(\"tried to send message before we are initialized\")\n        raw_msg = encode_msg(message_name, **kwargs)\n        self._store_raw_msg_if_local_update(raw_msg, message_name=message_name, channel_id=kwargs.get(\"channel_id\"))\n        self.transport.send_bytes(raw_msg)\n\n    def _store_raw_msg_if_local_update(self, raw_msg: bytes, *, message_name: str, channel_id: Optional[bytes]):\n        is_commitment_signed = message_name == \"commitment_signed\"\n        if not (message_name.startswith(\"update_\") or is_commitment_signed):\n            return\n        assert channel_id\n        chan = self.get_channel_by_id(channel_id)\n        if not chan:\n            raise Exception(f\"channel {channel_id.hex()} not found for peer {self.pubkey.hex()}\")\n        chan.hm.store_local_update_raw_msg(raw_msg, is_commitment_signed=is_commitment_signed)\n        if is_commitment_signed:\n            # saving now, to ensure replaying updates works (in case of channel reestablishment)\n            self.lnworker.save_channel(chan)\n\n    def maybe_set_initialized(self):\n        if self.initialized.done():\n            return\n        if self._sent_init and self._received_init:\n            self.initialized.set_result(True)\n\n    def is_initialized(self) -> bool:\n        return (self.initialized.done()\n                and not self.initialized.cancelled()\n                and self.initialized.exception() is None\n                and self.initialized.result() is True)\n\n    async def initialize(self):\n        # If outgoing transport, do handshake now. For incoming, it has already been done.\n        if isinstance(self.transport, LNTransport):\n            await self.transport.handshake()\n        self.logger.info(f\"handshake done for {self.transport.peer_addr or self.pubkey.hex()}\")\n        features = self.features.for_init_message()\n        flen = features.min_len()\n        self.send_message(\n            \"init\", gflen=0, flen=flen,\n            features=features,\n            init_tlvs={\n                'networks':\n                {'chains': constants.net.rev_genesis_bytes()}\n            })\n        self._sent_init = True\n        self.maybe_set_initialized()\n\n    @property\n    def channels(self) -> Dict[bytes, Channel]:\n        return self.lnworker.channels_for_peer(self.pubkey)\n\n    def get_channel_by_id(self, channel_id: bytes) -> Optional[Channel]:\n        # note: this is faster than self.channels.get(channel_id)\n        chan = self.lnworker.get_channel_by_id(channel_id)\n        if not chan:\n            return None\n        if chan.node_id != self.pubkey:\n            return None\n        return chan\n\n    def diagnostic_name(self):\n        lnw_name = self.lnworker.diagnostic_name() or self.lnworker.__class__.__name__\n        return lnw_name + ', ' + self.transport.name()\n\n    async def ping_if_required(self):\n        if time.time() - self.last_message_time > 30:\n            self.send_message('ping', num_pong_bytes=4, byteslen=4)\n            self.pong_event.clear()\n            await self.pong_event.wait()\n\n    async def _process_message(self, message: bytes) -> None:\n        try:\n            message_type, payload = decode_msg(message)\n        except UnknownOptionalMsgType as e:\n            self.logger.info(f\"received unknown message from peer. ignoring: {e!r}\")\n            return\n        except FailedToParseMsg as e:\n            self.logger.info(\n                f\"failed to parse message from peer. disconnecting. \"\n                f\"msg_type={e.msg_type_name}({e.msg_type_int}). exc={e!r}\")\n            #self.logger.info(f\"failed to parse message: message(SECRET?)={message.hex()}\")\n            raise GracefulDisconnect() from e\n        self.last_message_time = time.time()\n        if message_type not in self.SPAMMY_MESSAGES:\n            self.logger.debug(f\"Received {message_type.upper()}\")\n        # only process INIT if we are a backup\n        if self.is_channel_backup is True and message_type != 'init':\n            return\n        if message_type in self.ORDERED_MESSAGES:\n            chan_id = payload.get('channel_id') or payload[\"temporary_channel_id\"]\n            if (\n                chan_id not in self.channels\n                and chan_id not in self.temp_id_to_id\n                and chan_id not in self.temp_id_to_id.values()\n            ):\n                raise Exception(f\"received {message_type} for unknown {chan_id.hex()=}\")\n            self.ordered_message_queues[chan_id].put_nowait((message_type, payload))\n        else:\n            if message_type not in ('error', 'warning') and 'channel_id' in payload:\n                chan = self.get_channel_by_id(payload['channel_id'])\n                if chan is None:\n                    self.logger.info(f\"Received {message_type} for unknown channel {payload['channel_id'].hex()}\")\n                    return\n                args = (chan, payload)\n            else:\n                args = (payload,)\n            try:\n                f = getattr(self, 'on_' + message_type)\n            except AttributeError:\n                #self.logger.info(\"Received '%s'\" % message_type.upper(), payload)\n                return\n            # raw message is needed to check signature\n            if message_type in ['node_announcement', 'channel_announcement', 'channel_update']:\n                payload['raw'] = message\n                payload['sender_node_id'] = self.pubkey\n            # note: the message handler might be async or non-async. In either case, by default,\n            #       we wait for it to complete before we return, i.e. before the next message is processed.\n            if inspect.iscoroutinefunction(f):\n                async with AsyncHangDetector(\n                    message=f\"message handler still running for {message_type.upper()}\",\n                    logger=self.logger,\n                ):\n                    await f(*args)\n            else:\n                f(*args)\n\n    def non_blocking_msg_handler(func):\n        \"\"\"Makes a message handler non-blocking: while processing the message,\n        the message_loop keeps processing subsequent incoming messages asynchronously.\n        \"\"\"\n        assert inspect.iscoroutinefunction(func), 'func needs to be a coroutine'\n        @functools.wraps(func)\n        async def wrapper(self: 'Peer', *args, **kwargs):\n            return await self.taskgroup.spawn(func(self, *args, **kwargs))\n        return wrapper\n\n    def on_warning(self, payload):\n        chan_id = payload.get(\"channel_id\")\n        err_bytes = payload['data']\n        is_known_chan_id = (chan_id in self.channels) or (chan_id in self.temp_id_to_id)\n        self.logger.info(f\"remote peer sent warning [DO NOT TRUST THIS MESSAGE]: \"\n                         f\"{error_text_bytes_to_safe_str(err_bytes, max_len=None)}. chan_id={chan_id.hex()}. \"\n                         f\"{is_known_chan_id=}\")\n\n    def on_error(self, payload):\n        chan_id = payload.get(\"channel_id\")\n        err_bytes = payload['data']\n        is_known_chan_id = (chan_id in self.channels) or (chan_id in self.temp_id_to_id)\n        self.logger.info(f\"remote peer sent error [DO NOT TRUST THIS MESSAGE]: \"\n                         f\"{error_text_bytes_to_safe_str(err_bytes, max_len=None)}. chan_id={chan_id.hex()}. \"\n                         f\"{is_known_chan_id=}\")\n        if chan := self.get_channel_by_id(chan_id):\n            self.schedule_force_closing(chan_id)\n            self.ordered_message_queues[chan_id].put_nowait((None, {'error': err_bytes}))\n            chan.save_remote_peer_sent_error(err_bytes)\n        elif chan_id in self.temp_id_to_id:\n            chan_id = self.temp_id_to_id[chan_id] or chan_id\n            self.ordered_message_queues[chan_id].put_nowait((None, {'error': err_bytes}))\n        elif chan_id == bytes(32):\n            # if channel_id is all zero:\n            # - MUST fail all channels with the sending node.\n            for cid in self.channels:\n                self.schedule_force_closing(cid)\n                self.ordered_message_queues[cid].put_nowait((None, {'error': err_bytes}))\n        else:\n            # if no existing channel is referred to by channel_id:\n            # - MUST ignore the message.\n            return\n        raise GracefulDisconnect\n\n    def send_warning(self, channel_id: bytes, message: str = None, *, close_connection=False):\n        \"\"\"Sends a warning and disconnects if close_connection.\n\n        Note:\n        * channel_id is the temporary channel id when the channel id is not yet available\n\n        A sending node:\n        MAY set channel_id to all zero if the warning is not related to a specific channel.\n\n        when failure was caused by an invalid signature check:\n        * SHOULD include the raw, hex-encoded transaction in reply to a funding_created,\n          funding_signed, closing_signed, or commitment_signed message.\n        \"\"\"\n        assert isinstance(channel_id, bytes)\n        encoded_data = b'' if not message else message.encode('ascii')\n        self.send_message('warning', channel_id=channel_id, data=encoded_data, len=len(encoded_data))\n        if close_connection:\n            raise GracefulDisconnect\n\n    def send_error(self, channel_id: bytes, message: str = None, *, force_close_channel=False):\n        \"\"\"Sends an error message and force closes the channel.\n\n        Note:\n        * channel_id is the temporary channel id when the channel id is not yet available\n\n        A sending node:\n        * SHOULD send error for protocol violations or internal errors that make channels\n          unusable or that make further communication unusable.\n        * SHOULD send error with the unknown channel_id in reply to messages of type\n          32-255 related to unknown channels.\n        * MUST fail the channel(s) referred to by the error message.\n        * MAY set channel_id to all zero to indicate all channels.\n\n        when failure was caused by an invalid signature check:\n        * SHOULD include the raw, hex-encoded transaction in reply to a funding_created,\n          funding_signed, closing_signed, or commitment_signed message.\n        \"\"\"\n        assert isinstance(channel_id, bytes)\n        encoded_data = b'' if not message else message.encode('ascii')\n        self.send_message('error', channel_id=channel_id, data=encoded_data, len=len(encoded_data))\n        # MUST fail the channel(s) referred to by the error message:\n        #  we may violate this with force_close_channel\n        if force_close_channel:\n            if channel_id in self.channels:\n                self.schedule_force_closing(channel_id)\n            elif channel_id == bytes(32):\n                for cid in self.channels:\n                    self.schedule_force_closing(cid)\n        raise GracefulDisconnect\n\n    def on_ping(self, payload):\n        l = payload['num_pong_bytes']\n        self.send_message('pong', byteslen=l)\n\n    def on_pong(self, payload):\n        self.pong_event.set()\n\n    async def wait_for_message(self, expected_name: str, channel_id: bytes):\n        q = self.ordered_message_queues[channel_id]\n        name, payload = await util.wait_for2(q.get(), LN_P2P_NETWORK_TIMEOUT)\n        # raise exceptions for errors, so that the caller sees them\n        if (err_bytes := payload.get(\"error\")) is not None:\n            err_text = error_text_bytes_to_safe_str(err_bytes)\n            raise GracefulDisconnect(\n                f\"remote peer sent error [DO NOT TRUST THIS MESSAGE]: {err_text}\")\n        if name != expected_name:\n            raise Exception(f\"Received unexpected '{name}'\")\n        return payload\n\n    def on_init(self, payload):\n        if self._received_init:\n            self.logger.info(\"ALREADY INITIALIZED BUT RECEIVED INIT\")\n            return\n        _their_features = int.from_bytes(payload['features'], byteorder=\"big\")\n        _their_features |= int.from_bytes(payload['globalfeatures'], byteorder=\"big\")\n        try:\n            self.their_features = validate_features(_their_features)\n        except IncompatibleOrInsaneFeatures as e:\n            raise GracefulDisconnect(f\"remote sent insane features: {repr(e)}\")\n        # check if features are compatible, and set self.features to what we negotiated\n        try:\n            self.features = ln_compare_features(self.features, self.their_features)\n        except IncompatibleLightningFeatures as e:\n            self.initialized.set_exception(e)\n            raise GracefulDisconnect(f\"{str(e)}\")\n        self.logger.info(\n            f\"received INIT with features={str(self.their_features.get_names())}. \"\n            f\"negotiated={str(self.features)}\")\n        # check that they are on the same chain as us, if provided\n        their_networks = payload[\"init_tlvs\"].get(\"networks\")\n        if their_networks:\n            their_chains = list(chunks(their_networks[\"chains\"], 32))\n            if constants.net.rev_genesis_bytes() not in their_chains:\n                raise GracefulDisconnect(f\"no common chain found with remote. (they sent: {their_chains})\")\n        # all checks passed\n        self.lnworker.lnpeermgr.on_peer_successfully_established(self)\n        self._received_init = True\n        self.maybe_set_initialized()\n\n    def on_node_announcement(self, payload):\n        if self.lnworker.uses_trampoline():\n            return\n        if self.our_gossip_timestamp_filter is None:\n            return  # why is the peer sending this? should we disconnect?\n        self.recv_gossip_queue.put_nowait(('node_announcement', payload))\n\n    def on_channel_announcement(self, payload):\n        if self.lnworker.uses_trampoline():\n            return\n        if self.our_gossip_timestamp_filter is None:\n            return  # why is the peer sending this? should we disconnect?\n        self.recv_gossip_queue.put_nowait(('channel_announcement', payload))\n\n    def on_channel_update(self, payload):\n        self.maybe_save_remote_update(payload)\n        if self.lnworker.uses_trampoline():\n            return\n        if self.our_gossip_timestamp_filter is None:\n            return  # why is the peer sending this? should we disconnect?\n        self.recv_gossip_queue.put_nowait(('channel_update', payload))\n\n    def on_query_channel_range(self, payload):\n        if self.lnworker == self.lnworker.network.lngossip or not self._should_forward_gossip():\n            return\n        if not self._is_valid_channel_range_query(payload):\n            return self.send_warning(bytes(32), \"received invalid query_channel_range\")\n        if self.outgoing_gossip_reply:\n            return self.send_warning(bytes(32), \"received multiple queries at the same time\")\n        self.outgoing_gossip_reply = True\n        self.recv_gossip_queue.put_nowait(('query_channel_range', payload))\n\n    def on_query_short_channel_ids(self, payload):\n        if self.lnworker == self.lnworker.network.lngossip or not self._should_forward_gossip():\n            return\n        if self.outgoing_gossip_reply:\n            return self.send_warning(bytes(32), \"received multiple queries at the same time\")\n        if not self._is_valid_short_channel_id_query(payload):\n            return self.send_warning(bytes(32), \"invalid query_short_channel_ids\")\n        self.outgoing_gossip_reply = True\n        self.recv_gossip_queue.put_nowait(('query_short_channel_ids', payload))\n\n    def on_gossip_timestamp_filter(self, payload):\n        if self._should_forward_gossip():\n            self.set_gossip_timestamp_filter(payload)\n\n    def set_gossip_timestamp_filter(self, payload: dict) -> None:\n        \"\"\"Set the gossip_timestamp_filter for this peer. If the peer requested historical gossip,\n        the request is put on the queue, otherwise only the forwarding loop will check the filter\"\"\"\n        if payload.get('chain_hash') != constants.net.rev_genesis_bytes():\n            return\n        filter = GossipTimestampFilter.from_payload(payload)\n        self.their_gossip_timestamp_filter = filter\n        self.logger.debug(f\"got gossip_ts_filter from peer {self.pubkey.hex()}: \"\n                          f\"{str(self.their_gossip_timestamp_filter)}\")\n        if filter and not filter.only_forwarding:\n            self.recv_gossip_queue.put_nowait(('gossip_timestamp_filter', None))\n\n    def maybe_save_remote_update(self, payload):\n        if not self.channels:\n            return\n        for chan in self.channels.values():\n            if payload['short_channel_id'] in [chan.short_channel_id, chan.get_local_scid_alias()]:\n                # originator: node_id_1 if the least-significant bit of flags is 0 or node_id_2 otherwise\n                flags = int.from_bytes(payload['channel_flags'], byteorder='big', signed=False)\n                originator = sorted(self.node_ids)[flags & FLAG_DIRECTION]\n                if originator == self.lnworker.node_keypair.pubkey:\n                    self.logger.debug(f\"peer sent us our own channel update for chan {chan.get_id_for_log()}\")\n                    return\n                chan.set_remote_update(payload)\n                self.logger.info(f\"saved remote channel_update gossip msg for chan {chan.get_id_for_log()}\")\n                break\n        else:\n            # Save (some bounded number of) orphan channel updates for later\n            # as it might be for our own direct channel with this peer\n            # (and we might not yet know the short channel id for that)\n            # Background: this code is here to deal with a bug in LND,\n            # see https://github.com/lightningnetwork/lnd/issues/3651 (closed 2022-08-13, lnd-v0.15.1)\n            # and https://github.com/lightningnetwork/lightning-rfc/pull/657\n            # This code assumes gossip_queries is set. BOLT7: \"if the\n            # gossip_queries feature is negotiated, [a node] MUST NOT\n            # send gossip it did not generate itself\"\n            # NOTE: The definition of gossip_queries changed\n            # https://github.com/lightning/bolts/commit/fce8bab931674a81a9ea895c9e9162e559e48a65\n            short_channel_id = ShortChannelID(payload['short_channel_id'])\n            self.logger.debug(f'received orphan channel update {short_channel_id}')\n            self.orphan_channel_updates[short_channel_id] = payload\n            while len(self.orphan_channel_updates) > 25:\n                self.orphan_channel_updates.popitem(last=False)\n\n    def on_announcement_signatures(self, chan: Channel, payload):\n        if not chan.is_public() or chan.short_channel_id is None:\n            return\n        h = chan.get_channel_announcement_hash()\n        node_signature = payload[\"node_signature\"]\n        bitcoin_signature = payload[\"bitcoin_signature\"]\n        if not ECPubkey(chan.config[REMOTE].multisig_key.pubkey).ecdsa_verify(bitcoin_signature, h):\n            raise Exception(\"bitcoin_sig invalid in announcement_signatures\")\n        if not ECPubkey(self.pubkey).ecdsa_verify(node_signature, h):\n            raise Exception(\"node_sig invalid in announcement_signatures\")\n        chan.config[REMOTE].announcement_node_sig = node_signature\n        chan.config[REMOTE].announcement_bitcoin_sig = bitcoin_signature\n        self.lnworker.save_channel(chan)\n        self.maybe_send_announcement_signatures(chan, is_reply=True)\n\n    def handle_disconnect(func):\n        @functools.wraps(func)\n        async def wrapper_func(self, *args, **kwargs):\n            try:\n                return await func(self, *args, **kwargs)\n            except GracefulDisconnect as e:\n                self.logger.log(e.log_level, f\"Disconnecting: {repr(e)}\")\n            except (LightningPeerConnectionClosed, IncompatibleLightningFeatures,\n                    aiorpcx.socks.SOCKSError) as e:\n                self.logger.info(f\"Disconnecting: {repr(e)}\")\n            finally:\n                self.close_and_cleanup()\n        return wrapper_func\n\n    @ignore_exceptions  # do not kill outer taskgroup\n    @log_exceptions\n    @handle_disconnect\n    async def main_loop(self):\n        async with self.taskgroup as group:\n            await group.spawn(self._message_loop())  # initializes connection\n            try:\n                await util.wait_for2(self.initialized, LN_P2P_NETWORK_TIMEOUT)\n            except Exception as e:\n                raise GracefulDisconnect(f\"Failed to initialize: {e!r}\") from e\n            await group.spawn(self._query_gossip())\n            await group.spawn(self._process_gossip())\n            await group.spawn(self._send_own_gossip())\n            await group.spawn(self._forward_gossip())\n            if self.network.lngossip != self.lnworker:\n                await group.spawn(self.htlc_switch())\n\n    async def _process_gossip(self):\n        while True:\n            await asyncio.sleep(5)\n            if not self.network.lngossip:\n                continue\n            chan_anns = []\n            chan_upds = []\n            node_anns = []\n            while True:\n                name, payload = await self.recv_gossip_queue.get()\n                if name == 'channel_announcement':\n                    chan_anns.append(payload)\n                elif name == 'channel_update':\n                    chan_upds.append(payload)\n                elif name == 'node_announcement':\n                    node_anns.append(payload)\n                elif name == 'query_channel_range':\n                    await self.taskgroup.spawn(self._send_reply_channel_range(payload))\n                elif name == 'query_short_channel_ids':\n                    await self.taskgroup.spawn(self._send_reply_short_channel_ids(payload))\n                elif name == 'gossip_timestamp_filter':\n                    await self.taskgroup.spawn(self._handle_historical_gossip_request())\n                else:\n                    raise Exception('unknown message')\n                if self.recv_gossip_queue.empty():\n                    break\n            if self.network.lngossip:\n                await self.network.lngossip.process_gossip(chan_anns, node_anns, chan_upds)\n\n    async def _send_own_gossip(self):\n        if self.lnworker == self.lnworker.network.lngossip:\n            return\n        assert self.is_initialized()\n        await asyncio.sleep(10)\n        while True:\n            public_channels = [chan for chan in self.lnworker.channels.values() if chan.is_public()]\n            if public_channels:\n                alias = self.lnworker.config.LIGHTNING_NODE_ALIAS\n                color = self.lnworker.config.LIGHTNING_NODE_COLOR_RGB\n                self.send_node_announcement(alias, color)\n                for chan in public_channels:\n                    if chan.is_open() and chan.peer_state == PeerState.GOOD:\n                        self.maybe_send_channel_announcement(chan)\n                        self.maybe_send_channel_update(chan)\n            await asyncio.sleep(600)\n\n    def _should_forward_gossip(self) -> bool:\n        if (self.network.lngossip != self.lnworker\n                and not self.lnworker.uses_trampoline()\n                and self.features.supports(LnFeatures.GOSSIP_QUERIES_REQ)):\n            return True\n        return False\n\n    async def _forward_gossip(self):\n        assert self.is_initialized()\n        if not self._should_forward_gossip():\n            return\n\n        async def send_new_gossip_with_semaphore(gossip: List[GossipForwardingMessage]):\n            async with self.network.lngossip.gossip_request_semaphore:\n                sent = await self._send_gossip_messages(gossip)\n            if sent > 0:\n                self.logger.debug(f\"forwarded {sent} gossip messages to {self.pubkey.hex()}\")\n\n        lngossip = self.network.lngossip\n        last_gossip_batch_ts = 0\n        while True:\n            await asyncio.sleep(10)\n            if not self.their_gossip_timestamp_filter:\n                continue  # peer didn't request gossip\n\n            new_gossip, last_lngossip_refresh_ts = await lngossip.get_forwarding_gossip()\n            if not last_lngossip_refresh_ts > last_gossip_batch_ts:\n                continue  # no new batch available\n            last_gossip_batch_ts = last_lngossip_refresh_ts\n\n            await self.taskgroup.spawn(send_new_gossip_with_semaphore(new_gossip))\n\n    async def _handle_historical_gossip_request(self):\n        \"\"\"Called when a peer requests historical gossip with a gossip_timestamp_filter query.\"\"\"\n        filter = self.their_gossip_timestamp_filter\n        if not self._should_forward_gossip() or not filter or filter.only_forwarding:\n            return\n        async with self.network.lngossip.gossip_request_semaphore:\n            requested_gossip = self.lnworker.channel_db.get_gossip_in_timespan(filter)\n            filter.only_forwarding = True\n            sent = await self._send_gossip_messages(requested_gossip)\n            if sent > 0:\n                self._num_gossip_messages_forwarded += sent\n                #self.logger.debug(f\"forwarded {sent} historical gossip messages to {self.pubkey.hex()}\")\n\n    async def _send_gossip_messages(self, messages: List[GossipForwardingMessage]) -> int:\n        amount_sent = 0\n        for msg in messages:\n            if self.their_gossip_timestamp_filter.in_range(msg.timestamp) \\\n                and self.pubkey != msg.sender_node_id:\n                await self.transport.send_bytes_and_drain(msg.msg)\n                amount_sent += 1\n                if amount_sent % 250 == 0:\n                    # this can be a lot of messages, completely blocking the event loop\n                    await asyncio.sleep(self.DELAY_INC_MSG_PROCESSING_SLEEP)\n        return amount_sent\n\n    async def _query_gossip(self):\n        assert self.is_initialized()\n        if self.lnworker == self.lnworker.network.lngossip:\n            if not self.their_features.supports(LnFeatures.GOSSIP_QUERIES_OPT):\n                raise GracefulDisconnect(\"remote does not support gossip_queries, which we need\")\n            try:\n                ids, complete = await util.wait_for2(self.get_channel_range(), LN_P2P_NETWORK_TIMEOUT)\n            except asyncio.TimeoutError as e:\n                raise GracefulDisconnect(\"query_channel_range timed out\") from e\n            self.logger.info('Received {} channel ids. (complete: {})'.format(len(ids), complete))\n            await self.lnworker.add_new_ids(ids)\n            self.request_gossip(int(time.time()))\n            while True:\n                todo = self.lnworker.get_ids_to_query()\n                if not todo:\n                    await asyncio.sleep(1)\n                    continue\n                await self.get_short_channel_ids(todo)\n\n    @staticmethod\n    def _is_valid_channel_range_query(payload: dict) -> bool:\n        if payload.get('chain_hash') != constants.net.rev_genesis_bytes():\n            return False\n        if payload.get('first_blocknum', -1) < constants.net.BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS:\n            return False\n        if payload.get('number_of_blocks', 0) < 1:\n            return False\n        return True\n\n    def _is_valid_short_channel_id_query(self, payload: dict) -> bool:\n        if payload.get('chain_hash') != constants.net.rev_genesis_bytes():\n            return False\n        enc_short_ids = payload['encoded_short_ids']\n        if enc_short_ids[0] != 0:\n            self.logger.debug(f\"got query_short_channel_ids with invalid encoding: {repr(enc_short_ids[0])}\")\n            return False\n        if (len(enc_short_ids) - 1) % 8 != 0:\n            self.logger.debug(f\"got query_short_channel_ids with invalid length\")\n            return False\n        return True\n\n    async def _send_reply_channel_range(self, payload: dict):\n        \"\"\"https://github.com/lightning/bolts/blob/acd383145dd8c3fecd69ce94e4a789767b984ac0/07-routing-gossip.md#requirements-5\"\"\"\n        first_blockheight: int = payload['first_blocknum']\n\n        async with self.network.lngossip.gossip_request_semaphore:\n            sorted_scids: List[ShortChannelID] = self.lnworker.channel_db.get_channels_in_range(\n                first_blockheight,\n                payload['number_of_blocks']\n            )\n            self.logger.debug(f\"reply_channel_range to request \"\n                              f\"first_height={first_blockheight}, \"\n                              f\"num_blocks={payload['number_of_blocks']}, \"\n                              f\"sending {len(sorted_scids)} scids\")\n\n            complete: bool = False\n            while not complete:\n                # create a 64800 byte chunk of skids, split the remaining scids\n                encoded_scids, sorted_scids = b''.join(sorted_scids[:8100]), sorted_scids[8100:]\n                complete = len(sorted_scids) == 0  # if there are no scids remaining we are done\n                # number of blocks covered by the scids in this chunk\n                if complete:\n                    # LAST MESSAGE MUST have first_blocknum plus number_of_blocks equal or greater than\n                    # the query_channel_range first_blocknum plus number_of_blocks.\n                    number_of_blocks = ((payload['first_blocknum'] + payload['number_of_blocks'])\n                                        - first_blockheight)\n                else:\n                    # we cover the range until the height of the first scid in the next chunk\n                    number_of_blocks = sorted_scids[0].block_height - first_blockheight\n                self.send_message('reply_channel_range',\n                    chain_hash=constants.net.rev_genesis_bytes(),\n                    first_blocknum=first_blockheight,\n                    number_of_blocks=number_of_blocks,\n                    sync_complete=complete,\n                    len=1+len(encoded_scids),\n                    encoded_short_ids=b'\\x00' + encoded_scids)\n                if not complete:\n                    first_blockheight = sorted_scids[0].block_height\n                    await asyncio.sleep(self.DELAY_INC_MSG_PROCESSING_SLEEP)\n            self.outgoing_gossip_reply = False\n\n    async def get_channel_range(self):\n        self.reply_channel_range = asyncio.Queue()\n        first_block = constants.net.BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS\n        num_blocks = self.lnworker.network.get_local_height() - first_block\n        self.query_channel_range(first_block, num_blocks)\n        intervals = []\n        ids = set()\n        # note: implementations behave differently...\n        # \"sane implementation that follows BOLT-07\" example:\n        #   query_channel_range. <<< first_block 497000, num_blocks 79038\n        #   on_reply_channel_range. >>> first_block 497000, num_blocks 39516, num_ids 4648, complete True\n        #   on_reply_channel_range. >>> first_block 536516, num_blocks 19758, num_ids 5734, complete True\n        #   on_reply_channel_range. >>> first_block 556274, num_blocks 9879, num_ids 13712, complete True\n        #   on_reply_channel_range. >>> first_block 566153, num_blocks 9885, num_ids 18114, complete True\n        # lnd example:\n        #   query_channel_range. <<< first_block 497000, num_blocks 79038\n        #   on_reply_channel_range. >>> first_block 497000, num_blocks 79038, num_ids 8000, complete False\n        #   on_reply_channel_range. >>> first_block 497000, num_blocks 79038, num_ids 8000, complete False\n        #   on_reply_channel_range. >>> first_block 497000, num_blocks 79038, num_ids 8000, complete False\n        #   on_reply_channel_range. >>> first_block 497000, num_blocks 79038, num_ids 8000, complete False\n        #   on_reply_channel_range. >>> first_block 497000, num_blocks 79038, num_ids 5344, complete True\n        # ADDENDUM (01/2025): now it's 'MUST set sync_complete to false if this is not the final reply_channel_range.'\n        while True:\n            index, num, complete, _ids = await self.reply_channel_range.get()\n            ids.update(_ids)\n            intervals.append((index, index+num))\n            intervals.sort()\n            while len(intervals) > 1:\n                a,b = intervals[0]\n                c,d = intervals[1]\n                if not (a <= c and a <= b and c <= d):\n                    raise Exception(f\"insane reply_channel_range intervals {(a,b,c,d)}\")\n                if b >= c:\n                    intervals = [(a,d)] + intervals[2:]\n                else:\n                    break\n            if len(intervals) == 1 and complete:\n                a, b = intervals[0]\n                if a <= first_block and b >= first_block + num_blocks:\n                    break\n        self.reply_channel_range = None\n        return ids, complete\n\n    def request_gossip(self, timestamp=0):\n        if timestamp == 0:\n            self.logger.info('requesting whole channel graph')\n        else:\n            self.logger.info(f'requesting channel graph since {datetime.fromtimestamp(timestamp).isoformat()}')\n        timestamp_range = 0xFFFFFFFF\n        self.our_gossip_timestamp_filter = GossipTimestampFilter(\n            first_timestamp=timestamp,\n            timestamp_range=timestamp_range,\n        )\n        self.send_message(\n            'gossip_timestamp_filter',\n            chain_hash=constants.net.rev_genesis_bytes(),\n            first_timestamp=timestamp,\n            timestamp_range=timestamp_range,\n        )\n\n    def query_channel_range(self, first_block, num_blocks):\n        self.logger.info(f'query channel range {first_block} {num_blocks}')\n        self.send_message(\n            'query_channel_range',\n            chain_hash=constants.net.rev_genesis_bytes(),\n            first_blocknum=first_block,\n            number_of_blocks=num_blocks)\n\n    @staticmethod\n    def decode_short_ids(encoded):\n        if len(encoded) < 1 or (len(encoded) - 1) % 8 != 0:\n            raise Exception(f'decode_short_ids: invalid size: {len(encoded)=}')\n        elif encoded[0] != 0:\n            raise Exception(f'decode_short_ids: unexpected first byte: {encoded[0]}')\n        decoded = encoded[1:]\n        ids = [decoded[i:i+8] for i in range(0, len(decoded), 8)]\n        return ids\n\n    async def on_reply_channel_range(self, payload):\n        first = payload['first_blocknum']\n        num = payload['number_of_blocks']\n        complete = bool(int.from_bytes(payload['sync_complete'], 'big'))\n        encoded = payload['encoded_short_ids']\n        ids = self.decode_short_ids(encoded)\n        # self.logger.info(f\"on_reply_channel_range. >>> first_block {first}, num_blocks {num}, \"\n        #                  f\"num_ids {len(ids)}, complete {complete}\")\n        if self.reply_channel_range is None:\n            raise Exception(\"received 'reply_channel_range' without corresponding 'query_channel_range'\")\n        while self.reply_channel_range.qsize() > 10:\n            # we block process_message until the queue gets consumed\n            self.logger.info(\"reply_channel_range queue is overflowing. sleeping...\")\n            await asyncio.sleep(0.1)\n        self.reply_channel_range.put_nowait((first, num, complete, ids))\n\n    async def _send_reply_short_channel_ids(self, payload: dict):\n        async with self.network.lngossip.gossip_request_semaphore:\n            requested_scids = payload['encoded_short_ids']\n            decoded_scids = [ShortChannelID.normalize(scid)\n                             for scid in self.decode_short_ids(requested_scids)]\n            self.logger.debug(f\"serving query_short_channel_ids request: \"\n                              f\"requested {len(decoded_scids)} scids\")\n            chan_db = self.lnworker.channel_db\n            response: Set[bytes] = set()\n            for scid in decoded_scids:\n                requested_msgs = chan_db.get_gossip_for_scid_request(scid)\n                response.update(requested_msgs)\n            self.logger.debug(f\"found {len(response)} gossip messages to serve scid request\")\n            for index, msg in enumerate(response):\n                await self.transport.send_bytes_and_drain(msg)\n                if index % 250 == 0:\n                    await asyncio.sleep(self.DELAY_INC_MSG_PROCESSING_SLEEP)\n            self.send_message(\n                'reply_short_channel_ids_end',\n                chain_hash=constants.net.rev_genesis_bytes(),\n                full_information=self.network.lngossip.is_synced()\n            )\n            self.outgoing_gossip_reply = False\n\n    async def get_short_channel_ids(self, ids):\n        #self.logger.info(f'Querying {len(ids)} short_channel_ids')\n        assert not self.querying.is_set()\n        self.query_short_channel_ids(ids)\n        await self.querying.wait()\n        self.querying.clear()\n\n    def query_short_channel_ids(self, ids):\n        # compression MUST NOT be used according to updated bolt\n        # (https://github.com/lightning/bolts/pull/981)\n        ids = sorted(ids)\n        s = b''.join(ids)\n        prefix = b'\\x00'  # uncompressed\n        self.send_message(\n            'query_short_channel_ids',\n            chain_hash=constants.net.rev_genesis_bytes(),\n            len=1+len(s),\n            encoded_short_ids=prefix+s)\n\n    async def _message_loop(self):\n        try:\n            await util.wait_for2(self.initialize(), LN_P2P_NETWORK_TIMEOUT)\n        except (OSError, asyncio.TimeoutError, HandshakeFailed) as e:\n            raise GracefulDisconnect(f'initialize failed: {repr(e)}') from e\n        async for msg in self.transport.read_messages():\n            await self._process_message(msg)\n            if self.DELAY_INC_MSG_PROCESSING_SLEEP:\n                # rate-limit message-processing a bit, to make it harder\n                # for a single peer to bog down the event loop / cpu:\n                await asyncio.sleep(self.DELAY_INC_MSG_PROCESSING_SLEEP)\n            # If receiving too much gossip from this peer, we need to slow them down.\n            # note: if the gossip queue gets full, we will disconnect from them\n            #       and throw away unprocessed gossip.\n            if self.recv_gossip_queue.qsize() > self.RECV_GOSSIP_QUEUE_SOFT_MAXSIZE:\n                sleep = self.recv_gossip_queue.qsize() / 1000\n                self.logger.debug(\n                    f\"message_loop sleeping due to getting much gossip. qsize={self.recv_gossip_queue.qsize()}. \"\n                    f\"waiting for existing gossip data to be processed first.\")\n                await asyncio.sleep(sleep)\n\n    def on_reply_short_channel_ids_end(self, payload):\n        self.querying.set()\n\n    def close_and_cleanup(self):\n        # note: This method might get called multiple times!\n        #       E.g. if you call close_and_cleanup() to cause a disconnection from the peer,\n        #       it will get called a second time in handle_disconnect().\n        self.unregister_callbacks()\n        try:\n            if self.transport:\n                self.transport.close()\n        except Exception:\n            pass\n        self.lnworker.lnpeermgr.peer_closed(self)\n        self.got_disconnected.set()\n\n    def is_shutdown_anysegwit(self):\n        return self.features.supports(LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT)\n\n    def accepts_zeroconf(self):\n        return self.features.supports(LnFeatures.OPTION_ZEROCONF_OPT)\n\n    def is_upfront_shutdown_script(self):\n        return self.features.supports(LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT)\n\n    def use_anchors(self) -> bool:\n        return self.features.supports(LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT)\n\n    def upfront_shutdown_script_from_payload(self, payload, msg_identifier: str) -> Optional[bytes]:\n        if msg_identifier not in ['accept', 'open']:\n            raise ValueError(\"msg_identifier must be either 'accept' or 'open'\")\n\n        uss_tlv = payload[msg_identifier + '_channel_tlvs'].get(\n            'upfront_shutdown_script')\n\n        if uss_tlv and self.is_upfront_shutdown_script():\n            upfront_shutdown_script = uss_tlv['shutdown_scriptpubkey']\n        else:\n            upfront_shutdown_script = b''\n        self.logger.info(f\"upfront shutdown script received: {upfront_shutdown_script}\")\n        return upfront_shutdown_script\n\n    def temporarily_reserve_funding_tx_change_address(func):\n        # During the channel open flow, if we initiated, we might have used a change address\n        # of ours in the funding tx. The funding tx is not part of the wallet history\n        # at that point yet, but we should already consider this change address as 'used'.\n        @functools.wraps(func)\n        async def wrapper(self: 'Peer', *args, **kwargs):\n            funding_tx = kwargs['funding_tx']  # type: PartialTransaction\n            wallet = self.lnworker.wallet\n            change_addresses = [txout.address for txout in funding_tx.outputs()\n                                if wallet.is_change(txout.address)]\n            for addr in change_addresses:\n                wallet.set_reserved_state_of_address(addr, reserved=True)\n            try:\n                return await func(self, *args, **kwargs)\n            finally:\n                for addr in change_addresses:\n                    self.lnworker.wallet.set_reserved_state_of_address(addr, reserved=False)\n        return wrapper\n\n    @temporarily_reserve_funding_tx_change_address\n    async def channel_establishment_flow(\n            self, *,\n            funding_tx: 'PartialTransaction',\n            funding_sat: int,\n            push_msat: int,\n            public: bool,\n            zeroconf: bool = False,\n            temp_channel_id: bytes,\n            opening_fee: int = None,\n    ) -> Tuple[Channel, 'PartialTransaction']:\n        \"\"\"Implements the channel opening flow.\n\n        -> open_channel message\n        <- accept_channel message\n        -> funding_created message\n        <- funding_signed message\n\n        Channel configurations are initialized in this method.\n        \"\"\"\n\n        if public and not self.lnworker.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS:\n            raise Exception('Cannot create public channels')\n\n        if not self.lnworker.wallet.can_have_lightning():\n            # old wallet that cannot have lightning anymore\n            raise Exception('This wallet cannot create new channels')\n\n        # will raise if init fails\n        await util.wait_for2(self.initialized, LN_P2P_NETWORK_TIMEOUT)\n        # trampoline is not yet in features\n        if self.lnworker.uses_trampoline() and not self.lnworker.is_trampoline_peer(self.pubkey):\n            raise Exception('Not a trampoline node: ' + str(self.their_features))\n\n        channel_flags = CF_ANNOUNCE_CHANNEL if public else 0\n        feerate: Optional[int] = self.lnworker.current_target_feerate_per_kw(\n            has_anchors=self.use_anchors()\n        )\n        if feerate is None:\n            raise NoDynamicFeeEstimates()\n        # we set a channel type for internal bookkeeping\n        open_channel_tlvs = {}\n        assert self.their_features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)\n        our_channel_type = ChannelType(ChannelType.OPTION_STATIC_REMOTEKEY)\n        if self.use_anchors():\n            our_channel_type |= ChannelType(ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX)\n        if zeroconf:\n            our_channel_type |= ChannelType(ChannelType.OPTION_ZEROCONF)\n        # We do not set the option_scid_alias bit in channel_type because LND rejects it.\n        # Eclair accepts channel_type with that bit, but does not require it.\n\n        # if option_channel_type is negotiated: MUST set channel_type\n        # if it includes channel_type: MUST set it to a defined type representing the type it wants.\n        open_channel_tlvs['channel_type'] = {\n            'type': our_channel_type.to_bytes_minimal()\n        }\n\n        if our_channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX:\n            multisig_funding_keypair = lnutil.derive_multisig_funding_key_if_we_opened(\n                funding_root_secret=self.lnworker.funding_root_keypair.privkey,\n                remote_node_id_or_prefix=self.pubkey,\n                nlocktime=funding_tx.locktime,\n            )\n        else:\n            multisig_funding_keypair = None\n        local_config = self.lnworker.make_local_config_for_new_channel(\n            funding_sat=funding_sat,\n            push_msat=push_msat,\n            initiator=LOCAL,\n            channel_type=our_channel_type,\n            multisig_funding_keypair=multisig_funding_keypair,\n            peer_features=self.features,\n        )\n        # if it includes open_channel_tlvs: MUST include upfront_shutdown_script.\n        open_channel_tlvs['upfront_shutdown_script'] = {\n            'shutdown_scriptpubkey': local_config.upfront_shutdown_script\n        }\n        if opening_fee:\n            # todo: maybe add payment hash\n            open_channel_tlvs['channel_opening_fee'] = {\n                'channel_opening_fee': opening_fee\n            }\n        # for the first commitment transaction\n        per_commitment_secret_first = get_per_commitment_secret_from_seed(\n            local_config.per_commitment_secret_seed,\n            RevocationStore.START_INDEX\n        )\n        per_commitment_point_first = secret_to_pubkey(\n            int.from_bytes(per_commitment_secret_first, 'big'))\n\n        # store the temp id now, so that it is recognized for e.g. 'error' messages\n        self.temp_id_to_id[temp_channel_id] = None\n        self._cleanup_temp_channelids()\n        self.send_message(\n            \"open_channel\",\n            temporary_channel_id=temp_channel_id,\n            chain_hash=constants.net.rev_genesis_bytes(),\n            funding_satoshis=funding_sat,\n            push_msat=push_msat,\n            dust_limit_satoshis=local_config.dust_limit_sat,\n            feerate_per_kw=feerate,\n            max_accepted_htlcs=local_config.max_accepted_htlcs,\n            funding_pubkey=local_config.multisig_key.pubkey,\n            revocation_basepoint=local_config.revocation_basepoint.pubkey,\n            htlc_basepoint=local_config.htlc_basepoint.pubkey,\n            payment_basepoint=local_config.payment_basepoint.pubkey,\n            delayed_payment_basepoint=local_config.delayed_basepoint.pubkey,\n            first_per_commitment_point=per_commitment_point_first,\n            to_self_delay=local_config.to_self_delay,\n            max_htlc_value_in_flight_msat=local_config.max_htlc_value_in_flight_msat,\n            channel_flags=channel_flags,\n            channel_reserve_satoshis=local_config.reserve_sat,\n            htlc_minimum_msat=local_config.htlc_minimum_msat,\n            open_channel_tlvs=open_channel_tlvs,\n        )\n\n        # <- accept_channel\n        payload = await self.wait_for_message('accept_channel', temp_channel_id)\n        self.logger.debug(f\"received accept_channel for temp_channel_id={temp_channel_id.hex()}. {payload=}\")\n        remote_per_commitment_point = payload['first_per_commitment_point']\n        funding_txn_minimum_depth = payload['minimum_depth']\n        if not zeroconf and funding_txn_minimum_depth <= 0:\n            raise Exception(f\"minimum depth too low, {funding_txn_minimum_depth}\")\n        if funding_txn_minimum_depth > 30:\n            raise Exception(f\"minimum depth too high, {funding_txn_minimum_depth}\")\n\n        upfront_shutdown_script = self.upfront_shutdown_script_from_payload(\n            payload, 'accept')\n\n        accept_channel_tlvs = payload.get('accept_channel_tlvs')\n        their_channel_type = accept_channel_tlvs.get('channel_type') if accept_channel_tlvs else None\n        if their_channel_type:\n            their_channel_type = ChannelType.from_bytes(their_channel_type['type'], byteorder='big').discard_unknown_and_check()\n            # if channel_type is set, and channel_type was set in open_channel,\n            # and they are not equal types: MUST reject the channel.\n            if open_channel_tlvs.get('channel_type') is not None and their_channel_type != our_channel_type:\n                raise Exception(\"Channel type is not the one that we sent.\")\n\n        remote_config = RemoteConfig(\n            payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),\n            multisig_key=OnlyPubkeyKeypair(payload[\"funding_pubkey\"]),\n            htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']),\n            delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']),\n            revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']),\n            to_self_delay=payload['to_self_delay'],\n            dust_limit_sat=payload['dust_limit_satoshis'],\n            max_htlc_value_in_flight_msat=payload['max_htlc_value_in_flight_msat'],\n            max_accepted_htlcs=payload[\"max_accepted_htlcs\"],\n            initial_msat=push_msat,\n            reserve_sat=payload[\"channel_reserve_satoshis\"],\n            htlc_minimum_msat=payload['htlc_minimum_msat'],\n            next_per_commitment_point=remote_per_commitment_point,\n            current_per_commitment_point=None,\n            upfront_shutdown_script=upfront_shutdown_script,\n            announcement_node_sig=b'',\n            announcement_bitcoin_sig=b'',\n        )\n        ChannelConfig.cross_validate_params(\n            local_config=local_config,\n            remote_config=remote_config,\n            funding_sat=funding_sat,\n            is_local_initiator=True,\n            initial_feerate_per_kw=feerate,\n            config=self.network.config,\n            peer_features=self.features,\n            channel_type=our_channel_type,\n        )\n\n        # -> funding created\n        # replace dummy output in funding tx\n        redeem_script = funding_output_script(local_config, remote_config)\n        funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)\n        funding_output = PartialTxOutput.from_address_and_value(funding_address, funding_sat)\n        funding_tx.replace_output_address(DummyAddress.CHANNEL, funding_address)\n        # find and encrypt op_return data associated to funding_address\n        has_onchain_backup = self.lnworker and self.lnworker.has_recoverable_channels()\n        if has_onchain_backup:\n            backup_data = self.lnworker.cb_data(self.pubkey)\n            dummy_scriptpubkey = make_op_return(backup_data)\n            for o in funding_tx.outputs():\n                if o.scriptpubkey == dummy_scriptpubkey:\n                    encrypted_data = self.lnworker.encrypt_cb_data(backup_data, funding_address)\n                    assert len(encrypted_data) == len(backup_data)\n                    o.scriptpubkey = make_op_return(encrypted_data)\n                    break\n            else:\n                raise Exception('op_return output not found in funding tx')\n        # must not be malleable\n        funding_tx.set_rbf(False)\n        if not funding_tx.is_segwit():\n            raise Exception('Funding transaction is not segwit')\n        funding_txid = funding_tx.txid()\n        assert funding_txid\n        funding_index = funding_tx.outputs().index(funding_output)\n        # build remote commitment transaction\n        channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_index)\n        outpoint = Outpoint(funding_txid, funding_index)\n        constraints = ChannelConstraints(\n            flags=channel_flags,\n            capacity=funding_sat,\n            is_initiator=True,\n            funding_txn_minimum_depth=funding_txn_minimum_depth\n        )\n        storage = self.create_channel_storage(\n            channel_id, outpoint, local_config, remote_config, constraints, our_channel_type)\n        chan = Channel(\n            storage,\n            lnworker=self.lnworker,\n            initial_feerate=feerate\n        )\n        chan.storage['funding_inputs'] = [txin.prevout.to_json() for txin in funding_tx.inputs()]\n        chan.storage['has_onchain_backup'] = has_onchain_backup\n        chan.storage['init_height'] = self.lnworker.network.get_local_height()\n        chan.storage['init_timestamp'] = int(time.time())\n        if isinstance(self.transport, LNTransport):\n            chan.add_or_update_peer_addr(self.transport.peer_addr)\n        sig_64, _ = chan.sign_next_commitment()\n        self.temp_id_to_id[temp_channel_id] = channel_id\n\n        self.send_message(\"funding_created\",\n            temporary_channel_id=temp_channel_id,\n            funding_txid=funding_txid_bytes,\n            funding_output_index=funding_index,\n            signature=sig_64)\n        self.funding_created_sent.add(channel_id)\n\n        # <- funding signed\n        payload = await self.wait_for_message('funding_signed', channel_id)\n        self.logger.info('received funding_signed')\n        remote_sig = payload['signature']\n        try:\n            chan.receive_new_commitment(remote_sig, [])\n        except LNProtocolWarning as e:\n            self.send_warning(channel_id, message=str(e), close_connection=True)\n        chan.open_with_first_pcp(remote_per_commitment_point, remote_sig)\n        chan.set_state(ChannelState.OPENING)\n        if zeroconf:\n            chan.set_state(ChannelState.FUNDED)\n            self.send_channel_ready(chan)\n        self.lnworker.add_new_channel(chan)\n        return chan, funding_tx\n\n    def create_channel_storage(self, channel_id, outpoint, local_config, remote_config, constraints, channel_type):\n        chan_dict = {\n            \"node_id\": self.pubkey.hex(),\n            \"channel_id\": channel_id.hex(),\n            \"short_channel_id\": None,\n            \"funding_outpoint\": outpoint,\n            \"remote_config\": remote_config,\n            \"local_config\": local_config,\n            \"constraints\": constraints,\n            \"remote_update\": None,\n            \"state\": ChannelState.PREOPENING.name,\n            'onion_keys': {},\n            'data_loss_protect_remote_pcp': {},\n            \"log\": {},\n            \"unfulfilled_htlcs\": {},\n            \"revocation_store\": {},\n            \"channel_type\": channel_type,\n        }\n        return StoredDict(chan_dict, self.lnworker.db)\n\n    @non_blocking_msg_handler\n    async def on_open_channel(self, payload):\n        \"\"\"Implements the channel acceptance flow.\n\n        <- open_channel message\n        -> accept_channel message\n        <- funding_created message\n        -> funding_signed message\n\n        Channel configurations are initialized in this method.\n        \"\"\"\n\n        # <- open_channel\n        if payload['chain_hash'] != constants.net.rev_genesis_bytes():\n            raise Exception('wrong chain_hash')\n\n        open_channel_tlvs = payload.get('open_channel_tlvs')\n        channel_type = open_channel_tlvs.get('channel_type') if open_channel_tlvs else None\n        # The receiving node MAY fail the channel if:\n        # option_channel_type was negotiated but the message doesn't include a channel_type\n        if channel_type is None:\n            raise Exception(\"sender has advertised option_channel_type, but hasn't sent the channel type\")\n        # MUST fail the channel if it supports channel_type,\n        # channel_type was set, and the type is not suitable.\n        else:\n            channel_type = ChannelType.from_bytes(channel_type['type'], byteorder='big').discard_unknown_and_check()\n            if not channel_type.complies_with_features(self.features):\n                raise Exception(\"sender has sent a channel type we don't support\")\n        assert isinstance(channel_type, ChannelType)\n\n        is_zeroconf = bool(channel_type & ChannelType.OPTION_ZEROCONF)\n        if is_zeroconf and not self.network.config.ZEROCONF_TRUSTED_NODE.startswith(self.pubkey.hex()):\n            raise Exception(f\"not accepting zeroconf from node {self.pubkey}\")\n\n        if self.lnworker.has_recoverable_channels() and not is_zeroconf:\n            # FIXME: we might want to keep the connection open\n            raise Exception('not accepting channels')\n\n        if not self.lnworker.wallet.can_have_lightning():\n            # old wallet that cannot have lightning anymore\n            raise Exception('This wallet does not accept new channels')\n\n        funding_sat = payload['funding_satoshis']\n        push_msat = payload['push_msat']\n        feerate = payload['feerate_per_kw']  # note: we are not validating this\n        temp_chan_id = payload['temporary_channel_id']\n        # store the temp id now, so that it is recognized for e.g. 'error' messages\n        self.temp_id_to_id[temp_chan_id] = None\n        self._cleanup_temp_channelids()\n        channel_opening_fee_tlv = open_channel_tlvs.get('channel_opening_fee', {})\n        channel_opening_fee = channel_opening_fee_tlv.get('channel_opening_fee')\n        if channel_opening_fee:\n            # todo check that the fee is reasonable\n            assert is_zeroconf\n            self.logger.info(f\"just-in-time opening fee: {channel_opening_fee} msat\")\n            pass\n\n        if channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX:\n            multisig_funding_keypair = lnutil.derive_multisig_funding_key_if_they_opened(\n                funding_root_secret=self.lnworker.funding_root_keypair.privkey,\n                remote_node_id_or_prefix=self.pubkey,\n                remote_funding_pubkey=payload['funding_pubkey'],\n            )\n        else:\n            multisig_funding_keypair = None\n        local_config = self.lnworker.make_local_config_for_new_channel(\n            funding_sat=funding_sat,\n            push_msat=push_msat,\n            initiator=REMOTE,\n            channel_type=channel_type,\n            multisig_funding_keypair=multisig_funding_keypair,\n            peer_features=self.features,\n        )\n\n        upfront_shutdown_script = self.upfront_shutdown_script_from_payload(\n            payload, 'open')\n\n        remote_config = RemoteConfig(\n            payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),\n            multisig_key=OnlyPubkeyKeypair(payload['funding_pubkey']),\n            htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']),\n            delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']),\n            revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']),\n            to_self_delay=payload['to_self_delay'],\n            dust_limit_sat=payload['dust_limit_satoshis'],\n            max_htlc_value_in_flight_msat=payload['max_htlc_value_in_flight_msat'],\n            max_accepted_htlcs=payload['max_accepted_htlcs'],\n            initial_msat=funding_sat * 1000 - push_msat,\n            reserve_sat=payload['channel_reserve_satoshis'],\n            htlc_minimum_msat=payload['htlc_minimum_msat'],\n            next_per_commitment_point=payload['first_per_commitment_point'],\n            current_per_commitment_point=None,\n            upfront_shutdown_script=upfront_shutdown_script,\n            announcement_node_sig=b'',\n            announcement_bitcoin_sig=b'',\n        )\n        ChannelConfig.cross_validate_params(\n            local_config=local_config,\n            remote_config=remote_config,\n            funding_sat=funding_sat,\n            is_local_initiator=False,\n            initial_feerate_per_kw=feerate,\n            config=self.network.config,\n            peer_features=self.features,\n            channel_type=channel_type,\n        )\n\n        channel_flags = ord(payload['channel_flags'])\n\n        # -> accept channel\n        # for the first commitment transaction\n        per_commitment_secret_first = get_per_commitment_secret_from_seed(\n            local_config.per_commitment_secret_seed,\n            RevocationStore.START_INDEX\n        )\n        per_commitment_point_first = secret_to_pubkey(\n            int.from_bytes(per_commitment_secret_first, 'big'))\n\n        min_depth = 0 if is_zeroconf else 3\n\n        accept_channel_tlvs = {\n            'upfront_shutdown_script': {\n                'shutdown_scriptpubkey': local_config.upfront_shutdown_script\n            },\n            'channel_type': {\n                'type': channel_type.to_bytes_minimal(),\n            },\n        }\n\n        self.send_message(\n            'accept_channel',\n            temporary_channel_id=temp_chan_id,\n            dust_limit_satoshis=local_config.dust_limit_sat,\n            max_htlc_value_in_flight_msat=local_config.max_htlc_value_in_flight_msat,\n            channel_reserve_satoshis=local_config.reserve_sat,\n            htlc_minimum_msat=local_config.htlc_minimum_msat,\n            minimum_depth=min_depth,\n            to_self_delay=local_config.to_self_delay,\n            max_accepted_htlcs=local_config.max_accepted_htlcs,\n            funding_pubkey=local_config.multisig_key.pubkey,\n            revocation_basepoint=local_config.revocation_basepoint.pubkey,\n            payment_basepoint=local_config.payment_basepoint.pubkey,\n            delayed_payment_basepoint=local_config.delayed_basepoint.pubkey,\n            htlc_basepoint=local_config.htlc_basepoint.pubkey,\n            first_per_commitment_point=per_commitment_point_first,\n            accept_channel_tlvs=accept_channel_tlvs,\n        )\n\n        # <- funding created\n        funding_created = await self.wait_for_message('funding_created', temp_chan_id)\n\n        # -> funding signed\n        funding_idx = funding_created['funding_output_index']\n        funding_txid = funding_created['funding_txid'][::-1].hex()\n        channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx)\n        constraints = ChannelConstraints(\n            flags=channel_flags,\n            capacity=funding_sat,\n            is_initiator=False,\n            funding_txn_minimum_depth=min_depth,\n        )\n        outpoint = Outpoint(funding_txid, funding_idx)\n        chan_dict = self.create_channel_storage(\n            channel_id, outpoint, local_config, remote_config, constraints, channel_type)\n        chan = Channel(\n            chan_dict,\n            lnworker=self.lnworker,\n            initial_feerate=feerate,\n            jit_opening_fee = channel_opening_fee,\n        )\n        chan.storage['init_height'] = self.lnworker.network.get_local_height()\n        chan.storage['init_timestamp'] = int(time.time())\n        if isinstance(self.transport, LNTransport):\n            chan.add_or_update_peer_addr(self.transport.peer_addr)\n        remote_sig = funding_created['signature']\n        try:\n            chan.receive_new_commitment(remote_sig, [])\n        except LNProtocolWarning as e:\n            self.send_warning(channel_id, message=str(e), close_connection=True)\n        sig_64, _ = chan.sign_next_commitment()\n        self.send_message('funding_signed',\n            channel_id=channel_id,\n            signature=sig_64,\n        )\n        self.temp_id_to_id[temp_chan_id] = channel_id\n        self.funding_signed_sent.add(chan.channel_id)\n        chan.open_with_first_pcp(payload['first_per_commitment_point'], remote_sig)\n        chan.set_state(ChannelState.OPENING)\n        if is_zeroconf:\n            chan.set_state(ChannelState.FUNDED)\n            self.send_channel_ready(chan)\n        self.lnworker.add_new_channel(chan)\n\n    def _cleanup_temp_channelids(self) -> None:\n        self.temp_id_to_id = {\n            tmp_id: chan_id for (tmp_id, chan_id) in self.temp_id_to_id.items()\n            if chan_id not in self.channels\n        }\n        if len(self.temp_id_to_id) > 25:\n            # which one of us is opening all these chans?! let's disconnect\n            raise Exception(\"temp_id_to_id is getting too large.\")\n\n    async def request_force_close(self, channel_id: bytes):\n        \"\"\"Try to trigger the remote peer to force-close.\"\"\"\n        await self.initialized\n        self.logger.info(f\"trying to get remote peer to force-close chan {channel_id.hex()}\")\n        # First, we intentionally send a \"channel_reestablish\" msg with an old state.\n        # Many nodes (but not all) automatically force-close when seeing this.\n        latest_point = secret_to_pubkey(42) # we need a valid point (BOLT2)\n        self.send_message(\n            \"channel_reestablish\",\n            channel_id=channel_id,\n            next_commitment_number=0,\n            next_revocation_number=0,\n            your_last_per_commitment_secret=0,\n            my_current_per_commitment_point=latest_point)\n        # Newish nodes that have lightning/bolts/pull/950 force-close upon receiving an \"error\" msg,\n        # so send that too. E.g. old \"channel_reestablish\" is not enough for eclair 0.7+,\n        # but \"error\" is. see https://github.com/ACINQ/eclair/pull/2036\n        # The receiving node:\n        #   - upon receiving `error`:\n        #     - MUST fail the channel referred to by `channel_id`, if that channel is with the sending node.\n        self.send_message(\"error\", channel_id=channel_id, data=b\"\", len=0)\n\n    def schedule_force_closing(self, channel_id: bytes):\n        \"\"\" wrapper of lnworker's method, that raises if channel is not with this peer \"\"\"\n        channels_with_peer = list(self.channels.keys())\n        channels_with_peer.extend(self.temp_id_to_id.values())\n        if channel_id not in channels_with_peer:\n            raise ValueError(f\"channel {channel_id.hex()} does not belong to this peer\")\n        chan = self.get_channel_by_id(channel_id)\n        if not chan:\n            self.logger.warning(f\"tried to force-close channel {channel_id.hex()} but it is not in self.channels yet\")\n        if ChanCloseOption.LOCAL_FCLOSE in chan.get_close_options():\n            self.lnworker.schedule_force_closing(channel_id)\n        else:\n            self.logger.info(f\"tried to force-close channel {chan.get_id_for_log()} \"\n                             f\"but close option is not allowed. {chan.get_state()=!r}\")\n\n    async def on_channel_reestablish(self, chan: Channel, msg):\n        # Note: it is critical for this message handler to block processing of further messages,\n        #       until this msg is processed. If we are behind (lost state), and send chan_reest to the remote,\n        #       when the remote realizes we are behind, they might send an \"error\" message - but the spec mandates\n        #       they send chan_reest first. If we processed the error first, we might force-close and lose money!\n        their_next_local_ctn = msg[\"next_commitment_number\"]\n        their_oldest_unrevoked_remote_ctn = msg[\"next_revocation_number\"]\n        their_local_pcp = msg.get(\"my_current_per_commitment_point\")\n        their_claim_of_our_last_per_commitment_secret = msg.get(\"your_last_per_commitment_secret\")\n        self.logger.info(\n            f'channel_reestablish ({chan.get_id_for_log()}): received channel_reestablish with '\n            f'(their_next_local_ctn={their_next_local_ctn}, '\n            f'their_oldest_unrevoked_remote_ctn={their_oldest_unrevoked_remote_ctn})')\n        if chan.get_state() >= ChannelState.CLOSED:\n            self.logger.warning(\n                f\"on_channel_reestablish. dropping message. illegal action. \"\n                f\"chan={chan.get_id_for_log()}. {chan.get_state()=!r}. {chan.peer_state=!r}\")\n            return\n        # sanity checks of received values\n        if their_next_local_ctn < 0:\n            raise RemoteMisbehaving(f\"channel reestablish: their_next_local_ctn < 0\")\n        if their_oldest_unrevoked_remote_ctn < 0:\n            raise RemoteMisbehaving(f\"channel reestablish: their_oldest_unrevoked_remote_ctn < 0\")\n        # ctns\n        oldest_unrevoked_local_ctn = chan.get_oldest_unrevoked_ctn(LOCAL)\n        latest_local_ctn = chan.get_latest_ctn(LOCAL)\n        next_local_ctn = chan.get_next_ctn(LOCAL)\n        oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)\n        latest_remote_ctn = chan.get_latest_ctn(REMOTE)\n        next_remote_ctn = chan.get_next_ctn(REMOTE)\n        # compare remote ctns\n        we_are_ahead = False\n        they_are_ahead = False\n        we_must_resend_revoke_and_ack = False\n        if next_remote_ctn != their_next_local_ctn:\n            if their_next_local_ctn == latest_remote_ctn and chan.hm.is_revack_pending(REMOTE):\n                # We will replay the local updates (see reestablish_channel), which should contain a commitment_signed\n                # (due to is_revack_pending being true), and this should remedy this situation.\n                pass\n            else:\n                self.logger.warning(\n                    f\"channel_reestablish ({chan.get_id_for_log()}): \"\n                    f\"expected remote ctn {next_remote_ctn}, got {their_next_local_ctn}\")\n                if their_next_local_ctn < next_remote_ctn:\n                    we_are_ahead = True\n                else:\n                    they_are_ahead = True\n        # compare local ctns\n        if oldest_unrevoked_local_ctn != their_oldest_unrevoked_remote_ctn:\n            if oldest_unrevoked_local_ctn - 1 == their_oldest_unrevoked_remote_ctn:\n                # A node:\n                #    if next_revocation_number is equal to the commitment number of the last revoke_and_ack\n                #    the receiving node sent, AND the receiving node hasn't already received a closing_signed:\n                #        MUST re-send the revoke_and_ack.\n                we_must_resend_revoke_and_ack = True\n            else:\n                self.logger.warning(\n                    f\"channel_reestablish ({chan.get_id_for_log()}): \"\n                    f\"expected local ctn {oldest_unrevoked_local_ctn}, got {their_oldest_unrevoked_remote_ctn}\")\n                if their_oldest_unrevoked_remote_ctn < oldest_unrevoked_local_ctn:\n                    we_are_ahead = True\n                else:\n                    they_are_ahead = True\n        # option_data_loss_protect\n        assert self.features.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT)\n        def are_datalossprotect_fields_valid() -> bool:\n            if their_local_pcp is None or their_claim_of_our_last_per_commitment_secret is None:\n                return False\n            if their_oldest_unrevoked_remote_ctn > 0:\n                our_pcs, __ = chan.get_secret_and_point(LOCAL, their_oldest_unrevoked_remote_ctn - 1)\n            else:\n                assert their_oldest_unrevoked_remote_ctn == 0\n                our_pcs = bytes(32)\n            if our_pcs != their_claim_of_our_last_per_commitment_secret:\n                self.logger.error(\n                    f\"channel_reestablish ({chan.get_id_for_log()}): \"\n                    f\"(DLP) local PCS mismatch: {our_pcs.hex()} != {their_claim_of_our_last_per_commitment_secret.hex()}\")\n                return False\n            assert chan.is_static_remotekey_enabled()\n            return True\n        if not are_datalossprotect_fields_valid():\n            raise RemoteMisbehaving(\"channel_reestablish: data loss protect fields invalid\")\n        fut = self.channel_reestablish_msg[chan.channel_id]\n        if they_are_ahead:\n            self.logger.warning(\n                f\"channel_reestablish ({chan.get_id_for_log()}): \"\n                f\"remote is ahead of us! They should force-close. Remote PCP: {their_local_pcp.hex()}\")\n            # data_loss_protect_remote_pcp is used in lnsweep\n            chan.set_data_loss_protect_remote_pcp(their_next_local_ctn - 1, their_local_pcp)\n            chan.set_state(ChannelState.WE_ARE_TOXIC)\n            self.lnworker.save_channel(chan)\n            chan.peer_state = PeerState.BAD\n            # raise after we send channel_reestablish, so the remote can realize they are ahead\n            # FIXME what if we have multiple chans with peer? timing...\n            fut.set_exception(GracefulDisconnect(\"remote ahead of us\"))\n        elif we_are_ahead:\n            self.logger.warning(f\"channel_reestablish ({chan.get_id_for_log()}): we are ahead of remote! trying to force-close.\")\n            self.schedule_force_closing(chan.channel_id)\n            # FIXME what if we have multiple chans with peer? timing...\n            fut.set_exception(GracefulDisconnect(\"we are ahead of remote\"))\n        else:\n            # all good\n            fut.set_result((we_must_resend_revoke_and_ack, their_next_local_ctn))\n            # Block processing of further incoming messages until we finished our part of chan-reest.\n            # This is needed for the replaying of our local unacked updates to be sane (if the peer\n            # also replays some messages we must not react to them until we finished replaying our own).\n            # (it would be sufficient to only block messages related to this channel, but this is easier)\n            await self._chan_reest_finished[chan.channel_id].wait()\n            # Note: if the above event is never set, we won't detect if the connection was closed by remote...\n\n    def _send_channel_reestablish(self, chan: Channel):\n        assert self.is_initialized()\n        chan_id = chan.channel_id\n        # ctns\n        next_local_ctn = chan.get_next_ctn(LOCAL)\n        oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)\n        # send message\n        assert chan.is_static_remotekey_enabled()\n        latest_secret, latest_point = chan.get_secret_and_point(LOCAL, 0)\n        if oldest_unrevoked_remote_ctn == 0:\n            last_rev_secret = 0\n        else:\n            last_rev_index = oldest_unrevoked_remote_ctn - 1\n            last_rev_secret = chan.revocation_store.retrieve_secret(RevocationStore.START_INDEX - last_rev_index)\n        self.send_message(\n            \"channel_reestablish\",\n            channel_id=chan_id,\n            next_commitment_number=next_local_ctn,\n            next_revocation_number=oldest_unrevoked_remote_ctn,\n            your_last_per_commitment_secret=last_rev_secret,\n            my_current_per_commitment_point=latest_point)\n        self.logger.info(\n            f'channel_reestablish ({chan.get_id_for_log()}): sent channel_reestablish with '\n            f'(next_local_ctn={next_local_ctn}, '\n            f'oldest_unrevoked_remote_ctn={oldest_unrevoked_remote_ctn})')\n\n    async def reestablish_channel(self, chan: Channel):\n        await self.initialized\n        chan_id = chan.channel_id\n        if chan.should_request_force_close:\n            if chan.get_state() != ChannelState.WE_ARE_TOXIC:\n                chan.set_state(ChannelState.REQUESTED_FCLOSE)\n            await self.request_force_close(chan_id)\n            chan.should_request_force_close = False\n            return\n        if chan.get_state() == ChannelState.WE_ARE_TOXIC:\n            # Depending on timing, the remote might not know we are behind.\n            # We should let them know, so that they force-close.\n            # We do \"request force-close\" with ctn=0, instead of leaking our actual ctns,\n            # to decrease the remote's confidence of actual data loss on our part.\n            await self.request_force_close(chan_id)\n            return\n        if chan.get_state() == ChannelState.FORCE_CLOSING:\n            # We likely got here because we found out that we are ahead (i.e. remote lost state).\n            # Depending on timing, the remote might not know they are behind.\n            # We should let them know:\n            self._send_channel_reestablish(chan)\n            return\n        if self.network.blockchain().is_tip_stale() \\\n                or not self.lnworker.wallet.is_up_to_date() \\\n                or self.lnworker.current_target_feerate_per_kw(has_anchors=chan.has_anchors()) \\\n            is None:\n            # don't try to reestablish until we can do fee estimation and are up-to-date\n            return\n        # if we get here, we will try to do a proper reestablish\n        if not (ChannelState.PREOPENING < chan.get_state() < ChannelState.FORCE_CLOSING):\n            raise Exception(f\"unexpected {chan.get_state()=} for reestablish\")\n        if chan.peer_state != PeerState.DISCONNECTED:\n            self.logger.info(\n                f'reestablish_channel was called but channel {chan.get_id_for_log()} '\n                f'already in peer_state {chan.peer_state!r}')\n            return\n        chan.peer_state = PeerState.REESTABLISHING\n        util.trigger_callback('channel', self.lnworker.wallet, chan)\n        # ctns\n        oldest_unrevoked_local_ctn = chan.get_oldest_unrevoked_ctn(LOCAL)\n        next_local_ctn = chan.get_next_ctn(LOCAL)\n        oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)\n        # BOLT-02: \"A node [...] upon disconnection [...] MUST reverse any uncommitted updates sent by the other side\"\n        chan.hm.discard_unsigned_remote_updates()\n        # send message\n        self._send_channel_reestablish(chan)\n        # wait until we receive their channel_reestablish\n        fut = self.channel_reestablish_msg[chan_id]\n        await fut\n        we_must_resend_revoke_and_ack, their_next_local_ctn = fut.result()\n\n        def replay_updates_and_commitsig():\n            # Replay un-acked local updates (including commitment_signed) byte-for-byte.\n            # If we have sent them a commitment signature that they \"lost\" (due to disconnect),\n            # we need to make sure we replay the same local updates, as otherwise they could\n            # end up with two (or more) signed valid commitment transactions at the same ctn.\n            # Multiple valid ctxs at the same ctn is a major headache for pre-signing spending txns,\n            # e.g. for watchtowers, hence we must ensure these ctxs coincide.\n            # We replay the local updates even if they were not yet committed.\n            unacked = chan.hm.get_unacked_local_updates()\n            replayed_msgs = []\n            for ctn, messages in unacked.items():\n                if ctn < their_next_local_ctn:\n                    # They claim to have received these messages and the corresponding\n                    # commitment_signed, hence we must not replay them.\n                    continue\n                for raw_upd_msg in messages:\n                    self.transport.send_bytes(raw_upd_msg)\n                    replayed_msgs.append(raw_upd_msg)\n            self.logger.info(f'channel_reestablish ({chan.get_id_for_log()}): replayed {len(replayed_msgs)} unacked messages. '\n                             f'{[decode_msg(raw_upd_msg)[0] for raw_upd_msg in replayed_msgs]}')\n\n        def resend_revoke_and_ack():\n            last_secret, last_point = chan.get_secret_and_point(LOCAL, oldest_unrevoked_local_ctn - 1)\n            next_secret, next_point = chan.get_secret_and_point(LOCAL, oldest_unrevoked_local_ctn + 1)\n            self.send_message(\n                \"revoke_and_ack\",\n                channel_id=chan.channel_id,\n                per_commitment_secret=last_secret,\n                next_per_commitment_point=next_point)\n\n        # We need to preserve relative order of last revack and commitsig.\n        # note: it is not possible to recover and reestablish a channel if we are out-of-sync by\n        # more than one ctns, i.e. we will only ever retransmit up to one commitment_signed message.\n        # Hence, if we need to retransmit a revack, without loss of generality, we can either replay\n        # it as the first message or as the last message.\n        was_revoke_last = chan.hm.was_revoke_last()\n        if we_must_resend_revoke_and_ack and not was_revoke_last:\n            self.logger.info(f'channel_reestablish ({chan.get_id_for_log()}): replaying a revoke_and_ack first.')\n            resend_revoke_and_ack()\n        replay_updates_and_commitsig()\n        if we_must_resend_revoke_and_ack and was_revoke_last:\n            self.logger.info(f'channel_reestablish ({chan.get_id_for_log()}): replaying a revoke_and_ack last.')\n            resend_revoke_and_ack()\n\n        chan.peer_state = PeerState.GOOD\n        self._chan_reest_finished[chan.channel_id].set()\n        if chan.is_funded():\n            chan_just_became_ready = (their_next_local_ctn == next_local_ctn == 1)\n            if chan_just_became_ready or self.features.supports(LnFeatures.OPTION_SCID_ALIAS_OPT):\n                self.send_channel_ready(chan)\n\n        self.maybe_send_announcement_signatures(chan)\n        self.maybe_update_fee(chan)  # if needed, update fee ASAP, to avoid force-closures from this\n        # checks done\n        util.trigger_callback('channel', self.lnworker.wallet, chan)\n        # if we have sent a previous shutdown, it must be retransmitted (Bolt2)\n        if chan.get_state() == ChannelState.SHUTDOWN:\n            await self.taskgroup.spawn(self.send_shutdown(chan))\n\n    def send_channel_ready(self, chan: Channel):\n        assert chan.is_funded()\n        if chan.sent_channel_ready:\n            return\n        channel_id = chan.channel_id\n        per_commitment_secret_index = RevocationStore.START_INDEX - 1\n        second_per_commitment_point = secret_to_pubkey(int.from_bytes(\n            get_per_commitment_secret_from_seed(chan.config[LOCAL].per_commitment_secret_seed, per_commitment_secret_index), 'big'))\n        channel_ready_tlvs = {}\n        if self.features.supports(LnFeatures.OPTION_SCID_ALIAS_OPT):\n            # LND requires that we send an alias if the option has been negotiated in INIT.\n            # otherwise, the channel will not be marked as active.\n            # This does not apply if the channel was previously marked active without an alias.\n            channel_ready_tlvs['short_channel_id'] = {'alias': chan.get_local_scid_alias(create_new_if_needed=True)}\n        # note: if 'channel_ready' was not yet received, we might send it multiple times\n        self.send_message(\n            \"channel_ready\",\n            channel_id=channel_id,\n            second_per_commitment_point=second_per_commitment_point,\n            channel_ready_tlvs=channel_ready_tlvs)\n        chan.sent_channel_ready = True\n        self.maybe_mark_open(chan)\n\n    def on_channel_ready(self, chan: Channel, payload):\n        self.logger.info(f\"on_channel_ready. channel: {chan.channel_id.hex()}\")\n        if chan.peer_state != PeerState.GOOD:  # should never happen\n            raise Exception(f\"received channel_ready in unexpected {chan.peer_state=!r}\")\n        if chan.is_closed():\n            self.logger.warning(\n                f\"on_channel_ready. dropping message. illegal action. \"\n                f\"chan={chan.get_id_for_log()}. {chan.get_state()=!r}. {chan.peer_state=!r}\")\n            return\n        # save remote alias for use in invoices\n        scid_alias = payload.get('channel_ready_tlvs', {}).get('short_channel_id', {}).get('alias')\n        if scid_alias:\n            chan.save_remote_scid_alias(scid_alias)\n        if not chan.config[LOCAL].funding_locked_received:\n            their_next_point = payload[\"second_per_commitment_point\"]\n            chan.config[REMOTE].next_per_commitment_point = their_next_point\n            chan.config[LOCAL].funding_locked_received = True\n            self.lnworker.save_channel(chan)\n        self.maybe_mark_open(chan)\n\n    def send_node_announcement(self, alias:str, color_hex:str):\n        from .channel_db import NodeInfo\n        timestamp = int(time.time())\n        node_id = privkey_to_pubkey(self.privkey)\n        features = self.features.for_node_announcement()\n        flen = features.min_len()\n        rgb_color = bytes.fromhex(color_hex)\n        alias = bytes(alias, 'utf8')\n        alias += bytes(32 - len(alias))\n        if self.lnworker.config.LIGHTNING_LISTEN is not None:\n            addr = self.lnworker.config.LIGHTNING_LISTEN\n            try:\n                hostname, port = addr.split(':')\n                if port is None:  # use default port if not specified\n                    port = 9735\n                addresses = NodeInfo.to_addresses_field(hostname, int(port))\n            except Exception:\n                self.logger.exception(f\"Invalid lightning_listen address: {addr}\")\n                return\n        else:\n            addresses = b''\n        raw_msg = encode_msg(\n            \"node_announcement\",\n            flen=flen,\n            features=features,\n            timestamp=timestamp,\n            rgb_color=rgb_color,\n            node_id=node_id,\n            alias=alias,\n            addrlen=len(addresses),\n            addresses=addresses)\n        h = sha256d(raw_msg[64+2:])\n        signature = ecc.ECPrivkey(self.privkey).ecdsa_sign(h, sigencode=ecdsa_sig64_from_r_and_s)\n        message_type, payload = decode_msg(raw_msg)\n        payload['signature'] = signature\n        raw_msg = encode_msg(message_type, **payload)\n        self.transport.send_bytes(raw_msg)\n\n    def maybe_send_channel_announcement(self, chan: Channel):\n        node_sigs = [chan.config[REMOTE].announcement_node_sig, chan.config[LOCAL].announcement_node_sig]\n        bitcoin_sigs = [chan.config[REMOTE].announcement_bitcoin_sig, chan.config[LOCAL].announcement_bitcoin_sig]\n        if not bitcoin_sigs[0] or not bitcoin_sigs[1]:\n            return\n        raw_msg, is_reverse = chan.construct_channel_announcement_without_sigs()\n        if is_reverse:\n            node_sigs.reverse()\n            bitcoin_sigs.reverse()\n        message_type, payload = decode_msg(raw_msg)\n        payload['node_signature_1'] = node_sigs[0]\n        payload['node_signature_2'] = node_sigs[1]\n        payload['bitcoin_signature_1'] = bitcoin_sigs[0]\n        payload['bitcoin_signature_2'] = bitcoin_sigs[1]\n        raw_msg = encode_msg(message_type, **payload)\n        self.transport.send_bytes(raw_msg)\n\n    def maybe_send_channel_update(self, chan: Channel):\n        chan_upd = chan.get_outgoing_gossip_channel_update()\n        self.transport.send_bytes(chan_upd)\n\n    def maybe_mark_open(self, chan: Channel):\n        if not chan.sent_channel_ready:\n            return\n        if not chan.config[LOCAL].funding_locked_received:\n            return\n        self.mark_open(chan)\n\n    def mark_open(self, chan: Channel):\n        assert chan.is_funded()\n        # only allow state transition from \"FUNDED\" to \"OPEN\"\n        old_state = chan.get_state()\n        if old_state == ChannelState.OPEN:\n            return\n        if old_state != ChannelState.FUNDED:\n            self.logger.info(f\"cannot mark open ({chan.get_id_for_log()}), current state: {repr(old_state)}\")\n            return\n        assert chan.config[LOCAL].funding_locked_received\n        chan.set_state(ChannelState.OPEN)\n        util.trigger_callback('channel', self.lnworker.wallet, chan)\n        # peer may have sent us a channel update for the incoming direction previously\n        pending_channel_update = self.orphan_channel_updates.get(chan.short_channel_id)\n        if pending_channel_update:\n            chan.set_remote_update(pending_channel_update)\n        self.logger.info(f\"CHANNEL OPENING COMPLETED ({chan.get_id_for_log()})\")\n        if chan.is_public():\n            # send channel_update of outgoing edge to peer,\n            # so that channel can be used to receive payments\n            # Note: this is only useful for our unit tests. peers may discard\n            # channel updates if the channel has not been announced\n            self.maybe_send_channel_update(chan)\n\n    def maybe_send_announcement_signatures(self, chan: Channel, is_reply=False):\n        if not chan.is_public() or chan.short_channel_id is None:\n            return\n        if chan.sent_announcement_signatures:\n            return\n        if not is_reply and chan.config[REMOTE].announcement_node_sig:\n            return\n        h = chan.get_channel_announcement_hash()\n        bitcoin_signature = ecc.ECPrivkey(chan.config[LOCAL].multisig_key.privkey).ecdsa_sign(h, sigencode=ecdsa_sig64_from_r_and_s)\n        node_signature = ecc.ECPrivkey(self.privkey).ecdsa_sign(h, sigencode=ecdsa_sig64_from_r_and_s)\n        self.send_message(\n            \"announcement_signatures\",\n            channel_id=chan.channel_id,\n            short_channel_id=chan.short_channel_id,\n            node_signature=node_signature,\n            bitcoin_signature=bitcoin_signature\n        )\n        chan.config[LOCAL].announcement_node_sig = node_signature\n        chan.config[LOCAL].announcement_bitcoin_sig = bitcoin_signature\n        self.lnworker.save_channel(chan)\n        chan.sent_announcement_signatures = True\n\n    def on_update_fail_htlc(self, chan: Channel, payload):\n        htlc_id = payload[\"id\"]\n        reason = payload[\"reason\"]\n        self.logger.info(f\"on_update_fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}\")\n        if not chan.can_update_ctx(proposer=REMOTE):\n            self.logger.warning(\n                f\"on_update_fail_htlc. dropping message. illegal action. \"\n                f\"chan={chan.get_id_for_log()}. {htlc_id=}. {chan.get_state()=!r}. {chan.peer_state=!r}\")\n            return\n        chan.receive_fail_htlc(htlc_id, error_bytes=reason)  # TODO handle exc and maybe fail channel (e.g. bad htlc_id)\n\n    def maybe_send_commitment(self, chan: Channel) -> bool:\n        assert util.get_running_loop() == util.get_asyncio_loop(), f\"this must be run on the asyncio thread!\"\n        if not chan.can_update_ctx(proposer=LOCAL):\n            return False\n        # REMOTE should revoke first before we can sign a new ctx\n        if chan.hm.is_revack_pending(REMOTE):\n            return False\n        # if there are no changes, we will not (and must not) send a new commitment\n        if not chan.has_pending_changes(REMOTE):\n            return False\n        now = time.monotonic()\n        if now - self._last_commitsig_sent_time < self.MIN_TIME_BETWEEN_SENDING_COMMITSIGS:\n            # We recently sent \"commitment_signed\". Delay sending again, to allow batching updates.\n            # No need to set a timer, htlc_switch polling will call us again.\n            return False\n        self._last_commitsig_sent_time = now\n        self.logger.info(f'send_commitment. chan {chan.short_channel_id}. ctn: {chan.get_next_ctn(REMOTE)}.')\n        sig_64, htlc_sigs = chan.sign_next_commitment()\n        self.send_message(\"commitment_signed\", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b\"\".join(htlc_sigs))\n        return True\n\n    def send_htlc(\n        self,\n        *,\n        chan: Channel,\n        payment_hash: bytes,\n        amount_msat: int,\n        cltv_abs: int,\n        onion: OnionPacket,\n        session_key: Optional[bytes] = None,\n    ) -> UpdateAddHtlc:\n        assert chan.can_send_update_add_htlc(), f\"cannot send updates: {chan.short_channel_id}\"\n        htlc = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_abs=cltv_abs, timestamp=int(time.time()))\n        htlc = chan.add_htlc(htlc)\n        if session_key:\n            chan.set_onion_key(htlc.htlc_id, session_key) # should it be the outer onion secret?\n        self.logger.info(f\"starting payment. htlc: {htlc}\")\n        self.send_message(\n            \"update_add_htlc\",\n            channel_id=chan.channel_id,\n            id=htlc.htlc_id,\n            cltv_expiry=htlc.cltv_abs,\n            amount_msat=htlc.amount_msat,\n            payment_hash=htlc.payment_hash,\n            onion_routing_packet=onion.to_bytes())\n        self.maybe_send_commitment(chan)\n        return htlc\n\n    def pay(self, *,\n            route: 'LNPaymentRoute',\n            chan: Channel,\n            amount_msat: int,\n            total_msat: int,\n            payment_hash: bytes,\n            min_final_cltv_delta: int,\n            payment_secret: bytes,\n            trampoline_onion: Optional[OnionPacket] = None,\n        ) -> UpdateAddHtlc:\n\n        assert amount_msat > 0, \"amount_msat is not greater zero\"\n        assert len(route) > 0\n        if not chan.can_send_update_add_htlc():\n            raise PaymentFailure(\"Channel cannot send update_add_htlc\")\n        onion, amount_msat, cltv_abs, session_key = self.lnworker.create_onion_for_route(\n            route=route,\n            amount_msat=amount_msat,\n            total_msat=total_msat,\n            payment_hash=payment_hash,\n            min_final_cltv_delta=min_final_cltv_delta,\n            payment_secret=payment_secret,\n            trampoline_onion=trampoline_onion\n        )\n        htlc = self.send_htlc(\n            chan=chan,\n            payment_hash=payment_hash,\n            amount_msat=amount_msat,\n            cltv_abs=cltv_abs,\n            onion=onion,\n            session_key=session_key,\n        )\n        return htlc\n\n    def send_revoke_and_ack(self, chan: Channel) -> None:\n        if not chan.can_update_ctx(proposer=LOCAL):\n            return\n        self.logger.info(f'send_revoke_and_ack. chan {chan.short_channel_id}. ctn: {chan.get_oldest_unrevoked_ctn(LOCAL)}')\n        rev = chan.revoke_current_commitment()\n        self.lnworker.save_channel(chan)\n        self.send_message(\"revoke_and_ack\",\n            channel_id=chan.channel_id,\n            per_commitment_secret=rev.per_commitment_secret,\n            next_per_commitment_point=rev.next_per_commitment_point)\n        self.maybe_send_commitment(chan)\n\n    def on_commitment_signed(self, chan: Channel, payload) -> None:\n        self.logger.info(f'on_commitment_signed. chan {chan.short_channel_id}. ctn: {chan.get_next_ctn(LOCAL)}.')\n        if not chan.can_update_ctx(proposer=REMOTE):\n            self.logger.warning(\n                f\"on_commitment_signed. dropping message. illegal action. \"\n                f\"chan={chan.get_id_for_log()}. {chan.get_state()=!r}. {chan.peer_state=!r}\")\n            return\n        # make sure there were changes to the ctx, otherwise the remote peer is misbehaving\n        if not chan.has_pending_changes(LOCAL):\n            # TODO if feerate changed A->B->A; so there were updates but the value is identical,\n            #      then it might be legal to send a commitment_signature\n            #      see https://github.com/lightningnetwork/lightning-rfc/pull/618\n            raise RemoteMisbehaving('received commitment_signed without pending changes')\n        # REMOTE should wait until we have revoked\n        if chan.hm.is_revack_pending(LOCAL):\n            raise RemoteMisbehaving('received commitment_signed before we revoked previous ctx')\n        data = payload[\"htlc_signature\"]\n        htlc_sigs = list(chunks(data, 64))\n        chan.receive_new_commitment(payload[\"signature\"], htlc_sigs)\n        self.send_revoke_and_ack(chan)\n        self.received_commitsig_event.set()\n        self.received_commitsig_event.clear()\n\n    def on_update_fulfill_htlc(self, chan: Channel, payload):\n        preimage = payload[\"payment_preimage\"]\n        payment_hash = sha256(preimage)\n        htlc_id = payload[\"id\"]\n        self.logger.info(f\"on_update_fulfill_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}\")\n        if not chan.can_update_ctx(proposer=REMOTE):\n            self.logger.warning(\n                f\"on_update_fulfill_htlc. dropping message. illegal action. \"\n                f\"chan={chan.get_id_for_log()}. {htlc_id=}. {chan.get_state()=!r}. {chan.peer_state=!r}\")\n            return\n        chan.receive_htlc_settle(preimage, htlc_id)  # TODO handle exc and maybe fail channel (e.g. bad htlc_id)\n\n    def on_update_fail_malformed_htlc(self, chan: Channel, payload):\n        htlc_id = payload[\"id\"]\n        failure_code = payload[\"failure_code\"]\n        self.logger.info(f\"on_update_fail_malformed_htlc. chan {chan.get_id_for_log()}. \"\n                         f\"htlc_id {htlc_id}. failure_code={failure_code}\")\n        if not chan.can_update_ctx(proposer=REMOTE):\n            self.logger.warning(\n                f\"on_update_fail_malformed_htlc. dropping message. illegal action. \"\n                f\"chan={chan.get_id_for_log()}. {htlc_id=}. {chan.get_state()=!r}. {chan.peer_state=!r}\")\n            return\n        if failure_code & OnionFailureCodeMetaFlag.BADONION == 0:\n            self.schedule_force_closing(chan.channel_id)\n            raise RemoteMisbehaving(f\"received update_fail_malformed_htlc with unexpected failure code: {failure_code}\")\n        reason = OnionRoutingFailure(code=failure_code, data=payload[\"sha256_of_onion\"])\n        chan.receive_fail_htlc(htlc_id, error_bytes=None, reason=reason)\n\n    def on_update_add_htlc(self, chan: Channel, payload):\n        payment_hash = payload[\"payment_hash\"]\n        htlc_id = payload[\"id\"]\n        cltv_abs = payload[\"cltv_expiry\"]\n        amount_msat_htlc = payload[\"amount_msat\"]\n        onion_packet = payload[\"onion_routing_packet\"]\n        htlc = UpdateAddHtlc(\n            amount_msat=amount_msat_htlc,\n            payment_hash=payment_hash,\n            cltv_abs=cltv_abs,\n            timestamp=int(time.time()),\n            htlc_id=htlc_id)\n        self.logger.info(f\"on_update_add_htlc. chan {chan.short_channel_id}. htlc={str(htlc)}\")\n        if chan.get_state() != ChannelState.OPEN:\n            raise RemoteMisbehaving(f\"received update_add_htlc while chan.get_state() != OPEN. state was {chan.get_state()!r}\")\n        if not chan.can_update_ctx(proposer=REMOTE):\n            self.logger.warning(\n                f\"on_update_add_htlc. dropping message. illegal action. \"\n                f\"chan={chan.get_id_for_log()}. {htlc_id=}. {chan.get_state()=!r}. {chan.peer_state=!r}\")\n            return\n        if cltv_abs > bitcoin.NLOCKTIME_BLOCKHEIGHT_MAX:\n            self.schedule_force_closing(chan.channel_id)\n            raise RemoteMisbehaving(f\"received update_add_htlc with {cltv_abs=} > BLOCKHEIGHT_MAX\")\n        # add htlc\n        chan.receive_htlc(htlc, onion_packet)\n        util.trigger_callback('htlc_added', chan, htlc, RECEIVED)\n\n    @staticmethod\n    def _check_accepted_final_htlc(\n            *, chan: Channel,\n            htlc: UpdateAddHtlc,\n            processed_onion: ProcessedOnionPacket,\n            is_trampoline_onion: bool = False,\n            log_fail_reason: Callable[[str], None],\n    ) -> tuple[bytes, int, int, OnionRoutingFailure]:\n        \"\"\"\n        Perform checks that are invariant (results do not depend on height, network conditions, etc.)\n        for htlcs of which we are the receiver (forwarding htlcs will have their checks in maybe_forward_htlc).\n        May raise OnionRoutingFailure\n        \"\"\"\n        assert processed_onion.are_we_final, processed_onion\n        if (amt_to_forward := processed_onion.amt_to_forward) is None:\n            log_fail_reason(f\"'amt_to_forward' missing from onion\")\n            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\\x00\\x00\\x00')\n        if (cltv_abs_from_onion := processed_onion.outgoing_cltv_value) is None:\n            log_fail_reason(f\"'outgoing_cltv_value' missing from onion\")\n            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\\x00\\x00\\x00')\n        if cltv_abs_from_onion > htlc.cltv_abs:\n            log_fail_reason(f\"cltv_abs_from_onion != htlc.cltv_abs\")\n            raise OnionRoutingFailure(\n                code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,\n                data=htlc.cltv_abs.to_bytes(4, byteorder=\"big\"))\n\n        exc_incorrect_or_unknown_pd = OnionRoutingFailure(\n            code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,\n            data=amt_to_forward.to_bytes(8, byteorder=\"big\")) # height will be added later\n        if (total_msat := processed_onion.total_msat) is None:\n            log_fail_reason(f\"'total_msat' missing from onion\")\n            raise exc_incorrect_or_unknown_pd\n\n        if chan.jit_opening_fee:\n            channel_opening_fee = chan.jit_opening_fee\n            total_msat -= channel_opening_fee\n            amt_to_forward -= channel_opening_fee\n        else:\n            channel_opening_fee = 0\n\n        if not is_trampoline_onion:\n            # for inner trampoline onions amt_to_forward can be larger than the htlc amount\n            if amt_to_forward > htlc.amount_msat:\n                log_fail_reason(f\"{amt_to_forward=} > {htlc.amount_msat=}\")\n                raise OnionRoutingFailure(\n                    code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,\n                    data=htlc.amount_msat.to_bytes(8, byteorder=\"big\"))\n\n        if (payment_secret_from_onion := processed_onion.payment_secret) is None:\n            log_fail_reason(f\"'payment_secret' missing from onion\")\n            raise exc_incorrect_or_unknown_pd\n\n        return payment_secret_from_onion, total_msat, channel_opening_fee, exc_incorrect_or_unknown_pd\n\n    def _check_unfulfilled_htlc(\n        self, *,\n        chan: Channel,\n        htlc: UpdateAddHtlc,\n        processed_onion: ProcessedOnionPacket,\n        outer_onion_payment_secret: bytes = None,  # used to group trampoline htlcs for forwarding\n    ) -> str:\n        \"\"\"\n        Does additional checks on the incoming htlc and return the payment key if the tests pass,\n        otherwise raises OnionRoutingError which will get the htlc failed.\n        \"\"\"\n        _log_fail_reason = self._log_htlc_fail_reason_cb(chan.channel_id, htlc, processed_onion.hop_data.payload)\n\n        # Check that our blockchain tip is sufficiently recent so that we have an approx idea of the height.\n        # We should not release the preimage for an HTLC that its sender could already time out as\n        # then they might try to force-close and it becomes a race.\n        chain = self.network.blockchain()\n        local_height = chain.height()\n        blocks_to_expiry = max(htlc.cltv_abs - local_height, 0)\n        if chain.is_tip_stale():\n            _log_fail_reason(f\"our chain tip is stale: {local_height=}\")\n            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')\n\n        payment_hash = htlc.payment_hash\n        if not processed_onion.are_we_final:\n            if outer_onion_payment_secret:\n                # this is a trampoline forwarding htlc, multiple incoming trampoline htlcs can be collected\n                payment_key = (payment_hash + outer_onion_payment_secret).hex()\n                return payment_key\n            # this is a regular htlc to forward, it will get its own set of size 1 keyed by htlc_key\n            # Additional checks required only for forwarding nodes will be done in maybe_forward_htlc().\n            payment_key = serialize_htlc_key(chan.get_scid_or_local_alias(), htlc.htlc_id)\n            return payment_key\n\n        # parse parameters and perform checks that are invariant\n        payment_secret_from_onion, total_msat, channel_opening_fee, exc_incorrect_or_unknown_pd = (\n            self._check_accepted_final_htlc(\n                chan=chan,\n                htlc=htlc,\n                processed_onion=processed_onion,\n                is_trampoline_onion=bool(outer_onion_payment_secret),\n                log_fail_reason=_log_fail_reason,\n            ))\n        # trampoline htlcs of which we are the final receiver will first get grouped by the outer\n        # onions secret to allow grouping a multi-trampoline mpp in different sets. Once a trampoline\n        # payment part is completed (sum(htlcs) >= (trampoline-)amt_to_forward), its htlcs get moved into\n        # the htlc set representing the whole payment (payment key derived from trampoline/invoice secret).\n        payment_key = (payment_hash + (outer_onion_payment_secret or payment_secret_from_onion)).hex()\n\n        # for safety, still enforce MIN_FINAL_CLTV_DELTA here even if payment_hash is in dont_expire_htlcs\n        if blocks_to_expiry < MIN_FINAL_CLTV_DELTA_ACCEPTED:\n            # this check should be done here for new htlcs and ongoing on pending sets.\n            # Here it is done so that invalid received htlcs will never get added to a set,\n            # so the set still has a chance to succeed until mpp timeout.\n            _log_fail_reason(f\"htlc.cltv_abs is unreasonably close: {htlc.cltv_abs=}, {local_height=}\")\n            raise exc_incorrect_or_unknown_pd\n\n        # extract trampoline\n        if processed_onion.trampoline_onion_packet:\n            trampoline_onion = self._process_incoming_onion_packet(\n                processed_onion.trampoline_onion_packet,\n                payment_hash=payment_hash,\n                is_trampoline=True)\n\n            # compare trampoline onion against outer onion according to:\n            # https://github.com/lightning/bolts/blob/9938ab3d6160a3ba91f3b0e132858ab14bfe4f81/04-onion-routing.md?plain=1#L547-L553\n            if trampoline_onion.are_we_final:\n                try:\n                    assert not processed_onion.outgoing_cltv_value < trampoline_onion.outgoing_cltv_value\n                    is_mpp = processed_onion.total_msat > processed_onion.amt_to_forward\n                    if is_mpp:\n                        assert not processed_onion.total_msat < trampoline_onion.amt_to_forward\n                    else:\n                        assert not processed_onion.amt_to_forward < trampoline_onion.amt_to_forward\n                except AssertionError:\n                    _log_fail_reason(f'incorrect trampoline onion {processed_onion=}\\n{trampoline_onion=}')\n                    raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\\x00\\x00\\x00')\n\n            return self._check_unfulfilled_htlc(\n                chan=chan,\n                htlc=htlc,\n                processed_onion=trampoline_onion,\n                outer_onion_payment_secret=payment_secret_from_onion,\n            )\n\n        info = self.lnworker.get_payment_info(payment_hash, direction=RECEIVED)\n        if info is None:\n            _log_fail_reason(f\"no payment_info found for RHASH {payment_hash.hex()}\")\n            raise exc_incorrect_or_unknown_pd\n        elif info.status == PR_PAID:\n            _log_fail_reason(f\"invoice already paid: {payment_hash.hex()=}\")\n            raise exc_incorrect_or_unknown_pd\n        elif blocks_to_expiry < info.min_final_cltv_delta:\n            _log_fail_reason(\n                f\"min final cltv delta lower than requested: \"\n                f\"{payment_hash.hex()=} {htlc.cltv_abs=} {blocks_to_expiry=}\"\n            )\n            raise exc_incorrect_or_unknown_pd\n        elif htlc.timestamp > info.expiration_ts:  # the set will get failed too if now > exp_ts\n            _log_fail_reason(f\"not accepting htlc for expired invoice\")\n            raise exc_incorrect_or_unknown_pd\n        elif not info.invoice_features.supports(LnFeatures.BASIC_MPP_OPT) and total_msat > htlc.amount_msat:\n            # in _check_unfulfilled_htlc_set we check the count to prevent mpp through overpayment\n            _log_fail_reason(f\"got mpp but we requested no mpp in the invoice: {total_msat=} > {htlc.amount_msat=}\")\n            raise exc_incorrect_or_unknown_pd\n\n        expected_payment_secret = self.lnworker.get_payment_secret(payment_hash)\n        if not util.constant_time_compare(payment_secret_from_onion, expected_payment_secret):\n            _log_fail_reason(f'incorrect payment secret: {payment_secret_from_onion.hex()=}')\n            raise exc_incorrect_or_unknown_pd\n\n        invoice_msat = info.amount_msat\n        if channel_opening_fee:\n            # deduct just-in-time channel fees from invoice amount\n            invoice_msat -= channel_opening_fee\n\n        if not (invoice_msat is None or invoice_msat <= total_msat <= 2 * invoice_msat):\n            _log_fail_reason(f\"{total_msat=} too different from {invoice_msat=}\")\n            raise exc_incorrect_or_unknown_pd\n\n        return payment_key\n\n    def _fulfill_htlc_set(self, payment_key: str, preimage: bytes):\n        htlc_set = self.lnworker.received_mpp_htlcs[payment_key]\n        assert len(htlc_set.htlcs) > 0, f\"{htlc_set=}\"\n        assert htlc_set.resolution == RecvMPPResolution.SETTLING\n        assert htlc_set.parent_set_key is None, f\"Must not settle child {htlc_set=}\"\n        # get payment hash of any htlc in the set (they are all the same)\n        payment_hash = htlc_set.get_payment_hash()\n        assert payment_hash is not None, htlc_set\n        assert payment_hash.hex() not in self.lnworker.dont_settle_htlcs\n        self.lnworker.dont_expire_htlcs.pop(payment_hash.hex(), None)  # htlcs wont get expired anymore\n        for mpp_htlc in list(htlc_set.htlcs):\n            htlc_id = mpp_htlc.htlc.htlc_id\n            chan = self.get_channel_by_id(mpp_htlc.channel_id)\n            if chan is None:\n                # this htlc belongs to another peer and has to be settled in their htlc_switch\n                continue\n            if not chan.can_update_ctx(proposer=LOCAL):\n                continue\n            self.logger.info(f\"fulfill htlc: {chan.short_channel_id}. {htlc_id=}. {payment_hash.hex()=}\")\n            if chan.hm.was_htlc_preimage_released(htlc_id=htlc_id, htlc_proposer=REMOTE):\n                # this check is intended to gracefully handle stale htlcs in the set, e.g. after a crash\n                self.logger.debug(f\"{mpp_htlc=} was already settled before, dropping it.\")\n                htlc_set = htlc_set._replace(htlcs=htlc_set.htlcs - {mpp_htlc})\n                continue\n            self._fulfill_htlc(chan, htlc_id, preimage)\n            htlc_set = htlc_set._replace(htlcs=htlc_set.htlcs - {mpp_htlc})\n            # reset just-in-time opening fee of channel\n            chan.jit_opening_fee = None\n\n        self.lnworker.received_mpp_htlcs[payment_key] = htlc_set  # save updated set\n\n    def _fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes):\n        assert chan.hm.is_htlc_irrevocably_added_yet(htlc_proposer=REMOTE, htlc_id=htlc_id)\n        self.received_htlcs_pending_removal.add((chan, htlc_id))\n        chan.settle_htlc(preimage, htlc_id)\n        self.send_message(\n            \"update_fulfill_htlc\",\n            channel_id=chan.channel_id,\n            id=htlc_id,\n            payment_preimage=preimage)\n\n    def _fail_htlc_set(\n        self,\n        payment_key: str,\n        error_tuple: Tuple[Optional[bytes], Optional[OnionFailureCode | int], Optional[bytes]],\n    ):\n        htlc_set = self.lnworker.received_mpp_htlcs[payment_key]\n        assert htlc_set.resolution in (RecvMPPResolution.FAILED, RecvMPPResolution.EXPIRED)\n\n        raw_error, error_code, error_data = error_tuple\n        local_height = self.network.blockchain().height()\n        payment_hash = htlc_set.get_payment_hash()\n        assert payment_hash is not None, \"Empty htlc set?\"\n        for mpp_htlc in list(htlc_set.htlcs):\n            chan = self.get_channel_by_id(mpp_htlc.channel_id)\n            htlc_id = mpp_htlc.htlc.htlc_id\n            if chan is None:\n                # this htlc belongs to another peer and has to be settled in their htlc_switch\n                continue\n            if not chan.can_update_ctx(proposer=LOCAL):\n                continue\n            assert chan.hm.is_htlc_irrevocably_added_yet(htlc_proposer=REMOTE, htlc_id=htlc_id)\n            if chan.hm.was_htlc_failed(htlc_id=htlc_id, htlc_proposer=REMOTE):\n                # this check is intended to gracefully handle stale htlcs in the set, e.g. after a crash\n                self.logger.debug(f\"{mpp_htlc=} was already failed before, dropping it.\")\n                htlc_set = htlc_set._replace(htlcs=htlc_set.htlcs - {mpp_htlc})\n                continue\n            onion_packet = self._parse_onion_packet(mpp_htlc.unprocessed_onion)\n            processed_onion_packet = self._process_incoming_onion_packet(\n                onion_packet,\n                payment_hash=payment_hash,\n                is_trampoline=False,\n            )\n            if raw_error:\n                error_bytes = obfuscate_onion_error(raw_error, onion_packet.public_key, self.privkey)\n            else:\n                assert isinstance(error_code, (OnionFailureCode, int))\n                if error_code == OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS:\n                    amount_to_forward = processed_onion_packet.amt_to_forward\n                    # if this was a trampoline htlc we use the inner amount_to_forward as this is\n                    # the value known by the sender\n                    if processed_onion_packet.trampoline_onion_packet:\n                        processed_trampoline_onion_packet = self._process_incoming_onion_packet(\n                            processed_onion_packet.trampoline_onion_packet,\n                            payment_hash=payment_hash,\n                            is_trampoline=True,\n                        )\n                        amount_to_forward = processed_trampoline_onion_packet.amt_to_forward\n                    error_data = amount_to_forward.to_bytes(8, byteorder=\"big\")\n                e = OnionRoutingFailure(code=error_code, data=error_data or b'')\n                error_bytes = e.to_wire_msg(onion_packet, self.privkey, local_height)\n            self.fail_htlc(\n                chan=chan,\n                htlc_id=htlc_id,\n                error_bytes=error_bytes,\n            )\n            htlc_set = htlc_set._replace(htlcs=htlc_set.htlcs - {mpp_htlc})\n\n        self.lnworker.received_mpp_htlcs[payment_key] = htlc_set  # save updated set\n\n    def fail_htlc(self, *, chan: Channel, htlc_id: int, error_bytes: bytes):\n        self.logger.info(f\"fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}.\")\n        assert chan.can_update_ctx(proposer=LOCAL), f\"cannot send updates: {chan.short_channel_id}\"\n        self.received_htlcs_pending_removal.add((chan, htlc_id))\n        chan.fail_htlc(htlc_id)\n        self.send_message(\n            \"update_fail_htlc\",\n            channel_id=chan.channel_id,\n            id=htlc_id,\n            len=len(error_bytes),\n            reason=error_bytes)\n        self.maybe_send_commitment(chan)\n\n    def fail_malformed_htlc(self, *, chan: Channel, htlc_id: int, reason: OnionParsingError):\n        self.logger.info(f\"fail_malformed_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}.\")\n        assert chan.can_update_ctx(proposer=LOCAL), f\"cannot send updates: {chan.short_channel_id}\"\n        if not (reason.code & OnionFailureCodeMetaFlag.BADONION and len(reason.data) == 32):\n            raise Exception(f\"unexpected reason when sending 'update_fail_malformed_htlc': {reason!r}\")\n        self.received_htlcs_pending_removal.add((chan, htlc_id))\n        chan.fail_htlc(htlc_id)\n        self.send_message(\n            \"update_fail_malformed_htlc\",\n            channel_id=chan.channel_id,\n            id=htlc_id,\n            sha256_of_onion=reason.data,\n            failure_code=reason.code)\n        self.maybe_send_commitment(chan)\n\n    def on_revoke_and_ack(self, chan: Channel, payload) -> None:\n        self.logger.info(f'on_revoke_and_ack. chan {chan.short_channel_id}. ctn: {chan.get_oldest_unrevoked_ctn(REMOTE)}')\n        if not chan.can_update_ctx(proposer=REMOTE):\n            self.logger.warning(\n                f\"on_revoke_and_ack. dropping message. illegal action. \"\n                f\"chan={chan.get_id_for_log()}. {chan.get_state()=!r}. {chan.peer_state=!r}\")\n            return\n        rev = RevokeAndAck(payload[\"per_commitment_secret\"], payload[\"next_per_commitment_point\"])\n        chan.receive_revocation(rev)\n        self.lnworker.save_channel(chan)\n        self._received_revack_event.set()\n        self._received_revack_event.clear()\n\n    @event_listener\n    async def on_event_fee(self, *args):\n        async def async_wrapper():\n            for chan in self.channels.values():\n                self.maybe_update_fee(chan)\n        await self.taskgroup.spawn(async_wrapper)\n\n    def on_update_fee(self, chan: Channel, payload):\n        if not chan.can_update_ctx(proposer=REMOTE):\n            self.logger.warning(\n                f\"on_update_fee. dropping message. illegal action. \"\n                f\"chan={chan.get_id_for_log()}. {chan.get_state()=!r}. {chan.peer_state=!r}\")\n            return\n        feerate = payload[\"feerate_per_kw\"]\n        chan.update_fee(feerate, False)\n\n    def maybe_update_fee(self, chan: Channel):\n        \"\"\"\n        called when our fee estimates change\n        \"\"\"\n        if not chan.can_update_ctx(proposer=LOCAL):\n            return\n        if chan.get_state() != ChannelState.OPEN:\n            return\n        current_feerate_per_kw: Optional[int] = self.lnworker.current_target_feerate_per_kw(\n            has_anchors=chan.has_anchors()\n        )\n        if current_feerate_per_kw is None:\n            return\n        # add some buffer to anchor chan fees as we always act at the lower end and don't\n        # want to get kicked out of the mempool immediately if it grows\n        fee_buffer = current_feerate_per_kw * 0.5 if chan.has_anchors() else 0\n        update_feerate_per_kw = int(current_feerate_per_kw + fee_buffer)\n        def does_chan_fee_need_update(chan_feerate: Union[float, int]) -> Optional[bool]:\n            if chan.has_anchors():\n                # TODO: once package relay and electrum servers with submitpackage are more common,\n                # TODO: we should reconsider this logic and move towards 0 fee ctx\n                # update if we used up half of the buffer or the fee decreased a lot again\n                fee_increased = current_feerate_per_kw + (fee_buffer / 2) > chan_feerate\n                changed_significantly = abs((chan_feerate - update_feerate_per_kw) / chan_feerate) > 0.2\n                return fee_increased or changed_significantly\n            else:\n                # We raise fees more aggressively than we lower them. Overpaying is not too bad,\n                # but lowballing can be fatal if we can't even get into the mempool...\n                high_fee = 2 * current_feerate_per_kw  # type: # Union[float, int]\n                low_fee = self.lnworker.current_low_feerate_per_kw_srk_channel()  # type: Optional[Union[float, int]]\n                if low_fee is None:\n                    return None\n                low_fee = max(low_fee, 0.75 * current_feerate_per_kw)\n                # make sure low_feerate and target_feerate are not too close to each other:\n                low_fee = min(low_fee, current_feerate_per_kw - FEERATE_PER_KW_MIN_RELAY_LIGHTNING)\n                assert low_fee < high_fee, (low_fee, high_fee)\n                return not (low_fee < chan_feerate < high_fee)\n        if not chan.constraints.is_initiator:\n            if constants.net is not constants.BitcoinRegtest:\n                chan_feerate = chan.get_latest_feerate(LOCAL)\n                ratio = chan_feerate / update_feerate_per_kw\n                if ratio < 0.5:\n                    # Note that we trust the Electrum server about fee rates\n                    # Thus, automated force-closing might not be a good idea\n                    # Maybe we should display something in the GUI instead\n                    self.logger.warning(\n                        f\"({chan.get_id_for_log()}) feerate is {chan_feerate} sat/kw, \"\n                        f\"current recommended feerate is {update_feerate_per_kw} sat/kw, consider force closing!\")\n            return\n        # it is our responsibility to update the fee\n        chan_fee = chan.get_next_feerate(REMOTE)\n        if does_chan_fee_need_update(chan_fee):\n            self.logger.info(f\"({chan.get_id_for_log()}) onchain fees have changed considerably. updating fee.\")\n        elif chan.get_latest_ctn(REMOTE) == 0:\n            # workaround eclair issue https://github.com/ACINQ/eclair/issues/1730 (fixed in 2022)\n            self.logger.info(f\"({chan.get_id_for_log()}) updating fee to bump remote ctn\")\n            if current_feerate_per_kw == chan_fee:\n                update_feerate_per_kw += 1\n        else:\n            return\n        self.logger.info(f\"({chan.get_id_for_log()}) current pending feerate {chan_fee}. \"\n                         f\"new feerate {update_feerate_per_kw}\")\n        assert update_feerate_per_kw >= FEERATE_PER_KW_MIN_RELAY_LIGHTNING, f\"fee below minimum: {update_feerate_per_kw}\"\n        chan.update_fee(update_feerate_per_kw, True)\n        self.send_message(\n            \"update_fee\",\n            channel_id=chan.channel_id,\n            feerate_per_kw=update_feerate_per_kw)\n        self.maybe_send_commitment(chan)\n\n    @log_exceptions\n    async def close_channel(self, chan_id: bytes):\n        chan = self.get_channel_by_id(chan_id)\n        assert chan\n        self.shutdown_received[chan_id] = self.asyncio_loop.create_future()\n        await self.send_shutdown(chan)\n        payload = await self.shutdown_received[chan_id]\n        try:\n            txid = await self._shutdown(chan, payload, is_local=True)\n            self.logger.info(f'({chan.get_id_for_log()}) Channel closed {txid}')\n        except asyncio.TimeoutError:\n            txid = chan.unconfirmed_closing_txid\n            self.logger.warning(f'({chan.get_id_for_log()}) did not send closing_signed, {txid}')\n            if txid is None:\n                raise Exception('The remote peer did not send their final signature. The channel may not have been be closed')\n        return txid\n\n    @non_blocking_msg_handler\n    async def on_shutdown(self, chan: Channel, payload):\n        if chan.peer_state != PeerState.GOOD:  # should never happen\n            raise Exception(f\"received shutdown in unexpected {chan.peer_state=!r}\")\n        if not self.can_send_shutdown(chan, proposer=REMOTE):\n            self.logger.warning(\n                f\"on_shutdown. illegal action. \"\n                f\"chan={chan.get_id_for_log()}. {chan.get_state()=!r}. {chan.peer_state=!r}\")\n            self.send_error(chan.channel_id, message=\"cannot process 'shutdown' in current channel state.\")\n        their_scriptpubkey = payload['scriptpubkey']\n        their_upfront_scriptpubkey = chan.config[REMOTE].upfront_shutdown_script\n        # BOLT-02 check if they use the upfront shutdown script they advertised\n        if self.is_upfront_shutdown_script() and their_upfront_scriptpubkey:\n            if not (their_scriptpubkey == their_upfront_scriptpubkey):\n                self.send_warning(\n                    chan.channel_id,\n                    \"remote didn't use upfront shutdown script it committed to in channel opening\",\n                    close_connection=True)\n        else:\n            # BOLT-02 restrict the scriptpubkey to some templates:\n            if self.is_shutdown_anysegwit() and match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT):\n                pass\n            elif match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_WITNESS_V0):\n                pass\n            else:\n                self.send_warning(\n                    chan.channel_id,\n                    f'scriptpubkey in received shutdown message does not conform to any template: {their_scriptpubkey.hex()}',\n                    close_connection=True)\n\n        chan_id = chan.channel_id\n        if chan_id in self.shutdown_received:\n            self.shutdown_received[chan_id].set_result(payload)\n        else:\n            await self.send_shutdown(chan)\n            txid = await self._shutdown(chan, payload, is_local=False)\n            self.logger.info(f'({chan.get_id_for_log()}) Channel closed by remote peer {txid}')\n\n    def can_send_shutdown(self, chan: Channel, *, proposer: HTLCOwner) -> bool:\n        if chan.get_state() >= ChannelState.CLOSED:\n            return False\n        if chan.get_state() >= ChannelState.OPENING:\n            return True\n        if proposer == LOCAL:\n            if chan.constraints.is_initiator and chan.channel_id in self.funding_created_sent:\n                return True\n            if not chan.constraints.is_initiator and chan.channel_id in self.funding_signed_sent:\n                return True\n        else:  # proposer == REMOTE\n            # (from BOLT-02)\n            #   A receiving node:\n            #       - if it hasn't received a funding_signed (if it is a funder) or a funding_created (if it is a fundee):\n            #           - SHOULD send an error and fail the channel.\n            # ^ that check is equivalent to `chan.get_state() < ChannelState.OPENING`, which is already checked.\n            pass\n        return False\n\n    async def send_shutdown(self, chan: Channel):\n        if not self.can_send_shutdown(chan, proposer=LOCAL):\n            raise Exception(f\"cannot send shutdown. chan={chan.get_id_for_log()}. {chan.get_state()=!r}\")\n        if chan.config[LOCAL].upfront_shutdown_script:\n            scriptpubkey = chan.config[LOCAL].upfront_shutdown_script\n        else:\n            scriptpubkey = bitcoin.address_to_script(chan.get_sweep_address())\n        assert scriptpubkey\n        # wait until no more pending updates (bolt2)\n        chan.set_can_send_ctx_updates(False)\n        while chan.has_pending_changes(REMOTE):\n            await asyncio.sleep(0.1)\n        self.send_message('shutdown', channel_id=chan.channel_id, len=len(scriptpubkey), scriptpubkey=scriptpubkey)\n        chan.set_state(ChannelState.SHUTDOWN)\n        # can fulfill or fail htlcs. cannot add htlcs, because state != OPEN\n        chan.set_can_send_ctx_updates(True)\n\n    def get_shutdown_fee_range(self, chan, closing_tx, is_local):\n        \"\"\" return the closing fee and fee range we initially try to enforce \"\"\"\n        config = self.network.config\n        our_fee = None\n        if config.TEST_SHUTDOWN_FEE:\n            our_fee = config.TEST_SHUTDOWN_FEE\n        else:\n            fee_rate_per_kb = self.network.fee_estimates.eta_target_to_fee(FEE_LN_ETA_TARGET)\n            if fee_rate_per_kb is None:  # fallback\n                from .fee_policy import FeePolicy\n                fee_rate_per_kb = FeePolicy(config.FEE_POLICY).fee_per_kb(self.network)\n            if fee_rate_per_kb is not None:\n                our_fee = fee_rate_per_kb * closing_tx.estimated_size() // 1000\n            # TODO: anchors: remove this, as commitment fee rate can be below chain head fee rate?\n            # BOLT2: The sending node MUST set fee less than or equal to the base fee of the final ctx\n            max_fee = chan.get_latest_fee(LOCAL if is_local else REMOTE)\n            if our_fee is None:  # fallback\n                self.logger.warning(f\"got no fee estimates for co-op close! falling back to chan.get_latest_fee\")\n                our_fee = max_fee\n            our_fee = min(our_fee, max_fee)\n        # config modern_fee_negotiation can be set in tests\n        if config.TEST_SHUTDOWN_LEGACY:\n            our_fee_range = None\n        elif config.TEST_SHUTDOWN_FEE_RANGE:\n            our_fee_range = config.TEST_SHUTDOWN_FEE_RANGE\n        else:\n            # we aim at a fee between next block inclusion and some lower value\n            our_fee_range = {'min_fee_satoshis': our_fee // 2, 'max_fee_satoshis': our_fee * 2}\n        self.logger.info(f\"Our fee range: {our_fee_range} and fee: {our_fee}\")\n        return our_fee, our_fee_range\n\n    @log_exceptions\n    async def _shutdown(self, chan: Channel, payload, *, is_local: bool):\n        # wait until no HTLCs remain in either commitment transaction\n        while chan.has_unsettled_htlcs():\n            self.logger.info(f'(chan: {chan.short_channel_id}) waiting for htlcs to settle...')\n            await asyncio.sleep(1)\n        # if no HTLCs remain, we must not send updates\n        chan.set_can_send_ctx_updates(False)\n        their_scriptpubkey = payload['scriptpubkey']\n        if chan.config[LOCAL].upfront_shutdown_script:\n            our_scriptpubkey = chan.config[LOCAL].upfront_shutdown_script\n        else:\n            our_scriptpubkey = bitcoin.address_to_script(chan.get_sweep_address())\n        assert our_scriptpubkey\n        # estimate fee of closing tx\n        dummy_sig, dummy_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=0)\n        our_sig = None  # type: Optional[bytes]\n        closing_tx = None  # type: Optional[PartialTransaction]\n        is_initiator = chan.constraints.is_initiator\n        our_fee, our_fee_range = self.get_shutdown_fee_range(chan, dummy_tx, is_local)\n\n        def send_closing_signed(our_fee, our_fee_range, drop_remote):\n            nonlocal our_sig, closing_tx\n            if our_fee_range:\n                closing_signed_tlvs = {'fee_range': our_fee_range}\n            else:\n                closing_signed_tlvs = {}\n            our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=our_fee, drop_remote=drop_remote)\n            self.logger.info(f\"Sending fee range: {closing_signed_tlvs} and fee: {our_fee}\")\n            self.send_message(\n                'closing_signed',\n                channel_id=chan.channel_id,\n                fee_satoshis=our_fee,\n                signature=our_sig,\n                closing_signed_tlvs=closing_signed_tlvs,\n            )\n\n        def verify_signature(tx: 'PartialTransaction', sig) -> bool:\n            their_pubkey = chan.config[REMOTE].multisig_key.pubkey\n            pre_hash = tx.serialize_preimage(0)\n            msg_hash = sha256d(pre_hash)\n            return ECPubkey(their_pubkey).ecdsa_verify(sig, msg_hash)\n\n        async def receive_closing_signed():\n            nonlocal our_sig, closing_tx\n            try:\n                cs_payload = await self.wait_for_message('closing_signed', chan.channel_id)\n            except asyncio.exceptions.TimeoutError:\n                self.schedule_force_closing(chan.channel_id)\n                raise Exception(\"closing_signed not received, force closing.\")\n            their_fee = cs_payload['fee_satoshis']\n            their_fee_range = cs_payload['closing_signed_tlvs'].get('fee_range')\n            their_sig = cs_payload['signature']\n            # perform checks\n            our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=their_fee, drop_remote=False)\n            if verify_signature(closing_tx, their_sig):\n                drop_remote = False\n            else:\n                our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=their_fee, drop_remote=True)\n                if verify_signature(closing_tx, their_sig):\n                    drop_remote = True\n                else:\n                    # this can happen if we consider our output too valuable to drop,\n                    # but the remote drops it because it violates their dust limit\n                    raise Exception('failed to verify their signature')\n            # at this point we know how the closing tx looks like\n            # check that their output is above their scriptpubkey's network dust limit\n            to_remote_set = closing_tx.get_output_idxs_from_scriptpubkey(their_scriptpubkey)\n            if not drop_remote and to_remote_set:\n                to_remote_idx = to_remote_set.pop()\n                to_remote_amount = closing_tx.outputs()[to_remote_idx].value\n                transaction.check_scriptpubkey_template_and_dust(their_scriptpubkey, to_remote_amount)\n            return their_fee, their_fee_range, their_sig, drop_remote\n\n        def choose_new_fee(our_fee, our_fee_range, their_fee, their_fee_range, their_previous_fee):\n            assert our_fee != their_fee\n            fee_range_sent = our_fee_range and (is_initiator or (their_previous_fee is not None))\n\n            # The sending node, if it is not the funder:\n            if our_fee_range and their_fee_range and not is_initiator and not self.network.config.TEST_SHUTDOWN_FEE_RANGE:\n                # SHOULD set max_fee_satoshis to at least the max_fee_satoshis received\n                our_fee_range['max_fee_satoshis'] = max(their_fee_range['max_fee_satoshis'], our_fee_range['max_fee_satoshis'])\n                # SHOULD set min_fee_satoshis to a fairly low value\n                our_fee_range['min_fee_satoshis'] = min(their_fee_range['min_fee_satoshis'], our_fee_range['min_fee_satoshis'])\n                # Note: the BOLT describes what the sending node SHOULD do.\n                # However, this assumes that we have decided to send 'funding_signed' in response to their fee_range.\n                # In practice, we might prefer to fail the channel in some cases (TODO)\n\n            # the receiving node, if fee_satoshis matches its previously sent fee_range,\n            if fee_range_sent and (our_fee_range['min_fee_satoshis'] <= their_fee <= our_fee_range['max_fee_satoshis']):\n                # SHOULD reply with a closing_signed with the same fee_satoshis value if it is different from its previously sent fee_satoshis\n                our_fee = their_fee\n\n            # the receiving node, if the message contains a fee_range\n            elif our_fee_range and their_fee_range:\n                overlap_min = max(our_fee_range['min_fee_satoshis'], their_fee_range['min_fee_satoshis'])\n                overlap_max = min(our_fee_range['max_fee_satoshis'], their_fee_range['max_fee_satoshis'])\n                # if there is no overlap between that and its own fee_range\n                if overlap_min > overlap_max:\n                    # TODO: the receiving node should first send a warning, and fail the channel\n                    # only if it doesn't receive a satisfying fee_range after a reasonable amount of time\n                    self.schedule_force_closing(chan.channel_id)\n                    raise Exception(\"There is no overlap between between their and our fee range.\")\n                # otherwise, if it is the funder\n                if is_initiator:\n                    # if fee_satoshis is not in the overlap between the sent and received fee_range:\n                    if not (overlap_min <= their_fee <= overlap_max):\n                        # MUST fail the channel\n                        self.schedule_force_closing(chan.channel_id)\n                        raise Exception(\"Their fee is not in the overlap region, we force closed.\")\n                    # otherwise, MUST reply with the same fee_satoshis.\n                    our_fee = their_fee\n                # otherwise (it is not the funder):\n                else:\n                    # if it has already sent a closing_signed:\n                    if fee_range_sent:\n                        # fee_satoshis is not the same as the value we sent, we MUST fail the channel\n                        self.schedule_force_closing(chan.channel_id)\n                        raise Exception(\"Expected the same fee as ours, we force closed.\")\n                    # otherwise:\n                    # MUST propose a fee_satoshis in the overlap between received and (about-to-be) sent fee_range.\n                    our_fee = (overlap_min + overlap_max) // 2\n            else:\n                # otherwise, if fee_satoshis is not strictly between its last-sent fee_satoshis\n                # and its previously-received fee_satoshis, UNLESS it has since reconnected:\n                if their_previous_fee and not (min(our_fee, their_previous_fee) < their_fee < max(our_fee, their_previous_fee)):\n                    # SHOULD fail the connection.\n                    raise Exception('Their fee is not between our last sent and their last sent fee.')\n                # accept their fee if they are very close\n                if abs(their_fee - our_fee) < 2:\n                    our_fee = their_fee\n                else:\n                    # this will be \"strictly between\" (as in BOLT2) previous values because of the above\n                    our_fee = (our_fee + their_fee) // 2\n\n            return our_fee, our_fee_range\n\n        # Fee negotiation: both parties exchange 'funding_signed' messages.\n        # The funder sends the first message, the non-funder sends the last message.\n        # In the 'modern' case, at most 3 messages are exchanged, because choose_new_fee of the funder either returns their_fee or fails\n        their_fee = None\n        drop_remote = False  # does the peer drop its to_local output or not?\n        if is_initiator:\n            send_closing_signed(our_fee, our_fee_range, drop_remote)\n        while True:\n            their_previous_fee = their_fee\n            their_fee, their_fee_range, their_sig, drop_remote = await receive_closing_signed()\n            if our_fee == their_fee:\n                break\n            our_fee, our_fee_range = choose_new_fee(our_fee, our_fee_range, their_fee, their_fee_range, their_previous_fee)\n            if not is_initiator and our_fee == their_fee:\n                break\n            send_closing_signed(our_fee, our_fee_range, drop_remote)\n            if is_initiator and our_fee == their_fee:\n                break\n        if not is_initiator:\n            send_closing_signed(our_fee, our_fee_range, drop_remote)\n\n        # add signatures\n        closing_tx.add_signature_to_txin(\n            txin_idx=0,\n            signing_pubkey=chan.config[LOCAL].multisig_key.pubkey,\n            sig=ecdsa_der_sig_from_ecdsa_sig64(our_sig) + Sighash.to_sigbytes(Sighash.ALL))\n        closing_tx.add_signature_to_txin(\n            txin_idx=0,\n            signing_pubkey=chan.config[REMOTE].multisig_key.pubkey,\n            sig=ecdsa_der_sig_from_ecdsa_sig64(their_sig) + Sighash.to_sigbytes(Sighash.ALL))\n        # save local transaction and set state\n        try:\n            self.lnworker.wallet.adb.add_transaction(closing_tx)\n        except UnrelatedTransactionException:\n            pass  # this can happen if (~all the balance goes to REMOTE)\n        chan.set_state(ChannelState.CLOSING)\n        # broadcast\n        await self.network.try_broadcasting(closing_tx, 'closing')\n        return closing_tx.txid()\n\n    async def htlc_switch(self):\n        await self.initialized\n        # don't context switch in a htlc switch iteration as htlc sets are shared between peers\n        assert not inspect.iscoroutinefunction(self._run_htlc_switch_iteration)\n        while True:\n            await self.ping_if_required()\n            self._htlc_switch_iterdone_event.set()\n            self._htlc_switch_iterdone_event.clear()\n            # We poll every 0.1 sec to check if there is work to do,\n            # or we can also be triggered via events.\n            # When forwarding an HTLC originating from this peer (the upstream),\n            # we can get triggered for events that happen on the downstream peer.\n            # TODO: trampoline forwarding relies on the polling\n            async with ignore_after(0.1):\n                async with OldTaskGroup(wait=any) as group:\n                    await group.spawn(self._received_revack_event.wait())\n                    await group.spawn(self.downstream_htlc_resolved_event.wait())\n            self._htlc_switch_iterstart_event.set()\n            self._htlc_switch_iterstart_event.clear()\n            try:\n                self._run_htlc_switch_iteration()\n            except Exception as e:\n                # this is code with many asserts and dense logic so it seems useful to allow the user\n                # report to exceptions that otherwise might go unnoticed for some time\n                reported_exc = type(e)(\"redacted\")  # text could contain onions, payment hashes etc.\n                reported_exc.__traceback__ = e.__traceback__\n                util.send_exception_to_crash_reporter(reported_exc)\n                raise e\n\n    @util.profiler(min_threshold=0.02)\n    def _run_htlc_switch_iteration(self):\n        self._maybe_cleanup_received_htlcs_pending_removal()\n        # htlc processing happens in two steps:\n        # 1. Step: Iterating through all channels and their pending htlcs, doing validation\n        #    feasible for single htlcs (some checks only make sense on the whole mpp set) and\n        #    then collecting these htlcs in a mpp set by payment key.\n        #    HTLCs failing these checks will get failed directly and won't be added to any set.\n        #    No htlcs will get settled in this step, settling only happens on complete mpp sets.\n        #    If a new htlc belongs to a set which has already been failed, the htlc will be failed\n        #    and not added to any set.\n        #    Each htlc is only supposed to go through this first loop once when being received.\n        for chan_id, chan in self.channels.items():\n            if not chan.can_update_ctx(proposer=LOCAL):\n                continue\n            self.maybe_send_commitment(chan)\n            unfulfilled = chan.unfulfilled_htlcs\n            for htlc_id, onion_packet_hex in list(unfulfilled.items()):\n                if not chan.hm.is_htlc_irrevocably_added_yet(htlc_proposer=REMOTE, htlc_id=htlc_id):\n                    continue\n\n                htlc = chan.hm.get_htlc_by_id(REMOTE, htlc_id)\n                try:\n                    onion_packet = self._parse_onion_packet(onion_packet_hex)\n                except OnionParsingError as e:\n                    self.fail_malformed_htlc(\n                        chan=chan,\n                        htlc_id=htlc.htlc_id,\n                        reason=e,\n                    )\n                    del unfulfilled[htlc_id]\n                    continue\n\n                try:\n                    processed_onion_packet = self._process_incoming_onion_packet(\n                        onion_packet,\n                        payment_hash=htlc.payment_hash,\n                        is_trampoline=False,\n                    )\n                    payment_key: str = self._check_unfulfilled_htlc(\n                        chan=chan,\n                        htlc=htlc,\n                        processed_onion=processed_onion_packet,\n                    )\n                    self.lnworker.update_or_create_mpp_with_received_htlc(\n                        payment_key=payment_key,\n                        channel_id=chan.channel_id,\n                        htlc=htlc,\n                        unprocessed_onion_packet=onion_packet_hex,  # outer onion if trampoline\n                    )\n                except OnionParsingError as e:  # could be raised when parsing the inner trampoline onion\n                    self.fail_malformed_htlc(\n                        chan=chan,\n                        htlc_id=htlc.htlc_id,\n                        reason=e,\n                    )\n                except Exception as e:\n                    # Fail the htlc directly if it fails to pass these tests, it will not get added to a htlc set.\n                    # https://github.com/lightning/bolts/blob/14272b1bd9361750cfdb3e5d35740889a6b510b5/04-onion-routing.md?plain=1#L388\n                    reraise = False\n                    if isinstance(e, OnionRoutingFailure):\n                        orf = e\n                    else:\n                        orf = OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')\n                        reraise = True  # propagate this out, as this might suggest a bug\n                    error_bytes = orf.to_wire_msg(onion_packet, self.privkey, self.network.get_local_height())\n                    self.fail_htlc(\n                        chan=chan,\n                        htlc_id=htlc.htlc_id,\n                        error_bytes=error_bytes,\n                    )\n                    if reraise:\n                        raise\n                finally:\n                    del unfulfilled[htlc_id]\n\n        # 2. Step: Acting on sets of htlcs.\n        #    Doing further checks that have to be done on sets of htlcs (e.g. total amount checks)\n        #    and checks that have to be done continuously like checking for timeout.\n        #    A set marked as failed once must never settle any htlcs associated to it.\n        #    The sets are shared between all peers, so each peers htlc_switch acts on the same sets.\n        for payment_key, htlc_set in list(self.lnworker.received_mpp_htlcs.items()):\n            any_error, preimage, callback = self._check_unfulfilled_htlc_set(payment_key, htlc_set)\n            assert bool(any_error) + bool(preimage) + bool(callback) <= 1, \\\n                        f\"{any_error=}, {bool(preimage)=}, {callback=}\"\n            if any_error:\n                error_tuple = self.lnworker.set_htlc_set_error(payment_key, any_error)\n                self._fail_htlc_set(payment_key, error_tuple)\n            if preimage:\n                if self.lnworker.enable_htlc_settle:\n                    self.lnworker.set_request_status(htlc_set.get_payment_hash(), PR_PAID)\n                    self._fulfill_htlc_set(payment_key, preimage)\n            if callback:\n                task = asyncio.create_task(callback())\n                task.add_done_callback(  # handle exceptions occurring in callback\n                    lambda t: (util.send_exception_to_crash_reporter(t.exception()) if t.exception() else None)\n                )\n\n            if len(self.lnworker.received_mpp_htlcs[payment_key].htlcs) == 0:\n                self.logger.debug(f\"deleting resolved mpp set: {payment_key=}\")\n                del self.lnworker.received_mpp_htlcs[payment_key]\n                self.lnworker.maybe_cleanup_forwarding(payment_key)\n\n    def _maybe_cleanup_received_htlcs_pending_removal(self) -> None:\n        done = set()\n        for chan, htlc_id in self.received_htlcs_pending_removal:\n            if chan.hm.is_htlc_irrevocably_removed_yet(htlc_proposer=REMOTE, htlc_id=htlc_id):\n                done.add((chan, htlc_id))\n        if done:\n            for key in done:\n                self.received_htlcs_pending_removal.remove(key)\n            self.received_htlc_removed_event.set()\n            self.received_htlc_removed_event.clear()\n\n    async def wait_one_htlc_switch_iteration(self) -> None:\n        \"\"\"Waits until the HTLC switch does a full iteration or the peer disconnects,\n        whichever happens first.\n        \"\"\"\n        async def htlc_switch_iteration():\n            await self._htlc_switch_iterstart_event.wait()\n            await self._htlc_switch_iterdone_event.wait()\n\n        async with OldTaskGroup(wait=any) as group:\n            await group.spawn(htlc_switch_iteration())\n            await group.spawn(self.got_disconnected.wait())\n\n    def _log_htlc_fail_reason_cb(\n        self,\n        channel_id: bytes,\n        htlc: UpdateAddHtlc,\n        onion_payload: dict\n    ) -> Callable[[str], None]:\n        def _log_fail_reason(reason: str) -> None:\n            scid = self.lnworker.get_channel_by_id(channel_id).short_channel_id\n            self.logger.info(f\"will FAIL HTLC: {str(scid)=}. {reason=}. {str(htlc)=}. {onion_payload=}\")\n        return _log_fail_reason\n\n    def _log_htlc_set_fail_reason_cb(self, mpp_set: ReceivedMPPStatus) -> Callable[[str], None]:\n        def log_fail_reason(reason: str):\n            for mpp_htlc in mpp_set.htlcs:\n                try:\n                    processed_onion = self._process_incoming_onion_packet(\n                        onion_packet=self._parse_onion_packet(mpp_htlc.unprocessed_onion),\n                        payment_hash=mpp_htlc.htlc.payment_hash,\n                        is_trampoline=False,\n                    )\n                    onion_payload = processed_onion.hop_data.payload\n                except Exception:\n                    onion_payload = {}\n\n                self._log_htlc_fail_reason_cb(\n                    mpp_htlc.channel_id,\n                    mpp_htlc.htlc,\n                    onion_payload,\n                )(f\"mpp set {id(mpp_set)} failed: {reason}\")\n\n        return log_fail_reason\n\n    def _check_unfulfilled_htlc_set(\n        self,\n        payment_key: str,\n        mpp_set: ReceivedMPPStatus\n    ) -> Tuple[\n        Optional[Union[OnionRoutingFailure, OnionFailureCode, bytes]],  # error types used to fail the set\n        Optional[bytes],  # preimage to settle the set\n        Optional[Callable[[], Coroutine[Any, Any, None]]],  # callback\n    ]:\n        \"\"\"\n        Returns what to do next with the given set of htlcs:\n            * Fail whole set -> returns error code\n            * Settle whole set -> Returns preimage\n            * call callback (e.g. forwarding, hold invoice)\n        May modify the mpp set in lnworker.received_mpp_htlcs (e.g. by setting its resolution to COMPLETE).\n        \"\"\"\n        _log_fail_reason = self._log_htlc_set_fail_reason_cb(mpp_set)\n\n        if (final_state := self._check_final_mpp_set_state(payment_key, mpp_set)) is not None:\n            return final_state\n\n        assert mpp_set.resolution in (RecvMPPResolution.WAITING, RecvMPPResolution.COMPLETE)\n        chain = self.network.blockchain()\n        local_height = chain.height()\n        if chain.is_tip_stale():\n            _log_fail_reason(f\"our chain tip is stale: {local_height=}\")\n            return OnionFailureCode.TEMPORARY_NODE_FAILURE, None, None\n\n        amount_msat: int = 0  # sum(amount_msat of each htlc)\n        total_msat = None  # type: Optional[int]\n        payment_hash = mpp_set.get_payment_hash()\n        closest_cltv_abs = mpp_set.get_closest_cltv_abs()\n        first_htlc_timestamp = mpp_set.get_first_htlc_timestamp()\n        processed_onions = {}  # type: dict[ReceivedMPPHtlc, Tuple[ProcessedOnionPacket, Optional[ProcessedOnionPacket]]]\n        for mpp_htlc in mpp_set.htlcs:\n            processed_onion = self._process_incoming_onion_packet(\n                onion_packet=self._parse_onion_packet(mpp_htlc.unprocessed_onion),\n                payment_hash=payment_hash,\n                is_trampoline=False,  # this is always the outer onion\n            )\n            processed_onions[mpp_htlc] = (processed_onion, None)\n            inner_onion = None\n            if processed_onion.trampoline_onion_packet:\n                inner_onion = self._process_incoming_onion_packet(\n                    onion_packet=processed_onion.trampoline_onion_packet,\n                    payment_hash=payment_hash,\n                    is_trampoline=True,\n                )\n                processed_onions[mpp_htlc] = (processed_onion, inner_onion)\n\n            total_msat_outer_onion = processed_onion.total_msat\n            total_msat_inner_onion = inner_onion.total_msat if inner_onion else None\n            if total_msat is None:\n                total_msat = total_msat_inner_onion or total_msat_outer_onion\n\n            # check total_msat is equal for all htlcs of the set\n            if total_msat != (total_msat_inner_onion or total_msat_outer_onion):\n                _log_fail_reason(f\"total_msat is not uniform: {total_msat=} != {processed_onion.total_msat=}\")\n                return OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, None, None\n\n            amount_msat += mpp_htlc.htlc.amount_msat\n\n        # If the set contains outer onions with different payment secrets, the set's payment_key is\n        # derived from the trampoline/invoice/inner payment secret, so it is the second stage of a\n        # multi-trampoline payment in which all the trampoline parts/htlcs got combined.\n        # In this case the amt_to_forward cannot be compared as it may differ between the trampoline parts.\n        # However, amt_to_forward should be similar for all onions of a single trampoline part and gets\n        # compared in the first stage where the htlc set represents a single trampoline part.\n        outer_onions = [onions[0] for onions in processed_onions.values()]\n        can_have_different_amt_to_fwd = not all(o.payment_secret == outer_onions[0].payment_secret for o in outer_onions)\n        trampoline_onions = iter(onions[1] for onions in processed_onions.values())\n        if not lnonion.compare_trampoline_onions(trampoline_onions, exclude_amt_to_fwd=can_have_different_amt_to_fwd):\n            _log_fail_reason(f\"got inconsistent {trampoline_onions=}\")\n            return OnionFailureCode.INVALID_ONION_PAYLOAD, None, None\n\n        if len(processed_onions) == 1:\n            outer_onion, inner_onion = next(iter(processed_onions.values()))\n            if not outer_onion.are_we_final:\n                assert inner_onion is None, f\"{outer_onion=}\\n{inner_onion=}\"\n                if not self.lnworker.enable_htlc_forwarding:\n                    return None, None, None\n                # this is a single (non-trampoline) htlc set which needs to be forwarded.\n                # set to settling state so it will not be failed or forwarded twice.\n                self.lnworker.set_mpp_resolution(payment_key, RecvMPPResolution.SETTLING)\n                fwd_cb = lambda: self.lnworker.maybe_forward_htlc_set(payment_key, processed_htlc_set=processed_onions)\n                return None, None, fwd_cb\n\n        assert payment_hash is not None and total_msat is not None\n        # check for expiry over time and potentially fail the whole set if any\n        # htlc's cltv becomes too close\n        blocks_to_expiry = max(0, closest_cltv_abs - local_height)\n        accepted_expiry_delta = self.lnworker.dont_expire_htlcs.get(payment_hash.hex(), MIN_FINAL_CLTV_DELTA_ACCEPTED)\n        if accepted_expiry_delta is not None and blocks_to_expiry < accepted_expiry_delta:\n            _log_fail_reason(f\"htlc.cltv_abs is unreasonably close\")\n            return OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, None, None\n\n        # check for mpp expiry (if incomplete and expired -> fail)\n        if mpp_set.resolution == RecvMPPResolution.WAITING \\\n                or not self.lnworker.is_payment_bundle_complete(payment_key):\n            # maybe this set is COMPLETE but the bundle is not yet completed, so the bundle can be considered WAITING\n            if int(time.time()) - first_htlc_timestamp > self.lnworker.MPP_EXPIRY \\\n                    or self.lnworker.lnpeermgr.stopping_soon:\n                _log_fail_reason(f\"MPP TIMEOUT (> {self.lnworker.MPP_EXPIRY} sec)\")\n                return OnionFailureCode.MPP_TIMEOUT, None, None\n\n        if mpp_set.resolution == RecvMPPResolution.WAITING:\n            # calculate the sum of just in time channel opening fees, note jit only supports\n            # single part payments for now, this is enforced by checking against the invoice features\n            htlc_channels = [self.lnworker.get_channel_by_id(channel_id) for channel_id in set(h.channel_id for h in mpp_set.htlcs)]\n            jit_opening_fees_msat = sum((c.jit_opening_fee or 0) for c in htlc_channels)\n\n            # check if set is first stage multi-trampoline payment to us\n            # first stage trampoline payment:\n            # is a trampoline payment + we_are_final + payment key is derived from outer onion's payment secret\n            # (so it is not the payment secret we requested in the invoice, but some secret set by a\n            # trampoline forwarding node on the route).\n            # if it is first stage, check if sum(htlcs) >= amount_to_forward of the trampoline_payload.\n            # If this part is complete, move the htlcs to the overall mpp set of the payment (keyed by inner secret).\n            # Once the second stage set (the set containing all htlcs of the separate trampoline parts)\n            # is complete, the payment gets fulfilled.\n            trampoline_payment_key = None\n            any_trampoline_onion = next(iter(processed_onions.values()))[1]\n            if any_trampoline_onion and any_trampoline_onion.are_we_final:\n                trampoline_payment_secret = any_trampoline_onion.payment_secret\n                assert trampoline_payment_secret == self.lnworker.get_payment_secret(payment_hash)\n                trampoline_payment_key = (payment_hash + trampoline_payment_secret).hex()\n\n            if trampoline_payment_key and trampoline_payment_key != payment_key:\n                if jit_opening_fees_msat:\n                    # for jit openings we only accept a single htlc\n                    expected_amount_first_stage = any_trampoline_onion.total_msat - jit_opening_fees_msat\n                else:\n                    expected_amount_first_stage = any_trampoline_onion.amt_to_forward\n\n                # first stage of trampoline payment, the first stage must never get set COMPLETE\n                if amount_msat >= expected_amount_first_stage:\n                    # setting the parent key will mark the htlcs to be moved to the parent set\n                    self.logger.debug(f\"trampoline part complete. {len(mpp_set.htlcs)=}, \"\n                                      f\"{amount_msat=}. setting parent key: {trampoline_payment_key}\")\n                    self.lnworker.received_mpp_htlcs[payment_key] = mpp_set._replace(\n                        parent_set_key=trampoline_payment_key,\n                    )\n            elif amount_msat >= (total_msat - jit_opening_fees_msat):  # regular mpp or 2nd stage trampoline\n                # set mpp_set as completed as we have received the full total_msat\n                mpp_set = self.lnworker.set_mpp_resolution(\n                    payment_key=payment_key,\n                    new_resolution=RecvMPPResolution.COMPLETE,\n                )\n\n        # check if this set is a trampoline forwarding and potentially return forwarding callback\n        # note: all inner trampoline onions are equal (enforced above)\n        _, any_inner_onion = next(iter(processed_onions.values()))\n        if any_inner_onion and not any_inner_onion.are_we_final:\n            # this is a trampoline forwarding\n            can_forward = mpp_set.resolution == RecvMPPResolution.COMPLETE and self.lnworker.enable_htlc_forwarding\n            if not can_forward:\n                return None, None, None\n            self.lnworker.set_mpp_resolution(payment_key, RecvMPPResolution.SETTLING)\n            fwd_cb = lambda: self.lnworker.maybe_forward_htlc_set(payment_key, processed_htlc_set=processed_onions)\n            return None, None, fwd_cb\n\n        #  -- from here on it's assumed this set is a payment for us (not something to forward) --\n        payment_info = self.lnworker.get_payment_info(payment_hash, direction=RECEIVED)\n        if payment_info is None:\n            _log_fail_reason(f\"payment info has been deleted\")\n            return OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, None, None\n        elif not payment_info.invoice_features.supports(LnFeatures.BASIC_MPP_OPT) and len(mpp_set.htlcs) > 1:\n            # in _check_unfulfilled_htlc we already check amount == total_amount, however someone could\n            # send us multiple htlcs that all pay the full amount, so we also check the htlc count\n            _log_fail_reason(f\"got mpp but we requested no mpp in the invoice: {len(mpp_set.htlcs)=}\")\n            return OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, None, None\n\n        # check invoice expiry, fail set if the invoice has expired before it was completed\n        if mpp_set.resolution == RecvMPPResolution.WAITING:\n            if int(time.time()) > payment_info.expiration_ts:\n                _log_fail_reason(f\"invoice is expired {payment_info.expiration_ts=}\")\n                return OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, None, None\n            return None, None, None\n\n        preimage = self.lnworker.get_preimage(payment_hash)\n        settling_blocked = preimage is not None and payment_hash.hex() in self.lnworker.dont_settle_htlcs\n        waiting_for_preimage = preimage is None and payment_hash.hex() in self.lnworker.dont_expire_htlcs\n        if settling_blocked or waiting_for_preimage:\n            # used by hold invoice cli and JIT channels to prevent the htlcs from getting fulfilled automatically\n            return None, None, None\n\n        hold_invoice_callback = self.lnworker.hold_invoice_callbacks.get(payment_hash)\n        if not preimage and not hold_invoice_callback:\n            _log_fail_reason(f\"cannot settle, no preimage or callback found for {payment_hash.hex()=}\")\n            return OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, None, None\n\n        if not self.lnworker.is_payment_bundle_complete(payment_key):\n            # don't allow settling before all sets of the bundle are COMPLETE\n            return None, None, None\n        else:\n            # If this set is part of a bundle now all parts are COMPLETE so the bundle can be deleted\n            # so the individual sets will get fulfilled.\n            self.lnworker.delete_payment_bundle(payment_key=bytes.fromhex(payment_key))\n\n        assert mpp_set.resolution == RecvMPPResolution.COMPLETE, \"should return earlier if set is incomplete\"\n        if not preimage:\n            assert hold_invoice_callback is not None, \"should have been failed before\"\n            async def callback():\n                try:\n                    await hold_invoice_callback(payment_hash)\n                except OnionRoutingFailure as e:  # todo: should this catch all exceptions?\n                    _log_fail_reason(f\"hold invoice callback raised {e}\")\n                    self.lnworker.set_mpp_resolution(payment_key, RecvMPPResolution.FAILED)\n            # mpp set must not be failed unless the consumer calls unregister_hold_invoice and\n            # callback must only be called once. This is enforced by setting the set to SETTLING.\n            self.lnworker.set_mpp_resolution(payment_key, RecvMPPResolution.SETTLING)\n            return None, None, callback\n\n        # settle htlc set\n        self.lnworker.set_mpp_resolution(payment_key, RecvMPPResolution.SETTLING)\n        return None, preimage, None\n\n    def _check_final_mpp_set_state(\n        self,\n        payment_key: str,\n        mpp_set: ReceivedMPPStatus,\n    ) -> Optional[Tuple[\n            Optional[Union[OnionRoutingFailure, OnionFailureCode, bytes]],  # error types used to fail the set\n            Optional[bytes],  # preimage to settle the set\n            None,  # callback\n        ]]:\n        \"\"\"\n        handle sets that are already in a state eligible for fulfillment or failure and shouldn't\n        go through another iteration of _check_unfulfilled_htlc_set.\n        \"\"\"\n        if len(mpp_set.htlcs) == 0:\n            # stale set, will get deleted on the next iteration\n            return None, None, None\n\n        if mpp_set.resolution == RecvMPPResolution.FAILED:\n            error_bytes, failure_message = self.lnworker.get_forwarding_failure(payment_key)\n            if error_bytes or failure_message:\n                return error_bytes or failure_message, None, None\n            return OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, None, None\n        elif mpp_set.resolution == RecvMPPResolution.EXPIRED:\n            return OnionFailureCode.MPP_TIMEOUT, None, None\n\n        if mpp_set.parent_set_key:\n            # this is a complete trampoline part of a multi trampoline payment. Move the htlcs to parent.\n            parent = self.lnworker.received_mpp_htlcs.get(mpp_set.parent_set_key)\n            if not parent:\n                parent = ReceivedMPPStatus(\n                    resolution=RecvMPPResolution.WAITING,\n                    htlcs=frozenset(),\n                )\n            self.lnworker.received_mpp_htlcs[mpp_set.parent_set_key] = parent._replace(\n                htlcs=parent.htlcs | mpp_set.htlcs\n            )\n            self.lnworker.received_mpp_htlcs[payment_key] = mpp_set._replace(htlcs=frozenset())\n            return None, None, None  # this set will get deleted as there are no htlcs in it anymore\n\n        assert not mpp_set.parent_set_key\n        if mpp_set.resolution == RecvMPPResolution.SETTLING:\n            # this is an ongoing forwarding, or a set that has not yet been fully settled (and removed).\n            # note the htlcs in SETTLING will not get failed automatically,\n            # even if timeout comes close, so either a forwarding failure or preimage has to be set\n            error_bytes, failure_message = self.lnworker.get_forwarding_failure(payment_key)\n            if error_bytes or failure_message:\n                # this was a forwarding set and it failed\n                self.lnworker.set_mpp_resolution(payment_key, RecvMPPResolution.FAILED)\n                return error_bytes or failure_message, None, None\n            payment_hash = mpp_set.get_payment_hash()\n            if payment_hash.hex() in self.lnworker.dont_settle_htlcs:\n                return None, None, None\n            preimage = self.lnworker.get_preimage(payment_hash)\n            return None, preimage, None\n\n        return None\n\n    def _parse_onion_packet(self, onion_packet_hex: str) -> OnionPacket:\n        \"\"\"\n        https://github.com/lightning/bolts/blob/14272b1bd9361750cfdb3e5d35740889a6b510b5/02-peer-protocol.md?plain=1#L2352\n        \"\"\"\n        onion_packet_bytes = None\n        try:\n            onion_packet_bytes = bytes.fromhex(onion_packet_hex)\n            onion_packet = OnionPacket.from_bytes(onion_packet_bytes)\n        except Exception as parsing_exc:\n            self.logger.warning(f\"unable to parse onion: {str(parsing_exc)}\")\n            onion_parsing_error = OnionParsingError(\n                data=sha256(onion_packet_bytes or b''),\n            )\n            raise onion_parsing_error\n        return onion_packet\n\n    def _process_incoming_onion_packet(\n            self,\n            onion_packet: OnionPacket, *,\n            payment_hash: bytes,\n            is_trampoline: bool = False) -> ProcessedOnionPacket:\n        onion_hash = onion_packet.onion_hash\n        cache_key = sha256(onion_hash + payment_hash + bytes([is_trampoline]))  # type: ignore\n        if cached_onion := self._processed_onion_cache.get(cache_key):\n            return cached_onion\n        try:\n            processed_onion = lnonion.process_onion_packet(\n                onion_packet,\n                our_onion_private_key=self.privkey,\n                associated_data=payment_hash,\n                is_trampoline=is_trampoline)\n            self._processed_onion_cache[cache_key] = processed_onion\n        except UnsupportedOnionPacketVersion:\n            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=onion_hash)\n        except InvalidOnionPubkey:\n            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_KEY, data=onion_hash)\n        except InvalidOnionMac:\n            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_HMAC, data=onion_hash)\n        except Exception as e:\n            self.logger.warning(f\"error processing onion packet: {e!r}\")\n            raise OnionParsingError(data=onion_hash)\n        if self.network.config.TEST_FAIL_HTLCS_AS_MALFORMED:\n            raise OnionParsingError(data=onion_hash)\n        if self.network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE:\n            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')\n        return processed_onion\n\n    def on_onion_message(self, payload):\n        if hasattr(self.lnworker, 'onion_message_manager'):  # only on LNWallet\n            self.lnworker.onion_message_manager.on_onion_message(payload)\n"
  },
  {
    "path": "electrum/lnrater.py",
    "content": "# Copyright (C) 2020 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\"\"\"\nlnrater.py contains Lightning Network node rating functionality.\n\"\"\"\n\nimport asyncio\nfrom collections import defaultdict\nfrom pprint import pformat\nfrom random import choices\nfrom statistics import mean, median, stdev\nfrom typing import TYPE_CHECKING, Dict, NamedTuple, Tuple, List, Optional\nimport sys\nimport time\n\nfrom .logging import Logger\nfrom .util import profiler, get_running_loop\nfrom .lnrouter import fee_for_edge_msat\nfrom .lnutil import LnFeatures, ln_compare_features, IncompatibleLightningFeatures\nfrom .network import Network\n\nif TYPE_CHECKING:\n    from .channel_db import Policy, NodeInfo\n    from .lnchannel import ShortChannelID\n    from .lnworker import LNWallet\n\n\nMONTH_IN_BLOCKS = 6 * 24 * 30\n# the scores are only updated after this time interval\nRATER_UPDATE_TIME_SEC = 10 * 60\n# amount used for calculating an effective relative fee\nFEE_AMOUNT_MSAT = 100_000_000\n\n# define some numbers for minimal requirements of good nodes\n# exclude nodes with less number of channels\nEXCLUDE_NUM_CHANNELS = 15\n# exclude nodes with less mean capacity\nEXCLUDE_MEAN_CAPACITY_MSAT = 1_000_000_000\n# exclude nodes which are young\nEXCLUDE_NODE_AGE = 2 * MONTH_IN_BLOCKS\n# exclude nodes which have young mean channel age\nEXCLUDE_MEAN_CHANNEL_AGE = EXCLUDE_NODE_AGE\n# exclude nodes which charge a high fee\nEXCLUDE_EFFECTIVE_FEE_RATE = 0.001500\n# exclude nodes whose last channel open was a long time ago\nEXCLUDE_BLOCKS_LAST_CHANNEL = 3 * MONTH_IN_BLOCKS\n\n\nclass NodeStats(NamedTuple):\n    number_channels: int\n    # capacity related\n    total_capacity_msat: int\n    median_capacity_msat: float\n    mean_capacity_msat: float\n    # block height related\n    node_age_block_height: int\n    mean_channel_age_block_height: float\n    blocks_since_last_channel: int\n    # fees\n    mean_fee_rate: float\n\n\ndef weighted_sum(numbers: List[float], weights: List[float]) -> float:\n    running_sum = 0.0\n    for n, w in zip(numbers, weights):\n        running_sum += n * w\n    return running_sum/sum(weights)\n\n\nclass LNRater(Logger):\n    def __init__(self, lnworker: 'LNWallet', network: 'Network'):\n        \"\"\"LNRater can be used to suggest nodes to open up channels with.\n\n        The graph is analyzed and some heuristics are applied to sort out nodes\n        that are deemed to be bad routers or unmaintained.\n        \"\"\"\n        Logger.__init__(self)\n        self.lnworker = lnworker\n        self.network = network\n\n        self._node_stats: Dict[bytes, NodeStats] = {}  # node_id -> NodeStats\n        self._node_ratings: Dict[bytes, float] = {}  # node_id -> float\n        self._policies_by_nodes: Dict[bytes, List[Tuple[ShortChannelID, Policy]]] = defaultdict(list)  # node_id -> (short_channel_id, policy)\n        self._last_analyzed = 0  # timestamp\n        self._last_progress_percent = 0\n\n    def maybe_analyze_graph(self):\n        Network.run_from_another_thread(self._maybe_analyze_graph())\n\n    def analyze_graph(self):\n        \"\"\"Forces a graph analysis, e.g., due to external triggers like\n        the graph info reaching 50%.\"\"\"\n        Network.run_from_another_thread(self._analyze_graph())\n\n    async def _maybe_analyze_graph(self):\n        \"\"\"Analyzes the graph when in early sync stage (>30%) or when caching\n        time expires.\"\"\"\n        # gather information about graph sync status\n        current_channels, total, progress_percent = self.network.lngossip.get_sync_progress_estimate()\n\n        # gossip sync progress state could be None when not started, but channel\n        # db already knows something about the graph, which is why we allow to\n        # evaluate the graph early\n        if progress_percent is not None or self.network.channel_db.num_nodes > 500:\n            progress_percent = progress_percent or 0  # convert None to 0\n            now = time.time()\n            # graph should have changed significantly during the sync progress\n            # or last analysis was a long time ago\n            if (30 <= progress_percent and progress_percent - self._last_progress_percent >= 10 or\n                    self._last_analyzed + RATER_UPDATE_TIME_SEC < now):\n                await self._analyze_graph()\n                self._last_progress_percent = progress_percent\n                self._last_analyzed = now\n\n    async def _analyze_graph(self):\n        await self.network.channel_db.data_loaded.wait()\n        self._collect_policies_by_node()\n        loop = get_running_loop()\n        # the analysis is run in an executor because it's costly\n        await loop.run_in_executor(None, self._collect_purged_stats)\n        self._rate_nodes()\n        now = time.time()\n        self._last_analyzed = now\n\n    def _collect_policies_by_node(self):\n        policies = self.network.channel_db.get_node_policies()\n        for pv, p in policies.items():\n            # append tuples of ShortChannelID and Policy\n            self._policies_by_nodes[pv[0]].append((pv[1], p))\n\n    @profiler\n    def _collect_purged_stats(self):\n        \"\"\"Traverses through the graph and sorts out nodes.\"\"\"\n        current_height = self.network.get_local_height()\n        node_infos = self.network.channel_db.get_node_infos()\n\n        for n, channel_policies in self._policies_by_nodes.items():\n            try:\n                # use policies synonymously to channels\n                num_channels = len(channel_policies)\n\n                # save some time for nodes we are not interested in:\n                if num_channels < EXCLUDE_NUM_CHANNELS:\n                    continue\n\n                # analyze block heights\n                block_heights = [p[0].block_height for p in channel_policies]\n                node_age_bh = current_height - min(block_heights)\n                if node_age_bh < EXCLUDE_NODE_AGE:\n                    continue\n                mean_channel_age_bh = current_height - mean(block_heights)\n                if mean_channel_age_bh < EXCLUDE_MEAN_CHANNEL_AGE:\n                    continue\n                blocks_since_last_channel = current_height - max(block_heights)\n                if blocks_since_last_channel > EXCLUDE_BLOCKS_LAST_CHANNEL:\n                    continue\n\n                # analyze capacities\n                capacities = [p[1].htlc_maximum_msat for p in channel_policies]\n                if None in capacities:\n                    continue\n                total_capacity = sum(capacities)\n\n                mean_capacity = total_capacity / num_channels if num_channels else 0\n                if mean_capacity < EXCLUDE_MEAN_CAPACITY_MSAT:\n                    continue\n                median_capacity = median(capacities)\n\n                # analyze fees\n                effective_fee_rates = [fee_for_edge_msat(\n                    FEE_AMOUNT_MSAT,\n                    p[1].fee_base_msat,\n                    p[1].fee_proportional_millionths) / FEE_AMOUNT_MSAT for p in channel_policies]\n                mean_fees_rate = mean(effective_fee_rates)\n                if mean_fees_rate > EXCLUDE_EFFECTIVE_FEE_RATE:\n                    continue\n\n                self._node_stats[n] = NodeStats(\n                    number_channels=num_channels,\n                    total_capacity_msat=total_capacity,\n                    median_capacity_msat=median_capacity,\n                    mean_capacity_msat=mean_capacity,\n                    node_age_block_height=node_age_bh,\n                    mean_channel_age_block_height=mean_channel_age_bh,\n                    blocks_since_last_channel=blocks_since_last_channel,\n                    mean_fee_rate=mean_fees_rate\n                )\n\n            except Exception as e:\n                self.logger.exception(\"Could not use channel policies for \"\n                                      \"calculating statistics.\")\n                self.logger.debug(pformat(channel_policies))\n                continue\n\n        self.logger.info(f\"node statistics done, calculated statistics \"\n                         f\"for {len(self._node_stats)} nodes\")\n\n    def _rate_nodes(self):\n        \"\"\"Rate nodes by collected statistics.\"\"\"\n\n        max_capacity = 0\n        max_num_chan = 0\n        min_fee_rate = float('inf')\n        for stats in self._node_stats.values():\n            max_capacity = max(max_capacity, stats.total_capacity_msat)\n            max_num_chan = max(max_num_chan, stats.number_channels)\n            min_fee_rate = min(min_fee_rate, stats.mean_fee_rate)\n\n        for n, stats in self._node_stats.items():\n            heuristics = []\n            heuristics_weights = []\n\n            # Construct an average score which leads to recommendation of nodes\n            # with low fees, large capacity and reasonable number of channels.\n            # This is somewhat akin to preferential attachment, but low fee\n            # nodes are more favored. Here we make a compromise between user\n            # comfort and decentralization, tending towards user comfort.\n\n            # number of channels\n            heuristics.append(stats.number_channels / max_num_chan)\n            heuristics_weights.append(0.2)\n            # total capacity\n            heuristics.append(stats.total_capacity_msat / max_capacity)\n            heuristics_weights.append(0.8)\n            # inverse fees\n            fees = min(1E-6, min_fee_rate) / max(1E-10, stats.mean_fee_rate)\n            heuristics.append(fees)\n            heuristics_weights.append(1.0)\n\n            self._node_ratings[n] = weighted_sum(heuristics, heuristics_weights)\n\n    @profiler\n    def suggest_node_channel_open(self) -> Optional[bytes]:\n        node_stats = self._node_stats.copy()\n        node_ratings = self._node_ratings.copy()\n        channel_peers = self.lnworker.channel_peers()\n        node_info: Optional[\"NodeInfo\"] = None\n\n        while node_stats:\n            # randomly pick nodes weighted by node_rating\n            pk = choices(list(node_stats.keys()), weights=list(node_ratings.values()), k=1)[0]\n            # remove the pk so it doesn't get tried again\n            node_stats.pop(pk); node_ratings.pop(pk)\n            # node should have compatible features\n            node_info = self.network.channel_db.get_node_infos().get(pk, None)\n            peer_features = LnFeatures(node_info.features)\n            try:\n                ln_compare_features(self.lnworker.features, peer_features)\n            except IncompatibleLightningFeatures as e:\n                self.logger.info(\"suggested node is incompatible\")\n                continue\n\n            # don't want to connect to nodes we are already connected to\n            if pk in channel_peers:\n                continue\n            # don't want to connect to nodes we already have a channel with on another device\n            if self.lnworker.has_conflicting_backup_with(pk):\n                continue\n            # node should be on clearnet and have an address saved\n            for (hostname, _, _) in self.lnworker.channel_db.get_node_addresses(node_id=pk):\n                if not hostname.endswith(\".onion\"):\n                    break\n            else:\n                continue\n            break\n        else:\n            self.logger.info(f\"no suitable channel peer found\")\n            return None\n\n        alias = node_info.alias if node_info else 'unknown node alias'\n        self.logger.info(\n            f\"node rating for {alias}:\\n\"\n            f\"{pformat(self._node_stats[pk])} (score {self._node_ratings[pk]})\")\n\n        return pk\n\n    def suggest_peer(self) -> Optional[bytes]:\n        \"\"\"Suggests a LN node to open a channel with.\n        Returns a node ID (pubkey).\n        \"\"\"\n        self.maybe_analyze_graph()\n        if self._node_ratings:\n            return self.suggest_node_channel_open()\n        else:\n            return None\n"
  },
  {
    "path": "electrum/lnrouter.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2018 The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport queue\nfrom collections import defaultdict\nfrom typing import Sequence, Tuple, Optional, Dict, TYPE_CHECKING, Set, Callable\nimport time\nimport threading\nfrom threading import RLock\nfrom math import inf\n\nimport attr\n\nfrom .util import profiler, with_lock\nfrom .logging import Logger\nfrom .lnutil import (NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, LnFeatures,\n                     NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, PaymentFeeBudget)\nfrom .channel_db import ChannelDB, Policy, NodeInfo\n\nif TYPE_CHECKING:\n    from .lnchannel import Channel\n\nDEFAULT_PENALTY_BASE_MSAT = 500  # how much base fee we apply for unknown sending capability of a channel\nDEFAULT_PENALTY_PROPORTIONAL_MILLIONTH = 100  # how much relative fee we apply for unknown sending capability of a channel\nHINT_DURATION = 3600  # how long (in seconds) a liquidity hint remains valid\n\n\nclass NoChannelPolicy(Exception):\n    def __init__(self, short_channel_id: bytes):\n        short_channel_id = ShortChannelID.normalize(short_channel_id)\n        super().__init__(f'cannot find channel policy for short_channel_id: {short_channel_id}')\n\n\nclass LNPathInconsistent(Exception): pass\n\n\ndef fee_for_edge_msat(forwarded_amount_msat: int, fee_base_msat: int, fee_proportional_millionths: int) -> int:\n    return fee_base_msat \\\n           + (forwarded_amount_msat * fee_proportional_millionths // 1_000_000)\n\n\n@attr.s(slots=True)\nclass PathEdge:\n    start_node = attr.ib(type=bytes, kw_only=True, repr=lambda val: val.hex())\n    end_node = attr.ib(type=bytes, kw_only=True, repr=lambda val: val.hex())\n    short_channel_id = attr.ib(type=ShortChannelID, kw_only=True, repr=lambda val: str(val))\n\n    @property\n    def node_id(self) -> bytes:\n        # legacy compat  # TODO rm\n        return self.end_node\n\n@attr.s\nclass RouteEdge(PathEdge):\n    fee_base_msat = attr.ib(type=int, kw_only=True)                # for start_node\n    fee_proportional_millionths = attr.ib(type=int, kw_only=True)  # for start_node\n    cltv_delta = attr.ib(type=int, kw_only=True)                   # for start_node\n    node_features = attr.ib(type=int, kw_only=True, repr=lambda val: str(int(val)))  # note: for end_node!\n\n    def fee_for_edge(self, amount_msat: int) -> int:\n        return fee_for_edge_msat(forwarded_amount_msat=amount_msat,\n                                 fee_base_msat=self.fee_base_msat,\n                                 fee_proportional_millionths=self.fee_proportional_millionths)\n\n    @classmethod\n    def from_channel_policy(\n            cls,\n            *,\n            channel_policy: 'Policy',  # for start_node\n            short_channel_id: bytes,\n            start_node: bytes,\n            end_node: bytes,\n            node_info: Optional[NodeInfo],  # for end_node\n    ) -> 'RouteEdge':\n        assert isinstance(short_channel_id, bytes)\n        assert type(start_node) is bytes\n        assert type(end_node) is bytes\n        return RouteEdge(\n            start_node=start_node,\n            end_node=end_node,\n            short_channel_id=ShortChannelID.normalize(short_channel_id),\n            fee_base_msat=channel_policy.fee_base_msat,\n            fee_proportional_millionths=channel_policy.fee_proportional_millionths,\n            cltv_delta=channel_policy.cltv_delta,\n            node_features=node_info.features if node_info else 0)\n\n    def has_feature_varonion(self) -> bool:\n        features = LnFeatures(self.node_features)\n        return features.supports(LnFeatures.VAR_ONION_OPT)\n\n    def is_trampoline(self) -> bool:\n        return False\n\n@attr.s\nclass TrampolineEdge(RouteEdge):\n    invoice_routing_info = attr.ib(type=Sequence[bytes], default=None)\n    invoice_features = attr.ib(type=int, default=None)\n    # this is re-defined from parent just to specify a default value:\n    short_channel_id = attr.ib(default=ShortChannelID(8), repr=lambda val: str(val))\n\n    def is_trampoline(self):\n        return True\n\n\nLNPaymentPath = Sequence[PathEdge]\nLNPaymentRoute = Sequence[RouteEdge]\nLNPaymentTRoute = Sequence[TrampolineEdge]\n\n\ndef is_route_within_budget(\n    route: LNPaymentRoute,\n    *,\n    budget: PaymentFeeBudget,\n    amount_msat_for_dest: int,  # that final receiver gets\n    cltv_delta_for_dest: int,   # that final receiver gets\n) -> bool:\n    \"\"\"Run some sanity checks on the whole route, before attempting to use it.\n    called when we are paying; so e.g. lower cltv is better\n    \"\"\"\n    if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:\n        return False\n    amt = amount_msat_for_dest\n    cltv_cost_of_route = 0  # excluding cltv_delta_for_dest\n    for route_edge in reversed(route[1:]):\n        amt += route_edge.fee_for_edge(amt)\n        cltv_cost_of_route += route_edge.cltv_delta\n    fee_cost = amt - amount_msat_for_dest\n    # check against budget\n    if cltv_cost_of_route > budget.cltv:\n        return False\n    if fee_cost > budget.fee_msat:\n        return False\n    # sanity check\n    total_cltv_delta = cltv_cost_of_route + cltv_delta_for_dest\n    if total_cltv_delta > NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:\n        return False\n    return True\n\n\nclass LiquidityHint:\n    \"\"\"Encodes the amounts that can and cannot be sent over the direction of a\n    channel.\n\n    A LiquidityHint is the value of a dict, which is keyed to node ids and the\n    channel.\n    \"\"\"\n    def __init__(self):\n        # use \"can_send_forward + can_send_backward < cannot_send_forward + cannot_send_backward\" as a sanity check?\n        self._can_send_forward = None\n        self._cannot_send_forward = None\n        self._can_send_backward = None\n        self._cannot_send_backward = None\n        self.hint_timestamp = 0\n        self._inflight_htlcs_forward = 0\n        self._inflight_htlcs_backward = 0\n\n    def is_hint_invalid(self) -> bool:\n        now = int(time.time())\n        return now - self.hint_timestamp > HINT_DURATION\n\n    @property\n    def can_send_forward(self):\n        return None if self.is_hint_invalid() else self._can_send_forward\n\n    @can_send_forward.setter\n    def can_send_forward(self, amount):\n        # we don't want to record less significant info\n        # (sendable amount is lower than known sendable amount):\n        if self._can_send_forward and self._can_send_forward > amount:\n            return\n        self._can_send_forward = amount\n        # we make a sanity check that sendable amount is lower than not sendable amount\n        if self._cannot_send_forward and self._can_send_forward > self._cannot_send_forward:\n            self._cannot_send_forward = None\n\n    @property\n    def can_send_backward(self):\n        return None if self.is_hint_invalid() else self._can_send_backward\n\n    @can_send_backward.setter\n    def can_send_backward(self, amount):\n        if self._can_send_backward and self._can_send_backward > amount:\n            return\n        self._can_send_backward = amount\n        if self._cannot_send_backward and self._can_send_backward > self._cannot_send_backward:\n            self._cannot_send_backward = None\n\n    @property\n    def cannot_send_forward(self):\n        return None if self.is_hint_invalid() else self._cannot_send_forward\n\n    @cannot_send_forward.setter\n    def cannot_send_forward(self, amount):\n        # we don't want to record less significant info\n        # (not sendable amount is higher than known not sendable amount):\n        if self._cannot_send_forward and self._cannot_send_forward < amount:\n            return\n        self._cannot_send_forward = amount\n        if self._can_send_forward and self._can_send_forward > self._cannot_send_forward:\n            self._can_send_forward = None\n        # if we can't send over the channel, we should be able to send in the\n        # reverse direction\n        self.can_send_backward = amount\n\n    @property\n    def cannot_send_backward(self):\n        return None if self.is_hint_invalid() else self._cannot_send_backward\n\n    @cannot_send_backward.setter\n    def cannot_send_backward(self, amount):\n        if self._cannot_send_backward and self._cannot_send_backward < amount:\n            return\n        self._cannot_send_backward = amount\n        if self._can_send_backward and self._can_send_backward > self._cannot_send_backward:\n            self._can_send_backward = None\n        self.can_send_forward = amount\n\n    def can_send(self, is_forward_direction: bool):\n        # make info invalid after some time?\n        if is_forward_direction:\n            return self.can_send_forward\n        else:\n            return self.can_send_backward\n\n    def cannot_send(self, is_forward_direction: bool):\n        # make info invalid after some time?\n        if is_forward_direction:\n            return self.cannot_send_forward\n        else:\n            return self.cannot_send_backward\n\n    def update_can_send(self, is_forward_direction: bool, amount: int):\n        self.hint_timestamp = int(time.time())\n        if is_forward_direction:\n            self.can_send_forward = amount\n        else:\n            self.can_send_backward = amount\n\n    def update_cannot_send(self, is_forward_direction: bool, amount: int):\n        self.hint_timestamp = int(time.time())\n        if is_forward_direction:\n            self.cannot_send_forward = amount\n        else:\n            self.cannot_send_backward = amount\n\n    def num_inflight_htlcs(self, is_forward_direction: bool) -> int:\n        if is_forward_direction:\n            return self._inflight_htlcs_forward\n        else:\n            return self._inflight_htlcs_backward\n\n    def add_htlc(self, is_forward_direction: bool):\n        if is_forward_direction:\n            self._inflight_htlcs_forward += 1\n        else:\n            self._inflight_htlcs_backward += 1\n\n    def remove_htlc(self, is_forward_direction: bool):\n        if is_forward_direction:\n            self._inflight_htlcs_forward = max(0, self._inflight_htlcs_forward - 1)\n        else:\n            self._inflight_htlcs_backward = max(0, self._inflight_htlcs_backward - 1)\n\n    def __repr__(self):\n        return f\"forward: can send: {self._can_send_forward} msat, cannot send: {self._cannot_send_forward} msat, htlcs: {self._inflight_htlcs_forward}\\n\" \\\n               f\"backward: can send: {self._can_send_backward} msat, cannot send: {self._cannot_send_backward} msat, htlcs: {self._inflight_htlcs_backward}\\n\"\n\n\nclass LiquidityHintMgr:\n    \"\"\"Implements liquidity hints for channels in the graph.\n\n    This class can be used to update liquidity information about channels in the\n    graph. Implements a penalty function for edge weighting in the pathfinding\n    algorithm that favors channels which can route payments and penalizes\n    channels that cannot.\n    \"\"\"\n    # TODO: hints based on node pairs only (shadow channels, non-strict forwarding)?\n    def __init__(self):\n        self.lock = RLock()\n        self._liquidity_hints: Dict[ShortChannelID, LiquidityHint] = {}\n\n    @with_lock\n    def get_hint(self, channel_id: ShortChannelID) -> LiquidityHint:\n        hint = self._liquidity_hints.get(channel_id)\n        if not hint:\n            hint = LiquidityHint()\n            self._liquidity_hints[channel_id] = hint\n        return hint\n\n    @with_lock\n    def update_can_send(self, node_from: bytes, node_to: bytes, channel_id: ShortChannelID, amount: int):\n        hint = self.get_hint(channel_id)\n        hint.update_can_send(node_from < node_to, amount)\n\n    @with_lock\n    def update_cannot_send(self, node_from: bytes, node_to: bytes, channel_id: ShortChannelID, amount: int):\n        hint = self.get_hint(channel_id)\n        hint.update_cannot_send(node_from < node_to, amount)\n\n    @with_lock\n    def add_htlc(self, node_from: bytes, node_to: bytes, channel_id: ShortChannelID):\n        hint = self.get_hint(channel_id)\n        hint.add_htlc(node_from < node_to)\n\n    @with_lock\n    def remove_htlc(self, node_from: bytes, node_to: bytes, channel_id: ShortChannelID):\n        hint = self.get_hint(channel_id)\n        hint.remove_htlc(node_from < node_to)\n\n    def penalty(self, node_from: bytes, node_to: bytes, channel_id: ShortChannelID, amount: int) -> float:\n        \"\"\"Gives a penalty when sending from node1 to node2 over channel_id with an\n        amount in units of millisatoshi.\n\n        The penalty depends on the can_send and cannot_send values that was\n        possibly recorded in previous payment attempts.\n\n        A channel that can send an amount is assigned a penalty of zero, a\n        channel that cannot send an amount is assigned an infinite penalty.\n        If the sending amount lies between can_send and cannot_send, there's\n        uncertainty and we give a default penalty. The default penalty\n        serves the function of giving a positive offset (the Dijkstra\n        algorithm doesn't work with negative weights), from which we can discount\n        from. There is a competition between low-fee channels and channels where\n        we know with some certainty that they can support a payment. The penalty\n        ultimately boils down to: how much more fees do we want to pay for\n        certainty of payment success? This can be tuned via DEFAULT_PENALTY_BASE_MSAT\n        and DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH. A base _and_ relative penalty\n        was chosen such that the penalty will be able to compete with the regular\n        base and relative fees.\n        \"\"\"\n        # we only evaluate hints here, so use dict get (to not create many hints with self.get_hint)\n        hint = self._liquidity_hints.get(channel_id)\n        if not hint:\n            can_send, cannot_send, num_inflight_htlcs = None, None, 0\n        else:\n            can_send = hint.can_send(node_from < node_to)\n            cannot_send = hint.cannot_send(node_from < node_to)\n            num_inflight_htlcs = hint.num_inflight_htlcs(node_from < node_to)\n\n        if cannot_send is not None and amount >= cannot_send:\n            return inf\n        if can_send is not None and amount <= can_send:\n            return 0\n        success_fee = fee_for_edge_msat(amount, DEFAULT_PENALTY_BASE_MSAT, DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH)\n        inflight_htlc_fee = num_inflight_htlcs * success_fee\n        return success_fee + inflight_htlc_fee\n\n    @with_lock\n    def reset_liquidity_hints(self):\n        for k, v in self._liquidity_hints.items():\n            v.hint_timestamp = 0\n\n    def __repr__(self):\n        string = \"liquidity hints:\\n\"\n        if self._liquidity_hints:\n            for k, v in self._liquidity_hints.items():\n                string += f\"{k}: {v}\\n\"\n        return string\n\n\nclass LNPathFinder(Logger):\n\n    def __init__(self, channel_db: ChannelDB):\n        Logger.__init__(self)\n        self.channel_db = channel_db\n        self.liquidity_hints = LiquidityHintMgr()\n        self._edge_blacklist = dict()  # type: Dict[ShortChannelID, int]  # scid -> expiration\n        self._blacklist_lock = threading.Lock()\n\n    def _is_edge_blacklisted(self, short_channel_id: ShortChannelID, *, now: int) -> bool:\n        blacklist_expiration = self._edge_blacklist.get(short_channel_id)\n        if blacklist_expiration is None:\n            return False\n        if blacklist_expiration < now:\n            return False\n            # TODO rm expired entries from cache (note: perf vs thread-safety)\n        return True\n\n    def add_edge_to_blacklist(\n        self,\n        short_channel_id: ShortChannelID,\n        *,\n        now: int = None,\n        duration: int = 3600,  # seconds\n    ) -> None:\n        if now is None:\n            now = int(time.time())\n        with self._blacklist_lock:\n            blacklist_expiration = self._edge_blacklist.get(short_channel_id, 0)\n            self._edge_blacklist[short_channel_id] = max(blacklist_expiration, now + duration)\n\n    def clear_blacklist(self):\n        with self._blacklist_lock:\n            self._edge_blacklist = dict()\n\n    def update_liquidity_hints(\n            self,\n            route: LNPaymentRoute,\n            amount_msat: int,\n            failing_channel: ShortChannelID=None\n    ):\n        # go through the route and record successes until the failing channel is reached,\n        # for the failing channel, add a cannot_send liquidity hint\n        # note: actual routable amounts are slightly different than reported here\n        # as fees would need to be added\n        for r in route:\n            if r.short_channel_id != failing_channel:\n                self.logger.info(f\"report {r.short_channel_id} to be able to forward {amount_msat} msat\")\n                self.liquidity_hints.update_can_send(r.start_node, r.end_node, r.short_channel_id, amount_msat)\n            else:\n                self.logger.info(f\"report {r.short_channel_id} to be unable to forward {amount_msat} msat\")\n                self.liquidity_hints.update_cannot_send(r.start_node, r.end_node, r.short_channel_id, amount_msat)\n                break\n        else:\n            assert failing_channel is None\n\n    def update_inflight_htlcs(self, route: LNPaymentRoute, add_htlcs: bool):\n        self.logger.info(f\"{'Adding' if add_htlcs else 'Removing'} inflight htlcs to graph (liquidity hints).\")\n        for r in route:\n            if add_htlcs:\n                self.liquidity_hints.add_htlc(r.start_node, r.end_node, r.short_channel_id)\n            else:\n                self.liquidity_hints.remove_htlc(r.start_node, r.end_node, r.short_channel_id)\n\n    def _edge_cost(\n            self,\n            *,\n            short_channel_id: ShortChannelID,\n            start_node: bytes,\n            end_node: bytes,\n            payment_amt_msat: int,\n            ignore_costs=False,\n            is_mine=False,\n            my_channels: Dict[ShortChannelID, 'Channel'] = None,\n            private_route_edges: Dict[ShortChannelID, RouteEdge] = None,\n            now: int,  # unix ts\n    ) -> Tuple[float, int]:\n        \"\"\"Heuristic cost (distance metric) of going through a channel.\n        Returns (heuristic_cost, fee_for_edge_msat).\n        \"\"\"\n        if self._is_edge_blacklisted(short_channel_id, now=now):\n            return float('inf'), 0\n        if private_route_edges is None:\n            private_route_edges = {}\n        channel_info = self.channel_db.get_channel_info(\n            short_channel_id, my_channels=my_channels, private_route_edges=private_route_edges)\n        if channel_info is None:\n            return float('inf'), 0\n        channel_policy = self.channel_db.get_policy_for_node(\n            short_channel_id, start_node, my_channels=my_channels, private_route_edges=private_route_edges, now=now)\n        if channel_policy is None:\n            return float('inf'), 0\n        # channels that did not publish both policies often return temporary channel failure\n        channel_policy_backwards = self.channel_db.get_policy_for_node(\n            short_channel_id, end_node, my_channels=my_channels, private_route_edges=private_route_edges, now=now)\n        if (channel_policy_backwards is None\n                and not is_mine\n                and short_channel_id not in private_route_edges):\n            return float('inf'), 0\n        if channel_policy.is_disabled():\n            return float('inf'), 0\n        if payment_amt_msat < channel_policy.htlc_minimum_msat:\n            return float('inf'), 0  # payment amount too little\n        if channel_info.capacity_sat is not None and \\\n                payment_amt_msat // 1000 > channel_info.capacity_sat:\n            return float('inf'), 0  # payment amount too large\n        if channel_policy.htlc_maximum_msat is not None and \\\n                payment_amt_msat > channel_policy.htlc_maximum_msat:\n            return float('inf'), 0  # payment amount too large\n        route_edge = private_route_edges.get(short_channel_id, None)\n        if route_edge is None:\n            node_info = self.channel_db.get_node_info_for_node_id(node_id=end_node)\n            if node_info:\n                # it's ok if we are missing the node_announcement (node_info) for this node,\n                # but if we have it, we enforce that they support var_onion_optin\n                node_features = LnFeatures(node_info.features)\n                if not node_features.supports(LnFeatures.VAR_ONION_OPT):  # note: this is kind of slow. could be cached.\n                    return float('inf'), 0\n            route_edge = RouteEdge.from_channel_policy(\n                channel_policy=channel_policy,\n                short_channel_id=short_channel_id,\n                start_node=start_node,\n                end_node=end_node,\n                node_info=node_info)\n        # Cap cltv of any given edge at 2 weeks (the cost function would not work well for extreme cases)\n        if route_edge.cltv_delta > 14 * 144:\n            return float('inf'), 0\n        # Distance metric notes:  # TODO constants are ad-hoc\n        # ( somewhat based on https://github.com/lightningnetwork/lnd/pull/1358 )\n        # - Edges have a base cost. (more edges -> less likely none will fail)\n        # - The larger the payment amount, and the longer the CLTV,\n        #   the more irritating it is if the HTLC gets stuck.\n        # - Paying lower fees is better. :)\n        if ignore_costs:\n            return DEFAULT_PENALTY_BASE_MSAT, 0\n        fee_msat = route_edge.fee_for_edge(payment_amt_msat)\n        cltv_cost = route_edge.cltv_delta * payment_amt_msat * 15 / 1_000_000_000\n        # the liquidty penalty takes care we favor edges that should be able to forward\n        # the payment and penalize edges that cannot\n        liquidity_penalty = self.liquidity_hints.penalty(start_node, end_node, short_channel_id, payment_amt_msat)\n        overall_cost = fee_msat + cltv_cost + liquidity_penalty\n        return overall_cost, fee_msat\n\n    def get_shortest_path_hops(\n            self,\n            *,\n            nodeA: bytes, # nodeA is expected to be our node id if channels are passed in my_sending_channels\n            nodeB: bytes,\n            invoice_amount_msat: int,\n            my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,\n            private_route_edges: Dict[ShortChannelID, RouteEdge] = None,\n            node_filter: Optional[Callable[[bytes, NodeInfo], bool]] = None\n    ) -> Dict[bytes, PathEdge]:\n        # note: we don't lock self.channel_db, so while the path finding runs,\n        #       the underlying graph could potentially change... (not good but maybe ~OK?)\n\n        # if destination is filtered, there is no route\n        if node_filter:\n            node_info = self.channel_db.get_node_info_for_node_id(nodeB)\n            if not node_filter(nodeB, node_info):\n                return {}\n\n        # run Dijkstra\n        # The search is run in the REVERSE direction, from nodeB to nodeA,\n        # to properly calculate compound routing fees.\n        distance_from_start = defaultdict(lambda: float('inf'))\n        distance_from_start[nodeB] = 0\n        previous_hops = {}  # type: Dict[bytes, PathEdge]\n        nodes_to_explore = queue.PriorityQueue()\n        nodes_to_explore.put((0, invoice_amount_msat, nodeB))  # order of fields (in tuple) matters!\n        now = int(time.time())\n\n        # main loop of search\n        while nodes_to_explore.qsize() > 0:\n            dist_to_edge_endnode, amount_msat, edge_endnode = nodes_to_explore.get()\n            if edge_endnode == nodeA and previous_hops:  # previous_hops check for circular paths\n                self.logger.info(\"found a path\")\n                break\n            if dist_to_edge_endnode != distance_from_start[edge_endnode]:\n                # queue.PriorityQueue does not implement decrease_priority,\n                # so instead of decreasing priorities, we add items again into the queue.\n                # so there are duplicates in the queue, that we discard now:\n                continue\n\n            if nodeA == nodeB:  # we want circular paths\n                if not previous_hops:  # in the first node exploration step, we only take receiving channels\n                    channels_for_endnode = self.channel_db.get_channels_for_node(\n                        edge_endnode, my_channels={}, private_route_edges=private_route_edges)\n                else:  # in the next steps, we only take sending channels\n                    channels_for_endnode = self.channel_db.get_channels_for_node(\n                        edge_endnode, my_channels=my_sending_channels, private_route_edges={})\n            else:\n                channels_for_endnode = self.channel_db.get_channels_for_node(\n                    edge_endnode, my_channels=my_sending_channels, private_route_edges=private_route_edges)\n\n            for edge_channel_id in channels_for_endnode:\n                assert isinstance(edge_channel_id, bytes)\n                if self._is_edge_blacklisted(edge_channel_id, now=now):\n                    continue\n                channel_info = self.channel_db.get_channel_info(\n                    edge_channel_id, my_channels=my_sending_channels, private_route_edges=private_route_edges)\n                if channel_info is None:\n                    continue\n                edge_startnode = channel_info.node2_id if channel_info.node1_id == edge_endnode else channel_info.node1_id\n                if node_filter:\n                    node_info = self.channel_db.get_node_info_for_node_id(edge_startnode)\n                    if not node_filter(edge_startnode, node_info):\n                        continue\n                is_mine = edge_channel_id in my_sending_channels\n                if edge_startnode == nodeA and my_sending_channels:  # payment outgoing, on our channel\n                    if edge_channel_id not in my_sending_channels:\n                        continue\n                    if not my_sending_channels[edge_channel_id].can_pay(amount_msat, check_frozen=True):\n                        continue\n                edge_cost, fee_for_edge_msat = self._edge_cost(\n                    short_channel_id=edge_channel_id,\n                    start_node=edge_startnode,\n                    end_node=edge_endnode,\n                    payment_amt_msat=amount_msat,\n                    ignore_costs=(edge_startnode == nodeA),\n                    is_mine=is_mine,\n                    my_channels=my_sending_channels,\n                    private_route_edges=private_route_edges,\n                    now=now,\n                )\n                alt_dist_to_neighbour = distance_from_start[edge_endnode] + edge_cost\n                if alt_dist_to_neighbour < distance_from_start[edge_startnode]:\n                    distance_from_start[edge_startnode] = alt_dist_to_neighbour\n                    previous_hops[edge_startnode] = PathEdge(\n                        start_node=edge_startnode,\n                        end_node=edge_endnode,\n                        short_channel_id=ShortChannelID(edge_channel_id))\n                    amount_to_forward_msat = amount_msat + fee_for_edge_msat\n                    nodes_to_explore.put((alt_dist_to_neighbour, amount_to_forward_msat, edge_startnode))\n            # for circular paths, we already explored the end node, but this\n            # is also our start node, so set it to unexplored\n            if edge_endnode == nodeB and nodeA == nodeB:\n                distance_from_start[edge_endnode] = float('inf')\n        return previous_hops\n\n    @profiler\n    def find_path_for_payment(\n            self,\n            *,\n            nodeA: bytes,\n            nodeB: bytes,\n            invoice_amount_msat: int,\n            my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,\n            private_route_edges: Dict[ShortChannelID, RouteEdge] = None,\n            node_filter: Optional[Callable[[bytes, NodeInfo], bool]] = None\n    ) -> Optional[LNPaymentPath]:\n        \"\"\"Return a path from nodeA to nodeB.\"\"\"\n        assert type(nodeA) is bytes\n        assert type(nodeB) is bytes\n        assert type(invoice_amount_msat) is int\n        if my_sending_channels is None:\n            my_sending_channels = {}\n\n        previous_hops = self.get_shortest_path_hops(\n            nodeA=nodeA,\n            nodeB=nodeB,\n            invoice_amount_msat=invoice_amount_msat,\n            my_sending_channels=my_sending_channels,\n            private_route_edges=private_route_edges,\n            node_filter=node_filter)\n\n        if nodeA not in previous_hops:\n            return None  # no path found\n\n        # backtrack from search_end (nodeA) to search_start (nodeB)\n        # FIXME paths cannot be longer than 20 edges (onion packet)...\n        edge_startnode = nodeA\n        path = []\n        while edge_startnode != nodeB or not path:  # second condition for circular paths\n            edge = previous_hops[edge_startnode]\n            path += [edge]\n            edge_startnode = edge.node_id\n        return path\n\n    def create_route_from_path(\n            self,\n            path: Optional[LNPaymentPath],\n            *,\n            my_channels: Dict[ShortChannelID, 'Channel'] = None,\n            private_route_edges: Dict[ShortChannelID, RouteEdge] = None,\n    ) -> LNPaymentRoute:\n        if path is None:\n            raise Exception('cannot create route from None path')\n        if private_route_edges is None:\n            private_route_edges = {}\n        route = []\n        prev_end_node = path[0].start_node\n        for path_edge in path:\n            short_channel_id = path_edge.short_channel_id\n            _endnodes = self.channel_db.get_endnodes_for_chan(short_channel_id, my_channels=my_channels)\n            if _endnodes and sorted(_endnodes) != sorted([path_edge.start_node, path_edge.end_node]):\n                raise LNPathInconsistent(\"endpoints of edge inconsistent with short_channel_id\")\n            if path_edge.start_node != prev_end_node:\n                raise LNPathInconsistent(\"edges do not chain together\")\n            route_edge = private_route_edges.get(short_channel_id, None)\n            if route_edge is None:\n                channel_policy = self.channel_db.get_policy_for_node(\n                    short_channel_id=short_channel_id,\n                    node_id=path_edge.start_node,\n                    my_channels=my_channels)\n                if channel_policy is None:\n                    raise NoChannelPolicy(short_channel_id)\n                node_info = self.channel_db.get_node_info_for_node_id(node_id=path_edge.end_node)\n                route_edge = RouteEdge.from_channel_policy(\n                    channel_policy=channel_policy,\n                    short_channel_id=short_channel_id,\n                    start_node=path_edge.start_node,\n                    end_node=path_edge.end_node,\n                    node_info=node_info)\n            route.append(route_edge)\n            prev_end_node = path_edge.end_node\n        return route\n\n    def find_route(\n            self,\n            *,\n            nodeA: bytes,\n            nodeB: bytes,\n            invoice_amount_msat: int,\n            path: Optional[Sequence[PathEdge]] = None,\n            my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,\n            private_route_edges: Dict[ShortChannelID, RouteEdge] = None,\n    ) -> Optional[LNPaymentRoute]:\n        route = None\n        if not path:\n            path = self.find_path_for_payment(\n                nodeA=nodeA,\n                nodeB=nodeB,\n                invoice_amount_msat=invoice_amount_msat,\n                my_sending_channels=my_sending_channels,\n                private_route_edges=private_route_edges)\n        if path:\n            route = self.create_route_from_path(\n                path, my_channels=my_sending_channels, private_route_edges=private_route_edges)\n        return route\n"
  },
  {
    "path": "electrum/lnsweep.py",
    "content": "# Copyright (C) 2018 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nfrom typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Callable\n\nimport electrum_ecc as ecc\n\nfrom .util import bfh, UneconomicFee\nfrom .crypto import privkey_to_pubkey\nfrom .bitcoin import redeem_script_to_address, construct_witness\nfrom . import descriptor\nfrom . import bitcoin\n\nfrom .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script,\n                     derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey,\n                     make_htlc_tx_witness, make_htlc_tx_with_open_channel, UpdateAddHtlc,\n                     LOCAL, REMOTE, make_htlc_output_witness_script,\n                     get_ordered_channel_configs, get_per_commitment_secret_from_seed,\n                     RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED,\n                     map_htlcs_to_ctx_output_idxs, Direction, make_commitment_output_to_remote_witness_script,\n                     derive_payment_basepoint, ctx_has_anchors, SCRIPT_TEMPLATE_FUNDING, Keypair,\n                     derive_multisig_funding_key_if_we_opened, derive_multisig_funding_key_if_they_opened)\nfrom .transaction import (Transaction, TxInput, PartialTxInput,\n                          PartialTxOutput, TxOutpoint, script_GetOp, match_script_against_template)\nfrom .logging import get_logger, Logger\n\nif TYPE_CHECKING:\n    from .lnchannel import Channel, AbstractChannel, ChannelBackup\n\n\n_logger = get_logger(__name__)\n# note: better to use chan.logger instead, when applicable\n\nHTLC_TRANSACTION_DEADLINE_FRACTION = 4\nHTLC_TRANSACTION_SWEEP_TARGET = 10\nHTLCTX_INPUT_OUTPUT_INDEX = 0\n\n\nclass SweepInfo(NamedTuple):\n    name: str\n    cltv_abs: Optional[int] # set to None only if the script has no cltv\n    # TODO add asserts that cltv_abs is block-based (see NLOCKTIME_BLOCKHEIGHT_MAX)\n    txin: PartialTxInput\n    txout: Optional[PartialTxOutput]  # only for first-stage htlc tx\n    can_be_batched: bool # todo: this could be more fine-grained\n    dust_override: bool\n\n    def is_anchor(self):\n        return self.name in ['local_anchor', 'remote_anchor']\n\n    @property\n    def csv_delay(self):\n        return self.txin.get_block_based_relative_locktime() or 0\n\n\nclass KeepWatchingTXO(NamedTuple):\n    \"\"\"Used for UTXOs we don't yet know if we want to sweep, such as pending HTLCs for JIT channels.\"\"\"\n    name: str\n    until_height: int\n\n\nMaybeSweepInfo = SweepInfo | KeepWatchingTXO\n\n\ndef sweep_their_ctx_watchtower(\n        chan: 'Channel',\n        ctx: Transaction,\n        per_commitment_secret: bytes\n) -> List[PartialTxInput]:\n    \"\"\"Presign sweeping transactions using the just received revoked pcs.\n    These will only be utilised if the remote breaches.\n    Sweep 'to_local', and all the HTLCs (two cases: directly from ctx, or from HTLC tx).\n    \"\"\"\n    # prep\n    ctn = extract_ctn_from_tx_and_chan(ctx, chan)\n    pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)\n    breacher_conf, watcher_conf = get_ordered_channel_configs(chan=chan, for_us=False)\n    watcher_revocation_privkey = derive_blinded_privkey(\n        watcher_conf.revocation_basepoint.privkey,\n        per_commitment_secret\n    )\n    to_self_delay = watcher_conf.to_self_delay\n    breacher_delayed_pubkey = derive_pubkey(breacher_conf.delayed_basepoint.pubkey, pcp)\n    txins = []\n    # create justice tx for breacher's to_local output\n    revocation_pubkey = ecc.ECPrivkey(watcher_revocation_privkey).get_public_key_bytes(compressed=True)\n    witness_script = make_commitment_output_to_local_witness_script(\n        revocation_pubkey, to_self_delay, breacher_delayed_pubkey)\n    to_local_address = redeem_script_to_address('p2wsh', witness_script)\n    output_idxs = ctx.get_output_idxs_from_address(to_local_address)\n    if output_idxs:\n        output_idx = output_idxs.pop()\n        txin = sweep_ctx_to_local(\n            ctx=ctx,\n            output_idx=output_idx,\n            witness_script=witness_script,\n            privkey=watcher_revocation_privkey,\n            is_revocation=True,\n        )\n        if txin:\n            txins.append(txin)\n\n    # create justice txs for breacher's HTLC outputs\n    breacher_htlc_pubkey = derive_pubkey(breacher_conf.htlc_basepoint.pubkey, pcp)\n    watcher_htlc_pubkey = derive_pubkey(watcher_conf.htlc_basepoint.pubkey, pcp)\n    def txin_htlc(\n            htlc: 'UpdateAddHtlc', is_received_htlc: bool,\n            ctx_output_idx: int) -> None:\n        htlc_output_witness_script = make_htlc_output_witness_script(\n            is_received_htlc=is_received_htlc,\n            remote_revocation_pubkey=revocation_pubkey,\n            remote_htlc_pubkey=watcher_htlc_pubkey,\n            local_htlc_pubkey=breacher_htlc_pubkey,\n            payment_hash=htlc.payment_hash,\n            cltv_abs=htlc.cltv_abs,\n            has_anchors=chan.has_anchors()\n        )\n        cltv_abs = htlc.cltv_abs if is_received_htlc else 0\n        return sweep_their_ctx_htlc(\n            ctx=ctx,\n            witness_script=htlc_output_witness_script,\n            preimage=None,\n            output_idx=ctx_output_idx,\n            privkey=watcher_revocation_privkey,\n            is_revocation=True,\n            cltv_abs=cltv_abs,\n            has_anchors=chan.has_anchors()\n        )\n    htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(\n        chan=chan,\n        ctx=ctx,\n        pcp=pcp,\n        subject=REMOTE,\n        ctn=ctn)\n    for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():\n        txins.append(\n            txin_htlc(\n                htlc=htlc,\n                is_received_htlc=direction == RECEIVED,\n                ctx_output_idx=ctx_output_idx)\n        )\n    # for anchor channels we don't know the HTLC transaction's txid beforehand due\n    # to malleability because of ANYONECANPAY\n    if chan.has_anchors():\n        return txins\n\n    # create justice transactions for HTLC transaction's outputs\n    def sweep_their_htlctx_justice(\n            *,\n            htlc: 'UpdateAddHtlc',\n            htlc_direction: Direction,\n            ctx_output_idx: int\n    ) -> Optional[PartialTxInput]:\n        htlc_tx_witness_script, htlc_tx = make_htlc_tx_with_open_channel(\n            chan=chan,\n            pcp=pcp,\n            subject=REMOTE,\n            ctn=ctn,\n            htlc_direction=htlc_direction,\n            commit=ctx,\n            htlc=htlc,\n            ctx_output_idx=ctx_output_idx)\n        return sweep_htlctx_output(\n            htlc_tx=htlc_tx,\n            output_idx=HTLCTX_INPUT_OUTPUT_INDEX,\n            htlctx_witness_script=htlc_tx_witness_script,\n            privkey=watcher_revocation_privkey,\n            is_revocation=True,\n        )\n\n    htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(\n        chan=chan,\n        ctx=ctx,\n        pcp=pcp,\n        subject=REMOTE,\n        ctn=ctn)\n    for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():\n        secondstage_sweep_tx = sweep_their_htlctx_justice(\n            htlc=htlc,\n            htlc_direction=direction,\n            ctx_output_idx=ctx_output_idx)\n        if secondstage_sweep_tx:\n            txins.append(secondstage_sweep_tx)\n    return txins\n\n\ndef sweep_their_ctx_justice(\n        chan: 'Channel',\n        ctx: Transaction,\n        per_commitment_secret: bytes,\n) -> Optional[PartialTxInput]:\n    # prep\n    pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)\n    this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=False)\n    other_revocation_privkey = derive_blinded_privkey(other_conf.revocation_basepoint.privkey,\n                                                      per_commitment_secret)\n    to_self_delay = other_conf.to_self_delay\n    this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, pcp)\n\n    # to_local\n    revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True)\n    witness_script = make_commitment_output_to_local_witness_script(\n        revocation_pubkey, to_self_delay, this_delayed_pubkey)\n    to_local_address = redeem_script_to_address('p2wsh', witness_script)\n    output_idxs = ctx.get_output_idxs_from_address(to_local_address)\n    if output_idxs:\n        output_idx = output_idxs.pop()\n        sweep_txin = sweep_ctx_to_local(\n            ctx=ctx,\n            output_idx=output_idx,\n            witness_script=witness_script,\n            privkey=other_revocation_privkey,\n            is_revocation=True,\n        )\n        return sweep_txin\n    return None\n\n\ndef sweep_their_htlctx_justice(\n        chan: 'Channel',\n        ctx: Transaction,\n        htlc_tx: Transaction,\n) -> Dict[str, SweepInfo]:\n    \"\"\"Creates justice transactions for every output in the HTLC transaction.\n    Due to anchor type channels it can happen that a remote party batches HTLC transactions,\n    which is why this method can return multiple SweepInfos.\n    \"\"\"\n    x = extract_ctx_secrets(chan, ctx)\n    if not x:\n        return {}\n    ctn, their_pcp, is_revocation, per_commitment_secret = x\n    if not is_revocation:\n        return {}\n\n    # get HTLC constraints (secrets and locktime)\n    pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)\n    this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=False)\n    other_revocation_privkey = derive_blinded_privkey(\n        other_conf.revocation_basepoint.privkey,\n        per_commitment_secret)\n    to_self_delay = other_conf.to_self_delay\n    this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, pcp)\n    revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True)\n    # uses the same witness script as to_local\n    witness_script = make_commitment_output_to_local_witness_script(\n        revocation_pubkey, to_self_delay, this_delayed_pubkey)\n    htlc_address = redeem_script_to_address('p2wsh', witness_script)\n    # check that htlc transaction contains at least an output that is supposed to be\n    # spent via a second stage htlc transaction\n    htlc_outputs_idxs = [idx for idx, output in enumerate(htlc_tx.outputs()) if output.address == htlc_address]\n    if not htlc_outputs_idxs:\n        return {}\n\n    # generate justice transactions\n    def justice_txin(output_idx):\n        return sweep_htlctx_output(\n            output_idx=output_idx,\n            htlc_tx=htlc_tx,\n            htlctx_witness_script=witness_script,\n            privkey=other_revocation_privkey,\n            is_revocation=True,\n        )\n    index_to_sweepinfo = {}\n    for output_idx in htlc_outputs_idxs:\n        if txin := justice_txin(output_idx):\n            prevout = htlc_tx.txid() + f':{output_idx}'\n            index_to_sweepinfo[prevout] = SweepInfo(\n                name=f'second-stage-htlc:{output_idx}',\n                cltv_abs=None,\n                txin=txin,\n                txout=None,\n                can_be_batched=False,\n                dust_override=False,\n            )\n    return index_to_sweepinfo\n\n\ndef sweep_our_htlctx(\n        chan: 'AbstractChannel',\n        ctx: Transaction,\n        htlc_tx: Transaction):\n    txs = sweep_our_ctx(\n        chan=chan,\n        ctx=ctx,\n        actual_htlc_tx=htlc_tx)\n    return txs\n\n\ndef sweep_our_ctx(\n        *, chan: 'AbstractChannel',\n        ctx: Transaction,\n        actual_htlc_tx: Transaction=None, # if passed, return second stage htlcs\n) -> Dict[str, MaybeSweepInfo]:\n\n    \"\"\"Handle the case where we force-close unilaterally with our latest ctx.\n\n    We sweep:\n        to_local: CSV delayed\n        htlc success: CSV delay with anchors, no delay otherwise\n        htlc timeout: CSV delay with anchors, CLTV locktime\n        second-stage htlc transactions: CSV delay\n\n    'to_local' can be swept even if this is a breach (by us),\n    but HTLCs cannot (old HTLCs are no longer stored).\n\n    Outputs with CSV/CLTV are redeemed by LNWatcher.\n    \"\"\"\n    ctn = extract_ctn_from_tx_and_chan(ctx, chan)\n    our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)\n    our_per_commitment_secret = get_per_commitment_secret_from_seed(\n        our_conf.per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)\n    our_pcp = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True)\n    our_delayed_bp_privkey = ecc.ECPrivkey(our_conf.delayed_basepoint.privkey)\n    our_localdelayed_privkey = derive_privkey(our_delayed_bp_privkey.secret_scalar, our_pcp)\n    our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey)\n    their_revocation_pubkey = derive_blinded_pubkey(their_conf.revocation_basepoint.pubkey, our_pcp)\n    to_self_delay = their_conf.to_self_delay\n    our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'),\n                                       per_commitment_point=our_pcp).to_bytes(32, 'big')\n    our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True)\n    to_local_witness_script = make_commitment_output_to_local_witness_script(\n        their_revocation_pubkey, to_self_delay, our_localdelayed_pubkey)\n    to_local_address = redeem_script_to_address('p2wsh', to_local_witness_script)\n    to_remote_address = None\n    # test if this is our_ctx\n    found_to_local = bool(ctx.get_output_idxs_from_address(to_local_address))\n    if not chan.is_backup():\n        assert chan.is_static_remotekey_enabled()\n        their_payment_pubkey = their_conf.payment_basepoint.pubkey\n        to_remote_address = make_commitment_output_to_remote_address(their_payment_pubkey, has_anchors=chan.has_anchors())\n        found_to_remote = bool(ctx.get_output_idxs_from_address(to_remote_address))\n    else:\n        found_to_remote = False\n    if not found_to_local and not found_to_remote:\n        return {}\n    #chan.logger.debug(f'(lnsweep) found our ctx: {to_local_address} {to_remote_address}')\n    # other outputs are htlcs\n    # if they are spent, we need to generate the script\n    # so, second-stage htlc sweep should not be returned here\n    txs = {}  # type: Dict[str, MaybeSweepInfo]\n\n    # local anchor\n    if actual_htlc_tx is None and chan.has_anchors():\n        if txin := sweep_ctx_anchor(ctx=ctx, multisig_key=our_conf.multisig_key):\n            txs[txin.prevout.to_str()] = SweepInfo(\n                name='local_anchor',\n                cltv_abs=None,\n                txin=txin,\n                txout=None,\n                can_be_batched=True,\n                dust_override=True,\n            )\n\n    # to_local\n    output_idxs = ctx.get_output_idxs_from_address(to_local_address)\n    if actual_htlc_tx is None and output_idxs:\n        output_idx = output_idxs.pop()\n        if txin := sweep_ctx_to_local(\n                ctx=ctx,\n                output_idx=output_idx,\n                witness_script=to_local_witness_script,\n                privkey=our_localdelayed_privkey.get_secret_bytes(),\n                is_revocation=False,\n                to_self_delay=to_self_delay,\n        ):\n            prevout = ctx.txid() + ':%d'%output_idx\n            txs[prevout] = SweepInfo(\n                name='our_ctx_to_local',\n                cltv_abs=None,\n                txin=txin,\n                txout=None,\n                can_be_batched=True,\n                dust_override=False,\n            )\n    we_breached = ctn < chan.get_oldest_unrevoked_ctn(LOCAL)\n    if we_breached:\n        chan.logger.info(f\"(lnsweep) we breached. txid: {ctx.txid()}\")\n        # return only our_ctx_to_local, because we don't keep htlc_signatures for old states\n        return txs\n\n    # HTLCs\n    def txs_htlc(\n            *, htlc: 'UpdateAddHtlc',\n            htlc_direction: Direction,\n            ctx_output_idx: int,\n            htlc_relative_idx,\n            preimage: Optional[bytes]):\n\n        htlctx_witness_script, htlc_tx = tx_our_ctx_htlctx(\n            chan=chan,\n            our_pcp=our_pcp,\n            ctx=ctx,\n            htlc=htlc,\n            local_htlc_privkey=our_htlc_privkey,\n            preimage=preimage,\n            htlc_direction=htlc_direction,\n            ctx_output_idx=ctx_output_idx,\n            htlc_relative_idx=htlc_relative_idx)\n\n        if actual_htlc_tx is None:\n            name = 'offered-htlc' if htlc_direction == SENT else 'received-htlc'\n            prevout = ctx.txid() + f':{ctx_output_idx}'\n            txs[prevout] = SweepInfo(\n                name=name,\n                cltv_abs=htlc_tx.locktime,\n                txin=htlc_tx.inputs()[0],\n                txout=htlc_tx.outputs()[0],\n                can_be_batched=False,  # both parties can spend\n                # - actually, we might want to batch depending on the context\n                #   f(amount in htlc, remaining_time, number of available utxos for anchors)\n                #   - in particular, it would be safe to batch htlcs where\n                #        htlc_direction, htlc.payment_hash, htlc.cltv_abs\n                #     all match. That is, MPP htlcs for the same payment.\n                dust_override=False,\n            )\n        else:\n            # second-stage\n            address = bitcoin.script_to_p2wsh(htlctx_witness_script)\n            output_idxs = actual_htlc_tx.get_output_idxs_from_address(address)\n            for output_idx in output_idxs:\n                if sweep_txin := sweep_htlctx_output(\n                        to_self_delay=to_self_delay,\n                        htlc_tx=actual_htlc_tx,\n                        output_idx=output_idx,\n                        htlctx_witness_script=htlctx_witness_script,\n                        privkey=our_localdelayed_privkey.get_secret_bytes(),\n                        is_revocation=False,\n                ):\n                    prevout = actual_htlc_tx.txid() + f':{output_idx}'\n                    txs[prevout] = SweepInfo(\n                        name=f'second-stage-htlc:{output_idx}',\n                        cltv_abs=0,\n                        txin=sweep_txin,\n                        txout=None,\n                        # this is safe to batch, we are the only ones who can spend\n                        # (assuming we did not broadcast a revoked state)\n                        can_be_batched=True,\n                        dust_override=False,\n                    )\n\n    # offered HTLCs, in our ctx --> \"timeout\"\n    # received HTLCs, in our ctx --> \"success\"\n    htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(\n        chan=chan,\n        ctx=ctx,\n        pcp=our_pcp,\n        subject=LOCAL,\n        ctn=ctn)\n    for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():\n        preimage = None\n        if direction == RECEIVED:\n            # note: it is the first stage (witness of htlc_tx) that reveals the preimage,\n            #       so if we are already in second stage, it is already revealed.\n            #       However, here, we don't make a distinction.\n            preimage, keep_watching_txo = _maybe_reveal_preimage_for_htlc(\n                chan=chan, htlc=htlc,\n                sweep_info_name=f\"our_ctx_htlc_{ctx_output_idx}\",\n            )\n            if keep_watching_txo:\n                prevout = ctx.txid() + ':%d' % ctx_output_idx\n                txs[prevout] = keep_watching_txo\n            if not preimage:\n                continue\n        try:\n            txs_htlc(\n                htlc=htlc,\n                htlc_direction=direction,\n                ctx_output_idx=ctx_output_idx,\n                htlc_relative_idx=htlc_relative_idx,\n                preimage=preimage)\n        except UneconomicFee:\n            continue\n    return txs\n\n\ndef _maybe_reveal_preimage_for_htlc(\n    *,\n    chan: 'AbstractChannel',\n    htlc: 'UpdateAddHtlc',\n    sweep_info_name: str,\n) -> Tuple[Optional[bytes], Optional[KeepWatchingTXO]]:\n    \"\"\"Given a Remote-added-HTLC, return the preimage if it's okay to reveal it on-chain.\n\n    note: to be safe, even if we don't/can't reveal the preimage now, we should tell lnwatcher to\n          keep watching this HTLC at least until its CLTV, in case circumstances change.\n    \"\"\"\n    if not chan.lnworker.is_preimage_public(htlc.payment_hash) and not chan.lnworker.is_complete_mpp(htlc.payment_hash):\n        # - do not redeem this, it might publish the preimage of an incomplete MPP\n        # - OTOH maybe this chan just got closed, and we are still receiving new htlcs\n        #   for this MPP set. So the MPP set might still transition to complete!\n        #   The MPP_TIMEOUT is only around 2 minutes, so this window is short.\n        #   The default keep_watching logic in lnwatcher is sufficient to call us again.\n        keep_watching_txo = KeepWatchingTXO(\n            name=sweep_info_name + \"_preimage_not_public\",\n            until_height=htlc.cltv_abs,\n        )\n        return None, keep_watching_txo\n    if htlc.payment_hash.hex() in chan.lnworker.dont_settle_htlcs:\n        # we should not reveal the preimage *for now*, but we might still decide to reveal it later\n        keep_watching_txo = KeepWatchingTXO(\n            name=sweep_info_name + \"_dont_settle_htlcs\",\n            until_height=htlc.cltv_abs,\n        )\n        return None, keep_watching_txo\n    preimage = chan.lnworker.get_preimage(htlc.payment_hash)\n    if preimage is None:\n        keep_watching_txo = KeepWatchingTXO(\n            name=sweep_info_name + \"_preimage_missing\",\n            until_height=htlc.cltv_abs,\n        )\n        return None, keep_watching_txo\n    # this preimage will be revealed\n    assert preimage\n    chan.lnworker.save_preimage(htlc.payment_hash, preimage, mark_as_public=True)\n    return preimage, None\n\n\ndef extract_ctx_secrets(chan: 'Channel', ctx: Transaction):\n    # note: the remote sometimes has two valid non-revoked commitment transactions,\n    # either of which could be broadcast\n    our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)\n    ctn = extract_ctn_from_tx_and_chan(ctx, chan)\n    per_commitment_secret = None\n    oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)\n    if ctn == oldest_unrevoked_remote_ctn:\n        their_pcp = their_conf.current_per_commitment_point\n        is_revocation = False\n    elif ctn == oldest_unrevoked_remote_ctn + 1:\n        their_pcp = their_conf.next_per_commitment_point\n        is_revocation = False\n    elif ctn < oldest_unrevoked_remote_ctn:  # breach\n        try:\n            per_commitment_secret = chan.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)\n        except UnableToDeriveSecret:\n            return\n        their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)\n        is_revocation = True\n        #chan.logger.debug(f'(lnsweep) tx for revoked: {list(txs.keys())}')\n    elif chan.get_data_loss_protect_remote_pcp(ctn):\n        their_pcp = chan.get_data_loss_protect_remote_pcp(ctn)\n        is_revocation = False\n    else:\n        return\n    return ctn, their_pcp, is_revocation, per_commitment_secret\n\n\ndef extract_funding_pubkeys_from_ctx(txin: TxInput) -> Tuple[bytes, bytes]:\n    \"\"\"Extract the two funding pubkeys from the published commitment transaction.\n\n    We expect to see a witness script of: OP_2 pk1 pk2 OP_2 OP_CHECKMULTISIG\"\"\"\n    elements = txin.witness_elements()\n    witness_script = elements[-1]\n    assert match_script_against_template(witness_script, SCRIPT_TEMPLATE_FUNDING)\n    parsed_script = [x for x in script_GetOp(witness_script)]\n    pubkey1 = parsed_script[1][1]\n    pubkey2 = parsed_script[2][1]\n    return (pubkey1, pubkey2)\n\n\ndef sweep_their_ctx_to_remote_backup(\n        *, chan: 'ChannelBackup',\n        ctx: Transaction,\n        funding_tx: Transaction,\n) -> Optional[Dict[str, SweepInfo]]:\n    txs = {}  # type: Dict[str, SweepInfo]\n    \"\"\"If we only have a backup, and the remote force-closed with their ctx,\n    and anchors are enabled, we need to sweep to_remote.\"\"\"\n\n    if ctx_has_anchors(ctx):\n        # for anchors we need to sweep to_remote\n        funding_pubkeys = extract_funding_pubkeys_from_ctx(ctx.inputs()[0])\n        _logger.debug(f'checking their ctx for funding pubkeys: {[pk.hex() for pk in funding_pubkeys]}')\n        # check which of the pubkey was ours\n        for fp_idx, pubkey in enumerate(funding_pubkeys):\n            candidate_basepoint = derive_payment_basepoint(chan.lnworker.static_payment_key.privkey, funding_pubkey=pubkey)\n            candidate_to_remote_address = make_commitment_output_to_remote_address(candidate_basepoint.pubkey, has_anchors=True)\n            if ctx.get_output_idxs_from_address(candidate_to_remote_address):\n                our_payment_pubkey = candidate_basepoint\n                to_remote_address = candidate_to_remote_address\n                _logger.debug(f'found funding pubkey')\n                break\n        else:\n            return\n    else:\n        # we are dealing with static_remotekey which is locked to a wallet address\n        return {}\n\n    # remote anchor\n    # derive funding_privkey (\"multisig_key\")\n    # note: for imported backups, we already have this as 'local_config.multisig_key'\n    #       but for on-chain backups, we need to derive it.\n    #       For symmetry, we derive it now regardless of type\n    our_funding_pubkey = funding_pubkeys[fp_idx]\n    their_funding_pubkey = funding_pubkeys[1 - fp_idx]\n    remote_node_id = chan.node_id  # for onchain backups, this is only the prefix\n    if chan.is_initiator():\n        funding_kp_cand = derive_multisig_funding_key_if_we_opened(\n            funding_root_secret=chan.lnworker.funding_root_keypair.privkey,\n            remote_node_id_or_prefix=remote_node_id,\n            nlocktime=funding_tx.locktime,\n        )\n    else:\n        funding_kp_cand = derive_multisig_funding_key_if_they_opened(\n            funding_root_secret=chan.lnworker.funding_root_keypair.privkey,\n            remote_node_id_or_prefix=remote_node_id,\n            remote_funding_pubkey=their_funding_pubkey,\n        )\n    assert funding_kp_cand.pubkey == our_funding_pubkey, f\"funding pubkey mismatch1. {chan.is_initiator()=}\"\n    our_ms_funding_keypair = funding_kp_cand\n    # sanity check funding_privkey, if we had it already (if backup is imported):\n    if local_config := chan.config.get(LOCAL):\n        assert our_ms_funding_keypair == local_config.multisig_key, f\"funding pubkey mismatch2. {chan.is_initiator()=}\"\n\n    if our_ms_funding_keypair:\n        if txin := sweep_ctx_anchor(ctx=ctx, multisig_key=our_ms_funding_keypair):\n            txs[txin.prevout.to_str()] = SweepInfo(\n                name='remote_anchor',\n                cltv_abs=None,\n                txin=txin,\n                txout=None,\n                can_be_batched=True,\n                dust_override=True,\n            )\n\n    # to_remote\n    our_payment_privkey = ecc.ECPrivkey(our_payment_pubkey.privkey)\n    output_idxs = ctx.get_output_idxs_from_address(to_remote_address)\n    if output_idxs:\n        output_idx = output_idxs.pop()\n        prevout = ctx.txid() + ':%d' % output_idx\n        if txin := sweep_their_ctx_to_remote(\n                ctx=ctx,\n                output_idx=output_idx,\n                our_payment_privkey=our_payment_privkey,\n                has_anchors=True\n        ):\n            txs[prevout] = SweepInfo(\n                name='their_ctx_to_remote_backup',\n                cltv_abs=None,\n                txin=txin,\n                txout=None,\n                can_be_batched=True,\n                dust_override=False,\n            )\n    return txs\n\n\n\n\ndef sweep_their_ctx(\n        *, chan: 'Channel',\n        ctx: Transaction) -> Optional[Dict[str, MaybeSweepInfo]]:\n    \"\"\"Handle the case when the remote force-closes with their ctx.\n    Sweep outputs that do not have a CSV delay ('to_remote' and first-stage HTLCs).\n    Outputs with CSV delay ('to_local' and second-stage HTLCs) are redeemed by LNWatcher.\n\n    We sweep:\n        to_local: if revoked\n        to_remote: CSV delay with anchors, otherwise sweeping not needed\n        htlc success: CSV delay with anchors, no delay otherwise, or revoked\n        htlc timeout: CSV delay with anchors, CLTV locktime, or revoked\n        second-stage htlc transactions: CSV delay\n\n    Outputs with CSV/CLTV are redeemed by LNWatcher.\n    \"\"\"\n    txs = {}  # type: Dict[str, MaybeSweepInfo]\n    our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)\n    x = extract_ctx_secrets(chan, ctx)\n    if not x:\n        return\n    ctn, their_pcp, is_revocation, per_commitment_secret = x\n    # to_local\n    our_revocation_pubkey = derive_blinded_pubkey(our_conf.revocation_basepoint.pubkey, their_pcp)\n    their_delayed_pubkey = derive_pubkey(their_conf.delayed_basepoint.pubkey, their_pcp)\n    witness_script = make_commitment_output_to_local_witness_script(\n        our_revocation_pubkey, our_conf.to_self_delay, their_delayed_pubkey)\n    to_local_address = redeem_script_to_address('p2wsh', witness_script)\n    to_remote_address = None\n    # test if this is their ctx\n    found_to_local = bool(ctx.get_output_idxs_from_address(to_local_address))\n    if not chan.is_backup():\n        assert chan.is_static_remotekey_enabled()\n        our_payment_pubkey = our_conf.payment_basepoint.pubkey\n        to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=chan.has_anchors())\n        found_to_remote = bool(ctx.get_output_idxs_from_address(to_remote_address))\n    else:\n        found_to_remote = False\n    if not found_to_local and not found_to_remote:\n        return\n    chan.logger.debug(f'(lnsweep) found their ctx: {to_local_address} {to_remote_address}')\n\n    # remote anchor\n    if chan.has_anchors():\n        if txin := sweep_ctx_anchor(ctx=ctx, multisig_key=our_conf.multisig_key):\n            txs[txin.prevout.to_str()] = SweepInfo(\n                name='remote_anchor',\n                cltv_abs=None,\n                txin=txin,\n                txout=None,\n                can_be_batched=True,\n                dust_override=True,\n            )\n\n    # to_local is handled by lnwatcher\n    if is_revocation:\n        our_revocation_privkey = derive_blinded_privkey(our_conf.revocation_basepoint.privkey, per_commitment_secret)\n        if txin := sweep_their_ctx_justice(chan, ctx, per_commitment_secret):\n            txs[txin.prevout.to_str()] = SweepInfo(\n                name='to_local_for_revoked_ctx',\n                cltv_abs=None,\n                txin=txin,\n                txout=None,\n                can_be_batched=False,\n                dust_override=False,\n            )\n\n    # to_remote\n    if chan.has_anchors():\n        sweep_to_remote = True\n        our_payment_privkey = ecc.ECPrivkey(our_conf.payment_basepoint.privkey)\n    else:\n        assert chan.is_static_remotekey_enabled()\n        sweep_to_remote = False\n        our_payment_privkey = None\n\n    if sweep_to_remote:\n        assert our_payment_pubkey == our_payment_privkey.get_public_key_bytes(compressed=True)\n        output_idxs = ctx.get_output_idxs_from_address(to_remote_address)\n        if output_idxs:\n            output_idx = output_idxs.pop()\n            prevout = ctx.txid() + ':%d' % output_idx\n            if txin := sweep_their_ctx_to_remote(\n                    ctx=ctx,\n                    output_idx=output_idx,\n                    our_payment_privkey=our_payment_privkey,\n                    has_anchors=chan.has_anchors()\n            ):\n                # todo: we might not want to sweep this at all, if we add it to the wallet addresses\n                txs[prevout] = SweepInfo(\n                    name='their_ctx_to_remote',\n                    cltv_abs=None,\n                    txin=txin,\n                    txout=None,\n                    can_be_batched=True,\n                    dust_override=False,\n                )\n\n    # HTLCs\n    our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=their_pcp)\n    our_htlc_privkey = ecc.ECPrivkey.from_secret_scalar(our_htlc_privkey)\n    their_htlc_pubkey = derive_pubkey(their_conf.htlc_basepoint.pubkey, their_pcp)\n    def tx_htlc(\n            *, htlc: 'UpdateAddHtlc',\n            is_received_htlc: bool,\n            ctx_output_idx: int,\n            preimage: Optional[bytes]) -> None:\n        htlc_output_witness_script = make_htlc_output_witness_script(\n            is_received_htlc=is_received_htlc,\n            remote_revocation_pubkey=our_revocation_pubkey,\n            remote_htlc_pubkey=our_htlc_privkey.get_public_key_bytes(compressed=True),\n            local_htlc_pubkey=their_htlc_pubkey,\n            payment_hash=htlc.payment_hash,\n            cltv_abs=htlc.cltv_abs,\n            has_anchors=chan.has_anchors())\n\n        cltv_abs = htlc.cltv_abs if is_received_htlc and not is_revocation else 0\n        prevout = ctx.txid() + ':%d'%ctx_output_idx\n        if txin := sweep_their_ctx_htlc(\n                ctx=ctx,\n                witness_script=htlc_output_witness_script,\n                preimage=preimage,\n                output_idx=ctx_output_idx,\n                privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(),\n                is_revocation=is_revocation,\n                cltv_abs=cltv_abs,\n                has_anchors=chan.has_anchors(),\n        ):\n            txs[prevout] = SweepInfo(\n                name=f'their_ctx_htlc_{ctx_output_idx}{\"_for_revoked_ctx\" if is_revocation else \"\"}',\n                cltv_abs=cltv_abs,\n                txin=txin,\n                txout=None,\n                can_be_batched=False,   # both parties can spend\n                # (still, in some cases we could batch, see comment in sweep_our_ctx)\n                dust_override=False,\n            )\n    # received HTLCs, in their ctx --> \"timeout\"\n    # offered HTLCs, in their ctx --> \"success\"\n    htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(\n        chan=chan,\n        ctx=ctx,\n        pcp=their_pcp,\n        subject=REMOTE,\n        ctn=ctn)\n    for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():\n        preimage = None\n        is_received_htlc = direction == RECEIVED\n        if not is_received_htlc and not is_revocation:\n            preimage, keep_watching_txo = _maybe_reveal_preimage_for_htlc(\n                chan=chan, htlc=htlc,\n                sweep_info_name=f\"their_ctx_htlc_{ctx_output_idx}\",\n            )\n            if keep_watching_txo:\n                prevout = ctx.txid() + ':%d' % ctx_output_idx\n                txs[prevout] = keep_watching_txo\n            if not preimage:\n                continue\n        tx_htlc(\n            htlc=htlc,\n            is_received_htlc=is_received_htlc,\n            ctx_output_idx=ctx_output_idx,\n            preimage=preimage)\n    return txs\n\n\ndef tx_our_ctx_htlctx(\n        chan: 'Channel',\n        our_pcp: bytes,\n        ctx: Transaction,\n        htlc: 'UpdateAddHtlc',\n        local_htlc_privkey: bytes,\n        preimage: Optional[bytes],\n        htlc_direction: Direction,\n        htlc_relative_idx: int,\n        ctx_output_idx: int) -> Tuple[bytes, Transaction]:\n    assert (htlc_direction == RECEIVED) == bool(preimage), 'preimage is required iff htlc is received'\n    preimage = preimage or b''\n    ctn = extract_ctn_from_tx_and_chan(ctx, chan)\n    witness_script_out, maybe_zero_fee_htlc_tx = make_htlc_tx_with_open_channel(\n        chan=chan,\n        pcp=our_pcp,\n        subject=LOCAL,\n        ctn=ctn,\n        htlc_direction=htlc_direction,\n        commit=ctx,\n        htlc=htlc,\n        ctx_output_idx=ctx_output_idx,\n        name=f'our_ctx_{ctx_output_idx}_htlc_tx_{htlc.payment_hash.hex()}')\n\n    # sign HTLC output\n    remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc_relative_idx=htlc_relative_idx)\n    txin = maybe_zero_fee_htlc_tx.inputs()[HTLCTX_INPUT_OUTPUT_INDEX]\n    witness_script_in = txin.witness_script\n    assert witness_script_in\n    txin.privkey = local_htlc_privkey\n    txin.make_witness = lambda local_htlc_sig: make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_script_in)\n    return witness_script_out, maybe_zero_fee_htlc_tx\n\n\ndef sweep_their_ctx_htlc(\n        ctx: Transaction,\n        witness_script: bytes,\n        preimage: Optional[bytes], output_idx: int,\n        privkey: bytes, is_revocation: bool,\n        cltv_abs: int,\n        has_anchors: bool,\n) -> Optional[PartialTxInput]:\n    \"\"\"Deals with normal (non-CSV timelocked) HTLC output sweeps.\"\"\"\n    assert type(cltv_abs) is int\n    assert witness_script is not None\n    preimage = preimage or b''  # preimage is required iff (not is_revocation and htlc is offered)\n    val = ctx.outputs()[output_idx].value\n    prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)\n    txin = PartialTxInput(prevout=prevout)\n    txin._trusted_value_sats = val\n    txin.witness_script = witness_script\n    txin.script_sig = b''\n    txin.nsequence = 1 if has_anchors else 0xffffffff - 2\n    txin.privkey = privkey\n    if not is_revocation:\n        txin.make_witness = lambda sig: construct_witness([sig, preimage, witness_script])\n    else:\n        revocation_pubkey = privkey_to_pubkey(privkey)\n        txin.make_witness = lambda sig: construct_witness([sig, revocation_pubkey, witness_script])\n    return txin\n\n\n\ndef sweep_their_ctx_to_remote(\n        ctx: Transaction, output_idx: int,\n        our_payment_privkey: ecc.ECPrivkey,\n        has_anchors: bool,\n) -> Optional[PartialTxInput]:\n    assert has_anchors is True\n    our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True)\n    val = ctx.outputs()[output_idx].value\n    prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)\n    txin = PartialTxInput(prevout=prevout)\n    txin._trusted_value_sats = val\n    desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey.hex(), script_type='p2wpkh')\n    witness_script = make_commitment_output_to_remote_witness_script(our_payment_pubkey)\n    txin.script_descriptor = desc\n    txin.num_sig = 1\n    txin.script_sig = b''\n    txin.witness_script = witness_script\n    txin.nsequence = 1\n    txin.privkey = our_payment_privkey.get_secret_bytes()\n    txin.make_witness = lambda sig: construct_witness([sig, witness_script])\n    return txin\n\n\ndef sweep_ctx_anchor(*, ctx: Transaction, multisig_key: Keypair) -> Optional[PartialTxInput]:\n    from .lnutil import make_commitment_output_to_anchor_address, make_commitment_output_to_anchor_witness_script\n    local_funding_pubkey = multisig_key.pubkey\n    local_anchor_address = make_commitment_output_to_anchor_address(local_funding_pubkey)\n    witness_script = make_commitment_output_to_anchor_witness_script(local_funding_pubkey)\n    output_idxs = ctx.get_output_idxs_from_address(local_anchor_address)\n    if not output_idxs:\n        return\n    output_idx = output_idxs.pop()\n    val = ctx.outputs()[output_idx].value\n    prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)\n    txin = PartialTxInput(prevout=prevout)\n    txin._trusted_value_sats = val\n    txin.script_sig = b''\n    txin.witness_script = witness_script\n    txin.nsequence = 0xffffffff - 2\n    txin.privkey = multisig_key.privkey\n    txin.make_witness = lambda sig: construct_witness([sig, witness_script])\n    return txin\n\n\ndef sweep_ctx_to_local(\n        *, ctx: Transaction, output_idx: int, witness_script: bytes,\n        privkey: bytes, is_revocation: bool,\n        to_self_delay: int = None) -> Optional[PartialTxInput]:\n    \"\"\"Create a txin that sweeps the 'to_local' output of a commitment\n    transaction into our wallet.\n\n    privkey: either revocation_privkey or localdelayed_privkey\n    is_revocation: tells us which ^\n    \"\"\"\n    val = ctx.outputs()[output_idx].value\n    prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)\n    txin = PartialTxInput(prevout=prevout)\n    txin._trusted_value_sats = val\n    txin.script_sig = b''\n    txin.witness_script = witness_script\n    txin.nsequence = 0xffffffff - 2\n    if not is_revocation:\n        assert isinstance(to_self_delay, int)\n        txin.nsequence = to_self_delay\n    txin.privkey = privkey\n    assert txin.witness_script\n    txin.make_witness = lambda sig: construct_witness([sig, int(is_revocation), witness_script])\n    return txin\n\n\ndef sweep_htlctx_output(\n        *, htlc_tx: Transaction,\n        output_idx: int,\n        htlctx_witness_script: bytes,\n        privkey: bytes,\n        is_revocation: bool,\n        to_self_delay: int = None,\n) -> Optional[PartialTxInput]:\n    \"\"\"Create a txn that sweeps the output of a first stage htlc tx\n    (i.e. sweeps from an HTLC-Timeout or an HTLC-Success tx).\n    \"\"\"\n    # note: this is the same as sweeping the to_local output of the ctx,\n    #       as these are the same script (address-reuse).\n    return sweep_ctx_to_local(\n        ctx=htlc_tx,\n        output_idx=output_idx,\n        witness_script=htlctx_witness_script,\n        privkey=privkey,\n        is_revocation=is_revocation,\n        to_self_delay=to_self_delay,\n    )\n"
  },
  {
    "path": "electrum/lntransport.py",
    "content": "# Copyright (C) 2018 Adam Gibson (waxwing)\n# Copyright (C) 2018 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\n# Derived from https://gist.github.com/AdamISZ/046d05c156aaeb56cc897f85eecb3eb8\n\nimport re\nimport hashlib\nimport asyncio\nfrom asyncio import StreamReader, StreamWriter\nfrom functools import cached_property\nfrom typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence\n\nfrom aiorpcx import NetAddress\nimport electrum_ecc as ecc\n\nfrom .crypto import sha256, hmac_oneshot, chacha20_poly1305_encrypt, chacha20_poly1305_decrypt, get_ecdh, privkey_to_pubkey\nfrom .util import ESocksProxy\n\n\nclass LightningPeerConnectionClosed(Exception): pass\nclass HandshakeFailed(Exception): pass\nclass ConnStringFormatError(Exception): pass\n\n\nif TYPE_CHECKING:\n    from electrum.network import Network\n\n\nclass HandshakeState(object):\n    prologue = b\"lightning\"\n    protocol_name = b\"Noise_XK_secp256k1_ChaChaPoly_SHA256\"\n    handshake_version = b\"\\x00\"\n\n    def __init__(self, responder_pub):\n        self.responder_pub = responder_pub\n        self.h = sha256(self.protocol_name)\n        self.ck = self.h\n        self.update(self.prologue)\n        self.update(self.responder_pub)\n\n    def update(self, data):\n        self.h = sha256(self.h + data)\n        return self.h\n\n\ndef get_nonce_bytes(n):\n    \"\"\"BOLT 8 requires the nonce to be 12 bytes, 4 bytes leading\n    zeroes and 8 bytes little endian encoded 64 bit integer.\n    \"\"\"\n    return b\"\\x00\"*4 + n.to_bytes(8, 'little')\n\n\ndef aead_encrypt(key: bytes, nonce: int, associated_data: bytes, data: bytes) -> bytes:\n    nonce_bytes = get_nonce_bytes(nonce)\n    return chacha20_poly1305_encrypt(key=key,\n                                     nonce=nonce_bytes,\n                                     associated_data=associated_data,\n                                     data=data)\n\n\ndef aead_decrypt(key: bytes, nonce: int, associated_data: bytes, data: bytes) -> bytes:\n    nonce_bytes = get_nonce_bytes(nonce)\n    return chacha20_poly1305_decrypt(key=key,\n                                     nonce=nonce_bytes,\n                                     associated_data=associated_data,\n                                     data=data)\n\n\ndef get_bolt8_hkdf(salt, ikm):\n    \"\"\"RFC5869 HKDF instantiated in the specific form\n    used in Lightning BOLT 8:\n    Extract and expand to 64 bytes using HMAC-SHA256,\n    with info field set to a zero length string as per BOLT8\n    Return as two 32 byte fields.\n    \"\"\"\n    #Extract\n    prk = hmac_oneshot(salt, msg=ikm, digest=hashlib.sha256)\n    assert len(prk) == 32\n    #Expand\n    info = b\"\"\n    T0 = b\"\"\n    T1 = hmac_oneshot(prk, T0 + info + b\"\\x01\", digest=hashlib.sha256)\n    T2 = hmac_oneshot(prk, T1 + info + b\"\\x02\", digest=hashlib.sha256)\n    assert len(T1 + T2) == 64\n    return T1, T2\n\n\ndef act1_initiator_message(hs, epriv, epub):\n    ss = get_ecdh(epriv, hs.responder_pub)\n    ck2, temp_k1 = get_bolt8_hkdf(hs.ck, ss)\n    hs.ck = ck2\n    c = aead_encrypt(temp_k1, 0, hs.update(epub), b\"\")\n    #for next step if we do it\n    hs.update(c)\n    msg = hs.handshake_version + epub + c\n    assert len(msg) == 50\n    return msg, temp_k1\n\n\ndef create_ephemeral_key() -> (bytes, bytes):\n    privkey = ecc.ECPrivkey.generate_random_key()\n    return privkey.get_secret_bytes(), privkey.get_public_key_bytes()\n\n\ndef split_host_port(host_port: str) -> Tuple[str, str]: # port returned as string\n    ipv6  = re.compile(r'\\[(?P<host>[:0-9a-f]+)\\](?P<port>:\\d+)?$')\n    other = re.compile(r'(?P<host>[^:]+)(?P<port>:\\d+)?$')\n    m = ipv6.match(host_port)\n    if not m:\n        m = other.match(host_port)\n    if not m:\n        raise ConnStringFormatError('Connection strings must be in <node_pubkey>@<host>:<port> format')\n    host = m.group('host')\n    if m.group('port'):\n        port = m.group('port')[1:]\n    else:\n        port = '9735'\n    try:\n        int(port)\n    except ValueError:\n        raise ConnStringFormatError('Port number must be decimal')\n    return host, port\n\n\ndef extract_nodeid(connect_contents: str) -> Tuple[bytes, Optional[str]]:\n    \"\"\"Takes a connection-string-like str, and returns a tuple (node_id, rest),\n    where rest is typically a host (with maybe port). Examples:\n    - extract_nodeid(pubkey@host:port) == (pubkey, host:port)\n    - extract_nodeid(pubkey@host) == (pubkey, host)\n    - extract_nodeid(pubkey) == (pubkey, None)\n    Can raise ConnStringFormatError.\n    \"\"\"\n    rest = None\n    try:\n        # connection string?\n        nodeid_hex, rest = connect_contents.split(\"@\", 1)\n    except ValueError:\n        # node id as hex?\n        nodeid_hex = connect_contents\n    if rest == '':\n        raise ConnStringFormatError('At least a hostname must be supplied after the at symbol.')\n    try:\n        node_id = bytes.fromhex(nodeid_hex)\n        if len(node_id) != 33:\n            raise Exception()\n    except Exception:\n        raise ConnStringFormatError('Invalid node ID, must be 33 bytes and hexadecimal')\n    return node_id, rest\n\n\nclass LNPeerAddr:\n    # note: while not programmatically enforced, this class is meant to be *immutable*\n\n    def __init__(self, host: str, port: int, pubkey: bytes):\n        assert isinstance(host, str), repr(host)\n        assert isinstance(port, int), repr(port)\n        assert isinstance(pubkey, bytes), repr(pubkey)\n        try:\n            net_addr = NetAddress(host, port)  # this validates host and port\n        except Exception as e:\n            raise ValueError(f\"cannot construct LNPeerAddr: invalid host or port (host={host}, port={port})\") from e\n        # note: not validating pubkey as it would be too expensive:\n        # if not ECPubkey.is_pubkey_bytes(pubkey): raise ValueError()\n        self.host = host\n        self.port = port\n        self.pubkey = pubkey\n        self._net_addr = net_addr\n\n    def __str__(self):\n        return '{}@{}'.format(self.pubkey.hex(), self.net_addr_str())\n\n    @classmethod\n    def from_str(cls, s):\n        node_id, rest = extract_nodeid(s)\n        host, port = split_host_port(rest)\n        return LNPeerAddr(host, int(port), node_id)\n\n    def __repr__(self):\n        return f'<LNPeerAddr host={self.host} port={self.port} pubkey={self.pubkey.hex()}>'\n\n    def net_addr(self) -> NetAddress:\n        return self._net_addr\n\n    def net_addr_str(self) -> str:\n        return str(self._net_addr)\n\n    def is_onion(self) -> bool:\n        return self.host.endswith('.onion')\n\n    def __eq__(self, other):\n        if not isinstance(other, LNPeerAddr):\n            return False\n        return (self.host == other.host\n                and self.port == other.port\n                and self.pubkey == other.pubkey)\n\n    def __ne__(self, other):\n        return not (self == other)\n\n    def __hash__(self):\n        return hash((self.host, self.port, self.pubkey))\n\n\nclass LNTransportBase:\n    reader: StreamReader\n    writer: StreamWriter\n    privkey: bytes\n    peer_addr: Optional[LNPeerAddr] = None\n\n    def __init__(self):\n        self.drain_write_lock = asyncio.Lock()\n\n    def name(self) -> str:\n        pubkey = self.remote_pubkey()\n        pubkey_hex = pubkey.hex() if pubkey else pubkey\n        return f\"{pubkey_hex[:10]}-{self._id_hash[:8]}\"\n\n    @cached_property\n    def _id_hash(self) -> str:\n        id_int = id(self)\n        id_bytes = id_int.to_bytes((id_int.bit_length() + 7) // 8, byteorder='big')\n        return sha256(id_bytes).hex()\n\n    def send_bytes(self, msg: bytes) -> None:\n        l = len(msg).to_bytes(2, 'big')\n        lc = aead_encrypt(self.sk, self.sn(), b'', l)\n        c = aead_encrypt(self.sk, self.sn(), b'', msg)\n        assert len(lc) == 18\n        assert len(c) == len(msg) + 16\n        self.writer.write(lc+c)\n\n    async def send_bytes_and_drain(self, msg: bytes) -> None:\n        \"\"\"Should be used when possible (in async scope), to avoid memory exhaustion.\"\"\"\n        async with self.drain_write_lock:\n            self.send_bytes(msg)\n            await self.writer.drain()\n\n    async def read_messages(self):\n        buffer = bytearray()\n        while True:\n            rn_l, rk_l = self.rn()\n            rn_m, rk_m = self.rn()\n            while True:\n                if len(buffer) >= 18:\n                    lc = bytes(buffer[:18])\n                    l = aead_decrypt(rk_l, rn_l, b'', lc)\n                    length = int.from_bytes(l, 'big')\n                    offset = 18 + length + 16\n                    if len(buffer) >= offset:\n                        c = bytes(buffer[18:offset])\n                        del buffer[:offset]  # much faster than: buffer=buffer[offset:]\n                        msg = aead_decrypt(rk_m, rn_m, b'', c)\n                        yield msg\n                        break\n                try:\n                    s = await self.reader.read(2**10)\n                except Exception:\n                    s = None\n                if not s:\n                    raise LightningPeerConnectionClosed()\n                buffer += s\n\n    def rn(self):\n        o = self._rn, self.rk\n        self._rn += 1\n        if self._rn == 1000:\n            self.r_ck, self.rk = get_bolt8_hkdf(self.r_ck, self.rk)\n            self._rn = 0\n        return o\n\n    def sn(self):\n        o = self._sn\n        self._sn += 1\n        if self._sn == 1000:\n            self.s_ck, self.sk = get_bolt8_hkdf(self.s_ck, self.sk)\n            self._sn = 0\n        return o\n\n    def init_counters(self, ck):\n        # init counters\n        self._sn = 0\n        self._rn = 0\n        self.r_ck = ck\n        self.s_ck = ck\n\n    def close(self):\n        self.writer.close()\n\n    def remote_pubkey(self) -> Optional[bytes]:\n        raise NotImplementedError()\n\n\nclass LNResponderTransport(LNTransportBase):\n    \"\"\"Transport initiated by remote party.\"\"\"\n\n    def __init__(self, privkey: bytes, reader: StreamReader, writer: StreamWriter):\n        LNTransportBase.__init__(self)\n        self.reader = reader\n        self.writer = writer\n        self.privkey = privkey\n        self._pubkey = None  # remote pubkey\n\n    def name(self) -> str:\n        return f\"{super().name()}(in)\"\n\n    async def handshake(self, **kwargs):\n        hs = HandshakeState(privkey_to_pubkey(self.privkey))\n        act1 = b''\n        while len(act1) < 50:\n            buf = await self.reader.read(50 - len(act1))\n            if not buf:\n                raise HandshakeFailed('responder disconnected')\n            act1 += buf\n        if len(act1) != 50:\n            raise HandshakeFailed('responder: short act 1 read, length is ' + str(len(act1)))\n        if bytes([act1[0]]) != HandshakeState.handshake_version:\n            raise HandshakeFailed('responder: bad handshake version in act 1')\n        c = act1[-16:]\n        re = act1[1:34]\n        h = hs.update(re)\n        ss = get_ecdh(self.privkey, re)\n        ck, temp_k1 = get_bolt8_hkdf(sha256(HandshakeState.protocol_name), ss)\n        _p = aead_decrypt(temp_k1, 0, h, c)\n        hs.update(c)\n\n        # act 2\n        if 'epriv' not in kwargs:\n            epriv, epub = create_ephemeral_key()\n        else:\n            epriv = kwargs['epriv']\n            epub = ecc.ECPrivkey(epriv).get_public_key_bytes()\n        hs.ck = ck\n        hs.responder_pub = re\n\n        msg, temp_k2 = act1_initiator_message(hs, epriv, epub)\n        self.writer.write(msg)\n\n        # act 3\n        act3 = b''\n        while len(act3) < 66:\n            buf = await self.reader.read(66 - len(act3))\n            if not buf:\n                raise HandshakeFailed('responder disconnected')\n            act3 += buf\n        if len(act3) != 66:\n            raise HandshakeFailed('responder: short act 3 read, length is ' + str(len(act3)))\n        if bytes([act3[0]]) != HandshakeState.handshake_version:\n            raise HandshakeFailed('responder: bad handshake version in act 3')\n        c = act3[1:50]\n        t = act3[-16:]\n        rs = aead_decrypt(temp_k2, 1, hs.h, c)\n        ss = get_ecdh(epriv, rs)\n        ck, temp_k3 = get_bolt8_hkdf(hs.ck, ss)\n        _p = aead_decrypt(temp_k3, 0, hs.update(c), t)\n        self.rk, self.sk = get_bolt8_hkdf(ck, b'')\n        self.init_counters(ck)\n        self._pubkey = rs\n        return rs\n\n    def remote_pubkey(self) -> Optional[bytes]:\n        return self._pubkey\n\n\nclass LNTransport(LNTransportBase):\n    \"\"\"Transport initiated by local party.\"\"\"\n\n    def __init__(self, privkey: bytes, peer_addr: LNPeerAddr, *,\n                 e_proxy: Optional['ESocksProxy']):\n        LNTransportBase.__init__(self)\n        assert type(privkey) is bytes and len(privkey) == 32\n        self.privkey = privkey\n        self.peer_addr = peer_addr\n        self.e_proxy = e_proxy\n\n    async def handshake(self):\n        if not self.e_proxy:\n            self.reader, self.writer = await asyncio.open_connection(self.peer_addr.host, self.peer_addr.port)\n        else:\n            self.reader, self.writer = await self.e_proxy.open_connection(self.peer_addr.host, self.peer_addr.port)\n        hs = HandshakeState(self.peer_addr.pubkey)\n        # Get a new ephemeral key\n        epriv, epub = create_ephemeral_key()\n\n        msg, _temp_k1 = act1_initiator_message(hs, epriv, epub)\n        # act 1\n        self.writer.write(msg)\n        rspns = await self.reader.read(2**10)\n        if len(rspns) != 50:\n            raise HandshakeFailed(f\"Lightning handshake act 1 response has bad length, \"\n                                  f\"are you sure this is the right pubkey? {self.peer_addr}\")\n        hver, alice_epub, tag = rspns[0], rspns[1:34], rspns[34:]\n        if bytes([hver]) != hs.handshake_version:\n            raise HandshakeFailed(\"unexpected handshake version: {}\".format(hver))\n        # act 2\n        hs.update(alice_epub)\n        ss = get_ecdh(epriv, alice_epub)\n        ck, temp_k2 = get_bolt8_hkdf(hs.ck, ss)\n        hs.ck = ck\n        p = aead_decrypt(temp_k2, 0, hs.h, tag)\n        hs.update(tag)\n        # act 3\n        my_pubkey = privkey_to_pubkey(self.privkey)\n        c = aead_encrypt(temp_k2, 1, hs.h, my_pubkey)\n        hs.update(c)\n        ss = get_ecdh(self.privkey[:32], alice_epub)\n        ck, temp_k3 = get_bolt8_hkdf(hs.ck, ss)\n        hs.ck = ck\n        t = aead_encrypt(temp_k3, 0, hs.h, b'')\n        msg = hs.handshake_version + c + t\n        self.writer.write(msg)\n        self.sk, self.rk = get_bolt8_hkdf(hs.ck, b'')\n        self.init_counters(ck)\n\n    def remote_pubkey(self) -> Optional[bytes]:\n        return self.peer_addr.pubkey\n"
  },
  {
    "path": "electrum/lnurl.py",
    "content": "\"\"\"Module for lnurl-related functionality.\"\"\"\n# https://github.com/sipa/bech32/tree/master/ref/python\n# https://github.com/lnbits/lnurl\n\nimport asyncio\nimport json\nfrom typing import Callable, Optional, NamedTuple, Any, TYPE_CHECKING\nimport re\nimport urllib.parse\n\nimport aiohttp.client_exceptions\n\nfrom electrum import segwit_addr, util\nfrom electrum.segwit_addr import bech32_decode, Encoding, convertbits, bech32_encode\nfrom electrum.lnaddr import LnDecodeException, LnEncodeException\nfrom electrum.network import Network\nfrom electrum.logging import get_logger\nfrom electrum.i18n import _\n\n\n_logger = get_logger(__name__)\n\n\nclass LNURLError(Exception): pass\n\nclass UntrustedLNURLError(LNURLError):\n    def __init__(self, message=\"\"):\n        # use if error messages are returned by the LNURL server,\n        # some services could try to trick users into doing something\n        # by sending a malicious error message\n        if message:\n            message = (\n                f\"{_('[DO NOT TRUST THIS MESSAGE]:')}\\n\"\n                f\"{util.error_text_str_to_safe_str(message)}\"\n            )\n        super().__init__(message)\n\n\ndef decode_lnurl(lnurl: str) -> str:\n    \"\"\"Converts bech32 encoded lnurl to url.\"\"\"\n    decoded_bech32 = bech32_decode(\n        lnurl, ignore_long_length=True\n    )\n    hrp = decoded_bech32.hrp\n    data = decoded_bech32.data\n    if decoded_bech32.encoding is None:\n        raise LnDecodeException(\"Bad bech32 checksum\")\n    if decoded_bech32.encoding != Encoding.BECH32:\n        raise LnDecodeException(\"Bad bech32 encoding: must be using vanilla BECH32\")\n    if not hrp.startswith(\"lnurl\"):\n        raise LnDecodeException(\"Does not start with lnurl\")\n    data = convertbits(data, 5, 8, False)\n    url = bytes(data).decode(\"utf-8\")\n    return url\n\n\ndef encode_lnurl(url: str) -> str:\n    \"\"\"Encode url to bech32 lnurl string.\"\"\"\n    try:\n        url = url.encode(\"utf-8\")\n    except UnicodeError as e:\n        raise LnEncodeException(\"invalid url\") from e\n    bech32_data = convertbits(url, 8, 5, True)\n    assert bech32_data\n    lnurl = bech32_encode(\n        encoding=segwit_addr.Encoding.BECH32, hrp=\"lnurl\", data=bech32_data)\n    return lnurl.upper()\n\n\ndef _is_url_safe_enough_for_lnurl(url: str) -> bool:\n    u = urllib.parse.urlparse(url)\n    if u.scheme.lower() == \"https\":\n        return True\n    if u.netloc.endswith(\".onion\"):\n        return True\n    return False\n\n\ndef _parse_lnurl_response_callback_url(lnurl_response: dict) -> str:\n    try:\n        callback_url = lnurl_response['callback']\n    except KeyError as e:\n        raise LNURLError(f\"Missing 'callback' field in lnurl response.\") from e\n    if not _is_url_safe_enough_for_lnurl(callback_url):\n        raise LNURLError(\n            f\"This lnurl callback_url looks unsafe. It must use 'https://' or '.onion' (found: {callback_url[:10]}...)\")\n    return callback_url\n\n\n# payRequest\n# https://github.com/lnurl/luds/blob/227f850b701e9ba893c080103c683273e2feb521/06.md\nclass LNURL6Data(NamedTuple):\n    callback_url: str\n    max_sendable_sat: int\n    min_sendable_sat: int\n    metadata_plaintext: str\n    comment_allowed: int\n    #tag: str = \"payRequest\"\n\n# withdrawRequest\n# https://github.com/lnurl/luds/blob/227f850b701e9ba893c080103c683273e2feb521/03.md\nclass LNURL3Data(NamedTuple):\n    # The URL which LN SERVICE would accept a withdrawal Lightning invoice as query parameter\n    callback_url: str\n    # Random or non-random string to identify the user's LN WALLET when using the callback URL\n    k1: str\n    # A default withdrawal invoice description\n    default_description: str\n    # Min amount the user can withdraw from LN SERVICE, or 0\n    min_withdrawable_sat: int\n    # Max amount the user can withdraw from LN SERVICE,\n    # or equal to minWithdrawable if the user has no choice over the amounts\n    max_withdrawable_sat: int\n\nLNURLData = LNURL6Data | LNURL3Data\n\n\nasync def _request_lnurl(url: str) -> dict:\n    \"\"\"Requests payment data from a lnurl.\"\"\"\n    if not _is_url_safe_enough_for_lnurl(url):\n        raise LNURLError(f\"This lnurl looks unsafe. It must use 'https://' or '.onion' (found: {url[:10]}...)\")\n    try:\n        response_raw = await Network.async_send_http_on_proxy(\"get\", url, timeout=10)\n    except asyncio.TimeoutError as e:\n        raise LNURLError(\"LNURL server did not reply in time.\") from e\n    except aiohttp.client_exceptions.ClientError as e:\n        raise LNURLError(f\"Client error: {e}\") from e\n    try:\n        response = json.loads(response_raw)\n    except json.JSONDecodeError:\n        raise LNURLError(f\"Invalid response from LNURL server\")\n\n    status = response.get(\"status\")\n    if status and status == \"ERROR\":\n        raise UntrustedLNURLError(f\"LNURL request encountered an error: {response.get('reason', '<missing reason>')}\")\n    return response\n\n\ndef _parse_lnurl6_response(lnurl_response: dict) -> LNURL6Data:\n    # parse lnurl6 \"metadata\"\n    metadata_plaintext = \"\"\n    try:\n        metadata_raw = lnurl_response[\"metadata\"]\n        metadata = json.loads(metadata_raw)\n        for m in metadata:\n            if m[0] == 'text/plain':\n                metadata_plaintext = str(m[1])\n    except Exception as e:\n        raise LNURLError(\n            f\"Missing or malformed 'metadata' field in lnurl6 response. exc: {e!r}\") from e\n    # parse lnurl6 \"callback\"\n    callback_url = _parse_lnurl_response_callback_url(lnurl_response)\n    # parse lnurl6 \"minSendable\"/\"maxSendable\"\n    try:\n        max_sendable_sat = int(lnurl_response['maxSendable']) // 1000\n        min_sendable_sat = int(lnurl_response['minSendable']) // 1000\n    except Exception as e:\n        raise LNURLError(\n            f\"Missing or malformed 'minSendable'/'maxSendable' field in lnurl6 response. {e=!r}\") from e\n    # parse lnurl6 \"commentAllowed\" (optional, described in lnurl-12)\n    try:\n        comment_allowed = int(lnurl_response['commentAllowed']) if 'commentAllowed' in lnurl_response else 0\n    except Exception as e:\n        raise LNURLError(f\"Malformed 'commentAllowed' field in lnurl6 response. {e=!r}\") from e\n    data = LNURL6Data(\n        callback_url=callback_url,\n        max_sendable_sat=max_sendable_sat,\n        min_sendable_sat=min_sendable_sat,\n        metadata_plaintext=metadata_plaintext,\n        comment_allowed=comment_allowed,\n    )\n    return data\n\n\ndef _parse_lnurl3_response(lnurl_response: dict) -> LNURL3Data:\n    \"\"\"Parses the server response received when requesting a LNURL-withdraw (lud3) request\"\"\"\n    callback_url = _parse_lnurl_response_callback_url(lnurl_response)\n    if not (k1 := lnurl_response.get('k1')):\n        raise UntrustedLNURLError(f\"Missing k1 value in LNURL3 response: {lnurl_response=}\")\n    default_description = lnurl_response.get('defaultDescription', '')\n    try:\n        min_withdrawable_sat = int(lnurl_response['minWithdrawable'] or 0) // 1000\n        max_withdrawable_sat = int(lnurl_response['maxWithdrawable']) // 1000\n        assert max_withdrawable_sat >= min_withdrawable_sat, f\"Invalid amounts: max < min amount\"\n        assert max_withdrawable_sat > 0, f\"Invalid max amount: {max_withdrawable_sat} sat\"\n    except Exception as e:\n        raise LNURLError(\n            f\"Missing or malformed 'minWithdrawable'/'minWithdrawable' field in lnurl3 response. {e=!r}\") from e\n    return LNURL3Data(\n        callback_url=callback_url,\n        k1=k1,\n        default_description=default_description,\n        min_withdrawable_sat=min_withdrawable_sat,\n        max_withdrawable_sat=max_withdrawable_sat,\n    )\n\n\nasync def request_lnurl(url: str) -> LNURLData:\n    lnurl_dict = await _request_lnurl(url)\n    tag = lnurl_dict.get('tag')\n    if tag == 'payRequest':  # only LNURL6 is handled atm\n        return _parse_lnurl6_response(lnurl_dict)\n    elif tag == 'withdrawRequest':\n        return _parse_lnurl3_response(lnurl_dict)\n    raise UntrustedLNURLError(f\"Unknown subtype of lnurl. tag={tag}\")\n\n\nasync def try_resolve_lnurlpay(lnurl: Optional[str]) -> Optional[LNURL6Data]:\n    if lnurl:\n        try:\n            result = await request_lnurl(lnurl)\n            assert isinstance(result, LNURL6Data), f\"lnurl result is not LNURL-pay response: {result=}\"\n            return result\n        except Exception as request_error:\n            _logger.debug(f\"Error resolving lnurl: {request_error!r}\")\n    return None\n\nasync def request_lnurl_withdraw_callback(callback_url: str, k1: str, bolt_11: str) -> None:\n    assert bolt_11\n    params = {\n        \"k1\": k1,\n        \"pr\": bolt_11,\n    }\n    await callback_lnurl(\n        url=callback_url,\n        params=params\n    )\n\nasync def callback_lnurl(url: str, params: dict) -> dict:\n    \"\"\"Requests an invoice from a lnurl supporting server.\"\"\"\n    if not _is_url_safe_enough_for_lnurl(url):\n        raise LNURLError(f\"This lnurl looks unsafe. It must use 'https://' or '.onion' (found: {url[:10]}...)\")\n    try:\n        response_raw = await Network.async_send_http_on_proxy(\"get\", url, params=params)\n    except asyncio.TimeoutError as e:\n        raise LNURLError(\"LNURL server did not reply in time.\") from e\n    except aiohttp.client_exceptions.ClientError as e:\n        raise LNURLError(f\"Client error: {e}\") from e\n    try:\n        response = json.loads(response_raw)\n        _logger.debug(f\"lnurl response: {response}\")\n    except json.JSONDecodeError:\n        raise LNURLError(f\"Invalid response from LNURL server\")\n\n    status = response.get(\"status\")\n    if status and status == \"ERROR\":\n        raise UntrustedLNURLError(f\"LNURL request encountered an error: {response.get('reason', '<missing reason>')}\")\n    # TODO: handling of specific errors (validate fields, e.g. for lnurl6)\n    return response\n\n\ndef lightning_address_to_url(address: str) -> Optional[str]:\n    \"\"\"Converts an email-type lightning address to a decoded lnurl.\n    see https://github.com/fiatjaf/lnurl-rfc/blob/luds/16.md\n    \"\"\"\n    if re.match(r\"^[^@]+@[^.@]+(\\.[^.@]+)+$\", address):\n        username, domain = address.split(\"@\")\n        return f\"https://{domain}/.well-known/lnurlp/{username}\"\n"
  },
  {
    "path": "electrum/lnutil.py",
    "content": "# Copyright (C) 2018 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\nfrom enum import IntFlag, IntEnum\nimport enum\nfrom collections import defaultdict\nfrom typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence\nimport sys\nimport time\nfrom functools import lru_cache\n\nimport electrum_ecc as ecc\nfrom electrum_ecc import CURVE_ORDER, ecdsa_sig64_from_der_sig\nfrom electrum_ecc.util import bip340_tagged_hash\nimport dataclasses\nimport attr\n\nfrom .util import bfh, UserFacingException, list_enabled_bits, is_hex_str\nfrom .util import ShortID as ShortChannelID, format_short_id as format_short_channel_id\n\nfrom .crypto import sha256, pw_decode_with_version_and_mac\nfrom .transaction import (\n    Transaction, PartialTransaction, PartialTxInput, TxOutpoint, PartialTxOutput, opcodes, OPPushDataPubkey\n)\nfrom . import bitcoin, crypto, transaction, descriptor, segwit_addr\nfrom .bitcoin import redeem_script_to_address, address_to_script, construct_witness, \\\n    construct_script, NLOCKTIME_BLOCKHEIGHT_MAX\nfrom .i18n import _\nfrom .bip32 import BIP32Node, BIP32_PRIME\nfrom .transaction import BCDataStream, OPPushDataGeneric\nfrom .logging import get_logger\nfrom .fee_policy import FEERATE_PER_KW_MIN_RELAY_LIGHTNING\nfrom .json_db import StoredObject, stored_in, stored_as\n\n\nif TYPE_CHECKING:\n    from .lnchannel import Channel, AbstractChannel\n    from .lnrouter import LNPaymentRoute\n    from .lnonion import OnionRoutingFailure\n    from .simple_config import SimpleConfig\n\n\n_logger = get_logger(__name__)\n\n\n# defined in BOLT-03:\nHTLC_TIMEOUT_WEIGHT = 663\nHTLC_TIMEOUT_WEIGHT_ANCHORS = 666\nHTLC_SUCCESS_WEIGHT = 703\nHTLC_SUCCESS_WEIGHT_ANCHORS = 706\nCOMMITMENT_TX_WEIGHT = 724\nCOMMITMENT_TX_WEIGHT_ANCHORS = 1124\nHTLC_OUTPUT_WEIGHT = 172\nFIXED_ANCHOR_SAT = 330\n\nLN_MAX_FUNDING_SAT_LEGACY = pow(2, 24) - 1\nDUST_LIMIT_MAX = 1000\n\nSCRIPT_TEMPLATE_FUNDING = [opcodes.OP_2, OPPushDataPubkey, OPPushDataPubkey, opcodes.OP_2, opcodes.OP_CHECKMULTISIG]\n\n\ndef channel_id_from_funding_tx(funding_txid: str, funding_index: int) -> Tuple[bytes, bytes]:\n    funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]\n    i = int.from_bytes(funding_txid_bytes, 'big') ^ funding_index\n    return i.to_bytes(32, 'big'), funding_txid_bytes\n\n\ndef hex_to_bytes(arg: Optional[Union[bytes, str]]) -> Optional[bytes]:\n    return arg if isinstance(arg, bytes) else bytes.fromhex(arg) if arg is not None else None\n\n\ndef bytes_to_hex(arg: Optional[bytes]) -> Optional[str]:\n    return repr(arg.hex()) if arg is not None else None\n\n\ndef json_to_keypair(arg: Union['OnlyPubkeyKeypair', dict]) -> Union['OnlyPubkeyKeypair', 'Keypair']:\n    return arg if isinstance(arg, OnlyPubkeyKeypair) else Keypair(**arg) if len(arg) == 2 else OnlyPubkeyKeypair(**arg)\n\n\ndef serialize_htlc_key(scid: bytes, htlc_id: int) -> str:\n    return scid.hex() + ':%d' % htlc_id\n\n\ndef deserialize_htlc_key(htlc_key: str) -> Tuple[bytes, int]:\n    scid, htlc_id = htlc_key.split(':')\n    return bytes.fromhex(scid), int(htlc_id)\n\n\n@attr.s\nclass OnlyPubkeyKeypair(StoredObject):\n    pubkey = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)\n\n\n@attr.s\nclass Keypair(OnlyPubkeyKeypair):\n    privkey = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)\n\n\n@attr.s\nclass ChannelConfig(StoredObject):\n    # shared channel config fields\n    payment_basepoint = attr.ib(type=OnlyPubkeyKeypair, converter=json_to_keypair)\n    multisig_key = attr.ib(type=OnlyPubkeyKeypair, converter=json_to_keypair)\n    htlc_basepoint = attr.ib(type=OnlyPubkeyKeypair, converter=json_to_keypair)\n    delayed_basepoint = attr.ib(type=OnlyPubkeyKeypair, converter=json_to_keypair)\n    revocation_basepoint = attr.ib(type=OnlyPubkeyKeypair, converter=json_to_keypair)\n    to_self_delay = attr.ib(type=int)  # applies to OTHER ctx\n    dust_limit_sat = attr.ib(type=int)  # applies to SAME ctx\n    max_htlc_value_in_flight_msat = attr.ib(type=int)  # max val of INCOMING htlcs\n    max_accepted_htlcs = attr.ib(type=int)  # max num of INCOMING htlcs\n    initial_msat = attr.ib(type=int)\n    reserve_sat = attr.ib(type=int)  # applies to OTHER ctx\n    htlc_minimum_msat = attr.ib(type=int)  # smallest value for INCOMING htlc\n    upfront_shutdown_script = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)\n    announcement_node_sig = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)\n    announcement_bitcoin_sig = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)\n\n    def validate_params(self, *, funding_sat: int, config: 'SimpleConfig', peer_features: 'LnFeatures') -> None:\n        conf_name = type(self).__name__\n        for key in (\n                self.payment_basepoint,\n                self.multisig_key,\n                self.htlc_basepoint,\n                self.delayed_basepoint,\n                self.revocation_basepoint\n        ):\n            if not (len(key.pubkey) == 33 and ecc.ECPubkey.is_pubkey_bytes(key.pubkey)):\n                raise Exception(f\"{conf_name}. invalid pubkey in channel config\")\n        if funding_sat < MIN_FUNDING_SAT:\n            raise Exception(f\"funding_sat too low: {funding_sat} sat < {MIN_FUNDING_SAT}\")\n        if not peer_features.supports(LnFeatures.OPTION_SUPPORT_LARGE_CHANNEL_OPT):\n            # MUST set funding_satoshis to less than 2^24 satoshi\n            if funding_sat > LN_MAX_FUNDING_SAT_LEGACY:\n                raise Exception(f\"funding_sat too high: {funding_sat} sat > {LN_MAX_FUNDING_SAT_LEGACY} (legacy limit)\")\n        if funding_sat > config.LIGHTNING_MAX_FUNDING_SAT:\n            raise Exception(f\"funding_sat too high: {funding_sat} sat > {config.LIGHTNING_MAX_FUNDING_SAT} (config setting)\")\n        # MUST set push_msat to equal or less than 1000 * funding_satoshis\n        if not (0 <= self.initial_msat <= 1000 * funding_sat):\n            raise Exception(f\"{conf_name}. insane initial_msat={self.initial_msat}. (funding_sat={funding_sat})\")\n        if self.reserve_sat < self.dust_limit_sat:\n            raise Exception(f\"{conf_name}. MUST set channel_reserve_satoshis greater than or equal to dust_limit_satoshis\")\n        if self.dust_limit_sat < bitcoin.DUST_LIMIT_UNKNOWN_SEGWIT:\n            raise Exception(f\"{conf_name}. dust limit too low: {self.dust_limit_sat} sat\")\n        if self.dust_limit_sat > DUST_LIMIT_MAX:\n            raise Exception(f\"{conf_name}. dust limit too high: {self.dust_limit_sat} sat\")\n        if self.reserve_sat > funding_sat // 100:\n            raise Exception(f\"{conf_name}. reserve too high: {self.reserve_sat}, funding_sat: {funding_sat}\")\n        if self.htlc_minimum_msat > 1_000:\n            raise Exception(f\"{conf_name}. htlc_minimum_msat too high: {self.htlc_minimum_msat} msat\")\n        HTLC_MINIMUM_MSAT_MIN = 0  # should be at least 1 really, but apparently some nodes are sending zero...\n        if self.htlc_minimum_msat < HTLC_MINIMUM_MSAT_MIN:\n            raise Exception(f\"{conf_name}. htlc_minimum_msat too low: {self.htlc_minimum_msat} msat < {HTLC_MINIMUM_MSAT_MIN}\")\n        if self.max_accepted_htlcs < 5:\n            raise Exception(f\"{conf_name}. max_accepted_htlcs too low: {self.max_accepted_htlcs}\")\n        if self.max_accepted_htlcs > 483:\n            raise Exception(f\"{conf_name}. max_accepted_htlcs too high: {self.max_accepted_htlcs}\")\n        if self.to_self_delay > MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED:\n            raise Exception(f\"{conf_name}. to_self_delay too high: {self.to_self_delay} > {MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED}\")\n        if self.max_htlc_value_in_flight_msat < min(1000 * funding_sat, 90_000_000):\n            raise Exception(f\"{conf_name}. max_htlc_value_in_flight_msat is too small: {self.max_htlc_value_in_flight_msat}\")\n\n    @classmethod\n    def cross_validate_params(\n            cls,\n            *,\n            local_config: 'LocalConfig',\n            remote_config: 'RemoteConfig',\n            funding_sat: int,\n            is_local_initiator: bool,  # whether we are the funder\n            initial_feerate_per_kw: int,\n            config: 'SimpleConfig',\n            peer_features: 'LnFeatures',\n            channel_type: 'ChannelType',\n    ) -> None:\n        has_anchors = bool(channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX)\n        # first we validate the configs separately\n        local_config.validate_params(funding_sat=funding_sat, config=config, peer_features=peer_features)\n        remote_config.validate_params(funding_sat=funding_sat, config=config, peer_features=peer_features)\n        # now do tests that need access to both configs\n        if is_local_initiator:\n            funder, fundee = LOCAL, REMOTE\n            funder_config, fundee_config = local_config, remote_config\n        else:\n            funder, fundee = REMOTE, LOCAL\n            funder_config, fundee_config = remote_config, local_config\n        # if channel_reserve_satoshis is less than dust_limit_satoshis within the open_channel message:\n        #     MUST reject the channel.\n        if remote_config.reserve_sat < local_config.dust_limit_sat:\n            raise Exception(\"violated constraint: remote_config.reserve_sat < local_config.dust_limit_sat\")\n        # if channel_reserve_satoshis from the open_channel message is less than dust_limit_satoshis:\n        #     MUST reject the channel.\n        if local_config.reserve_sat < remote_config.dust_limit_sat:\n            raise Exception(\"violated constraint: local_config.reserve_sat < remote_config.dust_limit_sat\")\n        # The receiving node MUST fail the channel if:\n        #     the funder's amount for the initial commitment transaction is not\n        #     sufficient for full fee payment.\n        if funder_config.initial_msat < calc_fees_for_commitment_tx(\n                num_htlcs=0,\n                feerate=initial_feerate_per_kw,\n                is_local_initiator=is_local_initiator,\n                has_anchors=has_anchors,\n        )[funder]:\n            raise Exception(\n                \"the funder's amount for the initial commitment transaction \"\n                \"is not sufficient for full fee payment\")\n        # The receiving node MUST fail the channel if:\n        #     both to_local and to_remote amounts for the initial commitment transaction are\n        #     less than or equal to channel_reserve_satoshis (see BOLT 3).\n        if (max(local_config.initial_msat, remote_config.initial_msat)\n                <= 1000 * max(local_config.reserve_sat, remote_config.reserve_sat)):\n            raise Exception(\n                \"both to_local and to_remote amounts for the initial commitment \"\n                \"transaction are less than or equal to channel_reserve_satoshis\")\n        if initial_feerate_per_kw < FEERATE_PER_KW_MIN_RELAY_LIGHTNING:\n            raise Exception(f\"feerate lower than min relay fee. {initial_feerate_per_kw} sat/kw.\")\n\n\n@stored_as('local_config')\n@attr.s\nclass LocalConfig(ChannelConfig):\n    channel_seed = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)  # type: Optional[bytes]\n    funding_locked_received = attr.ib(type=bool)\n    current_commitment_signature = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)\n    current_htlc_signatures = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)\n    per_commitment_secret_seed = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)\n\n    @classmethod\n    def from_seed(cls, **kwargs):\n        channel_seed = kwargs['channel_seed']\n        node = BIP32Node.from_rootseed(channel_seed, xtype='standard')\n\n        def keypair_generator(family: 'LnKeyFamily') -> 'Keypair':\n            return generate_keypair(node, family)\n\n        kwargs['per_commitment_secret_seed'] = keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey\n        if kwargs['multisig_key'] is None:\n            kwargs['multisig_key'] = keypair_generator(LnKeyFamily.MULTISIG)\n        kwargs['htlc_basepoint'] = keypair_generator(LnKeyFamily.HTLC_BASE)\n        kwargs['delayed_basepoint'] = keypair_generator(LnKeyFamily.DELAY_BASE)\n        kwargs['revocation_basepoint'] = keypair_generator(LnKeyFamily.REVOCATION_BASE)\n        static_remotekey = kwargs.pop('static_remotekey')\n        static_payment_key = kwargs.pop('static_payment_key')\n        if static_payment_key:\n            # We derive the payment_basepoint from a static secret (derived from\n            # the wallet seed) and a public nonce that is revealed\n            # when the funding transaction is spent. This way we can restore the\n            # payment_basepoint, needed for sweeping in the event of a force close.\n            kwargs['payment_basepoint'] = derive_payment_basepoint(\n                static_payment_secret=static_payment_key.privkey,\n                funding_pubkey=kwargs['multisig_key'].pubkey\n            )\n        elif static_remotekey:  # we automatically sweep to a wallet address\n            kwargs['payment_basepoint'] = OnlyPubkeyKeypair(static_remotekey)\n        else:\n            # we expect all our channels to use option_static_remotekey, so ending up here likely indicates an issue...\n            kwargs['payment_basepoint'] = keypair_generator(LnKeyFamily.PAYMENT_BASE)\n\n        return LocalConfig(**kwargs)\n\n    def validate_params(self, *, funding_sat: int, config: 'SimpleConfig', peer_features: 'LnFeatures') -> None:\n        conf_name = type(self).__name__\n        # run base checks regardless whether LOCAL/REMOTE config\n        super().validate_params(funding_sat=funding_sat, config=config, peer_features=peer_features)\n        # run some stricter checks on LOCAL config (make sure we ourselves do the sane thing,\n        # even if we are lenient with REMOTE for compatibility reasons)\n        HTLC_MINIMUM_MSAT_MIN = 1\n        if self.htlc_minimum_msat < HTLC_MINIMUM_MSAT_MIN:\n            raise Exception(f\"{conf_name}. htlc_minimum_msat too low: {self.htlc_minimum_msat} msat < {HTLC_MINIMUM_MSAT_MIN}\")\n\n\n@stored_as('remote_config')\n@attr.s\nclass RemoteConfig(ChannelConfig):\n    next_per_commitment_point = attr.ib(type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)\n    current_per_commitment_point = attr.ib(default=None, type=bytes, converter=hex_to_bytes, repr=bytes_to_hex)\n\n\n@stored_in('fee_updates')\n@attr.s\nclass FeeUpdate(StoredObject):\n    rate = attr.ib(type=int)  # in sat/kw\n    ctn_local = attr.ib(default=None, type=int)\n    ctn_remote = attr.ib(default=None, type=int)\n\n\n@stored_as('constraints')\n@attr.s\nclass ChannelConstraints(StoredObject):\n    flags = attr.ib(type=int, converter=int)\n    capacity = attr.ib(type=int)  # in sat\n    is_initiator = attr.ib(type=bool)  # note: sometimes also called \"funder\"\n    funding_txn_minimum_depth = attr.ib(type=int)\n\n\nCHANNEL_BACKUP_VERSION_LATEST = 2\nKNOWN_CHANNEL_BACKUP_VERSIONS = (0, 1, 2, )\nassert CHANNEL_BACKUP_VERSION_LATEST in KNOWN_CHANNEL_BACKUP_VERSIONS\n\n\n@attr.s\nclass ChannelBackupStorage(StoredObject):\n    funding_txid = attr.ib(type=str)\n    funding_index = attr.ib(type=int, converter=int)\n    funding_address = attr.ib(type=str)\n    is_initiator = attr.ib(type=bool)\n\n    def funding_outpoint(self):\n        return Outpoint(self.funding_txid, self.funding_index)\n\n    def channel_id(self):\n        chan_id, _ = channel_id_from_funding_tx(self.funding_txid, self.funding_index)\n        return chan_id\n\n\n@stored_in('onchain_channel_backups')\n@attr.s\nclass OnchainChannelBackupStorage(ChannelBackupStorage):\n    node_id_prefix = attr.ib(type=bytes, converter=hex_to_bytes)  # remote node pubkey\n\n\n@stored_in('imported_channel_backups')\n@attr.s\nclass ImportedChannelBackupStorage(ChannelBackupStorage):\n    node_id = attr.ib(type=bytes, converter=hex_to_bytes)  # remote node pubkey\n    privkey = attr.ib(type=bytes, converter=hex_to_bytes)  # local node privkey\n    host = attr.ib(type=str)\n    port = attr.ib(type=int, converter=int)\n    channel_seed = attr.ib(type=bytes, converter=hex_to_bytes)\n    local_delay = attr.ib(type=int, converter=int)\n    remote_delay = attr.ib(type=int, converter=int)\n    remote_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes)\n    remote_revocation_pubkey = attr.ib(type=bytes, converter=hex_to_bytes)\n    local_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes)  # type: Optional[bytes]\n    multisig_funding_privkey = attr.ib(type=bytes, converter=hex_to_bytes)  # type: Optional[bytes]\n\n    def to_bytes(self) -> bytes:\n        vds = BCDataStream()\n        vds.write_uint16(CHANNEL_BACKUP_VERSION_LATEST)\n        vds.write_boolean(self.is_initiator)\n        vds.write_bytes(self.privkey, 32)\n        vds.write_bytes(self.channel_seed, 32)\n        vds.write_bytes(self.node_id, 33)\n        vds.write_bytes(bfh(self.funding_txid), 32)\n        vds.write_uint16(self.funding_index)\n        vds.write_string(self.funding_address)\n        vds.write_bytes(self.remote_payment_pubkey, 33)\n        vds.write_bytes(self.remote_revocation_pubkey, 33)\n        vds.write_uint16(self.local_delay)\n        vds.write_uint16(self.remote_delay)\n        vds.write_string(self.host)\n        vds.write_uint16(self.port)\n        vds.write_bytes(self.local_payment_pubkey, 33)\n        vds.write_bytes(self.multisig_funding_privkey, 32)\n        return bytes(vds.input)\n\n    @staticmethod\n    def from_bytes(s: bytes) -> 'ImportedChannelBackupStorage':\n        vds = BCDataStream()\n        vds.write(s)\n        version = vds.read_uint16()\n        if version not in KNOWN_CHANNEL_BACKUP_VERSIONS:\n            raise Exception(f\"unknown version for channel backup: {version}\")\n        is_initiator = vds.read_boolean()\n        privkey = vds.read_bytes(32)\n        channel_seed = vds.read_bytes(32)\n        node_id = vds.read_bytes(33)\n        funding_txid = vds.read_bytes(32).hex()\n        funding_index = vds.read_uint16()\n        funding_address = vds.read_string()\n        remote_payment_pubkey = vds.read_bytes(33)\n        remote_revocation_pubkey = vds.read_bytes(33)\n        local_delay = vds.read_uint16()\n        remote_delay = vds.read_uint16()\n        host = vds.read_string()\n        port = vds.read_uint16()\n        if version >= 1:\n            local_payment_pubkey = vds.read_bytes(33)\n        else:\n            local_payment_pubkey = None\n        if version >= 2:\n            multisig_funding_privkey = vds.read_bytes(32)\n        else:\n            multisig_funding_privkey = None\n        return ImportedChannelBackupStorage(\n            is_initiator=is_initiator,\n            privkey=privkey,\n            channel_seed=channel_seed,\n            node_id=node_id,\n            funding_txid=funding_txid,\n            funding_index=funding_index,\n            funding_address=funding_address,\n            remote_payment_pubkey=remote_payment_pubkey,\n            remote_revocation_pubkey=remote_revocation_pubkey,\n            local_delay=local_delay,\n            remote_delay=remote_delay,\n            host=host,\n            port=port,\n            local_payment_pubkey=local_payment_pubkey,\n            multisig_funding_privkey=multisig_funding_privkey,\n        )\n\n    @staticmethod\n    def from_encrypted_str(data: str, *, password: str) -> 'ImportedChannelBackupStorage':\n        if not data.startswith('channel_backup:'):\n            raise ValueError(\"missing or invalid magic bytes\")\n        encrypted = data[15:]\n        decrypted = pw_decode_with_version_and_mac(encrypted, password)\n        return ImportedChannelBackupStorage.from_bytes(decrypted)\n\n\nclass ScriptHtlc(NamedTuple):\n    redeem_script: bytes\n    htlc: 'UpdateAddHtlc'\n\n\n# FIXME duplicate of TxOutpoint in transaction.py??\n@stored_as('funding_outpoint')\n@attr.s\nclass Outpoint(StoredObject):\n    txid = attr.ib(type=str)\n    output_index = attr.ib(type=int)\n\n    def to_str(self):\n        return \"{}:{}\".format(self.txid, self.output_index)\n\n\nclass HtlcLog(NamedTuple):\n    success: bool\n    amount_msat: int  # amount for receiver (e.g. from invoice)\n    route: Optional['LNPaymentRoute'] = None\n    preimage: Optional[bytes] = None\n    error_bytes: Optional[bytes] = None\n    failure_msg: Optional['OnionRoutingFailure'] = None\n    sender_idx: Optional[int] = None\n    trampoline_fee_level: Optional[int] = None\n\n    def formatted_tuple(self):\n        route = self.route\n        route_str = '%d' % len(route)\n        short_channel_id = None\n        if not self.success:\n            sender_idx = self.sender_idx\n            failure_msg = self.failure_msg\n            if sender_idx is not None:\n                try:\n                    short_channel_id = route[sender_idx + 1].short_channel_id\n                except IndexError:\n                    # payment destination reported error\n                    short_channel_id = _(\"Destination node\")\n            message = failure_msg.code_name()\n        else:\n            short_channel_id = route[-1].short_channel_id\n            message = _('Success')\n        chan_str = str(short_channel_id) if short_channel_id else _(\"Unknown\")\n        return route_str, chan_str, message\n\n\nclass LightningError(Exception): pass\nclass UnableToDeriveSecret(LightningError): pass\nclass RemoteMisbehaving(LightningError): pass\nclass NotFoundChanAnnouncementForUpdate(Exception): pass\n\n\nclass InvalidGossipMsg(Exception):\n    \"\"\"e.g. signature check failed\"\"\"\n\n\nclass PaymentFailure(UserFacingException): pass\nclass PaymentSuccess(Exception): pass\n\n\nclass NoPathFound(PaymentFailure):\n    def __str__(self):\n        return _('No path found')\n\n\nclass FeeBudgetExceeded(PaymentFailure):\n    def __str__(self):\n        return _('Fee budget exceeded')\n\n\nclass LNProtocolError(Exception):\n    \"\"\"Raised in peer methods to trigger an error message.\"\"\"\n\n\nclass LNProtocolWarning(Exception):\n    \"\"\"Raised in peer methods to trigger a warning message.\"\"\"\n\n\n# TODO make some of these values configurable?\nREDEEM_AFTER_DOUBLE_SPENT_DELAY = 30\n\n# timeout after which we forget incoming channels if the funding tx has no confirmation\n# https://github.com/lightning/bolts/commit/ba00bf8f4cd85f21bacfc03adcafd4acc7d68382\nCHANNEL_OPENING_TIMEOUT_BLOCKS = 2016\nCHANNEL_OPENING_TIMEOUT_SEC = 14*24*60*60  # 2 weeks\n\n# Small capacity channels are problematic for many reasons. As the onchain fees start to become\n# significant compared to the capacity, things start to break down. e.g. the counterparty\n# force-closing the channel costs much of the funds in the channel.\n# Closing a channel uses ~200 vbytes onchain, feerates could spike to 100 sat/vbyte or even higher;\n# that in itself is already 20_000 sats. This mining fee is reserved and cannot be used for payments.\n# The value below is chosen arbitrarily to be one order of magnitude higher than that.\nMIN_FUNDING_SAT = 200_000\n\n\n##### CLTV-expiry-delta-related values\n# see https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#cltv_expiry_delta-selection\n\n# the minimum cltv_expiry accepted for newly received HTLCs\n# note: when changing, consider Blockchain.is_tip_stale()\nMIN_FINAL_CLTV_DELTA_ACCEPTED = 144\n\n# buffer added to min_final_cltv_delta of created bolt11 invoices to make verifying the cltv delta\n# of incoming payment htlcs reliable even if some blocks have been mined during forwarding\nMIN_FINAL_CLTV_DELTA_BUFFER_INVOICE = 3\n\n# the deadline for offered HTLCs:\n# the deadline after which the channel has to be failed and timed out on-chain\nNBLOCK_DEADLINE_DELTA_AFTER_EXPIRY_FOR_OFFERED_HTLCS = 1\n\n# the deadline for received HTLCs this node has fulfilled:\n# the deadline after which the channel has to be failed and the HTLC fulfilled on-chain before its cltv_expiry\nNBLOCK_DEADLINE_DELTA_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS = 72\n\nNBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE = 28 * 144\n\nMAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED = 2016\n\n# timeout after which we consider a zeroconf channel without funding tx to be failed\nZEROCONF_TIMEOUT = 60 * 10\n\n\nclass RevocationStore:\n    # closely based on code in lightningnetwork/lnd\n\n    START_INDEX = 2 ** 48 - 1\n\n    def __init__(self, storage):\n        if len(storage) == 0:\n            storage['index'] = self.START_INDEX\n            storage['buckets'] = {}\n        self.storage = storage\n        self.buckets = storage['buckets']\n\n    def add_next_entry(self, hsh):\n        index = self.storage['index']\n        new_element = ShachainElement(index=index, secret=hsh)\n        bucket = count_trailing_zeros(index)\n        for i in range(0, bucket):\n            this_bucket = self.buckets[i]\n            e = shachain_derive(new_element, this_bucket.index)\n            if e != this_bucket:\n                raise Exception(\"hash is not derivable: {} {} {}\".format(e.secret.hex(), this_bucket.secret.hex(), this_bucket.index))\n        self.buckets[bucket] = new_element\n        self.storage['index'] = index - 1\n\n    def retrieve_secret(self, index: int) -> bytes:\n        assert index <= self.START_INDEX, index\n        for i in range(0, 49):\n            bucket = self.buckets.get(i)\n            if bucket is None:\n                raise UnableToDeriveSecret()\n            try:\n                element = shachain_derive(bucket, index)\n            except UnableToDeriveSecret:\n                continue\n            return element.secret\n        raise UnableToDeriveSecret()\n\n\ndef count_trailing_zeros(index):\n    \"\"\" BOLT-03 (where_to_put_secret) \"\"\"\n    try:\n        return list(reversed(bin(index)[2:])).index(\"1\")\n    except ValueError:\n        return 48\n\n\ndef shachain_derive(element, to_index):\n    def get_prefix(index, pos):\n        mask = (1 << 64) - 1 - ((1 << pos) - 1)\n        return index & mask\n    from_index = element.index\n    zeros = count_trailing_zeros(from_index)\n    if from_index != get_prefix(to_index, zeros):\n        raise UnableToDeriveSecret(\"prefixes are different; index not derivable\")\n    return ShachainElement(\n        get_per_commitment_secret_from_seed(element.secret, to_index, zeros),\n        to_index)\n\n\nclass ShachainElement(NamedTuple):\n    secret: bytes\n    index: int\n\n    def __str__(self):\n        return \"ShachainElement(\" + self.secret.hex() + \",\" + str(self.index) + \")\"\n\n    @stored_in('buckets', tuple)\n    def read(*x):\n        return ShachainElement(bfh(x[0]), int(x[1]))\n\n\ndef get_per_commitment_secret_from_seed(seed: bytes, i: int, bits: int = 48) -> bytes:\n    \"\"\"Generate per commitment secret.\"\"\"\n    per_commitment_secret = bytearray(seed)\n    for bitindex in range(bits - 1, -1, -1):\n        mask = 1 << bitindex\n        if i & mask:\n            per_commitment_secret[bitindex // 8] ^= 1 << (bitindex % 8)\n            per_commitment_secret = bytearray(sha256(per_commitment_secret))\n    bajts = bytes(per_commitment_secret)\n    return bajts\n\n\ndef secret_to_pubkey(secret: int) -> bytes:\n    assert type(secret) is int\n    return ecc.ECPrivkey.from_secret_scalar(secret).get_public_key_bytes(compressed=True)\n\n\ndef derive_pubkey(basepoint: bytes, per_commitment_point: bytes) -> bytes:\n    p = ecc.ECPubkey(basepoint) + ecc.GENERATOR * ecc.string_to_number(sha256(per_commitment_point + basepoint))\n    return p.get_public_key_bytes()\n\n\ndef derive_privkey(secret: int, per_commitment_point: bytes) -> int:\n    assert type(secret) is int\n    basepoint_bytes = secret_to_pubkey(secret)\n    basepoint = secret + ecc.string_to_number(sha256(per_commitment_point + basepoint_bytes))\n    basepoint %= CURVE_ORDER\n    return basepoint\n\n\ndef derive_blinded_pubkey(basepoint: bytes, per_commitment_point: bytes) -> bytes:\n    k1 = ecc.ECPubkey(basepoint) * ecc.string_to_number(sha256(basepoint + per_commitment_point))\n    k2 = ecc.ECPubkey(per_commitment_point) * ecc.string_to_number(sha256(per_commitment_point + basepoint))\n    return (k1 + k2).get_public_key_bytes()\n\n\ndef derive_blinded_privkey(basepoint_secret: bytes, per_commitment_secret: bytes) -> bytes:\n    basepoint = ecc.ECPrivkey(basepoint_secret).get_public_key_bytes(compressed=True)\n    per_commitment_point = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)\n    k1 = ecc.string_to_number(basepoint_secret) * ecc.string_to_number(sha256(basepoint + per_commitment_point))\n    k2 = ecc.string_to_number(per_commitment_secret) * ecc.string_to_number(sha256(per_commitment_point + basepoint))\n    sum = (k1 + k2) % ecc.CURVE_ORDER\n    return int.to_bytes(sum, length=32, byteorder='big', signed=False)\n\n\ndef derive_payment_basepoint(static_payment_secret: bytes, funding_pubkey: bytes) -> Keypair:\n    assert isinstance(static_payment_secret, bytes)\n    assert isinstance(funding_pubkey, bytes)\n    payment_basepoint = ecc.ECPrivkey(sha256(static_payment_secret + funding_pubkey))\n    return Keypair(\n        pubkey=payment_basepoint.get_public_key_bytes(),\n        privkey=payment_basepoint.get_secret_bytes()\n    )\n\n\ndef derive_multisig_funding_key_if_we_opened(\n    *,\n    funding_root_secret: bytes,\n    remote_node_id_or_prefix: bytes,\n    nlocktime: int,\n) -> Keypair:\n    from .lnworker import NODE_ID_PREFIX_LEN\n    assert isinstance(funding_root_secret, bytes)\n    assert len(funding_root_secret) == 32\n    assert isinstance(remote_node_id_or_prefix, bytes)\n    assert len(remote_node_id_or_prefix) in (NODE_ID_PREFIX_LEN, 33)\n    assert isinstance(nlocktime, int)\n    nlocktime_bytes = int.to_bytes(nlocktime, length=4, byteorder=\"little\", signed=False)\n    node_id_prefix = remote_node_id_or_prefix[0:NODE_ID_PREFIX_LEN]\n    funding_key = ecc.ECPrivkey(bip340_tagged_hash(\n        tag=b\"electrum/ln_multisig_funding_key/we_opened\",\n        msg=funding_root_secret + node_id_prefix + nlocktime_bytes,\n    ))\n    return Keypair(\n        pubkey=funding_key.get_public_key_bytes(),\n        privkey=funding_key.get_secret_bytes(),\n    )\n\n\ndef derive_multisig_funding_key_if_they_opened(\n    *,\n    funding_root_secret: bytes,\n    remote_node_id_or_prefix: bytes,\n    remote_funding_pubkey: bytes,\n) -> Keypair:\n    from .lnworker import NODE_ID_PREFIX_LEN\n    assert isinstance(funding_root_secret, bytes)\n    assert len(funding_root_secret) == 32\n    assert isinstance(remote_node_id_or_prefix, bytes)\n    assert len(remote_node_id_or_prefix) in (NODE_ID_PREFIX_LEN, 33)\n    assert isinstance(remote_funding_pubkey, bytes)\n    assert len(remote_funding_pubkey) == 33\n    node_id_prefix = remote_node_id_or_prefix[0:NODE_ID_PREFIX_LEN]\n    funding_key = ecc.ECPrivkey(bip340_tagged_hash(\n        tag=b\"electrum/ln_multisig_funding_key/they_opened\",\n        msg=funding_root_secret + node_id_prefix + remote_funding_pubkey,\n    ))\n    return Keypair(\n        pubkey=funding_key.get_public_key_bytes(),\n        privkey=funding_key.get_secret_bytes(),\n    )\n\n\ndef make_htlc_tx_output(\n    amount_msat,\n    local_feerate,\n    revocationpubkey,\n    local_delayedpubkey,\n    success,\n    to_self_delay,\n    has_anchors: bool\n) -> Tuple[bytes, PartialTxOutput]:\n    assert type(amount_msat) is int\n    assert type(local_feerate) is int\n    script = make_commitment_output_to_local_witness_script(\n        revocation_pubkey=revocationpubkey,\n        to_self_delay=to_self_delay,\n        delayed_pubkey=local_delayedpubkey,\n    )\n\n    p2wsh = bitcoin.redeem_script_to_address('p2wsh', script)\n    weight = effective_htlc_tx_weight(success=success, has_anchors=has_anchors)\n    fee = local_feerate * weight\n    fee = fee // 1000 * 1000\n    final_amount_sat = (amount_msat - fee) // 1000\n    assert final_amount_sat > 0, final_amount_sat\n    output = PartialTxOutput.from_address_and_value(p2wsh, final_amount_sat)\n    return script, output\n\n\ndef make_htlc_tx_witness(\n        remotehtlcsig: bytes,\n        localhtlcsig: bytes,\n        payment_preimage: bytes,\n        witness_script: bytes\n) -> bytes:\n    assert type(remotehtlcsig) is bytes\n    assert type(localhtlcsig) is bytes\n    assert type(payment_preimage) is bytes\n    assert type(witness_script) is bytes\n    return construct_witness([0, remotehtlcsig, localhtlcsig, payment_preimage, witness_script])\n\n\ndef make_htlc_tx_inputs(\n        htlc_output_txid: str,\n        htlc_output_index: int,\n        amount_msat: int,\n        witness_script: bytes\n) -> List[PartialTxInput]:\n    assert type(htlc_output_txid) is str\n    assert type(htlc_output_index) is int\n    assert type(amount_msat) is int\n    assert type(witness_script) is bytes\n    txin = PartialTxInput(prevout=TxOutpoint(txid=bfh(htlc_output_txid), out_idx=htlc_output_index),\n                          nsequence=0)\n    txin.witness_script = witness_script\n    txin.script_sig = b''\n    txin._trusted_value_sats = amount_msat // 1000\n    c_inputs = [txin]\n    return c_inputs\n\n\ndef make_htlc_tx(*, cltv_abs: int, inputs: List[PartialTxInput], output: PartialTxOutput) -> PartialTransaction:\n    assert type(cltv_abs) is int\n    c_outputs = [output]\n    tx = PartialTransaction.from_io(inputs, c_outputs, locktime=cltv_abs, version=2)\n    return tx\n\n\ndef make_offered_htlc(\n    *,\n    revocation_pubkey: bytes,\n    remote_htlcpubkey: bytes,\n    local_htlcpubkey: bytes,\n    payment_hash: bytes,\n    has_anchors: bool,\n) -> bytes:\n    assert type(revocation_pubkey) is bytes\n    assert type(remote_htlcpubkey) is bytes\n    assert type(local_htlcpubkey) is bytes\n    assert type(payment_hash) is bytes\n    script_template = witness_template_offered_htlc(anchors=has_anchors)\n    script = construct_script(\n        script_template,\n        values={\n            2: bitcoin.hash_160(revocation_pubkey),\n            7: remote_htlcpubkey,\n            10: 32,\n            16: local_htlcpubkey,\n            21: crypto.ripemd(payment_hash),\n        },\n    )\n    return script\n\n\ndef make_received_htlc(\n    *,\n    revocation_pubkey: bytes,\n    remote_htlcpubkey: bytes,\n    local_htlcpubkey: bytes,\n    payment_hash: bytes,\n    cltv_abs: int,\n    has_anchors: bool,\n) -> bytes:\n    for i in [revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash]:\n        assert type(i) is bytes\n    assert type(cltv_abs) is int\n    script_template = witness_template_received_htlc(anchors=has_anchors)\n    script = construct_script(\n        script_template,\n        values={\n            2: bitcoin.hash_160(revocation_pubkey),\n            7: remote_htlcpubkey,\n            10: 32,\n            14: crypto.ripemd(payment_hash),\n            18: local_htlcpubkey,\n            23: cltv_abs,\n        },\n    )\n    return script\n\n\ndef witness_template_offered_htlc(anchors: bool):\n    return [\n        opcodes.OP_DUP,\n        opcodes.OP_HASH160,\n        OPPushDataGeneric(None),\n        opcodes.OP_EQUAL,\n        opcodes.OP_IF,\n        opcodes.OP_CHECKSIG,\n        opcodes.OP_ELSE,\n        OPPushDataGeneric(None),\n        opcodes.OP_SWAP,\n        opcodes.OP_SIZE,\n        OPPushDataGeneric(lambda x: x==1),\n        opcodes.OP_EQUAL,\n        opcodes.OP_NOTIF,\n        opcodes.OP_DROP,\n        opcodes.OP_2,\n        opcodes.OP_SWAP,\n        OPPushDataGeneric(None),\n        opcodes.OP_2,\n        opcodes.OP_CHECKMULTISIG,\n        opcodes.OP_ELSE,\n        opcodes.OP_HASH160,\n        OPPushDataGeneric(None),\n        opcodes.OP_EQUALVERIFY,\n        opcodes.OP_CHECKSIG,\n        opcodes.OP_ENDIF,\n    ] + ([\n        opcodes.OP_1,\n        opcodes.OP_CHECKSEQUENCEVERIFY,\n        opcodes.OP_DROP,\n    ] if anchors else [\n    ]) + [\n        opcodes.OP_ENDIF,\n    ]\n\n\nWITNESS_TEMPLATE_OFFERED_HTLC = witness_template_offered_htlc(anchors=False)\nWITNESS_TEMPLATE_OFFERED_HTLC_ANCHORS = witness_template_offered_htlc(anchors=True)\n\n\ndef witness_template_received_htlc(anchors: bool):\n    return [\n        opcodes.OP_DUP,\n        opcodes.OP_HASH160,\n        OPPushDataGeneric(None),\n        opcodes.OP_EQUAL,\n        opcodes.OP_IF,\n        opcodes.OP_CHECKSIG,\n        opcodes.OP_ELSE,\n        OPPushDataGeneric(None),\n        opcodes.OP_SWAP,\n        opcodes.OP_SIZE,\n        OPPushDataGeneric(lambda x: x==1),\n        opcodes.OP_EQUAL,\n        opcodes.OP_IF,\n        opcodes.OP_HASH160,\n        OPPushDataGeneric(None),\n        opcodes.OP_EQUALVERIFY,\n        opcodes.OP_2,\n        opcodes.OP_SWAP,\n        OPPushDataGeneric(None),\n        opcodes.OP_2,\n        opcodes.OP_CHECKMULTISIG,\n        opcodes.OP_ELSE,\n        opcodes.OP_DROP,\n        OPPushDataGeneric(None),\n        opcodes.OP_CHECKLOCKTIMEVERIFY,\n        opcodes.OP_DROP,\n        opcodes.OP_CHECKSIG,\n        opcodes.OP_ENDIF,\n    ] + ([\n        opcodes.OP_1,\n        opcodes.OP_CHECKSEQUENCEVERIFY,\n        opcodes.OP_DROP,\n    ] if anchors else [\n    ]) + [\n        opcodes.OP_ENDIF,\n    ]\n\n\nWITNESS_TEMPLATE_RECEIVED_HTLC = witness_template_received_htlc(anchors=False)\nWITNESS_TEMPLATE_RECEIVED_HTLC_ANCHORS = witness_template_received_htlc(anchors=True)\n\n\ndef make_htlc_output_witness_script(\n    *,\n    is_received_htlc: bool,\n    remote_revocation_pubkey: bytes,\n    remote_htlc_pubkey: bytes,\n    local_htlc_pubkey: bytes,\n    payment_hash: bytes,\n    cltv_abs: Optional[int],\n    has_anchors: bool,\n) -> bytes:\n    if is_received_htlc:\n        return make_received_htlc(\n            revocation_pubkey=remote_revocation_pubkey,\n            remote_htlcpubkey=remote_htlc_pubkey,\n            local_htlcpubkey=local_htlc_pubkey,\n            payment_hash=payment_hash,\n            cltv_abs=cltv_abs,\n            has_anchors=has_anchors,\n        )\n    else:\n        return make_offered_htlc(\n            revocation_pubkey=remote_revocation_pubkey,\n            remote_htlcpubkey=remote_htlc_pubkey,\n            local_htlcpubkey=local_htlc_pubkey,\n            payment_hash=payment_hash,\n            has_anchors=has_anchors,\n        )\n\n\ndef get_ordered_channel_configs(\n        chan: 'AbstractChannel',\n        for_us: bool\n) -> Tuple[Union[LocalConfig, RemoteConfig], Union[LocalConfig, RemoteConfig]]:\n    conf =       chan.config[LOCAL] if     for_us else chan.config[REMOTE]\n    other_conf = chan.config[LOCAL] if not for_us else chan.config[REMOTE]\n    return conf, other_conf\n\n\ndef possible_output_idxs_of_htlc_in_ctx(\n        *,\n        chan: 'Channel',\n        pcp: bytes,\n        subject: 'HTLCOwner',\n        htlc_direction: 'Direction',\n        ctx: Transaction,\n        htlc: 'UpdateAddHtlc'\n) -> Set[int]:\n    amount_msat, cltv_abs, payment_hash = htlc.amount_msat, htlc.cltv_abs, htlc.payment_hash\n    for_us = subject == LOCAL\n    conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us)\n\n    other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp)\n    other_htlc_pubkey = derive_pubkey(other_conf.htlc_basepoint.pubkey, pcp)\n    htlc_pubkey = derive_pubkey(conf.htlc_basepoint.pubkey, pcp)\n    witness_script = make_htlc_output_witness_script(\n        is_received_htlc=htlc_direction == RECEIVED,\n        remote_revocation_pubkey=other_revocation_pubkey,\n        remote_htlc_pubkey=other_htlc_pubkey,\n        local_htlc_pubkey=htlc_pubkey,\n        payment_hash=payment_hash,\n        cltv_abs=cltv_abs,\n        has_anchors=chan.has_anchors(),\n    )\n    htlc_address = redeem_script_to_address('p2wsh', witness_script)\n    candidates = ctx.get_output_idxs_from_address(htlc_address)\n    return {output_idx for output_idx in candidates\n            if ctx.outputs()[output_idx].value == htlc.amount_msat // 1000}\n\n\ndef map_htlcs_to_ctx_output_idxs(\n        *,\n        chan: 'Channel',\n        ctx: Transaction, pcp: bytes,\n        subject: 'HTLCOwner',\n        ctn: int\n) -> Dict[Tuple['Direction', 'UpdateAddHtlc'], Tuple[int, int]]:\n    \"\"\"Returns a dict from (htlc_dir, htlc) to (ctx_output_idx, htlc_relative_idx)\"\"\"\n    htlc_to_ctx_output_idx_map = {}  # type: Dict[Tuple[Direction, UpdateAddHtlc], int]\n    unclaimed_ctx_output_idxs = set(range(len(ctx.outputs())))\n    offered_htlcs = chan.included_htlcs(subject, SENT, ctn=ctn)\n    offered_htlcs.sort(key=lambda htlc: htlc.cltv_abs)\n    received_htlcs = chan.included_htlcs(subject, RECEIVED, ctn=ctn)\n    received_htlcs.sort(key=lambda htlc: htlc.cltv_abs)\n    for direction, htlcs in zip([SENT, RECEIVED], [offered_htlcs, received_htlcs]):\n        for htlc in htlcs:\n            cands = sorted(possible_output_idxs_of_htlc_in_ctx(\n                chan=chan, pcp=pcp, subject=subject, htlc_direction=direction, ctx=ctx, htlc=htlc\n            ))\n            for ctx_output_idx in cands:\n                if ctx_output_idx in unclaimed_ctx_output_idxs:\n                    unclaimed_ctx_output_idxs.discard(ctx_output_idx)\n                    htlc_to_ctx_output_idx_map[(direction, htlc)] = ctx_output_idx\n                    break\n    # calc htlc_relative_idx\n    inverse_map = {ctx_output_idx: (direction, htlc)\n                   for ((direction, htlc), ctx_output_idx) in htlc_to_ctx_output_idx_map.items()}\n\n    return {inverse_map[ctx_output_idx]: (ctx_output_idx, htlc_relative_idx)\n            for htlc_relative_idx, ctx_output_idx in enumerate(sorted(inverse_map))}\n\n\ndef make_htlc_tx_with_open_channel(\n        *, chan: 'Channel',\n        pcp: bytes,\n        subject: 'HTLCOwner',\n        ctn: int,\n        htlc_direction: 'Direction',\n        commit: Transaction,\n        ctx_output_idx: int,\n        htlc: 'UpdateAddHtlc',\n        name: str = None\n) -> Tuple[bytes, PartialTransaction]:\n    amount_msat, cltv_abs, payment_hash = htlc.amount_msat, htlc.cltv_abs, htlc.payment_hash\n    for_us = subject == LOCAL\n    conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us)\n\n    delayedpubkey = derive_pubkey(conf.delayed_basepoint.pubkey, pcp)\n    other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp)\n    other_htlc_pubkey = derive_pubkey(other_conf.htlc_basepoint.pubkey, pcp)\n    htlc_pubkey = derive_pubkey(conf.htlc_basepoint.pubkey, pcp)\n    # HTLC-success for the HTLC spending from a received HTLC output\n    # if we do not receive, and the commitment tx is not for us, they receive, so it is also an HTLC-success\n    is_htlc_success = htlc_direction == RECEIVED\n    witness_script_of_htlc_tx_output, htlc_tx_output = make_htlc_tx_output(\n        amount_msat=amount_msat,\n        local_feerate=chan.get_feerate(subject, ctn=ctn),\n        revocationpubkey=other_revocation_pubkey,\n        local_delayedpubkey=delayedpubkey,\n        success=is_htlc_success,\n        to_self_delay=other_conf.to_self_delay,\n        has_anchors=chan.has_anchors(),\n    )\n    witness_script_in = make_htlc_output_witness_script(\n        is_received_htlc=is_htlc_success,\n        remote_revocation_pubkey=other_revocation_pubkey,\n        remote_htlc_pubkey=other_htlc_pubkey,\n        local_htlc_pubkey=htlc_pubkey,\n        payment_hash=payment_hash,\n        cltv_abs=cltv_abs,\n        has_anchors=chan.has_anchors(),\n    )\n    htlc_tx_inputs = make_htlc_tx_inputs(\n        commit.txid(), ctx_output_idx,\n        amount_msat=amount_msat,\n        witness_script=witness_script_in)\n    if chan.has_anchors():\n        htlc_tx_inputs[0].nsequence = 1\n    if is_htlc_success:\n        cltv_abs = 0\n    htlc_tx = make_htlc_tx(cltv_abs=cltv_abs, inputs=htlc_tx_inputs, output=htlc_tx_output)\n    return witness_script_of_htlc_tx_output, htlc_tx\n\n\ndef make_funding_input(\n    local_funding_pubkey: bytes,\n    remote_funding_pubkey: bytes,\n    funding_pos: int,\n    funding_txid: str,\n    funding_sat: int\n) -> PartialTxInput:\n\n    pubkeys = sorted([local_funding_pubkey.hex(), remote_funding_pubkey.hex()])\n    # commitment tx input\n    prevout = TxOutpoint(txid=bfh(funding_txid), out_idx=funding_pos)\n    c_input = PartialTxInput(prevout=prevout)\n\n    ppubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys]\n    multi = descriptor.MultisigDescriptor(pubkeys=ppubkeys, thresh=2, is_sorted=True)\n    c_input.script_descriptor = descriptor.WSHDescriptor(subdescriptor=multi)\n    c_input._trusted_value_sats = funding_sat\n    return c_input\n\n\nclass HTLCOwner(IntEnum):\n    LOCAL = 1\n    REMOTE = -LOCAL\n\n    def inverted(self) -> 'HTLCOwner':\n        return -self\n\n    def __neg__(self) -> 'HTLCOwner':\n        return HTLCOwner(super().__neg__())\n\n\n# part of lightning_payments db keys\nclass Direction(IntEnum):\n    SENT = -1     # in the context of HTLCs: \"offered\" HTLCs\n    RECEIVED = 1  # in the context of HTLCs: \"received\" HTLCs\n\n\nSENT = Direction.SENT\nRECEIVED = Direction.RECEIVED\n\nLOCAL = HTLCOwner.LOCAL\nREMOTE = HTLCOwner.REMOTE\n\n\ndef make_commitment_outputs(\n    *,\n    fees_per_participant: Mapping[HTLCOwner, int],\n    local_amount_msat: int,\n    remote_amount_msat: int,\n    local_script: bytes,\n    remote_script: bytes,\n    htlcs: List[ScriptHtlc],\n    dust_limit_sat: int,\n    has_anchors: bool,\n    local_anchor_script: Optional[str],\n    remote_anchor_script: Optional[str]\n) -> Tuple[List[PartialTxOutput], List[PartialTxOutput]]:\n\n    # determine HTLC outputs and trim below dust to know if anchors need to be included\n    htlc_outputs = []\n    for script, htlc in htlcs:\n        addr = bitcoin.redeem_script_to_address('p2wsh', script)\n        if htlc.amount_msat // 1000 > dust_limit_sat:\n            htlc_outputs.append(\n                PartialTxOutput(\n                    scriptpubkey=address_to_script(addr),\n                    value=htlc.amount_msat // 1000\n                ))\n\n    # BOLT-03: \"Base commitment transaction fees are extracted from the funder's amount;\n    #           if that amount is insufficient, the entire amount of the funder's output is used.\"\n    non_htlc_outputs = []\n    to_local_amt_msat = local_amount_msat - fees_per_participant[LOCAL]\n    to_remote_amt_msat = remote_amount_msat - fees_per_participant[REMOTE]\n\n    anchor_outputs = []\n    # if no anchor scripts are set, we ignore anchor outputs, useful when this\n    # function is used to determine outputs for a collaborative close\n    if has_anchors and local_anchor_script and remote_anchor_script:\n        local_pays_anchors = bool(fees_per_participant[LOCAL])\n        # we always allocate for two anchor outputs even if they are not added\n        if local_pays_anchors:\n            to_local_amt_msat -= 2 * FIXED_ANCHOR_SAT * 1000\n        else:\n            to_remote_amt_msat -= 2 * FIXED_ANCHOR_SAT * 1000\n\n        # include anchors for outputs that materialize, include both if there are HTLCs present\n        if to_local_amt_msat // 1000 >= dust_limit_sat or htlc_outputs:\n            anchor_outputs.append(PartialTxOutput(scriptpubkey=local_anchor_script, value=FIXED_ANCHOR_SAT))\n        if to_remote_amt_msat // 1000 >= dust_limit_sat or htlc_outputs:\n            anchor_outputs.append(PartialTxOutput(scriptpubkey=remote_anchor_script, value=FIXED_ANCHOR_SAT))\n\n    # if funder cannot afford feerate, their output might go negative, so take max(0, x) here\n    to_local_amt_msat = max(0, to_local_amt_msat)\n    to_remote_amt_msat = max(0, to_remote_amt_msat)\n    non_htlc_outputs.append(PartialTxOutput(scriptpubkey=local_script, value=to_local_amt_msat // 1000))\n    non_htlc_outputs.append(PartialTxOutput(scriptpubkey=remote_script, value=to_remote_amt_msat // 1000))\n\n    c_outputs_filtered = list(filter(lambda x: x.value >= dust_limit_sat, non_htlc_outputs + htlc_outputs))\n    c_outputs = c_outputs_filtered + anchor_outputs\n    return htlc_outputs, c_outputs\n\n\ndef effective_htlc_tx_weight(success: bool, has_anchors: bool):\n    # for anchors-zero-fee-htlc we set an effective weight of zero\n    # we only trim htlcs below dust, as in the anchors commitment format,\n    # the fees for the hltc transaction don't need to be subtracted from\n    # the htlc output, but fees are taken from extra attached inputs\n    if has_anchors:\n        return 0 * HTLC_SUCCESS_WEIGHT_ANCHORS if success else 0 * HTLC_TIMEOUT_WEIGHT_ANCHORS\n    else:\n        return HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT\n\n\ndef offered_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int, has_anchors: bool) -> int:\n    # offered htlcs strictly below this amount will be trimmed (from ctx).\n    # feerate is in sat/kw\n    # returns value in sat\n    weight = effective_htlc_tx_weight(success=False, has_anchors=has_anchors)\n    return dust_limit_sat + weight * feerate // 1000\n\n\ndef received_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int, has_anchors: bool) -> int:\n    # received htlcs strictly below this amount will be trimmed (from ctx).\n    # feerate is in sat/kw\n    # returns value in sat\n    weight = effective_htlc_tx_weight(success=True, has_anchors=has_anchors)\n    return dust_limit_sat + weight * feerate // 1000\n\n\ndef fee_for_htlc_output(*, feerate: int) -> int:\n    # feerate is in sat/kw\n    # returns fee in msat\n    return feerate * HTLC_OUTPUT_WEIGHT\n\n\ndef calc_fees_for_commitment_tx(\n        *, num_htlcs: int,\n        feerate: int,\n        is_local_initiator: bool,\n        round_to_sat: bool = True,\n        has_anchors: bool\n) -> Dict['HTLCOwner', int]:\n    # feerate is in sat/kw\n    # returns fees in msats\n    # note: BOLT-02 specifies that msat fees need to be rounded down to sat.\n    #       However, the rounding needs to happen for the total fees, so if the return value\n    #       is to be used as part of additional fee calculation then rounding should be done after that.\n    if has_anchors:\n        commitment_tx_weight = COMMITMENT_TX_WEIGHT_ANCHORS\n    else:\n        commitment_tx_weight = COMMITMENT_TX_WEIGHT\n    overall_weight = commitment_tx_weight + num_htlcs * HTLC_OUTPUT_WEIGHT\n    fee = feerate * overall_weight\n    if round_to_sat:\n        fee = fee // 1000 * 1000\n    return {\n        LOCAL: fee if is_local_initiator else 0,\n        REMOTE: fee if not is_local_initiator else 0,\n    }\n\n\ndef make_commitment(\n        *,\n        ctn: int,\n        local_funding_pubkey: bytes,\n        remote_funding_pubkey: bytes,\n        remote_payment_pubkey: bytes,\n        funder_payment_basepoint: bytes,\n        fundee_payment_basepoint: bytes,\n        revocation_pubkey: bytes,\n        delayed_pubkey: bytes,\n        to_self_delay: int,\n        funding_txid: str,\n        funding_pos: int,\n        funding_sat: int,\n        local_amount: int,\n        remote_amount: int,\n        dust_limit_sat: int,\n        fees_per_participant: Mapping[HTLCOwner, int],\n        htlcs: List[ScriptHtlc],\n        has_anchors: bool\n) -> PartialTransaction:\n    c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey,\n                                 funding_pos, funding_txid, funding_sat)\n    obs = get_obscured_ctn(ctn, funder_payment_basepoint, fundee_payment_basepoint)\n    locktime = (0x20 << 24) + (obs & 0xffffff)\n    sequence = (0x80 << 24) + (obs >> 24)\n    c_input.nsequence = sequence\n\n    c_inputs = [c_input]\n\n    # commitment tx outputs\n    local_address = make_commitment_output_to_local_address(revocation_pubkey, to_self_delay, delayed_pubkey)\n    remote_address = make_commitment_output_to_remote_address(remote_payment_pubkey, has_anchors)\n    local_anchor_address = None\n    remote_anchor_address = None\n    if has_anchors:\n        local_anchor_address = make_commitment_output_to_anchor_address(local_funding_pubkey)\n        remote_anchor_address = make_commitment_output_to_anchor_address(remote_funding_pubkey)\n    # note: it is assumed that the given 'htlcs' are all non-dust (dust htlcs already trimmed)\n\n    # BOLT-03: \"Transaction Input and Output Ordering\n    #           Lexicographic ordering: see BIP69. In the case of identical HTLC outputs,\n    #           the outputs are ordered in increasing cltv_expiry order.\"\n    # so we sort by cltv_expiry now; and the later BIP69-sort is assumed to be *stable*\n    htlcs = list(htlcs)\n    htlcs.sort(key=lambda x: x.htlc.cltv_abs)\n\n    htlc_outputs, c_outputs_filtered = make_commitment_outputs(\n        fees_per_participant=fees_per_participant,\n        local_amount_msat=local_amount,\n        remote_amount_msat=remote_amount,\n        local_script=address_to_script(local_address),\n        remote_script=address_to_script(remote_address),\n        htlcs=htlcs,\n        dust_limit_sat=dust_limit_sat,\n        has_anchors=has_anchors,\n        local_anchor_script=address_to_script(local_anchor_address) if local_anchor_address else None,\n        remote_anchor_script=address_to_script(remote_anchor_address) if remote_anchor_address else None\n    )\n\n    assert sum(x.value for x in c_outputs_filtered) <= funding_sat, (c_outputs_filtered, funding_sat)\n\n    # create commitment tx\n    tx = PartialTransaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2)\n    return tx\n\n\ndef make_commitment_output_to_local_witness_script(\n        revocation_pubkey: bytes,\n        to_self_delay: int,\n        delayed_pubkey: bytes,\n) -> bytes:\n    assert type(revocation_pubkey) is bytes\n    assert type(to_self_delay) is int\n    assert type(delayed_pubkey) is bytes\n    script = construct_script([\n        opcodes.OP_IF,\n        revocation_pubkey,\n        opcodes.OP_ELSE,\n        to_self_delay,\n        opcodes.OP_CHECKSEQUENCEVERIFY,\n        opcodes.OP_DROP,\n        delayed_pubkey,\n        opcodes.OP_ENDIF,\n        opcodes.OP_CHECKSIG,\n    ])\n    return script\n\n\ndef make_commitment_output_to_local_address(\n        revocation_pubkey: bytes, to_self_delay: int, delayed_pubkey: bytes) -> str:\n    local_script = make_commitment_output_to_local_witness_script(revocation_pubkey, to_self_delay, delayed_pubkey)\n    return bitcoin.redeem_script_to_address('p2wsh', local_script)\n\n\ndef make_commitment_output_to_remote_witness_script(remote_payment_pubkey: bytes) -> bytes:\n    assert isinstance(remote_payment_pubkey, bytes)\n    script = construct_script([\n        remote_payment_pubkey,\n        opcodes.OP_CHECKSIGVERIFY,\n        opcodes.OP_1,\n        opcodes.OP_CHECKSEQUENCEVERIFY,\n    ])\n    return script\n\n\ndef make_commitment_output_to_remote_address(remote_payment_pubkey: bytes, has_anchors: bool) -> str:\n    if has_anchors:\n        remote_script = make_commitment_output_to_remote_witness_script(remote_payment_pubkey)\n        return bitcoin.redeem_script_to_address('p2wsh', remote_script)\n    else:\n        return bitcoin.pubkey_to_address('p2wpkh', remote_payment_pubkey.hex())\n\n\ndef make_commitment_output_to_anchor_witness_script(funding_pubkey: bytes) -> bytes:\n    assert isinstance(funding_pubkey, bytes)\n    script = construct_script([\n        funding_pubkey,\n        opcodes.OP_CHECKSIG,\n        opcodes.OP_IFDUP,\n        opcodes.OP_NOTIF,\n        opcodes.OP_16,\n        opcodes.OP_CHECKSEQUENCEVERIFY,\n        opcodes.OP_ENDIF,\n    ])\n    return script\n\n\ndef make_commitment_output_to_anchor_address(funding_pubkey: bytes) -> str:\n    script = make_commitment_output_to_anchor_witness_script(funding_pubkey)\n    return bitcoin.redeem_script_to_address('p2wsh', script)\n\n\ndef sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config):\n    tx.sign({local_config.multisig_key.pubkey: local_config.multisig_key.privkey})\n    sig = tx.inputs()[0].sigs_ecdsa[local_config.multisig_key.pubkey]\n    sig_64 = ecdsa_sig64_from_der_sig(sig[:-1])\n    return sig_64\n\n\ndef funding_output_script(local_config: 'LocalConfig', remote_config: 'RemoteConfig') -> bytes:\n    return funding_output_script_from_keys(local_config.multisig_key.pubkey, remote_config.multisig_key.pubkey)\n\n\ndef funding_output_script_from_keys(pubkey1: bytes, pubkey2: bytes) -> bytes:\n    pubkeys = sorted([pubkey1.hex(), pubkey2.hex()])\n    return transaction.multisig_script(pubkeys, 2)\n\n\ndef get_obscured_ctn(ctn: int, funder: bytes, fundee: bytes) -> int:\n    mask = int.from_bytes(sha256(funder + fundee)[-6:], 'big')\n    return ctn ^ mask\n\n\ndef extract_ctn_from_tx(tx: Transaction, txin_index: int, funder_payment_basepoint: bytes,\n                        fundee_payment_basepoint: bytes) -> int:\n    tx.deserialize()\n    locktime = tx.locktime\n    sequence = tx.inputs()[txin_index].nsequence\n    obs = ((sequence & 0xffffff) << 24) + (locktime & 0xffffff)\n    return get_obscured_ctn(obs, funder_payment_basepoint, fundee_payment_basepoint)\n\n\ndef extract_ctn_from_tx_and_chan(tx: Transaction, chan: 'AbstractChannel') -> int:\n    funder_conf = chan.config[LOCAL] if     chan.is_initiator() else chan.config[REMOTE]\n    fundee_conf = chan.config[LOCAL] if not chan.is_initiator() else chan.config[REMOTE]\n    return extract_ctn_from_tx(tx, txin_index=0,\n                               funder_payment_basepoint=funder_conf.payment_basepoint.pubkey,\n                               fundee_payment_basepoint=fundee_conf.payment_basepoint.pubkey)\n\n\ndef ctx_has_anchors(tx: Transaction):\n    output_values = [output.value for output in tx.outputs()]\n    if FIXED_ANCHOR_SAT in output_values:\n        return True\n    else:\n        return False\n\n\nclass LnFeatureContexts(enum.Flag):\n    INIT = enum.auto()\n    NODE_ANN = enum.auto()\n    CHAN_ANN_AS_IS = enum.auto()\n    CHAN_ANN_ALWAYS_ODD = enum.auto()\n    CHAN_ANN_ALWAYS_EVEN = enum.auto()\n    INVOICE = enum.auto()\n\n\nLNFC = LnFeatureContexts\n\n_ln_feature_direct_dependencies = defaultdict(set)  # type: Dict[LnFeatures, Set[LnFeatures]]\n_ln_feature_contexts = {}  # type: Dict[LnFeatures, LnFeatureContexts]\n\n\nclass LnFeatures(IntFlag):\n    OPTION_DATA_LOSS_PROTECT_REQ = 1 << 0\n    OPTION_DATA_LOSS_PROTECT_OPT = 1 << 1\n    _ln_feature_contexts[OPTION_DATA_LOSS_PROTECT_OPT] = (LNFC.INIT | LnFeatureContexts.NODE_ANN)\n    _ln_feature_contexts[OPTION_DATA_LOSS_PROTECT_REQ] = (LNFC.INIT | LnFeatureContexts.NODE_ANN)\n\n    INITIAL_ROUTING_SYNC = 1 << 3\n    _ln_feature_contexts[INITIAL_ROUTING_SYNC] = LNFC.INIT\n\n    OPTION_UPFRONT_SHUTDOWN_SCRIPT_REQ = 1 << 4\n    OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT = 1 << 5\n    _ln_feature_contexts[OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT] = (LNFC.INIT | LNFC.NODE_ANN)\n    _ln_feature_contexts[OPTION_UPFRONT_SHUTDOWN_SCRIPT_REQ] = (LNFC.INIT | LNFC.NODE_ANN)\n\n    GOSSIP_QUERIES_REQ = 1 << 6\n    GOSSIP_QUERIES_OPT = 1 << 7\n    _ln_feature_contexts[GOSSIP_QUERIES_OPT] = (LNFC.INIT | LNFC.NODE_ANN)\n    _ln_feature_contexts[GOSSIP_QUERIES_REQ] = (LNFC.INIT | LNFC.NODE_ANN)\n\n    VAR_ONION_REQ = 1 << 8\n    VAR_ONION_OPT = 1 << 9\n    _ln_feature_contexts[VAR_ONION_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)\n    _ln_feature_contexts[VAR_ONION_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)\n\n    GOSSIP_QUERIES_EX_REQ = 1 << 10\n    GOSSIP_QUERIES_EX_OPT = 1 << 11\n    _ln_feature_direct_dependencies[GOSSIP_QUERIES_EX_OPT] = {GOSSIP_QUERIES_OPT}\n    _ln_feature_contexts[GOSSIP_QUERIES_EX_OPT] = (LNFC.INIT | LNFC.NODE_ANN)\n    _ln_feature_contexts[GOSSIP_QUERIES_EX_REQ] = (LNFC.INIT | LNFC.NODE_ANN)\n\n    OPTION_STATIC_REMOTEKEY_REQ = 1 << 12\n    OPTION_STATIC_REMOTEKEY_OPT = 1 << 13\n    _ln_feature_contexts[OPTION_STATIC_REMOTEKEY_OPT] = (LNFC.INIT | LNFC.NODE_ANN)\n    _ln_feature_contexts[OPTION_STATIC_REMOTEKEY_REQ] = (LNFC.INIT | LNFC.NODE_ANN)\n\n    PAYMENT_SECRET_REQ = 1 << 14\n    PAYMENT_SECRET_OPT = 1 << 15\n    _ln_feature_direct_dependencies[PAYMENT_SECRET_OPT] = {VAR_ONION_OPT}\n    _ln_feature_contexts[PAYMENT_SECRET_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)\n    _ln_feature_contexts[PAYMENT_SECRET_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)\n\n    BASIC_MPP_REQ = 1 << 16\n    BASIC_MPP_OPT = 1 << 17\n    _ln_feature_direct_dependencies[BASIC_MPP_OPT] = {PAYMENT_SECRET_OPT}\n    _ln_feature_contexts[BASIC_MPP_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)\n    _ln_feature_contexts[BASIC_MPP_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)\n\n    OPTION_SUPPORT_LARGE_CHANNEL_REQ = 1 << 18\n    OPTION_SUPPORT_LARGE_CHANNEL_OPT = 1 << 19\n    _ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_OPT] = (LNFC.INIT | LNFC.NODE_ANN)\n    _ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_REQ] = (LNFC.INIT | LNFC.NODE_ANN)\n\n    OPTION_ANCHORS_ZERO_FEE_HTLC_REQ = 1 << 22\n    OPTION_ANCHORS_ZERO_FEE_HTLC_OPT = 1 << 23\n    _ln_feature_direct_dependencies[OPTION_ANCHORS_ZERO_FEE_HTLC_OPT] = {OPTION_STATIC_REMOTEKEY_OPT}\n    _ln_feature_contexts[OPTION_ANCHORS_ZERO_FEE_HTLC_REQ] = (LNFC.INIT | LNFC.NODE_ANN)\n    _ln_feature_contexts[OPTION_ANCHORS_ZERO_FEE_HTLC_OPT] = (LNFC.INIT | LNFC.NODE_ANN)\n\n    # Temporary number.\n    OPTION_TRAMPOLINE_ROUTING_REQ_ECLAIR = 1 << 148\n    OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR = 1 << 149\n\n    _ln_feature_contexts[OPTION_TRAMPOLINE_ROUTING_REQ_ECLAIR] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)\n    _ln_feature_contexts[OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)\n\n    # We use a different bit because Phoenix cannot do end-to-end multi-trampoline routes\n    OPTION_TRAMPOLINE_ROUTING_REQ_ELECTRUM = 1 << 150\n    OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM = 1 << 151\n\n    _ln_feature_contexts[OPTION_TRAMPOLINE_ROUTING_REQ_ELECTRUM] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)\n    _ln_feature_contexts[OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)\n\n    OPTION_SHUTDOWN_ANYSEGWIT_REQ = 1 << 26\n    OPTION_SHUTDOWN_ANYSEGWIT_OPT = 1 << 27\n\n    _ln_feature_contexts[OPTION_SHUTDOWN_ANYSEGWIT_REQ] = (LNFC.INIT | LNFC.NODE_ANN)\n    _ln_feature_contexts[OPTION_SHUTDOWN_ANYSEGWIT_OPT] = (LNFC.INIT | LNFC.NODE_ANN)\n\n    OPTION_ONION_MESSAGE_REQ = 1 << 38\n    OPTION_ONION_MESSAGE_OPT = 1 << 39\n\n    _ln_feature_contexts[OPTION_ONION_MESSAGE_REQ] = (LNFC.INIT | LNFC.NODE_ANN)\n    _ln_feature_contexts[OPTION_ONION_MESSAGE_OPT] = (LNFC.INIT | LNFC.NODE_ANN)\n\n    OPTION_CHANNEL_TYPE_REQ = 1 << 44\n    OPTION_CHANNEL_TYPE_OPT = 1 << 45\n\n    _ln_feature_contexts[OPTION_CHANNEL_TYPE_REQ] = (LNFC.INIT | LNFC.NODE_ANN)\n    _ln_feature_contexts[OPTION_CHANNEL_TYPE_OPT] = (LNFC.INIT | LNFC.NODE_ANN)\n\n    OPTION_SCID_ALIAS_REQ = 1 << 46\n    OPTION_SCID_ALIAS_OPT = 1 << 47\n\n    _ln_feature_contexts[OPTION_SCID_ALIAS_REQ] = (LNFC.INIT | LNFC.NODE_ANN)\n    _ln_feature_contexts[OPTION_SCID_ALIAS_OPT] = (LNFC.INIT | LNFC.NODE_ANN)\n\n    OPTION_ZEROCONF_REQ = 1 << 50\n    OPTION_ZEROCONF_OPT = 1 << 51\n\n    _ln_feature_direct_dependencies[OPTION_ZEROCONF_OPT] = {OPTION_SCID_ALIAS_OPT}\n    _ln_feature_contexts[OPTION_ZEROCONF_REQ] = (LNFC.INIT | LNFC.NODE_ANN)\n    _ln_feature_contexts[OPTION_ZEROCONF_OPT] = (LNFC.INIT | LNFC.NODE_ANN)\n\n    def validate_transitive_dependencies(self) -> bool:\n        # for all even bit set, set corresponding odd bit:\n        features = self  # copy\n        flags = list_enabled_bits(features)\n        for flag in flags:\n            if flag % 2 == 0:\n                features |= 1 << get_ln_flag_pair_of_bit(flag)\n        # Check dependencies. We only check that the direct dependencies of each flag set\n        # are satisfied: this implies that transitive dependencies are also satisfied.\n        flags = list_enabled_bits(features)\n        for flag in flags:\n            for dependency in _ln_feature_direct_dependencies[1 << flag]:\n                if not (dependency & features):\n                    return False\n        return True\n\n    def for_init_message(self) -> 'LnFeatures':\n        features = LnFeatures(0)\n        for flag in list_enabled_ln_feature_bits(self):\n            if LnFeatureContexts.INIT & _ln_feature_contexts[1 << flag]:\n                features |= (1 << flag)\n        return features\n\n    def for_node_announcement(self) -> 'LnFeatures':\n        features = LnFeatures(0)\n        for flag in list_enabled_ln_feature_bits(self):\n            if LnFeatureContexts.NODE_ANN & _ln_feature_contexts[1 << flag]:\n                features |= (1 << flag)\n        return features\n\n    def for_invoice(self) -> 'LnFeatures':\n        features = LnFeatures(0)\n        for flag in list_enabled_ln_feature_bits(self):\n            if LnFeatureContexts.INVOICE & _ln_feature_contexts[1 << flag]:\n                features |= (1 << flag)\n        return features\n\n    def for_channel_announcement(self) -> 'LnFeatures':\n        features = LnFeatures(0)\n        for flag in list_enabled_ln_feature_bits(self):\n            ctxs = _ln_feature_contexts[1 << flag]\n            if LnFeatureContexts.CHAN_ANN_AS_IS & ctxs:\n                features |= (1 << flag)\n            elif LnFeatureContexts.CHAN_ANN_ALWAYS_EVEN & ctxs:\n                if flag % 2 == 0:\n                    features |= (1 << flag)\n            elif LnFeatureContexts.CHAN_ANN_ALWAYS_ODD & ctxs:\n                if flag % 2 == 0:\n                    flag = get_ln_flag_pair_of_bit(flag)\n                features |= (1 << flag)\n        return features\n\n    def min_len(self) -> int:\n        b = int.bit_length(self)\n        return b // 8 + int(bool(b % 8))\n\n    def supports(self, feature: 'LnFeatures') -> bool:\n        \"\"\"Returns whether given feature is enabled.\n\n        Helper function that tries to hide the complexity of even/odd bits.\n        For example, instead of:\n          bool(myfeatures & LnFeatures.VAR_ONION_OPT or myfeatures & LnFeatures.VAR_ONION_REQ)\n        you can do:\n          myfeatures.supports(LnFeatures.VAR_ONION_OPT)\n        \"\"\"\n        if (1 << (feature.bit_length() - 1)) != feature:\n            raise ValueError(f\"'feature' cannot be a combination of features: {feature}\")\n        if feature.bit_length() % 2 == 0:  # feature is OPT\n            feature_other = feature >> 1\n        else:  # feature is REQ\n            feature_other = feature << 1\n        return (self & feature != 0) or (self & feature_other != 0)\n\n    def get_names(self) -> Sequence[str]:\n        r = []\n        for flag in list_enabled_bits(self):\n            feature_name = LnFeatures(1 << flag).name\n            r.append(feature_name or f\"bit_{flag}\")\n        return r\n\n    if hasattr(IntFlag, \"_numeric_repr_\"):  # python 3.11+\n        # performance improvement (avoid base2<->base10), see #8403\n        _numeric_repr_ = hex\n\n    def __repr__(self):\n        # performance improvement (avoid base2<->base10), see #8403\n        return f\"<{self._name_}: {hex(self._value_)}>\"\n\n    def __str__(self):\n        # performance improvement (avoid base2<->base10), see #8403\n        return hex(self._value_)\n\n\n@stored_as('channel_type', _type=None)\nclass ChannelType(IntFlag):\n    OPTION_LEGACY_CHANNEL = 0\n    OPTION_STATIC_REMOTEKEY = 1 << 12\n    OPTION_ANCHORS_ZERO_FEE_HTLC_TX = 1 << 22\n    OPTION_SCID_ALIAS = 1 << 46\n    OPTION_ZEROCONF = 1 << 50\n\n    def discard_unknown_and_check(self) -> 'ChannelType':\n        \"\"\"Discards unknown flags and checks flag combination.\"\"\"\n        flags = list_enabled_bits(self)\n        known_channel_types = []\n        for flag in flags:\n            channel_type = ChannelType(1 << flag)\n            if channel_type.name:\n                known_channel_types.append(channel_type)\n        final_channel_type = known_channel_types[0]\n        for channel_type in known_channel_types[1:]:\n            final_channel_type |= channel_type\n\n        final_channel_type.check_combinations()\n        return final_channel_type\n\n    def check_combinations(self):\n        basic_type = self & ~(ChannelType.OPTION_SCID_ALIAS | ChannelType.OPTION_ZEROCONF)\n        if basic_type not in [\n                ChannelType.OPTION_STATIC_REMOTEKEY,\n                ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX | ChannelType.OPTION_STATIC_REMOTEKEY\n        ]:\n            raise ValueError(\"Channel type is not a valid flag combination.\")\n\n    def complies_with_features(self, features: LnFeatures) -> bool:\n        flags = list_enabled_bits(self)\n        complies = True\n        for flag in flags:\n            feature = LnFeatures(1 << flag)\n            complies &= features.supports(feature)\n        return complies\n\n    def to_bytes_minimal(self):\n        # MUST use the smallest bitmap possible to represent the channel type.\n        bit_length = self.value.bit_length()\n        byte_length = bit_length // 8 + int(bool(bit_length % 8))\n        return self.to_bytes(byte_length, byteorder='big')\n\n    @property\n    def name_minimal(self):\n        if self.name:\n            return self.name.replace('OPTION_', '')\n        else:\n            return str(self)\n\n\ndel LNFC  # name is ambiguous without context\n\n# features that are actually implemented and understood in our codebase:\n# (note: this is not what we send in e.g. init!)\n# (note: specify both OPT and REQ here)\nLN_FEATURES_IMPLEMENTED = (\n        LnFeatures(0)\n        | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\n        | LnFeatures.GOSSIP_QUERIES_OPT | LnFeatures.GOSSIP_QUERIES_REQ\n        | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ\n        | LnFeatures.VAR_ONION_OPT | LnFeatures.VAR_ONION_REQ\n        | LnFeatures.PAYMENT_SECRET_OPT | LnFeatures.PAYMENT_SECRET_REQ\n        | LnFeatures.BASIC_MPP_OPT | LnFeatures.BASIC_MPP_REQ\n        | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM | LnFeatures.OPTION_TRAMPOLINE_ROUTING_REQ_ELECTRUM\n        | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_REQ\n        | LnFeatures.OPTION_CHANNEL_TYPE_OPT | LnFeatures.OPTION_CHANNEL_TYPE_REQ\n        | LnFeatures.OPTION_SCID_ALIAS_OPT | LnFeatures.OPTION_SCID_ALIAS_REQ\n        | LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT | LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_REQ\n)\n\n\ndef get_ln_flag_pair_of_bit(flag_bit: int) -> int:\n    \"\"\"Ln Feature flags are assigned in pairs, one even, one odd. See BOLT-09.\n    Return the other flag from the pair.\n    e.g. 6 -> 7\n    e.g. 7 -> 6\n    \"\"\"\n    if flag_bit % 2 == 0:\n        return flag_bit + 1\n    else:\n        return flag_bit - 1\n\n\nclass GossipTimestampFilter:\n    def __init__(self, first_timestamp: int, timestamp_range: int):\n        self.first_timestamp = first_timestamp\n        self.timestamp_range = timestamp_range\n        # True once we sent them the requested gossip and only forward\n        self.only_forwarding = False\n        if first_timestamp >= int(time.time()) - 20:\n            self.only_forwarding = True\n\n    def __str__(self):\n        return (f\"GossipTimestampFilter | first_timestamp={self.first_timestamp} | \"\n                f\"timestamp_range={self.timestamp_range}\")\n\n    def in_range(self, timestamp: int) -> bool:\n        return self.first_timestamp <= timestamp < self.first_timestamp + self.timestamp_range\n\n    @classmethod\n    def from_payload(cls, payload) -> Optional['GossipTimestampFilter']:\n        try:\n            first_timestamp = payload['first_timestamp']\n            timestamp_range = payload['timestamp_range']\n        except KeyError:\n            return None\n        if first_timestamp >= 0xFFFFFFFF:\n            return None\n        return cls(first_timestamp, timestamp_range)\n\n\nclass GossipForwardingMessage:\n    def __init__(self,\n                 msg: bytes,\n                 scid: Optional[ShortChannelID] = None,\n                 timestamp: Optional[int] = None,\n                 sender_node_id: Optional[bytes] = None):\n        self.scid: Optional[ShortChannelID] = scid\n        self.sender_node_id: Optional[bytes] = sender_node_id\n        self.msg = msg\n        self.timestamp = timestamp\n\n    @classmethod\n    def from_payload(cls, payload: dict) -> Optional['GossipForwardingMessage']:\n        try:\n            msg = payload['raw']\n            scid = ShortChannelID.normalize(payload.get('short_channel_id'))\n            sender_node_id = payload.get('sender_node_id')\n            timestamp = payload.get('timestamp')\n        except KeyError:\n            return None\n        return cls(msg, scid, timestamp, sender_node_id)\n\n\ndef list_enabled_ln_feature_bits(features: int) -> tuple[int, ...]:\n    \"\"\"Returns a list of enabled feature bits. If both opt and req are set, only\n    req will be included in the result.\"\"\"\n    all_enabled_bits = list_enabled_bits(features)\n    single_feature_bits: set[int] = set()\n    for bit in all_enabled_bits:\n        if bit % 2 == 0:  # even bit, always added\n            single_feature_bits.add(bit)\n        elif bit - 1 not in single_feature_bits:\n            # add if we haven't already added the corresponding req (even) bit\n            single_feature_bits.add(bit)\n    return tuple(sorted(single_feature_bits))\n\n\nclass IncompatibleOrInsaneFeatures(Exception): pass\nclass UnknownEvenFeatureBits(IncompatibleOrInsaneFeatures): pass\nclass IncompatibleLightningFeatures(IncompatibleOrInsaneFeatures): pass\n\n\ndef ln_compare_features(our_features: 'LnFeatures', their_features: int) -> 'LnFeatures':\n    \"\"\"Returns negotiated features.\n    Raises IncompatibleLightningFeatures if incompatible.\n    \"\"\"\n    our_flags = set(list_enabled_bits(our_features))\n    their_flags = set(list_enabled_bits(their_features))\n    # check that they have our required features, and disable the optional features they don't have\n    for flag in our_flags:\n        if flag not in their_flags and get_ln_flag_pair_of_bit(flag) not in their_flags:\n            # they don't have this feature we wanted :(\n            if flag % 2 == 0:  # even flags are compulsory\n                raise IncompatibleLightningFeatures(f\"remote does not support {LnFeatures(1 << flag)!r}\")\n            our_features ^= 1 << flag  # disable flag\n        else:\n            # They too have this flag.\n            # For easier feature-bit-testing, if this is an even flag, we also\n            # set the corresponding odd flag now.\n            if flag % 2 == 0 and our_features & (1 << flag):\n                our_features |= 1 << get_ln_flag_pair_of_bit(flag)\n    # check that we have their required features\n    for flag in their_flags:\n        if flag not in our_flags and get_ln_flag_pair_of_bit(flag) not in our_flags:\n            # we don't have this feature they wanted :(\n            if flag % 2 == 0:  # even flags are compulsory\n                raise IncompatibleLightningFeatures(f\"remote wanted feature we don't have: {LnFeatures(1 << flag)!r}\")\n    return our_features\n\n\nif hasattr(sys, \"get_int_max_str_digits\"):\n    # check that the user or other library has not lowered the limit (from default)\n    assert sys.get_int_max_str_digits() >= 4300, f\"sys.get_int_max_str_digits() too low: {sys.get_int_max_str_digits()}\"\n\n\n@lru_cache(maxsize=1000)  # massive speedup for the hot path of channel_db.load_data()\ndef validate_features(features: int) -> LnFeatures:\n    \"\"\"Raises IncompatibleOrInsaneFeatures if\n    - a mandatory feature is listed that we don't recognize, or\n    - the features are inconsistent\n    For convenience, returns the parsed features.\n    \"\"\"\n    if features.bit_length() > 10_000:\n        # This is an implementation-specific limit for how high feature bits we allow.\n        # Needed as LnFeatures subclasses IntFlag, and uses ints internally.\n        # See https://docs.python.org/3/library/stdtypes.html#integer-string-conversion-length-limitation\n        raise IncompatibleOrInsaneFeatures(f\"features bitvector too large: {features.bit_length()=} > 10_000\")\n    features = LnFeatures(features)\n    enabled_features = list_enabled_bits(features)\n    for fbit in enabled_features:\n        if (1 << fbit) & LN_FEATURES_IMPLEMENTED == 0 and fbit % 2 == 0:\n            raise UnknownEvenFeatureBits(fbit)\n    if not features.validate_transitive_dependencies():\n        raise IncompatibleOrInsaneFeatures(f\"not all transitive dependencies are set. \"\n                                           f\"features={features}\")\n    return features\n\n\ndef get_compressed_pubkey_from_bech32(bech32_pubkey: str) -> bytes:\n    decoded_bech32 = segwit_addr.bech32_decode(bech32_pubkey)\n    hrp = decoded_bech32.hrp\n    data_5bits = decoded_bech32.data\n    if decoded_bech32.encoding is None:\n        raise ValueError(\"Bad bech32 checksum\")\n    if decoded_bech32.encoding != segwit_addr.Encoding.BECH32:\n        raise ValueError(\"Bad bech32 encoding: must be using vanilla BECH32\")\n    if hrp != 'ln':\n        raise Exception('unexpected hrp: {}'.format(hrp))\n    data_8bits = segwit_addr.convertbits(data_5bits, 5, 8, False)\n    # pad with zeroes\n    COMPRESSED_PUBKEY_LENGTH = 33\n    data_8bits = data_8bits + ((COMPRESSED_PUBKEY_LENGTH - len(data_8bits)) * [0])\n    return bytes(data_8bits)\n\n\ndef make_closing_tx(\n        local_funding_pubkey: bytes,\n        remote_funding_pubkey: bytes,\n        funding_txid: str,\n        funding_pos: int,\n        funding_sat: int,\n        outputs: List[PartialTxOutput]\n) -> PartialTransaction:\n    c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey, funding_pos, funding_txid, funding_sat)\n    c_input.nsequence = 0xFFFF_FFFF\n    tx = PartialTransaction.from_io([c_input], outputs, locktime=0, version=2)\n    return tx\n\n\n# key derivation\n# originally based on lnd/keychain/derivation.go\n# notes:\n# - Add a new path for each use case. Do not reuse existing paths.\n#   (to avoid having to carefully consider if reuse would be safe)\n# - Always prefer to use hardened derivation for new paths you add.\n#   (to avoid having to carefully consider if unhardened would be safe)\nclass LnKeyFamily(IntEnum):\n    MULTISIG = 0 | BIP32_PRIME\n    REVOCATION_BASE = 1 | BIP32_PRIME\n    HTLC_BASE = 2 | BIP32_PRIME\n    PAYMENT_BASE = 3 | BIP32_PRIME\n    DELAY_BASE = 4 | BIP32_PRIME\n    REVOCATION_ROOT = 5 | BIP32_PRIME\n    NODE_KEY = 6\n    BACKUP_CIPHER = 7 | BIP32_PRIME\n    PAYMENT_SECRET_KEY = 8 | BIP32_PRIME\n    NOSTR_KEY = 9 | BIP32_PRIME\n    FUNDING_ROOT_KEY = 10 | BIP32_PRIME\n\n\ndef generate_keypair(node: BIP32Node, key_family: LnKeyFamily) -> Keypair:\n    node2 = node.subkey_at_private_derivation([key_family, 0, 0])\n    k = node2.eckey.get_secret_bytes()\n    cK = ecc.ECPrivkey(k).get_public_key_bytes()\n    return Keypair(cK, k)\n\n\ndef generate_random_keypair() -> Keypair:\n    import secrets\n    k = secrets.token_bytes(32)\n    cK = ecc.ECPrivkey(k).get_public_key_bytes()\n    return Keypair(cK, k)\n\n\nNUM_MAX_HOPS_IN_PAYMENT_PATH = 20\nNUM_MAX_EDGES_IN_PAYMENT_PATH = NUM_MAX_HOPS_IN_PAYMENT_PATH\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass UpdateAddHtlc:\n    amount_msat: int\n    payment_hash: bytes\n    cltv_abs: int\n    htlc_id: Optional[int] = dataclasses.field(default=None)\n    timestamp: int = dataclasses.field(default_factory=lambda: int(time.time()))\n\n    @staticmethod\n    @stored_in('adds', tuple)\n    def from_tuple(amount_msat, rhash, cltv_abs, htlc_id, timestamp) -> 'UpdateAddHtlc':\n        return UpdateAddHtlc(\n            amount_msat=amount_msat,\n            payment_hash=bytes.fromhex(rhash),\n            cltv_abs=cltv_abs,\n            htlc_id=htlc_id,\n            timestamp=timestamp)\n\n    def to_json(self):\n        self._validate()\n        return dataclasses.astuple(self)\n\n    def _validate(self):\n        assert isinstance(self.amount_msat, int), self.amount_msat\n        assert isinstance(self.payment_hash, bytes) and len(self.payment_hash) == 32\n        assert isinstance(self.cltv_abs, int) and self.cltv_abs <= NLOCKTIME_BLOCKHEIGHT_MAX, self.cltv_abs\n        assert isinstance(self.htlc_id, int) or self.htlc_id is None, self.htlc_id\n        assert isinstance(self.timestamp, int), self.timestamp\n\n    def __post_init__(self):\n        self._validate()\n\n\n# Note: these states are persisted in the wallet file.\n# Do not modify them without performing a wallet db upgrade\n# todo: if this changes again states could also be persisted by name instead of int value as done for ChannelState\nclass RecvMPPResolution(IntEnum):\n    WAITING = 0  # set is not complete yet, waiting for arrival of the remaining htlcs\n    EXPIRED = 1  # preimage must not be revealed\n    COMPLETE = 2  # set is complete but could still be failed (e.g. due to cltv timeout)\n    FAILED = 3  # preimage must not be revealed\n    SETTLING = 4  # Must not be failed, should be settled asap.\n                  # Also used when forwarding (for upstream), in which case a downstream\n                  # forwarding failure could still result in transitioning to FAILED.\n\n\nr = RecvMPPResolution\nallowed_mpp_set_transitions = (\n    (r.WAITING, r.EXPIRED),\n    (r.WAITING, r.FAILED),\n    (r.WAITING, r.COMPLETE),\n    (r.WAITING, r.SETTLING),  # normal htlc forwarding\n\n    (r.COMPLETE, r.SETTLING),\n    (r.COMPLETE, r.FAILED),\n    (r.COMPLETE, r.EXPIRED),  # this should only realistically happen for payment bundles\n\n    (r.SETTLING, r.FAILED),  # forwarding failure, hold invoice callback gets unregistered, and we don't have preimage\n\n    (r.EXPIRED, r.FAILED),  # doesn't seem useful but also not dangerous\n)\ndel r\n\n\nclass ReceivedMPPHtlc(NamedTuple):\n    channel_id: bytes\n    htlc: UpdateAddHtlc\n    unprocessed_onion: str\n\n    def __repr__(self):\n        return f\"chan_id={self.channel_id.hex()}, {self.htlc=}, {self.unprocessed_onion[:15]=}...\"\n\n    @staticmethod\n    def from_tuple(channel_id, htlc, unprocessed_onion) -> 'ReceivedMPPHtlc':\n        assert is_hex_str(unprocessed_onion) and is_hex_str(channel_id)\n        return ReceivedMPPHtlc(\n            channel_id=bytes.fromhex(channel_id),\n            htlc=UpdateAddHtlc.from_tuple(*htlc),\n            unprocessed_onion=unprocessed_onion,\n        )\n\n\nclass ReceivedMPPStatus(NamedTuple):\n    resolution: RecvMPPResolution\n    htlcs: frozenset[ReceivedMPPHtlc]\n    # parent_set_key is needed as trampoline allows MPP to be nested, the parent_set_key is the\n    # payment key of the final mpp set (derived from inner trampoline onion payment secret)\n    # to which the separate trampoline sets htlcs get added once they are complete.\n    # https://github.com/lightning/bolts/pull/829/commits/bc7a1a0bc97b2293e7f43dd8a06529e5fdcf7cd2\n    parent_set_key: str = None\n\n    def get_first_htlc_timestamp(self) -> Optional[int]:\n        return min([mpp_htlc.htlc.timestamp for mpp_htlc in self.htlcs], default=None)\n\n    def get_closest_cltv_abs(self) -> Optional[int]:\n        return min([mpp_htlc.htlc.cltv_abs for mpp_htlc in self.htlcs], default=None)\n\n    def get_payment_hash(self) -> Optional[bytes]:\n        mpp_htlcs = iter(self.htlcs)\n        first_mpp_htlc = next(mpp_htlcs, None)\n        payment_hash = first_mpp_htlc.htlc.payment_hash if first_mpp_htlc else None\n        for mpp_htlc in mpp_htlcs:\n            assert mpp_htlc.htlc.payment_hash == payment_hash, \"mpp set with inconsistent payment hashes\"\n        return payment_hash\n\n    @staticmethod\n    @stored_in('received_mpp_htlcs', tuple)\n    def from_tuple(resolution, htlc_list, parent_set_key=None) -> 'ReceivedMPPStatus':\n        assert isinstance(resolution, int)\n        htlc_set = frozenset(ReceivedMPPHtlc.from_tuple(*htlc_data) for htlc_data in htlc_list)\n        return ReceivedMPPStatus(\n            resolution=RecvMPPResolution(resolution),\n            htlcs=htlc_set,\n            parent_set_key=parent_set_key,\n        )\n\n\nclass OnionFailureCodeMetaFlag(IntFlag):\n    BADONION = 0x8000\n    PERM     = 0x4000\n    NODE     = 0x2000\n    UPDATE   = 0x1000\n\n\nclass PaymentFeeBudget(NamedTuple):\n    fee_msat: int\n\n    # The cltv budget covers the cost of route to get to the destination, but excluding the\n    # cltv-delta the destination wants for itself. (e.g. \"min_final_cltv_delta\" is excluded)\n    cltv: int  # this is cltv-delta-like, no absolute heights here!\n\n    PAYMENT_FEE_CUTOFF_CLAMP = 10_000_000  # [0, 10k sat]\n    PAYMENT_FEE_MILLIONTHS_CLAMP = 250_000  # [0, 25%]\n\n    @classmethod\n    def from_invoice_amount(\n        cls,\n        *,\n        invoice_amount_msat: int,\n        config: 'SimpleConfig',\n        max_cltv_delta: Optional[int] = None,\n        max_fee_msat: Optional[int] = None,\n    ) -> 'PaymentFeeBudget':\n        if max_fee_msat is None:\n            max_fee_msat = PaymentFeeBudget._calculate_fee_msat(\n                invoice_amount_msat=invoice_amount_msat,\n                config=config,\n            )\n        if max_cltv_delta is None:\n            max_cltv_delta = NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE\n        assert max_cltv_delta > 0, max_cltv_delta\n        return PaymentFeeBudget(\n            fee_msat=max_fee_msat,\n            cltv=max_cltv_delta,\n        )\n\n    @classmethod\n    def _calculate_fee_msat(\n        cls,\n        *,\n        invoice_amount_msat: int,\n        config: 'SimpleConfig',\n        fee_millionths: Optional[int] = None,\n        fee_cutoff_msat: Optional[int] = None,\n    ) -> int:\n        if fee_millionths is None:\n            fee_millionths = config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS\n        if fee_cutoff_msat is None:\n            fee_cutoff_msat = config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT\n        millionths_clamped = min(max(0, fee_millionths), cls.PAYMENT_FEE_MILLIONTHS_CLAMP)\n        cutoff_clamped = min(max(0, fee_cutoff_msat), cls.PAYMENT_FEE_CUTOFF_CLAMP)\n        if fee_millionths != millionths_clamped:\n            _logger.warning(\n                f\"PaymentFeeBudget. found insane fee millionths in config. \"\n                f\"clamped: {fee_millionths}->{millionths_clamped}\")\n        if fee_cutoff_msat != cutoff_clamped:\n            _logger.warning(\n                f\"PaymentFeeBudget. found insane fee cutoff in config. \"\n                f\"clamped: {fee_cutoff_msat}->{cutoff_clamped}\")\n        # for small payments, fees <= constant cutoff are fine\n        # for large payments, the max fee is percentage-based\n        fee_msat = invoice_amount_msat * millionths_clamped // 1_000_000\n        fee_msat = max(fee_msat, cutoff_clamped)\n        return fee_msat\n\n    @classmethod\n    def reverse_from_total_amount(cls, *, total_amount_msat: int, config: 'SimpleConfig') -> int:\n        \"\"\"\n        Given the total amount (including fees) return the amount of fees\n        included assuming highest allowed from config fees are being used.\n\n        This allows to guess a fee that has to be reserved to reliably allow\n        doing a \"Max\" amount lightning send (e.g. for submarine swaps).\n        \"\"\"\n        assert isinstance(total_amount_msat, int) and total_amount_msat >= 0, repr(total_amount_msat)\n\n        millionths_clamped = min(\n            max(0, config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS),\n            cls.PAYMENT_FEE_MILLIONTHS_CLAMP,\n        )\n        cutoff_clamped = min(\n            max(0, config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT),\n            cls.PAYMENT_FEE_CUTOFF_CLAMP,\n        )\n\n        # inverse of _calculate_fee_msat\n        amount_minus_fees = (total_amount_msat * 1_000_000) // (1_000_000 + millionths_clamped)\n        fees_msat = max(total_amount_msat - amount_minus_fees, cutoff_clamped)\n        fees_msat = min(fees_msat, total_amount_msat)  # to handle (invalid?) inputs below cutoff_clamped\n        return fees_msat\n"
  },
  {
    "path": "electrum/lnverifier.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2018 The Electrum developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport asyncio\nimport threading\nfrom typing import TYPE_CHECKING, Dict, Set\n\nimport aiorpcx\nimport electrum_ecc as ecc\nfrom electrum_ecc import ECPubkey\n\nfrom . import bitcoin\nfrom . import constants\nfrom .util import bfh, NetworkJobOnDefaultServer\nfrom .lnutil import funding_output_script_from_keys, ShortChannelID\nfrom .verifier import verify_tx_is_in_block, MerkleVerificationFailure\nfrom .transaction import Transaction\nfrom .interface import GracefulDisconnect\nfrom .crypto import sha256d\nfrom .lnmsg import decode_msg, encode_msg\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .lnrouter import ChannelDB\n\n\nclass LNChannelVerifier(NetworkJobOnDefaultServer):\n    \"\"\" Verify channel announcements for the Channel DB \"\"\"\n\n    # FIXME the initial routing sync is bandwidth-heavy, and the electrum server\n    # will start throttling us, making it even slower. one option would be to\n    # spread it over multiple servers.\n\n    def __init__(self, network: 'Network', channel_db: 'ChannelDB'):\n        self.channel_db = channel_db\n        self.lock = threading.Lock()\n        self.unverified_channel_info = {}  # type: Dict[ShortChannelID, dict]  # scid -> msg_dict\n        # channel announcements that seem to be invalid:\n        self.blacklist = set()  # type: Set[ShortChannelID]\n        NetworkJobOnDefaultServer.__init__(self, network)\n\n    def _reset(self):\n        super()._reset()\n        self.started_verifying_channel = set()  # type: Set[ShortChannelID]\n\n    # TODO make async; and rm self.lock completely\n    def add_new_channel_info(self, short_channel_id: ShortChannelID, msg: dict) -> bool:\n        if short_channel_id in self.unverified_channel_info:\n            return False\n        if short_channel_id in self.blacklist:\n            return False\n        with self.lock:\n            self.unverified_channel_info[short_channel_id] = msg\n            return True\n\n    async def _run_tasks(self, *, taskgroup):\n        await super()._run_tasks(taskgroup=taskgroup)\n        async with taskgroup as group:\n            await group.spawn(self.main)\n\n    async def main(self):\n        while True:\n            await self._verify_some_channels()\n            await asyncio.sleep(0.1)\n\n    async def _verify_some_channels(self):\n        blockchain = self.network.blockchain()\n        local_height = blockchain.height()\n\n        with self.lock:\n            unverified_channel_info = list(self.unverified_channel_info)\n\n        for short_channel_id in unverified_channel_info:\n            if short_channel_id in self.started_verifying_channel:\n                continue\n            block_height = short_channel_id.block_height\n            # only resolve short_channel_id if headers are available.\n            if block_height <= 0 or block_height > local_height:\n                continue\n            header = blockchain.read_header(block_height)\n            if header is None:\n                if block_height <= constants.net.max_checkpoint():\n                    await self.taskgroup.spawn(self.interface.request_chunk_below_max_checkpoint(height=block_height))\n                continue\n            self.started_verifying_channel.add(short_channel_id)\n            await self.taskgroup.spawn(self.verify_channel(block_height, short_channel_id))\n            #self.logger.info(f'requested short_channel_id {short_channel_id.hex()}')\n\n    async def verify_channel(self, block_height: int, short_channel_id: ShortChannelID):\n        # we are verifying channel announcements as they are from untrusted ln peers.\n        # we use electrum servers to do this. however we don't trust electrum servers either...\n        try:\n            async with self._network_request_semaphore:\n                result = await self.network.get_txid_from_txpos(\n                    block_height, short_channel_id.txpos, True)\n        except aiorpcx.jsonrpc.RPCError:\n            # the electrum server is complaining about the txpos for given block.\n            # it is not clear what to do now, but let's believe the server.\n            self._blacklist_short_channel_id(short_channel_id)\n            return\n        tx_hash = result['tx_hash']\n        merkle_branch = result['merkle']\n        # we need to wait if header sync/reorg is still ongoing, hence lock:\n        async with self.network.bhi_lock:\n            header = self.network.blockchain().read_header(block_height)\n        try:\n            verify_tx_is_in_block(tx_hash, merkle_branch, short_channel_id.txpos, header, block_height)\n        except MerkleVerificationFailure as e:\n            # the electrum server sent an incorrect proof. blame is on server, not the ln peer\n            raise GracefulDisconnect(e) from e\n        try:\n            async with self._network_request_semaphore:\n                raw_tx = await self.network.get_transaction(tx_hash)\n        except aiorpcx.jsonrpc.RPCError as e:\n            # the electrum server can't find the tx; but it was the\n            # one who told us about the txid!! blame is on server\n            raise GracefulDisconnect(e) from e\n        tx = Transaction(raw_tx)\n        try:\n            tx.deserialize()\n        except Exception:\n            # either bug in client, or electrum server is evil.\n            # if we connect to a diff server at some point, let's try again.\n            self.logger.warning(f\"cannot deserialize transaction, skipping {tx_hash}\")\n            return\n        if tx_hash != tx.txid():\n            # either bug in client, or electrum server is evil.\n            # if we connect to a diff server at some point, let's try again.\n            self.logger.info(f\"received tx does not match expected txid ({tx_hash} != {tx.txid()})\")\n            return\n        # check funding output\n        chan_ann_msg = self.unverified_channel_info[short_channel_id]\n        redeem_script = funding_output_script_from_keys(chan_ann_msg['bitcoin_key_1'], chan_ann_msg['bitcoin_key_2'])\n        expected_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)\n        try:\n            actual_output = tx.outputs()[short_channel_id.output_index]\n        except IndexError:\n            self._blacklist_short_channel_id(short_channel_id)\n            return\n        if expected_address != actual_output.address:\n            # FIXME what now? best would be to ban the originating ln peer.\n            self.logger.info(f\"funding output script mismatch for {short_channel_id}\")\n            self._remove_channel_from_unverified_db(short_channel_id)\n            return\n        # put channel into channel DB\n        self.channel_db.add_verified_channel_info(chan_ann_msg, capacity_sat=actual_output.value)\n        self._remove_channel_from_unverified_db(short_channel_id)\n\n    def _remove_channel_from_unverified_db(self, short_channel_id: ShortChannelID):\n        with self.lock:\n            self.unverified_channel_info.pop(short_channel_id, None)\n        self.started_verifying_channel.discard(short_channel_id)\n\n    def _blacklist_short_channel_id(self, short_channel_id: ShortChannelID) -> None:\n        self.blacklist.add(short_channel_id)\n        with self.lock:\n            self.unverified_channel_info.pop(short_channel_id, None)\n\n\ndef verify_sig_for_channel_update(chan_upd: dict, node_id: bytes) -> bool:\n    msg_bytes = chan_upd['raw']\n    pre_hash = msg_bytes[2+64:]\n    h = sha256d(pre_hash)\n    sig = chan_upd['signature']\n    if not ECPubkey(node_id).ecdsa_verify(sig, h):\n        return False\n    return True\n"
  },
  {
    "path": "electrum/lnwatcher.py",
    "content": "# Copyright (C) 2018 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nfrom typing import TYPE_CHECKING, Optional, Dict, Callable, Awaitable\n\nfrom . import util\nfrom .util import TxMinedInfo, BelowDustLimit, NoDynamicFeeEstimates\nfrom .util import EventListener, event_listener, log_exceptions, ignore_exceptions\nfrom .transaction import Transaction, TxOutpoint\nfrom .logging import Logger\nfrom .address_synchronizer import TX_HEIGHT_LOCAL\nfrom .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY\nfrom .lnsweep import KeepWatchingTXO, SweepInfo\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .lnworker import LNWallet\n    from .lnchannel import AbstractChannel\n\n\nclass LNWatcher(Logger, EventListener):\n\n    def __init__(self, lnworker: 'LNWallet'):\n        self.lnworker = lnworker\n        Logger.__init__(self)\n        self.adb = lnworker.wallet.adb\n        self.config = lnworker.config\n        self.callbacks = {}  # type: Dict[str, Callable[[], Awaitable[None]]]  # address -> lambda function\n        self.network = None\n        self.register_callbacks()\n        self._pending_force_closes = set()\n\n    def start_network(self, network: 'Network'):\n        self.network = network\n\n    def stop(self):\n        self.unregister_callbacks()\n\n    def remove_callback(self, address: str) -> None:\n        self.callbacks.pop(address, None)\n\n    def add_callback(\n        self,\n        address: str,\n        callback: Callable[[], Awaitable[None]],\n        *,\n        subscribe: bool = True,\n    ) -> None:\n        if subscribe:\n            # FIXME even when called with subscribe=False, adb likely already has this address.\n            #   wallet.adb==lnwatcher.adb, and adb.db==wallet.db, which is persisted to disk.\n            #   A call to adb.add_address at any time will add the address to the persistent DB.\n            #   So in practice adb has the channel-related and swap-related addresses we *ever*\n            #   subscribed to, and will have adb.synchronizer sub to them again.\n            #   (even for old redeemed channels and old swaps)\n            self.adb.add_address(address)\n        self.callbacks[address] = callback\n\n    async def trigger_callbacks(self, *, requires_synchronizer: bool = True):\n        if requires_synchronizer and not self.adb.synchronizer:\n            self.logger.info(\"synchronizer not set yet\")\n            return\n        for address, callback in list(self.callbacks.items()):\n            try:\n                await callback()\n            except Exception:\n                self.logger.exception(f\"LNWatcher callback failed {address=}\")\n        # send callback to GUI\n        util.trigger_callback('wallet_updated', self.lnworker.wallet)\n\n    @event_listener\n    async def on_event_blockchain_updated(self, *args):\n        await self.trigger_callbacks()\n\n    @event_listener\n    async def on_event_adb_added_tx(self, adb, tx_hash, tx):\n        # called if we add local tx\n        if adb != self.adb:\n            return\n        await self.trigger_callbacks()\n\n    @event_listener\n    async def on_event_adb_added_verified_tx(self, adb, tx_hash):\n        if adb != self.adb:\n            return\n        await self.trigger_callbacks()\n\n    @event_listener\n    async def on_event_adb_set_up_to_date(self, adb):\n        if adb != self.adb:\n            return\n        await self.trigger_callbacks()\n\n    def add_channel(self, chan: 'AbstractChannel') -> None:\n        outpoint = chan.funding_outpoint.to_str()\n        address = chan.get_funding_address()\n        callback = lambda: self.check_onchain_situation(address, outpoint)\n        self.add_callback(address, callback, subscribe=chan.need_to_subscribe())\n\n    @ignore_exceptions\n    @log_exceptions\n    async def check_onchain_situation(self, address: str, funding_outpoint: str) -> None:\n        # early return if address has not been added yet\n        if not self.adb.is_mine(address):\n            return\n        # inspect_tx_candidate might have added new addresses, in which case we return early\n        # note: maybe we should wait until adb.is_up_to_date... (?)\n        funding_txid = funding_outpoint.split(':')[0]\n        funding_height = self.adb.get_tx_height(funding_txid)\n        closing_txid = self.adb.get_spender(funding_outpoint)\n        closing_height = self.adb.get_tx_height(closing_txid)\n        if closing_txid:\n            closing_tx = self.adb.get_transaction(closing_txid)\n            if closing_tx:\n                keep_watching = await self.sweep_commitment_transaction(funding_outpoint, closing_tx)\n                if not keep_watching:\n                    self.remove_callback(address)\n            else:\n                self.logger.info(f\"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...\")\n                keep_watching = True\n        else:\n            keep_watching = True\n        await self.update_channel_state(\n            funding_outpoint=funding_outpoint,\n            funding_txid=funding_txid,\n            funding_height=funding_height,\n            closing_txid=closing_txid,\n            closing_height=closing_height,\n            keep_watching=keep_watching)\n\n    def diagnostic_name(self):\n        return f\"{self.lnworker.wallet.diagnostic_name()}-LNW\"\n\n    async def update_channel_state(\n            self, *, funding_outpoint: str, funding_txid: str,\n            funding_height: TxMinedInfo, closing_txid: str,\n            closing_height: TxMinedInfo, keep_watching: bool) -> None:\n        chan = self.lnworker.channel_by_txo(funding_outpoint)\n        if not chan:\n            return\n        chan.update_onchain_state(\n            funding_txid=funding_txid,\n            funding_height=funding_height,\n            closing_txid=closing_txid,\n            closing_height=closing_height,\n            keep_watching=keep_watching)\n        if closing_height.conf > 0:\n            self._pending_force_closes.discard(chan)\n        await self.lnworker.handle_onchain_state(chan)\n\n    async def sweep_commitment_transaction(self, funding_outpoint: str, closing_tx: Transaction) -> bool:\n        \"\"\"This function is called when a channel was closed. In this case\n        we need to check for redeemable outputs of the commitment transaction\n        or spenders down the line (HTLC-timeout/success transactions).\n\n        Returns whether we should continue to monitor.\n\n        Side-effects:\n          - sets defaults labels\n          - populates wallet._accounting_addresses\n        \"\"\"\n        assert closing_tx\n        chan = self.lnworker.channel_by_txo(funding_outpoint)\n        if not chan:\n            return False\n        local_height = self.adb.get_local_height()\n        # detect who closed and get information about how to claim outputs\n        is_local_ctx, sweep_info_dict = chan.get_ctx_sweep_info(closing_tx)\n        # note: we need to keep watching *at least* until the closing tx is deeply mined,\n        #       possibly longer if there are TXOs to sweep\n        keep_watching = not self.adb.is_deeply_mined(closing_tx.txid())\n        # create and broadcast transactions\n        for prevout, sweep_info in sweep_info_dict.items():\n            prev_txid, prev_index = prevout.split(':')\n            name = sweep_info.name + ' ' + chan.get_id_for_log()\n            self.lnworker.wallet.set_default_label(prevout, name)\n            if isinstance(sweep_info, KeepWatchingTXO):  # haven't yet decided if we want to sweep\n                keep_watching |= sweep_info.until_height > local_height\n                continue\n            assert isinstance(sweep_info, SweepInfo), sweep_info\n            if not self.adb.get_transaction(prev_txid):\n                # do not keep watching if prevout does not exist\n                self.logger.info(f'prevout does not exist for {name}: {prevout}')\n                continue\n            watch_sweep_info = self.maybe_redeem(sweep_info)\n            spender_txid = self.adb.get_spender(prevout)  # note: LOCAL spenders don't count\n            spender_tx = self.adb.get_transaction(spender_txid) if spender_txid else None\n            if spender_tx:\n                # the spender might be the remote, revoked or not\n                htlc_sweepinfo = chan.maybe_sweep_htlcs(closing_tx, spender_tx)\n                for prevout2, htlc_sweep_info in htlc_sweepinfo.items():\n                    self.lnworker.wallet.set_default_label(prevout2, htlc_sweep_info.name)\n                    if isinstance(htlc_sweep_info, KeepWatchingTXO):  # haven't yet decided if we want to sweep\n                        keep_watching |= htlc_sweep_info.until_height > local_height\n                        continue\n                    assert isinstance(htlc_sweep_info, SweepInfo), htlc_sweep_info\n                    watch_htlc_sweep_info = self.maybe_redeem(htlc_sweep_info)\n                    htlc_tx_spender = self.adb.get_spender(prevout2)\n                    if htlc_tx_spender:\n                        keep_watching |= not self.adb.is_deeply_mined(htlc_tx_spender)\n                        self.maybe_add_accounting_address(htlc_tx_spender, htlc_sweep_info)\n                    else:\n                        keep_watching |= watch_htlc_sweep_info\n                keep_watching |= not self.adb.is_deeply_mined(spender_txid)\n                self.maybe_extract_preimage(chan, spender_tx, prevout)\n                self.maybe_add_accounting_address(spender_txid, sweep_info)\n            else:\n                keep_watching |= watch_sweep_info\n            self.maybe_add_pending_forceclose(\n                chan=chan,\n                spender_txid=spender_txid,\n                is_local_ctx=is_local_ctx,\n                sweep_info=sweep_info,\n            )\n        return keep_watching\n\n    def get_pending_force_closes(self):\n        return self._pending_force_closes\n\n    def maybe_redeem(self, sweep_info: 'SweepInfo') -> bool:\n        \"\"\" returns 'keep_watching' \"\"\"\n        try:\n            self.lnworker.wallet.txbatcher.add_sweep_input('lnwatcher', sweep_info)\n        except BelowDustLimit:\n            self.logger.debug(f\"maybe_redeem: BelowDustLimit: {sweep_info.name}\")\n            # utxo is considered dust at *current* fee estimates.\n            # but maybe the fees atm are very high? We will retry later.\n            pass\n        except NoDynamicFeeEstimates:\n            self.logger.debug(f\"maybe_redeem: NoDynamicFeeEstimates: {sweep_info.name}\")\n            pass  # will retry later\n        if sweep_info.is_anchor():\n            return False\n        return True\n\n    def maybe_extract_preimage(self, chan: 'AbstractChannel', spender_tx: Transaction, prevout: str):\n        if not spender_tx.is_complete():\n            self.logger.info('spender tx is unsigned')\n            return\n        txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout))\n        assert txin_idx is not None\n        spender_txin = spender_tx.inputs()[txin_idx]\n        chan.extract_preimage_from_htlc_txin(\n            spender_txin,\n            is_deeply_mined=self.adb.is_deeply_mined(spender_tx.txid()),\n        )\n\n    def maybe_add_accounting_address(self, spender_txid: str, sweep_info: 'SweepInfo'):\n        spender_tx = self.adb.get_transaction(spender_txid) if spender_txid else None\n        if not spender_tx:\n            return\n        for i, txin in enumerate(spender_tx.inputs()):\n            if txin.prevout == sweep_info.txin.prevout:\n                break\n        else:\n            return\n        if sweep_info.name in ['offered-htlc', 'received-htlc']:\n            # always consider ours\n            pass\n        else:\n            witness = txin.witness_elements()\n            for sig in witness:\n                # fixme: verify sig is ours\n                witness2 = sweep_info.txin.make_witness(sig)\n                if txin.witness == witness2:\n                    break\n            else:\n                self.logger.info(f\"signature not found {sweep_info.name}, {txin.prevout.to_str()}\")\n                return\n        self.logger.info(f'adding txin address {sweep_info.name}, {txin.prevout.to_str()}')\n        prev_txid, prev_index = txin.prevout.to_str().split(':')\n        prev_tx = self.adb.get_transaction(prev_txid)\n        txout = prev_tx.outputs()[int(prev_index)]\n        self.lnworker.wallet._accounting_addresses.add(txout.address)\n\n    def maybe_add_pending_forceclose(\n        self,\n        *,\n        chan: 'AbstractChannel',\n        spender_txid: Optional[str],\n        is_local_ctx: bool,\n        sweep_info: 'SweepInfo',\n    ) -> None:\n        \"\"\"Adds chan into set of ongoing force-closures if the user should keep the wallet open, waiting for it.\n        (we are waiting for ctx to be confirmed and there are received htlcs)\n        \"\"\"\n        if is_local_ctx and sweep_info.name == 'received-htlc':\n            cltv = sweep_info.cltv_abs\n            assert cltv is not None, f\"missing cltv for {sweep_info}\"\n            if self.adb.get_local_height() > cltv + REDEEM_AFTER_DOUBLE_SPENT_DELAY:\n                # We had plenty of time to sweep. The remote also had time to time out the htlc.\n                # Maybe its value has been ~dust at current and past fee levels (every time we checked).\n                # We should not keep warning the user forever.\n                return\n            tx_mined_status = self.adb.get_tx_height(spender_txid)\n            if tx_mined_status.height() == TX_HEIGHT_LOCAL:\n                self._pending_force_closes.add(chan)\n"
  },
  {
    "path": "electrum/lnwire/README.md",
    "content": "These files have been generated from the BOLT repository:\n```\n$ python3 tools/extract-formats.py 01-*.md 02-*.md 07-*.md  > peer_wire.csv\n$ python3 tools/extract-formats.py 04-*.md  > onion_wire.csv\n```\n\nNote: Trampoline messages were added manually to onion_wire.csv\n"
  },
  {
    "path": "electrum/lnwire/onion_wire.csv",
    "content": "tlvtype,payload,amt_to_forward,2\ntlvdata,payload,amt_to_forward,amt_to_forward,tu64,\ntlvtype,payload,outgoing_cltv_value,4\ntlvdata,payload,outgoing_cltv_value,outgoing_cltv_value,tu32,\ntlvtype,payload,short_channel_id,6\ntlvdata,payload,short_channel_id,short_channel_id,short_channel_id,\ntlvtype,payload,payment_data,8\ntlvdata,payload,payment_data,payment_secret,byte,32\ntlvdata,payload,payment_data,total_msat,tu64,\ntlvtype,payload,encrypted_recipient_data,10\ntlvdata,payload,encrypted_recipient_data,encrypted_data,byte,...\ntlvtype,payload,current_blinding_point,12\ntlvdata,payload,current_blinding_point,blinding,point,\ntlvtype,payload,payment_metadata,16\ntlvdata,payload,payment_metadata,payment_metadata,byte,...\ntlvtype,payload,total_amount_msat,18\ntlvdata,payload,total_amount_msat,total_msat,tu64,\ntlvtype,payload,invoice_features,66097\ntlvdata,payload,invoice_features,invoice_features,u64,\ntlvtype,payload,outgoing_node_id,66098\ntlvdata,payload,outgoing_node_id,outgoing_node_id,byte,33\ntlvtype,payload,invoice_routing_info,66099\ntlvdata,payload,invoice_routing_info,invoice_routing_info,byte,...\ntlvtype,payload,trampoline_onion_packet,66100\ntlvdata,payload,trampoline_onion_packet,version,byte,1\ntlvdata,payload,trampoline_onion_packet,public_key,byte,33\ntlvdata,payload,trampoline_onion_packet,hops_data,byte,400\ntlvdata,payload,trampoline_onion_packet,hmac,byte,32\ntlvtype,encrypted_data_tlv,padding,1\ntlvdata,encrypted_data_tlv,padding,padding,byte,...\ntlvtype,encrypted_data_tlv,short_channel_id,2\ntlvdata,encrypted_data_tlv,short_channel_id,short_channel_id,short_channel_id,\ntlvtype,encrypted_data_tlv,next_node_id,4\ntlvdata,encrypted_data_tlv,next_node_id,node_id,point,\ntlvtype,encrypted_data_tlv,path_id,6\ntlvdata,encrypted_data_tlv,path_id,data,byte,...\ntlvtype,encrypted_data_tlv,next_path_key_override,8\ntlvdata,encrypted_data_tlv,next_path_key_override,path_key,point,\ntlvtype,encrypted_data_tlv,payment_relay,10\ntlvdata,encrypted_data_tlv,payment_relay,cltv_expiry_delta,u16,\ntlvdata,encrypted_data_tlv,payment_relay,fee_proportional_millionths,u32,\ntlvdata,encrypted_data_tlv,payment_relay,fee_base_msat,tu32,\ntlvtype,encrypted_data_tlv,payment_constraints,12\ntlvdata,encrypted_data_tlv,payment_constraints,max_cltv_expiry,u32,\ntlvdata,encrypted_data_tlv,payment_constraints,htlc_minimum_msat,tu64,\ntlvtype,encrypted_data_tlv,allowed_features,14\ntlvdata,encrypted_data_tlv,allowed_features,features,byte,...\ntlvtype,encrypted_data_tlv,unknown_tag_561,561\ntlvdata,encrypted_data_tlv,unknown_tag_561,data,byte,...\ntlvtype,encrypted_data_tlv,unknown_tag_65535,65535\ntlvdata,encrypted_data_tlv,unknown_tag_65535,data,byte,...\nmsgtype,invalid_realm,PERM|1\nmsgtype,temporary_node_failure,NODE|2\nmsgtype,permanent_node_failure,PERM|NODE|2\nmsgtype,required_node_feature_missing,PERM|NODE|3\nmsgtype,invalid_onion_version,BADONION|PERM|4\nmsgdata,invalid_onion_version,sha256_of_onion,sha256,\nmsgtype,invalid_onion_hmac,BADONION|PERM|5\nmsgdata,invalid_onion_hmac,sha256_of_onion,sha256,\nmsgtype,invalid_onion_key,BADONION|PERM|6\nmsgdata,invalid_onion_key,sha256_of_onion,sha256,\nmsgtype,temporary_channel_failure,UPDATE|7\nmsgdata,temporary_channel_failure,len,u16,\nmsgdata,temporary_channel_failure,channel_update,byte,len\nmsgtype,permanent_channel_failure,PERM|8\nmsgtype,required_channel_feature_missing,PERM|9\nmsgtype,unknown_next_peer,PERM|10\nmsgtype,amount_below_minimum,UPDATE|11\nmsgdata,amount_below_minimum,htlc_msat,u64,\nmsgdata,amount_below_minimum,len,u16,\nmsgdata,amount_below_minimum,channel_update,byte,len\nmsgtype,fee_insufficient,UPDATE|12\nmsgdata,fee_insufficient,htlc_msat,u64,\nmsgdata,fee_insufficient,len,u16,\nmsgdata,fee_insufficient,channel_update,byte,len\nmsgtype,incorrect_cltv_expiry,UPDATE|13\nmsgdata,incorrect_cltv_expiry,cltv_expiry,u32,\nmsgdata,incorrect_cltv_expiry,len,u16,\nmsgdata,incorrect_cltv_expiry,channel_update,byte,len\nmsgtype,expiry_too_soon,UPDATE|14\nmsgdata,expiry_too_soon,len,u16,\nmsgdata,expiry_too_soon,channel_update,byte,len\nmsgtype,incorrect_or_unknown_payment_details,PERM|15\nmsgdata,incorrect_or_unknown_payment_details,htlc_msat,u64,\nmsgdata,incorrect_or_unknown_payment_details,height,u32,\nmsgtype,final_incorrect_cltv_expiry,18\nmsgdata,final_incorrect_cltv_expiry,cltv_expiry,u32,\nmsgtype,final_incorrect_htlc_amount,19\nmsgdata,final_incorrect_htlc_amount,incoming_htlc_amt,u64,\nmsgtype,channel_disabled,UPDATE|20\nmsgdata,channel_disabled,disabled_flags,u16,\nmsgdata,channel_disabled,len,u16,\nmsgdata,channel_disabled,channel_update,byte,len\nmsgtype,expiry_too_far,21\nmsgtype,invalid_onion_payload,PERM|22\nmsgdata,invalid_onion_payload,type,bigsize,\nmsgdata,invalid_onion_payload,offset,u16,\nmsgtype,mpp_timeout,23\nmsgtype,invalid_onion_blinding,BADONION|PERM|24\nmsgdata,invalid_onion_blinding,sha256_of_onion,sha256,\ntlvtype,onionmsg_tlv,message,1\ntlvdata,onionmsg_tlv,message,text,byte,...\ntlvtype,onionmsg_tlv,reply_path,2\ntlvdata,onionmsg_tlv,reply_path,path,blinded_path,\ntlvtype,onionmsg_tlv,encrypted_recipient_data,4\ntlvdata,onionmsg_tlv,encrypted_recipient_data,encrypted_recipient_data,byte,...\ntlvtype,onionmsg_tlv,invoice_request,64\ntlvdata,onionmsg_tlv,invoice_request,invoice_request,byte,...\ntlvtype,onionmsg_tlv,invoice,66\ntlvdata,onionmsg_tlv,invoice,invoice,byte,...\ntlvtype,onionmsg_tlv,invoice_error,68\ntlvdata,onionmsg_tlv,invoice_error,invoice_error,byte,...\nsubtype,blinded_path\nsubtypedata,blinded_path,first_node_id,sciddir_or_pubkey,\nsubtypedata,blinded_path,first_path_key,point,\nsubtypedata,blinded_path,num_hops,byte,\nsubtypedata,blinded_path,path,blinded_path_hop,num_hops\nsubtype,blinded_path_hop\nsubtypedata,blinded_path_hop,blinded_node_id,point,\nsubtypedata,blinded_path_hop,enclen,u16,\nsubtypedata,blinded_path_hop,encrypted_recipient_data,byte,enclen\n"
  },
  {
    "path": "electrum/lnwire/peer_wire.csv",
    "content": "msgtype,init,16\nmsgdata,init,gflen,u16,\nmsgdata,init,globalfeatures,byte,gflen\nmsgdata,init,flen,u16,\nmsgdata,init,features,byte,flen\nmsgdata,init,tlvs,init_tlvs,\ntlvtype,init_tlvs,networks,1\ntlvdata,init_tlvs,networks,chains,chain_hash,...\ntlvtype,init_tlvs,remote_addr,3\ntlvdata,init_tlvs,remote_addr,data,byte,...\nmsgtype,error,17\nmsgdata,error,channel_id,channel_id,\nmsgdata,error,len,u16,\nmsgdata,error,data,byte,len\nmsgtype,warning,1\nmsgdata,warning,channel_id,channel_id,\nmsgdata,warning,len,u16,\nmsgdata,warning,data,byte,len\nmsgtype,ping,18\nmsgdata,ping,num_pong_bytes,u16,\nmsgdata,ping,byteslen,u16,\nmsgdata,ping,ignored,byte,byteslen\nmsgtype,pong,19\nmsgdata,pong,byteslen,u16,\nmsgdata,pong,ignored,byte,byteslen\ntlvtype,n1,tlv1,1\ntlvdata,n1,tlv1,amount_msat,tu64,\ntlvtype,n1,tlv2,2\ntlvdata,n1,tlv2,scid,short_channel_id,\ntlvtype,n1,tlv3,3\ntlvdata,n1,tlv3,node_id,point,\ntlvdata,n1,tlv3,amount_msat_1,u64,\ntlvdata,n1,tlv3,amount_msat_2,u64,\ntlvtype,n1,tlv4,254\ntlvdata,n1,tlv4,cltv_delta,u16,\ntlvtype,n2,tlv1,0\ntlvdata,n2,tlv1,amount_msat,tu64,\ntlvtype,n2,tlv2,11\ntlvdata,n2,tlv2,cltv_expiry,tu32,\nmsgtype,open_channel,32\nmsgdata,open_channel,chain_hash,chain_hash,\nmsgdata,open_channel,temporary_channel_id,byte,32\nmsgdata,open_channel,funding_satoshis,u64,\nmsgdata,open_channel,push_msat,u64,\nmsgdata,open_channel,dust_limit_satoshis,u64,\nmsgdata,open_channel,max_htlc_value_in_flight_msat,u64,\nmsgdata,open_channel,channel_reserve_satoshis,u64,\nmsgdata,open_channel,htlc_minimum_msat,u64,\nmsgdata,open_channel,feerate_per_kw,u32,\nmsgdata,open_channel,to_self_delay,u16,\nmsgdata,open_channel,max_accepted_htlcs,u16,\nmsgdata,open_channel,funding_pubkey,point,\nmsgdata,open_channel,revocation_basepoint,point,\nmsgdata,open_channel,payment_basepoint,point,\nmsgdata,open_channel,delayed_payment_basepoint,point,\nmsgdata,open_channel,htlc_basepoint,point,\nmsgdata,open_channel,first_per_commitment_point,point,\nmsgdata,open_channel,channel_flags,byte,\nmsgdata,open_channel,tlvs,open_channel_tlvs,\ntlvtype,open_channel_tlvs,upfront_shutdown_script,0\ntlvdata,open_channel_tlvs,upfront_shutdown_script,shutdown_scriptpubkey,byte,...\ntlvtype,open_channel_tlvs,channel_type,1\ntlvdata,open_channel_tlvs,channel_type,type,byte,...\ntlvtype,open_channel_tlvs,channel_opening_fee,10000\ntlvdata,open_channel_tlvs,channel_opening_fee,channel_opening_fee,u64,\nmsgtype,accept_channel,33\nmsgdata,accept_channel,temporary_channel_id,byte,32\nmsgdata,accept_channel,dust_limit_satoshis,u64,\nmsgdata,accept_channel,max_htlc_value_in_flight_msat,u64,\nmsgdata,accept_channel,channel_reserve_satoshis,u64,\nmsgdata,accept_channel,htlc_minimum_msat,u64,\nmsgdata,accept_channel,minimum_depth,u32,\nmsgdata,accept_channel,to_self_delay,u16,\nmsgdata,accept_channel,max_accepted_htlcs,u16,\nmsgdata,accept_channel,funding_pubkey,point,\nmsgdata,accept_channel,revocation_basepoint,point,\nmsgdata,accept_channel,payment_basepoint,point,\nmsgdata,accept_channel,delayed_payment_basepoint,point,\nmsgdata,accept_channel,htlc_basepoint,point,\nmsgdata,accept_channel,first_per_commitment_point,point,\nmsgdata,accept_channel,tlvs,accept_channel_tlvs,\ntlvtype,accept_channel_tlvs,upfront_shutdown_script,0\ntlvdata,accept_channel_tlvs,upfront_shutdown_script,shutdown_scriptpubkey,byte,...\ntlvtype,accept_channel_tlvs,channel_type,1\ntlvdata,accept_channel_tlvs,channel_type,type,byte,...\nmsgtype,funding_created,34\nmsgdata,funding_created,temporary_channel_id,byte,32\nmsgdata,funding_created,funding_txid,sha256,\nmsgdata,funding_created,funding_output_index,u16,\nmsgdata,funding_created,signature,signature,\nmsgtype,funding_signed,35\nmsgdata,funding_signed,channel_id,channel_id,\nmsgdata,funding_signed,signature,signature,\nmsgtype,channel_ready,36\nmsgdata,channel_ready,channel_id,channel_id,\nmsgdata,channel_ready,second_per_commitment_point,point,\nmsgdata,channel_ready,tlvs,channel_ready_tlvs,\ntlvtype,channel_ready_tlvs,short_channel_id,1\ntlvdata,channel_ready_tlvs,short_channel_id,alias,short_channel_id,\nmsgtype,shutdown,38\nmsgdata,shutdown,channel_id,channel_id,\nmsgdata,shutdown,len,u16,\nmsgdata,shutdown,scriptpubkey,byte,len\nmsgtype,closing_signed,39\nmsgdata,closing_signed,channel_id,channel_id,\nmsgdata,closing_signed,fee_satoshis,u64,\nmsgdata,closing_signed,signature,signature,\nmsgdata,closing_signed,tlvs,closing_signed_tlvs,\ntlvtype,closing_signed_tlvs,fee_range,1\ntlvdata,closing_signed_tlvs,fee_range,min_fee_satoshis,u64,\ntlvdata,closing_signed_tlvs,fee_range,max_fee_satoshis,u64,\nmsgtype,update_add_htlc,128\nmsgdata,update_add_htlc,channel_id,channel_id,\nmsgdata,update_add_htlc,id,u64,\nmsgdata,update_add_htlc,amount_msat,u64,\nmsgdata,update_add_htlc,payment_hash,sha256,\nmsgdata,update_add_htlc,cltv_expiry,u32,\nmsgdata,update_add_htlc,onion_routing_packet,byte,1366\ntlvtype,update_add_htlc_tlvs,blinding_point,0\ntlvdata,update_add_htlc_tlvs,blinding_point,blinding,point,\nmsgtype,update_fulfill_htlc,130\nmsgdata,update_fulfill_htlc,channel_id,channel_id,\nmsgdata,update_fulfill_htlc,id,u64,\nmsgdata,update_fulfill_htlc,payment_preimage,byte,32\nmsgtype,update_fail_htlc,131\nmsgdata,update_fail_htlc,channel_id,channel_id,\nmsgdata,update_fail_htlc,id,u64,\nmsgdata,update_fail_htlc,len,u16,\nmsgdata,update_fail_htlc,reason,byte,len\nmsgtype,update_fail_malformed_htlc,135\nmsgdata,update_fail_malformed_htlc,channel_id,channel_id,\nmsgdata,update_fail_malformed_htlc,id,u64,\nmsgdata,update_fail_malformed_htlc,sha256_of_onion,sha256,\nmsgdata,update_fail_malformed_htlc,failure_code,u16,\nmsgtype,commitment_signed,132\nmsgdata,commitment_signed,channel_id,channel_id,\nmsgdata,commitment_signed,signature,signature,\nmsgdata,commitment_signed,num_htlcs,u16,\nmsgdata,commitment_signed,htlc_signature,signature,num_htlcs\nmsgtype,revoke_and_ack,133\nmsgdata,revoke_and_ack,channel_id,channel_id,\nmsgdata,revoke_and_ack,per_commitment_secret,byte,32\nmsgdata,revoke_and_ack,next_per_commitment_point,point,\nmsgtype,update_fee,134\nmsgdata,update_fee,channel_id,channel_id,\nmsgdata,update_fee,feerate_per_kw,u32,\nmsgtype,channel_reestablish,136\nmsgdata,channel_reestablish,channel_id,channel_id,\nmsgdata,channel_reestablish,next_commitment_number,u64,\nmsgdata,channel_reestablish,next_revocation_number,u64,\nmsgdata,channel_reestablish,your_last_per_commitment_secret,byte,32\nmsgdata,channel_reestablish,my_current_per_commitment_point,point,\nmsgtype,announcement_signatures,259\nmsgdata,announcement_signatures,channel_id,channel_id,\nmsgdata,announcement_signatures,short_channel_id,short_channel_id,\nmsgdata,announcement_signatures,node_signature,signature,\nmsgdata,announcement_signatures,bitcoin_signature,signature,\nmsgtype,channel_announcement,256\nmsgdata,channel_announcement,node_signature_1,signature,\nmsgdata,channel_announcement,node_signature_2,signature,\nmsgdata,channel_announcement,bitcoin_signature_1,signature,\nmsgdata,channel_announcement,bitcoin_signature_2,signature,\nmsgdata,channel_announcement,len,u16,\nmsgdata,channel_announcement,features,byte,len\nmsgdata,channel_announcement,chain_hash,chain_hash,\nmsgdata,channel_announcement,short_channel_id,short_channel_id,\nmsgdata,channel_announcement,node_id_1,point,\nmsgdata,channel_announcement,node_id_2,point,\nmsgdata,channel_announcement,bitcoin_key_1,point,\nmsgdata,channel_announcement,bitcoin_key_2,point,\nmsgtype,node_announcement,257\nmsgdata,node_announcement,signature,signature,\nmsgdata,node_announcement,flen,u16,\nmsgdata,node_announcement,features,byte,flen\nmsgdata,node_announcement,timestamp,u32,\nmsgdata,node_announcement,node_id,point,\nmsgdata,node_announcement,rgb_color,byte,3\nmsgdata,node_announcement,alias,byte,32\nmsgdata,node_announcement,addrlen,u16,\nmsgdata,node_announcement,addresses,byte,addrlen\nmsgtype,channel_update,258\nmsgdata,channel_update,signature,signature,\nmsgdata,channel_update,chain_hash,chain_hash,\nmsgdata,channel_update,short_channel_id,short_channel_id,\nmsgdata,channel_update,timestamp,u32,\nmsgdata,channel_update,message_flags,byte,\nmsgdata,channel_update,channel_flags,byte,\nmsgdata,channel_update,cltv_expiry_delta,u16,\nmsgdata,channel_update,htlc_minimum_msat,u64,\nmsgdata,channel_update,fee_base_msat,u32,\nmsgdata,channel_update,fee_proportional_millionths,u32,\nmsgdata,channel_update,htlc_maximum_msat,u64,\nmsgtype,query_short_channel_ids,261\nmsgdata,query_short_channel_ids,chain_hash,chain_hash,\nmsgdata,query_short_channel_ids,len,u16,\nmsgdata,query_short_channel_ids,encoded_short_ids,byte,len\nmsgdata,query_short_channel_ids,tlvs,query_short_channel_ids_tlvs,\ntlvtype,query_short_channel_ids_tlvs,query_flags,1\ntlvdata,query_short_channel_ids_tlvs,query_flags,encoding_type,byte,\ntlvdata,query_short_channel_ids_tlvs,query_flags,encoded_query_flags,byte,...\nmsgtype,reply_short_channel_ids_end,262\nmsgdata,reply_short_channel_ids_end,chain_hash,chain_hash,\nmsgdata,reply_short_channel_ids_end,full_information,byte,\nmsgtype,query_channel_range,263\nmsgdata,query_channel_range,chain_hash,chain_hash,\nmsgdata,query_channel_range,first_blocknum,u32,\nmsgdata,query_channel_range,number_of_blocks,u32,\nmsgdata,query_channel_range,tlvs,query_channel_range_tlvs,\ntlvtype,query_channel_range_tlvs,query_option,1\ntlvdata,query_channel_range_tlvs,query_option,query_option_flags,bigsize,\nmsgtype,reply_channel_range,264\nmsgdata,reply_channel_range,chain_hash,chain_hash,\nmsgdata,reply_channel_range,first_blocknum,u32,\nmsgdata,reply_channel_range,number_of_blocks,u32,\nmsgdata,reply_channel_range,sync_complete,byte,\nmsgdata,reply_channel_range,len,u16,\nmsgdata,reply_channel_range,encoded_short_ids,byte,len\nmsgdata,reply_channel_range,tlvs,reply_channel_range_tlvs,\ntlvtype,reply_channel_range_tlvs,timestamps_tlv,1\ntlvdata,reply_channel_range_tlvs,timestamps_tlv,encoding_type,byte,\ntlvdata,reply_channel_range_tlvs,timestamps_tlv,encoded_timestamps,byte,...\ntlvtype,reply_channel_range_tlvs,checksums_tlv,3\ntlvdata,reply_channel_range_tlvs,checksums_tlv,checksums,channel_update_checksums,...\nsubtype,channel_update_timestamps\nsubtypedata,channel_update_timestamps,timestamp_node_id_1,u32,\nsubtypedata,channel_update_timestamps,timestamp_node_id_2,u32,\nsubtype,channel_update_checksums\nsubtypedata,channel_update_checksums,checksum_node_id_1,u32,\nsubtypedata,channel_update_checksums,checksum_node_id_2,u32,\nmsgtype,gossip_timestamp_filter,265\nmsgdata,gossip_timestamp_filter,chain_hash,chain_hash,\nmsgdata,gossip_timestamp_filter,first_timestamp,u32,\nmsgdata,gossip_timestamp_filter,timestamp_range,u32,\nmsgtype,onion_message,513\nmsgdata,onion_message,path_key,point,\nmsgdata,onion_message,len,u16,\nmsgdata,onion_message,onion_message_packet,byte,len\n"
  },
  {
    "path": "electrum/lnworker.py",
    "content": "# Copyright (C) 2018 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nimport asyncio\nimport os\nfrom decimal import Decimal\nimport random\nimport time\nfrom enum import IntEnum\nfrom typing import (\n    Optional, Sequence, Tuple, List, Set, Dict, TYPE_CHECKING, NamedTuple, Mapping, Any, Iterable, AsyncGenerator,\n    Callable, Awaitable, Union,\n)\nfrom types import MappingProxyType\nimport threading\nimport socket\nfrom functools import partial\nfrom collections import defaultdict\nimport concurrent\nfrom concurrent import futures\nimport urllib.parse\nimport itertools\nimport dataclasses\nfrom math import ceil\n\nimport aiohttp\nimport dns.asyncresolver\nimport dns.exception\nfrom aiorpcx import run_in_thread, NetAddress, ignore_after\n\nfrom .logging import Logger\nfrom .i18n import _\nfrom .json_db import stored_in\nfrom .channel_db import UpdateStatus, ChannelDBNotLoaded, get_mychannel_info, get_mychannel_policy\n\nfrom . import constants, util, lnutil\nfrom . import bitcoin\nfrom .util import (\n    profiler, OldTaskGroup, ESocksProxy, NetworkRetryManager, JsonRPCClient, NotEnoughFunds, EventListener,\n    event_listener, bfh, InvoiceError, resolve_dns_srv, is_ip_address, log_exceptions, ignore_exceptions,\n    make_aiohttp_session, random_shuffled_copy, is_private_netaddress,\n    UnrelatedTransactionException, LightningHistoryItem, get_asyncio_loop,\n)\nfrom .fee_policy import (\n    FeePolicy, FEERATE_FALLBACK_STATIC_FEE, FEE_LN_ETA_TARGET, FEE_LN_LOW_ETA_TARGET,\n    FEERATE_PER_KW_MIN_RELAY_LIGHTNING, FEE_LN_MINIMUM_ETA_TARGET\n)\nfrom .invoices import (Invoice, Request, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED, LN_EXPIRY_NEVER,\n                       BaseInvoice)\nfrom .bitcoin import COIN, opcodes, make_op_return, address_to_scripthash, DummyAddress\nfrom .bip32 import BIP32Node\nfrom .address_synchronizer import TX_HEIGHT_LOCAL\nfrom .transaction import (\n    Transaction, get_script_type_from_output_script, PartialTxOutput, PartialTransaction, PartialTxInput\n)\nfrom .crypto import (\n    sha256, chacha20_encrypt, chacha20_decrypt, pw_encode_with_version_and_mac, pw_decode_with_version_and_mac\n)\n\nfrom .onion_message import OnionMessageManager\nfrom .lntransport import (\n    LNTransport, LNResponderTransport, LNTransportBase, LNPeerAddr, split_host_port, extract_nodeid,\n    ConnStringFormatError\n)\nfrom .lnpeer import Peer, LN_P2P_NETWORK_TIMEOUT\nfrom .lnaddr import lnencode, LnAddr, lndecode\nfrom .lnchannel import Channel, AbstractChannel, ChannelState, PeerState, HTLCWithStatus, ChannelBackup\nfrom .lnrater import LNRater\nfrom .lnutil import (\n    get_compressed_pubkey_from_bech32, serialize_htlc_key, deserialize_htlc_key, PaymentFailure, generate_keypair,\n    LnKeyFamily, LOCAL, REMOTE, MIN_FINAL_CLTV_DELTA_ACCEPTED, SENT, RECEIVED, HTLCOwner, UpdateAddHtlc, LnFeatures,\n    ShortChannelID, HtlcLog, NoPathFound, InvalidGossipMsg, FeeBudgetExceeded, ImportedChannelBackupStorage,\n    OnchainChannelBackupStorage, ln_compare_features, IncompatibleLightningFeatures, PaymentFeeBudget,\n    NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, GossipForwardingMessage, MIN_FUNDING_SAT,\n    MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE, RecvMPPResolution, ReceivedMPPStatus, ReceivedMPPHtlc,\n    PaymentSuccess, ChannelType, LocalConfig, Keypair,\n)\nfrom .lnonion import (\n    decode_onion_error, OnionFailureCode, OnionRoutingFailure, OnionPacket,\n    ProcessedOnionPacket, calc_hops_data_for_payment, new_onion_packet,\n)\nfrom .lnmsg import decode_msg\nfrom .lnrouter import (\n    RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_within_budget, NoChannelPolicy,\n    LNPathInconsistent, fee_for_edge_msat,\n)\nfrom .lnwatcher import LNWatcher\nfrom .submarine_swaps import SwapManager\nfrom .mpp_split import suggest_splits, SplitConfigRating\nfrom .trampoline import (\n    create_trampoline_route_and_onion, is_legacy_relay, trampolines_by_id, hardcoded_trampoline_nodes,\n    is_hardcoded_trampoline, decode_routing_info\n)\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .wallet import Abstract_Wallet\n    from .channel_db import ChannelDB\n    from .simple_config import SimpleConfig\n\n\nSAVED_PR_STATUS = [PR_PAID, PR_UNPAID]  # status that are persisted\n\nNUM_PEERS_TARGET = 4\n\n# onchain channel backup data\nCB_VERSION = 0\nCB_MAGIC_BYTES = bytes([0, 0, 0, CB_VERSION])\nNODE_ID_PREFIX_LEN = 16\n\n\nclass PaymentDirection(IntEnum):\n    SENT = 0\n    RECEIVED = 1\n    SELF_PAYMENT = 2\n    FORWARDING = 3\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass PaymentInfo:\n    \"\"\"Information required to handle incoming htlcs for a payment request.\n\n    - Historically, we used to store \"bolt11, direction, status\", but deserializing bolt11 was too slow.\n      (even deserializing just once - all bolt11 during wallet-open - was slow)\n      - note: the deserialization code in lnaddr.py has been significantly sped up since\n    - For incoming payments, for unpaid requests, ~every time the user displays the unpaid bolt11,\n      we get a chance to display a new bolt11, with same payment_hash/amount, but with updated\n      routing_hints (channels might get closed/opened, or just liquidity changed drastically).\n    \"\"\"\n    payment_hash: bytes\n    amount_msat: Optional[int]\n    direction: lnutil.Direction\n    status: int\n    min_final_cltv_delta: int\n    expiry_delay: int\n    creation_ts: int = dataclasses.field(default_factory=lambda: int(time.time()))\n    invoice_features: LnFeatures\n\n    @property\n    def expiration_ts(self):\n        return self.creation_ts + self.expiry_delay\n\n    def validate(self):\n        assert isinstance(self.payment_hash, bytes) and len(self.payment_hash) == 32\n        assert isinstance(self.direction, int)\n        assert self.amount_msat is None or isinstance(self.amount_msat, int)\n        if self.direction == RECEIVED:\n            assert self.amount_msat != 0  # use amount_msat=None instead!\n        assert isinstance(self.status, int)\n        assert isinstance(self.min_final_cltv_delta, int)\n        assert isinstance(self.expiry_delay, int) and self.expiry_delay > 0, repr(self.expiry_delay)\n        assert isinstance(self.creation_ts, int)\n        assert isinstance(self.invoice_features, LnFeatures)\n\n    def __post_init__(self):\n        self.validate()\n\n    @property\n    def db_key(self) -> str:\n        return self.calc_db_key(payment_hash_hex=self.payment_hash.hex(), direction=self.direction)\n\n    @classmethod\n    def calc_db_key(cls, *, payment_hash_hex: str, direction: lnutil.Direction) -> str:\n        return f\"{payment_hash_hex}:{int(direction)}\"\n\n\nSentHtlcKey = Tuple[bytes, ShortChannelID, int]  # RHASH, scid, htlc_id\n\n\nclass SentHtlcInfo(NamedTuple):\n    route: LNPaymentRoute\n    payment_secret_orig: bytes\n    payment_secret_bucket: bytes\n    amount_msat: int\n    bucket_msat: int\n    amount_receiver_msat: int\n    trampoline_fee_level: Optional[int]\n    trampoline_route: Optional[LNPaymentRoute]\n\n\nclass ErrorAddingPeer(Exception): pass\n\n\n# set some feature flags as baseline for both LNWallet and LNGossip\n# note that e.g. DATA_LOSS_PROTECT and OPTION_CHANNEL_TYPE_OPT are needed for LNGossip as many peers require it\nBASE_FEATURES = (\n    LnFeatures(0)\n    | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT\n    | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT\n    | LnFeatures.VAR_ONION_OPT\n    | LnFeatures.PAYMENT_SECRET_OPT\n    | LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT\n    | LnFeatures.OPTION_CHANNEL_TYPE_OPT\n)\n\n# we do not want to receive unrequested gossip (see lnpeer.maybe_save_remote_update)\nLNWALLET_FEATURES = (\n    BASE_FEATURES\n    | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\n    | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ\n    | LnFeatures.VAR_ONION_REQ\n    | LnFeatures.PAYMENT_SECRET_REQ\n    | LnFeatures.BASIC_MPP_OPT\n    | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM\n    | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT\n    | LnFeatures.OPTION_SCID_ALIAS_OPT\n    | LnFeatures.OPTION_SUPPORT_LARGE_CHANNEL_OPT\n    | LnFeatures.OPTION_CHANNEL_TYPE_REQ\n)\n\nLNGOSSIP_FEATURES = (\n    BASE_FEATURES\n    # LNGossip doesn't serve gossip but weirdly have to signal so\n    # that peers satisfy our queries\n    | LnFeatures.GOSSIP_QUERIES_REQ\n    | LnFeatures.GOSSIP_QUERIES_OPT\n)\n\n\nclass LNPeerManager(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):\n\n    def __init__(\n        self, node_keypair,\n        *,\n        lnwallet_or_lngossip: 'LNWallet | LNGossip',\n        features: LnFeatures,\n        config: 'SimpleConfig',\n    ):\n        NetworkRetryManager.__init__(\n            self,\n            max_retry_delay_normal=3600,\n            init_retry_delay_normal=600,\n            max_retry_delay_urgent=300,\n            init_retry_delay_urgent=4,\n        )\n        self.lock = threading.RLock()\n        self.node_keypair = node_keypair\n        self._lnwallet_or_lngossip = lnwallet_or_lngossip\n        Logger.__init__(self)\n        self._peers = {}  # type: Dict[bytes, Peer]  # pubkey -> Peer  # needs self.lock\n        self._channelless_incoming_peers = set()  # type: Set[bytes]  # node_ids  # needs self.lock\n        self.taskgroup = OldTaskGroup()\n        self.listen_server = None  # type: Optional[asyncio.AbstractServer]\n        self.features = features\n        self.network = None  # type: Optional[Network]\n        self.config = config\n        self.stopping_soon = False  # whether we are being shut down\n        self.register_callbacks()\n\n    def diagnostic_name(self):\n        lnw = self._lnwallet_or_lngossip\n        return lnw.diagnostic_name() or lnw.__class__.__name__\n\n    @property\n    def channel_db(self) -> 'ChannelDB':\n        return self.network.channel_db if self.network else None\n\n    def uses_trampoline(self) -> bool:\n        return not bool(self.channel_db)\n\n    @property\n    def peers(self) -> Mapping[bytes, Peer]:\n        \"\"\"Returns a read-only copy of peers.\"\"\"\n        with self.lock:\n            return self._peers.copy()\n\n    def channels_for_peer(self, node_id: bytes) -> Dict[bytes, Channel]:\n        return self._lnwallet_or_lngossip.channels_for_peer(node_id)\n\n    def get_peer_by_pubkey(self, pubkey: bytes) -> Optional[Peer]:\n        return self._peers.get(pubkey)\n\n    def get_node_alias(self, node_id: bytes) -> Optional[str]:\n        \"\"\"Returns the alias of the node, or None if unknown.\"\"\"\n        node_alias = None\n        if not self.uses_trampoline():\n            node_info = self.channel_db.get_node_info_for_node_id(node_id)\n            if node_info:\n                node_alias = node_info.alias\n        else:\n            for k, v in hardcoded_trampoline_nodes().items():\n                if v.pubkey.startswith(node_id):\n                    node_alias = k\n                    break\n        return node_alias\n\n    async def maybe_listen(self):\n        # FIXME: only one LNPeerManager can listen at a time (single port)\n        listen_addr = self.config.LIGHTNING_LISTEN\n        if listen_addr:\n            self.logger.info(f'lightning_listen enabled. will try to bind: {listen_addr!r}')\n            try:\n                netaddr = NetAddress.from_string(listen_addr)\n            except Exception as e:\n                self.logger.error(f\"failed to parse config key '{self.config.cv.LIGHTNING_LISTEN.key()}'. got: {e!r}\")\n                return\n            addr = str(netaddr.host)\n\n            async def cb(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):\n                transport = LNResponderTransport(self.node_keypair.privkey, reader, writer)\n                try:\n                    node_id = await transport.handshake()\n                except Exception as e:\n                    self.logger.info(f'handshake failure from incoming connection: {e!r}')\n                    return\n                peername = writer.get_extra_info('peername')\n                self.logger.debug(f\"handshake done for incoming peer: {peername=}, node_id={node_id.hex()}\")\n                await self._add_peer_from_transport(node_id=node_id, transport=transport)\n            try:\n                self.listen_server = await asyncio.start_server(cb, addr, netaddr.port)\n            except OSError as e:\n                self.logger.error(f\"cannot listen for lightning p2p. error: {e!r}\")\n\n    async def main_loop(self):\n        self.logger.info(\"starting taskgroup.\")\n        try:\n            async with self.taskgroup as group:\n                await group.spawn(asyncio.Event().wait)  # run forever (until cancel)\n        except Exception as e:\n            self.logger.exception(\"taskgroup died.\")\n        finally:\n            self.logger.info(\"taskgroup stopped.\")\n\n    async def _maintain_connectivity(self):\n        while True:\n            await asyncio.sleep(1)\n            if self.stopping_soon:\n                return\n            now = time.time()\n            if len(self._peers) >= NUM_PEERS_TARGET:\n                continue\n            peers = await self._get_next_peers_to_try()\n            for peer in peers:\n                if self._can_retry_addr(peer, now=now):\n                    try:\n                        await self._add_peer(peer.host, peer.port, peer.pubkey)\n                    except ErrorAddingPeer as e:\n                        self.logger.info(f\"failed to add peer: {peer}. exc: {e!r}\")\n\n    async def _add_peer(self, host: str, port: int, node_id: bytes) -> Peer:\n        if node_id in self._peers:\n            return self._peers[node_id]\n        port = int(port)\n        peer_addr = LNPeerAddr(host, port, node_id)\n        self._trying_addr_now(peer_addr)\n        self.logger.info(f\"adding peer {peer_addr}\")\n        if node_id == self.node_keypair.pubkey or self.is_our_lnwallet(node_id):\n            raise ErrorAddingPeer(\"cannot connect to self\")\n        transport = LNTransport(self.node_keypair.privkey, peer_addr,\n                                e_proxy=ESocksProxy.from_network_settings(self.network))\n        peer = await self._add_peer_from_transport(node_id=node_id, transport=transport)\n        assert peer\n        return peer\n\n    async def _add_peer_from_transport(self, *, node_id: bytes, transport: LNTransportBase) -> Optional[Peer]:\n        with self.lock:\n            existing_peer = self._peers.get(node_id)\n            if existing_peer:\n                # Two instances of the same wallet are attempting to connect simultaneously.\n                # If we let the new connection replace the existing one, the two instances might\n                # both keep trying to reconnect, resulting in neither being usable.\n                if existing_peer.is_initialized():\n                    # give priority to the existing connection\n                    transport.close()\n                    return None\n                else:\n                    # Use the new connection. (e.g. old peer might be an outgoing connection\n                    # for an outdated host/port that will never connect)\n                    existing_peer.close_and_cleanup()\n            # limit max number of incoming channel-less peers.\n            # what to do if limit is reached?\n            # - chosen strategy: we don't allow new connections.\n            #   - drawback: attacker can use up all our slots\n            # - alternative: kick oldest channel-less peer\n            #   - drawback: if many legit peers want to connect to us, we will keep kicking them\n            #               in round-robin, and they will keep reconnecting. no stable state -> we self-DOS\n            # TODO make slots IP-based?\n            if isinstance(transport, LNResponderTransport):\n                assert node_id not in self._channelless_incoming_peers\n                chans = [chan for chan in self.channels_for_peer(node_id).values() if chan.is_funded()]\n                if not chans:\n                    if len(self._channelless_incoming_peers) > 100:\n                        transport.close()\n                        return None\n                    self._channelless_incoming_peers.add(node_id)\n            # checks done: we are adding this peer.\n            peer = Peer(self._lnwallet_or_lngossip, node_id, transport)\n            assert node_id not in self._peers\n            self._peers[node_id] = peer\n        await self.taskgroup.spawn(peer.main_loop())\n        return peer\n\n    def peer_closed(self, peer: Peer) -> None:\n        if isinstance(self._lnwallet_or_lngossip, LNWallet):\n            for chan in self.channels_for_peer(peer.pubkey).values():\n                chan.peer_state = PeerState.DISCONNECTED\n                util.trigger_callback('channel', self._lnwallet_or_lngossip.wallet, chan)\n        with self.lock:\n            peer2 = self._peers.get(peer.pubkey)\n            if peer2 is peer:\n                self._peers.pop(peer.pubkey)\n            self._channelless_incoming_peers.discard(peer.pubkey)\n\n    def num_peers(self) -> int:\n        return sum([p.is_initialized() for p in self.peers.values()])\n\n    def is_our_lnwallet(self, node_id: bytes) -> bool:\n        \"\"\"Check if node_id is one of our own wallets\"\"\"\n        wallets = self.network.daemon.get_wallets()\n        for wallet in wallets.values():\n            if wallet.lnworker and wallet.lnworker.node_keypair.pubkey == node_id:\n                return True\n        return False\n\n    def start_network(\n        self, network: 'Network', *,\n        listen: bool = False,\n        maintain_random_peers: bool = False,\n    ) -> None:\n        assert network\n        assert self.network is None, \"already started\"\n        self.network = network\n        self._add_peers_from_config()\n        asyncio.run_coroutine_threadsafe(self.main_loop(), get_asyncio_loop())\n        if listen:\n            tg_coro = self.taskgroup.spawn(self.maybe_listen())\n            asyncio.run_coroutine_threadsafe(tg_coro, get_asyncio_loop())\n        if maintain_random_peers:\n            tg_coro = self.taskgroup.spawn(self._maintain_connectivity())\n            asyncio.run_coroutine_threadsafe(tg_coro, get_asyncio_loop())\n\n    async def stop(self):\n        self.stopping_soon = True\n        if self.listen_server:\n            self.listen_server.close()\n        self.unregister_callbacks()\n        await self.taskgroup.cancel_remaining()\n\n    def _add_peers_from_config(self):\n        peer_list = self.config.LIGHTNING_PEERS or []\n        for host, port, pubkey in peer_list:\n            asyncio.run_coroutine_threadsafe(\n                self._add_peer(host, int(port), bfh(pubkey)),\n                get_asyncio_loop())\n\n    def is_good_peer(self, peer: LNPeerAddr) -> bool:\n        # the purpose of this method is to filter peers that advertise the desired feature bits\n        # it is disabled for now, because feature bits published in node announcements seem to be unreliable\n        return True\n        node_id = peer.pubkey\n        node = self.channel_db._nodes.get(node_id)\n        if not node:\n            return False\n        try:\n            ln_compare_features(self.features, node.features)\n        except IncompatibleLightningFeatures:\n            return False\n        #self.logger.info(f'is_good {peer.host}')\n        return True\n\n    def on_peer_successfully_established(self, peer: Peer) -> None:\n        if isinstance(peer.transport, LNTransport):\n            peer_addr = peer.transport.peer_addr\n            # reset connection attempt count\n            self._on_connection_successfully_established(peer_addr)\n            if not self.uses_trampoline():\n                # add into channel db\n                self.channel_db.add_recent_peer(peer_addr)\n            # save network address into channels we might have with peer\n            for chan in peer.channels.values():\n                chan.add_or_update_peer_addr(peer_addr)\n\n    async def _get_next_peers_to_try(self) -> Sequence[LNPeerAddr]:\n        now = time.time()\n        await self.channel_db.data_loaded.wait()\n        # first try from recent peers\n        recent_peers = self.channel_db.get_recent_peers()\n        for peer in recent_peers:\n            if not peer:\n                continue\n            if peer.pubkey in self._peers:\n                continue\n            if not self._can_retry_addr(peer, now=now):\n                continue\n            if not self.is_good_peer(peer):\n                continue\n            if peer.is_onion() and not self.network.proxy or not self.network.proxy.enabled:\n                continue\n            return [peer]\n        # try random peer from graph\n        unconnected_nodes = self.channel_db.get_200_randomly_sorted_nodes_not_in(self.peers.keys())\n        if unconnected_nodes:\n            for node_id in unconnected_nodes:\n                addrs = self.channel_db.get_node_addresses(node_id)\n                if not addrs:\n                    continue\n                address = self.choose_preferred_address(list(addrs))\n                if not address:\n                    continue\n                host, port, timestamp = address\n                try:\n                    peer = LNPeerAddr(host, port, node_id)\n                except ValueError:\n                    continue\n                if not self._can_retry_addr(peer, now=now):\n                    continue\n                if not self.is_good_peer(peer):\n                    continue\n                #self.logger.info('taking random ln peer from our channel db')\n                return [peer]\n\n        # getting desperate... let's try hardcoded fallback list of peers\n        fallback_list = constants.net.FALLBACK_LN_NODES\n        fallback_list = [peer for peer in fallback_list if self._can_retry_addr(peer, now=now)]\n        if fallback_list:\n            return [random.choice(fallback_list)]\n\n        # last resort: try dns seeds (BOLT-10)\n        return await self._get_peers_from_dns_seeds()\n\n    async def _get_peers_from_dns_seeds(self) -> Sequence[LNPeerAddr]:\n        # Return several peers to reduce the number of dns queries.\n        if not constants.net.LN_DNS_SEEDS:\n            return []\n        dns_seed = random.choice(constants.net.LN_DNS_SEEDS)\n        self.logger.info('asking dns seed \"{}\" for ln peers'.format(dns_seed))\n        try:\n            # note: this might block for several seconds\n            # this will include bech32-encoded-pubkeys and ports\n            srv_answers = await resolve_dns_srv('r{}.{}'.format(\n                constants.net.LN_REALM_BYTE, dns_seed))\n        except dns.exception.DNSException as e:\n            self.logger.info(f'failed querying (1) dns seed \"{dns_seed}\" for ln peers: {repr(e)}')\n            return []\n        random.shuffle(srv_answers)\n        num_peers = 2 * NUM_PEERS_TARGET\n        srv_answers = srv_answers[:num_peers]\n        # we now have pubkeys and ports but host is still needed\n        peers = []\n        for srv_ans in srv_answers:\n            try:\n                # note: this might take several seconds\n                answers = await dns.asyncresolver.resolve(srv_ans['host'])\n            except dns.exception.DNSException as e:\n                self.logger.info(f'failed querying (2) dns seed \"{dns_seed}\" for ln peers: {repr(e)}')\n                continue\n            try:\n                ln_host = str(answers[0])\n                port = int(srv_ans['port'])\n                bech32_pubkey = srv_ans['host'].split('.')[0]\n                pubkey = get_compressed_pubkey_from_bech32(bech32_pubkey)\n                peers.append(LNPeerAddr(ln_host, port, pubkey))\n            except Exception as e:\n                self.logger.info(f'error with parsing peer from dns seed: {repr(e)}')\n                continue\n        self.logger.info(f'got {len(peers)} ln peers from dns seed')\n        return peers\n\n    def choose_preferred_address(self, addr_list: Sequence[Tuple[str, int, int]]) -> Optional[Tuple[str, int, int]]:\n        assert len(addr_list) >= 1\n        # choose the most recent one that is an IP\n        for host, port, timestamp in sorted(addr_list, key=lambda a: -a[2]):\n            if is_ip_address(host):\n                return host, port, timestamp\n        if not self.network.proxy or not self.network.proxy.enabled:\n            addr_list = [(h, p, ts) for h, p, ts in addr_list if not h.endswith('.onion')]\n        if not addr_list:\n            return None\n        # otherwise choose one at random\n        choice = random.choice(addr_list)\n        return choice\n\n    @event_listener\n    def on_event_proxy_set(self, *args):\n        for peer in self.peers.values():\n            peer.close_and_cleanup()\n        self._clear_addr_retry_times()\n\n    @log_exceptions\n    async def add_peer(self, connect_str: str) -> Peer:\n        node_id, rest = extract_nodeid(connect_str)\n        peer = self._peers.get(node_id)\n        if not peer:\n            if rest is not None:\n                host, port = split_host_port(rest)\n            else:\n                if self.uses_trampoline():\n                    addr = trampolines_by_id().get(node_id)\n                    if not addr:\n                        raise ConnStringFormatError(_('Address unknown for node:') + ' ' + node_id.hex())\n                    host, port = addr.host, addr.port\n                else:\n                    addrs = self.channel_db.get_node_addresses(node_id)\n                    if not addrs or not (address := self.choose_preferred_address(list(addrs))):\n                        raise ConnStringFormatError(_('Don\\'t know any addresses for node:') + ' ' + node_id.hex())\n                    host, port, timestamp = address\n            port = int(port)\n\n            if not self.network.proxy or not self.network.proxy.enabled:\n                # Try DNS-resolving the host (if needed). This is simply so that\n                # the caller gets a nice exception if it cannot be resolved.\n                # (we don't do the DNS lookup if a proxy is set, to avoid a DNS-leak)\n                if host.endswith('.onion'):\n                    raise ConnStringFormatError(_('.onion address, but no proxy configured'))\n                try:\n                    await asyncio.get_running_loop().getaddrinfo(host, port)\n                except socket.gaierror:\n                    raise ConnStringFormatError(_('Hostname does not resolve (getaddrinfo failed)'))\n\n            # add peer\n            peer = await self._add_peer(host, port, node_id)\n        return peer\n\n    async def reestablish_peer_for_given_channel(self, chan: Channel) -> None:\n        await self.taskgroup.spawn(self._reestablish_peer_for_given_channel(chan))\n\n    @ignore_exceptions\n    @log_exceptions\n    async def _reestablish_peer_for_given_channel(self, chan: Channel) -> None:\n        now = time.time()\n        peer_addresses = []\n        if self.uses_trampoline():\n            addr = trampolines_by_id().get(chan.node_id)\n            if addr:\n                peer_addresses.append(addr)\n        else:\n            # will try last good address first, from gossip\n            last_good_addr = self.channel_db.get_last_good_address(chan.node_id)\n            if last_good_addr:\n                peer_addresses.append(last_good_addr)\n            # will try addresses for node_id from gossip\n            addrs_from_gossip = self.channel_db.get_node_addresses(chan.node_id) or []\n            for host, port, ts in addrs_from_gossip:\n                peer_addresses.append(LNPeerAddr(host, port, chan.node_id))\n        # will try addresses stored in channel storage\n        peer_addresses += list(chan.get_peer_addresses())\n        # Done gathering addresses.\n        # Now select first one that has not failed recently.\n        for peer in peer_addresses:\n            if self._can_retry_addr(peer, urgent=True, now=now):\n                await self._add_peer(peer.host, peer.port, peer.pubkey)\n                return\n\n    async def reestablish_peer_for_zero_conf_trusted_node(self) -> None:\n        if self.config.ZEROCONF_TRUSTED_NODE:\n            peer = LNPeerAddr.from_str(self.config.ZEROCONF_TRUSTED_NODE)\n            if self._can_retry_addr(peer, urgent=True):\n                await self._add_peer(peer.host, peer.port, peer.pubkey)\n\n\nclass LNGossip(Logger):\n    \"\"\"The LNGossip class is a separate, unannounced Lightning node with random id that is just querying\n    gossip from other nodes. The LNGossip node does not satisfy gossip queries, this is done by the\n    LNWallet class(es). LNWallets are the advertised nodes used for actual payments and only satisfy\n    peer queries without fetching gossip themselves. This separation is done so that gossip can be queried\n    independently of the active LNWallets. LNGossip keeps a curated batch of gossip in _forwarding_gossip\n    that is fetched by the LNWallets for regular forwarding.\"\"\"\n    max_age = 14*24*3600\n\n    def __init__(self, config: 'SimpleConfig'):\n        self.config = config\n        seed = os.urandom(32)\n        node = BIP32Node.from_rootseed(seed, xtype='standard')\n        xprv = node.to_xprv()\n        node_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.NODE_KEY)\n        Logger.__init__(self)\n        self.lnpeermgr = LNPeerManager(node_keypair, features=LNGOSSIP_FEATURES, config=self.config, lnwallet_or_lngossip=self)\n        self.taskgroup = OldTaskGroup()\n        self.unknown_ids = set()\n        self._forwarding_gossip = []  # type: List[GossipForwardingMessage]\n        self._last_gossip_batch_ts = 0  # type: int\n        self._forwarding_gossip_lock = asyncio.Lock()\n        self.gossip_request_semaphore = asyncio.Semaphore(5)\n        # statistics\n        self._num_chan_ann = 0\n        self._num_node_ann = 0\n        self._num_chan_upd = 0\n        self._num_chan_upd_good = 0\n\n    @property\n    def features(self) -> 'LnFeatures':\n        return self.lnpeermgr.features\n\n    @property\n    def network(self) -> Optional['Network']:\n        return self.lnpeermgr.network\n\n    @property\n    def channel_db(self) -> 'ChannelDB':\n        return self.network.channel_db if self.network else None\n\n    def uses_trampoline(self) -> bool:\n        return not bool(self.channel_db)\n\n    async def main_loop(self):\n        self.logger.info(\"starting taskgroup.\")\n        try:\n            async with self.taskgroup as group:\n                await group.spawn(asyncio.Event().wait)  # run forever (until cancel)\n        except Exception as e:\n            self.logger.exception(\"taskgroup died.\")\n        finally:\n            self.logger.info(\"taskgroup stopped.\")\n\n    def start_network(self, network: 'Network'):\n        asyncio.run_coroutine_threadsafe(self.main_loop(), get_asyncio_loop())\n        self.lnpeermgr.start_network(network, maintain_random_peers=True)\n        for coro in [\n                self.maintain_db(),\n                self._maintain_forwarding_gossip()\n        ]:\n            tg_coro = self.taskgroup.spawn(coro)\n            asyncio.run_coroutine_threadsafe(tg_coro, get_asyncio_loop())\n\n    async def stop(self):\n        await self.lnpeermgr.stop()\n        await self.taskgroup.cancel_remaining()\n\n    async def maintain_db(self):\n        await self.channel_db.data_loaded.wait()\n        while True:\n            if len(self.unknown_ids) == 0:\n                self.channel_db.prune_old_policies(self.max_age)\n                self.channel_db.prune_orphaned_channels()\n            await asyncio.sleep(120)\n\n    async def _maintain_forwarding_gossip(self):\n        await self.channel_db.data_loaded.wait()\n        await self.wait_for_sync()\n        while True:\n            async with self._forwarding_gossip_lock:\n                self._forwarding_gossip = self.channel_db.get_forwarding_gossip_batch()\n                self._last_gossip_batch_ts = int(time.time())\n            self.logger.debug(f\"{len(self._forwarding_gossip)} gossip messages available to forward\")\n            await asyncio.sleep(60)\n\n    async def get_forwarding_gossip(self) -> tuple[List[GossipForwardingMessage], int]:\n        async with self._forwarding_gossip_lock:\n            return self._forwarding_gossip, self._last_gossip_batch_ts\n\n    async def add_new_ids(self, ids: Iterable[bytes]):\n        known = self.channel_db.get_channel_ids()\n        new = set(ids) - set(known)\n        self.unknown_ids.update(new)\n        util.trigger_callback('unknown_channels', len(self.unknown_ids))\n        util.trigger_callback('gossip_peers', self.lnpeermgr.num_peers())\n        util.trigger_callback('ln_gossip_sync_progress')\n\n    def get_ids_to_query(self) -> Sequence[bytes]:\n        N = 500\n        l = list(self.unknown_ids)\n        self.unknown_ids = set(l[N:])\n        util.trigger_callback('unknown_channels', len(self.unknown_ids))\n        util.trigger_callback('ln_gossip_sync_progress')\n        return l[0:N]\n\n    def get_sync_progress_estimate(self) -> Tuple[Optional[int], Optional[int], Optional[int]]:\n        \"\"\"Estimates the gossip synchronization process and returns the number\n        of synchronized channels, the total channels in the network and a\n        rescaled percentage of the synchronization process.\"\"\"\n        if self.lnpeermgr.num_peers() == 0:\n            return None, None, None\n        nchans_with_0p, nchans_with_1p, nchans_with_2p = self.channel_db.get_num_channels_partitioned_by_policy_count()\n        num_db_channels = nchans_with_0p + nchans_with_1p + nchans_with_2p\n        num_nodes = self.channel_db.num_nodes\n        num_nodes_associated_to_chans = max(len(self.channel_db._channels_for_node.keys()), 1)\n        # some channels will never have two policies (only one is in gossip?...)\n        # so if we have at least 1 policy for a channel, we consider that channel \"complete\" here\n        current_est = num_db_channels - nchans_with_0p\n        total_est = len(self.unknown_ids) + num_db_channels\n\n        progress_chans = current_est / total_est if total_est and current_est else 0\n        # consider that we got at least 10% of the node anns of node ids we know about\n        progress_nodes = min((num_nodes / num_nodes_associated_to_chans) * 10, 1)\n        progress = (progress_chans * 3 + progress_nodes) / 4  # weigh the channel progress higher\n        # self.logger.debug(f\"Sync process chans: {progress_chans} | Progress nodes: {progress_nodes} | \"\n        #                   f\"Total progress: {progress} | NUM_NODES: {num_nodes} / {num_nodes_associated_to_chans}\")\n        progress_percent = (1.0 / 0.95 * progress) * 100\n        progress_percent = min(progress_percent, 100)\n        progress_percent = round(progress_percent)\n        # take a minimal number of synchronized channels to get a more accurate\n        # percentage estimate\n        if current_est < 200:\n            progress_percent = 0\n        return current_est, total_est, progress_percent\n\n    @ignore_exceptions\n    @log_exceptions\n    async def process_gossip(self, chan_anns, node_anns, chan_upds):\n        # note: we run in the originating peer's TaskGroup, so we can safely raise here\n        #       and disconnect only from that peer\n        await self.channel_db.data_loaded.wait()\n\n        # channel announcements\n        def process_chan_anns():\n            for payload in chan_anns:\n                self.channel_db.verify_channel_announcement(payload)\n            self.channel_db.add_channel_announcements(chan_anns)\n        await run_in_thread(process_chan_anns)\n\n        # node announcements\n        def process_node_anns():\n            for payload in node_anns:\n                self.channel_db.verify_node_announcement(payload)\n            self.channel_db.add_node_announcements(node_anns)\n        await run_in_thread(process_node_anns)\n        # channel updates\n        categorized_chan_upds = await run_in_thread(partial(\n            self.channel_db.add_channel_updates,\n            chan_upds,\n            max_age=self.max_age))\n        orphaned = categorized_chan_upds.orphaned\n        if orphaned:\n            self.logger.info(f'adding {len(orphaned)} unknown channel ids')\n            orphaned_ids = [c['short_channel_id'] for c in orphaned]\n            await self.add_new_ids(orphaned_ids)\n\n        self._num_chan_ann += len(chan_anns)\n        self._num_node_ann += len(node_anns)\n        self._num_chan_upd += len(chan_upds)\n        self._num_chan_upd_good += len(categorized_chan_upds.good)\n\n    def is_synced(self) -> bool:\n        _, _, percentage_synced = self.get_sync_progress_estimate()\n        if percentage_synced is not None and percentage_synced >= 100:\n            return True\n        return False\n\n    async def wait_for_sync(self, times_to_check: int = 3):\n        \"\"\"Check if we have 100% sync progress `times_to_check` times in a row (because the\n        estimate often jumps back after some seconds when doing initial sync).\"\"\"\n        while True:\n            if self.is_synced():\n                times_to_check -= 1\n                if times_to_check <= 0:\n                    return\n            await asyncio.sleep(10)\n            # flush the gossip queue so we don't forward old gossip after sync is complete\n            self.channel_db.get_forwarding_gossip_batch()\n\n    def channels_for_peer(self, node_id: bytes) -> Dict[bytes, Channel]:\n        return {}\n\n\nclass PaySession(Logger):\n\n    # how long we wait for another htlc to resolve after receiving a failure for one sent htlc.\n    TIMEOUT_WAIT_FOR_NEXT_RESOLVED_HTLC = 0.5\n\n    def __init__(\n            self,\n            *,\n            payment_hash: bytes,\n            payment_secret: bytes,\n            initial_trampoline_fee_level: int,\n            invoice_features: int,\n            r_tags,\n            min_final_cltv_delta: int,  # delta for last node (typically from invoice)\n            amount_to_pay: int,  # total payment amount final receiver will get\n            invoice_pubkey: bytes,\n            uses_trampoline: bool,  # whether sender uses trampoline or gossip\n            use_two_trampolines: bool,  # whether legacy payments will try to use two trampolines\n    ):\n        assert payment_hash\n        assert payment_secret\n        self.payment_hash = payment_hash\n        self.payment_secret = payment_secret\n        self.payment_key = payment_hash + payment_secret\n        Logger.__init__(self)\n\n        self.invoice_features = LnFeatures(invoice_features)\n        self.r_tags = r_tags\n        self.min_final_cltv_delta = min_final_cltv_delta\n        self.amount_to_pay = amount_to_pay\n        self.invoice_pubkey = invoice_pubkey\n\n        self.sent_htlcs_q = asyncio.Queue()  # type: asyncio.Queue[HtlcLog]\n        self.start_time = time.time()\n\n        self.uses_trampoline = uses_trampoline\n        self.trampoline_fee_level = initial_trampoline_fee_level\n        self.failed_trampoline_routes = []\n        self.use_two_trampolines = use_two_trampolines\n        self._sent_buckets = dict()  # psecret_bucket -> (amount_sent, amount_failed)\n\n        self._amount_inflight = 0  # what we sent in htlcs (that receiver gets, without fees)\n        self._nhtlcs_inflight = 0\n        self.is_active = True  # is still trying to send new htlcs?\n\n    def diagnostic_name(self):\n        pkey = sha256(self.payment_key)\n        return f\"{self.payment_hash[:4].hex()}-{pkey[:2].hex()}\"\n\n    @property\n    def number_htlcs_inflight(self) -> int:\n        return self._nhtlcs_inflight\n\n    def maybe_raise_trampoline_fee(self, htlc_log: HtlcLog):\n        if htlc_log.trampoline_fee_level == self.trampoline_fee_level:\n            self.trampoline_fee_level += 1\n            self.failed_trampoline_routes = []\n            self.logger.info(f'raising trampoline fee level {self.trampoline_fee_level}')\n        else:\n            self.logger.info(f'NOT raising trampoline fee level, already at {self.trampoline_fee_level}')\n\n    def handle_failed_trampoline_htlc(self, *, htlc_log: HtlcLog, failure_msg: OnionRoutingFailure):\n        # FIXME The trampoline nodes in the path are chosen randomly.\n        #       Some of the errors might depend on how we have chosen them.\n        #       Having more attempts is currently useful in part because of the randomness,\n        #       instead we should give feedback to create_routes_for_payment.\n        # Sometimes the trampoline node fails to send a payment and returns\n        # TEMPORARY_CHANNEL_FAILURE, while it succeeds with a higher trampoline fee.\n        if failure_msg.code in (\n                OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT,\n                OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON,\n                OnionFailureCode.TEMPORARY_CHANNEL_FAILURE):\n            # TODO: parse the node policy here (not returned by eclair yet)\n            # TODO: erring node is always the first trampoline even if second\n            #  trampoline demands more fees, we can't influence this\n            self.maybe_raise_trampoline_fee(htlc_log)\n        elif self.use_two_trampolines:\n            self.use_two_trampolines = False\n        elif failure_msg.code in (\n                OnionFailureCode.UNKNOWN_NEXT_PEER,\n                OnionFailureCode.TEMPORARY_NODE_FAILURE):\n            trampoline_route = htlc_log.route\n            r = [hop.end_node.hex() for hop in trampoline_route]\n            self.logger.info(f'failed trampoline route: {r}')\n            if r not in self.failed_trampoline_routes:\n                self.failed_trampoline_routes.append(r)\n            else:\n                pass  # maybe the route was reused between different MPP parts\n        else:\n            raise PaymentFailure(failure_msg.code_name())\n\n    async def wait_for_one_htlc_to_resolve(self) -> HtlcLog:\n        self.logger.info(f\"waiting... amount_inflight={self._amount_inflight}. nhtlcs_inflight={self._nhtlcs_inflight}\")\n        htlc_log = await self.sent_htlcs_q.get()\n        self._amount_inflight -= htlc_log.amount_msat\n        self._nhtlcs_inflight -= 1\n        if self._amount_inflight < 0 or self._nhtlcs_inflight < 0:\n            raise Exception(f\"amount_inflight={self._amount_inflight}, nhtlcs_inflight={self._nhtlcs_inflight}. both should be >= 0 !\")\n        return htlc_log\n\n    def add_new_htlc(self, sent_htlc_info: SentHtlcInfo):\n        self._nhtlcs_inflight += 1\n        self._amount_inflight += sent_htlc_info.amount_receiver_msat\n        if self._amount_inflight > self.amount_to_pay:  # safety belts\n            raise Exception(f\"amount_inflight={self._amount_inflight} > amount_to_pay={self.amount_to_pay}\")\n        shi = sent_htlc_info\n        bkey = shi.payment_secret_bucket\n        # if we sent MPP to a trampoline, add item to sent_buckets\n        if self.uses_trampoline and shi.amount_msat != shi.bucket_msat:\n            if bkey not in self._sent_buckets:\n                self._sent_buckets[bkey] = (0, 0)\n            amount_sent, amount_failed = self._sent_buckets[bkey]\n            amount_sent += shi.amount_receiver_msat\n            self._sent_buckets[bkey] = amount_sent, amount_failed\n\n    def on_htlc_fail_get_fail_amt_to_propagate(self, sent_htlc_info: SentHtlcInfo) -> Optional[int]:\n        shi = sent_htlc_info\n        # check sent_buckets if we use trampoline\n        bkey = shi.payment_secret_bucket\n        if self.uses_trampoline and bkey in self._sent_buckets:\n            amount_sent, amount_failed = self._sent_buckets[bkey]\n            amount_failed += shi.amount_receiver_msat\n            self._sent_buckets[bkey] = amount_sent, amount_failed\n            if amount_sent != amount_failed:\n                self.logger.info('bucket still active...')\n                return None\n            self.logger.info('bucket failed')\n            return amount_sent\n        # not using trampoline buckets\n        return shi.amount_receiver_msat\n\n    def get_outstanding_amount_to_send(self) -> int:\n        return self.amount_to_pay - self._amount_inflight\n\n    def can_be_deleted(self) -> bool:\n        \"\"\"Returns True iff finished sending htlcs AND all pending htlcs have resolved.\"\"\"\n        if self.is_active:\n            return False\n        # note: no one is consuming from sent_htlcs_q anymore\n        nhtlcs_resolved = self.sent_htlcs_q.qsize()\n        assert nhtlcs_resolved <= self._nhtlcs_inflight\n        return nhtlcs_resolved == self._nhtlcs_inflight\n\n\nclass LNWallet(Logger):\n\n    lnwatcher: Optional['LNWatcher']\n    MPP_EXPIRY = 120\n    TIMEOUT_SHUTDOWN_FAIL_PENDING_HTLCS = 3  # seconds\n    PAYMENT_TIMEOUT = 120\n    MPP_SPLIT_PART_FRACTION = 0.2\n    MPP_SPLIT_PART_MINAMT_MSAT = 5_000_000\n\n    def __init__(self, wallet: 'Abstract_Wallet', xprv, *, features: LnFeatures = None):\n        self.wallet = wallet\n        self.config = wallet.config\n        self.db = wallet.db\n        self.node_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.NODE_KEY)\n        self.backup_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.BACKUP_CIPHER).privkey\n        self.static_payment_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.PAYMENT_BASE)\n        self.payment_secret_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.PAYMENT_SECRET_KEY).privkey\n        self.funding_root_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.FUNDING_ROOT_KEY)\n        Logger.__init__(self)\n        if features is None:\n            features = LNWALLET_FEATURES\n            if self.config.ENABLE_ANCHOR_CHANNELS:\n                features |= LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT\n            if self.config.ACCEPT_ZEROCONF_CHANNELS:\n                features |= LnFeatures.OPTION_ZEROCONF_OPT\n            if self.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS or self.config.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS:\n                features |= LnFeatures.OPTION_ONION_MESSAGE_OPT\n            if self.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS and self.config.LIGHTNING_USE_GOSSIP:\n                features |= LnFeatures.GOSSIP_QUERIES_OPT  # signal we have gossip to fetch\n        Logger.__init__(self)\n        self.lock = threading.RLock()\n        self.lnpeermgr = LNPeerManager(self.node_keypair, features=features, config=self.config, lnwallet_or_lngossip=self)\n        self.taskgroup = OldTaskGroup()\n        self.lnwatcher = LNWatcher(self)\n        self.lnrater: LNRater = None\n        # \"RHASH:direction\" -> amount_msat, status, min_final_cltv_delta, expiry_delay, creation_ts, invoice_features\n        self.payment_info = self.db.get_dict('lightning_payments')  # type: dict[str, Tuple[Optional[int], int, int, int, int, int]]\n        self._preimages = self.db.get_dict('lightning_preimages')   # RHASH -> (preimage, is_public)\n        self._bolt11_cache = {}\n        # note: this sweep_address is only used as fallback; as it might result in address-reuse\n        self.logs = defaultdict(list)  # type: Dict[str, List[HtlcLog]]  # key is RHASH  # (not persisted)\n        # used in tests\n        self.enable_htlc_settle = True\n        self.enable_htlc_forwarding = True\n\n        # note: accessing channels (besides simple lookup) needs self.lock!\n        self._channels = {}  # type: Dict[bytes, Channel]\n        channels = self.db.get_dict(\"channels\")\n        for channel_id, c in random_shuffled_copy(channels.items()):\n            self._channels[bfh(channel_id)] = chan = Channel(c, lnworker=self)\n            self.wallet.set_reserved_addresses_for_chan(chan, reserved=True)\n\n        self._channel_backups = {}  # type: Dict[bytes, ChannelBackup]\n        # order is important: imported should overwrite onchain\n        for name in [\"onchain_channel_backups\", \"imported_channel_backups\"]:\n            channel_backups = self.db.get_dict(name)\n            for channel_id, storage in channel_backups.items():\n                self._channel_backups[bfh(channel_id)] = cb = ChannelBackup(storage, lnworker=self)\n                self.wallet.set_reserved_addresses_for_chan(cb, reserved=True)\n\n        self._paysessions = dict()                      # type: Dict[bytes, PaySession]\n        self.sent_htlcs_info = dict()                   # type: Dict[SentHtlcKey, SentHtlcInfo]\n        self.received_mpp_htlcs = self.db.get_dict('received_mpp_htlcs')   # type: Dict[str, ReceivedMPPStatus]  # payment_key -> ReceivedMPPStatus\n        self._channel_sending_capacity_lock = asyncio.Lock()\n\n        # detect inflight payments\n        self.inflight_payments = set()        # (not persisted) keys of invoices that are in PR_INFLIGHT state\n        for payment_hash in self.get_payments(status='inflight').keys():\n            self.set_invoice_status(payment_hash.hex(), PR_INFLIGHT)\n\n        # payment forwarding\n        self.active_forwardings = self.db.get_dict('active_forwardings')    # type: Dict[str, List[str]]        # Dict: payment_key -> list of htlc_keys\n        self.forwarding_failures = self.db.get_dict('forwarding_failures')  # type: Dict[str, Tuple[str, str]]  # Dict: payment_key -> (error_bytes, error_message)\n        self.downstream_to_upstream_htlc = {}                               # type: Dict[str, str]              # Dict: htlc_key -> htlc_key (not persisted)\n\n        # k: payment_hashes of htlcs that we should not expire even if we don't know the preimage\n        # v: If `None` the htlcs won't get expired and potentially get timed out in a force close.\n        #    Note: it might not be safe to release the preimage shortly before expiry as this would allow the\n        #          remote node to ignore our fulfill_htlc, wait until expiry and try to time out the htlc onchain\n        #          in a fee race against us and then use our released preimage to fulfill upstream.\n        # v: If `int`: Overwrites `MIN_FINAL_CLTV_DELTA_ACCEPTED` in htlc switch and allows to set custom\n        #              expiration delta. The htlcs will get expired if their blocks left to expiry are\n        #              below the specified expiration delta.\n        # htlcs will get settled as soon as the preimage becomes available\n        self.dont_expire_htlcs = self.db.get_dict('dont_expire_htlcs')      # type: Dict[str, Optional[int]]\n\n        # k: payment_hash of payments for which we don't want to release the preimage, no matter\n        # how close to expiry. Doesn't prevent htlcs from getting expired or failed if there is no\n        # preimage available. Might be used in combination with dont_expire_htlcs.\n        self.dont_settle_htlcs = self.db.get_dict('dont_settle_htlcs')  # type: Dict[str, None]\n\n        # payment_hash -> callback:\n        self.hold_invoice_callbacks = {}                # type: Dict[bytes, Callable[[bytes], Awaitable[None]]]\n        self._payment_bundles_pkey_to_canon = {}       # type: Dict[bytes, bytes]            # TODO: persist\n        self._payment_bundles_canon_to_pkeylist = {}   # type: Dict[bytes, Sequence[bytes]]  # TODO: persist\n\n        self.nostr_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.NOSTR_KEY)\n        self.swap_manager = SwapManager(wallet=self.wallet, lnworker=self)\n        self.onion_message_manager = OnionMessageManager(self)\n        self.subscribe_to_channels()\n\n    def subscribe_to_channels(self):\n        for chan in self.channels.values():\n            self.lnwatcher.add_channel(chan)\n        for cb in self.channel_backups.values():\n            self.lnwatcher.add_channel(cb)\n\n    def has_deterministic_node_id(self) -> bool:\n        return bool(self.db.get('lightning_xprv'))\n\n    def can_have_recoverable_channels(self) -> bool:\n        return (self.has_deterministic_node_id()\n                and not self.config.LIGHTNING_LISTEN)\n\n    def has_recoverable_channels(self) -> bool:\n        \"\"\"Whether *future* channels opened by this wallet would be recoverable\n        from seed (via putting OP_RETURN outputs into funding txs).\n        \"\"\"\n        return (self.can_have_recoverable_channels()\n                and self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS)\n\n    def has_anchor_channels(self) -> bool:\n        \"\"\"Returns True if any active channel is an anchor channel.\"\"\"\n        return any(chan.has_anchors() and not chan.is_closed()\n                   for chan in self.channels.values())\n\n    @property\n    def features(self) -> 'LnFeatures':\n        return self.lnpeermgr.features\n\n    @property\n    def network(self) -> Optional['Network']:\n        return self.lnpeermgr.network\n\n    @property\n    def channel_db(self) -> 'ChannelDB':\n        return self.network.channel_db if self.network else None\n\n    def uses_trampoline(self) -> bool:\n        return not bool(self.channel_db)\n\n    @property\n    def channels(self) -> Mapping[bytes, Channel]:\n        \"\"\"Returns a read-only copy of channels.\"\"\"\n        with self.lock:\n            return self._channels.copy()\n\n    @property\n    def channel_backups(self) -> Mapping[bytes, ChannelBackup]:\n        \"\"\"Returns a read-only copy of channels.\"\"\"\n        with self.lock:\n            return self._channel_backups.copy()\n\n    def get_channel_objects(self) -> Mapping[bytes, AbstractChannel]:\n        r = self.channel_backups\n        r.update(self.channels)\n        return r\n\n    def get_channel_by_id(self, channel_id: bytes) -> Optional[Channel]:\n        return self._channels.get(channel_id, None)\n\n    def diagnostic_name(self):\n        return self.wallet.diagnostic_name()\n\n    @ignore_exceptions\n    @log_exceptions\n    async def sync_with_remote_watchtower(self):\n        self.watchtower_ctns = {}\n        while True:\n            # periodically poll if the user updated 'watchtower_url'\n            await asyncio.sleep(5)\n            watchtower_url = self.config.WATCHTOWER_CLIENT_URL\n            if not watchtower_url:\n                continue\n            parsed_url = urllib.parse.urlparse(watchtower_url)\n            if not (parsed_url.scheme == 'https' or is_private_netaddress(parsed_url.hostname)):\n                self.logger.warning(f\"got watchtower URL for remote tower but we won't use it! \"\n                                    f\"can only use HTTPS (except if private IP): not using {watchtower_url!r}\")\n                continue\n            # try to sync with the remote watchtower\n            try:\n                async with make_aiohttp_session(proxy=self.network.proxy) as session:\n                    watchtower = JsonRPCClient(session, watchtower_url)\n                    watchtower.add_method('get_ctn')\n                    watchtower.add_method('add_sweep_tx')\n                    for chan in self.channels.values():\n                        await self.sync_channel_with_watchtower(chan, watchtower)\n            except aiohttp.client_exceptions.ClientConnectorError:\n                self.logger.info(f'could not contact remote watchtower {watchtower_url}')\n\n    def get_watchtower_ctn(self, channel_point):\n        return self.watchtower_ctns.get(channel_point)\n\n    async def sync_channel_with_watchtower(self, chan: Channel, watchtower):\n        outpoint = chan.funding_outpoint.to_str()\n        addr = chan.get_funding_address()\n        current_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)\n        watchtower_ctn = await watchtower.get_ctn(outpoint, addr)\n        for ctn in range(watchtower_ctn + 1, current_ctn):\n            sweeptxs = chan.create_sweeptxs_for_watchtower(ctn)\n            for tx in sweeptxs:\n                await watchtower.add_sweep_tx(outpoint, ctn, tx.inputs()[0].prevout.to_str(), tx.serialize())\n            self.watchtower_ctns[outpoint] = ctn\n\n    async def main_loop(self):\n        self.logger.info(\"starting taskgroup.\")\n        try:\n            async with self.taskgroup as group:\n                await group.spawn(asyncio.Event().wait)  # run forever (until cancel)\n        except Exception as e:\n            self.logger.exception(\"taskgroup died.\")\n        finally:\n            self.logger.info(\"taskgroup stopped.\")\n\n    def start_network(self, network: 'Network'):\n        asyncio.run_coroutine_threadsafe(self.main_loop(), get_asyncio_loop())\n        self.lnpeermgr.start_network(network, listen=True)\n        self.lnwatcher.start_network(network)\n        self.swap_manager.start_network(network)\n        self.lnrater = LNRater(self, network)\n        self.onion_message_manager.start_network(network=network)\n\n        for coro in [\n                self.lnwatcher.trigger_callbacks(),  # shortcut (don't block) if funding tx locked and verified\n                self.reestablish_peers_and_channels(),\n                self.sync_with_remote_watchtower(),\n        ]:\n            tg_coro = self.taskgroup.spawn(coro)\n            asyncio.run_coroutine_threadsafe(tg_coro, get_asyncio_loop())\n\n    async def stop(self):\n        self.lnpeermgr.stopping_soon = True\n        if self.lnpeermgr.listen_server:  # stop accepting new peers\n            self.lnpeermgr.listen_server.close()\n        async with ignore_after(self.TIMEOUT_SHUTDOWN_FAIL_PENDING_HTLCS):\n            await self.wait_for_received_pending_htlcs_to_get_removed()\n        await self.lnpeermgr.stop()\n        if self.lnwatcher:\n            self.lnwatcher.stop()\n            self.lnwatcher = None\n        if self.swap_manager and self.swap_manager.network:  # may not be present in tests\n            await self.swap_manager.stop()\n        if self.onion_message_manager:\n            await self.onion_message_manager.stop()\n        await self.taskgroup.cancel_remaining()\n\n    async def wait_for_received_pending_htlcs_to_get_removed(self):\n        assert self.lnpeermgr.stopping_soon is True\n        # We try to fail pending MPP HTLCs, and wait a bit for them to get removed.\n        # Note: even without MPP, if we just failed/fulfilled an HTLC, it is good\n        #       to wait a bit for it to become irrevocably removed.\n        # Note: we don't wait for *all htlcs* to get removed, only for those\n        #       that we can already fail/fulfill. e.g. forwarded htlcs cannot be removed\n        async with OldTaskGroup() as group:\n            for peer in self.lnpeermgr.peers.values():\n                if peer.is_initialized():\n                    await group.spawn(peer.wait_one_htlc_switch_iteration())\n        while True:\n            if all(not peer.received_htlcs_pending_removal for peer in self.lnpeermgr.peers.values()):\n                break\n            async with OldTaskGroup(wait=any) as group:\n                for peer in self.lnpeermgr.peers.values():\n                    await group.spawn(peer.received_htlc_removed_event.wait())\n\n    def get_payments(self, *, status=None) -> Mapping[bytes, List[HTLCWithStatus]]:\n        out = defaultdict(list)\n        for chan in self.channels.values():\n            d = chan.get_payments(status=status)\n            for payment_hash, plist in d.items():\n                out[payment_hash] += plist\n        return out\n\n    def get_payment_value(\n            self, sent_info: Optional['PaymentInfo'],\n            plist: List[HTLCWithStatus]\n    ) -> Tuple[PaymentDirection, int, Optional[int], int]:\n        \"\"\" fee_msat is included in amount_msat\"\"\"\n        assert plist\n        amount_msat = sum(int(x.direction) * x.htlc.amount_msat for x in plist)\n        if all(x.direction == SENT for x in plist):\n            direction = PaymentDirection.SENT\n            fee_msat = (- sent_info.amount_msat - amount_msat) if sent_info else None\n        elif all(x.direction == RECEIVED for x in plist):\n            direction = PaymentDirection.RECEIVED\n            fee_msat = None\n        elif amount_msat < 0:\n            direction = PaymentDirection.SELF_PAYMENT\n            fee_msat = - amount_msat\n        else:\n            direction = PaymentDirection.FORWARDING\n            fee_msat = - amount_msat\n        timestamp = min([htlc_with_status.htlc.timestamp for htlc_with_status in plist])\n        return direction, amount_msat, fee_msat, timestamp\n\n    def get_lightning_history(self) -> Dict[str, LightningHistoryItem]:\n        \"\"\"\n        side effect: sets defaults labels\n        note that the result is not ordered\n        \"\"\"\n        out = {}\n        for payment_hash, plist in self.get_payments(status='settled').items():\n            if len(plist) == 0:\n                continue\n            key = payment_hash.hex()\n            sent_info = self.get_payment_info(payment_hash, direction=SENT)\n            # note: just after successfully paying an invoice using MPP, amount and fee values might be shifted\n            #       temporarily: the amount only considers 'settled' htlcs (see plist above), but we might also\n            #       have some inflight htlcs still. Until all relevant htlcs settle, the amount will be lower than\n            #       expected and the fee higher (the inflight htlcs will be effectively counted as fees).\n            direction, amount_msat, fee_msat, timestamp = self.get_payment_value(sent_info, plist)\n            label = self.wallet.get_label_for_rhash(key)\n            if not label and direction == PaymentDirection.FORWARDING:\n                label = _('Forwarding')\n            preimage = self.get_preimage(payment_hash).hex()\n            group_id = self.swap_manager.get_group_id_for_payment_hash(payment_hash)\n            item = LightningHistoryItem(\n                type='payment',\n                payment_hash=payment_hash.hex(),\n                preimage=preimage,\n                amount_msat=amount_msat,\n                fee_msat=fee_msat,\n                group_id=group_id,\n                timestamp=timestamp or 0,\n                label=label,\n                direction=direction,\n            )\n            out[payment_hash.hex()] = item\n        now = int(time.time())\n        for chan in itertools.chain(self.channels.values(), self.channel_backups.values()):  # type: AbstractChannel\n            item = chan.get_funding_height()\n            if item is None:\n                continue\n            funding_txid, funding_height, funding_timestamp = item\n            label = _('Open channel') + ' ' + chan.get_id_for_log()\n            self.wallet.set_default_label(funding_txid, label)\n            self.wallet.set_group_label(funding_txid, label)\n            item = LightningHistoryItem(\n                type='channel_opening',\n                label=label,\n                group_id=funding_txid,\n                timestamp=funding_timestamp or now,\n                amount_msat=chan.balance(LOCAL, ctn=0),\n                fee_msat=None,\n                payment_hash=None,\n                preimage=None,\n                direction=None,\n            )\n            out[funding_txid] = item\n            item = chan.get_closing_height()\n            if item is None:\n                continue\n            closing_txid, closing_height, closing_timestamp = item\n            label = _('Close channel') + ' ' + chan.get_id_for_log()\n            self.wallet.set_default_label(closing_txid, label)\n            self.wallet.set_group_label(closing_txid, label)\n            item = LightningHistoryItem(\n                type='channel_closing',\n                label=label,\n                group_id=closing_txid,\n                timestamp=closing_timestamp or now,\n                amount_msat=-chan.balance(LOCAL),\n                fee_msat=None,\n                payment_hash=None,\n                preimage=None,\n                direction=None,\n            )\n            out[closing_txid] = item\n\n        # sanity check\n        balance_msat = sum([x.amount_msat for x in out.values()])\n        lb = sum(chan.balance(LOCAL) if not chan.is_closed_or_closing() else 0\n                 for chan in self.channels.values())\n        if balance_msat != lb:\n            # this typically happens when a channel is recently force closed\n            self.logger.info(f'get_lightning_history: balance mismatch {balance_msat - lb}')\n        return out\n\n    def get_groups_for_onchain_history(self) -> Dict[str, str]:\n        \"\"\"\n        returns dict: txid -> group_id\n        side effect: sets default labels\n        \"\"\"\n        groups = {}\n        # add funding events\n        for chan in itertools.chain(self.channels.values(), self.channel_backups.values()):  # type: AbstractChannel\n            item = chan.get_funding_height()\n            if item is None:\n                continue\n            funding_txid, funding_height, funding_timestamp = item\n            groups[funding_txid] = funding_txid\n            item = chan.get_closing_height()\n            if item is None:\n                continue\n            closing_txid, closing_height, closing_timestamp = item\n            groups[closing_txid] = closing_txid\n\n        d = self.swap_manager.get_groups_for_onchain_history()\n        for txid, v in d.items():\n            group_id = v['group_id']\n            label = v.get('label')\n            group_label = v.get('group_label') or label\n            groups[txid] = group_id\n            if label:\n                self.wallet.set_default_label(txid, label)\n            if group_label:\n                self.wallet.set_group_label(group_id, group_label)\n\n        return groups\n\n    def channel_peers(self) -> List[bytes]:\n        node_ids = [chan.node_id for chan in self.channels.values() if not chan.is_closed()]\n        return node_ids\n\n    def channels_for_peer(self, node_id: bytes) -> Dict[bytes, Channel]:\n        assert type(node_id) is bytes\n        return {chan_id: chan for (chan_id, chan) in self.channels.items()\n                if chan.node_id == node_id}\n\n    def channel_state_changed(self, chan: Channel):\n        if type(chan) is Channel:\n            self.save_channel(chan)\n        self.clear_invoices_cache()\n        if chan._state == ChannelState.REDEEMED:\n            self.maybe_cleanup_mpp(chan)\n        util.trigger_callback('channel', self.wallet, chan)\n\n    def save_channel(self, chan: Channel):\n        assert type(chan) is Channel\n        if chan.config[REMOTE].next_per_commitment_point == chan.config[REMOTE].current_per_commitment_point:\n            raise Exception(\"Tried to save channel with next_point == current_point, this should not happen\")\n        self.wallet.save_db()\n        util.trigger_callback('channel', self.wallet, chan)\n\n    def channel_by_txo(self, txo: str) -> Optional[AbstractChannel]:\n        for chan in self.channels.values():\n            if chan.funding_outpoint.to_str() == txo:\n                return chan\n        for chan in self.channel_backups.values():\n            if chan.funding_outpoint.to_str() == txo:\n                return chan\n        return None\n\n    async def handle_onchain_state(self, chan: Channel):\n        if self.network is None:\n            # network not started yet\n            return\n\n        if type(chan) is ChannelBackup:\n            util.trigger_callback('channel', self.wallet, chan)\n            return\n\n        if (chan.get_state() in (ChannelState.OPEN, ChannelState.SHUTDOWN)\n                and chan.should_be_closed_due_to_expiring_htlcs(self.wallet.adb.get_local_height())):\n            self.logger.info(f\"force-closing due to expiring htlcs\")\n            await self.schedule_force_closing(chan.channel_id)\n\n        elif chan.get_state() == ChannelState.FUNDED:\n            peer = self.lnpeermgr.get_peer_by_pubkey(chan.node_id)\n            if peer and peer.is_initialized() and chan.peer_state == PeerState.GOOD:\n                peer.send_channel_ready(chan)\n\n        elif chan.get_state() == ChannelState.OPEN:\n            peer = self.lnpeermgr.get_peer_by_pubkey(chan.node_id)\n            if peer and peer.is_initialized() and chan.peer_state == PeerState.GOOD:\n                peer.maybe_update_fee(chan)\n                peer.maybe_send_announcement_signatures(chan)\n\n        elif chan.get_state() == ChannelState.FORCE_CLOSING:\n            force_close_tx = chan.force_close_tx()\n            txid = force_close_tx.txid()\n            height = self.lnwatcher.adb.get_tx_height(txid).height()\n            if height == TX_HEIGHT_LOCAL:\n                self.logger.info('REBROADCASTING CLOSING TX')\n                await self.network.try_broadcasting(force_close_tx, 'force-close')\n\n    def get_peer_by_static_jit_scid_alias(self, scid_alias: bytes) -> Optional[Peer]:\n        for nodeid, peer in self.lnpeermgr.peers.items():\n            if scid_alias == self._scid_alias_of_node(nodeid):\n                return peer\n        return None\n\n    def _scid_alias_of_node(self, nodeid: bytes) -> bytes:\n        # scid alias for just-in-time channels\n        return sha256(b'Electrum' + nodeid)[0:8]\n\n    def get_static_jit_scid_alias(self) -> bytes:\n        return self._scid_alias_of_node(self.node_keypair.pubkey)\n\n    @log_exceptions\n    async def open_channel_just_in_time(\n        self,\n        *,\n        next_peer: Peer,\n        next_amount_msat_htlc: int,\n        next_cltv_abs: int,\n        payment_hash: bytes,\n        next_onion: OnionPacket,\n    ) -> str:\n        # if an exception is raised during negotiation, we raise an OnionRoutingFailure.\n        # this will cancel the incoming HTLC\n\n        # prevent settling the htlc until the channel opening was successful so we can fail it if needed\n        self.dont_settle_htlcs[payment_hash.hex()] = None\n        try:\n            funding_sat = 2 * (next_amount_msat_htlc // 1000) # try to fully spend htlcs\n            password = self.wallet.get_unlocked_password() if self.wallet.has_password() else None\n            channel_opening_fee = next_amount_msat_htlc // 100\n            if channel_opening_fee // 1000 < self.config.ZEROCONF_MIN_OPENING_FEE:\n                self.logger.info(f'rejecting JIT channel: payment too low')\n                raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'payment too low')\n            self.logger.info(f'channel opening fee (sats): {channel_opening_fee//1000}')\n            next_chan, funding_tx = await self.open_channel_with_peer(\n                next_peer, funding_sat,\n                push_sat=0,\n                zeroconf=True,\n                public=False,\n                opening_fee=channel_opening_fee,\n                password=password,\n            )\n            async def wait_for_channel():\n                while not next_chan.is_open():\n                    await asyncio.sleep(1)\n            await util.wait_for2(wait_for_channel(), LN_P2P_NETWORK_TIMEOUT)\n            next_chan.save_remote_scid_alias(self._scid_alias_of_node(next_peer.pubkey))\n            self.logger.info(f'JIT channel is open')\n            next_amount_msat_htlc -= channel_opening_fee\n            # fixme: some checks are missing\n            htlc = next_peer.send_htlc(\n                chan=next_chan,\n                payment_hash=payment_hash,\n                amount_msat=next_amount_msat_htlc,\n                cltv_abs=next_cltv_abs,\n                onion=next_onion)\n            async def wait_for_preimage():\n                while self.get_preimage(payment_hash) is None:\n                    await asyncio.sleep(1)\n            await util.wait_for2(wait_for_preimage(), LN_P2P_NETWORK_TIMEOUT)\n\n            # We have been paid and can broadcast\n            # todo: if broadcasting raise an exception, we should try to rebroadcast\n            await self.network.broadcast_transaction(funding_tx)\n        except OnionRoutingFailure:\n            raise\n        except Exception:\n            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')\n        finally:\n            del self.dont_settle_htlcs[payment_hash.hex()]\n\n        htlc_key = serialize_htlc_key(next_chan.get_scid_or_local_alias(), htlc.htlc_id)\n        return htlc_key\n\n    @log_exceptions\n    async def open_channel_with_peer(\n            self, peer, funding_sat, *,\n            push_sat: int = 0,\n            public: bool = False,\n            zeroconf: bool = False,\n            opening_fee: int = None,\n            password=None):\n        if self.config.ENABLE_ANCHOR_CHANNELS:\n            self.wallet.unlock(password)\n        coins = self.wallet.get_spendable_coins(None)\n        node_id = peer.pubkey\n        fee_policy = FeePolicy(self.config.FEE_POLICY)\n        funding_tx = self.mktx_for_open_channel(\n            coins=coins,\n            funding_sat=funding_sat,\n            node_id=node_id,\n            fee_policy=fee_policy)\n        chan, funding_tx = await self._open_channel_coroutine(\n            peer=peer,\n            funding_tx=funding_tx,\n            funding_sat=funding_sat,\n            push_sat=push_sat,\n            public=public,\n            zeroconf=zeroconf,\n            opening_fee=opening_fee,\n            password=password)\n        return chan, funding_tx\n\n    @log_exceptions\n    async def _open_channel_coroutine(\n            self, *,\n            peer: Peer,\n            funding_tx: PartialTransaction,\n            funding_sat: int,\n            push_sat: int,\n            public: bool,\n            zeroconf=False,\n            opening_fee=None,\n            password: Optional[str],\n    ) -> Tuple[Channel, PartialTransaction]:\n\n        if funding_sat > self.config.LIGHTNING_MAX_FUNDING_SAT:\n            raise Exception(\n                _(\"Requested channel capacity is over maximum.\")\n                + f\"\\n{funding_sat} sat > {self.config.LIGHTNING_MAX_FUNDING_SAT} sat\"\n            )\n        coro = peer.channel_establishment_flow(\n            funding_tx=funding_tx,\n            funding_sat=funding_sat,\n            push_msat=push_sat * 1000,\n            public=public,\n            zeroconf=zeroconf,\n            opening_fee=opening_fee,\n            temp_channel_id=os.urandom(32))\n        chan, funding_tx = await util.wait_for2(coro, LN_P2P_NETWORK_TIMEOUT)\n        util.trigger_callback('channels_updated', self.wallet)\n        self.wallet.adb.add_transaction(funding_tx)  # save tx as local into the wallet\n        self.wallet.sign_transaction(funding_tx, password)\n        if funding_tx.is_complete() and not zeroconf:\n            await self.network.try_broadcasting(funding_tx, 'open_channel')\n        return chan, funding_tx\n\n    def add_channel(self, chan: Channel):\n        with self.lock:\n            self._channels[chan.channel_id] = chan\n        self.lnwatcher.add_channel(chan)\n\n    def add_new_channel(self, chan: Channel):\n        self.add_channel(chan)\n        channels_db = self.db.get_dict('channels')\n        channels_db[chan.channel_id.hex()] = chan.storage\n        self.wallet.set_reserved_addresses_for_chan(chan, reserved=True)\n        try:\n            self.save_channel(chan)\n        except Exception:\n            chan.set_state(ChannelState.REDEEMED)\n            self.remove_channel(chan.channel_id)\n            raise\n\n    def make_local_config_for_new_channel(\n        self,\n        *,\n        funding_sat: int,\n        push_msat: int,\n        initiator: HTLCOwner,\n        channel_type: ChannelType,\n        multisig_funding_keypair: Optional[Keypair],  # if None, will get derived from channel_seed\n        peer_features: LnFeatures,\n        channel_seed: bytes = None,\n    ) -> LocalConfig:\n        if channel_seed is None:\n            channel_seed = os.urandom(32)\n        initial_msat = funding_sat * 1000 - push_msat if initiator == LOCAL else push_msat\n\n        # sending empty bytes as the upfront_shutdown_script will give us the\n        # flexibility to decide an address at closing time\n        upfront_shutdown_script = b''\n\n        assert channel_type is not None\n        if channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX:  # anchors\n            static_payment_key = self.static_payment_key\n            static_remotekey = None\n        else:  # static_remotekey\n            assert channel_type & channel_type.OPTION_STATIC_REMOTEKEY\n            wallet = self.wallet\n            assert wallet.txin_type == 'p2wpkh'\n            addr = wallet.get_new_sweep_address_for_channel()\n            static_payment_key = None\n            static_remotekey = bytes.fromhex(wallet.get_public_key(addr))\n\n        if multisig_funding_keypair:\n            for chan in self.channels.values():  # check against all chans of lnworker, for sanity\n                if multisig_funding_keypair.pubkey == chan.config[LOCAL].multisig_key.pubkey:\n                    raise Exception(\n                        \"Refusing to reuse multisig_funding_keypair for new channel. \"\n                        \"Wait one block before opening another channel with this peer.\"\n                    )\n\n        dust_limit_sat = bitcoin.DUST_LIMIT_P2PKH\n        reserve_sat = max(funding_sat // 100, dust_limit_sat)\n        # for comparison of defaults, see\n        # https://github.com/ACINQ/eclair/blob/afa378fbb73c265da44856b4ad0f2128a88ae6c6/eclair-core/src/main/resources/reference.conf#L66\n        # https://github.com/ElementsProject/lightning/blob/0056dd75572a8857cff36fcbdb1a2295a1ac9253/lightningd/options.c#L657\n        # https://github.com/lightningnetwork/lnd/blob/56b61078c5b2be007d318673a5f3b40c6346883a/config.go#L81\n        max_htlc_value_in_flight_msat = self.network.config.LIGHTNING_MAX_HTLC_VALUE_IN_FLIGHT_MSAT or funding_sat * 1000\n        local_config = LocalConfig.from_seed(\n            channel_seed=channel_seed,\n            static_remotekey=static_remotekey,\n            static_payment_key=static_payment_key,\n            multisig_key=multisig_funding_keypair,\n            upfront_shutdown_script=upfront_shutdown_script,\n            to_self_delay=self.network.config.LIGHTNING_TO_SELF_DELAY_CSV,\n            dust_limit_sat=dust_limit_sat,\n            max_htlc_value_in_flight_msat=max_htlc_value_in_flight_msat,\n            max_accepted_htlcs=30,\n            initial_msat=initial_msat,\n            reserve_sat=reserve_sat,\n            funding_locked_received=False,\n            current_commitment_signature=None,\n            current_htlc_signatures=b'',\n            htlc_minimum_msat=1,\n            announcement_node_sig=b'',\n            announcement_bitcoin_sig=b'',\n        )\n        local_config.validate_params(funding_sat=funding_sat, config=self.network.config, peer_features=peer_features)\n        return local_config\n\n    def cb_data(self, node_id: bytes) -> bytes:\n        return CB_MAGIC_BYTES + node_id[0:NODE_ID_PREFIX_LEN]\n\n    def decrypt_cb_data(self, encrypted_data: bytes, funding_address: str) -> bytes:\n        funding_scripthash = bytes.fromhex(address_to_scripthash(funding_address))\n        nonce = funding_scripthash[0:12]\n        return chacha20_decrypt(key=self.backup_key, data=encrypted_data, nonce=nonce)\n\n    def encrypt_cb_data(self, data: bytes, funding_address: str) -> bytes:\n        funding_scripthash = bytes.fromhex(address_to_scripthash(funding_address))\n        nonce = funding_scripthash[0:12]\n        # note: we are only using chacha20 instead of chacha20+poly1305 to save onchain space\n        #       (not have the 16 byte MAC). Otherwise, the latter would be preferable.\n        return chacha20_encrypt(key=self.backup_key, data=data, nonce=nonce)\n\n    def mktx_for_open_channel(\n            self, *,\n            coins: Sequence[PartialTxInput],\n            funding_sat: int,\n            node_id: bytes,\n            fee_policy: FeePolicy,\n    ) -> PartialTransaction:\n        from .wallet import get_locktime_for_new_transaction\n\n        outputs = [PartialTxOutput.from_address_and_value(DummyAddress.CHANNEL, funding_sat)]\n        if self.has_recoverable_channels():\n            dummy_scriptpubkey = make_op_return(self.cb_data(node_id))\n            outputs.append(PartialTxOutput(scriptpubkey=dummy_scriptpubkey, value=0))\n        tx = self.wallet.make_unsigned_transaction(\n            coins=coins,\n            outputs=outputs,\n            fee_policy=fee_policy,\n            # we do not know yet if peer accepts anchors, just assume they do\n            is_anchor_channel_opening=self.config.ENABLE_ANCHOR_CHANNELS,\n        )\n        tx.set_rbf(False)\n        # rm randomness from locktime, as we use the locktime as entropy for deriving the funding_privkey\n        # (and it would be confusing to get a collision as a consequence of the randomness)\n        tx.locktime = get_locktime_for_new_transaction(self.network, include_random_component=False)\n        return tx\n\n    def suggest_funding_amount(self, amount_to_pay: int, coins: Sequence[PartialTxInput]) -> Tuple[int, int] | None:\n        \"\"\" whether we can pay amount_sat after opening a new channel\"\"\"\n        num_sats_can_send = int(self.num_sats_can_send())\n        lightning_needed = amount_to_pay - num_sats_can_send\n        assert lightning_needed > 0\n        min_funding_sat = lightning_needed + (lightning_needed // 20) + 1000  # safety margin\n        min_funding_sat = max(min_funding_sat, MIN_FUNDING_SAT)  # at least MIN_FUNDING_SAT\n        if min_funding_sat > self.config.LIGHTNING_MAX_FUNDING_SAT:\n            return\n        fee_policy = FeePolicy(f'feerate:{FEERATE_FALLBACK_STATIC_FEE}')\n        try:\n            self.mktx_for_open_channel(\n                coins=coins, funding_sat=min_funding_sat, node_id=bytes(32), fee_policy=fee_policy)\n            funding_sat = min_funding_sat\n        except NotEnoughFunds:\n            return\n        # if available, suggest twice that amount:\n        if 2 * min_funding_sat <= self.config.LIGHTNING_MAX_FUNDING_SAT:\n            try:\n                self.mktx_for_open_channel(\n                    coins=coins, funding_sat=2*min_funding_sat, node_id=bytes(32), fee_policy=fee_policy)\n                funding_sat = 2 * min_funding_sat\n            except NotEnoughFunds:\n                pass\n        return funding_sat, min_funding_sat\n\n    def open_channel(\n            self, *,\n            connect_str: str,\n            funding_tx: PartialTransaction,\n            funding_sat: int,\n            push_amt_sat: int,\n            public: bool = False,\n            password: str = None,\n    ) -> Tuple[Channel, PartialTransaction]:\n\n        fut = asyncio.run_coroutine_threadsafe(self.lnpeermgr.add_peer(connect_str), get_asyncio_loop())\n        try:\n            peer = fut.result()\n        except concurrent.futures.TimeoutError:\n            raise Exception(_(\"add peer timed out\"))\n        coro = self._open_channel_coroutine(\n            peer=peer,\n            funding_tx=funding_tx,\n            funding_sat=funding_sat,\n            push_sat=push_amt_sat,\n            public=public,\n            password=password)\n        fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop())\n        try:\n            chan, funding_tx = fut.result()\n        except concurrent.futures.TimeoutError:\n            raise Exception(_(\"open_channel timed out\"))\n        return chan, funding_tx\n\n    def get_channel_by_short_id(self, short_channel_id: bytes) -> Optional[Channel]:\n        assert short_channel_id and isinstance(short_channel_id, bytes), repr(short_channel_id)\n        # First check against *real* SCIDs.\n        # This e.g. protects against maliciously chosen SCID aliases, and accidental collisions.\n        for chan in self.channels.values():\n            if chan.short_channel_id == short_channel_id:\n                return chan\n        # Now we also consider aliases.\n        # TODO we should split this as this search currently ignores the \"direction\"\n        #      of the aliases. We should only look at either the remote OR the local alias,\n        #      depending on context.\n        for chan in self.channels.values():\n            if chan.get_remote_scid_alias() == short_channel_id:\n                return chan\n            if chan.get_local_scid_alias() == short_channel_id:\n                return chan\n        return None\n\n    def can_pay_invoice(self, invoice: Invoice) -> bool:\n        assert invoice.is_lightning()\n        return (invoice.get_amount_sat() or 0) <= self.num_sats_can_send()\n\n    @log_exceptions\n    async def pay_invoice(\n            self, invoice: Invoice, *,\n            amount_msat: int = None,\n            attempts: int = None,  # used only in unit tests\n            full_path: LNPaymentPath = None,\n            channels: Optional[Sequence[Channel]] = None,\n            budget: Optional[PaymentFeeBudget] = None,\n    ) -> Tuple[bool, List[HtlcLog]]:\n        bolt11 = invoice.lightning_invoice\n        lnaddr = self._check_bolt11_invoice(bolt11, amount_msat=amount_msat)\n        min_final_cltv_delta = lnaddr.get_min_final_cltv_delta()\n        payment_hash = lnaddr.paymenthash\n        key = payment_hash.hex()\n        payment_secret = lnaddr.payment_secret\n        invoice_pubkey = lnaddr.pubkey.serialize()\n        invoice_features = lnaddr.get_features()\n        r_tags = lnaddr.get_routing_info('r')\n        amount_to_pay = lnaddr.get_amount_msat()\n        status = self.get_payment_status(payment_hash, direction=SENT)\n        if status == PR_PAID:\n            raise PaymentFailure(_(\"This invoice has been paid already\"))\n        if status == PR_INFLIGHT:\n            raise PaymentFailure(_(\"A payment was already initiated for this invoice\"))\n        if payment_hash in self.get_payments(status='inflight'):\n            raise PaymentFailure(_(\"A previous attempt to pay this invoice did not clear\"))\n        info = PaymentInfo(\n            payment_hash=payment_hash,\n            amount_msat=amount_to_pay,\n            direction=SENT,\n            status=PR_UNPAID,\n            min_final_cltv_delta=min_final_cltv_delta,\n            expiry_delay=LN_EXPIRY_NEVER,\n            invoice_features=invoice_features,\n        )\n        self.save_payment_info(info)\n        self.wallet.set_label(key, lnaddr.get_description())\n        self.set_invoice_status(key, PR_INFLIGHT)\n        if budget is None:\n            budget = PaymentFeeBudget.from_invoice_amount(invoice_amount_msat=amount_to_pay, config=self.config)\n        if attempts is None and self.uses_trampoline():\n            # we don't expect lots of failed htlcs with trampoline, so we can fail sooner\n            attempts = 30\n        success = False\n        try:\n            await self.pay_to_node(\n                node_pubkey=invoice_pubkey,\n                payment_hash=payment_hash,\n                payment_secret=payment_secret,\n                amount_to_pay=amount_to_pay,\n                min_final_cltv_delta=min_final_cltv_delta,\n                r_tags=r_tags,\n                invoice_features=invoice_features,\n                attempts=attempts,\n                full_path=full_path,\n                channels=channels,\n                budget=budget,\n            )\n            success = True\n        except PaymentFailure as e:\n            self.logger.info(f'payment failure: {e!r}')\n            reason = str(e)\n        except ChannelDBNotLoaded as e:\n            self.logger.info(f'payment failure: {e!r}')\n            reason = str(e)\n        finally:\n            self.logger.info(f\"pay_invoice ending session for RHASH={payment_hash.hex()}. {success=}\")\n        if success:\n            self.set_invoice_status(key, PR_PAID)\n            util.trigger_callback('payment_succeeded', self.wallet, key)\n        else:\n            self.set_invoice_status(key, PR_UNPAID)\n            util.trigger_callback('payment_failed', self.wallet, key, reason)\n        log = self.logs[key]\n        return success, log\n\n    async def pay_to_node(\n            self, *,\n            node_pubkey: bytes,\n            payment_hash: bytes,\n            payment_secret: bytes,\n            amount_to_pay: int,  # in msat\n            min_final_cltv_delta: int,\n            r_tags,\n            invoice_features: int,\n            attempts: int = None,\n            full_path: LNPaymentPath = None,\n            fwd_trampoline_onion: OnionPacket = None,\n            budget: PaymentFeeBudget,\n            channels: Optional[Sequence[Channel]] = None,\n            fw_payment_key: str = None,  # for forwarding\n    ) -> None:\n        \"\"\"\n        Can raise PaymentFailure, ChannelDBNotLoaded,\n        or OnionRoutingFailure (if forwarding trampoline).\n        \"\"\"\n\n        assert budget\n        assert budget.fee_msat >= 0, budget\n        assert budget.cltv >= 0, budget\n\n        payment_key = payment_hash + payment_secret\n        assert payment_key not in self._paysessions\n        self._paysessions[payment_key] = paysession = PaySession(\n            payment_hash=payment_hash,\n            payment_secret=payment_secret,\n            initial_trampoline_fee_level=self.config.INITIAL_TRAMPOLINE_FEE_LEVEL,\n            invoice_features=invoice_features,\n            r_tags=r_tags,\n            min_final_cltv_delta=min_final_cltv_delta,\n            amount_to_pay=amount_to_pay,\n            invoice_pubkey=node_pubkey,\n            uses_trampoline=self.uses_trampoline(),\n            # the config option to use two trampoline hops for legacy payments has been removed as\n            # the trampoline onion is too small (400 bytes) to accommodate two trampoline hops and\n            # routing hints, making the functionality unusable for payments that require routing hints.\n            # TODO: if you read this, the year is 2027 and there is no use for the second trampoline\n            # hop code anymore remove the code completely.\n            use_two_trampolines=False,\n        )\n        self.logs[payment_hash.hex()] = log = []  # TODO incl payment_secret in key (re trampoline forwarding)\n\n        paysession.logger.info(\n            f\"pay_to_node starting session for RHASH={payment_hash.hex()}. \"\n            f\"using_trampoline={self.uses_trampoline()}. \"\n            f\"invoice_features={paysession.invoice_features.get_names()}. \"\n            f\"r_tags={LnAddr.format_bolt11_routing_info_as_human_readable(r_tags)}. \"\n            f\"{amount_to_pay=} msat. {budget=}\")\n        if not self.uses_trampoline():\n            self.logger.info(\n                f\"gossip_db status. sync progress: {self.network.lngossip.get_sync_progress_estimate()}. \"\n                f\"num_nodes={self.channel_db.num_nodes}, \"\n                f\"num_channels={self.channel_db.num_channels}, \"\n                f\"num_policies={self.channel_db.num_policies}.\")\n\n        # when encountering trampoline forwarding difficulties in the legacy case, we\n        # sometimes need to fall back to a single trampoline forwarder, at the expense\n        # of privacy\n        try:\n            while True:\n                if (amount_to_send := paysession.get_outstanding_amount_to_send()) > 0:\n                    remaining_fee_budget_msat = (budget.fee_msat * amount_to_send) // amount_to_pay\n                    # splitting the amount of the payment between our channels requires the correct\n                    # available channel balance. to prevent concurrent splitting attempts from\n                    # using stale channel balances for the split calculation a lock needs to be\n                    # taken until the htlcs are added to the channel so the next splitting attempt\n                    # acts on a correct channel balance.\n                    async with self._channel_sending_capacity_lock:\n                        # 1. create a set of routes for remaining amount.\n                        # note: path-finding runs in a separate thread so that we don't block the asyncio loop\n                        # graph updates might occur during the computation\n                        routes = self.create_routes_for_payment(\n                            paysession=paysession,\n                            amount_msat=amount_to_send,\n                            full_path=full_path,\n                            fwd_trampoline_onion=fwd_trampoline_onion,\n                            channels=channels,\n                            budget=budget._replace(fee_msat=remaining_fee_budget_msat),\n                        )\n                        # 2. send htlcs\n                        async for sent_htlc_info, cltv_delta, trampoline_onion in routes:\n                            await self.pay_to_route(\n                                paysession=paysession,\n                                sent_htlc_info=sent_htlc_info,\n                                min_final_cltv_delta=cltv_delta,\n                                trampoline_onion=trampoline_onion,\n                                fw_payment_key=fw_payment_key,\n                            )\n                    # invoice_status is triggered in self.set_invoice_status when it actually changes.\n                    # It is also triggered here to update progress for a lightning payment in the GUI\n                    # (e.g. attempt counter)\n                    util.trigger_callback('invoice_status', self.wallet, payment_hash.hex(), PR_INFLIGHT)\n                # 3. await a queue, collect resolved htlcs\n                htlc_log = await paysession.wait_for_one_htlc_to_resolve()\n                while True:\n                    log.append(htlc_log)\n                    await self._process_htlc_log(\n                        paysession=paysession, htlc_log=htlc_log, is_forwarding_trampoline=bool(fwd_trampoline_onion))\n                    if paysession.number_htlcs_inflight < 1:\n                        break\n                    # wait a bit, more failures might come\n                    try:\n                        htlc_log = await util.wait_for2(\n                            paysession.wait_for_one_htlc_to_resolve(),\n                            timeout=paysession.TIMEOUT_WAIT_FOR_NEXT_RESOLVED_HTLC)\n                    except asyncio.TimeoutError:\n                        break\n\n                # max attempts or timeout\n                if (attempts is not None and len(log) >= attempts) or (attempts is None and time.time() - paysession.start_time > self.PAYMENT_TIMEOUT):\n                    raise PaymentFailure('Giving up after %d attempts'%len(log))\n        except PaymentSuccess:\n            pass\n        finally:\n            paysession.is_active = False\n            if paysession.can_be_deleted():\n                self._paysessions.pop(payment_key)\n            paysession.logger.info(f\"pay_to_node ending session for RHASH={payment_hash.hex()}\")\n\n    async def _process_htlc_log(\n        self,\n        *,\n        paysession: PaySession,\n        htlc_log: HtlcLog,\n        is_forwarding_trampoline: bool,\n    ) -> None:\n        \"\"\"Handle a single just-resolved HTLC, as part of a payment-session.\n\n        Can raise PaymentFailure, PaymentSuccess,\n        or OnionRoutingFailure (if forwarding trampoline).\n        \"\"\"\n        if htlc_log.success:\n            if self.network.path_finder:\n                # TODO: report every route to liquidity hints for mpp\n                # in the case of success, we report channels of the\n                # route as being able to send the same amount in the future,\n                # as we assume to not know the capacity\n                self.network.path_finder.update_liquidity_hints(htlc_log.route, htlc_log.amount_msat)\n                # remove inflight htlcs from liquidity hints\n                self.network.path_finder.update_inflight_htlcs(htlc_log.route, add_htlcs=False)\n            raise PaymentSuccess()\n        # htlc failed\n        # if we get a tmp channel failure, it might work to split the amount and try more routes\n        # if we get a channel update, we might retry the same route and amount\n        route = htlc_log.route\n        sender_idx = htlc_log.sender_idx\n        failure_msg = htlc_log.failure_msg\n        if sender_idx is None:\n            raise PaymentFailure(failure_msg.code_name())\n        erring_node_id = route[sender_idx].node_id\n        code, data = failure_msg.code, failure_msg.data\n        self.logger.info(f\"UPDATE_FAIL_HTLC. code={repr(code)}. \"\n                         f\"decoded_data={failure_msg.decode_data()}. data={data.hex()!r}\")\n        self.logger.info(f\"error reported by {erring_node_id.hex()}\")\n        if code == OnionFailureCode.MPP_TIMEOUT:\n            raise PaymentFailure(failure_msg.code_name())\n        # errors returned by the next trampoline.\n        if is_forwarding_trampoline and code in [\n                OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT,\n                OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON]:\n            raise failure_msg\n        # trampoline\n        if self.uses_trampoline():\n            paysession.handle_failed_trampoline_htlc(\n                htlc_log=htlc_log, failure_msg=failure_msg)\n        else:\n            self.handle_error_code_from_failed_htlc(\n                route=route, sender_idx=sender_idx, failure_msg=failure_msg, amount=htlc_log.amount_msat)\n\n    async def pay_to_route(\n            self, *,\n            paysession: PaySession,\n            sent_htlc_info: SentHtlcInfo,\n            min_final_cltv_delta: int,\n            trampoline_onion: Optional[OnionPacket] = None,\n            fw_payment_key: str = None,\n    ) -> None:\n        \"\"\"Sends a single HTLC.\"\"\"\n        shi = sent_htlc_info\n        del sent_htlc_info  # just renamed\n        short_channel_id = shi.route[0].short_channel_id\n        chan = self.get_channel_by_short_id(short_channel_id)\n        assert chan, ShortChannelID(short_channel_id)\n        peer = self.lnpeermgr.get_peer_by_pubkey(shi.route[0].node_id)\n        if not peer:\n            raise PaymentFailure('Dropped peer')\n        await peer.initialized\n        htlc = peer.pay(\n            route=shi.route,\n            chan=chan,\n            amount_msat=shi.amount_msat,\n            total_msat=shi.bucket_msat,\n            payment_hash=paysession.payment_hash,\n            min_final_cltv_delta=min_final_cltv_delta,\n            payment_secret=shi.payment_secret_bucket,\n            trampoline_onion=trampoline_onion)\n\n        key = (paysession.payment_hash, short_channel_id, htlc.htlc_id)\n        self.sent_htlcs_info[key] = shi\n        paysession.add_new_htlc(shi)\n        if fw_payment_key:\n            htlc_key = serialize_htlc_key(short_channel_id, htlc.htlc_id)\n            self.logger.info(f'adding active forwarding {fw_payment_key}')\n            self.active_forwardings[fw_payment_key].append(htlc_key)\n        if self.network.path_finder:\n            # add inflight htlcs to liquidity hints\n            self.network.path_finder.update_inflight_htlcs(shi.route, add_htlcs=True)\n        util.trigger_callback('htlc_added', chan, htlc, SENT)\n\n    def handle_error_code_from_failed_htlc(\n            self,\n            *,\n            route: LNPaymentRoute,\n            sender_idx: int,\n            failure_msg: OnionRoutingFailure,\n            amount: int) -> None:\n\n        assert self.channel_db  # cannot be in trampoline mode\n        assert self.network.path_finder\n\n        # remove inflight htlcs from liquidity hints\n        self.network.path_finder.update_inflight_htlcs(route, add_htlcs=False)\n\n        code, data = failure_msg.code, failure_msg.data\n        # TODO can we use lnmsg.OnionWireSerializer here?\n        # TODO update onion_wire.csv\n        # handle some specific error codes\n        failure_codes = {\n            OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 0,\n            OnionFailureCode.AMOUNT_BELOW_MINIMUM: 8,\n            OnionFailureCode.FEE_INSUFFICIENT: 8,\n            OnionFailureCode.INCORRECT_CLTV_EXPIRY: 4,\n            OnionFailureCode.EXPIRY_TOO_SOON: 0,\n            OnionFailureCode.CHANNEL_DISABLED: 2,\n        }\n        try:\n            failing_channel = route[sender_idx + 1].short_channel_id\n        except IndexError:\n            raise PaymentFailure(f'payment destination reported error: {failure_msg.code_name()}') from None\n\n        # TODO: handle unknown next peer?\n        # handle failure codes that include a channel update\n        if code in failure_codes:\n            offset = failure_codes[code]\n            channel_update_len = int.from_bytes(data[offset:offset+2], byteorder=\"big\")\n            channel_update_as_received = data[offset+2: offset+2+channel_update_len]\n            payload = self._decode_channel_update_msg(channel_update_as_received)\n            if payload is None:\n                self.logger.info(f'could not decode channel_update for failed htlc: '\n                                 f'{channel_update_as_received.hex()}')\n                blacklist = True\n            elif payload.get('short_channel_id') != failing_channel:\n                self.logger.info(f'short_channel_id in channel_update does not match our route')\n                blacklist = True\n            else:\n                # apply the channel update or get blacklisted\n                blacklist, update = self._handle_chanupd_from_failed_htlc(\n                    payload, route=route, sender_idx=sender_idx, failure_msg=failure_msg)\n                # we interpret a temporary channel failure as a liquidity issue\n                # in the channel and update our liquidity hints accordingly\n                if code == OnionFailureCode.TEMPORARY_CHANNEL_FAILURE:\n                    self.network.path_finder.update_liquidity_hints(\n                        route,\n                        amount,\n                        failing_channel=ShortChannelID(failing_channel))\n                # if we can't decide on some action, we are stuck\n                if not (blacklist or update):\n                    raise PaymentFailure(failure_msg.code_name())\n        # for errors that do not include a channel update\n        else:\n            blacklist = True\n        if blacklist:\n            self.network.path_finder.add_edge_to_blacklist(short_channel_id=failing_channel)\n\n    def _handle_chanupd_from_failed_htlc(\n        self, payload, *,\n        route: LNPaymentRoute,\n        sender_idx: int,\n        failure_msg: OnionRoutingFailure,\n    ) -> Tuple[bool, bool]:\n        blacklist = False\n        update = False\n        try:\n            r = self.channel_db.add_channel_update(payload, verify=True)\n        except InvalidGossipMsg:\n            return True, False  # blacklist\n        short_channel_id = ShortChannelID(payload['short_channel_id'])\n        if r == UpdateStatus.GOOD:\n            self.logger.info(f\"applied channel update to {short_channel_id}\")\n            # TODO: add test for this\n            # FIXME: this does not work for our own unannounced channels.\n            for chan in self.channels.values():\n                if chan.short_channel_id == short_channel_id:\n                    chan.set_remote_update(payload)\n            update = True\n        elif r == UpdateStatus.ORPHANED:\n            # maybe it is a private channel (and data in invoice was outdated)\n            self.logger.info(f\"Could not find {short_channel_id}. maybe update is for private channel?\")\n            start_node_id = route[sender_idx].node_id\n            cache_ttl = None\n            if failure_msg.code == OnionFailureCode.CHANNEL_DISABLED:\n                # eclair sends CHANNEL_DISABLED if its peer is offline. E.g. we might be trying to pay\n                # a mobile phone with the app closed. So we cache this with a short TTL.\n                cache_ttl = self.channel_db.PRIVATE_CHAN_UPD_CACHE_TTL_SHORT\n            update = self.channel_db.add_channel_update_for_private_channel(payload, start_node_id, cache_ttl=cache_ttl)\n            blacklist = not update\n        elif r == UpdateStatus.EXPIRED:\n            blacklist = True\n        elif r == UpdateStatus.DEPRECATED:\n            self.logger.info(f'channel update is not more recent.')\n            blacklist = True\n        elif r == UpdateStatus.UNCHANGED:\n            blacklist = True\n        return blacklist, update\n\n    @classmethod\n    def _decode_channel_update_msg(cls, chan_upd_msg: bytes) -> Optional[Dict[str, Any]]:\n        channel_update_as_received = chan_upd_msg\n        channel_update_typed = (258).to_bytes(length=2, byteorder=\"big\") + channel_update_as_received\n        # note: some nodes put channel updates in error msgs with the leading msg_type already there.\n        #       we try decoding both ways here.\n        try:\n            message_type, payload = decode_msg(channel_update_typed)\n            if payload['chain_hash'] != constants.net.rev_genesis_bytes(): raise Exception()\n            payload['raw'] = channel_update_typed\n            return payload\n        except Exception:  # FIXME: too broad\n            try:\n                message_type, payload = decode_msg(channel_update_as_received)\n                if payload['chain_hash'] != constants.net.rev_genesis_bytes(): raise Exception()\n                payload['raw'] = channel_update_as_received\n                return payload\n            except Exception:\n                return None\n\n    def _check_bolt11_invoice(self, bolt11_invoice: str, *, amount_msat: int = None) -> LnAddr:\n        \"\"\"Parses and validates a bolt11 invoice str into a LnAddr.\n        Includes pre-payment checks external to the parser.\n        \"\"\"\n        addr = lndecode(bolt11_invoice)\n        if addr.is_expired():\n            raise InvoiceError(_(\"This invoice has expired\"))\n        # check amount\n        if amount_msat:  # replace amt in invoice. main usecase is paying zero amt invoices\n            existing_amt_msat = addr.get_amount_msat()\n            if existing_amt_msat and amount_msat < existing_amt_msat:\n                raise Exception(\"cannot pay lower amt than what is originally in LN invoice\")\n            addr.amount = Decimal(amount_msat) / COIN / 1000\n        if addr.amount is None:\n            raise InvoiceError(_(\"Missing amount\"))\n        # check cltv\n        if addr.get_min_final_cltv_delta() > NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:\n            raise InvoiceError(\"{}\\n{}\".format(\n                _(\"Invoice wants us to risk locking funds for unreasonably long.\"),\n                f\"min_final_cltv_delta: {addr.get_min_final_cltv_delta()}\"))\n        # check features\n        addr.validate_and_compare_features(self.features)\n        return addr\n\n    def is_trampoline_peer(self, node_id: bytes) -> bool:\n        # until trampoline is advertised in lnfeatures, check against hardcoded list\n        if is_hardcoded_trampoline(node_id):\n            return True\n        peer = self.lnpeermgr.get_peer_by_pubkey(node_id)\n        if not peer:\n            return False\n        return (peer.their_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR)\n                or peer.their_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM))\n\n    def suggest_peer(self) -> Optional[bytes]:\n        if not self.uses_trampoline():\n            return self.lnrater.suggest_peer()\n        else:\n            return random.choice(list(hardcoded_trampoline_nodes().values())).pubkey\n\n    def suggest_payment_splits(\n        self,\n        *,\n        amount_msat: int,\n        final_total_msat: int,\n        my_active_channels: Sequence[Channel],\n        invoice_features: LnFeatures,\n        r_tags: Sequence[Sequence[Sequence[Any]]],\n        receiver_pubkey: bytes,\n    ) -> List['SplitConfigRating']:\n        channels_with_funds = {\n            (chan.channel_id, chan.node_id): ( int(chan.available_to_spend(HTLCOwner.LOCAL)), chan.htlc_slots_left(HTLCOwner.LOCAL))\n            for chan in my_active_channels\n        }\n        # if we have a direct channel it's preferable to send a single part directly through this\n        # channel, so this bool will disable excluding single part payments\n        have_direct_channel = any(chan.node_id == receiver_pubkey for chan in my_active_channels)\n        self.logger.info(f\"channels_with_funds: {channels_with_funds}, {have_direct_channel=}\")\n        exclude_single_part_payments = False\n        if self.uses_trampoline():\n            # in the case of a legacy payment, we don't allow splitting via different\n            # trampoline nodes, because of https://github.com/ACINQ/eclair/issues/2127\n            is_legacy, _ = is_legacy_relay(invoice_features, r_tags)\n            exclude_multinode_payments = is_legacy\n            # we don't split within a channel when sending to a trampoline node,\n            # the trampoline node will split for us\n            exclude_single_channel_splits = not self.config.TEST_FORCE_MPP\n        else:\n            exclude_multinode_payments = False\n            exclude_single_channel_splits = False\n            if invoice_features.supports(LnFeatures.BASIC_MPP_OPT) and not self.config.TEST_FORCE_DISABLE_MPP:\n                # if amt is still large compared to total_msat, split it:\n                if (amount_msat / final_total_msat > self.MPP_SPLIT_PART_FRACTION\n                        and amount_msat > self.MPP_SPLIT_PART_MINAMT_MSAT\n                        and not have_direct_channel):\n                    exclude_single_part_payments = True\n\n        split_configurations = suggest_splits(\n            amount_msat,\n            channels_with_funds,\n            exclude_single_part_payments=exclude_single_part_payments,\n            exclude_multinode_payments=exclude_multinode_payments,\n            exclude_single_channel_splits=exclude_single_channel_splits\n        )\n\n        self.logger.info(f'suggest_split {amount_msat} returned {len(split_configurations)} configurations')\n        return split_configurations\n\n    async def create_routes_for_payment(\n            self, *,\n            paysession: PaySession,\n            amount_msat: int,        # part of payment amount we want routes for now\n            fwd_trampoline_onion: OnionPacket = None,\n            full_path: LNPaymentPath = None,\n            channels: Optional[Sequence[Channel]] = None,\n            budget: PaymentFeeBudget,\n    ) -> AsyncGenerator[Tuple[SentHtlcInfo, int, Optional[OnionPacket]], None]:\n\n        \"\"\"Creates multiple routes for splitting a payment over the available\n        private channels.\n\n        We first try to conduct the payment over a single channel. If that fails\n        and mpp is supported by the receiver, we will split the payment.\"\"\"\n        trampoline_features = LnFeatures.VAR_ONION_OPT\n        local_height = self.wallet.adb.get_local_height()\n        fee_related_error = None  # type: Optional[FeeBudgetExceeded]\n        if channels:\n            my_active_channels = channels\n        else:\n            my_active_channels = [\n                chan for chan in self.channels.values() if\n                chan.is_active() and not chan.is_frozen_for_sending()]\n        # try random order\n        random.shuffle(my_active_channels)\n        split_configurations = self.suggest_payment_splits(\n            amount_msat=amount_msat,\n            final_total_msat=paysession.amount_to_pay,\n            my_active_channels=my_active_channels,\n            invoice_features=paysession.invoice_features,\n            r_tags=paysession.r_tags,\n            receiver_pubkey=paysession.invoice_pubkey,\n        )\n        for sc in split_configurations:\n            is_multichan_mpp = len(sc.config.items()) > 1\n            is_mpp = sc.config.number_parts() > 1\n            if is_mpp and not paysession.invoice_features.supports(LnFeatures.BASIC_MPP_OPT):\n                continue\n            if not is_mpp and self.config.TEST_FORCE_MPP:\n                continue\n            if is_mpp and self.config.TEST_FORCE_DISABLE_MPP:\n                continue\n            self.logger.info(f\"trying split configuration: {sc.config.values()} rating: {sc.rating}\")\n            routes = []\n            try:\n                if self.uses_trampoline():\n                    per_trampoline_channel_amounts = defaultdict(list)\n                    # categorize by trampoline nodes for trampoline mpp construction\n                    for (chan_id, _), part_amounts_msat in sc.config.items():\n                        chan = self._channels[chan_id]\n                        for part_amount_msat in part_amounts_msat:\n                            per_trampoline_channel_amounts[chan.node_id].append((chan_id, part_amount_msat))\n                    # for each trampoline forwarder, construct mpp trampoline\n                    for trampoline_node_id, trampoline_parts in per_trampoline_channel_amounts.items():\n                        per_trampoline_amount = sum([x[1] for x in trampoline_parts])\n                        trampoline_route, trampoline_onion, per_trampoline_amount_with_fees, per_trampoline_cltv_delta = create_trampoline_route_and_onion(\n                            amount_msat=per_trampoline_amount,\n                            total_msat=paysession.amount_to_pay,\n                            min_final_cltv_delta=paysession.min_final_cltv_delta,\n                            my_pubkey=self.node_keypair.pubkey,\n                            invoice_pubkey=paysession.invoice_pubkey,\n                            invoice_features=paysession.invoice_features,\n                            node_id=trampoline_node_id,\n                            r_tags=paysession.r_tags,\n                            payment_hash=paysession.payment_hash,\n                            payment_secret=paysession.payment_secret,\n                            local_height=local_height,\n                            trampoline_fee_level=paysession.trampoline_fee_level,\n                            use_two_trampolines=paysession.use_two_trampolines,\n                            failed_routes=paysession.failed_trampoline_routes,\n                            budget=budget._replace(fee_msat=budget.fee_msat // len(per_trampoline_channel_amounts)),\n                        )\n                        # node_features is only used to determine is_tlv\n                        per_trampoline_secret = os.urandom(32)\n                        per_trampoline_fees = per_trampoline_amount_with_fees - per_trampoline_amount\n                        self.logger.info(f'created route with trampoline fee level={paysession.trampoline_fee_level}')\n                        self.logger.info(f'trampoline hops: {[hop.end_node.hex() for hop in trampoline_route]}')\n                        self.logger.info(f'per trampoline fees: {per_trampoline_fees}')\n                        for chan_id, part_amount_msat in trampoline_parts:\n                            chan = self._channels[chan_id]\n                            margin = chan.available_to_spend(LOCAL) - part_amount_msat\n                            delta_fee = min(per_trampoline_fees, margin)\n                            # TODO: distribute trampoline fee over several channels?\n                            part_amount_msat_with_fees = part_amount_msat + delta_fee\n                            per_trampoline_fees -= delta_fee\n                            route = [\n                                RouteEdge(\n                                    start_node=self.node_keypair.pubkey,\n                                    end_node=trampoline_node_id,\n                                    short_channel_id=chan.short_channel_id,\n                                    fee_base_msat=0,\n                                    fee_proportional_millionths=0,\n                                    cltv_delta=0,\n                                    node_features=trampoline_features)\n                            ]\n                            self.logger.info(f'adding route {part_amount_msat} {delta_fee} {margin}')\n                            shi = SentHtlcInfo(\n                                route=route,\n                                payment_secret_orig=paysession.payment_secret,\n                                payment_secret_bucket=per_trampoline_secret,\n                                amount_msat=part_amount_msat_with_fees,\n                                bucket_msat=per_trampoline_amount_with_fees,\n                                amount_receiver_msat=part_amount_msat,\n                                trampoline_fee_level=paysession.trampoline_fee_level,\n                                trampoline_route=trampoline_route,\n                            )\n                            routes.append((shi, per_trampoline_cltv_delta, trampoline_onion))\n                        if per_trampoline_fees != 0:\n                            e = 'not enough margin to pay trampoline fee'\n                            self.logger.info(e)\n                            raise FeeBudgetExceeded(e)\n                else:\n                    # We atomically loop through a split configuration. If there was\n                    # a failure to find a path for a single part, we try the next configuration\n                    for (chan_id, _), part_amounts_msat in sc.config.items():\n                        for part_amount_msat in part_amounts_msat:\n                            channel = self._channels[chan_id]\n                            route = await run_in_thread(\n                                partial(\n                                    self.create_route_for_single_htlc,\n                                    amount_msat=part_amount_msat,\n                                    invoice_pubkey=paysession.invoice_pubkey,\n                                    min_final_cltv_delta=paysession.min_final_cltv_delta,\n                                    r_tags=paysession.r_tags,\n                                    invoice_features=paysession.invoice_features,\n                                    my_sending_channels=[channel] if is_multichan_mpp else my_active_channels,\n                                    full_path=full_path,\n                                    budget=budget._replace(fee_msat=budget.fee_msat // sc.config.number_parts()),\n                                )\n                            )\n                            shi = SentHtlcInfo(\n                                route=route,\n                                payment_secret_orig=paysession.payment_secret,\n                                payment_secret_bucket=paysession.payment_secret,\n                                amount_msat=part_amount_msat,\n                                bucket_msat=paysession.amount_to_pay,\n                                amount_receiver_msat=part_amount_msat,\n                                trampoline_fee_level=None,\n                                trampoline_route=None,\n                            )\n                            routes.append((shi, paysession.min_final_cltv_delta, fwd_trampoline_onion))\n            except NoPathFound:\n                continue\n            except FeeBudgetExceeded as e:\n                fee_related_error = e\n                continue\n            for route in routes:\n                yield route\n            return\n        if fee_related_error is not None:\n            raise fee_related_error\n        raise NoPathFound()\n\n    @profiler\n    def create_route_for_single_htlc(\n            self, *,\n            amount_msat: int,  # that final receiver gets\n            invoice_pubkey: bytes,\n            min_final_cltv_delta: int,\n            r_tags,\n            invoice_features: int,\n            my_sending_channels: List[Channel],\n            full_path: Optional[LNPaymentPath],\n            budget: PaymentFeeBudget,\n    ) -> LNPaymentRoute:\n\n        my_sending_aliases = set(chan.get_local_scid_alias() for chan in my_sending_channels)\n        my_sending_channels = {chan.short_channel_id: chan for chan in my_sending_channels\n            if chan.short_channel_id is not None}\n        # Collect all private edges from route hints.\n        # Note: if some route hints are multiple edges long, and these paths cross each other,\n        #       we allow our path finding to cross the paths; i.e. the route hints are not isolated.\n        private_route_edges = {}  # type: Dict[ShortChannelID, RouteEdge]\n        for private_path in r_tags:\n            # we need to shift the node pubkey by one towards the destination:\n            private_path_nodes = [edge[0] for edge in private_path][1:] + [invoice_pubkey]\n            private_path_rest = [edge[1:] for edge in private_path]\n            start_node = private_path[0][0]\n            # remove aliases from direct routes\n            if len(private_path) == 1 and private_path[0][1] in my_sending_aliases:\n                self.logger.info(f'create_route: skipping alias {ShortChannelID(private_path[0][1])}')\n                continue\n            for end_node, edge_rest in zip(private_path_nodes, private_path_rest):\n                short_channel_id, fee_base_msat, fee_proportional_millionths, cltv_delta = edge_rest\n                short_channel_id = ShortChannelID(short_channel_id)\n                if (our_chan := self.get_channel_by_short_id(short_channel_id)) is not None:\n                    # check if the channel is one of our channels and frozen for sending\n                    if our_chan.is_frozen_for_sending():\n                        continue\n                # if we have a routing policy for this edge in the db, that takes precedence,\n                # as it is likely from a previous failure\n                channel_policy = self.channel_db.get_policy_for_node(\n                    short_channel_id=short_channel_id,\n                    node_id=start_node,\n                    my_channels=my_sending_channels)\n                if channel_policy:\n                    fee_base_msat = channel_policy.fee_base_msat\n                    fee_proportional_millionths = channel_policy.fee_proportional_millionths\n                    cltv_delta = channel_policy.cltv_delta\n                node_info = self.channel_db.get_node_info_for_node_id(node_id=end_node)\n                route_edge = RouteEdge(\n                        start_node=start_node,\n                        end_node=end_node,\n                        short_channel_id=short_channel_id,\n                        fee_base_msat=fee_base_msat,\n                        fee_proportional_millionths=fee_proportional_millionths,\n                        cltv_delta=cltv_delta,\n                        node_features=node_info.features if node_info else 0)\n                private_route_edges[route_edge.short_channel_id] = route_edge\n                start_node = end_node\n        # now find a route, end to end: between us and the recipient\n        try:\n            route = self.network.path_finder.find_route(\n                nodeA=self.node_keypair.pubkey,\n                nodeB=invoice_pubkey,\n                invoice_amount_msat=amount_msat,\n                path=full_path,\n                my_sending_channels=my_sending_channels,\n                private_route_edges=private_route_edges)\n        except NoChannelPolicy as e:\n            raise NoPathFound() from e\n        if not route:\n            raise NoPathFound()\n        if not is_route_within_budget(\n            route, budget=budget, amount_msat_for_dest=amount_msat, cltv_delta_for_dest=min_final_cltv_delta,\n        ):\n            self.logger.info(f\"rejecting route (exceeds budget): {route=}. {budget=}\")\n            raise FeeBudgetExceeded()\n        assert len(route) > 0\n        if route[-1].end_node != invoice_pubkey:\n            raise LNPathInconsistent(\"last node_id != invoice pubkey\")\n        # add features from invoice\n        route[-1].node_features |= invoice_features\n        return route\n\n    def _get_invoice_features(self, amount_msat: Optional[int]) -> LnFeatures:\n        invoice_features = self.features.for_invoice()\n        if not self.uses_trampoline():\n            invoice_features &= ~ LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM\n        needs_jit: bool = self.receive_requires_jit_channel(amount_msat)\n        if needs_jit:\n            # jit only works with single htlcs, mpp will cause LSP to open channels for each htlc\n            invoice_features &= ~ LnFeatures.BASIC_MPP_OPT & ~ LnFeatures.BASIC_MPP_REQ\n        return invoice_features\n\n    def clear_invoices_cache(self):\n        self._bolt11_cache.clear()\n\n    def get_bolt11_invoice(\n            self, *,\n            payment_info: PaymentInfo,\n            message: str,\n            fallback_address: Optional[str],\n            channels: Optional[Sequence[Channel]] = None,\n    ) -> Tuple[LnAddr, str]:\n        amount_msat = payment_info.amount_msat\n        pair = self._bolt11_cache.get(payment_info.payment_hash)\n        if pair:\n            lnaddr, invoice = pair\n            assert lnaddr.get_amount_msat() == amount_msat\n            return pair\n\n        assert amount_msat is None or amount_msat > 0\n        timestamp = int(time.time())\n        routing_hints = self.calc_routing_hints_for_invoice(amount_msat, channels=channels)\n        formatted_r_hints = LnAddr.format_bolt11_routing_info_as_human_readable(routing_hints, has_explicit_r_tagtype=True)\n        self.logger.info(f\"creating bolt11 invoice with routing_hints: {formatted_r_hints}, sat: {(amount_msat or 0) // 1000}\")\n        payment_secret = self.get_payment_secret(payment_info.payment_hash)\n        amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None\n        min_final_cltv_delta = payment_info.min_final_cltv_delta + MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE\n        lnaddr = LnAddr(\n            paymenthash=payment_info.payment_hash,\n            amount=amount_btc,\n            tags=[\n                ('d', message),\n                ('c', min_final_cltv_delta),\n                ('x', payment_info.expiry_delay),\n                ('9', payment_info.invoice_features),\n                ('f', fallback_address),\n            ] + routing_hints,\n            date=timestamp,\n            payment_secret=payment_secret)\n        invoice = lnencode(lnaddr, self.node_keypair.privkey)\n        pair = lnaddr, invoice\n        self._bolt11_cache[payment_info.payment_hash] = pair\n        return pair\n\n    def get_payment_secret(self, payment_hash):\n        return sha256(sha256(self.payment_secret_key) + payment_hash)\n\n    def _get_payment_key(self, payment_hash: bytes) -> bytes:\n        \"\"\"Return payment bucket key.\n        We bucket htlcs based on payment_hash+payment_secret. payment_secret is included\n        as it changes over a trampoline path (in the outer onion), and these paths can overlap.\n        \"\"\"\n        payment_secret = self.get_payment_secret(payment_hash)\n        return payment_hash + payment_secret\n\n    def create_payment_info(\n        self, *,\n        amount_msat: Optional[int],\n        min_final_cltv_delta: Optional[int] = None,\n        exp_delay: int = LN_EXPIRY_NEVER,\n        write_to_disk=True\n    ) -> bytes:\n        if amount_msat == 0:\n            raise ValueError(\"amount_msat must not be 0. Use None instead.\")\n        payment_preimage = os.urandom(32)\n        payment_hash = sha256(payment_preimage)\n        min_final_cltv_delta = min_final_cltv_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED\n        invoice_features = self._get_invoice_features(amount_msat)\n        info = PaymentInfo(\n            payment_hash=payment_hash,\n            amount_msat=amount_msat,\n            direction=RECEIVED,\n            status=PR_UNPAID,\n            min_final_cltv_delta=min_final_cltv_delta,\n            expiry_delay=exp_delay or LN_EXPIRY_NEVER,\n            invoice_features=invoice_features,\n        )\n        self.save_preimage(payment_hash, payment_preimage, write_to_disk=False)\n        self.save_payment_info(info, write_to_disk=False)\n        if write_to_disk:\n            self.wallet.save_db()\n        return payment_hash\n\n    def bundle_payments(self, hash_list: Sequence[bytes]) -> None:\n        \"\"\"Bundle together a list of payment_hashes, for atomicity, so that either\n        - all gets fulfilled, or\n        - none of them gets fulfilled.\n        (we are the recipient of this payment)\n        \"\"\"\n        payment_keys = [self._get_payment_key(x) for x in hash_list]\n        with self.lock:\n            # We maintain two maps.\n            #   map1: payment_key -> bundle_key=canon_pkey (canonically smallest among pkeys)\n            #   map2: bundle_key -> list of pkeys in bundle\n            # assumption: bundles are immutable, so no adding extra pkeys after-the-fact\n            canon_pkey = min(payment_keys)\n            for pkey in payment_keys:\n                assert pkey not in self._payment_bundles_pkey_to_canon\n            for pkey in payment_keys:\n                self._payment_bundles_pkey_to_canon[pkey] = canon_pkey\n            self._payment_bundles_canon_to_pkeylist[canon_pkey] = tuple(payment_keys)\n\n    def get_payment_bundle(self, payment_key: Union[bytes, str]) -> Sequence[bytes]:\n        with self.lock:\n            if isinstance(payment_key, str):\n                try:\n                    payment_key = bytes.fromhex(payment_key)\n                except ValueError:\n                    # might be a forwarding payment_key which is not hex and will never have a bundle\n                    return []\n            canon_pkey =  self._payment_bundles_pkey_to_canon.get(payment_key)\n            if canon_pkey is None:\n                return []\n            return self._payment_bundles_canon_to_pkeylist[canon_pkey]\n\n    def is_payment_bundle_complete(self, any_payment_key: str) -> bool:\n        \"\"\"\n        complete means a htlc set is available for each payment key of the payment bundle and\n        all htlc sets have a resolution >= COMPLETE (we got the whole payment bundle amount)\n        \"\"\"\n        # get all payment keys covered by this bundle\n        bundle_payment_keys = self.get_payment_bundle(any_payment_key)\n        if not bundle_payment_keys:  # there is no payment bundle\n            return True\n        for payment_key in bundle_payment_keys:\n            mpp_set = self.received_mpp_htlcs.get(payment_key.hex())\n            if mpp_set is None:\n                # payment bundle is missing htlc set for payment request\n                # it might have already been failed and deleted\n                return False\n            elif mpp_set.resolution not in (RecvMPPResolution.COMPLETE, RecvMPPResolution.SETTLING):\n                return False\n        return True\n\n    def delete_payment_bundle(\n        self, *,\n        payment_hash: Optional[bytes] = None,\n        payment_key: Optional[bytes] = None,\n    ) -> None:\n        assert (payment_hash is not None) ^ (payment_key is not None), \\\n                    \"must provide exactly one of (payment_hash, payment_key)\"\n        if not payment_key:\n            payment_key = self._get_payment_key(payment_hash)\n        with self.lock:\n            canon_pkey = self._payment_bundles_pkey_to_canon.get(payment_key)\n            if canon_pkey is None:  # is it ok for bundle to be missing??\n                return\n            pkey_list = self._payment_bundles_canon_to_pkeylist[canon_pkey]\n            for pkey in pkey_list:\n                del self._payment_bundles_pkey_to_canon[pkey]\n            del self._payment_bundles_canon_to_pkeylist[canon_pkey]\n\n    def save_preimage(\n        self,\n        payment_hash: bytes,\n        preimage: bytes,\n        *,\n        write_to_disk: bool = True,\n        mark_as_public: bool = False,  # see is_preimage_public\n    ):\n        assert isinstance(payment_hash, bytes), f\"expected bytes, but got {type(payment_hash)}\"\n        assert isinstance(preimage, bytes), f\"expected bytes, but got {type(preimage)}\"\n        if sha256(preimage) != payment_hash:\n            raise Exception(\"tried to save incorrect preimage for payment_hash\")\n        old_tuple = _, old_is_public = self._preimages.get(payment_hash.hex(), (None, False))\n        mark_as_public |= old_is_public  # disallow True->False transition\n        # sanity checks and conversions done.\n        new_tuple = preimage.hex(), mark_as_public\n        if old_tuple == new_tuple:  # no change\n            return\n        self.logger.debug(f\"saving preimage for {payment_hash.hex()} (public={mark_as_public})\")\n        self._preimages[payment_hash.hex()] = new_tuple\n        if write_to_disk:\n            self.wallet.save_db()\n\n    def get_preimage(self, payment_hash: bytes) -> Optional[bytes]:\n        assert isinstance(payment_hash, bytes), f\"expected bytes, but got {type(payment_hash)}\"\n        preimage_hex, _ = self._preimages.get(payment_hash.hex(), (None, None))\n        if preimage_hex is None:\n            return None\n        preimage_bytes = bytes.fromhex(preimage_hex)\n        if sha256(preimage_bytes) != payment_hash:\n            raise Exception(\"found incorrect preimage for payment_hash\")\n        return preimage_bytes\n\n    def get_preimage_hex(self, payment_hash: str) -> Optional[str]:\n        preimage_bytes = self.get_preimage(bytes.fromhex(payment_hash)) or b\"\"\n        return preimage_bytes.hex() or None\n\n    def is_preimage_public(self, payment_hash: bytes) -> bool:\n        \"\"\"If another LN node knows a preimage besides us, we consider it public.\n        If a preimage is public, it is safe to reveal it in an arbitrary context.\n\n        For example, if there is a pending incoming partial MPP for an invoice we created,\n        we must not reveal the preimage, otherwise we will get paid less than invoice amount.\n        What if there is a force-close around that time? When is it safe to reveal the preimage on-chain?\n        e.g. if we already revealed the preimage either offchain or onchain, it is fine to reveal it again.\n        \"\"\"\n        assert isinstance(payment_hash, bytes), f\"expected bytes, but got {type(payment_hash)}\"\n        preimage_hex, is_public = self._preimages.get(payment_hash.hex(), (None, None))\n        return bool(is_public)\n\n    def get_payment_info(self, payment_hash: bytes, *, direction: lnutil.Direction) -> Optional[PaymentInfo]:\n        \"\"\"returns None if payment_hash is a payment we are forwarding\"\"\"\n        key = PaymentInfo.calc_db_key(payment_hash_hex=payment_hash.hex(), direction=direction)\n        with self.lock:\n            if key in self.payment_info:\n                stored_tuple = self.payment_info[key]\n                amount_msat, status, min_final_cltv_delta, expiry_delay, creation_ts, invoice_features = stored_tuple\n                return PaymentInfo(\n                    payment_hash=payment_hash,\n                    amount_msat=amount_msat,\n                    direction=direction,\n                    status=status,\n                    min_final_cltv_delta=min_final_cltv_delta,\n                    expiry_delay=expiry_delay,\n                    creation_ts=creation_ts,\n                    invoice_features=LnFeatures(invoice_features),\n                )\n            return None\n\n    def add_payment_info_for_hold_invoice(\n        self,\n        payment_hash: bytes, *,\n        lightning_amount_sat: Optional[int],\n        min_final_cltv_delta: int,\n        exp_delay: int,\n    ):\n        amount_msat = lightning_amount_sat * 1000 if lightning_amount_sat else None\n        info = PaymentInfo(\n            payment_hash=payment_hash,\n            amount_msat=amount_msat,\n            direction=RECEIVED,\n            status=PR_UNPAID,\n            min_final_cltv_delta=min_final_cltv_delta,\n            expiry_delay=exp_delay,\n            invoice_features=self._get_invoice_features(amount_msat),\n        )\n        self.save_payment_info(info, write_to_disk=False)\n\n    def register_hold_invoice(self, payment_hash: bytes, cb: Callable[[bytes], Awaitable[None]]):\n        assert self.get_preimage(payment_hash) is None, \"hold invoice cb won't get called if preimage is already set\"\n        self.hold_invoice_callbacks[payment_hash] = cb\n\n    def unregister_hold_invoice(self, payment_hash: bytes):\n        self.hold_invoice_callbacks.pop(payment_hash, None)\n        payment_key = self._get_payment_key(payment_hash).hex()\n        if payment_key in self.received_mpp_htlcs:\n            if self.get_preimage(payment_hash) is None:\n                # the pending mpp set can be failed as we don't have the preimage to settle it\n                self.set_mpp_resolution(payment_key, RecvMPPResolution.FAILED)\n\n    def save_payment_info(self, info: PaymentInfo, *, write_to_disk: bool = True) -> None:\n        assert info.status in SAVED_PR_STATUS\n        with self.lock:\n            if old_info := self.get_payment_info(payment_hash=info.payment_hash, direction=info.direction):\n                if info == old_info:\n                    return  # already saved\n                if info.direction == SENT and old_info.status in (PR_UNPAID, PR_FAILED):\n                    # allow saving of newer PaymentInfo if it is a sending attempt and the previous\n                    # payment failed or was not yet attempted\n                    old_info = dataclasses.replace(\n                        old_info,\n                        creation_ts=info.creation_ts,\n                        status=info.status,\n                        amount_msat=info.amount_msat,  # might retrying to pay 0 amount invoice\n                    )\n                if info != dataclasses.replace(old_info, status=info.status):\n                    # differs more than in status. let's fail\n                    raise Exception(f\"payment_hash already in use: {info=} != {old_info=}\")\n            v = info.amount_msat, info.status, info.min_final_cltv_delta, info.expiry_delay, info.creation_ts, int(info.invoice_features)\n            self.payment_info[info.db_key] = v\n        if write_to_disk:\n            self.wallet.save_db()\n\n    def update_or_create_mpp_with_received_htlc(\n        self,\n        *,\n        payment_key: str,\n        channel_id: bytes,\n        htlc: UpdateAddHtlc,\n        unprocessed_onion_packet: str,\n    ):\n        # Payment key creation:\n        #   * for regular forwarded htlcs -> \"scid.hex() + ':%d' % htlc_id\" [htlc key]\n        #   * for trampoline forwarding -> \"payment hash + payment secret from outer onion\"\n        #   * for final non-trampoline htlcs (we are receiver) -> \"payment hash + payment secret from onion\"\n        #   * for final trampoline htlcs (we are receiver) -> 2. step grouping:\n        #           1. grouping of htlcs by \"payments hash + outer onion payment secret\", a 'multi-trampoline mpp part'.\n        #           2. once the set of step 1. is COMPLETE (amount_fwd outer onion >= total_amt outer onion)\n        #              the htlcs get moved to the parent mpp set (created once first part is complete) grouped by:\n        #              \"payment_hash + inner onion payment secret (the one in the invoice)\"\n        #              After moving the htlcs the first set gets deleted.\n        #\n        # Add the validated htlc to the htlc set associated with the payment key.\n        # If no set exists, a new set in WAITING state is created.\n        mpp_status = self.received_mpp_htlcs.get(payment_key)\n        if mpp_status is None:\n            self.logger.debug(f\"creating new mpp set for {payment_key=}\")\n            mpp_status = ReceivedMPPStatus(\n                resolution=RecvMPPResolution.WAITING,\n                htlcs=frozenset(),\n            )\n\n        if mpp_status.resolution > RecvMPPResolution.WAITING:\n            # we are getting a htlc for a set that is not in WAITING state, it cannot be safely added\n            self.logger.info(f\"htlc set cannot accept htlc, failing htlc: {channel_id=} {htlc.htlc_id=}\")\n            if mpp_status == RecvMPPResolution.EXPIRED:\n                raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'')\n            raise OnionRoutingFailure(\n                code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,\n                data=htlc.amount_msat.to_bytes(8, byteorder=\"big\"),\n            )\n\n        new_htlc = ReceivedMPPHtlc(\n            channel_id=channel_id,\n            htlc=htlc,\n            unprocessed_onion=unprocessed_onion_packet,\n        )\n        assert new_htlc not in mpp_status.htlcs, \"each htlc should make it here only once?\"\n        assert isinstance(unprocessed_onion_packet, str)\n        new_htlcs = set(mpp_status.htlcs)\n        new_htlcs.add(new_htlc)\n        self.received_mpp_htlcs[payment_key] = mpp_status._replace(htlcs=frozenset(new_htlcs))\n\n    def set_mpp_resolution(self, payment_key: str, new_resolution: RecvMPPResolution) -> ReceivedMPPStatus:\n        mpp_status = self.received_mpp_htlcs[payment_key]\n        if mpp_status.resolution == new_resolution:\n            return mpp_status\n        if not (mpp_status.resolution, new_resolution) in lnutil.allowed_mpp_set_transitions:\n            raise ValueError(f'forbidden mpp set transition: {mpp_status.resolution} -> {new_resolution}')\n        self.logger.info(f'set_mpp_resolution {new_resolution.name} {len(mpp_status.htlcs)=}: {payment_key=}')\n        self.received_mpp_htlcs[payment_key] = mpp_status._replace(resolution=new_resolution)\n        self.wallet.save_db()\n        return self.received_mpp_htlcs[payment_key]\n\n    def set_htlc_set_error(\n        self,\n        payment_key: str,\n        error: Union[bytes, OnionFailureCode, OnionRoutingFailure],\n    ) -> Optional[Tuple[Optional[bytes], Optional[OnionFailureCode | int], Optional[bytes]]]:\n        \"\"\"\n        handles different types of errors and sets the htlc set to failed, then returns a more\n        structured tuple of error types which can then be used to fail the htlc set\n        \"\"\"\n        htlc_set = self.received_mpp_htlcs[payment_key]\n        assert htlc_set.resolution != RecvMPPResolution.SETTLING\n        raw_error, error_code, error_data = None, None, None\n        if isinstance(error, bytes):\n            raw_error = error\n        elif isinstance(error, OnionFailureCode):\n            error_code = error\n        elif isinstance(error, OnionRoutingFailure):\n            error_code, error_data = OnionFailureCode.from_int(error.code), error.data\n        else:\n            raise ValueError(f\"invalid error type: {repr(error)}\")\n\n        if error_code == OnionFailureCode.MPP_TIMEOUT:\n            self.set_mpp_resolution(payment_key=payment_key, new_resolution=RecvMPPResolution.EXPIRED)\n        else:\n            self.set_mpp_resolution(payment_key=payment_key, new_resolution=RecvMPPResolution.FAILED)\n\n        return raw_error, error_code, error_data\n\n    def get_mpp_resolution(self, payment_hash: bytes) -> Optional[RecvMPPResolution]:\n        payment_key = self._get_payment_key(payment_hash)\n        status = self.received_mpp_htlcs.get(payment_key.hex())\n        return status.resolution if status else None\n\n    def is_complete_mpp(self, payment_hash: bytes) -> bool:\n        resolution = self.get_mpp_resolution(payment_hash)\n        if resolution is not None:\n            return resolution in (RecvMPPResolution.COMPLETE, RecvMPPResolution.SETTLING)\n        return False\n\n    def get_payment_mpp_amount_msat(self, payment_hash: bytes) -> Optional[int]:\n        \"\"\"Returns the received mpp amount for given payment hash.\"\"\"\n        payment_key = self._get_payment_key(payment_hash)\n        total_msat = self.get_mpp_amounts(payment_key)\n        if not total_msat:\n            return None\n        return total_msat\n\n    def get_mpp_amounts(self, payment_key: bytes) -> Optional[int]:\n        \"\"\"Returns total received amount or None.\"\"\"\n        mpp_status = self.received_mpp_htlcs.get(payment_key.hex())\n        if not mpp_status:\n            return None\n        total = sum([mpp_htlc.htlc.amount_msat for mpp_htlc in mpp_status.htlcs])\n        return total\n\n    def maybe_cleanup_mpp(\n            self,\n            chan: Channel,\n    ) -> None:\n        \"\"\"\n        Remove all remaining mpp htlcs of the given channel after closing.\n        Usually they get removed in htlc_switch after all htlcs of the set are resolved,\n        however if there is a force close with pending htlcs they need to be removed after the channel\n        is closed.\n        \"\"\"\n        # only cleanup when channel is REDEEMED as mpp set is still required for lnsweep\n        assert chan._state == ChannelState.REDEEMED\n        for payment_key_hex, mpp_status in list(self.received_mpp_htlcs.items()):\n            htlcs_to_remove = [htlc for htlc in mpp_status.htlcs if htlc.channel_id == chan.channel_id]\n            new_htlcs = set(mpp_status.htlcs)\n            for stale_mpp_htlc in htlcs_to_remove:\n                assert mpp_status.resolution != RecvMPPResolution.WAITING\n                self.logger.info(f'maybe_cleanup_mpp: removing htlc of MPP {payment_key_hex}')\n                new_htlcs.remove(stale_mpp_htlc)\n            if htlcs_to_remove:\n                mpp_status = mpp_status._replace(htlcs=frozenset(new_htlcs))\n                self.received_mpp_htlcs[payment_key_hex] = mpp_status  # save changes to db\n            if len(mpp_status.htlcs) == 0:\n                self.logger.info(f'maybe_cleanup_mpp: removing mpp {payment_key_hex}')\n                del self.received_mpp_htlcs[payment_key_hex]\n                self.maybe_cleanup_forwarding(payment_key_hex)\n\n    def maybe_cleanup_forwarding(self, payment_key_hex: str) -> None:\n        self.active_forwardings.pop(payment_key_hex, None)\n        self.forwarding_failures.pop(payment_key_hex, None)\n\n    def get_payment_status(self, payment_hash: bytes, *, direction: lnutil.Direction) -> int:\n        info = self.get_payment_info(payment_hash, direction=direction)\n        return info.status if info else PR_UNPAID\n\n    def get_invoice_status(self, invoice: BaseInvoice) -> int:\n        invoice_id = invoice.rhash\n        assert isinstance(invoice, (Request, Invoice)), type(invoice)\n        direction = RECEIVED if isinstance(invoice, Request) else SENT\n        status = self.get_payment_status(bfh(invoice_id), direction=direction)\n        if status == PR_UNPAID and invoice_id in self.inflight_payments:\n            return PR_INFLIGHT\n        # status may be PR_FAILED\n        if status == PR_UNPAID and invoice_id in self.logs:\n            status = PR_FAILED\n        return status\n\n    def set_invoice_status(self, key: str, status: int) -> None:\n        if status == PR_INFLIGHT:\n            self.inflight_payments.add(key)\n        elif key in self.inflight_payments:\n            self.inflight_payments.remove(key)\n        if status in SAVED_PR_STATUS:\n            self.set_payment_status(bfh(key), status, direction=SENT)\n        util.trigger_callback('invoice_status', self.wallet, key, status)\n        self.logger.info(f\"set_invoice_status {key}: {status}\")\n        # liquidity changed\n        self.clear_invoices_cache()\n\n    def set_request_status(self, payment_hash: bytes, status: int) -> None:\n        if self.get_payment_status(payment_hash, direction=RECEIVED) == status:\n            return\n        self.set_payment_status(payment_hash, status, direction=RECEIVED)\n        request_id = payment_hash.hex()\n        req = self.wallet.get_request(request_id)\n        if req is None:\n            return\n        util.trigger_callback('request_status', self.wallet, request_id, status)\n\n    def set_payment_status(self, payment_hash: bytes, status: int, *, direction: lnutil.Direction) -> None:\n        info = self.get_payment_info(payment_hash, direction=direction)\n        if info is None:\n            # if we are forwarding\n            return\n        info = dataclasses.replace(info, status=status)\n        self.save_payment_info(info)\n\n    def is_forwarded_htlc(self, htlc_key) -> Optional[str]:\n        \"\"\"Returns whether this was a forwarded HTLC.\"\"\"\n        for payment_key, htlcs in self.active_forwardings.items():\n            if htlc_key in htlcs:\n                return payment_key\n        return None\n\n    def notify_upstream_peer(self, htlc_key: str) -> None:\n        \"\"\"Called when an HTLC we offered on chan gets irrevocably fulfilled or failed.\n        If we find this was a forwarded HTLC, the upstream peer is notified.\n        \"\"\"\n        upstream_key = self.downstream_to_upstream_htlc.pop(htlc_key, None)\n        if not upstream_key:\n            return\n        upstream_chan_scid, _ = deserialize_htlc_key(upstream_key)\n        upstream_chan = self.get_channel_by_short_id(upstream_chan_scid)\n        upstream_peer = self.lnpeermgr.get_peer_by_pubkey(upstream_chan.node_id) if upstream_chan else None\n        if upstream_peer:\n            upstream_peer.downstream_htlc_resolved_event.set()\n            upstream_peer.downstream_htlc_resolved_event.clear()\n\n    def htlc_fulfilled(self, chan: Channel, payment_hash: bytes, htlc_id: int):\n\n        util.trigger_callback('htlc_fulfilled', payment_hash, chan, htlc_id)\n        htlc_key = serialize_htlc_key(chan.get_scid_or_local_alias(), htlc_id)\n        fw_key = self.is_forwarded_htlc(htlc_key)\n        if fw_key:\n            fw_htlcs = self.active_forwardings[fw_key]\n            fw_htlcs.remove(htlc_key)\n\n        shi = self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id))\n        if shi and htlc_id in chan.onion_keys:\n            chan.pop_onion_key(htlc_id)\n            payment_key = payment_hash + shi.payment_secret_orig\n            paysession = self._paysessions[payment_key]\n            q = paysession.sent_htlcs_q\n            htlc_log = HtlcLog(\n                success=True,\n                route=shi.route,\n                amount_msat=shi.amount_receiver_msat,\n                trampoline_fee_level=shi.trampoline_fee_level)\n            q.put_nowait(htlc_log)\n            if paysession.can_be_deleted():\n                self._paysessions.pop(payment_key)\n                paysession_active = False\n            else:\n                paysession_active = True\n        else:\n            if fw_key:\n                paysession_active = False\n            else:\n                key = payment_hash.hex()\n                self.set_invoice_status(key, PR_PAID)\n                util.trigger_callback('payment_succeeded', self.wallet, key)\n\n        if fw_key:\n            fw_htlcs = self.active_forwardings[fw_key]\n            if len(fw_htlcs) == 0 and not paysession_active:\n                self.notify_upstream_peer(htlc_key)\n\n    def htlc_failed(\n            self,\n            chan: Channel,\n            payment_hash: bytes,\n            htlc_id: int,\n            error_bytes: Optional[bytes],\n            failure_message: Optional['OnionRoutingFailure']):\n        # note: this may be called several times for the same htlc\n\n        util.trigger_callback('htlc_failed', payment_hash, chan, htlc_id)\n        htlc_key = serialize_htlc_key(chan.get_scid_or_local_alias(), htlc_id)\n        fw_key = self.is_forwarded_htlc(htlc_key)\n        if fw_key:\n            fw_htlcs = self.active_forwardings[fw_key]\n            fw_htlcs.remove(htlc_key)\n\n        shi = self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id))\n        if shi and htlc_id in chan.onion_keys:\n            onion_key = chan.pop_onion_key(htlc_id)\n            payment_okey = payment_hash + shi.payment_secret_orig\n            paysession = self._paysessions[payment_okey]\n            q = paysession.sent_htlcs_q\n            # detect if it is part of a bucket\n            # if yes, wait until the bucket completely failed\n            route = shi.route\n            if error_bytes:\n                # TODO \"decode_onion_error\" might raise, catch and maybe blacklist/penalise someone?\n                try:\n                    failure_message, sender_idx = decode_onion_error(\n                        error_bytes,\n                        [x.node_id for x in route],\n                        onion_key)\n                except Exception as e:\n                    sender_idx = None\n                    failure_message = OnionRoutingFailure(OnionFailureCode.INVALID_ONION_PAYLOAD, str(e).encode())\n            else:\n                # probably got \"update_fail_malformed_htlc\". well... who to penalise now?\n                assert failure_message is not None\n                sender_idx = None\n            self.logger.info(f\"htlc_failed {failure_message}\")\n            amount_receiver_msat = paysession.on_htlc_fail_get_fail_amt_to_propagate(shi)\n            if amount_receiver_msat is None:\n                return\n            if shi.trampoline_route:\n                route = shi.trampoline_route\n            htlc_log = HtlcLog(\n                success=False,\n                route=route,\n                amount_msat=amount_receiver_msat,\n                error_bytes=error_bytes,\n                failure_msg=failure_message,\n                sender_idx=sender_idx,\n                trampoline_fee_level=shi.trampoline_fee_level)\n            q.put_nowait(htlc_log)\n            if paysession.can_be_deleted():\n                self._paysessions.pop(payment_okey)\n                paysession_active = False\n            else:\n                paysession_active = True\n        else:\n            if fw_key:\n                paysession_active = False\n            else:\n                self.logger.info(f\"received unknown htlc_failed, probably from previous session (phash={payment_hash.hex()})\")\n                key = payment_hash.hex()\n                invoice = self.wallet.get_invoice(key)\n                if invoice and self.get_invoice_status(invoice) != PR_UNPAID:\n                    self.set_invoice_status(key, PR_UNPAID)\n                    util.trigger_callback('payment_failed', self.wallet, key, '')\n\n        if fw_key:\n            fw_htlcs = self.active_forwardings[fw_key]\n            can_forward_failure = (len(fw_htlcs) == 0) and not paysession_active\n            if can_forward_failure:\n                self.logger.info(f'htlc_failed: save_forwarding_failure (phash={payment_hash.hex()})')\n                self.save_forwarding_failure(fw_key, error_bytes=error_bytes, failure_message=failure_message)\n                self.notify_upstream_peer(htlc_key)\n            else:\n                self.logger.info(f'htlc_failed: waiting for other htlcs to fail (phash={payment_hash.hex()})')\n\n    def calc_routing_hints_for_invoice(self, amount_msat: Optional[int], channels=None):\n        \"\"\"calculate routing hints (BOLT-11 'r' field)\"\"\"\n        routing_hints = []\n        if self.receive_requires_jit_channel(amount_msat):\n            self.logger.debug(f\"will request just-in-time channel\")\n            node_id, rest = extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)\n            alias_or_scid = self.get_static_jit_scid_alias()\n            routing_hints.append(('r', [(node_id, alias_or_scid, 0, 0, 144)]))\n            # no need for more because we cannot receive enough through the others and mpp is disabled for jit\n            channels = []\n        else:\n            if channels is None:\n                channels = list(self.get_channels_for_receiving(amount_msat=amount_msat, include_disconnected=True))\n                random.shuffle(channels)  # let's not leak channel order\n            scid_to_my_channels = {\n                chan.short_channel_id: chan for chan in channels\n                if chan.short_channel_id is not None\n            }\n        for chan in channels:\n            alias_or_scid = chan.get_remote_scid_alias() or chan.short_channel_id\n            assert isinstance(alias_or_scid, bytes), alias_or_scid\n            channel_info = get_mychannel_info(chan.short_channel_id, scid_to_my_channels)\n            # note: as a fallback, if we don't have a channel update for the\n            # incoming direction of our private channel, we fill the invoice with garbage.\n            # the sender should still be able to pay us, but will incur an extra round trip\n            # (they will get the channel update from the onion error)\n            # at least, that's the theory. https://github.com/lightningnetwork/lnd/issues/2066\n            fee_base_msat = fee_proportional_millionths = 0\n            cltv_delta = 1  # lnd won't even try with zero\n            missing_info = True\n            if channel_info:\n                policy = get_mychannel_policy(channel_info.short_channel_id, chan.node_id, scid_to_my_channels)\n                if policy:\n                    fee_base_msat = policy.fee_base_msat\n                    fee_proportional_millionths = policy.fee_proportional_millionths\n                    cltv_delta = policy.cltv_delta\n                    missing_info = False\n            if missing_info:\n                self.logger.info(\n                    f\"Warning. Missing channel update for our channel {chan.short_channel_id}; \"\n                    f\"filling invoice with incorrect data.\")\n            routing_hints.append(('r', [(\n                chan.node_id,\n                alias_or_scid,\n                fee_base_msat,\n                fee_proportional_millionths,\n                cltv_delta)]))\n        return routing_hints\n\n    def delete_payment_info(self, payment_hash_hex: str, *, direction: lnutil.Direction):\n        # This method is called when an invoice or request is deleted by the user.\n        # The GUI only lets the user delete invoices or requests that have not been paid.\n        # Once an invoice/request has been paid, it is part of the history,\n        # and get_lightning_history assumes that payment_info is there.\n        assert self.get_payment_status(bytes.fromhex(payment_hash_hex), direction=direction) != PR_PAID\n        with self.lock:\n            key = PaymentInfo.calc_db_key(payment_hash_hex=payment_hash_hex, direction=direction)\n            self.payment_info.pop(key, None)\n\n    def get_balance(self, *, frozen=False) -> Decimal:\n        with self.lock:\n            return Decimal(sum(\n                chan.balance(LOCAL) if not chan.is_closed() and (chan.is_frozen_for_sending() if frozen else True) else 0\n                for chan in self.channels.values())) / 1000\n\n    def get_channels_for_sending(self):\n        for c in self.channels.values():\n            if c.is_active() and not c.is_frozen_for_sending():\n                if self.channel_db or self.is_trampoline_peer(c.node_id):\n                    yield c\n\n    def estimate_fee_reserve_for_total_amount(self, amount_sat: int | Decimal) -> int:\n        \"\"\"\n        Estimate how much of the given amount needs to be reserved for\n        ln payment fees to reliably pay the remaining amount.\n        \"\"\"\n        amount_msat = ceil(amount_sat * 1000)  # round up to the next sat\n        fee_msat = PaymentFeeBudget.reverse_from_total_amount(\n            total_amount_msat=amount_msat,\n            config=self.config,\n        )\n        return ceil(Decimal(fee_msat) / 1000)\n\n    def num_sats_can_send(self, deltas=None) -> Decimal:\n        \"\"\"\n        without trampoline, sum of all channel capacity\n        with trampoline, MPP must use a single trampoline\n        \"\"\"\n        if deltas is None:\n            deltas = {}\n\n        def send_capacity(chan):\n            if chan in deltas:\n                delta_msat = deltas[chan] * 1000\n                if delta_msat > chan.available_to_spend(REMOTE):\n                    delta_msat = 0\n            else:\n                delta_msat = 0\n            return chan.available_to_spend(LOCAL) + delta_msat\n        can_send_dict = defaultdict(int)\n        with self.lock:\n            for c in self.get_channels_for_sending():\n                if not self.uses_trampoline():\n                    can_send_dict[0] += send_capacity(c)\n                else:\n                    can_send_dict[c.node_id] += send_capacity(c)\n        can_send = max(can_send_dict.values()) if can_send_dict else 0\n        can_send_sat = Decimal(can_send)/1000\n        can_send_sat -= self.estimate_fee_reserve_for_total_amount(can_send_sat)\n        return max(can_send_sat, 0)\n\n    def get_channels_for_receiving(\n        self, *, amount_msat: Optional[int] = None, include_disconnected: bool = False,\n    ) -> Sequence[Channel]:\n        if not amount_msat:  # assume we want to recv a large amt, e.g. finding max.\n            amount_msat = float('inf')\n        with self.lock:\n            channels = list(self.channels.values())\n            channels = [chan for chan in channels\n                        if chan.is_open() and not chan.is_frozen_for_receiving()]\n\n            if not include_disconnected:\n                channels = [chan for chan in channels if chan.is_active()]\n\n            # Filter out nodes that have low receive capacity compared to invoice amt.\n            # Even with MPP, below a certain threshold, including these channels probably\n            # hurts more than help, as they lead to many failed attempts for the sender.\n            channels = sorted(channels, key=lambda chan: -chan.available_to_spend(REMOTE))\n            selected_channels = []\n            running_sum = 0\n            cutoff_factor = 0.2  # heuristic\n            for chan in channels:\n                recv_capacity = chan.available_to_spend(REMOTE)\n                chan_can_handle_payment_as_single_part = recv_capacity >= amount_msat\n                chan_small_compared_to_running_sum = recv_capacity < cutoff_factor * running_sum\n                if not chan_can_handle_payment_as_single_part and chan_small_compared_to_running_sum:\n                    break\n                running_sum += recv_capacity\n                selected_channels.append(chan)\n            channels = selected_channels\n            del selected_channels\n            # cap max channels to include to keep QR code reasonably scannable\n            channels = channels[:10]\n            return channels\n\n    def num_sats_can_receive(self, deltas=None) -> Decimal:\n        \"\"\"\n        We no longer assume the sender to send MPP on different channels,\n        because channel liquidities are hard to guess\n        \"\"\"\n        if deltas is None:\n            deltas = {}\n\n        def recv_capacity(chan):\n            if chan in deltas:\n                delta_msat = deltas[chan] * 1000\n                if delta_msat > chan.available_to_spend(LOCAL):\n                    delta_msat = 0\n            else:\n                delta_msat = 0\n            return chan.available_to_spend(REMOTE) + delta_msat\n        with self.lock:\n            recv_channels = self.get_channels_for_receiving()\n            recv_chan_msats = [recv_capacity(chan) for chan in recv_channels]\n        if not recv_chan_msats:\n            return Decimal(0)\n        can_receive_msat = max(recv_chan_msats)\n        return Decimal(can_receive_msat) / 1000\n\n    def receive_requires_jit_channel(self, amount_msat: Optional[int]) -> bool:\n        \"\"\"Returns true if we cannot receive the amount and have set up a trusted LSP node.\n        Cannot work reliably with 0 amount invoices as we don't know if we are able to receive it.\n        \"\"\"\n        # zeroconf provider is configured and connected\n        if (self.can_get_zeroconf_channel()\n                # we cannot receive the amount specified\n                and ((amount_msat and self.num_sats_can_receive() < (amount_msat // 1000))\n                        # or we cannot receive anything, and it's a 0 amount invoice\n                        or (not amount_msat and self.num_sats_can_receive() < 1))):\n            return True\n        return False\n\n    def can_get_zeroconf_channel(self) -> bool:\n        if not self.config.ACCEPT_ZEROCONF_CHANNELS and self.config.ZEROCONF_TRUSTED_NODE:\n            # check if zeroconf is accepted and client has trusted zeroconf node configured\n            return False\n        try:\n            node_id = extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)[0]\n        except ConnStringFormatError:\n            # invalid connection string\n            return False\n        # only return True if we are connected to the zeroconf provider\n        return self.lnpeermgr.get_peer_by_pubkey(node_id) is not None\n\n    def _suggest_channels_for_rebalance(self, direction, amount_sat) -> Sequence[Tuple[Channel, int]]:\n        \"\"\"\n        Suggest a channel and amount to send/receive with that channel, so that we will be able to receive/send amount_sat\n        This is used when suggesting a swap or rebalance in order to receive a payment\n        \"\"\"\n        with self.lock:\n            func = self.num_sats_can_send if direction == SENT else self.num_sats_can_receive\n            suggestions = []\n            channels = self.get_channels_for_sending() if direction == SENT else self.get_channels_for_receiving()\n            for chan in channels:\n                available_sat = chan.available_to_spend(LOCAL if direction == SENT else REMOTE) // 1000\n                delta = amount_sat - available_sat\n                delta += self.estimate_fee_reserve_for_total_amount(amount_sat)\n                # add safety margin\n                delta += delta // 100 + 1\n                if func(deltas={chan:delta}) >= amount_sat:\n                    suggestions.append((chan, int(delta)))\n                elif direction == RECEIVED and func(deltas={chan:2*delta}) >= amount_sat:\n                    # MPP heuristics has a 0.5 slope\n                    suggestions.append((chan, int(2*delta)))\n        if not suggestions:\n            raise NotEnoughFunds\n        return suggestions\n\n    def _suggest_rebalance(self, direction, amount_sat):\n        \"\"\"\n        Suggest a rebalance in order to be able to send or receive amount_sat.\n        Returns (from_channel, to_channel, amount to shuffle)\n        \"\"\"\n        try:\n            suggestions = self._suggest_channels_for_rebalance(direction, amount_sat)\n        except NotEnoughFunds:\n            return False\n        for chan2, delta in suggestions:\n            # margin for fee caused by rebalancing\n            delta += self.estimate_fee_reserve_for_total_amount(amount_sat)\n            # find other channel or trampoline that can send delta\n            for chan1 in self.channels.values():\n                if chan1.is_frozen_for_sending() or not chan1.is_active():\n                    continue\n                if chan1 == chan2:\n                    continue\n                if self.uses_trampoline() and chan1.node_id == chan2.node_id:\n                    continue\n                if direction == SENT:\n                    if chan1.can_pay(delta*1000):\n                        return chan1, chan2, delta\n                else:\n                    if chan1.can_receive(delta*1000):\n                        return chan2, chan1, delta\n            else:\n                continue\n        else:\n            return False\n\n    def num_sats_can_rebalance(self, chan1, chan2):\n        # TODO: we should be able to spend 'max', with variable fee\n        n1 = chan1.available_to_spend(LOCAL)\n        n1 -= self.estimate_fee_reserve_for_total_amount(n1)\n        n2 = chan2.available_to_spend(REMOTE)\n        amount_sat = min(n1, n2) // 1000\n        return amount_sat\n\n    def suggest_rebalance_to_send(self, amount_sat):\n        return self._suggest_rebalance(SENT, amount_sat)\n\n    def suggest_rebalance_to_receive(self, amount_sat):\n        return self._suggest_rebalance(RECEIVED, amount_sat)\n\n    def suggest_swap_to_send(self, amount_sat, coins):\n        # fixme: if swap_amount_sat is lower than the minimum swap amount, we need to propose a higher value\n        assert amount_sat > self.num_sats_can_send()\n        try:\n            suggestions = self._suggest_channels_for_rebalance(SENT, amount_sat)\n        except NotEnoughFunds:\n            return None\n        for chan, swap_recv_amount in suggestions:\n            # check that we can send onchain\n            swap_server_mining_fee = 10000 # guessing, because we have not called get_pairs yet\n            swap_funding_sat = swap_recv_amount + swap_server_mining_fee\n            swap_output = PartialTxOutput.from_address_and_value(DummyAddress.SWAP, int(swap_funding_sat))\n            try:\n                # check if we have enough onchain funds\n                self.wallet.make_unsigned_transaction(\n                    coins=coins,\n                    outputs=[swap_output],\n                    fee_policy=FeePolicy(self.config.FEE_POLICY_SWAPS),\n                )\n            except NotEnoughFunds:\n                continue\n            return chan, swap_recv_amount\n        return None\n\n    def suggest_swap_to_receive(self, amount_sat: int):\n        assert amount_sat > self.num_sats_can_receive(), f\"{amount_sat=} | {self.num_sats_can_receive()=}\"\n        try:\n            suggestions = self._suggest_channels_for_rebalance(RECEIVED, amount_sat)\n        except NotEnoughFunds:\n            return\n        for chan, swap_recv_amount in suggestions:\n            return chan, swap_recv_amount\n\n    async def rebalance_channels(self, chan1: Channel, chan2: Channel, *, amount_msat: int):\n        if chan1 == chan2:\n            raise Exception('Rebalance requires two different channels')\n        if self.uses_trampoline() and chan1.node_id == chan2.node_id:\n            raise Exception('Rebalance requires channels from different trampolines')\n        payment_hash = self.create_payment_info(\n            amount_msat=amount_msat,\n            exp_delay=3600,\n        )\n        info = self.get_payment_info(payment_hash, direction=RECEIVED)\n        lnaddr, invoice = self.get_bolt11_invoice(\n            payment_info=info,\n            message='rebalance',\n            fallback_address=None,\n            channels=[chan2],\n        )\n        invoice_obj = Invoice.from_bech32(invoice)\n        return await self.pay_invoice(invoice_obj, channels=[chan1])\n\n    def can_receive_invoice(self, invoice: BaseInvoice) -> bool:\n        assert invoice.is_lightning()\n        return (invoice.get_amount_sat() or 0) <= self.num_sats_can_receive()\n\n    async def close_channel(self, chan_id):\n        chan = self._channels[chan_id]\n        peer = self.lnpeermgr.get_peer_by_pubkey(chan.node_id)\n        if peer is None:\n            raise KeyError\n        return await peer.close_channel(chan_id)\n\n    def _force_close_channel(self, chan_id: bytes) -> Transaction:\n        chan = self._channels[chan_id]\n        tx = chan.force_close_tx()\n        # We set the channel state to make sure we won't sign new commitment txs.\n        # We expect the caller to try to broadcast this tx, after which it is\n        # not safe to keep using the channel even if the broadcast errors (server could be lying).\n        # Until the tx is seen in the mempool, there will be automatic rebroadcasts.\n        chan.set_state(ChannelState.FORCE_CLOSING)\n        # Add local tx to wallet to also allow manual rebroadcasts.\n        try:\n            self.wallet.adb.add_transaction(tx)\n        except UnrelatedTransactionException:\n            pass  # this can happen if (~all the balance goes to REMOTE)\n        return tx\n\n    async def force_close_channel(self, chan_id: bytes) -> str:\n        \"\"\"Force-close the channel. Network-related exceptions are propagated to the caller.\n        (automatic rebroadcasts will be scheduled)\n        \"\"\"\n        # note: as we are async, it can take a few event loop iterations between the caller\n        #       \"calling us\" and us getting to run, and we only set the channel state now:\n        tx = self._force_close_channel(chan_id)\n        await self.network.broadcast_transaction(tx)\n        return tx.txid()\n\n    def schedule_force_closing(self, chan_id: bytes) -> 'asyncio.Task[bool]':\n        \"\"\"Schedules a task to force-close the channel and returns it.\n        Network-related exceptions are suppressed.\n        (automatic rebroadcasts will be scheduled)\n        Note: this method is intentionally not async so that callers have a guarantee\n              that the channel state is set immediately.\n        \"\"\"\n        tx = self._force_close_channel(chan_id)\n        return asyncio.create_task(self.network.try_broadcasting(tx, 'force-close'))\n\n    def remove_channel(self, chan_id):\n        chan = self._channels[chan_id]\n        assert chan.can_be_deleted()\n        with self.lock:\n            self._channels.pop(chan_id)\n            self.db.get('channels').pop(chan_id.hex())\n        self.wallet.set_reserved_addresses_for_chan(chan, reserved=False)\n\n        util.trigger_callback('channels_updated', self.wallet)\n        util.trigger_callback('wallet_updated', self.wallet)\n\n    async def reestablish_peers_and_channels(self):\n        while True:\n            await asyncio.sleep(1)\n            if self.lnpeermgr.stopping_soon:\n                return\n            await self.lnpeermgr.reestablish_peer_for_zero_conf_trusted_node()\n            for chan in self.channels.values():\n                # reestablish\n                # note: we delegate filtering out uninteresting chans to this:\n                if not chan.should_try_to_reestablish_peer():\n                    continue\n                peer = self.lnpeermgr.get_peer_by_pubkey(chan.node_id)\n                if peer:\n                    # FIXME maybe this should be the responsibility of the peer itself, done in peer.main_loop:\n                    await peer.taskgroup.spawn(peer.reestablish_channel(chan))\n                else:\n                    await self.lnpeermgr.reestablish_peer_for_given_channel(chan)\n\n    def current_target_feerate_per_kw(self, *, has_anchors: bool) -> Optional[int]:\n        target: int = FEE_LN_MINIMUM_ETA_TARGET if has_anchors else FEE_LN_ETA_TARGET\n        feerate_per_kvbyte = self.network.fee_estimates.eta_target_to_fee(target)\n        if feerate_per_kvbyte is None:\n            return None\n        if has_anchors:\n            # set a floor of 5 sat/vb to have some safety margin in case the mempool\n            # grows quickly\n            feerate_per_kvbyte = max(feerate_per_kvbyte, 5000)\n        return max(FEERATE_PER_KW_MIN_RELAY_LIGHTNING, feerate_per_kvbyte // 4)\n\n    def current_low_feerate_per_kw_srk_channel(self) -> Optional[int]:\n        \"\"\"Gets low feerate for static remote key channels.\"\"\"\n        if constants.net is constants.BitcoinRegtest:\n            feerate_per_kvbyte = 0\n        else:\n            feerate_per_kvbyte = self.network.fee_estimates.eta_target_to_fee(FEE_LN_LOW_ETA_TARGET)\n            if feerate_per_kvbyte is None:\n                return None\n        low_feerate_per_kw = max(FEERATE_PER_KW_MIN_RELAY_LIGHTNING, feerate_per_kvbyte // 4)\n        # make sure this is never higher than the target feerate:\n        current_target_feerate = self.current_target_feerate_per_kw(has_anchors=False)\n        if not current_target_feerate:\n            return None\n        low_feerate_per_kw = min(low_feerate_per_kw, current_target_feerate)\n        return low_feerate_per_kw\n\n    def create_channel_backup(self, channel_id: bytes):\n        chan = self._channels[channel_id]\n        # do not backup old-style channels\n        assert chan.is_static_remotekey_enabled()\n        peer_addresses = list(chan.get_peer_addresses())\n        peer_addr = peer_addresses[0]\n        return ImportedChannelBackupStorage(\n            node_id=chan.node_id,\n            privkey=self.node_keypair.privkey,\n            funding_txid=chan.funding_outpoint.txid,\n            funding_index=chan.funding_outpoint.output_index,\n            funding_address=chan.get_funding_address(),\n            host=peer_addr.host,\n            port=peer_addr.port,\n            is_initiator=chan.constraints.is_initiator,\n            channel_seed=chan.config[LOCAL].channel_seed,\n            local_delay=chan.config[LOCAL].to_self_delay,\n            remote_delay=chan.config[REMOTE].to_self_delay,\n            remote_revocation_pubkey=chan.config[REMOTE].revocation_basepoint.pubkey,\n            remote_payment_pubkey=chan.config[REMOTE].payment_basepoint.pubkey,\n            local_payment_pubkey=chan.config[LOCAL].payment_basepoint.pubkey,\n            multisig_funding_privkey=chan.config[LOCAL].multisig_key.privkey,\n        )\n\n    def export_channel_backup(self, channel_id):\n        xpub = self.wallet.get_fingerprint()\n        backup_bytes = self.create_channel_backup(channel_id).to_bytes()\n        assert backup_bytes == ImportedChannelBackupStorage.from_bytes(backup_bytes).to_bytes(), \"roundtrip failed\"\n        encrypted = pw_encode_with_version_and_mac(backup_bytes, xpub)\n        assert backup_bytes == pw_decode_with_version_and_mac(encrypted, xpub), \"encrypt failed\"\n        return 'channel_backup:' + encrypted\n\n    async def request_force_close(self, channel_id: bytes, *, connect_str=None) -> None:\n        if chan := self.get_channel_by_id(channel_id):\n            peer = self.lnpeermgr.get_peer_by_pubkey(chan.node_id)\n            chan.should_request_force_close = True\n            if peer:\n                peer.close_and_cleanup()  # to force a reconnect\n        elif connect_str:\n            peer = await self.lnpeermgr.add_peer(connect_str)\n            await peer.request_force_close(channel_id)\n        elif channel_id in self.channel_backups:\n            await self._request_force_close_from_backup(channel_id)\n        else:\n            raise Exception(f'Unknown channel {channel_id.hex()}')\n\n    def import_channel_backup(self, data):\n        xpub = self.wallet.get_fingerprint()\n        cb_storage = ImportedChannelBackupStorage.from_encrypted_str(data, password=xpub)\n        channel_id = cb_storage.channel_id()\n        if channel_id.hex() in self.db.get_dict(\"channels\"):\n            raise Exception('Channel already in wallet')\n        self.logger.info(f'importing channel backup: {channel_id.hex()}')\n        d = self.db.get_dict(\"imported_channel_backups\")\n        d[channel_id.hex()] = cb_storage\n        with self.lock:\n            cb = ChannelBackup(cb_storage, lnworker=self)\n            self._channel_backups[channel_id] = cb\n        self.wallet.set_reserved_addresses_for_chan(cb, reserved=True)\n        self.wallet.save_db()\n        util.trigger_callback('channels_updated', self.wallet)\n        self.lnwatcher.add_channel(cb)\n\n    def has_conflicting_backup_with(self, remote_node_id: bytes):\n        \"\"\" Returns whether we have an active channel with this node on another device, using same local node id. \"\"\"\n        channel_backup_peers = [\n            cb.node_id for cb in self.channel_backups.values()\n            if (not cb.is_closed() and cb.get_local_pubkey() == self.node_keypair.pubkey)]\n        return any(remote_node_id.startswith(cb_peer_nodeid) for cb_peer_nodeid in channel_backup_peers)\n\n    def remove_channel_backup(self, channel_id):\n        chan = self.channel_backups[channel_id]\n        assert chan.can_be_deleted()\n        found = False\n        onchain_backups = self.db.get_dict(\"onchain_channel_backups\")\n        imported_backups = self.db.get_dict(\"imported_channel_backups\")\n        if channel_id.hex() in onchain_backups:\n            onchain_backups.pop(channel_id.hex())\n            found = True\n        if channel_id.hex() in imported_backups:\n            imported_backups.pop(channel_id.hex())\n            found = True\n        if not found:\n            raise Exception('Channel not found')\n        with self.lock:\n            self._channel_backups.pop(channel_id)\n        self.wallet.set_reserved_addresses_for_chan(chan, reserved=False)\n        self.wallet.save_db()\n        util.trigger_callback('channels_updated', self.wallet)\n\n    @log_exceptions\n    async def _request_force_close_from_backup(self, channel_id: bytes):\n        cb = self.channel_backups.get(channel_id)\n        if not cb:\n            raise Exception(f'channel backup not found {self.channel_backups}')\n        cb = cb.cb # storage\n        self.logger.info(f'requesting channel force close: {channel_id.hex()}')\n        if isinstance(cb, ImportedChannelBackupStorage):\n            node_id = cb.node_id\n            privkey = cb.privkey\n            addresses = [(cb.host, cb.port, 0)]\n        else:\n            assert isinstance(cb, OnchainChannelBackupStorage)\n            privkey = self.node_keypair.privkey\n            for pubkey, peer_addr in trampolines_by_id().items():\n                if pubkey.startswith(cb.node_id_prefix):\n                    node_id = pubkey\n                    addresses = [(peer_addr.host, peer_addr.port, 0)]\n                    break\n            else:\n                # we will try with gossip (see below)\n                addresses = []\n\n        async def _request_fclose(addresses):\n            for host, port, timestamp in addresses:\n                peer_addr = LNPeerAddr(host, port, node_id)\n                transport = LNTransport(privkey, peer_addr, e_proxy=ESocksProxy.from_network_settings(self.network))\n                peer = Peer(self, node_id, transport, is_channel_backup=True)\n                try:\n                    async with OldTaskGroup(wait=any) as group:\n                        await group.spawn(peer._message_loop())\n                        await group.spawn(peer.request_force_close(channel_id))\n                    return True\n                except Exception as e:\n                    self.logger.info(f'failed to connect {host} {e}')\n                    continue\n            else:\n                return False\n        # try first without gossip db\n        success = await _request_fclose(addresses)\n        if success:\n            return\n        # try with gossip db\n        if self.uses_trampoline():\n            raise Exception(_('Please enable gossip'))\n        node_id = self.network.channel_db.get_node_by_prefix(cb.node_id_prefix)\n        addresses_from_gossip = self.network.channel_db.get_node_addresses(node_id)\n        if not addresses_from_gossip:\n            raise Exception('Peer not found in gossip database')\n        success = await _request_fclose(addresses_from_gossip)\n        if not success:\n            raise Exception('failed to connect')\n\n    def maybe_add_backup_from_tx(self, tx):\n        funding_address = None\n        node_id_prefix = None\n        for i, o in enumerate(tx.outputs()):\n            script_type = get_script_type_from_output_script(o.scriptpubkey)\n            if script_type == 'p2wsh':\n                funding_index = i\n                funding_address = o.address\n                for o2 in tx.outputs():\n                    if o2.scriptpubkey.startswith(bytes([opcodes.OP_RETURN])):\n                        encrypted_data = o2.scriptpubkey[2:]\n                        data = self.decrypt_cb_data(encrypted_data, funding_address)\n                        if data.startswith(CB_MAGIC_BYTES):\n                            node_id_prefix = data[len(CB_MAGIC_BYTES):]\n        if node_id_prefix is None:\n            return\n        funding_txid = tx.txid()\n        cb_storage = OnchainChannelBackupStorage(\n            node_id_prefix=node_id_prefix,\n            funding_txid=funding_txid,\n            funding_index=funding_index,\n            funding_address=funding_address,\n            is_initiator=True)\n        channel_id = cb_storage.channel_id().hex()\n        if channel_id in self.db.get_dict(\"channels\"):\n            return\n        self.logger.info(f\"adding backup from tx\")\n        d = self.db.get_dict(\"onchain_channel_backups\")\n        d[channel_id] = cb_storage\n        cb = ChannelBackup(cb_storage, lnworker=self)\n        self.wallet.set_reserved_addresses_for_chan(cb, reserved=True)\n        self.wallet.save_db()\n        with self.lock:\n            self._channel_backups[bfh(channel_id)] = cb\n        util.trigger_callback('channels_updated', self.wallet)\n        self.lnwatcher.add_channel(cb)\n\n    async def maybe_forward_htlc_set(\n        self,\n        payment_key: str, *,\n        processed_htlc_set: dict[ReceivedMPPHtlc, Tuple[ProcessedOnionPacket, Optional[ProcessedOnionPacket]]],\n    ) -> None:\n        assert self.enable_htlc_forwarding\n        assert payment_key not in self.active_forwardings, \"cannot forward set twice\"\n        self.active_forwardings[payment_key] = []\n        self.logger.debug(f\"adding active_forwarding: {payment_key=}\")\n\n        any_mpp_htlc, (any_outer_onion, any_trampoline_onion) = next(iter(processed_htlc_set.items()))\n        try:\n            if any_trampoline_onion is None:\n                assert not any_outer_onion.are_we_final\n                assert len(processed_htlc_set) == 1, processed_htlc_set\n                forward_htlc = any_mpp_htlc.htlc\n                incoming_chan = self._channels[any_mpp_htlc.channel_id]\n                next_htlc = await self._maybe_forward_htlc(\n                    incoming_chan=incoming_chan,\n                    htlc=forward_htlc,\n                    processed_onion=any_outer_onion,\n                )\n                htlc_key = serialize_htlc_key(incoming_chan.get_scid_or_local_alias(), forward_htlc.htlc_id)\n                self.active_forwardings[payment_key].append(next_htlc)\n                self.downstream_to_upstream_htlc[next_htlc] = htlc_key\n            else:\n                assert not any_trampoline_onion.are_we_final and any_outer_onion.are_we_final\n                # trampoline forwarding\n                min_inc_cltv_abs = min(\n                    mpp_htlc.htlc.cltv_abs\n                    for mpp_htlc in processed_htlc_set.keys())  # take \"min\" to assume worst-case\n                await self._maybe_forward_trampoline(\n                    payment_hash=any_mpp_htlc.htlc.payment_hash,\n                    closest_inc_cltv_abs=min_inc_cltv_abs,\n                    total_msat=any_outer_onion.total_msat,\n                    any_trampoline_onion=any_trampoline_onion,\n                    fw_payment_key=payment_key,\n                )\n        except OnionRoutingFailure as e:\n            self.logger.debug(f\"forwarding failed: {e=}\")\n            if len(self.active_forwardings[payment_key]) == 0:\n                self.save_forwarding_failure(payment_key, failure_message=e)\n        # TODO what about other errors?\n        #      Could we \"catch-all Exception\" and fail back the htlcs with e.g. TEMPORARY_NODE_FAILURE?\n        #        - we don't want to fail the inc-HTLC for a syntax error that happens in the callback\n        #      If we don't call save_forwarding_failure(), the inc-HTLC gets stuck until expiry\n        #      and then the inc-channel will get force-closed.\n        #      => forwarding_callback() could have an API with two exceptions types:\n        #        - type1, such as OnionRoutingFailure, that signals we need to fail back the inc-HTLC\n        #        - type2, such as NoPathFound, that signals we want to retry forwarding\n\n    async def _maybe_forward_htlc(\n            self, *,\n            incoming_chan: Channel,\n            htlc: UpdateAddHtlc,\n            processed_onion: ProcessedOnionPacket,\n    ) -> str:\n\n        # Forward HTLC\n        # FIXME: there are critical safety checks MISSING here\n        #        - for example; atm we forward first and then persist \"forwarding_info\",\n        #          so if we segfault in-between and restart, we might forward an HTLC twice...\n        #          (same for trampoline forwarding)\n        #        - we could check for the exposure to dust HTLCs, see:\n        #          https://github.com/ACINQ/eclair/pull/1985\n\n        def log_fail_reason(reason: str):\n            self.logger.debug(\n                f\"_maybe_forward_htlc. will FAIL HTLC: inc_chan={incoming_chan.get_id_for_log()}. \"\n                f\"{reason}. inc_htlc={str(htlc)}. onion_payload={processed_onion.hop_data.payload}\")\n\n        forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS\n        if not forwarding_enabled:\n            log_fail_reason(\"forwarding is disabled\")\n            raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')\n        chain = self.network.blockchain()\n        if chain.is_tip_stale():\n            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')\n        if (next_chan_scid := processed_onion.next_chan_scid) is None:\n            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\\x00\\x00\\x00')\n        if (next_amount_msat_htlc := processed_onion.amt_to_forward) is None:\n            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\\x00\\x00\\x00')\n        if (next_cltv_abs := processed_onion.outgoing_cltv_value) is None:\n            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\\x00\\x00\\x00')\n\n        next_chan = self.get_channel_by_short_id(next_chan_scid)\n\n        if self.features.supports(LnFeatures.OPTION_ZEROCONF_OPT):\n            next_peer = self.get_peer_by_static_jit_scid_alias(next_chan_scid)\n        else:\n            next_peer = None\n\n        if not next_chan and next_peer and next_peer.accepts_zeroconf():\n            # check if an already existing channel can be used.\n            # todo: split the payment\n            for next_chan in next_peer.channels.values():\n                if next_chan.can_pay(next_amount_msat_htlc):\n                    break\n            else:\n                return await self.open_channel_just_in_time(\n                    next_peer=next_peer,\n                    next_amount_msat_htlc=next_amount_msat_htlc,\n                    next_cltv_abs=next_cltv_abs,\n                    payment_hash=htlc.payment_hash,\n                    next_onion=processed_onion.next_packet)\n\n        local_height = chain.height()\n        if next_chan is None:\n            log_fail_reason(f\"cannot find next_chan {next_chan_scid}\")\n            raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')\n        outgoing_chan_upd = next_chan.get_outgoing_gossip_channel_update(scid=next_chan_scid)[2:]\n        outgoing_chan_upd_len = len(outgoing_chan_upd).to_bytes(2, byteorder=\"big\")\n        outgoing_chan_upd_message = outgoing_chan_upd_len + outgoing_chan_upd\n        if not next_chan.can_send_update_add_htlc():\n            log_fail_reason(\n                f\"next_chan {next_chan.get_id_for_log()} cannot send ctx updates. \"\n                f\"chan state {next_chan.get_state()!r}, peer state: {next_chan.peer_state!r}\")\n            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message)\n        if not next_chan.can_pay(next_amount_msat_htlc):\n            log_fail_reason(f\"transient error (likely due to insufficient funds): not next_chan.can_pay(amt)\")\n            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message)\n        if htlc.cltv_abs - next_cltv_abs < next_chan.forwarding_cltv_delta:\n            log_fail_reason(\n                f\"INCORRECT_CLTV_EXPIRY. \"\n                f\"{htlc.cltv_abs=} - {next_cltv_abs=} < {next_chan.forwarding_cltv_delta=}\")\n            data = htlc.cltv_abs.to_bytes(4, byteorder=\"big\") + outgoing_chan_upd_message\n            raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data)\n        if htlc.cltv_abs - lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED <= local_height \\\n                or next_cltv_abs <= local_height:\n            raise OnionRoutingFailure(code=OnionFailureCode.EXPIRY_TOO_SOON, data=outgoing_chan_upd_message)\n        if max(htlc.cltv_abs, next_cltv_abs) > local_height + lnutil.NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:\n            raise OnionRoutingFailure(code=OnionFailureCode.EXPIRY_TOO_FAR, data=b'')\n        forwarding_fees = fee_for_edge_msat(\n            forwarded_amount_msat=next_amount_msat_htlc,\n            fee_base_msat=next_chan.forwarding_fee_base_msat,\n            fee_proportional_millionths=next_chan.forwarding_fee_proportional_millionths)\n        if htlc.amount_msat - next_amount_msat_htlc < forwarding_fees:\n            data = next_amount_msat_htlc.to_bytes(8, byteorder=\"big\") + outgoing_chan_upd_message\n            raise OnionRoutingFailure(code=OnionFailureCode.FEE_INSUFFICIENT, data=data)\n        if self._maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(htlc.payment_hash):\n            log_fail_reason(f\"RHASH corresponds to payreq we created\")\n            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')\n        self.logger.info(\n            f\"maybe_forward_htlc. will forward HTLC: inc_chan={incoming_chan.short_channel_id}. inc_htlc={str(htlc)}. \"\n            f\"next_chan={next_chan.get_id_for_log()}.\")\n\n        next_peer = self.lnpeermgr.get_peer_by_pubkey(next_chan.node_id)\n        if next_peer is None:\n            log_fail_reason(f\"next_peer offline ({next_chan.node_id.hex()})\")\n            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message)\n        try:\n            next_htlc = next_peer.send_htlc(\n                chan=next_chan,\n                payment_hash=htlc.payment_hash,\n                amount_msat=next_amount_msat_htlc,\n                cltv_abs=next_cltv_abs,\n                onion=processed_onion.next_packet,\n            )\n        except BaseException as e:\n            log_fail_reason(f\"error sending message to next_peer={next_chan.node_id.hex()}\")\n            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message)\n\n        htlc_key = serialize_htlc_key(next_chan.get_scid_or_local_alias(), next_htlc.htlc_id)\n        return htlc_key\n\n    async def _maybe_forward_trampoline(\n            self, *,\n            payment_hash: bytes,\n            closest_inc_cltv_abs: int,\n            total_msat: int,  # total_msat of the outer onion\n            any_trampoline_onion: ProcessedOnionPacket,  # any trampoline onion of the incoming htlc set, they should be similar\n            fw_payment_key: str,\n    ) -> None:\n\n        forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS\n        forwarding_trampoline_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS\n        if not (forwarding_enabled and forwarding_trampoline_enabled):\n            self.logger.info(f\"trampoline forwarding is disabled. failing htlc.\")\n            raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')\n        payload = any_trampoline_onion.hop_data.payload\n        payment_data = payload.get('payment_data')\n        try:\n            payment_secret = payment_data['payment_secret'] if payment_data else os.urandom(32)\n            outgoing_node_id = payload[\"outgoing_node_id\"][\"outgoing_node_id\"]\n            amt_to_forward = payload[\"amt_to_forward\"][\"amt_to_forward\"]\n            out_cltv_abs = payload[\"outgoing_cltv_value\"][\"outgoing_cltv_value\"]\n            if \"invoice_features\" in payload:\n                self.logger.info('forward_trampoline: legacy')\n                next_trampoline_onion = None\n                invoice_features = payload[\"invoice_features\"][\"invoice_features\"]\n                invoice_routing_info = payload[\"invoice_routing_info\"][\"invoice_routing_info\"]\n                r_tags = decode_routing_info(invoice_routing_info)\n                self.logger.info(f'r_tags {LnAddr.format_bolt11_routing_info_as_human_readable(r_tags)}')\n                # TODO legacy mpp payment, use total_msat from trampoline onion\n            else:\n                self.logger.info('forward_trampoline: end-to-end')\n                invoice_features = LnFeatures.BASIC_MPP_OPT\n                next_trampoline_onion = any_trampoline_onion.next_packet\n                r_tags = []\n        except Exception as e:\n            self.logger.exception('')\n            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\\x00\\x00\\x00')\n\n        if self._maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(payment_hash):\n            self.logger.debug(\n                f\"maybe_forward_trampoline. will FAIL HTLC(s). \"\n                f\"RHASH corresponds to payreq we created. {payment_hash.hex()=}\")\n            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')\n\n        # these are the fee/cltv paid by the sender\n        # pay_to_node will raise if they are not sufficient\n        budget = PaymentFeeBudget(\n            fee_msat=total_msat - amt_to_forward,\n            cltv=closest_inc_cltv_abs - out_cltv_abs,\n        )\n        self.logger.info(f'trampoline forwarding. budget={budget}')\n        self.logger.info(f'trampoline forwarding. {closest_inc_cltv_abs=}, {out_cltv_abs=}')\n        # To convert abs vs rel cltvs, we need to guess blockheight used by original sender as \"current blockheight\".\n        # Blocks might have been mined since.\n        # - if we skew towards the past, we decrease our own cltv_budget accordingly (which is ok)\n        # - if we skew towards the future, we decrease the cltv_budget for the subsequent nodes in the path,\n        #   which can result in them failing the payment.\n        # So we skew towards the past and guess that there has been 1 new block mined since the payment began:\n        local_height_of_onion_creator = self.network.get_local_height() - 1\n        cltv_budget_for_rest_of_route = out_cltv_abs - local_height_of_onion_creator\n\n        if budget.fee_msat < 1000:\n            raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')\n        if budget.cltv < 576:\n            raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')\n\n        # do we have a connection to the node?\n        next_peer = self.lnpeermgr.get_peer_by_pubkey(outgoing_node_id)\n        if next_peer and next_peer.accepts_zeroconf():\n            self.logger.info(f'JIT: found next_peer')\n            for next_chan in next_peer.channels.values():\n                if next_chan.can_pay(amt_to_forward):\n                    # todo: detect if we can do mpp\n                    self.logger.info(f'jit: next_chan can pay')\n                    break\n            else:\n                scid_alias = self._scid_alias_of_node(next_peer.pubkey)\n                route = [RouteEdge(\n                    start_node=next_peer.pubkey,\n                    end_node=outgoing_node_id,\n                    short_channel_id=scid_alias,\n                    fee_base_msat=0,\n                    fee_proportional_millionths=0,\n                    cltv_delta=144,\n                    node_features=0\n                )]\n                next_onion, amount_msat, cltv_abs, session_key = self.create_onion_for_route(\n                    route=route,\n                    amount_msat=amt_to_forward,\n                    total_msat=amt_to_forward,\n                    payment_hash=payment_hash,\n                    min_final_cltv_delta=cltv_budget_for_rest_of_route,\n                    payment_secret=payment_secret,\n                    trampoline_onion=next_trampoline_onion,\n                )\n                await self.open_channel_just_in_time(\n                    next_peer=next_peer,\n                    next_amount_msat_htlc=amt_to_forward,\n                    next_cltv_abs=cltv_abs,\n                    payment_hash=payment_hash,\n                    next_onion=next_onion)\n                return\n\n        try:\n            await self.pay_to_node(\n                node_pubkey=outgoing_node_id,\n                payment_hash=payment_hash,\n                payment_secret=payment_secret,\n                amount_to_pay=amt_to_forward,\n                min_final_cltv_delta=cltv_budget_for_rest_of_route,\n                r_tags=r_tags,\n                invoice_features=invoice_features,\n                fwd_trampoline_onion=next_trampoline_onion,\n                budget=budget,\n                attempts=100,\n                fw_payment_key=fw_payment_key,\n            )\n        except OnionRoutingFailure as e:\n            raise\n        except FeeBudgetExceeded:\n            raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')\n        except PaymentFailure as e:\n            self.logger.debug(\n                f\"maybe_forward_trampoline. PaymentFailure for {payment_hash.hex()=}, {payment_secret.hex()=}: {e!r}\")\n            raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')\n\n    def _maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(self, payment_hash: bytes) -> bool:\n        \"\"\"Returns True if the HTLC should be failed.\n        We must not forward HTLCs with a matching payment_hash to a payment request we created.\n        Example attack:\n        - Bob creates payment request with HASH1, for 1 BTC; and gives the payreq to Alice\n        - Alice sends htlc A->B->C, for 1 sat, with HASH1\n        - Bob must not release the preimage of HASH1\n        \"\"\"\n        payment_info = self.get_payment_info(payment_hash, direction=RECEIVED)\n        # note: If we don't have the preimage for a payment request, then it must be a hold invoice.\n        #       Hold invoices are created by other parties (e.g. a counterparty initiating a submarine swap),\n        #       and it is the other party choosing the payment_hash. If we failed HTLCs with payment_hashes colliding\n        #       with hold invoices, then a party that can make us save a hold invoice for an arbitrary hash could\n        #       also make us fail arbitrary HTLCs.\n        return bool(payment_info and self.get_preimage(payment_hash))\n\n    def create_onion_for_route(\n        self, *,\n        route: 'LNPaymentRoute',\n        amount_msat: int,\n        total_msat: int,\n        payment_hash: bytes,\n        min_final_cltv_delta: int,\n        payment_secret: bytes,\n        trampoline_onion: Optional[OnionPacket] = None,\n    ):\n        # add features learned during \"init\" for direct neighbour:\n        route[0].node_features |= self.features\n        local_height = self.network.get_local_height()\n        final_cltv_abs = local_height + min_final_cltv_delta\n        hops_data, amount_msat, cltv_abs = calc_hops_data_for_payment(\n            route,\n            amount_msat,\n            final_cltv_abs=final_cltv_abs,\n            total_msat=total_msat,\n            payment_secret=payment_secret)\n        self.logger.info(f\"pay len(route)={len(route)}. for payment_hash={payment_hash.hex()}\")\n        for i in range(len(route)):\n            self.logger.info(f\"  {i}: edge={route[i].short_channel_id} hop_data={hops_data[i]!r}\")\n        assert final_cltv_abs <= cltv_abs, (final_cltv_abs, cltv_abs)\n        session_key = os.urandom(32) # session_key\n        # if we are forwarding a trampoline payment, add trampoline onion\n        if trampoline_onion:\n            self.logger.info(f'adding trampoline onion to final payload')\n            trampoline_payload = dict(hops_data[-1].payload)\n            trampoline_payload[\"trampoline_onion_packet\"] = {\n                \"version\": trampoline_onion.version,\n                \"public_key\": trampoline_onion.public_key,\n                \"hops_data\": trampoline_onion.hops_data,\n                \"hmac\": trampoline_onion.hmac\n            }\n            hops_data[-1] = dataclasses.replace(hops_data[-1], payload=trampoline_payload)\n            if t_hops_data := trampoline_onion._debug_hops_data:  # None if trampoline-forwarding\n                t_route = trampoline_onion._debug_route\n                assert t_route is not None\n                self.logger.info(f\"lnpeer.pay len(t_route)={len(t_route)}\")\n                for i in range(len(t_route)):\n                    self.logger.info(f\"  {i}: t_node={t_route[i].end_node.hex()} hop_data={t_hops_data[i]!r}\")\n        # create onion packet\n        payment_path_pubkeys = [x.node_id for x in route]\n        onion = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=payment_hash) # must use another sessionkey\n        self.logger.info(f\"starting payment. len(route)={len(hops_data)}.\")\n        # create htlc\n        if cltv_abs > local_height + lnutil.NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:\n            raise PaymentFailure(f\"htlc expiry too far into future. (in {cltv_abs-local_height} blocks)\")\n        return onion, amount_msat, cltv_abs, session_key\n\n    def save_forwarding_failure(\n            self,\n            payment_key: str,\n            *,\n            error_bytes: Optional[bytes] = None,\n            failure_message: Optional['OnionRoutingFailure'] = None\n    ) -> None:\n        error_hex = error_bytes.hex() if error_bytes else None\n        failure_hex = failure_message.to_bytes().hex() if failure_message else None\n        self.forwarding_failures[payment_key] = (error_hex, failure_hex)\n\n    def get_forwarding_failure(self, payment_key: str) -> Tuple[Optional[bytes], Optional['OnionRoutingFailure']]:\n        error_hex, failure_hex = self.forwarding_failures.get(payment_key, (None, None))\n        error_bytes = bytes.fromhex(error_hex) if error_hex else None\n        failure_message = OnionRoutingFailure.from_bytes(bytes.fromhex(failure_hex)) if failure_hex else None\n        return error_bytes, failure_message\n"
  },
  {
    "path": "electrum/logging.py",
    "content": "# Copyright (C) 2019 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file LICENCE or http://www.opensource.org/licenses/mit-license.php\n\nimport logging\nimport logging.handlers\nimport datetime\nimport sys\nimport pathlib\nimport os\nimport platform\nfrom typing import Optional, TYPE_CHECKING, Set\nimport copy\nimport subprocess\nimport hashlib\n\nif TYPE_CHECKING:\n    from .simple_config import SimpleConfig\n\n\nclass LogFormatterForFiles(logging.Formatter):\n\n    def formatTime(self, record, datefmt=None):\n        # timestamps follow ISO 8601 UTC\n        date = datetime.datetime.fromtimestamp(record.created).astimezone(datetime.timezone.utc)\n        if not datefmt:\n            datefmt = \"%Y%m%dT%H%M%S.%fZ\"\n        return date.strftime(datefmt)\n\n    def format(self, record):\n        record = _shorten_name_of_logrecord(record)\n        return super().format(record)\n\n\nfile_formatter = LogFormatterForFiles(fmt=\"%(asctime)22s | %(levelname)8s | %(name)s | %(message)s\")\n\n\nclass LogFormatterForConsole(logging.Formatter):\n\n    def formatTime(self, record, datefmt=None):\n        t = record.relativeCreated / 1000\n        return f\"{t:6.2f}\"\n\n    def format(self, record):\n        record = copy.copy(record)  # avoid mutating arg\n        record = _shorten_name_of_logrecord(record)\n        text = super().format(record)\n        return text\n\n\n# try to make console log lines short... no timestamp, short levelname, no \"electrum.\"\nconsole_formatter = LogFormatterForConsole(fmt=\"%(asctime)s | %(levelname).1s | %(name)s | %(message)s\")\n\n\ndef _shorten_name_of_logrecord(record: logging.LogRecord) -> logging.LogRecord:\n    record = copy.copy(record)  # avoid mutating arg\n    # strip the main module name from the logger name\n    if record.name.startswith(\"electrum.\"):\n        record.name = record.name[9:]\n    # manual map to shorten common module names\n    record.name = record.name.replace(\"interface.Interface\", \"interface\", 1)\n    record.name = record.name.replace(\"network.Network\", \"network\", 1)\n    record.name = record.name.replace(\"synchronizer.Synchronizer\", \"synchronizer\", 1)\n    record.name = record.name.replace(\"verifier.SPV\", \"verifier\", 1)\n    record.name = record.name.replace(\"gui.qt.main_window.ElectrumWindow\", \"gui.qt.main_window\", 1)\n    return record\n\n\nclass TruncatingMemoryHandler(logging.handlers.MemoryHandler):\n    \"\"\"An in-memory log handler that only keeps the first N log messages\n    and discards the rest.\n    \"\"\"\n    target: Optional['logging.Handler']\n\n    def __init__(self):\n        logging.handlers.MemoryHandler.__init__(\n            self,\n            capacity=1,  # note: this is the flushing frequency, ~unused by us\n            flushLevel=logging.DEBUG,\n        )\n        self.max_size = 100  # max num of messages we keep\n        self.num_messages_seen = 0\n        self.__never_dumped = True\n\n    # note: this flush implementation *keeps* the buffer as-is, instead of clearing it\n    def flush(self):\n        self.acquire()\n        try:\n            if self.target:\n                for record in self.buffer:\n                    if record.levelno >= self.target.level:\n                        self.target.handle(record)\n        finally:\n            self.release()\n\n    def dump_to_target(self, target: 'logging.Handler'):\n        self.acquire()\n        try:\n            self.setTarget(target)\n            self.flush()\n            self.setTarget(None)\n        finally:\n            self.__never_dumped = False\n            self.release()\n\n    def emit(self, record):\n        self.num_messages_seen += 1\n        if len(self.buffer) < self.max_size:\n            super().emit(record)\n\n    def close(self) -> None:\n        # Check if captured log lines were never to dumped to e.g. stderr,\n        # and if so, try to do it now. This is useful e.g. in case of sys.exit().\n        if self.__never_dumped:\n            _configure_stderr_logging()\n        super().close()\n\n\ndef _delete_old_logs(path, *, num_files_keep: int, max_total_size: int):\n    \"\"\"Delete old logfiles, only keeping the latest few.\"\"\"\n    def sortkey_oldest_first(p: pathlib.PurePath):\n        fname = p.name\n        basename, ext, counter = str(fname).partition(\".log\")\n        # - each time electrum is launched, there will be a new basename, ordered by date\n        # - for any given basename, there might be multiple log files, differing by counter\n        #   - empty counter is newest, then .1 is older, .2 is even older, etc\n        try:\n            counter = int(counter[1:]) if counter else 0  # convert \".2\" -> 2\n        except ValueError:\n            _logger.warning(f\"failed to parse log file name: {fname}\")\n            counter = 0\n        return basename, -counter\n    files = sorted(\n        list(pathlib.Path(path).glob(\"electrum_log_*.log*\")),\n        key=sortkey_oldest_first,\n    )\n    total_size = sum(os.stat(f).st_size for f in files)  # in bytes\n    num_files_remaining = len(files)\n    for f in files:\n        fsize = os.stat(f).st_size\n        if total_size < max_total_size and num_files_remaining <= num_files_keep:\n            break\n        total_size -= fsize\n        num_files_remaining -= 1\n        try:\n            os.remove(f)\n        except OSError as e:\n            _logger.warning(f\"cannot delete old logfile: {e}\")\n\n\n_logfile_path = None\ndef _configure_file_logging(\n    log_directory: pathlib.Path,\n    *,\n    num_files_keep: int,\n    max_total_size: int,\n):\n    from .util import os_chmod\n\n    global _logfile_path\n    assert _logfile_path is None, 'file logging already initialized'\n    log_directory.mkdir(exist_ok=True, mode=0o700)\n\n    _delete_old_logs(log_directory, num_files_keep=num_files_keep, max_total_size=max_total_size)\n\n    timestamp = datetime.datetime.now(datetime.timezone.utc).strftime(\"%Y%m%dT%H%M%SZ\")\n    PID = os.getpid()\n    _logfile_path = log_directory / f\"electrum_log_{timestamp}_{PID}.log\"\n    # we create the file with restrictive perms, instead of letting FileHandler create it\n    with open(_logfile_path, \"w+\") as f:\n        os_chmod(_logfile_path, 0o600)\n\n    logfile_backupcount = 4\n    file_handler = logging.handlers.RotatingFileHandler(\n        _logfile_path,\n        maxBytes=max_total_size // (logfile_backupcount+1),\n        backupCount=logfile_backupcount,\n        encoding='utf-8')\n    file_handler.setFormatter(file_formatter)\n    file_handler.setLevel(logging.DEBUG)\n    root_logger.addHandler(file_handler)\n    if _inmemory_startup_logs:\n        _inmemory_startup_logs.dump_to_target(file_handler)\n\n\nconsole_stderr_handler = None\ndef _configure_stderr_logging(*, verbosity=None):\n    # log to stderr; by default only WARNING and higher\n    global console_stderr_handler\n    if console_stderr_handler is not None:\n        _logger.warning(\"stderr handler already exists\")\n        return\n    console_stderr_handler = logging.StreamHandler(sys.stderr)\n    console_stderr_handler.setFormatter(console_formatter)\n    if not verbosity:\n        console_stderr_handler.setLevel(logging.WARNING)\n        root_logger.addHandler(console_stderr_handler)\n    else:\n        console_stderr_handler.setLevel(logging.DEBUG)\n        root_logger.addHandler(console_stderr_handler)\n        _process_verbosity_log_levels(verbosity)\n    if _inmemory_startup_logs:\n        _inmemory_startup_logs.dump_to_target(console_stderr_handler)\n\n\ndef _process_verbosity_log_levels(verbosity):\n    if verbosity == '*' or not isinstance(verbosity, str):\n        return\n    # example verbosity:\n    #   debug,network=error,interface=error      // effectively blacklists network and interface\n    #   warning,network=debug,interface=debug    // effectively whitelists network and interface\n    filters = verbosity.split(',')\n    for filt in filters:\n        if not filt: continue\n        items = filt.split('=')\n        if len(items) == 1:\n            level = items[0]\n            electrum_logger.setLevel(level.upper())\n        elif len(items) == 2:\n            logger_name, level = items\n            logger = get_logger(logger_name)\n            logger.setLevel(level.upper())\n        else:\n            raise Exception(f\"invalid log filter: {filt}\")\n\n\nclass _CustomLogger(logging.getLoggerClass()):\n    def __init__(self, name, *args, **kwargs):\n        super().__init__(name, *args, **kwargs)\n        self.msg_hashes_seen = set()  # type: Set[bytes]\n        # ^ note: size grows without bounds, but only for log lines using \"only_once\".\n\n    def _log(self, level, msg: str, *args, only_once: bool = False, **kwargs) -> None:\n        \"\"\"Overridden to add 'only_once' arg to logger.debug()/logger.info()/logger.warning()/etc.\"\"\"\n        if only_once:  # if set, this logger will only log this msg a single time during its lifecycle\n            msg_hash = hashlib.sha256(msg.encode(\"utf-8\")).digest()\n            if msg_hash in self.msg_hashes_seen:\n                return\n            self.msg_hashes_seen.add(msg_hash)\n        super()._log(level, msg, *args, **kwargs)\n\nlogging.setLoggerClass(_CustomLogger)\n\n\n# enable logs universally (including for other libraries)\nroot_logger = logging.getLogger()\nroot_logger.setLevel(logging.WARNING)\n\n# Start collecting log messages now, into an in-memory buffer. This buffer is only\n# used until the proper log handlers are fully configured, including their verbosity,\n# at which point we will dump its contents into those, and remove this log handler.\n# Note: this is set up at import-time instead of e.g. as part of a function that is\n#       called from run_electrum (the main script). This is to have this run as early\n#       as possible.\n# Note: some users might use Electrum as a python library and not use run_electrum,\n#       in which case these logs might never get redirected or cleaned up.\n#       Also, the python docs recommend libraries not to set a handler, to\n#       avoid interfering with the user's logging.\n_inmemory_startup_logs = None\nif getattr(sys, \"_ELECTRUM_RUNNING_VIA_RUNELECTRUM\", False):\n    _inmemory_startup_logs = TruncatingMemoryHandler()\n    root_logger.addHandler(_inmemory_startup_logs)\n\n# creates a logger specifically for electrum library\nelectrum_logger = logging.getLogger(\"electrum\")\nelectrum_logger.setLevel(logging.DEBUG)\n\n\n# --- External API\n\ndef get_logger(name: str) -> _CustomLogger:\n    prefix = \"electrum.\"\n    if name.startswith(prefix):\n        name = name[len(prefix):]\n    return electrum_logger.getChild(name)\n\n\n_logger = get_logger(__name__)\n_logger.setLevel(logging.INFO)\n\n\nclass Logger:\n\n    def __init__(self):\n        self.logger = self.__get_logger_for_obj()\n\n    def __get_logger_for_obj(self) -> logging.Logger:\n        cls = self.__class__\n        if cls.__module__:\n            name = f\"{cls.__module__}.{cls.__name__}\"\n        else:\n            name = cls.__name__\n        try:\n            diag_name = self.diagnostic_name()\n        except Exception as e:\n            raise Exception(\"diagnostic name not yet available?\") from e\n        if diag_name:\n            name += f\".[{diag_name}]\"\n        logger = get_logger(name)\n        return logger\n\n    def diagnostic_name(self):\n        return ''\n\n\ndef configure_logging(config: 'SimpleConfig', *, log_to_file: Optional[bool] = None) -> None:\n    from .util import is_android_debug_apk\n\n    verbosity = config.get('verbosity')\n    if not verbosity and config.GUI_ENABLE_DEBUG_LOGS:\n        verbosity = '*'\n    _configure_stderr_logging(verbosity=verbosity)\n\n    if log_to_file is None:\n        log_to_file = config.WRITE_LOGS_TO_DISK\n        log_to_file |= is_android_debug_apk()\n    if log_to_file:\n        log_directory = pathlib.Path(config.path) / \"logs\"\n        num_files_keep = config.LOGS_NUM_FILES_KEEP\n        max_total_size = config.LOGS_MAX_TOTAL_SIZE_BYTES\n        _configure_file_logging(log_directory, num_files_keep=num_files_keep, max_total_size=max_total_size)\n\n    # clean up and delete in-memory logs\n    global _inmemory_startup_logs\n    if _inmemory_startup_logs:\n        num_discarded = _inmemory_startup_logs.num_messages_seen - _inmemory_startup_logs.max_size\n        if num_discarded > 0:\n            _logger.warning(f\"Too many log messages! Some have been discarded. \"\n                            f\"(discarded {num_discarded} messages)\")\n        _inmemory_startup_logs.close()\n        root_logger.removeHandler(_inmemory_startup_logs)\n        _inmemory_startup_logs = None\n\n    from . import ELECTRUM_VERSION\n    from .constants import GIT_REPO_URL\n    _logger.info(f\"Electrum version: {ELECTRUM_VERSION} - https://electrum.org - {GIT_REPO_URL}\")\n    _logger.info(f\"Python version: {sys.version}. On platform: {describe_os_version()}\")\n    _logger.info(f\"Logging to file: {str(_logfile_path)}\")\n    _logger.info(f\"Log filters: verbosity {repr(verbosity)}\")\n\n\ndef get_logfile_path() -> Optional[pathlib.Path]:\n    return _logfile_path\n\n\ndef describe_os_version() -> str:\n    if 'ANDROID_DATA' in os.environ:\n        import jnius\n        bv = jnius.autoclass('android.os.Build$VERSION')\n        b = jnius.autoclass('android.os.Build')\n        return \"Android {} on {} {} ({})\".format(bv.RELEASE, b.BRAND, b.DEVICE, b.DISPLAY)\n    else:\n        return platform.platform()\n\n\ndef get_git_version() -> Optional[str]:\n    dir = os.path.dirname(os.path.realpath(__file__))\n    try:\n        version = subprocess.check_output(\n            ['git', 'describe', '--always', '--dirty'], cwd=dir)\n        version = str(version, \"utf8\").strip()\n    except Exception:\n        version = None\n    return version\n"
  },
  {
    "path": "electrum/lrucache.py",
    "content": "# The MIT License (MIT)\n#\n# Copyright (c) 2014-2022 Thomas Kemmer\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy of\n# this software and associated documentation files (the \"Software\"), to deal in\n# the Software without restriction, including without limitation the rights to\n# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n# the Software, and to permit persons to whom the Software is furnished to do so,\n# subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n# -----\n#\n# This is a stripped down LRU-cache from the \"cachetools\" library.\n# https://github.com/tkem/cachetools/blob/d991ac71b4eb6394be5ec572b835434081393215/src/cachetools/__init__.py\n\nimport collections\nimport collections.abc\nfrom typing import TypeVar, Dict\n\n\nclass _DefaultSize:\n\n    __slots__ = ()\n\n    def __getitem__(self, _):\n        return 1\n\n    def __setitem__(self, _, value):\n        assert value == 1\n\n    def pop(self, _):\n        return 1\n\n\n_KT = TypeVar(\"_KT\")\n_VT = TypeVar(\"_VT\")\nclass Cache(collections.abc.MutableMapping[_KT, _VT]):\n    \"\"\"Mutable mapping to serve as a simple cache or cache base class.\"\"\"\n\n    __marker = object()\n\n    __size = _DefaultSize()\n\n    def __init__(self, maxsize: int, getsizeof=None):\n        if getsizeof:\n            self.getsizeof = getsizeof\n        if self.getsizeof is not Cache.getsizeof:\n            self.__size = dict()\n        self.__data = dict()  # type: Dict[_KT, _VT]\n        self.__currsize = 0\n        self.__maxsize = maxsize\n\n    def __repr__(self):\n        return \"%s(%s, maxsize=%r, currsize=%r)\" % (\n            self.__class__.__name__,\n            repr(self.__data),\n            self.__maxsize,\n            self.__currsize,\n        )\n\n    def __getitem__(self, key: _KT) -> _VT:\n        try:\n            return self.__data[key]\n        except KeyError:\n            return self.__missing__(key)\n\n    def __setitem__(self, key: _KT, value: _VT) -> None:\n        maxsize = self.__maxsize\n        size = self.getsizeof(value)\n        if size > maxsize:\n            raise ValueError(\"value too large\")\n        if key not in self.__data or self.__size[key] < size:\n            while self.__currsize + size > maxsize:\n                self.popitem()\n        if key in self.__data:\n            diffsize = size - self.__size[key]\n        else:\n            diffsize = size\n        self.__data[key] = value\n        self.__size[key] = size\n        self.__currsize += diffsize\n\n    def __delitem__(self, key: _KT) -> None:\n        size = self.__size.pop(key)\n        del self.__data[key]\n        self.__currsize -= size\n\n    def __contains__(self, key: _KT) -> bool:\n        return key in self.__data\n\n    def __missing__(self, key: _KT):\n        raise KeyError(key)\n\n    def __iter__(self):\n        return iter(self.__data)\n\n    def __len__(self):\n        return len(self.__data)\n\n    def get(self, key: _KT, default: _VT = None) -> _VT | None:\n        if key in self:\n            return self[key]\n        else:\n            return default\n\n    def pop(self, key: _KT, default=__marker) -> _VT:\n        if key in self:\n            value = self[key]\n            del self[key]\n        elif default is self.__marker:\n            raise KeyError(key)\n        else:\n            value = default\n        return value\n\n    def setdefault(self, key: _KT, default: _VT = None) -> _VT | None:\n        if key in self:\n            value = self[key]\n        else:\n            self[key] = value = default\n        return value\n\n    @property\n    def maxsize(self) -> int:\n        \"\"\"The maximum size of the cache.\"\"\"\n        return self.__maxsize\n\n    @property\n    def currsize(self) -> int:\n        \"\"\"The current size of the cache.\"\"\"\n        return self.__currsize\n\n    @staticmethod\n    def getsizeof(value) -> int:\n        \"\"\"Return the size of a cache element's value.\"\"\"\n        return 1\n\n\nclass LRUCache(Cache[_KT, _VT]):\n    \"\"\"Least Recently Used (LRU) cache implementation.\"\"\"\n\n    def __init__(self, maxsize: int, getsizeof=None):\n        Cache.__init__(self, maxsize, getsizeof)\n        self.__order = collections.OrderedDict()\n\n    def __getitem__(self, key: _KT, cache_getitem=Cache.__getitem__) -> _VT | None:\n        value = cache_getitem(self, key)\n        if key in self:  # __missing__ may not store item\n            self.__update(key)\n        return value\n\n    def __setitem__(self, key: _KT, value, cache_setitem=Cache.__setitem__) -> None:\n        cache_setitem(self, key, value)\n        self.__update(key)\n\n    def __delitem__(self, key: _KT, cache_delitem=Cache.__delitem__) -> None:\n        cache_delitem(self, key)\n        del self.__order[key]\n\n    def popitem(self) -> tuple[_KT, _VT]:\n        \"\"\"Remove and return the `(key, value)` pair least recently used.\"\"\"\n        try:\n            key = next(iter(self.__order))\n        except StopIteration:\n            raise KeyError(\"%s is empty\" % type(self).__name__) from None\n        else:\n            return (key, self.pop(key))\n\n    def __update(self, key: _KT) -> None:\n        try:\n            self.__order.move_to_end(key)\n        except KeyError:\n            self.__order[key] = None\n"
  },
  {
    "path": "electrum/mnemonic.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2014 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport math\nimport hashlib\nimport unicodedata\nimport string\nfrom typing import Sequence, Dict, Iterator, Optional\nfrom types import MappingProxyType\n\nfrom .util import resource_path, bfh, randrange\nfrom .crypto import hmac_oneshot\nfrom . import version\nfrom .logging import Logger\n\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\ndef is_CJK(c: str) -> bool:\n    n = ord(c)\n    for imin, imax, name in CJK_INTERVALS:\n        if imin <= n <= imax:\n            return True\n    return False\n\n\ndef normalize_text(seed: str) -> str:\n    # normalize\n    seed = unicodedata.normalize('NFKD', seed)\n    # lower\n    seed = seed.lower()\n    # remove accents\n    seed = u''.join([c for c in seed if not unicodedata.combining(c)])\n    # normalize whitespaces\n    seed = u' '.join(seed.split())\n    # remove whitespaces between CJK\n    seed = u''.join([seed[i] for i in range(len(seed)) if not (seed[i] in string.whitespace and is_CJK(seed[i-1]) and is_CJK(seed[i+1]))])\n    return seed\n\n\ndef is_matching_seed(*, seed: str, seed_again: str) -> bool:\n    \"\"\"Compare two seeds for equality, as used in \"confirm seed\" screen in wizard.\n    Not just for electrum seeds, but other types (e.g. bip39) as well.\n    Note: we don't use normalize_text, as that is specific to electrum seeds.\n    \"\"\"\n    seed = \" \".join(seed.split())\n    seed_again = \" \".join(seed_again.split())\n    return seed == seed_again\n\n\n_WORDLIST_CACHE = {}  # type: Dict[str, Wordlist]\n\n\nclass Wordlist(tuple):\n\n    def __init__(self, words: Sequence[str]):\n        super().__init__()\n        index_from_word = {w: i for i, w in enumerate(words)}\n        self._index_from_word = MappingProxyType(index_from_word)  # no mutation\n\n    def index(self, word: str, start=None, stop=None) -> int:\n        try:\n            return self._index_from_word[word]\n        except KeyError as e:\n            raise ValueError from e\n\n    def __contains__(self, word: str) -> bool:\n        try:\n            self.index(word)\n        except ValueError:\n            return False\n        else:\n            return True\n\n    @classmethod\n    def from_file(cls, filename: str) -> 'Wordlist':\n        path = resource_path('wordlist', filename)\n        if path not in _WORDLIST_CACHE:\n            with open(path, 'r', encoding='utf-8') as f:\n                s = f.read().strip()\n            s = unicodedata.normalize('NFKD', s)\n            lines = s.split('\\n')\n            words = []\n            for line in lines:\n                line = line.split('#')[0]\n                line = line.strip(' \\r')\n                assert ' ' not in line\n                if line:\n                    words.append(line)\n\n            _WORDLIST_CACHE[path] = Wordlist(words)\n        return _WORDLIST_CACHE[path]\n\n\nfilenames = {\n    'en':'english.txt',\n    'es':'spanish.txt',\n    'ja':'japanese.txt',\n    'pt':'portuguese.txt',\n    'zh':'chinese_simplified.txt'\n}\n\n\nclass Mnemonic(Logger):\n    # Seed derivation does not follow BIP39\n    # Mnemonic phrase uses a hash based checksum, instead of a wordlist-dependent checksum\n\n    def __init__(self, lang: str = None):\n        Logger.__init__(self)\n        lang = lang or 'en'\n        self.logger.info(f'language {lang}')\n        filename = filenames.get(lang[0:2], 'english.txt')\n        self.wordlist = Wordlist.from_file(filename)\n        self.logger.info(f\"wordlist has {len(self.wordlist)} words\")\n\n    @classmethod\n    def mnemonic_to_seed(cls, mnemonic: str, *, passphrase: Optional[str]) -> bytes:\n        PBKDF2_ROUNDS = 2048\n        mnemonic = normalize_text(mnemonic)\n        passphrase = passphrase or ''\n        passphrase = normalize_text(passphrase)\n        return hashlib.pbkdf2_hmac('sha512', mnemonic.encode('utf-8'), b'electrum' + passphrase.encode('utf-8'), iterations = PBKDF2_ROUNDS)\n\n    def mnemonic_encode(self, i: int) -> str:\n        n = len(self.wordlist)\n        words = []\n        while i:\n            x = i % n\n            i = i//n\n            words.append(self.wordlist[x])\n        return ' '.join(words)\n\n    def get_suggestions(self, prefix: str) -> Iterator[str]:\n        for w in self.wordlist:\n            if w.startswith(prefix):\n                yield w\n\n    def mnemonic_decode(self, seed: str) -> int:\n        n = len(self.wordlist)\n        words = seed.split()\n        i = 0\n        while words:\n            w = words.pop()\n            k = self.wordlist.index(w)\n            i = i*n + k\n        return i\n\n    def make_seed(self, *, seed_type: str = None, num_bits: int = None) -> str:\n        from .keystore import bip39_is_checksum_valid\n        if seed_type is None:\n            seed_type = 'segwit'\n        if num_bits is None:\n            num_bits = 132\n        prefix = version.seed_prefix(seed_type)\n        # increase num_bits in order to obtain a uniform distribution for the last word\n        bpw = math.log(len(self.wordlist), 2)\n        num_bits = int(math.ceil(num_bits/bpw) * bpw)\n        self.logger.info(f\"make_seed. prefix: '{prefix}', entropy: {num_bits} bits\")\n        # generate random\n        entropy = 1\n        while entropy < pow(2, num_bits - bpw):  # try again if seed would not contain enough words\n            entropy = randrange(pow(2, num_bits))\n        # brute-force seed that has correct \"version number\"\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_old_seed(seed):\n                continue\n            # Make sure the mnemonic we generate is not also a valid bip39 seed\n            # by accident. Note that this test has not always been done historically,\n            # so it cannot be relied upon.\n            if bip39_is_checksum_valid(seed, wordlist=self.wordlist) == (True, True):\n                continue\n            if is_new_seed(seed, prefix):\n                break\n        num_words = len(seed.split())\n        self.logger.info(f'{num_words} words')\n        if (final_seed_type := calc_seed_type(seed)) != seed_type:\n            # note: I guess this can probabilistically happen for old \"2fa\" seeds that depend on the word count\n            raise Exception(f\"{final_seed_type=!r} does not match requested {seed_type=!r}. have {num_words=!r}\")\n        return seed\n\n\ndef is_new_seed(x: str, prefix=version.SEED_PREFIX) -> bool:\n    x = normalize_text(x)\n    s = hmac_oneshot(b\"Seed version\", x.encode('utf8'), hashlib.sha512).hex()\n    return s.startswith(prefix)\n\n\ndef is_old_seed(seed: str) -> bool:\n    from . import old_mnemonic\n    seed = normalize_text(seed)\n    words = seed.split()\n    try:\n        # checks here are deliberately left weak for legacy reasons, see #3149\n        old_mnemonic.mn_decode(words)\n        uses_electrum_words = True\n    except Exception:\n        uses_electrum_words = False\n    try:\n        seed = bfh(seed)\n        is_hex = (len(seed) == 16 or len(seed) == 32)\n    except Exception:\n        is_hex = False\n    return is_hex or (uses_electrum_words and (len(words) == 12 or len(words) == 24))\n\n\ndef calc_seed_type(x: str) -> str:\n    num_words = len(x.split())\n    if is_old_seed(x):\n        return 'old'\n    elif is_new_seed(x, version.SEED_PREFIX):\n        return 'standard'\n    elif is_new_seed(x, version.SEED_PREFIX_SW):\n        return 'segwit'\n    elif is_new_seed(x, version.SEED_PREFIX_2FA) and (num_words == 12 or num_words >= 20):\n        # Note: in Electrum 2.7, there was a breaking change in key derivation\n        #       for this seed type. Unfortunately the seed version/prefix was reused,\n        #       and now we can only distinguish them based on number of words. :(\n        return '2fa'\n    elif is_new_seed(x, version.SEED_PREFIX_2FA_SW):\n        return '2fa_segwit'\n    return ''\n\n\ndef can_seed_have_passphrase(seed: str) -> bool:\n    stype = calc_seed_type(seed)\n    if not stype:\n        raise Exception(f'unexpected seed type: {stype!r}')\n    if stype == 'old':\n        return False\n    if stype == '2fa':\n        # post-version-2.7 2fa seeds can have passphrase, but older ones cannot\n        num_words = len(seed.split())\n        if num_words == 12:\n            return True\n        else:\n            return False\n    # all other types can have a seed extension/passphrase\n    return True\n\n\ndef is_seed(x: str) -> bool:\n    return bool(calc_seed_type(x))\n\n\ndef is_any_2fa_seed_type(seed_type: str) -> bool:\n    return seed_type in ['2fa', '2fa_segwit']\n"
  },
  {
    "path": "electrum/mpp_split.py",
    "content": "import random\nimport math\nfrom typing import List, Tuple, Dict, NamedTuple\n\nfrom .lnutil import NoPathFound\n\nPART_PENALTY = 1.0  # 1.0 results in avoiding splits\nMIN_PART_SIZE_MSAT = 10_000_000  # we don't want to split indefinitely\nEXHAUST_DECAY_FRACTION = 10  # fraction of the local balance that should be reserved if possible\nRELATIVE_SPLIT_SPREAD = 0.3  # deviation from the mean when splitting amounts into parts\n\n# these parameters affect the computational work in the probabilistic algorithm\nCANDIDATES_PER_LEVEL = 20\nMAX_PARTS = 5  # maximum number of parts for splitting\n\n\n# maps a channel (channel_id, node_id) to the funds it has available\nChannelsFundsInfo = Dict[Tuple[bytes, bytes], Tuple[int, int]]\n\n\nclass SplitConfig(dict, Dict[Tuple[bytes, bytes], List[int]]):\n    \"\"\"maps a channel (channel_id, node_id) to a list of amounts\"\"\"\n    def number_parts(self) -> int:\n        return sum([len(v) for v in self.values() if sum(v)])\n\n    def number_nonzero_channels(self) -> int:\n        return len([v for v in self.values() if sum(v)])\n\n    def number_nonzero_nodes(self) -> int:\n        # using a set comprehension\n        return len({nodeid for (_, nodeid), amounts in self.items() if sum(amounts)})\n\n    def total_config_amount(self) -> int:\n        return sum([sum(c) for c in self.values()])\n\n    def is_any_amount_smaller_than_min_part_size(self) -> bool:\n        smaller = False\n        for amounts in self.values():\n            if any([amount < MIN_PART_SIZE_MSAT for amount in amounts]):\n                smaller |= True\n        return smaller\n\n\nclass SplitConfigRating(NamedTuple):\n    config: SplitConfig\n    rating: float\n\n\ndef split_amount_normal(total_amount: int, num_parts: int) -> List[int]:\n    \"\"\"Splits an amount into about `num_parts` parts, where the parts are split\n    randomly (normally distributed around amount/num_parts with certain spread).\"\"\"\n    parts = []\n    avg_amount = total_amount / num_parts\n    # roughly reach total_amount\n    while total_amount - sum(parts) > avg_amount:\n        amount_to_add = int(abs(random.gauss(avg_amount, RELATIVE_SPLIT_SPREAD * avg_amount)))\n        if sum(parts) + amount_to_add < total_amount:\n            parts.append(amount_to_add)\n    # add what's missing\n    parts.append(total_amount - sum(parts))\n    return parts\n\n\ndef remove_duplicates(configs: List[SplitConfig]) -> List[SplitConfig]:\n    unique_configs = set()\n    for config in configs:\n        # sort keys and values\n        config_sorted_values = {k: sorted(v) for k, v in config.items()}\n        config_sorted_keys = {k: config_sorted_values[k] for k in sorted(config_sorted_values.keys())}\n        hashable_config = tuple((c, tuple(sorted(config[c]))) for c in config_sorted_keys)\n        unique_configs.add(hashable_config)\n    unique_configs = [SplitConfig({c[0]: list(c[1]) for c in config}) for config in unique_configs]\n    return unique_configs\n\n\ndef remove_multiple_nodes(configs: List[SplitConfig]) -> List[SplitConfig]:\n    return [config for config in configs if config.number_nonzero_nodes() == 1]\n\n\ndef remove_single_part_configs(configs: List[SplitConfig]) -> List[SplitConfig]:\n    return [config for config in configs if config.number_parts() != 1]\n\n\ndef remove_single_channel_splits(configs: List[SplitConfig]) -> List[SplitConfig]:\n    return [\n        config for config in configs\n        if all(len(channel_splits) <= 1 for channel_splits in config.values())\n    ]\n\ndef rate_config(\n        config: SplitConfig,\n        channels_with_funds: ChannelsFundsInfo) -> float:\n    \"\"\"Defines an objective function to rate a configuration.\n\n    We calculate the normalized L2 norm for a configuration and\n    add a part penalty for each nonzero amount. The consequence is that\n    amounts that are equally distributed and have less parts are rated\n    lowest (best). A penalty depending on the total amount sent over a channel\n    counteracts channel exhaustion.\"\"\"\n    rating = 0\n    total_amount = config.total_config_amount()\n\n    for channel, amounts in config.items():\n        funds, slots = channels_with_funds[channel]\n        if amounts:\n            for amount in amounts:\n                rating += amount * amount / (total_amount * total_amount)  # penalty to favor equal distribution of amounts\n                rating += PART_PENALTY * PART_PENALTY  # penalty for each part\n            decay = funds / EXHAUST_DECAY_FRACTION\n            rating += math.exp((sum(amounts) - funds) / decay)  # penalty for channel exhaustion\n    return rating\n\n\ndef suggest_splits(\n        amount_msat: int,\n        channels_with_funds: ChannelsFundsInfo,\n        exclude_single_part_payments=False,\n        exclude_multinode_payments=False,\n        exclude_single_channel_splits=False\n) -> List[SplitConfigRating]:\n    \"\"\"Breaks amount_msat into smaller pieces and distributes them over the\n    channels according to the funds they can send.\n\n    Individual channels may be assigned multiple parts. The split configurations\n    are returned in sorted order, from best to worst rating.\n\n    Single part payments can be excluded, since they represent legacy payments.\n    Split configurations that send via multiple nodes can be excluded as well.\n    \"\"\"\n\n    configs = []\n    channel_keys = list(channels_with_funds.keys())\n\n    # generate multiple configurations to get more configurations (there is randomness in this loop)\n    for _ in range(CANDIDATES_PER_LEVEL):\n        # we want to have configurations with no splitting to many splittings\n        for target_parts in range(1, MAX_PARTS):\n            config = SplitConfig()\n            # randomly split amount into target_parts chunks\n            split_amounts = split_amount_normal(amount_msat, target_parts)\n            # randomly distribute amounts over channels\n            for amount in split_amounts:\n                random.shuffle(channel_keys)\n                # we check each channel and try to put the funds inside, break if we succeed\n                for c in channel_keys:\n                    if c not in config:\n                        config[c] = []\n                    channel_funds, channel_slots = channels_with_funds[c]\n                    if sum(config[c]) + amount <= channel_funds and len(config[c]) < channel_slots:\n                        config[c].append(amount)\n                        break\n                # if we don't succeed to put the amount anywhere,\n                # we try to fill up channels and put the rest somewhere else\n                else:\n                    distribute_amount = amount\n                    for c in channel_keys:\n                        channel_funds, channel_slots = channels_with_funds[c]\n                        slots_left = channel_slots - len(config[c])\n                        if slots_left == 0:\n                            # no slot left in that channel\n                            continue\n                        funds_left = channel_funds - sum(config[c])\n                        # it would be good to not fill the full channel if possible\n                        add_amount = min(funds_left, distribute_amount)\n                        config[c].append(add_amount)\n                        distribute_amount -= add_amount\n                        if distribute_amount == 0:\n                            break\n            if config.total_config_amount() != amount_msat:\n                continue\n            if target_parts > 1 and config.is_any_amount_smaller_than_min_part_size():\n                if target_parts == 2:\n                    # if there are already too small parts at the first split excluding single\n                    # part payments may return only few configurations, this will allow single part\n                    # payments for more payments, if they are too small to split\n                    exclude_single_part_payments = False\n                continue\n            assert config.total_config_amount() == amount_msat\n            configs.append(config)\n        if not configs:\n            raise NoPathFound('Cannot distribute payment over channels.')\n\n    configs = remove_duplicates(configs)\n\n    # we only take configurations that send via a single node (but there can be multiple parts)\n    if exclude_multinode_payments:\n        configs = remove_multiple_nodes(configs)\n\n    if exclude_single_part_payments:\n        configs = remove_single_part_configs(configs)\n\n    if exclude_single_channel_splits:\n        configs = remove_single_channel_splits(configs)\n\n    rated_configs = [SplitConfigRating(\n        config=c,\n        rating=rate_config(c, channels_with_funds)\n    ) for c in configs]\n    rated_configs.sort(key=lambda x: x.rating)\n\n    return rated_configs\n"
  },
  {
    "path": "electrum/network.py",
    "content": "# Electrum - Lightweight Bitcoin Client\n# Copyright (c) 2011-2016 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport asyncio\nimport time\nimport os\nimport random\nimport re\nfrom collections import defaultdict\nimport threading\nimport json\nfrom typing import (\n    NamedTuple, Optional, Sequence, List, Dict, Tuple, TYPE_CHECKING, Iterable, Set, Any, TypeVar,\n    Callable, Mapping,\n)\nimport copy\nimport functools\nfrom enum import IntEnum\nfrom contextlib import nullcontext\n\nimport aiorpcx\nfrom aiorpcx import ignore_after, NetAddress\nfrom aiohttp import ClientResponse\n\nfrom . import util\nfrom .util import (\n    log_exceptions, ignore_exceptions, OldTaskGroup, make_aiohttp_session,\n    NetworkRetryManager, error_text_str_to_safe_str, detect_tor_socks_proxy\n)\nfrom . import constants\nfrom . import blockchain\nfrom . import dns_hacks\nfrom .transaction import Transaction\nfrom .blockchain import Blockchain\nfrom .interface import (\n    Interface, PREFERRED_NETWORK_PROTOCOL, RequestTimedOut, NetworkTimeout, BUCKET_NAME_OF_ONION_SERVERS,\n    NetworkException, RequestCorrupted, ServerAddr, TxBroadcastError, KNOWN_ELEC_PROTOCOL_TRANSPORTS,\n)\nfrom .version import PROTOCOL_VERSION_MIN\nfrom .i18n import _\nfrom .logging import get_logger, Logger\nfrom .fee_policy import FeeHistogram, FeeTimeEstimates, FEE_ETA_TARGETS\n\n\nif TYPE_CHECKING:\n    from collections.abc import Coroutine\n    from .channel_db import ChannelDB\n    from .lnrouter import LNPathFinder\n    from .lnworker import LNGossip\n    from .daemon import Daemon\n    from .simple_config import SimpleConfig\n\n\n_logger = get_logger(__name__)\n\n\nNUM_TARGET_CONNECTED_SERVERS = 10\nNUM_STICKY_SERVERS = 4\nNUM_RECENT_SERVERS = 20\n\nT = TypeVar('T')\n\n\nclass ConnectionState(IntEnum):\n    DISCONNECTED  = 0\n    CONNECTING    = 1\n    CONNECTED     = 2\n\n\ndef parse_servers(result: Sequence[Tuple[str, str, List[str]]]) -> Dict[str, dict]:\n    \"\"\"Convert servers list (from protocol method \"server.peers.subscribe\") into dict format.\n    Also validate values, such as IP addresses and ports.\n    \"\"\"\n    servers = {}\n    for item in result:\n        host = item[1]\n        out = {}\n        version = None\n        pruning_level = '-'\n        if len(item) > 2:\n            for v in item[2]:\n                if re.match(r\"[st]\\d*\", v):\n                    protocol, port = v[0], v[1:]\n                    if port == '': port = constants.net.DEFAULT_PORTS[protocol]\n                    ServerAddr(host, port, protocol=protocol)  # check if raises\n                    out[protocol] = port\n                elif re.match(\"v(.?)+\", v):\n                    version = v[1:]\n                elif re.match(r\"p\\d*\", v):\n                    pruning_level = v[1:]\n                if pruning_level == '': pruning_level = '0'\n        if out:\n            out['pruning'] = pruning_level\n            out['version'] = version\n            servers[host] = out\n    return servers\n\n\ndef filter_version(servers):\n    def is_recent(version):\n        try:\n            return util.versiontuple(version) >= util.versiontuple(PROTOCOL_VERSION_MIN)\n        except Exception as e:\n            return False\n    return {k: v for k, v in servers.items() if is_recent(v.get('version'))}\n\n\ndef filter_noonion(servers):\n    return {k: v for k, v in servers.items() if not k.endswith('.onion')}\n\n\ndef filter_protocol(hostmap, *, allowed_protocols: Iterable[str] = None) -> Sequence[ServerAddr]:\n    \"\"\"Filters the hostmap for those implementing protocol.\"\"\"\n    if allowed_protocols is None:\n        allowed_protocols = {PREFERRED_NETWORK_PROTOCOL}\n    eligible = []\n    for host, portmap in hostmap.items():\n        for protocol in allowed_protocols:\n            port = portmap.get(protocol)\n            if port:\n                eligible.append(ServerAddr(host, port, protocol=protocol))\n    return eligible\n\n\ndef pick_random_server(hostmap=None, *, allowed_protocols: Iterable[str],\n                       exclude_set: Set[ServerAddr] = None) -> Optional[ServerAddr]:\n    if hostmap is None:\n        hostmap = constants.net.DEFAULT_SERVERS\n    if exclude_set is None:\n        exclude_set = set()\n    servers = set(filter_protocol(hostmap, allowed_protocols=allowed_protocols))\n    eligible = list(servers - exclude_set)\n    return random.choice(eligible) if eligible else None\n\n\ndef is_valid_port(ps: str):\n    try:\n        return 0 < int(ps) < 65535\n    except ValueError:\n        return False\n\n\ndef is_valid_host(ph: str):\n    try:\n        NetAddress(ph, '1')\n    except ValueError:\n        return False\n    return True\n\n\nclass ProxySettings:\n    MODES = ['socks4', 'socks5']\n\n    probe_fut = None\n\n    def __init__(self):\n        self.enabled = False\n        self.mode = 'socks5'\n        self.host = ''\n        self.port = ''\n        self.user = None\n        self.password = None\n\n    def set_defaults(self):\n        self.__init__()  # call __init__ for default values\n\n    def serialize_proxy_cfgstr(self):\n        return ':'.join([self.mode, self.host, self.port])\n\n    def deserialize_proxy_cfgstr(self, s: Optional[str], user: str = None, password: str = None) -> None:\n        if s is None or (isinstance(s, str) and s.lower() == 'none'):\n            self.set_defaults()\n            self.user = user\n            self.password = password\n            return\n\n        if not isinstance(s, str):\n            raise ValueError('proxy config not a string')\n\n        args = s.split(':')\n        if args[0] in ProxySettings.MODES:\n            self.mode = args[0]\n            args = args[1:]\n\n        # detect migrate from old settings\n        if len(args) == 4 and is_valid_host(args[0]) and is_valid_port(args[1]):  # host:port:user:pass,\n            self.host = args[0]\n            self.port = args[1]\n            self.user = args[2]\n            self.password = args[3]\n        else:\n            self.host = ':'.join(args[:-1])\n            self.port = args[-1]\n            self.user = user\n            self.password = password\n\n        if not is_valid_host(self.host) or not is_valid_port(self.port):\n            self.enabled = False\n\n    def to_dict(self):\n        return {\n            'enabled': self.enabled,\n            'mode': self.mode,\n            'host': self.host,\n            'port': self.port,\n            'user': self.user,\n            'password': self.password\n        }\n\n    @classmethod\n    def from_config(cls, config: 'SimpleConfig') -> 'ProxySettings':\n        proxy = ProxySettings()\n        proxy.deserialize_proxy_cfgstr(\n            config.NETWORK_PROXY, config.NETWORK_PROXY_USER, config.NETWORK_PROXY_PASSWORD\n        )\n        proxy.enabled = config.NETWORK_PROXY_ENABLED\n        return proxy\n\n    @classmethod\n    def from_dict(cls, d: dict) -> 'ProxySettings':\n        proxy = ProxySettings()\n        proxy.enabled = d.get('enabled', proxy.enabled)\n        proxy.mode = d.get('mode', proxy.mode)\n        proxy.host = d.get('host', proxy.host)\n        proxy.port = d.get('port', proxy.port)\n        proxy.user = d.get('user', proxy.user)\n        proxy.password = d.get('password', proxy.password)\n        return proxy\n\n    @classmethod\n    def probe_tor(cls, on_finished: Callable[[str | None, int | None], None]):\n        async def detect_task(finished: Callable[[str | None, int | None], None]):\n            try:\n                net_addr = await detect_tor_socks_proxy()\n                if net_addr is None:\n                    finished('', -1)\n                else:\n                    host = net_addr[0]\n                    port = net_addr[1]\n                    finished(host, port)\n            finally:\n                cls.probe_fut = None\n\n        if cls.probe_fut:  # one probe at a time\n            return\n        cls.probe_fut = asyncio.run_coroutine_threadsafe(detect_task(on_finished), util.get_asyncio_loop())\n\n    def __eq__(self, other):\n        return self.enabled == other.enabled \\\n            and self.mode == other.mode \\\n            and self.host == other.host \\\n            and self.port == other.port \\\n            and self.user == other.user \\\n            and self.password == other.password\n\n    def __str__(self):\n        return f'{self.enabled=} {self.mode=} {self.host=} {self.port=} {self.user=}'\n\n\nclass NetworkParameters(NamedTuple):\n    server: ServerAddr\n    proxy: ProxySettings\n    auto_connect: bool\n    oneserver: bool = False\n\n\nclass BestEffortRequestFailed(NetworkException): pass\n\n\nclass UntrustedServerReturnedError(NetworkException):\n    def __init__(self, *, original_exception):\n        self.original_exception = original_exception\n\n    def get_message_for_gui(self) -> str:\n        return str(self)\n\n    def get_untrusted_message(self) -> str:\n        e = self.original_exception\n        return (f\"<UntrustedServerReturnedError \"\n                f\"[DO NOT TRUST THIS MESSAGE] original_exception: {error_text_str_to_safe_str(repr(e))}>\")\n\n    def __str__(self):\n        # We should not show the untrusted text from self.original_exception,\n        # to avoid accidentally showing it in the GUI.\n        return _(\"The server returned an error.\")\n\n    def __repr__(self):\n        # We should not show the untrusted text from self.original_exception,\n        # to avoid accidentally showing it in the GUI.\n        return f\"<UntrustedServerReturnedError {str(self)!r}>\"\n\n\n_INSTANCE = None\n\n\nclass Network(Logger, NetworkRetryManager[ServerAddr]):\n    \"\"\"The Network class manages a set of connections to remote electrum\n    servers, each connected socket is handled by an Interface() object.\n    \"\"\"\n\n    taskgroup: Optional[OldTaskGroup]\n    interface: Optional[Interface]\n    interfaces: Dict[ServerAddr, Interface]\n    _connecting_ifaces: Set[ServerAddr]\n    _closing_ifaces: Set[ServerAddr]\n    default_server: ServerAddr\n    _recent_servers: List[ServerAddr]\n\n    channel_db: Optional['ChannelDB'] = None\n    lngossip: Optional['LNGossip'] = None\n    path_finder: Optional['LNPathFinder'] = None\n\n    def __init__(self, config: 'SimpleConfig', *, daemon: 'Daemon' = None):\n        global _INSTANCE\n        assert _INSTANCE is None, \"Network is a singleton!\"\n        _INSTANCE = self\n\n        Logger.__init__(self)\n        NetworkRetryManager.__init__(\n            self,\n            max_retry_delay_normal=600,\n            init_retry_delay_normal=15,\n            max_retry_delay_urgent=10,\n            init_retry_delay_urgent=1,\n        )\n\n        self.asyncio_loop = util.get_asyncio_loop()\n        assert self.asyncio_loop.is_running(), \"event loop not running\"\n\n        self.config = config\n        self.daemon = daemon\n\n        blockchain.read_blockchains(self.config)\n        blockchain.init_headers_file_for_best_chain()\n        self.logger.info(f\"blockchains {list(map(lambda b: b.forkpoint, blockchain.blockchains.values()))}\")\n        self._blockchain_preferred_block = self.config.BLOCKCHAIN_PREFERRED_BLOCK  # type: Dict[str, Any]\n        if self._blockchain_preferred_block is None:\n            self._set_preferred_chain(None)\n        self._blockchain = blockchain.get_best_chain()\n\n        self._allowed_protocols = {PREFERRED_NETWORK_PROTOCOL}\n\n        self.proxy = ProxySettings()\n        self.is_proxy_tor = None  # type: Optional[bool]  # tri-state. None means unknown.\n        self._init_parameters_from_config()\n\n        self.taskgroup = None\n\n        # locks\n        self.restart_lock = asyncio.Lock()\n        self.bhi_lock = asyncio.Lock()\n        self.recent_servers_lock = threading.RLock()       # <- re-entrant\n        self.interfaces_lock = threading.Lock()            # for mutating/iterating self.interfaces\n\n        self.server_peers = {}  # returned by interface (servers that the main interface knows about)\n        self._recent_servers = self._read_recent_servers()  # note: needs self.recent_servers_lock\n\n        self.banner = ''\n        self.donation_address = ''\n        self.relay_fee = None  # type: Optional[int]\n\n        dir_path = os.path.join(self.config.path, 'certs')\n        util.make_dir(dir_path)\n\n        # the main server we are currently communicating with\n        self.interface = None\n        self.default_server_changed_event = asyncio.Event()\n        # Set of servers we have an ongoing connection with.\n        # For any ServerAddr, at most one corresponding Interface object\n        # can exist at any given time. Depending on the state of that Interface,\n        # the ServerAddr can be found in one of the following sets.\n        # Note: during a transition, the ServerAddr can appear in two sets momentarily.\n        self._connecting_ifaces = set()\n        self.interfaces = {}  # these are the ifaces in \"initialised and usable\" state\n        self._closing_ifaces = set()\n\n        # Dump network messages (all interfaces).  Set at runtime from the console.\n        self.debug = False\n\n        self._set_status(ConnectionState.DISCONNECTED)\n        self._has_ever_managed_to_connect_to_server = False\n        self._was_started = False\n\n        self.mempool_fees = FeeHistogram()\n        self.fee_estimates = FeeTimeEstimates()\n        self.last_time_fee_estimates_requested = 0  # zero ensures immediate fees\n\n    def has_internet_connection(self) -> bool:\n        \"\"\"Our guess whether the device has Internet-connectivity.\"\"\"\n        return self._has_ever_managed_to_connect_to_server\n\n    def has_channel_db(self):\n        return self.channel_db is not None\n\n    def start_gossip(self):\n        from . import lnrouter\n        from . import channel_db\n        from . import lnworker\n        if not self.config.LIGHTNING_USE_GOSSIP:\n            return\n        if self.lngossip is None:\n            self.channel_db = channel_db.ChannelDB(self)\n            self.path_finder = lnrouter.LNPathFinder(self.channel_db)\n            self.channel_db.load_data()\n            self.lngossip = lnworker.LNGossip(self.config)\n            self.lngossip.start_network(self)\n\n    async def stop_gossip(self, *, full_shutdown: bool = False):\n        if self.lngossip:\n            await self.lngossip.stop()\n            self.lngossip = None\n            self.channel_db.stop()\n            if full_shutdown:\n                await self.channel_db.stopped_event.wait()\n            self.channel_db = None\n            self.path_finder = None\n\n    @classmethod\n    def run_from_another_thread(cls, coro: 'Coroutine[Any, Any, T]', *, timeout=None) -> T:\n        loop = util.get_asyncio_loop()\n        assert util.get_running_loop() != loop, 'must not be called from asyncio thread'\n        fut = asyncio.run_coroutine_threadsafe(coro, loop)\n        return fut.result(timeout)\n\n    @staticmethod\n    def get_instance() -> Optional[\"Network\"]:\n        \"\"\"Return the global singleton network instance.\n        Note that this can return None! If we are run with the --offline flag, there is no network.\n        \"\"\"\n        return _INSTANCE\n\n    def with_recent_servers_lock(func):\n        def func_wrapper(self, *args, **kwargs):\n            with self.recent_servers_lock:\n                return func(self, *args, **kwargs)\n        return func_wrapper\n\n    def _read_recent_servers(self) -> List[ServerAddr]:\n        if not self.config.path:\n            return []\n        path = os.path.join(self.config.path, \"recent_servers\")\n        try:\n            with open(path, \"r\", encoding='utf-8') as f:\n                data = f.read()\n                servers_list = json.loads(data)\n            return [ServerAddr.from_str(s) for s in servers_list]\n        except Exception:\n            return []\n\n    @with_recent_servers_lock\n    def _save_recent_servers(self):\n        if not self.config.path:\n            return\n        path = os.path.join(self.config.path, \"recent_servers\")\n        s = json.dumps(self._recent_servers, indent=4, sort_keys=True, default=str)\n        try:\n            with open(path, \"w\", encoding='utf-8') as f:\n                f.write(s)\n        except Exception:\n            pass\n\n    async def _server_is_lagging(self) -> bool:\n        sh = self.get_server_height()\n        if not sh:\n            self.logger.info('no height for main interface')\n            return True\n        lh = self.get_local_height()\n        result = (lh - sh) > 1\n        if result:\n            self.logger.info(f'{self.default_server} is lagging ({sh} vs {lh})')\n        return result\n\n    def _set_status(self, status):\n        self.connection_status = status\n        util.trigger_callback('status')\n\n    def is_connected(self):\n        interface = self.interface\n        return interface is not None and interface.is_connected_and_ready()\n\n    def is_connecting(self):\n        return self.connection_status == ConnectionState.CONNECTING\n\n    def get_connection_status_for_GUI(self):\n        ConnectionStates = {\n            ConnectionState.DISCONNECTED: _('Disconnected'),\n            ConnectionState.CONNECTING: _('Connecting'),\n            ConnectionState.CONNECTED: _('Connected'),\n        }\n        return ConnectionStates[self.connection_status]\n\n    async def _request_server_info(self, interface: 'Interface'):\n        await interface.ready\n        session = interface.session\n\n        async def get_banner():\n            self.banner = await interface.get_server_banner()\n            util.trigger_callback('banner', self.banner)\n\n        async def get_donation_address():\n            self.donation_address = await interface.get_donation_address()\n\n        async def get_server_peers():\n            server_peers = await session.send_request('server.peers.subscribe')\n            random.shuffle(server_peers)\n            max_accepted_peers = len(constants.net.DEFAULT_SERVERS) + NUM_RECENT_SERVERS\n            server_peers = server_peers[:max_accepted_peers]\n            # note that 'parse_servers' also validates the data (which is untrusted input!)\n            self.server_peers = parse_servers(server_peers)\n            util.trigger_callback('servers', self.get_servers())\n\n        async def get_relay_fee():\n            self.relay_fee = await interface.get_relay_fee()\n\n        async with OldTaskGroup() as group:\n            await group.spawn(get_banner)\n            await group.spawn(get_donation_address)\n            await group.spawn(get_server_peers)\n            await group.spawn(get_relay_fee)\n            await group.spawn(self._request_fee_estimates(interface))\n\n    async def _request_fee_estimates(self, interface):\n        self.requested_fee_estimates()\n        histogram = await interface.get_fee_histogram()\n        self.mempool_fees.set_data(histogram)\n        self.logger.info(f'fee_histogram {len(histogram)}')\n        util.trigger_callback('fee_histogram', self.mempool_fees)\n\n    def is_fee_estimates_update_required(self):\n        \"\"\"Checks time since last requested and updated fee estimates.\n        Returns True if an update should be requested.\n        \"\"\"\n        now = time.time()\n        return now - self.last_time_fee_estimates_requested > 60\n\n    def has_fee_etas(self):\n        return self.fee_estimates.has_data()\n\n    def has_fee_mempool(self) -> bool:\n        return self.mempool_fees.has_data()\n\n    def requested_fee_estimates(self):\n        self.last_time_fee_estimates_requested = time.time()\n\n    def get_parameters(self) -> NetworkParameters:\n        return NetworkParameters(server=self.default_server,\n                                 proxy=self.proxy,\n                                 auto_connect=self.auto_connect,\n                                 oneserver=self.oneserver)\n\n    def _init_parameters_from_config(self) -> None:\n        dns_hacks.configure_dns_resolver()\n        self.auto_connect = self.config.NETWORK_AUTO_CONNECT\n        if self.auto_connect and self.config.NETWORK_ONESERVER:\n            # enabling both oneserver and auto_connect doesn't really make sense\n            # assume oneserver is enabled for privacy reasons, disable auto_connect and assume server is unpredictable\n            self.logger.warning(f'both \"oneserver\" and \"auto_connect\" options enabled, disabling \"auto_connect\" and resetting \"server\".')\n            self.config.NETWORK_SERVER = \"\"  # let _set_default_server set harmless default (localhost)\n            self.auto_connect = False\n\n        self._set_default_server()\n        self._set_proxy(ProxySettings.from_config(self.config))\n        self._maybe_set_oneserver()\n\n    def get_donation_address(self):\n        if self.is_connected():\n            return self.donation_address\n\n    def get_interfaces(self) -> List[ServerAddr]:\n        \"\"\"The list of servers for the connected interfaces.\"\"\"\n        with self.interfaces_lock:\n            return list(self.interfaces)\n\n    def get_status(self):\n        n = len(self.get_interfaces())\n        return _(\"Connected to {0} nodes.\").format(n) if n > 1 else _(\"Connected to {0} node.\").format(n) if n == 1 else _(\"Not connected\")\n\n    def get_fee_estimates(self):\n        from statistics import median\n        if self.auto_connect:\n            with self.interfaces_lock:\n                out = {}\n                for n in FEE_ETA_TARGETS[0:-1]:\n                    try:\n                        out[n] = int(median(filter(None, [i.fee_estimates_eta.get(n) for i in self.interfaces.values()])))\n                    except Exception:\n                        continue\n                return out\n        else:\n            if not self.interface:\n                return {}\n            return self.interface.fee_estimates_eta\n\n    def update_fee_estimates(self, *, fee_est: Dict[int, int] = None):\n        if fee_est is None:\n            if self.config.TEST_DISABLE_AUTOMATIC_FEE_ETA_UPDATE:\n                return\n            fee_est = self.get_fee_estimates()\n        for nblock_target, fee in fee_est.items():\n            self.fee_estimates.set_data(nblock_target, fee)\n        if not hasattr(self, \"_prev_fee_est\") or self._prev_fee_est != fee_est:\n            self._prev_fee_est = copy.copy(fee_est)\n            self.logger.info(f'fee_estimates {fee_est}')\n        util.trigger_callback('fee', self.fee_estimates)\n\n\n    @with_recent_servers_lock\n    def get_servers(self) -> Mapping[str, Mapping[str, str]]:\n        # note: order of sources when adding servers here is crucial!\n        # don't let \"server_peers\" overwrite anything,\n        # otherwise main server can eclipse the client\n        out = dict()\n        # add servers received from main interface\n        server_peers = self.server_peers\n        if server_peers:\n            out.update(filter_version(server_peers.copy()))\n        # hardcoded servers\n        out.update(constants.net.DEFAULT_SERVERS)\n        # add recent servers\n        for server in self._recent_servers:\n            port = str(server.port)\n            if server.host in out:\n                out[server.host].update({server.protocol: port})\n            else:\n                out[server.host] = {server.protocol: port}\n        # add bookmarks\n        bookmarks = self.config.NETWORK_BOOKMARKED_SERVERS or []\n        for server_str in bookmarks:\n            try:\n                server = ServerAddr.from_str(server_str)\n            except ValueError:\n                continue\n            port = str(server.port)\n            if server.host in out:\n                out[server.host].update({server.protocol: port})\n            else:\n                out[server.host] = {server.protocol: port}\n        # potentially filter out some\n        if self.config.NETWORK_NOONION:\n            out = filter_noonion(out)\n        return out\n\n    def get_disconnected_server_addrs(self) -> Sequence[ServerAddr]:\n        hostmap = self.get_servers()\n        disconnected_server_addrs = []  # type: List[ServerAddr]\n        chains = self.get_blockchains()\n        connected_hosts = set([iface.host for ifaces in chains.values() for iface in ifaces])\n        # convert hostmap to list of ServerAddrs (one-to-many mapping)\n        server_addrs = []\n        for host, portmap in hostmap.items():\n            for protocol in KNOWN_ELEC_PROTOCOL_TRANSPORTS:\n                if port := portmap.get(protocol):\n                    server_addrs.append(ServerAddr(host, port, protocol=protocol))\n        # sort bookmarked servers to appear first\n        server_addrs.sort(key=lambda x: (-self.is_server_bookmarked(x), str(x)))\n        # filter out stuff\n        for server in server_addrs:\n            if server.host in connected_hosts:\n                continue\n            if not self.is_server_bookmarked(server):\n                if server.protocol != PREFERRED_NETWORK_PROTOCOL:\n                    continue\n                if server.host.endswith('.onion') and not self.is_proxy_tor:\n                    continue\n            disconnected_server_addrs.append(server)\n        return disconnected_server_addrs\n\n    def _get_next_server_to_try(self) -> Optional[ServerAddr]:\n        now = time.time()\n        with self.interfaces_lock:\n            connected_servers = set(self.interfaces) | self._connecting_ifaces | self._closing_ifaces\n        # First try from recent servers. (which are persisted)\n        # As these are servers we successfully connected to recently, they are\n        # most likely to work. This also makes servers \"sticky\".\n        # Note: with sticky servers, it is more difficult for an attacker to eclipse the client,\n        #       however if they succeed, the eclipsing would persist. To try to balance this,\n        #       we only give priority to recent_servers up to NUM_STICKY_SERVERS.\n        with self.recent_servers_lock:\n            recent_servers = list(self._recent_servers)\n        recent_servers = [s for s in recent_servers if s.protocol in self._allowed_protocols]\n        if len(connected_servers & set(recent_servers)) < NUM_STICKY_SERVERS:\n            for server in recent_servers:\n                if server in connected_servers:\n                    continue\n                if not self._can_retry_addr(server, now=now):\n                    continue\n                return server\n        # try all servers we know about, pick one at random\n        hostmap = self.get_servers()\n        servers = list(set(filter_protocol(hostmap, allowed_protocols=self._allowed_protocols)) - connected_servers)\n        random.shuffle(servers)\n        for server in servers:\n            if not self._can_retry_addr(server, now=now):\n                continue\n            return server\n        return None\n\n    def _set_default_server(self) -> None:\n        # Server for addresses and transactions\n        server = self.config.NETWORK_SERVER\n        # Sanitize default server\n        if server:\n            try:\n                self.default_server = ServerAddr.from_str(server)\n            except Exception:\n                self.logger.warning(f'failed to parse server-string ({server!r}); falling back to localhost:1:s.')\n                self.default_server = ServerAddr.from_str(\"localhost:1:s\")\n        else:\n            # if oneserver is enabled but no server specified then don't pick a random server\n            if self.config.NETWORK_ONESERVER:\n                self.logger.warning(f'\"oneserver\" option enabled, but no \"server\" defined; falling back to localhost:1:s.')\n                self.default_server = ServerAddr.from_str(\"localhost:1:s\")\n            else:\n                self.default_server = pick_random_server(allowed_protocols=self._allowed_protocols)\n        assert isinstance(self.default_server, ServerAddr), f\"invalid type for default_server: {self.default_server!r}\"\n\n    def _set_proxy(self, proxy: ProxySettings):\n        if self.proxy == proxy:\n            return\n\n        self.logger.info(f'setting proxy {proxy}')\n        self.proxy = proxy\n\n        # reset is_proxy_tor to unknown, and re-detect it:\n        self.is_proxy_tor = None\n        self._detect_if_proxy_is_tor()\n\n        util.trigger_callback('proxy_set', self.proxy)\n\n    def _detect_if_proxy_is_tor(self) -> None:\n        async def tor_probe_task(p):\n            assert p is not None\n            is_tor = await util.is_tor_socks_port(p.host, int(p.port))\n            if self.proxy == p:  # is this the proxy we probed?\n                if self.is_proxy_tor != is_tor:\n                    self.logger.info(f'Proxy is {\"\" if is_tor else \"not \"}TOR')\n                    self.is_proxy_tor = is_tor\n                util.trigger_callback('tor_probed', is_tor)\n\n        proxy = self.proxy\n        if proxy and proxy.enabled and proxy.mode == 'socks5':\n            asyncio.run_coroutine_threadsafe(tor_probe_task(proxy), self.asyncio_loop)\n\n    @log_exceptions\n    async def set_parameters(self, net_params: NetworkParameters):\n        proxy = net_params.proxy\n        proxy_str = proxy.serialize_proxy_cfgstr()\n        proxy_enabled = proxy.enabled\n        proxy_user = proxy.user\n        proxy_pass = proxy.password\n        server = net_params.server\n        # sanitize parameters\n        try:\n            if proxy:\n                # proxy_modes.index(proxy['mode']) + 1\n                ProxySettings.MODES.index(proxy.mode) + 1\n                # int(proxy['port'])\n                int(proxy.port)\n        except Exception:\n            proxy.enabled = False\n            # return\n        self.config.NETWORK_AUTO_CONNECT = net_params.auto_connect\n        self.config.NETWORK_ONESERVER = net_params.oneserver\n        self.config.NETWORK_PROXY_ENABLED = proxy_enabled\n        self.config.NETWORK_PROXY = proxy_str\n        self.config.NETWORK_PROXY_USER = proxy_user\n        self.config.NETWORK_PROXY_PASSWORD = proxy_pass\n        self.config.NETWORK_SERVER = str(server)\n        # abort if changes were not allowed by config\n        if self.config.NETWORK_SERVER != str(server) \\\n                or self.config.NETWORK_PROXY_ENABLED != proxy_enabled \\\n                or self.config.NETWORK_PROXY != proxy_str \\\n                or self.config.NETWORK_PROXY_USER != proxy_user \\\n                or self.config.NETWORK_PROXY_PASSWORD != proxy_pass \\\n                or self.config.NETWORK_ONESERVER != net_params.oneserver:\n            return\n\n        proxy_changed = self.proxy != proxy\n        oneserver_changed = self.oneserver != net_params.oneserver\n        default_server_changed = self.default_server != server\n        self._init_parameters_from_config()\n        if not self._was_started:\n            return\n\n        async with self.restart_lock:\n            if proxy_changed or oneserver_changed:\n                # Restart the network\n                await self.stop(full_shutdown=False)\n                await self._start()\n            elif default_server_changed:\n                await self.switch_to_interface(server)\n            else:\n                await self.switch_lagging_interface()\n        util.trigger_callback('network_updated')\n\n    def _maybe_set_oneserver(self) -> None:\n        oneserver = self.config.NETWORK_ONESERVER\n        self.oneserver = oneserver\n        self.num_server = NUM_TARGET_CONNECTED_SERVERS if not oneserver else 0\n\n    def is_server_bookmarked(self, server: ServerAddr) -> bool:\n        bookmarks = self.config.NETWORK_BOOKMARKED_SERVERS or []\n        return str(server) in bookmarks\n\n    def set_server_bookmark(self, server: ServerAddr, *, add: bool) -> None:\n        server_str = str(server)\n        with self.config.lock:\n            bookmarks = self.config.NETWORK_BOOKMARKED_SERVERS or []\n            if add:\n                if server_str not in bookmarks:\n                    bookmarks.append(server_str)\n            else:  # remove\n                if server_str in bookmarks:\n                    bookmarks.remove(server_str)\n            self.config.NETWORK_BOOKMARKED_SERVERS = bookmarks\n\n    async def _switch_to_random_interface(self):\n        '''Switch to a random connected server other than the current one'''\n        servers = self.get_interfaces()    # Those in connected state\n        if self.default_server in servers:\n            servers.remove(self.default_server)\n        if servers:\n            await self.switch_to_interface(random.choice(servers))\n\n    async def switch_lagging_interface(self):\n        \"\"\"If auto_connect and lagging, switch interface (only within fork).\"\"\"\n        if self.auto_connect and await self._server_is_lagging():\n            # switch to one that has the correct header (not height)\n            best_header = self.blockchain().header_at_tip()\n            with self.interfaces_lock: interfaces = list(self.interfaces.values())\n            filtered = list(filter(lambda iface: iface.tip_header == best_header, interfaces))\n            if filtered:\n                chosen_iface = random.choice(filtered)\n                await self.switch_to_interface(chosen_iface.server)\n\n    async def switch_unwanted_fork_interface(self) -> None:\n        \"\"\"If auto_connect, maybe switch to another fork/chain.\"\"\"\n        if not self.auto_connect or not self.interface:\n            return\n        with self.interfaces_lock: interfaces = list(self.interfaces.values())\n        pref_height = self._blockchain_preferred_block['height']\n        pref_hash   = self._blockchain_preferred_block['hash']\n        # shortcut for common case\n        if pref_height == 0:\n            return\n        # maybe try switching chains; starting with most desirable first\n        matching_chains = blockchain.get_chains_that_contain_header(pref_height, pref_hash)\n        chains_to_try = list(matching_chains) + [blockchain.get_best_chain()]\n        for rank, chain in enumerate(chains_to_try):\n            # check if main interface is already on this fork\n            if self.interface.blockchain == chain:\n                return\n            # switch to another random interface that is on this fork, if any\n            filtered = [iface for iface in interfaces\n                        if iface.blockchain == chain]\n            if filtered:\n                self.logger.info(f\"switching to (more) preferred fork (rank {rank})\")\n                chosen_iface = random.choice(filtered)\n                await self.switch_to_interface(chosen_iface.server)\n                return\n        self.logger.info(\"tried to switch to (more) preferred fork but no interfaces are on any\")\n\n    async def switch_to_interface(self, server: ServerAddr):\n        \"\"\"Switch to server as our main interface. If no connection exists,\n        queue interface to be started. The actual switch will\n        happen when the interface becomes ready.\n        \"\"\"\n        assert isinstance(server, ServerAddr), f\"expected ServerAddr, got {type(server)}\"\n        self.default_server = server\n        old_interface = self.interface\n        old_server = old_interface.server if old_interface else None\n\n        # Stop any current interface in order to terminate subscriptions,\n        # and to cancel tasks in interface.taskgroup.\n        if old_server and old_server != server:\n            # don't wait for old_interface to close as that might be slow:\n            await self.taskgroup.spawn(self._close_interface(old_interface))\n\n        if server not in self.interfaces:\n            self.interface = None\n            await self.taskgroup.spawn(self._run_new_interface(server))\n            return\n\n        i = self.interfaces[server]\n        if old_interface != i:\n            if not i.is_connected_and_ready():\n                return\n            self.logger.info(f\"switching to {server}\")\n            blockchain_updated = i.blockchain != self.blockchain()\n            self.interface = i\n            try:\n                await i.taskgroup.spawn(self._request_server_info(i))\n            except RuntimeError as e:  # see #7677\n                if len(e.args) >= 1 and e.args[0] == 'task group terminated':\n                    self.logger.warning(f\"tried to switch to {server} but interface.taskgroup is already dead.\")\n                    self.interface = None\n                    return\n                raise\n            util.trigger_callback('default_server_changed')\n            self.default_server_changed_event.set()\n            self.default_server_changed_event.clear()\n            self._set_status(ConnectionState.CONNECTED)\n            util.trigger_callback('network_updated')\n            if blockchain_updated:\n                util.trigger_callback('blockchain_updated')\n\n    async def _close_interface(self, interface: Optional[Interface]):\n        if not interface:\n            return\n        if interface.server in self._closing_ifaces:\n            return\n        self._closing_ifaces.add(interface.server)\n        with self.interfaces_lock:\n            if self.interfaces.get(interface.server) == interface:\n                self.interfaces.pop(interface.server)\n        if interface == self.interface:\n            self.interface = None\n        try:\n            # this can take some time if server/connection is slow:\n            await interface.close()\n            await interface.got_disconnected.wait()\n        finally:\n            self._closing_ifaces.discard(interface.server)\n\n    @with_recent_servers_lock\n    def _add_recent_server(self, server: ServerAddr) -> None:\n        self._on_connection_successfully_established(server)\n        # list is ordered\n        if server in self._recent_servers:\n            self._recent_servers.remove(server)\n        self._recent_servers.insert(0, server)\n        self._recent_servers = self._recent_servers[:NUM_RECENT_SERVERS]\n        self._save_recent_servers()\n\n    async def connection_down(self, interface: Interface):\n        '''A connection to server either went down, or was never made.\n        We distinguish by whether it is in self.interfaces.'''\n        if not interface: return\n        if interface.server == self.default_server:\n            self._set_status(ConnectionState.DISCONNECTED)\n        await self._close_interface(interface)\n        util.trigger_callback('network_updated')\n\n    def get_network_timeout_seconds(self, request_type=NetworkTimeout.Generic) -> int:\n        if self.config.NETWORK_TIMEOUT:\n            return self.config.NETWORK_TIMEOUT\n        if self.oneserver and not self.auto_connect:\n            return request_type.MOST_RELAXED\n        if self.proxy and self.proxy.enabled:\n            return request_type.RELAXED\n        return request_type.NORMAL\n\n    @ignore_exceptions  # do not kill outer taskgroup\n    @log_exceptions\n    async def _run_new_interface(self, server: ServerAddr):\n        assert isinstance(server, ServerAddr), f\"expected ServerAddr, got {type(server)}\"\n        if (server in self.interfaces\n                or server in self._connecting_ifaces\n                or server in self._closing_ifaces):\n            return\n        self._connecting_ifaces.add(server)\n        if server == self.default_server:\n            self.logger.info(f\"connecting to {server} as new interface\")\n            self._set_status(ConnectionState.CONNECTING)\n        self._trying_addr_now(server)\n\n        interface = Interface(network=self, server=server)\n        # note: using longer timeouts here as DNS can sometimes be slow!\n        timeout = self.get_network_timeout_seconds(NetworkTimeout.Generic)\n        try:\n            await util.wait_for2(interface.ready, timeout)\n        except BaseException as e:\n            self.logger.info(f\"couldn't launch iface {server} -- {repr(e)}\")\n            await interface.close()\n            return\n        else:\n            with self.interfaces_lock:\n                assert server not in self.interfaces\n                self.interfaces[server] = interface\n        finally:\n            self._connecting_ifaces.discard(server)\n\n        if server == self.default_server:\n            await self.switch_to_interface(server)\n\n        self._has_ever_managed_to_connect_to_server = True\n        self._add_recent_server(server)\n        util.trigger_callback('network_updated')\n        # When the proxy settings were set, the proxy (if any) might have been unreachable,\n        # resulting in a false-negative for Tor-detection. Given we just connected to a server, re-test now.\n        self._detect_if_proxy_is_tor()\n\n    def check_interface_against_healthy_spread_of_connected_servers(self, iface_to_check: Interface) -> bool:\n        # main interface is exempt. this makes switching servers easier\n        if iface_to_check.is_main_server():\n            return True\n        if not iface_to_check.bucket_based_on_ipaddress():\n            return True\n        # bucket connected interfaces\n        with self.interfaces_lock:\n            interfaces = list(self.interfaces.values())\n        if iface_to_check in interfaces:\n            interfaces.remove(iface_to_check)\n        buckets = defaultdict(list)\n        for iface in interfaces:\n            buckets[iface.bucket_based_on_ipaddress()].append(iface)\n        # check proposed server against buckets\n        onion_servers = buckets[BUCKET_NAME_OF_ONION_SERVERS]\n        if iface_to_check.is_tor():\n            # keep number of onion servers below half of all connected servers\n            if len(onion_servers) > NUM_TARGET_CONNECTED_SERVERS // 2:\n                return False\n        else:\n            bucket = iface_to_check.bucket_based_on_ipaddress()\n            if len(buckets[bucket]) > 0:\n                return False\n        return True\n\n    def best_effort_reliable(func):\n        @functools.wraps(func)\n        async def make_reliable_wrapper(self: 'Network', *args, **kwargs):\n            for i in range(10):\n                iface = self.interface\n                # retry until there is a main interface\n                if not iface:\n                    async with ignore_after(1):\n                        await self.default_server_changed_event.wait()\n                    continue  # try again\n                assert iface.ready.done(), \"interface not ready yet\"\n                # try actual request\n                try:\n                    async with OldTaskGroup(wait=any) as group:\n                        task = await group.spawn(func(self, *args, **kwargs))\n                        await group.spawn(iface.got_disconnected.wait())\n                except RequestTimedOut:\n                    await iface.close()\n                    await iface.got_disconnected.wait()\n                    continue  # try again\n                except RequestCorrupted as e:\n                    # TODO ban server?\n                    iface.logger.exception(f\"RequestCorrupted: {e}\")\n                    await iface.close()\n                    await iface.got_disconnected.wait()\n                    continue  # try again\n                if task.done() and not task.cancelled():\n                    return task.result()\n                # otherwise; try again\n            raise BestEffortRequestFailed('cannot establish a connection... gave up.')\n        return make_reliable_wrapper\n\n    def catch_server_exceptions(func):\n        \"\"\"Decorator that wraps server errors in UntrustedServerReturnedError,\n        to avoid showing untrusted arbitrary text to users.\n        \"\"\"\n        @functools.wraps(func)\n        async def wrapper(self, *args, **kwargs):\n            try:\n                return await func(self, *args, **kwargs)\n            except aiorpcx.jsonrpc.CodeMessageError as e:\n                wrapped_exc = UntrustedServerReturnedError(original_exception=e)\n                # log (sanitized) untrusted error text now, to ease debugging\n                self.logger.debug(f\"got error from server for {func.__qualname__}: {wrapped_exc.get_untrusted_message()!r}\")\n                raise wrapped_exc from e\n        return wrapper\n\n    @best_effort_reliable\n    @catch_server_exceptions\n    async def get_merkle_for_transaction(self, tx_hash: str, tx_height: int) -> dict:\n        if self.interface is None:  # handled by best_effort_reliable\n            raise RequestTimedOut()\n        return await self.interface.get_merkle_for_transaction(tx_hash=tx_hash, tx_height=tx_height)\n\n    @best_effort_reliable\n    async def broadcast_transaction(self, tx: 'Transaction', *, timeout=None) -> None:\n        \"\"\"caller should handle TxBroadcastError\"\"\"\n        if self.interface is None:  # handled by best_effort_reliable\n            raise RequestTimedOut()\n        await self.interface.broadcast_transaction(tx, timeout=timeout)\n\n    async def try_broadcasting(self, tx: 'Transaction', name: str) -> bool:\n        try:\n            await self.broadcast_transaction(tx)\n        except Exception as e:\n            self.logger.info(f'error: could not broadcast {name} {tx.txid()}, {str(e)}')\n            return False\n        else:\n            self.logger.info(f'success: broadcasting {name} {tx.txid()}')\n            return True\n\n    @best_effort_reliable\n    @catch_server_exceptions\n    async def get_transaction(self, tx_hash: str, *, timeout=None) -> str:\n        if self.interface is None:  # handled by best_effort_reliable\n            raise RequestTimedOut()\n        return await self.interface.get_transaction(tx_hash=tx_hash, timeout=timeout)\n\n    @best_effort_reliable\n    @catch_server_exceptions\n    async def get_history_for_scripthash(self, sh: str) -> List[dict]:\n        if self.interface is None:  # handled by best_effort_reliable\n            raise RequestTimedOut()\n        return await self.interface.get_history_for_scripthash(sh)\n\n    @best_effort_reliable\n    @catch_server_exceptions\n    async def listunspent_for_scripthash(self, sh: str) -> List[dict]:\n        if self.interface is None:  # handled by best_effort_reliable\n            raise RequestTimedOut()\n        return await self.interface.listunspent_for_scripthash(sh)\n\n    @best_effort_reliable\n    @catch_server_exceptions\n    async def get_balance_for_scripthash(self, sh: str) -> dict:\n        if self.interface is None:  # handled by best_effort_reliable\n            raise RequestTimedOut()\n        return await self.interface.get_balance_for_scripthash(sh)\n\n    @best_effort_reliable\n    @catch_server_exceptions\n    async def get_txid_from_txpos(self, tx_height, tx_pos, merkle):\n        if self.interface is None:  # handled by best_effort_reliable\n            raise RequestTimedOut()\n        return await self.interface.get_txid_from_txpos(tx_height, tx_pos, merkle)\n\n    def blockchain(self) -> Blockchain:\n        interface = self.interface\n        if interface and interface.blockchain is not None:\n            self._blockchain = interface.blockchain\n        return self._blockchain\n\n    def get_blockchains(self) -> Mapping[str, Sequence[Interface]]:\n        out = {}  # blockchain_id -> list(interfaces)\n        with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items())\n        with self.interfaces_lock: interfaces_values = list(self.interfaces.values())\n        for chain_id, bc in blockchain_items:\n            r = list(filter(lambda i: i.blockchain==bc, interfaces_values))\n            if r:\n                out[chain_id] = r\n        return out\n\n    def _set_preferred_chain(self, chain: Optional[Blockchain]):\n        if chain:\n            height = chain.get_max_forkpoint()\n            header_hash = chain.get_hash(height)\n        else:\n            height = 0\n            header_hash = constants.net.GENESIS\n        self._blockchain_preferred_block = {\n            'height': height,\n            'hash': header_hash,\n        }\n        self.config.BLOCKCHAIN_PREFERRED_BLOCK = self._blockchain_preferred_block\n\n    async def follow_chain_given_id(self, chain_id: str) -> None:\n        bc = blockchain.blockchains.get(chain_id)\n        if not bc:\n            raise Exception('blockchain {} not found'.format(chain_id))\n        self._set_preferred_chain(bc)\n        # select server on this chain\n        with self.interfaces_lock: interfaces = list(self.interfaces.values())\n        interfaces_on_selected_chain = list(filter(lambda iface: iface.blockchain == bc, interfaces))\n        if len(interfaces_on_selected_chain) == 0: return\n        chosen_iface = random.choice(interfaces_on_selected_chain)  # type: Interface\n        # switch to server (and save to config)\n        net_params = self.get_parameters()\n        # we select a random interface, so set connection mode back to autoconnect\n        net_params = net_params._replace(server=chosen_iface.server, auto_connect=True, oneserver=False)\n        await self.set_parameters(net_params)\n\n    def follow_chain_given_server(self, server: ServerAddr) -> None:\n        # note that server_str should correspond to a connected interface\n        iface = self.interfaces[server]\n        self._set_preferred_chain(iface.blockchain)\n        self.logger.debug(f\"following {self.config.BLOCKCHAIN_PREFERRED_BLOCK=}\")\n\n    def get_server_height(self) -> int:\n        \"\"\"Length of header chain, as claimed by main interface.\"\"\"\n        interface = self.interface\n        return interface.tip if interface else 0\n\n    def get_local_height(self) -> int:\n        \"\"\"Length of header chain, POW-verified.\n        In case of a chain split, this is for the branch the main interface is on,\n        but it is the tip of that branch (even if main interface is behind).\n        \"\"\"\n        return self.blockchain().height()\n\n    def export_checkpoints(self, path):\n        \"\"\"Run manually to generate blockchain checkpoints.\n        Kept for console use only.\n        \"\"\"\n        cp = self.blockchain().get_checkpoints()\n        with open(path, 'w', encoding='utf-8') as f:\n            f.write(json.dumps(cp, indent=4))\n\n    async def _start(self):\n        assert not self.taskgroup\n        self.taskgroup = taskgroup = OldTaskGroup()\n        assert not self.interface and not self.interfaces\n        assert not self._connecting_ifaces\n        assert not self._closing_ifaces\n        self.logger.info('starting network')\n        self._clear_addr_retry_times()\n        self._init_parameters_from_config()\n        await self.taskgroup.spawn(self._run_new_interface(self.default_server))\n\n        async def main():\n            self.logger.info(f\"starting taskgroup ({hex(id(taskgroup))}).\")\n            try:\n                # note: if a task finishes with CancelledError, that\n                # will NOT raise, and the group will keep the other tasks running\n                async with taskgroup as group:\n                    await group.spawn(self._maintain_sessions())\n                    [await group.spawn(job) for job in self._jobs]\n            except Exception as e:\n                self.logger.exception(f\"taskgroup died ({hex(id(taskgroup))}).\")\n            finally:\n                self.logger.info(f\"taskgroup stopped ({hex(id(taskgroup))}).\")\n        asyncio.run_coroutine_threadsafe(main(), self.asyncio_loop)\n\n        util.trigger_callback('network_updated')\n\n    def start(self, jobs: Iterable = None):\n        \"\"\"Schedule starting the network, along with the given job co-routines.\n\n        Note: the jobs will *restart* every time the network restarts, e.g. on proxy\n        setting changes.\n        \"\"\"\n        self._was_started = True\n        self._jobs = jobs or []\n        asyncio.run_coroutine_threadsafe(self._start(), self.asyncio_loop)\n\n    @log_exceptions\n    async def stop(self, *, full_shutdown: bool = True):\n        if not self._was_started:\n            self.logger.info(\"not stopping network as it was never started\")\n            return\n        self.logger.info(\"stopping network\")\n        # timeout: if full_shutdown, it is up to the caller to time us out,\n        #          otherwise if e.g. restarting due to proxy changes, we time out fast\n        async with (nullcontext() if full_shutdown else ignore_after(1)):\n            async with OldTaskGroup() as group:\n                await group.spawn(self.taskgroup.cancel_remaining())\n                if full_shutdown:\n                    await group.spawn(self.stop_gossip(full_shutdown=full_shutdown))\n        self.taskgroup = None\n        self.interface = None\n        self.interfaces = {}\n        self._connecting_ifaces.clear()\n        self._closing_ifaces.clear()\n        if not full_shutdown:\n            util.trigger_callback('network_updated')\n\n    async def _ensure_there_is_a_main_interface(self):\n        if self.interface:\n            return\n        # if auto_connect is set, try a different server\n        if self.auto_connect and not self.is_connecting():\n            await self._switch_to_random_interface()\n        # if auto_connect is not set, or still no main interface, retry current\n        if not self.interface and not self.is_connecting():\n            if self._can_retry_addr(self.default_server, urgent=True):\n                await self.switch_to_interface(self.default_server)\n\n    async def _maintain_sessions(self):\n        async def maybe_start_new_interfaces():\n            num_existing_ifaces = len(self.interfaces) + len(self._connecting_ifaces) + len(self._closing_ifaces)\n            for i in range(self.num_server - num_existing_ifaces):\n                # FIXME this should try to honour \"healthy spread of connected servers\"\n                server = self._get_next_server_to_try()\n                if server:\n                    assert isinstance(server, ServerAddr), f\"expected ServerAddr, got {type(server)}\"\n                    await self.taskgroup.spawn(self._run_new_interface(server))\n        async def maintain_healthy_spread_of_connected_servers():\n            with self.interfaces_lock: interfaces = list(self.interfaces.values())\n            random.shuffle(interfaces)\n            for iface in interfaces:\n                if not self.check_interface_against_healthy_spread_of_connected_servers(iface):\n                    self.logger.info(f\"disconnecting from {iface.server}. too many connected \"\n                                     f\"servers already in bucket {iface.bucket_based_on_ipaddress()}\")\n                    await self._close_interface(iface)\n        async def maintain_main_interface():\n            await self._ensure_there_is_a_main_interface()\n            if self.is_connected():\n                if self.is_fee_estimates_update_required():\n                    await self.interface.taskgroup.spawn(self._request_fee_estimates, self.interface)\n\n        while True:\n            await maybe_start_new_interfaces()\n            await maintain_healthy_spread_of_connected_servers()\n            await maintain_main_interface()\n            await asyncio.sleep(0.1)\n\n    @classmethod\n    async def async_send_http_on_proxy(\n            cls, method: str, url: str, *,\n            params: dict = None,\n            body: bytes = None,\n            json: dict = None,\n            headers=None,\n            on_finish=None,\n            timeout=None,\n    ):\n        async def default_on_finish(resp: ClientResponse):\n            resp.raise_for_status()\n            return await resp.text()\n        if headers is None:\n            headers = {}\n        if on_finish is None:\n            on_finish = default_on_finish\n        network = cls.get_instance()\n        proxy = network.proxy if network else None\n        async with make_aiohttp_session(proxy, timeout=timeout) as session:\n            if method == 'get':\n                async with session.get(url, params=params, headers=headers) as resp:\n                    return await on_finish(resp)\n            elif method == 'post':\n                assert body is not None or json is not None, 'body or json must be supplied if method is post'\n                if body is not None:\n                    async with session.post(url, data=body, headers=headers) as resp:\n                        return await on_finish(resp)\n                elif json is not None:\n                    async with session.post(url, json=json, headers=headers) as resp:\n                        return await on_finish(resp)\n            else:\n                raise Exception(f\"unexpected {method=!r}\")\n\n    @classmethod\n    def send_http_on_proxy(cls, method, url, **kwargs):\n        loop = util.get_asyncio_loop()\n        assert util.get_running_loop() != loop, 'must not be called from asyncio thread'\n        coro = asyncio.run_coroutine_threadsafe(cls.async_send_http_on_proxy(method, url, **kwargs), loop)\n        # note: _send_http_on_proxy has its own timeout, so no timeout here:\n        return coro.result()\n\n    # methods used in scripts\n    async def get_peers(self):\n        while not self.is_connected():\n            await asyncio.sleep(1)\n        session = self.interface.session\n        return parse_servers(await session.send_request('server.peers.subscribe'))\n\n    async def send_multiple_requests(\n            self,\n            servers: Sequence[ServerAddr],\n            method: str,\n            params: Sequence,\n            *,\n            timeout: int = None,\n    ):\n        if timeout is None:\n            timeout = self.get_network_timeout_seconds(NetworkTimeout.Urgent)\n        responses = dict()\n        async def get_response(server: ServerAddr):\n            interface = Interface(network=self, server=server)\n            try:\n                await util.wait_for2(interface.ready, timeout)\n            except BaseException as e:\n                await interface.close()\n                return\n            try:\n                res = await interface.session.send_request(method, params, timeout=10)\n            except Exception as e:\n                res = e\n            responses[interface.server] = res\n        async with OldTaskGroup() as group:\n            for server in servers:\n                await group.spawn(get_response(server))\n        return responses\n\n    async def prune_offline_servers(self, hostmap):\n        peers = filter_protocol(hostmap, allowed_protocols=(\"t\", \"s\",))\n        timeout = self.get_network_timeout_seconds(NetworkTimeout.Generic)\n        replies = await self.send_multiple_requests(peers, 'blockchain.headers.subscribe', [], timeout=timeout)\n        servers_replied = {serveraddr.host for serveraddr in replies.keys()}\n        servers_dict = {k: v for k, v in hostmap.items()\n                        if k in servers_replied}\n        return servers_dict\n"
  },
  {
    "path": "electrum/old_mnemonic.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2011 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom typing import Sequence, Union\n\nfrom .mnemonic import Wordlist\nfrom .util import is_hex_str\n\n\n# list of words from http://en.wiktionary.org/wiki/Wiktionary:Frequency_lists/Contemporary_poetry\n\n_words = (\n\"like\",\n\"just\",\n\"love\",\n\"know\",\n\"never\",\n\"want\",\n\"time\",\n\"out\",\n\"there\",\n\"make\",\n\"look\",\n\"eye\",\n\"down\",\n\"only\",\n\"think\",\n\"heart\",\n\"back\",\n\"then\",\n\"into\",\n\"about\",\n\"more\",\n\"away\",\n\"still\",\n\"them\",\n\"take\",\n\"thing\",\n\"even\",\n\"through\",\n\"long\",\n\"always\",\n\"world\",\n\"too\",\n\"friend\",\n\"tell\",\n\"try\",\n\"hand\",\n\"thought\",\n\"over\",\n\"here\",\n\"other\",\n\"need\",\n\"smile\",\n\"again\",\n\"much\",\n\"cry\",\n\"been\",\n\"night\",\n\"ever\",\n\"little\",\n\"said\",\n\"end\",\n\"some\",\n\"those\",\n\"around\",\n\"mind\",\n\"people\",\n\"girl\",\n\"leave\",\n\"dream\",\n\"left\",\n\"turn\",\n\"myself\",\n\"give\",\n\"nothing\",\n\"really\",\n\"off\",\n\"before\",\n\"something\",\n\"find\",\n\"walk\",\n\"wish\",\n\"good\",\n\"once\",\n\"place\",\n\"ask\",\n\"stop\",\n\"keep\",\n\"watch\",\n\"seem\",\n\"everything\",\n\"wait\",\n\"got\",\n\"yet\",\n\"made\",\n\"remember\",\n\"start\",\n\"alone\",\n\"run\",\n\"hope\",\n\"maybe\",\n\"believe\",\n\"body\",\n\"hate\",\n\"after\",\n\"close\",\n\"talk\",\n\"stand\",\n\"own\",\n\"each\",\n\"hurt\",\n\"help\",\n\"home\",\n\"god\",\n\"soul\",\n\"new\",\n\"many\",\n\"two\",\n\"inside\",\n\"should\",\n\"true\",\n\"first\",\n\"fear\",\n\"mean\",\n\"better\",\n\"play\",\n\"another\",\n\"gone\",\n\"change\",\n\"use\",\n\"wonder\",\n\"someone\",\n\"hair\",\n\"cold\",\n\"open\",\n\"best\",\n\"any\",\n\"behind\",\n\"happen\",\n\"water\",\n\"dark\",\n\"laugh\",\n\"stay\",\n\"forever\",\n\"name\",\n\"work\",\n\"show\",\n\"sky\",\n\"break\",\n\"came\",\n\"deep\",\n\"door\",\n\"put\",\n\"black\",\n\"together\",\n\"upon\",\n\"happy\",\n\"such\",\n\"great\",\n\"white\",\n\"matter\",\n\"fill\",\n\"past\",\n\"please\",\n\"burn\",\n\"cause\",\n\"enough\",\n\"touch\",\n\"moment\",\n\"soon\",\n\"voice\",\n\"scream\",\n\"anything\",\n\"stare\",\n\"sound\",\n\"red\",\n\"everyone\",\n\"hide\",\n\"kiss\",\n\"truth\",\n\"death\",\n\"beautiful\",\n\"mine\",\n\"blood\",\n\"broken\",\n\"very\",\n\"pass\",\n\"next\",\n\"forget\",\n\"tree\",\n\"wrong\",\n\"air\",\n\"mother\",\n\"understand\",\n\"lip\",\n\"hit\",\n\"wall\",\n\"memory\",\n\"sleep\",\n\"free\",\n\"high\",\n\"realize\",\n\"school\",\n\"might\",\n\"skin\",\n\"sweet\",\n\"perfect\",\n\"blue\",\n\"kill\",\n\"breath\",\n\"dance\",\n\"against\",\n\"fly\",\n\"between\",\n\"grow\",\n\"strong\",\n\"under\",\n\"listen\",\n\"bring\",\n\"sometimes\",\n\"speak\",\n\"pull\",\n\"person\",\n\"become\",\n\"family\",\n\"begin\",\n\"ground\",\n\"real\",\n\"small\",\n\"father\",\n\"sure\",\n\"feet\",\n\"rest\",\n\"young\",\n\"finally\",\n\"land\",\n\"across\",\n\"today\",\n\"different\",\n\"guy\",\n\"line\",\n\"fire\",\n\"reason\",\n\"reach\",\n\"second\",\n\"slowly\",\n\"write\",\n\"eat\",\n\"smell\",\n\"mouth\",\n\"step\",\n\"learn\",\n\"three\",\n\"floor\",\n\"promise\",\n\"breathe\",\n\"darkness\",\n\"push\",\n\"earth\",\n\"guess\",\n\"save\",\n\"song\",\n\"above\",\n\"along\",\n\"both\",\n\"color\",\n\"house\",\n\"almost\",\n\"sorry\",\n\"anymore\",\n\"brother\",\n\"okay\",\n\"dear\",\n\"game\",\n\"fade\",\n\"already\",\n\"apart\",\n\"warm\",\n\"beauty\",\n\"heard\",\n\"notice\",\n\"question\",\n\"shine\",\n\"began\",\n\"piece\",\n\"whole\",\n\"shadow\",\n\"secret\",\n\"street\",\n\"within\",\n\"finger\",\n\"point\",\n\"morning\",\n\"whisper\",\n\"child\",\n\"moon\",\n\"green\",\n\"story\",\n\"glass\",\n\"kid\",\n\"silence\",\n\"since\",\n\"soft\",\n\"yourself\",\n\"empty\",\n\"shall\",\n\"angel\",\n\"answer\",\n\"baby\",\n\"bright\",\n\"dad\",\n\"path\",\n\"worry\",\n\"hour\",\n\"drop\",\n\"follow\",\n\"power\",\n\"war\",\n\"half\",\n\"flow\",\n\"heaven\",\n\"act\",\n\"chance\",\n\"fact\",\n\"least\",\n\"tired\",\n\"children\",\n\"near\",\n\"quite\",\n\"afraid\",\n\"rise\",\n\"sea\",\n\"taste\",\n\"window\",\n\"cover\",\n\"nice\",\n\"trust\",\n\"lot\",\n\"sad\",\n\"cool\",\n\"force\",\n\"peace\",\n\"return\",\n\"blind\",\n\"easy\",\n\"ready\",\n\"roll\",\n\"rose\",\n\"drive\",\n\"held\",\n\"music\",\n\"beneath\",\n\"hang\",\n\"mom\",\n\"paint\",\n\"emotion\",\n\"quiet\",\n\"clear\",\n\"cloud\",\n\"few\",\n\"pretty\",\n\"bird\",\n\"outside\",\n\"paper\",\n\"picture\",\n\"front\",\n\"rock\",\n\"simple\",\n\"anyone\",\n\"meant\",\n\"reality\",\n\"road\",\n\"sense\",\n\"waste\",\n\"bit\",\n\"leaf\",\n\"thank\",\n\"happiness\",\n\"meet\",\n\"men\",\n\"smoke\",\n\"truly\",\n\"decide\",\n\"self\",\n\"age\",\n\"book\",\n\"form\",\n\"alive\",\n\"carry\",\n\"escape\",\n\"damn\",\n\"instead\",\n\"able\",\n\"ice\",\n\"minute\",\n\"throw\",\n\"catch\",\n\"leg\",\n\"ring\",\n\"course\",\n\"goodbye\",\n\"lead\",\n\"poem\",\n\"sick\",\n\"corner\",\n\"desire\",\n\"known\",\n\"problem\",\n\"remind\",\n\"shoulder\",\n\"suppose\",\n\"toward\",\n\"wave\",\n\"drink\",\n\"jump\",\n\"woman\",\n\"pretend\",\n\"sister\",\n\"week\",\n\"human\",\n\"joy\",\n\"crack\",\n\"grey\",\n\"pray\",\n\"surprise\",\n\"dry\",\n\"knee\",\n\"less\",\n\"search\",\n\"bleed\",\n\"caught\",\n\"clean\",\n\"embrace\",\n\"future\",\n\"king\",\n\"son\",\n\"sorrow\",\n\"chest\",\n\"hug\",\n\"remain\",\n\"sat\",\n\"worth\",\n\"blow\",\n\"daddy\",\n\"final\",\n\"parent\",\n\"tight\",\n\"also\",\n\"create\",\n\"lonely\",\n\"safe\",\n\"cross\",\n\"dress\",\n\"evil\",\n\"silent\",\n\"bone\",\n\"fate\",\n\"perhaps\",\n\"anger\",\n\"class\",\n\"scar\",\n\"snow\",\n\"tiny\",\n\"tonight\",\n\"continue\",\n\"control\",\n\"dog\",\n\"edge\",\n\"mirror\",\n\"month\",\n\"suddenly\",\n\"comfort\",\n\"given\",\n\"loud\",\n\"quickly\",\n\"gaze\",\n\"plan\",\n\"rush\",\n\"stone\",\n\"town\",\n\"battle\",\n\"ignore\",\n\"spirit\",\n\"stood\",\n\"stupid\",\n\"yours\",\n\"brown\",\n\"build\",\n\"dust\",\n\"hey\",\n\"kept\",\n\"pay\",\n\"phone\",\n\"twist\",\n\"although\",\n\"ball\",\n\"beyond\",\n\"hidden\",\n\"nose\",\n\"taken\",\n\"fail\",\n\"float\",\n\"pure\",\n\"somehow\",\n\"wash\",\n\"wrap\",\n\"angry\",\n\"cheek\",\n\"creature\",\n\"forgotten\",\n\"heat\",\n\"rip\",\n\"single\",\n\"space\",\n\"special\",\n\"weak\",\n\"whatever\",\n\"yell\",\n\"anyway\",\n\"blame\",\n\"job\",\n\"choose\",\n\"country\",\n\"curse\",\n\"drift\",\n\"echo\",\n\"figure\",\n\"grew\",\n\"laughter\",\n\"neck\",\n\"suffer\",\n\"worse\",\n\"yeah\",\n\"disappear\",\n\"foot\",\n\"forward\",\n\"knife\",\n\"mess\",\n\"somewhere\",\n\"stomach\",\n\"storm\",\n\"beg\",\n\"idea\",\n\"lift\",\n\"offer\",\n\"breeze\",\n\"field\",\n\"five\",\n\"often\",\n\"simply\",\n\"stuck\",\n\"win\",\n\"allow\",\n\"confuse\",\n\"enjoy\",\n\"except\",\n\"flower\",\n\"seek\",\n\"strength\",\n\"calm\",\n\"grin\",\n\"gun\",\n\"heavy\",\n\"hill\",\n\"large\",\n\"ocean\",\n\"shoe\",\n\"sigh\",\n\"straight\",\n\"summer\",\n\"tongue\",\n\"accept\",\n\"crazy\",\n\"everyday\",\n\"exist\",\n\"grass\",\n\"mistake\",\n\"sent\",\n\"shut\",\n\"surround\",\n\"table\",\n\"ache\",\n\"brain\",\n\"destroy\",\n\"heal\",\n\"nature\",\n\"shout\",\n\"sign\",\n\"stain\",\n\"choice\",\n\"doubt\",\n\"glance\",\n\"glow\",\n\"mountain\",\n\"queen\",\n\"stranger\",\n\"throat\",\n\"tomorrow\",\n\"city\",\n\"either\",\n\"fish\",\n\"flame\",\n\"rather\",\n\"shape\",\n\"spin\",\n\"spread\",\n\"ash\",\n\"distance\",\n\"finish\",\n\"image\",\n\"imagine\",\n\"important\",\n\"nobody\",\n\"shatter\",\n\"warmth\",\n\"became\",\n\"feed\",\n\"flesh\",\n\"funny\",\n\"lust\",\n\"shirt\",\n\"trouble\",\n\"yellow\",\n\"attention\",\n\"bare\",\n\"bite\",\n\"money\",\n\"protect\",\n\"amaze\",\n\"appear\",\n\"born\",\n\"choke\",\n\"completely\",\n\"daughter\",\n\"fresh\",\n\"friendship\",\n\"gentle\",\n\"probably\",\n\"six\",\n\"deserve\",\n\"expect\",\n\"grab\",\n\"middle\",\n\"nightmare\",\n\"river\",\n\"thousand\",\n\"weight\",\n\"worst\",\n\"wound\",\n\"barely\",\n\"bottle\",\n\"cream\",\n\"regret\",\n\"relationship\",\n\"stick\",\n\"test\",\n\"crush\",\n\"endless\",\n\"fault\",\n\"itself\",\n\"rule\",\n\"spill\",\n\"art\",\n\"circle\",\n\"join\",\n\"kick\",\n\"mask\",\n\"master\",\n\"passion\",\n\"quick\",\n\"raise\",\n\"smooth\",\n\"unless\",\n\"wander\",\n\"actually\",\n\"broke\",\n\"chair\",\n\"deal\",\n\"favorite\",\n\"gift\",\n\"note\",\n\"number\",\n\"sweat\",\n\"box\",\n\"chill\",\n\"clothes\",\n\"lady\",\n\"mark\",\n\"park\",\n\"poor\",\n\"sadness\",\n\"tie\",\n\"animal\",\n\"belong\",\n\"brush\",\n\"consume\",\n\"dawn\",\n\"forest\",\n\"innocent\",\n\"pen\",\n\"pride\",\n\"stream\",\n\"thick\",\n\"clay\",\n\"complete\",\n\"count\",\n\"draw\",\n\"faith\",\n\"press\",\n\"silver\",\n\"struggle\",\n\"surface\",\n\"taught\",\n\"teach\",\n\"wet\",\n\"bless\",\n\"chase\",\n\"climb\",\n\"enter\",\n\"letter\",\n\"melt\",\n\"metal\",\n\"movie\",\n\"stretch\",\n\"swing\",\n\"vision\",\n\"wife\",\n\"beside\",\n\"crash\",\n\"forgot\",\n\"guide\",\n\"haunt\",\n\"joke\",\n\"knock\",\n\"plant\",\n\"pour\",\n\"prove\",\n\"reveal\",\n\"steal\",\n\"stuff\",\n\"trip\",\n\"wood\",\n\"wrist\",\n\"bother\",\n\"bottom\",\n\"crawl\",\n\"crowd\",\n\"fix\",\n\"forgive\",\n\"frown\",\n\"grace\",\n\"loose\",\n\"lucky\",\n\"party\",\n\"release\",\n\"surely\",\n\"survive\",\n\"teacher\",\n\"gently\",\n\"grip\",\n\"speed\",\n\"suicide\",\n\"travel\",\n\"treat\",\n\"vein\",\n\"written\",\n\"cage\",\n\"chain\",\n\"conversation\",\n\"date\",\n\"enemy\",\n\"however\",\n\"interest\",\n\"million\",\n\"page\",\n\"pink\",\n\"proud\",\n\"sway\",\n\"themselves\",\n\"winter\",\n\"church\",\n\"cruel\",\n\"cup\",\n\"demon\",\n\"experience\",\n\"freedom\",\n\"pair\",\n\"pop\",\n\"purpose\",\n\"respect\",\n\"shoot\",\n\"softly\",\n\"state\",\n\"strange\",\n\"bar\",\n\"birth\",\n\"curl\",\n\"dirt\",\n\"excuse\",\n\"lord\",\n\"lovely\",\n\"monster\",\n\"order\",\n\"pack\",\n\"pants\",\n\"pool\",\n\"scene\",\n\"seven\",\n\"shame\",\n\"slide\",\n\"ugly\",\n\"among\",\n\"blade\",\n\"blonde\",\n\"closet\",\n\"creek\",\n\"deny\",\n\"drug\",\n\"eternity\",\n\"gain\",\n\"grade\",\n\"handle\",\n\"key\",\n\"linger\",\n\"pale\",\n\"prepare\",\n\"swallow\",\n\"swim\",\n\"tremble\",\n\"wheel\",\n\"won\",\n\"cast\",\n\"cigarette\",\n\"claim\",\n\"college\",\n\"direction\",\n\"dirty\",\n\"gather\",\n\"ghost\",\n\"hundred\",\n\"loss\",\n\"lung\",\n\"orange\",\n\"present\",\n\"swear\",\n\"swirl\",\n\"twice\",\n\"wild\",\n\"bitter\",\n\"blanket\",\n\"doctor\",\n\"everywhere\",\n\"flash\",\n\"grown\",\n\"knowledge\",\n\"numb\",\n\"pressure\",\n\"radio\",\n\"repeat\",\n\"ruin\",\n\"spend\",\n\"unknown\",\n\"buy\",\n\"clock\",\n\"devil\",\n\"early\",\n\"false\",\n\"fantasy\",\n\"pound\",\n\"precious\",\n\"refuse\",\n\"sheet\",\n\"teeth\",\n\"welcome\",\n\"add\",\n\"ahead\",\n\"block\",\n\"bury\",\n\"caress\",\n\"content\",\n\"depth\",\n\"despite\",\n\"distant\",\n\"marry\",\n\"purple\",\n\"threw\",\n\"whenever\",\n\"bomb\",\n\"dull\",\n\"easily\",\n\"grasp\",\n\"hospital\",\n\"innocence\",\n\"normal\",\n\"receive\",\n\"reply\",\n\"rhyme\",\n\"shade\",\n\"someday\",\n\"sword\",\n\"toe\",\n\"visit\",\n\"asleep\",\n\"bought\",\n\"center\",\n\"consider\",\n\"flat\",\n\"hero\",\n\"history\",\n\"ink\",\n\"insane\",\n\"muscle\",\n\"mystery\",\n\"pocket\",\n\"reflection\",\n\"shove\",\n\"silently\",\n\"smart\",\n\"soldier\",\n\"spot\",\n\"stress\",\n\"train\",\n\"type\",\n\"view\",\n\"whether\",\n\"bus\",\n\"energy\",\n\"explain\",\n\"holy\",\n\"hunger\",\n\"inch\",\n\"magic\",\n\"mix\",\n\"noise\",\n\"nowhere\",\n\"prayer\",\n\"presence\",\n\"shock\",\n\"snap\",\n\"spider\",\n\"study\",\n\"thunder\",\n\"trail\",\n\"admit\",\n\"agree\",\n\"bag\",\n\"bang\",\n\"bound\",\n\"butterfly\",\n\"cute\",\n\"exactly\",\n\"explode\",\n\"familiar\",\n\"fold\",\n\"further\",\n\"pierce\",\n\"reflect\",\n\"scent\",\n\"selfish\",\n\"sharp\",\n\"sink\",\n\"spring\",\n\"stumble\",\n\"universe\",\n\"weep\",\n\"women\",\n\"wonderful\",\n\"action\",\n\"ancient\",\n\"attempt\",\n\"avoid\",\n\"birthday\",\n\"branch\",\n\"chocolate\",\n\"core\",\n\"depress\",\n\"drunk\",\n\"especially\",\n\"focus\",\n\"fruit\",\n\"honest\",\n\"match\",\n\"palm\",\n\"perfectly\",\n\"pillow\",\n\"pity\",\n\"poison\",\n\"roar\",\n\"shift\",\n\"slightly\",\n\"thump\",\n\"truck\",\n\"tune\",\n\"twenty\",\n\"unable\",\n\"wipe\",\n\"wrote\",\n\"coat\",\n\"constant\",\n\"dinner\",\n\"drove\",\n\"egg\",\n\"eternal\",\n\"flight\",\n\"flood\",\n\"frame\",\n\"freak\",\n\"gasp\",\n\"glad\",\n\"hollow\",\n\"motion\",\n\"peer\",\n\"plastic\",\n\"root\",\n\"screen\",\n\"season\",\n\"sting\",\n\"strike\",\n\"team\",\n\"unlike\",\n\"victim\",\n\"volume\",\n\"warn\",\n\"weird\",\n\"attack\",\n\"await\",\n\"awake\",\n\"built\",\n\"charm\",\n\"crave\",\n\"despair\",\n\"fought\",\n\"grant\",\n\"grief\",\n\"horse\",\n\"limit\",\n\"message\",\n\"ripple\",\n\"sanity\",\n\"scatter\",\n\"serve\",\n\"split\",\n\"string\",\n\"trick\",\n\"annoy\",\n\"blur\",\n\"boat\",\n\"brave\",\n\"clearly\",\n\"cling\",\n\"connect\",\n\"fist\",\n\"forth\",\n\"imagination\",\n\"iron\",\n\"jock\",\n\"judge\",\n\"lesson\",\n\"milk\",\n\"misery\",\n\"nail\",\n\"naked\",\n\"ourselves\",\n\"poet\",\n\"possible\",\n\"princess\",\n\"sail\",\n\"size\",\n\"snake\",\n\"society\",\n\"stroke\",\n\"torture\",\n\"toss\",\n\"trace\",\n\"wise\",\n\"bloom\",\n\"bullet\",\n\"cell\",\n\"check\",\n\"cost\",\n\"darling\",\n\"during\",\n\"footstep\",\n\"fragile\",\n\"hallway\",\n\"hardly\",\n\"horizon\",\n\"invisible\",\n\"journey\",\n\"midnight\",\n\"mud\",\n\"nod\",\n\"pause\",\n\"relax\",\n\"shiver\",\n\"sudden\",\n\"value\",\n\"youth\",\n\"abuse\",\n\"admire\",\n\"blink\",\n\"breast\",\n\"bruise\",\n\"constantly\",\n\"couple\",\n\"creep\",\n\"curve\",\n\"difference\",\n\"dumb\",\n\"emptiness\",\n\"gotta\",\n\"honor\",\n\"plain\",\n\"planet\",\n\"recall\",\n\"rub\",\n\"ship\",\n\"slam\",\n\"soar\",\n\"somebody\",\n\"tightly\",\n\"weather\",\n\"adore\",\n\"approach\",\n\"bond\",\n\"bread\",\n\"burst\",\n\"candle\",\n\"coffee\",\n\"cousin\",\n\"crime\",\n\"desert\",\n\"flutter\",\n\"frozen\",\n\"grand\",\n\"heel\",\n\"hello\",\n\"language\",\n\"level\",\n\"movement\",\n\"pleasure\",\n\"powerful\",\n\"random\",\n\"rhythm\",\n\"settle\",\n\"silly\",\n\"slap\",\n\"sort\",\n\"spoken\",\n\"steel\",\n\"threaten\",\n\"tumble\",\n\"upset\",\n\"aside\",\n\"awkward\",\n\"bee\",\n\"blank\",\n\"board\",\n\"button\",\n\"card\",\n\"carefully\",\n\"complain\",\n\"crap\",\n\"deeply\",\n\"discover\",\n\"drag\",\n\"dread\",\n\"effort\",\n\"entire\",\n\"fairy\",\n\"giant\",\n\"gotten\",\n\"greet\",\n\"illusion\",\n\"jeans\",\n\"leap\",\n\"liquid\",\n\"march\",\n\"mend\",\n\"nervous\",\n\"nine\",\n\"replace\",\n\"rope\",\n\"spine\",\n\"stole\",\n\"terror\",\n\"accident\",\n\"apple\",\n\"balance\",\n\"boom\",\n\"childhood\",\n\"collect\",\n\"demand\",\n\"depression\",\n\"eventually\",\n\"faint\",\n\"glare\",\n\"goal\",\n\"group\",\n\"honey\",\n\"kitchen\",\n\"laid\",\n\"limb\",\n\"machine\",\n\"mere\",\n\"mold\",\n\"murder\",\n\"nerve\",\n\"painful\",\n\"poetry\",\n\"prince\",\n\"rabbit\",\n\"shelter\",\n\"shore\",\n\"shower\",\n\"soothe\",\n\"stair\",\n\"steady\",\n\"sunlight\",\n\"tangle\",\n\"tease\",\n\"treasure\",\n\"uncle\",\n\"begun\",\n\"bliss\",\n\"canvas\",\n\"cheer\",\n\"claw\",\n\"clutch\",\n\"commit\",\n\"crimson\",\n\"crystal\",\n\"delight\",\n\"doll\",\n\"existence\",\n\"express\",\n\"fog\",\n\"football\",\n\"gay\",\n\"goose\",\n\"guard\",\n\"hatred\",\n\"illuminate\",\n\"mass\",\n\"math\",\n\"mourn\",\n\"rich\",\n\"rough\",\n\"skip\",\n\"stir\",\n\"student\",\n\"style\",\n\"support\",\n\"thorn\",\n\"tough\",\n\"yard\",\n\"yearn\",\n\"yesterday\",\n\"advice\",\n\"appreciate\",\n\"autumn\",\n\"bank\",\n\"beam\",\n\"bowl\",\n\"capture\",\n\"carve\",\n\"collapse\",\n\"confusion\",\n\"creation\",\n\"dove\",\n\"feather\",\n\"girlfriend\",\n\"glory\",\n\"government\",\n\"harsh\",\n\"hop\",\n\"inner\",\n\"loser\",\n\"moonlight\",\n\"neighbor\",\n\"neither\",\n\"peach\",\n\"pig\",\n\"praise\",\n\"screw\",\n\"shield\",\n\"shimmer\",\n\"sneak\",\n\"stab\",\n\"subject\",\n\"throughout\",\n\"thrown\",\n\"tower\",\n\"twirl\",\n\"wow\",\n\"army\",\n\"arrive\",\n\"bathroom\",\n\"bump\",\n\"cease\",\n\"cookie\",\n\"couch\",\n\"courage\",\n\"dim\",\n\"guilt\",\n\"howl\",\n\"hum\",\n\"husband\",\n\"insult\",\n\"led\",\n\"lunch\",\n\"mock\",\n\"mostly\",\n\"natural\",\n\"nearly\",\n\"needle\",\n\"nerd\",\n\"peaceful\",\n\"perfection\",\n\"pile\",\n\"price\",\n\"remove\",\n\"roam\",\n\"sanctuary\",\n\"serious\",\n\"shiny\",\n\"shook\",\n\"sob\",\n\"stolen\",\n\"tap\",\n\"vain\",\n\"void\",\n\"warrior\",\n\"wrinkle\",\n\"affection\",\n\"apologize\",\n\"blossom\",\n\"bounce\",\n\"bridge\",\n\"cheap\",\n\"crumble\",\n\"decision\",\n\"descend\",\n\"desperately\",\n\"dig\",\n\"dot\",\n\"flip\",\n\"frighten\",\n\"heartbeat\",\n\"huge\",\n\"lazy\",\n\"lick\",\n\"odd\",\n\"opinion\",\n\"process\",\n\"puzzle\",\n\"quietly\",\n\"retreat\",\n\"score\",\n\"sentence\",\n\"separate\",\n\"situation\",\n\"skill\",\n\"soak\",\n\"square\",\n\"stray\",\n\"taint\",\n\"task\",\n\"tide\",\n\"underneath\",\n\"veil\",\n\"whistle\",\n\"anywhere\",\n\"bedroom\",\n\"bid\",\n\"bloody\",\n\"burden\",\n\"careful\",\n\"compare\",\n\"concern\",\n\"curtain\",\n\"decay\",\n\"defeat\",\n\"describe\",\n\"double\",\n\"dreamer\",\n\"driver\",\n\"dwell\",\n\"evening\",\n\"flare\",\n\"flicker\",\n\"grandma\",\n\"guitar\",\n\"harm\",\n\"horrible\",\n\"hungry\",\n\"indeed\",\n\"lace\",\n\"melody\",\n\"monkey\",\n\"nation\",\n\"object\",\n\"obviously\",\n\"rainbow\",\n\"salt\",\n\"scratch\",\n\"shown\",\n\"shy\",\n\"stage\",\n\"stun\",\n\"third\",\n\"tickle\",\n\"useless\",\n\"weakness\",\n\"worship\",\n\"worthless\",\n\"afternoon\",\n\"beard\",\n\"boyfriend\",\n\"bubble\",\n\"busy\",\n\"certain\",\n\"chin\",\n\"concrete\",\n\"desk\",\n\"diamond\",\n\"doom\",\n\"drawn\",\n\"due\",\n\"felicity\",\n\"freeze\",\n\"frost\",\n\"garden\",\n\"glide\",\n\"harmony\",\n\"hopefully\",\n\"hunt\",\n\"jealous\",\n\"lightning\",\n\"mama\",\n\"mercy\",\n\"peel\",\n\"physical\",\n\"position\",\n\"pulse\",\n\"punch\",\n\"quit\",\n\"rant\",\n\"respond\",\n\"salty\",\n\"sane\",\n\"satisfy\",\n\"savior\",\n\"sheep\",\n\"slept\",\n\"social\",\n\"sport\",\n\"tuck\",\n\"utter\",\n\"valley\",\n\"wolf\",\n\"aim\",\n\"alas\",\n\"alter\",\n\"arrow\",\n\"awaken\",\n\"beaten\",\n\"belief\",\n\"brand\",\n\"ceiling\",\n\"cheese\",\n\"clue\",\n\"confidence\",\n\"connection\",\n\"daily\",\n\"disguise\",\n\"eager\",\n\"erase\",\n\"essence\",\n\"everytime\",\n\"expression\",\n\"fan\",\n\"flag\",\n\"flirt\",\n\"foul\",\n\"fur\",\n\"giggle\",\n\"glorious\",\n\"ignorance\",\n\"law\",\n\"lifeless\",\n\"measure\",\n\"mighty\",\n\"muse\",\n\"north\",\n\"opposite\",\n\"paradise\",\n\"patience\",\n\"patient\",\n\"pencil\",\n\"petal\",\n\"plate\",\n\"ponder\",\n\"possibly\",\n\"practice\",\n\"slice\",\n\"spell\",\n\"stock\",\n\"strife\",\n\"strip\",\n\"suffocate\",\n\"suit\",\n\"tender\",\n\"tool\",\n\"trade\",\n\"velvet\",\n\"verse\",\n\"waist\",\n\"witch\",\n\"aunt\",\n\"bench\",\n\"bold\",\n\"cap\",\n\"certainly\",\n\"click\",\n\"companion\",\n\"creator\",\n\"dart\",\n\"delicate\",\n\"determine\",\n\"dish\",\n\"dragon\",\n\"drama\",\n\"drum\",\n\"dude\",\n\"everybody\",\n\"feast\",\n\"forehead\",\n\"former\",\n\"fright\",\n\"fully\",\n\"gas\",\n\"hook\",\n\"hurl\",\n\"invite\",\n\"juice\",\n\"manage\",\n\"moral\",\n\"possess\",\n\"raw\",\n\"rebel\",\n\"royal\",\n\"scale\",\n\"scary\",\n\"several\",\n\"slight\",\n\"stubborn\",\n\"swell\",\n\"talent\",\n\"tea\",\n\"terrible\",\n\"thread\",\n\"torment\",\n\"trickle\",\n\"usually\",\n\"vast\",\n\"violence\",\n\"weave\",\n\"acid\",\n\"agony\",\n\"ashamed\",\n\"awe\",\n\"belly\",\n\"blend\",\n\"blush\",\n\"character\",\n\"cheat\",\n\"common\",\n\"company\",\n\"coward\",\n\"creak\",\n\"danger\",\n\"deadly\",\n\"defense\",\n\"define\",\n\"depend\",\n\"desperate\",\n\"destination\",\n\"dew\",\n\"duck\",\n\"dusty\",\n\"embarrass\",\n\"engine\",\n\"example\",\n\"explore\",\n\"foe\",\n\"freely\",\n\"frustrate\",\n\"generation\",\n\"glove\",\n\"guilty\",\n\"health\",\n\"hurry\",\n\"idiot\",\n\"impossible\",\n\"inhale\",\n\"jaw\",\n\"kingdom\",\n\"mention\",\n\"mist\",\n\"moan\",\n\"mumble\",\n\"mutter\",\n\"observe\",\n\"ode\",\n\"pathetic\",\n\"pattern\",\n\"pie\",\n\"prefer\",\n\"puff\",\n\"rape\",\n\"rare\",\n\"revenge\",\n\"rude\",\n\"scrape\",\n\"spiral\",\n\"squeeze\",\n\"strain\",\n\"sunset\",\n\"suspend\",\n\"sympathy\",\n\"thigh\",\n\"throne\",\n\"total\",\n\"unseen\",\n\"weapon\",\n\"weary\",\n)\n\nwordlist = Wordlist(_words)\n\nn = len(wordlist)\nassert n == 1626\n\n\n# Note about US patent no 5892470: Here each word does not represent a given digit.\n# Instead, the digit represented by a word is variable, it depends on the previous word.\n\ndef mn_encode(message: str) -> Sequence[str]:\n    # note: to generate an 'old'-type mnemonic for testing:\n    #       \" \".join(electrum.old_mnemonic.mn_encode(secrets.token_hex(16)))\n    assert is_hex_str(message), f\"expected hex, got {type(message)}\"\n    assert len(message) % 8 == 0\n    out = []\n    for i in range(len(message)//8):\n        word = message[8*i:8*i+8]\n        x = int(word, 16)\n        w1 = (x%n)\n        w2 = ((x//n) + w1)%n\n        w3 = ((x//n//n) + w2)%n\n        out += [wordlist[w1], wordlist[w2], wordlist[w3]]\n    return out\n\n\ndef mn_decode(wlist: Sequence[str]) -> str:\n    out = ''\n    for i in range(len(wlist)//3):\n        word1, word2, word3 = wlist[3*i:3*i+3]\n        w1 =  wordlist.index(word1)\n        w2 = (wordlist.index(word2)) % n\n        w3 = (wordlist.index(word3)) % n\n        x = w1 +n*((w2-w1)%n) +n*n*((w3-w2)%n)\n        out += '%08x'%x\n    return out\n\n\nif __name__ == '__main__':\n    import sys\n    if len(sys.argv) == 1:\n        print('I need arguments: a hex string to encode, or a list of words to decode')\n    elif len(sys.argv) == 2:\n        print(' '.join(mn_encode(sys.argv[1])))\n    else:\n        print(mn_decode(sys.argv[1:]))\n"
  },
  {
    "path": "electrum/onion_message.py",
    "content": "# Electrum - Lightweight Bitcoin Client\n# Copyright (c) 2023-2024 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport asyncio\nimport copy\nimport io\nimport os\nimport threading\nimport time\nimport dataclasses\nfrom random import random\nfrom types import MappingProxyType\n\nfrom typing import TYPE_CHECKING, Optional, Sequence, NamedTuple, List\n\nimport electrum_ecc as ecc\n\nfrom electrum.lnrouter import PathEdge\nfrom electrum.logging import get_logger, Logger\nfrom electrum.crypto import sha256, get_ecdh\nfrom electrum.lnmsg import OnionWireSerializer\nfrom electrum.lnonion import (get_bolt04_onion_key, OnionPacket, process_onion_packet,\n                              OnionHopsDataSingle, decrypt_onionmsg_data_tlv, encrypt_onionmsg_data_tlv,\n                              get_shared_secrets_along_route, new_onion_packet, encrypt_hops_recipient_data)\nfrom electrum.lnutil import LnFeatures\nfrom electrum.util import OldTaskGroup, log_exceptions\n\n\ndef now():\n    return time.time()\n\n\nif TYPE_CHECKING:\n    from electrum.lnworker import LNWallet\n    from electrum.network import Network\n    from electrum.lnrouter import NodeInfo\n    from electrum.lntransport import LNPeerAddr\n    from asyncio import Task\n\nlogger = get_logger(__name__)\n\n\nREQUEST_REPLY_PATHS_MAX = 3\n\n\nclass NoRouteFound(Exception):\n    def __init__(self, *args, peer_address: 'LNPeerAddr' = None):\n        Exception.__init__(self, *args)\n        self.peer_address = peer_address\n\n\ndef create_blinded_path(\n        session_key: bytes,\n        path: Sequence[bytes],\n        final_recipient_data: dict,\n        *,\n        hop_extras: Optional[Sequence[dict]] = None,\n        dummy_hops: Optional[int] = 0\n) -> dict:\n    # dummy hops could be inserted anywhere in the path, but for compatibility just add them at the end\n    # because blinded paths are usually constructed towards ourselves, and we know we can handle dummy hops.\n    if dummy_hops:\n        logger.debug(f'adding {dummy_hops} dummy hops at the end')\n        path += [path[-1]] * dummy_hops\n\n    introduction_point = path[0]\n\n    blinding = ecc.ECPrivkey(session_key).get_public_key_bytes()\n\n    onionmsg_hops = []\n    shared_secrets, blinded_node_ids = get_shared_secrets_along_route(path, session_key)\n    for i, node_id in enumerate(path):\n        is_non_final_node = i < len(path) - 1\n\n        if is_non_final_node:\n            recipient_data = {\n                # TODO: SHOULD add padding data to ensure all encrypted_data_tlv(i) have the same length\n                'next_node_id': {'node_id': path[i+1]}\n            }\n            if hop_extras and i < len(hop_extras):  # extra hop data for debugging for now\n                recipient_data.update(hop_extras[i])\n        else:\n            # TODO: SHOULD add padding data to ensure all encrypted_data_tlv(i) have the same length\n            recipient_data = final_recipient_data\n\n        encrypted_recipient_data = encrypt_onionmsg_data_tlv(shared_secret=shared_secrets[i], **recipient_data)\n\n        hopdata = {\n            'blinded_node_id': blinded_node_ids[i],\n            'enclen': len(encrypted_recipient_data),\n            'encrypted_recipient_data': encrypted_recipient_data\n        }\n        onionmsg_hops.append(hopdata)\n\n    blinded_path = {\n        'first_node_id': introduction_point,\n        'first_path_key': blinding,\n        'num_hops': bytes([len(onionmsg_hops)]),\n        'path': onionmsg_hops\n    }\n\n    return blinded_path\n\n\ndef blinding_privkey(privkey: bytes, blinding: bytes) -> bytes:\n    shared_secret = get_ecdh(privkey, blinding)\n    b_hmac = get_bolt04_onion_key(b'blinded_node_id', shared_secret)\n    b_hmac_int = int.from_bytes(b_hmac, byteorder=\"big\")\n\n    our_privkey_int = int.from_bytes(privkey, byteorder=\"big\")\n    our_privkey_int = our_privkey_int * b_hmac_int % ecc.CURVE_ORDER\n    our_privkey = our_privkey_int.to_bytes(32, byteorder=\"big\")\n\n    return our_privkey\n\n\ndef is_onion_message_node(node_id: bytes, node_info: Optional['NodeInfo']) -> bool:\n    if not node_info:\n        return False\n    return LnFeatures(node_info.features).supports(LnFeatures.OPTION_ONION_MESSAGE_OPT)\n\n\ndef create_onion_message_route_to(lnwallet: 'LNWallet', node_id: bytes) -> Sequence[PathEdge]:\n    \"\"\"Constructs a route to the destination node_id, first by starting with peers with existing channels,\n       and if no route found, opening a direct peer connection if node_id is found with an address in\n       channel_db.\"\"\"\n    # TODO: is this the proper way to set up my_sending_channels?\n    my_active_channels = [\n        chan for chan in lnwallet.channels.values() if\n        chan.is_active() and not chan.is_frozen_for_sending()]\n    my_sending_channels = {chan.short_channel_id: chan for chan in my_active_channels\n                           if chan.short_channel_id is not None}\n    # find route to introduction point over existing channel mesh\n    # NOTE: nodes that are in channel_db but are offline are not removed from the set\n    if lnwallet.network.path_finder:\n        if path := lnwallet.network.path_finder.find_path_for_payment(\n            nodeA=lnwallet.node_keypair.pubkey,\n            nodeB=node_id,\n            invoice_amount_msat=10000,  # TODO: do this without amount constraints\n            node_filter=lambda x, y: True if x == lnwallet.node_keypair.pubkey else is_onion_message_node(x, y),\n            my_sending_channels=my_sending_channels\n        ): return path\n\n    # alt: dest is existing peer?\n    if lnwallet.lnpeermgr.get_peer_by_pubkey(node_id):\n        return [PathEdge(short_channel_id=None, start_node=None, end_node=node_id)]\n\n    # if we have an address, pass it.\n    if lnwallet.channel_db:\n        if peer_addr := lnwallet.channel_db.get_last_good_address(node_id):\n            raise NoRouteFound('no path found, peer_addr available', peer_address=peer_addr)\n\n    raise NoRouteFound('no path found')\n\n\ndef send_onion_message_to(\n        lnwallet: 'LNWallet',\n        node_id_or_blinded_path: bytes,\n        destination_payload: dict,\n        session_key: bytes = None\n) -> None:\n    if session_key is None:\n        session_key = os.urandom(32)\n\n    if len(node_id_or_blinded_path) > 33:  # assume blinded path\n        with io.BytesIO(node_id_or_blinded_path) as blinded_path_fd:\n            try:\n                blinded_path = OnionWireSerializer.read_field(\n                    fd=blinded_path_fd,\n                    field_type='blinded_path',\n                    count=1)\n                logger.debug(f'blinded path: {blinded_path!r}')\n            except Exception as e:\n                logger.error(f'e!r')\n                raise\n\n            introduction_point = blinded_path['first_node_id']\n            if len(introduction_point) != 33:\n                raise Exception('first_node_id not a nodeid but a sciddir, which is not supported')\n                # Note: blinded_path specifies type sciddir_or_nodeid for first_node_id\n                # but only nodeid is supported in onion_message context;\n                # https://github.com/lightning/bolts/blob/master/04-onion-routing.md\n                # \"MUST set first_node_id to N0\"\n\n            hops_data = []\n            blinded_node_ids = []\n\n            if lnwallet.node_keypair.pubkey == introduction_point:\n                # blinded path introduction point is me\n                our_blinding = blinded_path['first_path_key']\n                our_payload = blinded_path['path'][0]\n                remaining_blinded_path = blinded_path['path'][1:]\n                assert len(remaining_blinded_path) > 0, 'sending to myself?'\n\n                # decrypt\n                shared_secret = get_ecdh(lnwallet.node_keypair.privkey, our_blinding)\n                recipient_data = decrypt_onionmsg_data_tlv(\n                    shared_secret=shared_secret,\n                    encrypted_recipient_data=our_payload['encrypted_recipient_data']\n                )\n\n                peer = lnwallet.lnpeermgr.get_peer_by_pubkey(recipient_data['next_node_id']['node_id'])\n                assert peer, 'next_node_id not a peer'\n\n                # blinding override?\n                next_path_key_override = recipient_data.get('next_path_key_override')\n                if next_path_key_override:\n                    next_path_key = next_path_key_override.get('path_key')\n                else:\n                    # E_i+1=SHA256(E_i||ss_i) * E_i\n                    blinding_factor = sha256(our_blinding + shared_secret)\n                    blinding_factor_int = int.from_bytes(blinding_factor, byteorder=\"big\")\n                    next_public_key_int = ecc.ECPubkey(our_blinding) * blinding_factor_int\n                    next_path_key = next_public_key_int.get_public_key_bytes()\n\n                path_key = next_path_key\n\n            else:\n                # we need a route to introduction point\n                remaining_blinded_path = blinded_path['path']\n                if not isinstance(remaining_blinded_path, list):  # doesn't return list when num items == 1\n                    remaining_blinded_path = [remaining_blinded_path]\n\n                peer = lnwallet.lnpeermgr.get_peer_by_pubkey(introduction_point)\n                # if blinded path introduction point is our direct peer, no need to route-find\n                if peer:\n                    # start of blinded path is our peer\n                    path_key = blinded_path['first_path_key']\n                else:\n                    path = create_onion_message_route_to(lnwallet, introduction_point)\n\n                    # first edge must be to our peer\n                    peer = lnwallet.lnpeermgr.get_peer_by_pubkey(path[0].end_node)\n                    assert peer, 'first hop not a peer'\n\n                    # last edge is to introduction point and start of blinded path. remove from route\n                    assert path[-1].end_node == introduction_point, 'last hop in route must be introduction point'\n\n                    path = path[:-1]\n\n                    if len(path) == 0:\n                        path_key = blinded_path['first_path_key']\n                    else:\n                        payment_path_pubkeys = [edge.end_node for edge in path]\n                        hop_shared_secrets, blinded_node_ids = get_shared_secrets_along_route(\n                            payment_path_pubkeys,\n                            session_key)\n\n                        hops_data = [\n                            OnionHopsDataSingle(\n                                tlv_stream_name='onionmsg_tlv',\n                                blind_fields={'next_node_id': {'node_id': x.end_node}},\n                            ) for x in path[:-1]\n                        ]\n\n                        # final hop pre-ip, add next_path_key_override\n                        final_hop_pre_ip = OnionHopsDataSingle(\n                            tlv_stream_name='onionmsg_tlv',\n                            blind_fields={\n                                'next_node_id': {'node_id': introduction_point},\n                                'next_path_key_override': {'path_key': blinded_path['first_path_key']},\n                            },\n                        )\n                        hops_data.append(final_hop_pre_ip)\n\n                        # encrypt encrypted_data_tlv here\n                        for i in range(len(hops_data)):\n                            encrypted_recipient_data = encrypt_onionmsg_data_tlv(\n                                shared_secret=hop_shared_secrets[i],\n                                **hops_data[i].blind_fields)\n                            payload = dict(hops_data[i].payload)\n                            payload['encrypted_recipient_data'] = {\n                                'encrypted_recipient_data': encrypted_recipient_data\n                            }\n                            hops_data[i] = dataclasses.replace(hops_data[i], payload=payload)\n\n                        path_key = ecc.ECPrivkey(session_key).get_public_key_bytes()\n\n            # append (remaining) blinded path and payload\n            blinded_path_blinded_ids = []\n            for i, onionmsg_hop in enumerate(remaining_blinded_path):\n                blinded_path_blinded_ids.append(onionmsg_hop.get('blinded_node_id'))\n                payload = {\n                    'encrypted_recipient_data': {'encrypted_recipient_data': onionmsg_hop['encrypted_recipient_data']}\n                }\n                if i == len(remaining_blinded_path) - 1:  # final hop\n                    payload.update(destination_payload)\n                hop = OnionHopsDataSingle(tlv_stream_name='onionmsg_tlv', payload=payload)\n                hops_data.append(hop)\n\n            payment_path_pubkeys = blinded_node_ids + blinded_path_blinded_ids\n            hop_shared_secrets, _ = get_shared_secrets_along_route(payment_path_pubkeys, session_key)\n            encrypt_hops_recipient_data('onionmsg_tlv', hops_data, hop_shared_secrets)\n            packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, onion_message=True)\n            packet_b = packet.to_bytes()\n\n    else:  # node pubkey\n        pubkey = node_id_or_blinded_path\n\n        if lnwallet.node_keypair.pubkey == pubkey:\n            raise Exception('cannot send to myself')\n\n        hops_data = []\n        peer = lnwallet.lnpeermgr.get_peer_by_pubkey(pubkey)\n\n        if peer:\n            # destination is our direct peer, no need to route-find\n            path = [PathEdge(short_channel_id=None, start_node=None, end_node=pubkey)]\n        else:\n            path = create_onion_message_route_to(lnwallet, pubkey)\n\n            # first edge must be to our peer\n            peer = lnwallet.lnpeermgr.get_peer_by_pubkey(path[0].end_node)\n            assert peer, 'first hop not a peer'\n\n            hops_data = [\n                OnionHopsDataSingle(\n                    tlv_stream_name='onionmsg_tlv',\n                    blind_fields={'next_node_id': {'node_id': x.end_node}},\n                ) for x in path[1:]\n            ]\n\n        final_hop = OnionHopsDataSingle(\n            tlv_stream_name='onionmsg_tlv',\n            payload=destination_payload,\n        )\n\n        hops_data.append(final_hop)\n\n        payment_path_pubkeys = [edge.end_node for edge in path]\n\n        hop_shared_secrets, blinded_node_ids = get_shared_secrets_along_route(payment_path_pubkeys, session_key)\n        encrypt_hops_recipient_data('onionmsg_tlv', hops_data, hop_shared_secrets)\n        packet = new_onion_packet(blinded_node_ids, session_key, hops_data)\n        packet_b = packet.to_bytes()\n\n        path_key = ecc.ECPrivkey(session_key).get_public_key_bytes()\n\n    peer.send_message(\n        \"onion_message\",\n        path_key=path_key,\n        len=len(packet_b),\n        onion_message_packet=packet_b\n    )\n\n\ndef get_blinded_reply_paths(\n        lnwallet: 'LNWallet',\n        path_id: bytes,\n        *,\n        max_paths: int = REQUEST_REPLY_PATHS_MAX,\n        preferred_node_id: bytes = None\n) -> Sequence[dict]:\n    \"\"\"construct a list of blinded reply_paths.\n       current logic:\n       - uses current onion_message capable channel peers if exist\n       - otherwise, uses current onion_message capable peers\n       - prefers preferred_node_id if given\n       - reply_path introduction points are direct peers only (TODO: longer reply paths)\"\"\"\n    # TODO: build longer paths and/or add dummy hops to increase privacy\n    my_active_channels = [chan for chan in lnwallet.channels.values() if chan.is_active()]\n    my_onionmsg_channels = [chan for chan in my_active_channels if lnwallet.lnpeermgr.get_peer_by_pubkey(chan.node_id) and\n                            lnwallet.lnpeermgr.get_peer_by_pubkey(chan.node_id).their_features.supports(LnFeatures.OPTION_ONION_MESSAGE_OPT)]\n    my_onionmsg_peers = [peer for peer in lnwallet.lnpeermgr.peers.values() if peer.their_features.supports(LnFeatures.OPTION_ONION_MESSAGE_OPT)]\n\n    result = []\n    mynodeid = lnwallet.node_keypair.pubkey\n    mydata = {'path_id': {'data': path_id}}  # same path_id used in every reply path\n    if len(my_onionmsg_channels):\n        # randomize list, but prefer preferred_node_id\n        rchans = sorted(my_onionmsg_channels, key=lambda x: random() if x.node_id != preferred_node_id else 0)\n        for chan in rchans[:max_paths]:\n            blinded_path = create_blinded_path(os.urandom(32), [chan.node_id, mynodeid], mydata)\n            result.append(blinded_path)\n    elif len(my_onionmsg_peers):\n        # randomize list, but prefer preferred_node_id\n        rpeers = sorted(my_onionmsg_peers, key=lambda x: random() if x.pubkey != preferred_node_id else 0)\n        for peer in rpeers[:max_paths]:\n            blinded_path = create_blinded_path(os.urandom(32), [peer.pubkey, mynodeid], mydata)\n            result.append(blinded_path)\n\n    return result\n\n\nclass Timeout(Exception): pass\n\n\nclass OnionMessageManager(Logger):\n    \"\"\"handle state around onion message sends and receives.\n    - one instance per (ln)wallet\n    - association between onion message and their replies\n    - manage re-send attempts while iterating over possible routes. Onion messages are unreliable\n      and fail silently if they don't reach their destination (or the reply gets dropped along the route back),\n      so the BOLT-4 spec suggests to send multiple messages, each with a different route to the introduction point).\n    - forwards are best-effort. They should not need retrying, but a queue is used to limit the pacing of forwarding,\n      and limiting the number of outstanding forwards. Any onion message forwards arriving when the forward queue\n      is full will be dropped.\n\n    TODO: iterate through routes for each request\"\"\"\n\n    SLEEP_DELAY = 1\n    REQUEST_REPLY_TIMEOUT = 30\n    REQUEST_REPLY_RETRY_DELAY = 5\n    FORWARD_RETRY_TIMEOUT = 4\n    FORWARD_RETRY_DELAY = 2\n    FORWARD_MAX_QUEUE = 3\n\n    class Request(NamedTuple):\n        future: asyncio.Future\n        payload: dict\n        node_id_or_blinded_path: bytes\n\n    def __init__(self, lnwallet: 'LNWallet'):\n        Logger.__init__(self)\n        self.network = None  # type: Optional['Network']\n        self.taskgroup = None  # type: OldTaskGroup\n        self.lnwallet = lnwallet\n        self.pending = {}  # type: dict[bytes, OnionMessageManager.Request]\n        self.pending_lock = threading.Lock()\n        self.send_queue = asyncio.PriorityQueue()\n        self.forward_queue = asyncio.PriorityQueue()\n\n    def start_network(self, *, network: 'Network') -> None:\n        assert network\n        assert self.network is None, \"already started\"\n        self.network = network\n        self.taskgroup = OldTaskGroup()\n        asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)\n\n    @log_exceptions\n    async def main_loop(self) -> None:\n        self.logger.info(\"starting taskgroup.\")\n        async with self.taskgroup as group:\n            await group.spawn(self.process_send_queue())\n            await group.spawn(self.process_forward_queue())\n        self.logger.info(\"taskgroup stopped.\")\n\n    async def stop(self) -> None:\n        if self.taskgroup:\n            await self.taskgroup.cancel_remaining()\n\n    async def process_forward_queue(self) -> None:\n        while True:\n            scheduled, expires, onion_packet, blinding, node_id = await self.forward_queue.get()\n            if expires <= now():\n                self.logger.debug(f'forward expired {node_id=}')\n                continue\n            if scheduled > now():\n                # return to queue\n                self.forward_queue.put_nowait((scheduled, expires, onion_packet, blinding, node_id))\n                await asyncio.sleep(self.SLEEP_DELAY)  # sleep here, as the first queue item wasn't due yet\n                continue\n\n            try:\n                onion_packet_b = onion_packet.to_bytes()\n                next_peer = self.lnwallet.lnpeermgr.get_peer_by_pubkey(node_id)\n\n                if not next_peer.their_features.supports(LnFeatures.OPTION_ONION_MESSAGE_OPT):\n                    self.logger.debug('forward dropped, next peer is not ONION_MESSAGE capable')\n                    continue\n\n                next_peer.send_message(\n                    \"onion_message\",\n                    path_key=blinding,\n                    len=len(onion_packet_b),\n                    onion_message_packet=onion_packet_b\n                )\n            except BaseException as e:\n                self.logger.debug(f'error while sending {node_id=} e={e!r}')\n                # TODO: it is debatable whether we want to retry a forward.\n                self.forward_queue.put_nowait((now() + self.FORWARD_RETRY_DELAY, expires, onion_packet, blinding, node_id))\n\n    def submit_forward(\n            self, *,\n            onion_packet: OnionPacket,\n            blinding: bytes,\n            node_id: bytes) -> None:\n        if self.forward_queue.qsize() >= self.FORWARD_MAX_QUEUE:\n            self.logger.debug('forward queue full, dropping packet')\n            return\n        expires = now() + self.FORWARD_RETRY_TIMEOUT\n        queueitem = (now(), expires, onion_packet, blinding, node_id)\n        self.forward_queue.put_nowait(queueitem)\n\n    async def process_send_queue(self) -> None:\n        while True:\n            scheduled, expires, key = await self.send_queue.get()\n            req = self.pending.get(key)\n            if req is None:\n                self.logger.debug(f'no data for key {key=}')\n                continue\n            if req.future.done():\n                self.logger.debug(f'has result! {key=}')\n                continue\n            if expires <= now():\n                self.logger.debug(f'expired {key=}')\n                req.future.set_exception(Timeout())\n                continue\n            if scheduled > now():\n                # return to queue\n                self.logger.debug(f'return to queue {key=}, {scheduled - now()}')\n                self.send_queue.put_nowait((scheduled, expires, key))\n                await asyncio.sleep(self.SLEEP_DELAY)  # sleep here, as the first queue item wasn't due yet\n                continue\n            try:\n                self._send_pending_message(key)\n            except BaseException as e:\n                self.logger.debug(f'error while sending {key=} {e!r}')\n                req.future.set_exception(copy.copy(e))\n                # NOTE: above, when passing the caught exception instance e directly it leads to GeneratorExit() in\n                if isinstance(e, NoRouteFound) and e.peer_address:\n                    await self.lnwallet.lnpeermgr.add_peer(str(e.peer_address))\n            else:\n                self.logger.debug(f'resubmit {key=}')\n                self.send_queue.put_nowait((now() + self.REQUEST_REPLY_RETRY_DELAY, expires, key))\n\n    def _remove_pending_message(self, key: bytes) -> None:\n        with self.pending_lock:\n            if key in self.pending:\n                del self.pending[key]\n\n    def submit_send(\n            self, *,\n            payload: dict,\n            node_id_or_blinded_path: bytes,\n            key: bytes = None) -> 'Task':\n        \"\"\"Add onion message to queue for sending. Queued onion message payloads\n           are supplied with a path_id and a reply_path to determine which request\n           corresponds with arriving replies.\n\n           If caller has provided 'reply_path' in payload, caller should also provide associating key.\n\n           :return: returns awaitable task\"\"\"\n        if not key:\n            key = os.urandom(8)\n        assert type(key) is bytes and len(key) >= 8\n\n        self.logger.debug(f'submit_send {key=} {payload=} {node_id_or_blinded_path=}')\n\n        req = OnionMessageManager.Request(\n            future=asyncio.Future(),\n            payload=payload,\n            node_id_or_blinded_path=node_id_or_blinded_path\n        )\n        with self.pending_lock:\n            if key in self.pending:\n                raise Exception(f'{key=} already exists!')\n            self.pending[key] = req\n\n        # tuple = (when to process, when it expires, key)\n        expires = now() + self.REQUEST_REPLY_TIMEOUT\n        queueitem = (now(), expires, key)\n        self.send_queue.put_nowait(queueitem)\n        task = asyncio.create_task(self._wait_task(key, req.future))\n        return task\n\n    async def _wait_task(self, key: bytes, future: asyncio.Future):\n        try:\n            return await future\n        finally:\n            self._remove_pending_message(key)\n\n    def _send_pending_message(self, key: bytes) -> None:\n        \"\"\"adds reply_path to payload\"\"\"\n        req = self.pending.get(key)\n        payload = req.payload\n        node_id_or_blinded_path = req.node_id_or_blinded_path\n        self.logger.debug(f'send_pending_message {key=} {payload=} {node_id_or_blinded_path=}')\n\n        final_payload = copy.deepcopy(payload)\n\n        if 'reply_path' not in final_payload:\n            # unless explicitly set in payload, generate reply_path here\n            path_id = self._path_id_from_payload_and_key(payload, key)\n            reply_paths = get_blinded_reply_paths(self.lnwallet, path_id, max_paths=1)\n            if not reply_paths:\n                raise Exception(f'Could not create a reply_path for {key=}. No active peers?')\n\n            final_payload['reply_path'] = {'path': reply_paths}\n\n        # TODO: we should try alternate paths when retrying, this is currently not done.\n        # (send_onion_message_to decides path, without knowledge of prev attempts)\n        send_onion_message_to(self.lnwallet, node_id_or_blinded_path, final_payload)\n\n    def _path_id_from_payload_and_key(self, payload: dict, key: bytes) -> bytes:\n        # TODO: use payload to determine prefix?\n        return b'electrum' + key\n\n    def _get_request_for_path_id(self, recipient_data: dict) -> Optional[Request]:\n        path_id = recipient_data.get('path_id', {}).get('data')\n        if not path_id:\n            return None\n        if not path_id[:8] == b'electrum':\n            self.logger.warning('not a reply to our request (unknown path_id prefix)')\n            return None\n        key = path_id[8:]\n        req = self.pending.get(key)\n        if req is None:\n            self.logger.warning('not a reply to our request (unknown request)')\n        return req\n\n    def on_onion_message_received(self, recipient_data: dict, payload: dict) -> None:\n        # we are destination, sanity checks\n        # - if `encrypted_data_tlv` contains `allowed_features`:\n        #   - MUST ignore the message if:\n        #     - `encrypted_data_tlv.allowed_features.features` contains an unknown feature bit (even if it is odd).\n        #     - the message uses a feature not included in `encrypted_data_tlv.allowed_features.features`.\n        if 'allowed_features' in recipient_data:\n            # Note: These checks will be usecase specific (e.g. BOLT12) and probably should be checked\n            # by consumers of the message.\n            self.logger.debug(f'allowed_features={recipient_data[\"allowed_features\"].get(\"features\", b\"\").hex()}')\n\n        # - if `path_id` is set and corresponds to a path the reader has previously published in a `reply_path`:\n        #   - if the onion message is not a reply to that previous onion:\n        #     - MUST ignore the onion message\n        req = self._get_request_for_path_id(recipient_data)\n        if req is None:\n            # unsolicited onion_message\n            self.on_onion_message_received_unsolicited(recipient_data, payload)\n        else:\n            self.on_onion_message_received_reply(req, recipient_data, payload)\n\n    def on_onion_message_received_reply(self, request: Request, recipient_data: dict, payload: dict) -> None:\n        assert request is not None, 'Request is mandatory'\n        request.future.set_result((recipient_data, payload))\n\n    def on_onion_message_received_unsolicited(self, recipient_data: dict, payload: dict) -> None:\n        self.logger.debug('unsolicited onion_message received')\n        self.logger.debug(f'payload: {payload!r}')\n\n        # This func currently only accepts simple text 'message' payload, a.k.a 'unknown_tag_1'\n        # in the bolt-4 test vectors.\n        #\n        # TODO: for BOLT-12, handle invoice_request here, which should correspond with a previously generated Offer.\n        # as this is not strictly part of BOLT-4, we should probably create a registration mechanism\n        # for various types of payloads, so we can let external code plug into onion messages\n        # e.g. via a decorator, something like\n        # @onion_message_request_handler(payload_key='invoice_request') for BOLT12 invoice requests.\n\n        if 'message' not in payload:\n            self.logger.error('Unsupported onion message payload')\n            return\n\n        if 'text' not in payload['message'] or not isinstance(payload['message']['text'], bytes):\n            self.logger.error('Malformed \\'message\\' payload')\n            return\n\n        try:\n            text = payload['message']['text'].decode('utf-8')\n        except Exception as e:\n            self.logger.error(f'Malformed \\'message\\' payload: {e!r}')\n            return\n\n        self.logger.info(f'onion message with text received: {text}')\n\n    def on_onion_message_forward(\n            self,\n            recipient_data: dict,\n            onion_packet: OnionPacket,\n            blinding: bytes,\n            shared_secret: bytes\n    ) -> None:\n        if recipient_data.get('path_id'):\n            self.logger.error('cannot forward onion_message, path_id in encrypted_data_tlv')\n            return\n\n        next_node_id = recipient_data.get('next_node_id')\n        if not next_node_id:\n            self.logger.error('cannot forward onion_message, next_node_id missing in encrypted_data_tlv')\n            return\n        next_node_id = next_node_id['node_id']\n\n        is_dummy_hop = False\n        if next_node_id == self.lnwallet.node_keypair.pubkey:\n            self.logger.debug('dummy hop')\n            is_dummy_hop = True\n        else:\n            # not a dummy hop, check if we are configured to forward\n            if not self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS:\n                self.logger.info(\n                    'onion_message dropped (not forwarding due to lightning_forward_payments config option disabled')\n                return\n            # is next_node one of our peers?\n            next_peer = self.lnwallet.lnpeermgr.get_peer_by_pubkey(next_node_id)\n            if not next_peer:\n                self.logger.info(f'next node {next_node_id.hex()} not a peer, dropping message')\n                return\n\n        # blinding override?\n        next_path_key_override = recipient_data.get('next_path_key_override')\n        if next_path_key_override:\n            next_path_key = next_path_key_override.get('path_key')\n        else:\n            # E_i+1=SHA256(E_i||ss_i) * E_i\n            blinding_factor = sha256(blinding + shared_secret)\n            blinding_factor_int = int.from_bytes(blinding_factor, byteorder=\"big\")\n            next_public_key_int = ecc.ECPubkey(blinding) * blinding_factor_int\n            next_path_key = next_public_key_int.get_public_key_bytes()\n\n        if is_dummy_hop:\n            self.process_onion_message_packet(next_path_key, onion_packet)\n            return\n\n        self.submit_forward(onion_packet=onion_packet, blinding=next_path_key, node_id=next_node_id)\n\n    def on_onion_message(self, payload: dict) -> None:\n        \"\"\"handle arriving onion_message.\"\"\"\n        path_key = payload.get('path_key')\n        if not path_key:\n            self.logger.error('missing path_key')\n            return\n        packet = payload.get('onion_message_packet')\n        if payload.get('len', 0) != len(packet):\n            self.logger.error('invalid/missing length')\n            return\n\n        self.logger.debug('handling onion message')\n\n        onion_packet = OnionPacket.from_bytes(packet)\n        self.process_onion_message_packet(path_key, onion_packet)\n\n    def process_onion_message_packet(self, blinding: bytes, onion_packet: OnionPacket) -> None:\n        our_privkey = blinding_privkey(self.lnwallet.node_keypair.privkey, blinding)\n        processed_onion_packet = process_onion_packet(onion_packet, our_privkey, is_onion_message=True, tlv_stream_name='onionmsg_tlv')\n        payload = processed_onion_packet.hop_data.payload\n\n        self.logger.debug(f'onion peeled: {processed_onion_packet!r}')\n\n        if not processed_onion_packet.are_we_final:\n            if any([x not in ['encrypted_recipient_data'] for x in payload.keys()]):\n                self.logger.error('unexpected data in payload')  # non-final nodes only encrypted_recipient_data\n                return\n\n        # decrypt\n        shared_secret = get_ecdh(self.lnwallet.node_keypair.privkey, blinding)\n        recipient_data = decrypt_onionmsg_data_tlv(\n            shared_secret=shared_secret,\n            encrypted_recipient_data=payload['encrypted_recipient_data']['encrypted_recipient_data']\n        )\n\n        self.logger.debug(f'parsed recipient_data: {recipient_data!r}')\n\n        if processed_onion_packet.are_we_final:\n            self.on_onion_message_received(recipient_data, payload)\n        else:\n            self.on_onion_message_forward(recipient_data, processed_onion_packet.next_packet, blinding, shared_secret)\n"
  },
  {
    "path": "electrum/payment_identifier.py",
    "content": "import asyncio\nimport time\nimport urllib\nimport re\nfrom decimal import Decimal, InvalidOperation\nfrom enum import IntEnum\nfrom typing import NamedTuple, Optional, Callable, List, TYPE_CHECKING, Tuple, Union\n\nfrom . import bitcoin\nfrom .contacts import AliasNotFoundException\nfrom .i18n import _\nfrom .invoices import Invoice\nfrom .logging import Logger\nfrom .util import parse_max_spend, InvoiceError\nfrom .util import get_asyncio_loop, log_exceptions\nfrom .transaction import PartialTxOutput\nfrom .lnurl import (decode_lnurl, request_lnurl, callback_lnurl, LNURLError,\n                    lightning_address_to_url, try_resolve_lnurlpay, LNURL6Data,\n                    LNURL3Data, LNURLData)\nfrom .bitcoin import opcodes, construct_script\nfrom .lnaddr import LnInvoiceException\nfrom .lnutil import IncompatibleOrInsaneFeatures\nfrom .bip21 import parse_bip21_URI, InvalidBitcoinURI, LIGHTNING_URI_SCHEME, BITCOIN_BIP21_URI_SCHEME\nfrom .segwit_addr import bech32_decode\nfrom . import paymentrequest\n\nif TYPE_CHECKING:\n    from .wallet import Abstract_Wallet\n    from .transaction import Transaction\n\n\ndef maybe_extract_bech32_lightning_payment_identifier(data: str) -> Optional[str]:\n    data = remove_uri_prefix(data, prefix=LIGHTNING_URI_SCHEME)\n    if not data.startswith('ln'):\n        return None\n    decoded_bech32 = bech32_decode(data, ignore_long_length=True)\n    if not decoded_bech32.hrp or not decoded_bech32.data:\n        return None\n    return data\n\n\ndef remove_uri_prefix(data: str, *, prefix: str) -> str:\n    assert isinstance(data, str) and isinstance(prefix, str)\n    data = data.lower().strip()\n    data = data.removeprefix(prefix + ':')\n    return data\n\n\nRE_ALIAS = r'(.*?)\\s*\\<([0-9A-Za-z]{1,})\\>'\nRE_EMAIL = r'\\b[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+\\.)+[A-Z|a-z]{2,7}\\b'\nRE_DOMAIN = r'\\b([A-Za-z0-9-]+\\.)+[A-Z|a-z]{2,7}\\b'\nRE_SCRIPT_FN = r'script\\((.*)\\)'\n\n\nclass PaymentIdentifierState(IntEnum):\n    EMPTY = 0               # Initial state.\n    INVALID = 1             # Unrecognized PI\n    AVAILABLE = 2           # PI contains a payable destination\n                            # payable means there's enough addressing information to submit to one\n                            # of the channels Electrum supports (on-chain, lightning)\n    NEED_RESOLVE = 3        # PI contains a recognized destination format, but needs an online resolve step\n    LNURLP_FINALIZE = 4     # PI contains a resolved LNURLp, but needs amount and comment to resolve to a bolt11\n    LNURLW_FINALIZE = 5     # PI contains resolved LNURLw, user needs to enter amount and initiate withdraw\n    MERCHANT_NOTIFY = 6     # PI contains a valid payment request and on-chain destination. It should notify\n                            # the merchant payment processor of the tx after on-chain broadcast,\n                            # and supply a refund address (bip70)\n    MERCHANT_ACK = 7        # PI notified merchant. nothing to be done.\n    ERROR = 50              # generic error\n    NOT_FOUND = 51          # PI contains a recognized destination format, but resolve step was unsuccessful\n    MERCHANT_ERROR = 52     # PI failed notifying the merchant after broadcasting onchain TX\n    INVALID_AMOUNT = 53     # Specified amount not accepted\n\n\nclass PaymentIdentifierType(IntEnum):\n    UNKNOWN = 0\n    SPK = 1\n    BIP21 = 2\n    BIP70 = 3\n    MULTILINE = 4\n    BOLT11 = 5\n    LNURL = 6  # before the resolve it's unknown if pi is LNURLP or LNURLW\n    LNURLP = 7\n    LNURLW = 8\n    EMAILLIKE = 9\n    OPENALIAS = 10\n    LNADDR = 11\n    DOMAINLIKE = 12\n\n\nclass FieldsForGUI(NamedTuple):\n    recipient: Optional[str]\n    amount: Optional[int]\n    description: Optional[str]\n    validated: Optional[bool]\n    comment: Optional[int]\n    amount_range: Optional[Tuple[int, int]]\n\n\nclass PaymentIdentifier(Logger):\n    \"\"\"\n    Takes:\n        * bitcoin addresses or script\n        * paytomany csv\n        * openalias\n        * bip21 URI\n        * lightning-URI (containing bolt11 or lnurl)\n        * bolt11 invoice\n        * lnurl\n        * lightning address\n    \"\"\"\n\n    def __init__(self, wallet: Optional['Abstract_Wallet'], text: str):\n        Logger.__init__(self)\n        self._state = PaymentIdentifierState.EMPTY\n        self.wallet = wallet\n        self.contacts = wallet.contacts if wallet is not None else None\n        self.config = wallet.config if wallet is not None else None\n        self.text = text.strip()\n        self._type = PaymentIdentifierType.UNKNOWN\n        self.error = None    # if set, GUI should show error and stop\n        self.warning = None  # if set, GUI should ask user if they want to proceed\n        # more than one of those may be set\n        self.multiline_outputs = None\n        self._is_max = False\n        self.bolt11 = None  # type: Optional[Invoice]\n        self.bip21 = None\n        self.spk = None\n        self.spk_is_address = False\n        #\n        self.emaillike = None\n        self.domainlike = None\n        self.openalias_data = None\n        #\n        self.bip70 = None\n        self.bip70_data = None\n        self.merchant_ack_status = None\n        self.merchant_ack_message = None\n        #\n        self.lnurl = None  # type: Optional[str]\n        self.lnurl_data = None # type: Optional[LNURLData]\n\n        self.parse(text)\n\n    @property\n    def type(self):\n        return self._type\n\n    def set_state(self, state: 'PaymentIdentifierState'):\n        self.logger.debug(f'PI state {self._state.name} -> {state.name}')\n        self._state = state\n\n    @property\n    def state(self):\n        return self._state\n\n    def need_resolve(self):\n        return self._state == PaymentIdentifierState.NEED_RESOLVE\n\n    def need_finalize(self):\n        return self._state == PaymentIdentifierState.LNURLP_FINALIZE\n\n    def need_merchant_notify(self):\n        return self._state == PaymentIdentifierState.MERCHANT_NOTIFY\n\n    def is_valid(self):\n        return self._state not in [PaymentIdentifierState.INVALID, PaymentIdentifierState.EMPTY]\n\n    def is_available(self):\n        return self._state in [PaymentIdentifierState.AVAILABLE]\n\n    def is_lightning(self):\n        return bool(self.lnurl) or bool(self.bolt11)\n\n    def is_onchain(self):\n        if self._type in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE, PaymentIdentifierType.BIP70,\n                          PaymentIdentifierType.OPENALIAS]:\n            return True\n        if self._type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNADDR]:\n            return bool(self.bolt11) and bool(self.bolt11.get_address())\n        if self._type == PaymentIdentifierType.BIP21:\n            return bool(self.bip21.get('address', None)) or (bool(self.bolt11) and bool(self.bolt11.get_address()))\n\n    def is_multiline(self):\n        return bool(self.multiline_outputs)\n\n    def is_multiline_max(self):\n        return self.is_multiline() and self._is_max\n\n    def is_amount_locked(self):\n        if self._type == PaymentIdentifierType.BIP21:\n            return bool(self.bip21.get('amount'))\n        elif self._type == PaymentIdentifierType.BIP70:\n            return not self.need_resolve()  # always fixed after resolve?\n        elif self._type == PaymentIdentifierType.BOLT11:\n            return bool(self.bolt11.get_amount_sat())\n        elif self._type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR]:\n            # amount limits known after resolve, might be specific amount or locked to range\n            if self.need_resolve():\n                return False\n            if self.need_finalize():\n                self.logger.debug(f'lnurl f {self.lnurl_data.min_sendable_sat}-{self.lnurl_data.max_sendable_sat}')\n                return not (self.lnurl_data.min_sendable_sat < self.lnurl_data.max_sendable_sat)\n            return True\n        elif self._type == PaymentIdentifierType.MULTILINE:\n            return True\n        else:\n            return False\n\n    def is_error(self) -> bool:\n        return self._state >= PaymentIdentifierState.ERROR\n\n    def get_error(self) -> str:\n        return self.error\n\n    def parse(self, text: str):\n        # parse text, set self._type and self.error\n        text = text.strip()\n        if not text:\n            return\n        if outputs := self._parse_as_multiline(text):\n            self._type = PaymentIdentifierType.MULTILINE\n            self.multiline_outputs = outputs\n            if self.error:\n                self.set_state(PaymentIdentifierState.INVALID)\n            else:\n                self.set_state(PaymentIdentifierState.AVAILABLE)\n        elif invoice_or_lnurl := maybe_extract_bech32_lightning_payment_identifier(text):\n            if invoice_or_lnurl.startswith('lnurl'):\n                self._type = PaymentIdentifierType.LNURL\n                try:\n                    self.lnurl = decode_lnurl(invoice_or_lnurl)\n                    self.set_state(PaymentIdentifierState.NEED_RESOLVE)\n                except Exception as e:\n                    self.error = _(\"Error parsing LNURL\") + f\":\\n{e}\"\n                    self.set_state(PaymentIdentifierState.INVALID)\n                    return\n            else:\n                self._type = PaymentIdentifierType.BOLT11\n                try:\n                    self.bolt11 = Invoice.from_bech32(invoice_or_lnurl)\n                except InvoiceError as e:\n                    self.error = self._get_error_from_invoiceerror(e)\n                    self.set_state(PaymentIdentifierState.INVALID)\n                    self.logger.debug(f'Exception cause {e.args!r}')\n                    return\n                self.set_state(PaymentIdentifierState.AVAILABLE)\n        elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):\n            try:\n                out = parse_bip21_URI(text)\n            except InvalidBitcoinURI as e:\n                self.error = _(\"Error parsing URI\") + f\":\\n{e}\"\n                self.set_state(PaymentIdentifierState.INVALID)\n                return\n            self.bip21 = out\n            self.bip70 = out.get('r')\n            if self.bip70:\n                self._type = PaymentIdentifierType.BIP70\n                self.set_state(PaymentIdentifierState.NEED_RESOLVE)\n            else:\n                self._type = PaymentIdentifierType.BIP21\n                # check optional lightning in bip21, set self.bolt11 if valid\n                bolt11 = out.get('lightning')\n                if bolt11:\n                    try:\n                        self.bolt11 = Invoice.from_bech32(bolt11)\n                        # carry BIP21 onchain address in Invoice.outputs in case bolt11 doesn't contain a fallback\n                        # address but the BIP21 URI has one.\n                        if bip21_address := self.bip21.get('address'):\n                            amount = self.bip21.get('amount', 0)\n                            self.bolt11.outputs = [PartialTxOutput.from_address_and_value(bip21_address, amount)]\n                    except InvoiceError as e:\n                        self.logger.debug(self._get_error_from_invoiceerror(e))\n                elif not self.bip21.get('address'):\n                    # no address and no bolt11, invalid\n                    self.set_state(PaymentIdentifierState.INVALID)\n                    return\n                self.set_state(PaymentIdentifierState.AVAILABLE)\n        elif self.parse_output(text)[0]:\n            scriptpubkey, is_address = self.parse_output(text)\n            self._type = PaymentIdentifierType.SPK\n            self.spk = scriptpubkey\n            self.spk_is_address = is_address\n            self.set_state(PaymentIdentifierState.AVAILABLE)\n        elif self.contacts and (contact := self.contacts.by_name(text)):\n            if contact['type'] == 'address':\n                self._type = PaymentIdentifierType.BIP21\n                self.bip21 = {\n                    'address': contact['address'],\n                    'label': contact['name']\n                }\n                self.set_state(PaymentIdentifierState.AVAILABLE)\n            elif contact['type'] in ('openalias', 'lnaddress'):\n                self._type = PaymentIdentifierType.EMAILLIKE\n                self.emaillike = contact['address']\n                self.set_state(PaymentIdentifierState.NEED_RESOLVE)\n        elif re.match(RE_EMAIL, (maybe_emaillike := remove_uri_prefix(text, prefix=LIGHTNING_URI_SCHEME))):\n            self._type = PaymentIdentifierType.EMAILLIKE\n            self.emaillike = maybe_emaillike\n            self.set_state(PaymentIdentifierState.NEED_RESOLVE)\n        elif re.match(RE_DOMAIN, text):\n            self._type = PaymentIdentifierType.DOMAINLIKE\n            self.domainlike = text\n            self.set_state(PaymentIdentifierState.NEED_RESOLVE)\n        elif self.error is None:\n            truncated_text = f\"{text[:100]}...\" if len(text) > 100 else text\n            self.error = f\"Unknown payment identifier:\\n{truncated_text}\"\n            self.set_state(PaymentIdentifierState.INVALID)\n\n    def resolve(self, *, on_finished: Callable[['PaymentIdentifier'], None]) -> None:\n        assert self._state == PaymentIdentifierState.NEED_RESOLVE\n        coro = self._do_resolve(on_finished=on_finished)\n        asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop())\n\n    @log_exceptions\n    async def _do_resolve(self, *, on_finished: Callable[['PaymentIdentifier'], None] = None):\n        try:\n            if self.emaillike or self.domainlike:\n                openalias_key = self.emaillike if self.emaillike else self.domainlike\n                openalias_task = asyncio.create_task(self.resolve_openalias(openalias_key))\n\n                # prefers lnurl over openalias if both are available\n                lnurl = lightning_address_to_url(self.emaillike) if self.emaillike else None\n                if lnurl is not None and (lnurl_result := await try_resolve_lnurlpay(lnurl)):\n                    openalias_task.cancel()\n                    self._type = PaymentIdentifierType.LNADDR\n                    self.lnurl = lnurl\n                    self.lnurl_data = lnurl_result\n                    self.set_state(PaymentIdentifierState.LNURLP_FINALIZE)\n                elif openalias_result := await openalias_task:\n                    self.openalias_data = openalias_result\n                    address = openalias_result.get('address')\n                    try:\n                        # this assertion error message is shown in the GUI\n                        assert bitcoin.is_address(address), f\"{_('Openalias address invalid')}: {address[:100]}\"\n                        scriptpubkey = bitcoin.address_to_script(address)\n                        self._type = PaymentIdentifierType.OPENALIAS\n                        self.spk = scriptpubkey\n                        self.set_state(PaymentIdentifierState.AVAILABLE)\n                    except Exception as e:\n                        self.error = str(e)\n                        self.set_state(PaymentIdentifierState.NOT_FOUND)\n                else:\n                    self.set_state(PaymentIdentifierState.NOT_FOUND)\n            elif self.bip70:\n                pr = await paymentrequest.get_payment_request(self.bip70)\n                if await pr.verify():\n                    self.bip70_data = pr\n                    self.set_state(PaymentIdentifierState.MERCHANT_NOTIFY)\n                else:\n                    self.error = pr.error\n                    self.set_state(PaymentIdentifierState.ERROR)\n            elif self.lnurl:\n                data = await request_lnurl(self.lnurl)\n                self.lnurl_data = data\n                if isinstance(data, LNURL6Data):\n                    self._type = PaymentIdentifierType.LNURLP\n                    self.set_state(PaymentIdentifierState.LNURLP_FINALIZE)\n                elif isinstance(data, LNURL3Data):\n                    self._type = PaymentIdentifierType.LNURLW\n                    self.set_state(PaymentIdentifierState.LNURLW_FINALIZE)\n                else:\n                    raise NotImplementedError(f\"Invalid LNURL type? {data=}\")\n                self.logger.debug(f'LNURL data: {data!r}')\n            else:\n                self.set_state(PaymentIdentifierState.ERROR)\n                return\n        except Exception as e:\n            self.error = str(e)\n            self.logger.error(f\"_do_resolve() got error: {e!r}\")\n            self.set_state(PaymentIdentifierState.ERROR)\n        finally:\n            if on_finished:\n                on_finished(self)\n\n    def finalize(\n        self,\n        *,\n        amount_sat: int = 0,\n        comment: str = None,\n        on_finished: Callable[['PaymentIdentifier'], None] = None,\n    ):\n        assert self._state == PaymentIdentifierState.LNURLP_FINALIZE\n        coro = self._do_finalize(amount_sat=amount_sat, comment=comment, on_finished=on_finished)\n        asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop())\n\n    @log_exceptions\n    async def _do_finalize(\n        self,\n        *,\n        amount_sat: int = None,\n        comment: str = None,\n        on_finished: Callable[['PaymentIdentifier'], None] = None,\n    ):\n        from .invoices import Invoice\n        try:\n            if not self.lnurl_data:\n                raise Exception(\"Unexpected missing LNURL data\")\n\n            if not (self.lnurl_data.min_sendable_sat <= amount_sat <= self.lnurl_data.max_sendable_sat):\n                self.error = _('Amount must be between {} and {} sat.').format(\n                    self.lnurl_data.min_sendable_sat, self.lnurl_data.max_sendable_sat)\n                self.set_state(PaymentIdentifierState.INVALID_AMOUNT)\n                return\n\n            if self.lnurl_data.comment_allowed == 0:\n                comment = None\n            params = {'amount': amount_sat * 1000}\n            if comment:\n                params['comment'] = comment\n\n            try:\n                invoice_data = await callback_lnurl(self.lnurl_data.callback_url, params=params)\n            except LNURLError as e:\n                self.error = f\"LNURL request encountered error: {e}\"\n                self.set_state(PaymentIdentifierState.ERROR)\n                return\n\n            bolt11_invoice = invoice_data.get('pr')\n            invoice = Invoice.from_bech32(bolt11_invoice)\n            if invoice.get_amount_sat() != amount_sat:\n                raise Exception(\"lnurl returned invoice with wrong amount\")\n            # this will change what is returned by get_fields_for_GUI\n            self.bolt11 = invoice\n            self.set_state(PaymentIdentifierState.AVAILABLE)\n        except Exception as e:\n            self.error = str(e)\n            self.logger.error(f\"_do_finalize() got error: {e!r}\")\n            self.set_state(PaymentIdentifierState.ERROR)\n        finally:\n            if on_finished:\n                on_finished(self)\n\n    def notify_merchant(\n        self,\n        *,\n        tx: 'Transaction',\n        refund_address: str,\n        on_finished: Callable[['PaymentIdentifier'], None] = None,\n    ):\n        assert self._state == PaymentIdentifierState.MERCHANT_NOTIFY\n        assert tx\n        assert refund_address\n        coro = self._do_notify_merchant(tx, refund_address, on_finished=on_finished)\n        asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop())\n\n    @log_exceptions\n    async def _do_notify_merchant(\n        self,\n        tx: 'Transaction',\n        refund_address: str,\n        *,\n        on_finished: Callable[['PaymentIdentifier'], None] = None,\n    ):\n        try:\n            if not self.bip70_data:\n                self.set_state(PaymentIdentifierState.ERROR)\n                return\n\n            ack_status, ack_msg = await self.bip70_data.send_payment_and_receive_paymentack(tx.serialize(), refund_address)\n            self.logger.info(f\"Payment ACK: {ack_status}. Ack message: {ack_msg}\")\n            self.merchant_ack_status = ack_status\n            self.merchant_ack_message = ack_msg\n            self.set_state(PaymentIdentifierState.MERCHANT_ACK)\n        except Exception as e:\n            self.error = str(e)\n            self.logger.error(f\"_do_notify_merchant() got error: {e!r}\")\n            self.set_state(PaymentIdentifierState.MERCHANT_ERROR)\n        finally:\n            if on_finished:\n                on_finished(self)\n\n    def get_onchain_outputs(self, amount):\n        if self.bip70:\n            return self.bip70_data.get_outputs()\n        elif self.multiline_outputs:\n            return self.multiline_outputs\n        elif self.spk:\n            return [PartialTxOutput(scriptpubkey=self.spk, value=amount)]\n        elif self.bip21:\n            address = self.bip21.get('address')\n            scriptpubkey, is_address = self.parse_output(address)\n            assert is_address  # unlikely, but make sure it is an address, not a script\n            return [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)]\n        else:\n            raise Exception('not onchain')\n\n    def _parse_as_multiline(self, text: str):\n        # filter out empty lines\n        lines = text.split('\\n')\n        lines = [i for i in lines if i]\n        is_multiline = len(lines) > 1\n        outputs = []  # type: List[PartialTxOutput]\n        errors = ''\n        total = 0\n        self._is_max = False\n        for i, line in enumerate(lines):\n            try:\n                output = self.parse_address_and_amount(line)\n                outputs.append(output)\n                if parse_max_spend(output.value):\n                    self._is_max = True\n                else:\n                    total += output.value\n            except Exception as e:\n                errors = f'{errors}line #{i}: {str(e)}\\n'\n                continue\n        if is_multiline and errors:\n            self.error = errors.strip() if errors else None\n        self.logger.debug(f'multiline: {outputs!r}, {self.error}')\n        return outputs\n\n    def parse_address_and_amount(self, line: str) -> PartialTxOutput:\n        try:\n            x, y = line.split(',')\n        except ValueError:\n            raise Exception(\"expected two comma-separated values: (address, amount)\") from None\n        scriptpubkey, is_address = self.parse_output(x)\n        if not scriptpubkey:\n            raise Exception('Invalid address')\n        amount = self.parse_amount(y)\n        return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)\n\n    def parse_output(self, x: str) -> Tuple[Optional[bytes], bool]:\n        try:\n            address = self.parse_address(x)\n            return bitcoin.address_to_script(address), True\n        except Exception as e:\n            pass\n        try:\n            m = re.match('^' + RE_SCRIPT_FN + '$', x)\n            script = self.parse_script(str(m.group(1)))\n            return script, False\n        except Exception as e:\n            pass\n\n        return None, False\n\n    def parse_script(self, x: str) -> bytes:\n        script = bytearray()\n        for word in x.split():\n            if word[0:3] == 'OP_':\n                opcode_int = opcodes[word]\n                script += construct_script([opcode_int])\n            else:\n                bytes.fromhex(word)  # to test it is hex data\n                script += construct_script([word])\n        return bytes(script)\n\n    def parse_amount(self, x: str) -> Union[str, int]:\n        x = x.strip()\n        if not x:\n            raise Exception(\"Amount is empty\")\n        if parse_max_spend(x):\n            return x\n        p = pow(10, self.config.BTC_AMOUNTS_DECIMAL_POINT)\n        try:\n            return int(p * Decimal(x))\n        except InvalidOperation:\n            raise Exception(\"Invalid amount\")\n\n    def parse_address(self, line: str):\n        r = line.strip()\n        m = re.match('^' + RE_ALIAS + '$', r)\n        address = str(m.group(2) if m else r)\n        assert bitcoin.is_address(address)\n        return address\n\n    def _get_error_from_invoiceerror(self, e: 'InvoiceError') -> str:\n        error = _(\"Error parsing Lightning invoice\") + f\":\\n{e!r}\"\n        if e.args and len(e.args):\n            arg = e.args[0]\n            if isinstance(arg, LnInvoiceException):\n                error = _(\"Error parsing Lightning invoice\") + f\":\\n{e}\"\n            elif isinstance(arg, IncompatibleOrInsaneFeatures):\n                error = _(\"Invoice requires unknown or incompatible Lightning feature\") + f\":\\n{e!r}\"\n        return error\n\n    def get_fields_for_GUI(self) -> FieldsForGUI:\n        recipient = None\n        amount = None\n        description = None\n        validated = None\n        comment = None\n        amount_range = None\n\n        if (self.emaillike or self.domainlike) and self.openalias_data:\n            key = self.emaillike if self.emaillike else self.domainlike\n            address = self.openalias_data.get('address')\n            name = self.openalias_data.get('name')\n            description = name\n            recipient = key + ' <' + address + '>'\n\n        elif self.bolt11:\n            recipient, amount, description = self._get_bolt11_fields()\n\n        elif self.lnurl and self.lnurl_data:\n            assert isinstance(self.lnurl_data, LNURL6Data), f\"{self.lnurl_data=}\"\n            domain = urllib.parse.urlparse(self.lnurl).netloc\n            recipient = f\"{self.lnurl_data.metadata_plaintext} <{domain}>\"\n            description = self.lnurl_data.metadata_plaintext\n            if self.lnurl_data.comment_allowed:\n                comment = self.lnurl_data.comment_allowed\n            if self.lnurl_data.min_sendable_sat:\n                amount = self.lnurl_data.min_sendable_sat\n                if self.lnurl_data.min_sendable_sat != self.lnurl_data.max_sendable_sat:\n                    amount_range = (self.lnurl_data.min_sendable_sat, self.lnurl_data.max_sendable_sat)\n\n        elif self.bip70 and self.bip70_data:\n            pr = self.bip70_data\n            if pr.error:\n                self.error = pr.error\n            else:\n                recipient = pr.get_requestor()\n                amount = pr.get_amount()\n                description = pr.get_memo()\n                validated = not pr.has_expired()\n\n        elif self.spk:\n            pass\n\n        elif self.multiline_outputs:\n            pass\n\n        elif self.bip21:\n            label = self.bip21.get('label')\n            address = self.bip21.get('address')\n            recipient = f'{label} <{address}>' if label else address\n            amount = self.bip21.get('amount')\n            description = self.bip21.get('message')\n            # TODO: use label as description? (not BIP21 compliant)\n            # if label and not description:\n            #     description = label\n\n        return FieldsForGUI(recipient=recipient, amount=amount, description=description,\n                            comment=comment, validated=validated, amount_range=amount_range)\n\n    def _get_bolt11_fields(self):\n        lnaddr = self.bolt11._lnaddr # TODO: improve access to lnaddr\n        pubkey = lnaddr.pubkey.serialize().hex()\n        for k, v in lnaddr.tags:\n            if k == 'd':\n                description = v\n                break\n        else:\n            description = ''\n        amount = lnaddr.get_amount_sat()\n        return pubkey, amount, description\n\n    async def resolve_openalias(self, key: str) -> Optional[dict]:\n        parts = key.split(sep=',')  # assuming single line\n        if parts and len(parts) > 0 and bitcoin.is_address(parts[0]):\n            return None\n        try:\n            data = await self.contacts.resolve(key)  # TODO: don't use contacts as delegate to resolve openalias, separate.\n            self.logger.debug(f'OA: {data!r}')\n            return data\n        except AliasNotFoundException as e:\n            self.logger.info(f'OpenAlias not found: {repr(e)}')\n            return None\n        except Exception as e:\n            self.logger.info(f'error resolving address/alias: {repr(e)}')\n            return None\n\n    def has_expired(self):\n        if self.bip70 and self.bip70_data:\n            return self.bip70_data.has_expired()\n        elif self.bolt11:\n            return self.bolt11.has_expired()\n        elif self.bip21:\n            expires = self.bip21.get('exp') + self.bip21.get('time') if self.bip21.get('exp') else 0\n            return bool(expires) and expires < time.time()\n        return False\n\n\ndef invoice_from_payment_identifier(\n    pi: 'PaymentIdentifier',\n    wallet: 'Abstract_Wallet',\n    amount_sat: Union[int, str],\n    message: str = None\n) -> Optional[Invoice]:\n    assert pi.state in [PaymentIdentifierState.AVAILABLE, PaymentIdentifierState.MERCHANT_NOTIFY]\n    assert pi.is_onchain() if amount_sat == '!' else True  # MAX should only be allowed if pi has onchain destination\n\n    if pi.is_lightning() and not amount_sat == '!':\n        invoice = pi.bolt11\n        if not invoice:\n            return\n        if invoice._lnaddr.get_amount_msat() is None:\n            invoice.set_amount_msat(int(amount_sat * 1000))\n        return invoice\n    else:\n        outputs = pi.get_onchain_outputs(amount_sat)\n        message = pi.bip21.get('message') if pi.bip21 else message\n        bip70_data = pi.bip70_data if pi.bip70 else None\n        return wallet.create_invoice(\n            outputs=outputs,\n            message=message,\n            pr=bip70_data,\n            URI=pi.bip21)\n\n\n# Note: this is only really used for bip70 to handle MECHANT_NOTIFY state from\n# a saved bip70 invoice.\n# TODO: reflect bip70-only in function name, or implement other types as well.\ndef payment_identifier_from_invoice(\n    wallet: 'Abstract_Wallet',\n    invoice: Invoice\n) -> Optional[PaymentIdentifier]:\n    if not invoice:\n        return\n    pi = PaymentIdentifier(wallet, '')\n    if invoice.bip70:\n        pi._type = PaymentIdentifierType.BIP70\n        pi.bip70_data = paymentrequest.PaymentRequest(bytes.fromhex(invoice.bip70))\n        pi.set_state(PaymentIdentifierState.MERCHANT_NOTIFY)\n        return pi\n    # else:\n    #     if invoice.outputs:\n    #         if len(invoice.outputs) > 1:\n    #             pi._type = PaymentIdentifierType.MULTILINE\n    #             pi.multiline_outputs = invoice.outputs\n    #             pi.set_state(PaymentIdentifierState.AVAILABLE)\n    #         else:\n    #             pi._type = PaymentIdentifierType.BIP21\n    #             params = {}\n    #             if invoice.exp:\n    #                 params['exp'] = str(invoice.exp)\n    #             if invoice.time:\n    #                 params['time'] = str(invoice.time)\n    #             pi.bip21 = create_bip21_uri(invoice.outputs[0].address, invoice.get_amount_sat(), invoice.message,\n    #                                         extra_query_params=params)\n    #             pi.set_state(PaymentIdentifierState.AVAILABLE)\n    #     elif invoice.is_lightning():\n    #         pi._type = PaymentIdentifierType.BOLT11\n    #         pi.bolt11 = invoice\n    #         pi.set_state(PaymentIdentifierState.AVAILABLE)\n    #     else:\n    #         return None\n    #     return pi\n"
  },
  {
    "path": "electrum/paymentrequest.proto",
    "content": "//\n// Simple Bitcoin Payment Protocol messages\n//\n// Use fields 1000+ for extensions;\n// to avoid conflicts, register extensions via pull-req at\n// https://github.com/bitcoin/bips/blob/master/bip-0070/extensions.mediawiki\n//\n\nsyntax = \"proto2\";\npackage payments;\noption java_package = \"org.bitcoin.protocols.payments\";\noption java_outer_classname = \"Protos\";\n\n// Generalized form of \"send payment to this/these bitcoin addresses\"\nmessage Output {\n        optional uint64 amount = 1 [default = 0]; // amount is integer-number-of-satoshis\n        required bytes script = 2; // usually one of the standard Script forms\n}\nmessage PaymentDetails {\n        optional string network = 1 [default = \"main\"]; // \"main\" or \"test\"\n        repeated Output outputs = 2;        // Where payment should be sent\n        required uint64 time = 3;           // Timestamp; when payment request created\n        optional uint64 expires = 4;        // Timestamp; when this request should be considered invalid\n        optional string memo = 5;           // Human-readable description of request for the customer\n        optional string payment_url = 6;    // URL to send Payment and get PaymentACK\n        optional bytes merchant_data = 7;   // Arbitrary data to include in the Payment message\n}\nmessage PaymentRequest {\n        optional uint32 payment_details_version = 1 [default = 1];\n        optional string pki_type = 2 [default = \"none\"];  // none / x509+sha256 / x509+sha1\n        optional bytes pki_data = 3;                      // depends on pki_type\n        required bytes serialized_payment_details = 4;    // PaymentDetails\n        optional bytes signature = 5;                     // pki-dependent signature\n}\nmessage X509Certificates {\n        repeated bytes certificate = 1;    // DER-encoded X.509 certificate chain\n}\nmessage Payment {\n        optional bytes merchant_data = 1;  // From PaymentDetails.merchant_data\n        repeated bytes transactions = 2;   // Signed transactions that satisfy PaymentDetails.outputs\n        repeated Output refund_to = 3;     // Where to send refunds, if a refund is necessary\n        optional string memo = 4;          // Human-readable message for the merchant\n}\nmessage PaymentACK {\n        required Payment payment = 1;      // Payment message that triggered this ACK\n        optional string memo = 2;          // human-readable message for customer\n}\n"
  },
  {
    "path": "electrum/paymentrequest.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2014 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport hashlib\nimport sys\nimport time\nfrom typing import Optional, List, TYPE_CHECKING\nimport asyncio\nimport urllib.parse\n\nimport certifi\nimport aiohttp\nimport electrum_ecc as ecc\n\n\ntry:\n    from . import paymentrequest_pb2 as pb2\nexcept ImportError:\n    sys.exit(\"Error: could not find paymentrequest_pb2.py. Create it with 'contrib/generate_payreqpb2.sh'\")\n\nfrom . import bitcoin, constants, util, transaction, x509, rsakey\nfrom .util import (bfh, make_aiohttp_session, error_text_bytes_to_safe_str, get_running_loop,\n                   get_asyncio_loop)\nfrom .invoices import Invoice, get_id_from_onchain_outputs\nfrom .bitcoin import address_to_script\nfrom .transaction import PartialTxOutput\nfrom .network import Network\nfrom .logging import get_logger\nfrom .contacts import Contacts\n\nif TYPE_CHECKING:\n    from .simple_config import SimpleConfig\n\n\n_logger = get_logger(__name__)\n\n\nREQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'}\nACK_HEADERS = {'Content-Type': 'application/bitcoin-payment', 'Accept': 'application/bitcoin-paymentack', 'User-Agent': 'Electrum'}\n\nca_path = certifi.where()\nca_list = None\nca_keyID = None\n\n\ndef load_ca_list():\n    global ca_list, ca_keyID\n    if ca_list is None:\n        ca_list, ca_keyID = x509.load_certificates(ca_path)\n\n\nasync def get_payment_request(url: str) -> 'PaymentRequest':\n    u = urllib.parse.urlparse(url)\n    error = None\n    if u.scheme in ('http', 'https'):\n        resp_content = None\n        try:\n            proxy = Network.get_instance().proxy\n            async with make_aiohttp_session(proxy, headers=REQUEST_HEADERS) as session:\n                async with session.get(url) as response:\n                    resp_content = await response.read()\n                    response.raise_for_status()\n                    # Guard against `bitcoin:`-URIs with invalid payment request URLs\n                    if \"Content-Type\" not in response.headers \\\n                    or response.headers[\"Content-Type\"] != \"application/bitcoin-paymentrequest\":\n                        data = None\n                        error = \"payment URL not pointing to a payment request handling server\"\n                    else:\n                        data = resp_content\n                    data_len = len(data) if data is not None else None\n                    _logger.info(f'fetched payment request {url} {data_len}')\n        except (aiohttp.ClientError, asyncio.TimeoutError) as e:\n            error = f\"Error while contacting payment URL: {url}.\\nerror type: {type(e)}\"\n            if isinstance(e, aiohttp.ClientResponseError):\n                error += f\"\\nGot HTTP status code {e.status}.\"\n                if resp_content:\n                    error_text_received = error_text_bytes_to_safe_str(resp_content)\n                    error_text_received = error_text_received[:400]\n                    error_oneline = ' -- '.join(error.split('\\n'))\n                    _logger.info(f\"{error_oneline} -- [DO NOT TRUST THIS MESSAGE] \"\n                                 f\"{repr(e)} text: {error_text_received}\")\n            data = None\n    else:\n        data = None\n        error = f\"Unknown scheme for payment request. URL: {url}\"\n    pr = PaymentRequest(data, error=error)\n    # do x509/dnssec verification now. we still expect the caller to at least check pr.error!\n    await pr.verify()\n    return pr\n\n\nclass PaymentRequest:\n\n    def __init__(self, data: bytes, *, error=None):\n        self.raw = data\n        self.error = error  # type: Optional[str]\n        self._verified_success = None  # caches result of _verify\n        self._verified_success_msg = None  # type: Optional[str]\n        self._parse(data)\n        self.requestor = None # known after verify\n        self.tx = None\n\n    def __str__(self):\n        return str(self.raw)\n\n    def _parse(self, r: bytes):\n        self.outputs = []  # type: List[PartialTxOutput]\n        if self.error:\n            return\n        try:\n            self.data = pb2.PaymentRequest()\n            self.data.ParseFromString(r)\n        except Exception:\n            self.error = \"cannot parse payment request\"\n            return\n        self.details = pb2.PaymentDetails()\n        self.details.ParseFromString(self.data.serialized_payment_details)\n        pr_network = self.details.network\n        client_network = 'test' if constants.net.TESTNET else 'main'\n        if pr_network != client_network:\n            self.error = (f'Payment request network \"{pr_network}\" does not'\n                          f' match client network \"{client_network}\".')\n            return\n        for o in self.details.outputs:\n            addr = transaction.get_address_from_output_script(o.script)\n            if not addr:\n                # TODO maybe rm restriction but then get_requestor and get_id need changes\n                self.error = \"only addresses are allowed as outputs\"\n                return\n            self.outputs.append(PartialTxOutput.from_address_and_value(addr, o.amount))\n        self.memo = self.details.memo\n        self.payment_url = self.details.payment_url\n\n    async def verify(self) -> bool:\n        # FIXME: we should enforce that this method was called before we attempt payment\n        # note: this method might do network requests (at least for verify_dnssec)\n        if self._verified_success is True:\n            return True\n        if self.error:\n            return False\n        if not self.raw:\n            self.error = \"Empty request\"\n            return False\n        pr = pb2.PaymentRequest()\n        try:\n            pr.ParseFromString(self.raw)\n        except Exception:\n            self.error = \"Error: Cannot parse payment request\"\n            return False\n        if not pr.signature:\n            # the address will be displayed as requestor\n            self.requestor = None\n            return True\n        if pr.pki_type in [\"x509+sha256\", \"x509+sha1\"]:\n            return self.verify_x509(pr)\n        elif pr.pki_type in [\"dnssec+btc\", \"dnssec+ecdsa\"]:\n            return await self.verify_dnssec(pr)\n        else:\n            self.error = \"ERROR: Unsupported PKI Type for Message Signature\"\n            return False\n\n    def verify_x509(self, paymntreq):\n        load_ca_list()\n        if not ca_list:\n            self.error = \"Trusted certificate authorities list not found\"\n            return False\n        cert = pb2.X509Certificates()\n        cert.ParseFromString(paymntreq.pki_data)\n        # verify the chain of certificates\n        try:\n            x, ca = verify_cert_chain(cert.certificate)\n        except BaseException as e:\n            _logger.exception('')\n            self.error = str(e)\n            return False\n        # get requestor name\n        self.requestor = x.get_common_name()\n        if self.requestor.startswith('*.'):\n            self.requestor = self.requestor[2:]\n        # verify the BIP70 signature\n        pubkey0 = rsakey.RSAKey(x.modulus, x.exponent)\n        sig = paymntreq.signature\n        paymntreq.signature = b''\n        s = paymntreq.SerializeToString()\n        sigBytes = bytearray(sig)\n        msgBytes = bytearray(s)\n        if paymntreq.pki_type == \"x509+sha256\":\n            hashBytes = bytearray(hashlib.sha256(msgBytes).digest())\n            verify = pubkey0.verify(sigBytes, x509.PREFIX_RSA_SHA256 + hashBytes)\n        elif paymntreq.pki_type == \"x509+sha1\":\n            verify = pubkey0.hashAndVerify(sigBytes, msgBytes)\n        else:\n            self.error = f\"ERROR: unknown pki_type {paymntreq.pki_type} in Payment Request\"\n            return False\n        if not verify:\n            self.error = \"ERROR: Invalid Signature for Payment Request Data\"\n            return False\n        ### SIG Verified\n        self._verified_success_msg = 'Signed by Trusted CA: ' + ca.get_common_name()\n        self._verified_success = True\n        return True\n\n    async def verify_dnssec(self, pr):\n        sig = pr.signature\n        alias = pr.pki_data\n        info: dict = await Contacts.resolve_openalias(alias)\n        if pr.pki_type == \"dnssec+btc\":\n            self.requestor = alias\n            address = info.get('address')\n            pr.signature = b''\n            message = pr.SerializeToString()\n            if bitcoin.verify_usermessage_with_address(address, sig, message):\n                self._verified_success_msg = 'Verified with DNSSEC'\n                self._verified_success = True\n                return True\n            else:\n                self.error = \"verify failed\"\n                return False\n        else:\n            self.error = \"unknown algo\"\n            return False\n\n    def has_expired(self) -> Optional[bool]:\n        if not hasattr(self, 'details'):\n            return None\n        return self.details.expires and self.details.expires < int(time.time())\n\n    def get_time(self):\n        return self.details.time\n\n    def get_expiration_date(self):\n        return self.details.expires\n\n    def get_amount(self):\n        return sum(map(lambda x:x.value, self.outputs))\n\n    def get_address(self):\n        o = self.outputs[0]\n        addr = o.address\n        assert addr\n        return addr\n\n    def get_requestor(self):\n        return self.requestor if self.requestor else self.get_address()\n\n    def get_verify_status(self) -> str:\n        return (self.error or self._verified_success_msg) if self.requestor else \"No Signature\"\n\n    def get_memo(self):\n        return self.memo\n\n    def get_name_for_export(self) -> Optional[str]:\n        if not hasattr(self, 'details'):\n            return None\n        return get_id_from_onchain_outputs(self.outputs, timestamp=self.get_time())\n\n    def get_outputs(self):\n        return self.outputs[:]\n\n    async def send_payment_and_receive_paymentack(self, raw_tx, refund_addr):\n        pay_det = self.details\n        if not self.details.payment_url:\n            return False, \"no url\"\n        paymnt = pb2.Payment()\n        paymnt.merchant_data = pay_det.merchant_data\n        paymnt.transactions.append(bfh(raw_tx))\n        ref_out = paymnt.refund_to.add()\n        ref_out.script = address_to_script(refund_addr)\n        paymnt.memo = \"Paid using Electrum\"\n        pm = paymnt.SerializeToString()\n        payurl = urllib.parse.urlparse(pay_det.payment_url)\n        resp_content = None\n        try:\n            proxy = Network.get_instance().proxy\n            async with make_aiohttp_session(proxy, headers=ACK_HEADERS) as session:\n                async with session.post(payurl.geturl(), data=pm) as response:\n                    resp_content = await response.read()\n                    response.raise_for_status()\n                    try:\n                        paymntack = pb2.PaymentACK()\n                        paymntack.ParseFromString(resp_content)\n                    except Exception:\n                        return False, \"PaymentACK could not be processed. Payment was sent; please manually verify that payment was received.\"\n                    print(f\"PaymentACK message received: {paymntack.memo}\")\n                    return True, paymntack.memo\n        except aiohttp.ClientError as e:\n            error = f\"Payment Message/PaymentACK Failed:\\nerror type: {type(e)}\"\n            if isinstance(e, aiohttp.ClientResponseError):\n                error += f\"\\nGot HTTP status code {e.status}.\"\n                if resp_content:\n                    error_text_received = error_text_bytes_to_safe_str(resp_content)\n                    error_text_received = error_text_received[:400]\n                    error_oneline = ' -- '.join(error.split('\\n'))\n                    _logger.info(f\"{error_oneline} -- [DO NOT TRUST THIS MESSAGE] \"\n                                 f\"{repr(e)} text: {error_text_received}\")\n            return False, error\n\n\ndef make_unsigned_request(req: 'Invoice'):\n    addr = req.get_address()\n    time = req.time\n    exp = req.exp\n    if time and type(time) != int:\n        time = 0\n    if exp and type(exp) != int:\n        exp = 0\n    amount = req.get_amount_sat()\n    if amount is None:\n        amount = 0\n    memo = req.message\n    script = address_to_script(addr)\n    outputs = [(script, amount)]\n    pd = pb2.PaymentDetails()\n    if constants.net.TESTNET:\n        pd.network = 'test'\n    for script, amount in outputs:\n        pd.outputs.add(amount=amount, script=script)\n    pd.time = time\n    pd.expires = time + exp if exp else 0\n    pd.memo = memo\n    pr = pb2.PaymentRequest()\n    pr.serialized_payment_details = pd.SerializeToString()\n    pr.signature = util.to_bytes('')\n    return pr\n\n\ndef sign_request_with_alias(pr, alias, alias_privkey):\n    pr.pki_type = 'dnssec+btc'\n    pr.pki_data = str(alias)\n    message = pr.SerializeToString()\n    ec_key = ecc.ECPrivkey(alias_privkey)\n    compressed = bitcoin.is_compressed_privkey(alias_privkey)\n    pr.signature = bitcoin.ecdsa_sign_usermessage(ec_key, message, is_compressed=compressed)\n\n\ndef verify_cert_chain(chain):\n    \"\"\" Verify a chain of certificates. The last certificate is the CA\"\"\"\n    load_ca_list()\n    # parse the chain\n    cert_num = len(chain)\n    x509_chain = []\n    for i in range(cert_num):\n        x = x509.X509(bytearray(chain[i]))\n        x509_chain.append(x)\n        if i == 0:\n            x.check_date()\n        else:\n            if not x.check_ca():\n                raise Exception(\"ERROR: Supplied CA Certificate Error\")\n    if not cert_num > 1:\n        raise Exception(\"ERROR: CA Certificate Chain Not Provided by Payment Processor\")\n    # if the root CA is not supplied, add it to the chain\n    ca = x509_chain[cert_num-1]\n    if ca.getFingerprint() not in ca_list:\n        keyID = ca.get_issuer_keyID()\n        f = ca_keyID.get(keyID)\n        if f:\n            root = ca_list[f]\n            x509_chain.append(root)\n        else:\n            raise Exception(\"Supplied CA Not Found in Trusted CA Store.\")\n    # verify the chain of signatures\n    cert_num = len(x509_chain)\n    for i in range(1, cert_num):\n        x = x509_chain[i]\n        prev_x = x509_chain[i-1]\n        algo, sig, data = prev_x.get_signature()\n        sig = bytearray(sig)\n        pubkey = rsakey.RSAKey(x.modulus, x.exponent)\n        if algo == x509.ALGO_RSA_SHA1:\n            verify = pubkey.hashAndVerify(sig, data)\n        elif algo == x509.ALGO_RSA_SHA256:\n            hashBytes = bytearray(hashlib.sha256(data).digest())\n            verify = pubkey.verify(sig, x509.PREFIX_RSA_SHA256 + hashBytes)\n        elif algo == x509.ALGO_RSA_SHA384:\n            hashBytes = bytearray(hashlib.sha384(data).digest())\n            verify = pubkey.verify(sig, x509.PREFIX_RSA_SHA384 + hashBytes)\n        elif algo == x509.ALGO_RSA_SHA512:\n            hashBytes = bytearray(hashlib.sha512(data).digest())\n            verify = pubkey.verify(sig, x509.PREFIX_RSA_SHA512 + hashBytes)\n        else:\n            raise Exception(f\"Algorithm not supported: {algo}\")\n        if not verify:\n            raise Exception(\"Certificate not Signed by Provided CA Certificate Chain\")\n\n    return x509_chain[0], ca\n\n\ndef check_ssl_config(config: 'SimpleConfig'):\n    from . import pem\n    key_path = config.SSL_KEYFILE_PATH\n    cert_path = config.SSL_CERTFILE_PATH\n    with open(key_path, 'r', encoding='utf-8') as f:\n        params = pem.parse_private_key(f.read())\n    with open(cert_path, 'r', encoding='utf-8') as f:\n        s = f.read()\n    bList = pem.dePemList(s, \"CERTIFICATE\")\n    # verify chain\n    x, ca = verify_cert_chain(bList)\n    # verify that privkey and pubkey match\n    privkey = rsakey.RSAKey(*params)\n    pubkey = rsakey.RSAKey(x.modulus, x.exponent)\n    assert x.modulus == params[0]\n    assert x.exponent == params[1]\n    # return requestor\n    requestor = x.get_common_name()\n    if requestor.startswith('*.'):\n        requestor = requestor[2:]\n    return requestor\n\n\ndef sign_request_with_x509(pr, key_path, cert_path):\n    from . import pem\n    with open(key_path, 'r', encoding='utf-8') as f:\n        params = pem.parse_private_key(f.read())\n        privkey = rsakey.RSAKey(*params)\n    with open(cert_path, 'r', encoding='utf-8') as f:\n        s = f.read()\n        bList = pem.dePemList(s, \"CERTIFICATE\")\n    certificates = pb2.X509Certificates()\n    certificates.certificate.extend(map(bytes, bList))\n    pr.pki_type = 'x509+sha256'\n    pr.pki_data = certificates.SerializeToString()\n    msgBytes = bytearray(pr.SerializeToString())\n    hashBytes = bytearray(hashlib.sha256(msgBytes).digest())\n    sig = privkey.sign(x509.PREFIX_RSA_SHA256 + hashBytes)\n    pr.signature = bytes(sig)\n\n\ndef serialize_request(req):  # FIXME this is broken\n    pr = make_unsigned_request(req)\n    signature = req.get('sig')\n    requestor = req.get('name')\n    if requestor and signature:\n        pr.signature = bfh(signature)\n        pr.pki_type = 'dnssec+btc'\n        pr.pki_data = str(requestor)\n    return pr\n\n\ndef make_request(config: 'SimpleConfig', req: 'Invoice'):\n    pr = make_unsigned_request(req)\n    key_path = config.SSL_KEYFILE_PATH\n    cert_path = config.SSL_CERTFILE_PATH\n    if key_path and cert_path:\n        sign_request_with_x509(pr, key_path, cert_path)\n    return pr\n"
  },
  {
    "path": "electrum/paymentrequest_pb2.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: paymentrequest.proto\n\"\"\"Generated protocol buffer code.\"\"\"\nfrom google.protobuf.internal import builder as _builder\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import descriptor_pool as _descriptor_pool\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_pool.Default().AddSerializedFile(b'\\n\\x14paymentrequest.proto\\x12\\x08payments\\\"+\\n\\x06Output\\x12\\x11\\n\\x06\\x61mount\\x18\\x01 \\x01(\\x04:\\x01\\x30\\x12\\x0e\\n\\x06script\\x18\\x02 \\x02(\\x0c\\\"\\xa3\\x01\\n\\x0ePaymentDetails\\x12\\x15\\n\\x07network\\x18\\x01 \\x01(\\t:\\x04main\\x12!\\n\\x07outputs\\x18\\x02 \\x03(\\x0b\\x32\\x10.payments.Output\\x12\\x0c\\n\\x04time\\x18\\x03 \\x02(\\x04\\x12\\x0f\\n\\x07\\x65xpires\\x18\\x04 \\x01(\\x04\\x12\\x0c\\n\\x04memo\\x18\\x05 \\x01(\\t\\x12\\x13\\n\\x0bpayment_url\\x18\\x06 \\x01(\\t\\x12\\x15\\n\\rmerchant_data\\x18\\x07 \\x01(\\x0c\\\"\\x95\\x01\\n\\x0ePaymentRequest\\x12\\\"\\n\\x17payment_details_version\\x18\\x01 \\x01(\\r:\\x01\\x31\\x12\\x16\\n\\x08pki_type\\x18\\x02 \\x01(\\t:\\x04none\\x12\\x10\\n\\x08pki_data\\x18\\x03 \\x01(\\x0c\\x12\\\"\\n\\x1aserialized_payment_details\\x18\\x04 \\x02(\\x0c\\x12\\x11\\n\\tsignature\\x18\\x05 \\x01(\\x0c\\\"\\'\\n\\x10X509Certificates\\x12\\x13\\n\\x0b\\x63\\x65rtificate\\x18\\x01 \\x03(\\x0c\\\"i\\n\\x07Payment\\x12\\x15\\n\\rmerchant_data\\x18\\x01 \\x01(\\x0c\\x12\\x14\\n\\x0ctransactions\\x18\\x02 \\x03(\\x0c\\x12#\\n\\trefund_to\\x18\\x03 \\x03(\\x0b\\x32\\x10.payments.Output\\x12\\x0c\\n\\x04memo\\x18\\x04 \\x01(\\t\\\">\\n\\nPaymentACK\\x12\\\"\\n\\x07payment\\x18\\x01 \\x02(\\x0b\\x32\\x11.payments.Payment\\x12\\x0c\\n\\x04memo\\x18\\x02 \\x01(\\tB(\\n\\x1eorg.bitcoin.protocols.paymentsB\\x06Protos')\n\n_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())\n_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'paymentrequest_pb2', globals())\nif _descriptor._USE_C_DESCRIPTORS == False:\n\n  DESCRIPTOR._options = None\n  DESCRIPTOR._serialized_options = b'\\n\\036org.bitcoin.protocols.paymentsB\\006Protos'\n  _OUTPUT._serialized_start=34\n  _OUTPUT._serialized_end=77\n  _PAYMENTDETAILS._serialized_start=80\n  _PAYMENTDETAILS._serialized_end=243\n  _PAYMENTREQUEST._serialized_start=246\n  _PAYMENTREQUEST._serialized_end=395\n  _X509CERTIFICATES._serialized_start=397\n  _X509CERTIFICATES._serialized_end=436\n  _PAYMENT._serialized_start=438\n  _PAYMENT._serialized_end=543\n  _PAYMENTACK._serialized_start=545\n  _PAYMENTACK._serialized_end=607\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "electrum/pem.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\n# This module uses code from TLSLlite\n# TLSLite Author: Trevor Perrin)\n\n\nimport binascii\n\nfrom .x509 import ASN1_Node, bytestr_to_int, decode_OID\n\n\ndef a2b_base64(s):\n    try:\n        b = bytearray(binascii.a2b_base64(s))\n    except Exception as e:\n        raise SyntaxError(\"base64 error: %s\" % e)\n    return b\n\ndef b2a_base64(b):\n    return binascii.b2a_base64(b)\n\n\ndef dePem(s, name):\n    \"\"\"Decode a PEM string into a bytearray of its payload.\n\n    The input must contain an appropriate PEM prefix and postfix\n    based on the input name string, e.g. for name=\"CERTIFICATE\":\n\n    -----BEGIN CERTIFICATE-----\n    MIIBXDCCAUSgAwIBAgIBADANBgkqhkiG9w0BAQUFADAPMQ0wCwYDVQQDEwRUQUNL\n    ...\n    KoZIhvcNAQEFBQADAwA5kw==\n    -----END CERTIFICATE-----\n\n    The first such PEM block in the input will be found, and its\n    payload will be base64 decoded and returned.\n    \"\"\"\n    prefix  = \"-----BEGIN %s-----\" % name\n    postfix = \"-----END %s-----\" % name\n    start = s.find(prefix)\n    if start == -1:\n        raise SyntaxError(\"Missing PEM prefix\")\n    end = s.find(postfix, start+len(prefix))\n    if end == -1:\n        raise SyntaxError(\"Missing PEM postfix\")\n    s = s[start+len(\"-----BEGIN %s-----\" % name) : end]\n    retBytes = a2b_base64(s) # May raise SyntaxError\n    return retBytes\n\ndef dePemList(s, name):\n    \"\"\"Decode a sequence of PEM blocks into a list of bytearrays.\n\n    The input must contain any number of PEM blocks, each with the appropriate\n    PEM prefix and postfix based on the input name string, e.g. for\n    name=\"TACK BREAK SIG\".  Arbitrary text can appear between and before and\n    after the PEM blocks.  For example:\n\n    \" Created by TACK.py 0.9.3 Created at 2012-02-01T00:30:10Z -----BEGIN TACK\n    BREAK SIG-----\n    ATKhrz5C6JHJW8BF5fLVrnQss6JnWVyEaC0p89LNhKPswvcC9/s6+vWLd9snYTUv\n    YMEBdw69PUP8JB4AdqA3K6Ap0Fgd9SSTOECeAKOUAym8zcYaXUwpk0+WuPYa7Zmm\n    SkbOlK4ywqt+amhWbg9txSGUwFO5tWUHT3QrnRlE/e3PeNFXLx5Bckg= -----END TACK\n    BREAK SIG----- Created by TACK.py 0.9.3 Created at 2012-02-01T00:30:11Z\n    -----BEGIN TACK BREAK SIG-----\n    ATKhrz5C6JHJW8BF5fLVrnQss6JnWVyEaC0p89LNhKPswvcC9/s6+vWLd9snYTUv\n    YMEBdw69PUP8JB4AdqA3K6BVCWfcjN36lx6JwxmZQncS6sww7DecFO/qjSePCxwM\n    +kdDqX/9/183nmjx6bf0ewhPXkA0nVXsDYZaydN8rJU1GaMlnjcIYxY= -----END TACK\n    BREAK SIG----- \"\n\n    All such PEM blocks will be found, decoded, and return in an ordered list\n    of bytearrays, which may have zero elements if not PEM blocks are found.\n     \"\"\"\n    bList = []\n    prefix  = \"-----BEGIN %s-----\" % name\n    postfix = \"-----END %s-----\" % name\n    while 1:\n        start = s.find(prefix)\n        if start == -1:\n            return bList\n        end = s.find(postfix, start+len(prefix))\n        if end == -1:\n            raise SyntaxError(\"Missing PEM postfix\")\n        s2 = s[start+len(prefix) : end]\n        retBytes = a2b_base64(s2) # May raise SyntaxError\n        bList.append(retBytes)\n        s = s[end+len(postfix) : ]\n\ndef pem(b, name):\n    \"\"\"Encode a payload bytearray into a PEM string.\n\n    The input will be base64 encoded, then wrapped in a PEM prefix/postfix\n    based on the name string, e.g. for name=\"CERTIFICATE\":\n\n    -----BEGIN CERTIFICATE-----\n    MIIBXDCCAUSgAwIBAgIBADANBgkqhkiG9w0BAQUFADAPMQ0wCwYDVQQDEwRUQUNL\n    ...\n    KoZIhvcNAQEFBQADAwA5kw==\n    -----END CERTIFICATE-----\n    \"\"\"\n    s1 = b2a_base64(b)[:-1] # remove terminating \\n\n    s2 = b\"\"\n    while s1:\n        s2 += s1[:64] + b\"\\n\"\n        s1 = s1[64:]\n    s = (\"-----BEGIN %s-----\\n\" % name).encode('ascii') + s2 + \\\n        (\"-----END %s-----\\n\" % name).encode('ascii')\n    return s\n\ndef pemSniff(inStr, name):\n    searchStr = \"-----BEGIN %s-----\" % name\n    return searchStr in inStr\n\n\ndef parse_private_key(s):\n    \"\"\"Parse a string containing a PEM-encoded <privateKey>.\"\"\"\n    if pemSniff(s, \"PRIVATE KEY\"):\n        bytes = dePem(s, \"PRIVATE KEY\")\n        return _parsePKCS8(bytes)\n    elif pemSniff(s, \"RSA PRIVATE KEY\"):\n        bytes = dePem(s, \"RSA PRIVATE KEY\")\n        return _parseSSLeay(bytes)\n    else:\n        raise SyntaxError(\"Not a PEM private key file\")\n\n\ndef _parsePKCS8(_bytes):\n    s = ASN1_Node(_bytes)\n    root = s.root()\n    version_node = s.first_child(root)\n    version = bytestr_to_int(s.get_value_of_type(version_node, 'INTEGER'))\n    if version != 0:\n        raise SyntaxError(\"Unrecognized PKCS8 version\")\n    rsaOID_node = s.next_node(version_node)\n    ii = s.first_child(rsaOID_node)\n    rsaOID = decode_OID(s.get_value_of_type(ii, 'OBJECT IDENTIFIER'))\n    if rsaOID != '1.2.840.113549.1.1.1':\n        raise SyntaxError(\"Unrecognized AlgorithmIdentifier\")\n    privkey_node = s.next_node(rsaOID_node)\n    value = s.get_value_of_type(privkey_node, 'OCTET STRING')\n    return _parseASN1PrivateKey(value)\n\n\ndef _parseSSLeay(bytes):\n    return _parseASN1PrivateKey(ASN1_Node(bytes))\n\n\ndef bytesToNumber(s):\n    return int(binascii.hexlify(s), 16)\n\n\ndef _parseASN1PrivateKey(s):\n    s = ASN1_Node(s)\n    root = s.root()\n    version_node = s.first_child(root)\n    version = bytestr_to_int(s.get_value_of_type(version_node, 'INTEGER'))\n    if version != 0:\n        raise SyntaxError(\"Unrecognized RSAPrivateKey version\")\n    n = s.next_node(version_node)\n    e = s.next_node(n)\n    d = s.next_node(e)\n    p = s.next_node(d)\n    q = s.next_node(p)\n    dP = s.next_node(q)\n    dQ = s.next_node(dP)\n    qInv = s.next_node(dQ)\n    return list(map(lambda x: bytesToNumber(s.get_value_of_type(x, 'INTEGER')), [n, e, d, p, q, dP, dQ, qInv]))\n\n"
  },
  {
    "path": "electrum/plot.py",
    "content": "# note: This module takes 1-2 seconds to import. It should be imported *on-demand*.\n\nimport datetime\nfrom decimal import Decimal\nfrom collections import defaultdict\n\nimport matplotlib\nmatplotlib.use('QtAgg')\nimport matplotlib.pyplot as plt\nimport matplotlib.dates as md\n\nfrom .i18n import _\nfrom .bitcoin import COIN\n\n\nclass NothingToPlotException(Exception):\n    def __str__(self):\n        return _(\"Nothing to plot.\")\n\n\ndef plot_history(history):\n    if len(history) == 0:\n        raise NothingToPlotException()\n    hist_in = defaultdict(Decimal)\n    hist_out = defaultdict(Decimal)\n    for item in history:\n        is_lightning = item.get(\"lightning\", False)\n        if not is_lightning and not item['confirmations']:\n            continue\n        if item['timestamp'] is None:\n            continue\n        value = Decimal(item['value'].value)/COIN\n        date = item['date']\n        datenum = int(md.date2num(datetime.date(date.year, date.month, 1)))\n        if value > 0:\n            hist_in[datenum] += value\n        else:\n            hist_out[datenum] -= value\n\n    f, axarr = plt.subplots(2, sharex=True)\n    plt.subplots_adjust(bottom=0.2)\n    plt.xticks(rotation=25)\n    ax = plt.gca()\n    plt.ylabel('BTC')\n    plt.xlabel('Month')\n    xfmt = md.DateFormatter('%Y-%m-%d')\n    ax.xaxis.set_major_formatter(xfmt)\n    axarr[0].set_title('Monthly Volume')\n    xfmt = md.DateFormatter('%Y-%m')\n    ax.xaxis.set_major_formatter(xfmt)\n    width = 20\n\n    r1 = None\n    r2 = None\n    dates_values = list(zip(*sorted(hist_in.items())))\n    if dates_values and len(dates_values) == 2:\n        dates, values = dates_values\n        r1 = axarr[0].bar(dates, values, width, label='incoming')\n        axarr[0].legend(loc='upper left')\n    dates_values = list(zip(*sorted(hist_out.items())))\n    if dates_values and len(dates_values) == 2:\n        dates, values = dates_values\n        r2 = axarr[1].bar(dates, values, width, color='r', label='outgoing')\n        axarr[1].legend(loc='upper left')\n    if r1 is None and r2 is None:\n        raise NothingToPlotException()\n    return plt\n"
  },
  {
    "path": "electrum/plugin.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015-2024 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport json\nimport os\nimport pkgutil\nimport importlib.util\nimport time\nimport threading\nimport sys\nimport zipfile as zipfile_lib\nfrom urllib.parse import urlparse\n\nfrom typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple,\n                    Dict, Iterable, List, Sequence, Callable, TypeVar, Mapping)\nimport concurrent\nfrom concurrent.futures import Future\nimport zipimport\nfrom functools import wraps, partial\nfrom itertools import chain\n\nfrom electrum_ecc import ECPrivkey, ECPubkey\n\nfrom ._vendor.distutils.version import StrictVersion\nfrom .version import ELECTRUM_VERSION\nfrom .i18n import _\nfrom .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException, ChoiceItem,\n                   make_dir, make_aiohttp_session)\nfrom . import bip32\nfrom . import plugins\nfrom .simple_config import SimpleConfig\nfrom .logging import get_logger, Logger\nfrom .crypto import sha256\nfrom .network import Network\n\nif TYPE_CHECKING:\n    from .hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase\n    from .keystore import Hardware_KeyStore, KeyStore\n    from .wallet import Abstract_Wallet\n\n\n_logger = get_logger(__name__)\nplugin_loaders = {}\nhook_names = set()\nhooks = {}\n_exec_module_failure = {}  # type: Dict[str, Exception]\n\nPLUGIN_PASSWORD_VERSION = 1\n\n\nclass Plugins(DaemonThread):\n\n    pkgpath = os.path.dirname(plugins.__file__)\n    # TODO: use XDG Base Directory Specification instead of hardcoding /etc\n    keyfile_posix = '/etc/electrum/plugins_key'\n    keyfile_windows = r'HKEY_LOCAL_MACHINE\\SOFTWARE\\Electrum\\PluginsKey'\n\n    @profiler\n    def __init__(self, config: SimpleConfig, gui_name: str = None, cmd_only: bool = False):\n        self.config = config\n        self.cmd_only = cmd_only  # type: bool\n        self.internal_plugin_metadata = {}\n        self.external_plugin_metadata = {}\n        if cmd_only:\n            # only import the command modules of plugins\n            Logger.__init__(self)\n            self.find_plugins()\n            self.load_plugins()\n            return\n        DaemonThread.__init__(self)\n        self.device_manager = DeviceMgr(config)\n        self.name = 'Plugins'  # set name of thread\n        self._hw_wallets = {}\n        self.plugins = {}  # type: Dict[str, BasePlugin]\n        self.gui_name = gui_name\n        self.find_plugins()\n        self.load_plugins()\n        self.add_jobs(self.device_manager.thread_jobs())\n        self.start()\n\n    @property\n    def descriptions(self):\n        return dict(list(self.internal_plugin_metadata.items()) + list(self.external_plugin_metadata.items()))\n\n    def find_directory_plugins(self, pkg_path: str, external: bool):\n        \"\"\"Finds plugins in directory form from the given pkg_path and populates the metadata dicts\"\"\"\n        iter_modules = list(pkgutil.iter_modules([pkg_path]))\n        for loader, name, ispkg in iter_modules:\n            # FIXME pyinstaller binaries are packaging each built-in plugin twice:\n            #       once as data and once as code. To honor the \"no duplicates\" rule below,\n            #       we exclude the ones packaged as *code*, here:\n            if loader.__class__.__qualname__ == \"PyiFrozenImporter\":\n                continue\n            module_path = os.path.join(pkg_path, name)\n            if self.cmd_only and not self.config.get(f'plugins.{name}.enabled') is True:\n                continue\n            try:\n                with open(os.path.join(module_path, 'manifest.json'), 'r') as f:\n                    d = json.load(f)\n            except FileNotFoundError:\n                self.logger.info(f\"could not find manifest.json of plugin {name}, skipping...\")\n                continue\n            if 'fullname' not in d:\n                continue\n            d['path'] = module_path\n            if not self.cmd_only:\n                gui_good = self.gui_name in d.get('available_for', [])\n                if not gui_good:\n                    continue\n                details = d.get('registers_wallet_type')\n                if details:\n                    self.register_wallet_type(name, gui_good, details)\n                details = d.get('registers_keystore')\n                if details:\n                    self.register_keystore(name, details)\n            if name in self.internal_plugin_metadata or name in self.external_plugin_metadata:\n                _logger.info(f\"Found the following plugin modules: {iter_modules=}\")\n                _logger.info(f\"duplicate plugins? for {name=}\")\n                continue\n            if not external:\n                self.internal_plugin_metadata[name] = d\n            else:\n                self.external_plugin_metadata[name] = d\n\n    @staticmethod\n    def exec_module_from_spec(spec, path: str):\n        if prev_fail := _exec_module_failure.get(path):\n            raise Exception(f\"exec_module already failed once before, with: {prev_fail!r}\")\n        try:\n            module = importlib.util.module_from_spec(spec)\n            # sys.modules needs to be modified for relative imports to work\n            # see https://stackoverflow.com/a/50395128\n            sys.modules[path] = module\n            spec.loader.exec_module(module)\n        except Exception as e:\n            # We can't undo all side-effects, but we at least rm the module from sys.modules,\n            # so the import system knows it failed. If called again for the same plugin, we do not\n            # retry due to potential interactions with not-undone side-effects (e.g. plugin\n            # might have defined commands).\n            _exec_module_failure[path] = e\n            if path in sys.modules:\n                sys.modules.pop(path, None)\n            raise Exception(f\"Error pre-loading {path}: {repr(e)}\") from e\n        return module\n\n    def find_plugins(self):\n        internal_plugins_path = (self.pkgpath, False)\n        external_plugins_path = (self.get_external_plugin_dir(), True)\n        for pkg_path, external in (internal_plugins_path, external_plugins_path):\n            if pkg_path and os.path.exists(pkg_path):\n                if not external:\n                    self.find_directory_plugins(pkg_path=pkg_path, external=external)\n                else:\n                    self.find_zip_plugins(pkg_path=pkg_path, external=external)\n\n    def load_plugins(self):\n        for name, d in chain(self.internal_plugin_metadata.items(), self.external_plugin_metadata.items()):\n            if not d.get('requires_wallet_type') and self.config.get(f'plugins.{name}.enabled'):\n                try:\n                    if self.cmd_only:  # only load init method to register commands\n                        self.maybe_load_plugin_init_method(name)\n                    else:\n                        self.load_plugin_by_name(name)\n                except BaseException as e:\n                    self.logger.exception(f\"cannot initialize plugin {name}: {e}\")\n\n    def _has_root_permissions(self, path):\n        return os.stat(path).st_uid == 0 and not os.access(path, os.W_OK)\n\n    def get_keyfile_path(self, key_hex: Optional[str]) -> Tuple[str, str]:\n        if sys.platform in ['windows', 'win32']:\n            keyfile_path = self.keyfile_windows\n            keyfile_help = _('This file can be edited with Regedit')\n        elif 'ANDROID_DATA' in os.environ:\n            raise Exception('platform not supported')\n        else:\n            # treat unknown platforms and macOS as linux-like\n            keyfile_path = self.keyfile_posix\n            keyfile_help = \"\" if not key_hex else \"\".join([\n                                         _('The file must have root permissions'),\n                                         \".\\n\\n\",\n                                         _(\"To set it you can also use the Auto-Setup or run \"\n                                           \"the following terminal command\"),\n                                         \":\\n\\n\",\n                                         f\"sudo sh -c \\\"{self._posix_plugin_key_creation_command(key_hex)}\\\"\",\n            ])\n        return keyfile_path, keyfile_help\n\n    def try_auto_key_setup(self, pubkey_hex: str) -> bool:\n        \"\"\"Can be called from the GUI to store the plugin pubkey as root/admin user\"\"\"\n        try:\n            if sys.platform in ['windows', 'win32']:\n                self._write_key_to_regedit_windows(pubkey_hex)\n            elif 'ANDROID_DATA' in os.environ:\n                raise Exception('platform not supported')\n            elif sys.platform.startswith('darwin'):  # macOS\n                self._write_key_to_root_file_macos(pubkey_hex)\n            else:\n                self._write_key_to_root_file_linux(pubkey_hex)\n        except Exception:\n            self.logger.exception(f\"auto-key setup for {pubkey_hex} failed\")\n            return False\n        return True\n\n    def try_auto_key_reset(self) -> bool:\n        try:\n            if sys.platform in ['windows', 'win32']:\n                self._delete_plugin_key_from_windows_registry()\n            elif 'ANDROID_DATA' in os.environ:\n                raise Exception('platform not supported')\n            elif sys.platform.startswith('darwin'):  # macOS\n                self._delete_macos_plugin_keyfile()\n            else:\n                self._delete_linux_plugin_keyfile()\n        except Exception:\n            self.logger.exception(f'auto-reset of plugin key failed')\n            return False\n        return True\n\n    def _posix_plugin_key_creation_command(self, pubkey_hex: str) -> str:\n        \"\"\"creates the dir (dir_path), writes the key in file, and sets permissions to 644\"\"\"\n        dir_path: str = os.path.dirname(self.keyfile_posix)\n        sh_command = (\n                     f\"mkdir -p {dir_path} \"  # create the /etc/electrum dir\n                     f\"&& printf '%s' '{pubkey_hex}' > {self.keyfile_posix} \"  # write the key to the file\n                     f\"&& chmod 644 {self.keyfile_posix} \"  # set read permissions for the file\n                     f\"&& chmod 755 {dir_path}\"  # set read permissions for the dir\n        )\n        return sh_command\n\n    @staticmethod\n    def _get_macos_osascript_command(commands: List[str]) -> List[str]:\n        \"\"\"\n        Inspired by\n        https://github.com/barneygale/elevate/blob/01263b690288f022bf6fa702711ac96816bc0e74/elevate/posix.py\n        Wraps the given commands in a macOS osascript command to prompt for root permissions.\n        \"\"\"\n        from shlex import quote\n\n        def quote_shell(args):\n            return \" \".join(quote(arg) for arg in args)\n\n        def quote_applescript(string):\n            charmap = {\n                \"\\n\": \"\\\\n\",\n                \"\\r\": \"\\\\r\",\n                \"\\t\": \"\\\\t\",\n                \"\\\"\": \"\\\\\\\"\",\n                \"\\\\\": \"\\\\\\\\\",\n            }\n            return '\"%s\"' % \"\".join(charmap.get(char, char) for char in string)\n\n        commands = [\n            \"osascript\",\n            \"-e\",\n            \"do shell script %s \"\n            \"with administrator privileges \"\n            \"without altering line endings\"\n            % quote_applescript(quote_shell(commands))\n        ]\n        return commands\n\n    @staticmethod\n    def _run_win_regedit_as_admin(reg_exe_command: str) -> None:\n        \"\"\"\n        Runs reg.exe reg_exe_command and requests admin privileges through UAC prompt.\n        \"\"\"\n        # has to use ShellExecuteEx as ShellExecuteW (the simpler api) doesn't allow to wait\n        # for the result of the process (returns no process handle)\n        from ctypes import byref, sizeof, windll, Structure, c_ulong\n        from ctypes.wintypes import HANDLE, DWORD, HWND, HINSTANCE, HKEY, LPCWSTR\n\n        # https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfoa\n        class SHELLEXECUTEINFO(Structure):\n            _fields_ = [\n                ('cbSize', DWORD),\n                ('fMask', c_ulong),\n                ('hwnd', HWND),\n                ('lpVerb', LPCWSTR),\n                ('lpFile', LPCWSTR),\n                ('lpParameters', LPCWSTR),\n                ('lpDirectory', LPCWSTR),\n                ('nShow', c_ulong),\n                ('hInstApp', HINSTANCE),\n                ('lpIDList', c_ulong),\n                ('lpClass', LPCWSTR),\n                ('hkeyClass', HKEY),\n                ('dwHotKey', DWORD),\n                ('hIcon', HANDLE),\n                ('hProcess', HANDLE)\n            ]\n\n        info = SHELLEXECUTEINFO()\n        info.cbSize = sizeof(SHELLEXECUTEINFO)\n        info.fMask = 0x00000040 # SEE_MASK_NOCLOSEPROCESS (so we can check the result of the process)\n        info.hwnd = None\n        info.lpVerb = 'runas'  # run as administrator\n        info.lpFile = 'reg.exe'  # the executable to run\n        info.lpParameters = reg_exe_command  # the registry edit command\n        info.lpDirectory = None\n        info.nShow = 1\n\n        # Execute and wait\n        if not windll.shell32.ShellExecuteExW(byref(info)):\n            error = windll.kernel32.GetLastError()\n            raise Exception(f'Error executing registry command: {error}')\n\n        # block until the process is done or 5 sec timeout\n        windll.kernel32.WaitForSingleObject(info.hProcess, 0x1338)\n\n        # Close handle\n        windll.kernel32.CloseHandle(info.hProcess)\n\n    @staticmethod\n    def _execute_commands_in_subprocess(commands: List[str]) -> None:\n        \"\"\"\n        Executes the given commands in a subprocess and asserts that it was successful.\n        \"\"\"\n        import subprocess\n        with subprocess.Popen(\n            commands,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            text=True,\n        ) as process:\n            stdout, stderr = process.communicate()\n            if process.returncode != 0:\n                raise Exception(f'error executing command ({process.returncode}): {stderr}')\n\n    def _write_key_to_root_file_linux(self, key_hex: str) -> None:\n        \"\"\"\n        Spawns a pkexec subprocess to write the key to a file with root permissions.\n        This will open an OS dialog asking for the root password. Can only succeed if\n        the system has polkit installed.\n        \"\"\"\n        assert os.path.exists(\"/etc\"), \"System does not have /etc directory\"\n\n        sh_command: str = self._posix_plugin_key_creation_command(key_hex)\n        commands = ['pkexec', 'sh', '-c', sh_command]\n        self._execute_commands_in_subprocess(commands)\n\n        # check if the key was written correctly\n        with open(self.keyfile_posix, 'r') as f:\n            assert f.read() == key_hex, f'file content mismatch: {f.read()} != {key_hex}'\n        self.logger.debug(f'file saved successfully to {self.keyfile_posix}')\n\n    def _delete_linux_plugin_keyfile(self) -> None:\n        \"\"\"\n        Deletes the root owned key file at self.keyfile_posix.\n        \"\"\"\n        if not os.path.exists(self.keyfile_posix):\n            self.logger.debug(f'file {self.keyfile_posix} does not exist')\n            return\n        if not self._has_root_permissions(self.keyfile_posix):\n            os.unlink(self.keyfile_posix)\n            return\n\n        # use pkexec to delete the file as root user\n        commands = ['pkexec', 'rm', self.keyfile_posix]\n        self._execute_commands_in_subprocess(commands)\n        assert not os.path.exists(self.keyfile_posix), f'file {self.keyfile_posix} still exists'\n\n    def _write_key_to_root_file_macos(self, key_hex: str) -> None:\n        assert os.path.exists(\"/etc\"), \"System does not have /etc directory\"\n\n        sh_command: str = self._posix_plugin_key_creation_command(key_hex)\n        macos_commands = self._get_macos_osascript_command([\"sh\", \"-c\", sh_command])\n\n        self._execute_commands_in_subprocess(macos_commands)\n        with open(self.keyfile_posix, 'r') as f:\n            assert f.read() == key_hex, f'file content mismatch: {f.read()} != {key_hex}'\n        self.logger.debug(f'file saved successfully to {self.keyfile_posix}')\n\n    def _delete_macos_plugin_keyfile(self) -> None:\n        if not os.path.exists(self.keyfile_posix):\n            self.logger.debug(f'file {self.keyfile_posix} does not exist')\n            return\n        if not self._has_root_permissions(self.keyfile_posix):\n            os.unlink(self.keyfile_posix)\n            return\n        # use osascript to delete the file as root user\n        macos_commands = self._get_macos_osascript_command([\"rm\", self.keyfile_posix])\n        self._execute_commands_in_subprocess(macos_commands)\n        assert not os.path.exists(self.keyfile_posix), f'file {self.keyfile_posix} still exists'\n\n    def _write_key_to_regedit_windows(self, key_hex: str) -> None:\n        \"\"\"\n        Writes the key to the Windows registry with windows UAC prompt.\n        \"\"\"\n        from winreg import ConnectRegistry, OpenKey, QueryValue, HKEY_LOCAL_MACHINE\n\n        value_type = 'REG_SZ'\n        command = f'add \"{self.keyfile_windows}\" /ve /t {value_type} /d \"{key_hex}\" /f'\n\n        self._run_win_regedit_as_admin(command)\n\n        # check if the key was written correctly\n        with ConnectRegistry(None, HKEY_LOCAL_MACHINE) as hkey:\n            with OpenKey(hkey, r'SOFTWARE\\Electrum') as key:\n                assert key_hex == QueryValue(key, 'PluginsKey'), \"incorrect registry key value\"\n        self.logger.debug(f'key saved successfully to {self.keyfile_windows}')\n\n    def _delete_plugin_key_from_windows_registry(self) -> None:\n        \"\"\"\n        Deletes the PluginsKey dir in the Windows registry.\n        \"\"\"\n        from winreg import ConnectRegistry, OpenKey, HKEY_LOCAL_MACHINE\n\n        command = f'delete \"{self.keyfile_windows}\" /f'\n        self._run_win_regedit_as_admin(command)\n\n        try:\n            # do a sanity check to see if the key has been deleted\n            with ConnectRegistry(None, HKEY_LOCAL_MACHINE) as hkey:\n                with OpenKey(hkey, r'SOFTWARE\\Electrum\\PluginsKey'):\n                    raise Exception(f'Key {self.keyfile_windows} still exists, deletion failed')\n        except FileNotFoundError:\n            pass\n\n    def create_new_key(self, password:str) -> str:\n        salt = os.urandom(32)\n        privkey = self.derive_privkey(password, salt)\n        pubkey = privkey.get_public_key_bytes()\n        key = bytes([PLUGIN_PASSWORD_VERSION]) + salt + pubkey\n        return key.hex()\n\n    def get_pubkey_bytes(self) -> Tuple[Optional[bytes], Optional[bytes]]:\n        \"\"\"\n        returns pubkey, salt\n        returns None, None if the pubkey has not been set\n        \"\"\"\n        if sys.platform in ['windows', 'win32']:\n            import winreg\n            with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as hkey:\n                try:\n                    with winreg.OpenKey(hkey, r\"SOFTWARE\\\\Electrum\") as key:\n                        key_hex = winreg.QueryValue(key, \"PluginsKey\")\n                except Exception as e:\n                    self.logger.info(f'winreg error: {e}')\n                    return None, None\n        elif 'ANDROID_DATA' in os.environ:\n            return None, None\n        else:\n            # treat unknown platforms as linux-like\n            if not os.path.exists(self.keyfile_posix):\n                return None, None\n            if not self._has_root_permissions(self.keyfile_posix):\n                return None, None\n            with open(self.keyfile_posix) as f:\n                key_hex = f.read()\n        try:\n            key = bytes.fromhex(key_hex)\n            version = key[0]\n        except Exception:\n            self.logger.exception(f'{key_hex=} invalid')\n            return None, None\n        if version != PLUGIN_PASSWORD_VERSION:\n            self.logger.info(f'unknown plugin password version: {version}')\n            return None, None\n        # all good\n        salt = key[1:1+32]\n        pubkey = key[1+32:]\n        return pubkey, salt\n\n    def get_external_plugin_dir(self) -> str:\n        pkg_path = os.path.join(self.config.electrum_path(), 'plugins')\n        make_dir(pkg_path)\n        return pkg_path\n\n    async def download_external_plugin(self, url: str) -> str:\n        filename = os.path.basename(urlparse(url).path)\n        pkg_path = self.get_external_plugin_dir()\n        path = os.path.join(pkg_path, filename)\n        if os.path.exists(path):\n            raise FileExistsError(f\"Plugin {filename} already exists at {path}\")\n        network = Network.get_instance()\n        proxy = network.proxy if network else None\n        async with make_aiohttp_session(proxy=proxy) as session:\n            async with session.get(url) as resp:\n                if resp.status == 200:\n                    with open(path, 'wb') as fd:\n                        async for chunk in resp.content.iter_chunked(10):\n                            fd.write(chunk)\n        return path\n\n    def read_manifest(self, path) -> dict:\n        \"\"\" return json dict \"\"\"\n        with zipfile_lib.ZipFile(path) as file:\n            for filename in file.namelist():\n                if filename.endswith('manifest.json'):\n                    break\n            else:\n                raise Exception('could not find manifest.json in zip archive')\n            with file.open(filename, 'r') as f:\n                manifest = json.load(f)\n                manifest['path'] = path  # external, path of the zipfile\n                manifest['dirname'] = os.path.dirname(filename)  # internal\n                manifest['is_zip'] = True\n                manifest['zip_hash_sha256'] = get_file_hash256(path).hex()\n                return manifest\n\n    def zip_plugin_path(self, name) -> str:\n        path = self.get_metadata(name)['path']\n        filename = os.path.basename(path)\n        if name in self.internal_plugin_metadata:\n            pkg_path = self.pkgpath\n        else:\n            pkg_path = self.get_external_plugin_dir()\n        return os.path.join(pkg_path, filename)\n\n    def find_zip_plugins(self, pkg_path: str, external: bool):\n        \"\"\"Finds plugins in zip form in the given pkg_path and populates the metadata dicts\"\"\"\n        if pkg_path is None:\n            return\n        for filename in os.listdir(pkg_path):\n            path = os.path.join(pkg_path, filename)\n            if not filename.endswith('.zip'):\n                continue\n            try:\n                d = self.read_manifest(path)\n                name = d['name']\n            except Exception:\n                self.logger.info(f\"could not load manifest.json from zip plugin {filename}\", exc_info=True)\n                continue\n            if name in self.internal_plugin_metadata or name in self.external_plugin_metadata:\n                self.logger.info(f\"duplicate plugins for {name=}\")\n                continue\n            if self.cmd_only and not self.config.get(f'plugins.{name}.enabled'):\n                continue\n            min_version = d.get('min_electrum_version')\n            if min_version and StrictVersion(min_version) > StrictVersion(ELECTRUM_VERSION):\n                self.logger.info(f\"version mismatch for zip plugin {filename}\", exc_info=True)\n                continue\n            max_version = d.get('max_electrum_version')\n            if max_version and StrictVersion(max_version) < StrictVersion(ELECTRUM_VERSION):\n                self.logger.info(f\"version mismatch for zip plugin {filename}\", exc_info=True)\n                continue\n\n            if not self.cmd_only:\n                gui_good = self.gui_name in d.get('available_for', [])\n                if not gui_good:\n                    continue\n                if 'fullname' not in d:\n                    continue\n                details = d.get('registers_keystore')\n                if details:\n                    self.register_keystore(name, details)\n            if external:\n                self.external_plugin_metadata[name] = d\n            else:\n                self.internal_plugin_metadata[name] = d\n\n    def get(self, name):\n        return self.plugins.get(name)\n\n    def count(self):\n        return len(self.plugins)\n\n    def load_plugin(self, name) -> 'BasePlugin':\n        \"\"\"Imports the code of the given plugin.\n        note: can be called from any thread.\n        \"\"\"\n        if self.get_metadata(name):\n            return self.load_plugin_by_name(name)\n        else:\n            raise Exception(f\"could not find plugin {name!r}\")\n\n    def maybe_load_plugin_init_method(self, name: str) -> None:\n        \"\"\"Loads the __init__.py module of the plugin if it is not already loaded.\"\"\"\n        base_name = ('electrum_external_plugins.' if self.is_external(name) else 'electrum.plugins.') + name\n        if base_name not in sys.modules:\n            metadata = self.get_metadata(name)\n            is_zip = metadata.get('is_zip', False)\n            # if the plugin was not enabled on startup the init module hasn't been loaded yet\n            if not is_zip:\n                if self.is_external(name):\n                    # this branch is deprecated: external plugins are always zip files\n                    path = os.path.join(metadata['path'], '__init__.py')\n                    init_spec = importlib.util.spec_from_file_location(base_name, path)\n                else:\n                    init_spec = importlib.util.find_spec(base_name)\n            else:\n                zipfile = zipimport.zipimporter(metadata['path'])\n                dirname = metadata['dirname']\n                init_spec = zipfile.find_spec(dirname)\n\n            self.exec_module_from_spec(init_spec, base_name)\n\n    def load_plugin_by_name(self, name: str) -> 'BasePlugin':\n        if name in self.plugins:\n            return self.plugins[name]\n        # if the plugin was not enabled on startup the init module hasn't been loaded yet\n        self.maybe_load_plugin_init_method(name)\n        is_external = self.is_external(name)\n        if is_external and not self.is_authorized(name):\n            self.logger.info(f'plugin not authorized {name}')\n            return\n        if not is_external:\n            full_name = f'electrum.plugins.{name}.{self.gui_name}'\n        else:\n            full_name = f'electrum_external_plugins.{name}.{self.gui_name}'\n\n        spec = importlib.util.find_spec(full_name)\n        if spec is None:\n            raise RuntimeError(f\"{self.gui_name} implementation for {name} plugin not found\")\n        try:\n            module = self.exec_module_from_spec(spec, full_name)\n            plugin = module.Plugin(self, self.config, name)\n        except Exception as e:\n            raise Exception(f\"Error loading {name} plugin: {repr(e)}\") from e\n        self.add_jobs(plugin.thread_jobs())\n        self.plugins[name] = plugin\n        self.logger.info(f\"loaded plugin {name!r}. (from thread: {threading.current_thread().name!r})\")\n        return plugin\n\n    def close_plugin(self, plugin):\n        self.remove_jobs(plugin.thread_jobs())\n\n    def derive_privkey(self, pw: str, salt:bytes) -> ECPrivkey:\n        from hashlib import pbkdf2_hmac\n        secret = pbkdf2_hmac('sha256', pw.encode('utf-8'), salt, iterations=10**5)\n        return ECPrivkey(secret)\n\n    def uninstall(self, name: str):\n        if self.config.get(f'plugins.{name}'):\n            self.config.set_key(f'plugins.{name}', None)\n        if name in self.external_plugin_metadata:\n            zipfile = self.zip_plugin_path(name)\n            os.unlink(zipfile)\n            self.external_plugin_metadata.pop(name)\n\n    def is_internal(self, name) -> bool:\n        return name in self.internal_plugin_metadata\n\n    def is_external(self, name) -> bool:\n        return name in self.external_plugin_metadata\n\n    def is_auto_loaded(self, name):\n        metadata = self.external_plugin_metadata.get(name) or self.internal_plugin_metadata.get(name)\n        return metadata and (metadata.get('registers_keystore') or metadata.get('registers_wallet_type'))\n\n    def is_installed(self, name) -> bool:\n        \"\"\"an external plugin may be installed but not authorized \"\"\"\n        return (name in self.internal_plugin_metadata or name in self.external_plugin_metadata)\n\n    def is_authorized(self, name) -> bool:\n        if name in self.internal_plugin_metadata:\n            return True\n        if name not in self.external_plugin_metadata:\n            return False\n        pubkey_bytes, salt = self.get_pubkey_bytes()\n        if not pubkey_bytes:\n            return False\n        if not self.is_plugin_zip(name):\n            return False\n        filename = self.zip_plugin_path(name)\n        plugin_hash = get_file_hash256(filename)\n        sig = self.config.get(f'plugins.{name}.authorized')\n        if not sig:\n            return False\n        pubkey = ECPubkey(pubkey_bytes)\n        return pubkey.ecdsa_verify(bytes.fromhex(sig), plugin_hash)\n\n    def authorize_plugin(self, name: str, filename, privkey: ECPrivkey):\n        pubkey_bytes, salt = self.get_pubkey_bytes()\n        assert pubkey_bytes == privkey.get_public_key_bytes()\n        plugin_hash = get_file_hash256(filename)\n        sig = privkey.ecdsa_sign(plugin_hash)\n        value = sig.hex()\n        self.config.set_key(f'plugins.{name}.authorized', value)\n        self.config.set_key(f'plugins.{name}.enabled', True)\n\n    def enable(self, name: str) -> 'BasePlugin':\n        self.config.enable_plugin(name)\n        p = self.get(name)\n        if p:\n            return p\n        return self.load_plugin(name)\n\n    def disable(self, name: str) -> None:\n        self.config.disable_plugin(name)\n        p = self.get(name)\n        if not p:\n            return\n        self.plugins.pop(name)\n        p.close()\n        self.logger.info(f\"closed {name}\")\n\n    @classmethod\n    def is_plugin_enabler_config_key(cls, key: str) -> bool:\n        return key.startswith('plugins.')\n\n    def is_available(self, name: str) -> bool:\n        d = self.descriptions.get(name)\n        if not d:\n            return False\n        deps = d.get('requires', [])\n        for dep, s in deps:\n            try:\n                __import__(dep)\n            except ImportError as e:\n                self.logger.warning(f'Plugin {name} unavailable: {repr(e)}')\n                return False\n        return True\n\n    def get_hardware_support(self):\n        out = []\n        for name, details in self._hw_wallets.items():\n            try:\n                p = self.get_plugin(name)\n                if p.is_available():\n                    out.append(HardwarePluginToScan(\n                        name=name,\n                        description=details[2],\n                        plugin=p,\n                        exception=None))\n            except Exception as e:\n                self.logger.exception(f\"cannot load plugin for: {name}\")\n                out.append(HardwarePluginToScan(\n                    name=name,\n                    description=details[2],\n                    plugin=None,\n                    exception=e))\n        return out\n\n    def register_wallet_type(self, name, gui_good, wallet_type):\n        from .wallet import register_wallet_type, register_constructor\n        self.logger.info(f\"registering wallet type {(wallet_type, name)}\")\n\n        def loader():\n            plugin = self.get_plugin(name)\n            register_constructor(wallet_type, plugin.wallet_class)\n        register_wallet_type(wallet_type)\n        plugin_loaders[wallet_type] = loader\n\n    def register_keystore(self, name, details):\n        from .keystore import register_keystore\n\n        def dynamic_constructor(d):\n            return self.get_plugin(name).keystore_class(d)\n        if details[0] == 'hardware':\n            self._hw_wallets[name] = details\n            self.logger.info(f\"registering hardware {name}: {details}\")\n            register_keystore(details[1], dynamic_constructor)\n\n    def get_plugin(self, name: str) -> 'BasePlugin':\n        if name not in self.plugins:\n            self.load_plugin(name)\n        return self.plugins[name]\n\n    def is_plugin_zip(self, name: str) -> bool:\n        \"\"\"Returns True if the plugin is a zip file\"\"\"\n        if (metadata := self.get_metadata(name)) is None:\n            return False\n        return metadata.get('is_zip', False)\n\n    def get_metadata(self, name: str) -> Optional[dict]:\n        \"\"\"Returns the metadata of the plugin\"\"\"\n        metadata = self.internal_plugin_metadata.get(name) or self.external_plugin_metadata.get(name)\n        if not metadata:\n            return None\n        return metadata\n\n    def run(self):\n        while self.is_running():\n            self.wake_up_event.wait(0.1)  # time.sleep(0.1) OR event\n            self.run_jobs()\n        self.on_stop()\n\n    def read_file(self, name: str, filename: str) -> bytes:\n        if self.is_plugin_zip(name):\n            plugin_filename = self.zip_plugin_path(name)\n            metadata = self.external_plugin_metadata[name]\n            dirname = metadata['dirname']\n            with zipfile_lib.ZipFile(plugin_filename) as myzip:\n                with myzip.open(\"/\".join([dirname,filename])) as myfile:\n                    return myfile.read()\n        elif name in self.internal_plugin_metadata:\n            path = os.path.join(os.path.dirname(__file__), 'plugins', name, filename)\n            with open(path, 'rb') as myfile:\n                return myfile.read()\n        else:\n            # no icon\n            return None\n\n\ndef get_file_hash256(path: str) -> bytes:\n    \"\"\"Get the sha256 hash of a file, similar to `sha256sum`.\"\"\"\n    with open(path, 'rb') as f:\n        return sha256(f.read())\n\n\ndef hook(func):\n    hook_names.add(func.__name__)\n    return func\n\n\ndef run_hook(name, *args):\n    results = []\n    f_list = hooks.get(name, [])\n    for p, f in f_list:\n        if p.is_enabled():\n            try:\n                r = f(*args)\n            except Exception:\n                _logger.exception(f\"Plugin error. plugin: {p}, hook: {name}\")\n                r = False\n            if r:\n                results.append(r)\n\n    if results:\n        assert len(results) == 1, results\n        return results[0]\n\n\nclass BasePlugin(Logger):\n\n    def __init__(self, parent, config: 'SimpleConfig', name):\n        self.parent = parent  # type: Plugins  # The plugins object\n        self.name = name\n        self.config = config\n        Logger.__init__(self)\n        # add self to hooks\n        for k in dir(self):\n            if k in hook_names:\n                l = hooks.get(k, [])\n                l.append((self, getattr(self, k)))\n                hooks[k] = l\n\n    def __str__(self):\n        return self.name\n\n    def close(self):\n        # remove self from hooks\n        for attr_name in dir(self):\n            if attr_name in hook_names:\n                # found attribute in self that is also the name of a hook\n                l = hooks.get(attr_name, [])\n                try:\n                    l.remove((self, getattr(self, attr_name)))\n                except ValueError:\n                    # maybe attr name just collided with hook name and was not hook\n                    continue\n                hooks[attr_name] = l\n        self.parent.close_plugin(self)\n        self.on_close()\n\n    def on_close(self):\n        pass\n\n    def requires_settings(self) -> bool:\n        return False\n\n    def thread_jobs(self):\n        return []\n\n    def is_enabled(self):\n        if not self.is_available():\n            return False\n        if not self.parent.is_authorized(self.name):\n            return False\n        return self.config.is_plugin_enabled(self.name)\n\n    def is_available(self):\n        return True\n\n    def can_user_disable(self):\n        return True\n\n    def settings_widget(self, window):\n        raise NotImplementedError()\n\n    def settings_dialog(self, window):\n        raise NotImplementedError()\n\n    def read_file(self, filename: str) -> bytes:\n        return self.parent.read_file(self.name, filename)\n\n    def get_storage(self, wallet: 'Abstract_Wallet') -> dict:\n        \"\"\"Returns a dict which is persisted in the per-wallet database.\"\"\"\n        plugin_storage = wallet.db.get_plugin_storage()\n        return plugin_storage.setdefault(self.name, {})\n\n\nclass DeviceUnpairableError(UserFacingException): pass\nclass HardwarePluginLibraryUnavailable(Exception): pass\nclass CannotAutoSelectDevice(Exception): pass\n\n\nclass Device(NamedTuple):\n    path: Union[str, bytes]\n    interface_number: int\n    id_: str\n    product_key: Any   # when using hid, often Tuple[int, int]\n    usage_page: int\n    transport_ui_string: str\n\n\nclass DeviceInfo(NamedTuple):\n    device: Device\n    label: Optional[str] = None\n    initialized: Optional[bool] = None\n    exception: Optional[Exception] = None\n    plugin_name: Optional[str] = None  # manufacturer, e.g. \"trezor\"\n    soft_device_id: Optional[str] = None  # if available, used to distinguish same-type hw devices\n    model_name: Optional[str] = None  # e.g. \"Ledger Nano S\"\n\n    def label_for_device_select(self) -> str:\n        return (\n            \"{label} ({maybe_model}{init}, {transport})\"\n            .format(\n                label=self.label or _(\"An unnamed {}\").format(self.plugin_name),\n                init=(_(\"initialized\") if self.initialized else _(\"wiped\")),\n                transport=self.device.transport_ui_string,\n                maybe_model=f\"{self.model_name}, \" if self.model_name else \"\"\n            )\n        )\n\n\nclass HardwarePluginToScan(NamedTuple):\n    name: str\n    description: str\n    plugin: Optional['HW_PluginBase']\n    exception: Optional[Exception]\n\n\nPLACEHOLDER_HW_CLIENT_LABELS = {None, \"\", \" \"}\n\n\n# hidapi is not thread-safe\n# see https://github.com/signal11/hidapi/issues/205#issuecomment-527654560\n#     https://github.com/libusb/hidapi/issues/45\n#     https://github.com/signal11/hidapi/issues/45#issuecomment-4434598\n#     https://github.com/signal11/hidapi/pull/414#issuecomment-445164238\n# It is not entirely clear to me, exactly what is safe and what isn't, when\n# using multiple threads...\n# Hence, we use a single thread for all device communications, including\n# enumeration. Everything that uses hidapi, libusb, etc, MUST run on\n# the following thread:\n_hwd_comms_executor = concurrent.futures.ThreadPoolExecutor(\n    max_workers=1,\n    thread_name_prefix='hwd_comms_thread'\n)\n\n# hidapi needs to be imported from the main thread. Otherwise, at least on macOS,\n# segfaults will follow. (see https://github.com/trezor/cython-hidapi/pull/150#issuecomment-1542391087)\n# To keep it simple, let's just import it now, as we are likely in the main thread here.\nif threading.current_thread() is not threading.main_thread():\n    _logger.warning(\"expected to be in main thread... hidapi will not be safe to use now!\")\ntry:\n    import hid\nexcept ImportError:\n    pass\n\n\nT = TypeVar('T')\n\n\ndef run_in_hwd_thread(func: Callable[[], T]) -> T:\n    if threading.current_thread().name.startswith(\"hwd_comms_thread\"):\n        return func()\n    else:\n        fut = _hwd_comms_executor.submit(func)\n        return fut.result()\n        #except (concurrent.futures.CancelledError, concurrent.futures.TimeoutError) as e:\n\n\ndef runs_in_hwd_thread(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        return run_in_hwd_thread(partial(func, *args, **kwargs))\n    return wrapper\n\n\ndef assert_runs_in_hwd_thread():\n    if not threading.current_thread().name.startswith(\"hwd_comms_thread\"):\n        raise Exception(\"must only be called from HWD communication thread\")\n\n\nclass DeviceMgr(ThreadJob):\n    \"\"\"Manages hardware clients.  A client communicates over a hardware\n    channel with the device.\n\n    In addition to tracking device HID IDs, the device manager tracks\n    hardware wallets and manages wallet pairing.  A HID ID may be\n    paired with a wallet when it is confirmed that the hardware device\n    matches the wallet, i.e. they have the same master public key.  A\n    HID ID can be unpaired if e.g. it is wiped.\n\n    Because of hotplugging, a wallet must request its client\n    dynamically each time it is required, rather than caching it\n    itself.\n\n    The device manager is shared across plugins, so just one place\n    does hardware scans when needed.  By tracking HID IDs, if a device\n    is plugged into a different port the wallet is automatically\n    re-paired.\n\n    Wallets are informed on connect / disconnect events.  It must\n    implement connected(), disconnected() callbacks.  Being connected\n    implies a pairing.  Callbacks can happen in any thread context,\n    and we do them without holding the lock.\n\n    Confusingly, the HID ID (serial number) reported by the HID system\n    doesn't match the device ID reported by the device itself.  We use\n    the HID IDs.\n\n    This plugin is thread-safe.  Currently only devices supported by\n    hidapi are implemented.\"\"\"\n\n    def __init__(self, config: SimpleConfig):\n        ThreadJob.__init__(self)\n        # A pairing_code->id_ map. Item only present if we have active pairing. Needs self.lock.\n        self.pairing_code_to_id = {}  # type: Dict[str, str]\n        # A client->id_ map. Needs self.lock.\n        self.clients = {}  # type: Dict[HardwareClientBase, str]\n        # What we recognise.  (vendor_id, product_id) -> Plugin\n        self._recognised_hardware = {}  # type: Dict[Tuple[int, int], HW_PluginBase]\n        self._recognised_vendor = {}  # type: Dict[int, HW_PluginBase]  # vendor_id -> Plugin\n        # Custom enumerate functions for devices we don't know about.\n        self._enumerate_func = set()  # Needs self.lock.\n        self._ongoing_timeout_checks = {}  # type: Dict[str, Future]\n\n        self.lock = threading.RLock()\n\n        self.config = config\n\n    def thread_jobs(self):\n        # Thread job to handle device timeouts\n        return [self]\n\n    def run(self):\n        \"\"\"Handle device timeouts.  Runs in the context of the Plugins\n        thread.\"\"\"\n        with self.lock:\n            clients = list(self.clients.items())\n        cutoff = time.time() - self.config.get_session_timeout()\n        for client, client_id in clients:\n            if fut := self._ongoing_timeout_checks.get(client_id):\n                if not fut.done():\n                    continue\n            # scheduling the timeout check prevents blocking the Plugins DaemonThread if the\n            # _hwd_comms_executor Thread is blocked (e.g. due to it awaiting user input).\n            fut = _hwd_comms_executor.submit(client.timeout, cutoff)\n            self._ongoing_timeout_checks[client_id] = fut\n\n    def register_devices(self, device_pairs, *, plugin: 'HW_PluginBase'):\n        for pair in device_pairs:\n            self._recognised_hardware[pair] = plugin\n\n    def register_vendor_ids(self, vendor_ids: Iterable[int], *, plugin: 'HW_PluginBase'):\n        for vendor_id in vendor_ids:\n            self._recognised_vendor[vendor_id] = plugin\n\n    def register_enumerate_func(self, func):\n        with self.lock:\n            self._enumerate_func.add(func)\n\n    @runs_in_hwd_thread\n    def create_client(self, device: 'Device', handler: Optional['HardwareHandlerBase'],\n                      plugin: 'HW_PluginBase') -> Optional['HardwareClientBase']:\n        # Get from cache first\n        client = self._client_by_id(device.id_)\n        if client:\n            return client\n        client = plugin.create_client(device, handler)\n        if client:\n            self.logger.info(f\"Registering {client}\")\n            with self.lock:\n                self.clients[client] = device.id_\n        return client\n\n    def id_by_pairing_code(self, pairing_code):\n        with self.lock:\n            return self.pairing_code_to_id.get(pairing_code)\n\n    def pairing_code_by_id(self, id_):\n        with self.lock:\n            for pairing_code, id2 in self.pairing_code_to_id.items():\n                if id2 == id_:\n                    return pairing_code\n            return None\n\n    def unpair_pairing_code(self, pairing_code):\n        with self.lock:\n            if pairing_code not in self.pairing_code_to_id:\n                return\n            _id = self.pairing_code_to_id.pop(pairing_code)\n        self._close_client(_id)\n\n    def unpair_id(self, id_):\n        pairing_code = self.pairing_code_by_id(id_)\n        if pairing_code:\n            self.unpair_pairing_code(pairing_code)\n        else:\n            self._close_client(id_)\n\n    def _close_client(self, id_):\n        with self.lock:\n            client = self._client_by_id(id_)\n            self.clients.pop(client, None)\n            if fut := self._ongoing_timeout_checks.pop(id_, None):\n                fut.cancel()\n        if client:\n            client.close()\n\n    def _client_by_id(self, id_) -> Optional['HardwareClientBase']:\n        with self.lock:\n            for client, client_id in self.clients.items():\n                if client_id == id_:\n                    return client\n        return None\n\n    def client_by_id(self, id_, *, scan_now: bool = True) -> Optional['HardwareClientBase']:\n        \"\"\"Returns a client for the device ID if one is registered.  If\n        a device is wiped or in bootloader mode pairing is impossible;\n        in such cases we communicate by device ID and not wallet.\"\"\"\n        if scan_now:\n            self.scan_devices()\n        return self._client_by_id(id_)\n\n    @runs_in_hwd_thread\n    def client_for_keystore(self, plugin: 'HW_PluginBase', handler: Optional['HardwareHandlerBase'],\n                            keystore: 'Hardware_KeyStore',\n                            force_pair: bool, *,\n                            devices: Sequence['Device'] = None,\n                            allow_user_interaction: bool = True) -> Optional['HardwareClientBase']:\n        self.logger.info(\"getting client for keystore\")\n        if handler is None:\n            raise Exception(_(\"Handler not found for {}\").format(plugin.name) + '\\n' + _(\"A library is probably missing.\"))\n        handler.update_status(False)\n        pcode = keystore.pairing_code()\n        client = None\n        # search existing clients first (fast-path)\n        if not devices:\n            client = self.client_by_pairing_code(plugin=plugin, pairing_code=pcode, handler=handler, devices=[])\n        # search clients again, now allowing a (slow) scan\n        if client is None:\n            if devices is None:\n                devices = self.scan_devices()\n            client = self.client_by_pairing_code(plugin=plugin, pairing_code=pcode, handler=handler, devices=devices)\n        if client is None and force_pair:\n            try:\n                info = self.select_device(plugin, handler, keystore, devices,\n                                          allow_user_interaction=allow_user_interaction)\n            except CannotAutoSelectDevice:\n                pass\n            else:\n                client = self.force_pair_keystore(plugin=plugin, handler=handler, info=info, keystore=keystore)\n        if client:\n            handler.update_status(True)\n            # note: if select_device was called, we might also update label etc here:\n            keystore.opportunistically_fill_in_missing_info_from_device(client)\n        self.logger.info(\"end client for keystore\")\n        return client\n\n    def client_by_pairing_code(\n        self, *, plugin: 'HW_PluginBase', pairing_code: str, handler: 'HardwareHandlerBase',\n        devices: Sequence['Device'],\n    ) -> Optional['HardwareClientBase']:\n        _id = self.id_by_pairing_code(pairing_code)\n        client = self._client_by_id(_id)\n        if client:\n            if type(client.plugin) != type(plugin):\n                return\n            # An unpaired client might have another wallet's handler\n            # from a prior scan.  Replace to fix dialog parenting.\n            client.handler = handler\n            return client\n\n        for device in devices:\n            if device.id_ == _id:\n                return self.create_client(device, handler, plugin)\n\n    def force_pair_keystore(\n        self,\n        *,\n        plugin: 'HW_PluginBase',\n        handler: 'HardwareHandlerBase',\n        info: 'DeviceInfo',\n        keystore: 'Hardware_KeyStore',\n    ) -> 'HardwareClientBase':\n        xpub = keystore.xpub\n        derivation = keystore.get_derivation_prefix()\n        assert derivation is not None\n        xtype = bip32.xpub_type(xpub)\n        client = self._client_by_id(info.device.id_)\n        if client and client.is_pairable() and type(client.plugin) == type(plugin):\n            # See comment above for same code\n            client.handler = handler\n            # This will trigger a PIN/passphrase entry request\n            try:\n                client_xpub = client.get_xpub(derivation, xtype)\n            except (UserCancelled, RuntimeError):\n                # Bad / cancelled PIN / passphrase\n                client_xpub = None\n            if client_xpub == xpub:\n                keystore.opportunistically_fill_in_missing_info_from_device(client)\n                with self.lock:\n                    self.pairing_code_to_id[keystore.pairing_code()] = info.device.id_\n                return client\n\n        # The user input has wrong PIN or passphrase, or cancelled input,\n        # or it is not pairable\n        raise DeviceUnpairableError(\n            _('Electrum cannot pair with your {}.\\n\\n'\n              'Before you request bitcoins to be sent to addresses in this '\n              'wallet, ensure you can pair with your device, or that you have '\n              'its seed (and passphrase, if any).  Otherwise all bitcoins you '\n              'receive will be unspendable.').format(plugin.device))\n\n    def list_pairable_device_infos(\n        self,\n        *,\n        handler: Optional['HardwareHandlerBase'],\n        plugin: 'HW_PluginBase',\n        devices: Sequence['Device'] = None,\n        include_failing_clients: bool = False,\n    ) -> List['DeviceInfo']:\n        \"\"\"Returns a list of DeviceInfo objects: one for each connected device accepted by the plugin.\n        Already paired devices are also included, as it is okay to reuse them.\n        \"\"\"\n        if not plugin.libraries_available:\n            message = plugin.get_library_not_available_message()\n            raise HardwarePluginLibraryUnavailable(message)\n        if devices is None:\n            devices = self.scan_devices()\n        infos = []\n        for device in devices:\n            if not plugin.can_recognize_device(device):\n                continue\n            try:\n                client = self.create_client(device, handler, plugin)\n                if not client:\n                    continue\n                label = client.label()\n                is_initialized = client.is_initialized()\n                soft_device_id = client.get_soft_device_id()\n                model_name = client.device_model_name()\n            except Exception as e:\n                self.logger.error(f'failed to create client for {plugin.name} at {device.path}: {repr(e)}')\n                if include_failing_clients:\n                    infos.append(DeviceInfo(device=device, exception=e, plugin_name=plugin.name))\n                continue\n            infos.append(DeviceInfo(device=device,\n                                    label=label,\n                                    initialized=is_initialized,\n                                    plugin_name=plugin.name,\n                                    soft_device_id=soft_device_id,\n                                    model_name=model_name))\n\n        return infos\n\n    def select_device(self, plugin: 'HW_PluginBase', handler: 'HardwareHandlerBase',\n                      keystore: 'Hardware_KeyStore', devices: Sequence['Device'] = None,\n                      *, allow_user_interaction: bool = True) -> 'DeviceInfo':\n        \"\"\"Select the device to use for keystore.\"\"\"\n        # ideally this should not be called from the GUI thread...\n        # assert handler.get_gui_thread() != threading.current_thread(), 'must not be called from GUI thread'\n        while True:\n            infos = self.list_pairable_device_infos(handler=handler, plugin=plugin, devices=devices)\n            if infos:\n                break\n            if not allow_user_interaction:\n                raise CannotAutoSelectDevice()\n            msg = _('Please insert your {}').format(plugin.device)\n            msg += \" (\"\n            if keystore.label and keystore.label not in PLACEHOLDER_HW_CLIENT_LABELS:\n                msg += f\"label: {keystore.label}, \"\n            msg += f\"bip32 root fingerprint: {keystore.get_root_fingerprint()!r}\"\n            msg += ').\\n\\n{}\\n\\n{}'.format(\n                _('Verify the cable is connected and that '\n                  'no other application is using it.'),\n                _('Try to connect again?')\n            )\n            if not handler.yes_no_question(msg):\n                raise UserCancelled()\n            devices = None\n\n        # select device automatically. (but only if we have reasonable expectation it is the correct one)\n        # method 1: select device by id\n        if keystore.soft_device_id:\n            for info in infos:\n                if info.soft_device_id == keystore.soft_device_id:\n                    self.logger.debug(f\"select_device. auto-selected(1) {plugin.device}: soft_device_id matched\")\n                    return info\n        # method 2: select device by label\n        #           but only if not a placeholder label and only if there is no collision\n        device_labels = [info.label for info in infos]\n        if (keystore.label not in PLACEHOLDER_HW_CLIENT_LABELS\n                and device_labels.count(keystore.label) == 1):\n            for info in infos:\n                if info.label == keystore.label:\n                    self.logger.debug(f\"select_device. auto-selected(2) {plugin.device}: label recognised\")\n                    return info\n        # method 3: if there is only one device connected, and we don't have useful label/soft_device_id\n        #           saved for keystore anyway, select it\n        if (len(infos) == 1\n                and keystore.label in PLACEHOLDER_HW_CLIENT_LABELS\n                and keystore.soft_device_id is None):\n            self.logger.debug(f\"select_device. auto-selected(3) {plugin.device}: only one device\")\n            return infos[0]\n\n        self.logger.debug(f\"select_device. auto-select failed for {plugin.device}. {allow_user_interaction=}\")\n        if not allow_user_interaction:\n            raise CannotAutoSelectDevice()\n        # ask user to select device manually\n        msg = (\n                _(\"Could not automatically pair with device for given keystore.\") + \"\\n\"\n                + f\"(keystore label: {keystore.label!r}, \"\n                + f\"bip32 root fingerprint: {keystore.get_root_fingerprint()!r})\\n\\n\")\n        msg += _(\"Please select which {} device to use:\").format(plugin.device)\n        msg += \"\\n(\" + _(\"Or click cancel to skip this keystore instead.\") + \")\"\n        choices = [ChoiceItem(key=idx, label=info.label_for_device_select())\n                   for (idx, info) in enumerate(infos)]\n        self.logger.debug(f\"select_device. prompting user for manual selection of {plugin.device}. \"\n                          f\"num options: {len(infos)}. options: {infos}\")\n        c = handler.query_choice(msg, choices)\n        if c is None:\n            raise UserCancelled()\n        info = infos[c]\n        self.logger.debug(f\"select_device. user manually selected {plugin.device}. device info: {info}\")\n        # note: updated label/soft_device_id will be saved after pairing succeeds\n        return info\n\n    @runs_in_hwd_thread\n    def _scan_devices_with_hid(self) -> List['Device']:\n        try:\n            import hid  # noqa: F811\n        except ImportError:\n            return []\n\n        devices = []\n        for d in hid.enumerate(0, 0):\n            vendor_id = d['vendor_id']\n            product_key = (vendor_id, d['product_id'])\n            plugin = None\n            if product_key in self._recognised_hardware:\n                plugin = self._recognised_hardware[product_key]\n            elif vendor_id in self._recognised_vendor:\n                plugin = self._recognised_vendor[vendor_id]\n            if plugin:\n                device = plugin.create_device_from_hid_enumeration(d, product_key=product_key)\n                if device:\n                    devices.append(device)\n        return devices\n\n    @runs_in_hwd_thread\n    @profiler\n    def scan_devices(self) -> Sequence['Device']:\n        self.logger.info(\"scanning devices...\")\n\n        # First see what's connected that we know about\n        devices = self._scan_devices_with_hid()\n\n        # Let plugin handlers enumerate devices we don't know about\n        with self.lock:\n            enumerate_funcs = list(self._enumerate_func)\n        for f in enumerate_funcs:\n            try:\n                new_devices = f()\n            except BaseException as e:\n                self.logger.error(f'custom device enum failed. func {str(f)}, error {e!r}')\n            else:\n                devices.extend(new_devices)\n\n        # find out what was disconnected\n        client_ids = [dev.id_ for dev in devices]\n        disconnected_clients = []\n        with self.lock:\n            connected = {}\n            for client, id_ in self.clients.items():\n                if id_ in client_ids and client.has_usable_connection_with_device():\n                    connected[client] = id_\n                else:\n                    disconnected_clients.append((client, id_))\n            self.clients = connected\n\n        # Unpair disconnected devices\n        for client, id_ in disconnected_clients:\n            self.unpair_id(id_)\n            if client.handler:\n                client.handler.update_status(False)\n\n        return devices\n\n    @classmethod\n    def version_info(cls) -> Mapping[str, Optional[str]]:\n        ret = {}\n        # add libusb\n        try:\n            import usb1\n        except Exception as e:\n            ret[\"libusb.version\"] = None\n        else:\n            ret[\"libusb.version\"] = \".\".join(map(str, usb1.getVersion()[:4]))\n            try:\n                ret[\"libusb.path\"] = usb1.libusb1.libusb._name\n            except AttributeError:\n                ret[\"libusb.path\"] = None\n        # add hidapi\n        try:\n            import hid  # noqa: F811\n            ret[\"hidapi.version\"] = hid.__version__  # available starting with 0.12.0.post2\n        except Exception as e:\n            from importlib.metadata import version\n            try:\n                ret[\"hidapi.version\"] = version(\"hidapi\")\n            except ImportError:\n                ret[\"hidapi.version\"] = None\n        return ret\n\n    def trigger_pairings(\n            self,\n            keystores: Sequence['KeyStore'],\n            *,\n            allow_user_interaction: bool = True,\n            devices: Sequence['Device'] = None,\n    ) -> None:\n        \"\"\"Given a list of keystores, try to pair each with a connected hardware device.\n\n        E.g. for a multisig-wallet, it is more user-friendly to use this method than to\n        try to pair each keystore individually. Consider the following scenario:\n        - three hw keystores in a 2-of-3 multisig wallet, devices d2 (for ks2) and d3 (for ks3) are connected\n        - assume none of the devices are paired yet\n        1. if we tried to individually pair keystores, we might try with ks1 first\n           - but ks1 cannot be paired automatically, as neither d2 nor d3 matches the stored fingerprint\n           - the user might then be prompted if they want to manually pair ks1 with either d2 or d3,\n             which is confusing and error-prone. It's especially problematic if the hw device does\n             not support labels (such as Ledger), as then the user cannot easily distinguish\n             same-type devices. (see #4199)\n        2. instead, if using this method, we would auto-pair ks2-d2 and ks3-d3 first,\n           and then tell the user ks1 could not be paired (and there are no devices left to try)\n        \"\"\"\n        from .keystore import Hardware_KeyStore\n        keystores = [ks for ks in keystores if isinstance(ks, Hardware_KeyStore)]\n        if not keystores:\n            return\n        if devices is None:\n            devices = self.scan_devices()\n        # first pair with all devices that can be auto-selected\n        for ks in keystores:\n            try:\n                ks.get_client(\n                    force_pair=True,\n                    allow_user_interaction=False,\n                    devices=devices,\n                )\n            except UserCancelled:\n                pass\n        if allow_user_interaction:\n            # now do manual selections\n            for ks in keystores:\n                try:\n                    ks.get_client(\n                        force_pair=True,\n                        allow_user_interaction=True,\n                        devices=devices,\n                    )\n                except UserCancelled:\n                    pass\n"
  },
  {
    "path": "electrum/plugins/README",
    "content": "Plugin rules:\n\n * The plugin system of Electrum is designed to allow the development\n   of new features without increasing the core code of Electrum.\n\n * Electrum is written in pure python. if you want to add a feature\n   that requires non-python libraries, then it must be submitted as a\n   plugin. If the feature you want to add requires communication with\n   a remote server (not an Electrum server), then it should be a\n   plugin as well. If the feature you want to add introduces new\n   dependencies in the code, then it should probably be a plugin.\n\n * We expect plugin developers to maintain their plugin code. However,\n   once a plugin is merged in Electrum, we will have to maintain it\n   too, because changes in the Electrum code often require updates in\n   the plugin code. Therefore, plugins have to be easy to maintain. If\n   we believe that a plugin will create too much maintenance work in\n   the future, it will be rejected.\n\n * Plugins should be compatible with Electrum's conventions. If your\n   plugin does not fit with Electrum's architecture, or if we believe\n   that it will create too much maintenance work, it will not be\n   accepted. In particular, do not duplicate existing Electrum code in\n   your plugin.\n\n * We may decide to remove a plugin after it has been merged in\n   Electrum. For this reason, a plugin must be easily removable,\n   without putting at risk the user's bitcoins. If we feel that a\n   plugin cannot be removed without threatening users who rely on it,\n   we will not merge it.\n\n"
  },
  {
    "path": "electrum/plugins/__init__.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\n"
  },
  {
    "path": "electrum/plugins/audio_modem/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/plugins/audio_modem/manifest.json",
    "content": "{\n  \"name\": \"audio_modem\",\n  \"fullname\": \"Audio MODEM\",\n  \"description\": \"Provides support for air-gapped transaction signing.\",\n  \"requires\": [[\"amodem\", \"http://github.com/romanz/amodem/\"]],\n  \"author\":\"Roman Zeyde\",\n  \"icon\":\"speaker.png\",\n  \"available_for\": [\"qt\"]\n}\n"
  },
  {
    "path": "electrum/plugins/audio_modem/qt.py",
    "content": "from functools import partial\nimport zlib\nimport json\nfrom io import BytesIO\nimport sys\nimport platform\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtWidgets import (QComboBox, QGridLayout, QLabel, QPushButton)\n\nfrom electrum.plugin import BasePlugin, hook\nfrom electrum.gui.qt.util import WaitingDialog, EnterButton, WindowModalDialog, read_QIcon\nfrom electrum.i18n import _\nfrom electrum.logging import get_logger\nfrom electrum.gui.qt.util import read_QIcon_from_bytes\n\nif TYPE_CHECKING:\n    from electrum.gui.qt.transaction_dialog import TxDialog\n\n\n_logger = get_logger(__name__)\n\n\ntry:\n    import amodem.audio\n    import amodem.main\n    import amodem.config\n    _logger.info('Audio MODEM is available.')\n    amodem.log.addHandler(amodem.logging.StreamHandler(sys.stderr))\n    amodem.log.setLevel(amodem.logging.INFO)\nexcept ImportError:\n    amodem = None\n    _logger.info('Audio MODEM is not found.')\n\n\nclass Plugin(BasePlugin):\n\n    def __init__(self, parent, config, name):\n        BasePlugin.__init__(self, parent, config, name)\n        if self.is_available():\n            self.modem_config = amodem.config.slowest()\n            self.library_name = {\n                'Linux': 'libportaudio.so'\n            }[platform.system()]\n\n    def is_available(self):\n        return amodem is not None\n\n    def requires_settings(self):\n        return True\n\n    def settings_dialog(self, window, wallet):\n        d = WindowModalDialog(window, _(\"Audio Modem Settings\"))\n\n        layout = QGridLayout(d)\n        layout.addWidget(QLabel(_('Bit rate [kbps]: ')), 0, 0)\n\n        bitrates = list(sorted(amodem.config.bitrates.keys()))\n\n        def _index_changed(index):\n            bitrate = bitrates[index]\n            self.modem_config = amodem.config.bitrates[bitrate]\n\n        combo = QComboBox()\n        combo.addItems([str(x) for x in bitrates])\n        combo.currentIndexChanged.connect(_index_changed)\n        layout.addWidget(combo, 0, 1)\n\n        ok_button = QPushButton(_(\"OK\"))\n        ok_button.clicked.connect(d.accept)\n        layout.addWidget(ok_button, 1, 1)\n\n        return bool(d.exec())\n\n    @hook\n    def transaction_dialog(self, dialog: 'TxDialog'):\n        b = QPushButton()\n        icon = read_QIcon_from_bytes(self.read_file(\"speaker.png\"))\n        b.setIcon(icon)\n        def handler():\n            blob = dialog.tx.serialize()\n            self._send(parent=dialog, blob=blob)\n        b.clicked.connect(handler)\n        dialog.sharing_buttons.insert(-1, b)\n\n    @hook\n    def scan_text_edit(self, parent):\n        icon = read_QIcon_from_bytes(self.read_file(\"microphone.png\"))\n        parent.addButton(icon, partial(self._recv, parent), _(\"Read from microphone\"))\n\n    @hook\n    def show_text_edit(self, parent):\n        def handler():\n            blob = str(parent.toPlainText())\n            self._send(parent=parent, blob=blob)\n        icon = read_QIcon_from_bytes(self.read_file(\"speaker.png\"))\n        parent.addButton(icon, handler, _(\"Send to speaker\"))\n\n    def _audio_interface(self):\n        interface = amodem.audio.Interface(config=self.modem_config)\n        return interface.load(self.library_name)\n\n    def _send(self, parent, blob):\n        def sender_thread():\n            with self._audio_interface() as interface:\n                src = BytesIO(blob)\n                dst = interface.player()\n                amodem.main.send(config=self.modem_config, src=src, dst=dst)\n\n        _logger.info(f'Sending: {repr(blob)}')\n        blob = zlib.compress(blob.encode('ascii'))\n\n        kbps = self.modem_config.modem_bps / 1e3\n        msg = 'Sending to Audio MODEM ({0:.1f} kbps)...'.format(kbps)\n        WaitingDialog(parent, msg, sender_thread)\n\n    def _recv(self, parent):\n        def receiver_thread():\n            with self._audio_interface() as interface:\n                src = interface.recorder()\n                dst = BytesIO()\n                amodem.main.recv(config=self.modem_config, src=src, dst=dst)\n                return dst.getvalue()\n\n        def on_finished(blob):\n            if blob:\n                blob = zlib.decompress(blob).decode('ascii')\n                _logger.info(f'Received: {repr(blob)}')\n                parent.setText(blob)\n\n        kbps = self.modem_config.modem_bps / 1e3\n        msg = 'Receiving from Audio MODEM ({0:.1f} kbps)...'.format(kbps)\n        WaitingDialog(parent, msg, receiver_thread, on_finished)\n"
  },
  {
    "path": "electrum/plugins/bitbox02/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/plugins/bitbox02/bitbox02.py",
    "content": "#\n# BitBox02 Electrum plugin code.\n#\n\nimport hid\nfrom typing import TYPE_CHECKING, Dict, Tuple, Optional, List, Any, Callable\n\nimport electrum_ecc as ecc\n\nfrom electrum import bip32, constants\nfrom electrum.i18n import _\nfrom electrum.keystore import Hardware_KeyStore\nfrom electrum.transaction import PartialTransaction, Sighash\nfrom electrum.wallet import Multisig_Wallet, Deterministic_Wallet\nfrom electrum.util import UserFacingException\nfrom electrum.logging import get_logger\nfrom electrum.plugin import Device, DeviceInfo, runs_in_hwd_thread\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.storage import get_derivation_used_for_hw_device_encryption\nfrom electrum.bitcoin import OnchainOutputType\n\nimport electrum.bitcoin as bitcoin\n\nfrom electrum.hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase\n\nif TYPE_CHECKING:\n    from electrum.wizard import NewWalletWizard\n\n_logger = get_logger(__name__)\n\n\ntry:\n    from bitbox02 import bitbox02\n    from bitbox02 import util\n    from bitbox02.communication import (devices, HARDENED, u2fhid, bitbox_api_protocol,\n                                        FirmwareVersionOutdatedException)\n    requirements_ok = True\nexcept ImportError as e:\n    if not (isinstance(e, ModuleNotFoundError) and e.name == 'bitbox02'):\n        _logger.exception('error importing bitbox02 plugin deps')\n    requirements_ok = False\n\n\nclass BitBox02NotInitialized(UserFacingException):\n    pass\n\n\nclass BitBox02Client(HardwareClientBase):\n    # handler is a BitBox02_Handler, importing it would lead to a circular dependency\n    def __init__(self, handler: HardwareHandlerBase, device: Device, config: SimpleConfig, *, plugin: HW_PluginBase):\n        HardwareClientBase.__init__(self, plugin=plugin)\n        self.bitbox02_device = None  # type: Optional[bitbox02.BitBox02]\n        self.handler = handler\n        self.device_descriptor = device\n        self.config = config\n        self.bitbox_hid_info = None\n        if self.config.get(\"bitbox02\") is None:\n            bitbox02_config: dict = {\n                \"remote_static_noise_keys\": [],\n                \"noise_privkey\": None,\n            }\n            self.config.set_key(\"bitbox02\", bitbox02_config)\n\n        bitboxes = devices.get_any_bitbox02s()\n        for bitbox in bitboxes:\n            if (\n                bitbox[\"path\"] == self.device_descriptor.path\n                and bitbox[\"interface_number\"]\n                == self.device_descriptor.interface_number\n            ):\n                self.bitbox_hid_info = bitbox\n        if self.bitbox_hid_info is None:\n            raise Exception(\"No BitBox02 detected\")\n\n    def device_model_name(self) -> Optional[str]:\n        return 'BitBox02'\n\n    def is_initialized(self) -> bool:\n        return True\n\n    @runs_in_hwd_thread\n    def close(self):\n        try:\n            self.bitbox02_device.close()\n        except Exception:\n            pass\n\n    def has_usable_connection_with_device(self) -> bool:\n        if self.bitbox_hid_info is None:\n            return False\n        return True\n\n    @runs_in_hwd_thread\n    def get_soft_device_id(self) -> Optional[str]:\n        if self.handler is None:\n            # Can't do the pairing without the handler. This happens at wallet creation time, when\n            # listing the devices.\n            return None\n        if self.bitbox02_device is None:\n            self.pairing_dialog()\n        return self.bitbox02_device.root_fingerprint().hex()\n\n    @runs_in_hwd_thread\n    def pairing_dialog(self):\n        def pairing_step(code: str, device_response: Callable[[], bool]) -> bool:\n            msg = \"Please compare and confirm the pairing code on your BitBox02:\\n\" + code\n            self.handler.show_message(msg)\n            try:\n                res = device_response()\n            except Exception:\n                # Close the hid device on exception\n                hid_device.close()\n                raise\n            finally:\n                self.handler.finished()\n            return res\n\n        def exists_remote_static_pubkey(pubkey: bytes) -> bool:\n            bitbox02_config = self.config.get(\"bitbox02\")\n            noise_keys = bitbox02_config.get(\"remote_static_noise_keys\")\n            if noise_keys is not None:\n                if pubkey.hex() in noise_keys:\n                    return True\n            return False\n\n        def set_remote_static_pubkey(pubkey: bytes) -> None:\n            if not exists_remote_static_pubkey(pubkey):\n                bitbox02_config = self.config.get(\"bitbox02\")\n                if bitbox02_config.get(\"remote_static_noise_keys\") is not None:\n                    bitbox02_config[\"remote_static_noise_keys\"].append(pubkey.hex())\n                else:\n                    bitbox02_config[\"remote_static_noise_keys\"] = [pubkey.hex()]\n                self.config.set_key(\"bitbox02\", bitbox02_config)\n\n        def get_noise_privkey() -> Optional[bytes]:\n            bitbox02_config = self.config.get(\"bitbox02\")\n            privkey = bitbox02_config.get(\"noise_privkey\")\n            if privkey is not None:\n                return bytes.fromhex(privkey)\n            return None\n\n        def set_noise_privkey(privkey: bytes) -> None:\n            bitbox02_config = self.config.get(\"bitbox02\")\n            bitbox02_config[\"noise_privkey\"] = privkey.hex()\n            self.config.set_key(\"bitbox02\", bitbox02_config)\n\n        def attestation_warning() -> None:\n            self.handler.show_error(\n                \"The BitBox02 attestation failed.\\nTry reconnecting the BitBox02.\\nWarning: The device might not be genuine, if the\\n problem persists please contact Shift support.\",\n                blocking=True\n            )\n\n        class NoiseConfig(bitbox_api_protocol.BitBoxNoiseConfig):\n            \"\"\"NoiseConfig extends BitBoxNoiseConfig\"\"\"\n\n            def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool:\n                return pairing_step(code, device_response)\n\n            def attestation_check(self, result: bool) -> None:\n                if not result:\n                    attestation_warning()\n\n            def contains_device_static_pubkey(self, pubkey: bytes) -> bool:\n                return exists_remote_static_pubkey(pubkey)\n\n            def add_device_static_pubkey(self, pubkey: bytes) -> None:\n                return set_remote_static_pubkey(pubkey)\n\n            def get_app_static_privkey(self) -> Optional[bytes]:\n                return get_noise_privkey()\n\n            def set_app_static_privkey(self, privkey: bytes) -> None:\n                return set_noise_privkey(privkey)\n\n        if self.bitbox02_device is None:\n            hid_device = hid.device()\n            hid_device.open_path(self.bitbox_hid_info[\"path\"])\n\n            bitbox02_device = bitbox02.BitBox02(\n                transport=u2fhid.U2FHid(hid_device),\n                device_info=self.bitbox_hid_info,\n                noise_config=NoiseConfig(),\n            )\n            try:\n                bitbox02_device.check_min_version()\n            except FirmwareVersionOutdatedException:\n                raise\n            self.bitbox02_device = bitbox02_device\n\n        self.fail_if_not_initialized()\n\n    def fail_if_not_initialized(self) -> None:\n        assert self.bitbox02_device\n        if not self.bitbox02_device.device_info()[\"initialized\"]:\n            raise BitBox02NotInitialized(\n                \"Please initialize the BitBox02 using the BitBox app first before using the BitBox02 in electrum\"\n            )\n\n    def coin_network_from_electrum_network(self) -> int:\n        if constants.net.TESTNET:\n            return bitbox02.btc.TBTC\n        return bitbox02.btc.BTC\n\n    @runs_in_hwd_thread\n    def get_password_for_storage_encryption(self) -> str:\n        if self.bitbox02_device is None:\n            self.pairing_dialog()\n\n        if self.bitbox02_device is None:\n            raise Exception(\n                \"Need to setup communication first before attempting any BitBox02 calls\"\n            )\n\n        derivation = get_derivation_used_for_hw_device_encryption()\n        derivation_list = bip32.convert_bip32_strpath_to_intpath(derivation)\n        xpub = self.bitbox02_device.electrum_encryption_key(derivation_list)\n        node = bip32.BIP32Node.from_xkey(xpub, net = constants.BitcoinMainnet()).subkey_at_public_derivation(())\n        return node.eckey.get_public_key_bytes(compressed=True).hex()\n\n    @runs_in_hwd_thread\n    def get_xpub(self, bip32_path: str, xtype: str, *, display: bool = False) -> str:\n        if self.bitbox02_device is None:\n            self.pairing_dialog()\n\n        if self.bitbox02_device is None:\n            raise Exception(\n                \"Need to setup communication first before attempting any BitBox02 calls\"\n            )\n\n        self.fail_if_not_initialized()\n\n        xpub_keypath = bip32.convert_bip32_strpath_to_intpath(bip32_path)\n        coin_network = self.coin_network_from_electrum_network()\n\n        if xtype == \"p2wpkh\":\n            if coin_network == bitbox02.btc.BTC:\n                out_type = bitbox02.btc.BTCPubRequest.ZPUB\n            else:\n                out_type = bitbox02.btc.BTCPubRequest.VPUB\n        elif xtype == \"p2wpkh-p2sh\":\n            if coin_network == bitbox02.btc.BTC:\n                out_type = bitbox02.btc.BTCPubRequest.YPUB\n            else:\n                out_type = bitbox02.btc.BTCPubRequest.UPUB\n        elif xtype == \"p2wsh-p2sh\":\n            if coin_network == bitbox02.btc.BTC:\n                out_type = bitbox02.btc.BTCPubRequest.CAPITAL_YPUB\n            else:\n                out_type = bitbox02.btc.BTCPubRequest.CAPITAL_UPUB\n        elif xtype == \"p2wsh\":\n            if coin_network == bitbox02.btc.BTC:\n                out_type = bitbox02.btc.BTCPubRequest.CAPITAL_ZPUB\n            else:\n                out_type = bitbox02.btc.BTCPubRequest.CAPITAL_VPUB\n        # The other legacy types are not supported\n        else:\n            raise Exception(\"invalid xtype:{}\".format(xtype))\n\n        return self.bitbox02_device.btc_xpub(keypath=xpub_keypath, xpub_type=out_type, coin=coin_network,\n                                             display=display)\n\n    @runs_in_hwd_thread\n    def label(self) -> str:\n        if self.handler is None:\n            # Can't do the pairing without the handler. This happens at wallet creation time, when\n            # listing the devices.\n            return super().label()\n        if self.bitbox02_device is None:\n            self.pairing_dialog()\n        # We add the fingerprint to the label, as if there are two devices with the same label, the\n        # device manager can mistake one for another and fail.\n        return \"%s (%s)\" % (\n            self.bitbox02_device.device_info()[\"name\"],\n            self.bitbox02_device.root_fingerprint().hex(),\n        )\n\n    @runs_in_hwd_thread\n    def request_root_fingerprint_from_device(self) -> str:\n        if self.bitbox02_device is None:\n            raise Exception(\n                \"Need to setup communication first before attempting any BitBox02 calls\"\n            )\n\n        return self.bitbox02_device.root_fingerprint().hex()\n\n    def is_pairable(self) -> bool:\n        if self.bitbox_hid_info is None:\n            return False\n        return True\n\n    @runs_in_hwd_thread\n    def btc_multisig_config(\n        self, coin, bip32_path: List[int], wallet: Multisig_Wallet, xtype: str,\n    ):\n        \"\"\"\n        Set and get a multisig config with the current device and some other arbitrary xpubs.\n        Registers it on the device if not already registered.\n        xtype: 'p2wsh' | 'p2wsh-p2sh'\n        \"\"\"\n        assert xtype in (\"p2wsh\", \"p2wsh-p2sh\")\n        if self.bitbox02_device is None:\n            raise Exception(\n                \"Need to setup communication first before attempting any BitBox02 calls\"\n            )\n        account_keypath = bip32_path[:-2]\n        xpubs = wallet.get_master_public_keys()\n        our_xpub = self.get_xpub(\n            bip32.convert_bip32_intpath_to_strpath(account_keypath), xtype\n        )\n\n        multisig_config = bitbox02.btc.BTCScriptConfig(\n            multisig=bitbox02.btc.BTCScriptConfig.Multisig(\n                threshold=wallet.m,\n                xpubs=[util.parse_xpub(xpub) for xpub in xpubs],\n                our_xpub_index=xpubs.index(our_xpub),\n                script_type={\n                    \"p2wsh\": bitbox02.btc.BTCScriptConfig.Multisig.P2WSH,\n                    \"p2wsh-p2sh\": bitbox02.btc.BTCScriptConfig.Multisig.P2WSH_P2SH,\n                }[xtype]\n            )\n        )\n\n        is_registered = self.bitbox02_device.btc_is_script_config_registered(\n            coin, multisig_config, account_keypath\n        )\n        if not is_registered:\n            name = self.handler.name_multisig_account()\n            try:\n                self.bitbox02_device.btc_register_script_config(\n                    coin=coin,\n                    script_config=multisig_config,\n                    keypath=account_keypath,\n                    name=name,\n                )\n            except bitbox02.DuplicateEntryException:\n                raise\n            except Exception:\n                raise UserFacingException(_('Failed to register multisig\\naccount configuration on BitBox02'))\n        return multisig_config\n\n    @runs_in_hwd_thread\n    def show_address(\n        self, bip32_path: str, address_type: str, wallet: Deterministic_Wallet\n    ) -> str:\n\n        if self.bitbox02_device is None:\n            raise Exception(\n                \"Need to setup communication first before attempting any BitBox02 calls\"\n            )\n\n        address_keypath = bip32.convert_bip32_strpath_to_intpath(bip32_path)\n        coin_network = self.coin_network_from_electrum_network()\n\n        if address_type == \"p2wpkh\":\n            script_config = bitbox02.btc.BTCScriptConfig(\n                simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH\n            )\n        elif address_type == \"p2wpkh-p2sh\":\n            script_config = bitbox02.btc.BTCScriptConfig(\n                simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH\n            )\n        elif address_type in (\"p2wsh-p2sh\", \"p2wsh\"):\n            if type(wallet) is Multisig_Wallet:\n                script_config = self.btc_multisig_config(\n                    coin_network, address_keypath, wallet, address_type,\n                )\n            else:\n                raise Exception(\"Can only use p2wsh-p2sh or p2wsh with multisig wallets\")\n        else:\n            raise Exception(\n                \"invalid address xtype: {} is not supported by the BitBox02\".format(\n                    address_type\n                )\n            )\n\n        return self.bitbox02_device.btc_address(\n            keypath=address_keypath,\n            coin=coin_network,\n            script_config=script_config,\n            display=True,\n        )\n\n    def _get_coin(self):\n        return bitbox02.btc.TBTC if constants.net.TESTNET else bitbox02.btc.BTC\n\n    @runs_in_hwd_thread\n    def sign_transaction(\n        self,\n        keystore: Hardware_KeyStore,\n        tx: PartialTransaction,\n        wallet: Deterministic_Wallet,\n    ):\n        if tx.is_complete():\n            return\n\n        if self.bitbox02_device is None:\n            raise Exception(\n                \"Need to setup communication first before attempting any BitBox02 calls\"\n            )\n\n        coin = self._get_coin()\n        tx_script_type = None\n\n        # Build BTCInputType list\n        inputs = []\n        for txin in tx.inputs():\n            my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)\n\n            if full_path is None:\n                raise Exception(\n                    \"A wallet owned pubkey was not found in the transaction input to be signed\"\n                )\n\n            prev_tx = txin.utxo\n            if prev_tx is None:\n                raise UserFacingException(_('Missing previous tx.'))\n\n            prev_inputs: List[bitbox02.BTCPrevTxInputType] = []\n            prev_outputs: List[bitbox02.BTCPrevTxOutputType] = []\n            for prev_txin in prev_tx.inputs():\n                prev_inputs.append(\n                    {\n                        \"prev_out_hash\": prev_txin.prevout.txid[::-1],\n                        \"prev_out_index\": prev_txin.prevout.out_idx,\n                        \"signature_script\": prev_txin.script_sig,\n                        \"sequence\": prev_txin.nsequence,\n                    }\n                )\n            for prev_txout in prev_tx.outputs():\n                prev_outputs.append(\n                    {\n                        \"value\": prev_txout.value,\n                        \"pubkey_script\": prev_txout.scriptpubkey,\n                    }\n                )\n\n            inputs.append(\n                {\n                    \"prev_out_hash\": txin.prevout.txid[::-1],\n                    \"prev_out_index\": txin.prevout.out_idx,\n                    \"prev_out_value\": txin.value_sats(),\n                    \"sequence\": txin.nsequence,\n                    \"keypath\": full_path,\n                    \"script_config_index\": 0,\n                    \"prev_tx\": {\n                        \"version\": prev_tx.version,\n                        \"locktime\": prev_tx.locktime,\n                        \"inputs\": prev_inputs,\n                        \"outputs\": prev_outputs,\n                    },\n                }\n            )\n\n            desc = txin.script_descriptor\n            assert desc\n            if tx_script_type is None:\n                tx_script_type = desc.to_legacy_electrum_script_type()\n            elif tx_script_type != desc.to_legacy_electrum_script_type():\n                raise Exception(\"Cannot mix different input script types\")\n\n        if tx_script_type == \"p2wpkh\":\n            tx_script_type = bitbox02.btc.BTCScriptConfig(\n                simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH\n            )\n        elif tx_script_type == \"p2wpkh-p2sh\":\n            tx_script_type = bitbox02.btc.BTCScriptConfig(\n                simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH\n            )\n        elif tx_script_type in (\"p2wsh-p2sh\", \"p2wsh\"):\n            if type(wallet) is Multisig_Wallet:\n                tx_script_type = self.btc_multisig_config(coin, full_path, wallet, tx_script_type)\n            else:\n                raise Exception(\"Can only use p2wsh-p2sh or p2wsh with multisig wallets\")\n        else:\n            raise UserFacingException(\n                _('Invalid input script type: {} is not supported by the BitBox02').format(tx_script_type)\n            )\n\n        # Build BTCOutputType list\n        outputs = []\n        for txout in tx.outputs():\n            assert txout.address\n            # check for change\n            if txout.is_change:\n                my_pubkey, change_pubkey_path = keystore.find_my_pubkey_in_txinout(txout)\n                outputs.append(\n                    bitbox02.BTCOutputInternal(\n                        keypath=change_pubkey_path, value=txout.value, script_config_index=0,\n                    )\n                )\n            else:\n                addrtype, payload = bitcoin.address_to_payload(txout.address)\n                if addrtype == OnchainOutputType.P2PKH:\n                    output_type = bitbox02.btc.P2PKH\n                elif addrtype == OnchainOutputType.P2SH:\n                    output_type = bitbox02.btc.P2SH\n                elif addrtype == OnchainOutputType.WITVER0_P2WPKH:\n                    output_type = bitbox02.btc.P2WPKH\n                elif addrtype == OnchainOutputType.WITVER0_P2WSH:\n                    output_type = bitbox02.btc.P2WSH\n                elif addrtype == OnchainOutputType.WITVER1_P2TR:\n                    output_type = bitbox02.btc.P2TR\n                else:\n                    raise UserFacingException(\n                        _('Received unsupported output type during transaction signing: {} is not supported by the BitBox02').format(\n                            addrtype\n                        )\n                    )\n                outputs.append(\n                    bitbox02.BTCOutputExternal(\n                        output_type=output_type,\n                        output_payload=payload,\n                        value=txout.value,\n                    )\n                )\n\n        keypath_account = full_path[:-2]\n\n        format_unit = bitbox02.btc.BTCSignInitRequest.FormatUnit.DEFAULT\n        # Base unit is configured to be \"sat\":\n        if self.config.BTC_AMOUNTS_DECIMAL_POINT == 0:\n            format_unit = bitbox02.btc.BTCSignInitRequest.FormatUnit.SAT\n\n        sigs = self.bitbox02_device.btc_sign(\n            coin,\n            [bitbox02.btc.BTCScriptConfigWithKeypath(\n                script_config=tx_script_type,\n                keypath=keypath_account,\n            )],\n            inputs=inputs,\n            outputs=outputs,\n            locktime=tx.locktime,\n            version=tx.version,\n            format_unit=format_unit,\n        )\n\n        # Fill signatures\n        if len(sigs) != len(tx.inputs()):\n            raise Exception(\"Incorrect number of inputs signed.\")  # Should never occur\n        sighash = Sighash.to_sigbytes(Sighash.ALL)\n        signatures = [ecc.ecdsa_der_sig_from_ecdsa_sig64(x[1]) + sighash for x in sigs]\n        tx.update_signatures(signatures)\n\n    def sign_message(self, keypath: str, message: bytes, script_type: str) -> bytes:\n        if self.bitbox02_device is None:\n            raise Exception(\n                \"Need to setup communication first before attempting any BitBox02 calls\"\n            )\n\n        try:\n            simple_type = {\n                \"p2wpkh-p2sh\":bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH,\n                \"p2wpkh\": bitbox02.btc.BTCScriptConfig.P2WPKH,\n            }[script_type]\n        except KeyError:\n            raise UserFacingException(_('The BitBox02 does not support signing messages for this address type: {}').format(script_type))\n\n        _a, _b, signature = self.bitbox02_device.btc_sign_msg(\n            self._get_coin(),\n            bitbox02.btc.BTCScriptConfigWithKeypath(\n                script_config=bitbox02.btc.BTCScriptConfig(\n                    simple_type=simple_type,\n                ),\n                keypath=bip32.convert_bip32_strpath_to_intpath(keypath),\n            ),\n            message,\n        )\n        return signature\n\n\nclass BitBox02_KeyStore(Hardware_KeyStore):\n    hw_type = \"bitbox02\"\n    device = \"BitBox02\"\n    plugin: \"BitBox02Plugin\"\n\n    def __init__(self, d: dict):\n        super().__init__(d)\n        self.ux_busy = False\n\n    def give_error(self, message: str | BaseException):\n        self.logger.info(message)\n        if not self.ux_busy:\n            self.handler.show_error(str(message))\n        else:\n            self.ux_busy = False\n        raise UserFacingException(message)\n\n    def decrypt_message(self, pubkey, message, password):\n        raise UserFacingException(\n            _('Message encryption, decryption and signing are currently not supported for {}').format(self.device)\n        )\n\n    def sign_message(self, sequence, message, password, *, script_type=None):\n        if constants.net.TESTNET:\n            raise UserFacingException(\n                _(\"The {} only supports message signing on mainnet.\").format(self.device)\n            )\n        if password:\n            raise Exception(\"BitBox02 does not accept a password from the host\")\n        client = self.get_client()\n        keypath = self.get_derivation_prefix() + \"/%d/%d\" % sequence\n        return client.sign_message(keypath, message.encode(\"utf-8\"), script_type)\n\n    @runs_in_hwd_thread\n    def sign_transaction(self, tx: PartialTransaction, password: str):\n        if tx.is_complete():\n            return\n        client = self.get_client()\n        assert isinstance(client, BitBox02Client)\n\n        try:\n            try:\n                self.handler.show_message(\"Authorize Transaction...\")\n                client.sign_transaction(self, tx, self.handler.get_wallet())\n            finally:\n                self.handler.finished()\n\n        except Exception as e:\n            self.logger.exception(\"\")\n            self.give_error(e)\n            return\n\n    @runs_in_hwd_thread\n    def show_address(\n        self, sequence: Tuple[int, int], txin_type: str, wallet: Deterministic_Wallet\n    ):\n        client = self.get_client()\n        address_path = \"{}/{}/{}\".format(\n            self.get_derivation_prefix(), sequence[0], sequence[1]\n        )\n        try:\n            try:\n                self.handler.show_message(_(\"Showing address ...\"))\n                dev_addr = client.show_address(address_path, txin_type, wallet)\n            finally:\n                self.handler.finished()\n        except Exception as e:\n            self.logger.exception(\"\")\n            self.handler.show_error(str(e))\n\n\nclass BitBox02Plugin(HW_PluginBase):\n    keystore_class = BitBox02_KeyStore\n    minimum_library = (7, 0, 0)\n    DEVICE_IDS = [(0x03EB, 0x2403)]\n\n    SUPPORTED_XTYPES = (\"p2wpkh-p2sh\", \"p2wpkh\", \"p2wsh\", \"p2wsh-p2sh\")\n\n    def __init__(self, parent: HW_PluginBase, config: SimpleConfig, name: str):\n        super().__init__(parent, config, name)\n\n        self.libraries_available = self.check_libraries_available()\n        if not self.libraries_available:\n            return\n        self.device_manager().register_devices(self.DEVICE_IDS, plugin=self)\n\n    def get_library_version(self):\n        try:\n            from bitbox02 import bitbox02\n            version = bitbox02.__version__\n        except Exception:\n            version = \"unknown\"\n        if requirements_ok:\n            return version\n        else:\n            raise ImportError()\n\n    @runs_in_hwd_thread\n    def create_client(self, device, handler) -> BitBox02Client:\n        return BitBox02Client(handler, device, self.config, plugin=self)\n\n    @runs_in_hwd_thread\n    def show_address(\n        self,\n        wallet: Deterministic_Wallet,\n        address: str,\n        keystore: BitBox02_KeyStore = None,\n    ):\n        if keystore is None:\n            keystore = wallet.get_keystore()\n        if not self.show_address_helper(wallet, address, keystore):\n            return\n\n        txin_type = wallet.get_txin_type(address)\n        sequence = wallet.get_address_index(address)\n        keystore.show_address(sequence, txin_type, wallet)\n\n    @runs_in_hwd_thread\n    def show_xpub(self, keystore: BitBox02_KeyStore):\n        client = keystore.get_client()\n        assert isinstance(client, BitBox02Client)\n        derivation = keystore.get_derivation_prefix()\n        xtype = keystore.get_bip32_node_for_xpub().xtype\n        client.get_xpub(derivation, xtype, display=True)\n\n    def create_device_from_hid_enumeration(self, d: dict, *, product_key) -> 'Device':\n        device = super().create_device_from_hid_enumeration(d, product_key=product_key)\n        # The BitBox02's product_id is not unique per device, thus use the path instead to\n        # distinguish devices.\n        id_ = str(d['path'])\n        return device._replace(id_=id_)\n\n    def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str:\n        # Note: device_info.initialized for this hardware doesn't imply a seed is present,\n        # only that it has firmware installed\n        if new_wallet:\n            return 'bitbox02_start' if device_info.initialized else 'bitbox02_not_initialized'\n        else:\n            return 'bitbox02_unlock'\n\n    # insert bitbox02 pages in new wallet wizard\n    def extend_wizard(self, wizard: 'NewWalletWizard'):\n        views = {\n            'bitbox02_start': {\n                'next': 'bitbox02_xpub',\n            },\n            'bitbox02_xpub': {\n                'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore',\n                'accept': wizard.maybe_master_pubkey,\n                'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d)\n            },\n            'bitbox02_not_initialized': {},\n            'bitbox02_unlock': {\n                'last': True\n            },\n        }\n        wizard.navmap_merge(views)\n"
  },
  {
    "path": "electrum/plugins/bitbox02/manifest.json",
    "content": "{\n  \"name\": \"bitbox02\",\n  \"fullname\": \"BitBox02\",\n  \"description\": \"Provides support for the BitBox02 hardware wallet\",\n  \"requires\": [[\"bitbox02\", \"https://github.com/digitalbitbox/bitbox02-firmware/tree/master/py/bitbox02\"]],\n  \"registers_keystore\": [\"hardware\", \"bitbox02\", \"BitBox02\"],\n  \"icon\":\"bitbox02.png\",\n  \"available_for\": [\"qt\"]\n}\n"
  },
  {
    "path": "electrum/plugins/bitbox02/qt.py",
    "content": "import threading\nfrom functools import partial\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import Qt, QMetaObject, Q_RETURN_ARG, pyqtSlot, pyqtSignal\nfrom PyQt6.QtWidgets import QLabel, QVBoxLayout, QLineEdit, QHBoxLayout\n\nfrom electrum.i18n import _\nfrom electrum.plugin import hook\nfrom electrum.util import UserCancelled, UserFacingException\n\nfrom .bitbox02 import BitBox02Plugin\nfrom electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase\nfrom electrum.hw_wallet.plugin import only_hook_if_libraries_available, OperationCancelled\n\nfrom electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWUninitialized, WCHWXPub\nfrom electrum.gui.qt.util import WindowModalDialog, OkButton, ButtonsTextEdit, read_QIcon\n\nif TYPE_CHECKING:\n    from electrum.gui.qt.wizard.wallet import QENewWalletWizard\n\n\nclass Plugin(BitBox02Plugin, QtPluginBase):\n    icon_unpaired = \"bitbox02_unpaired.png\"\n    icon_paired = \"bitbox02.png\"\n\n    def create_handler(self, window):\n        return BitBox02_Handler(window)\n\n    @only_hook_if_libraries_available\n    @hook\n    def receive_menu(self, menu, addrs, wallet):\n        if len(addrs) == 1:\n            self._add_menu_action(menu, addrs[0], wallet)\n\n    @only_hook_if_libraries_available\n    @hook\n    def transaction_dialog_address_menu(self, menu, addr, wallet):\n        self._add_menu_action(menu, addr, wallet)\n\n    @only_hook_if_libraries_available\n    @hook\n    def show_xpub_button(self, mpk_text: ButtonsTextEdit, keystore):\n        # user is about to see the \"Wallet Information\" dialog\n        # - add a button to show the xpub on the BitBox02 device\n        if type(keystore) != self.keystore_class:\n            return\n\n        def on_button_click():\n            keystore.thread.add(\n                partial(self.show_xpub, keystore=keystore)\n            )\n\n        device_name = \"{} ({})\".format(self.device, keystore.label)\n        mpk_text.addButton(read_QIcon(\"eye1.png\"), on_button_click, _(\"Show on {}\").format(device_name))\n\n    # insert bitbox02 pages in new wallet wizard\n    def extend_wizard(self, wizard: 'QENewWalletWizard'):\n        super().extend_wizard(wizard)\n        views = {\n            'bitbox02_start': {'gui': WCBitbox02ScriptAndDerivation},\n            'bitbox02_xpub': {'gui': WCHWXPub},\n            'bitbox02_not_initialized': {'gui': WCHWUninitialized},\n            'bitbox02_unlock': {'gui': WCHWUnlock}\n        }\n        wizard.navmap_merge(views)\n\n\nclass BitBox02_Handler(QtHandlerBase):\n    MESSAGE_DIALOG_TITLE = _(\"BitBox02 Status\")\n\n    def __init__(self, win):\n        super(BitBox02_Handler, self).__init__(win, \"BitBox02\")\n\n    def name_multisig_account(self):\n        return QMetaObject.invokeMethod(self, \"_name_multisig_account\", Qt.ConnectionType.BlockingQueuedConnection, Q_RETURN_ARG(str))\n\n    @pyqtSlot(result=str)\n    def _name_multisig_account(self):\n        dialog = WindowModalDialog(None, \"Create Multisig Account\")\n        vbox = QVBoxLayout()\n        label = QLabel(\n            _(\n                \"Enter a descriptive name for your multisig account.\\nYou should later be able to use the name to uniquely identify this multisig account\"\n            )\n        )\n        hl = QHBoxLayout()\n        hl.addWidget(label)\n        name = QLineEdit()\n        name.setMaxLength(30)\n        name.resize(200, 40)\n        he = QHBoxLayout()\n        he.addWidget(name)\n        okButton = OkButton(dialog)\n        hlb = QHBoxLayout()\n        hlb.addWidget(okButton)\n        hlb.addStretch(2)\n        vbox.addLayout(hl)\n        vbox.addLayout(he)\n        vbox.addLayout(hlb)\n        dialog.setLayout(vbox)\n        dialog.exec()\n        return name.text().strip()\n\n\nclass WCBitbox02ScriptAndDerivation(WCScriptAndDerivation):\n    def __init__(self, parent, wizard):\n        WCScriptAndDerivation.__init__(self, parent, wizard)\n        self._busy = True\n        self.title = ''\n        self.client = None\n\n    def on_ready(self):\n        super().on_ready()\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = current_cosigner['hardware_device']\n        plugin = self.wizard.plugins.get_plugin(_info.plugin_name)\n\n        device_id = _info.device.id_\n        self.client = self.wizard.plugins.device_manager.client_by_id(device_id, scan_now=False)\n        if not self.client.handler:\n            self.client.handler = plugin.create_handler(self.wizard)\n        self.client.setupRunning = True\n        self.check_device()\n\n    def check_device(self):\n        self.error = None\n        self.valid = False\n        self.busy = True\n\n        def check_task():\n            try:\n                self.client.pairing_dialog()\n                self.title = _('Script type and Derivation path')\n                self.valid = True\n            except (UserCancelled, OperationCancelled):\n                self.error = _('Cancelled')\n                self.wizard.requestPrev.emit()\n            except UserFacingException as e:\n                self.error = str(e)\n            except Exception as e:\n                self.error = repr(e)\n                self.logger.exception(repr(e))\n            finally:\n                self.busy = False\n\n        t = threading.Thread(target=check_task, daemon=True)\n        t.start()\n"
  },
  {
    "path": "electrum/plugins/coldcard/README.md",
    "content": "# Coldcard Hardware Wallet Plugin\n\n## Just the glue please\n\nThis code connects the public USB API and Electrum. Leverages all\nthe good work that's been done by the Electrum team to support\nhardware wallets.\n\n## Background\n\nThe Coldcard has a larger screen (128x64) and a number pad. For\nthis reason, all PIN code entry is done directly on the device.\nColdcard does not appear on the USB bus until unlocked with appropriate\nPIN. Initial setup, and seed generation must be done offline.\n\nColdcard uses the standard for unsigned transactions:\n\nPSBT = Partially Signed Bitcoin Transaction = BIP174\n\nThe Coldcard can be used 100% offline: it can generate a skeleton\nElectrum wallet and save it to MicroSD card. Transport that file\nto Electrum and it will fetch history, blockchain details and then\noperate in \"unpaired\" mode.\n\nSpending transactions can be saved to MicroSD using by exporting them\nfrom transaction preview dialog (when this plugin is\nowner of the wallet). That PSBT is then signed on the Coldcard\n(again using MicroSD both ways). The result is a ready-to-transmit\nbitcoin transaction, which can be transmitted using Tools > Load\nTransaction > From File in Electrum or really any tool.\n\n<https://coldcardwallet.com>\n\n## TODO Items\n\n- No effort yet to support translations or languages other than English, sorry.\n- We support multisig hardware wallets based on PSBT where each participant\n  is using different devices/systems for signing.\n\n### Ctags\n\n- I find this command useful (at top level) ... but I'm a VIM user.\n\n    ctags -f .tags electrum `find . -name ENV -prune -o -name \\*.py`\n\n"
  },
  {
    "path": "electrum/plugins/coldcard/__init__.py",
    "content": "\n"
  },
  {
    "path": "electrum/plugins/coldcard/cmdline.py",
    "content": "from electrum.plugin import hook\nfrom electrum.util import print_msg, raw_input, print_stderr\nfrom electrum.logging import get_logger\n\nfrom electrum.hw_wallet.cmdline import CmdLineHandler\n\nfrom .coldcard import ColdcardPlugin\n\n\n_logger = get_logger(__name__)\n\n\nclass ColdcardCmdLineHandler(CmdLineHandler):\n\n    def get_passphrase(self, msg, confirm):\n        raise NotImplementedError\n\n    def get_pin(self, msg, *, show_strength=True):\n        raise NotImplementedError\n\n    def prompt_auth(self, msg):\n        raise NotImplementedError\n\n    def yes_no_question(self, msg):\n        print_msg(msg)\n        return raw_input() in 'yY'\n\n    def stop(self):\n        pass\n\n    def update_status(self, b):\n        _logger.info(f'hw device status {b}')\n\n    def finished(self):\n        pass\n\nclass Plugin(ColdcardPlugin):\n    handler = ColdcardCmdLineHandler()\n\n    @hook\n    def init_keystore(self, keystore):\n        if not isinstance(keystore, self.keystore_class):\n            return\n        keystore.handler = self.handler\n\n    def create_handler(self, window):\n        return self.handler\n\n# EOF\n"
  },
  {
    "path": "electrum/plugins/coldcard/coldcard.py",
    "content": "#\n# Coldcard Electrum plugin main code.\n#\n#\nimport os\nimport time\nfrom typing import TYPE_CHECKING, Optional\nimport struct\n\nfrom electrum import bip32\nfrom electrum.bip32 import BIP32Node, InvalidMasterKeyVersionBytes\nfrom electrum.i18n import _\nfrom electrum.plugin import Device, hook, runs_in_hwd_thread\nfrom electrum.keystore import Hardware_KeyStore, KeyStoreWithMPK\nfrom electrum.transaction import PartialTransaction\nfrom electrum.wallet import Standard_Wallet, Multisig_Wallet, Abstract_Wallet\nfrom electrum.util import bfh, versiontuple, UserFacingException\nfrom electrum.logging import get_logger\n\nfrom electrum.hw_wallet import HW_PluginBase, HardwareClientBase\nfrom electrum.hw_wallet.plugin import LibraryFoundButUnusable, only_hook_if_libraries_available\n\nif TYPE_CHECKING:\n    from electrum.plugin import DeviceInfo\n    from electrum.wizard import NewWalletWizard\n\n_logger = get_logger(__name__)\n\n\ntry:\n    import hid\n    from ckcc.protocol import CCProtocolPacker, CCProtocolUnpacker\n    from ckcc.protocol import CCProtoError, CCUserRefused, CCBusyError\n    from ckcc.constants import (MAX_MSG_LEN, MAX_BLK_LEN, MSG_SIGNING_MAX_LENGTH, MAX_TXN_LEN,\n        AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH)\n\n    from ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID\n\n    try:  # >= v1.5.0\n        from ckcc.client import DEFAULT_SIM_SOCKET as CKCC_SIMULATOR_PATH\n    except ImportError:  # <= v1.4.x\n        from ckcc.client import CKCC_SIMULATOR_PATH\n\n    requirements_ok = True\n\n\n    class ElectrumColdcardDevice(ColdcardDevice):\n        # avoid use of pycoin for MiTM message signature test\n        def mitm_verify(self, sig, expect_xpub):\n            # verify a signature (65 bytes) over the session key, using the master bip32 node\n            # - customized to use specific EC library of Electrum.\n            pubkey = BIP32Node.from_xkey(expect_xpub).eckey\n            return pubkey.ecdsa_verify(sig[1:65], self.session_key)\n\nexcept ImportError as e:\n    if not (isinstance(e, ModuleNotFoundError) and e.name == 'ckcc'):\n        _logger.exception('error importing coldcard plugin deps')\n    requirements_ok = False\n\n    COINKITE_VID = 0xd13e\n    CKCC_PID     = 0xcc10\n\nCKCC_SIMULATED_PID = CKCC_PID ^ 0x55aa\n\n\nclass CKCCClient(HardwareClientBase):\n    def __init__(self, plugin, handler, dev_path, *, is_simulator=False):\n        HardwareClientBase.__init__(self, plugin=plugin)\n        self.device = plugin.device\n        self.handler = handler\n\n        # if we know what the (xfp, xpub) \"should be\" then track it here\n        self._expected_device = None\n\n        if is_simulator:\n            self.dev = ElectrumColdcardDevice(dev_path, encrypt=True)\n        else:\n            # open the real HID device\n            hd = hid.device(path=dev_path)\n            try:\n                hd.open_path(dev_path)\n            except OSError:\n                _logger.error('cannot open hid path. Did you forget to configure udev rules?')\n                raise\n\n            self.dev = ElectrumColdcardDevice(dev=hd, encrypt=True)\n\n        # NOTE: MiTM test is delayed until we have a hint as to what XPUB we\n        # should expect. It's also kinda slow.\n\n    def device_model_name(self) -> Optional[str]:\n        return 'Coldcard'\n\n    def get_soft_device_id(self) -> Optional[str]:\n        try:\n            super().get_soft_device_id()\n        except CCProtoError:\n            return None\n\n    def __repr__(self):\n        return '<CKCCClient: xfp=%s label=%r>' % (xfp2str(self.dev.master_fingerprint),\n                                                        self.label())\n\n    @runs_in_hwd_thread\n    def verify_connection(self, expected_xfp: int, expected_xpub: str):\n        ex = (expected_xfp, expected_xpub)\n\n        if self._expected_device == ex:\n            # all is as expected\n            return\n\n        assert expected_xpub\n\n        if ((self._expected_device is not None)\n                or (self.dev.master_fingerprint != expected_xfp)\n                or (self.dev.master_xpub != expected_xpub)):\n            # probably indicating programming error, not hacking\n            _logger.info(f\"xpubs. reported by device: {self.dev.master_xpub}. \"\n                         f\"stored in file: {expected_xpub}\")\n            raise RuntimeError(\"Expecting %s but that's not what's connected?!\" %\n                               xfp2str(expected_xfp))\n\n        # check signature over session key\n        # - mitm might have lied about xfp and xpub up to here\n        # - important that we use value capture at wallet creation time, not some value\n        #   we read over USB today\n        self.dev.check_mitm(expected_xpub=expected_xpub)\n\n        self._expected_device = ex\n\n        _logger.info(\"Successfully verified against MiTM\")\n\n    def is_pairable(self):\n        # can't do anything w/ devices that aren't setup (this code not normally reachable)\n        return bool(self.dev.master_xpub)\n\n    @runs_in_hwd_thread\n    def close(self):\n        # close the HID device (so can be reused)\n        self.dev.close()\n        self.dev = None\n\n    def is_initialized(self):\n        return bool(self.dev.master_xpub)\n\n    def label(self):\n        # 'label' of this Coldcard. Warning: gets saved into wallet file, which might\n        # not be encrypted, so better for privacy if based on xpub/fingerprint rather than\n        # USB serial number.\n        if self.dev.is_simulator:\n            lab = 'Coldcard Simulator ' + xfp2str(self.dev.master_fingerprint)\n        elif not self.dev.master_fingerprint:\n            # failback; not expected\n            lab = 'Coldcard #' + self.dev.serial\n        else:\n            lab = 'Coldcard ' + xfp2str(self.dev.master_fingerprint)\n\n        return lab\n\n    def _get_ckcc_master_xpub_from_device(self):\n        master_xpub = self.dev.master_xpub\n        if master_xpub is not None:\n            try:\n                node = BIP32Node.from_xkey(master_xpub)\n            except InvalidMasterKeyVersionBytes:\n                raise UserFacingException(\n                    _('Invalid xpub magic. Make sure your {} device is set to the correct chain.').format(self.device) + ' ' +\n                    _('You might have to unplug and plug it in again.')\n                ) from None\n            return master_xpub\n\n    @runs_in_hwd_thread\n    def has_usable_connection_with_device(self):\n        # Do end-to-end ping test\n        try:\n            self.ping_check()\n            return True\n        except Exception:\n            return False\n\n    @runs_in_hwd_thread\n    def get_xpub(self, bip32_path, xtype):\n        assert xtype in ColdcardPlugin.SUPPORTED_XTYPES\n        _logger.info('Derive xtype = %r' % xtype)\n        xpub = self.dev.send_recv(CCProtocolPacker.get_xpub(bip32_path), timeout=5000)\n        # TODO handle timeout?\n        # change type of xpub to the requested type\n        try:\n            node = BIP32Node.from_xkey(xpub)\n        except InvalidMasterKeyVersionBytes:\n            raise UserFacingException(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.')\n                                      .format(self.device)) from None\n        if xtype != 'standard':\n            xpub = node._replace(xtype=xtype).to_xpub()\n        return xpub\n\n    @runs_in_hwd_thread\n    def ping_check(self):\n        # check connection is working\n        assert self.dev.session_key, 'not encrypted?'\n        req = b'1234 Electrum Plugin 4321'      # free up to 59 bytes\n        try:\n            echo = self.dev.send_recv(CCProtocolPacker.ping(req))\n            assert echo == req\n        except Exception:\n            raise RuntimeError(\"Communication trouble with Coldcard\")\n\n    @runs_in_hwd_thread\n    def show_address(self, path, addr_fmt):\n        # prompt user w/ address, also returns it immediately.\n        return self.dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)\n\n    @runs_in_hwd_thread\n    def show_p2sh_address(self, *args, **kws):\n        # prompt user w/ p2sh address, also returns it immediately.\n        return self.dev.send_recv(CCProtocolPacker.show_p2sh_address(*args, **kws), timeout=None)\n\n    @runs_in_hwd_thread\n    def get_version(self):\n        # gives list of strings\n        return self.dev.send_recv(CCProtocolPacker.version(), timeout=1000).split('\\n')\n\n    @runs_in_hwd_thread\n    def sign_message_start(self, path, msg, addr_fmt):\n        # this starts the UX experience.\n        self.dev.send_recv(CCProtocolPacker.sign_message(msg, path, addr_fmt), timeout=None)\n\n    @runs_in_hwd_thread\n    def sign_message_poll(self):\n        # poll device... if user has approved, will get tuple: (addr, sig) else None\n        return self.dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None)\n\n    @runs_in_hwd_thread\n    def sign_transaction_start(self, raw_psbt: bytes, *, finalize: bool = False):\n        # Multiple steps to sign:\n        # - upload binary\n        # - start signing UX\n        # - wait for coldcard to complete process, or have it refused.\n        # - download resulting txn\n        assert 20 <= len(raw_psbt) < MAX_TXN_LEN, 'PSBT is too big'\n        dlen, chk = self.dev.upload_file(raw_psbt)\n\n        resp = self.dev.send_recv(CCProtocolPacker.sign_transaction(dlen, chk, finalize=finalize),\n                                    timeout=None)\n\n        if resp is not None:\n            raise ValueError(resp)\n\n    @runs_in_hwd_thread\n    def sign_transaction_poll(self):\n        # poll device... if user has approved, will get tuple: (length, checksum) else None\n        return self.dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None)\n\n    @runs_in_hwd_thread\n    def download_file(self, length, checksum, file_number=1):\n        # get a file\n        return self.dev.download_file(length, checksum, file_number=file_number)\n\n\n\nclass Coldcard_KeyStore(Hardware_KeyStore):\n    hw_type = 'coldcard'\n    device = 'Coldcard'\n\n    plugin: 'ColdcardPlugin'\n\n    def __init__(self, d):\n        Hardware_KeyStore.__init__(self, d)\n        self.ux_busy = False\n\n        # we need to know at least the fingerprint of the master xpub to verify against MiTM\n        # - device reports these value during encryption setup process\n        # - full xpub value now optional\n        self.ckcc_xpub = d.get('ckcc_xpub', None)\n\n    def dump(self):\n        # our additions to the stored data about keystore -- only during creation?\n        d = Hardware_KeyStore.dump(self)\n        d['ckcc_xpub'] = self.ckcc_xpub\n        return d\n\n    def get_xfp_int(self) -> int:\n        xfp = self.get_root_fingerprint()\n        assert xfp is not None\n        return xfp_int_from_xfp_bytes(bfh(xfp))\n\n    def opportunistically_fill_in_missing_info_from_device(self, client: 'CKCCClient'):\n        super().opportunistically_fill_in_missing_info_from_device(client)\n        if self.ckcc_xpub is None:\n            self.ckcc_xpub = client._get_ckcc_master_xpub_from_device()\n            self.is_requesting_to_be_rewritten_to_wallet_file = True\n\n    def get_client(self, *args, **kwargs):\n        # called when user tries to do something like view address, sign something.\n        # - not called during probing/setup\n        # - will fail if indicated device can't produce the xpub (at derivation) expected\n        client = super().get_client(*args, **kwargs)  # type: Optional[CKCCClient]\n        if client:\n            xfp_int = self.get_xfp_int()\n            client.verify_connection(xfp_int, self.ckcc_xpub)\n\n        return client\n\n    def give_error(self, message: str | BaseException):\n        self.logger.info(message)\n        if not self.ux_busy:\n            self.handler.show_error(str(message))\n        else:\n            self.ux_busy = False\n        raise UserFacingException(message)\n\n    def wrap_busy(func):\n        # decorator: function takes over the UX on the device.\n        def wrapper(self, *args, **kwargs):\n            try:\n                self.ux_busy = True\n                return func(self, *args, **kwargs)\n            finally:\n                self.ux_busy = False\n        return wrapper\n\n    def decrypt_message(self, pubkey, message, password):\n        raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device))\n\n    @wrap_busy\n    def sign_message(self, sequence, message, password, *, script_type=None):\n        # Sign a message on device. Since we have big screen, of course we\n        # have to show the message unabiguously there first!\n        try:\n            msg = message.encode('ascii', errors='strict')\n            assert 1 <= len(msg) <= MSG_SIGNING_MAX_LENGTH\n        except (UnicodeError, AssertionError):\n            # there are other restrictions on message content,\n            # but let the device enforce and report those\n            self.handler.show_error('Only short (%d max) ASCII messages can be signed.'\n                                            % MSG_SIGNING_MAX_LENGTH)\n            return b''\n\n        path = self.get_derivation_prefix() + (\"/%d/%d\" % sequence)\n\n        if script_type:\n            addr_fmt = self._encode_txin_type(script_type)\n        else:\n            addr_fmt = AF_CLASSIC\n\n        try:\n            cl = self.get_client()\n            try:\n                self.handler.show_message(\"Signing message (using %s)...\" % path)\n\n                cl.sign_message_start(path, msg, addr_fmt)\n\n                while 1:\n                    # How to kill some time, without locking UI?\n                    time.sleep(0.250)\n\n                    resp = cl.sign_message_poll()\n                    if resp is not None:\n                        break\n\n            finally:\n                self.handler.finished()\n\n            assert len(resp) == 2\n            addr, raw_sig = resp\n\n            # already encoded in Bitcoin fashion, binary.\n            assert 40 < len(raw_sig) <= 65\n\n            return raw_sig\n\n        except (CCUserRefused, CCBusyError) as exc:\n            self.handler.show_error(str(exc))\n        except CCProtoError as exc:\n            self.logger.exception('Error showing address')\n            self.handler.show_error('{}\\n\\n{}'.format(\n                _('Error showing address') + ':', str(exc)))\n        except Exception as e:\n            self.give_error(e)\n\n        # give empty bytes for error cases; it seems to clear the old signature box\n        return b''\n\n    @wrap_busy\n    def sign_transaction(self, tx, password):\n        # Upload PSBT for signing.\n        # - we can also work offline (without paired device present)\n        if tx.is_complete():\n            return\n\n        client = self.get_client()\n\n        assert client.dev.master_fingerprint == self.get_xfp_int()\n\n        raw_psbt = tx.serialize_as_bytes()\n\n        try:\n            try:\n                self.handler.show_message(\"Authorize Transaction...\")\n\n                client.sign_transaction_start(raw_psbt)\n\n                while 1:\n                    # How to kill some time, without locking UI?\n                    time.sleep(0.250)\n\n                    resp = client.sign_transaction_poll()\n                    if resp is not None:\n                        break\n\n                rlen, rsha = resp\n\n                # download the resulting txn.\n                raw_resp = client.download_file(rlen, rsha)\n\n            finally:\n                self.handler.finished()\n\n        except (CCUserRefused, CCBusyError) as exc:\n            self.logger.info(f'Did not sign: {exc}')\n            self.handler.show_error(str(exc))\n            return\n        except BaseException as e:\n            self.logger.exception('')\n            self.give_error(e)\n            return\n\n        tx2 = PartialTransaction.from_raw_psbt(raw_resp)\n        # apply partial signatures back into txn\n        tx.combine_with_other_psbt(tx2)\n        # caller's logic looks at tx now and if it's sufficiently signed,\n        # will send it if that's the user's intent.\n\n    @staticmethod\n    def _encode_txin_type(txin_type):\n        # Map from Electrum code names to our code numbers.\n        return {'standard': AF_CLASSIC, 'p2pkh': AF_CLASSIC,\n                'p2sh': AF_P2SH,\n                'p2wpkh-p2sh': AF_P2WPKH_P2SH,\n                'p2wpkh': AF_P2WPKH,\n                'p2wsh-p2sh': AF_P2WSH_P2SH,\n                'p2wsh': AF_P2WSH,\n                }[txin_type]\n\n    @wrap_busy\n    def show_address(self, sequence, txin_type):\n        client = self.get_client()\n        address_path = self.get_derivation_prefix()[2:] + \"/%d/%d\"%sequence\n        addr_fmt = self._encode_txin_type(txin_type)\n        try:\n            try:\n                self.handler.show_message(_(\"Showing address ...\"))\n                dev_addr = client.show_address(address_path, addr_fmt)\n                # we could double check address here\n            finally:\n                self.handler.finished()\n        except CCProtoError as exc:\n            self.logger.exception('Error showing address')\n            self.handler.show_error('{}\\n\\n{}'.format(\n                _('Error showing address') + ':', str(exc)))\n        except BaseException as exc:\n            self.logger.exception('')\n            self.handler.show_error(str(exc))\n\n    @wrap_busy\n    def show_p2sh_address(self, M, script, xfp_paths, txin_type):\n        client = self.get_client()\n        addr_fmt = self._encode_txin_type(txin_type)\n        try:\n            try:\n                self.handler.show_message(_(\"Showing address ...\"))\n                dev_addr = client.show_p2sh_address(M, xfp_paths, script, addr_fmt=addr_fmt)\n                # we could double check address here\n            finally:\n                self.handler.finished()\n        except CCProtoError as exc:\n            self.logger.exception('Error showing address')\n            self.handler.show_error('{}.\\n{}\\n\\n{}'.format(\n                _('Error showing address'),\n                _('Make sure you have imported the correct wallet description '\n                  'file on the device for this multisig wallet.'),\n                str(exc)))\n        except BaseException as exc:\n            self.logger.exception('')\n            self.handler.show_error(str(exc))\n\n\nclass ColdcardPlugin(HW_PluginBase):\n    keystore_class = Coldcard_KeyStore\n    minimum_library = (0, 7, 7)\n\n    DEVICE_IDS = [\n        (COINKITE_VID, CKCC_PID),\n        (COINKITE_VID, CKCC_SIMULATED_PID)\n    ]\n\n    SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')\n\n    def __init__(self, parent, config, name):\n        HW_PluginBase.__init__(self, parent, config, name)\n\n        self.libraries_available = self.check_libraries_available()\n        if not self.libraries_available:\n            return\n\n        self.device_manager().register_devices(self.DEVICE_IDS, plugin=self)\n        self.device_manager().register_enumerate_func(self.detect_simulator)\n\n    def get_library_version(self):\n        import ckcc\n        try:\n            version = ckcc.__version__\n        except AttributeError:\n            version = 'unknown'\n        if requirements_ok:\n            return version\n        else:\n            raise LibraryFoundButUnusable(library_version=version)\n\n    def detect_simulator(self):\n        # if there is a simulator running on this machine,\n        # return details about it so it's offered as a pairing choice\n        fn = CKCC_SIMULATOR_PATH\n\n        if os.path.exists(fn):\n            return [Device(path=fn,\n                           interface_number=-1,\n                           id_=fn,\n                           product_key=(COINKITE_VID, CKCC_SIMULATED_PID),\n                           usage_page=0,\n                           transport_ui_string='simulator')]\n\n        return []\n\n    @runs_in_hwd_thread\n    def create_client(self, device, handler):\n        # We are given a HID device, or at least some details about it.\n        # Not sure why not we aren't just given a HID library handle, but\n        # the 'path' is unabiguous, so we'll use that.\n        try:\n            rv = CKCCClient(self, handler, device.path,\n                            is_simulator=(device.product_key[1] == CKCC_SIMULATED_PID))\n            return rv\n        except Exception as e:\n            self.logger.exception('late failure connecting to device?')\n            return None\n\n    @runs_in_hwd_thread\n    def get_client(self, keystore, force_pair=True, *,\n                   devices=None, allow_user_interaction=True) -> Optional['CKCCClient']:\n        # Acquire a connection to the hardware device (via USB)\n        client = super().get_client(keystore, force_pair,\n                                    devices=devices,\n                                    allow_user_interaction=allow_user_interaction)\n\n        if client is not None:\n            client.ping_check()\n\n        return client\n\n    @staticmethod\n    def export_ms_wallet(wallet: Multisig_Wallet, fp, name):\n        # Build the text file Coldcard needs to understand the multisig wallet\n        # it is participating in. All involved Coldcards can share same file.\n        assert isinstance(wallet, Multisig_Wallet)\n\n        print('# Exported from Electrum', file=fp)\n        print(f'Name: {name:.20s}', file=fp)\n        print(f'Policy: {wallet.m} of {wallet.n}', file=fp)\n        print(f'Format: {wallet.txin_type.upper()}', file=fp)\n\n        xpubs = []\n        for xpub, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()):  # type: str, KeyStoreWithMPK\n            fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[], only_der_suffix=False)\n            fp_hex = fp_bytes.hex().upper()\n            der_prefix_str = bip32.convert_bip32_intpath_to_strpath(der_full)\n            xpubs.append((fp_hex, xpub, der_prefix_str))\n\n        # Before v3.2.1 derivation didn't matter too much to the Coldcard, since it\n        # could use key path data from PSBT or USB request as needed. However,\n        # derivation data is now required.\n\n        print('', file=fp)\n\n        assert len(xpubs) == wallet.n\n        for xfp, xpub, der_prefix in xpubs:\n            print(f'Derivation: {der_prefix}', file=fp)\n            print(f'{xfp}: {xpub}\\n', file=fp)\n\n    def show_address(self, wallet, address, keystore: 'Coldcard_KeyStore' = None):\n        if keystore is None:\n            keystore = wallet.get_keystore()\n        if not self.show_address_helper(wallet, address, keystore):\n            return\n\n        txin_type = wallet.get_txin_type(address)\n\n        # Standard_Wallet => not multisig, must be bip32\n        if type(wallet) is Standard_Wallet:\n            sequence = wallet.get_address_index(address)\n            keystore.show_address(sequence, txin_type)\n        elif type(wallet) is Multisig_Wallet:\n            assert isinstance(wallet, Multisig_Wallet)  # only here for type-hints in IDE\n            # More involved for P2SH/P2WSH addresses: need M, and all public keys, and their\n            # derivation paths. Must construct script, and track fingerprints+paths for\n            # all those keys\n\n            pubkey_deriv_info = wallet.get_public_keys_with_deriv_info(address)\n            pubkey_hexes = sorted([pk.hex() for pk in list(pubkey_deriv_info)])\n            xfp_paths = []\n            for pubkey_hex in pubkey_hexes:\n                pubkey = bytes.fromhex(pubkey_hex)\n                ks, der_suffix = pubkey_deriv_info[pubkey]\n                fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix, only_der_suffix=False)\n                xfp_int = xfp_int_from_xfp_bytes(fp_bytes)\n                xfp_paths.append([xfp_int] + list(der_full))\n\n            script = wallet.pubkeys_to_scriptcode(pubkey_hexes)\n\n            keystore.show_p2sh_address(wallet.m, script, xfp_paths, txin_type)\n\n        else:\n            keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device))\n            return\n\n    def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str:\n        if new_wallet:\n            return 'coldcard_start' if device_info.initialized else 'coldcard_not_initialized'\n        else:\n            return 'coldcard_unlock'\n\n    # insert coldcard pages in new wallet wizard\n    def extend_wizard(self, wizard: 'NewWalletWizard'):\n        views = {\n            'coldcard_start': {\n                'next': 'coldcard_xpub',\n            },\n            'coldcard_xpub': {\n                'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore',\n                'accept': wizard.maybe_master_pubkey,\n                'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d)\n            },\n            'coldcard_not_initialized': {},\n            'coldcard_unlock': {\n                'last': True\n            },\n        }\n        wizard.navmap_merge(views)\n\n\ndef xfp_int_from_xfp_bytes(fp_bytes: bytes) -> int:\n    return int.from_bytes(fp_bytes, byteorder=\"little\", signed=False)\n\n\ndef xfp2str(xfp: int) -> str:\n    # Standardized way to show an xpub's fingerprint... it's a 4-byte string\n    # and not really an integer. Used to show as '0x%08x' but that's wrong endian.\n    return struct.pack('<I', xfp).hex().lower()\n\n# EOF\n"
  },
  {
    "path": "electrum/plugins/coldcard/manifest.json",
    "content": "{\n  \"name\": \"coldcard\",\n  \"fullname\": \"Coldcard Wallet\",\n  \"description\": \"Provides support for the Coldcard hardware wallet from Coinkite\",\n  \"requires\": [[\"ckcc-protocol\", \"github.com/Coldcard/ckcc-protocol\"]],\n  \"registers_keystore\": [\"hardware\", \"coldcard\", \"Coldcard Wallet\"],\n  \"icon\":\"coldcard.png\",\n  \"available_for\": [\"qt\", \"cmdline\"]\n}\n"
  },
  {
    "path": "electrum/plugins/coldcard/qt.py",
    "content": "from functools import partial\nfrom typing import TYPE_CHECKING, Sequence\n\nfrom PyQt6.QtCore import Qt\nfrom PyQt6.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout\n\nfrom electrum.i18n import _\nfrom electrum.plugin import hook\nfrom electrum.wallet import Multisig_Wallet\nfrom electrum.keystore import Hardware_KeyStore\nfrom electrum.util import ChoiceItem\n\nfrom electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase\nfrom electrum.hw_wallet.plugin import only_hook_if_libraries_available\n\nfrom electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWXPub, WCHWUninitialized, WCHWUnlock\nfrom electrum.gui.qt.util import WindowModalDialog, CloseButton, getOpenFileName, getSaveFileName, RichLabel\nfrom electrum.gui.qt.main_window import ElectrumWindow\n\nfrom .coldcard import ColdcardPlugin, xfp2str\n\nif TYPE_CHECKING:\n    from electrum.gui.qt.wizard.wallet import QENewWalletWizard\n\nCC_DEBUG = False\n\n\nclass Plugin(ColdcardPlugin, QtPluginBase):\n    icon_unpaired = \"coldcard_unpaired.png\"\n    icon_paired = \"coldcard.png\"\n\n    def create_handler(self, window):\n        return Coldcard_Handler(window)\n\n    def trim_file_suffix(self, path):\n        return path.rsplit('.', 1)[0]\n\n    @only_hook_if_libraries_available\n    @hook\n    def receive_menu(self, menu, addrs, wallet):\n        if len(addrs) != 1:\n            return\n        self._add_menu_action(menu, addrs[0], wallet)\n\n    @only_hook_if_libraries_available\n    @hook\n    def transaction_dialog_address_menu(self, menu, addr, wallet):\n        self._add_menu_action(menu, addr, wallet)\n\n    @only_hook_if_libraries_available\n    @hook\n    def wallet_info_buttons(self, main_window: 'ElectrumWindow', dialog):\n        # user is about to see the \"Wallet Information\" dialog\n        # - add a button if multisig wallet, and a Coldcard is a cosigner.\n        assert isinstance(main_window, ElectrumWindow), f\"{type(main_window)}\"\n\n        buttons = []\n        wallet = main_window.wallet\n\n        if type(wallet) is not Multisig_Wallet:\n            return\n\n        coldcard_keystores = [\n            ks\n            for ks in wallet.get_keystores()\n            if type(ks) == self.keystore_class\n        ]\n        if not coldcard_keystores:\n            # doesn't involve a Coldcard wallet, hide feature\n            return\n\n        btn_export = QPushButton(_(\"Export multisig for Coldcard as file\"))\n        btn_export.clicked.connect(lambda unused: self.export_multisig_setup(main_window, wallet))\n        buttons.append(btn_export)\n        btn_import_usb = QPushButton(_(\"Export multisig to Coldcard via USB\"))\n        btn_import_usb.clicked.connect(lambda unused: self.import_multisig_wallet_to_cc(main_window, coldcard_keystores))\n        buttons.append(btn_import_usb)\n        return buttons\n\n    def import_multisig_wallet_to_cc(self, main_window: 'ElectrumWindow', coldcard_keystores: Sequence[Hardware_KeyStore]):\n        from io import StringIO\n        from ckcc.protocol import CCProtocolPacker\n\n        index = main_window.query_choice(\n            _(\"Please select which {} device to use:\").format(self.device),\n            [ChoiceItem(key=i, label=ks.label) for i, ks in enumerate(coldcard_keystores)]\n        )\n        if index is not None:\n            selected_keystore = coldcard_keystores[index]\n            client = self.get_client(selected_keystore, force_pair=True, allow_user_interaction=False)\n            if client is None:\n                main_window.show_error(\"{} not connected.\").format(selected_keystore.label)\n                return\n\n            wallet = main_window.wallet\n            sio = StringIO()\n            basename = self.trim_file_suffix(wallet.basename())\n            ColdcardPlugin.export_ms_wallet(wallet, sio, basename)\n            sio.seek(0)\n            file_len, sha = client.dev.upload_file(sio.read().encode(\"utf-8\"), verify=True)\n            client.dev.send_recv(CCProtocolPacker.multisig_enroll(file_len, sha))\n            main_window.show_message('\\n'.join([\n                _(\"Wallet setup file '{}' imported successfully.\").format(basename),\n                _(\"Confirm import on your {} device.\").format(selected_keystore.label)\n            ]))\n\n    def export_multisig_setup(self, main_window, wallet):\n        basename = self.trim_file_suffix(wallet.basename())\n        name = f'{basename}-cc-export.txt'.replace(' ', '-')\n        fileName = getSaveFileName(\n            parent=main_window,\n            title=_(\"Select where to save the setup file\"),\n            filename=name,\n            filter=\"*.txt\",\n            config=self.config,\n        )\n        if fileName:\n            with open(fileName, \"wt\") as f:\n                ColdcardPlugin.export_ms_wallet(wallet, f, basename)\n            main_window.show_message(_(\"Wallet setup file '{}' exported successfully\").format(name))\n\n    def show_settings_dialog(self, window, keystore):\n        # When they click on the icon for CC we come here.\n        # - doesn't matter if device not connected, continue\n        CKCCSettingsDialog(window, self, keystore).exec()\n\n    # insert coldcard pages in new wallet wizard\n    def extend_wizard(self, wizard: 'QENewWalletWizard'):\n        super().extend_wizard(wizard)\n        views = {\n            'coldcard_start': {'gui': WCScriptAndDerivation},\n            'coldcard_xpub': {'gui': WCHWXPub},\n            'coldcard_not_initialized': {'gui': WCHWUninitialized},\n            'coldcard_unlock': {'gui': WCHWUnlock}\n        }\n        wizard.navmap_merge(views)\n\n\nclass Coldcard_Handler(QtHandlerBase):\n    MESSAGE_DIALOG_TITLE = _(\"Coldcard Status\")\n\n    def __init__(self, win):\n        super(Coldcard_Handler, self).__init__(win, 'Coldcard')\n\n\nclass CKCCSettingsDialog(WindowModalDialog):\n\n    def __init__(self, window: ElectrumWindow, plugin, keystore):\n        title = _(\"{} Settings\").format(plugin.device)\n        super(CKCCSettingsDialog, self).__init__(window, title)\n        self.setMaximumWidth(540)\n\n        # Note: Coldcard may **not** be connected at present time. Keep working!\n\n        devmgr = plugin.device_manager()\n        #config = devmgr.config\n        #handler = keystore.handler\n        self.thread = thread = keystore.thread\n        self.keystore = keystore\n        assert isinstance(window, ElectrumWindow), f\"{type(window)}\"\n        self.window = window\n\n        def connect_and_doit():\n            # Attempt connection to device, or raise.\n            device_id = plugin.choose_device(window, keystore)\n            if not device_id:\n                raise RuntimeError(\"Device not connected\")\n            client = devmgr.client_by_id(device_id)\n            if not client:\n                raise RuntimeError(\"Device not connected\")\n            return client\n\n        body = QWidget()\n        body_layout = QVBoxLayout(body)\n        grid = QGridLayout()\n        grid.setColumnStretch(2, 1)\n\n        title = RichLabel('''<center>\n<span style=\"font-size: x-large\">Coldcard Wallet</span>\n<br><span style=\"font-size: medium\">from Coinkite Inc.</span>\n<br><a href=\"https://coldcardwallet.com\">coldcardwallet.com</a>''')\n\n        grid.addWidget(title, 0, 0, 1, 2, Qt.AlignmentFlag.AlignHCenter)\n        y = 3\n\n        rows = [\n            ('xfp', _(\"Master Fingerprint\")),\n            ('serial', _(\"USB Serial\")),\n            ('fw_version', _(\"Firmware Version\")),\n            ('fw_built', _(\"Build Date\")),\n            ('bl_version', _(\"Bootloader\")),\n        ]\n        for row_num, (member_name, label) in enumerate(rows):\n            # XXX we know xfp already, even if not connected\n            widget = QLabel('<tt>000000000000')\n            widget.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextSelectableByKeyboard)\n\n            grid.addWidget(QLabel(label), y, 0, 1, 1, Qt.AlignmentFlag.AlignRight)\n            grid.addWidget(widget, y, 1, 1, 1, Qt.AlignmentFlag.AlignLeft)\n            setattr(self, member_name, widget)\n            y += 1\n        body_layout.addLayout(grid)\n\n        upg_btn = QPushButton(_('Upgrade'))\n        #upg_btn.setDefault(False)\n        def _start_upgrade():\n            thread.add(connect_and_doit, on_success=self.start_upgrade)\n        upg_btn.clicked.connect(_start_upgrade)\n\n        y += 3\n        grid.addWidget(upg_btn, y, 0)\n        grid.addWidget(CloseButton(self), y, 1)\n\n        dialog_vbox = QVBoxLayout(self)\n        dialog_vbox.addWidget(body)\n\n        # Fetch firmware/versions values and show them.\n        thread.add(connect_and_doit, on_success=self.show_values, on_error=self.show_placeholders)\n\n    def show_placeholders(self, unclear_arg):\n        # device missing, so hide lots of detail.\n        self.xfp.setText('<tt>%s' % self.keystore.get_root_fingerprint())\n        self.serial.setText('(not connected)')\n        self.fw_version.setText('')\n        self.fw_built.setText('')\n        self.bl_version.setText('')\n\n    def show_values(self, client):\n\n        dev = client.dev\n\n        self.xfp.setText('<tt>%s' % xfp2str(dev.master_fingerprint))\n        self.serial.setText('<tt>%s' % dev.serial)\n\n        # ask device for versions: allow extras for future\n        fw_date, fw_rel, bl_rel, *rfu = client.get_version()\n\n        self.fw_version.setText('<tt>%s' % fw_rel)\n        self.fw_built.setText('<tt>%s' % fw_date)\n        self.bl_version.setText('<tt>%s' % bl_rel)\n\n    def start_upgrade(self, client):\n        # ask for a filename (must have already downloaded it)\n        dev = client.dev\n\n        fileName = getOpenFileName(\n            parent=self,\n            title=\"Select upgraded firmware file\",\n            filter=\"*.dfu\",\n            config=self.window.config,\n        )\n        if not fileName:\n            return\n\n        from ckcc.utils import dfu_parse\n        from ckcc.sigheader import FW_HEADER_SIZE, FW_HEADER_OFFSET, FW_HEADER_MAGIC\n        from ckcc.protocol import CCProtocolPacker\n        import struct\n\n        try:\n            with open(fileName, 'rb') as fd:\n\n                # unwrap firmware from the DFU\n                offset, size, *ignored = dfu_parse(fd)\n\n                fd.seek(offset)\n                firmware = fd.read(size)\n\n            hpos = FW_HEADER_OFFSET\n            hdr = bytes(firmware[hpos:hpos + FW_HEADER_SIZE])        # needed later too\n            magic = struct.unpack_from(\"<I\", hdr)[0]\n\n            if magic != FW_HEADER_MAGIC:\n                raise ValueError(\"Bad magic\")\n        except Exception as exc:\n            self.window.show_error(\"Does not appear to be a Coldcard firmware file.\\n\\n%s\" % exc)\n            return\n\n        # TODO:\n        # - detect if they are trying to downgrade; aint gonna work\n        # - warn them about the reboot?\n        # - length checks\n        # - add progress local bar\n        self.window.show_message(\"Ready to Upgrade.\\n\\nBe patient. Unit will reboot itself when complete.\")\n\n        def doit():\n            dlen, _ = dev.upload_file(firmware, verify=True)\n            assert dlen == len(firmware)\n\n            # append the firmware header a second time\n            result = dev.send_recv(CCProtocolPacker.upload(size, size+FW_HEADER_SIZE, hdr))\n\n            # make it reboot into bootloader which might install it\n            dev.send_recv(CCProtocolPacker.reboot())\n\n        self.thread.add(doit)\n        self.close()\n"
  },
  {
    "path": "electrum/plugins/digitalbitbox/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/plugins/digitalbitbox/cmdline.py",
    "content": "from electrum.plugin import hook\nfrom .digitalbitbox import DigitalBitboxPlugin\nfrom electrum.hw_wallet import CmdLineHandler\n\nclass Plugin(DigitalBitboxPlugin):\n    handler = CmdLineHandler()\n    @hook\n    def init_keystore(self, keystore):\n        if not isinstance(keystore, self.keystore_class):\n            return\n        keystore.handler = self.handler\n\n    def create_handler(self, window):\n        return self.handler\n"
  },
  {
    "path": "electrum/plugins/digitalbitbox/digitalbitbox.py",
    "content": "# ----------------------------------------------------------------------------------\n# Electrum plugin for the Digital Bitbox hardware wallet by Shift Devices AG\n# digitalbitbox.com\n#\n\nimport base64\nimport binascii\nimport hashlib\nimport hmac\nimport json\nimport math\nimport os\nimport re\nimport struct\nimport sys\nimport time\nimport copy\nfrom typing import TYPE_CHECKING, Optional\n\nimport electrum_ecc as ecc\n\nfrom electrum.crypto import sha256d, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot\nfrom electrum.bitcoin import public_key_to_p2pkh, usermessage_magic, verify_usermessage_with_address\nfrom electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation\nfrom electrum.bip32 import normalize_bip32_derivation\nfrom electrum import descriptor\nfrom electrum.wallet import Standard_Wallet\nfrom electrum import constants\nfrom electrum.transaction import Transaction, PartialTransaction, PartialTxInput, Sighash\nfrom electrum.i18n import _\nfrom electrum.keystore import Hardware_KeyStore\nfrom electrum.util import to_string, UserCancelled, UserFacingException, bfh, ChoiceItem\nfrom electrum.network import Network\nfrom electrum.logging import get_logger\nfrom electrum.plugin import runs_in_hwd_thread, run_in_hwd_thread\n\nfrom electrum.hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase\nfrom electrum.hw_wallet.plugin import OperationCancelled\n\nif TYPE_CHECKING:\n    from electrum.plugin import DeviceInfo\n    from electrum.wizard import NewWalletWizard\n\n_logger = get_logger(__name__)\n\n\ntry:\n    import hid\n    DIGIBOX = True\nexcept ImportError as e:\n    DIGIBOX = False\n\n\nclass DeviceErased(UserFacingException):\n    pass\n\n# ----------------------------------------------------------------------------------\n# USB HID interface\n#\n\n\ndef to_hexstr(s):\n    return binascii.hexlify(s).decode('ascii')\n\n\ndef derive_keys(x):\n    h = sha256d(x)\n    h = hashlib.sha512(h).digest()\n    return (h[:32],h[32:])\n\n\nMIN_MAJOR_VERSION = 5\n\nENCRYPTION_PRIVKEY_KEY = 'encryptionprivkey'\nCHANNEL_ID_KEY = 'comserverchannelid'\n\n\nclass DigitalBitbox_Client(HardwareClientBase):\n    def __init__(self, plugin, hidDevice):\n        HardwareClientBase.__init__(self, plugin=plugin)\n        self.dbb_hid = hidDevice\n        self.opened = True\n        self.password = None\n        self.isInitialized = False\n        self.setupRunning = False\n        self.usbReportSize = 64  # firmware > v2.0.0\n\n    def device_model_name(self) -> Optional[str]:\n        return 'Digital BitBox'\n\n    @runs_in_hwd_thread\n    def close(self):\n        if self.opened:\n            try:\n                self.dbb_hid.close()\n            except Exception:\n                pass\n        self.opened = False\n\n    def is_pairable(self):\n        return True\n\n    def is_initialized(self):\n        return self.dbb_has_password()\n\n    def is_paired(self):\n        return self.password is not None\n\n    def has_usable_connection_with_device(self):\n        try:\n            self.dbb_has_password()\n        except BaseException:\n            return False\n        return True\n\n    def _get_xpub(self, bip32_path: str):\n        bip32_path = normalize_bip32_derivation(bip32_path, hardened_char=\"'\")\n        if self.check_device_dialog():\n            return self.hid_send_encrypt(('{\"xpub\": \"%s\"}' % bip32_path).encode('utf8'))\n\n    def get_xpub(self, bip32_path, xtype):\n        assert xtype in self.plugin.SUPPORTED_XTYPES\n\n        if is_all_public_derivation(bip32_path):\n            raise UserFacingException(_('This device does not reveal xpubs corresponding to non-hardened paths'))\n\n        reply = self._get_xpub(bip32_path)\n        if reply:\n            xpub = reply['xpub']\n            # Change type of xpub to the requested type. The firmware\n            # only ever returns the mainnet standard type, but it is agnostic\n            # to the type when signing.\n            if xtype != 'standard' or constants.net.TESTNET:\n                node = BIP32Node.from_xkey(xpub, net=constants.BitcoinMainnet)\n                xpub = node._replace(xtype=xtype).to_xpub()\n            return xpub\n        else:\n            raise Exception('no reply')\n\n    def get_soft_device_id(self):\n        return None\n\n    def dbb_has_password(self):\n        reply = self.hid_send_plain(b'{\"ping\":\"\"}')\n        if 'ping' not in reply:\n            raise UserFacingException(_('Device communication error. Please unplug and replug your Digital Bitbox.'))\n        if reply['ping'] == 'password':\n            return True\n        return False\n\n    def stretch_key(self, key: bytes):\n        return to_hexstr(hashlib.pbkdf2_hmac('sha512', key, b'Digital Bitbox', iterations = 20480))\n\n    def backup_password_dialog(self):\n        msg = _(\"Enter the password used when the backup was created:\")\n        while True:\n            password = self.handler.get_passphrase(msg, False)\n            if password is None:\n                return None\n            if len(password) < 4:\n                msg = _(\"Password must have at least 4 characters.\") \\\n                      + \"\\n\\n\" + _(\"Enter password:\")\n            elif len(password) > 64:\n                msg = _(\"Password must have less than 64 characters.\") \\\n                      + \"\\n\\n\" + _(\"Enter password:\")\n            else:\n                return password.encode('utf8')\n\n    def password_dialog(self, msg):\n        while True:\n            password = self.handler.get_passphrase(msg, False)\n            if password is None:\n                return False\n            if len(password) < 4:\n                msg = _(\"Password must have at least 4 characters.\") + \\\n                      \"\\n\\n\" + _(\"Enter password:\")\n            elif len(password) > 64:\n                msg = _(\"Password must have less than 64 characters.\") + \\\n                      \"\\n\\n\" + _(\"Enter password:\")\n            else:\n                self.password = password.encode('utf8')\n                return True\n\n    def check_firmware_version(self):\n        match = re.search(r'v([0-9])+\\.[0-9]+\\.[0-9]+',\n                          run_in_hwd_thread(self.dbb_hid.get_serial_number_string))\n        if match is None:\n            raise Exception(\"error detecting firmware version\")\n        major_version = int(match.group(1))\n        if major_version < MIN_MAJOR_VERSION:\n            raise Exception(\"Please upgrade to the newest firmware using the BitBox Desktop app: https://shiftcrypto.ch/start\")\n\n    def check_device_dialog(self):\n        self.check_firmware_version()\n        # Set password if fresh device\n        if self.password is None and not self.dbb_has_password():\n            if not self.setupRunning:\n                return False # A fresh device cannot connect to an existing wallet\n            msg = _(\"An uninitialized Digital Bitbox is detected.\") + \" \" + \\\n                  _(\"Enter a new password below.\") + \"\\n\\n\" + \\\n                  _(\"REMEMBER THE PASSWORD!\") + \"\\n\\n\" + \\\n                  _(\"You cannot access your coins or a backup without the password.\") + \"\\n\" + \\\n                  _(\"A backup is saved automatically when generating a new wallet.\")\n            if self.password_dialog(msg):\n                reply = self.hid_send_plain(b'{\"password\":\"' + self.password + b'\"}')\n            else:\n                return False\n\n        # Get password from user if not yet set\n        msg = _(\"Enter your Digital Bitbox password:\")\n        while self.password is None:\n            if not self.password_dialog(msg):\n                raise UserCancelled()\n            reply = self.hid_send_encrypt(b'{\"led\":\"blink\"}')\n            if 'error' in reply:\n                self.password = None\n                if reply['error']['code'] == 109:\n                    msg = _(\"Incorrect password entered.\") + \"\\n\\n\" + \\\n                          reply['error']['message'] + \"\\n\\n\" + \\\n                          _(\"Enter your Digital Bitbox password:\")\n                else:\n                    # Should never occur\n                    msg = _(\"Unexpected error occurred.\") + \"\\n\\n\" + \\\n                          reply['error']['message'] + \"\\n\\n\" + \\\n                          _(\"Enter your Digital Bitbox password:\")\n\n        # Initialize device if not yet initialized\n        if not self.setupRunning:\n            self.isInitialized = True # Wallet exists. Electrum code later checks if the device matches the wallet\n        elif not self.isInitialized:\n            reply = self.hid_send_encrypt(b'{\"device\":\"info\"}')\n            if reply['device']['id'] != \"\":\n                self.recover_or_erase_dialog() # Already seeded\n            else:\n                self.seed_device_dialog() # Seed if not initialized\n            self.mobile_pairing_dialog()\n        return self.isInitialized\n\n    def recover_or_erase_dialog(self):\n        msg = _(\"The Digital Bitbox is already seeded. Choose an option:\") + \"\\n\"\n        choices = [\n            ChoiceItem(key=\"create\", label=_(\"Create a wallet using the current seed\")),\n            ChoiceItem(key=\"erase\", label=_(\"Erase the Digital Bitbox\")),\n        ]\n        reply = self.handler.query_choice(msg, choices)\n        if reply is None:\n            raise UserCancelled()\n        if reply == \"erase\":\n            self.dbb_erase()\n        else:\n            if self.hid_send_encrypt(b'{\"device\":\"info\"}')['device']['lock']:\n                raise UserFacingException(_(\"Full 2FA enabled. This is not supported yet.\"))\n            # Use existing seed\n        self.isInitialized = True\n\n    def seed_device_dialog(self):\n        msg = _(\"Choose how to initialize your Digital Bitbox:\") + \"\\n\"\n        choices = [\n            ChoiceItem(key=\"generate\", label=_(\"Generate a new random wallet\")),\n            ChoiceItem(key=\"load\", label=_(\"Load a wallet from the micro SD card\")),\n        ]\n        reply = self.handler.query_choice(msg, choices)\n        if reply is None:\n            raise UserCancelled()\n        if reply == \"generate\":\n            self.dbb_generate_wallet()\n        else:\n            if not self.dbb_load_backup(show_msg=False):\n                return\n        self.isInitialized = True\n\n    def mobile_pairing_dialog(self):\n        dbb_user_dir = None\n        if sys.platform == 'darwin':\n            dbb_user_dir = os.path.join(os.environ.get(\"HOME\", \"\"), \"Library\", \"Application Support\", \"DBB\")\n        elif sys.platform == 'win32':\n            dbb_user_dir = os.path.join(os.environ[\"APPDATA\"], \"DBB\")\n        else:\n            dbb_user_dir = os.path.join(os.environ[\"HOME\"], \".dbb\")\n\n        if not dbb_user_dir:\n            return\n\n        try:\n            with open(os.path.join(dbb_user_dir, \"config.dat\")) as f:\n                dbb_config = json.load(f)\n        except (FileNotFoundError, json.JSONDecodeError):\n            return\n\n        if ENCRYPTION_PRIVKEY_KEY not in dbb_config or CHANNEL_ID_KEY not in dbb_config:\n            return\n\n        choices = [\n            ChoiceItem(key=0, label=_('Do not pair')),\n            ChoiceItem(key=1, label=_('Import pairing from the Digital Bitbox desktop app')),\n        ]\n        reply = self.handler.query_choice(_('Mobile pairing options'), choices)\n        if reply is None:\n            raise UserCancelled()\n\n        if reply == 0:\n            if self.plugin.is_mobile_paired():\n                del self.plugin.digitalbitbox_config[ENCRYPTION_PRIVKEY_KEY]\n                del self.plugin.digitalbitbox_config[CHANNEL_ID_KEY]\n        elif reply == 1:\n            # import pairing from dbb app\n            self.plugin.digitalbitbox_config[ENCRYPTION_PRIVKEY_KEY] = dbb_config[ENCRYPTION_PRIVKEY_KEY]\n            self.plugin.digitalbitbox_config[CHANNEL_ID_KEY] = dbb_config[CHANNEL_ID_KEY]\n        self.plugin.config.set_key('digitalbitbox', self.plugin.digitalbitbox_config)\n\n    def dbb_generate_wallet(self):\n        key = self.stretch_key(self.password)\n        filename = (\"Electrum-\" + time.strftime(\"%Y-%m-%d-%H-%M-%S\") + \".pdf\")\n        msg = ('{\"seed\":{\"source\": \"create\", \"key\": \"%s\", \"filename\": \"%s\", \"entropy\": \"%s\"}}' % (key, filename, to_hexstr(os.urandom(32)))).encode('utf8')\n        reply = self.hid_send_encrypt(msg)\n        if 'error' in reply:\n            raise UserFacingException(reply['error']['message'])\n\n    def dbb_erase(self):\n        self.handler.show_message(_(\"Are you sure you want to erase the Digital Bitbox?\") + \"\\n\\n\" +\n                                  _(\"To continue, touch the Digital Bitbox's light for 3 seconds.\") + \"\\n\\n\" +\n                                  _(\"To cancel, briefly touch the light or wait for the timeout.\"))\n        hid_reply = self.hid_send_encrypt(b'{\"reset\":\"__ERASE__\"}')\n        self.handler.finished()\n        if 'error' in hid_reply:\n            if hid_reply['error'].get('code') in (600, 601):\n                raise OperationCancelled()\n            raise UserFacingException(hid_reply['error']['message'])\n        else:\n            self.password = None\n            raise DeviceErased('Device erased')\n\n    def dbb_load_backup(self, show_msg=True):\n        backups = self.hid_send_encrypt(b'{\"backup\":\"list\"}')\n        if 'error' in backups:\n            raise UserFacingException(backups['error']['message'])\n        backup_choices = [ChoiceItem(key=idx, label=v) for (idx, v) in enumerate(backups['backup'])]\n        f = self.handler.query_choice(_(\"Choose a backup file:\"), backup_choices)\n        if f is None:\n            raise UserCancelled()\n        key = self.backup_password_dialog()\n        if key is None:\n            raise UserCancelled('No backup password provided')\n        key = self.stretch_key(key)\n        if show_msg:\n            self.handler.show_message(_(\"Loading backup...\") + \"\\n\\n\" +\n                                      _(\"To continue, touch the Digital Bitbox's light for 3 seconds.\") + \"\\n\\n\" +\n                                      _(\"To cancel, briefly touch the light or wait for the timeout.\"))\n        msg = ('{\"seed\":{\"source\": \"backup\", \"key\": \"%s\", \"filename\": \"%s\"}}' % (key, backups['backup'][f])).encode('utf8')\n        hid_reply = self.hid_send_encrypt(msg)\n        self.handler.finished()\n        if 'error' in hid_reply:\n            if hid_reply['error'].get('code') in (600, 601):\n                raise OperationCancelled()\n            raise UserFacingException(hid_reply['error']['message'])\n        return True\n\n    @runs_in_hwd_thread\n    def hid_send_frame(self, data):\n        HWW_CID = 0xFF000000\n        HWW_CMD = 0x80 + 0x40 + 0x01\n        data_len = len(data)\n        seq = 0\n        idx = 0\n        write = []\n        while idx < data_len:\n            if idx == 0:\n                # INIT frame\n                write = data[idx : idx + min(data_len, self.usbReportSize - 7)]\n                self.dbb_hid.write(b'\\0' + struct.pack(\">IBH\", HWW_CID, HWW_CMD, data_len & 0xFFFF) + write + b'\\xEE' * (self.usbReportSize - 7 - len(write)))\n            else:\n                # CONT frame\n                write = data[idx : idx + min(data_len, self.usbReportSize - 5)]\n                self.dbb_hid.write(b'\\0' + struct.pack(\">IB\", HWW_CID, seq) + write + b'\\xEE' * (self.usbReportSize - 5 - len(write)))\n                seq += 1\n            idx += len(write)\n\n    @runs_in_hwd_thread\n    def hid_read_frame(self):\n        # INIT response\n        read = bytearray(self.dbb_hid.read(self.usbReportSize))\n        cid = ((read[0] * 256 + read[1]) * 256 + read[2]) * 256 + read[3]\n        cmd = read[4]\n        data_len = read[5] * 256 + read[6]\n        data = read[7:]\n        idx = len(read) - 7\n        while idx < data_len:\n            # CONT response\n            read = bytearray(self.dbb_hid.read(self.usbReportSize))\n            data += read[5:]\n            idx += len(read) - 5\n        return data\n\n    @runs_in_hwd_thread\n    def hid_send_plain(self, msg):\n        reply = \"\"\n        try:\n            serial_number = self.dbb_hid.get_serial_number_string()\n            if \"v2.0.\" in serial_number or \"v1.\" in serial_number:\n                hidBufSize = 4096\n                self.dbb_hid.write('\\0' + msg + '\\0' * (hidBufSize - len(msg)))\n                r = bytearray()\n                while len(r) < hidBufSize:\n                    r += bytearray(self.dbb_hid.read(hidBufSize))\n            else:\n                self.hid_send_frame(msg)\n                r = self.hid_read_frame()\n            r = r.rstrip(b' \\t\\r\\n\\0')\n            r = r.replace(b\"\\0\", b'')\n            r = to_string(r, 'utf8')\n            reply = json.loads(r)\n        except Exception as e:\n            _logger.info(f'Exception caught {repr(e)}')\n        return reply\n\n    @runs_in_hwd_thread\n    def hid_send_encrypt(self, msg):\n        sha256_byte_len = 32\n        reply = \"\"\n        try:\n            encryption_key, authentication_key = derive_keys(self.password)\n            msg = EncodeAES_bytes(encryption_key, msg)\n            hmac_digest = hmac_oneshot(authentication_key, msg, hashlib.sha256)\n            authenticated_msg = base64.b64encode(msg + hmac_digest)\n            reply = self.hid_send_plain(authenticated_msg)\n            if 'ciphertext' in reply:\n                b64_unencoded = bytes(base64.b64decode(''.join(reply[\"ciphertext\"]), validate=True))\n                reply_hmac = b64_unencoded[-sha256_byte_len:]\n                hmac_calculated = hmac_oneshot(authentication_key, b64_unencoded[:-sha256_byte_len], hashlib.sha256)\n                if not hmac.compare_digest(reply_hmac, hmac_calculated):\n                    raise Exception(\"Failed to validate HMAC\")\n                reply = DecodeAES_bytes(encryption_key, b64_unencoded[:-sha256_byte_len])\n                reply = to_string(reply, 'utf8')\n                reply = json.loads(reply)\n            if 'error' in reply:\n                self.password = None\n        except Exception as e:\n            _logger.info(f'Exception caught {repr(e)}')\n        return reply\n\n\n\n# ----------------------------------------------------------------------------------\n#\n#\n\nclass DigitalBitbox_KeyStore(Hardware_KeyStore):\n    hw_type = 'digitalbitbox'\n    device = 'DigitalBitbox'\n\n    plugin: 'DigitalBitboxPlugin'\n\n    def __init__(self, d):\n        Hardware_KeyStore.__init__(self, d)\n        self.maxInputs = 14 # maximum inputs per single sign command\n\n    def give_error(self, message: str | BaseException):\n        raise Exception(message)\n\n    def decrypt_message(self, pubkey, message, password):\n        raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device))\n\n    def sign_message(self, sequence, message, password, *, script_type=None):\n        sig = None\n        try:\n            message = message.encode('utf8')\n            inputPath = self.get_derivation_prefix() + \"/%d/%d\" % sequence\n            inputPath = normalize_bip32_derivation(inputPath, hardened_char=\"'\")\n            msg_hash = sha256d(usermessage_magic(message))\n            inputHash = to_hexstr(msg_hash)\n            hasharray = []\n            hasharray.append({'hash': inputHash, 'keypath': inputPath})\n            hasharray = json.dumps(hasharray)\n\n            msg = ('{\"sign\":{\"meta\":\"sign message\", \"data\":%s}}' % hasharray).encode('utf8')\n\n            dbb_client = self.plugin.get_client(self)\n\n            if not dbb_client.is_paired():\n                raise Exception(_(\"Could not sign message.\"))\n\n            reply = dbb_client.hid_send_encrypt(msg)\n            self.handler.show_message(_(\"Signing message ...\") + \"\\n\\n\" +\n                                      _(\"To continue, touch the Digital Bitbox's blinking light for 3 seconds.\") + \"\\n\\n\" +\n                                      _(\"To cancel, briefly touch the blinking light or wait for the timeout.\"))\n            reply = dbb_client.hid_send_encrypt(msg) # Send twice, first returns an echo for smart verification (not implemented)\n            self.handler.finished()\n\n            if 'error' in reply:\n                raise Exception(reply['error']['message'])\n\n            if 'sign' not in reply:\n                raise Exception(_(\"Could not sign message.\"))\n\n            if 'recid' in reply['sign'][0]:\n                # firmware > v2.1.1\n                sig_string = binascii.unhexlify(reply['sign'][0]['sig'])\n                recid = int(reply['sign'][0]['recid'], 16)\n                sig = ecc.construct_ecdsa_sig65(sig_string, recid, is_compressed=True)\n                pubkey, compressed, txin_type_guess = ecc.ECPubkey.from_ecdsa_sig65(sig, msg_hash)\n                addr = public_key_to_p2pkh(pubkey.get_public_key_bytes(compressed=compressed))\n                if verify_usermessage_with_address(addr, sig, message) is False:\n                    raise Exception(_(\"Could not sign message\"))\n            elif 'pubkey' in reply['sign'][0]:\n                # firmware <= v2.1.1\n                for recid in range(4):\n                    sig_string = binascii.unhexlify(reply['sign'][0]['sig'])\n                    sig = ecc.construct_ecdsa_sig65(sig_string, recid, is_compressed=True)\n                    try:\n                        addr = public_key_to_p2pkh(binascii.unhexlify(reply['sign'][0]['pubkey']))\n                        if verify_usermessage_with_address(addr, sig, message):\n                            break\n                    except Exception:\n                        continue\n                else:\n                    raise Exception(_(\"Could not sign message\"))\n\n        except BaseException as e:\n            self.give_error(e)\n        return sig\n\n    def sign_transaction(self, tx, password):\n        if tx.is_complete():\n            return\n\n        try:\n            p2pkhTransaction = True\n            inputhasharray = []\n            hasharray = []\n            pubkeyarray = []\n\n            # Build hasharray from inputs\n            for i, txin in enumerate(tx.inputs()):\n                if txin.is_coinbase_input():\n                    self.give_error(\"Coinbase not supported\") # should never happen\n\n                desc = txin.script_descriptor\n                assert desc\n                if desc.to_legacy_electrum_script_type() != 'p2pkh':\n                    p2pkhTransaction = False\n\n                my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin)\n                if not inputPath:\n                    self.give_error(\"No matching pubkey for sign_transaction\")  # should never happen\n                inputPath = convert_bip32_intpath_to_strpath(inputPath)\n                inputHash = sha256d(tx.serialize_preimage(i))\n                hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath}\n                hasharray.append(hasharray_i)\n                inputhasharray.append(inputHash)\n\n            # Build pubkeyarray from outputs\n            for txout in tx.outputs():\n                assert txout.address\n                if txout.is_change:\n                    changePubkey, changePath = self.find_my_pubkey_in_txinout(txout)\n                    assert changePath\n                    changePath = convert_bip32_intpath_to_strpath(changePath)\n                    changePubkey = changePubkey.hex()\n                    pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath}\n                    pubkeyarray.append(pubkeyarray_i)\n\n            # Special serialization of the unsigned transaction for\n            # the mobile verification app.\n            # At the moment, verification only works for p2pkh transactions.\n            if p2pkhTransaction:\n                tx_copy = copy.deepcopy(tx)\n                # monkey-patch method of tx_copy instance to change serialization\n                def input_script(self, txin: PartialTxInput, *, estimate_size=False) -> bytes:\n                    desc = txin.script_descriptor\n                    if isinstance(desc, descriptor.PKHDescriptor):\n                        return txin.get_scriptcode_for_sighash()\n                    raise Exception(f\"unsupported txin type. only p2pkh is supported. got: {desc.to_string()[:10]}\")\n                tx_copy.input_script = input_script.__get__(tx_copy, PartialTransaction)\n                tx_dbb_serialized = tx_copy.serialize_to_network()\n            else:\n                # We only need this for the signing echo / verification.\n                tx_dbb_serialized = None\n\n            # Build sign command\n            dbb_signatures = []\n            steps = math.ceil(1.0 * len(hasharray) / self.maxInputs)\n            for step in range(int(steps)):\n                hashes = hasharray[step * self.maxInputs : (step + 1) * self.maxInputs]\n\n                msg = {\n                    \"sign\": {\n                        \"data\": hashes,\n                        \"checkpub\": pubkeyarray,\n                    },\n                }\n                if tx_dbb_serialized is not None:\n                    msg[\"sign\"][\"meta\"] = to_hexstr(sha256d(tx_dbb_serialized))\n                msg = json.dumps(msg).encode('ascii')\n                dbb_client = self.plugin.get_client(self)\n\n                if not dbb_client.is_paired():\n                    raise Exception(\"Could not sign transaction.\")\n\n                reply = dbb_client.hid_send_encrypt(msg)\n                if 'error' in reply:\n                    raise Exception(reply['error']['message'])\n\n                if 'echo' not in reply:\n                    raise Exception(\"Could not sign transaction.\")\n\n                if self.plugin.is_mobile_paired() and tx_dbb_serialized is not None:\n                    reply['tx'] = tx_dbb_serialized\n                    self.plugin.comserver_post_notification(reply, handler=self.handler)\n\n                if steps > 1:\n                    self.handler.show_message(_(\"Signing large transaction. Please be patient ...\") + \"\\n\\n\" +\n                                              _(\"To continue, touch the Digital Bitbox's blinking light for 3 seconds.\") + \" \" +\n                                              _(\"(Touch {} of {})\").format((step + 1), steps) + \"\\n\\n\" +\n                                              _(\"To cancel, briefly touch the blinking light or wait for the timeout.\") + \"\\n\\n\")\n                else:\n                    self.handler.show_message(_(\"Signing transaction...\") + \"\\n\\n\" +\n                                              _(\"To continue, touch the Digital Bitbox's blinking light for 3 seconds.\") + \"\\n\\n\" +\n                                              _(\"To cancel, briefly touch the blinking light or wait for the timeout.\"))\n\n                # Send twice, first returns an echo for smart verification\n                reply = dbb_client.hid_send_encrypt(msg)\n                self.handler.finished()\n\n                if 'error' in reply:\n                    if reply[\"error\"].get('code') in (600, 601):\n                        # aborted via LED short touch or timeout\n                        raise UserCancelled()\n                    raise Exception(reply['error']['message'])\n\n                if 'sign' not in reply:\n                    raise Exception(\"Could not sign transaction.\")\n\n                dbb_signatures.extend(reply['sign'])\n\n            # Fill signatures\n            if len(dbb_signatures) != len(tx.inputs()):\n                raise Exception(\"Incorrect number of transactions signed.\") # Should never occur\n            for i, txin in enumerate(tx.inputs()):\n                for pubkey_bytes in txin.pubkeys:\n                    if txin.is_complete():\n                        break\n                    signed = dbb_signatures[i]\n                    if 'recid' in signed:\n                        # firmware > v2.1.1\n                        recid = int(signed['recid'], 16)\n                        s = binascii.unhexlify(signed['sig'])\n                        h = inputhasharray[i]\n                        pk = ecc.ECPubkey.from_ecdsa_sig64(s, recid, h)\n                        pk = pk.get_public_key_hex(compressed=True)\n                    elif 'pubkey' in signed:\n                        # firmware <= v2.1.1\n                        pk = signed['pubkey']\n                    if pk != pubkey_bytes.hex():\n                        continue\n                    sig_r = int(signed['sig'][:64], 16)\n                    sig_s = int(signed['sig'][64:], 16)\n                    sig = ecc.ecdsa_der_sig_from_r_and_s(sig_r, sig_s)\n                    sig = sig + Sighash.to_sigbytes(Sighash.ALL)\n                    tx.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey_bytes, sig=sig)\n        except UserCancelled:\n            raise\n        except BaseException as e:\n            self.give_error(e)\n        else:\n            _logger.info(f\"Transaction is_complete {tx.is_complete()}\")\n\n\nclass DigitalBitboxPlugin(HW_PluginBase):\n\n    libraries_available = DIGIBOX\n    keystore_class = DigitalBitbox_KeyStore\n    DEVICE_IDS = [\n                   (0x03eb, 0x2402) # Digital Bitbox\n                 ]\n    SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')\n\n    def __init__(self, parent, config, name):\n        HW_PluginBase.__init__(self, parent, config, name)\n        if self.libraries_available:\n            self.device_manager().register_devices(self.DEVICE_IDS, plugin=self)\n\n        self.digitalbitbox_config = self.config.get('digitalbitbox', {})\n\n    @runs_in_hwd_thread\n    def get_dbb_device(self, device):\n        dev = hid.device()\n        dev.open_path(device.path)\n        return dev\n\n    def create_client(self, device, handler):\n        if device.interface_number == 0 or device.usage_page == 0xffff:\n            client = self.get_dbb_device(device)\n            if client is not None:\n                client = DigitalBitbox_Client(self, client)\n            return client\n        else:\n            return None\n\n    def is_mobile_paired(self):\n        return ENCRYPTION_PRIVKEY_KEY in self.digitalbitbox_config\n\n    def comserver_post_notification(self, payload, *, handler: 'HardwareHandlerBase'):\n        assert self.is_mobile_paired(), \"unexpected mobile pairing error\"\n        url = 'https://digitalbitbox.com/smartverification/index.php'\n        key_s = base64.b64decode(self.digitalbitbox_config[ENCRYPTION_PRIVKEY_KEY], validate=True)\n        ciphertext = EncodeAES_bytes(key_s, json.dumps(payload).encode('ascii'))\n        args = 'c=data&s=0&dt=0&uuid=%s&pl=%s' % (\n            self.digitalbitbox_config[CHANNEL_ID_KEY],\n            base64.b64encode(ciphertext).decode('ascii'),\n        )\n        try:\n            text = Network.send_http_on_proxy('post', url, body=args.encode('ascii'), headers={'content-type': 'application/x-www-form-urlencoded'})\n            _logger.info(f'digitalbitbox reply from server {text}')\n        except Exception as e:\n            _logger.exception(\"\")\n            handler.show_error(repr(e))  # repr because str(Exception()) == ''\n\n    def get_client(self, keystore, force_pair=True, *,\n                   devices=None, allow_user_interaction=True):\n        client = super().get_client(keystore, force_pair,\n                                    devices=devices,\n                                    allow_user_interaction=allow_user_interaction)\n        if client is not None:\n            client.check_device_dialog()\n        return client\n\n    def show_address(self, wallet, address, keystore=None):\n        if keystore is None:\n            keystore = wallet.get_keystore()\n        if not self.show_address_helper(wallet, address, keystore):\n            return\n        if type(wallet) is not Standard_Wallet:\n            keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device))\n            return\n        if not self.is_mobile_paired():\n            keystore.handler.show_error(_('This function is only available after pairing your {} with a mobile device.').format(self.device))\n            return\n        if wallet.get_txin_type(address) != 'p2pkh':\n            keystore.handler.show_error(_('This function is only available for p2pkh keystores when using {}.').format(self.device))\n            return\n        change, index = wallet.get_address_index(address)\n        keypath = '%s/%d/%d' % (keystore.get_derivation_prefix(), change, index)\n        xpub = self.get_client(keystore)._get_xpub(keypath)\n        verify_request_payload = {\n            \"type\": 'p2pkh',\n            \"echo\": xpub['echo'],\n        }\n        self.comserver_post_notification(verify_request_payload, handler=keystore.handler)\n\n    def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str:\n        if new_wallet:\n            return 'dbitbox_start'\n        else:\n            return 'dbitbox_unlock'\n\n    # insert digitalbitbox pages in new wallet wizard\n    def extend_wizard(self, wizard: 'NewWalletWizard'):\n        views = {\n            'dbitbox_start': {\n                'next': 'dbitbox_xpub',\n            },\n            'dbitbox_xpub': {\n                'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore',\n                'accept': wizard.maybe_master_pubkey,\n                'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d)\n            },\n            'dbitbox_unlock': {\n                'last': True\n            },\n        }\n        wizard.navmap_merge(views)\n"
  },
  {
    "path": "electrum/plugins/digitalbitbox/manifest.json",
    "content": "{\n  \"name\": \"digitalbitbox\",\n  \"fullname\": \"Digital Bitbox\",\n  \"description\": \"Provides support for Digital Bitbox hardware wallet\",\n  \"registers_keystore\": [\"hardware\", \"digitalbitbox\", \"Digital Bitbox wallet\"],\n  \"icon\":\"digitalbitbox.png\",\n  \"available_for\": [\"qt\", \"cmdline\"]\n}\n"
  },
  {
    "path": "electrum/plugins/digitalbitbox/qt.py",
    "content": "import threading\nfrom functools import partial\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import pyqtSignal\n\nfrom electrum.i18n import _\nfrom electrum.plugin import hook\nfrom electrum.wallet import Standard_Wallet, Abstract_Wallet\nfrom electrum.util import UserCancelled, UserFacingException\n\nfrom electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase\nfrom electrum.hw_wallet.plugin import only_hook_if_libraries_available, OperationCancelled\n\nfrom electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWXPub, WCHWUnlock\n\nfrom .digitalbitbox import DigitalBitboxPlugin, DeviceErased\n\nif TYPE_CHECKING:\n    from electrum.gui.qt.wizard.wallet import QENewWalletWizard\n\n\nclass Plugin(DigitalBitboxPlugin, QtPluginBase):\n    icon_unpaired = \"digitalbitbox_unpaired.png\"\n    icon_paired = \"digitalbitbox.png\"\n\n    def create_handler(self, window):\n        return DigitalBitbox_Handler(window)\n\n    @only_hook_if_libraries_available\n    @hook\n    def receive_menu(self, menu, addrs, wallet):\n        if not self.is_mobile_paired():\n            return\n        if len(addrs) != 1:\n            return\n        if wallet.get_txin_type(addrs[0]) != 'p2pkh':\n            return\n        self._add_menu_action(menu, addrs[0], wallet)\n\n    @only_hook_if_libraries_available\n    @hook\n    def transaction_dialog_address_menu(self, menu, addr, wallet):\n        if not self.is_mobile_paired():\n            return\n        if wallet.get_txin_type(addr) != 'p2pkh':\n            return\n        self._add_menu_action(menu, addr, wallet)\n\n    # insert digitalbitbox pages in new wallet wizard\n    def extend_wizard(self, wizard: 'QENewWalletWizard'):\n        super().extend_wizard(wizard)\n        views = {\n            'dbitbox_start': {'gui': WCDigitalBitboxScriptAndDerivation},\n            'dbitbox_xpub': {'gui': WCHWXPub},\n            'dbitbox_unlock': {'gui': WCHWUnlock}\n        }\n        wizard.navmap_merge(views)\n\n\nclass DigitalBitbox_Handler(QtHandlerBase):\n    def __init__(self, win):\n        super(DigitalBitbox_Handler, self).__init__(win, 'Digital Bitbox')\n\n\nclass WCDigitalBitboxScriptAndDerivation(WCScriptAndDerivation):\n    requestRecheck = pyqtSignal()\n\n    def __init__(self, parent, wizard):\n        WCScriptAndDerivation.__init__(self, parent, wizard)\n        self._busy = True\n        self.title = ''\n        self.client = None\n\n        self.requestRecheck.connect(self.check_device)\n\n    def on_ready(self):\n        super().on_ready()\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = current_cosigner['hardware_device']\n        plugin = self.wizard.plugins.get_plugin(_info.plugin_name)\n\n        device_id = _info.device.id_\n        self.client = self.wizard.plugins.device_manager.client_by_id(device_id, scan_now=False)\n        if not self.client.handler:\n            self.client.handler = plugin.create_handler(self.wizard)\n        self.client.setupRunning = True\n        self.check_device()\n\n    def check_device(self):\n        self.error = None\n        self.busy = True\n\n        def check_task():\n            try:\n                self.client.check_device_dialog()\n                self.title = _('Script type and Derivation path')\n                self.valid = True\n            except (UserCancelled, OperationCancelled):\n                self.error = _('Cancelled')\n                self.wizard.requestPrev.emit()\n            except DeviceErased:\n                self.error = _('Device erased')\n                self.requestRecheck.emit()\n            except UserFacingException as e:\n                self.error = str(e)\n            finally:\n                self.busy = False\n\n        t = threading.Thread(target=check_task, daemon=True)\n        t.start()\n"
  },
  {
    "path": "electrum/plugins/jade/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/plugins/jade/cmdline.py",
    "content": "from electrum.plugin import hook\nfrom .jade import JadePlugin\nfrom electrum.hw_wallet import CmdLineHandler\n\nclass Plugin(JadePlugin):\n    handler = CmdLineHandler()\n    @hook\n    def init_keystore(self, keystore):\n        if not isinstance(keystore, self.keystore_class):\n            return\n        keystore.handler = self.handler\n\n    def create_handler(self, window):\n        return self.handler\n"
  },
  {
    "path": "electrum/plugins/jade/jade.py",
    "content": "import os\nimport base64\nimport json\nfrom typing import Optional, TYPE_CHECKING\n\nfrom electrum import bip32, constants\nfrom electrum.crypto import sha256\nfrom electrum.i18n import _\nfrom electrum.keystore import Hardware_KeyStore\nfrom electrum.transaction import PartialTransaction, Transaction\nfrom electrum.wallet import Multisig_Wallet\nfrom electrum.util import UserFacingException\nfrom electrum.logging import get_logger\nfrom electrum.plugin import runs_in_hwd_thread, Device\nfrom electrum.network import Network\n\nfrom electrum.hw_wallet import HW_PluginBase, HardwareClientBase\nfrom electrum.hw_wallet.plugin import OutdatedHwFirmwareException\n\nif TYPE_CHECKING:\n    from electrum.plugin import DeviceInfo\n    from electrum.wizard import NewWalletWizard\n\n_logger = get_logger(__name__)\n\n#import logging\n#LOGGING = logging.INFO\n#if LOGGING:\n#    logger = logging.getLogger('electrum.plugins.jade.jadepy.jade')\n#    logger.setLevel(LOGGING)\n#    device_logger = logging.getLogger('electrum.plugins.jade.jadepy.jade-device')\n#    device_logger.setLevel(LOGGING)\n\ntry:\n    # Do imports\n    from .jadepy import jade\n    from .jadepy.jade import JadeAPI\n    from .jadepy.jade_serial import JadeSerialImpl\n    from serial.tools import list_ports\nexcept ImportError as e:\n    _logger.exception('error importing Jade plugin deps')\n\n# Ignore -beta and -rc etc labels\ndef _versiontuple(v):\n    return tuple(map(int, (v.split('-')[0].split('.'))))\n\ndef _is_multisig(wallet):\n    return type(wallet) is Multisig_Wallet\n\n# Ensure a multisig wallet is registered on Jade hw.\n# Derives and returns the deterministic name for that multisig registration\ndef _register_multisig_wallet(wallet, keystore, address):\n    wallet_fingerprint_hash = sha256(wallet.get_fingerprint())\n    multisig_name = 'ele' + wallet_fingerprint_hash.hex()[:12]\n\n    # Collect all the signer data in case we need to register the\n    # multisig wallet on the Jade hw - NOTE: re-register is a no-op.\n    signers = []\n    for kstore in wallet.get_keystores():\n        fingerprint = kstore.get_root_fingerprint()\n        bip32_path_prefix = kstore.get_derivation_prefix()\n        derivation_path = bip32.convert_bip32_strpath_to_intpath(bip32_path_prefix)\n\n        # Jade only understands standard xtypes, so convert here\n        node = bip32.BIP32Node.from_xkey(kstore.xpub)\n        standard_xpub = node._replace(xtype='standard').to_xkey()\n\n        signers.append({'fingerprint': bytes.fromhex(fingerprint),\n                        'derivation': derivation_path,\n                        'xpub': standard_xpub,\n                        'path': []})\n\n    # Check multisig is registered - re-registering is a no-op\n    # NOTE: electrum multisigs appear to always be sorted-multisig\n    txin_type = wallet.get_txin_type(address)\n    keystore.register_multisig(multisig_name, txin_type, True, wallet.m, signers)\n\n    # Return the name used to register the wallet\n    return multisig_name\n\n# Helper to adapt Jade's http call/data to Network.send_http_on_proxy()\ndef _http_request(params):\n    # Use the first non-onion url\n    url = [url for url in params['urls'] if not url.endswith('.onion')][0]\n    method = params['method'].lower()\n    json_payload = params.get('data')\n    json_response = Network.send_http_on_proxy(method, url, json=json_payload)\n    return {'body': json.loads(json_response)}\n\nclass Jade_Client(HardwareClientBase):\n\n    @staticmethod\n    def _network() -> str:\n        return 'localtest' if constants.net.NET_NAME == 'regtest' else constants.net.NET_NAME\n\n    ADDRTYPES = {'standard': 'pkh(k)',\n                 'p2pkh': 'pkh(k)',\n                 'p2wpkh': 'wpkh(k)',\n                 'p2wpkh-p2sh': 'sh(wpkh(k))'}\n\n    MULTI_ADDRTYPES = {'standard': 'sh(multi(k))',\n                       'p2sh': 'sh(multi(k))',\n                       'p2wsh': 'wsh(multi(k))',\n                       'p2wsh-p2sh': 'sh(wsh(multi(k)))'}\n\n    @classmethod\n    def _convertAddrType(cls, addrType: str, multisig: bool) -> str:\n        return cls.MULTI_ADDRTYPES[addrType] if multisig else cls.ADDRTYPES[addrType]\n\n    def __init__(self, device: str, plugin: HW_PluginBase):\n        HardwareClientBase.__init__(self, plugin=plugin)\n\n        # Connect with a small timeout to test connection\n        self.jade = JadeAPI.create_serial(device, timeout=1)\n        self.jade.connect()\n\n        verinfo = self.jade.get_version_info()\n        self.fwversion = _versiontuple(verinfo['JADE_VERSION'])\n        self.efusemac = verinfo['EFUSEMAC']\n        self.jade.disconnect()\n\n        # Reconnect with the default timeout for all subsequent calls\n        self.jade = JadeAPI.create_serial(device)\n        self.jade.connect()\n\n        # Push some host entropy into jade\n        self.jade.add_entropy(os.urandom(32))\n\n    @runs_in_hwd_thread\n    def authenticate(self):\n        # Ensure Jade unlocked - always call hw unit at least once\n        # If the hw is already unlocked, this call returns immediately/no-op\n        # NOTE: uses provided http/networking which respects any user proxy\n        authenticated = False\n        while not authenticated:\n            authenticated = self.jade.auth_user(self._network(), http_request_fn=_http_request)\n\n    def is_pairable(self):\n        return True\n\n    @runs_in_hwd_thread\n    def close(self):\n        self.jade.disconnect()\n        self.jade = None\n\n    @runs_in_hwd_thread\n    def is_initialized(self):\n        verinfo = self.jade.get_version_info()\n        return verinfo['JADE_STATE'] != 'UNINIT'\n\n    def label(self) -> Optional[str]:\n        return self.efusemac[-6:]\n\n    def get_soft_device_id(self):\n        return f'Jade {self.label()}'\n\n    def device_model_name(self):\n        return 'Blockstream Jade'\n\n    @runs_in_hwd_thread\n    def has_usable_connection_with_device(self):\n        if self.efusemac is None:\n            return False\n\n        try:\n            verinfo = self.jade.get_version_info()\n            return verinfo['EFUSEMAC'] == self.efusemac\n        except BaseException:\n            return False\n\n    @runs_in_hwd_thread\n    def get_xpub(self, bip32_path, xtype):\n        self.authenticate()\n\n        # Jade only provides traditional xpubs ...\n        path = bip32.convert_bip32_strpath_to_intpath(bip32_path)\n        xpub = self.jade.get_xpub(self._network(), path)\n\n        # ... so convert to relevant xtype locally\n        node = bip32.BIP32Node.from_xkey(xpub)\n        return node._replace(xtype=xtype).to_xkey()\n\n    @runs_in_hwd_thread\n    def sign_message(self, bip32_path_prefix, sequence, message):\n        self.authenticate()\n\n        path = bip32.convert_bip32_strpath_to_intpath(bip32_path_prefix)\n        path.extend(sequence)\n\n        if isinstance(message, bytes) or isinstance(message, bytearray):\n            message = message.decode('utf-8')\n\n        # Signature verification does not work with anti-exfil, so stick with default (rfc6979)\n        sig = self.jade.sign_message(path, message)\n        return base64.b64decode(sig, validate=True)\n\n    @runs_in_hwd_thread\n    def sign_psbt(self, psbt_bytes):\n        self.authenticate()\n\n        # Pass as PSBT to Jade for signing.  As of fw v0.1.47 Jade should handle PSBT natively.\n        return self.jade.sign_psbt(self._network(), psbt_bytes)\n\n    @runs_in_hwd_thread\n    def show_address(self, bip32_path_prefix, sequence, txin_type):\n        self.authenticate()\n        path = bip32.convert_bip32_strpath_to_intpath(bip32_path_prefix)\n        path.extend(sequence)\n        script_variant = self._convertAddrType(txin_type, multisig=False)\n        address = self.jade.get_receive_address(self._network(), path, variant=script_variant)\n        return address\n\n    @runs_in_hwd_thread\n    def register_multisig(self, multisig_name, txin_type, sorted, threshold, signers):\n        self.authenticate()\n        variant = self._convertAddrType(txin_type, multisig=True)\n        return self.jade.register_multisig(self._network(), multisig_name, variant, sorted, threshold, signers)\n\n    @runs_in_hwd_thread\n    def show_address_multi(self, multisig_name, paths):\n        self.authenticate()\n        return self.jade.get_receive_address(self._network(), paths, multisig_name=multisig_name)\n\nclass Jade_KeyStore(Hardware_KeyStore):\n    hw_type = 'jade'\n    device = 'Jade'\n\n    plugin: 'JadePlugin'\n\n    def decrypt_message(self, sequence, message, password):\n        raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device))\n\n    @runs_in_hwd_thread\n    def sign_message(self, sequence, message, password, *, script_type=None):\n        self.handler.show_message(_(\"Please confirm signing the message with your Jade device...\"))\n        try:\n            client = self.get_client()\n            bip32_path_prefix = self.get_derivation_prefix()\n            return client.sign_message(bip32_path_prefix, sequence, message)\n        finally:\n            self.handler.finished()\n\n    @runs_in_hwd_thread\n    def sign_transaction(self, tx, password):\n        if tx.is_complete():\n            return\n\n        self.handler.show_message(_(\"Preparing to sign transaction ...\"))\n        try:\n            wallet = self.handler.get_wallet()\n            if _is_multisig(wallet):\n                # Register multisig on Jade using any change addresses\n                for txout in tx.outputs():\n                    if txout.is_mine and txout.is_change:\n                        # Multisig - wallet details must be registered on Jade hw\n                        _register_multisig_wallet(wallet, self, txout.address)\n\n            # NOTE: sign_psbt() does not use AE signatures, so sticks with default (rfc6979)\n            self.handler.show_message(_(\"Please confirm the transaction details on your Jade device...\"))\n            client = self.get_client()\n\n            psbt_bytes = tx.serialize_as_bytes()\n            psbt_bytes = client.sign_psbt(psbt_bytes)\n            signed_tx = PartialTransaction.from_raw_psbt(psbt_bytes)\n\n            # Copy signatures into original tx\n            tx.combine_with_other_psbt(signed_tx)\n\n        finally:\n            self.handler.finished()\n\n    @runs_in_hwd_thread\n    def show_address(self, sequence, txin_type):\n        self.handler.show_message(_(\"Showing address ...\"))\n        try:\n            client = self.get_client()\n            bip32_path_prefix = self.get_derivation_prefix()\n            return client.show_address(bip32_path_prefix, sequence, txin_type)\n        finally:\n            self.handler.finished()\n\n    @runs_in_hwd_thread\n    def register_multisig(self, name, txin_type, sorted, threshold, signers):\n        self.handler.show_message(_(\"Please confirm the multisig wallet details on your Jade device...\"))\n        try:\n            client = self.get_client()\n            return client.register_multisig(name, txin_type, sorted, threshold, signers)\n        finally:\n            self.handler.finished()\n\n    @runs_in_hwd_thread\n    def show_address_multi(self, multisig_name, paths):\n        self.handler.show_message(_(\"Showing address ...\"))\n        try:\n            client = self.get_client()\n            return client.show_address_multi(multisig_name, paths)\n        finally:\n            self.handler.finished()\n\n\nclass JadePlugin(HW_PluginBase):\n    keystore_class = Jade_KeyStore\n    minimum_library = (0, 0, 1)\n    DEVICE_IDS = JadeSerialImpl.JADE_DEVICE_IDS\n    SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')\n    MIN_SUPPORTED_FW_VERSION = (0, 1, 47)\n\n    # For testing with qemu simulator (experimental)\n    SIMULATOR_PATH = None  # 'tcp:127.0.0.1:2222'\n    SIMULATOR_TEST_SEED = None  # bytes.fromhex('b90e532426d0dc20fffe01037048c018e940300038b165c211915c672e07762c')\n\n    def enumerate_serial(self):\n        # Jade is not really an HID device, it shows as a serial/com port device.\n        # Scan com ports looking for the relevant vid and pid, and use 'path' to\n        # hold the path to the serial port device, eg. /dev/ttyUSB0\n        devices = []\n        for devinfo in list_ports.comports():\n            device_product_key = (devinfo.vid, devinfo.pid)\n            if device_product_key in self.DEVICE_IDS:\n                device = Device(path=devinfo.device,\n                                interface_number=-1,\n                                id_=devinfo.serial_number,\n                                product_key=device_product_key,\n                                usage_page=-1,\n                                transport_ui_string=devinfo.device)\n                devices.append(device)\n\n        # Maybe look for Jade Qemu simulator if the vars are set (experimental)\n        if self.SIMULATOR_PATH is not None and self.SIMULATOR_TEST_SEED is not None:\n            try:\n                # If we can connect to a simulator and poke a seed in, add that too\n                client = Jade_Client(self.SIMULATOR_PATH, plugin=self)\n                device = Device(path=self.SIMULATOR_PATH,\n                                interface_number=-1,\n                                id_='Jade Qemu Simulator',\n                                product_key=self.DEVICE_IDS[0],\n                                usage_page=-1,\n                                transport_ui_string='simulator')\n                if client.jade.set_seed(self.SIMULATOR_TEST_SEED):\n                    devices.append(device)\n                client.close()\n            except Exception as e:\n                # If we get any sort of error do not add the simulator\n                _logger.debug(\"Failed to connect to Jade simulator at {}\".format(self.SIMULATOR_PATH))\n                _logger.debug(e)\n\n        return devices\n\n    def __init__(self, parent, config, name):\n        HW_PluginBase.__init__(self, parent, config, name)\n\n        self.libraries_available = self.check_libraries_available()\n        if not self.libraries_available:\n            return\n\n        # Register our own serial/com port scanning function\n        self.device_manager().register_enumerate_func(self.enumerate_serial)\n\n    def get_library_version(self):\n        try:\n            from . import jadepy\n            version = jadepy.__version__\n        except ImportError:\n            raise\n        except Exception:\n            version = \"unknown\"\n        return version\n\n    @runs_in_hwd_thread\n    def create_client(self, device, handler):\n        client = Jade_Client(device.path, plugin=self)\n\n        # Check minimum supported firmware version\n        if self.MIN_SUPPORTED_FW_VERSION > client.fwversion:\n            msg = (_('Outdated {} firmware for device labelled {}. Please '\n                     'update using a Blockstream Green companion app')\n                   .format(self.device, client.label()))\n            self.logger.info(msg)\n\n            if handler:\n                handler.show_error(msg)\n\n            raise OutdatedHwFirmwareException(msg)\n\n        return client\n\n    def show_address(self, wallet, address, keystore=None):\n        if keystore is None:\n            keystore = wallet.get_keystore()\n        if not self.show_address_helper(wallet, address, keystore):\n            return\n\n        path_suffix = wallet.get_address_index(address)\n        if _is_multisig(wallet):\n            # Multisig - wallet details must be registered on Jade hw\n            multisig_name = _register_multisig_wallet(wallet, keystore, address)\n\n            # Jade only needs the path suffix(es) and the multisig registration\n            # name to generate the address, as the fixed derivation part is\n            # embedded in the multisig wallet registration record\n            # NOTE: all cosigners have same path suffix\n            paths = [path_suffix] * wallet.n\n            hw_address = keystore.show_address_multi(multisig_name, paths)\n        else:\n            # Single-sig/standard\n            txin_type = wallet.get_txin_type(address)\n            hw_address = keystore.show_address(path_suffix, txin_type)\n\n        if hw_address != address:\n            keystore.handler.show_error(_('The address generated by {} does not match!').format(self.device))\n\n    def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str:\n        if new_wallet:\n            return 'jade_start' if device_info.initialized else 'jade_not_initialized'\n        else:\n            return 'jade_unlock'\n\n    # insert jade pages in new wallet wizard\n    def extend_wizard(self, wizard: 'NewWalletWizard'):\n        views = {\n            'jade_start': {\n                'next': 'jade_xpub',\n            },\n            'jade_xpub': {\n                'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore',\n                'accept': wizard.maybe_master_pubkey,\n                'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d)\n            },\n            'jade_not_initialized': {},\n            'jade_unlock': {\n                'last': True\n            },\n        }\n        wizard.navmap_merge(views)\n"
  },
  {
    "path": "electrum/plugins/jade/jadepy/README.md",
    "content": "# Python Jade Library\n\nThis is a slightly modified version of the official [Jade](https://github.com/Blockstream/Jade) python library.\n\nThis modified version was made from tag [1.0.31](https://github.com/Blockstream/Jade/releases/tag/1.0.31).\n\n## Changes\n\n- Removed BLE module, reducing transitive dependencies\n- _http_request() function removed, so cannot be used as unintentional fallback\n"
  },
  {
    "path": "electrum/plugins/jade/jadepy/__init__.py",
    "content": "from .jade import JadeAPI\nfrom .jade_error import JadeError\n\n__version__ = \"1.0.31\"\n"
  },
  {
    "path": "electrum/plugins/jade/jadepy/jade.py",
    "content": "import cbor2 as cbor\nimport hashlib\nimport json\nimport time\nimport logging\nimport collections\nimport collections.abc\nimport traceback\nimport random\nimport socket\nimport sys\n\n# JadeError\nfrom .jade_error import JadeError\n\n# Low-level comms backends\nfrom .jade_serial import JadeSerialImpl\nfrom .jade_tcp import JadeTCPImpl\n\n# 'jade' logger\nlogger = logging.getLogger(__name__)\ndevice_logger = logging.getLogger(f'{__name__}-device')\n\n# BLE comms backend is optional\n# It relies on the BLE dependencies being available\ntry:\n    from .jade_ble import JadeBleImpl\nexcept (ImportError, FileNotFoundError) as e:\n    logger.warning(e)\n    logger.warning('BLE scanning/connectivity will not be available')\n\n\n# Default serial connection\nDEFAULT_BAUD_RATE = 115200\nDEFAULT_SERIAL_TIMEOUT = 120\n\n# Default BLE connection\nDEFAULT_BLE_DEVICE_NAME = 'Jade'\nDEFAULT_BLE_SERIAL_NUMBER = None\nDEFAULT_BLE_SCAN_TIMEOUT = 60\n\n\ndef _hexlify(data):\n    \"\"\"\n    Helper to map bytes-like types into hex-strings\n    to make for prettier message-logging.\n\n    Parameters\n    ----------\n    data : any\n        The object to hexlify.\n        - bytes or bytearrays have 'hex()' method invoked\n        - list and dicts (values) have this function mapped over them\n        - Otherwise the input is returned unchanged\n    \"\"\"\n    if data is None:\n        return None\n    elif isinstance(data, bytes) or isinstance(data, bytearray):\n        return data.hex()\n    elif isinstance(data, list):\n        return [_hexlify(item) for item in data]\n    elif isinstance(data, dict):\n        return {k: _hexlify(v) for k, v in data.items()}\n    else:\n        return data\n\n# NOTE: Removed entirely for electrum - so it is not used silently as a fallback.\n# (hard error preferred in that case)\n# Jade repo api will be improved to make enabling this function more explicit\n# try:\n#     import requests\n#\n#     def _http_request(params):\n#         \"\"\"\n#         Simple http request function which can be used when a Jade response\n#         requires an external http call.\n#         The default implementation used in JadeAPI._jadeRpc() below.\n#         NOTE: Only available if the 'requests' dependency is available.\n#\n#         Callers can supply their own implementation of this call where it is required.\n#\n#         Parameters\n#         ----------\n#         data : dict\n#             A dictionary structure describing the http call to make\n#\n#         Returns\n#         -------\n#         dict\n#             with single key 'body', whose value is the json returned from the call\n#\n#         \"\"\"\n#         logger.debug('_http_request: {}'.format(params))\n#\n#         # Use the first non-onion url\n#         url = [url for url in params['urls'] if not url.endswith('.onion')][0]\n#\n#         if params['method'] == 'GET':\n#             assert 'data' not in params, 'Cannot pass body to requests.get'\n#             def http_call_fn(): return requests.get(url)\n#         elif params['method'] == 'POST':\n#             data = json.dumps(params['data'])\n#             def http_call_fn(): return requests.post(url, data)\n#         else:\n#             raise JadeError(1, \"Only GET and POST methods supported\", params['method'])\n#\n#         try:\n#             f = http_call_fn()\n#             logger.debug(\"http_request received reply: {}\".format(f.text))\n#\n#             if f.status_code != 200:\n#                 logger.error(\"http error {} : {}\".format(f.status_code, f.text))\n#                 raise ValueError(f.status_code)\n#\n#             assert params['accept'] == 'json'\n#             f = f.json()\n#         except Exception as e:\n#             logging.error(e)\n#             f = None\n#\n#         return {'body': f}\n#\n# except ImportError as e:\n#     logger.info(e)\n#     logger.info('Default _http_requests() function will not be available')\n\ndef generate_dump():\n    while True:\n        try:\n            with socket.create_connection((\"localhost\", 4444)) as s:\n                output = b\"\"\n                while b\"Open On-Chip Debugger\" not in output:\n                    data = s.recv(1024)\n                    if not data:\n                        continue\n                    output += data\n\n                s.sendall(b\"esp gcov dump\\n\")\n\n                output = b\"\"\n                while b\"Targets disconnected.\" not in output:\n                    data = s.recv(1024)\n                    if not data:\n                        continue\n                    output += data\n                s.sendall(b\"resume\\n\")\n                time.sleep(1)\n            return\n        except ConnectionRefusedError:\n            pass\n\n\nclass JadeAPI:\n    \"\"\"\n    High-Level Jade Client API\n    Builds on a JadeInterface to provide a meaningful API\n\n    Either:\n    a) use with JadeAPI.create_[serial|ble]() as jade:\n    (recommended)\n    or:\n    b) use JadeAPI.create_[serial|ble], then call connect() before\n    using, and disconnect() when finished\n    (caveat cranium)\n    or:\n    c) use ctor to wrap existing JadeInterface instance\n    (caveat cranium)\n    \"\"\"\n\n    def __init__(self, jade):\n        assert jade is not None\n        self.jade = jade\n\n    def __enter__(self):\n        self.connect()\n        return self\n\n    def __exit__(self, exc_type, exc, tb):\n        if (exc_type):\n            logger.info(\"Exception causing JadeAPI context exit.\")\n            logger.info(exc_type)\n            logger.info(exc)\n            traceback.print_tb(tb)\n        self.disconnect(exc_type is not None)\n\n    @staticmethod\n    def create_serial(device=None, baud=None, timeout=None):\n        \"\"\"\n        Create a JadeAPI object using the serial interface described.\n\n        Parameters\n        ----------\n        device : str, optional\n            The device identifier for the serial device.\n            Underlying implementation will default (to /dev/ttyUSB0)\n\n        baud : int, optional\n            The communication baud rate.\n            Underlying implementation will default (to 115200)\n\n        timeout : int, optional\n            The serial read timeout when awaiting messages.\n            Underlying implementation will default (to 120s)\n\n        Returns\n        -------\n        JadeAPI\n            API object configured to use given serial parameters.\n            NOTE: the api instance has not yet tried to contact the hw\n            - caller must call 'connect()' before trying to use the Jade.\n        \"\"\"\n        impl = JadeInterface.create_serial(device, baud, timeout)\n        return JadeAPI(impl)\n\n    @staticmethod\n    def create_ble(device_name=None, serial_number=None,\n                   scan_timeout=None, loop=None):\n        \"\"\"\n        Create a JadeAPI object using the BLE interface described.\n        NOTE: raises JadeError if BLE dependencies not installed.\n\n        Parameters\n        ----------\n        device_name : str, optional\n            The device name of the desired BLE device.\n            Underlying implementation will default (to 'Jade')\n\n        serial_number : int, optional\n            The serial number of the desired BLE device\n            - used to disambiguate multiple beacons with the same 'device name'\n            Underlying implementation will connect to the first beacon it scans\n            with the matching 'device name'.\n\n        scan_timeout : int, optional\n            The timeout when scanning for devices which match the device name/serial number.\n            Underlying implementation will default (to 60s)\n\n        loop : optional\n            The asynchio event loop to use, if required.\n            Underlying implementation will default (to asyncio.get_event_loop())\n\n        Returns\n        -------\n        JadeAPI\n            API object configured to use given BLE parameters.\n            NOTE: the api instance has not yet tried to contact the hw\n            - caller must call 'connect()' before trying to use the Jade.\n\n        Raises\n        ------\n        JadeError if BLE backend not available (ie. BLE dependencies not installed)\n        \"\"\"\n        impl = JadeInterface.create_ble(device_name, serial_number,\n                                        scan_timeout, loop)\n        return JadeAPI(impl)\n\n    def connect(self):\n        \"\"\"\n        Try to connect the underlying transport interface (eg. serial, ble, etc.)\n        Raises an exception on failure.\n        \"\"\"\n        self.jade.connect()\n\n    def disconnect(self, drain=False):\n        \"\"\"\n        Disconnect the underlying transport (eg. serial, ble, etc.)\n\n        Parameters\n        ----------\n        drain : bool, optional\n            When true log any/all remaining messages/data, otherwise silently discard.\n            NOTE: can prevent disconnection if data is arriving constantly.\n            Defaults to False.\n        \"\"\"\n        self.jade.disconnect(drain)\n\n    def drain(self):\n        \"\"\"\n        Log any/all outstanding messages/data.\n        NOTE: can run indefinitely if data is arriving constantly.\n        \"\"\"\n        self.jade.drain()\n\n    @staticmethod\n    def _get_result_or_raise_error(reply):\n        \"\"\"\n        Raise any error message returned from a Jade rpc call as an exception.\n\n        Parameters\n        ----------\n        reply : dict\n            Dictionary representing a reply from a Jade rpc call.\n\n        Returns\n        -------\n        dict\n            Any nested 'result' structure, if the reply is not an error.\n\n        Raises\n        ------\n        JadeError\n            If the reply represented an error, including all details received.\n        \"\"\"\n        if 'error' in reply:\n            e = reply['error']\n            raise JadeError(e.get('code'), e.get('message'), e.get('data'))\n\n        return reply['result']\n\n    def _jadeRpc(self, method, params=None, inputid=None, http_request_fn=None, long_timeout=False):\n        \"\"\"\n        Helper to make a request/reply rpc call over the underlying transport interface.\n        NOTE: interface must be 'connected'.\n\n        If the call returns an 'http_request' structure, this is handled here and the http\n        call is made, and the result is passed into the rpc method given in 'on reply', by\n        calling this function recursively.\n\n        Parameters\n        ----------\n        method : str\n            rpc method to invoke\n\n        params : dict, optional\n            any parameters to pass to the rpc method\n            Defaults to None.\n\n        inputid : str, optional\n            Any specific 'id' to use in the rpc message.\n            Defaults to a using a pseudo-random id generated in-situ.\n\n        http_request_fn : function, optional\n            A function which accepts a dict (containing a description of the http request), makes\n            the described http call, and returns the body data in an element called 'body'.\n            Defaults to _http_request() above.\n\n        long_timeout : bool, optional\n            Whether the rpc call should use an indefinitely long timeout, rather than that set on\n            construction.\n            (Useful if the call involves a non-trivial user interaction with the device.)\n            Defaults to False.\n\n        Returns\n        -------\n        dict\n            The reply from the rpc call.\n            NOTE: will return the last/final reply after a sequence of calls, where 'http_request'\n            was returned and remote data was fetched and passed into s subsequent call.\n        \"\"\"\n        newid = inputid if inputid else str(random.randint(100000, 999999))\n        request = self.jade.build_request(newid, method, params)\n        reply = self.jade.make_rpc_call(request, long_timeout)\n        result = self._get_result_or_raise_error(reply)\n\n        # The Jade can respond with a request for interaction with a remote\n        # http server. This is used for interaction with the pinserver but the\n        # code below acts as a dumb proxy and simply makes the http request and\n        # forwards the response back to the Jade.\n        # Note: the function called to make the http-request can be passed in,\n        # or it can default to the simple _http_request() function above, if available.\n        if isinstance(result, collections.abc.Mapping) and 'http_request' in result:\n            this_module = sys.modules[__name__]\n            make_http_request = http_request_fn or getattr(this_module, '_http_request', None)\n            assert make_http_request, 'Default _http_request() function not available'\n\n            http_request = result['http_request']\n            http_response = make_http_request(http_request['params'])\n            return self._jadeRpc(\n                http_request['on-reply'],\n                http_response['body'],\n                http_request_fn=make_http_request,\n                long_timeout=long_timeout)\n\n        return result\n\n    def ping(self):\n        \"\"\"\n        RPC call to test the connection to Jade and that Jade is powered on and receiving data, and\n        return whether the main task is currently handling a message, handling user menu navigation\n        or is idle.\n\n        NOTE: unlike all other calls this is not queued and handled in fifo order - this message is\n        handled immediately and the response sent as quickly as possible.  This call does not block.\n        If this call is made in parallel with Jade processing other messages, the replies may be\n        out of order (although the message 'id' should still be correct).  Use with caution.\n\n        Returns\n        -------\n        0 if the main task is currently idle\n        1 if the main task is handling a client message\n        2 if the main task is handling user ui menu navigation\n        \"\"\"\n        return self._jadeRpc('ping')\n\n    def get_version_info(self, nonblocking=False):\n        \"\"\"\n        RPC call to fetch summary details pertaining to the hardware unit and running firmware.\n\n        Parameters\n        ----------\n        nonblocking : bool\n            If True message will be handled immediately (see also ping()) *experimental feature*\n\n        Returns\n        -------\n        dict\n            Contains keys for various info describing the hw and running fw\n        \"\"\"\n        params = {'nonblocking': True} if nonblocking else None\n        return self._jadeRpc('get_version_info', params)\n\n    def add_entropy(self, entropy):\n        \"\"\"\n        RPC call to add client entropy into the unit RNG entropy pool.\n\n        Parameters\n        ----------\n        entropy : bytes\n            Bytes to fold into the hw entropy pool.\n\n        Returns\n        -------\n        bool\n            True on success\n        \"\"\"\n        params = {'entropy': entropy}\n        return self._jadeRpc('add_entropy', params)\n\n    def set_epoch(self, epoch=None):\n        \"\"\"\n        RPC call to set the current time epoch value, required for TOTP use.\n        NOTE: The time is lost on each power-down and must be reset on restart/reconnect before\n        TOTP can be used.\n\n        Parameters\n        ----------\n        epoch : int, optional\n            Current epoch value, in seconds.  Defaults to int(time.time()) value.\n\n        Returns\n        -------\n        bool\n            True on success\n        \"\"\"\n        params = {'epoch': epoch if epoch is not None else int(time.time())}\n        return self._jadeRpc('set_epoch', params)\n\n    def logout(self):\n        \"\"\"\n        RPC call to logout of any wallet loaded on the Jade unit.\n        Any key material is freed and zero'd.\n        Call always returns true.\n\n        Returns\n        -------\n        bool\n            True\n        \"\"\"\n        return self._jadeRpc('logout')\n\n    def ota_update(self, fwcmp, fwlen, chunksize, fwhash=None, patchlen=None, cb=None,\n                   gcov_dump=False):\n        \"\"\"\n        RPC call to attempt to update the unit's firmware.\n\n        Parameters\n        ----------\n        fwcmp : bytes\n            The compressed firmware image to upload to the Jade unit.  Can be a full firmware or\n            and incremental diff to be applied to the currently running firmware image.\n        fwlen : int\n            The size of the new complete (uncompressed) firmware image (after any delta is applied).\n        chunksize : int\n            The size of the chunks used to upload the compressed firmware.  Each chunk is uploaded\n            and ack'd by the hw unit.\n            The maximum supported chunk size is given in the version info data, under the key\n            'JADE_OTA_MAX_CHUNK'.\n        fwhash: 32-bytes, optional\n            The sha256 hash of the full uncompressed final firmware image.  In the case of a full\n            firmware upload this should be the hash of the uncompressed file.  In the case of a\n            delta update this is the hash of the expected final image - ie. the existing firmware\n            with the uploaded delta applied.  ie. it is a verification of the fw image Jade will try\n            to boot. Optional for backward-compatibility - may become mandatory in a future release.\n        patchlen: int, optional\n            If the compressed firmware bytes are an incremental diff to be applied to the running\n            firmware image, this is the size of that patch when uncompressed.\n            Defaults to None, implying the compressed data is a full firmware image upload.\n            (Compare with fwlen - the size of the final fw image.)\n        cb : function, optional\n            Callback function accepting two integers - the amount of compressed firmware sent thus\n            far, and the total length of the compressed firmware to send.\n            If passed, this function is invoked each time a fw chunk is successfully uploaded and\n            ack'd by the hw, to notify of upload progress.\n            Defaults to None, and nothing is called to report upload progress.\n\n        Returns\n        -------\n        bool\n            True if no errors were reported - on next restart the hw unit will attempt to boot the\n            new firmware.\n        \"\"\"\n\n        # Compute the sha256 hash of the compressed file being uploaded\n        cmphasher = hashlib.sha256()\n        cmphasher.update(fwcmp)\n        cmphash = cmphasher.digest()\n        cmplen = len(fwcmp)\n\n        # Initiate OTA\n        ota_method = 'ota'\n        params = {'fwsize': fwlen,\n                  'cmpsize': cmplen,\n                  'cmphash': cmphash}\n\n        if fwhash is not None:\n            params['fwhash'] = fwhash\n\n        if patchlen is not None:\n            ota_method = 'ota_delta'\n            params['patchsize'] = patchlen\n\n        result = self._jadeRpc(ota_method, params)\n        assert result is True\n\n        # Write binary chunks\n        written = 0\n        while written < cmplen:\n            remaining = cmplen - written\n            length = min(remaining, chunksize)\n            chunk = bytes(fwcmp[written:written + length])\n            result = self._jadeRpc('ota_data', chunk)\n            assert result is True\n            written += length\n\n            if (cb):\n                cb(written, cmplen)\n\n        if gcov_dump:\n            self.run_remote_gcov_dump()\n\n        # All binary data uploaded\n        return self._jadeRpc('ota_complete')\n\n    def run_remote_selfcheck(self):\n        \"\"\"\n        RPC call to run in-built tests.\n        NOTE: Only available in a DEBUG build of the firmware.\n\n        Returns\n        -------\n        int\n            Time in ms for the internal tests to run, as measured on the hw.\n            ie. excluding any messaging overhead\n        \"\"\"\n        return self._jadeRpc('debug_selfcheck', long_timeout=True)\n\n    def run_remote_gcov_dump(self):\n        \"\"\"\n        RPC call to run in-built gcov-dump.\n        NOTE: Only available in a DEBUG build of the firmware.\n\n        Returns\n        -------\n        bool\n            Always True.\n        \"\"\"\n        result = self._jadeRpc('debug_gcov_dump', long_timeout=True)\n        time.sleep(0.5)\n        generate_dump()\n        time.sleep(2)\n        return result\n\n    def capture_image_data(self, check_qr=False):\n        \"\"\"\n        RPC call to capture raw image data from the camera.\n        See also scan_qr() below.\n        NOTE: Only available in a DEBUG build of the firmware.\n\n        Parameters\n        ----------\n        check_qr : bool, optional\n            If True only images which contain a valid qr code are captured and returned.\n            If False, any image is considered valid and is returned.\n            Defaults to False\n\n        Returns\n        -------\n        bytes\n            Raw image data from the camera framebuffer\n        \"\"\"\n        params = {'check_qr': check_qr}\n        return self._jadeRpc('debug_capture_image_data', params)\n\n    def scan_qr(self, image):\n        \"\"\"\n        RPC call to scan a passed image and return any data extracted from any qr image.\n        Exercises the camera image capture, but ignores result and uses passed image instead.\n        See also capture_image_data() above.\n        NOTE: Only available in a DEBUG build of the firmware.\n\n        Parameters\n        ----------\n        image : bytes\n            The image data (as obtained from capture_image_data() above).\n\n        Returns\n        -------\n        bytes\n            String or byte data obtained from the image (via qr code)\n        \"\"\"\n        params = {'image': image}\n        return self._jadeRpc('debug_scan_qr', params)\n\n    def clean_reset(self):\n        \"\"\"\n        RPC call to clean/reset memory and storage, as much as is practical.\n        NOTE: Only available in a DEBUG build of the firmware.\n\n        Returns\n        -------\n        bool\n            True on success.\n        \"\"\"\n        return self._jadeRpc('debug_clean_reset')\n\n    def set_mnemonic(self, mnemonic, passphrase=None, temporary_wallet=False):\n        \"\"\"\n        RPC call to set the wallet mnemonic (in RAM only - flash storage is untouched).\n        NOTE: Only available in a DEBUG build of the firmware.\n\n        Parameters\n        ----------\n        mnemonic : str\n            The wallet mnemonic to set.\n\n        passphrase : str, optional\n            Any bip39 passphrase to apply.\n            Defaults to None.\n\n        temporary_wallet : bool, optional\n            Whether to treat this wallet/mnemonic as an 'Emergency Restore' temporary wallet, as\n            opposed to one successfully loaded from the flash storage.\n            NOTE: in either case the wallet is only set in RAM, and flash storage is not affected.\n            Defaults to False.\n\n        Returns\n        -------\n        bool\n            True on success.\n        \"\"\"\n        params = {'mnemonic': mnemonic, 'passphrase': passphrase,\n                  'temporary_wallet': temporary_wallet}\n        return self._jadeRpc('debug_set_mnemonic', params)\n\n    def set_seed(self, seed):\n        \"\"\"\n        RPC call to set the wallet seed.\n        NOTE: Only available in a DEBUG build of the firmware.\n        NOTE: Setting a seed always sets a 'temporary' wallet.\n\n        Parameters\n        ----------\n        seed : bytes\n            The wallet seed to set as a temporary wallet (cannot be persisted in flash).\n\n        Returns\n        -------\n        bool\n            True on success.\n        \"\"\"\n        params = {'seed': seed}\n        return self._jadeRpc('debug_set_mnemonic', params)\n\n    def get_bip85_bip39_entropy(self, num_words, index, pubkey):\n        \"\"\"\n        RPC call to fetch encrypted bip85-bip39 entropy.\n        NOTE: Only available in a DEBUG build of the firmware.\n\n        Parameters\n        ----------\n        num_words : int\n            The number of words the entropy is required to produce.\n\n        index : int\n            The index to use in the bip32 path to calculate the entropy.\n\n        pubkey: 33-bytes\n            The host ephemeral pubkey to use to generate a shared ecdh secret to use as an AES key\n            to encrypt the returned entropy.\n\n        Returns\n        -------\n        dict\n            pubkey - 33-bytes, Jade's ephemeral pubkey used to generate a shared ecdh secret used as\n            an AES key to encrypt the returned entropy\n            encrypted - bytes, the requested bip85 bip39 entropy, AES encrypted with the first key\n            derived from the ecdh shared secret, prefixed with the iv\n            hmac - 32-bytes, the hmac of the encrypted buffer, using the second key derived from the\n            ecdh shared secret\n        \"\"\"\n        params = {'num_words': num_words,\n                  'index': index,\n                  'pubkey': pubkey}\n        return self._jadeRpc('get_bip85_bip39_entropy', params)\n\n    def set_pinserver(self, urlA=None, urlB=None, pubkey=None, cert=None):\n        \"\"\"\n        RPC call to explicitly set (override) the details of the blind pinserver used to\n        authenticate the PIN entered on the Jade unit.\n        This data is recorded in the hw flash, and returned to the caller when authenticating\n        (in auth_user(), below).\n\n        Parameters\n        ----------\n        urlA : str, optional\n            The primary url of the pinserver to use.\n\n        urlB : str, optional\n            Any secondary url of the pinserver to use.\n\n        pubkey : bytes, optional\n            The public key used to verify pinserver signed payloads.\n\n        cert : bytes, optional\n            Any additional certificate required to verify the pinserver identity.\n\n        Returns\n        -------\n        bool\n            True on success.\n        \"\"\"\n        params = {}\n        if urlA is not None or urlB is not None:\n            params['urlA'] = urlA\n            params['urlB'] = urlB\n        if pubkey is not None:\n            params['pubkey'] = pubkey\n        if cert is not None:\n            params['certificate'] = cert\n        return self._jadeRpc('update_pinserver', params)\n\n    def reset_pinserver(self, reset_details, reset_certificate):\n        \"\"\"\n        RPC call to reset any formerly overridden pinserver details to their defaults.\n\n        Parameters\n        ----------\n        reset_details : bool, optional\n            If set, any overridden urls and pubkey are reset to their defaults.\n\n        reset_certificate : bool, optional\n            If set, any additional certificate is reset (to None).\n\n        Returns\n        -------\n        bool\n            True on success.\n        \"\"\"\n        params = {'reset_details': reset_details,\n                  'reset_certificate': reset_certificate}\n        return self._jadeRpc('update_pinserver', params)\n\n    def auth_user(self, network, http_request_fn=None, epoch=None):\n        \"\"\"\n        RPC call to authenticate the user on the hw device, for using with the network provided.\n\n        Parameters\n        ----------\n        network : str\n            The name of the network intended for use - eg. 'mainnet', 'liquid', 'testnet' etc.\n            This is verified against the networks allowed on the hardware.\n\n        http_request_fn : function, optional\n            Optional http-request function to pass http requests to the Jade pinserver.\n            Default behaviour is to use the '_http_request()' function which defers to the\n            'requests' module.\n            If the 'reqests' module is not available, no default http-request function is created,\n            and one must be supplied here.\n\n        epoch : int, optional\n            Current epoch value, in seconds.  Defaults to int(time.time()) value.\n\n        Returns\n        -------\n        bool\n            True is returned immediately if the hw is already unlocked for use on the given network.\n            True if the PIN is entered and verified with the remote blind pinserver.\n            False if the PIN entered was incorrect.\n        \"\"\"\n        params = {'network': network, 'epoch': epoch if epoch is not None else int(time.time())}\n        return self._jadeRpc('auth_user', params,\n                             http_request_fn=http_request_fn,\n                             long_timeout=True)\n\n    def register_otp(self, otp_name, otp_uri):\n        \"\"\"\n        RPC call to register a new OTP record on the hw device.\n\n        Parameters\n        ----------\n        otp_name : str\n            An identifying name for this OTP record\n\n        otp_uri : str\n            The uri of this OTP record - must begin 'otpauth://'\n\n        Returns\n        -------\n        bool\n            True if the OTP uri was validated and persisted on the hw\n        \"\"\"\n        params = {'name': otp_name, 'uri': otp_uri}\n        return self._jadeRpc('register_otp', params)\n\n    def get_otp_code(self, otp_name, value_override=None):\n        \"\"\"\n        RPC call to fetch a new OTP code from the hw device.\n\n        Parameters\n        ----------\n        otp_name : str\n            An identifying name for the OTP record to use\n\n        value_override : int\n            An overriding HOTP counter or TOTP timestamp to use.\n            NOTE: Only available in a DEBUG build of the firmware.\n\n        Returns\n        -------\n        bool\n            True if the OTP uri was validated and persisted on the hw\n        \"\"\"\n        params = {'name': otp_name}\n        if value_override is not None:\n            params['override'] = value_override\n        return self._jadeRpc('get_otp_code', params)\n\n    def get_xpub(self, network, path):\n        \"\"\"\n        RPC call to fetch an xpub for the given bip32 path for the given network.\n\n        Parameters\n        ----------\n        network : str\n            Network to which the xpub applies - eg. 'mainnet', 'liquid', 'testnet', etc.\n\n        path : [int]\n            bip32 path for which the xpub should be generated.\n\n        Returns\n        -------\n        str\n            base58 encoded xpub\n        \"\"\"\n        params = {'network': network, 'path': path}\n        return self._jadeRpc('get_xpub', params)\n\n    def get_registered_multisigs(self):\n        \"\"\"\n        RPC call to fetch brief summaries of any multisig wallets registered to this signer.\n\n        Returns\n        -------\n        dict\n            Brief description of registered multisigs, keyed by registration name.\n            Each entry contains keys:\n                variant - str, script type, eg. 'sh(wsh(multi(k)))'\n                sorted - boolean, whether bip67 key sorting is applied\n                threshold - int, number of signers required,N\n                num_signers - total number of signatories, M\n                master_blinding_key - 32-bytes, any liquid master blinding key for this wallet\n        \"\"\"\n        return self._jadeRpc('get_registered_multisigs')\n\n    def get_registered_multisig(self, multisig_name, as_file=False):\n        \"\"\"\n        RPC call to fetch details of a named multisig wallet registered to this signer.\n        NOTE: the multisig wallet must have been registered with firmware v1.0.23 or later\n        for the full signer details to be persisted and available.\n\n        Parameters\n        ----------\n        multisig_name : string\n            Name of multsig registration record to return.\n\n        as_file : string, optional\n            If true the flat file format is returned, otherwise structured json is returned.\n            Defaults to false.\n\n        Returns\n        -------\n        dict\n            Description of registered multisig wallet identified by registration name.\n            Contains keys:\n                is_file is true:\n                    multisig_file - str, the multisig file as produced by several wallet apps.\n                    eg:\n                        Name: MainWallet\n                        Policy: 2 of 3\n                        Format: P2WSH\n                        Derivation: m/48'/0'/0'/2'\n\n                        B237FE9D: xpub6E8C7BX4c7qfTsX7urnXggcAyFuhDmYLQhwRwZGLD9maUGWPinuc9k96ej...\n                        249192D2: xpub6EbXynW6xjYR3crcztum6KzSWqDJoAJQoovwamwVnLaCSHA6syXKPnJo6U...\n                        67F90FFC: xpub6EHuWWrYd8bp5FS1XAZsMPkmCqLSjpULmygWqAqWRCCjSWQwz6ntq5KnuQ...\n\n                is_file is false:\n                    multisig_name - str, name of multisig registration\n                    variant - str, script type, eg. 'sh(wsh(multi(k)))'\n                    sorted - boolean, whether bip67 key sorting is applied\n                    threshold - int, number of signers required,N\n                    master_blinding_key - 32-bytes, any liquid master blinding key for this wallet\n                    signers - dict containing keys:\n                        fingerprint - 4 bytes, origin fingerprint\n                        derivation - [int], bip32 path from origin to signer xpub provided\n                        xpub - str, base58 xpub of signer\n                        path - [int], any fixed path to always apply after the xpub - usually empty.\n\n        \"\"\"\n        params = {'multisig_name': multisig_name,\n                  'as_file': as_file}\n        return self._jadeRpc('get_registered_multisig', params)\n\n    def register_multisig(self, network, multisig_name, variant, sorted_keys, threshold, signers,\n                          master_blinding_key=None):\n        \"\"\"\n        RPC call to register a new multisig wallet, which must contain the hw signer.\n        A registration name is provided - if it already exists that record is overwritten.\n\n        Parameters\n        ----------\n        network : string\n            Network to which the multisig should apply - eg. 'mainnet', 'liquid', 'testnet', etc.\n\n        multisig_name : string\n            Name to use to identify this multisig wallet registration record.\n            If a registration record exists with the name given, that record is overwritten.\n\n        variant : str\n            The script type - one of 'sh(multi(k))', 'wsh(multi(k))', 'sh(wsh(multi(k)))'\n\n        sorted_keys : bool\n            Whether this is a 'sortedmulti()' wallet - ie. whether to apply bip67 sorting to the\n            pubkeys when generating redeem scripts.\n\n        threshold : int\n            Number of signers required.\n\n        signers : [dict]\n            Description of signers - should include keys:\n                - 'fingerprint' - 4 bytes, origin fingerprint\n                - 'derivation' - [int], bip32 path from origin to signer xpub provided\n                - 'xpub' - str, base58 xpub of signer - will be verified for hw unit signer\n                - 'path' - [int], any fixed path to always apply after the xpub - usually empty.\n\n        master_blinding_key : 32-bytes, optional\n            The master blinding key to use for this multisig wallet on liquid.\n            Optional, defaults to None.\n            Logically mandatory when 'network' indicates a liquid network and the Jade is to be\n            used to generate confidential addresses, blinding keys, blinding nonces, asset blinding\n            factors or output commitments.\n\n        Returns\n        -------\n        bool\n            True on success, implying the mutisig wallet can now be used.\n        \"\"\"\n        params = {'network': network, 'multisig_name': multisig_name,\n                  'descriptor': {'variant': variant, 'sorted': sorted_keys,\n                                 'threshold': threshold, 'signers': signers,\n                                 'master_blinding_key': master_blinding_key}}\n        return self._jadeRpc('register_multisig', params)\n\n    def register_multisig_file(self, multisig_file):\n        \"\"\"\n        RPC call to register a new multisig wallet, which must contain the hw signer.\n        A registration file is provided - as produced my several wallet apps.\n\n        Parameters\n        ----------\n        multisig_file : string\n            The multisig file as produced by several wallet apps.\n            eg:\n                Name: MainWallet\n                Policy: 2 of 3\n                Format: P2WSH\n                Derivation: m/48'/0'/0'/2'\n\n                B237FE9D: xpub6E8C7BX4c7qfTsX7urnXggcAyFuhDmYLQhwRwZGLD9maUGWPinuc9k96ejhEQ1DCk...\n                249192D2: xpub6EbXynW6xjYR3crcztum6KzSWqDJoAJQoovwamwVnLaCSHA6syXKPnJo6U3bVeGde...\n                67F90FFC: xpub6EHuWWrYd8bp5FS1XAZsMPkmCqLSjpULmygWqAqWRCCjSWQwz6ntq5KnuQnL23No2...\n\n    Returns\n    -------\n    bool\n        True on success, implying the mutisig wallet can now be used.\n    \"\"\"\n        params = {'multisig_file': multisig_file}\n        return self._jadeRpc('register_multisig', params)\n\n    def get_registered_descriptors(self):\n        \"\"\"\n        RPC call to fetch brief summaries of any descriptor wallets registered to this signer.\n\n        Returns\n        -------\n        dict\n            Brief description of registered descriptor, keyed by registration name.\n            Each entry contains keys:\n                descriptor_len - int, length of descriptor output script\n                num_datavalues - int, total number of substitution placeholders passed with script\n                master_blinding_key - 32-bytes, any liquid master blinding key for this wallet\n        \"\"\"\n        return self._jadeRpc('get_registered_descriptors')\n\n    def get_registered_descriptor(self, descriptor_name):\n        \"\"\"\n        RPC call to fetch details of a named descriptor wallet registered to this signer.\n\n        Parameters\n        ----------\n        descriptor_name : string\n            Name of descriptor registration record to return.\n\n        Returns\n        -------\n        dict\n            Description of registered descriptor wallet identified by registration name.\n            Contains keys:\n                descriptor_name - str, name of descriptor registration\n                descriptor - str, descriptor output script, may contain substitution placeholders\n                datavalues - dict containing placeholders for substitution into script\n        \"\"\"\n        params = {'descriptor_name': descriptor_name}\n        return self._jadeRpc('get_registered_descriptor', params)\n\n    def register_descriptor(self, network, descriptor_name, descriptor_script, datavalues=None):\n        \"\"\"\n        RPC call to register a new descriptor wallet, which must contain the hw signer.\n        A registration name is provided - if it already exists that record is overwritten.\n\n        Parameters\n        ----------\n        network : string\n            Network to which the descriptor should apply - eg. 'mainnet', 'liquid', 'testnet', etc.\n\n        descriptor_name : string\n            Name to use to identify this descriptor wallet registration record.\n            If a registration record exists with the name given, that record is overwritten.\n\n        Returns\n        -------\n        bool\n            True on success, implying the descriptor wallet can now be used.\n        \"\"\"\n        params = {'network': network, 'descriptor_name': descriptor_name,\n                  'descriptor': descriptor_script, 'datavalues': datavalues}\n        return self._jadeRpc('register_descriptor', params)\n\n    def get_receive_address(self, *args, recovery_xpub=None, csv_blocks=0,\n                            variant=None, multisig_name=None, descriptor_name=None,\n                            confidential=None):\n        \"\"\"\n        RPC call to generate, show, and return an address for the given path.\n        The call has three forms.\n\n        Parameters\n        ----------\n        network: str\n            Network to which the address should apply - eg. 'mainnet', 'liquid', 'testnet', etc.\n\n        Then either:\n\n        1. Blockstream Green (multisig shield) addresses\n            subaccount : int\n                Blockstream Green subaccount\n\n            branch : int\n                Blockstream Green derivation branch\n\n            pointer : int\n                Blockstream Green address pointer\n\n            recovery_xpub : str, optional\n                xpub of recovery key for 2of3 subaccounts.  Otherwise should be omitted.\n                Defaults to None (ie. not a 2of3 subaccount).\n\n            csv_blocks : int, optional\n                Number of blocks to include in csv redeem script, if this is a csv-enabled account.\n                Otherwise should be omitted.\n                Defaults to 0 (ie. does not apply/not a csv-enabled account.)\n\n        2. Generic single-sig addresses\n            path: [int]\n                bip32 path for which the xpub should be generated.\n\n            variant: str\n                The script type - one of 'pkh(k)', 'wpkh(k)', 'sh(wpkh(k))'\n\n        3. Generic multisig addresses\n            paths: [[int]]\n                bip32 path suffixes, one for each signer, applied as a suffix to the registered\n                signer path. Usually these path suffixes will all be identical.\n\n            multisig_name : str\n                The name of the registered multisig wallet record used to generate the address.\n\n        4. Descriptor wallet addresses\n            branch : int\n                Multi-path derivation branch, usually 0.\n\n            pointer : int\n                Path index to descriptor\n\n            descriptor_name : str\n                The name of the registered descriptor wallet record used to generate the address.\n\n        Returns\n        -------\n        str\n            The address generated for the given parameters.\n\n        \"\"\"\n        if multisig_name is not None:\n            assert len(args) == 2\n            keys = ['network', 'paths', 'multisig_name']\n            args += (multisig_name,)\n        elif descriptor_name is not None:\n            assert len(args) == 3\n            keys = ['network', 'branch', 'pointer', 'descriptor_name']\n            args += (descriptor_name,)\n        elif variant is not None:\n            assert len(args) == 2\n            keys = ['network', 'path', 'variant']\n            args += (variant,)\n        else:\n            assert len(args) == 4\n            keys = ['network', 'subaccount', 'branch', 'pointer', 'recovery_xpub', 'csv_blocks']\n            args += (recovery_xpub, csv_blocks)\n\n        params = dict(zip(keys, args))\n        if confidential is not None:\n            params['confidential'] = confidential\n\n        return self._jadeRpc('get_receive_address', params)\n\n    def sign_message(self, path, message, use_ae_signatures=False,\n                     ae_host_commitment=None, ae_host_entropy=None):\n        \"\"\"\n        RPC call to format and sign the given message, using the given bip32 path.\n        Supports RFC6979 and anti-exfil signatures.\n\n        Parameters\n        ----------\n        path : [int]\n            bip32 path for which the signature should be generated.\n\n        message : str\n            Message string to format and sign.\n\n        ae_host_commitment : 32-bytes, optional\n            The host-commitment to use for Antil-Exfil signatures\n\n        ae_host_entropy : 32-bytes, optional\n            The host-entropy to use for Antil-Exfil signatures\n\n        Returns\n        -------\n        1. Legacy/RFC6979 signatures\n        str\n            base64-encoded signature\n\n        2. Anti-exfil signatures\n        (bytes, str)\n            signer-commitment, base64-encoded signature\n        \"\"\"\n        if use_ae_signatures:\n            # Anti-exfil protocol:\n            # We send the signing request and receive the signer-commitment in\n            # reply once the user confirms.\n            # We can then request the actual signature passing the ae-entropy.\n            params = {'path': path, 'message': message, 'ae_host_commitment': ae_host_commitment}\n            signer_commitment = self._jadeRpc('sign_message', params)\n            params = {'ae_host_entropy': ae_host_entropy}\n            signature = self._jadeRpc('get_signature', params)\n            return signer_commitment, signature\n        else:\n            # Standard EC signature, simple case\n            params = {'path': path, 'message': message}\n            return self._jadeRpc('sign_message', params)\n\n    def sign_message_file(self, message_file):\n        \"\"\"\n        RPC call to format and sign the given message, using the given bip32 path.\n        A message file is provided - as produced by eg. Specter wallet.\n        Supports RFC6979 only.\n\n        Parameters\n        ----------\n        message_file : str\n            Message file to parse and produce signature for.\n            eg:  'signmessage m/84h/0h/0h/0/0 ascii:this is a test message'\n\n        Returns\n        -------\n        str\n            base64-encoded RFC6979 signature\n        \"\"\"\n        params = {'message_file': message_file}\n        return self._jadeRpc('sign_message', params)\n\n    def get_identity_pubkey(self, identity, curve, key_type, index=0):\n        \"\"\"\n        RPC call to fetch a pubkey for the given identity (slip13/slip17).\n        NOTE: this api returns an uncompressed public key\n\n        Parameters\n        ----------\n        identity : str\n            Identity string to format and sign. For example ssh://satoshi@bitcoin.org\n\n        curve : str\n            Name of curve to use - currently only 'nist256p1' is supported\n\n        key_type : str\n            Key derivation type - must be either 'slip-0013' for an identity pubkey, or 'slip-0017'\n            for an ecdh pubkey.\n\n        index : int, optional\n            Index number (if require multiple keys/sigs per identity)\n            Defaults to 0\n\n        Returns\n        -------\n        65-bytes\n            Uncompressed public key for the given identity and index.\n            Consistent with 'sign_identity' or 'get_identity_shared_key', depending on the\n            'key_type'.\n\n        \"\"\"\n        params = {'identity': identity, 'curve': curve, 'type': key_type, 'index': index}\n        return self._jadeRpc('get_identity_pubkey', params)\n\n    def get_identity_shared_key(self, identity, curve, their_pubkey, index=0):\n        \"\"\"\n        RPC call to fetch a SLIP-0017 shared ecdh key for the identity and counterparty public key.\n        NOTE: this api takes an uncompressed public key\n\n        Parameters\n        ----------\n        identity : str\n            Identity string to format and sign. For example ssh://satoshi@bitcoin.org\n\n        curve : str\n            Name of curve to use - currently only 'nist256p1' is supported\n\n        their_pubkey : 65-bytes\n            The counterparty's uncompressed public key\n\n        index : int, optional\n            Index number (if require multiple keys/sigs per identity)\n            Defaults to 0\n\n        Returns\n        -------\n        32-bytes\n            The shared ecdh key for the given identity and cpty public key\n            Consistent with 'get_identity_pubkey' with 'key_type=slip-0017'\n        \"\"\"\n        params = {'identity': identity, 'curve': curve, 'index': index,\n                  'their_pubkey': their_pubkey}\n        return self._jadeRpc('get_identity_shared_key', params)\n\n    def sign_identity(self, identity, curve, challenge, index=0):\n        \"\"\"\n        RPC call to authenticate the given identity through a challenge.\n        Supports RFC6979.\n        Returns the signature and the associated SLIP-0013 pubkey\n        NOTE: this api returns an uncompressed public key\n\n        Parameters\n        ----------\n        identity : str\n            Identity string to format and sign. For example ssh://satoshi@bitcoin.org\n\n        curve : str\n            Name of curve to use - currently only 'nist256p1' is supported\n\n        challenge : bytes\n            Challenge bytes to sign\n\n        index : int, optional\n            Index number (if require multiple keys/sigs per identity)\n            Defaults to 0\n\n        Returns\n        -------\n        dict\n            Contains keys:\n            pubkey - 65-bytes, the uncompressed SLIP-0013 public key, consistent with\n            'get_identity_pubkey' with 'key_type=slip-0013'\n            signature - 65-bytes, RFC6979 deterministic signature, prefixed with 0x00\n        \"\"\"\n        params = {'identity': identity, 'curve': curve, 'index': index, 'challenge': challenge}\n        return self._jadeRpc('sign_identity', params)\n\n    def sign_attestation(self, challenge):\n        \"\"\"\n        RPC call to sign passed challenge with embedded hw RSA-4096 key, such that the caller\n        can check the authenticity of the hardware unit.  eg. whether it is a genuine\n        Blockstream production Jade unit.\n        Caller must have the public key of the external verifying authority they wish to validate\n        against (eg. Blockstream's Jade verification public key).\n        NOTE: only supported by ESP32S3-based hardware units.\n\n        Parameters\n        ----------\n        challenge : bytes\n            Challenge bytes to sign\n\n        Returns\n        -------\n        dict\n            Contains keys:\n            signature - 512-bytes, hardware RSA signature of the SHA256 hash of the passed\n                        challenge bytes.\n            pubkey_pem - str, PEM export of RSA pubkey of the hardware unit, to verify the returned\n            RSA signature.\n            ext_signature - bytes, RSA signature of the verifying authority over the returned\n            pubkey_pem data.\n            (Caller can verify this signature with the public key of the verifying authority.)\n        \"\"\"\n        params = {'challenge': challenge}\n        return self._jadeRpc('sign_attestation', params)\n\n    def get_master_blinding_key(self, only_if_silent=False):\n        \"\"\"\n        RPC call to fetch the master (SLIP-077) blinding key for the hw signer.\n        May block temporarily to request the user's permission to export.  Passing 'only_if_silent'\n        causes the call to return the 'denied' error if it would normally ask the user.\n        NOTE: the master blinding key of any registered multisig wallets can be obtained from\n        the result of `get_registered_multisigs()`.\n\n        Parameters\n        ----------\n        only_if_silent : boolean, optional\n            If True Jade will return the denied error if it would normally ask the user's permission\n            to export the master blinding key.  Passing False (or letting default) may block while\n            asking the user to confirm the export on Jade.\n\n        Returns\n        -------\n        32-bytes\n            SLIP-077 master blinding key\n        \"\"\"\n        params = {'only_if_silent': only_if_silent}\n        return self._jadeRpc('get_master_blinding_key', params)\n\n    def get_blinding_key(self, script, multisig_name=None):\n        \"\"\"\n        RPC call to fetch the public blinding key for the hw signer.\n\n        Parameters\n        ----------\n        script : bytes\n            The script for which the public blinding key is required.\n\n        multisig_name : str, optional\n            The name of any registered multisig wallet for which to fetch the blinding key.\n            Defaults to None\n\n        Returns\n        -------\n        33-bytes\n            Public blinding key for the passed script.\n        \"\"\"\n        params = {'script': script, 'multisig_name': multisig_name}\n        return self._jadeRpc('get_blinding_key', params)\n\n    def get_shared_nonce(self, script, their_pubkey, include_pubkey=False, multisig_name=None):\n        \"\"\"\n        RPC call to get the shared secret to unblind a tx, given the receiving script and\n        the pubkey of the sender (sometimes called \"blinding nonce\" in Liquid).\n        Optionally fetch the hw signer's public blinding key also.\n\n        Parameters\n        ----------\n        script : bytes\n            The script for which the blinding nonce is required.\n\n        their_pubkey : 33-bytes\n            The counterparty public key.\n\n        include_pubkey : bool, optional\n            Whether to also return the wallet's public blinding key.\n            Defaults to False.\n\n        multisig_name : str, optional\n            The name of any registered multisig wallet for which to fetch the blinding nonce.\n            Defaults to None\n\n        Returns\n        -------\n        1. include_pubkey is False\n        33-bytes\n            Public blinding nonce for the passed script and counterparty public key.\n\n        2. include_pubkey is True\n        dict\n            Contains keys:\n            shared_nonce - 32-bytes, public blinding nonce for the passed script as above.\n            blinding_key - 33-bytes, public blinding key for the passed script.\n        \"\"\"\n        params = {'script': script, 'their_pubkey': their_pubkey,\n                  'include_pubkey': include_pubkey, 'multisig_name': multisig_name}\n        return self._jadeRpc('get_shared_nonce', params)\n\n    def get_blinding_factor(self, hash_prevouts, output_index, bftype, multisig_name=None):\n        \"\"\"\n        RPC call to get deterministic blinding factors to blind an output.\n        Predicated on the host calculating the 'hash_prevouts' value correctly.\n        Can fetch abf, vbf, or both together.\n\n        Parameters\n        ----------\n\n        hash_prevouts : 32-bytes\n            This value should be computed by the host as specified in bip143.\n            It is not verified by Jade, since at this point Jade does not have the tx in question.\n\n        output_index : int\n            The index of the output we are trying to blind\n\n        bftype : str\n            Can be \"ASSET\", \"VALUE\", or \"ASSET_AND_VALUE\", to generate abf, vbf, or both.\n\n        multisig_name : str, optional\n            The name of any registered multisig wallet for which to fetch the blinding factor.\n            Defaults to None\n\n        Returns\n        -------\n        32-bytes or 64-bytes\n            The blinding factor for \"ASSET\" and \"VALUE\" requests, or both concatenated abf|vbf\n            ie. the first 32 bytes being abf, the second 32 bytes being vbf.\n        \"\"\"\n        params = {'hash_prevouts': hash_prevouts,\n                  'output_index': output_index,\n                  'type': bftype,\n                  'multisig_name': multisig_name}\n        return self._jadeRpc('get_blinding_factor', params)\n\n    def get_commitments(self,\n                        asset_id,\n                        value,\n                        hash_prevouts,\n                        output_index,\n                        vbf=None,\n                        multisig_name=None):\n        \"\"\"\n        RPC call to generate deterministic blinding factors and commitments for a given output.\n        Can optionally get a \"custom\" VBF, normally used for the last input where the vbf is not\n        computed here, but generated on the host according to all the other values.\n        The commitments generated here should be passed back into `sign_liquid_tx()`.\n\n        Parameters\n        ----------\n        asset_id : 32-bytes\n            asset_id as usually displayed - ie. reversed compared to network/consensus order\n\n        value : int\n            value in 'satoshi' or equivalent atomic integral unit\n\n        hash_prevouts : 32-bytes\n            This value is computed as specified in bip143.\n            It is verified immediately since at this point Jade doesn't have the tx in question.\n            It will be checked later during `sign_liquid_tx()`.\n\n        output_index : int\n            The index of the output we are trying to blind\n\n        vbf : 32-bytes, optional\n            The vbf to use, in preference to deterministically generating one in this call.\n\n        multisig_name : str, optional\n            The name of any registered multisig wallet for which to fetch the blinding factor.\n            Defaults to None\n\n        Returns\n        -------\n        dict\n            Containing the blinding factors and output commitments.\n        \"\"\"\n        params = {'asset_id': asset_id,\n                  'value': value,\n                  'hash_prevouts': hash_prevouts,\n                  'output_index': output_index,\n                  'vbf': vbf,\n                  'multisig_name': multisig_name}\n        return self._jadeRpc('get_commitments', params)\n\n    def _send_tx_inputs(self, base_id, inputs, use_ae_signatures):\n        \"\"\"\n        Helper call to send the tx inputs to Jade for signing.\n        Handles legacy RFC6979 signatures, as well as the Anti-Exfil protocol.\n\n        Parameters\n        ----------\n        base_id : int\n            The ids of the messages sent will be increments from this base id.\n\n        inputs : [dict]\n            The tx inputs - see `sign_tx()` / `sign_liquid_tx()` for details.\n\n        use_ae_signatures : bool\n            Whether to use the anti-exfil protocol to generate the signatures\n\n        Returns\n        -------\n        1. if use_ae_signatures is False\n        [bytes]\n            An array of signatures corresponding to the array of inputs passed.\n            The signatures are in DER format with the sighash appended.\n            'None' placeholder elements are used for inputs not requiring a signature.\n\n        2. if use_ae_signatures is True\n        [(32-bytes, bytes)]\n            An array of pairs of signer-commitments and signatures corresponding to the inputs.\n            The signatures are in DER format with the sighash appended.\n            (None, None) placeholder elements are used for inputs not requiring a signature.\n        \"\"\"\n        if use_ae_signatures:\n            # Anti-exfil protocol:\n            # We send one message per input (which includes host-commitment *but\n            # not* the host entropy) and receive the signer-commitment in reply.\n            # Once all n input messages are sent, we can request the actual signatures\n            # (as the user has a chance to confirm/cancel at this point).\n            # We request the signatures passing the ae-entropy for each one.\n            # Send inputs one at a time, receiving 'signer-commitment' in reply\n            signer_commitments = []\n            host_ae_entropy_values = []\n            for txinput in inputs:\n                # ae-protocol - do not send the host entropy immediately\n                txinput = txinput.copy() if txinput else {}  # shallow copy\n                host_ae_entropy_values.append(txinput.pop('ae_host_entropy', None))\n\n                base_id += 1\n                input_id = str(base_id)\n                reply = self._jadeRpc('tx_input', txinput, input_id)\n                signer_commitments.append(reply)\n\n            # Request the signatures one at a time, sending the entropy\n            signatures = []\n            for (i, host_ae_entropy) in enumerate(host_ae_entropy_values, 1):\n                base_id += 1\n                sig_id = str(base_id)\n                params = {'ae_host_entropy': host_ae_entropy}\n                reply = self._jadeRpc('get_signature', params, sig_id)\n                signatures.append(reply)\n\n            assert len(signatures) == len(inputs)\n            return list(zip(signer_commitments, signatures))\n        else:\n            # Legacy protocol:\n            # We send one message per input - without expecting replies.\n            # Once all n input messages are sent, the hw then sends all n replies\n            # (as the user has a chance to confirm/cancel at this point).\n            # Then receive all n replies for the n signatures.\n            # NOTE: *NOT* a sequence of n blocking rpc calls.\n            # NOTE: at some point this flow should be removed in favour of the one\n            # above, albeit without passing anti-exfil entropy or commitment data.\n\n            # Send all n inputs\n            requests = []\n            for txinput in inputs:\n                if txinput is None:\n                    txinput = {}\n\n                base_id += 1\n                msg_id = str(base_id)\n                request = self.jade.build_request(msg_id, 'tx_input', txinput)\n                self.jade.write_request(request)\n                requests.append(request)\n                time.sleep(0.1)\n\n            # Receive all n signatures\n            signatures = []\n            for request in requests:\n                reply = self.jade.read_response()\n                self.jade.validate_reply(request, reply)\n                signature = self._get_result_or_raise_error(reply)\n                signatures.append(signature)\n\n            assert len(signatures) == len(inputs)\n            return signatures\n\n    def sign_liquid_tx(self, network, txn, inputs, commitments, change, use_ae_signatures=False,\n                       asset_info=None, additional_info=None):\n        \"\"\"\n        RPC call to sign a liquid transaction.\n\n        Parameters\n        ----------\n        network : str\n            Network to which the txn should apply - eg. 'liquid', 'liquid-testnet', etc.\n\n        txn : bytes\n            The transaction to sign\n\n        inputs : [dict]\n            The tx inputs.\n                If signing this input, should contain keys:\n                is_witness, bool - whether this is a segwit input\n                script, bytes- the redeem script\n                path, [int] - the bip32 path to sign with\n                value_commitment, 33-bytes - The value commitment of the input\n\n                This is optional if signing this input:\n                sighash, int - The sighash to use, defaults to 0x01 (SIGHASH_ALL)\n\n                These are only required for Anti-Exfil signatures:\n                ae_host_commitment, 32-bytes - The host-commitment for Anti-Exfil signatures\n                ae_host_entropy, 32-bytes - The host-entropy for Anti-Exfil signatures\n\n                These are only required for advanced transactions, eg. swaps, and only when the\n                inputs need unblinding.\n                Not needed for vanilla send-payment/redeposit etc:\n                abf, 32-bytes - asset blinding factor\n                asset_id, 32-bytes - the unblinded asset-id\n                asset_generator, 33-bytes - the (blinded) asset-generator\n                vbf, 32-bytes - the value blinding factor\n                value, int - the unblinded sats value of the input\n\n                If not signing this input a null or an empty dict can be passed.\n\n        commitments : [dict]\n            An array sized for the number of outputs.\n            Unblinded outputs should have a 'null' placeholder element.\n            The commitments as retrieved from `get_commitments()`, with the addition of:\n                'blinding_key', <bytes> - the output's public blinding key\n                    (as retrieved from `get_blinding_key()`)\n\n        change : [dict]\n            An array sized for the number of outputs.\n            Outputs which are not to this wallet should have a 'null' placeholder element.\n            The output scripts for the elements with data will be verified by Jade.\n            Unless the element also contains 'is_change': False, these outputs will automatically\n            be approved and not be verified by the user.\n            Populated elements should contain sufficient data to generate the wallet address.\n            See `get_receive_address()`\n\n        use_ae_signatures : bool, optional\n            Whether to use the anti-exfil protocol to generate the signatures.\n            Defaults to False.\n\n        asset_info : [dict], optional\n            Any asset-registry data relevant to the assets being transacted, such that Jade can\n            display a meaningful name, issuer, ticker etc. rather than just asset-id.\n            At the very least must contain 'asset_id', 'contract' and 'issuance_prevout' items,\n            exactly as in the registry data.  NOTE: asset_info for the network policy-asset is\n            not required.\n            Defaults to None.\n\n        additional_info: dict, optional\n            Extra data about the transaction.  Only required for advanced transactions, eg. swaps.\n            Not needed for vanilla send-payment/redeposit etc:\n            tx_type, str: 'swap' indicates the tx represents an asset-swap proposal or transaction.\n            wallet_input_summary, dict:  a list of entries containing 'asset_id' (32-bytes) and\n            'satoshi' (int) showing net movement of assets out of the wallet (ie. sum of wallet\n            inputs per asset, minus any change outputs).\n            wallet_output_summary, dict:  a list of entries containing 'asset_id' (32-bytes) and\n            'satoshi' (int) showing net movement of assets into the wallet (ie. sum of wallet\n            outputs per asset, excluding any change outputs).\n\n        Returns\n        -------\n        1. if use_ae_signatures is False\n        [bytes]\n            An array of signatures corresponding to the array of inputs passed.\n            The signatures are in DER format with the sighash appended.\n            'None' placeholder elements are used for inputs not requiring a signature.\n\n        2. if use_ae_signatures is True\n        [(32-bytes, bytes)]\n            An array of pairs of signer-commitments and signatures corresponding to the inputs.\n            The signatures are in DER format with the sighash appended.\n            (None, None) placeholder elements are used for inputs not requiring a signature.\n        \"\"\"\n        # 1st message contains txn and number of inputs we are going to send.\n        # Reply ok if that corresponds to the expected number of inputs (n).\n        base_id = 100 * random.randint(1000, 9999)\n        params = {'network': network,\n                  'txn': txn,\n                  'num_inputs': len(inputs),\n                  'trusted_commitments': commitments,\n                  'use_ae_signatures': use_ae_signatures,\n                  'change': change,\n                  'asset_info': asset_info,\n                  'additional_info': additional_info}\n\n        reply = self._jadeRpc('sign_liquid_tx', params, str(base_id))\n        assert reply\n\n        # Send inputs and receive signatures\n        return self._send_tx_inputs(base_id, inputs, use_ae_signatures)\n\n    def sign_tx(self, network, txn, inputs, change, use_ae_signatures=False):\n        \"\"\"\n        RPC call to sign a btc transaction.\n\n        Parameters\n        ----------\n        network : str\n            Network to which the txn should apply - eg. 'mainnet', 'testnet', etc.\n\n        txn : bytes\n            The transaction to sign\n\n        inputs : [dict]\n            The tx inputs.   Should contain keys:\n                One of these is required:\n                input_tx, bytes - The prior transaction which created the utxo of this input\n                satoshi, int - The satoshi amount of this input - can be used in place of\n                    'input_tx' for a tx with a single segwit input\n\n                These are only required if signing this input:\n                is_witness, bool - whether this is a segwit input\n                script, bytes- the redeem script\n                path, [int] - the bip32 path to sign with\n\n                This is optional if signing this input:\n                sighash, int - The sighash to use, defaults to 0x01 (SIGHASH_ALL)\n\n                These are only required for Anti-Exfil signatures:\n                ae_host_commitment, 32-bytes - The host-commitment for Anti-Exfil signatures\n                ae_host_entropy, 32-bytes - The host-entropy for Anti-Exfil signatures\n\n        change : [dict]\n            An array sized for the number of outputs.\n            Outputs which are not to this wallet should have a 'null' placeholder element.\n            The output scripts for the elements with data will be verified by Jade.\n            Unless the element also contains 'is_change': False, these outputs will automatically\n            be approved and not be verified by the user.\n            Populated elements should contain sufficient data to generate the wallet address.\n            See `get_receive_address()`\n\n        use_ae_signatures : bool\n            Whether to use the anti-exfil protocol to generate the signatures\n\n        Returns\n        -------\n        1. if use_ae_signatures is False\n        [bytes]\n            An array of signatures corresponding to the array of inputs passed.\n            The signatures are in DER format with the sighash appended.\n            'None' placeholder elements are used for inputs not requiring a signature.\n\n        2. if use_ae_signatures is True\n        [(32-bytes, bytes)]\n            An array of pairs of signer-commitments and signatures corresponding to the inputs.\n            The signatures are in DER format with the sighash appended.\n            (None, None) placeholder elements are used for inputs not requiring a signature.\n        \"\"\"\n        # 1st message contains txn and number of inputs we are going to send.\n        # Reply ok if that corresponds to the expected number of inputs (n).\n        base_id = 100 * random.randint(1000, 9999)\n        params = {'network': network,\n                  'txn': txn,\n                  'num_inputs': len(inputs),\n                  'use_ae_signatures': use_ae_signatures,\n                  'change': change}\n\n        reply = self._jadeRpc('sign_tx', params, str(base_id))\n        assert reply\n\n        # Send inputs and receive signatures\n        return self._send_tx_inputs(base_id, inputs, use_ae_signatures)\n\n    def sign_psbt(self, network, psbt):\n        \"\"\"\n        RPC call to sign a passed psbt as required\n\n        Parameters\n        ----------\n        network : str\n            Network to which the txn should apply - eg. 'mainnet', 'testnet', etc.\n\n        psbt : bytes\n            The psbt formatted as bytes\n\n        Returns\n        -------\n        bytes\n            The psbt, updated with any signatures required from the hw signer\n        \"\"\"\n        # Send PSBT message\n        params = {'network': network, 'psbt': psbt}\n        msgid = str(random.randint(100000, 999999))\n        request = self.jade.build_request(msgid, 'sign_psbt', params)\n        self.jade.write_request(request)\n\n        # Read replies until we have them all, collate data and return.\n        # NOTE: we send 'get_extended_data' messages to request more 'chunks' of the reply data.\n        psbt_out = bytearray()\n        while True:\n            reply = self.jade.read_response()\n            self.jade.validate_reply(request, reply)\n            psbt_out.extend(self._get_result_or_raise_error(reply))\n\n            if 'seqnum' not in reply or reply['seqnum'] == reply['seqlen']:\n                break\n\n            newid = str(random.randint(100000, 999999))\n            params = {'origid': msgid,\n                      'orig': 'sign_psbt',\n                      'seqnum': reply['seqnum'] + 1,\n                      'seqlen': reply['seqlen']}\n            request = self.jade.build_request(newid, 'get_extended_data', params)\n            self.jade.write_request(request)\n\n        return psbt_out\n\n\nclass JadeInterface:\n    \"\"\"\n    Mid-level interface to Jade\n    Wraps either a serial or a ble connection\n    Calls to send and receive bytes and cbor messages over the interface.\n\n    Either:\n     a) use wrapped with JadeAPI\n    (recommended)\n    or:\n     b) use with JadeInterface.create_[serial|ble]() as jade:\n          ...\n    or:\n     c) use JadeInterface.create_[serial|ble], then call connect() before\n        using, and disconnect() when finished\n    (caveat cranium)\n    or:\n     d) use ctor to wrap existing low-level implementation instance\n    (caveat cranium)\n    \"\"\"\n\n    def __init__(self, impl):\n        assert impl is not None\n        self.impl = impl\n\n    def __enter__(self):\n        self.connect()\n        return self\n\n    def __exit__(self, exc_type, exc, tb):\n        if (exc_type):\n            logger.info(\"Exception causing JadeInterface context exit.\")\n            logger.info(exc_type)\n            logger.info(exc)\n            traceback.print_tb(tb)\n        self.disconnect(exc_type is not None)\n\n    @staticmethod\n    def create_serial(device=None, baud=None, timeout=None):\n        \"\"\"\n        Create a JadeInterface object using the serial interface described.\n\n        Parameters\n        ----------\n        device : str, optional\n            The device identifier for the serial device.\n            Underlying implementation will default (to /dev/ttyUSB0)\n\n        baud : int, optional\n            The communication baud rate.\n            Underlying implementation will default (to 115200)\n\n        timeout : int, optional\n            The serial read timeout when awaiting messages.\n            Underlying implementation will default (to 120s)\n\n        Returns\n        -------\n        JadeInterface\n            Interface object configured to use given serial parameters.\n            NOTE: the instance has not yet tried to contact the hw\n            - caller must call 'connect()' before trying to use the Jade.\n        \"\"\"\n        if device and JadeTCPImpl.isSupportedDevice(device):\n            impl = JadeTCPImpl(device, timeout or DEFAULT_SERIAL_TIMEOUT)\n        else:\n            impl = JadeSerialImpl(device,\n                                  baud or DEFAULT_BAUD_RATE,\n                                  timeout or DEFAULT_SERIAL_TIMEOUT)\n        return JadeInterface(impl)\n\n    @staticmethod\n    def create_ble(device_name=None, serial_number=None,\n                   scan_timeout=None, loop=None):\n        \"\"\"\n        Create a JadeInterface object using the BLE interface described.\n        NOTE: raises JadeError if BLE dependencies not installed.\n\n        Parameters\n        ----------\n        device_name : str, optional\n            The device name of the desired BLE device.\n            Underlying implementation will default (to 'Jade')\n\n        serial_number : int, optional\n            The serial number of the desired BLE device\n            - used to disambiguate multiple beacons with the same 'device name'\n            Underlying implementation will connect to the first beacon it scans\n            with the matching 'device name'.\n\n        scan_timeout : int, optional\n            The timeout when scanning for devices which match the device name/serial number.\n            Underlying implementation will default (to 60s)\n\n        loop : optional\n            The asynchio event loop to use, if required.\n            Underlying implementation will default (to asyncio.get_event_loop())\n\n        Returns\n        -------\n        JadeInterface\n            Interface object configured to use given BLE parameters.\n            NOTE: the instance has not yet tried to contact the hw\n            - caller must call 'connect()' before trying to use the Jade.\n\n        Raises\n        ------\n        JadeError if BLE backend not available (ie. BLE dependencies not installed)\n        \"\"\"\n        this_module = sys.modules[__name__]\n        if not hasattr(this_module, \"JadeBleImpl\"):\n            raise JadeError(1, \"BLE support not installed\", None)\n\n        impl = JadeBleImpl(device_name or DEFAULT_BLE_DEVICE_NAME,\n                           serial_number or DEFAULT_BLE_SERIAL_NUMBER,\n                           scan_timeout or DEFAULT_BLE_SCAN_TIMEOUT,\n                           loop=loop)\n        return JadeInterface(impl)\n\n    def connect(self):\n        \"\"\"\n        Try to connect the underlying transport interface (eg. serial, ble, etc.)\n        Raises an exception on failure.\n        \"\"\"\n        self.impl.connect()\n\n    def disconnect(self, drain=False):\n        \"\"\"\n        Disconnect the underlying transport (eg. serial, ble, etc.)\n\n        Parameters\n        ----------\n        drain : bool, optional\n            When true log any/all remaining messages/data, otherwise silently discard.\n            NOTE: can prevent disconnection if data is arriving constantly.\n            Defaults to False.\n        \"\"\"\n        if drain:\n            self.drain()\n\n        self.impl.disconnect()\n\n    def drain(self):\n        \"\"\"\n        Log any/all outstanding messages/data.\n        NOTE: can run indefinitely if data is arriving constantly.\n        \"\"\"\n        logger.warning(\"Draining interface...\")\n        drained = bytearray()\n        finished = False\n\n        while not finished:\n            byte_ = self.impl.read(1)\n            drained.extend(byte_)\n            finished = byte_ == b''\n\n            if finished or byte_ == b'\\n' or len(drained) > 256:\n                try:\n                    device_logger.warning(drained.decode('utf-8'))\n                except Exception as e:\n                    # Dump the bytes raw and as hex if decoding as utf-8 failed\n                    device_logger.warning(\"Raw:\")\n                    device_logger.warning(drained)\n                    device_logger.warning(\"----\")\n                    device_logger.warning(\"Hex dump:\")\n                    device_logger.warning(drained.hex())\n\n                # Clear and loop to continue collecting\n                drained.clear()\n\n    @staticmethod\n    def build_request(input_id, method, params=None):\n        \"\"\"\n        Build a request dict from passed parameters\n\n        Parameters\n        ----------\n        input_id : str\n            The id of the request message to construct\n\n        method : str\n            rpc method to invoke\n\n        params : dict, optional\n            any parameters to pass to the rpc method\n            Defaults to None.\n\n        Returns\n        -------\n        dict\n            The request object as a dict\n        \"\"\"\n        request = {\"method\": method, \"id\": input_id}\n        if params is not None:\n            request[\"params\"] = params\n        return request\n\n    @staticmethod\n    def serialise_cbor_request(request):\n        \"\"\"\n        Method to format a request dict as a cbor message\n\n        Parameters\n        ----------\n        request : dict\n            The request dict\n\n        Returns\n        -------\n        bytes\n            The request formatted as cbor message bytes\n        \"\"\"\n        dump = cbor.dumps(request)\n        len_dump = len(dump)\n        if 'method' in request and 'ota_data' in request['method']:\n            msg = 'Sending ota_data message {} as cbor of size {}'.format(request['id'], len_dump)\n            logger.info(msg)\n        else:\n            logger.info('Sending: {} as cbor of size {}'.format(_hexlify(request), len_dump))\n        return dump\n\n    def write(self, bytes_):\n        \"\"\"\n        Write bytes over the underlying interface\n\n        Parameters\n        ----------\n        bytes_ : bytes\n            The bytes to write\n\n        Returns\n        -------\n        int\n            The number of bytes written\n        \"\"\"\n        logger.debug(\"Sending: {} bytes\".format(len(bytes_)))\n        wrote = self.impl.write(bytes_)\n        logger.debug(\"Sent: {} bytes\".format(len(bytes_)))\n        return wrote\n\n    def write_request(self, request):\n        \"\"\"\n        Write a request dict over the underlying interface, formatted as cbor.\n\n        Parameters\n        ----------\n        request : dict\n            The request dict to write\n        \"\"\"\n        msg = self.serialise_cbor_request(request)\n        written = 0\n        while written < len(msg):\n            written += self.write(msg[written:])\n\n    def read(self, n):\n        \"\"\"\n        Try to read bytes from the underlying interface.\n\n        Returns\n        -------\n        bytes\n            The bytes received\n        \"\"\"\n        logger.debug(\"Reading {} bytes...\".format(n))\n        bytes_ = self.impl.read(n)\n        logger.debug(\"Received: {} bytes\".format(len(bytes_)))\n        return bytes_\n\n    def read_cbor_message(self):\n        \"\"\"\n        Try to read a single cbor (response) message from the underlying interface.\n        Respects the any read timeout.\n        If any 'log' messages are received, logs them locally at the nearest corresponding level\n        and awaits the next message.  Returns when it receives what appears to be a reply message.\n\n        Returns\n        -------\n        dict\n            The message received, as a dict\n        \"\"\"\n        while True:\n            # 'self' is sufficiently 'file-like' to act as a load source.\n            # Throws EOFError on end of stream/timeout/lost-connection etc.\n            message = cbor.load(self)\n\n            if isinstance(message, collections.abc.Mapping):\n                # A message response (to a prior request)\n                if 'id' in message:\n                    logger.info(\"Received msg: {}\".format(_hexlify(message)))\n                    return message\n\n                # A log message - handle as normal\n                if 'log' in message:\n                    response = message['log']\n                    log_method = device_logger.error\n                    try:\n                        response = message['log'].decode(\"utf-8\")\n                        log_methods = {\n                            'E': device_logger.error,\n                            'W': device_logger.warning,\n                            'I': device_logger.info,\n                            'D': device_logger.debug,\n                            'V': device_logger.debug,\n                        }\n                        if len(response) > 1 and response[1] == ' ':\n                            lvl = response[0]\n                            log_method = log_methods.get(lvl, device_logger.error)\n                    except Exception as e:\n                        logger.error('Error processing log message: {}'.format(e))\n                    log_method('>> {}'.format(response))\n                    continue\n\n            # Unknown/unhandled/unexpected message\n            logger.error(\"Unhandled message received\")\n            device_logger.error(message)\n\n    def read_response(self, long_timeout=False):\n        \"\"\"\n        Try to read a single cbor (response) message from the underlying interface.\n        If any 'log' messages are received, logs them locally at the nearest corresponding level\n        and awaits the next message.  Returns when it receives what appears to be a reply message.\n        If `long_timeout` is false, any read-timeout is respected.  If True, the call will block\n        indefinitely awaiting a response message.\n\n        Parameters\n        ----------\n        long_timeout : bool\n            Whether to wait indefinitely for the next (response) message.\n\n        Returns\n        -------\n        dict\n            The message received, as a dict\n        \"\"\"\n        while True:\n            try:\n                return self.read_cbor_message()\n            except EOFError as e:\n                if not long_timeout:\n                    raise\n\n    @staticmethod\n    def validate_reply(request, reply):\n        \"\"\"\n        Helper to minimally validate a reply message, in the context of a request.\n        Asserts if the reply does contain the expected minimal fields.\n        \"\"\"\n        assert isinstance(reply, dict) and 'id' in reply\n        assert ('result' in reply) != ('error' in reply)\n        assert reply['id'] == request['id'] or \\\n            reply['id'] == '00' and 'error' in reply\n\n    def make_rpc_call(self, request, long_timeout=False):\n        \"\"\"\n        Method to send a request over the underlying interface, and await a response.\n        The request is minimally validated before it is sent, and the response is similarly\n        validated before being returned.\n        Any read-timeout is respected unless 'long_timeout' is passed, in which case the call\n        blocks indefinitely awaiting a response.\n\n        Parameters\n        ----------\n        long_timeout : bool\n            Whether to wait indefinitely for the response.\n\n        Returns\n        -------\n        dict\n            The (minimally validated) response message received, as a dict\n        \"\"\"\n        # Write outgoing request message\n        assert isinstance(request, dict)\n        assert 'id' in request and len(request['id']) > 0\n        assert 'method' in request and len(request['method']) > 0\n        assert len(request['id']) < 16 and len(request['method']) < 32\n        self.write_request(request)\n\n        # Read and validate incoming message\n        reply = self.read_response(long_timeout)\n        self.validate_reply(request, reply)\n\n        return reply\n"
  },
  {
    "path": "electrum/plugins/jade/jadepy/jade_error.py",
    "content": "class JadeError(Exception):\n    # RPC error codes\n    INVALID_REQUEST = -32600\n    UNKNOWN_METHOD = -32601\n    BAD_PARAMETERS = -32602\n    INTERNAL_ERROR = -32603\n\n    # Implementation specific error codes: -32000 to -32099\n    USER_CANCELLED = -32000\n    PROTOCOL_ERROR = -32001\n    HW_LOCKED = -32002\n    NETWORK_MISMATCH = -32003\n\n    def __init__(self, code, message, data):\n        self.code = code\n        self.message = message\n        self.data = data\n\n    def __repr__(self):\n        return \"JadeError: \" + str(self.code) + \" - \" + self.message \\\n            + \" (Data: \" + repr(self.data) + \")\"\n\n    def __str__(self):\n        return repr(self)\n"
  },
  {
    "path": "electrum/plugins/jade/jadepy/jade_serial.py",
    "content": "import serial\nimport logging\n\nfrom serial.tools import list_ports\nfrom .jade_error import JadeError\n\nlogger = logging.getLogger(__name__)\n\n\n#\n# Low-level Serial backend interface to Jade\n# Calls to send and receive bytes over the interface.\n# Intended for use via JadeInterface wrapper.\n#\n# Either:\n#  a) use via JadeInterface.create_serial() (see JadeInterface)\n# (recommended)\n# or:\n#  b) use JadeSerialImpl() directly, and call connect() before\n#     using, and disconnect() when finished,\n# (caveat cranium)\n#\nclass JadeSerialImpl:\n    # Used when searching for devices that might be a Jade/compatible hw\n    JADE_DEVICE_IDS = [\n            (0x10c4, 0xea60), (0x1a86, 0x55d4), (0x0403, 0x6001),\n            (0x1a86, 0x7523), (0x303a, 0x4001), (0x303a, 0x1001)]\n\n    @classmethod\n    def _get_first_compatible_device(cls):\n        jades = []\n        for devinfo in list_ports.comports():\n            if (devinfo.vid, devinfo.pid) in cls.JADE_DEVICE_IDS:\n                jades.append(devinfo.device)\n\n        if len(jades) > 1:\n            logger.warning(f'Multiple potential jade devices detected: {jades}')\n\n        return jades[0] if jades else None\n\n    def __init__(self, device, baud, timeout):\n        self.device = device or self._get_first_compatible_device()\n        self.baud = baud\n        self.timeout = timeout\n        self.ser = None\n\n    def connect(self):\n        assert self.ser is None\n\n        logger.info('Connecting to {} at {}'.format(self.device, self.baud))\n        self.ser = serial.Serial(self.device, self.baud,\n                                 timeout=self.timeout,\n                                 write_timeout=self.timeout)\n        assert self.ser is not None\n\n        if not self.ser.is_open:\n            try:\n                self.ser.open()\n            except serial.serialutil.SerialException:\n                raise JadeError(1, \"Unable to open port\", self.device)\n\n        # Ensure RTS and DTR are not set (as this can cause the hw to reboot)\n        self.ser.setRTS(False)\n        self.ser.setDTR(False)\n\n        logger.info('Connected')\n\n    def disconnect(self):\n        assert self.ser is not None\n\n        # Ensure RTS and DTR are not set (as this can cause the hw to reboot)\n        # and then close the connection\n        self.ser.setRTS(False)\n        self.ser.setDTR(False)\n        self.ser.close()\n\n        # Reset state\n        self.ser = None\n\n    def write(self, bytes_):\n        assert self.ser is not None\n        return self.ser.write(bytes_)\n\n    def read(self, n):\n        assert self.ser is not None\n        return self.ser.read(n)\n"
  },
  {
    "path": "electrum/plugins/jade/jadepy/jade_tcp.py",
    "content": "import socket\nimport logging\n\n\nlogger = logging.getLogger(__name__)\n\n\n#\n# Low-level Serial-via-TCP backend interface to Jade\n# Calls to send and receive bytes over the interface.\n# Intended for use via JadeInterface wrapper.\n#\n# Either:\n#  a) use via JadeInterface.create_serial() (see JadeInterface)\n# (recommended)\n# or:\n#  b) use JadeTCPImpl() directly, and call connect() before\n#     using, and disconnect() when finished,\n# (caveat cranium)\n#\nclass JadeTCPImpl:\n    PROTOCOL_PREFIX = 'tcp:'\n\n    @classmethod\n    def isSupportedDevice(cls, device):\n        return device is not None and device.startswith(cls.PROTOCOL_PREFIX)\n\n    def __init__(self, device, timeout):\n        assert self.isSupportedDevice(device)\n        self.device = device\n        self.timeout = timeout\n        self.tcp_sock = None\n\n    def connect(self):\n        assert self.isSupportedDevice(self.device)\n        assert self.tcp_sock is None\n\n        logger.info('Connecting to {}'.format(self.device))\n        self.tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n        self.tcp_sock.settimeout(self.timeout)\n\n        url = self.device[len(self.PROTOCOL_PREFIX):].split(':')\n        self.tcp_sock.connect((url[0], int(url[1])))\n        assert self.tcp_sock is not None\n\n        self.tcp_sock.__enter__()\n        logger.info('Connected')\n\n    def disconnect(self):\n        assert self.tcp_sock is not None\n        self.tcp_sock.__exit__()\n\n        # Reset state\n        self.tcp_sock = None\n\n    def write(self, bytes_):\n        assert self.tcp_sock is not None\n        return self.tcp_sock.send(bytes_)\n\n    def read(self, n):\n        assert self.tcp_sock is not None\n        buf = self.tcp_sock.recv(n)\n        while len(buf) < n:\n            buf += self.tcp_sock.recv(n - len(buf))\n        return buf\n"
  },
  {
    "path": "electrum/plugins/jade/manifest.json",
    "content": "{\n  \"name\": \"jade\",\n  \"fullname\": \"Blockstream Jade Wallet\",\n  \"description\": \"Provides support for the Blockstream Jade hardware wallet\",\n  \"registers_keystore\": [\"hardware\", \"jade\", \"Jade wallet\"],\n  \"icon\":\"jade.png\",\n  \"available_for\": [\"qt\", \"cmdline\"]\n}\n"
  },
  {
    "path": "electrum/plugins/jade/qt.py",
    "content": "from functools import partial\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import pyqtSignal\n\nfrom electrum.i18n import _\nfrom electrum.plugin import hook\nfrom electrum.wallet import Standard_Wallet\n\nfrom electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase\nfrom electrum.hw_wallet import plugin\nfrom electrum.hw_wallet.plugin import only_hook_if_libraries_available\nfrom electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWXPub, WCHWUninitialized\n\nfrom .jade import JadePlugin\n\nif TYPE_CHECKING:\n    from electrum.gui.qt.wizard.wallet import QENewWalletWizard\n\n\nclass Plugin(JadePlugin, QtPluginBase):\n    icon_unpaired = \"jade_unpaired.png\"\n    icon_paired = \"jade.png\"\n\n    def create_handler(self, window):\n        return Jade_Handler(window)\n\n    @only_hook_if_libraries_available\n    @hook\n    def receive_menu(self, menu, addrs, wallet):\n        if len(addrs) != 1:\n            return\n        if type(wallet) is not Standard_Wallet:\n            return\n        self._add_menu_action(menu, addrs[0], wallet)\n\n    @only_hook_if_libraries_available\n    @hook\n    def transaction_dialog_address_menu(self, menu, addr, wallet):\n        if type(wallet) is not Standard_Wallet:\n            return\n        self._add_menu_action(menu, addr, wallet)\n\n    # insert jade pages in new wallet wizard\n    def extend_wizard(self, wizard: 'QENewWalletWizard'):\n        super().extend_wizard(wizard)\n        views = {\n            'jade_start': {'gui': WCScriptAndDerivation},\n            'jade_xpub': {'gui': WCHWXPub},\n            'jade_not_initialized': {'gui': WCHWUninitialized},\n            'jade_unlock': {'gui': WCHWUnlock}\n        }\n        wizard.navmap_merge(views)\n\n\nclass Jade_Handler(QtHandlerBase):\n    setup_signal = pyqtSignal()\n    auth_signal = pyqtSignal(object, object)\n\n    MESSAGE_DIALOG_TITLE = _(\"Jade Status\")\n\n    def __init__(self, win):\n        super(Jade_Handler, self).__init__(win, 'Jade')\n\n"
  },
  {
    "path": "electrum/plugins/keepkey/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/plugins/keepkey/client.py",
    "content": "from .keepkeylib.keepkeylib.client import proto, BaseClient, ProtocolMixin\nfrom .clientbase import KeepKeyClientBase\n\nclass KeepKeyClient(KeepKeyClientBase, ProtocolMixin, BaseClient):\n    def __init__(self, transport, handler, plugin):\n        BaseClient.__init__(self, transport)\n        ProtocolMixin.__init__(self, transport)\n        KeepKeyClientBase.__init__(self, handler, plugin, proto)\n\n    def recovery_device(self, *args):\n        ProtocolMixin.recovery_device(self, False, *args)\n\n\nKeepKeyClientBase.wrap_methods(KeepKeyClient)\n"
  },
  {
    "path": "electrum/plugins/keepkey/clientbase.py",
    "content": "import time\nfrom struct import pack\nfrom typing import Optional\n\nimport electrum_ecc as ecc\n\nfrom electrum.i18n import _\nfrom electrum.util import UserCancelled\nfrom electrum.keystore import bip39_normalize_passphrase\nfrom electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath\nfrom electrum.logging import Logger\nfrom electrum.plugin import runs_in_hwd_thread\nfrom electrum.hw_wallet.plugin import HardwareClientBase, HardwareHandlerBase\n\n\nclass GuiMixin(object):\n    # Requires: self.proto, self.device\n    handler: Optional[HardwareHandlerBase]\n\n    messages = {\n        3: _(\"Confirm the transaction output on your {} device\"),\n        4: _(\"Confirm internal entropy on your {} device to begin\"),\n        5: _(\"Write down the seed word shown on your {}\"),\n        6: _(\"Confirm on your {} that you want to wipe it clean\"),\n        7: _(\"Confirm on your {} device the message to sign\"),\n        8: _(\"Confirm the total amount spent and the transaction fee on your \"\n             \"{} device\"),\n        10: _(\"Confirm wallet address on your {} device\"),\n        'default': _(\"Check your {} device to continue\"),\n    }\n\n    def callback_Failure(self, msg):\n        # BaseClient's unfortunate call() implementation forces us to\n        # raise exceptions on failure in order to unwind the stack.\n        # However, making the user acknowledge they cancelled\n        # gets old very quickly, so we suppress those.  The NotInitialized\n        # one is misnamed and indicates a passphrase request was cancelled.\n        if msg.code in (self.types.Failure_PinCancelled,\n                        self.types.Failure_ActionCancelled,\n                        self.types.Failure_NotInitialized):\n            raise UserCancelled()\n        raise RuntimeError(msg.message)\n\n    def callback_ButtonRequest(self, msg):\n        message = self.msg\n        if not message:\n            message = self.messages.get(msg.code, self.messages['default'])\n        self.handler.show_message(message.format(self.device), self.cancel)\n        return self.proto.ButtonAck()\n\n    def callback_PinMatrixRequest(self, msg):\n        show_strength = True\n        if msg.type == 2:\n            msg = _(\"Enter a new PIN for your {}:\")\n        elif msg.type == 3:\n            msg = (_(\"Re-enter the new PIN for your {}.\\n\\n\"\n                     \"NOTE: the positions of the numbers have changed!\"))\n        else:\n            msg = _(\"Enter your current {} PIN:\")\n            show_strength = False\n        pin = self.handler.get_pin(msg.format(self.device), show_strength=show_strength)\n        if len(pin) > 9:\n            self.handler.show_error(_('The PIN cannot be longer than 9 characters.'))\n            pin = ''  # to cancel below\n        if not pin:\n            return self.proto.Cancel()\n        return self.proto.PinMatrixAck(pin=pin)\n\n    def callback_PassphraseRequest(self, req):\n        if self.creating_wallet:\n            msg = _(\"Enter a passphrase to generate this wallet.  Each time \"\n                    \"you use this wallet your {} will prompt you for the \"\n                    \"passphrase.  If you forget the passphrase you cannot \"\n                    \"access the bitcoins in the wallet.\").format(self.device)\n        else:\n            msg = _(\"Enter the passphrase to unlock this wallet:\")\n        passphrase = self.handler.get_passphrase(msg, self.creating_wallet)\n        if passphrase is None:\n            return self.proto.Cancel()\n        passphrase = bip39_normalize_passphrase(passphrase)\n\n        ack = self.proto.PassphraseAck(passphrase=passphrase)\n        length = len(ack.passphrase)\n        if length > 50:\n            self.handler.show_error(_(\"Too long passphrase ({} > 50 chars).\").format(length))\n            return self.proto.Cancel()\n        return ack\n\n    def callback_WordRequest(self, msg):\n        self.step += 1\n        msg = _(\"Step {}/24.  Enter seed word as explained on \"\n                \"your {}:\").format(self.step, self.device)\n        word = self.handler.get_word(msg)\n        # Unfortunately the device can't handle self.proto.Cancel()\n        return self.proto.WordAck(word=word)\n\n    def callback_CharacterRequest(self, msg):\n        char_info = self.handler.get_char(msg)\n        if not char_info:\n            return self.proto.Cancel()\n        return self.proto.CharacterAck(**char_info)\n\n\nclass KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):\n\n    def __init__(self, handler, plugin, proto):\n        assert hasattr(self, 'tx_api')  # ProtocolMixin already constructed?\n        HardwareClientBase.__init__(self, plugin=plugin)\n        self.proto = proto\n        self.device = plugin.device\n        self.handler = handler\n        self.tx_api = plugin\n        self.types = plugin.types\n        self.msg = None\n        self.creating_wallet = False\n        Logger.__init__(self)\n        self.used()\n\n    def __str__(self):\n        return \"%s/%s\" % (self.label(), self.features.device_id)\n\n    def label(self):\n        return self.features.label\n\n    def get_soft_device_id(self):\n        return self.features.device_id\n\n    def is_initialized(self):\n        return self.features.initialized\n\n    def is_pairable(self):\n        return not self.features.bootloader_mode\n\n    @runs_in_hwd_thread\n    def has_usable_connection_with_device(self):\n        try:\n            res = self.ping(\"electrum pinging device\")\n            assert res == \"electrum pinging device\"\n        except BaseException:\n            return False\n        return True\n\n    def used(self):\n        self.last_operation = time.time()\n\n    def prevent_timeouts(self):\n        self.last_operation = float('inf')\n\n    @runs_in_hwd_thread\n    def timeout(self, cutoff):\n        '''Time out the client if the last operation was before cutoff.'''\n        if self.last_operation < cutoff:\n            self.logger.info(\"timed out\")\n            self.clear_session()\n\n    @staticmethod\n    def expand_path(n):\n        return convert_bip32_strpath_to_intpath(n)\n\n    @runs_in_hwd_thread\n    def cancel(self):\n        '''Provided here as in keepkeylib but not trezorlib.'''\n        self.transport.write(self.proto.Cancel())\n\n    def i4b(self, x):\n        if x < 0:\n            # hack. workaround for https://github.com/spesmilo/electrum/issues/7779\n            x += 2 ** 32\n        return pack('>I', x)\n\n    @runs_in_hwd_thread\n    def get_xpub(self, bip32_path, xtype):\n        address_n = self.expand_path(bip32_path)\n        creating = False\n        node = self.get_public_node(address_n, creating).node\n        return BIP32Node(xtype=xtype,\n                         eckey=ecc.ECPubkey(node.public_key),\n                         chaincode=node.chain_code,\n                         depth=node.depth,\n                         fingerprint=self.i4b(node.fingerprint),\n                         child_number=self.i4b(node.child_num)).to_xpub()\n\n    @runs_in_hwd_thread\n    def toggle_passphrase(self):\n        if self.features.passphrase_protection:\n            self.msg = _(\"Confirm on your {} device to disable passphrases\")\n        else:\n            self.msg = _(\"Confirm on your {} device to enable passphrases\")\n        enabled = not self.features.passphrase_protection\n        self.apply_settings(use_passphrase=enabled)\n\n    @runs_in_hwd_thread\n    def change_label(self, label):\n        self.msg = _(\"Confirm the new label on your {} device\")\n        self.apply_settings(label=label)\n\n    @runs_in_hwd_thread\n    def change_homescreen(self, homescreen):\n        self.msg = _(\"Confirm on your {} device to change your home screen\")\n        self.apply_settings(homescreen=homescreen)\n\n    @runs_in_hwd_thread\n    def set_pin(self, remove):\n        if remove:\n            self.msg = _(\"Confirm on your {} device to disable PIN protection\")\n        elif self.features.pin_protection:\n            self.msg = _(\"Confirm on your {} device to change your PIN\")\n        else:\n            self.msg = _(\"Confirm on your {} device to set a PIN\")\n        self.change_pin(remove)\n\n    @runs_in_hwd_thread\n    def clear_session(self):\n        '''Clear the session to force pin (and passphrase if enabled)\n        re-entry.  Does not leak exceptions.'''\n        self.logger.info(f\"clear session: {self}\")\n        self.prevent_timeouts()\n        try:\n            super(KeepKeyClientBase, self).clear_session()\n        except BaseException as e:\n            # If the device was removed it has the same effect...\n            self.logger.info(f\"clear_session: ignoring error {e}\")\n\n    @runs_in_hwd_thread\n    def get_public_node(self, address_n, creating):\n        self.creating_wallet = creating\n        return super(KeepKeyClientBase, self).get_public_node(address_n)\n\n    @runs_in_hwd_thread\n    def close(self):\n        '''Called when Our wallet was closed or the device removed.'''\n        self.logger.info(\"closing client\")\n        self.clear_session()\n        # Release the device\n        self.transport.close()\n\n    def firmware_version(self):\n        f = self.features\n        return (f.major_version, f.minor_version, f.patch_version)\n\n    def atleast_version(self, major, minor=0, patch=0):\n        return self.firmware_version() >= (major, minor, patch)\n\n    @staticmethod\n    def wrapper(func):\n        '''Wrap methods to clear any message box they opened.'''\n\n        def wrapped(self, *args, **kwargs):\n            try:\n                self.prevent_timeouts()\n                return func(self, *args, **kwargs)\n            finally:\n                self.used()\n                self.handler.finished()\n                self.creating_wallet = False\n                self.msg = None\n\n        return wrapped\n\n    @staticmethod\n    def wrap_methods(cls):\n        for method in ['apply_settings', 'change_pin',\n                       'get_address', 'get_public_node',\n                       'load_device_by_mnemonic', 'load_device_by_xprv',\n                       'recovery_device', 'reset_device', 'sign_message',\n                       'sign_tx', 'wipe_device']:\n            setattr(cls, method, cls.wrapper(getattr(cls, method)))\n"
  },
  {
    "path": "electrum/plugins/keepkey/cmdline.py",
    "content": "from electrum.plugin import hook\nfrom electrum.hw_wallet import CmdLineHandler\nfrom .keepkey import KeepKeyPlugin\n\nclass Plugin(KeepKeyPlugin):\n    handler = CmdLineHandler()\n    @hook\n    def init_keystore(self, keystore):\n        if not isinstance(keystore, self.keystore_class):\n            return\n        keystore.handler = self.handler\n\n    def create_handler(self, window):\n        return self.handler\n"
  },
  {
    "path": "electrum/plugins/keepkey/keepkey.py",
    "content": "from typing import Optional, TYPE_CHECKING, Sequence\n\nfrom electrum.util import UserFacingException\nfrom electrum.bip32 import BIP32Node\nfrom electrum import descriptor\nfrom electrum import constants\nfrom electrum.i18n import _\nfrom electrum.transaction import Transaction, PartialTransaction, PartialTxInput, Sighash\nfrom electrum.keystore import Hardware_KeyStore\nfrom electrum.plugin import Device, runs_in_hwd_thread\n\nfrom electrum.hw_wallet import HW_PluginBase\nfrom electrum.hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data\n\nif TYPE_CHECKING:\n    import usb1\n    from .client import KeepKeyClient\n    from electrum.plugin import DeviceInfo\n    from electrum.wizard import NewWalletWizard\n\n\n# TREZOR initialization methods\nTIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)\n\n\nclass KeepKey_KeyStore(Hardware_KeyStore):\n    hw_type = 'keepkey'\n    device = 'KeepKey'\n\n    plugin: 'KeepKeyPlugin'\n\n    def decrypt_message(self, sequence, message, password):\n        raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device))\n\n    @runs_in_hwd_thread\n    def sign_message(self, sequence, message, password, *, script_type=None):\n        client = self.get_client()\n        address_path = self.get_derivation_prefix() + \"/%d/%d\"%sequence\n        address_n = client.expand_path(address_path)\n        msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)\n        return msg_sig.signature\n\n    @runs_in_hwd_thread\n    def sign_transaction(self, tx, password):\n        if tx.is_complete():\n            return\n        # previous transactions used as inputs\n        prev_tx = {}\n        for txin in tx.inputs():\n            tx_hash = txin.prevout.txid.hex()\n            if txin.utxo is None and not txin.is_segwit():\n                raise UserFacingException(_('Missing previous tx for legacy input.'))\n            prev_tx[tx_hash] = txin.utxo\n\n        self.plugin.sign_transaction(self, tx, prev_tx)\n\n\nclass KeepKeyPlugin(HW_PluginBase):\n    # Derived classes provide:\n    #\n    #  class-static variables: client_class, firmware_URL, handler_class,\n    #     libraries_available, libraries_URL, minimum_firmware,\n    #     wallet_class, ckd_public, types, HidTransport\n\n    firmware_URL = 'https://www.keepkey.com'\n    libraries_URL = 'https://github.com/keepkey/python-keepkey'\n    minimum_firmware = (1, 0, 0)\n    keystore_class = KeepKey_KeyStore\n    SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')\n\n    MAX_LABEL_LEN = 32\n\n    def __init__(self, parent, config, name):\n        HW_PluginBase.__init__(self, parent, config, name)\n\n        try:\n            from . import client\n            from .keepkeylib import keepkeylib\n            from .keepkeylib.keepkeylib import ckd_public, transport_hid, transport_webusb\n            self.client_class = client.KeepKeyClient\n            self.ckd_public = keepkeylib.ckd_public\n            self.types = keepkeylib.client.types\n            self.DEVICE_IDS = (keepkeylib.transport_hid.DEVICE_IDS +\n                               keepkeylib.transport_webusb.DEVICE_IDS)\n            # only \"register\" hid device id:\n            self.device_manager().register_devices(keepkeylib.transport_hid.DEVICE_IDS, plugin=self)\n            # for webusb transport, use custom enumerate function:\n            self.device_manager().register_enumerate_func(self.enumerate)\n            self.libraries_available = True\n        except ImportError:\n            self.logger.debug(\"error importing keepkeylib\", exc_info=True)\n            self.libraries_available = False\n\n    @runs_in_hwd_thread\n    def enumerate(self):\n        from .keepkeylib.keepkeylib.transport_webusb import WebUsbTransport\n        results = []\n        for dev in WebUsbTransport.enumerate():\n            path = self._dev_to_str(dev)\n            results.append(Device(path=path,\n                                  interface_number=-1,\n                                  id_=path,\n                                  product_key=(dev.getVendorID(), dev.getProductID()),\n                                  usage_page=0,\n                                  transport_ui_string=f\"webusb:{path}\"))\n        return results\n\n    @staticmethod\n    def _dev_to_str(dev: \"usb1.USBDevice\") -> str:\n        return \":\".join(str(x) for x in [\"%03i\" % (dev.getBusNumber(),)] + dev.getPortNumberList())\n\n    @runs_in_hwd_thread\n    def hid_transport(self, pair):\n        from .keepkeylib.keepkeylib.transport_hid import HidTransport\n        return HidTransport(pair)\n\n    @runs_in_hwd_thread\n    def webusb_transport(self, device):\n        from .keepkeylib.keepkeylib.transport_webusb import WebUsbTransport\n        for dev in WebUsbTransport.enumerate():\n            if device.path == self._dev_to_str(dev):\n                return WebUsbTransport(dev)\n\n    @runs_in_hwd_thread\n    def _try_hid(self, device):\n        self.logger.info(\"Trying to connect over USB...\")\n        if device.interface_number == 1:\n            pair = [None, device.path]\n        else:\n            pair = [device.path, None]\n\n        try:\n            return self.hid_transport(pair)\n        except BaseException as e:\n            # see fdb810ba622dc7dbe1259cbafb5b28e19d2ab114\n            # raise\n            self.logger.info(f\"cannot connect at {device.path} {e}\")\n            return None\n\n    @runs_in_hwd_thread\n    def _try_webusb(self, device):\n        self.logger.info(\"Trying to connect over WebUSB...\")\n        try:\n            return self.webusb_transport(device)\n        except BaseException as e:\n            self.logger.info(f\"cannot connect at {device.path} {e}\")\n            return None\n\n    @runs_in_hwd_thread\n    def create_client(self, device, handler):\n        if device.product_key[1] == 2:\n            transport = self._try_webusb(device)\n        else:\n            transport = self._try_hid(device)\n\n        if not transport:\n            self.logger.info(\"cannot connect to device\")\n            return\n\n        self.logger.info(f\"connected to device at {device.path}\")\n\n        client = self.client_class(transport, handler, self)\n\n        # Try a ping for device sanity\n        try:\n            client.ping('t')\n        except BaseException as e:\n            self.logger.info(f\"ping failed {e}\")\n            return None\n\n        if not client.atleast_version(*self.minimum_firmware):\n            msg = (_('Outdated {} firmware for device labelled {}. Please '\n                     'download the updated firmware from {}')\n                   .format(self.device, client.label(), self.firmware_URL))\n            self.logger.info(msg)\n            if handler:\n                handler.show_error(msg)\n            else:\n                raise UserFacingException(msg)\n            return None\n\n        return client\n\n    @runs_in_hwd_thread\n    def get_client(self, keystore, force_pair=True, *,\n                   devices=None, allow_user_interaction=True) -> Optional['KeepKeyClient']:\n        client = super().get_client(keystore, force_pair,\n                                    devices=devices,\n                                    allow_user_interaction=allow_user_interaction)\n        # returns the client for a given keystore. can use xpub\n        if client:\n            client.used()\n        return client\n\n    def get_coin_name(self):\n        return \"Testnet\" if constants.net.TESTNET else \"Bitcoin\"\n\n    @runs_in_hwd_thread\n    def _initialize_device(self, settings, method, device_id, handler):\n        item, label, pin_protection, passphrase_protection = settings\n\n        language = 'english'\n        devmgr = self.device_manager()\n        client = devmgr.client_by_id(device_id)\n        if not client:\n            raise Exception(_(\"The device was disconnected.\"))\n\n        if method == TIM_NEW:\n            strength = 64 * (item + 2)  # 128, 192 or 256\n            client.reset_device(True, strength, passphrase_protection,\n                                pin_protection, label, language)\n        elif method == TIM_RECOVER:\n            word_count = 24  # looks like this value is ignored by the device, but it has to be one of {12,18,24}\n            client.step = 0\n            client.recovery_device(word_count, passphrase_protection,\n                                       pin_protection, label, language)\n        elif method == TIM_MNEMONIC:\n            pin = pin_protection  # It's the pin, not a boolean\n            client.load_device_by_mnemonic(str(item), pin,\n                                           passphrase_protection,\n                                           label, language)\n        else:\n            pin = pin_protection  # It's the pin, not a boolean\n            client.load_device_by_xprv(item, pin, passphrase_protection,\n                                       label, language)\n\n    def _make_node_path(self, xpub: str, address_n: Sequence[int]):\n        bip32node = BIP32Node.from_xkey(xpub)\n        node = self.types.HDNodeType(\n            depth=bip32node.depth,\n            fingerprint=int.from_bytes(bip32node.fingerprint, 'big'),\n            child_num=int.from_bytes(bip32node.child_number, 'big'),\n            chain_code=bip32node.chaincode,\n            public_key=bip32node.eckey.get_public_key_bytes(compressed=True),\n        )\n        return self.types.HDNodePathType(node=node, address_n=address_n)\n\n    def get_keepkey_input_script_type(self, electrum_txin_type: str):\n        if electrum_txin_type in ('p2wpkh', 'p2wsh'):\n            return self.types.SPENDWITNESS\n        if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'):\n            return self.types.SPENDP2SHWITNESS\n        if electrum_txin_type in ('p2pkh',):\n            return self.types.SPENDADDRESS\n        if electrum_txin_type in ('p2sh',):\n            return self.types.SPENDMULTISIG\n        raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))\n\n    def get_keepkey_output_script_type(self, electrum_txin_type: str):\n        if electrum_txin_type in ('p2wpkh', 'p2wsh'):\n            return self.types.PAYTOWITNESS\n        if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'):\n            return self.types.PAYTOP2SHWITNESS\n        if electrum_txin_type in ('p2pkh',):\n            return self.types.PAYTOADDRESS\n        if electrum_txin_type in ('p2sh',):\n            return self.types.PAYTOMULTISIG\n        raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))\n\n    @runs_in_hwd_thread\n    def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):\n        self.prev_tx = prev_tx\n        client = self.get_client(keystore)\n        inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)\n        outputs = self.tx_outputs(tx, keystore=keystore)\n        signatures = client.sign_tx(self.get_coin_name(), inputs, outputs,\n                                    lock_time=tx.locktime, version=tx.version)[0]\n        sighash = Sighash.to_sigbytes(Sighash.ALL)\n        signatures = [(sig + sighash) for sig in signatures]\n        tx.update_signatures(signatures)\n\n    @runs_in_hwd_thread\n    def show_address(self, wallet, address, keystore=None):\n        if keystore is None:\n            keystore = wallet.get_keystore()\n        if not self.show_address_helper(wallet, address, keystore):\n            return\n        client = self.get_client(keystore)\n        if not client.atleast_version(1, 3):\n            keystore.handler.show_error(_(\"Your device firmware is too old\"))\n            return\n        deriv_suffix = wallet.get_address_index(address)\n        derivation = keystore.get_derivation_prefix()\n        address_path = \"%s/%d/%d\"%(derivation, *deriv_suffix)\n        address_n = client.expand_path(address_path)\n        script_type = self.get_keepkey_input_script_type(wallet.txin_type)\n\n        # prepare multisig, if available:\n        desc = wallet.get_script_descriptor_for_address(address)\n        if multi := desc.get_simple_multisig():\n            multisig = self._make_multisig(multi)\n        else:\n            multisig = None\n\n        client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)\n\n    def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'KeepKey_KeyStore' = None):\n        inputs = []\n        for txin in tx.inputs():\n            txinputtype = self.types.TxInputType()\n            if txin.is_coinbase_input():\n                prev_hash = b\"\\x00\"*32\n                prev_index = 0xffffffff  # signed int -1\n            else:\n                if for_sig:\n                    assert isinstance(tx, PartialTransaction)\n                    assert isinstance(txin, PartialTxInput)\n                    assert keystore\n                    desc = txin.script_descriptor\n                    assert desc\n                    if multi := desc.get_simple_multisig():\n                        multisig = self._make_multisig(multi)\n                    else:\n                        multisig = None\n                    script_type = self.get_keepkey_input_script_type(desc.to_legacy_electrum_script_type())\n                    txinputtype = self.types.TxInputType(\n                        script_type=script_type,\n                        multisig=multisig)\n                    my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)\n                    if full_path:\n                        txinputtype.address_n.extend(full_path)\n\n                prev_hash = txin.prevout.txid\n                prev_index = txin.prevout.out_idx\n\n            if txin.value_sats() is not None:\n                txinputtype.amount = txin.value_sats()\n            txinputtype.prev_hash = prev_hash\n            txinputtype.prev_index = prev_index\n\n            if txin.script_sig is not None:\n                txinputtype.script_sig = txin.script_sig\n\n            txinputtype.sequence = txin.nsequence\n\n            inputs.append(txinputtype)\n\n        return inputs\n\n    def _make_multisig(self, desc: descriptor.MultisigDescriptor):\n        pubkeys = []\n        for pubkey_provider in desc.pubkeys:\n            assert not pubkey_provider.is_range()\n            assert pubkey_provider.extkey is not None\n            xpub = pubkey_provider.pubkey\n            der_suffix = pubkey_provider.get_der_suffix_int_list()\n            pubkeys.append(self._make_node_path(xpub, der_suffix))\n        return self.types.MultisigRedeemScriptType(\n            pubkeys=pubkeys,\n            signatures=[b''] * len(pubkeys),\n            m=desc.thresh)\n\n    def tx_outputs(self, tx: PartialTransaction, *, keystore: 'KeepKey_KeyStore'):\n\n        def create_output_by_derivation():\n            desc = txout.script_descriptor\n            assert desc\n            script_type = self.get_keepkey_output_script_type(desc.to_legacy_electrum_script_type())\n            if multi := desc.get_simple_multisig():\n                multisig = self._make_multisig(multi)\n            else:\n                multisig = None\n            my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)\n            assert full_path\n            txoutputtype = self.types.TxOutputType(\n                multisig=multisig,\n                amount=txout.value,\n                address_n=full_path,\n                script_type=script_type)\n            return txoutputtype\n\n        def create_output_by_address():\n            txoutputtype = self.types.TxOutputType()\n            txoutputtype.amount = txout.value\n            if address:\n                txoutputtype.script_type = self.types.PAYTOADDRESS\n                txoutputtype.address = address\n            else:\n                txoutputtype.script_type = self.types.PAYTOOPRETURN\n                txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout)\n            return txoutputtype\n\n        outputs = []\n        has_change = False\n        any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)\n\n        for txout in tx.outputs():\n            address = txout.address\n            use_create_by_derivation = False\n\n            if txout.is_mine and not has_change:\n                # prioritise hiding outputs on the 'change' branch from user\n                # because no more than one change address allowed\n                if txout.is_change == any_output_on_change_branch:\n                    use_create_by_derivation = True\n                    has_change = True\n\n            if use_create_by_derivation:\n                txoutputtype = create_output_by_derivation()\n            else:\n                txoutputtype = create_output_by_address()\n            outputs.append(txoutputtype)\n\n        return outputs\n\n    def electrum_tx_to_txtype(self, tx: Optional[Transaction]):\n        t = self.types.TransactionType()\n        if tx is None:\n            # probably for segwit input and we don't need this prev txn\n            return t\n        tx.deserialize()\n        t.version = tx.version\n        t.lock_time = tx.locktime\n        inputs = self.tx_inputs(tx)\n        t.inputs.extend(inputs)\n        for out in tx.outputs():\n            o = t.bin_outputs.add()\n            o.amount = out.value\n            o.script_pubkey = out.scriptpubkey\n        return t\n\n    # This function is called from the TREZOR libraries (via tx_api)\n    def get_tx(self, tx_hash):\n        tx = self.prev_tx[tx_hash]\n        return self.electrum_tx_to_txtype(tx)\n\n    def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str:\n        if new_wallet:\n            return 'keepkey_start' if device_info.initialized else 'keepkey_not_initialized'\n        else:\n            return 'keepkey_unlock'\n\n    # insert keepkey pages in new wallet wizard\n    def extend_wizard(self, wizard: 'NewWalletWizard'):\n        views = {\n            'keepkey_start': {\n                'next': 'keepkey_xpub',\n            },\n            'keepkey_xpub': {\n                'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore',\n                'accept': wizard.maybe_master_pubkey,\n                'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d)\n            },\n            'keepkey_not_initialized': {\n                'next': 'keepkey_choose_new_recover',\n            },\n            'keepkey_choose_new_recover': {\n                'next': 'keepkey_do_init',\n            },\n            'keepkey_do_init': {\n                'next': 'keepkey_start',\n            },\n            'keepkey_unlock': {\n                'last': True\n            },\n        }\n        wizard.navmap_merge(views)\n"
  },
  {
    "path": "electrum/plugins/keepkey/manifest.json",
    "content": "{\n  \"name\": \"keepkey\",\n  \"fullname\": \"KeepKey\",\n  \"description\": \"Provides support for KeepKey hardware wallet\",\n  \"requires\": [[\"keepkeylib\", \"github.com/keepkey/python-keepkey\"]],\n  \"registers_keystore\": [\"hardware\", \"keepkey\", \"KeepKey wallet\"],\n  \"icon\":\"keepkey.png\",\n  \"available_for\": [\"qt\", \"cmdline\"]\n}\n"
  },
  {
    "path": "electrum/plugins/keepkey/qt.py",
    "content": "import threading\nfrom functools import partial\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import Qt, QEventLoop, pyqtSignal, QRegularExpression\nfrom PyQt6.QtGui import QRegularExpressionValidator\nfrom PyQt6.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,\n                             QHBoxLayout, QButtonGroup, QGroupBox, QDialog,\n                             QTextEdit, QLineEdit, QRadioButton, QCheckBox, QWidget,\n                             QMessageBox, QSlider, QTabWidget)\n\nfrom electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,\n                                  OkButton, CloseButton, ChoiceWidget)\nfrom electrum.i18n import _\nfrom electrum.plugin import hook\nfrom electrum.logging import Logger\nfrom electrum.util import ChoiceItem\n\nfrom electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase\nfrom electrum.hw_wallet.trezor_qt_pinmatrix import PinMatrixWidget\nfrom electrum.hw_wallet.plugin import only_hook_if_libraries_available\n\nfrom .keepkey import KeepKeyPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY\n\nfrom electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWXPub, WalletWizardComponent\n\nif TYPE_CHECKING:\n    from electrum.gui.qt.wizard.wallet import QENewWalletWizard\n\nPASSPHRASE_HELP_SHORT =_(\n    \"Passphrases allow you to access new wallets, each \"\n    \"hidden behind a particular case-sensitive passphrase.\")\nPASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + \"  \" + _(\n    \"You need to create a separate Electrum wallet for each passphrase \"\n    \"you use as they each generate different addresses.  Changing \"\n    \"your passphrase does not lose other wallets, each is still \"\n    \"accessible behind its own passphrase.\")\nRECOMMEND_PIN = _(\n    \"You should enable PIN protection.  Your PIN is the only protection \"\n    \"for your bitcoins if your device is lost or stolen.\")\nPASSPHRASE_NOT_PIN = _(\n    \"If you forget a passphrase you will be unable to access any \"\n    \"bitcoins in the wallet behind it.  A passphrase is not a PIN. \"\n    \"Only change this if you are sure you understand it.\")\nCHARACTER_RECOVERY = (\n    \"Use the recovery cipher shown on your device to input your seed words.  \"\n    \"The cipher changes with every keypress.\\n\"\n    \"After at most 4 letters the device will auto-complete a word.\\n\"\n    \"Press SPACE or the Accept Word button to accept the device's auto-\"\n    \"completed word and advance to the next one.\\n\"\n    \"Press BACKSPACE to go back a character or word.\\n\"\n    \"Press ENTER or the Seed Entered button once the last word in your \"\n    \"seed is auto-completed.\")\n\n\nclass CharacterButton(QPushButton):\n    def __init__(self, text=None):\n        QPushButton.__init__(self, text)\n\n    def keyPressEvent(self, event):\n        event.setAccepted(False)   # Pass through Enter and Space keys\n\n\nclass CharacterDialog(WindowModalDialog):\n\n    def __init__(self, parent):\n        super(CharacterDialog, self).__init__(parent)\n        self.setWindowTitle(_(\"KeepKey Seed Recovery\"))\n        self.character_pos = 0\n        self.word_pos = 0\n        self.loop = QEventLoop()\n        self.word_help = QLabel()\n        self.char_buttons = []\n\n        vbox = QVBoxLayout(self)\n        vbox.addWidget(WWLabel(CHARACTER_RECOVERY))\n        hbox = QHBoxLayout()\n        hbox.addWidget(self.word_help)\n        for i in range(4):\n            char_button = CharacterButton('*')\n            char_button.setMaximumWidth(36)\n            self.char_buttons.append(char_button)\n            hbox.addWidget(char_button)\n        self.accept_button = CharacterButton(_(\"Accept Word\"))\n        self.accept_button.clicked.connect(partial(self.process_key, 32))\n        self.rejected.connect(partial(self.loop.exit, 1))\n        hbox.addWidget(self.accept_button)\n        hbox.addStretch(1)\n        vbox.addLayout(hbox)\n\n        self.finished_button = QPushButton(_(\"Seed Entered\"))\n        self.cancel_button = QPushButton(_(\"Cancel\"))\n        self.finished_button.clicked.connect(partial(self.process_key,\n                                                     Qt.Key.Key_Return))\n        self.cancel_button.clicked.connect(self.rejected)\n        buttons = Buttons(self.finished_button, self.cancel_button)\n        vbox.addSpacing(40)\n        vbox.addLayout(buttons)\n        self.refresh()\n        self.show()\n\n    def refresh(self):\n        self.word_help.setText(\"Enter seed word %2d:\" % (self.word_pos + 1))\n        self.accept_button.setEnabled(self.character_pos >= 3)\n        self.finished_button.setEnabled((self.word_pos in (11, 17, 23)\n                                         and self.character_pos >= 3))\n        for n, button in enumerate(self.char_buttons):\n            button.setEnabled(n == self.character_pos)\n            if n == self.character_pos:\n                button.setFocus()\n\n    def is_valid_alpha_space(self, key):\n        # Auto-completion requires at least 3 characters\n        if key == ord(' ') and self.character_pos >= 3:\n            return True\n        # Firmware aborts protocol if the 5th character is non-space\n        if self.character_pos >= 4:\n            return False\n        return (key >= ord('a') and key <= ord('z')\n                or (key >= ord('A') and key <= ord('Z')))\n\n    def process_key(self, key):\n        self.data = None\n        if key == Qt.Key.Key_Return and self.finished_button.isEnabled():\n            self.data = {'done': True}\n        elif key == Qt.Key.Key_Backspace and (self.word_pos or self.character_pos):\n            self.data = {'delete': True}\n        elif self.is_valid_alpha_space(key):\n            self.data = {'character': chr(key).lower()}\n        if self.data:\n            self.loop.exit(0)\n\n    def keyPressEvent(self, event):\n        self.process_key(event.key())\n        if not self.data:\n            QDialog.keyPressEvent(self, event)\n\n    def get_char(self, word_pos, character_pos):\n        self.word_pos = word_pos\n        self.character_pos = character_pos\n        self.refresh()\n        if self.loop.exec():\n            self.data = None  # User cancelled\n\n\nclass QtHandler(QtHandlerBase):\n    char_signal = pyqtSignal(object)\n    pin_signal = pyqtSignal(object, object)\n    close_char_dialog_signal = pyqtSignal()\n\n    def __init__(self, win, pin_matrix_widget_class, device):\n        super(QtHandler, self).__init__(win, device)\n        self.char_signal.connect(self.update_character_dialog)\n        self.pin_signal.connect(self.pin_dialog)\n        self.close_char_dialog_signal.connect(self._close_char_dialog)\n        self.pin_matrix_widget_class = pin_matrix_widget_class\n        self.character_dialog = None\n\n    def get_char(self, msg):\n        self.done.clear()\n        self.char_signal.emit(msg)\n        self.done.wait()\n        data = self.character_dialog.data\n        if not data or 'done' in data:\n            self.close_char_dialog_signal.emit()\n        return data\n\n    def _close_char_dialog(self):\n        if self.character_dialog:\n            self.character_dialog.accept()\n            self.character_dialog = None\n\n    def get_pin(self, msg, *, show_strength=True):\n        self.done.clear()\n        self.pin_signal.emit(msg, show_strength)\n        self.done.wait()\n        return self.response\n\n    def pin_dialog(self, msg, show_strength):\n        # Needed e.g. when resetting a device\n        self.clear_dialog()\n        dialog = WindowModalDialog(self.top_level_window(), _(\"Enter PIN\"))\n        matrix = self.pin_matrix_widget_class(show_strength)\n        vbox = QVBoxLayout()\n        vbox.addWidget(QLabel(msg))\n        vbox.addWidget(matrix)\n        vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))\n        dialog.setLayout(vbox)\n        dialog.exec()\n        self.response = str(matrix.get_value())\n        self.done.set()\n\n    def update_character_dialog(self, msg):\n        if not self.character_dialog:\n            self.character_dialog = CharacterDialog(self.top_level_window())\n        self.character_dialog.get_char(msg.word_pos, msg.character_pos)\n        self.done.set()\n\n\nclass QtPlugin(QtPluginBase):\n    # Derived classes must provide the following class-static variables:\n    #   icon_file\n    #   pin_matrix_widget_class\n\n    @only_hook_if_libraries_available\n    @hook\n    def receive_menu(self, menu, addrs, wallet):\n        if len(addrs) != 1:\n            return\n        self._add_menu_action(menu, addrs[0], wallet)\n\n    @only_hook_if_libraries_available\n    @hook\n    def transaction_dialog_address_menu(self, menu, addr, wallet):\n        self._add_menu_action(menu, addr, wallet)\n\n    def show_settings_dialog(self, window, keystore):\n        def connect():\n            device_id = self.choose_device(window, keystore)\n            return device_id\n        def show_dialog(device_id):\n            if device_id:\n                SettingsDialog(window, self, keystore, device_id).exec()\n        keystore.thread.add(connect, on_success=show_dialog)\n\n\ndef clean_text(widget):\n    text = widget.toPlainText().strip()\n    return ' '.join(text.split())\n\n\nclass KeepkeyInitLayout(QVBoxLayout):\n    validChanged = pyqtSignal([bool], arguments=['valid'])\n\n    def __init__(self, method, device):\n        QVBoxLayout.__init__(self)\n        self.method = method\n\n        label = QLabel(_(\"Enter a label to name your device:\"))\n        self.label_e = QLineEdit()\n        hl = QHBoxLayout()\n        hl.addWidget(label)\n        hl.addWidget(self.label_e)\n        hl.addStretch(1)\n        self.addLayout(hl)\n\n        if self.method in [TIM_NEW, TIM_RECOVER]:\n            gb = QGroupBox()\n            hbox1 = QHBoxLayout()\n            gb.setLayout(hbox1)\n            # KeepKey recovery doesn't need a word count\n            if self.method == TIM_NEW:\n                self.addWidget(gb)\n            gb.setTitle(_(\"Select your seed length:\"))\n            self.bg = QButtonGroup()\n            for i, count in enumerate([12, 18, 24]):\n                rb = QRadioButton(gb)\n                rb.setText(_(\"{} words\").format(count))\n                self.bg.addButton(rb)\n                self.bg.setId(rb, i)\n                hbox1.addWidget(rb)\n                rb.setChecked(True)\n            self.cb_pin = QCheckBox(_('Enable PIN protection'))\n            self.cb_pin.setChecked(True)\n        else:\n            self.text_e = QTextEdit()\n            self.text_e.setMaximumHeight(60)\n            if method == TIM_MNEMONIC:\n                msg = _(\"Enter your BIP39 mnemonic:\")\n                # TODO: validation?\n            else:\n                msg = _(\"Enter the master private key beginning with xprv:\")\n\n                def set_enabled():\n                    from electrum.bip32 import is_xprv\n                    self.validChanged.emit(is_xprv(clean_text(self.text_e)))\n                self.text_e.textChanged.connect(set_enabled)\n\n            self.addWidget(QLabel(msg))\n            self.addWidget(self.text_e)\n            self.pin = QLineEdit()\n            self.pin.setValidator(QRegularExpressionValidator(QRegularExpression('[1-9]{0,9}')))\n            self.pin.setMaximumWidth(100)\n            hbox_pin = QHBoxLayout()\n            hbox_pin.addWidget(QLabel(_(\"Enter your PIN (digits 1-9):\")))\n            hbox_pin.addWidget(self.pin)\n            hbox_pin.addStretch(1)\n\n        if method in [TIM_NEW, TIM_RECOVER]:\n            self.addWidget(WWLabel(RECOMMEND_PIN))\n            self.addWidget(self.cb_pin)\n        else:\n            self.addLayout(hbox_pin)\n\n        passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT)\n        passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)\n        passphrase_warning.setStyleSheet(\"color: red\")\n        self.cb_phrase = QCheckBox(_('Enable passphrases'))\n        self.cb_phrase.setChecked(False)\n        self.addWidget(passphrase_msg)\n        self.addWidget(passphrase_warning)\n        self.addWidget(self.cb_phrase)\n\n    def get_settings(self):\n        if self.method in [TIM_NEW, TIM_RECOVER]:\n            item = self.bg.checkedId()\n            pin = self.cb_pin.isChecked()\n        else:\n            item = ' '.join(str(clean_text(self.text_e)).split())\n            pin = str(self.pin.text())\n\n        return item, self.label_e.text(), pin, self.cb_phrase.isChecked()\n\n\nclass Plugin(KeepKeyPlugin, QtPlugin):\n    icon_paired = \"keepkey.png\"\n    icon_unpaired = \"keepkey_unpaired.png\"\n\n    def create_handler(self, window):\n        return QtHandler(window, self.pin_matrix_widget_class(), self.device)\n\n    @classmethod\n    def pin_matrix_widget_class(self):\n        return PinMatrixWidget\n\n    # insert keepkey pages in new wallet wizard\n    def extend_wizard(self, wizard: 'QENewWalletWizard'):\n        super().extend_wizard(wizard)\n        views = {\n            'keepkey_start': {'gui': WCScriptAndDerivation},\n            'keepkey_xpub': {'gui': WCHWXPub},\n            'keepkey_not_initialized': {'gui': WCKeepkeyInitMethod},\n            'keepkey_choose_new_recover': {'gui': WCKeepkeyInitParams},\n            'keepkey_do_init': {'gui': WCKeepkeyInit},\n            'keepkey_unlock': {'gui': WCHWUnlock}\n        }\n        wizard.navmap_merge(views)\n\n\nclass SettingsDialog(WindowModalDialog):\n    '''This dialog doesn't require a device be paired with a wallet.\n    We want users to be able to wipe a device even if they've forgotten\n    their PIN.'''\n\n    def __init__(self, window, plugin, keystore, device_id):\n        title = _(\"{} Settings\").format(plugin.device)\n        super(SettingsDialog, self).__init__(window, title)\n        self.setMaximumWidth(540)\n\n        devmgr = plugin.device_manager()\n        config = devmgr.config\n        handler = keystore.handler\n        thread = keystore.thread\n\n        def invoke_client(method, *args, **kw_args):\n            unpair_after = kw_args.pop('unpair_after', False)\n\n            def task():\n                client = devmgr.client_by_id(device_id)\n                if not client:\n                    raise RuntimeError(\"Device not connected\")\n                if method:\n                    getattr(client, method)(*args, **kw_args)\n                if unpair_after:\n                    devmgr.unpair_id(device_id)\n                return client.features\n\n            thread.add(task, on_success=update)\n\n        def update(features):\n            self.features = features\n            set_label_enabled()\n            bl_hash = features.bootloader_hash.hex()\n            bl_hash = \"\\n\".join([bl_hash[:32], bl_hash[32:]])\n            noyes = [_(\"No\"), _(\"Yes\")]\n            endis = [_(\"Enable Passphrases\"), _(\"Disable Passphrases\")]\n            disen = [_(\"Disabled\"), _(\"Enabled\")]\n            setchange = [_(\"Set a PIN\"), _(\"Change PIN\")]\n\n            version = \"%d.%d.%d\" % (features.major_version,\n                                    features.minor_version,\n                                    features.patch_version)\n            coins = \", \".join(coin.coin_name for coin in features.coins)\n\n            device_label.setText(features.label)\n            pin_set_label.setText(noyes[features.pin_protection])\n            passphrases_label.setText(disen[features.passphrase_protection])\n            bl_hash_label.setText(bl_hash)\n            label_edit.setText(features.label)\n            device_id_label.setText(features.device_id)\n            initialized_label.setText(noyes[features.initialized])\n            version_label.setText(version)\n            coins_label.setText(coins)\n            clear_pin_button.setVisible(features.pin_protection)\n            clear_pin_warning.setVisible(features.pin_protection)\n            pin_button.setText(setchange[features.pin_protection])\n            pin_msg.setVisible(not features.pin_protection)\n            passphrase_button.setText(endis[features.passphrase_protection])\n            language_label.setText(features.language)\n\n        def set_label_enabled():\n            label_apply.setEnabled(label_edit.text() != self.features.label)\n\n        def rename():\n            invoke_client('change_label', label_edit.text())\n\n        def toggle_passphrase():\n            title = _(\"Confirm Toggle Passphrase Protection\")\n            currently_enabled = self.features.passphrase_protection\n            if currently_enabled:\n                msg = _(\"After disabling passphrases, you can only pair this \"\n                        \"Electrum wallet if it had an empty passphrase.  \"\n                        \"If its passphrase was not empty, you will need to \"\n                        \"create a new wallet.  You can use this wallet again \"\n                        \"at any time by re-enabling passphrases and entering \"\n                        \"its passphrase.\")\n            else:\n                msg = _(\"Your current Electrum wallet can only be used with \"\n                        \"an empty passphrase.  You must create a separate \"\n                        \"wallet for other passphrases as each one generates \"\n                        \"a new set of addresses.\")\n            msg += \"\\n\\n\" + _(\"Are you sure you want to proceed?\")\n            if not self.question(msg, title=title):\n                return\n            invoke_client('toggle_passphrase', unpair_after=currently_enabled)\n\n        def set_pin():\n            invoke_client('set_pin', remove=False)\n\n        def clear_pin():\n            invoke_client('set_pin', remove=True)\n\n        def wipe_device():\n            wallet = window.wallet\n            if wallet and sum(wallet.get_balance()):\n                title = _(\"Confirm Device Wipe\")\n                msg = _(\"Are you SURE you want to wipe the device?\\n\"\n                        \"Your wallet still has bitcoins in it!\")\n                if not self.question(msg, title=title,\n                                     icon=QMessageBox.Icon.Critical):\n                    return\n            invoke_client('wipe_device', unpair_after=True)\n\n        def slider_moved():\n            mins = timeout_slider.sliderPosition()\n            timeout_minutes.setText(_(\"{:2d} minutes\").format(mins))\n\n        def slider_released():\n            config.set_session_timeout(timeout_slider.sliderPosition() * 60)\n\n        # Information tab\n        info_tab = QWidget()\n        info_layout = QVBoxLayout(info_tab)\n        info_glayout = QGridLayout()\n        info_glayout.setColumnStretch(2, 1)\n        device_label = QLabel()\n        pin_set_label = QLabel()\n        passphrases_label = QLabel()\n        version_label = QLabel()\n        device_id_label = QLabel()\n        bl_hash_label = QLabel()\n        bl_hash_label.setWordWrap(True)\n        coins_label = QLabel()\n        coins_label.setWordWrap(True)\n        language_label = QLabel()\n        initialized_label = QLabel()\n        rows = [\n            (_(\"Device Label\"), device_label),\n            (_(\"PIN set\"), pin_set_label),\n            (_(\"Passphrases\"), passphrases_label),\n            (_(\"Firmware Version\"), version_label),\n            (_(\"Device ID\"), device_id_label),\n            (_(\"Bootloader Hash\"), bl_hash_label),\n            (_(\"Supported Coins\"), coins_label),\n            (_(\"Language\"), language_label),\n            (_(\"Initialized\"), initialized_label),\n        ]\n        for row_num, (label, widget) in enumerate(rows):\n            info_glayout.addWidget(QLabel(label), row_num, 0)\n            info_glayout.addWidget(widget, row_num, 1)\n        info_layout.addLayout(info_glayout)\n\n        # Settings tab\n        settings_tab = QWidget()\n        settings_layout = QVBoxLayout(settings_tab)\n        settings_glayout = QGridLayout()\n\n        # Settings tab - Label\n        label_msg = QLabel(_(\"Name this {}.  If you have multiple devices \"\n                             \"their labels help distinguish them.\")\n                           .format(plugin.device))\n        label_msg.setWordWrap(True)\n        label_label = QLabel(_(\"Device Label\"))\n        label_edit = QLineEdit()\n        label_edit.setMinimumWidth(150)\n        label_edit.setMaxLength(plugin.MAX_LABEL_LEN)\n        label_apply = QPushButton(_(\"Apply\"))\n        label_apply.clicked.connect(rename)\n        label_edit.textChanged.connect(set_label_enabled)\n        settings_glayout.addWidget(label_label, 0, 0)\n        settings_glayout.addWidget(label_edit, 0, 1, 1, 2)\n        settings_glayout.addWidget(label_apply, 0, 3)\n        settings_glayout.addWidget(label_msg, 1, 1, 1, -1)\n\n        # Settings tab - PIN\n        pin_label = QLabel(_(\"PIN Protection\"))\n        pin_button = QPushButton()\n        pin_button.clicked.connect(set_pin)\n        settings_glayout.addWidget(pin_label, 2, 0)\n        settings_glayout.addWidget(pin_button, 2, 1)\n        pin_msg = QLabel(_(\"PIN protection is strongly recommended.  \"\n                           \"A PIN is your only protection against someone \"\n                           \"stealing your bitcoins if they obtain physical \"\n                           \"access to your {}.\").format(plugin.device))\n        pin_msg.setWordWrap(True)\n        pin_msg.setStyleSheet(\"color: red\")\n        settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)\n\n        # Settings tab - Session Timeout\n        timeout_label = QLabel(_(\"Session Timeout\"))\n        timeout_minutes = QLabel()\n        timeout_slider = QSlider(Qt.Orientation.Horizontal)\n        timeout_slider.setRange(1, 60)\n        timeout_slider.setSingleStep(1)\n        timeout_slider.setTickInterval(5)\n        timeout_slider.setTickPosition(QSlider.TickPosition.TicksBelow)\n        timeout_slider.setTracking(True)\n        timeout_msg = QLabel(\n            _(\"Clear the session after the specified period \"\n              \"of inactivity.  Once a session has timed out, \"\n              \"your PIN and passphrase (if enabled) must be \"\n              \"re-entered to use the device.\"))\n        timeout_msg.setWordWrap(True)\n        timeout_slider.setSliderPosition(config.get_session_timeout() // 60)\n        slider_moved()\n        timeout_slider.valueChanged.connect(slider_moved)\n        timeout_slider.sliderReleased.connect(slider_released)\n        settings_glayout.addWidget(timeout_label, 6, 0)\n        settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)\n        settings_glayout.addWidget(timeout_minutes, 6, 4)\n        settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)\n        settings_layout.addLayout(settings_glayout)\n        settings_layout.addStretch(1)\n\n        # Advanced tab\n        advanced_tab = QWidget()\n        advanced_layout = QVBoxLayout(advanced_tab)\n        advanced_glayout = QGridLayout()\n\n        # Advanced tab - clear PIN\n        clear_pin_button = QPushButton(_(\"Disable PIN\"))\n        clear_pin_button.clicked.connect(clear_pin)\n        clear_pin_warning = QLabel(\n            _(\"If you disable your PIN, anyone with physical access to your \"\n              \"{} device can spend your bitcoins.\").format(plugin.device))\n        clear_pin_warning.setWordWrap(True)\n        clear_pin_warning.setStyleSheet(\"color: red\")\n        advanced_glayout.addWidget(clear_pin_button, 0, 2)\n        advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5)\n\n        # Advanced tab - toggle passphrase protection\n        passphrase_button = QPushButton()\n        passphrase_button.clicked.connect(toggle_passphrase)\n        passphrase_msg = WWLabel(PASSPHRASE_HELP)\n        passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)\n        passphrase_warning.setStyleSheet(\"color: red\")\n        advanced_glayout.addWidget(passphrase_button, 3, 2)\n        advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5)\n        advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5)\n\n        # Advanced tab - wipe device\n        wipe_device_button = QPushButton(_(\"Wipe Device\"))\n        wipe_device_button.clicked.connect(wipe_device)\n        wipe_device_msg = QLabel(\n            _(\"Wipe the device, removing all data from it.  The firmware \"\n              \"is left unchanged.\"))\n        wipe_device_msg.setWordWrap(True)\n        wipe_device_warning = QLabel(\n            _(\"Only wipe a device if you have the recovery seed written down \"\n              \"and the device wallet(s) are empty, otherwise the bitcoins \"\n              \"will be lost forever.\"))\n        wipe_device_warning.setWordWrap(True)\n        wipe_device_warning.setStyleSheet(\"color: red\")\n        advanced_glayout.addWidget(wipe_device_button, 6, 2)\n        advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5)\n        advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5)\n        advanced_layout.addLayout(advanced_glayout)\n        advanced_layout.addStretch(1)\n\n        tabs = QTabWidget(self)\n        tabs.addTab(info_tab, _(\"Information\"))\n        tabs.addTab(settings_tab, _(\"Settings\"))\n        tabs.addTab(advanced_tab, _(\"Advanced\"))\n        dialog_vbox = QVBoxLayout(self)\n        dialog_vbox.addWidget(tabs)\n        dialog_vbox.addLayout(Buttons(CloseButton(self)))\n\n        # Update information\n        invoke_client(None)\n\n\nclass WCKeepkeyInitMethod(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('KeepKey Setup'))\n\n    def on_ready(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = current_cosigner['hardware_device']\n        msg = _(\"Choose how you want to initialize your {}.\\n\\n\"\n                \"The first two methods are secure as no secret information \"\n                \"is entered into your computer.\\n\\n\"\n                \"For the last two methods you input secrets on your keyboard \"\n                \"and upload them to your {}, and so you should \"\n                \"only do those on a computer you know to be trustworthy \"\n                \"and free of malware.\"\n                ).format(_info.model_name, _info.model_name)\n        choices = [\n            # Must be short as QT doesn't word-wrap radio button text\n            ChoiceItem(key=TIM_NEW, label=_(\"Let the device generate a completely new seed randomly\")),\n            ChoiceItem(key=TIM_RECOVER, label=_(\"Recover from a seed you have previously written down\")),\n            ChoiceItem(key=TIM_MNEMONIC, label=_(\"Upload a BIP39 mnemonic to generate the seed\")),\n            ChoiceItem(key=TIM_PRIVKEY, label=_(\"Upload a master private key\")),\n        ]\n        self.choice_w = ChoiceWidget(message=msg, choices=choices)\n        self.layout().addWidget(self.choice_w)\n        self.layout().addStretch(1)\n\n        self._valid = True\n\n    def apply(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        current_cosigner['keepkey_init'] = self.choice_w.selected_key\n\n\nclass WCKeepkeyInitParams(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('KeepKey Setup'))\n        self.plugins = wizard.plugins\n        self._busy = True\n\n    def on_ready(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = current_cosigner['hardware_device']\n        self.settings_layout = KeepkeyInitLayout(current_cosigner['keepkey_init'], _info.device.id_)\n        self.settings_layout.validChanged.connect(self.on_settings_valid_changed)\n        self.layout().addLayout(self.settings_layout)\n        self.layout().addStretch(1)\n\n        self.valid = current_cosigner['keepkey_init'] != TIM_PRIVKEY  # TODO: only privkey is validated\n        self.busy = False\n\n    def on_settings_valid_changed(self, is_valid: bool):\n        self.valid = is_valid\n\n    def apply(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        current_cosigner['keepkey_settings'] = self.settings_layout.get_settings()\n\n\nclass WCKeepkeyInit(WalletWizardComponent, Logger):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('KeepKey Setup'))\n        Logger.__init__(self)\n        self.plugins = wizard.plugins\n        self.plugin = self.plugins.get_plugin('keepkey')\n\n        self.layout().addWidget(WWLabel('Done'))\n\n        self._busy = True\n\n    def on_ready(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        settings = current_cosigner['keepkey_settings']\n        method = current_cosigner['keepkey_init']\n        _name, _info = current_cosigner['hardware_device']\n        device_id = _info.device.id_\n        client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)\n        client.handler = self.plugin.create_handler(self.wizard)\n\n        def initialize_device_task(settings, method, device_id, handler):\n            try:\n                self.plugin._initialize_device(settings, method, device_id, handler)\n                self.logger.info('Done initialize device')\n                self.valid = True\n                self.wizard.requestNext.emit()  # triggers Next GUI thread from event loop\n            except Exception as e:\n                self.valid = False\n                self.error = repr(e)\n                self.logger.exception(repr(e))\n            finally:\n                self.busy = False\n\n        t = threading.Thread(\n            target=initialize_device_task,\n            args=(settings, method, device_id, client.handler),\n            daemon=True)\n        t.start()\n\n    def apply(self):\n        pass\n"
  },
  {
    "path": "electrum/plugins/labels/Labels.qml",
    "content": "import QtQuick 2.6\nimport QtQuick.Layouts 1.0\nimport QtQuick.Controls 2.14\nimport QtQuick.Controls.Material 2.0\n\nimport org.electrum 1.0\n\n//import \"controls\"\n\nItem {\n    width: parent.width\n    height: rootLayout.height\n\n    property QtObject plugin\n\n    RowLayout {\n        id: rootLayout\n        Button {\n            text: 'Force upload'\n            enabled: !plugin.busy\n            onClicked: plugin.upload()\n        }\n        Button {\n            text: 'Force download'\n            enabled: !plugin.busy\n            onClicked: plugin.download()\n        }\n    }\n\n    Connections {\n        target: plugin\n        function onUploadSuccess() {\n            console.log('upload success')\n        }\n        function onUploadFailed() {\n            console.log('upload failed')\n        }\n        function onDownloadSuccess() {\n            console.log('download success')\n        }\n        function onDownloadFailed() {\n            console.log('download failed')\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/plugins/labels/__init__.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nfrom electrum.commands import plugin_command\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from .labels import LabelsPlugin\n    from electrum.commands import Commands\n\nplugin_name = \"labels\"\n\n\n@plugin_command('w', plugin_name)\nasync def push(self: 'Commands', plugin: 'LabelsPlugin' = None, wallet=None) -> int:\n    \"\"\" push labels to server \"\"\"\n    return await plugin.push_thread(wallet)\n\n\n@plugin_command('w', plugin_name)\nasync def pull(self: 'Commands', plugin: 'LabelsPlugin' = None, wallet=None, force=False) -> int:\n    \"\"\"\n    pull missing labels from server\n\n    arg:bool:force:pull all labels\n    \"\"\"\n    return await plugin.pull_thread(wallet, force=force)\n"
  },
  {
    "path": "electrum/plugins/labels/cmdline.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nfrom .labels import LabelsPlugin\nfrom electrum.plugin import hook\n\n\nclass Plugin(LabelsPlugin):\n\n    @hook\n    def load_wallet(self, wallet, window):\n        self.start_wallet(wallet)\n\n    def on_pulled(self, wallet):\n        self.logger.info('labels pulled from server')\n"
  },
  {
    "path": "electrum/plugins/labels/labels.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport asyncio\nimport hashlib\nimport json\nfrom typing import Union, TYPE_CHECKING\n\nimport base64\n\nfrom electrum import util\nfrom electrum.plugin import BasePlugin, hook\nfrom electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv\nfrom electrum.i18n import _\nfrom electrum.util import log_exceptions, ignore_exceptions, make_aiohttp_session\nfrom electrum.network import Network\n\nif TYPE_CHECKING:\n    from electrum.wallet import Abstract_Wallet\n\n\nclass ErrorConnectingServer(Exception):\n    def __init__(self, reason: Union[str, Exception] = None):\n        self.reason = reason\n\n    def __str__(self):\n        header = _(\"Error connecting to {} server\").format('Labels')\n        reason = self.reason\n        if isinstance(reason, BaseException):\n            reason = repr(reason)\n        return f\"{header}: {reason}\" if reason else header\n\n\nclass LabelsPlugin(BasePlugin):\n\n    def __init__(self, parent, config, name):\n        BasePlugin.__init__(self, parent, config, name)\n        self.target_host = 'labels.electrum.org'\n        self.wallets = {}\n\n    def encode(self, wallet: 'Abstract_Wallet', msg: str) -> str:\n        password, iv, wallet_id = self.wallets[wallet]\n        encrypted = aes_encrypt_with_iv(password, iv, msg.encode('utf8'))\n        # FIXME: ^ we are reusing the IV between all labels in the wallet, in CBC mode...\n        return base64.b64encode(encrypted).decode()\n\n    def decode(self, wallet: 'Abstract_Wallet', message: str) -> str:\n        password, iv, wallet_id = self.wallets[wallet]\n        decoded = base64.b64decode(message, validate=True)\n        decrypted = aes_decrypt_with_iv(password, iv, decoded)\n        return decrypted.decode('utf8')\n\n    def get_nonce(self, wallet: 'Abstract_Wallet'):\n        # nonce is the nonce to be used with the next change\n        nonce = wallet.db.get('wallet_nonce')\n        if nonce is None:\n            nonce = 1\n            self.set_nonce(wallet, nonce)\n        return nonce\n\n    def set_nonce(self, wallet: 'Abstract_Wallet', nonce):\n        self.logger.info(f\"set {wallet.basename()} nonce to {nonce}\")\n        wallet.db.put(\"wallet_nonce\", nonce)\n\n    @hook\n    def set_label(self, wallet: 'Abstract_Wallet', item, label):\n        if wallet not in self.wallets:\n            return\n        if not item:\n            return\n        if label is None:\n            # note: the server should not know whether a label is empty\n            #       FIXME but it does! we are reusing the IV with AES-CBC: there is no randomness between labels,\n            #       all empty labels in given wallet look the same.\n            label = ''\n        nonce = self.get_nonce(wallet)\n        wallet_id = self.wallets[wallet][2]\n        bundle = {\n            \"walletId\": wallet_id,\n            \"walletNonce\": nonce,\n            \"externalId\": self.encode(wallet, item),\n            \"encryptedLabel\": self.encode(wallet, label)\n        }\n        asyncio.run_coroutine_threadsafe(self.do_post_safe(\"/label\", bundle), wallet.network.asyncio_loop)\n        # Caller will write the wallet\n        self.set_nonce(wallet, nonce + 1)\n\n    @ignore_exceptions\n    @log_exceptions\n    async def do_post_safe(self, *args):\n        await self.do_post(*args)\n\n    async def do_get(self, url = \"/labels\"):\n        url = 'https://' + self.target_host + url\n        network = Network.get_instance()\n        proxy = network.proxy if network else None\n        async with make_aiohttp_session(proxy) as session:\n            async with session.get(url) as result:\n                return await result.json()\n\n    async def do_post(self, url = \"/labels\", data=None):\n        url = 'https://' + self.target_host + url\n        network = Network.get_instance()\n        proxy = network.proxy if network else None\n        async with make_aiohttp_session(proxy) as session:\n            async with session.post(url, json=data) as result:\n                try:\n                    return await result.json()\n                except Exception as e:\n                    raise Exception('Could not decode: ' + await result.text()) from e\n\n    async def push_thread(self, wallet: 'Abstract_Wallet') -> int:\n        wallet_data = self.wallets.get(wallet, None)\n        if not wallet_data:\n            raise Exception('Wallet {} not loaded'.format(wallet))\n        wallet_id = wallet_data[2]\n        bundle = {\"labels\": [],\n                  \"walletId\": wallet_id,\n                  \"walletNonce\": self.get_nonce(wallet)}\n        for key, value in wallet.get_all_labels().items():\n            try:\n                encoded_key = self.encode(wallet, key)\n                encoded_value = self.encode(wallet, value)\n            except Exception:\n                self.logger.info(f'cannot encode {repr(key)} {repr(value)}')\n                continue\n            bundle[\"labels\"].append({'encryptedLabel': encoded_value,\n                                     'externalId': encoded_key})\n        await self.do_post(\"/labels\", bundle)\n        return len(bundle['labels'])\n\n    async def pull_thread(self, wallet: 'Abstract_Wallet', force: bool) -> int:\n        wallet_data = self.wallets.get(wallet, None)\n        if not wallet_data:\n            raise Exception('Wallet {} not loaded'.format(wallet))\n        wallet_id = wallet_data[2]\n        nonce = 1 if force else self.get_nonce(wallet) - 1\n        self.logger.info(f\"asking for labels since nonce {nonce}\")\n        try:\n            response = await self.do_get(\"/labels/since/%d/for/%s\" % (nonce, wallet_id))\n        except Exception as e:\n            raise ErrorConnectingServer(e) from e\n        if response[\"labels\"] is None or len(response[\"labels\"]) == 0:\n            self.logger.info('no new labels')\n            return 0\n\n        self.logger.info(f'received {len(response[\"labels\"])} labels')\n        result = {}\n        for label in response[\"labels\"]:\n            try:\n                key = self.decode(wallet, label[\"externalId\"])\n                value = self.decode(wallet, label[\"encryptedLabel\"])\n            except Exception:\n                continue\n            try:\n                json.dumps(key)\n                json.dumps(value)\n            except Exception:\n                self.logger.info(f'error: no json {key}')\n                continue\n            if value:\n                result[key] = value\n\n        for key, value in result.items():\n            wallet._set_label(key, value)\n\n        self.set_nonce(wallet, response[\"nonce\"] + 1)\n        util.trigger_callback('labels_received', wallet, result)\n        self.on_pulled(wallet)\n        return len(result)\n\n    def on_pulled(self, wallet: 'Abstract_Wallet') -> None:\n        pass\n\n    @ignore_exceptions\n    @log_exceptions\n    async def pull_safe_thread(self, wallet: 'Abstract_Wallet', force: bool):\n        try:\n            await self.pull_thread(wallet, force)\n        except ErrorConnectingServer as e:\n            self.logger.info(repr(e))\n\n    def pull(self, wallet: 'Abstract_Wallet', force: bool):\n        if not wallet.network:\n            raise Exception(_('You are offline.'))\n        return asyncio.run_coroutine_threadsafe(self.pull_thread(wallet, force), wallet.network.asyncio_loop).result()\n\n    def push(self, wallet: 'Abstract_Wallet'):\n        if not wallet.network:\n            raise Exception(_('You are offline.'))\n        return asyncio.run_coroutine_threadsafe(self.push_thread(wallet), wallet.network.asyncio_loop).result()\n\n    def start_wallet(self, wallet: 'Abstract_Wallet'):\n        if not wallet.network:\n            return  # 'offline' mode\n        mpk = wallet.get_fingerprint()\n        if not mpk:\n            return\n        mpk = mpk.encode('ascii')\n        password = hashlib.sha1(mpk).hexdigest()[:32].encode('ascii')\n        iv = hashlib.sha256(password).digest()[:16]\n        wallet_id = hashlib.sha256(mpk).hexdigest()\n        self.wallets[wallet] = (password, iv, wallet_id)\n        nonce = self.get_nonce(wallet)\n        self.logger.info(f\"wallet {wallet.basename()} nonce is {nonce}\")\n        # If there is an auth token we can try to actually start syncing\n        asyncio.run_coroutine_threadsafe(self.pull_safe_thread(wallet, False), wallet.network.asyncio_loop)\n\n    def stop_wallet(self, wallet):\n        self.wallets.pop(wallet, None)\n"
  },
  {
    "path": "electrum/plugins/labels/manifest.json",
    "content": "{\n  \"name\": \"labels\",\n  \"fullname\": \"LabelSync\",\n  \"author\": \"The Electrum Developers\",\n  \"description\": \"Save your wallet labels on a remote server, and synchronize them across multiple devices where you use Electrum. Labels, transactions IDs and addresses are encrypted before they are sent to the remote server.\",\n  \"icon\": \"labelsync.png\",\n  \"available_for\": [\"qt\", \"qml\", \"cmdline\"]\n}\n"
  },
  {
    "path": "electrum/plugins/labels/qml.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport threading\n\nfrom PyQt6.QtCore import pyqtSignal, pyqtSlot\n\nfrom electrum.i18n import _\nfrom electrum.plugin import hook\n\nfrom electrum.gui.qml.qewallet import QEWallet\nfrom electrum.gui.common_qt.plugins import PluginQObject\n\nfrom .labels import LabelsPlugin\n\n\nclass Plugin(LabelsPlugin):\n\n    class QSignalObject(PluginQObject):\n        labelsChanged = pyqtSignal()\n        uploadSuccess = pyqtSignal()\n        uploadFailed = pyqtSignal()\n        downloadSuccess = pyqtSignal()\n        downloadFailed = pyqtSignal()\n\n        _name = _('LabelSync Plugin')\n\n        def __init__(self, plugin, parent):\n            super().__init__(plugin, parent)\n\n        @pyqtSlot(result=str)\n        def settingsComponent(self): return '../../../plugins/labels/Labels.qml'\n\n        @pyqtSlot()\n        def upload(self):\n            assert self.plugin\n\n            self._busy = True\n            self.busyChanged.emit()\n\n            self.plugin.push_async()\n\n        def upload_finished(self, result):\n            if result:\n                self.uploadSuccess.emit()\n            else:\n                self.uploadFailed.emit()\n            self._busy = False\n            self.busyChanged.emit()\n\n        @pyqtSlot()\n        def download(self):\n            assert self.plugin\n\n            self._busy = True\n            self.busyChanged.emit()\n\n            self.plugin.pull_async()\n\n        def download_finished(self, result):\n            if result:\n                self.downloadSuccess.emit()\n            else:\n                self.downloadFailed.emit()\n            self._busy = False\n            self.busyChanged.emit()\n\n    def __init__(self, *args):\n        LabelsPlugin.__init__(self, *args)\n        self._app = None\n        self.so = None\n\n    @hook\n    def load_wallet(self, wallet):\n        self.logger.debug(f'plugin enabled for wallet \"{str(wallet)}\"')\n        self.start_wallet(wallet)\n\n    def push_async(self):\n        if not self._app.daemon.currentWallet:\n            self.logger.error('No current wallet')\n            self.so.download_finished(False)\n            return\n\n        wallet = self._app.daemon.currentWallet.wallet\n\n        def push_thread(_wallet):\n            try:\n                self.push(_wallet)\n                self.so.upload_finished(True)\n                self._app.appController.userNotify.emit(_('Labels uploaded'))\n            except Exception as e:\n                self.logger.error(repr(e))\n                self.so.upload_finished(False)\n                self._app.appController.userNotify.emit(repr(e))\n\n        threading.Thread(target=push_thread, args=[wallet]).start()\n\n    def pull_async(self):\n        if not self._app.daemon.currentWallet:\n            self.logger.error('No current wallet')\n            self.so.download_finished(False)\n            return\n\n        wallet = self._app.daemon.currentWallet.wallet\n\n        def pull_thread(_wallet):\n            try:\n                self.pull(_wallet, True)\n                self.so.download_finished(True)\n                self._app.appController.userNotify.emit(_('Labels downloaded'))\n            except Exception as e:\n                self.logger.error(repr(e))\n                self.so.download_finished(False)\n                self._app.appController.userNotify.emit(repr(e))\n\n        threading.Thread(target=pull_thread, args=[wallet]).start()\n\n    def on_pulled(self, wallet):\n        _wallet = QEWallet.getInstanceFor(wallet)\n        self.logger.debug('wallet ' + ('found' if _wallet else 'not found'))\n\n    @hook\n    def init_qml(self, app):\n        self.logger.debug(f'init_qml hook called, gui={str(type(app))}')\n        self.logger.debug(f'app={self._app!r}, so={self.so!r}')\n        self._app = app\n        # important: QSignalObject needs to be parented, as keeping a ref\n        # in the plugin is not enough to avoid gc\n        self.so = Plugin.QSignalObject(self, self._app)\n\n        # If the user just enabled the plugin, the 'load_wallet' hook would not\n        # get called for already loaded wallets, hence we call it manually for those:\n        for wallet_name, wallet in app.daemon.daemon._wallets.items():\n            self.load_wallet(wallet)\n"
  },
  {
    "path": "electrum/plugins/labels/qt.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nfrom functools import partial\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import QObject, pyqtSignal\n\nfrom electrum.plugin import hook\nfrom electrum.i18n import _\n\nfrom electrum.gui.common_qt.util import TaskThread\nfrom electrum.gui.qt.util import read_QIcon_from_bytes\n\nfrom .labels import LabelsPlugin\n\nif TYPE_CHECKING:\n    from electrum.gui.qt.main_window import ElectrumWindow\n    from electrum.wallet import Abstract_Wallet\n\n\nclass QLabelsSignalObject(QObject):\n    labels_changed_signal = pyqtSignal(object)\n\n\nclass Plugin(LabelsPlugin):\n\n    def __init__(self, *args):\n        LabelsPlugin.__init__(self, *args)\n        self.obj = QLabelsSignalObject()\n        self._init_qt_received = False\n\n    @hook\n    def init_menubar(self, window: 'ElectrumWindow'):\n        wallet = window.wallet\n        if not wallet.get_fingerprint():\n            return\n        m = window.wallet_menu.addMenu('LabelSync')\n        icon = read_QIcon_from_bytes(self.read_file('labelsync.png'))\n        m.setIcon(icon)\n        m.addAction(\"Force upload\", lambda: self.do_push(window))\n        m.addAction(\"Force download\", lambda: self.do_pull(window))\n\n    def do_push(self, window: 'ElectrumWindow'):\n        thread = TaskThread(window)\n        thread.add(\n            partial(self.push, window.wallet),\n            partial(self.done_processing_success, window),\n            thread.stop,\n            partial(self.done_processing_error, window))\n\n    def do_pull(self, window: 'ElectrumWindow'):\n        thread = TaskThread(window)\n        thread.add(\n            partial(self.pull, window.wallet, True),\n            partial(self.done_processing_success, window),\n            thread.stop,\n            partial(self.done_processing_error, window))\n\n    def on_pulled(self, wallet: 'Abstract_Wallet'):\n        self.obj.labels_changed_signal.emit(wallet)\n\n    def done_processing_success(self, dialog, result):\n        dialog.show_message(_(\"Your labels have been synchronised.\"))\n\n    def done_processing_error(self, dialog, exc_info):\n        self.logger.error(\"Error synchronising labels\", exc_info=exc_info)\n        dialog.show_error(_(\"Error synchronising labels\") + f':\\n{repr(exc_info[1])}')\n\n    @hook\n    def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'):\n        self.obj.labels_changed_signal.connect(window.update_tabs)\n        self.start_wallet(wallet)\n\n    @hook\n    def on_close_window(self, window):\n        try:\n            self.obj.labels_changed_signal.disconnect(window.update_tabs)\n        except TypeError:\n            pass  # 'method' object is not connected\n        self.stop_wallet(window.wallet)\n"
  },
  {
    "path": "electrum/plugins/ledger/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/plugins/ledger/cmdline.py",
    "content": "from electrum.plugin import hook\nfrom electrum.hw_wallet import CmdLineHandler\n\nfrom .ledger import LedgerPlugin\n\nclass Plugin(LedgerPlugin):\n    handler = CmdLineHandler()\n    @hook\n    def init_keystore(self, keystore):\n        if not isinstance(keystore, self.keystore_class):\n            return\n        keystore.handler = self.handler\n\n    def create_handler(self, window):\n        return self.handler\n"
  },
  {
    "path": "electrum/plugins/ledger/ledger.py",
    "content": "# Some parts of this code are adapted from bitcoin-core/HWI:\n# https://github.com/bitcoin-core/HWI/blob/e731395bde13362950e9f13e01689c475545e4dc/hwilib/devices/ledger.py\n\nfrom abc import ABC, abstractmethod\nimport base64\nimport hashlib\nfrom typing import Dict, List, Optional, Sequence, Tuple, TYPE_CHECKING, Union\n\nimport electrum_ecc as ecc\n\nfrom electrum import bip32, constants\nfrom electrum import descriptor\nfrom electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, normalize_bip32_derivation\nfrom electrum.bitcoin import EncodeBase58Check, is_b58_address, is_segwit_script_type, var_int\nfrom electrum.crypto import hash_160\nfrom electrum.i18n import _\nfrom electrum.keystore import Hardware_KeyStore\nfrom electrum.logging import get_logger\nfrom electrum.plugin import Device, runs_in_hwd_thread\nfrom electrum.transaction import PartialTransaction, Transaction, PartialTxInput\nfrom electrum.util import bfh, UserFacingException, versiontuple\nfrom electrum.wallet import Standard_Wallet\n\nfrom electrum.hw_wallet import HardwareClientBase, HW_PluginBase\nfrom electrum.hw_wallet.plugin import is_any_tx_output_on_change_branch, validate_op_return_output, LibraryFoundButUnusable\nfrom electrum.hw_wallet.plugin import HardwareClientDummy\n\nif TYPE_CHECKING:\n    from electrum.plugin import DeviceInfo\n    from electrum.wizard import NewWalletWizard\n\n_logger = get_logger(__name__)\n\n\ntry:\n    import ledger_bitcoin\n    from ledger_bitcoin import WalletPolicy, MultisigWallet, AddressType, Chain\n    from ledger_bitcoin.exception.errors import DenyError, NotSupportedError, SecurityStatusNotSatisfiedError\n    from ledger_bitcoin.key import KeyOriginInfo\n    from ledgercomm.interfaces.hid_device import HID\n\n    # legacy imports\n    import hid\n    from ledger_bitcoin.btchip.btchipComm import HIDDongleHIDAPI\n    from ledger_bitcoin.btchip.btchip import btchip\n    from ledger_bitcoin.btchip.btchipUtils import compress_public_key\n    from ledger_bitcoin.btchip.bitcoinTransaction import bitcoinTransaction\n    from ledger_bitcoin.btchip.btchipException import BTChipException\n\n    LEDGER_BITCOIN = True\nexcept ImportError as e:\n    if not (isinstance(e, ModuleNotFoundError) and e.name == 'ledger_bitcoin'):\n        _logger.exception('error importing ledger plugin deps')\n\n    LEDGER_BITCOIN = False\n\n\nMSG_NEEDS_FW_UPDATE_GENERIC = _('Firmware version too old. Please update at') + \\\n    ' https://www.ledger.com'\nMSG_NEEDS_FW_UPDATE_SEGWIT = _('Firmware version (or \"Bitcoin\" app) too old for Segwit support. Please update at') + \\\n    ' https://www.ledger.com'\nMULTI_OUTPUT_SUPPORT = '1.1.4'\nSEGWIT_SUPPORT = '1.1.10'\nSEGWIT_SUPPORT_SPECIAL = '1.0.4'\nSEGWIT_TRUSTEDINPUTS = '1.4.0'\n\n\ndef is_policy_standard(wp: 'WalletPolicy', fpr: bytes, exp_coin_type: int) -> bool:\n    \"\"\"Returns True if the wallet policy can be used without registration.\"\"\"\n\n    if wp.name != \"\" or wp.n_keys != 1:\n        return False\n\n    key_info = wp.keys_info[0]\n\n    if key_info[0] != '[':\n        # no key origin info\n        return False\n\n    try:\n        key_orig_end = key_info.index(']')\n    except ValueError:\n        # invalid key_info\n        return False\n\n    key_fpr, key_path = key_info[1:key_orig_end].split('/', maxsplit=1)\n\n    if key_fpr != fpr.hex():\n        # not an internal key\n        return False\n\n    key_path_parts = key_path.split('/')\n\n    # Account key should be exactly 3 hardened derivation steps\n    if len(key_path_parts) != 3 or any(part[-1] != \"'\" for part in key_path_parts):\n        return False\n\n    purpose, coin_type, account_index = key_path_parts\n\n    if coin_type != f\"{exp_coin_type}'\" or int(account_index[:-1]) > 100:\n        return False\n\n    if wp.descriptor_template == \"pkh(@0/**)\":\n        # BIP-44\n        return purpose == \"44'\"\n    elif wp.descriptor_template == \"sh(wpkh(@0/**))\":\n        # BIP-49, nested SegWit\n        return purpose == \"49'\"\n    elif wp.descriptor_template == \"wpkh(@0/**)\":\n        # BIP-84, native SegWit\n        return purpose == \"84'\"\n    elif wp.descriptor_template == \"tr(@0/**)\":\n        # BIP-86, taproot single key\n        return purpose == \"86'\"\n    else:\n        # unknown\n        return False\n\n\ndef convert_xpub(xpub: str, xtype='standard') -> str:\n    bip32node = BIP32Node.from_xkey(xpub)\n    return BIP32Node(\n        xtype=xtype,\n        eckey=bip32node.eckey,\n        chaincode=bip32node.chaincode,\n        depth=bip32node.depth,\n        fingerprint=bip32node.fingerprint,\n        child_number=bip32node.child_number).to_xpub()\n\n\ndef test_pin_unlocked(func):\n    \"\"\"Function decorator to test the Ledger for being unlocked, and if not,\n    raise a human-readable exception.\n    \"\"\"\n    def catch_exception(self, *args, **kwargs):\n        try:\n            return func(self, *args, **kwargs)\n        except SecurityStatusNotSatisfiedError:\n            raise UserFacingException(_('Your Ledger is locked. Please unlock it.'))\n        except OSError as e:\n            _logger.exception('')\n            raise UserFacingException(\n                _('Communication with Ledger failed. Open the Bitcoin app and try again.') + f'\\n{str(e)}',\n            )\n    return catch_exception\n\n\n# from HWI\ndef is_witness(script: bytes) -> Tuple[bool, int, bytes]:\n    \"\"\"\n    Determine whether a script is a segwit output script.\n    If so, also returns the witness version and witness program.\n\n    :param script: The script\n    :returns: A tuple of a bool indicating whether the script is a segwit output script,\n        an int representing the witness version,\n        and the bytes of the witness program.\n    \"\"\"\n    if len(script) < 4 or len(script) > 42:\n        return (False, 0, b\"\")\n\n    if script[0] != 0 and (script[0] < 81 or script[0] > 96):\n        return (False, 0, b\"\")\n\n    if script[1] + 2 == len(script):\n        return (True, script[0] - 0x50 if script[0] else 0, script[2:])\n\n    return (False, 0, b\"\")\n\n\n# from HWI\n# Only handles up to 15 of 15. Returns None if this script is not a\n# multisig script. Returns (m, pubkeys) otherwise.\ndef parse_multisig(script: bytes) -> Optional[Tuple[int, Sequence[bytes]]]:\n    \"\"\"\n    Determine whether a script is a multisig script. If so, determine the parameters of that multisig.\n\n    :param script: The script\n    :returns: ``None`` if the script is not multisig.\n        If multisig, returns a tuple of the number of signers required,\n        and a sequence of public key bytes.\n    \"\"\"\n    # Get m\n    m = script[0] - 80\n    if m < 1 or m > 15:\n        return None\n\n    # Get pubkeys\n    pubkeys = []\n    offset = 1\n    while True:\n        pubkey_len = script[offset]\n        if pubkey_len != 33:\n            break\n        offset += 1\n        pubkeys.append(script[offset:offset + 33])\n        offset += 33\n\n    # Check things at the end\n    n = script[offset] - 80\n    if n != len(pubkeys):\n        return None\n    offset += 1\n    op_cms = script[offset]\n    if op_cms != 174:\n        return None\n\n    return (m, pubkeys)\n\n\nHARDENED_FLAG = 1 << 31\n\n\ndef H_(x: int) -> int:\n    \"\"\"\n    Shortcut function that \"hardens\" a number in a BIP44 path.\n    \"\"\"\n    return x | HARDENED_FLAG\n\n\ndef is_hardened(i: int) -> bool:\n    \"\"\"\n    Returns whether an index is hardened\n    \"\"\"\n    return i & HARDENED_FLAG != 0\n\n\ndef get_bip44_purpose(addrtype: 'AddressType') -> int:\n    \"\"\"\n    Determine the BIP 44 purpose based on the given :class:`~hwilib.common.AddressType`.\n\n    :param addrtype: The address type\n    \"\"\"\n    if addrtype == AddressType.LEGACY:\n        return 44\n    elif addrtype == AddressType.SH_WIT:\n        return 49\n    elif addrtype == AddressType.WIT:\n        return 84\n    elif addrtype == AddressType.TAP:\n        return 86\n    else:\n        raise ValueError(\"Unknown address type\")\n\n\ndef get_bip44_chain(chain: 'Chain') -> int:\n    \"\"\"\n    Determine the BIP 44 coin type based on the Bitcoin chain type.\n\n    For the Bitcoin mainnet chain, this returns 0. For the other chains, this returns 1.\n\n    :param chain: The chain\n    \"\"\"\n    if chain == Chain.MAIN:\n        return 0\n    else:\n        return 1\n\n\ndef get_addrtype_from_bip44_purpose(index: int) -> Optional['AddressType']:\n    purpose = index & ~HARDENED_FLAG\n\n    if purpose == 44:\n        return AddressType.LEGACY\n    elif purpose == 49:\n        return AddressType.SH_WIT\n    elif purpose == 84:\n        return AddressType.WIT\n    elif purpose == 86:\n        return AddressType.TAP\n    else:\n        return None\n\n\ndef is_standard_path(\n    path: Sequence[int],\n    addrtype: 'AddressType',\n    chain: 'Chain',\n) -> bool:\n    if len(path) != 5:\n        return False\n    if not is_hardened(path[0]) or not is_hardened(path[1]) or not is_hardened(path[2]):\n        return False\n    if is_hardened(path[3]) or is_hardened(path[4]):\n        return False\n    computed_addrtype = get_addrtype_from_bip44_purpose(path[0])\n    if computed_addrtype is None:\n        return False\n    if computed_addrtype != addrtype:\n        return False\n    if path[1] != H_(get_bip44_chain(chain)):\n        return False\n    if path[3] not in [0, 1]:\n        return False\n    return True\n\n\ndef get_chain() -> 'Chain':\n    if constants.net.NET_NAME == \"mainnet\":\n        return Chain.MAIN\n    elif constants.net.NET_NAME == \"testnet\":\n        return Chain.TEST\n    elif constants.net.NET_NAME == \"signet\":\n        return Chain.SIGNET\n    elif constants.net.NET_NAME == \"regtest\":\n        return Chain.REGTEST\n    else:\n        raise ValueError(\"Unsupported network\")\n\n\nclass Ledger_Client(HardwareClientBase, ABC):\n    is_legacy: bool\n\n    @staticmethod\n    def construct_new(\n        *args, device: Device, plugin: 'LedgerPlugin', **kwargs,\n    ) -> Union['Ledger_Client', HardwareClientDummy]:\n        \"\"\"The 'real' constructor, that automatically decides which subclass to use.\"\"\"\n        if LedgerPlugin.is_hw1(device.product_key):\n            return HardwareClientDummy(\n                plugin=plugin,\n                error_text=\"ledger hw.1 devices are no longer supported\",\n            )\n        # for nano S or newer hw, decide which client impl to use based on software/firmware version:\n        hid_device = HID()\n        hid_device.path = device.path\n        hid_device.open()\n        transport = ledger_bitcoin.TransportClient('hid', hid=hid_device)\n        try:\n            cl = ledger_bitcoin.createClient(transport, chain=get_chain())\n        except (ledger_bitcoin.exception.errors.InsNotSupportedError,\n                ledger_bitcoin.exception.errors.ClaNotSupportedError) as e:\n            # This can happen on very old versions.\n            # E.g. with a \"nano s\", with bitcoin app 1.1.10, SE 1.3.1, MCU 1.0,\n            #      - on machine one, ghost43 got InsNotSupportedError\n            #      - on machine two, thomasv got ClaNotSupportedError\n            #      unclear why the different exceptions, ledger_bitcoin version 0.2.1 in both cases\n            _logger.info(f\"ledger_bitcoin.createClient() got exc: {e}. falling back to old plugin.\")\n            cl = None\n        if isinstance(cl, ledger_bitcoin.client.NewClient):\n            _logger.debug(f\"Ledger_Client.construct_new(). creating NewClient for {device=}.\")\n            return Ledger_Client_New(hid_device, *args, plugin=plugin, **kwargs)\n        else:\n            _logger.debug(f\"Ledger_Client.construct_new(). creating LegacyClient for {device=}.\")\n            return Ledger_Client_Legacy(hid_device, *args, plugin=plugin, **kwargs)\n\n    def __init__(self, *, plugin: HW_PluginBase):\n        HardwareClientBase.__init__(self, plugin=plugin)\n\n    def get_master_fingerprint(self) -> bytes:\n        return self.request_root_fingerprint_from_device()\n\n    @abstractmethod\n    def show_address(self, address_path: str, txin_type: str):\n        pass\n\n    @abstractmethod\n    def sign_transaction(self, keystore: Hardware_KeyStore, tx: PartialTransaction, password: str):\n        pass\n\n    @abstractmethod\n    def sign_message(\n            self,\n            address_path: str,\n            message: str,\n            password,\n            *,\n            script_type: Optional[str] = None,\n    ) -> bytes:\n        pass\n\n\nclass Ledger_Client_Legacy(Ledger_Client):\n    \"\"\"Client based on the bitchip library, targeting versions 2.0.* and below.\"\"\"\n    is_legacy = True\n\n    def __init__(self, hidDevice: 'HID', *, product_key: Tuple[int, int],\n                 plugin: HW_PluginBase):\n        Ledger_Client.__init__(self, plugin=plugin)\n\n        # Hack, we close the old object and instantiate a new one\n        hidDevice.close()\n        dev = hid.device()\n        dev.open_path(hidDevice.path)\n        dev.set_nonblocking(True)\n        self.dongleObject = btchip(HIDDongleHIDAPI(dev, True, False))\n\n        self.signing = False\n\n        self._product_key = product_key\n        self._soft_device_id = None\n\n    def is_pairable(self):\n        return True\n\n    def set_and_unset_signing(func):\n        \"\"\"Function decorator to set and unset self.signing.\"\"\"\n        def wrapper(self, *args, **kwargs):\n            try:\n                self.signing = True\n                return func(self, *args, **kwargs)\n            finally:\n                self.signing = False\n        return wrapper\n\n    def give_error(self, message: str | BaseException):\n        _logger.info(message)\n        if not self.signing:\n            self.handler.show_error(str(message))\n        else:\n            self.signing = False\n        raise UserFacingException(message)\n\n    @runs_in_hwd_thread\n    def close(self):\n        self.dongleObject.dongle.close()\n\n    def is_initialized(self):\n        return True\n\n    @runs_in_hwd_thread\n    def get_soft_device_id(self):\n        if self._soft_device_id is None:\n            # modern ledger can provide xpub without user interaction\n            # (hw1 would prompt for PIN)\n            if not self.is_hw1():\n                self._soft_device_id = self.request_root_fingerprint_from_device()\n        return self._soft_device_id\n\n    def is_hw1(self) -> bool:\n        return LedgerPlugin.is_hw1(self._product_key)\n\n    def device_model_name(self):\n        return LedgerPlugin.device_name_from_product_key(self._product_key)\n\n    @runs_in_hwd_thread\n    def has_usable_connection_with_device(self):\n        try:\n            self.dongleObject.getFirmwareVersion()\n        except BaseException:\n            return False\n        return True\n\n    @runs_in_hwd_thread\n    @test_pin_unlocked\n    def get_xpub(self, bip32_path, xtype):\n        self.checkDevice()\n        # bip32_path is of the form 44'/0'/1'\n        # S-L-O-W - we don't handle the fingerprint directly, so compute\n        # it manually from the previous node\n        # This only happens once so it's bearable\n        # self.get_client() # prompt for the PIN before displaying the dialog if necessary\n        # self.handler.show_message(\"Computing master public key\")\n        if xtype in ['p2wpkh', 'p2wsh'] and not self.supports_native_segwit():\n            raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT)\n        if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit():\n            raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT)\n        bip32_path = bip32.normalize_bip32_derivation(bip32_path, hardened_char=\"'\")\n        bip32_intpath = bip32.convert_bip32_strpath_to_intpath(bip32_path)\n        bip32_path = bip32_path[2:]  # cut off \"m/\"\n        if len(bip32_intpath) >= 1:\n            prevPath = bip32.convert_bip32_intpath_to_strpath(bip32_intpath[:-1])[2:]\n            nodeData = self.dongleObject.getWalletPublicKey(prevPath)\n            publicKey = compress_public_key(nodeData['publicKey'])\n            fingerprint_bytes = hash_160(publicKey)[0:4]\n            childnum_bytes = bip32_intpath[-1].to_bytes(length=4, byteorder=\"big\")\n        else:\n            fingerprint_bytes = bytes(4)\n            childnum_bytes = bytes(4)\n        nodeData = self.dongleObject.getWalletPublicKey(bip32_path)\n        publicKey = compress_public_key(nodeData['publicKey'])\n        depth = len(bip32_intpath)\n        return BIP32Node(xtype=xtype,\n                         eckey=ecc.ECPubkey(bytes(publicKey)),\n                         chaincode=nodeData['chainCode'],\n                         depth=depth,\n                         fingerprint=fingerprint_bytes,\n                         child_number=childnum_bytes).to_xpub()\n\n    def has_detached_pin_support(self, client: 'btchip'):\n        try:\n            client.getVerifyPinRemainingAttempts()\n            return True\n        except BTChipException as e:\n            if e.sw == 0x6d00:\n                return False\n            raise e\n\n    def is_pin_validated(self, client: 'btchip'):\n        try:\n            # Invalid SET OPERATION MODE to verify the PIN status\n            client.dongle.exchange(bytearray([0xe0, 0x26, 0x00, 0x00, 0x01, 0xAB]))\n        except BTChipException as e:\n            if (e.sw == 0x6982):\n                return False\n            if (e.sw == 0x6A80):\n                return True\n            raise e\n\n    def supports_multi_output(self):\n        return self.multiOutputSupported\n\n    def supports_segwit(self):\n        return self.segwitSupported\n\n    def supports_native_segwit(self):\n        return self.nativeSegwitSupported\n\n    def supports_segwit_trustedInputs(self):\n        return self.segwitTrustedInputs\n\n    @runs_in_hwd_thread\n    def checkDevice(self):\n        firmwareInfo = self.dongleObject.getFirmwareVersion()\n        firmware = firmwareInfo['version']\n        self.multiOutputSupported = versiontuple(firmware) >= versiontuple(MULTI_OUTPUT_SUPPORT)\n        self.nativeSegwitSupported = versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT)\n        self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT_SPECIAL))\n        self.segwitTrustedInputs = versiontuple(firmware) >= versiontuple(SEGWIT_TRUSTEDINPUTS)\n\n    def password_dialog(self, msg=None):\n        response = self.handler.get_word(msg)\n        if response is None:\n            return False, None, None\n        return True, response, response\n\n    @runs_in_hwd_thread\n    @test_pin_unlocked\n    @set_and_unset_signing\n    def show_address(self, address_path: str, txin_type: str):\n        self.handler.show_message(_(\"Showing address ...\"))\n        segwit = is_segwit_script_type(txin_type)\n        segwitNative = txin_type == 'p2wpkh'\n        try:\n            self.dongleObject.getWalletPublicKey(address_path, showOnScreen=True, segwit=segwit, segwitNative=segwitNative)\n        except BTChipException as e:\n            if e.sw == 0x6985:  # cancelled by user\n                pass\n            elif e.sw == 0x6982:\n                raise  # pin lock. decorator will catch it\n            elif e.sw == 0x6b00:  # hw.1 raises this\n                self.handler.show_error('{}\\n{}\\n{}'.format(\n                    _('Error showing address') + ':',\n                    e,\n                    _('Your device might not have support for this functionality.')))\n            else:\n                _logger.exception('')\n                self.handler.show_error(str(e))\n        except BaseException as e:\n            _logger.exception('')\n            self.handler.show_error(str(e))\n        finally:\n            self.handler.finished()\n\n    @runs_in_hwd_thread\n    @test_pin_unlocked\n    @set_and_unset_signing\n    def sign_transaction(self, keystore: Hardware_KeyStore, tx: PartialTransaction, password: str):\n        if tx.is_complete():\n            return\n\n        inputs = []\n        inputsPaths = []\n        chipInputs = []\n        redeemScripts = []\n        changePath = \"\"\n        p2shTransaction = False\n        segwitTransaction = False\n        pin = \"\"\n        # prompt for the PIN before displaying the dialog if necessary\n\n        def is_txin_legacy_multisig(txin: PartialTxInput) -> bool:\n            desc = txin.script_descriptor\n            return (isinstance(desc, descriptor.SHDescriptor)\n                    and isinstance(desc.subdescriptors[0], descriptor.MultisigDescriptor))\n\n        # Fetch inputs of the transaction to sign\n        for txin in tx.inputs():\n            if txin.is_coinbase_input():\n                self.give_error(\"Coinbase not supported\")     # should never happen\n\n            if is_txin_legacy_multisig(txin):\n                p2shTransaction = True\n\n            if txin.is_p2sh_segwit():\n                if not self.supports_segwit():\n                    self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)\n                segwitTransaction = True\n\n            if txin.is_native_segwit():\n                if not self.supports_native_segwit():\n                    self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)\n                segwitTransaction = True\n\n            my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)\n            if not full_path:\n                self.give_error(\"No matching pubkey for sign_transaction\")  # should never happen\n            full_path = convert_bip32_intpath_to_strpath(full_path)[2:]\n\n            redeemScript = txin.get_scriptcode_for_sighash().hex()\n            txin_prev_tx = txin.utxo\n            if txin_prev_tx is None and not txin.is_segwit():\n                raise UserFacingException(_('Missing previous tx for legacy input.'))\n            txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None\n            inputs.append([txin_prev_tx_raw,\n                           txin.prevout.out_idx,\n                           redeemScript,\n                           txin.prevout.txid.hex(),\n                           my_pubkey,\n                           txin.nsequence,\n                           txin.value_sats()])\n            inputsPaths.append(full_path)\n\n        # Sanity check\n        if p2shTransaction:\n            for txin in tx.inputs():\n                if not is_txin_legacy_multisig(txin):\n                    self.give_error(\"P2SH / regular input mixed in same transaction not supported\")  # should never happen\n\n        if not self.supports_multi_output():\n            if len(tx.outputs()) > 2:\n                self.give_error(\"Transaction with more than 2 outputs not supported\")\n        for txout in tx.outputs():\n            if not txout.address:\n                # note: max_size based on https://github.com/LedgerHQ/ledger-app-btc/commit/3a78dee9c0484821df58975803e40d58fbfc2c38#diff-c61ccd96a6d8b54d48f54a3bc4dfa7e2R26\n                validate_op_return_output(txout, max_size=190)\n\n        # Output \"change\" detection\n        # - at most one output can bypass confirmation (~change)\n        if not p2shTransaction:\n            has_change = False\n            any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)\n            for txout in tx.outputs():\n                if txout.is_mine and len(tx.outputs()) > 1 \\\n                        and not has_change:\n                    # prioritise hiding outputs on the 'change' branch from user\n                    # because no more than one change address allowed\n                    if txout.is_change == any_output_on_change_branch:\n                        my_pubkey, changePath = keystore.find_my_pubkey_in_txinout(txout)\n                        assert changePath\n                        changePath = convert_bip32_intpath_to_strpath(changePath)[2:]\n                        has_change = True\n\n        try:\n            # Get trusted inputs from the original transactions\n            for input_idx, utxo in enumerate(inputs):\n                self.handler.show_message(_(\"Preparing transaction inputs...\") + f\" (phase1, {input_idx}/{len(inputs)})\")\n                sequence = int.to_bytes(utxo[5], length=4, byteorder=\"little\", signed=False).hex()\n                if segwitTransaction and not self.supports_segwit_trustedInputs():\n                    tmp = bfh(utxo[3])[::-1]\n                    tmp += int.to_bytes(utxo[1], length=4, byteorder=\"little\", signed=False)\n                    tmp += int.to_bytes(utxo[6], length=8, byteorder=\"little\", signed=False)  # txin['value']\n                    chipInputs.append({'value': tmp, 'witness': True, 'sequence': sequence})\n                    redeemScripts.append(bfh(utxo[2]))\n                elif (not p2shTransaction) or self.supports_multi_output():\n                    txtmp = bitcoinTransaction(bfh(utxo[0]))\n                    trustedInput = self.dongleObject.getTrustedInput(txtmp, utxo[1])\n                    trustedInput['sequence'] = sequence\n                    if segwitTransaction:\n                        trustedInput['witness'] = True\n                    chipInputs.append(trustedInput)\n                    if p2shTransaction or segwitTransaction:\n                        redeemScripts.append(bfh(utxo[2]))\n                    else:\n                        redeemScripts.append(txtmp.outputs[utxo[1]].script)\n                else:\n                    tmp = bfh(utxo[3])[::-1]\n                    tmp += int.to_bytes(utxo[1], length=4, byteorder=\"little\", signed=False)\n                    chipInputs.append({'value': tmp, 'sequence': sequence})\n                    redeemScripts.append(bfh(utxo[2]))\n\n            self.handler.show_message(_(\"Confirm Transaction on your Ledger device...\"))\n            # Sign all inputs\n            firstTransaction = True\n            inputIndex = 0\n            rawTx = tx.serialize_to_network(include_sigs=False)\n            if segwitTransaction:\n                self.dongleObject.startUntrustedTransaction(True, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version)\n                # we don't set meaningful outputAddress, amount and fees\n                # as we only care about the alternateEncoding==True branch\n                outputData = self.dongleObject.finalizeInput(b'', 0, 0, changePath, bfh(rawTx))\n                while inputIndex < len(inputs):\n                    self.handler.show_message(_(\"Signing transaction...\") + f\" (phase2, {inputIndex}/{len(inputs)})\")\n                    singleInput = [chipInputs[inputIndex]]\n                    self.dongleObject.startUntrustedTransaction(False, 0,\n                                                                singleInput, redeemScripts[inputIndex], version=tx.version)\n                    inputSignature = self.dongleObject.untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)\n                    inputSignature[0] = 0x30  # force for 1.4.9+\n                    my_pubkey = inputs[inputIndex][4]\n                    tx.add_signature_to_txin(txin_idx=inputIndex,\n                                             signing_pubkey=my_pubkey,\n                                             sig=inputSignature)\n                    inputIndex = inputIndex + 1\n            else:\n                while inputIndex < len(inputs):\n                    self.handler.show_message(_(\"Signing transaction...\") + f\" (phase2, {inputIndex}/{len(inputs)})\")\n                    self.dongleObject.startUntrustedTransaction(firstTransaction, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version)\n                    # we don't set meaningful outputAddress, amount and fees\n                    # as we only care about the alternateEncoding==True branch\n                    outputData = self.dongleObject.finalizeInput(b'', 0, 0, changePath, bfh(rawTx))\n                    # Sign input with the provided PIN\n                    inputSignature = self.dongleObject.untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)\n                    inputSignature[0] = 0x30  # force for 1.4.9+\n                    my_pubkey = inputs[inputIndex][4]\n                    tx.add_signature_to_txin(txin_idx=inputIndex,\n                                             signing_pubkey=my_pubkey,\n                                             sig=inputSignature)\n                    inputIndex = inputIndex + 1\n                    firstTransaction = False\n        except UserWarning:\n            self.handler.show_error(_('Cancelled by user'))\n            return\n        except BTChipException as e:\n            if e.sw in (0x6985, 0x6d00):  # cancelled by user\n                return\n            elif e.sw == 0x6982:\n                raise  # pin lock. decorator will catch it\n            else:\n                _logger.exception('')\n                self.give_error(e)\n        except BaseException as e:\n            _logger.exception('')\n            self.give_error(e)\n        finally:\n            self.handler.finished()\n\n    @runs_in_hwd_thread\n    @test_pin_unlocked\n    @set_and_unset_signing\n    def sign_message(\n            self,\n            address_path: str,\n            message: str,\n            password,\n            *,\n            script_type: Optional[str] = None,\n    ) -> bytes:\n        message = message.encode('utf8')\n        message_hash = hashlib.sha256(message).hexdigest().upper()\n\n        self.handler.show_message(\"Signing message ...\\r\\nMessage hash: \" + message_hash)\n        try:\n            info = self.dongleObject.signMessagePrepare(address_path, message)\n            pin = \"\"\n            signature = self.dongleObject.signMessageSign(pin)\n        except BTChipException as e:\n            if e.sw == 0x6a80:\n                self.give_error(\"Unfortunately, this message cannot be signed by the Ledger wallet. \"\n                                \"Only alphanumerical messages shorter than 140 characters are supported. \"\n                                \"Please remove any extra characters (tab, carriage return) and retry.\")\n            elif e.sw == 0x6985:  # cancelled by user\n                return b''\n            elif e.sw == 0x6982:\n                raise  # pin lock. decorator will catch it\n            else:\n                self.give_error(e)\n        except UserWarning:\n            self.handler.show_error(_('Cancelled by user'))\n            return b''\n        except Exception as e:\n            self.give_error(e)\n        finally:\n            self.handler.finished()\n        # Parse the ASN.1 signature\n        rLength = signature[3]\n        r = signature[4: 4 + rLength]\n        sLength = signature[4 + rLength + 1]\n        s = signature[4 + rLength + 2:]\n        if rLength == 33:\n            r = r[1:]\n        if sLength == 33:\n            s = s[1:]\n        # And convert it\n\n        # Pad r and s points with 0x00 bytes when the point is small to get valid signature.\n        r_padded = bytes([0x00]) * (32 - len(r)) + r\n        s_padded = bytes([0x00]) * (32 - len(s)) + s\n\n        return bytes([27 + 4 + (signature[0] & 0x01)]) + r_padded + s_padded\n\n\nclass Ledger_Client_New(Ledger_Client):\n    \"\"\"Client based on the ledger_bitcoin library, targeting versions 2.1.* and above.\"\"\"\n\n    is_legacy = False\n\n    def __init__(self, hidDevice: 'HID', *, product_key: Tuple[int, int],\n                 plugin: HW_PluginBase):\n        Ledger_Client.__init__(self, plugin=plugin)\n\n        transport = ledger_bitcoin.TransportClient('hid', hid=hidDevice)\n        self.client = ledger_bitcoin.client.NewClient(transport, get_chain())\n\n        self._product_key = product_key\n        self._soft_device_id = None\n\n        self.master_fingerprint = None\n\n        self._known_xpubs: Dict[str, str] = {}  # path ==> xpub\n        self._registered_policies: Dict[bytes, bytes] = {}  # wallet id => wallet hmac\n\n    def is_pairable(self):\n        return True\n\n    @runs_in_hwd_thread\n    def close(self):\n        self.client.stop()\n\n    def is_initialized(self):\n        return True\n\n    @runs_in_hwd_thread\n    def get_soft_device_id(self):\n        if self._soft_device_id is None:\n            self._soft_device_id = self.request_root_fingerprint_from_device()\n        return self._soft_device_id\n\n    def device_model_name(self):\n        return LedgerPlugin.device_name_from_product_key(self._product_key)\n\n    @runs_in_hwd_thread\n    def has_usable_connection_with_device(self):\n        try:\n            self.client.get_version()\n        except BaseException:\n            return False\n        return True\n\n    @runs_in_hwd_thread\n    @test_pin_unlocked\n    def get_xpub(self, bip32_path: str, xtype):\n        # try silently first; if not a standard path, repeat with on-screen display\n\n        bip32_path = normalize_bip32_derivation(bip32_path, hardened_char=\"'\")\n\n        # cache known path/xpubs combinations in order to avoid requesting them many times\n        if bip32_path in self._known_xpubs:\n            xpub = self._known_xpubs[bip32_path]\n        else:\n            try:\n                xpub = self.client.get_extended_pubkey(bip32_path)\n            except NotSupportedError:\n                xpub = self.client.get_extended_pubkey(bip32_path, True)\n            self._known_xpubs[bip32_path] = xpub\n\n        # Ledger always returns 'standard' xpubs; convert to the right xtype\n        return convert_xpub(xpub, xtype)\n\n    @runs_in_hwd_thread\n    def request_root_fingerprint_from_device(self) -> str:\n        return self.client.get_master_fingerprint().hex()\n\n    @runs_in_hwd_thread\n    @test_pin_unlocked\n    def get_master_fingerprint(self) -> bytes:\n        if self.master_fingerprint is None:\n            self.master_fingerprint = self.client.get_master_fingerprint()\n        return self.master_fingerprint\n\n    @runs_in_hwd_thread\n    @test_pin_unlocked\n    def get_singlesig_default_wallet_policy(self, addr_type: 'AddressType', account: int) -> 'WalletPolicy':\n        assert account >= HARDENED_FLAG\n\n        if addr_type == AddressType.LEGACY:\n            template = \"pkh(@0/**)\"\n        elif addr_type == AddressType.WIT:\n            template = \"wpkh(@0/**)\"\n        elif addr_type == AddressType.SH_WIT:\n            template = \"sh(wpkh(@0/**))\"\n        elif addr_type == AddressType.TAP:\n            template = \"tr(@0/**)\"\n        else:\n            raise ValueError(\"Unknown address type\")\n\n        fpr = self.get_master_fingerprint()\n        key_origin_steps = f\"{get_bip44_purpose(addr_type)}'/{get_bip44_chain(self.client.chain)}'/{account & ~HARDENED_FLAG}'\"\n        xpub = self.get_xpub(f\"m/{key_origin_steps}\", 'standard')\n        key_str = f\"[{fpr.hex()}/{key_origin_steps}]{xpub}\"\n\n        # Make the Wallet object\n        return WalletPolicy(name=\"\", descriptor_template=template, keys_info=[key_str])\n\n    @runs_in_hwd_thread\n    @test_pin_unlocked\n    def get_singlesig_policy_for_path(self, path: str, xtype: str, master_fp: bytes) -> Optional['WalletPolicy']:\n        path = path.replace(\"h\", \"'\")\n        path_parts = path.split(\"/\")\n\n        if not 5 <= len(path_parts) <= 6:\n            raise UserFacingException(_('Unsupported derivation path: {}').format(path))\n\n        path_root = \"/\".join(path_parts[:-2])\n\n        fpr = self.get_master_fingerprint()\n\n        # Ledger always uses standard xpubs in wallet policies\n        xpub = self.get_xpub(f\"m/{path_root}\", 'standard')\n\n        key_info = f\"[{fpr.hex()}/{path_root}]{xpub}\"\n\n        if xtype == 'p2pkh':\n            name = \"Legacy P2PKH\"\n            descriptor_template = \"pkh(@0/**)\"\n        elif xtype == 'p2wpkh-p2sh':\n            name = \"Nested SegWit\"\n            descriptor_template = \"sh(wpkh(@0/**))\"\n        elif xtype == 'p2wpkh':\n            name = \"SegWit\"\n            descriptor_template = \"wpkh(@0/**)\"\n        elif xtype == 'p2tr':\n            name = \"Taproot\"\n            descriptor_template = \"tr(@0/**)\"\n        else:\n            return None\n\n        policy = WalletPolicy(\"\", descriptor_template, [key_info])\n        if is_policy_standard(policy, master_fp, constants.net.BIP44_COIN_TYPE):\n            return policy\n\n        # Non standard policy, so give it a name\n        return WalletPolicy(name, descriptor_template, [key_info])\n\n    def password_dialog(self, msg=None):\n        response = self.handler.get_word(msg)\n        if response is None:\n            return False, None, None\n        return True, response, response\n\n    def _register_policy_if_needed(self, wallet_policy: 'WalletPolicy') -> Tuple[bytes, bytes]:\n        # If the policy is not register, registers it and saves its hmac on success\n        # Returns the pair of wallet id and wallet hmac\n        if wallet_policy.id not in self._registered_policies:\n            wallet_id, wallet_hmac = self.client.register_wallet(wallet_policy)\n            assert wallet_id == wallet_policy.id\n            self._registered_policies[wallet_id] = wallet_hmac\n        return wallet_policy.id, self._registered_policies[wallet_policy.id]\n\n    @runs_in_hwd_thread\n    @test_pin_unlocked\n    def show_address(self, address_path: str, txin_type: str):\n        client_ledger = self.client\n        self.handler.show_message(_(\"Showing address ...\"))\n\n        # TODO: generalize for multisignature\n\n        try:\n            master_fp = client_ledger.get_master_fingerprint()\n            wallet_policy = self.get_singlesig_policy_for_path(address_path, txin_type, master_fp)\n\n            change, addr_index = [int(i) for i in address_path.split(\"/\")[-2:]]\n\n            wallet_hmac = None\n            if not is_policy_standard(wallet_policy, master_fp, constants.net.BIP44_COIN_TYPE):\n                wallet_id, wallet_hmac = self._register_policy_if_needed(wallet_policy)\n\n            self.client.get_wallet_address(wallet_policy, wallet_hmac, change, addr_index, True)\n        except DenyError:\n            pass  # cancelled by user\n        except BaseException as e:\n            _logger.exception('Error while showing an address')\n            self.handler.show_error(str(e))\n        finally:\n            self.handler.finished()\n\n    @runs_in_hwd_thread\n    @test_pin_unlocked\n    def sign_transaction(self, keystore: Hardware_KeyStore, tx: PartialTransaction, password: str):\n        if tx.is_complete():\n            return\n\n        # mostly adapted from HWI\n\n        psbt_bytes = tx.serialize_as_bytes()\n        psbt = ledger_bitcoin.client.PSBT()\n        psbt.deserialize(base64.b64encode(psbt_bytes).decode('ascii'))\n\n        try:\n\n            master_fp = self.client.get_master_fingerprint()\n\n            # Figure out which wallets are signing\n            wallets: Dict[bytes, Tuple[AddressType, WalletPolicy, Optional[bytes]]] = {}\n            for input_num, (electrum_txin, psbt_in) in enumerate(zip(tx.inputs(), psbt.inputs)):\n                if electrum_txin.is_coinbase_input():\n                    raise UserFacingException(_('Coinbase not supported'))     # should never happen\n\n                utxo = None\n                if psbt_in.witness_utxo:\n                    utxo = psbt_in.witness_utxo\n                if psbt_in.non_witness_utxo:\n                    if psbt_in.prev_txid != psbt_in.non_witness_utxo.hash:\n                        raise UserFacingException(_('Input {} has a non_witness_utxo with the wrong hash').format(input_num))\n                    assert psbt_in.prev_out is not None\n                    utxo = psbt_in.non_witness_utxo.vout[psbt_in.prev_out]\n\n                if utxo is None:\n                    continue\n                if (desc := electrum_txin.script_descriptor) is None:\n                    raise Exception(\"script_descriptor missing for txin \")\n                scriptcode = desc.expand().scriptcode_for_sighash\n\n                is_wit, wit_ver, __ = is_witness(psbt_in.redeem_script or utxo.scriptPubKey)\n\n                script_addrtype = AddressType.LEGACY\n                if is_wit:\n                    # if it's a segwit spend (any version), make sure the witness_utxo is also present\n                    psbt_in.witness_utxo = utxo\n\n                    if electrum_txin.is_p2sh_segwit():\n                        if wit_ver == 0:\n                            script_addrtype = AddressType.SH_WIT\n                        else:\n                            raise UserFacingException(_('Cannot have witness v1+ in p2sh'))\n                    else:\n                        if wit_ver == 0:\n                            script_addrtype = AddressType.WIT\n                        elif wit_ver == 1:\n                            script_addrtype = AddressType.TAP\n                        else:\n                            continue\n\n                multisig = parse_multisig(scriptcode)\n                if multisig is not None:\n                    k, ms_pubkeys = multisig\n\n                    # Figure out the parent xpubs\n                    key_exprs: List[str] = []\n                    ok = True\n                    our_keys = 0\n                    for pub in ms_pubkeys:\n                        if pub in psbt_in.hd_keypaths:\n                            pk_origin = psbt_in.hd_keypaths[pub]\n                            if pk_origin.fingerprint == master_fp:\n                                our_keys += 1\n\n                            for xpub_bytes, xpub_origin in psbt.xpub.items():\n                                xpub_str = EncodeBase58Check(xpub_bytes)\n                                if (xpub_origin.fingerprint == pk_origin.fingerprint) and (xpub_origin.path == pk_origin.path[:len(xpub_origin.path)]):\n                                    key_origin_full = pk_origin.to_string().replace('h', '\\'')\n                                    # strip last two steps of derivation\n                                    key_origin_parts = key_origin_full.split('/')\n                                    if len(key_origin_parts) < 3:\n                                        raise UserFacingException(_('Unable to sign this transaction'))\n                                    key_origin = '/'.join(key_origin_parts[:-2])\n\n                                    key_exprs.append(f\"[{key_origin}]{xpub_str}\")\n                                    break\n\n                            else:\n                                # No xpub, Ledger will not accept this multisig\n                                ok = False\n\n                    if not ok:\n                        continue\n\n                    # Electrum uses sortedmulti; we make sure that the array of key information is normalized in a consistent order\n                    key_exprs = list(sorted(key_exprs))\n\n                    # Make and register the MultisigWallet\n                    msw = MultisigWallet(f\"{k} of {len(key_exprs)} Multisig\", script_addrtype, k, key_exprs)\n                    msw_id = msw.id\n                    if msw_id not in wallets:\n                        __, registered_hmac = self._register_policy_if_needed(msw)\n                        wallets[msw_id] = (\n                            script_addrtype,\n                            msw,\n                            registered_hmac,\n                        )\n                else:\n                    def process_origin(origin: KeyOriginInfo, *, script_addrtype=script_addrtype) -> None:\n                        if is_standard_path(origin.path, script_addrtype, get_chain()):\n                            # these policies do not need to be registered\n                            policy = self.get_singlesig_default_wallet_policy(script_addrtype, origin.path[2])\n                            wallets[policy.id] = (\n                                script_addrtype,\n                                self.get_singlesig_default_wallet_policy(script_addrtype, origin.path[2]),\n                                None,  # Wallet hmac\n                            )\n                        else:\n                            # register the policy\n                            if script_addrtype == AddressType.LEGACY:\n                                name = \"Legacy\"\n                                template = \"pkh(@0/**)\"\n                            elif script_addrtype == AddressType.WIT:\n                                name = \"Native SegWit\"\n                                template = \"wpkh(@0/**)\"\n                            elif script_addrtype == AddressType.SH_WIT:\n                                name = \"Nested SegWit\"\n                                template = \"sh(wpkh(@0/**))\"\n                            elif script_addrtype == AddressType.TAP:\n                                name = \"Taproot\"\n                                template = \"tr(@0/**)\"\n                            else:\n                                raise ValueError(\"Unknown address type\")\n\n                            key_origin_info = origin.to_string()\n                            key_origin_steps = key_origin_info.replace('h', '\\'').split('/')[1:]\n                            if len(key_origin_steps) < 3:\n                                # Skip this input, not able to sign\n                                return\n\n                            # remove the last two steps\n                            account_key_origin = \"/\".join(key_origin_steps[:-2])\n\n                            # get the account-level xpub\n                            xpub = self.get_xpub(f\"m/{account_key_origin}\", 'standard')\n                            key_str = f\"[{master_fp.hex()}/{account_key_origin}]{xpub}\"\n\n                            policy = WalletPolicy(name, template, [key_str])\n                            __, registered_hmac = self.client.register_wallet(policy)\n                            wallets[policy.id] = (\n                                script_addrtype,\n                                policy,\n                                registered_hmac,\n                            )\n                    for key, origin in psbt_in.hd_keypaths.items():\n                        if origin.fingerprint == master_fp:\n                            process_origin(origin)\n\n                    for key, (__, origin) in psbt_in.tap_bip32_paths.items():\n                        # TODO: Support script path signing\n                        if key == psbt_in.tap_internal_key and origin.fingerprint == master_fp:\n                            process_origin(origin)\n\n            self.handler.show_message(_(\"Confirm Transaction on your Ledger device...\"))\n\n            if len(wallets) == 0:\n                # Could not find a WalletPolicy to sign with\n                raise UserFacingException(_('Unable to sign this transaction'))\n\n            # For each wallet, sign\n            for __, (__, wallet, wallet_hmac) in wallets.items():\n                input_sigs = self.client.sign_psbt(psbt, wallet, wallet_hmac)\n                for idx, part_sig in input_sigs:\n                    tx.add_signature_to_txin(\n                        txin_idx=idx, signing_pubkey=part_sig.pubkey, sig=part_sig.signature)\n        except DenyError:\n            pass  # cancelled by user\n        except BaseException as e:\n            _logger.exception('Error while signing')\n            self.handler.show_error(str(e))\n        finally:\n            self.handler.finished()\n\n    @runs_in_hwd_thread\n    @test_pin_unlocked\n    def sign_message(\n            self,\n            address_path: str,\n            message: str,\n            password,\n            *,\n            script_type: Optional[str] = None,\n    ) -> bytes:\n        message = message.encode('utf8')\n        message_hash = hashlib.sha256(message).hexdigest().upper()\n        # prompt for the PIN before displaying the dialog if necessary\n        self.handler.show_message(\"Signing message ...\\r\\nMessage hash: \" + message_hash)\n\n        result = b''\n        try:\n            sig_str = self.client.sign_message(message, address_path)\n            result = base64.b64decode(sig_str, validate=True)\n        except DenyError:\n            pass  # cancelled by user\n        except BaseException as e:\n            _logger.exception('')\n            self.handler.show_error(str(e))\n        finally:\n            self.handler.finished()\n\n        return result\n\n\nclass Ledger_KeyStore(Hardware_KeyStore):\n    \"\"\"Ledger keystore. Targets all versions, will have different behavior with different clients.\"\"\"\n\n    hw_type = 'ledger'\n    device = 'Ledger'\n\n    plugin: 'LedgerPlugin'\n\n    def __init__(self, d):\n        Hardware_KeyStore.__init__(self, d)\n        self.cfg = d.get('cfg', {'mode': 0})\n\n    def dump(self):\n        obj = Hardware_KeyStore.dump(self)\n        obj['cfg'] = self.cfg\n        return obj\n\n    def get_client_dongle_object(self, *, client: Optional[Ledger_Client] = None) -> Ledger_Client:\n        if client is None:\n            client = self.get_client()\n        return client\n\n    def decrypt_message(self, pubkey, message, password):\n        raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device))\n\n    def sign_message(self, sequence, *args, **kwargs):\n        address_path = self.get_derivation_prefix() + \"/%d/%d\" % sequence\n        address_path = normalize_bip32_derivation(address_path, hardened_char=\"'\")\n        address_path = address_path[2:]  # cut m/\n        return self.get_client_dongle_object().sign_message(address_path, *args, **kwargs)\n\n    def sign_transaction(self, *args, **kwargs):\n        return self.get_client_dongle_object().sign_transaction(self, *args, **kwargs)\n\n    def show_address(self, sequence, *args, **kwargs):\n        address_path = self.get_derivation_prefix() + \"/%d/%d\" % sequence\n        address_path = normalize_bip32_derivation(address_path, hardened_char=\"'\")\n        address_path = address_path[2:]  # cut m/\n        return self.get_client_dongle_object().show_address(address_path, *args, **kwargs)\n\n\nclass LedgerPlugin(HW_PluginBase):\n    keystore_class = Ledger_KeyStore\n    minimum_library = (0, 2, 0)\n    maximum_library = (1, 0)\n    DEVICE_IDS = [(0x2581, 0x1807),  # HW.1 legacy btchip            # not supported anymore (but we log an exception)\n                  (0x2581, 0x2b7c),  # HW.1 transitional production  # not supported anymore\n                  (0x2581, 0x3b7c),  # HW.1 ledger production        # not supported anymore\n                  (0x2581, 0x4b7c),  # HW.1 ledger test              # not supported anymore\n                  (0x2c97, 0x0000),  # Blue\n                  (0x2c97, 0x0001),  # Nano-S\n                  (0x2c97, 0x0004),  # Nano-X\n                  (0x2c97, 0x0005),  # Nano-S Plus\n                  (0x2c97, 0x0006),  # Stax\n                  (0x2c97, 0x0007),  # Flex\n                  (0x2c97, 0x0008),  # Nano Gen5\n                  (0x2c97, 0x0009),  # RFU\n                  (0x2c97, 0x000a)]  # RFU\n    VENDOR_IDS = (0x2c97,)\n    LEDGER_MODEL_IDS = {\n        0x10: \"Ledger Nano S\",\n        0x40: \"Ledger Nano X\",\n        0x50: \"Ledger Nano S Plus\",\n        0x60: \"Ledger Stax\",\n        0x70: \"Ledger Flex\",\n        0x80: \"Ledger Nano Gen5\",\n    }\n\n    SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')\n\n    def __init__(self, parent, config, name):\n        HW_PluginBase.__init__(self, parent, config, name)\n        self.libraries_available = self.check_libraries_available()\n        if not self.libraries_available:\n            _logger.info(\"Library unavailable\")\n            return\n        # to support legacy devices and legacy firmwares\n        self.device_manager().register_devices(self.DEVICE_IDS, plugin=self)\n        # to support modern firmware\n        self.device_manager().register_vendor_ids(self.VENDOR_IDS, plugin=self)\n\n    def get_library_version(self):\n        try:\n            import ledger_bitcoin\n            version = ledger_bitcoin.__version__\n        except ImportError:\n            raise\n        except Exception:\n            version = \"unknown\"\n        if LEDGER_BITCOIN:\n            return version\n        else:\n            raise LibraryFoundButUnusable(library_version=version)\n\n    @classmethod\n    def is_hw1(cls, product_key) -> bool:\n        return product_key[0] == 0x2581\n\n    @classmethod\n    def _recognize_device(cls, product_key) -> Tuple[bool, Optional[str]]:\n        \"\"\"Returns (can_recognize, model_name) tuple.\"\"\"\n        # legacy product_keys\n        if product_key in cls.DEVICE_IDS:\n            if cls.is_hw1(product_key):\n                return True, \"Ledger HW.1\"\n            if product_key == (0x2c97, 0x0000):\n                return True, \"Ledger Blue\"\n            if product_key == (0x2c97, 0x0001):\n                return True, \"Ledger Nano S\"\n            if product_key == (0x2c97, 0x0004):\n                return True, \"Ledger Nano X\"\n            if product_key == (0x2c97, 0x0005):\n                return True, \"Ledger Nano S Plus\"\n            if product_key == (0x2c97, 0x0006):\n                return True, \"Ledger Stax\"\n            if product_key == (0x2c97, 0x0007):\n                return True, \"Ledger Flex\"\n            if product_key == (0x2c97, 0x0008):\n                return True, \"Ledger Nano Gen5\"\n            return True, None\n        # modern product_keys\n        if product_key[0] == 0x2c97:\n            product_id = product_key[1]\n            model_id = product_id >> 8\n            if model_id in cls.LEDGER_MODEL_IDS:\n                model_name = cls.LEDGER_MODEL_IDS[model_id]\n                return True, model_name\n        # give up\n        return False, None\n\n    def can_recognize_device(self, device: Device) -> bool:\n        can_recognize = self._recognize_device(device.product_key)[0]\n        if can_recognize:\n            # Do a further check, duplicated from:\n            # https://github.com/LedgerHQ/ledgercomm/blob/bc5ada865980cb63c2b9b71a916e01f2f8e53716/ledgercomm/interfaces/hid_device.py#L79-L82\n            # Modern ledger devices can have multiple interfaces picked up by hid, only one of which is usable by us.\n            # If we try communicating with the wrong one, we might not get a reply and block forever.\n            if device.product_key[0] == 0x2c97:\n                if not (device.interface_number == 0 or device.usage_page == 0xffa0):\n                    return False\n        return can_recognize\n\n    @classmethod\n    def device_name_from_product_key(cls, product_key) -> Optional[str]:\n        return cls._recognize_device(product_key)[1]\n\n    def create_device_from_hid_enumeration(self, d, *, product_key):\n        device = super().create_device_from_hid_enumeration(d, product_key=product_key)\n        if not self.can_recognize_device(device):\n            return None\n        return device\n\n    @runs_in_hwd_thread\n    def create_client(self, device, handler) -> Union[Ledger_Client, None, HardwareClientDummy]:\n        try:\n            return Ledger_Client.construct_new(device=device, product_key=device.product_key, plugin=self)\n        except Exception as e:\n            self.logger.info(f\"cannot connect at {device.path} {e}\", exc_info=e)\n        return None\n\n    @runs_in_hwd_thread\n    def show_address(self, wallet, address, keystore=None):\n        if keystore is None:\n            keystore = wallet.get_keystore()\n        if not self.show_address_helper(wallet, address, keystore):\n            return\n        if type(wallet) is not Standard_Wallet:\n            keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device))\n            return\n        sequence = wallet.get_address_index(address)\n        txin_type = wallet.get_txin_type(address)\n\n        keystore.show_address(sequence, txin_type)\n\n    def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str:\n        if new_wallet:\n            return 'ledger_start' if device_info.initialized else 'ledger_not_initialized'\n        else:\n            return 'ledger_unlock'\n\n    # insert ledger pages in new wallet wizard\n    def extend_wizard(self, wizard: 'NewWalletWizard'):\n        views = {\n            'ledger_start': {\n                'next': 'ledger_xpub',\n            },\n            'ledger_xpub': {\n                'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore',\n                'accept': wizard.maybe_master_pubkey,\n                'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d)\n            },\n            'ledger_not_initialized': {},\n            'ledger_unlock': {\n                'last': True\n            },\n        }\n        wizard.navmap_merge(views)\n\n"
  },
  {
    "path": "electrum/plugins/ledger/manifest.json",
    "content": "{\n  \"name\": \"ledger\",\n  \"fullname\": \"Ledger Wallet\",\n  \"description\": \"Provides support for Ledger hardware wallet\",\n  \"requires\": [[\"ledger_bitcoin\", \"github.com/LedgerHQ/app-bitcoin-new\"]],\n  \"registers_keystore\": [\"hardware\", \"ledger\", \"Ledger wallet\"],\n  \"icon\":\"ledger.png\",\n  \"available_for\": [\"qt\", \"cmdline\"]\n}\n"
  },
  {
    "path": "electrum/plugins/ledger/qt.py",
    "content": "from functools import partial\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import pyqtSignal\nfrom PyQt6.QtWidgets import QInputDialog, QLineEdit\n\nfrom electrum.i18n import _\nfrom electrum.plugin import hook\nfrom electrum.wallet import Standard_Wallet\nfrom electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase\nfrom electrum.hw_wallet.plugin import only_hook_if_libraries_available\n\nfrom .ledger import LedgerPlugin, Ledger_Client\nfrom electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUninitialized, WCHWUnlock, WCHWXPub\n\nif TYPE_CHECKING:\n    from electrum.gui.qt.wizard.wallet import QENewWalletWizard\n\n\nclass Plugin(LedgerPlugin, QtPluginBase):\n    icon_unpaired = \"ledger_unpaired.png\"\n    icon_paired = \"ledger.png\"\n\n    def create_handler(self, window):\n        return Ledger_Handler(window)\n\n    @only_hook_if_libraries_available\n    @hook\n    def receive_menu(self, menu, addrs, wallet):\n        if len(addrs) != 1:\n            return\n        if type(wallet) is not Standard_Wallet:\n            return\n        self._add_menu_action(menu, addrs[0], wallet)\n\n    @only_hook_if_libraries_available\n    @hook\n    def transaction_dialog_address_menu(self, menu, addr, wallet):\n        if type(wallet) is not Standard_Wallet:\n            return\n        self._add_menu_action(menu, addr, wallet)\n\n    # insert ledger pages in new wallet wizard\n    def extend_wizard(self, wizard: 'QENewWalletWizard'):\n        super().extend_wizard(wizard)\n        views = {\n            'ledger_start': {'gui': WCScriptAndDerivation},\n            'ledger_xpub': {'gui': WCHWXPub},\n            'ledger_not_initialized': {'gui': WCHWUninitialized},\n            'ledger_unlock': {'gui': WCHWUnlock}\n        }\n        wizard.navmap_merge(views)\n\n\nclass Ledger_Handler(QtHandlerBase):\n\n    MESSAGE_DIALOG_TITLE = _(\"Ledger Status\")\n\n    def __init__(self, win):\n        super(Ledger_Handler, self).__init__(win, 'Ledger')\n\n"
  },
  {
    "path": "electrum/plugins/nwc/__init__.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nfrom typing import TYPE_CHECKING\n\nfrom electrum.commands import plugin_command\nfrom electrum.simple_config import SimpleConfig, ConfigVar\n\nif TYPE_CHECKING:\n    from .nwcserver import NWCServerPlugin\n    from electrum.commands import Commands\n\nplugin_name = \"nwc\"\n\n# Most NWC clients only use the first relay encoded in the connection string.\n# This relay will be used as the first relay in the connection string.\nSimpleConfig.NWC_RELAY = ConfigVar(\n    key='plugins.nwc.relay',\n    default='wss://relay.getalby.com/v1',\n    type_=str,\n    plugin=plugin_name\n)\n\n\n@plugin_command('', plugin_name)\nasync def add_connection(\n    self: 'Commands',\n    name: str,\n    daily_limit_sat=None,\n    valid_for_sec=None,\n    plugin: 'NWCServerPlugin' = None\n) -> str:\n    \"\"\"\n    Create a new NWC connection string.\n\n    arg:str:name:name for the connection (e.g. nostr client name)\n    arg:int:daily_limit_sat:optional daily spending limit in satoshis\n    arg:int:valid_for_sec:optional lifetime of the connection string in seconds\n    \"\"\"\n    connection_string: str = plugin.create_connection(name, daily_limit_sat, valid_for_sec)\n    return connection_string\n\n\n@plugin_command('', plugin_name)\nasync def remove_connection(self: 'Commands', name: str, plugin=None) -> str:\n    \"\"\"\n    Remove a connection by name.\n    arg:str:name:connection name, use list_connections to see all connections\n    \"\"\"\n    plugin.remove_connection(name)\n    return f\"removed connection {name}\"\n\n\n@plugin_command('', plugin_name)\nasync def list_connections(self: 'Commands', plugin=None) -> dict:\n    \"\"\"\n    List all connections by name.\n    \"\"\"\n    connections: dict = plugin.list_connections()\n    return connections\n"
  },
  {
    "path": "electrum/plugins/nwc/cmdline.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nfrom typing import TYPE_CHECKING\n\nfrom electrum.plugin import hook\n\nfrom .nwcserver import NWCServerPlugin\n\nif TYPE_CHECKING:\n    from electrum.daemon import Daemon\n    from electrum.wallet import Abstract_Wallet\n\n\nclass Plugin(NWCServerPlugin):\n\n    def __init__(self, *args):\n        NWCServerPlugin.__init__(self, *args)\n\n    @hook\n    def daemon_wallet_loaded(self, daemon: 'Daemon', wallet: 'Abstract_Wallet'):\n        self.start_plugin(wallet)\n"
  },
  {
    "path": "electrum/plugins/nwc/manifest.json",
    "content": "{\n  \"name\": \"nwc\",\n  \"fullname\": \"Nostr Wallet Connect\",\n  \"description\": \"This plugin allows remote control of Electrum lightning wallets via Nostr NIP-47.\",\n  \"author\": \"The Electrum Developers\",\n  \"available_for\": [\"cmdline\", \"qt\"],\n  \"icon\":\"nwc.png\",\n  \"version\": \"0.0.2\"\n}\n"
  },
  {
    "path": "electrum/plugins/nwc/nwcserver.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport asyncio\nimport json\nimport time\nimport ssl\nimport logging\nimport urllib.parse\nfrom typing import TYPE_CHECKING, Optional, List, Tuple, Awaitable\n\nimport electrum_aionostr as aionostr\nfrom electrum_aionostr.event import Event as nEvent\nfrom electrum_aionostr.key import PrivateKey\n\nfrom electrum.lnworker import PaymentDirection\nfrom electrum.plugin import BasePlugin, hook\nfrom electrum.logging import Logger\nfrom electrum.util import log_exceptions, ca_path, OldTaskGroup, get_asyncio_loop, InvoiceError, \\\n    LightningHistoryItem, event_listener, EventListener, make_aiohttp_proxy_connector, \\\n    get_running_loop, run_sync_function_on_asyncio_thread\nfrom electrum.invoices import Invoice, Request, PR_UNKNOWN, PR_PAID, BaseInvoice, PR_INFLIGHT, PR_FAILED, PR_EXPIRED, PR_UNPAID\nfrom electrum import constants\nfrom electrum.lnutil import RECEIVED, PaymentFeeBudget\n\nif TYPE_CHECKING:\n    from aiohttp_socks import ProxyConnector\n\n    from electrum.simple_config import SimpleConfig\n    from electrum.wallet import Abstract_Wallet\n\n\nclass NWCServerPlugin(BasePlugin):\n    URI_SCHEME = 'nostr+walletconnect://'\n\n    def __init__(self, parent, config: 'SimpleConfig', name):\n        BasePlugin.__init__(self, parent, config, name)\n        self.config = config\n        self.connections = None  # type: Optional[dict[str, dict]]  # pubkey_hex -> connection data\n        self.nwc_server = None   # type: Optional[NWCServer]\n        self.taskgroup = OldTaskGroup()\n        self.initialized = False\n        if not self.config.NWC_RELAY:  # type: ignore  # defined in __init__\n            self.config.NWC_RELAY = self.config.NOSTR_RELAYS.split(',')[0]\n        self.logger.debug(f\"NWCServerPlugin created, waiting for wallet to load...\")\n\n    def start_plugin(self, wallet: 'Abstract_Wallet'):\n        if not wallet.has_lightning():\n            return\n        if self.initialized:\n            # this might be called for several wallets. only use one.\n            return\n        storage = self.get_storage(wallet)\n        self.connections = storage.setdefault('connections', {})\n        self.delete_expired_connections()\n        self.nwc_server = NWCServer(self.config, wallet, self.taskgroup, self.connections)\n        asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.nwc_server.run()), get_asyncio_loop())\n        self.initialized = True\n\n    @hook\n    def close_wallet(self, *args, **kwargs):\n        async def close():\n            if self.nwc_server and self.nwc_server.manager:\n                self.nwc_server.do_stop = True\n                await self.nwc_server.manager.close()\n            await self.taskgroup.cancel_remaining()\n        asyncio.run_coroutine_threadsafe(\n            close(),\n            get_asyncio_loop()\n        )\n        self.logger.debug(f\"NWCServerPlugin closed, stopping taskgroup\")\n\n    def delete_expired_connections(self):\n        if self.connections is None:\n            return\n        now = int(time.time())\n        connections = list(self.connections.items())\n        for pubkey, conn in connections:\n            if 'valid_until' in conn and conn['valid_until'] <= now:\n                del self.connections[pubkey]\n                self.logger.info(f\"Deleting expired NWC connection: {pubkey}\")\n        if len(self.connections) != len(connections) and self.nwc_server:\n            self.nwc_server.restart_event_handler()\n\n    def create_connection(self, name: str, daily_limit_sat: Optional[int], valid_for_sec: Optional[int]) -> str:\n        assert self.connections is not None, f\"Wallet not loaded yet\"\n        assert len(name) > 0, f\"Invalid or missing connection name: {name}\"\n\n        for conn in self.connections.values():\n            if conn['name'] == name:\n                raise ValueError(f\"Connection name already exists: {name}\")\n\n        our_connection_secret = PrivateKey()\n        our_connection_pubkey: str = our_connection_secret.public_key.hex()\n\n        client_secret = PrivateKey()\n        client_pubkey: str = client_secret.public_key.hex()\n\n        connection = {\n            \"name\": name,\n            \"our_secret\": our_connection_secret.hex(),\n            \"budget_spends\": []\n        }\n        if daily_limit_sat is not None:\n            connection['daily_limit_sat'] = daily_limit_sat\n        if valid_for_sec:\n            connection['valid_until'] = int(time.time()) + valid_for_sec\n        connection_string = self.serialize_connection_uri(client_secret.hex(), our_connection_pubkey)\n        self.connections[client_pubkey] = connection\n        self.logger.debug(f\"Added nwc connection: {name=}, {valid_for_sec=}, {daily_limit_sat=}\")\n\n        if self.nwc_server:\n            self.nwc_server.restart_event_handler()\n\n        return connection_string\n\n    def remove_connection(self, name: str) -> None:\n        assert self.connections is not None, f\"Wallet not loaded yet\"\n        for pubkey, conn in self.connections.items():\n            if conn['name'] == name:\n                del self.connections[pubkey]\n                return\n        raise ValueError(f\"Connection name not found: {name}\")\n\n    def list_connections(self) -> dict:\n        assert self.connections is not None, f\"Wallet not loaded yet\"\n        self.delete_expired_connections()\n        connections_without_secrets = {}\n        for client_pub, conn in self.connections.items():\n            data = {\n                'valid_until': conn.get('valid_until', \"unset\"),\n                'daily_limit_sat': conn.get('daily_limit_sat', \"unset\"),\n                'client_pub': client_pub,\n            }\n            connections_without_secrets[conn['name']] = data\n        return connections_without_secrets\n\n    def serialize_connection_uri(self, client_secret_hex: str, our_pubkey_hex: str) -> str:\n        base_uri = f\"{self.URI_SCHEME}{our_pubkey_hex}\"\n\n        # the NWC_RELAY is added first as this is the first relay parsed by clients\n        query_params = [f\"relay={urllib.parse.quote(self.config.NWC_RELAY)}\"]  # type: ignore\n        for relay in self.config.NOSTR_RELAYS.split(\",\")[:5]:\n            if relay != self.config.NWC_RELAY:  # type: ignore\n                query_params.append(f\"relay={urllib.parse.quote(relay)}\")\n\n        query_params.append(f\"secret={client_secret_hex}\")\n\n        # Construct the final URI\n        query_string = \"&\".join(query_params)\n        uri = f\"{base_uri}?{query_string}\"\n\n        return uri\n\n\nclass NWCServer(Logger, EventListener):\n    INFO_EVENT_KIND: int        = 13194\n    REQUEST_EVENT_KIND: int     = 23194\n    RESPONSE_EVENT_KIND: int    = 23195\n    NOTIFICATION_EVENT_KIND: int = 23196\n    SUPPORTED_SPENDING_METHODS: set[str] = {'pay_invoice'}\n    SUPPORTED_METHODS: set[str] = {'make_invoice', 'lookup_invoice', 'get_balance', 'get_info',\n                                   'list_transactions', 'notifications'}.union(SUPPORTED_SPENDING_METHODS)\n    SUPPORTED_NOTIFICATIONS: list[str] = [\"payment_sent\", \"payment_received\"]\n    SUPPORTED_ENCRYPTION_SCHEMES: set[str] = {'nip04'}\n\n    def __init__(\n        self,\n        config: 'SimpleConfig',\n        wallet: 'Abstract_Wallet',\n        taskgroup: 'OldTaskGroup',\n        connection_storage: dict,\n    ):\n        Logger.__init__(self)\n        self.config = config  # type: 'SimpleConfig'\n        self.wallet = wallet  # type: 'Abstract_Wallet'\n        self.connections = connection_storage  # type: dict[str, dict]  # client hex pubkey -> connection data\n        self.relays = config.NOSTR_RELAYS.split(\",\") or []  # type: List[str]\n        self.do_stop = False\n        self.taskgroup = taskgroup  # type: 'OldTaskGroup'\n        self.ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path)\n        self.manager = None  # type: Optional[aionostr.Manager]\n        # the task is stored so it can be cancelled when the connections change\n        self.event_handler_task = None  # type: Optional[asyncio.Task]\n        self.register_callbacks()\n\n    def get_relay_manager(self) -> aionostr.Manager:\n        assert get_asyncio_loop() == get_running_loop(), \"NWCServer must run in the aio event loop\"\n        nostr_logger = self.logger.getChild('aionostr')\n        nostr_logger.setLevel(logging.INFO)\n        network = self.wallet.lnworker.network\n        if network.proxy and network.proxy.enabled:\n            proxy = make_aiohttp_proxy_connector(network.proxy, self.ssl_context)\n        else:\n            proxy: Optional['ProxyConnector'] = None\n        return aionostr.Manager(\n            # ensure that we also connect to NWC_RELAY, even if it's not in the NOSTR_RELAYS\n            relays=set(self.config.NOSTR_RELAYS.split(\",\")) | {self.config.NWC_RELAY},  # type: ignore\n            private_key=PrivateKey().hex(),  # use random private key\n            log=nostr_logger,\n            ssl_context=self.ssl_context,\n            proxy=proxy\n        )\n\n    @log_exceptions\n    async def run(self) -> None:\n        while True:\n            # wait until connections have been set up and network is available\n            while (not self.connections\n                        or not self.relays\n                        or not self.wallet.network\n                        or not self.wallet.network.is_connected()\n                        or not self.wallet.lnworker):\n                await asyncio.sleep(5)\n\n            if not await self.refresh_manager():\n                await asyncio.sleep(30)\n                continue\n\n            try:\n                await self.publish_info_event()\n                self.event_handler_task = await self.taskgroup.spawn(self.handle_requests())\n                await self.event_handler_task\n            except asyncio.CancelledError:\n                if self.do_stop:\n                    return\n                self.logger.debug(\"Restarting nwc event handler\")\n            except Exception as e:\n                self.logger.exception(f\"Restarting nwc event handler after exception: {e}\")\n                if self.manager:  # close the manager so refresh_manager() will recreate it\n                    await self.manager.close()\n                    self.manager = None\n                await asyncio.sleep(60)\n\n    async def refresh_manager(self) -> bool:\n        \"\"\"Checks if manager is still connected to relays, if not recreates it and reconnects\"\"\"\n        if self.manager is None:\n            # on startup and proxy change\n            self.manager = self.get_relay_manager()\n\n        if len(self.manager.relays) <= 0 < len(self.relays):\n            # manager lost all connections (relays)\n            # setup new manager so relays are populated again\n            await self.manager.close()\n            self.manager = self.get_relay_manager()\n\n        if not self.manager.connected:\n            # not set in new manager instances\n            await self.manager.connect()\n\n        if len(self.manager.relays) <= 0:\n            # manager should still have relays after connecting\n            self.logger.warning(f\"Could not connect to any relays!\")\n            return False\n\n        return True\n\n    def restart_event_handler(self) -> None:\n        \"\"\"To be called when the connections change so we restart with a new filter\"\"\"\n        if self.event_handler_task:\n            run_sync_function_on_asyncio_thread(self.event_handler_task.cancel, block=True)\n\n    @event_listener\n    def on_event_proxy_set(self, *args):\n        async def restart_manager():\n            if self.manager:\n                await self.manager.close()\n                self.manager = None\n            await asyncio.sleep(5)\n            self.restart_event_handler()\n            self.logger.info(\"proxy changed, restarting nwc plugin nostr transport\")\n        asyncio.run_coroutine_threadsafe(restart_manager(), get_asyncio_loop())\n\n    async def handle_requests(self) -> None:\n        query = {\n            \"authors\": list(self.connections.keys()),  # the pubkeys of the client connections\n            \"kinds\": [self.REQUEST_EVENT_KIND],\n            \"limit\": 0,  # requests only new events after creating this subscription\n            \"since\": int(time.time())\n        }\n        async for event in self.manager.get_events(query, single_event=False, only_stored=False):\n            if event.pubkey not in self.connections.keys():\n                continue\n\n            # check if the connection is expired, if so we delete it and send an error\n            valid_until: Optional[int] = self.connections[event.pubkey].get('valid_until')\n            if valid_until and valid_until <= int(time.time()):\n                await self.send_error(event, \"UNAUTHORIZED\", \"Connection expired\")\n                del self.connections[event.pubkey]\n                self.logger.info(f\"Deleting expired NWC connection: {event.pubkey}\")\n                self.restart_event_handler()\n                continue\n\n            if event.kind != self.REQUEST_EVENT_KIND:\n                self.logger.debug(f\"Unknown nwc request event kind: {event.kind}\")\n                await self.send_error(event, \"NOT_IMPLEMENTED\")\n                continue\n\n            # if the request has an explicitly set expiration tag, ignore it if it is expired\n            # otherwise ignore requests older than 30 sec to not handle requests the user may\n            # already expect to have timed out\n            if event.expires_at() is not None:\n                if event.is_expired():\n                    self.logger.debug(f\"expired nwc request event: {event.content}\")\n                    continue\n            elif event.created_at < int(time.time()) - 30:\n                self.logger.debug(f\"old nwc request event: {event.content}\")\n                await self.send_error(event, \"OTHER\", f\"not handling too old request\")\n                continue\n\n            # check encryption scheme\n            for tag in event.tags:\n                if len(tag) == 2 and tag[0] == 'encryption':\n                    if tag[1] not in self.SUPPORTED_ENCRYPTION_SCHEMES:\n                        await self.send_error(event, \"UNSUPPORTED_ENCRYPTION\", \" \".join(self.SUPPORTED_ENCRYPTION_SCHEMES))\n                    break\n\n            # decrypt the requests content\n            our_secret: str = self.connections[event.pubkey]['our_secret']\n            our_connection_secret = PrivateKey(raw_secret=bytes.fromhex(our_secret))\n            try:\n                content = our_connection_secret.decrypt_message(event.content, event.pubkey)\n                content = json.loads(content)\n                if not isinstance(content, dict):\n                    raise Exception(\"malformed content, not dict\")\n                params: dict = content['params']\n                if not isinstance(params, dict):\n                    raise Exception(\"malformed params, not dict\")\n            except Exception:\n                self.logger.debug(f\"Invalid request event content: {event.content}\", exc_info=True)\n                continue\n\n            # run the according method\n            method: str = content.get('method')\n            self.logger.debug(f\"got request: {method=}, {params=}\")\n            task: Optional[Awaitable] = None\n            if method == \"pay_invoice\" and not self.is_receive_only(event.pubkey):\n                task = self.handle_pay_invoice(event, params)\n            elif method == \"make_invoice\":\n                task = self.handle_make_invoice(event, params)\n            elif method == \"lookup_invoice\":\n                task = self.handle_lookup_invoice(event, params)\n            elif method == \"get_balance\":\n                task = self.handle_get_balance(event)\n            elif method == \"get_info\":\n                task = self.handle_get_info(event)\n            elif method == \"list_transactions\":\n                task = self.handle_list_transactions(event, params)\n            else:\n                self.logger.debug(f\"Unsupported nwc method requested: {method}\")\n                await self.send_error(event, \"NOT_IMPLEMENTED\", f\"{method} not supported\", error_restype=method)\n                continue\n\n            if task:\n                await self.taskgroup.spawn(self.run_request_task(task, request_event=event, request_method=method))\n\n    async def run_request_task(self, task: Awaitable, *, request_event: nEvent, request_method: str = None) -> None:\n        \"\"\"Catches request handling exceptions and send an error response\"\"\"\n        try:\n            await task\n        except Exception as e:\n            self.logger.exception(\"Error handling nwc request\")\n            await self.send_error(\n                request_event, \"INTERNAL\", f\"Error handling request: {str(e)[:100]}\",\n                error_restype=request_method,\n            )\n\n    async def send_error(\n        self,\n        causing_event: nEvent,\n        error_type: str,\n        error_msg: str = \"\",\n        *,\n        error_restype: str = None,\n    ) -> None:\n        \"\"\"Sends an error as response to the passed nEvent, containing the error type and message\"\"\"\n        to_pubkey_hex = causing_event.pubkey\n        response_to_id = causing_event.id\n        content = self.get_error_response(error_type, error_msg, error_restype)\n        await self.send_encrypted_response(to_pubkey_hex, json.dumps(content), response_to_id)\n\n    @staticmethod\n    def get_error_response(error_type: str, error_msg: str = \"\", method: Optional[str] = None) -> dict:\n        content = {\n            \"error\": {\n                \"code\": error_type,\n                \"message\": error_msg\n            }\n        }\n        if method:\n            content['result_type'] = method\n        return content\n\n    async def send_encrypted_response(\n            self,\n            to_pubkey_hex: str,\n            content: str,\n            response_event_id: str,\n            *,\n            add_tags: Optional[List] = None\n    ) -> None:\n        \"\"\"Encrypts content for the given pubkey and sends it as response to the given event id\"\"\"\n        our_secret: str = self.connections[to_pubkey_hex]['our_secret']\n        tags = [['p', to_pubkey_hex], ['e', response_event_id]]\n        if add_tags:\n            tags.extend(add_tags)\n\n        await self.taskgroup.spawn(aionostr._add_event(\n            self.manager,\n            kind=self.RESPONSE_EVENT_KIND,\n            tags=tags,\n            content=self.encrypt_to_pubkey(content, to_pubkey_hex),\n            # use the private key we generated for this specific client\n            private_key=our_secret\n            )\n        )\n\n    @log_exceptions\n    async def handle_pay_invoice(self, request_event: nEvent, params: dict) -> None:\n        \"\"\"\n        Handler for pay_invoice method\n        https://github.com/nostr-protocol/nips/blob/75f246ed987c23c99d77bfa6aeeb1afb669e23f7/47.md#pay_invoice\n        \"\"\"\n        invoice: str = params.get('invoice', \"\")\n        amount_msat: Optional[int] = params.get('amount')\n        response = await self.pay_invoice(invoice, amount_msat, request_event.pubkey)\n        response['result_type'] = 'pay_invoice'\n        await self.send_encrypted_response(request_event.pubkey, json.dumps(response), request_event.id)\n\n    @log_exceptions\n    async def handle_make_invoice(self, request_event: nEvent, params: dict):\n        \"\"\"\n        Handler for make_invoice method.\n        https://github.com/nostr-protocol/nips/blob/75f246ed987c23c99d77bfa6aeeb1afb669e23f7/47.md#make_invoice\n        \"\"\"\n        amount_msat = params.get('amount', 0)  # type: Optional[int]\n        description = params.get('description', params.get('description_hash', \"\"))  # type: str\n        expiry = params.get('expiry', 3600)  # type: int\n        # create payment request\n        key: str = self.wallet.create_request(\n            amount_sat=amount_msat // 1000,\n            message=description,\n            exp_delay=expiry,\n            address=None\n        )\n        req: Request = self.wallet.get_request(key)\n        info = self.wallet.lnworker.get_payment_info(req.payment_hash, direction=RECEIVED)\n        try:\n            lnaddr, b11 = self.wallet.lnworker.get_bolt11_invoice(\n                payment_info=info,\n                message=description,\n                fallback_address=None\n            )\n        except Exception:\n            self.logger.exception(f\"failed to create invoice\")\n            response = self.get_error_response(\"INTERNAL\", \"Failed to create invoice\", \"make_invoice\")\n            return await self.send_encrypted_response(request_event.pubkey, json.dumps(response), request_event.id)\n        response = {\n            \"result_type\": \"make_invoice\",\n            \"result\": {\n                \"type\": \"incoming\",\n                \"state\": \"pending\",\n                \"invoice\": b11,\n                \"description\": description,\n                \"payment_hash\": lnaddr.paymenthash.hex(),\n                \"amount\": amount_msat,\n                \"created_at\": lnaddr.date,\n                \"expires_at\": req.get_expiration_date(),\n                \"metadata\": {},\n                \"fees_paid\": 0  # the spec wants this??\n            }\n        }\n        self.logger.debug(f\"make_invoice response: {response}\")\n        await self.send_encrypted_response(request_event.pubkey, json.dumps(response), request_event.id)\n\n    @log_exceptions\n    async def handle_lookup_invoice(self, request_event: nEvent, params: dict):\n        \"\"\"\n        https://github.com/nostr-protocol/nips/blob/75f246ed987c23c99d77bfa6aeeb1afb669e23f7/47.md#lookup_invoice\n        \"\"\"\n        invoice = params.get('invoice')\n        payment_hash = params.get('payment_hash')\n        if invoice:\n            invoice = Invoice.from_bech32(invoice)\n        elif payment_hash:\n            invoice = self.wallet.get_invoice(payment_hash) or self.wallet.get_request(payment_hash)\n        else:\n            response = self.get_error_response(\"NOT_FOUND\", \"Missing invoice or payment_hash\")\n            return await self.send_encrypted_response(request_event.pubkey, json.dumps(response), request_event.id)\n\n        status = None\n        if invoice and invoice.is_lightning():\n            status = self.wallet.get_invoice_status(invoice)\n        if not invoice or status is None or status == PR_UNKNOWN:\n            response = self.get_error_response(\"NOT_FOUND\", \"Invoice not found\")\n            return await self.send_encrypted_response(request_event.pubkey, json.dumps(response), request_event.id)\n\n        direction = None\n        b11 = None\n        if self.wallet.get_invoice(invoice.rhash):\n            direction = \"outgoing\"\n            b11 = invoice.lightning_invoice\n        elif self.wallet.get_request(invoice.rhash):\n            direction = \"incoming\"\n            info = self.wallet.lnworker.get_payment_info(bytes.fromhex(invoice.rhash), direction=RECEIVED)\n            _, b11 = self.wallet.lnworker.get_bolt11_invoice(\n                payment_info=info,\n                message=invoice.message,\n                fallback_address=None\n            )\n\n        response = {\n            \"result_type\": \"lookup_invoice\",\n            \"result\": {\n                \"description\": invoice.message,\n                \"payment_hash\": invoice.rhash,\n                \"amount\": invoice.get_amount_msat(),\n                \"created_at\": invoice.time,\n                \"expires_at\": invoice.get_expiration_date(),\n                \"fees_paid\": 0,\n                \"invoice\": b11,\n                \"metadata\": {}\n            }\n        }\n        if nip47_status := self.invoice_status_to_nip47_state(status):\n            response['result']['state'] = nip47_status\n\n        info = self.get_payment_info(invoice.rhash)\n        if info:\n            _, _, fee_msat, settled_at = info\n            if fee_msat:\n                response['result']['fees_paid'] = fee_msat\n            response['result']['settled_at'] = settled_at\n\n        if direction:\n            response['result']['type'] = direction\n        if status == PR_PAID:\n            response['result']['preimage'] = self.wallet.lnworker.get_preimage_hex(invoice.rhash) or \"not found\"\n        self.logger.debug(f\"lookup_invoice response: {response}\")\n        await self.send_encrypted_response(request_event.pubkey, json.dumps(response), request_event.id)\n\n    @log_exceptions\n    async def handle_get_balance(self, request_event: nEvent):\n        \"\"\"\n        https://github.com/nostr-protocol/nips/blob/75f246ed987c23c99d77bfa6aeeb1afb669e23f7/47.md#get_balance\n        \"\"\"\n        balance = int(self.wallet.lnworker.get_balance())\n        response = {\n            \"result_type\": \"get_balance\",\n            \"result\": {\n                \"balance\": balance * 1000,\n            }\n        }\n        await self.send_encrypted_response(request_event.pubkey, json.dumps(response), request_event.id)\n\n    @log_exceptions\n    async def handle_get_info(self, request_event: nEvent):\n        \"\"\"\n        https://github.com/nostr-protocol/nips/blob/75f246ed987c23c99d77bfa6aeeb1afb669e23f7/47.md#get_info\n        \"\"\"\n        height = self.wallet.lnworker.network.blockchain().height()\n        blockhash = self.wallet.lnworker.network.blockchain().get_hash(height)\n        supported_methods = self.SUPPORTED_METHODS.copy()\n        if self.is_receive_only(request_event.pubkey):\n            supported_methods -= self.SUPPORTED_SPENDING_METHODS\n        response = {\n            \"result_type\": \"get_info\",\n            \"result\": {\n                \"alias\": self.config.LIGHTNING_NODE_ALIAS,\n                \"color\": self.config.LIGHTNING_NODE_COLOR_RGB,\n                \"pubkey\": self.wallet.lnworker.node_keypair.pubkey.hex(),\n                \"network\": constants.net.NET_NAME,\n                \"block_height\": height,\n                \"block_hash\": blockhash,\n                \"methods\": list(supported_methods),\n            }\n        }\n        if self.SUPPORTED_NOTIFICATIONS:\n            response['result']['notifications'] = self.SUPPORTED_NOTIFICATIONS\n        await self.send_encrypted_response(request_event.pubkey, json.dumps(response), request_event.id)\n\n    @log_exceptions\n    async def handle_list_transactions(self, request_event: nEvent, params: dict):\n        \"\"\"\n        https://github.com/nostr-protocol/nips/blob/75f246ed987c23c99d77bfa6aeeb1afb669e23f7/47.md#list_transactions\n        Lists invoices and payments. If type is not specified, both invoices and payments are returned.\n        The from and until parameters are timestamps in seconds since epoch.\n        If from is not specified, it defaults to 0. If until is not specified, it defaults to the current time.\n        Transactions are returned in descending order of creation time.\n        \"\"\"\n        t0 = time.time()\n        from_ts = int(params.get('from', 0))\n        until_ts = int(params.get('until', time.time()))\n        limit: Optional[int] = params.get('limit')\n        offset: Optional[int] = params.get('offset')\n        include_unpaid_reqs = bool(params.get('unpaid', False))\n        # this is not in spec but alby go requests it\n        include_unpaid_outgoing = bool(params.get('unpaid_outgoing', False))\n        req_type = params.get('type', \"undefined\")\n\n        lightning_history = self.wallet.lnworker.get_lightning_history()\n        lightning_history = lightning_history.values()\n\n        if req_type == \"incoming\":\n            lightning_history = [tx for tx in lightning_history if tx.direction == PaymentDirection.RECEIVED]\n        elif req_type == \"outgoing\":\n            lightning_history = [tx for tx in lightning_history if tx.direction == PaymentDirection.SENT]\n        else:\n            directions = [PaymentDirection.SENT, PaymentDirection.RECEIVED]\n            lightning_history = [tx for tx in lightning_history if tx.direction in directions]\n\n        if include_unpaid_reqs:\n            requests = self.wallet.get_unpaid_requests()\n            for req in requests:\n                if not req.is_lightning() or not (from_ts <= req.time <= until_ts):\n                    continue\n                lightning_history.append(\n                    # append the payment request as LightingHistoryItem so they can be filtered\n                    # together with the real lightning history items\n                    LightningHistoryItem(\n                        type='unpaid',\n                        payment_hash=req.rhash,\n                        preimage=None,\n                        amount_msat=req.get_amount_msat() or 0,\n                        fee_msat=None,\n                        timestamp=req.time,\n                        direction=PaymentDirection.RECEIVED,\n                        group_id=None,\n                        label=None\n                    )\n                )\n\n        if include_unpaid_outgoing:\n            \"\"\"Alby Go requests unpaid_outgoing (out of nip47 spec) but then shows them as sent in the tx history.\n            So we only return PR_INFLIGHT here so its not totally misleading in the history.\"\"\"\n            invoices = self.wallet.get_invoices()\n            for inv in invoices:\n                if (not inv.is_lightning()\n                        or not (from_ts <= inv.time <= until_ts)\n                        or not self.wallet.get_invoice_status(inv) == PR_INFLIGHT):\n                    continue\n                lightning_history.append(\n                    LightningHistoryItem(\n                        type='pending',\n                        payment_hash=inv.rhash,\n                        preimage=None,\n                        amount_msat=inv.get_amount_msat() or 0,\n                        fee_msat=None,\n                        timestamp=inv.time,\n                        direction=PaymentDirection.SENT,\n                        group_id=None,\n                        label=None\n                    )\n                )\n\n        if from_ts > 0 or until_ts < time.time() - 50:\n            # filter out transactions that are not in the time range\n            lightning_history = [tx for tx in lightning_history if from_ts <= tx.timestamp <= until_ts]\n\n        lightning_history = sorted(lightning_history, key=lambda tx: tx.timestamp, reverse=True)\n        if offset and offset > 0:\n            lightning_history = lightning_history[offset:]\n        if limit and limit > 0:\n            lightning_history = lightning_history[:limit]\n        transactions = []\n        for history_tx in lightning_history:\n            tx = {\n                \"payment_hash\": history_tx.payment_hash,\n                \"amount\": abs(history_tx.amount_msat),\n                \"metadata\": {},\n                \"fees_paid\": 0\n            }\n            payment: Optional[BaseInvoice] = None\n            if history_tx.direction == PaymentDirection.RECEIVED:\n                tx['type'] = \"incoming\"\n                payment = self.wallet.get_request(history_tx.payment_hash)\n            elif history_tx.direction == PaymentDirection.SENT:\n                tx['type'] = \"outgoing\"\n                payment = self.wallet.get_invoice(history_tx.payment_hash)\n            if not payment:\n                # don't include txs with semi complete information as this will cause some clients\n                # to fail displaying any transaction at all\n                continue\n            if include_unpaid_outgoing and history_tx.type == 'pending':\n                tx['description'] = f\"pending! {payment.message}\"\n            else:\n                tx['description'] = payment.message\n            tx['expires_at'] = payment.get_expiration_date()\n            tx['created_at'] = payment.time\n            status = self.wallet.get_invoice_status(payment)\n            if nip47_status := self.invoice_status_to_nip47_state(status):\n                tx['state'] = nip47_status\n            if (not include_unpaid_reqs and not include_unpaid_outgoing) or history_tx.type == 'payment':\n                tx['settled_at'] = history_tx.timestamp\n                tx['preimage'] = history_tx.preimage\n            if history_tx.fee_msat:\n                tx['fees_paid'] = history_tx.fee_msat\n            transactions.append(tx)\n\n        response = {\n            \"result_type\": \"list_transactions\",\n            \"result\": {\n                \"transactions\": transactions,\n            }\n        }\n        self.logger.debug(f\"list_transactions: returning {len(transactions)} txs in {time.time() - t0:.2f}s\")\n        await self.send_encrypted_response(request_event.pubkey, json.dumps(response), request_event.id)\n\n    @event_listener\n    def on_event_request_status(self, wallet, key, status):\n        if wallet != self.wallet:\n            return\n        request: Optional[Request] = self.wallet.get_request(key)\n        if not request or not request.is_lightning() or not status == PR_PAID:\n            return\n        info = self.wallet.lnworker.get_payment_info(request.payment_hash, direction=RECEIVED)\n        _, b11 = self.wallet.lnworker.get_bolt11_invoice(\n            payment_info=info,\n            message=request.message,\n            fallback_address=None\n        )\n\n        payment_info = self.get_payment_info(request.rhash)\n        if payment_info:\n            _, _, _, settled_at = payment_info\n        else:\n            settled_at = None\n\n        notification = {\n            \"type\": \"incoming\",\n            \"invoice\": b11,\n            \"description\": request.message,\n            \"payment_hash\": request.rhash,\n            \"amount\": request.get_amount_msat(),\n            \"created_at\": request.time,\n            \"expires_at\": request.get_expiration_date(),\n            \"preimage\": self.wallet.lnworker.get_preimage_hex(request.rhash) or \"not found\",\n            \"metadata\": {},\n            \"fees_paid\": 0\n        }\n        if settled_at:\n            notification['state'] = 'settled'\n            notification['settled_at'] = settled_at\n\n        self.publish_notification_event({\n            \"notification_type\": \"payment_received\",\n            \"notification\": notification,\n        })\n\n    @event_listener\n    def on_event_payment_succeeded(self, wallet, key):\n        if wallet != self.wallet:\n            return\n        invoice: Optional[Invoice] = self.wallet.get_invoice(key)\n        if not invoice or not invoice.is_lightning():\n            return\n\n        payment_info = self.get_payment_info(key)\n        if not payment_info:\n            return\n        _, _, fee_msat, settled_at = payment_info\n\n        assert key == invoice.rhash, f\"{key=!r} != {invoice.rhash=!r}\"\n        notification = {\n            \"type\": \"outgoing\",\n            \"invoice\": invoice.lightning_invoice or \"\",\n            \"description\": invoice.message,\n            \"preimage\": self.wallet.lnworker.get_preimage_hex(invoice.rhash) or \"not found\",\n            \"payment_hash\": invoice.rhash,\n            \"amount\": invoice.get_amount_msat(),\n            \"created_at\": invoice.time,\n            \"expires_at\": invoice.get_expiration_date(),\n            \"metadata\": {}\n        }\n        if fee_msat:\n            notification['fees_paid'] = fee_msat\n        if settled_at:\n            notification['state'] = 'settled'\n            notification['settled_at'] = settled_at\n        content = {\n            \"notification_type\": \"payment_sent\",\n            \"notification\": notification,\n        }\n        self.publish_notification_event(content)\n\n    async def pay_invoice(self, b11: str, amount_msat: Optional[int], request_pub: str) -> dict:\n        try:\n            invoice: Invoice = Invoice.from_bech32(b11)\n        except InvoiceError:\n            return self.get_error_response(\"INTERNAL\", \"Invalid invoice\")\n\n        if invoice.get_amount_msat() is None and not amount_msat:\n            return self.get_error_response(\"INTERNAL\", \"Missing amount\")\n        elif invoice.get_amount_msat() is None:\n            invoice.set_amount_msat(amount_msat)\n\n        # add the maximum allowed fee to the budget and update it with the actual fee once the payment succeeds\n        # to prevent exceeding the budget through high fees\n        payment_amount_msat = amount_msat or invoice.get_amount_msat()\n        fee_budget = PaymentFeeBudget.from_invoice_amount(\n            config=self.wallet.config,\n            invoice_amount_msat=payment_amount_msat,\n        )\n        budget_amount_msat = payment_amount_msat + fee_budget.fee_msat\n        try:\n            budget_item = self.add_to_budget(request_pub, msat_requested=budget_amount_msat)\n        except ValueError as e:\n            return self.get_error_response(\"QUOTA_EXCEEDED\", str(e))\n\n        self.wallet.save_invoice(invoice)\n        success = None\n        try:\n            success, log = await self.wallet.lnworker.pay_invoice(\n                invoice=invoice,\n                amount_msat=amount_msat,\n                budget=fee_budget,\n            )\n        except Exception as e:\n            self.logger.exception(f\"failed to pay nwc invoice\")\n            return self.get_error_response(\"PAYMENT_FAILED\", str(e))\n        finally:\n            # note: if the user shuts down or the application crashes before the payment ends, it will not\n            # get deducted from the budget, even if the htlcs later get failed on wallet restart.\n            if success:\n                # replace the spend in the budget, this time using the actual (lower) fees that have been paid\n                info = self.get_payment_info(invoice.rhash)\n                assert info, \"info should exist after successful payment\"\n                _, total_amount_msat, _, _ = info\n                assert payment_amount_msat <= total_amount_msat <= budget_amount_msat\n                self._update_budget_item_amount(request_pub, old_budget_item=budget_item, new_msat=total_amount_msat)\n            else:\n                self.remove_from_budget(request_pub, budget_item)\n\n        preimage: bytes = self.wallet.lnworker.get_preimage(bytes.fromhex(invoice.rhash))\n        response = {}\n        if not success or not preimage:\n            return self.get_error_response(\"PAYMENT_FAILED\", str(log))\n        response['result'] = {\n            'preimage': preimage.hex(),\n        }\n        if success:\n            self.logger.info(f\"paid invoice request from NWC for {invoice.get_amount_sat()} sat\")\n        else:\n            self.logger.info(f\"failed to pay invoice request from NWC: {log}\")\n        return response\n\n    def remove_from_budget(self, client_pub: str, budget_item: list[int]) -> None:\n        assert len(budget_item) == 2, budget_item\n        budget_spends = self.connections[client_pub].get('budget_spends', [])\n        try:\n            budget_spends.remove(budget_item)\n            self.logger.debug(f\"removed {budget_item=} from {client_pub[:4]}...{client_pub[-4:]} budget\")\n        except ValueError:\n            self.logger.debug(f\"failed to remove {budget_item=} from {client_pub[:4]}...{client_pub[-4:]} budget: not found\")\n            pass\n\n    def get_used_budget_msat(self, client_pub: str) -> int:\n        \"\"\"\n        Returns the used budget for the given client_pubkey in millisatoshi.\n        \"\"\"\n        if 'budget_spends' not in self.connections[client_pub]:\n            return 0\n        used_budget: int = 0\n        budget_spends = self.connections[client_pub]['budget_spends']\n        for amount_msat, timestamp in list(budget_spends):\n            if timestamp > int(time.time()) - 24 * 3600:\n                used_budget += amount_msat\n            elif timestamp < int(time.time()) - 24 * 3600:\n                # remove old expense\n                try:\n                    budget_spends.remove([amount_msat, timestamp])\n                except ValueError:\n                    self.logger.debug(\"\", exc_info=True)\n                    continue  # could happen if there is a race\n        return used_budget\n\n    def add_to_budget(self, client_pub: str, *, msat_requested: int) -> list[int]:\n        if 'budget_spends' not in self.connections[client_pub]:\n            self.connections[client_pub]['budget_spends'] = []\n\n        # check if budget allows this spend\n        client_budget_sat: Optional[int] = self.connections[client_pub].get('daily_limit_sat')\n        if client_budget_sat is not None:\n            used_budget_msat: int = self.get_used_budget_msat(client_pub)\n            if used_budget_msat + msat_requested > client_budget_sat * 1000:\n                raise ValueError(\"spend exceeds daily budget\")\n\n        self.logger.debug(f\"adding {msat_requested=} msat to {client_pub[:4]}...{client_pub[-4:]} budget\")\n        # tuples don't work because jsondb converts them to lists on reload\n        budget_item = [msat_requested, int(time.time())]\n        self.connections[client_pub]['budget_spends'].append(budget_item)\n        return budget_item\n\n    def _update_budget_item_amount(self, client_pub: str, *, old_budget_item: list[int], new_msat: int) -> None:\n        assert len(old_budget_item) == 2, old_budget_item\n        budget_spends = self.connections[client_pub].setdefault('budget_spends', [])\n        try:\n            budget_spends.remove(old_budget_item)\n        except ValueError:\n            self.logger.debug(\n            f\"failed to rm and update {old_budget_item=} from {client_pub[:4]}...{client_pub[-4:]} budget: not found\")\n            return\n        # tuples don't work because jsondb converts them to lists on reload\n        new_budget_item = [new_msat, int(time.time())]\n        budget_spends.append(new_budget_item)\n\n    async def publish_info_event(self):\n        \"\"\"\n        Publishes the info event according to spec, announcing the supported methods.\n        We publish one info event for each client connection.\n        https://github.com/nostr-protocol/nips/blob/75f246ed987c23c99d77bfa6aeeb1afb669e23f7/47.md#example-nip-47-info-event\n        \"\"\"\n        tags = []\n        if self.SUPPORTED_NOTIFICATIONS:\n            tags.append(['notifications', ' '.join(self.SUPPORTED_NOTIFICATIONS)])\n        if self.SUPPORTED_ENCRYPTION_SCHEMES:\n            tags.append(['encryption', ' '.join(self.SUPPORTED_ENCRYPTION_SCHEMES)])\n        for client_pubkey, connection in list(self.connections.items()):\n            supported_methods = self.SUPPORTED_METHODS.copy()\n            if self.is_receive_only(client_pubkey):\n                supported_methods -= self.SUPPORTED_SPENDING_METHODS\n            content = ' '.join(supported_methods)\n            event_id = await aionostr._add_event(\n                self.manager,\n                kind=self.INFO_EVENT_KIND,\n                tags=tags or None,\n                content=content,\n                private_key=connection['our_secret']\n            )\n            self.logger.debug(f\"Published info event {event_id} to {client_pubkey}\")\n\n    def publish_notification_event(self, content: dict):\n        \"\"\"\n        https://github.com/nostr-protocol/nips/blob/75f246ed987c23c99d77bfa6aeeb1afb669e23f7/47.md#notification-events\n        \"\"\"\n        self.logger.debug(f\"Publishing notification event: {content}\")\n        for client_pubkey, connection in list(self.connections.items()):\n            coro = self.taskgroup.spawn(aionostr._add_event(\n                self.manager,\n                kind=self.NOTIFICATION_EVENT_KIND,\n                tags=[['p', client_pubkey]],\n                content=self.encrypt_to_pubkey(json.dumps(content), client_pubkey),\n                private_key=connection['our_secret']\n                )\n            )\n            asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop())\n\n    def encrypt_to_pubkey(self, msg: str, pubkey: str) -> str:\n        \"\"\"\n        Encrypts the given message to the given pubkey using the connection secret.\n        \"\"\"\n        our_secret: str = self.connections[pubkey]['our_secret']\n        our_secret_key = PrivateKey(raw_secret=bytes.fromhex(our_secret))\n        encrypted_content: str = our_secret_key.encrypt_message(msg, pubkey)\n        return encrypted_content\n\n    def get_payment_info(self, payment_hash: str) \\\n        -> Optional[Tuple[PaymentDirection, int, Optional[int], int]]:\n        payment_hash: bytes = bytes.fromhex(payment_hash)\n        payments = self.wallet.lnworker.get_payments(status=None)\n        plist = payments.get(payment_hash)\n        if plist and any(htlc.status == 'settled' for htlc in plist):\n            direction = plist[0].direction\n            info = self.wallet.lnworker.get_payment_info(payment_hash, direction=direction)\n            if info:\n                # assumes inflight htlcs will get settled and counts them into the payment values\n                active_htlcs = [htlc for htlc in plist if htlc.status in ('settled', 'inflight')]\n                dir, amount, fee, ts = self.wallet.lnworker.get_payment_value(info, active_htlcs)\n                fee = abs(fee) if fee else None\n                return dir, abs(amount), fee, ts\n        return None\n\n    def is_receive_only(self, pubkey: str) -> bool:\n        return self.connections[pubkey].get('daily_limit_sat') == 0\n\n    @staticmethod\n    def invoice_status_to_nip47_state(status) -> Optional[str]:\n        if status == PR_EXPIRED:\n            return \"expired\"\n        elif status == PR_FAILED:\n            return \"failed\"\n        elif status == PR_PAID:\n            return \"settled\"\n        elif status == PR_UNPAID:\n            return \"pending\"\n        return None\n"
  },
  {
    "path": "electrum/plugins/nwc/qt.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nfrom typing import TYPE_CHECKING, Optional\nfrom functools import partial\nfrom datetime import datetime\nfrom decimal import Decimal\n\nfrom PyQt6.QtWidgets import (\n    QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTreeWidget, QTreeWidgetItem,\n    QTextEdit, QApplication, QSpinBox, QSizePolicy, QComboBox, QLineEdit,\n)\nfrom PyQt6.QtGui import QPixmap, QImage\nfrom PyQt6.QtCore import Qt, QTimer\n\nfrom electrum.i18n import _\nfrom electrum.plugin import hook\nfrom electrum.gui.qt.util import (\n    WindowModalDialog, Buttons, OkButton, CancelButton, CloseButton,\n    read_QIcon_from_bytes, read_QPixmap_from_bytes,\n)\nfrom electrum.gui.common_qt.util import paintQR\n\nfrom .nwcserver import NWCServerPlugin\n\nif TYPE_CHECKING:\n    from electrum.wallet import Abstract_Wallet\n    from electrum.gui.qt.main_window import ElectrumWindow\n\n\nclass Plugin(NWCServerPlugin):\n    def __init__(self, *args):\n        NWCServerPlugin.__init__(self, *args)\n        self._init_qt_received = False\n\n    @hook\n    def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'):\n        if not wallet.has_lightning():\n            return\n        self.start_plugin(wallet)\n\n    @hook\n    def init_menubar(self, window):\n        ma = window.wallet_menu.addAction('Nostr Wallet Connect', partial(self.settings_dialog, window))\n        icon = read_QIcon_from_bytes(self.read_file('nwc.png'))\n        ma.setIcon(icon)\n\n    def settings_dialog(self, window: WindowModalDialog):\n        if not self.initialized:\n            window.show_error(\n                _(\"{} plugin requires a lightning enabled wallet. Open a lightning-enabled wallet first.\")\n                .format(\"NWC\"))\n            return\n        if window.wallet != self.nwc_server.wallet:\n            window.show_error('not using this wallet')\n            return\n\n        d = WindowModalDialog(window, _(\"Nostr Wallet Connect\"))\n        main_layout = QHBoxLayout(d)\n\n        # Create the logo label.\n        logo_label = QLabel()\n        pixmap = read_QPixmap_from_bytes(self.read_file('nwc.png'))\n        logo_label.setPixmap(pixmap.scaled(50, 50))\n        logo_label.setAlignment(Qt.AlignmentFlag.AlignLeft)\n\n        vbox = QVBoxLayout()\n        main_layout.addWidget(logo_label)\n        main_layout.addSpacing(16)\n        main_layout.addLayout(vbox)\n\n        # Connections list\n        connections_list = QTreeWidget()\n        connections_list.setHeaderLabels([_(\"Name\"), _(\"Budget [{}]\").format(self.config.get_base_unit()), _(\"Expiry\")])\n        # Set the resize mode for all columns to adjust to content\n        header = connections_list.header()\n        header.setSectionResizeMode(0, header.ResizeMode.Stretch)\n        header.setSectionResizeMode(1, header.ResizeMode.ResizeToContents)\n        header.setSectionResizeMode(2, header.ResizeMode.ResizeToContents)\n        header.setStretchLastSection(False)\n        # Set size policy to expand horizontally\n        connections_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)\n        # Make the widget update its size when data changes\n        connections_list.setAutoExpandDelay(0)\n\n        def update_connections_list():\n            # Clear the list and repopulate it\n            connections_list.clear()\n            connections = self.list_connections()\n            for name, conn in connections.items():\n                if conn['valid_until'] == 'unset':\n                    expiry = _(\"never\")\n                else:\n                    expiry = datetime.fromtimestamp(conn['valid_until']).isoformat(' ')[:-3]\n                if conn['daily_limit_sat'] == 'unset':\n                    limit = _('unlimited')\n                else:\n                    budget = self.config.format_amount(conn['daily_limit_sat'])\n                    used = self.config.format_amount(\n                        amount_sat=round(Decimal(self.nwc_server.get_used_budget_msat(conn['client_pub'])) / 1000)\n                    )\n                    limit = f\"{used}/{budget}\"\n                item = QTreeWidgetItem(\n                    [\n                        name,\n                        limit,\n                        expiry\n                    ]\n                )\n                connections_list.addTopLevelItem(item)\n\n        update_connections_list()\n        connections_list.setMinimumHeight(min(connections_list.sizeHint().height(), 400))\n\n        # Delete button - initially disabled\n        delete_btn = QPushButton(_(\"Delete\"))\n        delete_btn.setEnabled(False)\n\n        # Function to delete the selected connection\n        def delete_selected_connection():\n            selected_items = connections_list.selectedItems()\n            if not selected_items:\n                return\n            for item in selected_items:\n                try:\n                    self.remove_connection(item.text(0))\n                except ValueError:\n                    self.logger.error(f\"Failed to remove connection: {item.text(0)}\")\n                    return\n                update_connections_list()\n            if self.nwc_server:\n                self.nwc_server.restart_event_handler()\n            delete_btn.setEnabled(False)\n\n        # Enable delete button when an item is selected\n        def on_item_selected():\n            delete_btn.setEnabled(bool(connections_list.selectedItems()))\n\n        connections_list.itemSelectionChanged.connect(on_item_selected)\n        delete_btn.clicked.connect(delete_selected_connection)\n\n        # Create Connection button\n        create_btn = QPushButton(_(\"Create\"))\n\n        def create_connection():\n            # Show a dialog to create a new connection\n            connection_string = self.connection_info_input_dialog(window)\n            if connection_string:\n                update_connections_list()\n                self.show_new_connection_dialog(window, connection_string)\n        create_btn.clicked.connect(create_connection)\n\n        # Add the info and close button to the footer\n        close_button = OkButton(d, label=_(\"Close\"))\n        info_button = QPushButton(_(\"Help\"))\n        info = _(\"This plugin allows you to create Nostr Wallet Connect connections and \"\n                 \"remote control your wallet using Nostr NIP-47.\")\n        warning = _(\"Most NWC clients only use a single of your relays, so ensure the relays accept your events.\")\n        supported_methods = _(\"Supported NIP-47 methods: {}\").format(\", \".join(self.nwc_server.SUPPORTED_METHODS))\n        info_msg = f\"{info}\\n\\n{warning}\\n\\n{supported_methods}\"\n        info_button.clicked.connect(lambda: window.show_message(info_msg))\n\n        title_hbox = QHBoxLayout()\n        title_hbox.addStretch(1)\n        title_hbox.addWidget(info_button)\n\n        footer_buttons = Buttons(\n            create_btn,\n            delete_btn,\n            close_button,\n        )\n\n        vbox.addLayout(title_hbox)\n        vbox.addWidget(QLabel(_('Existing Connections:')))\n        vbox.addWidget(connections_list)\n        vbox.addLayout(footer_buttons)\n        d.setLayout(main_layout)\n\n        # update the list from time to time to see if budgets have changed\n        refresh_timer = QTimer(d)\n        refresh_timer.timeout.connect(update_connections_list)\n        refresh_timer.start(5_000)  # msec\n\n        return bool(d.exec())\n\n    def connection_info_input_dialog(self, window) -> Optional[str]:\n        # Create input dialog for connection parameters\n        input_dialog = WindowModalDialog(window, _(\"Enter NWC connection parameters\"))\n        layout = QVBoxLayout(input_dialog)\n\n        # Name field (mandatory)\n        layout.addWidget(QLabel(_(\"Connection Name (required):\")))\n        name_edit = QLineEdit()\n        name_edit.setMaximumHeight(30)\n        layout.addWidget(name_edit)\n\n        # Daily limit field (optional)\n        layout.addWidget(QLabel(_(\"Daily Satoshi Budget (optional):\")))\n        limit_edit = OptionalSpinBox()\n        limit_edit.setRange(-1, 100_000_000)\n        limit_edit.setMaximumHeight(30)\n        layout.addWidget(limit_edit)\n\n        # Validity period field (optional)\n        layout.addWidget(QLabel(_(\"Valid for seconds (optional):\")))\n        validity_edit = OptionalSpinBox()\n        validity_edit.setRange(-1, 63072000)\n        validity_edit.setMaximumHeight(30)\n        layout.addWidget(validity_edit)\n\n        def change_nwc_relay(url):\n            self.config.NWC_RELAY = url\n\n        # dropdown menu to select prioritized nwc relay from self.config.NOSTR_RELAYS\n        main_relay_label = QLabel(_(\"Main NWC Relay:\"))\n        relay_tooltip = (\n            _(\"Most clients only use the first relay url encoded in the connection string.\")\n            + \"\\n\" + _(\"The selected relay will be put first in the connection string.\"))\n        main_relay_label.setToolTip(relay_tooltip)\n        layout.addWidget(main_relay_label)\n        relay_combo = QComboBox()\n        relay_combo.setMaximumHeight(30)\n        relay_combo.addItems(self.config.NOSTR_RELAYS.split(\",\"))\n        relay_combo.setCurrentText(self.config.NWC_RELAY)  # type: ignore\n        relay_combo.currentTextChanged.connect(lambda: change_nwc_relay(relay_combo.currentText()))\n        layout.addWidget(relay_combo)\n\n        # Buttons\n        buttons = Buttons(OkButton(input_dialog), CancelButton(input_dialog))\n        layout.addLayout(buttons)\n\n        if not input_dialog.exec():\n            return None\n\n        # Validate inputs\n        name = name_edit.text().strip()\n        if not name or len(name) < 1:\n            window.show_error(_(\"Connection name is required\"))\n            return None\n        duration_limit = validity_edit.value() if validity_edit.value() else None\n\n        # Call create_connection function with user-provided parameters\n        try:\n            connection_string = self.create_connection(\n                name=name,\n                daily_limit_sat=limit_edit.value(),\n                valid_for_sec=duration_limit\n            )\n        except ValueError as e:\n            window.show_error(str(e))\n            return None\n\n        if not connection_string:\n            window.show_error(_(\"Failed to create connection\"))\n            return None\n\n        return connection_string\n\n    @staticmethod\n    def show_new_connection_dialog(window, connection_string: str):\n        # Create popup with QR code\n        popup = WindowModalDialog(window, _(\"New NWC Connection\"))\n        vbox = QVBoxLayout(popup)\n\n        qr: Optional[QImage] = paintQR(connection_string)\n        if not qr:\n            return\n        qr_pixmap = QPixmap.fromImage(qr)\n        qr_label = QLabel()\n        qr_label.setPixmap(qr_pixmap)\n        qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n\n        vbox.addWidget(QLabel(_(\"Scan this QR code with your nwc client:\")))\n        vbox.addWidget(qr_label)\n\n        # Add connection string text that can be copied\n        vbox.addWidget(QLabel(_(\"Or copy this connection string:\")))\n        text_edit = QTextEdit()\n        text_edit.setText(connection_string)\n        text_edit.setReadOnly(True)\n        text_edit.setMaximumHeight(80)\n        vbox.addWidget(text_edit)\n\n        warning_label = QLabel(_(\"After closing this window you won't be able to \"\n                                 \"access the connection string again!\"))\n        warning_label.setStyleSheet(\"color: red;\")\n        warning_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        vbox.addWidget(warning_label)\n\n        # Button to copy to clipboard\n        copy_btn = QPushButton(_(\"Copy to clipboard\"))\n        copy_btn.clicked.connect(lambda: QApplication.clipboard().setText(connection_string))\n\n        vbox.addLayout(Buttons(copy_btn, CloseButton(popup)))\n\n        popup.setLayout(vbox)\n        popup.exec()\n\n\nclass OptionalSpinBox(QSpinBox):\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.setSpecialValueText(\" \")\n        self.setMinimum(-1)\n        self.setValue(-1)\n\n    def value(self):\n        # Return None if at special value, otherwise return the actual value\n        val = super().value()\n        return None if val == -1 else val\n\n    def setValue(self, value):\n        # Accept None to set to the special empty value\n        super().setValue(-1 if value is None else value)\n"
  },
  {
    "path": "electrum/plugins/psbt_nostr/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/plugins/psbt_nostr/manifest.json",
    "content": "{\n  \"name\": \"psbt_nostr\",\n  \"fullname\": \"Nostr Cosigner\",\n  \"description\": \"This plugin facilitates the use of multi-signatures wallets. It sends and receives partially signed transactions from/to your cosigner wallet. PSBTs are sent and retrieved from Nostr relays.\",\n  \"author\": \"The Electrum Developers\",\n  \"icon\": \"nostr_multisig.png\",\n  \"available_for\": [\"qt\", \"qml\"],\n  \"version\": \"0.0.1\"\n}\n"
  },
  {
    "path": "electrum/plugins/psbt_nostr/psbt_nostr.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport asyncio\nimport json\nimport ssl\nimport time\nfrom contextlib import asynccontextmanager\n\nimport electrum_ecc as ecc\nimport electrum_aionostr as aionostr\nfrom electrum_aionostr.key import PrivateKey\nfrom typing import Dict, TYPE_CHECKING, Union, List, Tuple, Optional, Callable\n\nfrom electrum import util, Transaction\nfrom electrum.crypto import sha256\nfrom electrum.i18n import _\nfrom electrum.logging import Logger\nfrom electrum.plugin import BasePlugin\nfrom electrum.transaction import PartialTransaction, tx_from_any\nfrom electrum.util import (\n    log_exceptions, OldTaskGroup, ca_path, trigger_callback, event_listener, json_decode,\n    make_aiohttp_proxy_connector, run_sync_function_on_asyncio_thread,\n)\nfrom electrum.wallet import Multisig_Wallet\n\nif TYPE_CHECKING:\n    from electrum.wallet import Abstract_Wallet\n    from aiohttp_socks import ProxyConnector\n\n# event kind used for nostr messages (with expiration tag)\nNOSTR_EVENT_KIND = 4\n\nnow = lambda: int(time.time())\n\n\nclass PsbtNostrPlugin(BasePlugin):\n\n    def __init__(self, parent, config, name):\n        BasePlugin.__init__(self, parent, config, name)\n        self.cosigner_wallets = {}  # type: Dict[Abstract_Wallet, CosignerWallet]\n\n    def is_available(self):\n        return True\n\n    def add_cosigner_wallet(self, wallet: 'Abstract_Wallet', cosigner_wallet: 'CosignerWallet'):\n        assert isinstance(wallet, Multisig_Wallet)\n        self.cosigner_wallets[wallet] = cosigner_wallet\n\n    def remove_cosigner_wallet(self, wallet: 'Abstract_Wallet'):\n        if cw := self.cosigner_wallets.get(wallet):\n            cw.close()\n            self.cosigner_wallets.pop(wallet)\n\n\nclass CosignerWallet(Logger):  # children have to inherit EventListener and register callbacks\n    # one for each open window (Qt) / open wallet (QML)\n    # if user signs a tx, we have the password\n    # if user receives a dm? needs to enter password first\n\n    KEEP_DELAY = 24*60*60\n\n    def __init__(self, wallet: 'Multisig_Wallet', db_storage: dict):\n        assert isinstance(wallet, Multisig_Wallet)\n        self.wallet = wallet\n\n        Logger.__init__(self)\n\n        self.network = wallet.network\n        self.config = self.wallet.config\n\n        self.pending = asyncio.Event()\n\n        self.known_events = db_storage.setdefault('cosigner_events', {})\n\n        for k, v in list(self.known_events.items()):\n            if v < now() - self.KEEP_DELAY:\n                self.logger.info(f'deleting old event {k}')\n                self.known_events.pop(k)\n        self.ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path)\n        self.logger.info(f\"relays {self.config.NOSTR_RELAYS.split(',')}\")\n\n        self.cosigner_list = []  # type: List[Tuple[str, str]]\n        self.nostr_pubkey = None\n\n        for keystore in wallet.get_keystores():\n            # note: there should be domain separation between testnet/mainnet.\n            #       Currently there is, due to the xpub str encoding it in its header.\n            xpub = keystore.get_master_public_key()  # type: str\n            privkey = sha256('nostr_psbt:' + xpub)\n            pubkey = ecc.ECPrivkey(privkey).get_public_key_bytes()[1:]\n            if self.nostr_pubkey is None and not keystore.is_watching_only():\n                self.nostr_privkey = privkey.hex()\n                self.nostr_pubkey = pubkey.hex()\n                self.logger.info(f'nostr pubkey: {self.nostr_pubkey}')\n            else:\n                self.cosigner_list.append((xpub, pubkey.hex()))\n\n        self.messages = asyncio.Queue()\n        self.taskgroup = OldTaskGroup()\n        if self.network and self.nostr_pubkey:\n            asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)\n\n    @event_listener\n    async def on_event_proxy_set(self, *args):\n        # note: the callbacks get registered in the child classes of CosignerWallet\n        if not (self.network and self.nostr_pubkey):\n            return\n        await self.stop()\n        self.taskgroup = OldTaskGroup()\n        asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)\n\n    @log_exceptions\n    async def main_loop(self):\n        self.logger.info(\"starting taskgroup.\")\n        try:\n            # start processing PSBTs only after wallet is_up_to_date\n            while not self.wallet.is_up_to_date():\n                await self.wallet.up_to_date_changed_event.wait()\n            self.logger.debug('starting handling of PSBTs')\n            async with self.taskgroup as group:\n                await group.spawn(self.check_direct_messages())\n        except Exception as e:\n            self.logger.exception(\"taskgroup died.\")\n        finally:\n            self.logger.info(\"taskgroup stopped.\")\n\n    async def stop(self):\n        await self.taskgroup.cancel_remaining()\n\n    @asynccontextmanager\n    async def nostr_manager(self):\n        if self.network.proxy and self.network.proxy.enabled:\n            proxy = make_aiohttp_proxy_connector(self.network.proxy, self.ssl_context)\n        else:\n            proxy: Optional['ProxyConnector'] = None\n        manager_logger = self.logger.getChild('aionostr')\n        manager_logger.setLevel(\"INFO\")  # set to INFO because DEBUG is very spammy\n        async with aionostr.Manager(\n                relays=self.config.NOSTR_RELAYS.split(','),\n                private_key=self.nostr_privkey,\n                ssl_context=self.ssl_context,\n                proxy=proxy,\n                log=manager_logger\n        ) as manager:\n            yield manager\n\n    @log_exceptions\n    async def send_direct_messages(self, messages: List[Tuple[str, dict]]):\n        our_private_key: PrivateKey = aionostr.key.PrivateKey(bytes.fromhex(self.nostr_privkey))\n        async with self.nostr_manager() as manager:\n            for pubkey, msg in messages:\n                encrypted_msg: str = our_private_key.encrypt_message(json.dumps(msg), pubkey)\n                eid = await aionostr._add_event(\n                    manager,\n                    kind=NOSTR_EVENT_KIND,\n                    content=encrypted_msg,\n                    private_key=self.nostr_privkey,\n                    tags=[['p', pubkey], ['expiration', str(int(now() + self.KEEP_DELAY))]])\n                self.logger.info(f'message sent to {pubkey}: {eid}')\n\n    @log_exceptions\n    async def check_direct_messages(self):\n        privkey = PrivateKey(bytes.fromhex(self.nostr_privkey))\n        async with self.nostr_manager() as manager:\n            await manager.connect()\n            query = {\n                \"kinds\": [NOSTR_EVENT_KIND],\n                \"limit\": 100,\n                \"#p\": [self.nostr_pubkey],\n                \"since\": int(now() - self.KEEP_DELAY),\n            }\n            async for event in manager.get_events(query, single_event=False, only_stored=False):\n                if event.id in self.known_events:\n                    self.logger.info(f'known event {event.id} {util.age(event.created_at)}')\n                    continue\n                if not any(event.pubkey == pubkey for _xpub, pubkey in self.cosigner_list):\n                    self.logger.warning(f\"got event from unknown author: {event.pubkey}\")\n                    continue\n                if event.created_at > now() + self.KEEP_DELAY:\n                    # might be malicious\n                    continue\n                if event.created_at < now() - self.KEEP_DELAY:\n                    continue\n                self.logger.info(f'new event {event.id}')\n                try:\n                    message = privkey.decrypt_message(event.content, event.pubkey)\n                except Exception as e:\n                    self.logger.info(f'could not decrypt message {event.pubkey}')\n                    self.known_events[event.id] = now()\n                    continue\n                try:\n                    message = json_decode(message)\n                    if not isinstance(message, dict):\n                        raise Exception(\"malformed message, not dict\")\n                    tx_hex = message.get('tx')\n                    label = message.get('label', '')\n                    tx = tx_from_any(tx_hex)\n                except Exception as e:\n                    self.logger.info(_(\"Unable to deserialize the transaction:\") + \"\\n\" + str(e))\n                    self.known_events[event.id] = now()\n                    continue\n                self.logger.info(f\"received PSBT from {event.pubkey}\")\n                trigger_callback('psbt_nostr_received', self.wallet, event.pubkey, event.id, tx, label)\n                await self.pending.wait()\n                self.pending.clear()\n\n    def diagnostic_name(self):\n        return self.wallet.diagnostic_name()\n\n    def close(self):\n        if not self.network:\n            return\n        self.logger.info(\"shutting down listener\")\n        asyncio.run_coroutine_threadsafe(self.stop(), self.network.asyncio_loop)\n\n    def cosigner_can_sign(self, tx: Transaction, cosigner_xpub: str) -> bool:\n        # TODO implement this properly:\n        #      should return True iff cosigner (with given xpub) can sign and has not yet signed.\n        #      note that tx could also be unrelated from wallet?... (not ismine inputs)\n        return True\n\n    def can_send_psbt(self, tx: Union[Transaction, PartialTransaction]) -> bool:\n        if tx.is_complete() or self.wallet.can_sign(tx):\n            return False\n        for xpub, pubkey in self.cosigner_list:\n            if self.cosigner_can_sign(tx, xpub):\n                return True\n        return False\n\n    def mark_pending_event_rcvd(self, event_id):\n        self.logger.debug('marking event rcvd')\n        self.known_events[event_id] = now()\n        run_sync_function_on_asyncio_thread(self.pending.set, block=False)\n\n    def prepare_messages(self, tx: Union[Transaction, PartialTransaction], label: str = None) -> List[Tuple[str, dict]]:\n        messages = []\n        for xpub, pubkey in self.cosigner_list:\n            if not self.cosigner_can_sign(tx, xpub):\n                continue\n            payload = {'tx': tx.serialize_as_bytes().hex()}\n            if label:\n                payload['label'] = label\n            messages.append((pubkey, payload))\n        return messages\n\n    def send_psbt(self, tx: Union[Transaction, PartialTransaction], label: str):\n        self.do_send(self.prepare_messages(tx, label), tx.txid())\n\n    def do_send(self, messages: List[Tuple[str, dict]], txid: Optional[str] = None):\n        raise NotImplementedError()\n\n    def on_receive(self, pubkey, event_id, tx, label: str):\n        raise NotImplementedError()\n\n    def add_transaction_to_wallet(\n        self,\n        tx: Union['Transaction', 'PartialTransaction'],\n        *,\n        label: str = None,\n        on_failure: Callable[[str], None] = None,\n        on_success: Callable[[], None] = None\n    ) -> None:\n        assert tx.txid(), \"Shouldn't allow to save tx without txid\"\n        try:\n            # TODO: adding tx should be handled more gracefully here:\n            # 1) don't replace tx with same tx with less signatures\n            # 2) we could combine signatures if tx will become more complete\n            # 3) ... more heuristics?\n            if not self.wallet.adb.add_transaction(tx):\n                # TODO: instead of bool return value, we could use specific fail reason exceptions here\n                raise Exception('transaction was not added')\n            if label:\n                self.wallet.set_label(tx.txid(), label)\n        except Exception as e:\n            if on_failure:\n                on_failure(str(e))\n        else:\n            self.wallet.save_db()\n            if on_success:\n                on_success()\n"
  },
  {
    "path": "electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml",
    "content": "import QtQuick\nimport QtQuick.Layouts\nimport QtQuick.Controls\nimport QtQuick.Controls.Material\n\nimport \"../../../gui/qml/components/controls\"\n\nElDialog {\n    id: dialog\n    title: qsTr(\"PSBT received\")\n    iconSource: Qt.resolvedUrl('../../../gui/icons/question.png')\n\n    enum Choice {\n        None,\n        Open,\n        Save\n    }\n\n    property string tx_label\n    property bool can_be_saved\n    property int choice: PsbtReceiveDialog.Choice.None\n\n    // TODO: it might be better to defer popup until no dialogs are shown\n    z: 1 // raise z so it also covers dialogs using overlay as parent\n\n    anchors.centerIn: parent\n\n    padding: 0\n    needsSystemBarPadding: false\n\n    width: rootLayout.width\n\n    ColumnLayout {\n        id: rootLayout\n        width: dialog.parent.width * 2/3\n\n        ColumnLayout {\n            Layout.margins: constants.paddingMedium\n            Layout.fillWidth: true\n\n            TextArea {\n                id: message\n                Layout.fillWidth: true\n                readOnly: true\n                wrapMode: TextInput.WordWrap\n                textFormat: TextEdit.RichText\n                background: Rectangle {\n                    color: 'transparent'\n                }\n\n                text: [\n                    tx_label\n                        ? qsTr('A transaction was received from your cosigner with label: <br/><b>%1</b><br/>').arg(tx_label)\n                        : qsTr('A transaction was received from your cosigner.'),\n                    qsTr('Do you want to open it now?')\n                ].join('<br/>')\n            }\n        }\n\n        ButtonContainer {\n            Layout.fillWidth: true\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Open')\n                icon.source: Qt.resolvedUrl('../../../gui/icons/confirmed.png')\n                onClicked: {\n                    choice = PsbtReceiveDialog.Choice.Open\n                    doAccept()\n                }\n            }\n\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Discard')\n                icon.source: Qt.resolvedUrl('../../../gui/icons/closebutton.png')\n                onClicked: doReject()\n            }\n            FlatButton {\n                Layout.fillWidth: true\n                Layout.preferredWidth: 1\n                text: qsTr('Save to Wallet')\n                icon.source: Qt.resolvedUrl('../../../gui/icons/wallet.png')\n                visible: dialog.can_be_saved\n                onClicked: {\n                    choice = PsbtReceiveDialog.Choice.Save\n                    doAccept()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/plugins/psbt_nostr/qml/main.qml",
    "content": "import QtQuick\n\nimport org.electrum\n\nimport \"../../../gui/qml/components/controls\"\n\nItem {\n    Connections {\n        target: AppController ? AppController.plugin('psbt_nostr') : null\n        function onCosignerReceivedPsbt(pubkey, event, tx, label, can_be_saved) {\n            var dialog = psbtReceiveDialog.createObject(app, {\n                tx_label: label,\n                can_be_saved: can_be_saved\n            })\n            dialog.accepted.connect(function () {\n                if (dialog.choice == PsbtReceiveDialog.Choice.Open) {\n                    console.log('label:' + label)\n                    console.log('tx:' + tx)\n                    target.saveTxLabel(Daemon.currentWallet, tx, label)\n                    var page = app.stack.push(Qt.resolvedUrl('../../../gui/qml/components/TxDetails.qml'), {\n                        rawtx: tx\n                    })\n                    page.closed.connect(function () {\n                        target.acceptPsbt(Daemon.currentWallet, event)\n                    })\n                } else if (dialog.choice == PsbtReceiveDialog.Choice.Save) {\n                    target.acceptPsbt(Daemon.currentWallet, event, true)\n                } else {\n                    console.log('choice not set')\n                }\n            })\n            dialog.rejected.connect(function () {\n                target.rejectPsbt(Daemon.currentWallet, event)\n            })\n            dialog.open()\n        }\n    }\n\n    Component {\n        id: psbtReceiveDialog\n        PsbtReceiveDialog {\n            onClosed: destroy()\n        }\n    }\n\n    property variant export_tx_button: Component {\n        FlatButton {\n            id: psbt_nostr_send_button\n            property variant dialog\n            text: qsTr('Nostr')\n            icon.source: Qt.resolvedUrl('../../../gui/icons/network.png')\n            visible: AppController.plugin('psbt_nostr').canSendPsbt(Daemon.currentWallet, dialog.text)\n            onClicked: {\n                console.log('about to psbt nostr send')\n                psbt_nostr_send_button.enabled = false\n                AppController.plugin('psbt_nostr').sendPsbt(Daemon.currentWallet, dialog.text, dialog.tx_label)\n            }\n            Connections {\n                target: AppController ? AppController.plugin('psbt_nostr') : null\n                function onSendPsbtSuccess() {\n                    dialog.close()\n                    var msgdialog = app.messageDialog.createObject(app, {\n                        text: qsTr('PSBT sent successfully')\n                    })\n                    msgdialog.open()\n                }\n                function onSendPsbtFailed(message) {\n                    psbt_nostr_send_button.enabled = true\n                    var msgdialog = app.messageDialog.createObject(app, {\n                        text: qsTr('Sending PSBT to co-signer failed:\\n%1').arg(message),\n                        iconSource: Qt.resolvedUrl('../../../gui/icons/warning.png')\n                    })\n                    msgdialog.open()\n                }\n            }\n\n        }\n    }\n\n}\n"
  },
  {
    "path": "electrum/plugins/psbt_nostr/qml.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport asyncio\nimport concurrent\nfrom typing import TYPE_CHECKING, List, Tuple, Optional\n\nfrom PyQt6.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot\n\nfrom electrum import util\nfrom electrum.plugin import hook\nfrom electrum.transaction import PartialTransaction, tx_from_any\nfrom electrum.wallet import Multisig_Wallet\nfrom electrum.util import EventListener, event_listener\n\nfrom electrum.gui.qml.qewallet import QEWallet\n\nfrom .psbt_nostr import PsbtNostrPlugin, CosignerWallet\n\nif TYPE_CHECKING:\n    from electrum.wallet import Abstract_Wallet\n    from electrum.gui.qml import ElectrumQmlApplication\n\n\nclass QReceiveSignalObject(QObject):\n    def __init__(self, plugin: 'Plugin'):\n        QObject.__init__(self)\n        self._plugin = plugin\n\n    cosignerReceivedPsbt = pyqtSignal(str, str, str, str, bool)\n    sendPsbtFailed = pyqtSignal(str, arguments=['reason'])\n    sendPsbtSuccess = pyqtSignal()\n\n    @pyqtProperty(str)\n    def loader(self):\n        return 'main.qml'\n\n    @pyqtSlot(QEWallet, str, result=bool)\n    def canSendPsbt(self, wallet: 'QEWallet', tx: str) -> bool:\n        cosigner_wallet = self._plugin.cosigner_wallets.get(wallet.wallet)\n        if not cosigner_wallet:\n            return False\n        return cosigner_wallet.can_send_psbt(tx_from_any(tx, deserialize=True))\n\n    @pyqtSlot(QEWallet, str)\n    @pyqtSlot(QEWallet, str, str)\n    def sendPsbt(self, wallet: 'QEWallet', tx: str, label: str = None):\n        cosigner_wallet = self._plugin.cosigner_wallets.get(wallet.wallet)\n        if not cosigner_wallet:\n            return\n        cosigner_wallet.send_psbt(tx_from_any(tx, deserialize=True), label)\n\n    @pyqtSlot(QEWallet, str, str)\n    def saveTxLabel(self, wallet: 'QEWallet', tx: str, label: str):\n        cosigner_wallet = self._plugin.cosigner_wallets.get(wallet.wallet)\n        if not cosigner_wallet:\n            return\n        cosigner_wallet.save_tx_label(tx_from_any(tx, deserialize=True), label)\n\n    @pyqtSlot(QEWallet, str)\n    @pyqtSlot(QEWallet, str, bool)\n    def acceptPsbt(self, wallet: 'QEWallet', event_id: str, save_to_wallet: bool = False):\n        cosigner_wallet = self._plugin.cosigner_wallets.get(wallet.wallet)\n        if not cosigner_wallet:\n            return\n        cosigner_wallet.accept_psbt(event_id, save_to_wallet)\n        if save_to_wallet:\n            # let GUI update view through wallet_updated callback\n            util.trigger_callback('wallet_updated', wallet.wallet)\n\n    @pyqtSlot(QEWallet, str)\n    def rejectPsbt(self, wallet: 'QEWallet', event_id: str):\n        cosigner_wallet = self._plugin.cosigner_wallets.get(wallet.wallet)\n        if not cosigner_wallet:\n            return\n        cosigner_wallet.reject_psbt(event_id)\n\n\nclass Plugin(PsbtNostrPlugin):\n    def __init__(self, parent, config, name):\n        super().__init__(parent, config, name)\n        self.so = QReceiveSignalObject(self)\n        self._app = None\n\n    @hook\n    def init_qml(self, app: 'ElectrumQmlApplication'):\n        self._app = app\n        self.so.setParent(app)  # parent in QObject tree\n        # plugin enable for already open wallet\n        wallet = app.daemon.currentWallet.wallet if app.daemon.currentWallet else None\n        if wallet:\n            self.load_wallet(wallet)\n\n    @hook\n    def load_wallet(self, wallet: 'Abstract_Wallet'):\n        # remove existing, only foreground wallet active\n        for _wallet in self.cosigner_wallets.copy().keys():\n            self.remove_cosigner_wallet(_wallet)\n        if not isinstance(wallet, Multisig_Wallet):\n            return\n        if wallet.wallet_type == '2fa':\n            return\n        self.add_cosigner_wallet(wallet, QmlCosignerWallet(wallet, self))\n\n\nclass QmlCosignerWallet(EventListener, CosignerWallet):\n\n    def __init__(self, wallet: 'Multisig_Wallet', plugin: 'Plugin'):\n        db_storage = plugin.get_storage(wallet)\n        CosignerWallet.__init__(self, wallet, db_storage)\n        self.plugin = plugin\n        self.register_callbacks()\n\n        self.tx = None\n\n    @event_listener\n    def on_event_psbt_nostr_received(self, wallet, pubkey, event_id, tx: 'PartialTransaction', label: str):\n        if self.wallet == wallet:\n            self.tx = tx\n            can_be_saved = tx.txid() is not None\n            self.plugin.so.cosignerReceivedPsbt.emit(pubkey, event_id, tx.serialize(), label, can_be_saved)\n\n    def close(self):\n        super().close()\n        self.unregister_callbacks()\n\n    def do_send(self, messages: List[Tuple[str, dict]], txid: Optional[str] = None):\n        if not messages:\n            return\n        coro = self.send_direct_messages(messages)\n\n        loop = util.get_asyncio_loop()\n        assert util.get_running_loop() != loop, 'must not be called from asyncio thread'\n        self._result = None\n        self._future = asyncio.run_coroutine_threadsafe(coro, loop)\n\n        try:\n            self._result = self._future.result()\n            self.plugin.so.sendPsbtSuccess.emit()\n        except concurrent.futures.CancelledError:\n            pass\n        except Exception as e:\n            self.plugin.so.sendPsbtFailed.emit(str(e))\n\n    def save_tx_label(self, tx, label):\n        self.wallet.set_label(tx.txid(), label)\n\n    def accept_psbt(self, event_id, save: bool = False):\n        if save:\n            self.add_transaction_to_wallet(self.tx, on_failure=self.on_add_fail)\n        self.mark_pending_event_rcvd(event_id)\n\n    def reject_psbt(self, event_id):\n        self.mark_pending_event_rcvd(event_id)\n\n    def on_add_fail(self, error_msg: str):\n        self.logger.error(f'failed to add tx to wallet: {error_msg}')\n"
  },
  {
    "path": "electrum/plugins/psbt_nostr/qt.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport asyncio\nfrom functools import partial\nfrom typing import TYPE_CHECKING, List, Tuple, Optional, Union\n\nfrom PyQt6.QtCore import QObject, pyqtSignal\nfrom PyQt6.QtWidgets import QPushButton, QMessageBox\n\nfrom electrum.plugin import hook\nfrom electrum.i18n import _\nfrom electrum.wallet import Multisig_Wallet, Abstract_Wallet\nfrom electrum.util import UserCancelled, event_listener, EventListener\nfrom electrum.gui.qt.transaction_dialog import show_transaction, TxDialog\nfrom electrum.gui.qt.util import read_QIcon_from_bytes\n\nfrom .psbt_nostr import PsbtNostrPlugin, CosignerWallet\n\nif TYPE_CHECKING:\n    from electrum.transaction import Transaction, PartialTransaction\n    from electrum.gui.qt.main_window import ElectrumWindow\n\n\nclass QReceiveSignalObject(QObject):\n    cosignerReceivedPsbt = pyqtSignal(str, str, object, str)\n\n\nclass Plugin(PsbtNostrPlugin):\n    def __init__(self, parent, config, name):\n        super().__init__(parent, config, name)\n        self._init_qt_received = False\n\n    @hook\n    def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'):\n        if not isinstance(wallet, Multisig_Wallet):\n            return\n        if wallet.wallet_type == '2fa':\n            return\n        self.add_cosigner_wallet(wallet, QtCosignerWallet(wallet, window, self))\n\n    @hook\n    def on_close_window(self, window):\n        wallet = window.wallet\n        self.remove_cosigner_wallet(wallet)\n\n    @hook\n    def transaction_dialog(self, d: 'TxDialog'):\n        if cw := self.cosigner_wallets.get(d.wallet):\n            assert isinstance(cw, QtCosignerWallet)\n            d.cosigner_send_button = b = QPushButton(_(\"Send to cosigner\"))\n            icon = read_QIcon_from_bytes(self.read_file(\"nostr_multisig.png\"))\n            b.setIcon(icon)\n            b.clicked.connect(lambda: cw.send_to_cosigners(d.tx, d.desc))\n            d.buttons.insert(0, b)\n            b.setVisible(False)\n\n    @hook\n    def transaction_dialog_update(self, d: 'TxDialog'):\n        if cw := self.cosigner_wallets.get(d.wallet):\n            assert isinstance(cw, QtCosignerWallet)\n            d.cosigner_send_button.setVisible(cw.can_send_psbt(d.tx))\n\n\nclass QtCosignerWallet(EventListener, CosignerWallet):\n    def __init__(self, wallet: 'Multisig_Wallet', window: 'ElectrumWindow', plugin: 'Plugin'):\n        db_storage = plugin.get_storage(wallet)\n        CosignerWallet.__init__(self, wallet, db_storage)\n        self.window = window\n        self.obj = QReceiveSignalObject()\n        self.obj.cosignerReceivedPsbt.connect(self.on_receive)\n        self.register_callbacks()\n\n    def close(self):\n        super().close()\n        self.unregister_callbacks()\n\n    @event_listener\n    def on_event_psbt_nostr_received(self, wallet, *args):\n        if self.wallet == wallet:\n            self.obj.cosignerReceivedPsbt.emit(*args)  # put on UI thread via signal\n\n    def send_to_cosigners(self, tx: Union['Transaction', 'PartialTransaction'], label: str):\n        if tx.txid():\n            self.add_transaction_to_wallet(tx, label=label, on_failure=self.on_add_fail)\n        self.send_psbt(tx, label)\n\n    def do_send(self, messages: List[Tuple[str, dict]], txid: Optional[str] = None):\n        if not messages:\n            return\n        coro = self.send_direct_messages(messages)\n        text = _('Sending transaction to your Nostr relays...')\n        try:\n            result = self.window.run_coroutine_dialog(coro, text)\n        except UserCancelled:\n            return\n        except asyncio.exceptions.TimeoutError:\n            self.window.show_error(_('relay timeout'))\n            return\n        except Exception as e:\n            self.window.show_error(str(e))\n            return\n        message = _(\"Your transaction was sent to your cosigners via Nostr.\")\n        if txid:\n            message += '\\n\\n' + txid\n        self.window.show_message(message)\n\n    def on_receive(self, pubkey, event_id, tx, label):\n        msg = '<br/>'.join([\n            _(\"A transaction was received from your cosigner.\") if not label else\n            _(\"A transaction was received from your cosigner with label: <br/><big>{}</big><br/>\").format(label),\n            _(\"Do you want to open it now?\")\n        ])\n        buttons = [\n            QMessageBox.StandardButton.Open,\n            (QPushButton('Discard'), QMessageBox.ButtonRole.DestructiveRole, 100),\n        ]\n        if tx.txid():  # cannot add tx without txid to wallet history (e.g. unsigned legacy tx)\n            buttons.append(\n                (QPushButton('Save to wallet'), QMessageBox.ButtonRole.AcceptRole, 101)  # type: ignore\n            )\n        result = self.window.show_message(msg, rich_text=True, icon=QMessageBox.Icon.Question, buttons=buttons)\n        if result == QMessageBox.StandardButton.Open:\n            if label and tx.txid():\n                self.wallet.set_label(tx.txid(), label)\n            show_transaction(tx, parent=self.window, prompt_if_unsaved=True, on_closed=partial(self.on_tx_dialog_closed, event_id))\n        else:\n            self.mark_pending_event_rcvd(event_id)\n            if result == 100:  # Discard\n                return\n            self.add_transaction_to_wallet(tx, label=label, on_failure=self.on_add_fail)\n            self.window.update_tabs()\n\n    def on_tx_dialog_closed(self, event_id, _tx: Optional['Transaction']):\n        self.mark_pending_event_rcvd(event_id)\n\n    def on_add_fail(self, msg: str):\n        self.window.show_error(msg)\n"
  },
  {
    "path": "electrum/plugins/revealer/LICENSE_DEJAVU.txt",
    "content": "Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.\nGlyphs imported from Arev fonts are (c) Tavmjong Bah (see below)\n\nBitstream Vera Fonts Copyright\n------------------------------\n\nCopyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is\na trademark of Bitstream, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof the fonts accompanying this license (\"Fonts\") and associated\ndocumentation files (the \"Font Software\"), to reproduce and distribute the\nFont Software, including without limitation the rights to use, copy, merge,\npublish, distribute, and/or sell copies of the Font Software, and to permit\npersons to whom the Font Software is furnished to do so, subject to the\nfollowing conditions:\n\nThe above copyright and trademark notices and this permission notice shall\nbe included in all copies of one or more of the Font Software typefaces.\n\nThe Font Software may be modified, altered, or added to, and in particular\nthe designs of glyphs or characters in the Fonts may be modified and\nadditional glyphs or characters may be added to the Fonts, only if the fonts\nare renamed to names not containing either the words \"Bitstream\" or the word\n\"Vera\".\n\nThis License becomes null and void to the extent applicable to Fonts or Font\nSoftware that has been modified and is distributed under the \"Bitstream\nVera\" names.\n\nThe Font Software may be sold as part of a larger software package but no\ncopy of one or more of the Font Software typefaces may be sold by itself.\n\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,\nTRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME\nFOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING\nANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF\nTHE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE\nFONT SOFTWARE.\n\nExcept as contained in this notice, the names of Gnome, the Gnome\nFoundation, and Bitstream Inc., shall not be used in advertising or\notherwise to promote the sale, use or other dealings in this Font Software\nwithout prior written authorization from the Gnome Foundation or Bitstream\nInc., respectively. For further information, contact: fonts at gnome dot\norg. \n\nArev Fonts Copyright\n------------------------------\n\nCopyright (c) 2006 by Tavmjong Bah. All Rights Reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the fonts accompanying this license (\"Fonts\") and\nassociated documentation files (the \"Font Software\"), to reproduce\nand distribute the modifications to the Bitstream Vera Font Software,\nincluding without limitation the rights to use, copy, merge, publish,\ndistribute, and/or sell copies of the Font Software, and to permit\npersons to whom the Font Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright and trademark notices and this permission notice\nshall be included in all copies of one or more of the Font Software\ntypefaces.\n\nThe Font Software may be modified, altered, or added to, and in\nparticular the designs of glyphs or characters in the Fonts may be\nmodified and additional glyphs or characters may be added to the\nFonts, only if the fonts are renamed to names not containing either\nthe words \"Tavmjong Bah\" or the word \"Arev\".\n\nThis License becomes null and void to the extent applicable to Fonts\nor Font Software that has been modified and is distributed under the \n\"Tavmjong Bah Arev\" names.\n\nThe Font Software may be sold as part of a larger software package but\nno copy of one or more of the Font Software typefaces may be sold by\nitself.\n\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL\nTAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n\nExcept as contained in this notice, the name of Tavmjong Bah shall not\nbe used in advertising or otherwise to promote the sale, use or other\ndealings in this Font Software without prior written authorization\nfrom Tavmjong Bah. For further information, contact: tavmjong @ free\n. fr.\n\n$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $\n"
  },
  {
    "path": "electrum/plugins/revealer/SIL Open Font License.txt",
    "content": "Copyright 2010-2024 Adobe (http://www.adobe.com/), with Reserved Font Name\n'Source'. All Rights Reserved. Source is a trademark of Adobe in the United\nStates and/or other countries.\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded,\nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "electrum/plugins/revealer/__init__.py",
    "content": "\n\n"
  },
  {
    "path": "electrum/plugins/revealer/hmac_drbg.py",
    "content": "'''\nCopyright (c) 2014 David Lazar <lazard@mit.edu>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n'''\n\nimport hashlib\nimport hmac\n\nclass DRBG(object):\n    def __init__(self, seed):\n        self.key = b'\\x00' * 64\n        self.val = b'\\x01' * 64\n        self.reseed(seed)\n\n    def hmac(self, key, val):\n        return hmac.new(key, val, hashlib.sha512).digest()\n\n    def reseed(self, data=b''):\n        self.key = self.hmac(self.key, self.val + b'\\x00' + data)\n        self.val = self.hmac(self.key, self.val)\n\n        if data:\n            self.key = self.hmac(self.key, self.val + b'\\x01' + data)\n            self.val = self.hmac(self.key, self.val)\n\n    def generate(self, n):\n        xs = b''\n        while len(xs) < n:\n            self.val = self.hmac(self.key, self.val)\n            xs += self.val\n\n        self.reseed()\n\n        return xs[:n]\n"
  },
  {
    "path": "electrum/plugins/revealer/manifest.json",
    "content": "{\n    \"name\": \"revealer\",\n    \"fullname\": \"Revealer Backup Utility\",\n    \"description\": \"This plug-in allows you to create a visually encrypted backup of your wallet seeds, or of custom alphanumeric secrets.\",\n    \"icon\": \"revealer.png\",\n    \"available_for\": [\"qt\"]\n}\n"
  },
  {
    "path": "electrum/plugins/revealer/qt.py",
    "content": "'''\n\nRevealer\nDo you have something to hide?\nSecret backup plug-in for the electrum wallet.\nhttps://web.archive.org/web/20181204193709/https://revealer.cc/how-it-works/\n\nCopyright:\n    2017 Tiago Romagnani Silveira\n    2023-2024 Soren Stoutner <soren@debian.org>\n\nDistributed under the MIT software license, see the accompanying\nfile LICENCE or http://www.opensource.org/licenses/mit-license.php\n\n'''\n\nimport os\nimport random\nfrom decimal import Decimal\nfrom functools import partial\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtPrintSupport import QPrinter\nfrom PyQt6.QtCore import Qt, QRectF, QRect, QSizeF, QUrl, QPoint, QSize, QMarginsF\nfrom PyQt6.QtGui import (QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont,\n                         QColor, QDesktopServices, qRgba, QPainterPath, QPageSize, QPageLayout)\nfrom PyQt6.QtWidgets import (QGridLayout, QVBoxLayout, QHBoxLayout, QLabel,\n                             QPushButton, QLineEdit)\n\nfrom electrum.plugin import hook\nfrom electrum.i18n import _\nfrom electrum.util import make_dir, InvalidPassword, UserCancelled\nfrom electrum.gui.qt.util import (read_QIcon, EnterButton, WWLabel, icon_path,\n                                  internal_plugin_icon_path, WindowModalDialog, Buttons,\n                                  CloseButton, OkButton, HelpButton)\nfrom electrum.gui.qt.qrtextedit import ScanQRTextEdit\nfrom electrum.gui.qt.main_window import StatusBarButton\nfrom electrum.gui.qt.util import read_QIcon_from_bytes, read_QPixmap_from_bytes\nfrom electrum.gui.common_qt.util import paintQR\n\nfrom .revealer import RevealerPlugin\n\n\nif TYPE_CHECKING:\n    from electrum.gui.qt import ElectrumGui\n\n\nclass Plugin(RevealerPlugin):\n\n    MAX_PLAINTEXT_LEN = 189  # chars\n    HELP_TEXT = \"\\n\".join([\n        _(\"Revealer is a tool to encrypt your secrets visually.\"),\n        _(\"Revealer is based on the scheme 'Visual Cryptography' by Moni Naor and Adi Shamir.\"),\n        \"\",\n        _(\"Each Revealer has a unique code. It starts with a version number, \"\n          \"then 128 bits of entropy encoded in hex format, and the last three \"\n          \"digits as a checksum.\"),\n        _(\"This code is visible in the bottom right corner of the Revealer.\"),\n        _(\"With the 128bits of entropy as a random seed, the software generates a noise image.\"),\n        _(\"In the following step your secret is encrypted into a second image.\"),\n        _(\"Then you can print those two images on transparent film.\"),\n        _(\"To decrypt the secret, you need to overlay the two films, \"\n          \"then your secret will become visible.\"),\n        \"\",\n        _(\"You can calibrate your printer in the plugin settings to achieve better print quality.\"),\n    ])\n\n    def __init__(self, parent, config, name):\n        RevealerPlugin.__init__(self, parent, config, name)\n        self.base_dir = os.path.join(config.electrum_path(), 'revealer')\n\n        if self.config.get('calibration_h') is None:\n            self.config.set_key('calibration_h', 0)\n        if self.config.get('calibration_v') is None:\n            self.config.set_key('calibration_v', 0)\n\n        self.calibration_h = self.config.get('calibration_h')\n        self.calibration_v = self.config.get('calibration_v')\n\n        self.f_size = QSize(1014*2, 642*2)\n        self.abstand_h = 21\n        self.abstand_v = 34\n        self.calibration_noise = int('10' * 128)\n        self.rawnoise = False\n        make_dir(self.base_dir)\n\n        self.extension = False\n        self._init_qt_received = False\n        self.icon_bytes = self.read_file(\"revealer.png\")\n\n    @hook\n    def load_wallet(self, wallet, window):\n        if self._init_qt_received:  # only need/want the first signal\n            return\n        self._init_qt_received = True\n        # load custom fonts (note: here, and not in __init__, as it needs the QApplication to be created)\n        QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(__file__), 'SourceSans3-Bold.otf'))\n        QFontDatabase.addApplicationFont(os.path.join(os.path.dirname(__file__), 'DejaVuSansMono-Bold.ttf'))\n\n    @hook\n    def init_menubar(self, window):\n        ma = window.wallet_menu.addAction('Revealer', partial(self.setup_dialog, window))\n        icon = read_QIcon_from_bytes(self.icon_bytes)\n        ma.setIcon(icon)\n\n    def requires_settings(self):\n        return True\n\n    def settings_dialog(self, window, wallet):\n        return self.calibration_dialog(window)\n\n    def password_dialog(self, msg=None, parent=None):\n        from electrum.gui.qt.password_dialog import PasswordDialog\n        parent = parent or self\n        d = PasswordDialog(parent, msg)\n        return d.run()\n\n    def get_seed(self):\n        password = None\n        if self.wallet.has_keystore_encryption():\n            password = self.password_dialog(parent=self.d.parent())\n            if not password:\n                raise UserCancelled()\n\n        keystore = self.wallet.get_keystore()\n        if not keystore or not keystore.has_seed():\n            return\n        self.extension = bool(keystore.get_passphrase(password))\n        return keystore.get_seed(password)\n\n    def setup_dialog(self, window):\n        self.wallet = window.wallet\n        self.update_wallet_name(self.wallet)\n        self.user_input = False\n\n        self.d = WindowModalDialog(window, \"Revealer Visual Cryptography Plugin - Select Noise File\")\n        self.d.setContentsMargins(11,11,1,1)\n\n        # Create an HBox layout.  The logo will be on the left and the rest of the dialog on the right.\n        hbox_layout = QHBoxLayout(self.d)\n\n        # Create the logo label.\n        logo_label = QLabel()\n\n        # Set the logo label pixmap.\n        logo_label.setPixmap(read_QPixmap_from_bytes(self.icon_bytes))\n\n        # Align the logo label to the top left.\n        logo_label.setAlignment(Qt.AlignmentFlag.AlignLeft)\n\n        # help text button\n        help_button = HelpButton(self.HELP_TEXT)\n\n        # Create a VBox layout for the main contents of the dialog.\n        vbox_layout = QVBoxLayout()\n\n        # create a HBox for the first line to show help button and label side by side\n        first_line_hbox = QHBoxLayout()\n\n        # Populate the HBox layout with spacing between the two columns.\n        hbox_layout.addWidget(logo_label)\n        hbox_layout.addSpacing(16)\n        hbox_layout.addLayout(vbox_layout)\n\n        # Create the labels.\n        create_or_load_noise_file_label = QLabel(_(\"To encrypt a secret, you must first create or load a noise file.\"))\n        instructions_label = QLabel(_(\"Click the button above or type an existing revealer code in the box below.\"))\n\n        first_line_hbox.addWidget(create_or_load_noise_file_label)\n        first_line_hbox.addWidget(help_button)\n\n        # Allow users to select text in the labels.\n        create_or_load_noise_file_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n        instructions_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n\n        # Create the buttons.\n        create_button = QPushButton(_(\"Create a new Revealer noise file\"))\n        self.next_button = QPushButton(_(\"Next\"), self.d)\n\n        # Calculate the desired width of the create button\n        create_button_width = create_button.fontMetrics().boundingRect(create_button.text()).width() + 40\n\n        # Set the create button width.\n        create_button.setMaximumWidth(create_button_width)\n\n        # Set the create button to be the default.\n        create_button.setDefault(True)\n\n        # Initially disable the next button.\n        self.next_button.setEnabled(False)\n\n        # Define the create noise file function.\n        def create_noise_file():\n            self.make_digital(self.d)\n            self.cypherseed_dialog(window)\n\n        # Handle clicks on the buttons.\n        create_button.clicked.connect(create_noise_file)\n        self.next_button.clicked.connect(self.d.close)\n        self.next_button.clicked.connect(partial(self.cypherseed_dialog, window))\n\n        # Create the noise scan QR text edit.\n        self.noise_scan_qr_textedit = ScanQRTextEdit(config=self.config)\n\n        # Make tabs change focus from the text edit instead of inserting a tab into the field.\n        self.noise_scan_qr_textedit.setTabChangesFocus(True)\n\n        # Update the UI when the text changes.\n        self.noise_scan_qr_textedit.textChanged.connect(self.on_edit)\n\n        # Populate the VBox layout.\n        vbox_layout.addLayout(first_line_hbox)\n        vbox_layout.addWidget(create_button, alignment=Qt.AlignmentFlag.AlignCenter)\n        vbox_layout.addWidget(instructions_label)\n        vbox_layout.addWidget(self.noise_scan_qr_textedit)\n        vbox_layout.addLayout(Buttons(self.next_button))\n\n        # Add stretches to the end of the layouts to prevent the contents from spreading when the dialog is enlarged.\n        hbox_layout.addStretch(1)\n        vbox_layout.addStretch(1)\n\n        return bool(self.d.exec())\n\n    def get_noise(self):\n        # Get the text from the scan QR text edit.\n        text = self.noise_scan_qr_textedit.text()\n        return ''.join(text.split()).lower()\n\n    def on_edit(self):\n        txt = self.get_noise()\n        versioned_seed = self.get_versioned_seed_from_user_input(txt)\n        if versioned_seed:\n            self.versioned_seed = versioned_seed\n        self.user_input = bool(versioned_seed)\n        self.next_button.setEnabled(bool(versioned_seed))\n\n    def make_digital(self, dialog):\n        self.make_rawnoise(True)\n        self.bdone(dialog)\n        self.d.close()\n\n    def get_path_to_revealer_file(self, ext: str= '') -> str:\n        version = self.versioned_seed.version\n        code_id = self.versioned_seed.checksum\n        filename = self.filename_prefix + version + \"_\" + code_id + ext\n        path = os.path.join(self.base_dir, filename)\n        return os.path.normcase(os.path.abspath(path))\n\n    def get_path_to_calibration_file(self):\n        path = os.path.join(self.base_dir, 'calibration.pdf')\n        return os.path.normcase(os.path.abspath(path))\n\n    def bcrypt(self, dialog):\n        self.rawnoise = False\n        version = self.versioned_seed.version\n        code_id = self.versioned_seed.checksum\n        dialog.show_message(''.join([\n            _(\"{} encrypted for Revealer {}_{} saved as PNG and PDF at: \").format(self.was, version, code_id),\n            \"<b>\",\n            self.get_path_to_revealer_file(),\n            \"</b>\",\n            \"<br/>\",\n            \"<br/>\",\n            \"<b>\",\n            _(\"Always check your backups.\")\n        ]), rich_text=True)\n        dialog.close()\n\n    def ext_warning(self, dialog):\n        dialog.show_message(''.join([\n            \"<b>\",\n            _(\"Warning\"),\n            \": </b>\",\n            _(\"your seed extension will <b>not</b> be included in the encrypted backup.\")\n        ]), rich_text=True)\n        dialog.close()\n\n    def bdone(self, dialog):\n        version = self.versioned_seed.version\n        code_id = self.versioned_seed.checksum\n        dialog.show_message(''.join([\n            _(\"Digital Revealer ({}_{}) saved as PNG and PDF at:\").format(version, code_id),\n            \"<br/>\",\n            \"<b>\",\n            self.get_path_to_revealer_file(),\n            '</b>'\n        ]), rich_text=True)\n\n\n    def customtxt_limits(self):\n        txt = self.custom_secret_scan_qr_textedit.text()\n        self.custom_secret_character_count_label.setText(f\"({len(txt)}/{self.MAX_PLAINTEXT_LEN})\")\n\n        # Hide the custom secret maximum characters warning label.\n        self.custom_secret_maximum_characters_warning_label.setVisible(False)\n\n        # Update the status of the encrypt custom secret button.\n        self.encrypt_custom_secret_button.setEnabled(len(txt)>0)\n\n        # Check to make sure the length of the text has not exceeded the limit.\n        if len(txt) > self.MAX_PLAINTEXT_LEN:\n            # Truncate the text to the maximum limit.\n            self.custom_secret_scan_qr_textedit.setPlainText(txt[:self.MAX_PLAINTEXT_LEN])\n\n            # Get the text cursor.\n            textCursor = self.custom_secret_scan_qr_textedit.textCursor()\n\n            # Move the cursor position to the end (setting the text above automatically moves the cursor to the beginning, which is undesirable)\n            textCursor.movePosition(textCursor.MoveOperation.End)\n\n            # Set the text cursor with the corrected position.\n            self.custom_secret_scan_qr_textedit.setTextCursor(textCursor)\n\n            # Display the custom secret maximum characters warning label.\n            self.custom_secret_maximum_characters_warning_label.setVisible(True)\n\n    def t(self):\n        self.txt = self.custom_secret_scan_qr_textedit.text()\n        self.seed_img(is_seed=False)\n\n    def warn_old_revealer(self):\n        if self.versioned_seed.version == '0':\n            link = \"https://revealer.cc/revealer-warning-and-upgrade/\"\n            self.d.show_warning((\"<b>{warning}: </b>{ver0}<br>\"\n                                 \"{url}<br>\"\n                                 \"{risk}\")\n                                .format(warning=_(\"Warning\"),\n                                        ver0=_(\"Revealers starting with 0 are not secure due to a vulnerability.\"),\n                                        url=_(\"More info at: {}\").format(f'<a href=\"{link}\">{link}</a>'),\n                                        risk=_(\"Proceed at your own risk.\")),\n                                rich_text=True)\n\n    def cypherseed_dialog(self, window):\n        self.warn_old_revealer()\n\n        d = WindowModalDialog(window, \"Revealer Visual Cryptography Plugin - Encryption Data\")\n        d.setContentsMargins(11, 11, 1, 1)\n        self.c_dialog = d\n\n        # Create an HBox layout.  The logo will be on the left and the rest of the dialog on the right.\n        hbox_layout = QHBoxLayout(d)\n\n        # Create the logo label.\n        logo_label = QLabel()\n\n        # Set the logo label pixmap.\n        logo_label.setPixmap(read_QPixmap_from_bytes(self.icon_bytes))\n\n        # Align the logo label to the top left.\n        logo_label.setAlignment(Qt.AlignmentFlag.AlignLeft)\n\n        # Create a VBox layout for the main contents of the dialog.\n        vbox_layout = QVBoxLayout()\n\n        # Populate the HBox layout.\n        hbox_layout.addWidget(logo_label)\n        hbox_layout.addSpacing(16)\n        hbox_layout.addLayout(vbox_layout)\n\n        # Create the labels.\n        ready_to_encrypt_label = QLabel(_(\"Ready to encrypt for revealer {}.\").format(self.versioned_seed.version+'_'+self.versioned_seed.checksum))\n        instructions_label = QLabel(_(\"Click the button above to encrypt the seed or type a custom alphanumerical secret below.\"))\n        self.custom_secret_character_count_label = QLabel(f\"(0/{self.MAX_PLAINTEXT_LEN})\")\n        self.custom_secret_maximum_characters_warning_label = QLabel(\"<font color='red'>\"\n                                                       + _(\"This version supports a maximum of {} characters.\").format(self.MAX_PLAINTEXT_LEN)\n                                                       +\"</font>\")\n        one_time_pad_warning_label = QLabel(\"<b>\" + _(\"Warning \") + \"</b>: \" + _(\"each Revealer is a one-time-pad, use it for a single secret.\"))\n\n        # Allow users to select text in the labels.\n        ready_to_encrypt_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n        instructions_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n        self.custom_secret_character_count_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n        one_time_pad_warning_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n\n        # Align the custom secret character count label to the right.\n        self.custom_secret_character_count_label.setAlignment(Qt.AlignmentFlag.AlignRight)\n\n        # Initially hide the custom secret character count label.\n        self.custom_secret_maximum_characters_warning_label.setVisible(False)\n\n        # Create the buttons.\n        encrypt_seed_button = QPushButton(_(\"Encrypt {}'s seed\").format(self.wallet_name))\n        self.encrypt_custom_secret_button = QPushButton(_(\"Encrypt custom secret\"))\n\n        # Calculate the desired width of the buttons.\n        encrypt_seed_button_width = encrypt_seed_button.fontMetrics().boundingRect(encrypt_seed_button.text()).width() + 40\n        encrypt_custom_secret_button_width = self.encrypt_custom_secret_button.fontMetrics().boundingRect(self.encrypt_custom_secret_button.text()).width() + 40\n\n        # Set the button widths.\n        encrypt_seed_button.setMaximumWidth(encrypt_seed_button_width)\n        self.encrypt_custom_secret_button.setMaximumWidth(encrypt_custom_secret_button_width)\n\n        # Set the encrypt seed button to be the default.\n        encrypt_seed_button.setDefault(True)\n\n        # Initially disable the encrypt custom secret button.\n        self.encrypt_custom_secret_button.setEnabled(False)\n\n        # Handle clicks on the buttons.\n        encrypt_seed_button.clicked.connect(partial(self.seed_img, True))\n        self.encrypt_custom_secret_button.clicked.connect(self.t)\n\n        # Create the custom secret scan QR text edit.\n        self.custom_secret_scan_qr_textedit = ScanQRTextEdit(config=self.config)\n\n        # Make tabs change focus from the text edit instead of inserting a tab into the field.\n        self.custom_secret_scan_qr_textedit.setTabChangesFocus(True)\n\n        # Update the UI when the custom secret text changes.\n        self.custom_secret_scan_qr_textedit.textChanged.connect(self.customtxt_limits)\n\n        # Populate the VBox layout.\n        vbox_layout.addWidget(ready_to_encrypt_label)\n        vbox_layout.addWidget(encrypt_seed_button, alignment=Qt.AlignmentFlag.AlignCenter)\n        vbox_layout.addWidget(instructions_label)\n        vbox_layout.addWidget(self.custom_secret_scan_qr_textedit)\n        vbox_layout.addWidget(self.custom_secret_character_count_label)\n        vbox_layout.addWidget(self.custom_secret_maximum_characters_warning_label)\n        vbox_layout.addWidget(self.encrypt_custom_secret_button, alignment=Qt.AlignmentFlag.AlignCenter)\n        vbox_layout.addSpacing(40)\n        vbox_layout.addWidget(one_time_pad_warning_label)\n        vbox_layout.addLayout(Buttons(CloseButton(d)))\n\n        # Add stretches to the end of the layouts to prevent the contents from spreading when the dialog is enlarged.\n        hbox_layout.addStretch(1)\n        vbox_layout.addStretch(1)\n\n        return bool(d.exec())\n\n    def update_wallet_name(self, name):\n        self.wallet_name = str(name)\n\n    def seed_img(self, is_seed = True):\n\n        if is_seed:\n            try:\n                cseed = self.get_seed()\n            except UserCancelled:\n                return\n            except InvalidPassword as e:\n                self.d.show_error(str(e))\n                return\n            if not cseed:\n                self.d.show_message(_(\"This wallet has no seed\"))\n                return\n            txt = cseed.upper()\n        else:\n            txt = self.txt.upper()\n\n        img = QImage(self.SIZE[0], self.SIZE[1], QImage.Format.Format_Mono)\n        bitmap = QBitmap.fromImage(img, Qt.ImageConversionFlag.MonoOnly)\n        bitmap.fill(Qt.GlobalColor.white)\n        painter = QPainter()\n        painter.begin(bitmap)\n        if len(txt) < 102 :\n            fontsize = 15\n            linespace = 15\n            max_letters = 17\n            max_lines = 6\n            max_words = 3\n        else:\n            fontsize = 12\n            linespace = 10\n            max_letters = 21\n            max_lines = 9\n            max_words = int(max_letters/4)\n\n        font = QFont('Source Sans 3', fontsize, QFont.Weight.Bold)\n        font.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 100)\n        font.setPixelSize(fontsize)\n        painter.setFont(font)\n        seed_array = txt.split(' ')\n\n        for n in range(max_lines):\n            nwords = max_words\n            temp_seed = seed_array[:nwords]\n            while len(' '.join(map(str, temp_seed))) > max_letters:\n               nwords = nwords - 1\n               temp_seed = seed_array[:nwords]\n            painter.drawText(QRect(0, linespace*n, self.SIZE[0], self.SIZE[1]), Qt.AlignmentFlag.AlignHCenter, ' '.join(map(str, temp_seed)))\n            del seed_array[:nwords]\n\n        painter.end()\n        img = bitmap.toImage()\n        if not self.rawnoise:\n            self.make_rawnoise()\n\n        self.make_cypherseed(img, self.rawnoise, False, is_seed)\n        return img\n\n    def make_rawnoise(self, create_revealer=False):\n        if not self.user_input:\n            self.versioned_seed = self.gen_random_versioned_seed()\n        assert self.versioned_seed\n        w, h = self.SIZE\n        rawnoise = QImage(w, h, QImage.Format.Format_Mono)\n\n        noise_map = self.get_noise_map(self.versioned_seed)\n        for (x,y), pixel in noise_map.items():\n            rawnoise.setPixel(x, y, pixel)\n\n        self.rawnoise = rawnoise\n        if create_revealer:\n            self.make_revealer()\n\n    def make_calnoise(self):\n        random.seed(self.calibration_noise)\n        w, h = self.SIZE\n        rawnoise = QImage(w, h, QImage.Format.Format_Mono)\n        for x in range(w):\n            for y in range(h):\n                rawnoise.setPixel(x,y,random.randint(0, 1))\n        self.calnoise = self.pixelcode_2x2(rawnoise)\n\n    def make_revealer(self):\n        revealer = self.pixelcode_2x2(self.rawnoise)\n        revealer.invertPixels()\n        revealer = QBitmap.fromImage(revealer)\n        revealer = revealer.scaled(self.f_size, Qt.AspectRatioMode.KeepAspectRatio)\n        revealer = self.overlay_marks(revealer)\n\n        self.filename_prefix = 'revealer_'\n        revealer.save(self.get_path_to_revealer_file('.png'))\n        self.toPdf(QImage(revealer))\n\n    def make_cypherseed(self, img, rawnoise, calibration=False, is_seed = True):\n        img = img.convertToFormat(QImage.Format.Format_Mono)\n        p = QPainter()\n        p.begin(img)\n        p.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceXorDestination) #xor\n        p.drawImage(0, 0, rawnoise)\n        p.end()\n        cypherseed = self.pixelcode_2x2(img)\n        cypherseed = QBitmap.fromImage(cypherseed)\n        cypherseed = cypherseed.scaled(self.f_size, Qt.AspectRatioMode.KeepAspectRatio)\n        cypherseed = self.overlay_marks(cypherseed, True, calibration)\n\n        if not is_seed:\n            self.filename_prefix = 'custom_secret_'\n            self.was = _('Custom secret')\n        else:\n            self.filename_prefix = self.wallet_name + '_seed_'\n            self.was = self.wallet_name + ' ' + _('seed')\n            if self.extension:\n                self.ext_warning(self.c_dialog)\n\n\n        if not calibration:\n            self.toPdf(QImage(cypherseed))\n            cypherseed.save(self.get_path_to_revealer_file('.png'))\n            self.bcrypt(self.c_dialog)\n        return cypherseed\n\n    def calibration(self):\n        img = QImage(self.SIZE[0], self.SIZE[1], QImage.Format.Format_Mono)\n        bitmap = QBitmap.fromImage(img, Qt.ImageConversionFlag.MonoOnly)\n        bitmap.fill(Qt.GlobalColor.black)\n        self.make_calnoise()\n        img = self.overlay_marks(self.calnoise.scaledToHeight(self.f_size.height()), False, True)\n        self.calibration_pdf(img)\n        QDesktopServices.openUrl(QUrl.fromLocalFile(self.get_path_to_calibration_file()))\n        return img\n\n    def toPdf(self, image):\n        printer = QPrinter()\n        printer.setPageSize(QPageSize(QSizeF(210, 297), QPageSize.Unit.Millimeter))\n        printer.setResolution(600)\n        printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)\n        printer.setOutputFileName(self.get_path_to_revealer_file('.pdf'))\n        printer.setPageMargins(QMarginsF(0, 0, 0, 0), QPageLayout.Unit.Millimeter)\n        painter = QPainter()\n        painter.begin(printer)\n\n        delta_h = round(image.width()/self.abstand_v)\n        delta_v = round(image.height()/self.abstand_h)\n\n        size_h = round(2028+((int(self.calibration_h)*2028/(2028-(delta_h*2)+int(self.calibration_h)))//2))\n        size_v = round(1284+((int(self.calibration_v)*1284/(1284-(delta_v*2)+int(self.calibration_v)))//2))\n\n        image =  image.scaled(size_h, size_v)\n\n        painter.drawImage(553,533, image)\n        wpath = QPainterPath()\n        wpath.addRoundedRect(QRectF(553,533, size_h, size_v), 19, 19)\n        painter.setPen(QPen(Qt.GlobalColor.black, 1))\n        painter.drawPath(wpath)\n        painter.end()\n\n    def calibration_pdf(self, image):\n        printer = QPrinter()\n        printer.setPageSize(QPageSize(QSizeF(210, 297), QPageSize.Unit.Millimeter))\n        printer.setResolution(600)\n        printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)\n        printer.setOutputFileName(self.get_path_to_calibration_file())\n        printer.setPageMargins(QMarginsF(0, 0, 0, 0), QPageLayout.Unit.Millimeter)\n\n        painter = QPainter()\n        painter.begin(printer)\n        painter.drawImage(553,533, image)\n        font = QFont('Source Sans 3', 10, QFont.Weight.Bold)\n        painter.setFont(font)\n        painter.drawText(254,277, _(\"Calibration sheet\"))\n        font = QFont('Source Sans 3', 7, QFont.Weight.Bold)\n        painter.setFont(font)\n        painter.drawText(600,2077, _(\"Instructions:\"))\n        font = QFont(\"\", 7, QFont.Weight.Normal)\n        painter.setFont(font)\n        painter.drawText(700, 2177, _(\"1. Place this paper on a flat and well illuminated surface.\"))\n        painter.drawText(700, 2277, _(\"2. Align your Revealer borderlines to the dashed lines on the top and left.\"))\n        painter.drawText(700, 2377, _(\"3. Press slightly the Revealer against the paper and read the numbers that best \"\n                                      \"match on the opposite sides. \"))\n        painter.drawText(700, 2477, _(\"4. Type the numbers in the software\"))\n        painter.end()\n\n    def pixelcode_2x2(self, img):\n        result = QImage(img.width()*2, img.height()*2, QImage.Format.Format_ARGB32)\n        white = qRgba(255,255,255,0)\n        black = qRgba(0,0,0,255)\n\n        for x in range(img.width()):\n            for y in range(img.height()):\n                c = img.pixel(QPoint(x,y))\n                colors = QColor(c).getRgbF()\n                if colors[0]:\n                    result.setPixel(x*2+1,y*2+1, black)\n                    result.setPixel(x*2,y*2+1, white)\n                    result.setPixel(x*2+1,y*2, white)\n                    result.setPixel(x*2, y*2, black)\n\n                else:\n                    result.setPixel(x*2+1,y*2+1, white)\n                    result.setPixel(x*2,y*2+1, black)\n                    result.setPixel(x*2+1,y*2, black)\n                    result.setPixel(x*2, y*2, white)\n        return result\n\n    def overlay_marks(self, img, is_cseed=False, calibration_sheet=False):\n        border_color = Qt.GlobalColor.white\n        base_img = QImage(self.f_size.width(),self.f_size.height(), QImage.Format.Format_ARGB32)\n        base_img.fill(border_color)\n        img = QImage(img)\n\n        painter = QPainter()\n        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)\n        painter.begin(base_img)\n\n        total_distance_h = round(base_img.width() / self.abstand_v)\n        dist_v = round(total_distance_h) // 2\n        dist_h = round(total_distance_h) // 2\n\n        img = img.scaledToWidth(base_img.width() - (2 * (total_distance_h)))\n        painter.drawImage(total_distance_h,\n                          total_distance_h,\n                          img)\n\n        #frame around image\n        pen = QPen(Qt.GlobalColor.black, 2)\n        painter.setPen(pen)\n\n        #horz\n        painter.drawLine(0, total_distance_h, base_img.width(), total_distance_h)\n        painter.drawLine(0, base_img.height()-(total_distance_h), base_img.width(), base_img.height()-(total_distance_h))\n        #vert\n        painter.drawLine(total_distance_h, 0,  total_distance_h, base_img.height())\n        painter.drawLine(base_img.width()-(total_distance_h), 0,  base_img.width()-(total_distance_h), base_img.height())\n\n        #border around img\n        border_thick = 6\n        Rpath = QPainterPath()\n        Rpath.addRect(QRectF((total_distance_h)+(border_thick/2),\n                             (total_distance_h)+(border_thick/2),\n                             base_img.width()-((total_distance_h)*2)-((border_thick)-1),\n                             (base_img.height()-((total_distance_h))*2)-((border_thick)-1)))\n        pen = QPen(Qt.GlobalColor.black, border_thick)\n        pen.setJoinStyle(Qt.PenJoinStyle.MiterJoin)\n\n        painter.setPen(pen)\n        painter.drawPath(Rpath)\n\n        Bpath = QPainterPath()\n        Bpath.addRect(QRectF((total_distance_h), (total_distance_h),\n                             base_img.width()-((total_distance_h)*2), (base_img.height()-((total_distance_h))*2)))\n        pen = QPen(Qt.GlobalColor.black, 1)\n        painter.setPen(pen)\n        painter.drawPath(Bpath)\n\n        pen = QPen(Qt.GlobalColor.black, 1)\n        painter.setPen(pen)\n        painter.drawLine(0, base_img.height()//2, total_distance_h, base_img.height()//2)\n        painter.drawLine(base_img.width()//2, 0, base_img.width()//2, total_distance_h)\n\n        painter.drawLine(base_img.width()-total_distance_h, base_img.height()//2, base_img.width(), base_img.height()//2)\n        painter.drawLine(base_img.width()//2, base_img.height(), base_img.width()//2, base_img.height() - total_distance_h)\n\n        #print code\n        f_size = 37\n        font = QFont(\"DejaVu Sans Mono\", f_size-11, QFont.Weight.Bold)\n        font.setPixelSize(35)\n        painter.setFont(font)\n\n        if not calibration_sheet:\n            if is_cseed: #its a secret\n                painter.setPen(QPen(Qt.GlobalColor.black, 1, Qt.PenStyle.DashDotDotLine))\n                painter.drawLine(0, dist_v, base_img.width(), dist_v)\n                painter.drawLine(dist_h, 0,  dist_h, base_img.height())\n                painter.drawLine(0, base_img.height()-dist_v, base_img.width(), base_img.height()-(dist_v))\n                painter.drawLine(base_img.width()-(dist_h), 0,  base_img.width()-(dist_h), base_img.height())\n\n                painter.drawImage(((total_distance_h))+11, ((total_distance_h))+11,\n                                  QImage(internal_plugin_icon_path(self.name, 'electrumb.png')).scaledToWidth(round(2.1*total_distance_h), Qt.TransformationMode.SmoothTransformation))\n\n                painter.setPen(QPen(Qt.GlobalColor.white, border_thick*8))\n                painter.drawLine(int(base_img.width()-total_distance_h-(border_thick*8)/2-(border_thick/2)-2),\n                                 int(base_img.height()-total_distance_h-((border_thick*8)/2)-(border_thick/2)-2),\n                                 int(base_img.width()-total_distance_h-(border_thick*8)/2-(border_thick/2)-2 - 77),\n                                 int(base_img.height()-total_distance_h-((border_thick*8)/2)-(border_thick/2)-2))\n                painter.setPen(QColor(0,0,0,255))\n                painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick - 11,\n                                       base_img.height()-total_distance_h - border_thick), Qt.AlignmentFlag.AlignRight,\n                                 self.versioned_seed.version + '_'+self.versioned_seed.checksum)\n                painter.end()\n\n            else: # revealer\n\n                painter.setPen(QPen(border_color, 17))\n                painter.drawLine(0, dist_v, base_img.width(), dist_v)\n                painter.drawLine(dist_h, 0,  dist_h, base_img.height())\n                painter.drawLine(0, base_img.height()-dist_v, base_img.width(), base_img.height()-(dist_v))\n                painter.drawLine(base_img.width()-(dist_h), 0,  base_img.width()-(dist_h), base_img.height())\n\n                painter.setPen(QPen(Qt.GlobalColor.black, 2))\n                painter.drawLine(0, dist_v, base_img.width(), dist_v)\n                painter.drawLine(dist_h, 0,  dist_h, base_img.height())\n                painter.drawLine(0, base_img.height()-dist_v, base_img.width(), base_img.height()-(dist_v))\n                painter.drawLine(base_img.width()-(dist_h), 0,  base_img.width()-(dist_h), base_img.height())\n                logo = QImage(internal_plugin_icon_path(self.name, 'revealer_c.png')).scaledToWidth(round(1.3*(total_distance_h)))\n                painter.drawImage(int(total_distance_h+border_thick), int(total_distance_h+border_thick), logo)\n\n                #frame around logo\n                painter.setPen(QPen(Qt.GlobalColor.black, border_thick))\n                painter.drawLine(int(total_distance_h+border_thick), int(total_distance_h+logo.height()+3*(border_thick/2)),\n                                 int(total_distance_h+logo.width()+border_thick), int(total_distance_h+logo.height()+3*(border_thick/2)))\n                painter.drawLine(int(logo.width()+total_distance_h+3*(border_thick/2)), int(total_distance_h+(border_thick)),\n                                 int(total_distance_h+logo.width()+3*(border_thick/2)), int(total_distance_h+logo.height()+(border_thick)))\n\n                #frame around code/qr\n                qr_size = 179\n\n                painter.drawLine(int((base_img.width()-((total_distance_h))-(border_thick/2)-2)-qr_size),\n                                 int((base_img.height()-((total_distance_h)))-((border_thick*8))-(border_thick/2)-2),\n                                 int((base_img.width()//2+(total_distance_h/2)-border_thick-(border_thick*8)//2)-qr_size),\n                                 int((base_img.height()-((total_distance_h)))-((border_thick*8))-(border_thick/2)-2))\n\n                painter.drawLine(int((base_img.width()//2+(total_distance_h/2)-border_thick-(border_thick*8)//2)-qr_size),\n                                 int((base_img.height()-((total_distance_h)))-((border_thick*8))-(border_thick/2)-2),\n                                 int(base_img.width()//2 + (total_distance_h/2)-border_thick-(border_thick*8)//2-qr_size),\n                                 int((base_img.height()-((total_distance_h)))-(border_thick/2)-2))\n\n                painter.setPen(QPen(Qt.GlobalColor.white, border_thick * 8))\n                painter.drawLine(\n                    int(base_img.width() - ((total_distance_h)) - (border_thick * 8) / 2 - (border_thick / 2) - 2),\n                    int((base_img.height() - ((total_distance_h))) - ((border_thick * 8) / 2) - (border_thick / 2) - 2),\n                    int(base_img.width() / 2 + (total_distance_h / 2) - border_thick - qr_size),\n                    int((base_img.height() - ((total_distance_h))) - ((border_thick * 8) / 2) - (border_thick / 2) - 2))\n\n                painter.setPen(QColor(0,0,0,255))\n                painter.drawText(QRect(int(((base_img.width()/2) +21)-qr_size),\n                                       int(base_img.height()-107),\n                                       int(base_img.width()-total_distance_h - border_thick -93),\n                                       int(base_img.height()-total_distance_h - border_thick)),\n                                 Qt.AlignmentFlag.AlignLeft, self.versioned_seed.get_ui_string_version_plus_seed())\n                painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick -3 -qr_size,\n                                       base_img.height()-total_distance_h - border_thick), Qt.AlignmentFlag.AlignRight, self.versioned_seed.checksum)\n\n                # draw qr code\n                qr_qt = paintQR(self.versioned_seed.get_ui_string_version_plus_seed()\n                                     + self.versioned_seed.checksum)\n                target = QRectF(base_img.width()-65-qr_size,\n                                base_img.height()-65-qr_size,\n                                qr_size, qr_size)\n                painter.drawImage(target, qr_qt)\n                painter.setPen(QPen(Qt.GlobalColor.black, 4))\n                painter.drawLine(\n                    int(base_img.width()-65-qr_size),\n                    int(base_img.height()-65-qr_size),\n                    int(base_img.width() - 65 - qr_size),\n                    int((base_img.height() - total_distance_h) - (border_thick * 8) - (border_thick / 2) - 4),\n                )\n                painter.drawLine(\n                    int(base_img.width()-65-qr_size),\n                    int(base_img.height()-65-qr_size),\n                    int(base_img.width() - 65),\n                    int(base_img.height()-65-qr_size),\n                )\n                painter.end()\n\n        else: # calibration only\n            painter.end()\n            cal_img = QImage(self.f_size.width() + 100, self.f_size.height() + 100,\n                              QImage.Format.Format_ARGB32)\n            cal_img.fill(Qt.GlobalColor.white)\n\n            cal_painter = QPainter()\n            cal_painter.begin(cal_img)\n            cal_painter.drawImage(0,0, base_img)\n\n            #black lines in the middle of border top left only\n            cal_painter.setPen(QPen(Qt.GlobalColor.black, 1, Qt.PenStyle.DashDotDotLine))\n            cal_painter.drawLine(0, dist_v, base_img.width(), dist_v)\n            cal_painter.drawLine(dist_h, 0,  dist_h, base_img.height())\n\n            pen = QPen(Qt.GlobalColor.black, 2, Qt.PenStyle.DashDotDotLine)\n            cal_painter.setPen(pen)\n            n=15\n\n            cal_painter.setFont(QFont(\"DejaVu Sans Mono\", 21, QFont.Weight.Bold))\n            for x in range(-n,n):\n                #lines on bottom (vertical calibration)\n                cal_painter.drawLine(int((((base_img.width())/(n*2)) *(x))+ (base_img.width()//2)-13),\n                                     int(x+2+base_img.height()-(dist_v)),\n                                     int((((base_img.width())/(n*2)) *(x))+ (base_img.width()//2)+13),\n                                     int(x+2+base_img.height()-(dist_v)))\n\n                num_pos = 9\n                if x > 9 : num_pos = 17\n                if x < 0 : num_pos = 20\n                if x < -9: num_pos = 27\n\n                cal_painter.drawText(int((((base_img.width())/(n*2)) *(x)) + (base_img.width()//2)-num_pos),\n                                     int(50+base_img.height()-(dist_v)),\n                                     str(x))\n\n                #lines on the right (horizontal calibrations)\n\n                cal_painter.drawLine(int(x+2+(base_img.width()-(dist_h))),\n                                     int(((base_img.height()/(2*n)) *(x))+ (base_img.height()/n)+(base_img.height()//2)-13),\n                                     int(x+2+(base_img.width()-(dist_h))),\n                                     int(((base_img.height()/(2*n)) *(x))+ (base_img.height()/n)+(base_img.height()//2)+13))\n\n\n                cal_painter.drawText(int(30+(base_img.width()-(dist_h))),\n                                     int(((base_img.height()/(2*n)) *(x))+ (base_img.height()//2)+13),\n                                     str(x))\n\n            cal_painter.end()\n            base_img = cal_img\n\n        return base_img\n\n    def calibration_dialog(self, window):\n        d = WindowModalDialog(window, _(\"Revealer - Printer calibration settings\"))\n\n        d.setMinimumSize(100, 200)\n\n        vbox = QVBoxLayout(d)\n        vbox.addWidget(QLabel(''.join([\"<br/>\", _(\"If you have an old printer, or want optimal precision\"),\"<br/>\",\n                                       _(\"print the calibration pdf and follow the instructions \"), \"<br/>\",\"<br/>\",\n                                    ])))\n        self.calibration_h = self.config.get('calibration_h')\n        self.calibration_v = self.config.get('calibration_v')\n        cprint = QPushButton(_(\"Open calibration pdf\"))\n        cprint.clicked.connect(self.calibration)\n        vbox.addWidget(cprint)\n\n        vbox.addWidget(QLabel(_('Calibration values:')))\n        grid = QGridLayout()\n        vbox.addLayout(grid)\n        grid.addWidget(QLabel(_('Right side')), 0, 0)\n        horizontal = QLineEdit()\n        horizontal.setText(str(self.calibration_h))\n        grid.addWidget(horizontal, 0, 1)\n\n        grid.addWidget(QLabel(_('Bottom')), 1, 0)\n        vertical = QLineEdit()\n        vertical.setText(str(self.calibration_v))\n        grid.addWidget(vertical, 1, 1)\n\n        vbox.addStretch()\n        vbox.addSpacing(13)\n        vbox.addLayout(Buttons(CloseButton(d), OkButton(d)))\n\n        if not d.exec():\n            return\n\n        self.calibration_h = int(Decimal(horizontal.text()))\n        self.config.set_key('calibration_h', self.calibration_h)\n        self.calibration_v = int(Decimal(vertical.text()))\n        self.config.set_key('calibration_v', self.calibration_v)\n\n\n"
  },
  {
    "path": "electrum/plugins/revealer/revealer.py",
    "content": "import random\nimport os\nfrom hashlib import sha256\nfrom typing import NamedTuple, Optional, Dict, Tuple\n\nfrom electrum.plugin import BasePlugin\nfrom electrum.util import to_bytes, bfh\n\nfrom .hmac_drbg import DRBG\n\n\nclass VersionedSeed(NamedTuple):\n    version: str\n    seed: str\n    checksum: str\n\n    def get_ui_string_version_plus_seed(self):\n        version, seed = self.version, self.seed\n        assert isinstance(version, str) and len(version) == 1, version\n        assert isinstance(seed, str) and len(seed) >= 32\n        ret = version + seed\n        ret = ret.upper()\n        return ' '.join(ret[i : i+4] for i in range(0, len(ret), 4))\n\n\nclass RevealerPlugin(BasePlugin):\n\n    LATEST_VERSION = '1'\n    KNOWN_VERSIONS = ('0', '1')\n    assert LATEST_VERSION in KNOWN_VERSIONS\n\n    SIZE = (159, 97)\n\n    def __init__(self, parent, config, name):\n        BasePlugin.__init__(self, parent, config, name)\n\n    @classmethod\n    def code_hashid(cls, txt: str) -> str:\n        txt = txt.lower()\n        x = to_bytes(txt, 'utf8')\n        hash = sha256(x).hexdigest()\n        return hash[-3:].upper()\n\n    @classmethod\n    def get_versioned_seed_from_user_input(cls, txt: str) -> Optional[VersionedSeed]:\n        if len(txt) < 34:\n            return None\n        try:\n            int(txt, 16)\n        except Exception:\n            return None\n        version = txt[0]\n        if version not in cls.KNOWN_VERSIONS:\n            return None\n        checksum = cls.code_hashid(txt[:-3])\n        if txt[-3:].upper() != checksum.upper():\n            return None\n        return VersionedSeed(version=version.upper(),\n                             seed=txt[1:-3].upper(),\n                             checksum=checksum.upper())\n\n    @classmethod\n    def get_noise_map(cls, versioned_seed: VersionedSeed) -> Dict[Tuple[int, int], int]:\n        \"\"\"Returns a map from (x,y) coordinate to pixel value 0/1, to be used as rawnoise.\"\"\"\n        w, h = cls.SIZE\n        version  = versioned_seed.version\n        hex_seed = versioned_seed.seed\n        checksum = versioned_seed.checksum\n        noise_map = {}\n        if version == '0':\n            random.seed(int(hex_seed, 16))\n            for x in range(w):\n                for y in range(h):\n                    noise_map[(x, y)] = random.randint(0, 1)\n        elif version == '1':\n            prng_seed = bfh(hex_seed + version + checksum)\n            drbg = DRBG(prng_seed)\n            num_noise_bytes = 1929  # ~ w*h\n            noise_array = bin(int.from_bytes(drbg.generate(num_noise_bytes), 'big'))[2:]\n            # there's an approx 1/1024 chance that the generated number is 'too small'\n            # and we would get IndexError below. easiest backwards compat fix:\n            noise_array += '0' * (w * h - len(noise_array))\n            i = 0\n            for x in range(w):\n                for y in range(h):\n                    noise_map[(x, y)] = int(noise_array[i])\n                    i += 1\n        else:\n            raise Exception(f\"unexpected revealer version: {version}\")\n        return noise_map\n\n    @classmethod\n    def gen_random_versioned_seed(cls):\n        version = cls.LATEST_VERSION\n        hex_seed = os.urandom(16).hex()\n        checksum = cls.code_hashid(version + hex_seed)\n        return VersionedSeed(version=version.upper(),\n                             seed=hex_seed.upper(),\n                             checksum=checksum.upper())\n\n\nif __name__ == '__main__':\n    for i in range(10**4):\n        vs = RevealerPlugin.gen_random_versioned_seed()\n        nm = RevealerPlugin.get_noise_map(vs)\n"
  },
  {
    "path": "electrum/plugins/safe_t/__init__.py",
    "content": "\n"
  },
  {
    "path": "electrum/plugins/safe_t/client.py",
    "content": "from safetlib.client import proto, BaseClient, ProtocolMixin\nfrom .clientbase import SafeTClientBase\n\nclass SafeTClient(SafeTClientBase, ProtocolMixin, BaseClient):\n    def __init__(self, transport, handler, plugin):\n        BaseClient.__init__(self, transport=transport)\n        ProtocolMixin.__init__(self, transport=transport)\n        SafeTClientBase.__init__(self, handler, plugin, proto)\n\n\nSafeTClientBase.wrap_methods(SafeTClient)\n"
  },
  {
    "path": "electrum/plugins/safe_t/clientbase.py",
    "content": "import time\nfrom struct import pack\nfrom typing import Optional\n\nimport electrum_ecc as ecc\n\nfrom electrum.i18n import _\nfrom electrum.util import UserCancelled\nfrom electrum.keystore import bip39_normalize_passphrase\nfrom electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath\nfrom electrum.logging import Logger\nfrom electrum.plugin import runs_in_hwd_thread\nfrom electrum.hw_wallet.plugin import HardwareClientBase, HardwareHandlerBase\n\n\nclass GuiMixin(object):\n    # Requires: self.proto, self.device\n    handler: Optional[HardwareHandlerBase]\n\n    # ref: https://github.com/trezor/trezor-common/blob/44dfb07cfaafffada4b2ce0d15ba1d90d17cf35e/protob/types.proto#L89\n    messages = {\n        3: _(\"Confirm the transaction output on your {} device\"),\n        4: _(\"Confirm internal entropy on your {} device to begin\"),\n        5: _(\"Write down the seed word shown on your {}\"),\n        6: _(\"Confirm on your {} that you want to wipe it clean\"),\n        7: _(\"Confirm on your {} device the message to sign\"),\n        8: _(\"Confirm the total amount spent and the transaction fee on your \"\n             \"{} device\"),\n        10: _(\"Confirm wallet address on your {} device\"),\n        14: _(\"Choose on your {} device where to enter your passphrase\"),\n        'default': _(\"Check your {} device to continue\"),\n    }\n\n    def callback_Failure(self, msg):\n        # BaseClient's unfortunate call() implementation forces us to\n        # raise exceptions on failure in order to unwind the stack.\n        # However, making the user acknowledge they cancelled\n        # gets old very quickly, so we suppress those.  The NotInitialized\n        # one is misnamed and indicates a passphrase request was cancelled.\n        if msg.code in (self.types.FailureType.PinCancelled,\n                        self.types.FailureType.ActionCancelled,\n                        self.types.FailureType.NotInitialized):\n            raise UserCancelled()\n        raise RuntimeError(msg.message)\n\n    def callback_ButtonRequest(self, msg):\n        message = self.msg\n        if not message:\n            message = self.messages.get(msg.code, self.messages['default'])\n        self.handler.show_message(message.format(self.device), self.cancel)\n        return self.proto.ButtonAck()\n\n    def callback_PinMatrixRequest(self, msg):\n        show_strength = True\n        if msg.type == 2:\n            msg = _(\"Enter a new PIN for your {}:\")\n        elif msg.type == 3:\n            msg = (_(\"Re-enter the new PIN for your {}.\\n\\n\"\n                     \"NOTE: the positions of the numbers have changed!\"))\n        else:\n            msg = _(\"Enter your current {} PIN:\")\n            show_strength = False\n        pin = self.handler.get_pin(msg.format(self.device), show_strength=show_strength)\n        if len(pin) > 9:\n            self.handler.show_error(_('The PIN cannot be longer than 9 characters.'))\n            pin = ''  # to cancel below\n        if not pin:\n            return self.proto.Cancel()\n        return self.proto.PinMatrixAck(pin=pin)\n\n    def callback_PassphraseRequest(self, req):\n        if req and hasattr(req, 'on_device') and req.on_device is True:\n            return self.proto.PassphraseAck()\n\n        if self.creating_wallet:\n            msg = _(\"Enter a passphrase to generate this wallet.  Each time \"\n                    \"you use this wallet your {} will prompt you for the \"\n                    \"passphrase.  If you forget the passphrase you cannot \"\n                    \"access the bitcoins in the wallet.\").format(self.device)\n        else:\n            msg = _(\"Enter the passphrase to unlock this wallet:\")\n        passphrase = self.handler.get_passphrase(msg, self.creating_wallet)\n        if passphrase is None:\n            return self.proto.Cancel()\n        passphrase = bip39_normalize_passphrase(passphrase)\n\n        ack = self.proto.PassphraseAck(passphrase=passphrase)\n        length = len(ack.passphrase)\n        if length > 50:\n            self.handler.show_error(_(\"Too long passphrase ({} > 50 chars).\").format(length))\n            return self.proto.Cancel()\n        return ack\n\n    def callback_PassphraseStateRequest(self, msg):\n        return self.proto.PassphraseStateAck()\n\n    def callback_WordRequest(self, msg):\n        self.step += 1\n        msg = _(\"Step {}/24.  Enter seed word as explained on \"\n                \"your {}:\").format(self.step, self.device)\n        word = self.handler.get_word(msg)\n        # Unfortunately the device can't handle self.proto.Cancel()\n        return self.proto.WordAck(word=word)\n\n\nclass SafeTClientBase(HardwareClientBase, GuiMixin, Logger):\n\n    def __init__(self, handler, plugin, proto):\n        assert hasattr(self, 'tx_api')  # ProtocolMixin already constructed?\n        HardwareClientBase.__init__(self, plugin=plugin)\n        self.proto = proto\n        self.device = plugin.device\n        self.handler = handler\n        self.tx_api = plugin\n        self.types = plugin.types\n        self.msg = None\n        self.creating_wallet = False\n        Logger.__init__(self)\n        self.used()\n\n    def device_model_name(self) -> Optional[str]:\n        return 'Safe-T'\n\n    def __str__(self):\n        return \"%s/%s\" % (self.label(), self.features.device_id)\n\n    def label(self):\n        return self.features.label\n\n    def get_soft_device_id(self):\n        return self.features.device_id\n\n    def is_initialized(self):\n        return self.features.initialized\n\n    def is_pairable(self):\n        return not self.features.bootloader_mode\n\n    @runs_in_hwd_thread\n    def has_usable_connection_with_device(self):\n        try:\n            res = self.ping(\"electrum pinging device\")\n            assert res == \"electrum pinging device\"\n        except BaseException:\n            return False\n        return True\n\n    def used(self):\n        self.last_operation = time.time()\n\n    def prevent_timeouts(self):\n        self.last_operation = float('inf')\n\n    @runs_in_hwd_thread\n    def timeout(self, cutoff):\n        '''Time out the client if the last operation was before cutoff.'''\n        if self.last_operation < cutoff:\n            self.logger.info(\"timed out\")\n            self.clear_session()\n\n    @staticmethod\n    def expand_path(n):\n        return convert_bip32_strpath_to_intpath(n)\n\n    @runs_in_hwd_thread\n    def cancel(self):\n        '''Provided here as in keepkeylib but not safetlib.'''\n        self.transport.write(self.proto.Cancel())\n\n    def i4b(self, x):\n        return pack('>I', x)\n\n    @runs_in_hwd_thread\n    def get_xpub(self, bip32_path, xtype):\n        address_n = self.expand_path(bip32_path)\n        creating = False\n        node = self.get_public_node(address_n, creating).node\n        return BIP32Node(xtype=xtype,\n                         eckey=ecc.ECPubkey(node.public_key),\n                         chaincode=node.chain_code,\n                         depth=node.depth,\n                         fingerprint=self.i4b(node.fingerprint),\n                         child_number=self.i4b(node.child_num)).to_xpub()\n\n    @runs_in_hwd_thread\n    def toggle_passphrase(self):\n        if self.features.passphrase_protection:\n            self.msg = _(\"Confirm on your {} device to disable passphrases\")\n        else:\n            self.msg = _(\"Confirm on your {} device to enable passphrases\")\n        enabled = not self.features.passphrase_protection\n        self.apply_settings(use_passphrase=enabled)\n\n    @runs_in_hwd_thread\n    def change_label(self, label):\n        self.msg = _(\"Confirm the new label on your {} device\")\n        self.apply_settings(label=label)\n\n    @runs_in_hwd_thread\n    def change_homescreen(self, homescreen):\n        self.msg = _(\"Confirm on your {} device to change your home screen\")\n        self.apply_settings(homescreen=homescreen)\n\n    @runs_in_hwd_thread\n    def set_pin(self, remove):\n        if remove:\n            self.msg = _(\"Confirm on your {} device to disable PIN protection\")\n        elif self.features.pin_protection:\n            self.msg = _(\"Confirm on your {} device to change your PIN\")\n        else:\n            self.msg = _(\"Confirm on your {} device to set a PIN\")\n        self.change_pin(remove)\n\n    @runs_in_hwd_thread\n    def clear_session(self):\n        '''Clear the session to force pin (and passphrase if enabled)\n        re-entry.  Does not leak exceptions.'''\n        self.logger.info(f\"clear session: {self}\")\n        self.prevent_timeouts()\n        try:\n            super(SafeTClientBase, self).clear_session()\n        except BaseException as e:\n            # If the device was removed it has the same effect...\n            self.logger.info(f\"clear_session: ignoring error {e}\")\n\n    @runs_in_hwd_thread\n    def get_public_node(self, address_n, creating):\n        self.creating_wallet = creating\n        return super(SafeTClientBase, self).get_public_node(address_n)\n\n    @runs_in_hwd_thread\n    def close(self):\n        '''Called when Our wallet was closed or the device removed.'''\n        self.logger.info(\"closing client\")\n        self.clear_session()\n        # Release the device\n        self.transport.close()\n\n    def firmware_version(self):\n        f = self.features\n        return (f.major_version, f.minor_version, f.patch_version)\n\n    def atleast_version(self, major, minor=0, patch=0):\n        return self.firmware_version() >= (major, minor, patch)\n\n    @staticmethod\n    def wrapper(func):\n        '''Wrap methods to clear any message box they opened.'''\n\n        def wrapped(self, *args, **kwargs):\n            try:\n                self.prevent_timeouts()\n                return func(self, *args, **kwargs)\n            finally:\n                self.used()\n                self.handler.finished()\n                self.creating_wallet = False\n                self.msg = None\n\n        return wrapped\n\n    @staticmethod\n    def wrap_methods(cls):\n        for method in ['apply_settings', 'change_pin',\n                       'get_address', 'get_public_node',\n                       'load_device_by_mnemonic', 'load_device_by_xprv',\n                       'recovery_device', 'reset_device', 'sign_message',\n                       'sign_tx', 'wipe_device']:\n            setattr(cls, method, cls.wrapper(getattr(cls, method)))\n"
  },
  {
    "path": "electrum/plugins/safe_t/cmdline.py",
    "content": "from electrum.plugin import hook\nfrom electrum.hw_wallet import CmdLineHandler\n\nfrom .safe_t import SafeTPlugin\n\nclass Plugin(SafeTPlugin):\n    handler = CmdLineHandler()\n    @hook\n    def init_keystore(self, keystore):\n        if not isinstance(keystore, self.keystore_class):\n            return\n        keystore.handler = self.handler\n\n    def create_handler(self, window):\n        return self.handler\n"
  },
  {
    "path": "electrum/plugins/safe_t/manifest.json",
    "content": "{\n  \"name\": \"safe_t\",\n  \"fullname\": \"Safe-T mini Wallet\",\n  \"description\": \"Provides support for Safe-T mini hardware wallet\",\n  \"requires\": [[\"safetlib\", \"github.com/archos-safe-t/python-safet\"]],\n  \"registers_keystore\": [\"hardware\", \"safe_t\", \"Safe-T mini wallet\"],\n  \"icon\":\"safe-t.png\",\n  \"available_for\": [\"qt\", \"cmdline\"]\n}\n"
  },
  {
    "path": "electrum/plugins/safe_t/qt.py",
    "content": "import threading\nfrom functools import partial\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import Qt, pyqtSignal, QRegularExpression\nfrom PyQt6.QtGui import QRegularExpressionValidator\nfrom PyQt6.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,\n                             QHBoxLayout, QButtonGroup, QGroupBox,\n                             QTextEdit, QLineEdit, QRadioButton, QCheckBox, QWidget,\n                             QMessageBox, QFileDialog, QSlider, QTabWidget)\n\nfrom electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,\n                                  OkButton, CloseButton, getOpenFileName, ChoiceWidget)\nfrom electrum.i18n import _\nfrom electrum.plugin import hook\nfrom electrum.logging import Logger\nfrom electrum.util import ChoiceItem\n\nfrom electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase\nfrom electrum.hw_wallet.trezor_qt_pinmatrix import PinMatrixWidget\nfrom electrum.hw_wallet.plugin import only_hook_if_libraries_available\n\nfrom .safe_t import SafeTPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY\n\nfrom electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWXPub, WalletWizardComponent\n\nif TYPE_CHECKING:\n    from electrum.gui.qt.wizard.wallet import QENewWalletWizard\n\nPASSPHRASE_HELP_SHORT =_(\n    \"Passphrases allow you to access new wallets, each \"\n    \"hidden behind a particular case-sensitive passphrase.\")\nPASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + \"  \" + _(\n    \"You need to create a separate Electrum wallet for each passphrase \"\n    \"you use as they each generate different addresses.  Changing \"\n    \"your passphrase does not lose other wallets, each is still \"\n    \"accessible behind its own passphrase.\")\nRECOMMEND_PIN = _(\n    \"You should enable PIN protection.  Your PIN is the only protection \"\n    \"for your bitcoins if your device is lost or stolen.\")\nPASSPHRASE_NOT_PIN = _(\n    \"If you forget a passphrase you will be unable to access any \"\n    \"bitcoins in the wallet behind it.  A passphrase is not a PIN. \"\n    \"Only change this if you are sure you understand it.\")\n\n\nclass QtHandler(QtHandlerBase):\n\n    pin_signal = pyqtSignal(object, object)\n\n    def __init__(self, win, pin_matrix_widget_class, device):\n        super(QtHandler, self).__init__(win, device)\n        self.pin_signal.connect(self.pin_dialog)\n        self.pin_matrix_widget_class = pin_matrix_widget_class\n\n    def get_pin(self, msg, *, show_strength=True):\n        self.done.clear()\n        self.pin_signal.emit(msg, show_strength)\n        self.done.wait()\n        return self.response\n\n    def pin_dialog(self, msg, show_strength):\n        # Needed e.g. when resetting a device\n        self.clear_dialog()\n        dialog = WindowModalDialog(self.top_level_window(), _(\"Enter PIN\"))\n        matrix = self.pin_matrix_widget_class(show_strength)\n        vbox = QVBoxLayout()\n        vbox.addWidget(QLabel(msg))\n        vbox.addWidget(matrix)\n        vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))\n        dialog.setLayout(vbox)\n        dialog.exec()\n        self.response = str(matrix.get_value())\n        self.done.set()\n\n\nclass QtPlugin(QtPluginBase):\n    # Derived classes must provide the following class-static variables:\n    #   icon_file\n    #   pin_matrix_widget_class\n\n    @only_hook_if_libraries_available\n    @hook\n    def receive_menu(self, menu, addrs, wallet):\n        if len(addrs) != 1:\n            return\n        self._add_menu_action(menu, addrs[0], wallet)\n\n    @only_hook_if_libraries_available\n    @hook\n    def transaction_dialog_address_menu(self, menu, addr, wallet):\n        self._add_menu_action(menu, addr, wallet)\n\n    def show_settings_dialog(self, window, keystore):\n        def connect():\n            device_id = self.choose_device(window, keystore)\n            return device_id\n        def show_dialog(device_id):\n            if device_id:\n                SettingsDialog(window, self, keystore, device_id).exec()\n        keystore.thread.add(connect, on_success=show_dialog)\n\n\ndef clean_text(widget):\n    text = widget.toPlainText().strip()\n    return ' '.join(text.split())\n\n\nclass SafeTInitLayout(QVBoxLayout):\n    validChanged = pyqtSignal([bool], arguments=['valid'])\n\n    def __init__(self, method, device):\n        super().__init__()\n\n        self.method = method\n\n        label = QLabel(_(\"Enter a label to name your device:\"))\n        self.label_e = QLineEdit()\n        hl = QHBoxLayout()\n        hl.addWidget(label)\n        hl.addWidget(self.label_e)\n        hl.addStretch(1)\n        self.addLayout(hl)\n\n        if method in [TIM_NEW, TIM_RECOVER]:\n            gb = QGroupBox()\n            hbox1 = QHBoxLayout()\n            gb.setLayout(hbox1)\n            self.addWidget(gb)\n            gb.setTitle(_(\"Select your seed length:\"))\n            self.bg = QButtonGroup()\n            for i, count in enumerate([12, 18, 24]):\n                rb = QRadioButton(gb)\n                rb.setText(_(\"{:d} words\").format(count))\n                self.bg.addButton(rb)\n                self.bg.setId(rb, i)\n                hbox1.addWidget(rb)\n                rb.setChecked(True)\n            self.cb_pin = QCheckBox(_('Enable PIN protection'))\n            self.cb_pin.setChecked(True)\n        else:\n            self.text_e = QTextEdit()\n            self.text_e.setMaximumHeight(60)\n            if method == TIM_MNEMONIC:\n                msg = _(\"Enter your BIP39 mnemonic:\")\n                # TODO: no validation?\n            else:\n                msg = _(\"Enter the master private key beginning with xprv:\")\n\n                def set_enabled():\n                    from electrum.bip32 import is_xprv\n                    self.validChanged.emit(is_xprv(clean_text(self.text_e)))\n                self.text_e.textChanged.connect(set_enabled)\n\n            self.addWidget(QLabel(msg))\n            self.addWidget(self.text_e)\n            self.pin = QLineEdit()\n            self.pin.setValidator(QRegularExpressionValidator(QRegularExpression('[1-9]{0,9}')))\n            self.pin.setMaximumWidth(100)\n            hbox_pin = QHBoxLayout()\n            hbox_pin.addWidget(QLabel(_(\"Enter your PIN (digits 1-9):\")))\n            hbox_pin.addWidget(self.pin)\n            hbox_pin.addStretch(1)\n\n        if method in [TIM_NEW, TIM_RECOVER]:\n            self.addWidget(WWLabel(RECOMMEND_PIN))\n            self.addWidget(self.cb_pin)\n        else:\n            self.addLayout(hbox_pin)\n\n        passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT)\n        passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)\n        passphrase_warning.setStyleSheet(\"color: red\")\n        self.cb_phrase = QCheckBox(_('Enable passphrases'))\n        self.cb_phrase.setChecked(False)\n        self.addWidget(passphrase_msg)\n        self.addWidget(passphrase_warning)\n        self.addWidget(self.cb_phrase)\n\n    def get_settings(self):\n        if self.method in [TIM_NEW, TIM_RECOVER]:\n            item = self.bg.checkedId()\n            pin = self.cb_pin.isChecked()\n        else:\n            item = ' '.join(str(clean_text(self.text_e)).split())\n            pin = str(self.pin.text())\n\n        return item, self.label_e.text(), pin, self.cb_phrase.isChecked()\n\n\nclass Plugin(SafeTPlugin, QtPlugin):\n    icon_unpaired = \"safe-t_unpaired.png\"\n    icon_paired = \"safe-t.png\"\n\n    def create_handler(self, window):\n        return QtHandler(window, self.pin_matrix_widget_class(), self.device)\n\n    @classmethod\n    def pin_matrix_widget_class(self):\n        return PinMatrixWidget\n\n    # insert safe_t pages in new wallet wizard\n    def extend_wizard(self, wizard: 'QENewWalletWizard'):\n        super().extend_wizard(wizard)\n        views = {\n            'safet_start': {'gui': WCScriptAndDerivation},\n            'safet_xpub': {'gui': WCHWXPub},\n            'safet_not_initialized': {'gui': WCSafeTInitMethod},\n            'safet_choose_new_recover': {'gui': WCSafeTInitParams},\n            'safet_do_init': {'gui': WCSafeTInit},\n            'safet_unlock': {'gui': WCHWUnlock}\n        }\n        wizard.navmap_merge(views)\n\n\nclass SettingsDialog(WindowModalDialog):\n    '''This dialog doesn't require a device be paired with a wallet.\n    We want users to be able to wipe a device even if they've forgotten\n    their PIN.'''\n\n    def __init__(self, window, plugin, keystore, device_id):\n        title = _(\"{} Settings\").format(plugin.device)\n        super(SettingsDialog, self).__init__(window, title)\n        self.setMaximumWidth(540)\n\n        devmgr = plugin.device_manager()\n        config = devmgr.config\n        handler = keystore.handler\n        thread = keystore.thread\n        hs_cols, hs_rows = (128, 64)\n\n        def invoke_client(method, *args, **kw_args):\n            unpair_after = kw_args.pop('unpair_after', False)\n\n            def task():\n                client = devmgr.client_by_id(device_id)\n                if not client:\n                    raise RuntimeError(\"Device not connected\")\n                if method:\n                    getattr(client, method)(*args, **kw_args)\n                if unpair_after:\n                    devmgr.unpair_id(device_id)\n                return client.features\n\n            thread.add(task, on_success=update)\n\n        def update(features):\n            self.features = features\n            set_label_enabled()\n            if features.bootloader_hash:\n                bl_hash = features.bootloader_hash.hex()\n                bl_hash = \"\\n\".join([bl_hash[:32], bl_hash[32:]])\n            else:\n                bl_hash = \"N/A\"\n            noyes = [_(\"No\"), _(\"Yes\")]\n            endis = [_(\"Enable Passphrases\"), _(\"Disable Passphrases\")]\n            disen = [_(\"Disabled\"), _(\"Enabled\")]\n            setchange = [_(\"Set a PIN\"), _(\"Change PIN\")]\n\n            version = \"%d.%d.%d\" % (features.major_version,\n                                    features.minor_version,\n                                    features.patch_version)\n\n            device_label.setText(features.label)\n            pin_set_label.setText(noyes[features.pin_protection])\n            passphrases_label.setText(disen[features.passphrase_protection])\n            bl_hash_label.setText(bl_hash)\n            label_edit.setText(features.label)\n            device_id_label.setText(features.device_id)\n            initialized_label.setText(noyes[features.initialized])\n            version_label.setText(version)\n            clear_pin_button.setVisible(features.pin_protection)\n            clear_pin_warning.setVisible(features.pin_protection)\n            pin_button.setText(setchange[features.pin_protection])\n            pin_msg.setVisible(not features.pin_protection)\n            passphrase_button.setText(endis[features.passphrase_protection])\n            language_label.setText(features.language)\n\n        def set_label_enabled():\n            label_apply.setEnabled(label_edit.text() != self.features.label)\n\n        def rename():\n            invoke_client('change_label', label_edit.text())\n\n        def toggle_passphrase():\n            title = _(\"Confirm Toggle Passphrase Protection\")\n            currently_enabled = self.features.passphrase_protection\n            if currently_enabled:\n                msg = _(\"After disabling passphrases, you can only pair this \"\n                        \"Electrum wallet if it had an empty passphrase.  \"\n                        \"If its passphrase was not empty, you will need to \"\n                        \"create a new wallet.  You can use this wallet again \"\n                        \"at any time by re-enabling passphrases and entering \"\n                        \"its passphrase.\")\n            else:\n                msg = _(\"Your current Electrum wallet can only be used with \"\n                        \"an empty passphrase.  You must create a separate \"\n                        \"wallet for other passphrases as each one generates \"\n                        \"a new set of addresses.\")\n            msg += \"\\n\\n\" + _(\"Are you sure you want to proceed?\")\n            if not self.question(msg, title=title):\n                return\n            invoke_client('toggle_passphrase', unpair_after=currently_enabled)\n\n        def change_homescreen():\n            filename = getOpenFileName(\n                parent=self,\n                title=_(\"Choose Homescreen\"),\n                config=config,\n            )\n            if not filename:\n                return  # user cancelled\n\n            if filename.endswith('.toif'):\n                img = open(filename, 'rb').read()\n                if img[:8] != b'TOIf\\x90\\x00\\x90\\x00':\n                    handler.show_error('File is not a TOIF file with size of 144x144')\n                    return\n            else:\n                from PIL import Image # FIXME\n                im = Image.open(filename)\n                if im.size != (128, 64):\n                    handler.show_error('Image must be 128 x 64 pixels')\n                    return\n                im = im.convert('1')\n                pix = im.load()\n                img = bytearray(1024)\n                for j in range(64):\n                    for i in range(128):\n                        if pix[i, j]:\n                            o = (i + j * 128)\n                            img[o // 8] |= (1 << (7 - o % 8))\n                img = bytes(img)\n            invoke_client('change_homescreen', img)\n\n        def clear_homescreen():\n            invoke_client('change_homescreen', b'\\x00')\n\n        def set_pin():\n            invoke_client('set_pin', remove=False)\n\n        def clear_pin():\n            invoke_client('set_pin', remove=True)\n\n        def wipe_device():\n            wallet = window.wallet\n            if wallet and sum(wallet.get_balance()):\n                title = _(\"Confirm Device Wipe\")\n                msg = _(\"Are you SURE you want to wipe the device?\\n\"\n                        \"Your wallet still has bitcoins in it!\")\n                if not self.question(msg, title=title,\n                                     icon=QMessageBox.Icon.Critical):\n                    return\n            invoke_client('wipe_device', unpair_after=True)\n\n        def slider_moved():\n            mins = timeout_slider.sliderPosition()\n            timeout_minutes.setText(_(\"{:2d} minutes\").format(mins))\n\n        def slider_released():\n            config.set_session_timeout(timeout_slider.sliderPosition() * 60)\n\n        # Information tab\n        info_tab = QWidget()\n        info_layout = QVBoxLayout(info_tab)\n        info_glayout = QGridLayout()\n        info_glayout.setColumnStretch(2, 1)\n        device_label = QLabel()\n        pin_set_label = QLabel()\n        passphrases_label = QLabel()\n        version_label = QLabel()\n        device_id_label = QLabel()\n        bl_hash_label = QLabel()\n        bl_hash_label.setWordWrap(True)\n        language_label = QLabel()\n        initialized_label = QLabel()\n        rows = [\n            (_(\"Device Label\"), device_label),\n            (_(\"PIN set\"), pin_set_label),\n            (_(\"Passphrases\"), passphrases_label),\n            (_(\"Firmware Version\"), version_label),\n            (_(\"Device ID\"), device_id_label),\n            (_(\"Bootloader Hash\"), bl_hash_label),\n            (_(\"Language\"), language_label),\n            (_(\"Initialized\"), initialized_label),\n        ]\n        for row_num, (label, widget) in enumerate(rows):\n            info_glayout.addWidget(QLabel(label), row_num, 0)\n            info_glayout.addWidget(widget, row_num, 1)\n        info_layout.addLayout(info_glayout)\n\n        # Settings tab\n        settings_tab = QWidget()\n        settings_layout = QVBoxLayout(settings_tab)\n        settings_glayout = QGridLayout()\n\n        # Settings tab - Label\n        label_msg = QLabel(_(\"Name this {}.  If you have multiple devices \"\n                             \"their labels help distinguish them.\")\n                           .format(plugin.device))\n        label_msg.setWordWrap(True)\n        label_label = QLabel(_(\"Device Label\"))\n        label_edit = QLineEdit()\n        label_edit.setMinimumWidth(150)\n        label_edit.setMaxLength(plugin.MAX_LABEL_LEN)\n        label_apply = QPushButton(_(\"Apply\"))\n        label_apply.clicked.connect(rename)\n        label_edit.textChanged.connect(set_label_enabled)\n        settings_glayout.addWidget(label_label, 0, 0)\n        settings_glayout.addWidget(label_edit, 0, 1, 1, 2)\n        settings_glayout.addWidget(label_apply, 0, 3)\n        settings_glayout.addWidget(label_msg, 1, 1, 1, -1)\n\n        # Settings tab - PIN\n        pin_label = QLabel(_(\"PIN Protection\"))\n        pin_button = QPushButton()\n        pin_button.clicked.connect(set_pin)\n        settings_glayout.addWidget(pin_label, 2, 0)\n        settings_glayout.addWidget(pin_button, 2, 1)\n        pin_msg = QLabel(_(\"PIN protection is strongly recommended.  \"\n                           \"A PIN is your only protection against someone \"\n                           \"stealing your bitcoins if they obtain physical \"\n                           \"access to your {}.\").format(plugin.device))\n        pin_msg.setWordWrap(True)\n        pin_msg.setStyleSheet(\"color: red\")\n        settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)\n\n        # Settings tab - Homescreen\n        homescreen_label = QLabel(_(\"Homescreen\"))\n        homescreen_change_button = QPushButton(_(\"Change...\"))\n        homescreen_clear_button = QPushButton(_(\"Reset\"))\n        homescreen_change_button.clicked.connect(change_homescreen)\n        try:\n            import PIL\n        except ImportError:\n            homescreen_change_button.setDisabled(True)\n            homescreen_change_button.setToolTip(\n                _(\"Required package 'PIL' is not available - Please install it.\")\n            )\n        homescreen_clear_button.clicked.connect(clear_homescreen)\n        homescreen_msg = QLabel(_(\"You can set the homescreen on your \"\n                                  \"device to personalize it.  You must \"\n                                  \"choose a {} x {} monochrome black and \"\n                                  \"white image.\").format(hs_cols, hs_rows))\n        homescreen_msg.setWordWrap(True)\n        settings_glayout.addWidget(homescreen_label, 4, 0)\n        settings_glayout.addWidget(homescreen_change_button, 4, 1)\n        settings_glayout.addWidget(homescreen_clear_button, 4, 2)\n        settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1)\n\n        # Settings tab - Session Timeout\n        timeout_label = QLabel(_(\"Session Timeout\"))\n        timeout_minutes = QLabel()\n        timeout_slider = QSlider(Qt.Orientation.Horizontal)\n        timeout_slider.setRange(1, 60)\n        timeout_slider.setSingleStep(1)\n        timeout_slider.setTickInterval(5)\n        timeout_slider.setTickPosition(QSlider.TickPosition.TicksBelow)\n        timeout_slider.setTracking(True)\n        timeout_msg = QLabel(\n            _(\"Clear the session after the specified period \"\n              \"of inactivity.  Once a session has timed out, \"\n              \"your PIN and passphrase (if enabled) must be \"\n              \"re-entered to use the device.\"))\n        timeout_msg.setWordWrap(True)\n        timeout_slider.setSliderPosition(config.get_session_timeout() // 60)\n        slider_moved()\n        timeout_slider.valueChanged.connect(slider_moved)\n        timeout_slider.sliderReleased.connect(slider_released)\n        settings_glayout.addWidget(timeout_label, 6, 0)\n        settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)\n        settings_glayout.addWidget(timeout_minutes, 6, 4)\n        settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)\n        settings_layout.addLayout(settings_glayout)\n        settings_layout.addStretch(1)\n\n        # Advanced tab\n        advanced_tab = QWidget()\n        advanced_layout = QVBoxLayout(advanced_tab)\n        advanced_glayout = QGridLayout()\n\n        # Advanced tab - clear PIN\n        clear_pin_button = QPushButton(_(\"Disable PIN\"))\n        clear_pin_button.clicked.connect(clear_pin)\n        clear_pin_warning = QLabel(\n            _(\"If you disable your PIN, anyone with physical access to your \"\n              \"{} device can spend your bitcoins.\").format(plugin.device))\n        clear_pin_warning.setWordWrap(True)\n        clear_pin_warning.setStyleSheet(\"color: red\")\n        advanced_glayout.addWidget(clear_pin_button, 0, 2)\n        advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5)\n\n        # Advanced tab - toggle passphrase protection\n        passphrase_button = QPushButton()\n        passphrase_button.clicked.connect(toggle_passphrase)\n        passphrase_msg = WWLabel(PASSPHRASE_HELP)\n        passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)\n        passphrase_warning.setStyleSheet(\"color: red\")\n        advanced_glayout.addWidget(passphrase_button, 3, 2)\n        advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5)\n        advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5)\n\n        # Advanced tab - wipe device\n        wipe_device_button = QPushButton(_(\"Wipe Device\"))\n        wipe_device_button.clicked.connect(wipe_device)\n        wipe_device_msg = QLabel(\n            _(\"Wipe the device, removing all data from it.  The firmware \"\n              \"is left unchanged.\"))\n        wipe_device_msg.setWordWrap(True)\n        wipe_device_warning = QLabel(\n            _(\"Only wipe a device if you have the recovery seed written down \"\n              \"and the device wallet(s) are empty, otherwise the bitcoins \"\n              \"will be lost forever.\"))\n        wipe_device_warning.setWordWrap(True)\n        wipe_device_warning.setStyleSheet(\"color: red\")\n        advanced_glayout.addWidget(wipe_device_button, 6, 2)\n        advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5)\n        advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5)\n        advanced_layout.addLayout(advanced_glayout)\n        advanced_layout.addStretch(1)\n\n        tabs = QTabWidget(self)\n        tabs.addTab(info_tab, _(\"Information\"))\n        tabs.addTab(settings_tab, _(\"Settings\"))\n        tabs.addTab(advanced_tab, _(\"Advanced\"))\n        dialog_vbox = QVBoxLayout(self)\n        dialog_vbox.addWidget(tabs)\n        dialog_vbox.addLayout(Buttons(CloseButton(self)))\n\n        # Update information\n        invoke_client(None)\n\n\nclass WCSafeTInitMethod(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Safe-T Setup'))\n\n    def on_ready(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = current_cosigner['hardware_device']\n        msg = _(\"Choose how you want to initialize your {}.\\n\\n\"\n                \"The first two methods are secure as no secret information \"\n                \"is entered into your computer.\\n\\n\"\n                \"For the last two methods you input secrets on your keyboard \"\n                \"and upload them to your {}, and so you should \"\n                \"only do those on a computer you know to be trustworthy \"\n                \"and free of malware.\"\n                ).format(_info.model_name, _info.model_name)\n        choices = [\n            # Must be short as QT doesn't word-wrap radio button text\n            ChoiceItem(key=TIM_NEW, label=_(\"Let the device generate a completely new seed randomly\")),\n            ChoiceItem(key=TIM_RECOVER, label=_(\"Recover from a seed you have previously written down\")),\n            ChoiceItem(key=TIM_MNEMONIC, label=_(\"Upload a BIP39 mnemonic to generate the seed\")),\n            ChoiceItem(key=TIM_PRIVKEY, label=_(\"Upload a master private key\"))\n        ]\n        self.choice_w = ChoiceWidget(message=msg, choices=choices)\n        self.layout().addWidget(self.choice_w)\n        self.layout().addStretch(1)\n\n        self._valid = True\n\n    def apply(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        current_cosigner['safe_t_init'] = self.choice_w.selected_key\n\n\nclass WCSafeTInitParams(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Safe-T Setup'))\n        self.plugins = wizard.plugins\n        self._busy = True\n\n    def on_ready(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = current_cosigner['hardware_device']\n        self.settings_layout = SafeTInitLayout(current_cosigner['safe_t_init'], _info.device.id_)\n        self.settings_layout.validChanged.connect(self.on_settings_valid_changed)\n        self.layout().addLayout(self.settings_layout)\n        self.layout().addStretch(1)\n\n        self.valid = current_cosigner['safe_t_init'] != TIM_PRIVKEY\n        self.busy = False\n\n    def on_settings_valid_changed(self, is_valid: bool):\n        self.valid = is_valid\n\n    def apply(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        current_cosigner['safe_t_settings'] = self.settings_layout.get_settings()\n\n\nclass WCSafeTInit(WalletWizardComponent, Logger):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Safe-T Setup'))\n        Logger.__init__(self)\n        self.plugins = wizard.plugins\n        self.plugin = self.plugins.get_plugin('safe_t')\n\n        self.layout().addWidget(WWLabel('Done'))\n\n        self._busy = True\n\n    def on_ready(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        settings = current_cosigner['safe_t_settings']\n        method = current_cosigner['safe_t_init']\n        _name, _info = current_cosigner['hardware_device']\n        device_id = _info.device.id_\n        client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)\n        client.handler = self.plugin.create_handler(self.wizard)\n\n        def initialize_device_task(settings, method, device_id, handler):\n            try:\n                self.plugin._initialize_device(settings, method, device_id, handler)\n                self.logger.info('Done initialize device')\n                self.valid = True\n                self.wizard.requestNext.emit()  # triggers Next GUI thread from event loop\n            except Exception as e:\n                self.valid = False\n                self.error = repr(e)\n                self.logger.exception(repr(e))\n            finally:\n                self.busy = False\n\n        t = threading.Thread(\n            target=initialize_device_task,\n            args=(settings, method, device_id, client.handler),\n            daemon=True)\n        t.start()\n\n    def apply(self):\n        pass\n"
  },
  {
    "path": "electrum/plugins/safe_t/safe_t.py",
    "content": "from typing import Optional, TYPE_CHECKING, Sequence\n\nfrom electrum.util import UserFacingException\nfrom electrum.bip32 import BIP32Node\nfrom electrum import descriptor\nfrom electrum import constants\nfrom electrum.i18n import _\nfrom electrum.plugin import Device, runs_in_hwd_thread\nfrom electrum.transaction import Transaction, PartialTransaction, PartialTxInput, Sighash\nfrom electrum.keystore import Hardware_KeyStore\n\nfrom electrum.hw_wallet import HW_PluginBase\nfrom electrum.hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data\n\nif TYPE_CHECKING:\n    from .client import SafeTClient\n    from electrum.plugin import DeviceInfo\n    from electrum.wizard import NewWalletWizard\n\n# Safe-T mini initialization methods\nTIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)\n\n\nclass SafeTKeyStore(Hardware_KeyStore):\n    hw_type = 'safe_t'\n    device = 'Safe-T mini'\n\n    plugin: 'SafeTPlugin'\n\n    def decrypt_message(self, sequence, message, password):\n        raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device))\n\n    @runs_in_hwd_thread\n    def sign_message(self, sequence, message, password, *, script_type=None):\n        client = self.get_client()\n        address_path = self.get_derivation_prefix() + \"/%d/%d\"%sequence\n        address_n = client.expand_path(address_path)\n        msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)\n        return msg_sig.signature\n\n    @runs_in_hwd_thread\n    def sign_transaction(self, tx, password):\n        if tx.is_complete():\n            return\n        # previous transactions used as inputs\n        prev_tx = {}\n        for txin in tx.inputs():\n            tx_hash = txin.prevout.txid.hex()\n            if txin.utxo is None and not txin.is_segwit():\n                raise UserFacingException(_('Missing previous tx for legacy input.'))\n            prev_tx[tx_hash] = txin.utxo\n\n        self.plugin.sign_transaction(self, tx, prev_tx)\n\n\nclass SafeTPlugin(HW_PluginBase):\n    # Derived classes provide:\n    #\n    #  class-static variables: client_class, firmware_URL, handler_class,\n    #     libraries_available, libraries_URL, minimum_firmware,\n    #     wallet_class, types\n\n    firmware_URL = 'https://safe-t.io'\n    libraries_URL = 'https://github.com/archos-safe-t/python-safet'\n    minimum_firmware = (1, 0, 5)\n    keystore_class = SafeTKeyStore\n    minimum_library = (0, 1, 0)\n    SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')\n\n    MAX_LABEL_LEN = 32\n\n    def __init__(self, parent, config, name):\n        HW_PluginBase.__init__(self, parent, config, name)\n\n        self.libraries_available = self.check_libraries_available()\n        if not self.libraries_available:\n            return\n\n        from . import client\n        from . import transport\n        import safetlib.messages\n        self.client_class = client.SafeTClient\n        self.types = safetlib.messages\n        self.DEVICE_IDS = ('Safe-T mini',)\n\n        self.transport_handler = transport.SafeTTransport()\n        self.device_manager().register_enumerate_func(self.enumerate)\n\n    def get_library_version(self):\n        import safetlib\n        try:\n            return safetlib.__version__\n        except AttributeError:\n            return 'unknown'\n\n    @runs_in_hwd_thread\n    def enumerate(self):\n        devices = self.transport_handler.enumerate_devices()\n        return [Device(path=d.get_path(),\n                       interface_number=-1,\n                       id_=d.get_path(),\n                       product_key='Safe-T mini',\n                       usage_page=0,\n                       transport_ui_string=d.get_path())\n                for d in devices]\n\n    @runs_in_hwd_thread\n    def create_client(self, device, handler):\n        try:\n            self.logger.info(f\"connecting to device at {device.path}\")\n            transport = self.transport_handler.get_transport(device.path)\n        except BaseException as e:\n            self.logger.info(f\"cannot connect at {device.path} {e}\")\n            return None\n\n        if not transport:\n            self.logger.info(f\"cannot connect at {device.path}\")\n            return\n\n        self.logger.info(f\"connected to device at {device.path}\")\n        client = self.client_class(transport, handler, self)\n\n        # Try a ping for device sanity\n        try:\n            client.ping('t')\n        except BaseException as e:\n            self.logger.info(f\"ping failed {e}\")\n            return None\n\n        if not client.atleast_version(*self.minimum_firmware):\n            msg = (_('Outdated {} firmware for device labelled {}. Please '\n                     'download the updated firmware from {}')\n                   .format(self.device, client.label(), self.firmware_URL))\n            self.logger.info(msg)\n            if handler:\n                handler.show_error(msg)\n            else:\n                raise UserFacingException(msg)\n            return None\n\n        return client\n\n    @runs_in_hwd_thread\n    def get_client(self, keystore, force_pair=True, *,\n                   devices=None, allow_user_interaction=True) -> Optional['SafeTClient']:\n        client = super().get_client(keystore, force_pair,\n                                    devices=devices,\n                                    allow_user_interaction=allow_user_interaction)\n        # returns the client for a given keystore. can use xpub\n        if client:\n            client.used()\n        return client\n\n    def get_coin_name(self):\n        return \"Testnet\" if constants.net.TESTNET else \"Bitcoin\"\n\n    @runs_in_hwd_thread\n    def _initialize_device(self, settings, method, device_id, handler):\n        item, label, pin_protection, passphrase_protection = settings\n\n        if method == TIM_RECOVER:\n            handler.show_error(_(\n                \"You will be asked to enter 24 words regardless of your \"\n                \"seed's actual length.  If you enter a word incorrectly or \"\n                \"misspell it, you cannot change it or go back - you will need \"\n                \"to start again from the beginning.\\n\\nSo please enter \"\n                \"the words carefully!\"),\n                blocking=True)\n\n        language = 'english'\n        devmgr = self.device_manager()\n        client = devmgr.client_by_id(device_id)\n        if not client:\n            raise Exception(_(\"The device was disconnected.\"))\n\n        if method == TIM_NEW:\n            strength = 64 * (item + 2)  # 128, 192 or 256\n            u2f_counter = 0\n            skip_backup = False\n            client.reset_device(True, strength, passphrase_protection,\n                                pin_protection, label, language,\n                                u2f_counter, skip_backup)\n        elif method == TIM_RECOVER:\n            word_count = 6 * (item + 2)  # 12, 18 or 24\n            client.step = 0\n            client.recovery_device(word_count, passphrase_protection,\n                                       pin_protection, label, language)\n        elif method == TIM_MNEMONIC:\n            pin = pin_protection  # It's the pin, not a boolean\n            client.load_device_by_mnemonic(str(item), pin,\n                                           passphrase_protection,\n                                           label, language)\n        else:\n            pin = pin_protection  # It's the pin, not a boolean\n            client.load_device_by_xprv(item, pin, passphrase_protection,\n                                       label, language)\n\n    def _make_node_path(self, xpub: str, address_n: Sequence[int]):\n        bip32node = BIP32Node.from_xkey(xpub)\n        node = self.types.HDNodeType(\n            depth=bip32node.depth,\n            fingerprint=int.from_bytes(bip32node.fingerprint, 'big'),\n            child_num=int.from_bytes(bip32node.child_number, 'big'),\n            chain_code=bip32node.chaincode,\n            public_key=bip32node.eckey.get_public_key_bytes(compressed=True),\n        )\n        return self.types.HDNodePathType(node=node, address_n=address_n)\n\n    def get_safet_input_script_type(self, electrum_txin_type: str):\n        if electrum_txin_type in ('p2wpkh', 'p2wsh'):\n            return self.types.InputScriptType.SPENDWITNESS\n        if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'):\n            return self.types.InputScriptType.SPENDP2SHWITNESS\n        if electrum_txin_type in ('p2pkh',):\n            return self.types.InputScriptType.SPENDADDRESS\n        if electrum_txin_type in ('p2sh',):\n            return self.types.InputScriptType.SPENDMULTISIG\n        raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))\n\n    def get_safet_output_script_type(self, electrum_txin_type: str):\n        if electrum_txin_type in ('p2wpkh', 'p2wsh'):\n            return self.types.OutputScriptType.PAYTOWITNESS\n        if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'):\n            return self.types.OutputScriptType.PAYTOP2SHWITNESS\n        if electrum_txin_type in ('p2pkh',):\n            return self.types.OutputScriptType.PAYTOADDRESS\n        if electrum_txin_type in ('p2sh',):\n            return self.types.OutputScriptType.PAYTOMULTISIG\n        raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))\n\n    @runs_in_hwd_thread\n    def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):\n        self.prev_tx = prev_tx\n        client = self.get_client(keystore)\n        inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)\n        outputs = self.tx_outputs(tx, keystore=keystore)\n        signatures = client.sign_tx(self.get_coin_name(), inputs, outputs,\n                                    lock_time=tx.locktime, version=tx.version)[0]\n        sighash = Sighash.to_sigbytes(Sighash.ALL)\n        signatures = [(sig + sighash) for sig in signatures]\n        tx.update_signatures(signatures)\n\n    @runs_in_hwd_thread\n    def show_address(self, wallet, address, keystore=None):\n        if keystore is None:\n            keystore = wallet.get_keystore()\n        if not self.show_address_helper(wallet, address, keystore):\n            return\n        client = self.get_client(keystore)\n        if not client.atleast_version(1, 0):\n            keystore.handler.show_error(_(\"Your device firmware is too old\"))\n            return\n        deriv_suffix = wallet.get_address_index(address)\n        derivation = keystore.get_derivation_prefix()\n        address_path = \"%s/%d/%d\"%(derivation, *deriv_suffix)\n        address_n = client.expand_path(address_path)\n        script_type = self.get_safet_input_script_type(wallet.txin_type)\n\n        # prepare multisig, if available:\n        desc = wallet.get_script_descriptor_for_address(address)\n        if multi := desc.get_simple_multisig():\n            multisig = self._make_multisig(multi)\n        else:\n            multisig = None\n\n        client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)\n\n    def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'SafeTKeyStore' = None):\n        inputs = []\n        for txin in tx.inputs():\n            txinputtype = self.types.TxInputType()\n            if txin.is_coinbase_input():\n                prev_hash = b\"\\x00\"*32\n                prev_index = 0xffffffff  # signed int -1\n            else:\n                if for_sig:\n                    assert isinstance(tx, PartialTransaction)\n                    assert isinstance(txin, PartialTxInput)\n                    assert keystore\n                    desc = txin.script_descriptor\n                    assert desc\n                    if multi := desc.get_simple_multisig():\n                        multisig = self._make_multisig(multi)\n                    else:\n                        multisig = None\n                    script_type = self.get_safet_input_script_type(desc.to_legacy_electrum_script_type())\n                    txinputtype = self.types.TxInputType(\n                        script_type=script_type,\n                        multisig=multisig)\n                    my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)\n                    if full_path:\n                        txinputtype._extend_address_n(full_path)\n\n                prev_hash = txin.prevout.txid\n                prev_index = txin.prevout.out_idx\n\n            if txin.value_sats() is not None:\n                txinputtype.amount = txin.value_sats()\n            txinputtype.prev_hash = prev_hash\n            txinputtype.prev_index = prev_index\n\n            if txin.script_sig is not None:\n                txinputtype.script_sig = txin.script_sig\n\n            txinputtype.sequence = txin.nsequence\n\n            inputs.append(txinputtype)\n\n        return inputs\n\n    def _make_multisig(self, desc: descriptor.MultisigDescriptor):\n        pubkeys = []\n        for pubkey_provider in desc.pubkeys:\n            assert not pubkey_provider.is_range()\n            assert pubkey_provider.extkey is not None\n            xpub = pubkey_provider.pubkey\n            der_suffix = pubkey_provider.get_der_suffix_int_list()\n            pubkeys.append(self._make_node_path(xpub, der_suffix))\n        return self.types.MultisigRedeemScriptType(\n            pubkeys=pubkeys,\n            signatures=[b''] * len(pubkeys),\n            m=desc.thresh)\n\n    def tx_outputs(self, tx: PartialTransaction, *, keystore: 'SafeTKeyStore'):\n\n        def create_output_by_derivation():\n            desc = txout.script_descriptor\n            assert desc\n            script_type = self.get_safet_output_script_type(desc.to_legacy_electrum_script_type())\n            if multi := desc.get_simple_multisig():\n                multisig = self._make_multisig(multi)\n            else:\n                multisig = None\n            my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)\n            assert full_path\n            txoutputtype = self.types.TxOutputType(\n                multisig=multisig,\n                amount=txout.value,\n                address_n=full_path,\n                script_type=script_type)\n            return txoutputtype\n\n        def create_output_by_address():\n            txoutputtype = self.types.TxOutputType()\n            txoutputtype.amount = txout.value\n            if address:\n                txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS\n                txoutputtype.address = address\n            else:\n                txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN\n                txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout)\n            return txoutputtype\n\n        outputs = []\n        has_change = False\n        any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)\n\n        for txout in tx.outputs():\n            address = txout.address\n            use_create_by_derivation = False\n\n            if txout.is_mine and not has_change:\n                # prioritise hiding outputs on the 'change' branch from user\n                # because no more than one change address allowed\n                # note: ^ restriction can be removed once we require fw\n                # that has https://github.com/trezor/trezor-mcu/pull/306\n                if txout.is_change == any_output_on_change_branch:\n                    use_create_by_derivation = True\n                    has_change = True\n\n            if use_create_by_derivation:\n                txoutputtype = create_output_by_derivation()\n            else:\n                txoutputtype = create_output_by_address()\n            outputs.append(txoutputtype)\n\n        return outputs\n\n    def electrum_tx_to_txtype(self, tx: Optional[Transaction]):\n        t = self.types.TransactionType()\n        if tx is None:\n            # probably for segwit input and we don't need this prev txn\n            return t\n        tx.deserialize()\n        t.version = tx.version\n        t.lock_time = tx.locktime\n        inputs = self.tx_inputs(tx)\n        t._extend_inputs(inputs)\n        for out in tx.outputs():\n            o = t._add_bin_outputs()\n            o.amount = out.value\n            o.script_pubkey = out.scriptpubkey\n        return t\n\n    # This function is called from the TREZOR libraries (via tx_api)\n    def get_tx(self, tx_hash):\n        tx = self.prev_tx[tx_hash]\n        return self.electrum_tx_to_txtype(tx)\n\n    def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str:\n        if new_wallet:\n            return 'safet_start' if device_info.initialized else 'safet_not_initialized'\n        else:\n            return 'safet_unlock'\n\n    # insert safe_t pages in new wallet wizard\n    def extend_wizard(self, wizard: 'NewWalletWizard'):\n        views = {\n            'safet_start': {\n                'next': 'safet_xpub',\n            },\n            'safet_xpub': {\n                'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore',\n                'accept': wizard.maybe_master_pubkey,\n                'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d)\n            },\n            'safet_not_initialized': {\n                'next': 'safet_choose_new_recover',\n            },\n            'safet_choose_new_recover': {\n                'next': 'safet_do_init',\n            },\n            'safet_do_init': {\n                'next': 'safet_start',\n            },\n            'safet_unlock': {\n                'last': True\n            },\n        }\n        wizard.navmap_merge(views)\n"
  },
  {
    "path": "electrum/plugins/safe_t/transport.py",
    "content": "from electrum.logging import get_logger\n\n\n_logger = get_logger(__name__)\n\n\nclass SafeTTransport:\n\n    @staticmethod\n    def all_transports():\n        \"\"\"Reimplemented safetlib.transport.all_transports so that we can\n        enable/disable specific transports.\n        \"\"\"\n        # NOTE: the bridge and UDP transports are disabled as they are using\n        # the same ports as trezor\n        try:\n            # only to detect safetlib version\n            from safetlib.transport import all_transports\n        except ImportError:\n            # old safetlib. compat for safetlib < 0.9.2\n            transports = []\n            #try:\n            #    from safetlib.transport_bridge import BridgeTransport\n            #    transports.append(BridgeTransport)\n            #except BaseException:\n            #    pass\n            try:\n                from safetlib.transport_hid import HidTransport\n                transports.append(HidTransport)\n            except BaseException:\n                pass\n            #try:\n            #    from safetlib.transport_udp import UdpTransport\n            #    transports.append(UdpTransport)\n            #except BaseException:\n            #    pass\n            try:\n                from safetlib.transport_webusb import WebUsbTransport\n                transports.append(WebUsbTransport)\n            except BaseException:\n                pass\n        else:\n            # new safetlib.\n            transports = []\n            #try:\n            #    from safetlib.transport.bridge import BridgeTransport\n            #    transports.append(BridgeTransport)\n            #except BaseException:\n            #    pass\n            try:\n                from safetlib.transport.hid import HidTransport\n                transports.append(HidTransport)\n            except BaseException:\n                pass\n            #try:\n            #    from safetlib.transport.udp import UdpTransport\n            #    transports.append(UdpTransport)\n            #except BaseException:\n            #    pass\n            try:\n                from safetlib.transport.webusb import WebUsbTransport\n                transports.append(WebUsbTransport)\n            except BaseException:\n                pass\n            return transports\n        return transports\n\n    def enumerate_devices(self):\n        \"\"\"Just like safetlib.transport.enumerate_devices,\n        but with exception catching, so that transports can fail separately.\n        \"\"\"\n        devices = []\n        for transport in self.all_transports():\n            try:\n                new_devices = transport.enumerate()\n            except BaseException as e:\n                _logger.info(f'enumerate failed for {transport.__name__}. error {e}')\n            else:\n                devices.extend(new_devices)\n        return devices\n\n    def get_transport(self, path=None):\n        \"\"\"Reimplemented safetlib.transport.get_transport,\n        (1) for old safetlib\n        (2) to be able to disable specific transports\n        (3) to call our own enumerate_devices that catches exceptions\n        \"\"\"\n        if path is None:\n            try:\n                return self.enumerate_devices()[0]\n            except IndexError:\n                raise Exception(\"No Safe-T mini found\") from None\n\n        def match_prefix(a, b):\n            return a.startswith(b) or b.startswith(a)\n        transports = [t for t in self.all_transports() if match_prefix(path, t.PATH_PREFIX)]\n        if transports:\n            return transports[0].find_by_path(path)\n        raise Exception(\"Unknown path prefix '%s'\" % path)\n"
  },
  {
    "path": "electrum/plugins/swapserver/__init__.py",
    "content": "from typing import TYPE_CHECKING, List\n\nfrom electrum.simple_config import ConfigVar, SimpleConfig\nfrom electrum.commands import plugin_command\nfrom electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED\n\nif TYPE_CHECKING:\n    from electrum.commands import Commands\n    from electrum.wallet import Abstract_Wallet\n\n\nplugin_name = \"swapserver\"\n\n\nSimpleConfig.SWAPSERVER_PORT = ConfigVar('plugins.swapserver.port', default=None, type_=int, plugin=__name__)\nSimpleConfig.SWAPSERVER_FEE_MILLIONTHS = ConfigVar('plugins.swapserver.fee_millionths', default=5000, type_=int, plugin=__name__)\nSimpleConfig.SWAPSERVER_ANN_POW_NONCE = ConfigVar('plugins.swapserver.ann_pow_nonce', default=0, type_=int, plugin=__name__)\n\n\n@plugin_command('wl', plugin_name)\nasync def get_history(self: 'Commands', wallet: 'Abstract_Wallet' = None, plugin = None) -> List[dict]:\n    \"\"\"\n    Get a list of all confirmed swaps provided by this swapserver.\n    Single elements can potentially cover multiple swaps if transactions have been batched.\n\n    Example result:\n\n        [\n            {\n                \"date\": \"2025-09-04\",\n                \"label\": \"Forward swap 0.2018 mBTC\",\n                \"timestamp\": 1756982141,  # unix timestamp\n                \"return_sat\": -205  # value in satoshi that has been earned or lost with this swap\n            },\n            {\n                \"date\": \"2025-09-04\",\n                \"label\": \"Reverse swap 0.30406 mBTC\",\n                \"timestamp\": 1756983236,\n                \"return_sat\": 64\n            }\n        ]\n    \"\"\"\n    assert wallet.lnworker, \"lightning not available\"\n    assert wallet.lnworker.swap_manager, \"swap manager not available\"\n\n    sm = wallet.lnworker.swap_manager\n    swap_group_ids = set()\n    for swap in sm._swaps.values():\n        group_id = swap.spending_txid if swap.is_reverse else swap.funding_txid\n        if group_id is None:\n            continue\n        if swap.spending_txid is None \\\n                or wallet.adb.get_tx_height(swap.spending_txid).height() <= TX_HEIGHT_UNCONFIRMED:\n            # get only final swaps so the history is stable and doesn't include pending swaps\n            continue\n        swap_group_ids.add(group_id)\n\n    swap_history_items = []\n    full_history = wallet.get_full_history()\n    for swap_group_id in swap_group_ids:\n        if swap_history_item := full_history.get('group:' + swap_group_id):\n            swap_history_items.append(swap_history_item)\n\n    result = []\n    for swap in swap_history_items:\n        result.append({\n            'label': swap['label'],\n            'return_sat': int(swap['value'].value),\n            'date': swap['date'].strftime(\"%Y-%m-%d\"),\n            'timestamp': swap['timestamp']\n        })\n    result = sorted(result, key=lambda x: x['timestamp'])\n    return result\n\n\n@plugin_command('wl', plugin_name)\nasync def get_summary(self: 'Commands', wallet: 'Abstract_Wallet' = None, plugin = None) -> dict:\n    \"\"\"Get a summary of all confirmed swaps provided by this swapserver.\n    Can become incorrect if closed lightning channels have been deleted in this wallet.\n\n    Example result:\n    {\n        \"num_swaps\": 160,\n        \"overall_return_sat\": 159052,  # value earned or lost in satoshi\n        \"swaps_per_day\": 0.78  # between first swap and last swap\n    }\n    \"\"\"\n    swap_history = await get_history(self)\n    profit_loss_sum = sum(swap['return_sat'] for swap in swap_history) if swap_history else 0\n    first_swap = min(swap['timestamp'] for swap in swap_history) if swap_history else 0\n    last_swap = max(swap['timestamp'] for swap in swap_history) if swap_history else 0\n    days_in_operation = (last_swap - first_swap) // 86400\n    swaps_per_day = (len(swap_history) / days_in_operation) if days_in_operation > 0 else 0\n\n    return {\n        'num_swaps': len(swap_history),\n        'overall_return_sat': profit_loss_sum,\n        'swaps_per_day': round(swaps_per_day, 2),\n    }\n"
  },
  {
    "path": "electrum/plugins/swapserver/cmdline.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - Lightweight Bitcoin Client\n# Copyright (C) 2023 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\nfrom .swapserver import SwapServerPlugin\n\n\nclass Plugin(SwapServerPlugin):\n    pass\n\n"
  },
  {
    "path": "electrum/plugins/swapserver/manifest.json",
    "content": "{\n  \"name\": \"swapserver\",\n  \"fullname\": \"SwapServer\",\n  \"description\": \"Submarine swap server for an Electrum daemon.\\n\\nExample setup:\\n\\n  electrum -o setconfig plugins.swapserver.enabled True\\n  electrum -o setconfig plugins.swapserver.port 5455\\n  electrum daemon -v\\n\\n\",\n  \"available_for\": [\"cmdline\"]\n}\n"
  },
  {
    "path": "electrum/plugins/swapserver/server.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2025 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport os\nimport asyncio\nfrom collections import defaultdict\nfrom typing import TYPE_CHECKING\n\nfrom aiohttp import web\n\nfrom electrum.util import log_exceptions, ignore_exceptions\nfrom electrum.logging import Logger\nfrom electrum.util import EventListener\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n    from electrum.wallet import Abstract_Wallet\n\n\nclass HttpSwapServer(Logger, EventListener):\n    \"\"\"\n    public API:\n    - getpairs\n    - createswap\n    \"\"\"\n\n    WWW_DIR = os.path.join(os.path.dirname(__file__), 'www')\n\n    def __init__(self, config: 'SimpleConfig', wallet: 'Abstract_Wallet'):\n        Logger.__init__(self)\n        self.config = config\n        self.wallet = wallet\n        self.sm = self.wallet.lnworker.swap_manager\n        self.port = self.config.SWAPSERVER_PORT\n        self.register_callbacks() # eventlistener\n\n        self.pending = defaultdict(asyncio.Event)\n        self.pending_msg = {}\n\n    @ignore_exceptions\n    @log_exceptions\n    async def run(self):\n\n        while self.wallet.has_password() and self.wallet.get_unlocked_password() is None:\n            self.logger.info(\"This wallet is password-protected. Please unlock it to start the swapserver plugin\")\n            await asyncio.sleep(10)\n\n        app = web.Application()\n        app.add_routes([web.get('/getpairs', self.get_pairs)])\n        app.add_routes([web.post('/createswap', self.create_swap)])\n        app.add_routes([web.post('/createnormalswap', self.create_normal_swap)])\n        app.add_routes([web.post('/addswapinvoice', self.add_swap_invoice)])\n\n        runner = web.AppRunner(app)\n        await runner.setup()\n        site = web.TCPSite(runner, host='localhost', port=self.port)\n        await site.start()\n        self.logger.info(f\"running and listening on port {self.port}\")\n\n    async def get_pairs(self, r):\n        sm = self.sm\n        sm.server_update_pairs()\n        pairs = {\n            \"info\": [],\n            \"warnings\": [],\n            \"htlcFirst\": True,\n            \"pairs\": {\n                \"BTC/BTC\": {\n                    \"rate\": 1,\n                    \"limits\": {\n                        \"maximal\": min(sm._max_forward, sm._max_reverse),  # legacy\n                        \"max_forward_amount\": sm._max_forward,  # new version, uses 2 separate limits\n                        \"max_reverse_amount\": sm._max_reverse,\n                        \"minimal\": sm._min_amount,\n                    },\n                    \"fees\": {\n                        \"percentage\": float(sm.percentage),  # cast to float for <= 4.7.1 backwards compatibility\n                        \"minerFees\": {\n                            \"baseAsset\": {\n                                \"normal\": sm.mining_fee,\n                                \"reverse\": {\n                                    \"claim\": sm.mining_fee,\n                                    \"lockup\": sm.mining_fee\n                                },\n                                \"mining_fee\": sm.mining_fee\n                            },\n                            \"quoteAsset\": {\n                                \"normal\": sm.mining_fee,\n                                \"reverse\": {\n                                    \"claim\": sm.mining_fee,\n                                    \"lockup\": sm.mining_fee\n                                },\n                                \"mining_fee\": sm.mining_fee\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        return web.json_response(pairs)\n\n    async def add_swap_invoice(self, r):\n        request = await r.json()\n        self.sm.server_add_swap_invoice(request)\n        return web.json_response({})\n\n    async def create_normal_swap(self, r):\n        request = await r.json()\n        response = self.sm.server_create_normal_swap(request)\n        return web.json_response(response)\n\n    async def create_swap(self, r):\n        request = await r.json()\n        response = self.sm.server_create_swap(request)\n        return web.json_response(response)\n"
  },
  {
    "path": "electrum/plugins/swapserver/swapserver.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - Lightweight Bitcoin Client\n# Copyright (C) 2023 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nfrom typing import TYPE_CHECKING\n\nfrom electrum.plugin import BasePlugin, hook\n\nfrom .server import HttpSwapServer\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n    from electrum.daemon import Daemon\n    from electrum.wallet import Abstract_Wallet\n\n\nclass SwapServerPlugin(BasePlugin):\n\n    def __init__(self, parent, config: 'SimpleConfig', name):\n        BasePlugin.__init__(self, parent, config, name)\n        self.config = config\n        self.server = None\n\n    @hook\n    def daemon_wallet_loaded(self, daemon: 'Daemon', wallet: 'Abstract_Wallet'):\n        # we use the first wallet loaded\n        if self.server is not None:\n            return\n        sm = wallet.lnworker.swap_manager\n        sm.is_server = True\n        sm.http_server = HttpSwapServer(self.config, wallet)\n"
  },
  {
    "path": "electrum/plugins/timelock_recovery/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/plugins/timelock_recovery/intro.txt",
    "content": "Timelock Recovery is a mechanism which, in case of a catastrophic event\n(death or loss of your master key), can send your Bitcoin to a secondary wallet of your choice\nwithin a time-window (i.e. 90 days).\n<br />\nDuring that time-window, you can see that the Timelock Recovery mechanism has been triggered (a\ntransaction from your wallet to itself is created on the Bitcoin blockchain), and if this\nhas happened against your will, you can use your master key to cancel the process (by moving\nthe funds elsewhere before the time-window expires).\n<br />\nThe implementation of Timelock Recovery is done with two transactions that are signed in advance,\nbut broadcast only when needed:\n<ol>\n    <li>\n        An <i>Alert Transaction</i> which sends the funds from the wallet to itself (consolidating the UTXOs).\n    </li>\n    <li>\n        A <i>Recovery Transaction</i> which sends the funds to a secondary wallet of your choice and can\n        be added to the blockchain only X days after the <i>Alert Transaction</i> has been broadcast (and mined).\n    </li>\n</ol>\nOptionally, this extension will also let you sign-in-advance a <i>Cancellation Transaction</i> which can be\nused to cancel the Timelock Recovery process, by broadcasting it before the time-window expires.\nIf the <i>Alert Transaction</i> has been broadcast, the Cancellation Transaction will send the funds again to\nthe same wallet, which would invalidate the <i>Recovery Transaction</i> (technically: the <i>Recovery Transaction</i>\nwill be seen as a transaction that is trying to spend a UTXO that has already been spent).\n<br />\nTimelock Recovery plans do not require any involvement of a third party.\nHowever, two precautions should be taken:\n<ol>\n    <li>\n        Due to the way Bitcoin transactions and UTXOs work, spending funds from the wallet might break\n        the entire Timelock Recovery plan.\n    </li>\n    <li>\n        Adding more funds to the wallet will not be covered by the Timelock Recovery plan.\n    </li>\n</ol>\n<br />\nTherefore it is highly recommended not to use the wallet for any purpose after creating a\nTimelock Recovery plan (other than long-term storage).\nInstead, for daily purposes use a separate wallet (with a seed in a place that\nyour loved ones could easily find) and only after accumulating enough funds relevant for long-term\nstorage, move them to a new highly secured wallet (i.e. with a long passphrase that only you memorize) for\nwhich you create a new Timelock Recovery plan (back to the daily-purpose wallet or to your inheritors' wallet).\n<br />\nEach accumulation should be done in a new highly secured wallet, but these are easy to create, i.e. you can\nmemorize a long passphrase and add a counter at the end (1 for the first accumlation, 2 for the second, etc.).\n<br />\nFor more details, visit: <a target=\"_blank\" href=\"https://timelockrecovery.com\" rel=\"noopener noreferrer\">https://timelockrecovery.com</a>.\n<br />\nBefore we begin, please note:\n<ol>\n    <li>\n        Please prepare in advance the addresses of your inheritors/backup-wallets.\n    </li>\n    <li>\n        Since we are preparing this recovery plan for the long future, it is hard\n        to estimate what the required mining fees will be.\n        If the fee is too low, your inheritors, who don't have access to the master\n        keys, will not be able to simply \"replace-by-fee\" and use a higher fee.\n        At the moment of writing this code (year 2025) this is not a big deal, because\n        there are acceleration-services, such as\n        <a target=\"_blank\" href=\"https://mempool.space/accelerator\" rel=\"noopener noreferrer\">\n            mempool.space's accelerator\n        </a>, that allows to boost selected transactions for direct payment.\n        Just in case this service will not be available in the future, the\n        <i>Alert Transaction</i> will send a small amount of 600 sats to each\n        destination address. This will allow advance users to boost the\n        first transaction by spending their unmined UTXO in a mechanism called\n        Child-Pay-For-Parent.\n    </li>\n</ol>\n"
  },
  {
    "path": "electrum/plugins/timelock_recovery/manifest.json",
    "content": "{\n  \"fullname\": \"Timelock Recovery Utility\",\n  \"description\": \"<br/>This plug-in allows you to create Timelock Recovery Plans for your wallet. See: <a href='https://timelockrecovery.com'>timelockrecovery.com</a>\",\n  \"author\": \"orenz0@protonmail.com\",\n  \"available_for\": [\"qt\"],\n  \"icon\":\"timelock_recovery_60.png\",\n  \"version\": \"0.1.0\"\n}\n"
  },
  {
    "path": "electrum/plugins/timelock_recovery/qt.py",
    "content": "'''\n\nTimelock Recovery\n\nCopyright:\n    2025 Oren <orenz0@protonmail.com>\n\nDistributed under the MIT software license, see the accompanying\nfile LICENCE or http://www.opensource.org/licenses/mit-license.php\n\n'''\n\nimport os\nimport shutil\nimport tempfile\nimport uuid\nimport json\nimport hashlib\nfrom datetime import datetime\nfrom functools import partial\nfrom typing import TYPE_CHECKING, Any, List, Optional, Tuple\nfrom decimal import Decimal\n\nimport qrcode\nfrom PyQt6.QtPrintSupport import QPrinter\nfrom PyQt6.QtCore import Qt, QRectF, QMarginsF\nfrom PyQt6.QtGui import (QImage, QPainter, QFont, QIntValidator, QAction,\n                         QPageSize, QPageLayout, QFontMetrics)\nfrom PyQt6.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QMenu, QCheckBox, QToolButton,\n                             QPushButton, QLineEdit, QScrollArea, QGridLayout, QFileDialog, QMessageBox)\n\nfrom electrum import constants, version\nfrom electrum.gui.common_qt.util import draw_qr, get_font_id\nfrom electrum.gui.qt.paytoedit import PayToEdit\nfrom electrum.payment_identifier import PaymentIdentifierType\nfrom electrum.plugin import hook\nfrom electrum.i18n import _\nfrom electrum.transaction import PartialTxOutput, Transaction\nfrom electrum.util import NotEnoughFunds, make_dir\nfrom electrum.gui.qt.util import ColorScheme, WindowModalDialog, Buttons, HelpLabel\nfrom electrum.gui.qt.util import read_QIcon_from_bytes, read_QPixmap_from_bytes, WaitingDialog\nfrom electrum.fee_policy import FeePolicy\nfrom electrum.gui.qt.fee_slider import FeeSlider, FeeComboBox\n\nfrom .timelock_recovery import TimelockRecoveryPlugin, TimelockRecoveryContext\n\n\nif TYPE_CHECKING:\n    from electrum.gui.qt import ElectrumGui\n    from electrum.gui.qt.main_window import ElectrumWindow\n\n\nAGREEMENT_TEXT = _(\"I understand that the Timelock Recovery plan will be broken if I keep using this wallet\")\nMIN_LOCKTIME_DAYS = 2\n# 0xFFFF * 512 seconds = 388.36 days.\nMAX_LOCKTIME_DAYS = 388\n\n\ndef selectable_label(text: str) -> QLabel:\n    label = QLabel(text)\n    label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)\n    return label\n\n\nclass FontManager:\n    def __init__(self, font_name: str, resolution: int):\n        pixels_per_point = resolution / 72.0\n        self.header_font = QFont(font_name, 8)\n        self.header_line_spacing = QFontMetrics(self.header_font).lineSpacing() * pixels_per_point\n        self.title_font = QFont(font_name, 18, QFont.Weight.Bold)\n        self.title_line_spacing = QFontMetrics(self.title_font).height() * pixels_per_point\n        self.subtitle_font = QFont(font_name, 10)\n        self.subtitle_line_spacing = QFontMetrics(self.subtitle_font).height() * pixels_per_point\n        self.title_small_font = QFont(font_name, 16, QFont.Weight.Bold)\n        self.title_small_line_spacing = QFontMetrics(self.title_small_font).height() * pixels_per_point\n        self.body_font = QFont(font_name, 9)\n        self.body_small_font = QFont(font_name, 8)\n        self.body_small_line_spacing = QFontMetrics(self.body_small_font).lineSpacing() * pixels_per_point\n\n\nclass Plugin(TimelockRecoveryPlugin):\n    base_dir: str\n    _init_qt_received: bool\n    font_name: str\n    small_logo_bytes: bytes\n    large_logo_bytes: bytes\n    intro_text: str\n\n    def __init__(self, parent, config, name: str):\n        TimelockRecoveryPlugin.__init__(self, parent, config, name)\n        self.base_dir = os.path.join(config.electrum_path(), 'timelock_recovery')\n        make_dir(self.base_dir)\n\n        self._init_qt_received = False\n        self.font_name = 'Monospace'\n        self.small_logo_bytes = self.read_file(\"timelock_recovery_60.png\")\n        self.large_logo_bytes = self.read_file(\"timelock_recovery_820.png\")\n        self.intro_text = self.read_file(\"intro.txt\").decode('utf-8')\n        plugin_metadata: Optional[dict] = parent.get_metadata('timelock_recovery')\n        self.plugin_version: str = plugin_metadata['version'] if plugin_metadata else 'unknown'\n\n    @hook\n    def load_wallet(self, wallet, window):\n        if self._init_qt_received:  # only need/want the first signal\n            return\n        self._init_qt_received = True\n        # load custom fonts (note: here, and not in __init__, as it needs the QApplication to be created)\n        if get_font_id('PTMono-Regular.ttf') >= 0 and get_font_id('PTMono-Bold.ttf') >= 0:\n            self.font_name = 'PT Mono'\n\n    @hook\n    def init_menubar(self, window):\n        m = window.wallet_menu.addAction('Timelock Recovery', lambda: self.setup_dialog(window))\n        icon = read_QIcon_from_bytes(self.read_file('timelock_recovery_60.png'))\n        m.setIcon(icon)\n\n    def setup_dialog(self, main_window: 'ElectrumWindow') -> bool:\n        context = TimelockRecoveryContext(main_window.wallet)\n        context.main_window = main_window\n        return self.create_plan_dialog(context)\n\n    def create_intro_dialog(self, context: TimelockRecoveryContext) -> bool:\n        intro_dialog = WindowModalDialog(context.main_window, \"Timelock Recovery\")\n        intro_dialog.setContentsMargins(11, 11, 1, 1)\n\n        # Create an HBox layout.  The logo will be on the left and the rest of the dialog on the right.\n        hbox_layout = QHBoxLayout(intro_dialog)\n\n        # Create the logo label.\n        logo_label = QLabel()\n\n        # Set the logo label pixmap.\n        logo_label.setPixmap(read_QPixmap_from_bytes(self.small_logo_bytes))\n\n        # Align the logo label to the top left.\n        logo_label.setAlignment(Qt.AlignmentFlag.AlignLeft)\n\n        # Create a VBox layout for the main contents of the dialog.\n        vbox_layout = QVBoxLayout()\n\n        # Populate the HBox layout with spacing between the two columns.\n        hbox_layout.addWidget(logo_label)\n        hbox_layout.addSpacing(16)\n        hbox_layout.addLayout(vbox_layout)\n\n        title_label = QLabel(_(\"What Is Timelock Recovery?\"))\n        vbox_layout.addWidget(title_label)\n\n        intro_label = QLabel(self.intro_text)\n        intro_label.setWordWrap(True)\n        intro_label.setTextFormat(Qt.TextFormat.RichText)\n        intro_label.setOpenExternalLinks(True)\n        intro_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)\n\n        intro_wrapper = QScrollArea()\n        intro_wrapper.setWidget(intro_label)\n        intro_wrapper.setWidgetResizable(True)\n        intro_wrapper.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)\n        intro_wrapper.setFrameStyle(0)\n        intro_wrapper.setMinimumHeight(200)\n\n        vbox_layout.addWidget(intro_wrapper)\n\n        close_button = QPushButton(_(\"Close\"), intro_dialog)\n        close_button.clicked.connect(intro_dialog.close)\n        vbox_layout.addLayout(Buttons(close_button))\n\n        # Add stretches to the end of the layouts to prevent the contents from spreading when the dialog is enlarged.\n        hbox_layout.addStretch(1)\n        vbox_layout.addStretch(1)\n\n        return bool(intro_dialog.exec())\n\n\n    def create_plan_dialog(self, context: TimelockRecoveryContext) -> bool:\n        plan_dialog = WindowModalDialog(context.main_window, \"Timelock Recovery\")\n        plan_dialog.setContentsMargins(11, 11, 1, 1)\n        plan_dialog.resize(800, plan_dialog.height())\n        fee_policy = FeePolicy('eta:1')\n        create_cancel_cb = QCheckBox('', checked=False)\n        alert_tx_fee_label = QLabel('')\n        alert_tx_complete_label = QLabel('')\n        recovery_tx_fee_label = QLabel('')\n        recovery_tx_complete_label = QLabel('')\n        cancellation_tx_fee_label = QLabel('')\n        cancellation_tx_complete_label = QLabel('')\n\n        if not context.get_alert_address():\n            plan_dialog.show_error(''.join([\n                _(\"No more addresses in your wallet.\"), \" \",\n                _(\"You are using a non-deterministic wallet, which cannot create new addresses.\"), \" \",\n                _(\"If you want to create new addresses, use a deterministic wallet instead.\"),\n            ]))\n            plan_dialog.close()\n            return\n\n        title_hbox = QHBoxLayout()\n        title_hbox.addWidget(QLabel(_('To create a recovery plan, enter a recipient and a cancellation time window')))\n        title_hbox.addStretch(1)\n        help_button = QPushButton(_(\"Help\"))\n        help_button.clicked.connect(lambda: self.create_intro_dialog(context))\n        title_hbox.addWidget(help_button)\n\n        next_button = QPushButton(_(\"Next\"), plan_dialog)\n        next_button.clicked.connect(plan_dialog.close)\n        next_button.clicked.connect(lambda: self.start_plan(context))\n        next_button.setEnabled(False)\n\n        plan_grid = QGridLayout()\n        plan_grid.setSpacing(8)\n        grid_row = 0\n\n        payto_e = PayToEdit(context.main_window.send_tab) # Reuse configuration from send tab\n        payto_e.toggle_paytomany()\n\n        context.timelock_days = 90\n        timelock_days_widget = QLineEdit()\n        timelock_days_widget.setValidator(QIntValidator(2, 388))\n        timelock_days_widget.setText(str(context.timelock_days))\n\n        def update_transactions():\n            is_valid = self._validate_input_values(\n                    context=context,\n                    payto_e=payto_e,\n                    timelock_days_widget=timelock_days_widget,\n            )\n            if not is_valid:\n                view_alert_tx_button.setEnabled(False)\n                view_recovery_tx_button.setEnabled(False)\n                view_cancellation_tx_button.setEnabled(False)\n                next_button.setEnabled(False)\n                next_button.setToolTip(\"\")\n                return\n            try:\n                new_alert_tx = context.make_unsigned_alert_tx(fee_policy)\n                alert_changed = False\n                if not context.alert_tx or context.alert_tx.txid() != new_alert_tx.txid():\n                    context.alert_tx = new_alert_tx\n                    alert_changed = True\n                assert all(tx_input.is_segwit() for tx_input in context.alert_tx.inputs())\n                alert_tx_complete_label.setText(_(\"✓ Signed\") if context.alert_tx.is_complete() else \"\")\n                alert_tx_fee_label.setText(_(\"Fee: {}\").format(self.config.format_amount_and_units(context.alert_tx.get_fee())))\n                new_recovery_tx = context.make_unsigned_recovery_tx(fee_policy)\n                if alert_changed or not context.recovery_tx or context.recovery_tx.txid() != new_recovery_tx.txid():\n                    context.recovery_tx = new_recovery_tx\n                    context.add_input_info_to_recovery_tx()\n                assert all(tx_input.is_segwit() for tx_input in context.recovery_tx.inputs())\n                recovery_tx_complete_label.setText(_(\"✓ Signed\") if context.recovery_tx.is_complete() else \"\")\n                recovery_tx_fee_label.setText(_(\"Fee: {}\").format(self.config.format_amount_and_units(context.recovery_tx.get_fee())))\n                if create_cancel_cb.isChecked():\n                    new_cancellation_tx = context.make_unsigned_cancellation_tx(fee_policy)\n                    if alert_changed or not context.cancellation_tx or context.cancellation_tx.txid() != new_cancellation_tx.txid():\n                        context.cancellation_tx = new_cancellation_tx\n                        context.add_input_info_to_cancellation_tx()\n                    assert all(tx_input.is_segwit() for tx_input in context.cancellation_tx.inputs())\n                    cancellation_tx_complete_label.setText(_(\"✓ Signed\") if context.cancellation_tx.is_complete() else \"\")\n                    cancellation_tx_fee_label.setText(_(\"Fee: {}\").format(self.config.format_amount_and_units(context.cancellation_tx.get_fee())))\n                else:\n                    context.cancellation_tx = None\n                cancellation_tx_complete_label.setText(_(\"✓ Signed\") if context.cancellation_tx is not None and context.cancellation_tx.is_complete() else \"\")\n            except NotEnoughFunds:\n                view_alert_tx_button.setEnabled(False)\n                alert_tx_complete_label.setText(\"\")\n                alert_tx_fee_label.setText(\"\")\n                view_recovery_tx_button.setEnabled(False)\n                recovery_tx_complete_label.setText(\"\")\n                recovery_tx_fee_label.setText(\"\")\n                view_cancellation_tx_button.setEnabled(False)\n                cancellation_tx_complete_label.setText(\"\")\n                cancellation_tx_fee_label.setText(\"\")\n                payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))\n                payto_e.setToolTip(_(\"Not enough funds to create the transactions.\"))\n                next_button.setEnabled(False)\n                next_button.setToolTip(\"\")\n                return\n            view_alert_tx_button.setEnabled(True)\n            view_recovery_tx_button.setEnabled(True)\n            view_cancellation_tx_button.setEnabled(True)\n            payto_e.setStyleSheet(ColorScheme.GREEN.as_stylesheet(True))\n            payto_e.setToolTip(\"\")\n            if context.main_window.wallet.is_watching_only():\n                if not context.alert_tx.is_complete() or not context.recovery_tx.is_complete() or (context.cancellation_tx is not None and not context.cancellation_tx.is_complete()):\n                    next_button.setEnabled(False)\n                    next_button.setToolTip(_(\"This is a watching-only wallet. You must sign the transactions externally - use the View button of each transaction.\"))\n                    return\n            next_button.setEnabled(True)\n            next_button.setToolTip(\"\")\n\n\n        payto_e.paymentIdentifierChanged.connect(update_transactions)\n        timelock_days_widget.textChanged.connect(update_transactions)\n\n        plan_grid.addWidget(HelpLabel(\n            _(\"Recipient of the funds\"),\n            (\n                _(\"Recipient of the funds, after the cancellation time window has expired\")\n                + \"\\n\\n\"\n                + _(\"This field must contain a single Bitcoin address, or multiple lines in the format: 'address, amount'.\") + \"\\n\"\n                + \"\\n\"\n                + _(\"If multiple lines are used, at least one line must be set to 'max', using the '!' special character.\") + \"\\n\"\n                + _(\"Integers weights can also be used in conjunction with '!', \"\n                    \"e.g. set one amount to '2!' and another to '3!' to split your coins 40-60.\")\n            ),\n        ), grid_row, 0)\n        plan_grid.addWidget(payto_e, grid_row, 1, 1, 4)\n        grid_row += 1\n\n        plan_grid.addWidget(HelpLabel(\n            _(\"Cancellation time-window (days)\"),\n            (\n                _(\"After broadcasting the Alert Transaction, you have a limited time to cancel the transaction.\") + \"\\n\"\n                + _(\"Value must be between {} and {} days.\").format(MIN_LOCKTIME_DAYS, MAX_LOCKTIME_DAYS)\n            )\n        ), grid_row, 0)\n        plan_grid.addWidget(timelock_days_widget, grid_row, 1)\n        grid_row += 1\n        plan_grid.addWidget(HelpLabel(\n            _('Create a cancellation transaction'),\n            '\\n'.join([\n                _(\n                    \"If the Alert transaction is has been broadcast against your intention,\" +\n                    \" you will be able to broadcast the Cancellation transaction within {} days,\" +\n                    \" to invalidate the Recovery transaction and keep the funds in this wallet\" +\n                    \" - without the need to restore the seed of this wallet (i.e. in case you have split or hidden it).\"\n                ).format(context.timelock_days),\n                _(\n                    \"However, if the seed of this wallet is lost, broadcasting the Cancellation transaction\" +\n                    \" might lock the funds on this wallet forever.\"\n                )\n            ])\n        ), grid_row, 0)\n        plan_grid.addWidget(create_cancel_cb, grid_row, 1, 1, 4)\n        grid_row += 1\n\n        fee_slider = FeeSlider(\n            parent=plan_dialog, network=context.main_window.network,\n            fee_policy=fee_policy,\n            callback=lambda x: update_transactions()\n        )\n\n        fee_combo = FeeComboBox(fee_slider)\n        plan_grid.addWidget(QLabel(_('Fee policy')), grid_row, 0)\n        plan_grid.addWidget(fee_slider, grid_row, 1)\n        plan_grid.addWidget(fee_combo, grid_row, 2)\n        grid_row += 1\n\n        plan_grid.addWidget(QLabel(_('Alert transaction')), grid_row, 0)\n        plan_grid.addWidget(alert_tx_fee_label, grid_row, 1, 1, 2)\n        plan_grid.addWidget(alert_tx_complete_label, grid_row, 3)\n        view_alert_tx_button = QPushButton(_('View'))\n        def on_alert_tx_closed(tx: Optional[Transaction]):\n            if tx is not None and context.alert_tx is not None and tx.txid() == context.alert_tx.txid() and tx.is_complete():\n                old_alert_tx_complete = context.alert_tx and context.alert_tx.is_complete()\n                context.alert_tx = tx\n                if not old_alert_tx_complete and context.alert_tx.is_complete():\n                    context.add_input_info_to_recovery_tx()\n                    context.add_input_info_to_cancellation_tx()\n                update_transactions()\n        view_alert_tx_button.clicked.connect(lambda: context.main_window.show_transaction(\n            context.alert_tx,\n            prompt_if_complete_unsaved=False,\n            show_broadcast_button=False,\n            on_closed=on_alert_tx_closed\n        ))\n        plan_grid.addWidget(view_alert_tx_button, grid_row, 4)\n        grid_row += 1\n\n        plan_grid.addWidget(QLabel(_('Recovery transaction')), grid_row, 0)\n        plan_grid.addWidget(recovery_tx_fee_label, grid_row, 1, 1, 2)\n        plan_grid.addWidget(recovery_tx_complete_label, grid_row, 3)\n        view_recovery_tx_button = QPushButton(_('View'))\n        def on_recovery_tx_closed(tx: Optional[Transaction]):\n            if tx is not None and context.recovery_tx is not None and tx.txid() == context.recovery_tx.txid() and tx.is_complete():\n                context.recovery_tx = tx\n                update_transactions()\n        view_recovery_tx_button.clicked.connect(lambda: context.main_window.show_transaction(\n            context.recovery_tx,\n            prompt_if_complete_unsaved=False,\n            show_broadcast_button=False,\n            on_closed=on_recovery_tx_closed\n        ))\n        plan_grid.addWidget(view_recovery_tx_button, grid_row, 4)\n        grid_row += 1\n\n        cancellation_label = QLabel(_('Cancellation transaction'))\n        plan_grid.addWidget(cancellation_label, grid_row, 0)\n        plan_grid.addWidget(cancellation_tx_fee_label, grid_row, 1, 1, 2)\n        plan_grid.addWidget(cancellation_tx_complete_label, grid_row, 3)\n        view_cancellation_tx_button = QPushButton(_('View'))\n        def on_cancellation_tx_closed(tx: Optional[Transaction]):\n            if tx is not None and context.cancellation_tx is not None and tx.txid() == context.cancellation_tx.txid() and tx.is_complete():\n                context.cancellation_tx = tx\n                update_transactions()\n        view_cancellation_tx_button.clicked.connect(lambda: context.main_window.show_transaction(\n            context.cancellation_tx,\n            prompt_if_complete_unsaved=False,\n            show_broadcast_button=False,\n            on_closed=on_cancellation_tx_closed\n        ))\n        plan_grid.addWidget(view_cancellation_tx_button, grid_row, 4)\n        grid_row += 1\n\n        plan_grid.setRowStretch(grid_row, 1)  # Make sure the grid does not stretch\n        # Create an HBox layout.  The logo will be on the left and the rest of the dialog on the right.\n        hbox_layout = QHBoxLayout(plan_dialog)\n\n        def on_cb_change(x):\n            cancellation_label.setVisible(x)\n            cancellation_tx_fee_label.setVisible(x)\n            view_cancellation_tx_button.setVisible(x)\n            update_transactions()\n        create_cancel_cb.stateChanged.connect(on_cb_change)\n\n        logo_label = QLabel()\n        logo_label.setPixmap(read_QPixmap_from_bytes(self.small_logo_bytes))\n        logo_label.setAlignment(Qt.AlignmentFlag.AlignLeft)\n\n        # Create a VBox layout for the main contents of the dialog.\n        vbox_layout = QVBoxLayout()\n        vbox_layout.addLayout(title_hbox)\n        vbox_layout.addStretch(1)\n        vbox_layout.addLayout(plan_grid, stretch=1)\n        vbox_layout.addLayout(Buttons(next_button))\n\n        # Populate the HBox layout.\n        hbox_layout.addWidget(logo_label)\n        hbox_layout.addSpacing(16)\n        hbox_layout.addLayout(vbox_layout, stretch=1)\n\n        # initialize\n        on_cb_change(False)\n\n        return bool(plan_dialog.exec())\n\n    def _validate_input_values(\n            self,\n            context: TimelockRecoveryContext,\n            payto_e: PayToEdit,\n            timelock_days_widget: QLineEdit) -> bool:\n        context.timelock_days = None\n        try:\n            timelock_days_str = timelock_days_widget.text()\n            timelock_days = int(timelock_days_str)\n            if str(timelock_days) != timelock_days_str or timelock_days < MIN_LOCKTIME_DAYS or timelock_days > MAX_LOCKTIME_DAYS:\n                raise ValueError(\"Timelock Days value not in range.\")\n            context.timelock_days = timelock_days\n            timelock_days_widget.setStyleSheet(None)\n            timelock_days_widget.setToolTip(\"\")\n        except ValueError:\n            timelock_days_widget.setStyleSheet(ColorScheme.RED.as_stylesheet(True))\n            timelock_days_widget.setToolTip(_(\"Value must be between {} and {} days.\").format(MIN_LOCKTIME_DAYS, MAX_LOCKTIME_DAYS))\n            return False\n        pi = payto_e.payment_identifier\n        if not pi:\n            return False\n        if not pi.is_valid():\n            # Don't make background red - maybe the user did not complete typing yet.\n            payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True) if '\\n' in pi.text.strip() else '')\n            payto_e.setToolTip((pi.get_error() or _(\"Invalid address.\")) if pi.text else \"\")\n            return False\n        elif pi.is_multiline():\n            if not pi.is_multiline_max():\n                payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))\n                payto_e.setToolTip(_(\"At least one line must be set to max spend ('!' in the amount column).\"))\n                return False\n            for output in pi.multiline_outputs:  # type: PartialTxOutput\n                if not output.address:\n                    payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))\n                    payto_e.setToolTip(_(\"Recovery should only send to addresses.\"))\n                    return False\n                else:\n                    if context.wallet.is_mine(output.address):\n                        payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))\n                        payto_e.setToolTip(_(\"Recovery should not send to same wallet.\"))\n                        return False\n            context.outputs = pi.multiline_outputs\n        else:\n            if not pi.is_available() or pi.type != PaymentIdentifierType.SPK or not pi.spk_is_address:\n                payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))\n                payto_e.setToolTip(_(\"Invalid address type - must be a Bitcoin address.\"))\n                return False\n            assert pi.spk and pi.spk_is_address\n            if context.wallet.is_mine(pi.text):\n                payto_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))\n                payto_e.setToolTip(_(\"Recovery should not send to same wallet.\"))\n                return False\n            context.outputs = [PartialTxOutput(scriptpubkey=pi.spk, value='!')]\n        return True\n\n    def start_plan(self, context: TimelockRecoveryContext):\n        main_window = context.main_window\n        wallet = main_window.wallet\n        password = main_window.get_password()\n\n        def task():\n            if not context.alert_tx.is_complete():\n                wallet.sign_transaction(context.alert_tx, password, ignore_warnings=True)\n                context.add_input_info_to_recovery_tx()\n                context.add_input_info_to_cancellation_tx()\n            if not context.alert_tx.is_complete():\n                raise Exception(_(\"Alert transaction signing was not completed\"))\n            if not context.recovery_tx.is_complete():\n                wallet.sign_transaction(context.recovery_tx, password, ignore_warnings=True)\n            if not context.recovery_tx.is_complete():\n                raise Exception(_(\"Recovery transaction signing was not completed\"))\n            if context.cancellation_tx is not None:\n                if not context.cancellation_tx.is_complete():\n                    wallet.sign_transaction(context.cancellation_tx, password, ignore_warnings=True)\n                if not context.cancellation_tx.is_complete():\n                    raise Exception(_(\"Cancellation transaction signing was not completed\"))\n\n        def on_success(result):\n            self.create_download_dialog(context)\n        def on_failure(exc_info):\n            main_window.on_error(exc_info)\n        msg = _('Signing transaction...')\n        WaitingDialog(main_window, msg, task, on_success, on_failure)\n\n\n    def create_download_dialog(self, context: TimelockRecoveryContext) -> bool:\n        context.recovery_plan_id = str(uuid.uuid4())\n        context.recovery_plan_created_at = datetime.now().astimezone()\n        download_dialog = WindowModalDialog(context.main_window, \"Timelock Recovery - Download\")\n        download_dialog.setContentsMargins(11, 11, 1, 1)\n        download_dialog.resize(800, download_dialog.height())\n\n        # Create an HBox layout. The logo will be on the left and the rest of the dialog on the right.\n        hbox_layout = QHBoxLayout(download_dialog)\n\n        # Create the logo label\n        logo_label = QLabel()\n        logo_label.setPixmap(read_QPixmap_from_bytes(self.small_logo_bytes))\n        logo_label.setAlignment(Qt.AlignmentFlag.AlignLeft)\n\n        # Create a VBox layout for the main contents\n        vbox_layout = QVBoxLayout()\n\n        # Create and populate the grid\n        grid = QGridLayout()\n        grid.setSpacing(8)\n        grid.setColumnStretch(3, 1)\n\n        line_number = 0\n\n        # Add Recovery Plan ID row\n        grid.addWidget(HelpLabel(\n            _(\"Recovery Plan ID\"),\n            _(\"Unique identifier for this recovery plan\"),\n        ), 0, 0)\n        grid.addWidget(selectable_label(context.recovery_plan_id), line_number, 1, 1, 4)\n        line_number += 1\n        # Add Creation Date row\n        grid.addWidget(HelpLabel(\n            _(\"Created At\"),\n            _(\"Date and time when this recovery plan was created\"),\n        ), 1, 0)\n        grid.addWidget(selectable_label(context.recovery_plan_created_at.strftime(\"%Y-%m-%d %H:%M:%S %Z (%z)\")), line_number, 1, 1, 4)\n        line_number += 1\n\n        grid.addWidget(HelpLabel(\n            _(\"Alert Address\"),\n            _(\"This address in your wallet will receive the funds when the Alert Transaction is broadcast.\"),\n        ), line_number, 0)\n        alert_address = context.get_alert_address()\n        grid.addWidget(selectable_label(alert_address), line_number, 1, 1, 3)\n        copy_button = QPushButton(_(\"Copy\"))\n        copy_button.clicked.connect(lambda: context.main_window.do_copy(alert_address))\n        grid.addWidget(copy_button, line_number, 4)\n        line_number += 1\n\n        if context.cancellation_tx is not None:\n            cancellation_address = context.get_cancellation_address()\n            grid.addWidget(HelpLabel(\n                _(\"Cancellation Address\"),\n                _(\"This address in your wallet will receive the funds when the Cancellation transaction is broadcast.\"),\n            ), line_number, 0)\n            grid.addWidget(selectable_label(cancellation_address), line_number, 1, 1, 3)\n            copy_button2 = QPushButton(_(\"Copy\"))\n            copy_button2.clicked.connect(lambda: context.main_window.do_copy(cancellation_address))\n            grid.addWidget(copy_button2, line_number, 4)\n            line_number += 1\n\n        grid.addWidget(HelpLabel(\n            _(\"Alert Transaction ID\"),\n            _(\"ID of the Alert transaction\"),\n        ), line_number, 0)\n        grid.addWidget(selectable_label(context.alert_tx.txid()), line_number, 1, 1, 3)\n        line_number += 1\n\n        grid.addWidget(HelpLabel(\n            _(\"Recovery Transaction ID\"),\n            _(\"ID of the Recovery transaction\"),\n        ), line_number, 0)\n        grid.addWidget(selectable_label(context.recovery_tx.txid()), line_number, 1, 1, 4)\n        line_number += 1\n\n        if context.cancellation_tx is not None:\n            grid.addWidget(HelpLabel(\n                _(\"Cancellation Transaction ID\"),\n                _(\"ID of the Cancellation transaction\"),\n            ), line_number, 0)\n            grid.addWidget(selectable_label(context.cancellation_tx.txid()), line_number, 1, 1, 4)\n            line_number += 1\n\n        grid.setRowStretch(line_number, 1)\n        # Create buttons\n        recovery_menu = QMenu()\n        action = QAction(_('Save as PDF'), recovery_menu)\n        action.triggered.connect(partial(self._save_recovery_plan_pdf, context, download_dialog))\n        recovery_menu.addAction(action)\n        action = QAction(_('Save as JSON'), recovery_menu)\n        action.triggered.connect(partial(self._save_recovery_plan_json, context, download_dialog))\n        recovery_menu.addAction(action)\n        recovery_button = QToolButton()\n        recovery_button.setText(_(\"Save Recovery Plan\"))\n        recovery_button.setMenu(recovery_menu)\n        recovery_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)\n        # Save Cancellation Plan button row (if applicable)\n        cancellation_menu = QMenu()\n        action = QAction(_('Save as PDF'), cancellation_menu)\n        action.triggered.connect(partial(self._save_cancellation_plan_pdf, context, download_dialog))\n        cancellation_menu.addAction(action)\n        action = QAction(_('Save as JSON'), cancellation_menu)\n        action.triggered.connect(partial(self._save_cancellation_plan_json, context, download_dialog))\n        cancellation_menu.addAction(action)\n        cancellation_button = QToolButton()\n        cancellation_button.setText(_(\"Save Cancellation Plan\"))\n        cancellation_button.setMenu(cancellation_menu)\n        cancellation_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)\n        # Add layouts to main vbox\n        vbox_layout.addLayout(grid)\n        vbox_layout.addStretch()\n        download_hbox = QHBoxLayout()\n        download_hbox.addWidget(recovery_button)\n        if context.cancellation_tx is not None:\n            download_hbox.addWidget(cancellation_button)\n        # agree checkbox\n        def on_agreement(b):\n            recovery_button.setEnabled(bool(b))\n            cancellation_button.setEnabled(bool(b))\n        on_agreement(False)\n        agree_cb = QCheckBox(AGREEMENT_TEXT)\n        agree_cb.stateChanged.connect(on_agreement)\n        vbox_layout.addWidget(agree_cb)\n        vbox_layout.addStretch()\n        vbox_layout.addLayout(download_hbox)\n        close_button = QPushButton(_(\"Close\"), download_dialog)\n        def on_close():\n            if context.cancellation_tx is not None and not context.cancellation_plan_saved:\n                if not context.recovery_plan_saved:\n                    is_sure = download_dialog.question(\n                        _(\"Are you sure you want to close this dialog without saving any of the files?\"),\n                        title=_(\"Close\"),\n                        icon=QMessageBox.Icon.Question\n                    )\n                    if not is_sure:\n                        return\n                else:\n                    is_sure = download_dialog.question(\n                        _(\"Are you sure you want to close this dialog without saving the cancellation-plan?\"),\n                        title=_(\"Close\"),\n                        icon=QMessageBox.Icon.Question\n                    )\n                    if not is_sure:\n                        return\n            elif not context.recovery_plan_saved:\n                is_sure = download_dialog.question(\n                    _(\"Are you sure you want to close this dialog without saving the recovery-plan?\"),\n                    title=_(\"Close\"),\n                    icon=QMessageBox.Icon.Question\n                )\n                if not is_sure:\n                    return\n            download_dialog.close()\n        close_button.clicked.connect(on_close)\n        vbox_layout.addLayout(Buttons(close_button))\n        # Populate the HBox layout.\n        hbox_layout.addWidget(logo_label)\n        hbox_layout.addSpacing(16)\n        hbox_layout.addLayout(vbox_layout, stretch=1)\n\n        return bool(download_dialog.exec())\n\n    def _save_recovery_plan_json(self, context: TimelockRecoveryContext, download_dialog: WindowModalDialog):\n        try:\n            # Open a Save As dialog to get the file path\n            file_path, _selected_filter = QFileDialog.getSaveFileName(\n                download_dialog,\n                _(\"Save Recovery Plan JSON...\"),\n                os.path.join(self.base_dir, \"timelock-recovery-plan-{}.json\".format(context.recovery_plan_id)),\n                _(\"JSON files (*.json)\")\n            )\n            if not file_path:\n                return\n            with open(file_path, \"w\") as json_file:\n                json_data = {\n                    \"kind\": \"timelock-recovery-plan\",\n                    \"id\": context.recovery_plan_id,\n                    \"created_at\": context.recovery_plan_created_at.isoformat(),\n                    \"plugin_version\": self.plugin_version,\n                    \"wallet_kind\": \"Electrum\",\n                    \"wallet_version\": version.ELECTRUM_VERSION,\n                    \"wallet_name\": context.wallet_name,\n                    \"timelock_days\": context.timelock_days,\n                    \"anchor_amount_sats\": context.ANCHOR_OUTPUT_AMOUNT_SATS,\n                    \"anchor_addresses\": [output.address for output in context.outputs],\n                    \"alert_address\": context.get_alert_address(),\n                    \"alert_inputs\": [tx_input.prevout.to_str() for tx_input in context.alert_tx.inputs()],\n                    \"alert_tx\": context.alert_tx.serialize().upper(),\n                    \"alert_txid\": context.alert_tx.txid(),\n                    \"alert_fee\": context.alert_tx.get_fee(),\n                    \"alert_weight\": context.alert_tx.estimated_weight(),\n                    \"recovery_tx\": context.recovery_tx.serialize().upper(),\n                    \"recovery_txid\": context.recovery_tx.txid(),\n                    \"recovery_fee\": context.recovery_tx.get_fee(),\n                    \"recovery_weight\": context.recovery_tx.estimated_weight(),\n                    \"recovery_outputs\": [[tx_output.address, tx_output.value] for tx_output in context.recovery_tx.outputs()],\n                }\n                # Simple checksum to ensure the file is not corrupted by foolish users\n                json_data[\"checksum\"] = self.json_checksum(json_data)\n                json.dump(json_data, json_file, indent=2)\n            download_dialog.show_message(_(\"File saved successfully\"))\n            context.recovery_plan_saved = True\n        except Exception as e:\n            self.logger.exception(repr(e))\n            download_dialog.show_error(_(\"Error saving file\"))\n\n    def _save_cancellation_plan_json(self, context: TimelockRecoveryContext, download_dialog: WindowModalDialog):\n        try:\n            # Open a Save As dialog to get the file path\n            file_path, _selected_filter = QFileDialog.getSaveFileName(\n                download_dialog,\n                _(\"Save Cancellation Plan JSON...\"),\n                os.path.join(self.base_dir, \"timelock-cancellation-plan-{}.json\".format(context.recovery_plan_id)),\n                _(\"JSON files (*.json)\")\n            )\n            if not file_path:\n                return\n            with open(file_path, \"w\") as f:\n                json_data = {\n                    \"kind\": \"timelock-cancellation-plan\",\n                    \"id\": context.recovery_plan_id,\n                    \"created_at\": context.recovery_plan_created_at.isoformat(),\n                    \"plugin_version\": self.plugin_version,\n                    \"wallet_kind\": \"Electrum\",\n                    \"wallet_version\": version.ELECTRUM_VERSION,\n                    \"wallet_name\": context.wallet_name,\n                    \"timelock_days\": context.timelock_days,\n                    \"alert_txid\": context.alert_tx.txid(),\n                    \"cancellation_address\": context.get_cancellation_address(),\n                    \"cancellation_tx\": context.cancellation_tx.serialize().upper(),\n                    \"cancellation_txid\": context.cancellation_tx.txid(),\n                    \"cancellation_fee\": context.cancellation_tx.get_fee(),\n                    \"cancellation_weight\": context.cancellation_tx.estimated_weight(),\n                    \"cancellation_amount\": context.cancellation_tx.output_value(),\n                }\n                # Simple checksum to ensure the file is not corrupted by foolish users\n                json_data[\"checksum\"] = self.json_checksum(json_data)\n                json.dump(json_data, f, indent=2)\n            download_dialog.show_message(_(\"File saved successfully\"))\n            context.cancellation_plan_saved = True\n        except Exception as e:\n            self.logger.exception(repr(e))\n            download_dialog.show_error(_(\"Error saving file\"))\n\n    def _create_pdf_printer(self, file_path: str) -> QPrinter:\n        printer = QPrinter()\n        printer.setResolution(600)\n        printer.setPageSize(QPageSize(QPageSize.PageSizeId.A4))\n        printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)\n        printer.setOutputFileName(file_path)\n        printer.setPageMargins(QMarginsF(20, 20, 20, 20), QPageLayout.Unit.Point)\n        return printer\n\n    def _paint_scaled_logo(self, painter: QPainter, page_width: int, current_height: float) -> int:\n        logo_pixmap = read_QPixmap_from_bytes(self.large_logo_bytes)\n        logo_size = int(page_width / 10)\n        scaled_logo = logo_pixmap.scaled(\n            logo_size,\n            logo_size,\n            Qt.AspectRatioMode.KeepAspectRatio,\n            Qt.TransformationMode.SmoothTransformation\n        )\n        # Center the logo horizontally and draw at current_height\n        logo_x = (page_width - scaled_logo.width()) / 2\n        painter.drawPixmap(int(logo_x), int(current_height), scaled_logo)\n        return scaled_logo.height()\n\n    def _save_recovery_plan_pdf(self, context: TimelockRecoveryContext, download_dialog: WindowModalDialog):\n        # Open a Save As dialog to get the file path\n        file_path, _selected_filter = QFileDialog.getSaveFileName(\n            download_dialog,\n            _(\"Save Recovery Plan PDF...\"),\n            os.path.join(self.base_dir, \"timelock-recovery-plan-{}.pdf\".format(context.recovery_plan_id)),\n            _(\"PDF files (*.pdf)\")\n        )\n        if not file_path:\n            return\n\n        painter = QPainter()\n        temp_file_path: Optional[str] = None\n\n        try:\n            with tempfile.NamedTemporaryFile(dir=os.path.dirname(file_path), prefix=f\"{os.path.basename(file_path)}-\", delete=False) as temp_file:\n                temp_file_path = temp_file.name\n            printer = self._create_pdf_printer(temp_file_path)\n            if not painter.begin(printer):\n                return\n            self._paint_recovery_plan_pdf(context, painter, printer)\n            painter.end()\n            shutil.move(temp_file_path, file_path)\n            download_dialog.show_message(_(\"File saved successfully\"))\n            context.recovery_plan_saved = True\n        except (IOError, MemoryError) as e:\n            self.logger.exception(repr(e))\n            download_dialog.show_error(_(\"Error saving file\"))\n            if temp_file_path is not None and os.path.exists(temp_file_path):\n                os.remove(temp_file_path)\n        finally:\n            if painter.isActive():\n                painter.end()\n\n    def _paint_recovery_plan_pdf(self, context: TimelockRecoveryContext, painter: QPainter, printer: QPrinter):\n        font_manager = FontManager(self.font_name, printer.resolution())\n\n        # Get page dimensions\n        page_rect = printer.pageRect(QPrinter.Unit.DevicePixel)\n        page_width = page_rect.width()\n        page_height = page_rect.height()\n\n        current_height = 0\n        page_number = 1\n\n        # Header\n        painter.setFont(font_manager.header_font)\n        painter.drawText(\n            QRectF(0, 0, page_width, font_manager.header_line_spacing + 20),\n            Qt.AlignmentFlag.AlignHCenter,\n            f\"Recovery-Guide  Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')}  ID: {context.recovery_plan_id}  Page: {page_number}\",\n        )\n        current_height += font_manager.header_line_spacing + 40\n\n        current_height += self._paint_scaled_logo(painter, page_width, current_height) + 40\n\n        # Title\n        painter.setFont(font_manager.title_font)\n        painter.drawText(QRectF(0, current_height, page_width, font_manager.title_line_spacing + 20), Qt.AlignmentFlag.AlignHCenter, \"Timelock-Recovery Guide\")\n        current_height += font_manager.title_line_spacing + 20\n\n        # Subtitle\n        painter.setFont(font_manager.subtitle_font)\n        painter.drawText(\n            QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing + 20), Qt.AlignmentFlag.AlignCenter,\n            f\"Electrum Version: {version.ELECTRUM_VERSION} - Plugin Version: {self.plugin_version}\"\n        )\n        current_height += font_manager.subtitle_line_spacing + 60\n\n        # Main content\n        recovery_tx_outputs = context.recovery_tx.outputs()\n        painter.setFont(font_manager.body_font)\n        intro_text = (\n            f\"This document will guide you through the process of recovering the funds on wallet: {context.wallet_name}. \"\n            f\"The process will take at least {context.timelock_days} days, and will eventually send the following amount \"\n            f\"to the following {'address' if len(recovery_tx_outputs) == 1 else 'addresses'}:\\n\\n\"\n            + '\\n'.join(f'• {output.address}: {context.main_window.config.format_amount_and_units(output.value)}' for output in recovery_tx_outputs) + \"\\n\\n\"\n            f\"Before proceeding, MAKE SURE THAT YOU HAVE ACCESS TO THE {'WALLET OF THIS ADDRESS' if len(recovery_tx_outputs) == 1 else 'WALLETS OF THESE ADDRESSES'}, \"\n            f\"OR TRUST THE {'OWNER OF THIS ADDRESS' if len(recovery_tx_outputs) == 1 else 'OWNERS OF THESE ADDRESSES'}. \"\n            \"The simplest way to do so is to send a small amount to the address, and then trying \"\n            \"to send all funds from that wallet to a different wallet. Also important: make sure that the \"\n            \"seed-phrase of this wallet has not been compromised, or else a malicious actor could steal \"\n            \"the funds the moment they reach their destination.\\n\\n\"\n            \"For more information, visit: https://timelockrecovery.com\\n\"\n        )\n\n        drawn_rect = painter.drawText(\n            QRectF(0, current_height, page_width, page_height - current_height),\n            Qt.TextFlag.TextWordWrap,\n            intro_text,\n        )\n        current_height += drawn_rect.height() + 20\n\n        # Step 1\n        painter.setFont(font_manager.title_small_font)\n        painter.drawText(\n            QRectF(0, current_height, page_width, font_manager.title_small_line_spacing + 20),Qt.AlignmentFlag.AlignLeft,\n            \"Step 1 - Broadcasting the Alert transaction\",\n        )\n        current_height += font_manager.title_small_line_spacing + 20\n\n        painter.setFont(font_manager.body_font)\n        # Calculate number of anchors\n        num_anchors = len(context.alert_tx.outputs()) - 1\n\n        # Split alert tx into parts if needed\n        alert_raw = context.alert_tx.serialize().upper()\n        if len(alert_raw) < 2300:\n            alert_raw_parts = [alert_raw]\n        else:\n            alert_raw_parts = []\n            for i in range(0, len(alert_raw), 2100):\n                alert_raw_parts.append(alert_raw[i:i+2100])\n\n        # Step 1 explanation text\n        step1_text = (\n            f\"The first step is to broadcast the Alert transaction. \"\n            f\"This transaction will keep most funds in the same wallet {context.wallet_name}, \"\n        )\n\n        if num_anchors > 0:\n            step1_text += (\n                f\"except for 600 sats that will be sent to \"\n                f\"{'each of the following addresses' if num_anchors > 1 else 'the following address'} \"\n                f\"(and can be used in case you need to accelerate the transaction via Child-Pay-For-Parent, \"\n                f\"as we'll explain later):\\n\"\n            )\n            for output in context.alert_tx.outputs():\n                if output.address != context.get_alert_address() and output.value == context.ANCHOR_OUTPUT_AMOUNT_SATS:\n                    step1_text += f\"• {output.address}\\n\"\n        else:\n            step1_text += \"except for a small fee.\\n\"\n\n        step1_text += (\n            f\"\\nTo broadcast the Alert transaction, \"\n            f\"{'scan the QR code on the next page' if len(alert_raw_parts) <= 1 else f'scan the QR codes on the next {len(alert_raw_parts)} pages, concatenate the contents of the QR codes (without spaces),'} \"\n            f\"and paste the content in one of the following Bitcoin block-explorer websites:\\n\"\n            \"• https://mempool.space/tx/push\\n\"\n            \"• https://blockstream.info/tx/push\\n\"\n            \"• https://coinb.in/#broadcast\\n\\n\"\n            f\"You should then see a success message for broadcasting transaction-id: {context.alert_tx.txid()}\"\n        )\n\n        drawn_rect = painter.drawText(\n            QRectF(0, current_height, page_width, page_height - current_height),\n            Qt.TextFlag.TextWordWrap,\n            step1_text\n        )\n        current_height += drawn_rect.height() + 20\n\n        # Generate QR pages for alert tx parts\n        for i, alert_part in enumerate(alert_raw_parts):\n            # Add new page\n            printer.newPage()\n            page_number += 1\n            current_height = 20\n\n            # Header\n            painter.setFont(font_manager.header_font)\n            painter.drawText(\n                QRectF(0, current_height, page_width, font_manager.header_line_spacing),\n                Qt.AlignmentFlag.AlignCenter,\n                f\"Recovery-Guide  Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')}  ID: {context.recovery_plan_id}  Page: {page_number}\"\n            )\n            current_height += font_manager.header_line_spacing + 20\n\n            # Title\n            painter.setFont(font_manager.title_font)\n            painter.drawText(\n                QRectF(0, current_height, page_width, font_manager.title_line_spacing),\n                Qt.AlignmentFlag.AlignCenter,\n                \"Alert Transaction\"\n            )\n            current_height += font_manager.title_line_spacing + 20\n\n            # Transaction ID\n            painter.setFont(font_manager.subtitle_font)\n            painter.drawText(\n                QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing),\n                Qt.AlignmentFlag.AlignCenter,\n                f\"Transaction Id: {context.alert_tx.txid()}\"\n            )\n            current_height += font_manager.subtitle_line_spacing + 20\n\n            # Part number if multiple parts\n            if len(alert_raw_parts) > 1:\n                painter.setFont(font_manager.subtitle_font)\n                painter.drawText(\n                    QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing),\n                    Qt.AlignmentFlag.AlignCenter,\n                    f\"Part {i+1} of {len(alert_raw_parts)}\"\n                )\n                current_height += font_manager.subtitle_line_spacing + 20\n\n            # QR Code\n            qr = qrcode.main.QRCode(\n                error_correction=qrcode.constants.ERROR_CORRECT_Q,\n            )\n            qr.add_data(alert_part)\n            qr.make()\n            qr_image = self._paint_qr(qr)\n\n            # Calculate QR position to center it\n            qr_width = int(page_width * 0.6)\n            qr_x = (page_width - qr_width) / 2\n            painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image)\n            current_height += qr_width + 40\n\n            # Raw text below QR\n            painter.setFont(font_manager.body_font)\n            painter.drawText(\n                QRectF(20, current_height, page_width, page_height - current_height),\n                Qt.TextFlag.TextWrapAnywhere,\n                alert_part\n            )\n\n        printer.newPage()\n        page_number += 1\n        current_height = 20\n        # Header\n        painter.setFont(font_manager.header_font)\n        painter.drawText(\n            QRectF(0, current_height, page_width, font_manager.header_line_spacing),\n            Qt.AlignmentFlag.AlignCenter,\n            f\"Recovery-Guide  Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')}  ID: {context.recovery_plan_id}  Page: {page_number}\"\n        )\n        current_height += font_manager.header_line_spacing + 20\n\n        # Step 2 page\n        painter.setFont(font_manager.title_small_font)\n        painter.drawText(QRectF(20, current_height, page_width, font_manager.title_small_line_spacing), Qt.AlignmentFlag.AlignLeft, \"Step 2 - Waiting for the Alert transaction confirmation\")\n        current_height += font_manager.title_small_line_spacing + 20\n\n        painter.setFont(font_manager.body_font)\n        painter.drawText(QRectF(20, current_height, page_width, font_manager.subtitle_line_spacing), Qt.AlignmentFlag.AlignLeft, \"You can follow the Alert transaction via any of the following links:\")\n        current_height += font_manager.subtitle_line_spacing + 20\n\n        # QR codes and links for transaction tracking\n        for link in [f\"https://mempool.space/tx/{context.alert_tx.txid()}\", f\"https://blockstream.info/tx/{context.alert_tx.txid()}\"]:\n            qr = qrcode.main.QRCode(\n                error_correction=qrcode.constants.ERROR_CORRECT_H,\n            )\n            qr.add_data(link)\n            qr.make()\n            qr_image = self._paint_qr(qr)\n\n            qr_width = int(page_width * 0.2)\n            qr_x = (page_width - qr_width) / 2\n            painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image)\n            current_height += qr_width + 20\n\n            painter.setFont(font_manager.body_small_font)\n            painter.drawText(QRectF(0, current_height, page_width, font_manager.body_small_line_spacing), Qt.AlignmentFlag.AlignCenter, link)\n            current_height += font_manager.body_small_line_spacing + 20\n\n        # Explanation text\n        painter.setFont(font_manager.body_font)\n        explanation_text = (\n            \"Please wait for a while until the transaction is marked as \\\"confirmed\\\" (number of confirmations greater than 0). \"\n            \"The time that takes a transaction to confirm depends on the fee that it pays, compared to the fee that other \"\n            \"pending transactions are willing to pay. At the time this document was created, it was hard to predict what a \"\n            \"reasonable fee would be today. If the transaction is not confirmed after 24 hours, you may try paying to a \"\n            \"Transaction Acceleration service, such as the one offered by: https://mempool.space .\"\n        )\n        if len(context.outputs) > 0:\n            explanation_text += (\n                f\" Another solution, which may be cheaper but requires more technical skill, would be to use\"\n                f\"{' one of the wallets that receive 600 sats (addresses mentioned in Step 1),' if len(context.outputs) > 1 else ' the wallet that receive 600 sats (address mentioned in Step 1),'}\"\n                \" and send a high-fee transaction that includes that 600 sats UTXO (this transaction could also be from the\"\n                \" wallet to itself). For more information, visit: https://timelockrecovery.com .\"\n            )\n\n        drawn_rect = painter.drawText(QRectF(20, current_height, page_width, page_height - current_height), Qt.TextFlag.TextWordWrap, explanation_text)\n        current_height += drawn_rect.height() + 40\n\n        # Step 3 header\n        painter.setFont(font_manager.title_small_font)\n        painter.drawText(QRectF(20, current_height, page_width, font_manager.title_small_line_spacing), Qt.AlignmentFlag.AlignLeft, \"Step 3 - Broadcasting the Recovery transaction\")\n        current_height += font_manager.title_small_line_spacing + 20\n\n        # Split recovery transaction if needed\n        recovery_raw = context.recovery_tx.serialize().upper()\n        recovery_raw_parts = [recovery_raw[i:i+2100] for i in range(0, len(recovery_raw), 2100)] if len(recovery_raw) > 2300 else [recovery_raw]\n\n        # Step 3 explanation\n        painter.setFont(font_manager.body_font)\n        step3_text = (\n            f\"Approximately {context.timelock_days} days after the Alert transaction has been confirmed, you \"\n            \"will be able to broadcast the second Recovery transaction that will send the funds to the final\"\n            f\"{' destinations,' if len(recovery_tx_outputs) > 1 else ' destination,'} mentioned on the first page. This can be done using the same websites mentioned in Step 1, but \"\n            f\"this time you will need to {'scan the QR code on page ' + str(page_number + 1) if len(recovery_raw_parts) <= 1 else 'scan the QR codes on pages ' + str(page_number + 1) + '-' + str(page_number + len(recovery_raw_parts)) + ' and concatenate their content (without spaces)'}. If this transaction remains unconfirmed for a \"\n            \"long time, you should use the Transaction Acceleration service mentioned on Step 2, or use the \"\n            \"Child-Pay-For-Parent technique.\"\n        )\n        painter.drawText(QRectF(20, current_height, page_width, page_height - current_height), Qt.TextFlag.TextWordWrap, step3_text)\n\n        # Recovery transaction pages\n        for i, recovery_part in enumerate(recovery_raw_parts):\n            printer.newPage()\n            page_number += 1\n            current_height = 20\n\n            # Header\n            painter.setFont(font_manager.header_font)\n            painter.drawText(\n                QRectF(0, current_height, page_width, font_manager.header_line_spacing),\n                Qt.AlignmentFlag.AlignCenter,\n                f\"Recovery-Guide  Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')}  ID: {context.recovery_plan_id}  Page: {page_number}\"\n            )\n            current_height += font_manager.header_line_spacing + 20\n\n            # Title\n            painter.setFont(font_manager.title_font)\n            painter.drawText(\n                QRectF(0, current_height, page_width, font_manager.title_line_spacing),\n                Qt.AlignmentFlag.AlignCenter,\n                \"Recovery Transaction\"\n            )\n            current_height += font_manager.title_line_spacing + 20\n\n            # Transaction ID\n            painter.setFont(font_manager.subtitle_font)\n            painter.drawText(\n                QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing),\n                Qt.AlignmentFlag.AlignCenter,\n                f\"Transaction Id: {context.recovery_tx.txid()}\"\n            )\n            current_height += font_manager.subtitle_line_spacing + 20\n\n            # Part number if multiple parts\n            if len(recovery_raw_parts) > 1:\n                painter.setFont(font_manager.subtitle_font)\n                painter.drawText(\n                    QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing),\n                    Qt.AlignmentFlag.AlignCenter,\n                    f\"Part {i+1} of {len(recovery_raw_parts)}\"\n                )\n                current_height += font_manager.subtitle_line_spacing + 20\n\n            # QR Code\n            qr = qrcode.main.QRCode(\n                error_correction=qrcode.constants.ERROR_CORRECT_Q,\n            )\n            qr.add_data(recovery_part)\n            qr.make()\n            qr_image = self._paint_qr(qr)\n\n            # Calculate QR position to center it\n            qr_width = int(page_width * 0.6)\n            qr_x = (page_width - qr_width) / 2\n            painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image)\n            current_height += qr_width + 40\n\n            # Raw text below QR\n            painter.setFont(font_manager.body_font)\n            painter.drawText(\n                QRectF(20, current_height, page_width, page_height - current_height),\n                Qt.TextFlag.TextWrapAnywhere,\n                recovery_part\n            )\n\n    def _save_cancellation_plan_pdf(self, context: TimelockRecoveryContext, download_dialog: WindowModalDialog):\n        # Open a Save As dialog to get the file path\n        file_path, _selected_filter = QFileDialog.getSaveFileName(\n            download_dialog,\n            _(\"Save Cancellation Plan PDF...\"),\n            os.path.join(self.base_dir, \"timelock-cancellation-plan-{}.pdf\".format(context.recovery_plan_id)),\n            _(\"PDF files (*.pdf)\")\n        )\n        if not file_path:\n            return\n\n        painter = QPainter()\n        temp_file_path: Optional[str] = None\n\n        try:\n            with tempfile.NamedTemporaryFile(dir=os.path.dirname(file_path), prefix=f\"{os.path.basename(file_path)}-\", delete=False) as temp_file:\n                temp_file_path = temp_file.name\n            printer = self._create_pdf_printer(temp_file_path)\n            if not painter.begin(printer):\n                return\n            self._paint_cancellation_plan_pdf(context, painter, printer)\n            painter.end()\n            shutil.move(temp_file_path, file_path)\n            download_dialog.show_message(_(\"File saved successfully\"))\n            context.cancellation_plan_saved = True\n        except (IOError, MemoryError) as e:\n            self.logger.exception(repr(e))\n            download_dialog.show_error(_(\"Error saving file\"))\n            if temp_file_path is not None and os.path.exists(temp_file_path):\n                os.remove(temp_file_path)\n        finally:\n            if painter.isActive():\n                painter.end()\n\n    def _paint_cancellation_plan_pdf(self, context: TimelockRecoveryContext, painter: QPainter, printer: QPrinter):\n        cancellation_raw = context.cancellation_tx.serialize().upper()\n        if len(cancellation_raw) > 2300:\n            # Splitting the cancellation transaction into multiple QR codes is not implemented\n            # because it is unexpected to happen anyways.\n            raise Exception(\"Cancellation transaction is too large to be saved as a single QR code\")\n\n        font_manager = FontManager(self.font_name, printer.resolution())\n\n        # Get page dimensions\n        page_rect = printer.pageRect(QPrinter.Unit.DevicePixel)\n        page_width = page_rect.width()\n        page_height = page_rect.height()\n\n        current_height = 0\n        page_number = 1\n\n        # Header\n        painter.setFont(font_manager.header_font)\n        painter.drawText(\n            QRectF(0, current_height, page_width, font_manager.header_line_spacing),\n            Qt.AlignmentFlag.AlignCenter,\n            f\"Cancellation-Guide  Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')}  ID: {context.recovery_plan_id}  Page: {page_number}\"\n        )\n        current_height += font_manager.header_line_spacing + 40\n\n        current_height += self._paint_scaled_logo(painter, page_width, current_height) + 40\n\n        # Title\n        painter.setFont(font_manager.title_font)\n        painter.drawText(\n            QRectF(0, current_height, page_width, font_manager.title_line_spacing),\n            Qt.AlignmentFlag.AlignCenter,\n            \"Timelock-Recovery Cancellation Guide\"\n        )\n        current_height += font_manager.title_line_spacing + 20\n\n        # Subtitle\n        painter.setFont(font_manager.subtitle_font)\n        painter.drawText(\n            QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing + 20), Qt.AlignmentFlag.AlignCenter,\n            f\"Electrum Version: {version.ELECTRUM_VERSION} - Plugin Version: {self.plugin_version}\"\n        )\n        current_height += font_manager.subtitle_line_spacing + 60\n\n        # Main text\n        painter.setFont(font_manager.body_font)\n        explanation_text = (\n            f\"This document is intended solely for the eyes of the owner of wallet: {context.wallet_name}. \"\n            f\"The Recovery Guide (the other document) will allow to transfer the funds from this wallet to \"\n            f\"a different wallet within {context.timelock_days} days. To prevent this from happening accidentally \"\n            f\"or maliciously by someone who found that document, you should periodically check if the Alert \"\n            f\"transaction has been broadcast, using a Bitcoin block-explorer website such as:\"\n        )\n        drawn_rect = painter.drawText(\n            QRectF(20, current_height, page_width - 40, page_height),\n            Qt.TextFlag.TextWordWrap,\n            explanation_text\n        )\n        current_height += drawn_rect.height() + 40\n\n        # QR codes and links for transaction tracking\n        for link in [f\"https://mempool.space/tx/{context.alert_tx.txid()}\", f\"https://blockstream.info/tx/{context.alert_tx.txid()}\"]:\n            qr = qrcode.main.QRCode(\n                error_correction=qrcode.constants.ERROR_CORRECT_H,\n            )\n            qr.add_data(link)\n            qr.make()\n            qr_image = self._paint_qr(qr)\n\n            qr_width = int(page_width * 0.2)\n            qr_x = (page_width - qr_width) / 2\n            painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image)\n            current_height += qr_width + 20\n\n            painter.setFont(font_manager.body_small_font)\n            painter.drawText(\n                QRectF(0, current_height, page_width, font_manager.body_small_line_spacing),\n                Qt.AlignmentFlag.AlignCenter,\n                link\n            )\n            current_height += font_manager.body_small_line_spacing + 20\n\n        # Watch tower text\n        painter.setFont(font_manager.body_font)\n        drawn_rect = painter.drawText(\n            QRectF(20, current_height, page_width - 40, page_height - current_height),\n            Qt.TextFlag.TextWordWrap,\n            \"It is also recommended to use a Watch-Tower service that will notify you immediately if the\"\n            \" Alert transaction has been broadcast. For more details, visit: https://timelockrecovery.com .\"\n        )\n        current_height += drawn_rect.height() + 40\n\n        # Cancellation transaction section\n        cancellation_text = (\n            \"In case the Alert transaction has been broadcast, and you want to stop the funds from \"\n            \"leaving this wallet, you can scan the QR code on page 2, and broadcast \"\n            \"the content using one of the following Bitcoin block-explorer websites:\\n\\n\"\n            \"• https://mempool.space/tx/push\\n\"\n            \"• https://blockstream.info/tx/push\\n\"\n            \"• https://coinb.in/#broadcast\\n\\n\"\n            \"If the transaction is not confirmed within reasonable time due to a low fee, you will have \"\n            \"to access the wallet and use Replace-By-Fee/Child-Pay-For-Parent to move the funds to a new \"\n            \"address on your wallet. (you can also pay to an Acceleration Service such as the one offered \"\n            \"by https://mempool.space)\\n\\n\"\n            f\"IMPORTANT NOTICE: If you lost the keys to access wallet {context.wallet_name} - do not broadcast the \"\n            \"transaction on page 2! In this case it is recommended to destroy all copies of this document.\"\n        )\n        painter.drawText(\n            QRectF(20, current_height, page_width - 40, page_height),\n            Qt.TextFlag.TextWordWrap,\n            cancellation_text\n        )\n\n        # New page for cancellation transaction\n        printer.newPage()\n        page_number += 1\n        current_height = 20\n\n        # Header\n        painter.setFont(font_manager.header_font)\n        painter.drawText(\n            QRectF(0, current_height, page_width, font_manager.header_line_spacing),\n            Qt.AlignmentFlag.AlignCenter,\n            f\"Cancellation-Guide  Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')}  ID: {context.recovery_plan_id}  Page: {page_number}\"\n        )\n        current_height += font_manager.header_line_spacing + 20\n\n        # Cancellation transaction title\n        painter.setFont(font_manager.title_font)\n        painter.drawText(\n            QRectF(0, current_height, page_width, font_manager.title_line_spacing),\n            Qt.AlignmentFlag.AlignCenter,\n            \"Cancellation Transaction\"\n        )\n        current_height += font_manager.title_line_spacing + 20\n\n        # Transaction ID\n        painter.setFont(font_manager.subtitle_font)\n        painter.drawText(\n            QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing),\n            Qt.AlignmentFlag.AlignCenter,\n            f\"Transaction Id: {context.cancellation_tx.txid()}\"\n        )\n        current_height += font_manager.subtitle_line_spacing + 20\n\n        # QR Code for cancellation transaction\n        qr = qrcode.main.QRCode(\n            error_correction=qrcode.constants.ERROR_CORRECT_Q,\n        )\n        qr.add_data(cancellation_raw)\n        qr.make()\n        qr_image = self._paint_qr(qr)\n\n        qr_width = int(page_width * 0.6)\n        qr_x = (page_width - qr_width) / 2\n        painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image)\n        current_height += qr_width + 40\n\n        # Raw transaction text\n        painter.setFont(font_manager.body_font)\n        painter.drawText(\n            QRectF(20, current_height, page_width - 40, page_height),\n            Qt.TextFlag.TextWrapAnywhere,\n            cancellation_raw\n        )\n\n    @classmethod\n    def _paint_qr(cls, qr: qrcode.main.QRCode) -> QImage:\n        k = len(qr.get_matrix())\n        base_img = QImage(k * 5, k * 5, QImage.Format.Format_ARGB32)\n        draw_qr(qr=qr, paint_device=base_img)\n        return base_img\n"
  },
  {
    "path": "electrum/plugins/timelock_recovery/timelock_recovery.py",
    "content": "from datetime import datetime\nimport hashlib\nimport json\nfrom typing import TYPE_CHECKING, Callable, List, Optional, Sequence, Tuple, Any\n\nfrom electrum.bitcoin import address_to_script\nfrom electrum.plugin import BasePlugin\nfrom electrum.transaction import PartialTxOutput, PartialTxInput, TxOutpoint\nfrom electrum.util import bfh\n\nif TYPE_CHECKING:\n    from electrum.gui.qt import ElectrumWindow\n    from electrum.transaction import PartialTransaction, TxOutput\n    from electrum.wallet import Abstract_Wallet\n\nALERT_ADDRESS_LABEL = \"Timelock Recovery Alert Address\"\nCANCELLATION_ADDRESS_LABEL = \"Timelock Recovery Cancellation Address\"\n\nclass PartialTxInputWithFixedNsequence(PartialTxInput):\n    _fixed_nsequence: int\n\n    def __init__(self, *args, nsequence: int = 0xfffffffe, **kwargs):\n        self._fixed_nsequence = nsequence\n        super().__init__(*args, **kwargs)\n\n    @property\n    def nsequence(self) -> int:\n        return self._fixed_nsequence\n\n    @nsequence.setter\n    def nsequence(self, value: int):\n        pass # ignore override attempts\n\nclass TimelockRecoveryContext:\n    wallet: 'Abstract_Wallet'\n    wallet_name: str\n    main_window: Optional['ElectrumWindow'] = None\n    timelock_days: Optional[int] = None\n    cancellation_address: Optional[str] = None\n    outputs: Optional[List['PartialTxOutput']] = None\n    alert_tx: Optional['PartialTransaction'] = None\n    recovery_tx: Optional['PartialTransaction'] = None\n    cancellation_tx: Optional['PartialTransaction'] = None\n    recovery_plan_id: Optional[str] = None\n    recovery_plan_created_at: Optional[datetime] = None\n    _alert_address: Optional[str] = None\n    _cancellation_address: Optional[str] = None\n    recovery_plan_saved: bool = False\n    cancellation_plan_saved: bool = False\n\n    ANCHOR_OUTPUT_AMOUNT_SATS = 600\n\n    def __init__(self, wallet: 'Abstract_Wallet'):\n        self.wallet = wallet\n        self.wallet_name = str(self.wallet)\n\n    def _get_address_by_label(self, label: str) -> str:\n        unused_addresses = list(self.wallet.get_unused_addresses())\n        for addr in unused_addresses:\n            if self.wallet.get_label_for_address(addr) == label:\n                return addr\n        for addr in unused_addresses:\n            if not self.wallet.is_address_reserved(addr) and not self.wallet.get_label_for_address(addr):\n                self.wallet.set_label(addr, label)\n                return addr\n        if self.wallet.is_deterministic():\n            addr = self.wallet.create_new_address(False)\n            if addr:\n                self.wallet.set_label(addr, label)\n                return addr\n        return ''\n\n    def get_alert_address(self) -> str:\n        if self._alert_address is None:\n            self._alert_address = self._get_address_by_label(ALERT_ADDRESS_LABEL)\n        return self._alert_address\n\n    def get_cancellation_address(self) -> str:\n        if self._cancellation_address is None:\n            self._cancellation_address = self._get_address_by_label(CANCELLATION_ADDRESS_LABEL)\n        return self._cancellation_address\n\n    def make_unsigned_alert_tx(self, fee_policy) -> 'PartialTransaction':\n        alert_tx_outputs = [\n            PartialTxOutput(scriptpubkey=address_to_script(self.get_alert_address()), value='!'),\n        ] + [\n            PartialTxOutput(scriptpubkey=output.scriptpubkey, value=self.ANCHOR_OUTPUT_AMOUNT_SATS)\n            for output in self.outputs\n        ]\n        return self.wallet.make_unsigned_transaction(\n            coins=self.wallet.get_spendable_coins(confirmed_only=False),\n            outputs=alert_tx_outputs,\n            fee_policy=fee_policy,\n            is_sweep=False,\n            locktime=self.alert_tx.locktime if self.alert_tx else None,\n        )\n\n    def _alert_tx_output(self) -> Tuple[int, 'TxOutput']:\n        tx_outputs: List[Tuple[int, 'TxOutput']] = [\n            (index, tx_output) for index, tx_output in enumerate(self.alert_tx.outputs())\n            if tx_output.address == self.get_alert_address() and tx_output.value != self.ANCHOR_OUTPUT_AMOUNT_SATS\n        ]\n        if len(tx_outputs) != 1:\n            # Safety check - not expected to happen\n            raise ValueError(f\"Expected 1 output from the Alert transaction to the Alert Address, but got {len(tx_outputs)}.\")\n        return tx_outputs[0]\n\n    def _alert_tx_outpoint(self, out_idx: int) -> TxOutpoint:\n        return TxOutpoint(txid=bfh(self.alert_tx.txid()), out_idx=out_idx)\n\n    def make_unsigned_recovery_tx(self, fee_policy) -> 'PartialTransaction':\n        prevout_index, prevout = self._alert_tx_output()\n        nsequence: int = round(self.timelock_days * 24 * 60 * 60 / 512)\n        if nsequence > 0xFFFF:\n            # Safety check - not expected to happen\n            raise ValueError(\"Sequence number is too large\")\n        nsequence += 0x00400000 # time based lock instead of block-height based lock\n        recovery_tx_input = PartialTxInputWithFixedNsequence(\n            prevout=self._alert_tx_outpoint(prevout_index),\n            nsequence=nsequence,\n        )\n        recovery_tx_input.witness_utxo = prevout\n\n        return self.wallet.make_unsigned_transaction(\n            coins=[recovery_tx_input],\n            outputs=[output for output in self.outputs if output.value != 0],\n            fee_policy=fee_policy,\n            is_sweep=False,\n            locktime=self.recovery_tx.locktime if self.recovery_tx else None,\n        )\n\n    def add_input_info_to_recovery_tx(self):\n        if self.recovery_tx and self.alert_tx.is_complete():\n            self.recovery_tx.inputs()[0].utxo = self.alert_tx\n\n    def add_input_info_to_cancellation_tx(self):\n        if self.cancellation_tx and self.alert_tx.is_complete():\n            self.cancellation_tx.inputs()[0].utxo = self.alert_tx\n\n    def make_unsigned_cancellation_tx(self, fee_policy) -> 'PartialTransaction':\n        prevout_index, prevout = self._alert_tx_output()\n        cancellation_tx_input = PartialTxInput(\n            prevout=self._alert_tx_outpoint(prevout_index),\n        )\n        cancellation_tx_input.witness_utxo = prevout\n\n        return self.wallet.make_unsigned_transaction(\n            coins=[cancellation_tx_input],\n            outputs=[\n                PartialTxOutput(scriptpubkey=address_to_script(self.get_cancellation_address()), value='!'),\n            ],\n            fee_policy=fee_policy,\n            is_sweep=False,\n            locktime=self.cancellation_tx.locktime if self.cancellation_tx else None,\n        )\n\nclass TimelockRecoveryPlugin(BasePlugin):\n    def __init__(self, parent, config, name):\n        BasePlugin.__init__(self, parent, config, name)\n\n    @classmethod\n    def json_checksum(cls, json_data: dict[str, Any]) -> str:\n        # Assumes the values have a consistent json representation (not a key-value\n        # object whose fields can be ordered in multiple ways).\n        return hashlib.sha256(json.dumps(\n            sorted(json_data.items()),\n            skipkeys=False, ensure_ascii=False, check_circular=True,\n            allow_nan=True, cls=None, indent=None, separators=(',', ':'),\n            default=None, sort_keys=False,\n        ).encode()).hexdigest()[:8]\n"
  },
  {
    "path": "electrum/plugins/trezor/__init__.py",
    "content": "\n"
  },
  {
    "path": "electrum/plugins/trezor/clientbase.py",
    "content": "import time\nfrom struct import pack\n\nimport electrum_ecc as ecc\n\nfrom electrum.i18n import _\nfrom electrum.util import UserCancelled, UserFacingException\nfrom electrum.keystore import bip39_normalize_passphrase\nfrom electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath as parse_path\nfrom electrum.logging import Logger\nfrom electrum.plugin import runs_in_hwd_thread\nfrom electrum.hw_wallet.plugin import OutdatedHwFirmwareException, HardwareClientBase\n\nfrom trezorlib.client import TrezorClient, PASSPHRASE_ON_DEVICE\nfrom trezorlib.exceptions import TrezorFailure, Cancelled, OutdatedFirmwareError\nfrom trezorlib.messages import WordRequestType, FailureType, ButtonRequestType\nimport trezorlib.btc\nimport trezorlib.device\n\ntry:\n    # trezor >= 0.13.9\n    from trezorlib.messages import RecoveryDeviceInputMethod\nexcept ImportError:\n    # Backward compatibility for trezor < 0.13.9\n    from trezorlib.messages import RecoveryDeviceType as RecoveryDeviceInputMethod\n\n\nMESSAGES = {\n    ButtonRequestType.ConfirmOutput:\n        _(\"Confirm the transaction output on your {} device\"),\n    ButtonRequestType.ResetDevice:\n        _(\"Complete the initialization process on your {} device\"),\n    ButtonRequestType.ConfirmWord:\n        _(\"Write down the seed word shown on your {}\"),\n    ButtonRequestType.WipeDevice:\n        _(\"Confirm on your {} that you want to wipe it clean\"),\n    ButtonRequestType.ProtectCall:\n        _(\"Confirm on your {} device the message to sign\"),\n    ButtonRequestType.SignTx:\n        _(\"Confirm the total amount spent and the transaction fee on your {} device\"),\n    ButtonRequestType.Address:\n        _(\"Confirm wallet address on your {} device\"),\n    ButtonRequestType._Deprecated_ButtonRequest_PassphraseType:\n        _(\"Choose on your {} device where to enter your passphrase\"),\n    ButtonRequestType.PassphraseEntry:\n        _(\"Please enter your passphrase on the {} device\"),\n    'default': _(\"Check your {} device to continue\"),\n}\n\n\nclass TrezorClientBase(HardwareClientBase, Logger):\n    def __init__(self, transport, handler, plugin):\n        HardwareClientBase.__init__(self, plugin=plugin)\n        if plugin.is_outdated_fw_ignored():\n            TrezorClient.is_outdated = lambda *args, **kwargs: False\n        self.client = TrezorClient(transport, ui=self)\n        self.device = plugin.device\n        self.handler = handler\n        Logger.__init__(self)\n\n        self.msg = None\n        self.creating_wallet = False\n\n        self.in_flow = False\n\n        self.used()\n\n    def run_flow(self, message=None, creating_wallet=False):\n        if self.in_flow:\n            raise RuntimeError(\"Overlapping call to run_flow\")\n\n        self.in_flow = True\n        self.msg = message\n        self.creating_wallet = creating_wallet\n        self.prevent_timeouts()\n        return self\n\n    def end_flow(self):\n        self.in_flow = False\n        self.msg = None\n        self.creating_wallet = False\n        self.handler.finished()\n        self.used()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, e, traceback):\n        self.end_flow()\n        if e is not None:\n            if isinstance(e, Cancelled):\n                raise UserCancelled() from e\n            elif isinstance(e, TrezorFailure):\n                raise RuntimeError(str(e)) from e\n            elif isinstance(e, OutdatedFirmwareError):\n                raise OutdatedHwFirmwareException(e) from e\n            else:\n                return False\n        return True\n\n    @property\n    def features(self):\n        return self.client.features\n\n    def __str__(self):\n        return \"%s/%s\" % (self.label(), self.features.device_id)\n\n    def label(self):\n        return self.features.label\n\n    def get_soft_device_id(self):\n        return self.features.device_id\n\n    def is_initialized(self):\n        return self.features.initialized\n\n    def is_pairable(self):\n        return not self.features.bootloader_mode\n\n    @runs_in_hwd_thread\n    def has_usable_connection_with_device(self):\n        if self.in_flow:\n            return True\n\n        try:\n            self.client.init_device()\n        except BaseException:\n            return False\n        return True\n\n    def used(self):\n        self.last_operation = time.time()\n\n    def prevent_timeouts(self):\n        self.last_operation = float('inf')\n\n    @runs_in_hwd_thread\n    def timeout(self, cutoff):\n        '''Time out the client if the last operation was before cutoff.'''\n        if self.last_operation < cutoff:\n            self.logger.info(\"timed out\")\n            self.clear_session()\n\n    def i4b(self, x):\n        return pack('>I', x)\n\n    @runs_in_hwd_thread\n    def get_xpub(self, bip32_path, xtype, creating=False):\n        address_n = parse_path(bip32_path)\n        with self.run_flow(creating_wallet=creating):\n            node = trezorlib.btc.get_public_node(self.client, address_n).node\n        return BIP32Node(xtype=xtype,\n                         eckey=ecc.ECPubkey(node.public_key),\n                         chaincode=node.chain_code,\n                         depth=node.depth,\n                         fingerprint=self.i4b(node.fingerprint),\n                         child_number=self.i4b(node.child_num)).to_xpub()\n\n    @runs_in_hwd_thread\n    def toggle_passphrase(self):\n        if self.features.passphrase_protection:\n            msg = _(\"Confirm on your {} device to disable passphrases\")\n        else:\n            msg = _(\"Confirm on your {} device to enable passphrases\")\n        enabled = not self.features.passphrase_protection\n        with self.run_flow(msg):\n            trezorlib.device.apply_settings(self.client, use_passphrase=enabled)\n\n    @runs_in_hwd_thread\n    def change_label(self, label):\n        with self.run_flow(_(\"Confirm the new label on your {} device\")):\n            trezorlib.device.apply_settings(self.client, label=label)\n\n    @runs_in_hwd_thread\n    def change_homescreen(self, homescreen):\n        with self.run_flow(_(\"Confirm on your {} device to change your home screen\")):\n            trezorlib.device.apply_settings(self.client, homescreen=homescreen)\n\n    @runs_in_hwd_thread\n    def set_pin(self, remove):\n        if remove:\n            msg = _(\"Confirm on your {} device to disable PIN protection\")\n        elif self.features.pin_protection:\n            msg = _(\"Confirm on your {} device to change your PIN\")\n        else:\n            msg = _(\"Confirm on your {} device to set a PIN\")\n        with self.run_flow(msg):\n            trezorlib.device.change_pin(self.client, remove)\n\n    @runs_in_hwd_thread\n    def clear_session(self):\n        '''Clear the session to force pin (and passphrase if enabled)\n        re-entry.  Does not leak exceptions.'''\n        self.logger.info(f\"clear session: {self}\")\n        self.prevent_timeouts()\n        try:\n            self.client.clear_session()\n        except BaseException as e:\n            # If the device was removed it has the same effect...\n            self.logger.info(f\"clear_session: ignoring error {e}\")\n\n    @runs_in_hwd_thread\n    def close(self):\n        '''Called when Our wallet was closed or the device removed.'''\n        self.logger.info(\"closing client\")\n        self.clear_session()\n\n    @runs_in_hwd_thread\n    def is_uptodate(self):\n        if self.client.is_outdated():\n            return False\n        return self.client.version >= self.plugin.minimum_firmware\n\n    def get_trezor_model(self):\n        \"\"\"Returns '1' for Trezor One, 'T' for Trezor T, etc.\"\"\"\n        return self.features.model\n\n    def device_model_name(self):\n        model = self.get_trezor_model()\n        if model == '1':\n            return \"Trezor One\"\n        elif model == 'T':\n            return \"Trezor T\"\n        elif model == \"Safe 3\":\n            return \"Trezor Safe 3\"\n        elif model == \"Safe 5\":\n            return \"Trezor Safe 5\"\n        return None\n\n    @runs_in_hwd_thread\n    def show_address(self, address_str, script_type, multisig=None):\n        coin_name = self.plugin.get_coin_name()\n        address_n = parse_path(address_str)\n        with self.run_flow():\n            return trezorlib.btc.get_address(\n                self.client,\n                coin_name,\n                address_n,\n                show_display=True,\n                script_type=script_type,\n                multisig=multisig)\n\n    @runs_in_hwd_thread\n    def sign_message(self, address_str, message, *, script_type):\n        coin_name = self.plugin.get_coin_name()\n        address_n = parse_path(address_str)\n        with self.run_flow():\n            return trezorlib.btc.sign_message(\n                self.client,\n                coin_name,\n                address_n,\n                message,\n                script_type=script_type,\n                no_script_type=True)\n\n    @runs_in_hwd_thread\n    def recover_device(self, recovery_type, *args, **kwargs):\n        input_callback = self.mnemonic_callback(recovery_type)\n        with self.run_flow():\n            return trezorlib.device.recover(\n                self.client,\n                *args,\n                input_callback=input_callback,\n                type=recovery_type,\n                **kwargs)\n\n    # ========= Unmodified trezorlib methods =========\n\n    @runs_in_hwd_thread\n    def sign_tx(self, *args, **kwargs):\n        with self.run_flow():\n            return trezorlib.btc.sign_tx(self.client, *args, **kwargs)\n\n    @runs_in_hwd_thread\n    def get_ownership_id(self, *args, **kwargs):\n        with self.run_flow():\n            return trezorlib.btc.get_ownership_id(self.client, *args, **kwargs)\n\n    @runs_in_hwd_thread\n    def get_ownership_proof(self, *args, **kwargs):\n        with self.run_flow():\n            return trezorlib.btc.get_ownership_proof(self.client, *args, **kwargs)\n\n    @runs_in_hwd_thread\n    def reset_device(self, *args, **kwargs):\n        with self.run_flow():\n            return trezorlib.device.reset(self.client, *args, **kwargs)\n\n    @runs_in_hwd_thread\n    def wipe_device(self, *args, **kwargs):\n        with self.run_flow():\n            return trezorlib.device.wipe(self.client, *args, **kwargs)\n\n    # ========= UI methods ==========\n\n    def button_request(self, br):\n        message = self.msg or MESSAGES.get(br.code) or MESSAGES['default']\n        self.handler.show_message(message.format(self.device), self.client.cancel)\n\n    def get_pin(self, code=None):\n        show_strength = True\n        if code == 2:\n            msg = _(\"Enter a new PIN for your {}:\")\n        elif code == 3:\n            msg = (_(\"Re-enter the new PIN for your {}.\\n\\n\"\n                     \"NOTE: the positions of the numbers have changed!\"))\n        else:\n            msg = _(\"Enter your current {} PIN:\")\n            show_strength = False\n        pin = self.handler.get_pin(msg.format(self.device), show_strength=show_strength)\n        if not pin:\n            raise Cancelled\n        # check PIN length. Depends on model and firmware version\n        # https://github.com/trezor/trezor-firmware/issues/1167\n        limit = 9\n        if self.get_trezor_model() == \"1\":\n            if (1, 10, 0) <= self.client.version:\n                limit = 50\n        else:\n            if (2, 4, 0) <= self.client.version:\n                limit = 50\n        if len(pin) > limit:\n            self.handler.show_error(_('The PIN cannot be longer than {} characters.').format(limit))\n            raise Cancelled\n        return pin\n\n    def get_passphrase(self, available_on_device):\n        if self.creating_wallet:\n            msg = _(\"Enter a passphrase to generate this wallet.  Each time \"\n                    \"you use this wallet your {} will prompt you for the \"\n                    \"passphrase.  If you forget the passphrase you cannot \"\n                    \"access the bitcoins in the wallet.\").format(self.device)\n        else:\n            msg = _(\"Enter the passphrase to unlock this wallet:\")\n\n        self.handler.passphrase_on_device = available_on_device\n        passphrase = self.handler.get_passphrase(msg, self.creating_wallet)\n        if passphrase is PASSPHRASE_ON_DEVICE:\n            return passphrase\n        if passphrase is None:\n            raise Cancelled\n        passphrase = bip39_normalize_passphrase(passphrase)\n        length = len(passphrase)\n        if length > 50:\n            self.handler.show_error(_(\"Too long passphrase ({} > 50 chars).\").format(length))\n            raise Cancelled\n        return passphrase\n\n    def _matrix_char(self, matrix_type):\n        num = 9 if matrix_type == WordRequestType.Matrix9 else 6\n        char = self.handler.get_matrix(num)\n        if char == 'x':\n            raise Cancelled\n        return char\n\n    def mnemonic_callback(self, recovery_type):\n        if recovery_type is None:\n            return None\n\n        if recovery_type == RecoveryDeviceInputMethod.Matrix:\n            return self._matrix_char\n\n        step = 0\n        def word_callback(_ignored):\n            nonlocal step\n            step += 1\n            msg = _(\"Step {}/24.  Enter seed word as explained on your {}:\").format(step, self.device)\n            word = self.handler.get_word(msg)\n            if not word:\n                raise Cancelled\n            return word\n        return word_callback\n"
  },
  {
    "path": "electrum/plugins/trezor/cmdline.py",
    "content": "from electrum.plugin import hook\nfrom electrum.i18n import _\nfrom electrum.util import print_stderr\nfrom electrum.hw_wallet import CmdLineHandler\n\nfrom .trezor import TrezorPlugin, PASSPHRASE_ON_DEVICE\n\nclass TrezorCmdLineHandler(CmdLineHandler):\n    def __init__(self):\n        self.passphrase_on_device = False\n        super().__init__()\n\n    def get_passphrase(self, msg, confirm):\n        import getpass\n        print_stderr(msg)\n        if self.passphrase_on_device and self.yes_no_question(_('Enter passphrase on device?')):\n            return PASSPHRASE_ON_DEVICE\n        else:\n            return getpass.getpass('')\n\nclass Plugin(TrezorPlugin):\n    handler = CmdLineHandler()\n    @hook\n    def init_keystore(self, keystore):\n        if not isinstance(keystore, self.keystore_class):\n            return\n        keystore.handler = self.handler\n\n    def create_handler(self, window):\n        return self.handler\n"
  },
  {
    "path": "electrum/plugins/trezor/manifest.json",
    "content": "{\n  \"name\": \"trezor\",\n  \"fullname\": \"Trezor Wallet\",\n  \"description\": \"Provides support for Trezor hardware wallet\",\n  \"requires\": [[\"trezorlib\",\"pypi.org/project/trezor/\"]],\n  \"registers_keystore\": [\"hardware\", \"trezor\", \"Trezor wallet\"],\n  \"icon\":\"trezor.png\",\n  \"available_for\": [\"qt\", \"cmdline\"]\n}\n"
  },
  {
    "path": "electrum/plugins/trezor/qt.py",
    "content": "from functools import partial\nimport threading\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import Qt, QEventLoop, pyqtSignal\nfrom PyQt6.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,\n                             QHBoxLayout, QButtonGroup, QGroupBox, QDialog,\n                             QLineEdit, QRadioButton, QCheckBox, QWidget,\n                             QMessageBox, QSlider, QTabWidget)\n\nfrom electrum.i18n import _\nfrom electrum.logging import Logger\nfrom electrum.plugin import hook\nfrom electrum.keystore import ScriptTypeNotSupported\nfrom electrum.util import ChoiceItem\n\nfrom electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase\nfrom electrum.hw_wallet.trezor_qt_pinmatrix import PinMatrixWidget\nfrom electrum.hw_wallet.plugin import only_hook_if_libraries_available, OutdatedHwFirmwareException\n\nfrom electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,\n                                  OkButton, CloseButton, PasswordLineEdit, getOpenFileName, ChoiceWidget)\nfrom electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWXPub, WalletWizardComponent\n\nfrom .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings,\n                     PASSPHRASE_ON_DEVICE, Capability, BackupType, RecoveryDeviceInputMethod)\n\nif TYPE_CHECKING:\n    from electrum.gui.qt.wizard.wallet import QENewWalletWizard\n\nPASSPHRASE_HELP_SHORT = _(\n    \"Passphrases allow you to access new wallets, each \"\n    \"hidden behind a particular case-sensitive passphrase.\")\nPASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + \"  \" + _(\n    \"You need to create a separate Electrum wallet for each passphrase \"\n    \"you use as they each generate different addresses.  Changing \"\n    \"your passphrase does not lose other wallets, each is still \"\n    \"accessible behind its own passphrase.\")\nRECOMMEND_PIN = _(\n    \"You should enable PIN protection.  Your PIN is the only protection \"\n    \"for your bitcoins if your device is lost or stolen.\")\nPASSPHRASE_NOT_PIN = _(\n    \"If you forget a passphrase you will be unable to access any \"\n    \"bitcoins in the wallet behind it.  A passphrase is not a PIN. \"\n    \"Only change this if you are sure you understand it.\")\nMATRIX_RECOVERY = _(\n    \"Enter the recovery words by pressing the buttons according to what \"\n    \"the device shows on its display.  You can also use your NUMPAD.\\n\"\n    \"Press BACKSPACE to go back a choice or word.\\n\")\nSEEDLESS_MODE_WARNING = _(\n    \"In seedless mode, the mnemonic seed words are never shown to the user.\\n\"\n    \"There is no backup, and the user has a proof of this.\\n\"\n    \"This is an advanced feature, only suggested to be used in redundant multisig setups.\")\n\n\nclass MatrixDialog(WindowModalDialog):\n\n    def __init__(self, parent):\n        super(MatrixDialog, self).__init__(parent)\n        self.setWindowTitle(_(\"Trezor Matrix Recovery\"))\n        self.num = 9\n        self.loop = QEventLoop()\n\n        vbox = QVBoxLayout(self)\n        vbox.addWidget(WWLabel(MATRIX_RECOVERY))\n\n        grid = QGridLayout()\n        grid.setSpacing(0)\n        self.char_buttons = []\n        for y in range(3):\n            for x in range(3):\n                button = QPushButton('?')\n                button.clicked.connect(partial(self.process_key, ord('1') + y * 3 + x))\n                grid.addWidget(button, 3 - y, x)\n                self.char_buttons.append(button)\n        vbox.addLayout(grid)\n\n        self.backspace_button = QPushButton(\"<=\")\n        self.backspace_button.clicked.connect(partial(self.process_key, Qt.Key.Key_Backspace))\n        self.cancel_button = QPushButton(_(\"Cancel\"))\n        self.cancel_button.clicked.connect(partial(self.process_key, Qt.Key.Key_Escape))\n        buttons = Buttons(self.backspace_button, self.cancel_button)\n        vbox.addSpacing(40)\n        vbox.addLayout(buttons)\n        self.refresh()\n        self.show()\n\n    def refresh(self):\n        for y in range(3):\n            self.char_buttons[3 * y + 1].setEnabled(self.num == 9)\n\n    def is_valid(self, key):\n        return key >= ord('1') and key <= ord('9')\n\n    def process_key(self, key):\n        self.data = None\n        if key == Qt.Key.Key_Backspace:\n            self.data = '\\010'\n        elif key == Qt.Key.Key_Escape:\n            self.data = 'x'\n        elif self.is_valid(key):\n            self.char_buttons[key - ord('1')].setFocus()\n            self.data = '%c' % key\n        if self.data:\n            self.loop.exit(0)\n\n    def keyPressEvent(self, event):\n        self.process_key(event.key())\n        if not self.data:\n            QDialog.keyPressEvent(self, event)\n\n    def get_matrix(self, num):\n        self.num = num\n        self.refresh()\n        self.loop.exec()\n\n\nclass QtHandler(QtHandlerBase):\n\n    pin_signal = pyqtSignal(object, object)\n    matrix_signal = pyqtSignal(object)\n    close_matrix_dialog_signal = pyqtSignal()\n\n    def __init__(self, win, pin_matrix_widget_class, device):\n        super(QtHandler, self).__init__(win, device)\n        self.pin_signal.connect(self.pin_dialog)\n        self.matrix_signal.connect(self.matrix_recovery_dialog)\n        self.close_matrix_dialog_signal.connect(self._close_matrix_dialog)\n        self.pin_matrix_widget_class = pin_matrix_widget_class\n        self.matrix_dialog = None\n        self.passphrase_on_device = False\n\n    def get_pin(self, msg, *, show_strength=True):\n        self.done.clear()\n        self.pin_signal.emit(msg, show_strength)\n        self.done.wait()\n        return self.response\n\n    def get_matrix(self, msg):\n        self.done.clear()\n        self.matrix_signal.emit(msg)\n        self.done.wait()\n        data = self.matrix_dialog.data\n        if data == 'x':\n            self.close_matrix_dialog()\n        return data\n\n    def _close_matrix_dialog(self):\n        if self.matrix_dialog:\n            self.matrix_dialog.accept()\n            self.matrix_dialog = None\n\n    def close_matrix_dialog(self):\n        self.close_matrix_dialog_signal.emit()\n\n    def pin_dialog(self, msg, show_strength):\n        # Needed e.g. when resetting a device\n        self.clear_dialog()\n        dialog = WindowModalDialog(self.top_level_window(), _(\"Enter PIN\"))\n        matrix = self.pin_matrix_widget_class(show_strength)\n        vbox = QVBoxLayout()\n        vbox.addWidget(QLabel(msg))\n        vbox.addWidget(matrix)\n        vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))\n        dialog.setLayout(vbox)\n        dialog.exec()\n        self.response = str(matrix.get_value())\n        self.done.set()\n\n    def matrix_recovery_dialog(self, msg):\n        if not self.matrix_dialog:\n            self.matrix_dialog = MatrixDialog(self.top_level_window())\n        self.matrix_dialog.get_matrix(msg)\n        self.done.set()\n\n    def passphrase_dialog(self, msg, confirm):\n        # If confirm is true, require the user to enter the passphrase twice\n        parent = self.top_level_window()\n        d = WindowModalDialog(parent, _('Enter Passphrase'))\n\n        OK_button = OkButton(d, _('Enter Passphrase'))\n        OnDevice_button = QPushButton(_('Enter Passphrase on Device'))\n\n        new_pw = PasswordLineEdit()\n        conf_pw = PasswordLineEdit()\n\n        vbox = QVBoxLayout()\n        label = QLabel(msg + \"\\n\")\n        label.setWordWrap(True)\n\n        grid = QGridLayout()\n        grid.setSpacing(8)\n        grid.setColumnMinimumWidth(0, 150)\n        grid.setColumnMinimumWidth(1, 100)\n        grid.setColumnStretch(1,1)\n\n        vbox.addWidget(label)\n\n        grid.addWidget(QLabel(_('Passphrase:')), 0, 0)\n        grid.addWidget(new_pw, 0, 1)\n\n        if confirm:\n            grid.addWidget(QLabel(_('Confirm Passphrase:')), 1, 0)\n            grid.addWidget(conf_pw, 1, 1)\n\n        vbox.addLayout(grid)\n\n        def enable_OK():\n            if not confirm:\n                ok = True\n            else:\n                ok = new_pw.text() == conf_pw.text()\n            OK_button.setEnabled(ok)\n\n        new_pw.textChanged.connect(enable_OK)\n        conf_pw.textChanged.connect(enable_OK)\n\n        vbox.addWidget(OK_button)\n\n        if self.passphrase_on_device:\n            vbox.addWidget(OnDevice_button)\n\n        d.setLayout(vbox)\n\n        self.passphrase = None\n\n        def ok_clicked():\n            self.passphrase = new_pw.text()\n\n        def on_device_clicked():\n            self.passphrase = PASSPHRASE_ON_DEVICE\n\n        OK_button.clicked.connect(ok_clicked)\n        OnDevice_button.clicked.connect(on_device_clicked)\n        OnDevice_button.clicked.connect(d.accept)\n\n        d.exec()\n        self.done.set()\n\n\nclass QtPlugin(QtPluginBase):\n    # Derived classes must provide the following class-static variables:\n    #   icon_file\n    #   pin_matrix_widget_class\n\n    @only_hook_if_libraries_available\n    @hook\n    def receive_menu(self, menu, addrs, wallet):\n        if len(addrs) != 1:\n            return\n        self._add_menu_action(menu, addrs[0], wallet)\n\n    @only_hook_if_libraries_available\n    @hook\n    def transaction_dialog_address_menu(self, menu, addr, wallet):\n        self._add_menu_action(menu, addr, wallet)\n\n    def show_settings_dialog(self, window, keystore):\n        def connect():\n            device_id = self.choose_device(window, keystore)\n            return device_id\n        def show_dialog(device_id):\n            if device_id:\n                SettingsDialog(window, self, keystore, device_id).exec()\n        keystore.thread.add(connect, on_success=show_dialog)\n\n\nclass InitSettingsLayout(QVBoxLayout):\n    def __init__(self, devmgr, method, device_id) -> QVBoxLayout:\n        super().__init__()\n\n        client = devmgr.client_by_id(device_id)\n        if not client:\n            raise Exception(_(\"The device was disconnected.\"))\n        model = client.get_trezor_model()\n        fw_version = client.client.version\n        capabilities = client.client.features.capabilities\n        have_shamir = Capability.Shamir in capabilities\n\n        # label\n        label = QLabel(_(\"Enter a label to name your device:\"))\n        self.name = QLineEdit()\n        hl = QHBoxLayout()\n        hl.addWidget(label)\n        hl.addWidget(self.name)\n        hl.addStretch(1)\n        self.addLayout(hl)\n\n        # Backup type\n        gb_backuptype = QGroupBox()\n        hbox_backuptype = QHBoxLayout()\n        gb_backuptype.setLayout(hbox_backuptype)\n        self.addWidget(gb_backuptype)\n        gb_backuptype.setTitle(_('Select backup type:'))\n        self.bg_backuptype = QButtonGroup()\n\n        rb_single = QRadioButton(gb_backuptype)\n        rb_single.setText(_('Single seed (BIP39)'))\n        self.bg_backuptype.addButton(rb_single)\n        self.bg_backuptype.setId(rb_single, BackupType.Bip39)\n        hbox_backuptype.addWidget(rb_single)\n        rb_single.setChecked(True)\n\n        rb_shamir = QRadioButton(gb_backuptype)\n        rb_shamir.setText(_('Shamir'))\n        self.bg_backuptype.addButton(rb_shamir)\n        self.bg_backuptype.setId(rb_shamir, BackupType.Slip39_Basic)\n        hbox_backuptype.addWidget(rb_shamir)\n        rb_shamir.setEnabled(Capability.Shamir in capabilities)\n        rb_shamir.setVisible(False)  # visible with \"expert settings\"\n\n        rb_shamir_groups = QRadioButton(gb_backuptype)\n        rb_shamir_groups.setText(_('Super Shamir'))\n        self.bg_backuptype.addButton(rb_shamir_groups)\n        self.bg_backuptype.setId(rb_shamir_groups, BackupType.Slip39_Advanced)\n        hbox_backuptype.addWidget(rb_shamir_groups)\n        rb_shamir_groups.setEnabled(Capability.ShamirGroups in capabilities)\n        rb_shamir_groups.setVisible(False)  # visible with \"expert settings\"\n\n        # word count\n        word_count_buttons = {}\n\n        gb_numwords = QGroupBox()\n        hbox1 = QHBoxLayout()\n        gb_numwords.setLayout(hbox1)\n        self.addWidget(gb_numwords)\n        gb_numwords.setTitle(_(\"Select seed/share length:\"))\n        self.bg_numwords = QButtonGroup()\n        for count in (12, 18, 20, 24, 33):\n            rb = QRadioButton(gb_numwords)\n            word_count_buttons[count] = rb\n            rb.setText(_(\"{:d} words\").format(count))\n            self.bg_numwords.addButton(rb)\n            self.bg_numwords.setId(rb, count)\n            hbox1.addWidget(rb)\n            rb.setChecked(True)\n\n        def configure_word_counts():\n            if model == \"1\":\n                checked_wordcount = 24\n            else:\n                checked_wordcount = 12\n\n            if method == TIM_RECOVER:\n                if have_shamir:\n                    valid_word_counts = (12, 18, 20, 24, 33)\n                else:\n                    valid_word_counts = (12, 18, 24)\n            elif rb_single.isChecked():\n                valid_word_counts = (12, 18, 24)\n                gb_numwords.setTitle(_('Select seed length:'))\n            else:\n                valid_word_counts = (20, 33)\n                checked_wordcount = 20\n                gb_numwords.setTitle(_('Select share length:'))\n\n            word_count_buttons[checked_wordcount].setChecked(True)\n            for c, btn in word_count_buttons.items():\n                btn.setVisible(c in valid_word_counts)\n\n        self.bg_backuptype.buttonClicked.connect(configure_word_counts)\n        configure_word_counts()\n\n        # set up conditional visibility:\n        # 1. backup_type is only visible when creating new seed\n        gb_backuptype.setVisible(method == TIM_NEW)\n        # 2. word_count is not visible when recovering on TT\n        if method == TIM_RECOVER and model != \"1\":\n            gb_numwords.setVisible(False)\n\n        # PIN\n        self.cb_pin = QCheckBox(_('Enable PIN protection'))\n        self.cb_pin.setChecked(True)\n        self.addWidget(WWLabel(RECOMMEND_PIN))\n        self.addWidget(self.cb_pin)\n\n        # \"expert settings\" button\n        expert_vbox = QVBoxLayout()\n        expert_widget = QWidget()\n        expert_widget.setLayout(expert_vbox)\n        expert_widget.setVisible(False)\n        expert_button = QPushButton(_(\"Show expert settings\"))\n        def show_expert_settings():\n            expert_button.setVisible(False)\n            expert_widget.setVisible(True)\n            rb_shamir.setVisible(True)\n            rb_shamir_groups.setVisible(True)\n        expert_button.clicked.connect(show_expert_settings)\n        self.addWidget(expert_button)\n\n        # passphrase\n        passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT)\n        passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)\n        passphrase_warning.setStyleSheet(\"color: red\")\n        self.cb_phrase = QCheckBox(_('Enable passphrases'))\n        self.cb_phrase.setChecked(False)\n        expert_vbox.addWidget(passphrase_msg)\n        expert_vbox.addWidget(passphrase_warning)\n        expert_vbox.addWidget(self.cb_phrase)\n\n        # ask for recovery type (random word order OR matrix)\n        self.bg_rectype = None\n        if method == TIM_RECOVER and model == '1':\n            gb_rectype = QGroupBox()\n            hbox_rectype = QHBoxLayout()\n            gb_rectype.setLayout(hbox_rectype)\n            expert_vbox.addWidget(gb_rectype)\n            gb_rectype.setTitle(_(\"Select recovery type:\"))\n            self.bg_rectype = QButtonGroup()\n\n            rb1 = QRadioButton(gb_rectype)\n            rb1.setText(_('Scrambled words'))\n            self.bg_rectype.addButton(rb1)\n            self.bg_rectype.setId(rb1, RecoveryDeviceInputMethod.ScrambledWords)\n            hbox_rectype.addWidget(rb1)\n            rb1.setChecked(True)\n\n            rb2 = QRadioButton(gb_rectype)\n            rb2.setText(_('Matrix'))\n            self.bg_rectype.addButton(rb2)\n            self.bg_rectype.setId(rb2, RecoveryDeviceInputMethod.Matrix)\n            hbox_rectype.addWidget(rb2)\n\n        # no backup\n        self.cb_no_backup = None\n        if method == TIM_NEW:\n            self.cb_no_backup = QCheckBox(_('Enable seedless mode'))\n            self.cb_no_backup.setChecked(False)\n            supports_no_backup = False\n            if model == '1':\n                if fw_version >= (1, 7, 1):\n                    supports_no_backup = True\n            else:\n                if fw_version >= (2, 0, 9):\n                    supports_no_backup = True\n            if supports_no_backup:\n                self.cb_no_backup.setEnabled(True)\n                self.cb_no_backup.setToolTip(SEEDLESS_MODE_WARNING)\n            else:\n                self.cb_no_backup.setEnabled(False)\n                self.cb_no_backup.setToolTip(_('Firmware version too old.'))\n            expert_vbox.addWidget(self.cb_no_backup)\n\n        self.addWidget(expert_widget)\n\n    def get_settings(self):\n        return TrezorInitSettings(\n            word_count=self.bg_numwords.checkedId(),\n            label=self.name.text(),\n            pin_enabled=self.cb_pin.isChecked(),\n            passphrase_enabled=self.cb_phrase.isChecked(),\n            recovery_type=self.bg_rectype.checkedId() if self.bg_rectype else None,\n            backup_type=self.bg_backuptype.checkedId(),\n            no_backup=self.cb_no_backup.isChecked() if self.cb_no_backup else False,\n        )\n\n\nclass Plugin(TrezorPlugin, QtPlugin):\n    icon_unpaired = \"trezor_unpaired.png\"\n    icon_paired = \"trezor.png\"\n\n    def create_handler(self, window):\n        return QtHandler(window, self.pin_matrix_widget_class(), self.device)\n\n    @classmethod\n    def pin_matrix_widget_class(self):\n        return PinMatrixWidget\n\n    # insert trezor pages in new wallet wizard\n    def extend_wizard(self, wizard: 'QENewWalletWizard'):\n        super().extend_wizard(wizard)\n        views = {\n            'trezor_start': {'gui': WCScriptAndDerivation},\n            'trezor_xpub': {'gui': WCTrezorXPub},\n            'trezor_not_initialized': {'gui': WCTrezorInitMethod},\n            'trezor_choose_new_recover': {'gui': WCTrezorInitParams},\n            'trezor_do_init': {'gui': WCTrezorInit},\n            'trezor_unlock': {'gui': WCHWUnlock},\n        }\n        wizard.navmap_merge(views)\n\n\nclass SettingsDialog(WindowModalDialog):\n    '''This dialog doesn't require a device be paired with a wallet.\n    We want users to be able to wipe a device even if they've forgotten\n    their PIN.'''\n\n    def __init__(self, window, plugin, keystore, device_id):\n        title = _(\"{} Settings\").format(plugin.device)\n        super(SettingsDialog, self).__init__(window, title)\n        self.setMaximumWidth(540)\n\n        devmgr = plugin.device_manager()\n        config = devmgr.config\n        handler = keystore.handler\n        thread = keystore.thread\n        hs_cols, hs_rows = (128, 64)\n\n        def invoke_client(method, *args, **kw_args):\n            unpair_after = kw_args.pop('unpair_after', False)\n\n            def task():\n                client = devmgr.client_by_id(device_id)\n                if not client:\n                    raise RuntimeError(\"Device not connected\")\n                if method:\n                    getattr(client, method)(*args, **kw_args)\n                if unpair_after:\n                    devmgr.unpair_id(device_id)\n                return client.features\n\n            thread.add(task, on_success=update)\n\n        def update(features):\n            self.features = features\n            set_label_enabled()\n            if features.bootloader_hash:\n                bl_hash = features.bootloader_hash.hex()\n                bl_hash = \"\\n\".join([bl_hash[:32], bl_hash[32:]])\n            else:\n                bl_hash = \"N/A\"\n            noyes = [_(\"No\"), _(\"Yes\")]\n            endis = [_(\"Enable Passphrases\"), _(\"Disable Passphrases\")]\n            disen = [_(\"Disabled\"), _(\"Enabled\")]\n            setchange = [_(\"Set a PIN\"), _(\"Change PIN\")]\n\n            version = \"%d.%d.%d\" % (features.major_version,\n                                    features.minor_version,\n                                    features.patch_version)\n\n            device_label.setText(features.label)\n            pin_set_label.setText(noyes[features.pin_protection])\n            passphrases_label.setText(disen[features.passphrase_protection])\n            bl_hash_label.setText(bl_hash)\n            label_edit.setText(features.label)\n            device_id_label.setText(features.device_id)\n            initialized_label.setText(noyes[features.initialized])\n            version_label.setText(version)\n            clear_pin_button.setVisible(features.pin_protection)\n            clear_pin_warning.setVisible(features.pin_protection)\n            pin_button.setText(setchange[features.pin_protection])\n            pin_msg.setVisible(not features.pin_protection)\n            passphrase_button.setText(endis[features.passphrase_protection])\n            language_label.setText(features.language)\n\n        def set_label_enabled():\n            label_apply.setEnabled(label_edit.text() != self.features.label)\n\n        def rename():\n            invoke_client('change_label', label_edit.text())\n\n        def toggle_passphrase():\n            title = _(\"Confirm Toggle Passphrase Protection\")\n            currently_enabled = self.features.passphrase_protection\n            if currently_enabled:\n                msg = _(\"After disabling passphrases, you can only pair this \"\n                        \"Electrum wallet if it had an empty passphrase.  \"\n                        \"If its passphrase was not empty, you will need to \"\n                        \"create a new wallet.  You can use this wallet again \"\n                        \"at any time by re-enabling passphrases and entering \"\n                        \"its passphrase.\")\n            else:\n                msg = _(\"Your current Electrum wallet can only be used with \"\n                        \"an empty passphrase.  You must create a separate \"\n                        \"wallet for other passphrases as each one generates \"\n                        \"a new set of addresses.\")\n            msg += \"\\n\\n\" + _(\"Are you sure you want to proceed?\")\n            if not self.question(msg, title=title):\n                return\n            invoke_client('toggle_passphrase', unpair_after=currently_enabled)\n\n        def change_homescreen():\n            filename = getOpenFileName(\n                parent=self,\n                title=_(\"Choose Homescreen\"),\n                config=config,\n            )\n            if not filename:\n                return  # user cancelled\n\n            if filename.endswith('.toif'):\n                img = open(filename, 'rb').read()\n                if img[:8] != b'TOIf\\x90\\x00\\x90\\x00':\n                    handler.show_error('File is not a TOIF file with size of 144x144')\n                    return\n            else:\n                from PIL import Image # FIXME\n                im = Image.open(filename)\n                if im.size != (128, 64):\n                    handler.show_error('Image must be 128 x 64 pixels')\n                    return\n                im = im.convert('1')\n                pix = im.load()\n                img = bytearray(1024)\n                for j in range(64):\n                    for i in range(128):\n                        if pix[i, j]:\n                            o = (i + j * 128)\n                            img[o // 8] |= (1 << (7 - o % 8))\n                img = bytes(img)\n            invoke_client('change_homescreen', img)\n\n        def clear_homescreen():\n            invoke_client('change_homescreen', b'\\x00')\n\n        def set_pin():\n            invoke_client('set_pin', remove=False)\n\n        def clear_pin():\n            invoke_client('set_pin', remove=True)\n\n        def wipe_device():\n            wallet = window.wallet\n            if wallet and sum(wallet.get_balance()):\n                title = _(\"Confirm Device Wipe\")\n                msg = _(\"Are you SURE you want to wipe the device?\\n\"\n                        \"Your wallet still has bitcoins in it!\")\n                if not self.question(msg, title=title,\n                                     icon=QMessageBox.Icon.Critical):\n                    return\n            invoke_client('wipe_device', unpair_after=True)\n\n        def slider_moved():\n            mins = timeout_slider.sliderPosition()\n            timeout_minutes.setText(_(\"{:2d} minutes\").format(mins))\n\n        def slider_released():\n            config.set_session_timeout(timeout_slider.sliderPosition() * 60)\n\n        # Information tab\n        info_tab = QWidget()\n        info_layout = QVBoxLayout(info_tab)\n        info_glayout = QGridLayout()\n        info_glayout.setColumnStretch(2, 1)\n        device_label = QLabel()\n        pin_set_label = QLabel()\n        passphrases_label = QLabel()\n        version_label = QLabel()\n        device_id_label = QLabel()\n        bl_hash_label = QLabel()\n        bl_hash_label.setWordWrap(True)\n        language_label = QLabel()\n        initialized_label = QLabel()\n        rows = [\n            (_(\"Device Label\"), device_label),\n            (_(\"PIN set\"), pin_set_label),\n            (_(\"Passphrases\"), passphrases_label),\n            (_(\"Firmware Version\"), version_label),\n            (_(\"Device ID\"), device_id_label),\n            (_(\"Bootloader Hash\"), bl_hash_label),\n            (_(\"Language\"), language_label),\n            (_(\"Initialized\"), initialized_label),\n        ]\n        for row_num, (label, widget) in enumerate(rows):\n            info_glayout.addWidget(QLabel(label), row_num, 0)\n            info_glayout.addWidget(widget, row_num, 1)\n        info_layout.addLayout(info_glayout)\n\n        # Settings tab\n        settings_tab = QWidget()\n        settings_layout = QVBoxLayout(settings_tab)\n        settings_glayout = QGridLayout()\n\n        # Settings tab - Label\n        label_msg = QLabel(_(\"Name this {}.  If you have multiple devices \"\n                             \"their labels help distinguish them.\")\n                           .format(plugin.device))\n        label_msg.setWordWrap(True)\n        label_label = QLabel(_(\"Device Label\"))\n        label_edit = QLineEdit()\n        label_edit.setMinimumWidth(150)\n        label_edit.setMaxLength(plugin.MAX_LABEL_LEN)\n        label_apply = QPushButton(_(\"Apply\"))\n        label_apply.clicked.connect(rename)\n        label_edit.textChanged.connect(set_label_enabled)\n        settings_glayout.addWidget(label_label, 0, 0)\n        settings_glayout.addWidget(label_edit, 0, 1, 1, 2)\n        settings_glayout.addWidget(label_apply, 0, 3)\n        settings_glayout.addWidget(label_msg, 1, 1, 1, -1)\n\n        # Settings tab - PIN\n        pin_label = QLabel(_(\"PIN Protection\"))\n        pin_button = QPushButton()\n        pin_button.clicked.connect(set_pin)\n        settings_glayout.addWidget(pin_label, 2, 0)\n        settings_glayout.addWidget(pin_button, 2, 1)\n        pin_msg = QLabel(_(\"PIN protection is strongly recommended.  \"\n                           \"A PIN is your only protection against someone \"\n                           \"stealing your bitcoins if they obtain physical \"\n                           \"access to your {}.\").format(plugin.device))\n        pin_msg.setWordWrap(True)\n        pin_msg.setStyleSheet(\"color: red\")\n        settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)\n\n        # Settings tab - Homescreen\n        homescreen_label = QLabel(_(\"Homescreen\"))\n        homescreen_change_button = QPushButton(_(\"Change...\"))\n        homescreen_clear_button = QPushButton(_(\"Reset\"))\n        homescreen_change_button.clicked.connect(change_homescreen)\n        try:\n            import PIL\n        except ImportError:\n            homescreen_change_button.setDisabled(True)\n            homescreen_change_button.setToolTip(\n                _(\"Required package 'PIL' is not available - Please install it or use the Trezor website instead.\")\n            )\n        homescreen_clear_button.clicked.connect(clear_homescreen)\n        homescreen_msg = QLabel(_(\"You can set the homescreen on your \"\n                                  \"device to personalize it.  You must \"\n                                  \"choose a {} x {} monochrome black and \"\n                                  \"white image.\").format(hs_cols, hs_rows))\n        homescreen_msg.setWordWrap(True)\n        settings_glayout.addWidget(homescreen_label, 4, 0)\n        settings_glayout.addWidget(homescreen_change_button, 4, 1)\n        settings_glayout.addWidget(homescreen_clear_button, 4, 2)\n        settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1)\n\n        # Settings tab - Session Timeout\n        timeout_label = QLabel(_(\"Session Timeout\"))\n        timeout_minutes = QLabel()\n        timeout_slider = QSlider(Qt.Orientation.Horizontal)\n        timeout_slider.setRange(1, 60)\n        timeout_slider.setSingleStep(1)\n        timeout_slider.setTickInterval(5)\n        timeout_slider.setTickPosition(QSlider.TickPosition.TicksBelow)\n        timeout_slider.setTracking(True)\n        timeout_msg = QLabel(\n            _(\"Clear the session after the specified period \"\n              \"of inactivity.  Once a session has timed out, \"\n              \"your PIN and passphrase (if enabled) must be \"\n              \"re-entered to use the device.\"))\n        timeout_msg.setWordWrap(True)\n        timeout_slider.setSliderPosition(config.get_session_timeout() // 60)\n        slider_moved()\n        timeout_slider.valueChanged.connect(slider_moved)\n        timeout_slider.sliderReleased.connect(slider_released)\n        settings_glayout.addWidget(timeout_label, 6, 0)\n        settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)\n        settings_glayout.addWidget(timeout_minutes, 6, 4)\n        settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)\n        settings_layout.addLayout(settings_glayout)\n        settings_layout.addStretch(1)\n\n        # Advanced tab\n        advanced_tab = QWidget()\n        advanced_layout = QVBoxLayout(advanced_tab)\n        advanced_glayout = QGridLayout()\n\n        # Advanced tab - clear PIN\n        clear_pin_button = QPushButton(_(\"Disable PIN\"))\n        clear_pin_button.clicked.connect(clear_pin)\n        clear_pin_warning = QLabel(\n            _(\"If you disable your PIN, anyone with physical access to your \"\n              \"{} device can spend your bitcoins.\").format(plugin.device))\n        clear_pin_warning.setWordWrap(True)\n        clear_pin_warning.setStyleSheet(\"color: red\")\n        advanced_glayout.addWidget(clear_pin_button, 0, 2)\n        advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5)\n\n        # Advanced tab - toggle passphrase protection\n        passphrase_button = QPushButton()\n        passphrase_button.clicked.connect(toggle_passphrase)\n        passphrase_msg = WWLabel(PASSPHRASE_HELP)\n        passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)\n        passphrase_warning.setStyleSheet(\"color: red\")\n        advanced_glayout.addWidget(passphrase_button, 3, 2)\n        advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5)\n        advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5)\n\n        # Advanced tab - wipe device\n        wipe_device_button = QPushButton(_(\"Wipe Device\"))\n        wipe_device_button.clicked.connect(wipe_device)\n        wipe_device_msg = QLabel(\n            _(\"Wipe the device, removing all data from it.  The firmware \"\n              \"is left unchanged.\"))\n        wipe_device_msg.setWordWrap(True)\n        wipe_device_warning = QLabel(\n            _(\"Only wipe a device if you have the recovery seed written down \"\n              \"and the device wallet(s) are empty, otherwise the bitcoins \"\n              \"will be lost forever.\"))\n        wipe_device_warning.setWordWrap(True)\n        wipe_device_warning.setStyleSheet(\"color: red\")\n        advanced_glayout.addWidget(wipe_device_button, 6, 2)\n        advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5)\n        advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5)\n        advanced_layout.addLayout(advanced_glayout)\n        advanced_layout.addStretch(1)\n\n        tabs = QTabWidget(self)\n        tabs.addTab(info_tab, _(\"Information\"))\n        tabs.addTab(settings_tab, _(\"Settings\"))\n        tabs.addTab(advanced_tab, _(\"Advanced\"))\n        dialog_vbox = QVBoxLayout(self)\n        dialog_vbox.addWidget(tabs)\n        dialog_vbox.addLayout(Buttons(CloseButton(self)))\n\n        # Update information\n        invoke_client(None)\n\n\nclass WCTrezorXPub(WCHWXPub):\n    def __init__(self, parent, wizard):\n        WCHWXPub.__init__(self, parent, wizard)\n\n    def get_xpub_from_client(self, client, derivation, xtype):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = current_cosigner['hardware_device']\n        if xtype not in self.plugin.SUPPORTED_XTYPES:\n            raise ScriptTypeNotSupported(_('This type of script is not supported with {}').format(_info.model_name))\n        if not client.is_uptodate():\n            msg = (_('Outdated {} firmware for device labelled {}. Please '\n                     'download the updated firmware from {}')\n                     .format(_info.model_name, _info.label, self.plugin.firmware_URL))\n            raise OutdatedHwFirmwareException(msg)\n        return client.get_xpub(derivation, xtype, True)\n\n\nclass WCTrezorInitMethod(WalletWizardComponent, Logger):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Trezor Setup'))\n        Logger.__init__(self)\n        self.plugins = wizard.plugins\n        self.plugin = None\n\n    def on_ready(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = current_cosigner['hardware_device']\n        self.plugin = self.plugins.get_plugin(_info.plugin_name)\n        device_id = _info.device.id_\n        client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)\n        if client.features.bootloader_mode:\n            msg = (_(\"Looks like your device is in bootloader mode. Try reconnecting it.\\n\"\n                     \"If you haven't installed a firmware on it yet, you can download it from {}\")\n                   .format(self.plugin.firmware_URL))\n            self.error = msg\n            return\n        if not client.is_uptodate():\n            msg = (_('Outdated {} firmware for device labelled {}. Please '\n                     'download the updated firmware from {}')\n                     .format(_info.model_name, _info.label, self.plugin.firmware_URL))\n            self.error = msg\n            return\n\n        message = _('Choose how you want to initialize your {}.').format(_info.model_name)\n        choices = [\n            # Must be short as QT doesn't word-wrap radio button text\n            ChoiceItem(key=TIM_NEW, label=_(\"Let the device generate a completely new seed randomly\")),\n            ChoiceItem(key=TIM_RECOVER, label=_(\"Recover from a seed you have previously written down\")),\n        ]\n        self.choice_w = ChoiceWidget(message=message, choices=choices)\n        self.layout().addWidget(self.choice_w)\n        self.layout().addStretch(1)\n\n        self._valid = True\n\n    def apply(self):\n        if not self.valid:\n            return\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        current_cosigner['trezor_init'] = self.choice_w.selected_key\n\n\nclass WCTrezorInitParams(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Trezor Setup'))\n        self.plugins = wizard.plugins\n        self._busy = True\n\n    def on_ready(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        _name, _info = current_cosigner['hardware_device']\n        self.settings_layout = InitSettingsLayout(self.plugins.device_manager, current_cosigner['trezor_init'], _info.device.id_)\n        self.layout().addLayout(self.settings_layout)\n        self.layout().addStretch(1)\n\n        self.valid = True\n        self.busy = False\n\n    def apply(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        current_cosigner['trezor_settings'] = self.settings_layout.get_settings()\n\n\nclass WCTrezorInit(WalletWizardComponent, Logger):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Trezor Setup'))\n        Logger.__init__(self)\n        self.plugins = wizard.plugins\n        self.plugin = self.plugins.get_plugin('trezor')\n\n        self.layout().addWidget(WWLabel('Done'))\n\n        self._busy = True\n\n    def on_ready(self):\n        current_cosigner = self.wizard.current_cosigner(self.wizard_data)\n        settings = current_cosigner['trezor_settings']\n        method = current_cosigner['trezor_init']\n        _name, _info = current_cosigner['hardware_device']\n        device_id = _info.device.id_\n        client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)\n        client.handler = self.plugin.create_handler(self.wizard)\n\n        def initialize_device_task(settings, method, device_id, handler):\n            try:\n                self.plugin._initialize_device(settings, method, device_id, handler)\n                self.logger.info('Done initialize device')\n                self.valid = True\n                self.wizard.requestNext.emit()  # triggers Next GUI thread from event loop\n            except Exception as e:\n                self.valid = False\n                self.error = repr(e)\n                self.logger.exception(repr(e))\n            finally:\n                self.busy = False\n\n        t = threading.Thread(\n            target=initialize_device_task,\n            args=(settings, method, device_id, client.handler),\n            daemon=True)\n        t.start()\n\n    def apply(self):\n        pass\n"
  },
  {
    "path": "electrum/plugins/trezor/trezor.py",
    "content": "from typing import NamedTuple, Any, Optional, TYPE_CHECKING, Sequence\n\nfrom electrum.util import bfh, UserCancelled, UserFacingException\nfrom electrum.bip32 import BIP32Node\nfrom electrum import descriptor\nfrom electrum import constants\nfrom electrum.i18n import _\nfrom electrum.plugin import Device, runs_in_hwd_thread\nfrom electrum.transaction import Transaction, PartialTransaction, PartialTxInput, Sighash\nfrom electrum.keystore import Hardware_KeyStore\nfrom electrum.logging import get_logger\n\nfrom electrum.hw_wallet import HW_PluginBase\nfrom electrum.hw_wallet.plugin import is_any_tx_output_on_change_branch, \\\n    trezor_validate_op_return_output_and_get_data, LibraryFoundButUnusable, OutdatedHwFirmwareException\n\nif TYPE_CHECKING:\n    from electrum.plugin import DeviceInfo\n    from electrum.wizard import NewWalletWizard\n\n_logger = get_logger(__name__)\n\n\ntry:\n    import trezorlib\n    import trezorlib.transport\n    from trezorlib.transport.bridge import BridgeTransport, call_bridge\n\n    from .clientbase import TrezorClientBase, RecoveryDeviceInputMethod\n\n    from trezorlib.messages import (\n        Capability, BackupType, HDNodeType, HDNodePathType,\n        InputScriptType, OutputScriptType, MultisigRedeemScriptType,\n        TxInputType, TxOutputType, TxOutputBinType, TransactionType, AmountUnit)\n\n    from trezorlib.client import PASSPHRASE_ON_DEVICE\n    import trezorlib.log\n    #trezorlib.log.enable_debug_output()\n\n    TREZORLIB = True\nexcept Exception as e:\n    if not (isinstance(e, ModuleNotFoundError) and e.name == 'trezorlib'):\n        _logger.exception('error importing trezor plugin deps')\n    TREZORLIB = False\n\n    class _EnumMissing:\n        def __init__(self):\n            self.counter = 0\n            self.values = {}\n\n        def __getattr__(self, key):\n            if key not in self.values:\n                self.values[key] = self.counter\n                self.counter += 1\n            return self.values[key]\n\n    Capability = _EnumMissing()\n    BackupType = _EnumMissing()\n    RecoveryDeviceInputMethod = _EnumMissing()\n    AmountUnit = _EnumMissing()\n\n    PASSPHRASE_ON_DEVICE = object()\n\n\n# Trezor initialization methods\nTIM_NEW, TIM_RECOVER = range(2)\n\nTREZOR_PRODUCT_KEY = 'Trezor'\n\n\nclass TrezorKeyStore(Hardware_KeyStore):\n    hw_type = 'trezor'\n    device = TREZOR_PRODUCT_KEY\n\n    plugin: 'TrezorPlugin'\n\n    def decrypt_message(self, sequence, message, password):\n        raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device))\n\n    def sign_message(self, sequence, message, password, *, script_type=None):\n        client = self.get_client()\n        address_path = self.get_derivation_prefix() + \"/%d/%d\"%sequence\n        script_type = self.plugin.get_trezor_input_script_type(script_type)\n        msg_sig = client.sign_message(address_path, message, script_type=script_type)\n        return msg_sig.signature\n\n    def sign_transaction(self, tx, password):\n        if tx.is_complete():\n            return\n        # previous transactions used as inputs\n        prev_tx = {}\n        for txin in tx.inputs():\n            tx_hash = txin.prevout.txid.hex()\n            if txin.utxo is None:\n                raise UserFacingException(_('Missing previous tx.'))\n            prev_tx[tx_hash] = txin.utxo\n\n        self.plugin.sign_transaction(self, tx, prev_tx)\n\n    def has_support_for_slip_19_ownership_proofs(self) -> bool:\n        return True\n\n    def add_slip_19_ownership_proofs_to_tx(self, tx: 'PartialTransaction', password) -> None:\n        assert isinstance(tx, PartialTransaction)\n        client = self.get_client()\n        assert isinstance(client, TrezorClientBase), client\n        for txin in tx.inputs():\n            if txin.is_coinbase_input():\n                continue\n            # note: we add proofs even for txin.is_complete() inputs.\n            if not txin.is_mine:\n                continue\n            assert txin.scriptpubkey\n            desc = txin.script_descriptor\n            assert desc\n            trezor_multisig = None\n            if multi := desc.get_simple_multisig():\n                # trezor_multisig = self._make_multisig(multi)\n                raise Exception(\"multisig not supported for slip-19 ownership proof\")\n            trezor_script_type = self.plugin.get_trezor_input_script_type(desc.to_legacy_electrum_script_type())\n            my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin)\n            if full_path:\n                trezor_address_n = full_path\n            else:\n                continue\n            proof, _proof_sig = client.get_ownership_proof(\n                coin_name=self.plugin.get_coin_name(),\n                n=trezor_address_n,\n                multisig=trezor_multisig,\n                script_type=trezor_script_type,\n            )\n            txin.slip_19_ownership_proof = proof\n\n\nclass TrezorInitSettings(NamedTuple):\n    word_count: int\n    label: str\n    pin_enabled: bool\n    passphrase_enabled: bool\n    recovery_type: Any = None\n    backup_type: int = BackupType.Bip39\n    no_backup: bool = False\n\n\nclass TrezorPlugin(HW_PluginBase):\n    # Derived classes provide:\n    #\n    #  class-static variables: client_class, firmware_URL, handler_class,\n    #     libraries_available, libraries_URL, minimum_firmware,\n    #     wallet_class, types\n\n    firmware_URL = 'https://wallet.trezor.io'\n    libraries_URL = 'https://pypi.org/project/trezor/'\n    minimum_firmware = (1, 5, 2)\n    keystore_class = TrezorKeyStore\n    minimum_library = (0, 13, 0)\n    maximum_library = (0, 14)\n    SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')\n    DEVICE_IDS = (TREZOR_PRODUCT_KEY,)\n\n    MAX_LABEL_LEN = 32\n\n    def __init__(self, parent, config, name):\n        super().__init__(parent, config, name)\n\n        self.libraries_available = self.check_libraries_available()\n        if not self.libraries_available:\n            return\n        self.device_manager().register_enumerate_func(self.enumerate)\n        self._is_bridge_available = None\n\n    def get_library_version(self):\n        import trezorlib\n        try:\n            version = trezorlib.__version__\n        except Exception:\n            version = 'unknown'\n        if TREZORLIB:\n            return version\n        else:\n            raise LibraryFoundButUnusable(library_version=version)\n\n    @runs_in_hwd_thread\n    def is_bridge_available(self) -> bool:\n        # Testing whether the Bridge is available can take several seconds\n        # (when it is not), as it is slow to timeout, hence we cache it.\n        if self._is_bridge_available is None:\n            try:\n                call_bridge(\"enumerate\")\n            except Exception:\n                self._is_bridge_available = False\n                # never again try with Bridge due to slow timeout\n                BridgeTransport.ENABLED = False\n            else:\n                self._is_bridge_available = True\n        return self._is_bridge_available\n\n    @runs_in_hwd_thread\n    def enumerate(self):\n        # Set lower timeout for UDP enumeration (used for emulator).\n        # The default of 10 sec is very long, and I often hit it for some reason on Windows (no emu running),\n        # blocking the whole enumeration.\n        from trezorlib.transport.udp import UdpTransport\n        trezorlib.transport.udp.SOCKET_TIMEOUT = 1\n        # If there is a bridge, prefer that.\n        # On Windows, the bridge runs as Admin (and Electrum usually does not),\n        # so the bridge has better chances of finding devices. see #5420\n        # This also avoids duplicate entries.\n        if self.is_bridge_available():\n            devices = BridgeTransport.enumerate()\n        else:\n            devices = trezorlib.transport.enumerate_devices()\n        return [Device(path=d.get_path(),\n                       interface_number=-1,\n                       id_=d.get_path(),\n                       product_key=TREZOR_PRODUCT_KEY,\n                       usage_page=0,\n                       transport_ui_string=d.get_path())\n                for d in devices]\n\n    @runs_in_hwd_thread\n    def create_client(self, device, handler):\n        try:\n            self.logger.info(f\"connecting to device at {device.path}\")\n            transport = trezorlib.transport.get_transport(device.path)\n        except BaseException as e:\n            self.logger.info(f\"cannot connect at {device.path} {e}\")\n            return None\n\n        if not transport:\n            self.logger.info(f\"cannot connect at {device.path}\")\n            return\n\n        self.logger.info(f\"connected to device at {device.path}\")\n        # note that this call can still raise!\n        return TrezorClientBase(transport, handler, self)\n\n    @runs_in_hwd_thread\n    def get_client(self, keystore, force_pair=True, *,\n                   devices=None, allow_user_interaction=True) -> Optional['TrezorClientBase']:\n        client = super().get_client(keystore, force_pair,\n                                    devices=devices,\n                                    allow_user_interaction=allow_user_interaction)\n        # returns the client for a given keystore. can use xpub\n        if client:\n            client.used()\n        return client\n\n    def get_coin_name(self):\n        return \"Testnet\" if constants.net.TESTNET else \"Bitcoin\"\n\n    @runs_in_hwd_thread\n    def _initialize_device(self, settings: TrezorInitSettings, method, device_id, handler):\n        if method == TIM_RECOVER and settings.recovery_type == RecoveryDeviceInputMethod.ScrambledWords:\n            handler.show_error(_(\n                \"You will be asked to enter 24 words regardless of your \"\n                \"seed's actual length.  If you enter a word incorrectly or \"\n                \"misspell it, you cannot change it or go back - you will need \"\n                \"to start again from the beginning.\\n\\nSo please enter \"\n                \"the words carefully!\"),\n                blocking=True)\n\n        devmgr = self.device_manager()\n        client = devmgr.client_by_id(device_id)\n        if not client:\n            raise Exception(_(\"The device was disconnected.\"))\n\n        if method == TIM_NEW:\n            strength_from_word_count = {12: 128, 18: 192, 20: 128, 24: 256, 33: 256}\n            client.reset_device(\n                strength=strength_from_word_count[settings.word_count],\n                passphrase_protection=settings.passphrase_enabled,\n                pin_protection=settings.pin_enabled,\n                label=settings.label,\n                backup_type=settings.backup_type,\n                no_backup=settings.no_backup)\n        elif method == TIM_RECOVER:\n            client.recover_device(\n                recovery_type=settings.recovery_type,\n                word_count=settings.word_count,\n                passphrase_protection=settings.passphrase_enabled,\n                pin_protection=settings.pin_enabled,\n                label=settings.label)\n            if settings.recovery_type == RecoveryDeviceInputMethod.Matrix:\n                handler.close_matrix_dialog()\n        else:\n            raise RuntimeError(\"Unsupported recovery method\")\n\n    def _make_node_path(self, xpub: str, address_n: Sequence[int]):\n        bip32node = BIP32Node.from_xkey(xpub)\n        node = HDNodeType(\n            depth=bip32node.depth,\n            fingerprint=int.from_bytes(bip32node.fingerprint, 'big'),\n            child_num=int.from_bytes(bip32node.child_number, 'big'),\n            chain_code=bip32node.chaincode,\n            public_key=bip32node.eckey.get_public_key_bytes(compressed=True),\n        )\n        return HDNodePathType(node=node, address_n=address_n)\n\n    def get_trezor_input_script_type(self, electrum_txin_type: str):\n        if electrum_txin_type in ('p2wpkh', 'p2wsh'):\n            return InputScriptType.SPENDWITNESS\n        if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'):\n            return InputScriptType.SPENDP2SHWITNESS\n        if electrum_txin_type in ('p2pkh',):\n            return InputScriptType.SPENDADDRESS\n        if electrum_txin_type in ('p2sh',):\n            return InputScriptType.SPENDMULTISIG\n        if electrum_txin_type in ('p2tr',):\n            return InputScriptType.SPENDTAPROOT\n        raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))\n\n    def get_trezor_output_script_type(self, electrum_txin_type: str):\n        if electrum_txin_type in ('p2wpkh', 'p2wsh'):\n            return OutputScriptType.PAYTOWITNESS\n        if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'):\n            return OutputScriptType.PAYTOP2SHWITNESS\n        if electrum_txin_type in ('p2pkh',):\n            return OutputScriptType.PAYTOADDRESS\n        if electrum_txin_type in ('p2sh',):\n            return OutputScriptType.PAYTOMULTISIG\n        if electrum_txin_type in ('p2tr',):\n            return OutputScriptType.PAYTOTAPROOT\n        raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))\n\n    def get_trezor_amount_unit(self):\n        if self.config.BTC_AMOUNTS_DECIMAL_POINT == 0:\n            return AmountUnit.SATOSHI\n        elif self.config.BTC_AMOUNTS_DECIMAL_POINT == 2:\n            return AmountUnit.MICROBITCOIN\n        elif self.config.BTC_AMOUNTS_DECIMAL_POINT == 5:\n            return AmountUnit.MILLIBITCOIN\n        else:\n            return AmountUnit.BITCOIN\n\n    @runs_in_hwd_thread\n    def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):\n        prev_tx = {bfh(txhash): self.electrum_tx_to_txtype(tx) for txhash, tx in prev_tx.items()}\n        client = self.get_client(keystore)\n        inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)\n        outputs = self.tx_outputs(tx, keystore=keystore, firmware_version=client.client.version)\n        signatures, _ = client.sign_tx(self.get_coin_name(),\n                                       inputs, outputs,\n                                       lock_time=tx.locktime,\n                                       version=tx.version,\n                                       amount_unit=self.get_trezor_amount_unit(),\n                                       serialize=False,\n                                       prev_txes=prev_tx)\n        sighash = Sighash.to_sigbytes(Sighash.ALL)\n        signatures = [((sig + sighash) if sig else None) for sig in signatures]\n        tx.update_signatures(signatures)\n\n    @runs_in_hwd_thread\n    def show_address(self, wallet, address, keystore=None):\n        if keystore is None:\n            keystore = wallet.get_keystore()\n        if not self.show_address_helper(wallet, address, keystore):\n            return\n        deriv_suffix = wallet.get_address_index(address)\n        derivation = keystore.get_derivation_prefix()\n        address_path = \"%s/%d/%d\"%(derivation, *deriv_suffix)\n        script_type = self.get_trezor_input_script_type(wallet.txin_type)\n\n        # prepare multisig, if available:\n        desc = wallet.get_script_descriptor_for_address(address)\n        if multi := desc.get_simple_multisig():\n            multisig = self._make_multisig(multi)\n        else:\n            multisig = None\n\n        client = self.get_client(keystore)\n        client.show_address(address_path, script_type, multisig)\n\n    def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'TrezorKeyStore' = None):\n        inputs = []\n        for txin in tx.inputs():\n            if txin.is_coinbase_input():\n                txinputtype = TxInputType(\n                    prev_hash=b\"\\x00\"*32,\n                    prev_index=0xffffffff,  # signed int -1\n                )\n            else:\n                txinputtype = TxInputType(\n                    prev_hash=txin.prevout.txid,\n                    prev_index=txin.prevout.out_idx,\n                )\n                if for_sig:\n                    assert isinstance(tx, PartialTransaction)\n                    assert isinstance(txin, PartialTxInput)\n                    assert keystore\n                    if txin.is_complete() or not txin.is_mine:  # we don't sign\n                        txinputtype.script_type = InputScriptType.EXTERNAL\n                        assert txin.scriptpubkey\n                        txinputtype.script_pubkey = txin.scriptpubkey\n                        # note: we add the ownership proof, if present, regardless of txin.is_complete().\n                        #       The \"Trezor One\" model always requires it for external inputs. (see #8910)\n                        if not txin.is_mine and txin.slip_19_ownership_proof:\n                            txinputtype.ownership_proof = txin.slip_19_ownership_proof\n                    else:  # we sign\n                        desc = txin.script_descriptor\n                        assert desc\n                        if multi := desc.get_simple_multisig():\n                            txinputtype.multisig = self._make_multisig(multi)\n                        txinputtype.script_type = self.get_trezor_input_script_type(desc.to_legacy_electrum_script_type())\n                        my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)\n                        if full_path:\n                            txinputtype.address_n = full_path\n                    # Add witness if any. This is useful when signing a tx (for_sig=True)\n                    # that has some already pre-signed external inputs.\n                    txinputtype.witness = txin.witness\n\n            txinputtype.amount = txin.value_sats()\n            txinputtype.script_sig = txin.script_sig\n            txinputtype.sequence = txin.nsequence\n\n            inputs.append(txinputtype)\n\n        return inputs\n\n    def _make_multisig(self, desc: descriptor.MultisigDescriptor):\n        pubkeys = []\n        for pubkey_provider in desc.pubkeys:\n            assert not pubkey_provider.is_range()\n            assert pubkey_provider.extkey is not None\n            xpub = pubkey_provider.pubkey\n            der_suffix = pubkey_provider.get_der_suffix_int_list()\n            pubkeys.append(self._make_node_path(xpub, der_suffix))\n        return MultisigRedeemScriptType(\n            pubkeys=pubkeys,\n            signatures=[b''] * len(pubkeys),\n            m=desc.thresh)\n\n    def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore', firmware_version: Sequence[int]):\n\n        def create_output_by_derivation():\n            desc = txout.script_descriptor\n            assert desc\n            script_type = self.get_trezor_output_script_type(desc.to_legacy_electrum_script_type())\n            if multi := desc.get_simple_multisig():\n                multisig = self._make_multisig(multi)\n            else:\n                multisig = None\n            my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)\n            assert full_path\n            txoutputtype = TxOutputType(\n                multisig=multisig,\n                amount=txout.value,\n                address_n=full_path,\n                script_type=script_type)\n            return txoutputtype\n\n        def create_output_by_address():\n            if address:\n                return TxOutputType(\n                    amount=txout.value,\n                    script_type=OutputScriptType.PAYTOADDRESS,\n                    address=address,\n                )\n            else:\n                return TxOutputType(\n                    amount=txout.value,\n                    script_type=OutputScriptType.PAYTOOPRETURN,\n                    op_return_data=trezor_validate_op_return_output_and_get_data(txout),\n                )\n\n        outputs = []\n        has_change = False\n        any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)\n\n        for txout in tx.outputs():\n            address = txout.address\n            use_create_by_derivation = False\n\n            if txout.is_mine:\n                if tuple(firmware_version) >= (1, 6, 1):\n                    use_create_by_derivation = True\n                else:\n                    if not has_change:\n                        # prioritise hiding outputs on the 'change' branch from user\n                        # because no more than one change address allowed\n                        # note: ^ restriction can be removed once we require fw 1.6.1\n                        # that has https://github.com/trezor/trezor-mcu/pull/306\n                        if txout.is_change == any_output_on_change_branch:\n                            use_create_by_derivation = True\n                            has_change = True\n\n            if use_create_by_derivation:\n                txoutputtype = create_output_by_derivation()\n            else:\n                txoutputtype = create_output_by_address()\n            outputs.append(txoutputtype)\n\n        return outputs\n\n    def electrum_tx_to_txtype(self, tx: Optional[Transaction]):\n        t = TransactionType()\n        if tx is None:\n            # probably for segwit input and we don't need this prev txn\n            return t\n        tx.deserialize()\n        t.version = tx.version\n        t.lock_time = tx.locktime\n        t.inputs = self.tx_inputs(tx)\n        t.bin_outputs = [\n            TxOutputBinType(amount=o.value, script_pubkey=o.scriptpubkey)\n            for o in tx.outputs()\n        ]\n        return t\n\n    def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str:\n        if new_wallet:  # new wallet\n            return 'trezor_not_initialized' if not device_info.initialized else 'trezor_start'\n        else:  # unlock existing wallet\n            return 'trezor_unlock'\n\n    # insert trezor pages in new wallet wizard\n    def extend_wizard(self, wizard: 'NewWalletWizard'):\n        views = {\n            'trezor_start': {\n                'next': 'trezor_xpub',\n            },\n            'trezor_xpub': {\n                'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore',\n                'accept': wizard.maybe_master_pubkey,\n                'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d)\n            },\n            'trezor_not_initialized': {\n                'next': 'trezor_choose_new_recover',\n            },\n            'trezor_choose_new_recover': {\n                'next': 'trezor_do_init',\n            },\n            'trezor_do_init': {\n                'next': 'trezor_start',\n            },\n            'trezor_unlock': {\n                'last': True\n            },\n        }\n        wizard.navmap_merge(views)\n"
  },
  {
    "path": "electrum/plugins/trustedcoin/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/plugins/trustedcoin/cmdline.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - Lightweight Bitcoin Client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom electrum.i18n import _\nfrom .trustedcoin import TrustedCoinPlugin\n\n\nclass Plugin(TrustedCoinPlugin):\n\n    def prompt_user_for_otp(self, wallet, tx):  # FIXME this is broken\n        if not isinstance(wallet, self.wallet_class):\n            return\n        if not wallet.can_sign_without_server():\n            self.logger.info(\"twofactor:sign_tx\")\n            auth_code = None\n            if wallet.keystores['x3'].can_sign(tx, ignore_watching_only=True):\n                msg = _('Please enter your Google Authenticator code:')\n                auth_code = int(input(msg))\n            else:\n                self.logger.info(\"twofactor: xpub3 not needed\")\n            wallet.auth_code = auth_code\n\n"
  },
  {
    "path": "electrum/plugins/trustedcoin/common_qt.py",
    "content": "import threading\nimport socket\nimport base64\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtCore import pyqtSignal, pyqtProperty, pyqtSlot\n\nfrom electrum.i18n import _\nfrom electrum.bip32 import BIP32Node\nfrom electrum import bitcoin\n\nfrom .trustedcoin import (server, ErrorConnectingServer, MOBILE_DISCLAIMER, TrustedCoinException)\nfrom electrum.gui.common_qt.plugins import PluginQObject\n\nif TYPE_CHECKING:\n    from electrum.wizard import NewWalletWizard\n\n\nclass TrustedcoinPluginQObject(PluginQObject):\n    canSignWithoutServerChanged = pyqtSignal()\n    termsAndConditionsRetrieved = pyqtSignal([str], arguments=['message'])\n    termsAndConditionsError = pyqtSignal([str], arguments=['message'])\n    otpError = pyqtSignal([str], arguments=['message'])\n    otpSuccess = pyqtSignal()\n    disclaimerChanged = pyqtSignal()\n    keystoreChanged = pyqtSignal()\n    otpSecretChanged = pyqtSignal()\n    shortIdChanged = pyqtSignal()\n    billingModelChanged = pyqtSignal()\n\n    remoteKeyStateChanged = pyqtSignal()\n    remoteKeyError = pyqtSignal([str], arguments=['message'])\n\n    requestOtp = pyqtSignal()\n\n    def __init__(self, plugin, wizard: 'NewWalletWizard', parent):\n        super().__init__(plugin, parent)\n        self.wizard = wizard\n        self._canSignWithoutServer = False\n        self._otpSecret = ''\n        self._shortId = ''\n        self._billingModel = []\n        self._remoteKeyState = ''\n        self._verifyingOtp = False\n\n    @pyqtProperty(str, notify=disclaimerChanged)\n    def disclaimer(self):\n        return '\\n\\n'.join(MOBILE_DISCLAIMER)\n\n    @pyqtProperty(bool, notify=canSignWithoutServerChanged)\n    def canSignWithoutServer(self):\n        return self._canSignWithoutServer\n\n    @pyqtProperty('QVariantMap', notify=keystoreChanged)\n    def keystore(self):\n        return self._keystore\n\n    @pyqtProperty(str, notify=otpSecretChanged)\n    def otpSecret(self):\n        return self._otpSecret\n\n    @pyqtProperty(str, notify=shortIdChanged)\n    def shortId(self):\n        return self._shortId\n\n    @pyqtSlot(str)\n    def otpSubmit(self, otp):\n        self._plugin.on_otp(otp)\n\n    @pyqtProperty(str, notify=remoteKeyStateChanged)\n    def remoteKeyState(self):\n        return self._remoteKeyState\n\n    @remoteKeyState.setter\n    def remoteKeyState(self, new_state):\n        if self._remoteKeyState != new_state:\n            self._remoteKeyState = new_state\n            self.remoteKeyStateChanged.emit()\n\n    @pyqtProperty('QVariantList', notify=billingModelChanged)\n    def billingModel(self):\n        return self._billingModel\n\n    def updateBillingInfo(self, wallet):\n        billing_model = []\n\n        price_per_tx = wallet.price_per_tx\n        for k, v in sorted(price_per_tx.items()):\n            if k == 1:\n                continue\n            item = {\n                'text': 'Pay every %d transactions' % k,\n                'value': k,\n                'sats_per_tx': v / k\n            }\n            billing_model.append(item)\n\n        self._billingModel = billing_model\n        self.billingModelChanged.emit()\n\n    @pyqtSlot()\n    def fetchTermsAndConditions(self):\n        def fetch_task():\n            try:\n                self.plugin.logger.debug('TOS')\n                tos = server.get_terms_of_service()\n            except ErrorConnectingServer as e:\n                self.termsAndConditionsError.emit(_('Error connecting to server'))\n            except Exception as e:\n                self.termsAndConditionsError.emit('%s: %s' % (_('Error'), repr(e)))\n            else:\n                self.termsAndConditionsRetrieved.emit(tos)\n            finally:\n                self._busy = False\n                self.busyChanged.emit()\n\n        self._busy = True\n        self.busyChanged.emit()\n        t = threading.Thread(target=fetch_task)\n        t.daemon = True\n        t.start()\n\n    @pyqtSlot()\n    def createKeystore(self):\n        email = 'dummy@electrum.org'\n\n        self.remoteKeyState = ''\n        self._otpSecret = ''\n        self.otpSecretChanged.emit()\n\n        wizard_data = self.wizard.get_wizard_data()\n\n        xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys(wizard_data)\n\n        def create_remote_key_task():\n            try:\n                self.plugin.logger.debug('create remote key')\n                r = server.create(xpub1, xpub2, email)\n\n                otp_secret = r['otp_secret']\n                _xpub3 = r['xpubkey_cosigner']\n                _id = r['id']\n            except (socket.error, ErrorConnectingServer) as e:\n                self.remoteKeyState = 'error'\n                self.remoteKeyError.emit(f'Network error: {str(e)}')\n            except TrustedCoinException as e:\n                if e.status_code == 409:\n                    self.remoteKeyState = 'wallet_known'\n                    self._shortId = short_id\n                    self.shortIdChanged.emit()\n                else:\n                    self.remoteKeyState = 'error'\n                    self.logger.warning(str(e))\n                    self.remoteKeyError.emit(f'Service error: {str(e)}')\n            except (KeyError, TypeError) as e:  # catch any assumptions\n                self.remoteKeyState = 'error'\n                self.remoteKeyError.emit(f'Error: {str(e)}')\n                self.logger.error(str(e))\n            else:\n                if short_id != _id:\n                    self.remoteKeyState = 'error'\n                    self.logger.error(\"unexpected trustedcoin short_id: expected {}, received {}\".format(short_id, _id))\n                    self.remoteKeyError.emit('Unexpected short_id')\n                    return\n                if xpub3 != _xpub3:\n                    self.remoteKeyState = 'error'\n                    self.logger.error(\"unexpected trustedcoin xpub3: expected {}, received {}\".format(xpub3, _xpub3))\n                    self.remoteKeyError.emit('Unexpected trustedcoin xpub3')\n                    return\n                self.remoteKeyState = 'new'\n                self._otpSecret = otp_secret\n                self.otpSecretChanged.emit()\n                self._shortId = short_id\n                self.shortIdChanged.emit()\n            finally:\n                self._busy = False\n                self.busyChanged.emit()\n\n        self._busy = True\n        self.busyChanged.emit()\n\n        t = threading.Thread(target=create_remote_key_task)\n        t.daemon = True\n        t.start()\n\n    @pyqtSlot()\n    def resetOtpSecret(self):\n        self.remoteKeyState = ''\n\n        wizard_data = self.wizard.get_wizard_data()\n\n        xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys(wizard_data)\n\n        def reset_otp_task():\n            try:\n                self.plugin.logger.debug('reset_otp')\n                r = server.get_challenge(short_id)\n                challenge = r.get('challenge')\n                message = 'TRUSTEDCOIN CHALLENGE: ' + challenge\n\n                def f(xprv):\n                    rootnode = BIP32Node.from_xkey(xprv)\n                    key = rootnode.subkey_at_private_derivation((0, 0)).eckey\n                    sig = bitcoin.ecdsa_sign_usermessage(key, message, is_compressed=True)\n                    return base64.b64encode(sig).decode()\n\n                signatures = [f(x) for x in [xprv1, xprv2]]\n                r = server.reset_auth(short_id, challenge, signatures)\n                otp_secret = r.get('otp_secret')\n            except (socket.error, ErrorConnectingServer) as e:\n                self.remoteKeyState = 'error'\n                self.remoteKeyError.emit(f'Network error: {str(e)}')\n            except Exception as e:\n                self.remoteKeyState = 'error'\n                self.remoteKeyError.emit(f'Error: {str(e)}')\n            else:\n                self.remoteKeyState = 'reset'\n                self._otpSecret = otp_secret\n                self.otpSecretChanged.emit()\n            finally:\n                self._busy = False\n                self.busyChanged.emit()\n\n        self._busy = True\n        self.busyChanged.emit()\n\n        t = threading.Thread(target=reset_otp_task, daemon=True)\n        t.start()\n\n    @pyqtSlot(str, int)\n    def checkOtp(self, short_id, otp):\n        assert type(otp) is int  # make sure this doesn't fail subtly\n\n        def check_otp_task():\n            try:\n                self.plugin.logger.debug(f'check OTP, shortId={short_id}, otp={otp}')\n                server.auth(short_id, otp)\n            except TrustedCoinException as e:\n                if e.status_code == 400:  # invalid OTP\n                    self.plugin.logger.debug('Invalid one-time password.')\n                    self.otpError.emit(_('Invalid one-time password.'))\n                else:\n                    self.plugin.logger.error(str(e))\n                    self.otpError.emit(f'Service error: {str(e)}')\n            except Exception as e:\n                self.plugin.logger.error(str(e))\n                self.otpError.emit(f'Error: {str(e)}')\n            else:\n                self.plugin.logger.debug('OTP verify success')\n                self.otpSuccess.emit()\n            finally:\n                self._busy = False\n                self.busyChanged.emit()\n                self._verifyingOtp = False\n\n        self._verifyingOtp = True\n        self._busy = True\n        self.busyChanged.emit()\n        t = threading.Thread(target=check_otp_task, daemon=True)\n        t.start()\n"
  },
  {
    "path": "electrum/plugins/trustedcoin/manifest.json",
    "content": "{\n  \"name\": \"trustedcoin\",\n  \"fullname\": \"Two Factor Authentication\",\n  \"description\": \"This plugin adds two-factor authentication to your wallet.<br/>For more information, visit <a href=\\\"https://api.trustedcoin.com/#/electrum-help\\\">https://api.trustedcoin.com/#/electrum-help</a>\",\n  \"requires_wallet_type\": [\"2fa\"],\n  \"registers_wallet_type\": \"2fa\",\n  \"icon\":\"trustedcoin-status.png\",\n  \"available_for\": [\"qt\", \"cmdline\", \"qml\"]\n}\n"
  },
  {
    "path": "electrum/plugins/trustedcoin/qml/ChooseSeed.qml",
    "content": "import QtQuick 2.6\nimport QtQuick.Layouts 1.0\nimport QtQuick.Controls 2.1\n\nimport \"../../../gui/qml/components/wizard\"\n\nWizardComponent {\n    valid: keystoregroup.checkedButton !== null\n\n    onAccept: {\n        wizard_data['keystore_type'] = keystoregroup.checkedButton.keystoretype\n    }\n\n    ButtonGroup {\n        id: keystoregroup\n    }\n\n    ColumnLayout {\n        width: parent.width\n        Label {\n            text: qsTr('Do you want to create a new seed, or restore a wallet using an existing seed?')\n            Layout.preferredWidth: parent.width\n            wrapMode: Text.Wrap\n        }\n        RadioButton {\n            ButtonGroup.group: keystoregroup\n            property string keystoretype: 'createseed'\n            checked: true\n            text: qsTr('Create a new seed')\n        }\n        RadioButton {\n            ButtonGroup.group: keystoregroup\n            property string keystoretype: 'haveseed'\n            text: qsTr('I already have a seed')\n        }\n    }\n}\n\n"
  },
  {
    "path": "electrum/plugins/trustedcoin/qml/Disclaimer.qml",
    "content": "import QtQuick 2.6\nimport QtQuick.Layouts 1.0\nimport QtQuick.Controls 2.1\n\nimport org.electrum 1.0\n\nimport \"../../../gui/qml/components/wizard\"\n\nWizardComponent {\n    valid: true\n\n    property QtObject plugin\n\n    ColumnLayout {\n        width: parent.width\n\n        Image {\n            Layout.alignment: Qt.AlignHCenter\n            Layout.bottomMargin: constants.paddingLarge\n            source: '../trustedcoin-wizard.png'\n        }\n\n        Label {\n            Layout.fillWidth: true\n            text: plugin ? plugin.disclaimer : ''\n            wrapMode: Text.Wrap\n        }\n    }\n\n    Component.onCompleted: {\n        plugin = AppController.plugin('trustedcoin')\n    }\n}\n"
  },
  {
    "path": "electrum/plugins/trustedcoin/qml/KeepDisable.qml",
    "content": "import QtQuick 2.6\nimport QtQuick.Layouts 1.0\nimport QtQuick.Controls 2.1\n\nimport \"../../../gui/qml/components/wizard\"\n\nWizardComponent {\n    valid: keepordisablegroup.checkedButton\n\n    function apply() {\n        wizard_data['trustedcoin_keepordisable'] = keepordisablegroup.checkedButton.keepordisable\n    }\n\n    ButtonGroup {\n        id: keepordisablegroup\n        onCheckedButtonChanged: checkIsLast()\n    }\n\n    ColumnLayout {\n        Label {\n            text: qsTr('Restore 2FA wallet')\n        }\n        RadioButton {\n            ButtonGroup.group: keepordisablegroup\n            property string keepordisable: 'keep'\n            checked: true\n            text: qsTr('Keep')\n        }\n        RadioButton {\n            ButtonGroup.group: keepordisablegroup\n            property string keepordisable: 'disable'\n            text: qsTr('Disable')\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml",
    "content": "import QtQuick 2.6\nimport QtQuick.Layouts 1.0\nimport QtQuick.Controls 2.1\n\nimport \"../../../gui/qml/components/wizard\"\nimport \"../../../gui/qml/components/controls\"\n\nWizardComponent {\n    valid: otpVerified\n\n    property QtObject plugin\n\n    property bool otpVerified: false\n\n    ColumnLayout {\n        width: parent.width\n\n        Label {\n            text: qsTr('Authenticator secret')\n        }\n\n        InfoTextArea {\n            id: errorBox\n            Layout.fillWidth: true\n            iconStyle: InfoTextArea.IconStyle.Error\n            visible: !otpVerified && plugin.remoteKeyState == 'error'\n        }\n\n        InfoTextArea {\n            Layout.fillWidth: true\n            iconStyle: InfoTextArea.IconStyle.Warn\n            visible: plugin.remoteKeyState == 'wallet_known'\n            text: qsTr('This wallet is already registered with TrustedCoin. ')\n                + qsTr('To finalize wallet creation, please enter your Google Authenticator Code. ')\n        }\n\n        QRImage {\n            Layout.alignment: Qt.AlignHCenter\n            visible: plugin.remoteKeyState == 'new' || plugin.remoteKeyState == 'reset'\n            qrdata: encodeURI('otpauth://totp/Electrum 2FA ' + wizard_data['wallet_name']\n                    + '?secret=' + plugin.otpSecret + '&digits=6')\n            render: plugin.otpSecret\n        }\n\n        TextHighlightPane {\n            Layout.alignment: Qt.AlignHCenter\n            visible: plugin.otpSecret\n            Label {\n                text: plugin.otpSecret\n                font.family: FixedFont\n                font.bold: true\n            }\n        }\n\n        Label {\n            Layout.fillWidth: true\n            visible: !otpVerified && plugin.otpSecret\n            wrapMode: Text.Wrap\n            text: qsTr('Enter or scan into authenticator app. Then authenticate below')\n        }\n\n        Label {\n            Layout.fillWidth: true\n            visible: !otpVerified && plugin.remoteKeyState == 'wallet_known'\n            wrapMode: Text.Wrap\n            text: qsTr('If you still have your OTP secret, then authenticate below')\n        }\n\n        TextField {\n            id: otp_auth\n            visible: !otpVerified && (plugin.otpSecret || plugin.remoteKeyState == 'wallet_known')\n            Layout.alignment: Qt.AlignHCenter\n            focus: true\n            inputMethodHints: Qt.ImhSensitiveData | Qt.ImhDigitsOnly\n            validator: IntValidator {bottom: 0; top: 999999;}\n            font.family: FixedFont\n            font.pixelSize: constants.fontSizeLarge\n            onTextChanged: {\n                if (text.length >= 6) {\n                    plugin.checkOtp(plugin.shortId, otp_auth.text)\n                    text = ''\n                }\n            }\n        }\n\n        Label {\n            Layout.fillWidth: true\n            visible: !otpVerified && plugin.remoteKeyState == 'wallet_known'\n            wrapMode: Text.Wrap\n            text: qsTr('Otherwise, you can request your OTP secret from the server, by pressing the button below')\n        }\n\n        Button {\n            Layout.alignment: Qt.AlignHCenter\n            visible: plugin.remoteKeyState == 'wallet_known' && !otpVerified\n            text: qsTr('Request OTP secret')\n            onClicked: plugin.resetOtpSecret()\n        }\n\n        Image {\n            Layout.alignment: Qt.AlignHCenter\n            source: '../../../gui/icons/confirmed.png'\n            visible: otpVerified\n            Layout.preferredWidth: constants.iconSizeXLarge\n            Layout.preferredHeight: constants.iconSizeXLarge\n        }\n    }\n\n    BusyIndicator {\n        anchors.centerIn: parent\n        visible: plugin ? plugin.busy : false\n        running: visible\n    }\n\n    Component.onCompleted: {\n        plugin = AppController.plugin('trustedcoin')\n        plugin.createKeystore()\n        otp_auth.forceActiveFocus()\n    }\n\n    Connections {\n        target: plugin\n        function onOtpError(message) {\n            console.log('OTP verify error')\n            errorBox.text = message\n        }\n        function onOtpSuccess() {\n            console.log('OTP verify success')\n            otpVerified = true\n        }\n        function onRemoteKeyError(message) {\n            errorBox.text = message\n        }\n    }\n}\n\n"
  },
  {
    "path": "electrum/plugins/trustedcoin/qml/Terms.qml",
    "content": "import QtQuick 2.6\nimport QtQuick.Layouts 1.0\nimport QtQuick.Controls 2.1\n\nimport org.electrum 1.0\n\nimport \"../../../gui/qml/components/wizard\"\nimport \"../../../gui/qml/components/controls\"\n\nWizardComponent {\n    valid: !plugin ? false\n                   : tosShown\n\n    property QtObject plugin\n    property bool tosShown: false\n\n    ColumnLayout {\n        anchors.fill: parent\n\n        Label {\n            text: qsTr('Terms and conditions')\n        }\n\n        TextHighlightPane {\n            Layout.fillWidth: true\n            Layout.fillHeight: true\n            rightPadding: 0\n\n            Flickable {\n                anchors.fill: parent\n                contentHeight: termsText.height\n                clip: true\n                boundsBehavior: Flickable.StopAtBounds\n\n                Label {\n                    id: termsText\n                    width: parent.width\n                    rightPadding: constants.paddingSmall\n                    wrapMode: Text.Wrap\n                }\n                ScrollIndicator.vertical: ScrollIndicator { }\n            }\n\n            BusyIndicator {\n                anchors.centerIn: parent\n                visible: plugin ? plugin.busy : false\n                running: visible\n            }\n        }\n    }\n\n    Component.onCompleted: {\n        plugin = AppController.plugin('trustedcoin')\n        plugin.fetchTermsAndConditions()\n    }\n\n    Connections {\n        target: plugin\n        function onTermsAndConditionsRetrieved(message) {\n            termsText.text = message\n            tosShown = true\n        }\n        function onTermsAndConditionsError(message) {\n            termsText.text = message\n        }\n    }\n}\n"
  },
  {
    "path": "electrum/plugins/trustedcoin/qml.py",
    "content": "from typing import TYPE_CHECKING\n\nfrom electrum.i18n import _\nfrom electrum.plugin import hook\nfrom electrum.util import UserFacingException\n\nfrom electrum.gui.qml.qewallet import QEWallet\nfrom electrum.gui.qml.qedaemon import QEDaemon\n\nfrom .common_qt import TrustedcoinPluginQObject\nfrom .trustedcoin import TrustedCoinPlugin, TrustedCoinException\n\nif TYPE_CHECKING:\n    from electrum.gui.qml import ElectrumQmlApplication\n    from electrum.wallet import Abstract_Wallet\n    from electrum.wizard import NewWalletWizard\n\n\nclass Plugin(TrustedCoinPlugin):\n    def __init__(self, *args):\n        super().__init__(*args)\n        self._app = None\n        self.so = None\n        self.on_success = None\n        self.on_failure = None\n        self.tx = None\n\n    @hook\n    def load_wallet(self, wallet: 'Abstract_Wallet'):\n        if not isinstance(wallet, self.wallet_class):\n            return\n        self.logger.debug(f'plugin enabled for wallet \"{str(wallet)}\"')\n        if wallet.can_sign_without_server():\n            self.so._canSignWithoutServer = True\n            self.so.canSignWithoutServerChanged.emit()\n\n            msg = ' '.join([\n                _('This wallet was restored from seed, and it contains two master private keys.'),\n                _('Therefore, two-factor authentication is disabled.')\n            ])\n            self.logger.info(msg)\n        self.start_request_thread(wallet)\n\n    @hook\n    def init_qml(self, app: 'ElectrumQmlApplication'):\n        self.logger.debug(f'init_qml hook called, gui={str(type(app))}')\n        self._app = app\n        wizard = QEDaemon.instance.newWalletWizard\n        # important: TrustedcoinPluginQObject needs to be parented, as keeping a ref\n        # in the plugin is not enough to avoid gc\n        # Note: storing the trustedcoin qt helper in the plugin is different from the desktop client,\n        # which stores the helper in the wizard object. As the mobile client only shows a single wizard\n        # at a time, this is ok for now.\n        self.so = TrustedcoinPluginQObject(self, wizard, self._app)\n        # extend wizard\n        self.extend_wizard(wizard)\n\n    # wizard support functions\n\n    def extend_wizard(self, wizard: 'NewWalletWizard'):\n        super().extend_wizard(wizard)\n        views = {\n            'trustedcoin_start': {\n                'gui': '../../../../plugins/trustedcoin/qml/Disclaimer',\n            },\n            'trustedcoin_choose_seed': {\n                'gui': '../../../../plugins/trustedcoin/qml/ChooseSeed',\n            },\n            'trustedcoin_create_seed': {\n                'gui': 'WCCreateSeed',\n            },\n            'trustedcoin_create_ext': {\n                'gui': 'WCEnterExt',\n            },\n            'trustedcoin_confirm_seed': {\n                'gui': 'WCConfirmSeed',\n            },\n            'trustedcoin_confirm_ext': {\n                'gui': 'WCConfirmExt',\n            },\n            'trustedcoin_have_seed': {\n                'gui': 'WCHaveSeed',\n            },\n            'trustedcoin_have_ext': {\n                'gui': 'WCEnterExt',\n            },\n            'trustedcoin_keep_disable': {\n                'gui': '../../../../plugins/trustedcoin/qml/KeepDisable',\n            },\n            'trustedcoin_tos': {\n                'gui': '../../../../plugins/trustedcoin/qml/Terms',\n            },\n            'trustedcoin_keystore_unlock': {\n                # TODO when QML can import external wallet files\n            },\n            'trustedcoin_show_confirm_otp': {\n                'gui': '../../../../plugins/trustedcoin/qml/ShowConfirmOTP',\n            }\n        }\n        wizard.navmap_merge(views)\n\n    # running wallet functions\n\n    def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):\n        self.logger.debug('prompt_user_for_otp')\n        self.on_success = on_success\n        self.on_failure = on_failure if on_failure else lambda x: self.logger.error(x)\n        self.wallet = wallet\n        self.tx = tx\n        qewallet = QEWallet.getInstanceFor(wallet)\n        qewallet.request_otp(self.on_otp)\n\n    def on_otp(self, otp):\n        if not otp:\n            self.on_failure(_('No auth code'))\n            return\n\n        self.logger.debug(f'on_otp {otp} for tx {repr(self.tx)}')\n\n        try:\n            self.wallet.on_otp(self.tx, otp)\n        except UserFacingException as e:\n            self.on_failure(_('Invalid one-time password.'))\n        except TrustedCoinException as e:\n            if e.status_code == 400:  # invalid OTP\n                self.on_failure(_('Invalid one-time password.'))\n            else:\n                self.on_failure(_('Service Error') + ':\\n' + str(e))\n        except Exception as e:\n            self.on_failure(_('Error') + ':\\n' + str(e))\n        else:\n            self.on_success(self.tx)\n\n    def billing_info_retrieved(self, wallet):\n        self.logger.info('billing_info_retrieved')\n        qewallet = QEWallet.getInstanceFor(wallet)\n        qewallet.billingInfoChanged.emit()\n        self.so.updateBillingInfo(wallet)\n"
  },
  {
    "path": "electrum/plugins/trustedcoin/qt.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - Lightweight Bitcoin Client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom functools import partial\nimport os\nfrom typing import TYPE_CHECKING\n\nfrom PyQt6.QtGui import QPixmap, QMovie, QColor\nfrom PyQt6.QtCore import QObject, pyqtSignal, QSize, Qt\nfrom PyQt6.QtWidgets import (QTextEdit, QVBoxLayout, QLabel, QGridLayout, QHBoxLayout,\n                             QRadioButton, QCheckBox, QPushButton, QWidget)\n\nfrom electrum.i18n import _\nfrom electrum.plugin import hook\nfrom electrum.util import InvalidPassword, ChoiceItem\nfrom electrum.logging import Logger, get_logger\nfrom electrum import keystore\n\nfrom electrum.gui.qt.util import (WindowModalDialog, WaitingDialog, OkButton, CancelButton, Buttons, icon_path,\n                                  internal_plugin_icon_path, WWLabel, CloseButton, ColorScheme,\n                                  ChoiceWidget, PasswordLineEdit, char_width_in_lineedit)\nfrom electrum.gui.qt.qrcodewidget import QRCodeWidget\nfrom electrum.gui.qt.amountedit import AmountEdit\nfrom electrum.gui.qt.main_window import StatusBarButton\nfrom electrum.gui.qt.wizard.wallet import (WCCreateSeed, WCConfirmSeed, WCHaveSeed, WCEnterExt, WCConfirmExt,\n                                           WalletWizardComponent)\nfrom electrum.gui.qt.util import read_QIcon_from_bytes\n\nfrom .common_qt import TrustedcoinPluginQObject\nfrom .trustedcoin import TrustedCoinPlugin, DISCLAIMER\n\nif TYPE_CHECKING:\n    from electrum.gui.qt.main_window import ElectrumWindow\n    from electrum.wallet import Abstract_Wallet\n    from electrum.gui.qt.wizard.wallet import QENewWalletWizard\n\n\nclass TOS(QTextEdit):\n    tos_signal = pyqtSignal()\n    error_signal = pyqtSignal(object)\n\n\nclass HandlerTwoFactor(QObject, Logger):\n\n    def __init__(self, plugin, window):\n        QObject.__init__(self)\n        self.plugin = plugin\n        self.window = window\n        Logger.__init__(self)\n\n    def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):\n        if not isinstance(wallet, self.plugin.wallet_class):\n            return\n        if wallet.can_sign_without_server():\n            return\n        if not wallet.keystores['x3'].can_sign(tx, ignore_watching_only=True):\n            self.logger.info(\"twofactor: xpub3 not needed\")\n            return\n        window = self.window.top_level_window()\n        auth_code = self.plugin.auth_dialog(window)\n        WaitingDialog(parent=window,\n                      message=_('Waiting for TrustedCoin server to sign transaction...'),\n                      task=lambda: wallet.on_otp(tx, auth_code),\n                      on_success=lambda *args: on_success(tx),\n                      on_error=on_failure)\n\n\nclass Plugin(TrustedCoinPlugin):\n\n    def __init__(self, parent, config, name):\n        super().__init__(parent, config, name)\n\n    @hook\n    def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'):\n        if not isinstance(wallet, self.wallet_class):\n            return\n        wallet.handler_2fa = HandlerTwoFactor(self, window)\n        if wallet.can_sign_without_server():\n            msg = ' '.join([\n                _('This wallet was restored from seed, and it contains two master private keys.'),\n                _('Therefore, two-factor authentication is disabled.')\n            ])\n            action = lambda: window.show_message(msg)\n            icon = read_QIcon_from_bytes(self.read_file(\"trustedcoin-status-disabled.png\"))\n        else:\n            action = partial(self.settings_dialog, window)\n            icon = read_QIcon_from_bytes(self.read_file(\"trustedcoin-status.png\"))\n        sb = window.statusBar()\n        button = StatusBarButton(icon, _(\"TrustedCoin\"), action, sb.height())\n        sb.addPermanentWidget(button)\n        self.start_request_thread(window.wallet)\n\n    def auth_dialog(self, window):\n        d = WindowModalDialog(window, _(\"Authorization\"))\n        vbox = QVBoxLayout(d)\n        pw = AmountEdit(None, is_int=True)\n        msg = _('Please enter your Google Authenticator code')\n        vbox.addWidget(QLabel(msg))\n        grid = QGridLayout()\n        grid.setSpacing(8)\n        grid.addWidget(QLabel(_('Code')), 1, 0)\n        grid.addWidget(pw, 1, 1)\n        vbox.addLayout(grid)\n        msg = _('If you have lost your second factor, you need to restore your wallet from seed in order to request a new code.')\n        label = QLabel(msg)\n        label.setWordWrap(1)\n        vbox.addWidget(label)\n        vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))\n        if not d.exec():\n            return\n        return pw.get_amount()\n\n    def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):\n        wallet.handler_2fa.prompt_user_for_otp(wallet, tx, on_success, on_failure)\n\n    def waiting_dialog_for_billing_info(self, window, *, on_finished=None):\n        def task():\n            return self.request_billing_info(window.wallet, suppress_connection_error=False)\n\n        def on_error(exc_info):\n            e = exc_info[1]\n            window.show_error(\"{header}\\n{exc}\\n\\n{tor}\"\n                              .format(header=_('Error getting TrustedCoin account info.'),\n                                      exc=repr(e),\n                                      tor=_('If you keep experiencing network problems, try using a Tor proxy.')))\n        return WaitingDialog(parent=window,\n                             message=_('Requesting account info from TrustedCoin server...'),\n                             task=task,\n                             on_success=on_finished,\n                             on_error=on_error)\n\n    @hook\n    def abort_send(self, window):\n        wallet = window.wallet\n        if not isinstance(wallet, self.wallet_class):\n            return\n        if wallet.can_sign_without_server():\n            return\n        if wallet.billing_info is None:\n            self.waiting_dialog_for_billing_info(window)\n            return True\n        return False\n\n    def settings_dialog(self, window):\n        self.waiting_dialog_for_billing_info(window,\n                                             on_finished=partial(self.show_settings_dialog, window))\n\n    def icon_path(self, name):\n        return internal_plugin_icon_path(self.name, name)\n\n    def show_settings_dialog(self, window, success):\n        if not success:\n            window.show_message(_('Server not reachable.'))\n            return\n\n        wallet = window.wallet\n        d = WindowModalDialog(window, _(\"TrustedCoin Information\"))\n        d.setMinimumSize(500, 200)\n        vbox = QVBoxLayout(d)\n        hbox = QHBoxLayout()\n\n        logo = QLabel()\n        logo.setPixmap(QPixmap(self.icon_path(\"trustedcoin-status.png\")))\n        msg = _('This wallet is protected by TrustedCoin\\'s two-factor authentication.') + '<br/>'\\\n              + _(\"For more information, visit\") + \" <a href=\\\"https://api.trustedcoin.com/#/electrum-help\\\">https://api.trustedcoin.com/#/electrum-help</a>\"\n        label = QLabel(msg)\n        label.setOpenExternalLinks(1)\n\n        hbox.addStretch(10)\n        hbox.addWidget(logo)\n        hbox.addStretch(10)\n        hbox.addWidget(label)\n        hbox.addStretch(10)\n\n        vbox.addLayout(hbox)\n        vbox.addStretch(10)\n\n        msg = _('TrustedCoin charges a small fee to co-sign transactions. The fee depends on how many prepaid transactions you buy. An extra output is added to your transaction every time you run out of prepaid transactions.') + '<br/>'\n        label = QLabel(msg)\n        label.setWordWrap(1)\n        vbox.addWidget(label)\n\n        vbox.addStretch(10)\n        grid = QGridLayout()\n        vbox.addLayout(grid)\n\n        price_per_tx = wallet.price_per_tx\n        n_prepay = wallet.num_prepay()\n        i = 0\n        for k, v in sorted(price_per_tx.items()):\n            if k == 1:\n                continue\n            grid.addWidget(QLabel(\"Pay every %d transactions:\"%k), i, 0)\n            grid.addWidget(QLabel(window.format_amount(v/k) + ' ' + window.base_unit() + \"/tx\"), i, 1)\n            b = QRadioButton()\n            b.setChecked(k == n_prepay)\n\n            def on_click(b, k):\n                self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY = k\n            b.clicked.connect(partial(on_click, k=k))\n            grid.addWidget(b, i, 2)\n            i += 1\n\n        n = wallet.billing_info.get('tx_remaining', 0)\n        grid.addWidget(QLabel(_(\"Your wallet has {} prepaid transactions.\").format(n)), i, 0)\n        vbox.addLayout(Buttons(CloseButton(d)))\n        d.exec()\n\n    @hook\n    def init_wallet_wizard(self, wizard: 'QENewWalletWizard'):\n        wizard.trustedcoin_qhelper = TrustedcoinPluginQObject(self, wizard, None)\n        self.extend_wizard(wizard)\n        if wizard.start_viewstate and wizard.start_viewstate.view.startswith('trustedcoin_'):\n            wizard.start_viewstate.params.update({'icon': self.icon_path('trustedcoin-wizard.png')})\n\n    def extend_wizard(self, wizard: 'QENewWalletWizard'):\n        super().extend_wizard(wizard)\n        views = {\n            'trustedcoin_start': {\n                'gui': WCDisclaimer,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            },\n            'trustedcoin_choose_seed': {\n                'gui': WCChooseSeed,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            },\n            'trustedcoin_create_seed': {\n                'gui': WCCreateSeed,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            },\n            'trustedcoin_create_ext': {\n                'gui': WCEnterExt,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            },\n            'trustedcoin_confirm_seed': {\n                'gui': WCConfirmSeed,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            },\n            'trustedcoin_confirm_ext': {\n                'gui': WCConfirmExt,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            },\n            'trustedcoin_have_seed': {\n                'gui': WCHaveSeed,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            },\n            'trustedcoin_have_ext': {\n                'gui': WCEnterExt,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            },\n            'trustedcoin_keep_disable': {\n                'gui': WCKeepDisable,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            },\n            'trustedcoin_tos': {\n                'gui': WCTerms,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            },\n            'trustedcoin_keystore_unlock': {\n                'gui': WCKeystorePassword,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            },\n            'trustedcoin_show_confirm_otp': {\n                'gui': WCShowConfirmOTP,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n            }\n        }\n        wizard.navmap_merge(views)\n\n        # insert page offering choice to go online or continue on another system\n        ext_online = {\n            'trustedcoin_continue_online': {\n                'gui': WCContinueOnline,\n                'params': {'icon': self.icon_path('trustedcoin-wizard.png')},\n                'next': lambda d: 'trustedcoin_tos' if d['trustedcoin_go_online'] else 'wallet_password',\n                'accept': self.on_continue_online,\n                'last': lambda d: not d['trustedcoin_go_online'] and wizard.is_single_password()\n            },\n            'trustedcoin_confirm_seed': {\n                'next': lambda d: 'trustedcoin_confirm_ext' if wizard.wants_ext(d) else 'trustedcoin_continue_online'\n            },\n            'trustedcoin_confirm_ext': {\n                'next': 'trustedcoin_continue_online',\n            },\n            'trustedcoin_keep_disable': {\n                'next': lambda d: 'trustedcoin_continue_online' if d['trustedcoin_keepordisable'] != 'disable'\n                else 'wallet_password',\n            }\n        }\n        wizard.navmap_merge(ext_online)\n\n    def on_continue_online(self, wizard_data):\n        if not wizard_data['trustedcoin_go_online']:\n            self.logger.debug('Staying offline, create keystores here')\n            xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.create_keys(wizard_data)\n            k1 = keystore.from_xprv(xprv1)\n            k2 = keystore.from_xpub(xpub2)\n\n            wizard_data['x1'] = k1.dump()\n            wizard_data['x2'] = k2.dump()\n\n\nclass WCDisclaimer(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Disclaimer'))\n\n        self.layout().addWidget(WWLabel('\\n\\n'.join(DISCLAIMER)))\n        self.layout().addStretch(1)\n\n        self._valid = True\n\n    def apply(self):\n        pass\n\n\nclass WCChooseSeed(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Create or restore'))\n        message = _('Do you want to create a new seed, or restore a wallet using an existing seed?')\n        choices = [\n            ChoiceItem(key='createseed', label=_('Create a new seed')),\n            ChoiceItem(key='haveseed', label=_('I already have a seed')),\n        ]\n\n        self.choice_w = ChoiceWidget(message=message, choices=choices)\n        self.layout().addWidget(self.choice_w)\n        self.layout().addStretch(1)\n\n        self._valid = True\n\n    def apply(self):\n        self.wizard_data['keystore_type'] = self.choice_w.selected_key\n\n\nclass WCTerms(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Terms and conditions'))\n        self._has_tos = False\n        self.tos_e = TOS()\n        self.tos_e.setReadOnly(True)\n        self.layout().addWidget(self.tos_e)\n\n    def on_ready(self):\n        self.fetch_terms_and_conditions()\n\n    def fetch_terms_and_conditions(self):\n        self.wizard.trustedcoin_qhelper.busyChanged.connect(self.on_busy_changed)\n        self.wizard.trustedcoin_qhelper.termsAndConditionsRetrieved.connect(self.on_terms_retrieved)\n        self.wizard.trustedcoin_qhelper.termsAndConditionsError.connect(self.on_terms_error)\n        self.wizard.trustedcoin_qhelper.fetchTermsAndConditions()\n\n    def on_busy_changed(self):\n        self.busy = self.wizard.trustedcoin_qhelper.busy\n\n    def on_terms_retrieved(self, tos: str) -> None:\n        self._has_tos = True\n        self.tos_e.setText(tos)\n        self.validate()\n\n    def on_terms_error(self, error: str) -> None:\n        self.error = error\n\n    def validate(self):\n        self.valid = self._has_tos\n\n    def apply(self):\n        pass\n\n\nclass WCShowConfirmOTP(WalletWizardComponent):\n    _logger = get_logger(__name__)\n\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Authenticator secret'))\n        self._otp_verified = False\n        self._is_online_continuation = False\n\n        self.new_otp = QWidget()\n        new_otp_layout = QVBoxLayout()\n        scanlabel = WWLabel(_('Enter or scan into authenticator app. Then authenticate below'))\n        new_otp_layout.addWidget(scanlabel)\n        self.qr = QRCodeWidget('')\n        new_otp_layout.addWidget(self.qr)\n        self.secretlabel = WWLabel()\n        new_otp_layout.addWidget(self.secretlabel)\n        self.new_otp.setLayout(new_otp_layout)\n\n        self.exist_otp = QWidget()\n        exist_otp_layout = QVBoxLayout()\n        knownlabel = WWLabel(_('This wallet is already registered with TrustedCoin.'))\n        exist_otp_layout.addWidget(knownlabel)\n        self.knownsecretlabel = WWLabel(_('If you still have your OTP secret, then authenticate below to finalize wallet creation'))\n        exist_otp_layout.addWidget(self.knownsecretlabel)\n        self.exist_otp.setLayout(exist_otp_layout)\n\n        self.authlabelnew = WWLabel(_('Then, enter your Google Authenticator code:'))\n        self.authlabelexist = WWLabel(_('Google Authenticator code:'))\n\n        self.spinner = QMovie(icon_path('spinner.gif'))\n        self.spinner.setScaledSize(QSize(24, 24))\n        self.spinner.setBackgroundColor(QColor('black'))\n        self.spinner_l = QLabel()\n        self.spinner_l.setMargin(5)\n        self.spinner_l.setVisible(False)\n        self.spinner_l.setMovie(self.spinner)\n\n        self.otp_status_l = QLabel()\n        self.otp_status_l.setAlignment(Qt.AlignmentFlag.AlignHCenter)\n        self.otp_status_l.setVisible(False)\n\n        self.resetlabel = WWLabel(_('If you have lost your OTP secret, click the button below to request a new secret from the server.'))\n        self.button = QPushButton('Request OTP secret')\n        self.button.clicked.connect(self.on_request_otp)\n\n        hbox = QHBoxLayout()\n        hbox.addWidget(self.authlabelnew)\n        hbox.addWidget(self.authlabelexist)\n        hbox.addStretch(1)\n        hbox.addWidget(self.spinner_l)\n        self.otp_e = AmountEdit(None, is_int=True)\n        self.otp_e.setFocus()\n        self.otp_e.setMaximumWidth(150)\n        self.otp_e.textEdited.connect(self.on_otp_edited)\n        hbox.addWidget(self.otp_e)\n\n        self.layout().addWidget(self.new_otp)\n        self.layout().addWidget(self.exist_otp)\n        self.layout().addLayout(hbox)\n        self.layout().addWidget(self.otp_status_l)\n        self.layout().addWidget(self.resetlabel)\n        self.layout().addWidget(self.button)\n        self.layout().addStretch(1)\n\n    def on_ready(self):\n        self.wizard.trustedcoin_qhelper.busyChanged.connect(self.on_busy_changed)\n        self.wizard.trustedcoin_qhelper.remoteKeyError.connect(self.on_remote_key_error)\n        self.wizard.trustedcoin_qhelper.otpSuccess.connect(self.on_otp_success)\n        self.wizard.trustedcoin_qhelper.otpError.connect(self.on_otp_error)\n        self.wizard.trustedcoin_qhelper.remoteKeyError.connect(self.on_remote_key_error)\n\n        # set higher minHeight so the qr code and the input field are shown without scrolling\n        prev_height = self.wizard.height()\n        prev_min_height = self.wizard.minimumHeight()\n        def restore_prev_height():\n            self.wizard.setMinimumHeight(prev_min_height)\n            self.wizard.resize(self.wizard.width(), prev_height)\n            self.wizard.next_button.clicked.disconnect(restore_prev_height)\n            self.wizard.back_button.clicked.disconnect(restore_prev_height)\n        self.wizard.setMinimumHeight(530)\n        self.wizard.next_button.clicked.connect(restore_prev_height)\n        self.wizard.back_button.clicked.connect(restore_prev_height)\n\n        self._is_online_continuation = 'seed' not in self.wizard_data\n        if self._is_online_continuation:\n            self.knownsecretlabel.setText(_('Authenticate below to finalize wallet creation'))\n\n        self.wizard.trustedcoin_qhelper.createKeystore()\n\n    def update(self):\n        is_new = bool(self.wizard.trustedcoin_qhelper.remoteKeyState != 'wallet_known')\n        self.new_otp.setVisible(is_new)\n        self.exist_otp.setVisible(not is_new)\n        self.authlabelnew.setVisible(is_new)\n        self.authlabelexist.setVisible(not is_new)\n        self.authlabelexist.setEnabled(not self._otp_verified)\n        self.otp_e.setEnabled(not self._otp_verified)\n        self.resetlabel.setVisible(not is_new and not self._otp_verified and not self._is_online_continuation)\n        self.button.setVisible(not is_new and not self._otp_verified and not self._is_online_continuation)\n\n        if self.wizard.trustedcoin_qhelper.otpSecret:\n            self.secretlabel.setText(self.wizard.trustedcoin_qhelper.otpSecret)\n            uri = 'otpauth://totp/Electrum 2FA %s?secret=%s&digits=6' % (\n                os.path.basename(self.wizard_data['wallet_name']), self.wizard.trustedcoin_qhelper.otpSecret)\n            self.qr.setData(uri)\n\n    def on_busy_changed(self):\n        if not self.wizard.trustedcoin_qhelper._verifyingOtp:\n            self.busy = self.wizard.trustedcoin_qhelper.busy\n            if not self.busy:\n                self.update()\n\n    def on_remote_key_error(self, text):\n        self._logger.error(text)\n        self.error = text\n\n    def on_request_otp(self):\n        self.otp_status_l.setVisible(False)\n        self.wizard.trustedcoin_qhelper.resetOtpSecret()\n        self.update()\n\n    def on_otp_success(self):\n        self._otp_verified = True\n        self.otp_status_l.setText('Valid!')\n        self.otp_status_l.setVisible(True)\n        self.otp_status_l.setStyleSheet(ColorScheme.GREEN.as_stylesheet(False))\n        self.setEnabled(True)\n        self.spinner_l.setVisible(False)\n        self.spinner.stop()\n\n        self.valid = True\n\n    def on_otp_error(self, message):\n        self.otp_status_l.setText(message)\n        self.otp_status_l.setVisible(True)\n        self.otp_status_l.setStyleSheet(ColorScheme.RED.as_stylesheet(False))\n        self.setEnabled(True)\n        self.spinner_l.setVisible(False)\n        self.spinner.stop()\n\n    def on_otp_edited(self):\n        self.otp_status_l.setVisible(False)\n        text = self.otp_e.text()\n        if len(text) > 0:\n            try:\n                otp_int = int(text)\n            except ValueError:\n                return\n        if len(text) == 6:\n            # verify otp\n            self.wizard.trustedcoin_qhelper.checkOtp(self.wizard.trustedcoin_qhelper.shortId, otp_int)\n            self.setEnabled(False)\n            self.spinner_l.setVisible(True)\n            self.spinner.start()\n            self.otp_e.setText('')\n\n    def apply(self):\n        pass\n\n\nclass WCKeepDisable(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Restore 2FA wallet'))\n        message = ' '.join([\n            'You are going to restore a wallet protected with two-factor authentication.',\n            'Do you want to keep using two-factor authentication with this wallet,',\n            'or do you want to disable it, and have two master private keys in your wallet?'\n        ])\n        choices = [\n            ChoiceItem(key='keep', label=_('Keep')),\n            ChoiceItem(key='disable', label=_('Disable')),\n        ]\n        self.choice_w = ChoiceWidget(message=message, choices=choices)\n        self.layout().addWidget(self.choice_w)\n        self.layout().addStretch(1)\n\n        self._valid = True\n\n    def apply(self):\n        self.wizard_data['trustedcoin_keepordisable'] = self.choice_w.selected_key\n\n\nclass WCContinueOnline(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Continue Online'))\n        self.cb_online = QCheckBox(_('Go online to complete wallet creation'))\n\n    def on_ready(self):\n        path = os.path.join(os.path.dirname(self.wizard._daemon.config.get_wallet_path()), self.wizard_data['wallet_name'])\n        msg = [\n            _(\"Your wallet file is: {}.\").format(path),\n            _(\"You need to be online in order to complete the creation of \"\n              \"your wallet. If you want to continue online, keep the checkbox \"\n              \"checked and press Next.\"),\n            _(\"If you want this system to stay offline \"\n              \"and continue the completion of the wallet on an online system, \"\n              \"uncheck the checkbox and press Finish.\")\n        ]\n\n        self.layout().addWidget(WWLabel('\\n\\n'.join(msg)))\n        self.layout().addStretch(1)\n\n        self.cb_online.setChecked(True)\n        self.cb_online.stateChanged.connect(self.on_updated)\n        # self.cb_online.setToolTip(_(\"Check this box to request a new secret. You will need to retype your seed.\"))\n        self.layout().addWidget(self.cb_online)\n        self.layout().setAlignment(self.cb_online, Qt.AlignmentFlag.AlignHCenter)\n        self.layout().addStretch(1)\n\n        self._valid = True\n\n    def apply(self):\n        self.wizard_data['trustedcoin_go_online'] = self.cb_online.isChecked()\n\n\nclass WCKeystorePassword(WalletWizardComponent):\n    def __init__(self, parent, wizard):\n        WalletWizardComponent.__init__(self, parent, wizard, title=_('Unlock Keystore'))\n        self.layout().addStretch(1)\n\n        hbox2 = QHBoxLayout()\n        hbox2.addStretch(1)\n        self.pw_e = PasswordLineEdit('', self)\n        self.pw_e.setFixedWidth(17 * char_width_in_lineedit())\n        self.pw_e.textEdited.connect(self.on_text)\n        pw_label = QLabel(_('Password') + ':')\n        hbox2.addWidget(pw_label)\n        hbox2.addWidget(self.pw_e)\n        hbox2.addStretch(1)\n        self.layout().addLayout(hbox2)\n        self.layout().addStretch(1)\n\n        self.ks = None\n\n    def on_ready(self):\n        self.ks = self.wizard_data['xprv1']\n\n    def on_text(self):\n        try:\n            self.ks.check_password(self.pw_e.text())\n        except InvalidPassword:\n            self.valid = False\n            return\n        self.valid = True\n\n    def apply(self):\n        if self.valid:\n            self.wizard_data['xprv1'] = self.ks.get_master_private_key(self.pw_e.text())\n            self.wizard_data['password'] = self.pw_e.text()\n"
  },
  {
    "path": "electrum/plugins/trustedcoin/trustedcoin.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - Lightweight Bitcoin Client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport json\nimport time\nimport hashlib\nfrom typing import Dict, Union, Sequence, List, TYPE_CHECKING\nfrom urllib.parse import urljoin\nfrom urllib.parse import quote\n\nfrom aiohttp import ClientResponse\n\nimport electrum_ecc as ecc\n\nfrom electrum import constants, keystore, version, bip32, bitcoin\nfrom electrum.bip32 import BIP32Node, xpub_type, is_xprv\nfrom electrum.crypto import sha256\nfrom electrum.transaction import PartialTxOutput, PartialTxInput, PartialTransaction, Transaction\nfrom electrum.mnemonic import Mnemonic, calc_seed_type, is_any_2fa_seed_type\nfrom electrum.wallet import Multisig_Wallet, Deterministic_Wallet\nfrom electrum.i18n import _\nfrom electrum.plugin import BasePlugin, hook\nfrom electrum.util import NotEnoughFunds, UserFacingException, error_text_str_to_safe_str\nfrom electrum.network import Network\nfrom electrum.logging import Logger\nfrom electrum.keystore import KeyStore\n\nif TYPE_CHECKING:\n    from electrum.wizard import NewWalletWizard\n\n\ndef get_signing_xpub(xtype):\n    if not constants.net.TESTNET:\n        xpub = \"xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL\"\n    else:\n        xpub = \"tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY\"\n    if xtype not in ('standard', 'p2wsh'):\n        raise NotImplementedError('xtype: {}'.format(xtype))\n    if xtype == 'standard':\n        return xpub\n    node = BIP32Node.from_xkey(xpub)\n    return node._replace(xtype=xtype).to_xpub()\n\n\ndef get_billing_xpub():\n    if constants.net.TESTNET:\n        return \"tpubD6NzVbkrYhZ4X11EJFTJujsYbUmVASAYY7gXsEt4sL97AMBdypiH1E9ZVTpdXXEy3Kj9Eqd1UkxdGtvDt5z23DKsh6211CfNJo8bLLyem5r\"\n    else:\n        return \"xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU\"\n\n\nDESKTOP_DISCLAIMER = [\n    _(\"Two-factor authentication is a service provided by TrustedCoin.  \"\n      \"It uses a multi-signature wallet, where you own 2 of 3 keys.  \"\n      \"The third key is stored on a remote server that signs transactions on \"\n      \"your behalf.  To use this service, you will need a smartphone with \"\n      \"Google Authenticator installed.\"),\n    _(\"A small fee will be charged on each transaction that uses the \"\n      \"remote server.  You may check and modify your billing preferences \"\n      \"once the installation is complete.\"),\n    _(\"Note that your coins are not locked in this service.  You may withdraw \"\n      \"your funds at any time and at no cost, without the remote server, by \"\n      \"using the 'restore wallet' option with your wallet seed.\"),\n    _(\"The next step will generate the seed of your wallet.  This seed will \"\n      \"NOT be saved in your computer, and it must be stored on paper.  \"\n      \"To be safe from malware, you may want to do this on an offline \"\n      \"computer, and move your wallet later to an online computer.\"),\n]\nDISCLAIMER = DESKTOP_DISCLAIMER\n\nMOBILE_DISCLAIMER = [\n    _(\"Two-factor authentication is a service provided by TrustedCoin. \"\n      \"To use it, you must have a separate device with Google Authenticator.\"),\n    _(\"This service uses a multi-signature wallet, where you own 2 of 3 keys.  \"\n      \"The third key is stored on a remote server that signs transactions on \"\n      \"your behalf. A small fee will be charged on each transaction that uses the \"\n      \"remote server.\"),\n    _(\"Note that your coins are not locked in this service.  You may withdraw \"\n      \"your funds at any time and at no cost, without the remote server, by \"\n      \"using the 'restore wallet' option with your wallet seed.\"),\n]\n\nRESTORE_MSG = _(\"Enter the seed for your 2-factor wallet:\")\n\n\nclass TrustedCoinException(Exception):\n    def __init__(self, message, *, status_code=0):\n        # note: 'message' is arbitrary text coming from the server\n        safer_message = (\n            f\"Received error from 2FA server\\n\"\n            f\"[DO NOT TRUST THIS MESSAGE]:\\n\\n\"\n            f\"status_code={status_code}\\n\\n\"\n            f\"{error_text_str_to_safe_str(message)}\")\n        Exception.__init__(self, safer_message)\n        self.status_code = status_code\n\n\nclass ErrorConnectingServer(Exception):\n    def __init__(self, reason: Union[str, Exception] = None):\n        self.reason = reason\n\n    def __str__(self):\n        header = _(\"Error connecting to {} server\").format('TrustedCoin')\n        reason = self.reason\n        if isinstance(reason, BaseException):\n            reason = repr(reason)\n        return f\"{header}:\\n{reason}\" if reason else header\n\n\nclass TrustedCoinCosignerClient(Logger):\n    def __init__(self, user_agent=None, base_url='https://api.trustedcoin.com/2/'):\n        self.base_url = base_url\n        self.debug = False\n        self.user_agent = user_agent\n        Logger.__init__(self)\n\n    async def handle_response(self, resp: ClientResponse):\n        if resp.status != 200:\n            try:\n                r = await resp.json()\n                message = r['message']\n            except Exception:\n                message = await resp.text()\n            raise TrustedCoinException(message, status_code=resp.status)\n        try:\n            return await resp.json()\n        except Exception:\n            return await resp.text()\n\n    def send_request(self, method, relative_url, data=None, *, timeout=None):\n        network = Network.get_instance()\n        if not network:\n            raise ErrorConnectingServer('You are offline.')\n        url = urljoin(self.base_url, relative_url)\n        if self.debug:\n            self.logger.debug(f'<-- {method} {url} {data}')\n        headers = {}\n        if self.user_agent:\n            headers['user-agent'] = self.user_agent\n        try:\n            if method == 'get':\n                response = Network.send_http_on_proxy(method, url,\n                                                      params=data,\n                                                      headers=headers,\n                                                      on_finish=self.handle_response,\n                                                      timeout=timeout)\n            elif method == 'post':\n                response = Network.send_http_on_proxy(method, url,\n                                                      json=data,\n                                                      headers=headers,\n                                                      on_finish=self.handle_response,\n                                                      timeout=timeout)\n            else:\n                raise Exception(f\"unexpected {method=!r}\")\n        except TrustedCoinException:\n            raise\n        except Exception as e:\n            raise ErrorConnectingServer(e)\n        else:\n            if self.debug:\n                self.logger.debug(f'--> {response}')\n            return response\n\n    def get_terms_of_service(self, billing_plan='electrum-per-tx-otp'):\n        \"\"\"\n        Returns the TOS for the given billing plan as a plain/text unicode string.\n        :param billing_plan: the plan to return the terms for\n        \"\"\"\n        payload = {'billing_plan': billing_plan}\n        return self.send_request('get', 'tos', payload)\n\n    def create(self, xpubkey1, xpubkey2, email, billing_plan='electrum-per-tx-otp'):\n        \"\"\"\n        Creates a new cosigner resource.\n        :param xpubkey1: a bip32 extended public key (customarily the hot key)\n        :param xpubkey2: a bip32 extended public key (customarily the cold key)\n        :param email: a contact email\n        :param billing_plan: the billing plan for the cosigner\n        \"\"\"\n        payload = {\n            'email': email,\n            'xpubkey1': xpubkey1,\n            'xpubkey2': xpubkey2,\n            'billing_plan': billing_plan,\n        }\n        return self.send_request('post', 'cosigner', payload)\n\n    def auth(self, id, otp):\n        \"\"\"\n        Attempt to authenticate for a particular cosigner.\n        :param id: the id of the cosigner\n        :param otp: the one time password\n        \"\"\"\n        payload = {'otp': otp}\n        return self.send_request('post', 'cosigner/%s/auth' % quote(id), payload)\n\n    def get(self, id):\n        \"\"\" Get billing info \"\"\"\n        return self.send_request('get', 'cosigner/%s' % quote(id))\n\n    def get_challenge(self, id):\n        \"\"\" Get challenge to reset Google Auth secret \"\"\"\n        return self.send_request('get', 'cosigner/%s/otp_secret' % quote(id))\n\n    def reset_auth(self, id, challenge, signatures):\n        \"\"\" Reset Google Auth secret \"\"\"\n        payload = {'challenge': challenge, 'signatures': signatures}\n        return self.send_request('post', 'cosigner/%s/otp_secret' % quote(id), payload)\n\n    def sign(self, id, transaction, otp):\n        \"\"\"\n        Attempt to authenticate for a particular cosigner.\n        :param id: the id of the cosigner\n        :param transaction: the hex encoded [partially signed] compact transaction to sign\n        :param otp: the one time password\n        \"\"\"\n        payload = {\n            'otp': otp,\n            'transaction': transaction\n        }\n        return self.send_request('post', 'cosigner/%s/sign' % quote(id), payload,\n                                 timeout=60)\n\n\nserver = TrustedCoinCosignerClient(user_agent=\"Electrum/\" + version.ELECTRUM_VERSION)\n\n\nclass Wallet_2fa(Multisig_Wallet):\n    plugin: 'TrustedCoinPlugin'\n    wallet_type = '2fa'\n\n    def __init__(self, db, *, config):\n        self.m, self.n = 2, 3\n        Deterministic_Wallet.__init__(self, db, config=config)\n        self.is_billing = False\n        self.billing_info = None\n        self._load_billing_addresses()\n\n    def _load_billing_addresses(self):\n        billing_addresses = {\n            'legacy': self.db.get('trustedcoin_billing_addresses', {}),\n            'segwit': self.db.get('trustedcoin_billing_addresses_segwit', {})\n        }\n        self._billing_addresses = {}  # type: Dict[str, Dict[int, str]]  # addr_type -> index -> addr\n        self._billing_addresses_set = set()  # set of addrs\n        for addr_type, d in list(billing_addresses.items()):\n            self._billing_addresses[addr_type] = {}\n            # convert keys from str to int\n            for index, addr in d.items():\n                self._billing_addresses[addr_type][int(index)] = addr\n                self._billing_addresses_set.add(addr)\n\n    def can_sign_without_server(self):\n        return not self.keystores['x2'].is_watching_only()\n\n    def get_user_id(self):\n        return get_user_id(self.db)\n\n    def min_prepay(self):\n        return min(self.price_per_tx.keys())\n\n    def num_prepay(self):\n        default_fallback = self.min_prepay()\n        num = self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY\n        if num not in self.price_per_tx:\n            num = default_fallback\n        return num\n\n    def extra_fee(self):\n        if self.can_sign_without_server():\n            return 0\n        if self.billing_info is None:\n            self.plugin.start_request_thread(self)\n            return 0\n        if self.billing_info.get('tx_remaining'):\n            return 0\n        if self.is_billing:\n            return 0\n        n = self.num_prepay()\n        price = int(self.price_per_tx[n])\n        # sanity check: price capped at 0.5 mBTC per tx or 20 mBTC total\n        #               (note that the server can influence our choice of n by sending unexpected values)\n        if price > min(50_000 * n, 2_000_000):\n            raise Exception(f\"too high trustedcoin fee ({price} for {n} txns)\")\n        return price\n\n    def make_unsigned_transaction(\n            self, *,\n            outputs: List[PartialTxOutput],\n            is_sweep=False,\n            **kwargs,\n    ) -> PartialTransaction:\n\n        mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction(\n            self, outputs=o, **kwargs)\n        extra_fee = self.extra_fee() if not is_sweep else 0\n        if extra_fee:\n            address = self.billing_info['billing_address_segwit']\n            fee_output = PartialTxOutput.from_address_and_value(address, extra_fee)\n            try:\n                tx = mk_tx(outputs + [fee_output])\n            except NotEnoughFunds:\n                # TrustedCoin won't charge if the total inputs is\n                # lower than their fee\n                tx = mk_tx(outputs)\n                if tx.input_value() >= extra_fee:\n                    raise\n                self.logger.info(\"not charging for this tx\")\n        else:\n            tx = mk_tx(outputs)\n        return tx\n\n    def on_otp(self, tx: PartialTransaction, otp):\n        if not otp:\n            self.logger.info(\"sign_transaction: no auth code\")\n            return\n        otp = int(otp)\n        long_user_id, short_id = self.get_user_id()\n        raw_tx = tx.serialize_as_bytes().hex()\n        assert raw_tx[:10] == \"70736274ff\", f\"bad magic. {raw_tx[:10]}\"\n        try:\n            r = server.sign(short_id, raw_tx, otp)\n        except TrustedCoinException as e:\n            if e.status_code == 400:  # invalid OTP\n                raise UserFacingException(_('Invalid one-time password.')) from e\n            else:\n                raise\n        if r:\n            received_raw_tx = r.get('transaction')\n            received_tx = Transaction(received_raw_tx)\n            tx.combine_with_other_psbt(received_tx)\n        self.logger.info(f\"twofactor: is complete {tx.is_complete()}\")\n        # reset billing_info\n        self.billing_info = None\n        self.plugin.start_request_thread(self)\n\n    def add_new_billing_address(self, billing_index: int, address: str, addr_type: str):\n        billing_addresses_of_this_type = self._billing_addresses[addr_type]\n        saved_addr = billing_addresses_of_this_type.get(billing_index)\n        if saved_addr is not None:\n            if saved_addr == address:\n                return  # already saved this address\n            else:\n                raise Exception('trustedcoin billing address inconsistency.. '\n                                'for index {}, already saved {}, now got {}'\n                                .format(billing_index, saved_addr, address))\n        # do we have all prior indices? (are we synced?)\n        largest_index_we_have = max(billing_addresses_of_this_type) if billing_addresses_of_this_type else -1\n        if largest_index_we_have + 1 < billing_index:  # need to sync\n            for i in range(largest_index_we_have + 1, billing_index):\n                addr = make_billing_address(self, i, addr_type=addr_type)\n                billing_addresses_of_this_type[i] = addr\n                self._billing_addresses_set.add(addr)\n        # save this address; and persist to disk\n        billing_addresses_of_this_type[billing_index] = address\n        self._billing_addresses_set.add(address)\n        self._billing_addresses[addr_type] = billing_addresses_of_this_type\n        self.db.put('trustedcoin_billing_addresses', self._billing_addresses['legacy'])\n        self.db.put('trustedcoin_billing_addresses_segwit', self._billing_addresses['segwit'])\n\n    def is_billing_address(self, addr: str) -> bool:\n        return addr in self._billing_addresses_set\n\n    def can_enable_disable_keystore(self, ks: KeyStore) -> bool:\n        return False\n\n    def enable_keystore(self, keystore, is_hardware_keystore, password):\n        raise Exception(\"2fa wallet cannot enable keystore\")\n\n    def disable_keystore(self, keystore):\n        raise Exception(\"2fa wallet cannot disable keystore\")\n\n\n# Utility functions\n\ndef get_user_id(db):\n    def make_long_id(xpub_hot, xpub_cold):\n        return sha256(''.join(sorted([xpub_hot, xpub_cold])))\n    xpub1 = db.get('x1')['xpub']\n    xpub2 = db.get('x2')['xpub']\n    long_id = make_long_id(xpub1, xpub2)\n    short_id = hashlib.sha256(long_id).hexdigest()\n    return long_id, short_id\n\n\ndef make_xpub(xpub, s) -> str:\n    rootnode = BIP32Node.from_xkey(xpub)\n    child_pubkey, child_chaincode = bip32._CKD_pub(parent_pubkey=rootnode.eckey.get_public_key_bytes(compressed=True),\n                                                   parent_chaincode=rootnode.chaincode,\n                                                   child_index=s)\n    child_node = BIP32Node(xtype=rootnode.xtype,\n                           eckey=ecc.ECPubkey(child_pubkey),\n                           chaincode=child_chaincode)\n    return child_node.to_xpub()\n\n\ndef make_billing_address(wallet, num, addr_type):\n    long_id, short_id = wallet.get_user_id()\n    xpub = make_xpub(get_billing_xpub(), long_id)\n    usernode = BIP32Node.from_xkey(xpub)\n    child_node = usernode.subkey_at_public_derivation([num])\n    pubkey = child_node.eckey.get_public_key_bytes(compressed=True)\n    if addr_type == 'legacy':\n        return bitcoin.public_key_to_p2pkh(pubkey)\n    elif addr_type == 'segwit':\n        return bitcoin.public_key_to_p2wpkh(pubkey)\n    else:\n        raise ValueError(f'unexpected billing type: {addr_type}')\n\n\ndef finish_requesting(func):\n    def f(self, *args, **kwargs):\n        try:\n            return func(self, *args, **kwargs)\n        finally:\n            self.requesting = False\n    return f\n\n\nclass TrustedCoinPlugin(BasePlugin):\n    wallet_class = Wallet_2fa\n    disclaimer_msg = DISCLAIMER\n\n    def __init__(self, parent, config, name):\n        BasePlugin.__init__(self, parent, config, name)\n        self.wallet_class.plugin = self\n        self.requesting = False\n\n    def is_available(self):\n        return True\n\n    def is_enabled(self):\n        return True\n\n    def can_user_disable(self):\n        return False\n\n    @hook\n    def tc_sign_wrapper(self, wallet, tx, on_success, on_failure):\n        if not isinstance(wallet, self.wallet_class):\n            return\n        if tx.is_complete():\n            return\n        if wallet.can_sign_without_server():\n            return\n        if not wallet.keystores['x3'].can_sign(tx, ignore_watching_only=True):\n            self.logger.info(\"twofactor: xpub3 not needed\")\n            return\n\n        def wrapper(tx):\n            assert tx\n            self.prompt_user_for_otp(wallet, tx, on_success, on_failure)\n\n        return wrapper\n\n    def prompt_user_for_otp(self, wallet, tx, on_success, on_failure) -> None:\n        raise NotImplementedError()\n\n    @hook\n    def get_tx_extra_fee(self, wallet, tx: Transaction):\n        if type(wallet) is not Wallet_2fa:\n            return\n        for o in tx.outputs():\n            if wallet.is_billing_address(o.address):\n                return o.address, o.value\n\n    @finish_requesting\n    def request_billing_info(self, wallet: 'Wallet_2fa', *, suppress_connection_error=True):\n        if wallet.can_sign_without_server():\n            return\n        self.logger.info(\"request billing info\")\n        try:\n            billing_info = server.get(wallet.get_user_id()[1])\n        except ErrorConnectingServer as e:\n            if suppress_connection_error:\n                self.logger.info(repr(e))\n                return\n            raise\n        billing_index = billing_info['billing_index']\n        # add segwit billing address; this will be used for actual billing\n        billing_address = make_billing_address(wallet, billing_index, addr_type='segwit')\n        if billing_address != billing_info['billing_address_segwit']:\n            raise Exception(f'unexpected trustedcoin billing address: '\n                            f'calculated {billing_address}, received {billing_info[\"billing_address_segwit\"]}')\n        wallet.add_new_billing_address(billing_index, billing_address, addr_type='segwit')\n        # also add legacy billing address; only used for detecting past payments in GUI\n        billing_address = make_billing_address(wallet, billing_index, addr_type='legacy')\n        wallet.add_new_billing_address(billing_index, billing_address, addr_type='legacy')\n\n        wallet.billing_info = billing_info\n        wallet.price_per_tx = dict(billing_info['price_per_tx'])\n        wallet.price_per_tx.pop(1, None)\n        self.billing_info_retrieved(wallet)\n        return True\n\n    def billing_info_retrieved(self, wallet):\n        # override to handle billing info when it becomes available\n        pass\n\n    def start_request_thread(self, wallet):\n        from threading import Thread\n        if self.requesting is False:\n            self.requesting = True\n            t = Thread(target=self.request_billing_info, args=(wallet,))\n            t.daemon = True\n            t.start()\n            return t\n\n    def make_seed(self, seed_type):\n        if not is_any_2fa_seed_type(seed_type):\n            raise Exception(f'unexpected seed type: {seed_type!r}')\n        return Mnemonic('english').make_seed(seed_type=seed_type)\n\n    @hook\n    def do_clear(self, window):\n        window.wallet.is_billing = False\n\n    @classmethod\n    def get_xkeys(cls, seed, t, passphrase, derivation):\n        assert is_any_2fa_seed_type(t)\n        xtype = 'standard' if t == '2fa' else 'p2wsh'\n        bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase=passphrase)\n        rootnode = BIP32Node.from_rootseed(bip32_seed, xtype=xtype)\n        child_node = rootnode.subkey_at_private_derivation(derivation)\n        return child_node.to_xprv(), child_node.to_xpub()\n\n    @classmethod\n    def xkeys_from_seed(cls, seed, passphrase):\n        t = calc_seed_type(seed)\n        if not is_any_2fa_seed_type(t):\n            raise Exception(f'unexpected seed type: {t!r}')\n        words = seed.split()\n        n = len(words)\n        if t == '2fa':\n            if n >= 20:  # old scheme\n                # note: pre-2.7 2fa seeds were typically 24-25 words, however they\n                # could probabilistically be arbitrarily shorter due to a bug. (see #3611)\n                # the probability of it being < 20 words is about 2^(-(256+12-19*11)) = 2^(-59)\n                if passphrase:\n                    raise Exception(\"old '2fa'-type electrum seed cannot have passphrase\")\n                xprv1, xpub1 = cls.get_xkeys(' '.join(words[0:12]), t, '', \"m/\")\n                xprv2, xpub2 = cls.get_xkeys(' '.join(words[12:]), t, '', \"m/\")\n            elif n == 12:  # new scheme\n                xprv1, xpub1 = cls.get_xkeys(seed, t, passphrase, \"m/0'/\")\n                xprv2, xpub2 = cls.get_xkeys(seed, t, passphrase, \"m/1'/\")\n            else:\n                raise Exception(f'unrecognized seed length for \"2fa\" seed: {n}')\n        elif t == '2fa_segwit':\n            xprv1, xpub1 = cls.get_xkeys(seed, t, passphrase, \"m/0'/\")\n            xprv2, xpub2 = cls.get_xkeys(seed, t, passphrase, \"m/1'/\")\n        else:\n            raise Exception(f'unexpected seed type: {t!r}')\n        return xprv1, xpub1, xprv2, xpub2\n\n    @hook\n    def get_action(self, db):\n        if db.get('wallet_type') != '2fa':\n            return\n        if not db.get('x1'):\n            return self, 'show_disclaimer'\n        if not db.get('x2'):\n            return self, 'show_disclaimer'\n        if not db.get('x3'):\n            return self, 'accept_terms_of_use'\n\n    # insert trustedcoin pages in new wallet wizard\n    def extend_wizard(self, wizard: 'NewWalletWizard'):\n        views = {\n            'trustedcoin_start': {\n                'next': 'trustedcoin_choose_seed',\n            },\n            'trustedcoin_choose_seed': {\n                'next': lambda d: 'trustedcoin_create_seed' if d['keystore_type'] == 'createseed'\n                        else 'trustedcoin_have_seed'\n            },\n            'trustedcoin_create_seed': {\n                'next': lambda d: 'trustedcoin_create_ext' if wizard.wants_ext(d) else 'trustedcoin_confirm_seed',\n            },\n            'trustedcoin_create_ext': {\n                'next': 'trustedcoin_confirm_seed',\n            },\n            'trustedcoin_confirm_seed': {\n                'next': lambda d: 'trustedcoin_confirm_ext' if wizard.wants_ext(d) else 'trustedcoin_tos',\n            },\n            'trustedcoin_confirm_ext': {\n                'next': 'trustedcoin_tos',\n            },\n            'trustedcoin_have_seed': {\n                'next': lambda d: 'trustedcoin_have_ext' if wizard.wants_ext(d) else 'trustedcoin_keep_disable',\n            },\n            'trustedcoin_have_ext': {\n                'next': 'trustedcoin_keep_disable',\n            },\n            'trustedcoin_keep_disable': {\n                'next': lambda d: 'trustedcoin_tos' if d['trustedcoin_keepordisable'] != 'disable'\n                        else 'wallet_password',\n                'accept': self.recovery_disable,\n                'last': lambda d: wizard.is_single_password() and d['trustedcoin_keepordisable'] == 'disable'\n            },\n            'trustedcoin_tos': {\n                'next': lambda d: 'trustedcoin_show_confirm_otp' if 'xprv1' not in d or is_xprv(d['xprv1'])\n                        else 'trustedcoin_keystore_unlock'\n            },\n            'trustedcoin_keystore_unlock': {\n                'next': 'trustedcoin_show_confirm_otp'\n            },\n            'trustedcoin_show_confirm_otp': {\n                'accept': self.on_accept_otp_secret,\n                'next': 'wallet_password',\n                'last': lambda d: wizard.is_single_password() or 'xprv1' in d\n            }\n        }\n        wizard.navmap_merge(views)\n\n    # combined create_keystore and create_remote_key pre\n    def create_keys(self, wizard_data):\n        if 'seed' not in wizard_data:\n            # online continuation\n            xprv1, xpub1, xprv2, xpub2 = (wizard_data['xprv1'], wizard_data['xpub1'], None, wizard_data['xpub2'])\n        else:\n            seed_extension = wizard_data['seed_extra_words'] if wizard_data['seed_extend'] else ''\n            xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(wizard_data['seed'], seed_extension)\n\n        data = {'x1': {'xpub': xpub1}, 'x2': {'xpub': xpub2}}\n\n        # Generate third key deterministically.\n        long_user_id, short_id = get_user_id(data)\n        xtype = xpub_type(xpub1)\n        xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id)\n\n        return xprv1, xpub1, xprv2, xpub2, xpub3, short_id\n\n    def on_accept_otp_secret(self, wizard_data):\n        self.logger.debug('OTP secret accepted, creating keystores')\n        xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.create_keys(wizard_data)\n        k1 = keystore.from_xprv(xprv1)\n        k2 = keystore.from_xpub(xpub2)\n        k3 = keystore.from_xpub(xpub3)\n\n        wizard_data['x1'] = k1.dump()\n        wizard_data['x2'] = k2.dump()\n        wizard_data['x3'] = k3.dump()\n\n    def recovery_disable(self, wizard_data):\n        if wizard_data['trustedcoin_keepordisable'] != 'disable':\n            return\n\n        self.logger.debug('2fa disabled, creating keystores')\n        xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.create_keys(wizard_data)\n        k1 = keystore.from_xprv(xprv1)\n        k2 = keystore.from_xprv(xprv2)\n        k3 = keystore.from_xpub(xpub3)\n\n        wizard_data['x1'] = k1.dump()\n        wizard_data['x2'] = k2.dump()\n        wizard_data['x3'] = k3.dump()\n\n"
  },
  {
    "path": "electrum/plugins/watchtower/__init__.py",
    "content": "from electrum.simple_config import ConfigVar, SimpleConfig\n\nSimpleConfig.WATCHTOWER_SERVER_PORT = ConfigVar('plugins.watchtower.server_port', default=None, type_=int, plugin=__name__)\nSimpleConfig.WATCHTOWER_SERVER_USER = ConfigVar('plugins.watchtower.server_user', default=None, type_=str, plugin=__name__)\nSimpleConfig.WATCHTOWER_SERVER_PASSWORD = ConfigVar('plugins.watchtower.server_password', default=None, type_=str, plugin=__name__)\n"
  },
  {
    "path": "electrum/plugins/watchtower/cmdline.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - Lightweight Bitcoin Client\n# Copyright (C) 2023 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\nfrom .watchtower import WatchtowerPlugin\n\nclass Plugin(WatchtowerPlugin):\n    pass\n\n"
  },
  {
    "path": "electrum/plugins/watchtower/manifest.json",
    "content": "{\n  \"name\": \"watchtower\",\n  \"fullname\": \"Watchtower\",\n  \"description\": \"A watchtower is a daemon that watches your channels and prevents the other party from stealing funds by broadcasting an old state.\\n\\nExample:\\n\\ndaemon setup:\\n\\n  electrum -o setconfig enable_plugin_watchtower True\\n  electrum -o setconfig watchtower_user wtuser\\n  electrum -o setconfig watchtower_password wtpassword\\n  electrum -o setconfig watchtower_port 12345\\n  electrum daemon -v\\n\\nclient setup:\\n\\n  electrum -o setconfig watchtower_url http://wtuser:wtpassword@127.0.0.1:12345\\n\\n\",\n  \"available_for\": [\"cmdline\"]\n}\n"
  },
  {
    "path": "electrum/plugins/watchtower/server.py",
    "content": "import os\nimport asyncio\nfrom collections import defaultdict\nfrom typing import TYPE_CHECKING\n\nfrom aiohttp import web\n\nfrom electrum.util import log_exceptions, ignore_exceptions\nfrom electrum.logging import Logger\nfrom electrum.util import EventListener\nfrom electrum.lnaddr import lndecode\nfrom electrum.daemon import AuthenticatedServer\n\n\nif TYPE_CHECKING:\n    from electrum.network import Network\n\n\nclass WatchTowerServer(AuthenticatedServer):\n\n    def __init__(self, watchtower, network: 'Network', port:int):\n        self.port = port\n        self.config = network.config\n        self.network = network\n        watchtower_user = self.config.WATCHTOWER_SERVER_USER or \"\"\n        watchtower_password = self.config.WATCHTOWER_SERVER_PASSWORD or \"\"\n        AuthenticatedServer.__init__(self, watchtower_user, watchtower_password)\n        self.lnwatcher = watchtower\n        self.app = web.Application()\n        self.app.router.add_post(\"/\", self.handle)\n        self.register_method('get_ctn', self.get_ctn)\n        self.register_method('add_sweep_tx', self.add_sweep_tx)\n\n    async def run(self):\n        self.runner = web.AppRunner(self.app)\n        await self.runner.setup()\n        site = web.TCPSite(self.runner, host='localhost', port=self.port)\n        await site.start()\n        self.logger.info(f\"running and listening on port {self.port}\")\n\n    async def get_ctn(self, *args):\n        return await self.lnwatcher.get_ctn(*args)\n\n    async def add_sweep_tx(self, *args):\n        return await self.lnwatcher.sweepstore.add_sweep_tx(*args)\n\n"
  },
  {
    "path": "electrum/plugins/watchtower/watchtower.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - Lightweight Bitcoin Client\n# Copyright (C) 2023 The Electrum Developers\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\nimport asyncio\nimport os\nfrom typing import TYPE_CHECKING\nfrom typing import Dict\n\nfrom electrum.util import log_exceptions, random_shuffled_copy\nfrom electrum.plugin import BasePlugin\nfrom electrum.sql_db import SqlDB, sql\nfrom electrum.transaction import Transaction, match_script_against_template\nfrom electrum.network import Network\nfrom electrum.address_synchronizer import AddressSynchronizer, TX_HEIGHT_LOCAL\nfrom electrum.wallet_db import WalletDB\nfrom electrum.lnutil import WITNESS_TEMPLATE_RECEIVED_HTLC, WITNESS_TEMPLATE_OFFERED_HTLC\nfrom electrum.logging import Logger\nfrom electrum.util import EventListener, event_listener\n\nfrom .server import WatchTowerServer\n\nif TYPE_CHECKING:\n    from electrum.simple_config import SimpleConfig\n\n\nclass WatchtowerPlugin(BasePlugin):\n\n    def __init__(self, parent, config: 'SimpleConfig', name):\n        BasePlugin.__init__(self, parent, config, name)\n        self.config = config\n        self.network = Network.get_instance()\n        if self.network is None:\n            return\n\n        self.watchtower = WatchTower(self.network)\n        asyncio.run_coroutine_threadsafe(self.watchtower.start_watching(), self.network.asyncio_loop)\n        if watchtower_port := self.config.WATCHTOWER_SERVER_PORT:\n            self.server = WatchTowerServer(self.watchtower, self.network, watchtower_port)\n            asyncio.run_coroutine_threadsafe(self.network.taskgroup.spawn(self.server.run), self.network.asyncio_loop)\n\n\nclass WatchTower(Logger, EventListener):\n\n    def __init__(self, network: 'Network'):\n        Logger.__init__(self)\n        self.config = network.config\n        wallet_db = WalletDB('', storage=None, upgrade=True)\n        self.adb = AddressSynchronizer(wallet_db, self.config, name=self.diagnostic_name())\n        self.adb.start_network(network)\n        self.callbacks = {}  # address -> lambda function\n        self.register_callbacks()\n        # status gets populated when we run\n        self.channel_status = {}\n        self.network = network\n        self.sweepstore = SweepStore(os.path.join(self.config.path, \"watchtower_db\"), network)\n\n    def remove_callback(self, address):\n        self.callbacks.pop(address, None)\n\n    def add_callback(self, address, callback):\n        self.adb.add_address(address)\n        self.callbacks[address] = callback\n\n    @event_listener\n    async def on_event_blockchain_updated(self, *args):\n        await self.trigger_callbacks()\n\n    @event_listener\n    async def on_event_wallet_updated(self, wallet):\n        # called if we add local tx\n        if wallet.adb != self.adb:\n            return\n        await self.trigger_callbacks()\n\n    @event_listener\n    async def on_event_adb_added_verified_tx(self, adb, tx_hash):\n        if adb != self.adb:\n            return\n        await self.trigger_callbacks()\n\n    @event_listener\n    async def on_event_adb_set_up_to_date(self, adb):\n        if adb != self.adb:\n            return\n        await self.trigger_callbacks()\n\n    @log_exceptions\n    async def trigger_callbacks(self):\n        if not self.adb.synchronizer:\n            self.logger.info(\"synchronizer not set yet\")\n            return\n        for address, callback in list(self.callbacks.items()):\n            await callback()\n\n    async def stop(self):\n        self.unregister_callbacks()\n        await self.adb.stop()\n\n    def add_channel(self, outpoint: str, address: str) -> None:\n        callback = lambda: self.check_onchain_situation(address, outpoint)\n        self.add_callback(address, callback)\n\n    def diagnostic_name(self):\n        return \"watchtower\"\n\n    @log_exceptions\n    async def start_watching(self):\n        # I need to watch the addresses from sweepstore\n        lst = await self.sweepstore.list_channels()\n        for outpoint, address in random_shuffled_copy(lst):\n            self.add_channel(outpoint, address)\n\n    async def check_onchain_situation(self, address, funding_outpoint):\n        # early return if address has not been added yet\n        if not self.adb.is_mine(address):\n            return\n        # inspect_tx_candidate might have added new addresses, in which case we return early\n        closing_txid = self.adb.get_spender(funding_outpoint)\n        if closing_txid:\n            closing_tx = self.adb.get_transaction(closing_txid)\n            if closing_tx:\n                keep_watching = await self.sweep_commitment_transaction(funding_outpoint, closing_tx)\n            else:\n                self.logger.info(f\"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...\")\n                keep_watching = True\n        else:\n            keep_watching = True\n        if not keep_watching:\n            await self.unwatch_channel(address, funding_outpoint)\n\n    def inspect_tx_candidate(self, outpoint, n: int) -> Dict[str, str]:\n        \"\"\"\n        returns a dict of spenders for a transaction of interest.\n        subscribes to addresses as a side effect.\n        n==0 => outpoint is a channel funding.\n        n==1 => outpoint is a commitment or close output: to_local, to_remote or first-stage htlc\n        n==2 => outpoint is a second-stage htlc\n        \"\"\"\n        prev_txid, index = outpoint.split(':')\n        spender_txid = self.adb.db.get_spent_outpoint(prev_txid, int(index))\n        result = {outpoint:spender_txid}\n        if n == 0:\n            if spender_txid is None:\n                self.channel_status[outpoint] = 'open'\n            elif not self.adb.is_deeply_mined(spender_txid):\n                self.channel_status[outpoint] = 'closed (%d)' % self.adb.get_tx_height(spender_txid).conf\n            else:\n                self.channel_status[outpoint] = 'closed (deep)'\n        if spender_txid is None:\n            return result\n        spender_tx = self.adb.get_transaction(spender_txid)\n        if n == 1:\n            # if tx input is not a first-stage HTLC, we can stop recursion\n            # FIXME: this is not true for anchor channels\n            if len(spender_tx.inputs()) != 1:\n                return result\n            o = spender_tx.inputs()[0]\n            witness = o.witness_elements()\n            if not witness:\n                # This can happen if spender_tx is a local unsigned tx in the wallet history, e.g.:\n                # channel is coop-closed, outpoint is for our coop-close output, and spender_tx is an\n                # arbitrary wallet-spend.\n                return result\n            redeem_script = witness[-1]\n            if match_script_against_template(redeem_script, WITNESS_TEMPLATE_OFFERED_HTLC):\n                #self.logger.info(f\"input script matches offered htlc {redeem_script.hex()}\")\n                pass\n            elif match_script_against_template(redeem_script, WITNESS_TEMPLATE_RECEIVED_HTLC):\n                #self.logger.info(f\"input script matches received htlc {redeem_script.hex()}\")\n                pass\n            else:\n                return result\n        for i, o in enumerate(spender_tx.outputs()):\n            if o.address is None:\n                continue\n            if not self.adb.is_mine(o.address):\n                self.adb.add_address(o.address)\n            elif n < 2:\n                r = self.inspect_tx_candidate(spender_txid + ':%d' % i, n + 1)\n                result.update(r)\n        return result\n\n    async def sweep_commitment_transaction(self, funding_outpoint: str, closing_tx: Transaction) -> bool:\n        assert closing_tx\n        spenders = self.inspect_tx_candidate(funding_outpoint, 0)\n        keep_watching = not self.adb.is_deeply_mined(closing_tx.txid())\n        for prevout, spender in spenders.items():\n            if spender is not None:\n                keep_watching |= not self.adb.is_deeply_mined(spender)\n                continue\n            sweep_txns = await self.sweepstore.get_sweep_tx(funding_outpoint, prevout)\n            for tx in sweep_txns:\n                await self.broadcast_or_log(funding_outpoint, tx)\n                keep_watching = True\n        return keep_watching\n\n    async def broadcast_or_log(self, funding_outpoint: str, tx: Transaction):\n        height = self.adb.get_tx_height(tx.txid()).height()\n        if height != TX_HEIGHT_LOCAL:\n            return\n        try:\n            txid = await self.network.broadcast_transaction(tx)\n        except Exception as e:\n            self.logger.info(f'broadcast failure: txid={tx.txid()}, funding_outpoint={funding_outpoint}: {repr(e)}')\n        else:\n            self.logger.info(f'broadcast success: txid={tx.txid()}, funding_outpoint={funding_outpoint}')\n            return txid\n\n    async def get_ctn(self, outpoint, addr):\n        if addr not in self.callbacks.keys():\n            self.logger.info(f'watching new channel: {outpoint} {addr}')\n            self.add_channel(outpoint, addr)\n        return await self.sweepstore.get_ctn(outpoint, addr)\n\n    def get_num_tx(self, outpoint):\n        async def f():\n            return await self.sweepstore.get_num_tx(outpoint)\n        return self.network.run_from_another_thread(f())\n\n    def list_sweep_tx(self):\n        async def f():\n            return await self.sweepstore.list_sweep_tx()\n        return self.network.run_from_another_thread(f())\n\n    def list_channels(self):\n        async def f():\n            return await self.sweepstore.list_channels()\n        return self.network.run_from_another_thread(f())\n\n    async def unwatch_channel(self, address, funding_outpoint):\n        await self.sweepstore.remove_sweep_tx(funding_outpoint)\n        await self.sweepstore.remove_channel(funding_outpoint)\n\n\ncreate_sweep_txs=\"\"\"\nCREATE TABLE IF NOT EXISTS sweep_txs (\nfunding_outpoint VARCHAR(34) NOT NULL,\nctn INTEGER NOT NULL,\nprevout VARCHAR(34),\ntx VARCHAR\n)\"\"\"\n\ncreate_channel_info=\"\"\"\nCREATE TABLE IF NOT EXISTS channel_info (\noutpoint VARCHAR(34) NOT NULL,\naddress VARCHAR(32),\nPRIMARY KEY(outpoint)\n)\"\"\"\n\n\nclass SweepStore(SqlDB):\n\n    def __init__(self, path, network):\n        super().__init__(network.asyncio_loop, path)\n\n    def create_database(self):\n        c = self.conn.cursor()\n        c.execute(create_channel_info)\n        c.execute(create_sweep_txs)\n        self.conn.commit()\n\n    @sql\n    def get_sweep_tx(self, funding_outpoint, prevout):\n        c = self.conn.cursor()\n        c.execute(\"SELECT tx FROM sweep_txs WHERE funding_outpoint=? AND prevout=?\", (funding_outpoint, prevout))\n        return [Transaction(r[0].hex()) for r in c.fetchall()]\n\n    @sql\n    def list_sweep_tx(self):\n        c = self.conn.cursor()\n        c.execute(\"SELECT funding_outpoint FROM sweep_txs\")\n        return set([r[0] for r in c.fetchall()])\n\n    @sql\n    def add_sweep_tx(self, funding_outpoint, ctn, prevout, raw_tx):\n        c = self.conn.cursor()\n        assert Transaction(raw_tx).is_complete()\n        c.execute(\"\"\"INSERT INTO sweep_txs (funding_outpoint, ctn, prevout, tx) VALUES (?,?,?,?)\"\"\", (funding_outpoint, ctn, prevout, bytes.fromhex(raw_tx)))\n        self.conn.commit()\n\n    @sql\n    def get_num_tx(self, funding_outpoint):\n        c = self.conn.cursor()\n        c.execute(\"SELECT count(*) FROM sweep_txs WHERE funding_outpoint=?\", (funding_outpoint,))\n        return int(c.fetchone()[0])\n\n    @sql\n    def get_ctn(self, outpoint, addr):\n        if not self._has_channel(outpoint):\n            self._add_channel(outpoint, addr)\n        c = self.conn.cursor()\n        c.execute(\"SELECT max(ctn) FROM sweep_txs WHERE funding_outpoint=?\", (outpoint,))\n        return int(c.fetchone()[0] or 0)\n\n    @sql\n    def remove_sweep_tx(self, funding_outpoint):\n        c = self.conn.cursor()\n        c.execute(\"DELETE FROM sweep_txs WHERE funding_outpoint=?\", (funding_outpoint,))\n        self.conn.commit()\n\n    def _add_channel(self, outpoint, address):\n        c = self.conn.cursor()\n        c.execute(\"INSERT INTO channel_info (address, outpoint) VALUES (?,?)\", (address, outpoint))\n        self.conn.commit()\n\n    @sql\n    def remove_channel(self, outpoint):\n        c = self.conn.cursor()\n        c.execute(\"DELETE FROM channel_info WHERE outpoint=?\", (outpoint,))\n        self.conn.commit()\n\n    def _has_channel(self, outpoint):\n        c = self.conn.cursor()\n        c.execute(\"SELECT * FROM channel_info WHERE outpoint=?\", (outpoint,))\n        r = c.fetchone()\n        return r is not None\n\n    @sql\n    def get_address(self, outpoint):\n        c = self.conn.cursor()\n        c.execute(\"SELECT address FROM channel_info WHERE outpoint=?\", (outpoint,))\n        r = c.fetchone()\n        return r[0] if r else None\n\n    @sql\n    def list_channels(self):\n        c = self.conn.cursor()\n        c.execute(\"SELECT outpoint, address FROM channel_info\")\n        return [(r[0], r[1]) for r in c.fetchall()]\n"
  },
  {
    "path": "electrum/qrreader/__init__.py",
    "content": "#!/usr/bin/env python3\n#\n# Electron Cash - lightweight Bitcoin client\n# Copyright (C) 2019 Axel Gembe <derago@gmail.com>\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n#\n# A module, that, given an image (buffer), finds and decodes a QR code in it.\n\nfrom typing import Optional\n\nfrom ..logging import get_logger\n\nfrom .abstract_base import AbstractQrCodeReader, QrCodeResult\n\n\n_logger = get_logger(__name__)\n\n\nclass MissingQrDetectionLib(RuntimeError):\n    ''' Raised if we can't find zbar or whatever other platform lib\n    we require to detect QR in image frames. '''\n\n\ndef get_qr_reader() -> AbstractQrCodeReader:\n    \"\"\"\n    Get the Qr code reader for the current platform.\n    Might raise exception: MissingQrDetectionLib.\n    \"\"\"\n    excs = []\n    try:\n        from .zbar import ZbarQrCodeReader\n        return ZbarQrCodeReader()\n        \"\"\"\n        # DEBUG CODE BELOW\n        # If you want to test this code on a platform that doesn't yet work or have\n        # zbar, use the below...\n        class Fake(AbstractQrCodeReader):\n            def read_qr_code(self, buffer, buffer_size, dummy, width, height, frame_id = -1):\n                ''' fake noop to test '''\n                return []\n        return Fake()\n        \"\"\"\n    except MissingLib as e:\n        _logger.exception(\"\")\n        excs.append(e)\n\n    raise MissingQrDetectionLib(f\"The platform QR detection library is not available.\\nerrors: {excs!r}\")\n\n\n# --- Internals below (not part of external API)\n\nclass MissingLib(RuntimeError):\n    ''' Raised by underlying implementation if missing libs '''\n    pass\n"
  },
  {
    "path": "electrum/qrreader/abstract_base.py",
    "content": "#!/usr/bin/env python3\n#\n# Electron Cash - lightweight Bitcoin client\n# Copyright (C) 2019 Axel Gembe <derago@gmail.com>\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport ctypes\nfrom typing import List, Tuple\nfrom abc import ABC, abstractmethod\n\nQrCodePoint = Tuple[int, int]\nQrCodePointList = List[QrCodePoint]\n\n\nclass QrCodeResult():\n    \"\"\"\n    A detected QR code.\n    \"\"\"\n    def __init__(self, data: str, center: QrCodePoint, points: QrCodePointList):\n        self.data: str = data\n        self.center: QrCodePoint = center\n        self.points: QrCodePointList = points\n\n    def __str__(self) -> str:\n        return 'data: {} center: {} points: {}'.format(self.data, self.center, self.points)\n\n    def __hash__(self):\n        return hash(self.data)\n\n    def __eq__(self, other):\n        return self.data == other.data\n\n    def __ne__(self, other):\n        return not self == other\n\nclass AbstractQrCodeReader(ABC):\n    \"\"\"\n    Abstract base class for QR code readers.\n    \"\"\"\n\n    def interval(self) -> float:\n        ''' Reimplement to specify a time (in seconds) that the implementation\n        recommends elapse between subsequent calls to read_qr_code.\n        Implementations that have very expensive and/or slow detection code\n        may want to rate-limit read_qr_code calls by overriding this function.\n        e.g.: to make detection happen every 200ms, you would return 0.2 here.\n        Defaults to 0.0'''\n        return 0.0\n\n    @abstractmethod\n    def read_qr_code(self, buffer: ctypes.c_void_p,\n                     buffer_size: int,  # overall image size in bytes\n                     rowlen_bytes: int, # the scan line length in bytes. (many libs, such as OSX, expect this value to properly grok image data)\n                     width: int, height: int, frame_id: int = -1) -> List[QrCodeResult]:\n        \"\"\"\n        Reads a QR code from an image buffer in Y800 / GREY format.\n        Returns a list of detected QR codes which includes their data and positions.\n        \"\"\"\n"
  },
  {
    "path": "electrum/qrreader/zbar.py",
    "content": "#!/usr/bin/env python3\n#\n# Electron Cash - lightweight Bitcoin client\n# Copyright (C) 2019 Axel Gembe <derago@gmail.com>\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport sys\nimport ctypes\nimport os\nfrom typing import List\nfrom enum import IntEnum\n\nfrom ..logging import get_logger\n\nfrom . import MissingLib\nfrom .abstract_base import AbstractQrCodeReader, QrCodeResult\n\n\n_logger = get_logger(__name__)\n\nif 'ANDROID_DATA' in os.environ:\n    LIBNAME = 'libzbar.so'\nelif sys.platform == 'darwin':\n    LIBNAME = 'libzbar.0.dylib'\nelif sys.platform in ('windows', 'win32'):\n    LIBNAME = 'libzbar-0.dll'\nelse:\n    LIBNAME = 'libzbar.so.0'\n\ntry:\n    try:\n        LIBZBAR = ctypes.cdll.LoadLibrary(os.path.join(os.path.dirname(__file__), '..', LIBNAME))\n    except OSError as e:\n        LIBZBAR = ctypes.cdll.LoadLibrary(LIBNAME)\n\n    LIBZBAR.zbar_image_create.restype = ctypes.c_void_p\n    LIBZBAR.zbar_image_scanner_create.restype = ctypes.c_void_p\n    LIBZBAR.zbar_image_scanner_get_results.restype = ctypes.c_void_p\n    LIBZBAR.zbar_symbol_set_first_symbol.restype = ctypes.c_void_p\n    LIBZBAR.zbar_symbol_get_data.restype = ctypes.POINTER(ctypes.c_char_p)\n    LIBZBAR.zbar_image_scanner_set_config.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int]\n    LIBZBAR.zbar_image_set_sequence.argtypes = [ctypes.c_void_p, ctypes.c_int]\n    LIBZBAR.zbar_image_set_size.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int]\n    LIBZBAR.zbar_image_set_format.argtypes = [ctypes.c_void_p, ctypes.c_int]\n    LIBZBAR.zbar_image_set_data.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]\n    LIBZBAR.zbar_image_scanner_recycle_image.argtypes = [ctypes.c_void_p, ctypes.c_void_p]\n    LIBZBAR.zbar_scan_image.argtypes = [ctypes.c_void_p, ctypes.c_void_p]\n    LIBZBAR.zbar_image_scanner_get_results.argtypes = [ctypes.c_void_p]\n    LIBZBAR.zbar_symbol_set_first_symbol.argtypes = [ctypes.c_void_p]\n    LIBZBAR.zbar_symbol_get_data_length.argtypes = [ctypes.c_void_p]\n    LIBZBAR.zbar_symbol_get_data.argtypes = [ctypes.c_void_p]\n    LIBZBAR.zbar_symbol_get_loc_size.argtypes = [ctypes.c_void_p]\n    LIBZBAR.zbar_symbol_get_loc_x.argtypes = [ctypes.c_void_p, ctypes.c_int]\n    LIBZBAR.zbar_symbol_get_loc_y.argtypes = [ctypes.c_void_p, ctypes.c_int]\n    LIBZBAR.zbar_symbol_next.argtypes = [ctypes.c_void_p]\n    LIBZBAR.zbar_image_scanner_destroy.argtypes = [ctypes.c_void_p]\n    LIBZBAR.zbar_image_destroy.argtypes = [ctypes.c_void_p]\n\n    #if is_verbose:\n        #LIBZBAR.zbar_set_verbosity(100)\nexcept OSError:\n    _logger.exception(\"Failed to load zbar\")\n    LIBZBAR = None\n\nFOURCC_Y800 = 0x30303859\n\n@ctypes.CFUNCTYPE(None, ctypes.c_void_p)\ndef zbar_cleanup(image):\n    \"\"\"\n    Do nothing, this is just so zbar doesn't try to manage our QImage buffers\n    \"\"\"\n\nclass ZbarSymbolType(IntEnum):\n    \"\"\"\n    Supported symbol types, see zbar_symbol_type_e in zbar.h\n    \"\"\"\n    EAN2 = 2\n    EAN5 = 5\n    EAN8 = 8\n    UPCE = 9\n    ISBN10 = 10\n    UPCA = 12\n    EAN13 = 13\n    ISBN13 = 14\n    COMPOSITE = 15\n    I25 = 25\n    DATABAR = 34\n    DATABAR_EXP = 35\n    CODABAR = 38\n    CODE39 = 39\n    PDF417 = 57\n    QRCODE = 64\n    SQCODE = 80\n    CODE93 = 93\n    CODE128 = 128\n\nclass ZbarConfig(IntEnum):\n    \"\"\"\n    Supported configuration options, see zbar_config_e in zbar.h\n    \"\"\"\n    ENABLE = 0\n\nclass ZbarQrCodeReader(AbstractQrCodeReader):\n    \"\"\"\n    Reader that uses libzbar\n    \"\"\"\n\n    def __init__(self):\n        if not LIBZBAR:\n            raise MissingLib('Zbar library not found')\n        # Set up zbar\n        self.zbar_scanner = LIBZBAR.zbar_image_scanner_create()\n        self.zbar_image = LIBZBAR.zbar_image_create()\n\n        # Disable all symbols\n        for sym_type in ZbarSymbolType:\n            LIBZBAR.zbar_image_scanner_set_config(self.zbar_scanner, sym_type, ZbarConfig.ENABLE, 0)\n\n        # Enable only QR codes\n        LIBZBAR.zbar_image_scanner_set_config(self.zbar_scanner, ZbarSymbolType.QRCODE,\n                                              ZbarConfig.ENABLE, 1)\n\n    def __del__(self):\n        if LIBZBAR:\n            LIBZBAR.zbar_image_scanner_destroy(self.zbar_scanner)\n            LIBZBAR.zbar_image_destroy(self.zbar_image)\n\n    def read_qr_code(self, buffer: ctypes.c_void_p, buffer_size: int,\n                     rowlen_bytes: int,  # this param is ignored in this implementation\n                     width: int, height: int, frame_id: int = -1) -> List[QrCodeResult]:\n        LIBZBAR.zbar_image_set_sequence(self.zbar_image, frame_id)\n        LIBZBAR.zbar_image_set_size(self.zbar_image, width, height)\n        LIBZBAR.zbar_image_set_format(self.zbar_image, FOURCC_Y800)\n        LIBZBAR.zbar_image_set_data(self.zbar_image, buffer, buffer_size, zbar_cleanup)\n        LIBZBAR.zbar_image_scanner_recycle_image(self.zbar_scanner, self.zbar_image)\n        LIBZBAR.zbar_scan_image(self.zbar_scanner, self.zbar_image)\n\n        result_set = LIBZBAR.zbar_image_scanner_get_results(self.zbar_scanner)\n\n        res = []\n        symbol = LIBZBAR.zbar_symbol_set_first_symbol(result_set)\n        while symbol:\n            symbol_data_len = LIBZBAR.zbar_symbol_get_data_length(symbol)\n            symbol_data_ptr = LIBZBAR.zbar_symbol_get_data(symbol)\n            symbol_data_bytes = ctypes.string_at(symbol_data_ptr, symbol_data_len)\n            symbol_data = symbol_data_bytes.decode('utf-8')\n\n            symbol_loc = []\n            symbol_loc_len = LIBZBAR.zbar_symbol_get_loc_size(symbol)\n            for i in range(0, symbol_loc_len):\n                # Normalize the coordinates into 0..1 range by dividing by width / height\n                symbol_loc_x = LIBZBAR.zbar_symbol_get_loc_x(symbol, i)\n                symbol_loc_y = LIBZBAR.zbar_symbol_get_loc_y(symbol, i)\n                symbol_loc.append((symbol_loc_x, symbol_loc_y))\n\n            # Find the center by getting the average values of the corners x and y coordinates\n            symbol_loc_sum_x = sum([l[0] for l in symbol_loc])\n            symbol_loc_sum_y = sum([l[1] for l in symbol_loc])\n            symbol_loc_center = (int(symbol_loc_sum_x / symbol_loc_len), int(symbol_loc_sum_y / symbol_loc_len))\n\n            res.append(QrCodeResult(symbol_data, symbol_loc_center, symbol_loc))\n\n            symbol = LIBZBAR.zbar_symbol_next(symbol)\n\n        return res\n"
  },
  {
    "path": "electrum/qrscanner.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n#\n# A QR scanner that uses zbar (via ctypes)\n# - to access the camera,\n# - and to find and decode QR codes (visible in the live feed).\n\nimport os\nimport sys\nimport ctypes\nfrom typing import Optional, Mapping\n\nfrom .util import UserFacingException\nfrom .i18n import _\nfrom .logging import get_logger\n\n\n_logger = get_logger(__name__)\n\n\nif sys.platform == 'darwin':\n    name = 'libzbar.0.dylib'\nelif sys.platform in ('windows', 'win32'):\n    name = 'libzbar-0.dll'\nelse:\n    name = 'libzbar.so.0'\n\ntry:\n    libzbar = ctypes.cdll.LoadLibrary(os.path.join(os.path.dirname(__file__), name))\nexcept BaseException as e1:\n    try:\n        libzbar = ctypes.cdll.LoadLibrary(name)\n    except BaseException as e2:\n        libzbar = None\n        _logger.error(f\"failed to load zbar. exceptions: {[e1,e2]!r}\")\n\n\ndef scan_barcode(device='', timeout=-1, display=True, threaded=False) -> Optional[str]:\n    if libzbar is None:\n        raise UserFacingException(_('Cannot start QR scanner: zbar not available.'))\n    libzbar.zbar_symbol_get_data.restype = ctypes.c_char_p\n    libzbar.zbar_processor_create.restype = ctypes.POINTER(ctypes.c_int)\n    libzbar.zbar_processor_get_results.restype = ctypes.POINTER(ctypes.c_int)\n    libzbar.zbar_symbol_set_first_symbol.restype = ctypes.POINTER(ctypes.c_int)\n    # libzbar.zbar_set_verbosity(100)  # verbose logs for debugging\n    proc = libzbar.zbar_processor_create(threaded)\n    libzbar.zbar_processor_request_size(proc, 640, 480)\n    if libzbar.zbar_processor_init(proc, device.encode('utf-8'), display) != 0:\n        raise UserFacingException(\n            _(\"Cannot start QR scanner: initialization failed.\") + \"\\n\" +\n            _(\"Make sure you have a camera connected and enabled.\"))\n    libzbar.zbar_processor_set_visible(proc)\n    if libzbar.zbar_process_one(proc, timeout):\n        symbols = libzbar.zbar_processor_get_results(proc)\n    else:\n        symbols = None\n    libzbar.zbar_processor_destroy(proc)\n    if symbols is None:\n        return\n    if not libzbar.zbar_symbol_set_get_size(symbols):\n        return\n    symbol = libzbar.zbar_symbol_set_first_symbol(symbols)\n    data = libzbar.zbar_symbol_get_data(symbol)\n    return data.decode('utf8')\n\n\ndef find_system_cameras() -> Mapping[str, str]:\n    device_root = \"/sys/class/video4linux\"\n    devices = {} # Name -> device\n    if os.path.exists(device_root):\n        for device in os.listdir(device_root):\n            path = os.path.join(device_root, device, 'name')\n            try:\n                with open(path, encoding='utf-8') as f:\n                    name = f.read()\n            except Exception:\n                continue\n            name = name.strip('\\n')\n            devices[name] = os.path.join(\"/dev\", device)\n    return devices\n\n\ndef version_info() -> Mapping[str, Optional[str]]:\n    return {\n        \"libzbar.path\": libzbar._name if libzbar else None,\n    }\n\n\nif __name__ == \"__main__\":\n    print(scan_barcode())\n"
  },
  {
    "path": "electrum/ripemd.py",
    "content": "## ripemd.py - pure Python implementation of the RIPEMD-160 algorithm.\n## Bjorn Edstrom <be@bjrn.se> 16 december 2007.\n##\n## Copyrights\n## ==========\n##\n## This code is a derived from an implementation by Markus Friedl which is\n## subject to the following license. This Python implementation is not\n## subject to any other license.\n##\n##/*\n## * Copyright (c) 2001 Markus Friedl.  All rights reserved.\n## *\n## * Redistribution and use in source and binary forms, with or without\n## * modification, are permitted provided that the following conditions\n## * are met:\n## * 1. Redistributions of source code must retain the above copyright\n## *    notice, this list of conditions and the following disclaimer.\n## * 2. Redistributions in binary form must reproduce the above copyright\n## *    notice, this list of conditions and the following disclaimer in the\n## *    documentation and/or other materials provided with the distribution.\n## *\n## * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n## * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n## * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n## * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n## * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n## * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n## * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n## * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n## * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n## * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n## */\n##/*\n## * Preneel, Bosselaers, Dobbertin, \"The Cryptographic Hash Function RIPEMD-160\",\n## * RSA Laboratories, CryptoBytes, Volume 3, Number 2, Autumn 1997,\n## * ftp://ftp.rsasecurity.com/pub/cryptobytes/crypto3n2.pdf\n## */\n\n#block_size = 1\ndigest_size = 20\ndigestsize = 20\n\nclass RIPEMD160:\n    \"\"\"Return a new RIPEMD160 object. An optional string argument\n    may be provided; if present, this string will be automatically\n    hashed.\"\"\"\n\n    def __init__(self, arg=None):\n        self.ctx = RMDContext()\n        if arg:\n            self.update(arg)\n        self.dig = None\n\n    def update(self, arg):\n        \"\"\"update(arg)\"\"\"\n        RMD160Update(self.ctx, arg, len(arg))\n        self.dig = None\n\n    def digest(self):\n        \"\"\"digest()\"\"\"\n        if self.dig:\n            return self.dig\n        ctx = self.ctx.copy()\n        self.dig = RMD160Final(self.ctx)\n        self.ctx = ctx\n        return self.dig\n\n    def hexdigest(self):\n        \"\"\"hexdigest()\"\"\"\n        dig = self.digest()\n        hex_digest = ''\n        for d in dig:\n            hex_digest += '%02x' % d\n        return hex_digest\n\n    def copy(self):\n        \"\"\"copy()\"\"\"\n        import copy\n        return copy.deepcopy(self)\n\n\n\ndef new(arg=None):\n    \"\"\"Return a new RIPEMD160 object. An optional string argument\n    may be provided; if present, this string will be automatically\n    hashed.\"\"\"\n    return RIPEMD160(arg)\n\n\n\n#\n# Private.\n#\n\nclass RMDContext:\n    def __init__(self):\n        self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE,\n                      0x10325476, 0xC3D2E1F0] # uint32\n        self.count = 0 # uint64\n        self.buffer = [0]*64 # uchar\n    def copy(self):\n        ctx = RMDContext()\n        ctx.state = self.state[:]\n        ctx.count = self.count\n        ctx.buffer = self.buffer[:]\n        return ctx\n\nK0 = 0x00000000\nK1 = 0x5A827999\nK2 = 0x6ED9EBA1\nK3 = 0x8F1BBCDC\nK4 = 0xA953FD4E\n\nKK0 = 0x50A28BE6\nKK1 = 0x5C4DD124\nKK2 = 0x6D703EF3\nKK3 = 0x7A6D76E9\nKK4 = 0x00000000\n\ndef ROL(n, x):\n    return ((x << n) & 0xffffffff) | (x >> (32 - n))\n\ndef F0(x, y, z):\n    return x ^ y ^ z\n\ndef F1(x, y, z):\n    return (x & y) | (((~x) % 0x100000000) & z)\n\ndef F2(x, y, z):\n    return (x | ((~y) % 0x100000000)) ^ z\n\ndef F3(x, y, z):\n    return (x & z) | (((~z) % 0x100000000) & y)\n\ndef F4(x, y, z):\n    return x ^ (y | ((~z) % 0x100000000))\n\ndef R(a, b, c, d, e, Fj, Kj, sj, rj, X):\n    a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e\n    c = ROL(10, c)\n    return a % 0x100000000, c\n\nPADDING = [0x80] + [0]*63\n\nimport sys\nimport struct\n\ndef RMD160Transform(state, block): #uint32 state[5], uchar block[64]\n    x = [0]*16\n    if sys.byteorder == 'little':\n        x = struct.unpack('<16L', bytes([x for x in block[0:64]]))\n    else:\n        raise Exception(f\"unsupported {sys.byteorder=!r}\")\n    a = state[0]\n    b = state[1]\n    c = state[2]\n    d = state[3]\n    e = state[4]\n\n    #/* Round 1 */\n    a, c = R(a, b, c, d, e, F0, K0, 11,  0, x)\n    e, b = R(e, a, b, c, d, F0, K0, 14,  1, x)\n    d, a = R(d, e, a, b, c, F0, K0, 15,  2, x)\n    c, e = R(c, d, e, a, b, F0, K0, 12,  3, x)\n    b, d = R(b, c, d, e, a, F0, K0,  5,  4, x)\n    a, c = R(a, b, c, d, e, F0, K0,  8,  5, x)\n    e, b = R(e, a, b, c, d, F0, K0,  7,  6, x)\n    d, a = R(d, e, a, b, c, F0, K0,  9,  7, x)\n    c, e = R(c, d, e, a, b, F0, K0, 11,  8, x)\n    b, d = R(b, c, d, e, a, F0, K0, 13,  9, x)\n    a, c = R(a, b, c, d, e, F0, K0, 14, 10, x)\n    e, b = R(e, a, b, c, d, F0, K0, 15, 11, x)\n    d, a = R(d, e, a, b, c, F0, K0,  6, 12, x)\n    c, e = R(c, d, e, a, b, F0, K0,  7, 13, x)\n    b, d = R(b, c, d, e, a, F0, K0,  9, 14, x)\n    a, c = R(a, b, c, d, e, F0, K0,  8, 15, x)  #/* #15 */\n    #/* Round 2 */\n    e, b = R(e, a, b, c, d, F1, K1,  7,  7, x)\n    d, a = R(d, e, a, b, c, F1, K1,  6,  4, x)\n    c, e = R(c, d, e, a, b, F1, K1,  8, 13, x)\n    b, d = R(b, c, d, e, a, F1, K1, 13,  1, x)\n    a, c = R(a, b, c, d, e, F1, K1, 11, 10, x)\n    e, b = R(e, a, b, c, d, F1, K1,  9,  6, x)\n    d, a = R(d, e, a, b, c, F1, K1,  7, 15, x)\n    c, e = R(c, d, e, a, b, F1, K1, 15,  3, x)\n    b, d = R(b, c, d, e, a, F1, K1,  7, 12, x)\n    a, c = R(a, b, c, d, e, F1, K1, 12,  0, x)\n    e, b = R(e, a, b, c, d, F1, K1, 15,  9, x)\n    d, a = R(d, e, a, b, c, F1, K1,  9,  5, x)\n    c, e = R(c, d, e, a, b, F1, K1, 11,  2, x)\n    b, d = R(b, c, d, e, a, F1, K1,  7, 14, x)\n    a, c = R(a, b, c, d, e, F1, K1, 13, 11, x)\n    e, b = R(e, a, b, c, d, F1, K1, 12,  8, x)  #/* #31 */\n    #/* Round 3 */\n    d, a = R(d, e, a, b, c, F2, K2, 11,  3, x)\n    c, e = R(c, d, e, a, b, F2, K2, 13, 10, x)\n    b, d = R(b, c, d, e, a, F2, K2,  6, 14, x)\n    a, c = R(a, b, c, d, e, F2, K2,  7,  4, x)\n    e, b = R(e, a, b, c, d, F2, K2, 14,  9, x)\n    d, a = R(d, e, a, b, c, F2, K2,  9, 15, x)\n    c, e = R(c, d, e, a, b, F2, K2, 13,  8, x)\n    b, d = R(b, c, d, e, a, F2, K2, 15,  1, x)\n    a, c = R(a, b, c, d, e, F2, K2, 14,  2, x)\n    e, b = R(e, a, b, c, d, F2, K2,  8,  7, x)\n    d, a = R(d, e, a, b, c, F2, K2, 13,  0, x)\n    c, e = R(c, d, e, a, b, F2, K2,  6,  6, x)\n    b, d = R(b, c, d, e, a, F2, K2,  5, 13, x)\n    a, c = R(a, b, c, d, e, F2, K2, 12, 11, x)\n    e, b = R(e, a, b, c, d, F2, K2,  7,  5, x)\n    d, a = R(d, e, a, b, c, F2, K2,  5, 12, x)  #/* #47 */\n    #/* Round 4 */\n    c, e = R(c, d, e, a, b, F3, K3, 11,  1, x)\n    b, d = R(b, c, d, e, a, F3, K3, 12,  9, x)\n    a, c = R(a, b, c, d, e, F3, K3, 14, 11, x)\n    e, b = R(e, a, b, c, d, F3, K3, 15, 10, x)\n    d, a = R(d, e, a, b, c, F3, K3, 14,  0, x)\n    c, e = R(c, d, e, a, b, F3, K3, 15,  8, x)\n    b, d = R(b, c, d, e, a, F3, K3,  9, 12, x)\n    a, c = R(a, b, c, d, e, F3, K3,  8,  4, x)\n    e, b = R(e, a, b, c, d, F3, K3,  9, 13, x)\n    d, a = R(d, e, a, b, c, F3, K3, 14,  3, x)\n    c, e = R(c, d, e, a, b, F3, K3,  5,  7, x)\n    b, d = R(b, c, d, e, a, F3, K3,  6, 15, x)\n    a, c = R(a, b, c, d, e, F3, K3,  8, 14, x)\n    e, b = R(e, a, b, c, d, F3, K3,  6,  5, x)\n    d, a = R(d, e, a, b, c, F3, K3,  5,  6, x)\n    c, e = R(c, d, e, a, b, F3, K3, 12,  2, x)  #/* #63 */\n    #/* Round 5 */\n    b, d = R(b, c, d, e, a, F4, K4,  9,  4, x)\n    a, c = R(a, b, c, d, e, F4, K4, 15,  0, x)\n    e, b = R(e, a, b, c, d, F4, K4,  5,  5, x)\n    d, a = R(d, e, a, b, c, F4, K4, 11,  9, x)\n    c, e = R(c, d, e, a, b, F4, K4,  6,  7, x)\n    b, d = R(b, c, d, e, a, F4, K4,  8, 12, x)\n    a, c = R(a, b, c, d, e, F4, K4, 13,  2, x)\n    e, b = R(e, a, b, c, d, F4, K4, 12, 10, x)\n    d, a = R(d, e, a, b, c, F4, K4,  5, 14, x)\n    c, e = R(c, d, e, a, b, F4, K4, 12,  1, x)\n    b, d = R(b, c, d, e, a, F4, K4, 13,  3, x)\n    a, c = R(a, b, c, d, e, F4, K4, 14,  8, x)\n    e, b = R(e, a, b, c, d, F4, K4, 11, 11, x)\n    d, a = R(d, e, a, b, c, F4, K4,  8,  6, x)\n    c, e = R(c, d, e, a, b, F4, K4,  5, 15, x)\n    b, d = R(b, c, d, e, a, F4, K4,  6, 13, x)  #/* #79 */\n\n    aa = a\n    bb = b\n    cc = c\n    dd = d\n    ee = e\n\n    a = state[0]\n    b = state[1]\n    c = state[2]\n    d = state[3]\n    e = state[4]\n\n    #/* Parallel round 1 */\n    a, c = R(a, b, c, d, e, F4, KK0,  8,  5, x)\n    e, b = R(e, a, b, c, d, F4, KK0,  9, 14, x)\n    d, a = R(d, e, a, b, c, F4, KK0,  9,  7, x)\n    c, e = R(c, d, e, a, b, F4, KK0, 11,  0, x)\n    b, d = R(b, c, d, e, a, F4, KK0, 13,  9, x)\n    a, c = R(a, b, c, d, e, F4, KK0, 15,  2, x)\n    e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x)\n    d, a = R(d, e, a, b, c, F4, KK0,  5,  4, x)\n    c, e = R(c, d, e, a, b, F4, KK0,  7, 13, x)\n    b, d = R(b, c, d, e, a, F4, KK0,  7,  6, x)\n    a, c = R(a, b, c, d, e, F4, KK0,  8, 15, x)\n    e, b = R(e, a, b, c, d, F4, KK0, 11,  8, x)\n    d, a = R(d, e, a, b, c, F4, KK0, 14,  1, x)\n    c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x)\n    b, d = R(b, c, d, e, a, F4, KK0, 12,  3, x)\n    a, c = R(a, b, c, d, e, F4, KK0,  6, 12, x) #/* #15 */\n    #/* Parallel round 2 */\n    e, b = R(e, a, b, c, d, F3, KK1,  9,  6, x)\n    d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x)\n    c, e = R(c, d, e, a, b, F3, KK1, 15,  3, x)\n    b, d = R(b, c, d, e, a, F3, KK1,  7,  7, x)\n    a, c = R(a, b, c, d, e, F3, KK1, 12,  0, x)\n    e, b = R(e, a, b, c, d, F3, KK1,  8, 13, x)\n    d, a = R(d, e, a, b, c, F3, KK1,  9,  5, x)\n    c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x)\n    b, d = R(b, c, d, e, a, F3, KK1,  7, 14, x)\n    a, c = R(a, b, c, d, e, F3, KK1,  7, 15, x)\n    e, b = R(e, a, b, c, d, F3, KK1, 12,  8, x)\n    d, a = R(d, e, a, b, c, F3, KK1,  7, 12, x)\n    c, e = R(c, d, e, a, b, F3, KK1,  6,  4, x)\n    b, d = R(b, c, d, e, a, F3, KK1, 15,  9, x)\n    a, c = R(a, b, c, d, e, F3, KK1, 13,  1, x)\n    e, b = R(e, a, b, c, d, F3, KK1, 11,  2, x) #/* #31 */\n    #/* Parallel round 3 */\n    d, a = R(d, e, a, b, c, F2, KK2,  9, 15, x)\n    c, e = R(c, d, e, a, b, F2, KK2,  7,  5, x)\n    b, d = R(b, c, d, e, a, F2, KK2, 15,  1, x)\n    a, c = R(a, b, c, d, e, F2, KK2, 11,  3, x)\n    e, b = R(e, a, b, c, d, F2, KK2,  8,  7, x)\n    d, a = R(d, e, a, b, c, F2, KK2,  6, 14, x)\n    c, e = R(c, d, e, a, b, F2, KK2,  6,  6, x)\n    b, d = R(b, c, d, e, a, F2, KK2, 14,  9, x)\n    a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x)\n    e, b = R(e, a, b, c, d, F2, KK2, 13,  8, x)\n    d, a = R(d, e, a, b, c, F2, KK2,  5, 12, x)\n    c, e = R(c, d, e, a, b, F2, KK2, 14,  2, x)\n    b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x)\n    a, c = R(a, b, c, d, e, F2, KK2, 13,  0, x)\n    e, b = R(e, a, b, c, d, F2, KK2,  7,  4, x)\n    d, a = R(d, e, a, b, c, F2, KK2,  5, 13, x) #/* #47 */\n    #/* Parallel round 4 */\n    c, e = R(c, d, e, a, b, F1, KK3, 15,  8, x)\n    b, d = R(b, c, d, e, a, F1, KK3,  5,  6, x)\n    a, c = R(a, b, c, d, e, F1, KK3,  8,  4, x)\n    e, b = R(e, a, b, c, d, F1, KK3, 11,  1, x)\n    d, a = R(d, e, a, b, c, F1, KK3, 14,  3, x)\n    c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x)\n    b, d = R(b, c, d, e, a, F1, KK3,  6, 15, x)\n    a, c = R(a, b, c, d, e, F1, KK3, 14,  0, x)\n    e, b = R(e, a, b, c, d, F1, KK3,  6,  5, x)\n    d, a = R(d, e, a, b, c, F1, KK3,  9, 12, x)\n    c, e = R(c, d, e, a, b, F1, KK3, 12,  2, x)\n    b, d = R(b, c, d, e, a, F1, KK3,  9, 13, x)\n    a, c = R(a, b, c, d, e, F1, KK3, 12,  9, x)\n    e, b = R(e, a, b, c, d, F1, KK3,  5,  7, x)\n    d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x)\n    c, e = R(c, d, e, a, b, F1, KK3,  8, 14, x) #/* #63 */\n    #/* Parallel round 5 */\n    b, d = R(b, c, d, e, a, F0, KK4,  8, 12, x)\n    a, c = R(a, b, c, d, e, F0, KK4,  5, 15, x)\n    e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x)\n    d, a = R(d, e, a, b, c, F0, KK4,  9,  4, x)\n    c, e = R(c, d, e, a, b, F0, KK4, 12,  1, x)\n    b, d = R(b, c, d, e, a, F0, KK4,  5,  5, x)\n    a, c = R(a, b, c, d, e, F0, KK4, 14,  8, x)\n    e, b = R(e, a, b, c, d, F0, KK4,  6,  7, x)\n    d, a = R(d, e, a, b, c, F0, KK4,  8,  6, x)\n    c, e = R(c, d, e, a, b, F0, KK4, 13,  2, x)\n    b, d = R(b, c, d, e, a, F0, KK4,  6, 13, x)\n    a, c = R(a, b, c, d, e, F0, KK4,  5, 14, x)\n    e, b = R(e, a, b, c, d, F0, KK4, 15,  0, x)\n    d, a = R(d, e, a, b, c, F0, KK4, 13,  3, x)\n    c, e = R(c, d, e, a, b, F0, KK4, 11,  9, x)\n    b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) #/* #79 */\n\n    t = (state[1] + cc + d) % 0x100000000\n    state[1] = (state[2] + dd + e) % 0x100000000\n    state[2] = (state[3] + ee + a) % 0x100000000\n    state[3] = (state[4] + aa + b) % 0x100000000\n    state[4] = (state[0] + bb + c) % 0x100000000\n    state[0] = t % 0x100000000\n\n    pass\n\n\ndef RMD160Update(ctx, inp, inplen):\n    if type(inp) == str:\n        inp = [ord(i)&0xff for i in inp]\n\n    have = (ctx.count // 8) % 64\n    need = 64 - have\n    ctx.count += 8 * inplen\n    off = 0\n    if inplen >= need:\n        if have:\n            for i in range(need):\n                ctx.buffer[have+i] = inp[i]\n            RMD160Transform(ctx.state, ctx.buffer)\n            off = need\n            have = 0\n        while off + 64 <= inplen:\n            RMD160Transform(ctx.state, inp[off:]) #<---\n            off += 64\n    if off < inplen:\n        # memcpy(ctx->buffer + have, input+off, len-off);\n        for i in range(inplen - off):\n            ctx.buffer[have+i] = inp[off+i]\n\ndef RMD160Final(ctx):\n    size = struct.pack(\"<Q\", ctx.count)\n    padlen = 64 - ((ctx.count // 8) % 64)\n    if padlen < 1+8:\n        padlen += 64\n    RMD160Update(ctx, PADDING, padlen-8)\n    RMD160Update(ctx, size, 8)\n    return struct.pack(\"<5L\", *ctx.state)\n\n\nassert '37f332f68db77bd9d7edd4969571ad671cf9dd3b' == \\\n       new(b'The quick brown fox jumps over the lazy dog').hexdigest()\nassert '132072df690933835eb8b6ad0b77e7b6f14acad7' == \\\n       new(b'The quick brown fox jumps over the lazy cog').hexdigest()\nassert '9c1185a5c5e9fc54612808977ee8f548b2258d31' == \\\n       new('').hexdigest()\n"
  },
  {
    "path": "electrum/rsakey.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n# This module uses functions from TLSLite (public domain)\n#\n# TLSLite Authors:\n#   Trevor Perrin\n#   Martin von Loewis - python 3 port\n#   Yngve Pettersen (ported by Paul Sokolovsky) - TLS 1.2\n#\n\n\"\"\"Pure-Python RSA implementation.\"\"\"\n\nimport os\nimport math\nimport hashlib\n\n\ndef SHA1(x):\n    return hashlib.sha1(x).digest()\n\n\n# **************************************************************************\n# PRNG Functions\n# **************************************************************************\n\n\ndef getRandomBytes(howMany):\n    b = bytearray(os.urandom(howMany))\n    assert len(b) == howMany\n    return b\n\nprngName = \"os.urandom\"\n\n\n# **************************************************************************\n# Converter Functions\n# **************************************************************************\n\ndef bytesToNumber(b):\n    total = 0\n    multiplier = 1\n    for count in range(len(b)-1, -1, -1):\n        byte = b[count]\n        total += multiplier * byte\n        multiplier *= 256\n    return total\n\ndef numberToByteArray(n, howManyBytes=None):\n    \"\"\"Convert an integer into a bytearray, zero-pad to howManyBytes.\n\n    The returned bytearray may be smaller than howManyBytes, but will\n    not be larger.  The returned bytearray will contain a big-endian\n    encoding of the input integer (n).\n    \"\"\"\n    if howManyBytes is None:\n        howManyBytes = numBytes(n)\n    b = bytearray(howManyBytes)\n    for count in range(howManyBytes-1, -1, -1):\n        b[count] = int(n % 256)\n        n >>= 8\n    return b\n\ndef mpiToNumber(mpi): #mpi is an openssl-format bignum string\n    if (ord(mpi[4]) & 0x80) !=0: #Make sure this is a positive number\n        raise AssertionError()\n    b = bytearray(mpi[4:])\n    return bytesToNumber(b)\n\ndef numberToMPI(n):\n    b = numberToByteArray(n)\n    ext = 0\n    #If the high-order bit is going to be set,\n    #add an extra byte of zeros\n    if (numBits(n) & 0x7)==0:\n        ext = 1\n    length = numBytes(n) + ext\n    b = bytearray(4+ext) + b\n    b[0] = (length >> 24) & 0xFF\n    b[1] = (length >> 16) & 0xFF\n    b[2] = (length >> 8) & 0xFF\n    b[3] = length & 0xFF\n    return bytes(b)\n\n\n# **************************************************************************\n# Misc. Utility Functions\n# **************************************************************************\n\ndef numBits(n):\n    if n==0:\n        return 0\n    s = \"%x\" % n\n    return ((len(s)-1)*4) + \\\n    {'0':0, '1':1, '2':2, '3':2,\n     '4':3, '5':3, '6':3, '7':3,\n     '8':4, '9':4, 'a':4, 'b':4,\n     'c':4, 'd':4, 'e':4, 'f':4,\n     }[s[0]]\n\ndef numBytes(n):\n    if n==0:\n        return 0\n    bits = numBits(n)\n    return int(math.ceil(bits / 8.0))\n\n# **************************************************************************\n# Big Number Math\n# **************************************************************************\n\ndef getRandomNumber(low, high):\n    if low >= high:\n        raise AssertionError()\n    howManyBits = numBits(high)\n    howManyBytes = numBytes(high)\n    lastBits = howManyBits % 8\n    while 1:\n        bytes = getRandomBytes(howManyBytes)\n        if lastBits:\n            bytes[0] = bytes[0] % (1 << lastBits)\n        n = bytesToNumber(bytes)\n        if n >= low and n < high:\n            return n\n\ndef gcd(a,b):\n    a, b = max(a,b), min(a,b)\n    while b:\n        a, b = b, a % b\n    return a\n\ndef lcm(a, b):\n    return (a * b) // gcd(a, b)\n\n#Returns inverse of a mod b, zero if none\n#Uses Extended Euclidean Algorithm\ndef invMod(a, b):\n    c, d = a, b\n    uc, ud = 1, 0\n    while c != 0:\n        q = d // c\n        c, d = d-(q*c), c\n        uc, ud = ud - (q * uc), uc\n    if d == 1:\n        return ud % b\n    return 0\n\n\ndef powMod(base, power, modulus):\n    if power < 0:\n        result = pow(base, power*-1, modulus)\n        result = invMod(result, modulus)\n        return result\n    else:\n        return pow(base, power, modulus)\n\n#Pre-calculate a sieve of the ~100 primes < 1000:\ndef makeSieve(n):\n    sieve = list(range(n))\n    for count in range(2, int(math.sqrt(n))+1):\n        if sieve[count] == 0:\n            continue\n        x = sieve[count] * 2\n        while x < len(sieve):\n            sieve[x] = 0\n            x += sieve[count]\n    sieve = [x for x in sieve[2:] if x]\n    return sieve\n\nsieve = makeSieve(1000)\n\ndef isPrime(n, iterations=5, display=False):\n    #Trial division with sieve\n    for x in sieve:\n        if x >= n: return True\n        if n % x == 0: return False\n    #Passed trial division, proceed to Rabin-Miller\n    #Rabin-Miller implemented per Ferguson & Schneier\n    #Compute s, t for Rabin-Miller\n    if display: print(\"*\", end=' ')\n    s, t = n-1, 0\n    while s % 2 == 0:\n        s, t = s//2, t+1\n    #Repeat Rabin-Miller x times\n    a = 2 #Use 2 as a base for first iteration speedup, per HAC\n    for count in range(iterations):\n        v = powMod(a, s, n)\n        if v==1:\n            continue\n        i = 0\n        while v != n-1:\n            if i == t-1:\n                return False\n            else:\n                v, i = powMod(v, 2, n), i+1\n        a = getRandomNumber(2, n)\n    return True\n\ndef getRandomPrime(bits, display=False):\n    if bits < 10:\n        raise AssertionError()\n    #The 1.5 ensures the 2 MSBs are set\n    #Thus, when used for p,q in RSA, n will have its MSB set\n    #\n    #Since 30 is lcm(2,3,5), we'll set our test numbers to\n    #29 % 30 and keep them there\n    low = ((2 ** (bits-1)) * 3) // 2\n    high = 2 ** bits - 30\n    p = getRandomNumber(low, high)\n    p += 29 - (p % 30)\n    while 1:\n        if display: print(\".\", end=' ')\n        p += 30\n        if p >= high:\n            p = getRandomNumber(low, high)\n            p += 29 - (p % 30)\n        if isPrime(p, display=display):\n            return p\n\n#Unused at the moment...\ndef getRandomSafePrime(bits, display=False):\n    if bits < 10:\n        raise AssertionError()\n    #The 1.5 ensures the 2 MSBs are set\n    #Thus, when used for p,q in RSA, n will have its MSB set\n    #\n    #Since 30 is lcm(2,3,5), we'll set our test numbers to\n    #29 % 30 and keep them there\n    low = (2 ** (bits-2)) * 3//2\n    high = (2 ** (bits-1)) - 30\n    q = getRandomNumber(low, high)\n    q += 29 - (q % 30)\n    while 1:\n        if display: print(\".\", end=' ')\n        q += 30\n        if (q >= high):\n            q = getRandomNumber(low, high)\n            q += 29 - (q % 30)\n        #Ideas from Tom Wu's SRP code\n        #Do trial division on p and q before Rabin-Miller\n        if isPrime(q, 0, display=display):\n            p = (2 * q) + 1\n            if isPrime(p, display=display):\n                if isPrime(q, display=display):\n                    return p\n\n\nclass RSAKey(object):\n\n    def __init__(self, n=0, e=0, d=0, p=0, q=0, dP=0, dQ=0, qInv=0):\n        if (n and not e) or (e and not n):\n            raise AssertionError()\n        self.n = n\n        self.e = e\n        self.d = d\n        self.p = p\n        self.q = q\n        self.dP = dP\n        self.dQ = dQ\n        self.qInv = qInv\n        self.blinder = 0\n        self.unblinder = 0\n\n    def __len__(self):\n        \"\"\"Return the length of this key in bits.\n\n        @rtype: int\n        \"\"\"\n        return numBits(self.n)\n\n    def hasPrivateKey(self):\n        return self.d != 0\n\n    def hashAndSign(self, bytes):\n        \"\"\"Hash and sign the passed-in bytes.\n\n        This requires the key to have a private component.  It performs\n        a PKCS1-SHA1 signature on the passed-in data.\n\n        @type bytes: str or L{bytearray} of unsigned bytes\n        @param bytes: The value which will be hashed and signed.\n\n        @rtype: L{bytearray} of unsigned bytes.\n        @return: A PKCS1-SHA1 signature on the passed-in data.\n        \"\"\"\n        hashBytes = SHA1(bytearray(bytes))\n        prefixedHashBytes = self._addPKCS1SHA1Prefix(hashBytes)\n        sigBytes = self.sign(prefixedHashBytes)\n        return sigBytes\n\n    def hashAndVerify(self, sigBytes, bytes):\n        \"\"\"Hash and verify the passed-in bytes with the signature.\n\n        This verifies a PKCS1-SHA1 signature on the passed-in data.\n\n        @type sigBytes: L{bytearray} of unsigned bytes\n        @param sigBytes: A PKCS1-SHA1 signature.\n\n        @type bytes: str or L{bytearray} of unsigned bytes\n        @param bytes: The value which will be hashed and verified.\n\n        @rtype: bool\n        @return: Whether the signature matches the passed-in data.\n        \"\"\"\n        hashBytes = SHA1(bytearray(bytes))\n\n        # Try it with/without the embedded NULL\n        prefixedHashBytes1 = self._addPKCS1SHA1Prefix(hashBytes, False)\n        prefixedHashBytes2 = self._addPKCS1SHA1Prefix(hashBytes, True)\n        result1 = self.verify(sigBytes, prefixedHashBytes1)\n        result2 = self.verify(sigBytes, prefixedHashBytes2)\n        return (result1 or result2)\n\n    def sign(self, bytes):\n        \"\"\"Sign the passed-in bytes.\n\n        This requires the key to have a private component.  It performs\n        a PKCS1 signature on the passed-in data.\n\n        @type bytes: L{bytearray} of unsigned bytes\n        @param bytes: The value which will be signed.\n\n        @rtype: L{bytearray} of unsigned bytes.\n        @return: A PKCS1 signature on the passed-in data.\n        \"\"\"\n        if not self.hasPrivateKey():\n            raise AssertionError()\n        paddedBytes = self._addPKCS1Padding(bytes, 1)\n        m = bytesToNumber(paddedBytes)\n        if m >= self.n:\n            raise ValueError()\n        c = self._rawPrivateKeyOp(m)\n        sigBytes = numberToByteArray(c, numBytes(self.n))\n        return sigBytes\n\n    def verify(self, sigBytes, bytes):\n        \"\"\"Verify the passed-in bytes with the signature.\n\n        This verifies a PKCS1 signature on the passed-in data.\n\n        @type sigBytes: L{bytearray} of unsigned bytes\n        @param sigBytes: A PKCS1 signature.\n\n        @type bytes: L{bytearray} of unsigned bytes\n        @param bytes: The value which will be verified.\n\n        @rtype: bool\n        @return: Whether the signature matches the passed-in data.\n        \"\"\"\n        if len(sigBytes) != numBytes(self.n):\n            return False\n        paddedBytes = self._addPKCS1Padding(bytes, 1)\n        c = bytesToNumber(sigBytes)\n        if c >= self.n:\n            return False\n        m = self._rawPublicKeyOp(c)\n        checkBytes = numberToByteArray(m, numBytes(self.n))\n        return checkBytes == paddedBytes\n\n    def encrypt(self, bytes):\n        \"\"\"Encrypt the passed-in bytes.\n\n        This performs PKCS1 encryption of the passed-in data.\n\n        @type bytes: L{bytearray} of unsigned bytes\n        @param bytes: The value which will be encrypted.\n\n        @rtype: L{bytearray} of unsigned bytes.\n        @return: A PKCS1 encryption of the passed-in data.\n        \"\"\"\n        paddedBytes = self._addPKCS1Padding(bytes, 2)\n        m = bytesToNumber(paddedBytes)\n        if m >= self.n:\n            raise ValueError()\n        c = self._rawPublicKeyOp(m)\n        encBytes = numberToByteArray(c, numBytes(self.n))\n        return encBytes\n\n    def decrypt(self, encBytes):\n        \"\"\"Decrypt the passed-in bytes.\n\n        This requires the key to have a private component.  It performs\n        PKCS1 decryption of the passed-in data.\n\n        @type encBytes: L{bytearray} of unsigned bytes\n        @param encBytes: The value which will be decrypted.\n\n        @rtype: L{bytearray} of unsigned bytes or None.\n        @return: A PKCS1 decryption of the passed-in data or None if\n        the data is not properly formatted.\n        \"\"\"\n        if not self.hasPrivateKey():\n            raise AssertionError()\n        if len(encBytes) != numBytes(self.n):\n            return None\n        c = bytesToNumber(encBytes)\n        if c >= self.n:\n            return None\n        m = self._rawPrivateKeyOp(c)\n        decBytes = numberToByteArray(m, numBytes(self.n))\n        #Check first two bytes\n        if decBytes[0] != 0 or decBytes[1] != 2:\n            return None\n        #Scan through for zero separator\n        for x in range(1, len(decBytes)-1):\n            if decBytes[x]== 0:\n                break\n        else:\n            return None\n        return decBytes[x+1:] #Return everything after the separator\n\n\n\n\n    # **************************************************************************\n    # Helper Functions for RSA Keys\n    # **************************************************************************\n\n    def _addPKCS1SHA1Prefix(self, bytes, withNULL=True):\n        # There is a long history of confusion over whether the SHA1\n        # algorithmIdentifier should be encoded with a NULL parameter or\n        # with the parameter omitted.  While the original intention was\n        # apparently to omit it, many toolkits went the other way.  TLS 1.2\n        # specifies the NULL should be included, and this behavior is also\n        # mandated in recent versions of PKCS #1, and is what tlslite has\n        # always implemented.  Anyways, verification code should probably\n        # accept both.  However, nothing uses this code yet, so this is\n        # all fairly moot.\n        if not withNULL:\n            prefixBytes = bytearray(\\\n            [0x30,0x1f,0x30,0x07,0x06,0x05,0x2b,0x0e,0x03,0x02,0x1a,0x04,0x14])\n        else:\n            prefixBytes = bytearray(\\\n            [0x30,0x21,0x30,0x09,0x06,0x05,0x2b,0x0e,0x03,0x02,0x1a,0x05,0x00,0x04,0x14])\n        prefixedBytes = prefixBytes + bytes\n        return prefixedBytes\n\n    def _addPKCS1Padding(self, bytes, blockType):\n        padLength = (numBytes(self.n) - (len(bytes)+3))\n        if blockType == 1: #Signature padding\n            pad = [0xFF] * padLength\n        elif blockType == 2: #Encryption padding\n            pad = bytearray(0)\n            while len(pad) < padLength:\n                padBytes = getRandomBytes(padLength * 2)\n                pad = [b for b in padBytes if b != 0]\n                pad = pad[:padLength]\n        else:\n            raise AssertionError()\n\n        padding = bytearray([0,blockType] + pad + [0])\n        paddedBytes = padding + bytes\n        return paddedBytes\n\n\n\n\n    def _rawPrivateKeyOp(self, m):\n        #Create blinding values, on the first pass:\n        if not self.blinder:\n            self.unblinder = getRandomNumber(2, self.n)\n            self.blinder = powMod(invMod(self.unblinder, self.n), self.e,\n                                  self.n)\n\n        #Blind the input\n        m = (m * self.blinder) % self.n\n\n        #Perform the RSA operation\n        c = self._rawPrivateKeyOpHelper(m)\n\n        #Unblind the output\n        c = (c * self.unblinder) % self.n\n\n        #Update blinding values\n        self.blinder = (self.blinder * self.blinder) % self.n\n        self.unblinder = (self.unblinder * self.unblinder) % self.n\n\n        #Return the output\n        return c\n\n\n    def _rawPrivateKeyOpHelper(self, m):\n        #Non-CRT version\n        #c = powMod(m, self.d, self.n)\n\n        #CRT version  (~3x faster)\n        s1 = powMod(m, self.dP, self.p)\n        s2 = powMod(m, self.dQ, self.q)\n        h = ((s1 - s2) * self.qInv) % self.p\n        c = s2 + self.q * h\n        return c\n\n    def _rawPublicKeyOp(self, c):\n        m = powMod(c, self.e, self.n)\n        return m\n\n    def acceptsPassword(self):\n        return False\n\n    def generate(bits):\n        key = RSAKey()\n        p = getRandomPrime(bits//2, False)\n        q = getRandomPrime(bits//2, False)\n        t = lcm(p-1, q-1)\n        key.n = p * q\n        key.e = 65537\n        key.d = invMod(key.e, t)\n        key.p = p\n        key.q = q\n        key.dP = key.d % (p-1)\n        key.dQ = key.d % (q-1)\n        key.qInv = invMod(q, p)\n        return key\n    generate = staticmethod(generate)\n"
  },
  {
    "path": "electrum/scripts/README.md",
    "content": "These are scripts and small utilities developers or power-users might find interesting.\n\nThey are standalone, don't depend on each other and nothing depends on them.\nNeither the GUI nor the CLI should depend on these in any way.\n\nThe scripts use electrum as a python library, sometimes as a goal in itself, to showcase how to script something.\n"
  },
  {
    "path": "electrum/scripts/bip39_recovery.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nimport asyncio\n\nfrom electrum.util import json_encode, print_msg, create_and_start_event_loop, log_exceptions\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.network import Network\nfrom electrum.keystore import bip39_to_seed\nfrom electrum.bip32 import BIP32Node\nfrom electrum.bip39_recovery import account_discovery\n\ntry:\n    mnemonic = sys.argv[1]\n    passphrase = sys.argv[2] if len(sys.argv) > 2 else \"\"\nexcept Exception:\n    print(\"usage: bip39_recovery <mnemonic> [<passphrase>]\")\n    sys.exit(1)\n\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\n\nconfig = SimpleConfig()\nnetwork = Network(config)\nnetwork.start()\n\n@log_exceptions\nasync def f():\n    try:\n        def get_account_xpub(account_path):\n            root_seed = bip39_to_seed(mnemonic, passphrase=passphrase)\n            root_node = BIP32Node.from_rootseed(root_seed, xtype=\"standard\")\n            account_node = root_node.subkey_at_private_derivation(account_path)\n            account_xpub = account_node.to_xpub()\n            return account_xpub\n        active_accounts = await account_discovery(network, get_account_xpub)\n        print_msg(json_encode(active_accounts))\n    finally:\n        stopping_fut.set_result(1)\n\nasyncio.run_coroutine_threadsafe(f(), loop)\nwhile loop_thread.is_alive():\n    loop_thread.join(1)\n"
  },
  {
    "path": "electrum/scripts/block_headers.py",
    "content": "#!/usr/bin/env python3\n\n# A simple script that connects to a server and displays block headers\n\nimport time\nimport asyncio\n\nfrom electrum.network import Network\nfrom electrum.util import print_msg, json_encode, create_and_start_event_loop, log_exceptions\nfrom electrum.simple_config import SimpleConfig\n\nconfig = SimpleConfig()\n\n# start network\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\nnetwork = Network(config)\nnetwork.start()\n\n# wait until connected\nwhile not network.is_connected():\n    time.sleep(1)\n    print_msg(\"waiting for network to get connected...\")\n\nheader_queue = asyncio.Queue()\n\n@log_exceptions\nasync def f():\n    try:\n        await network.interface.session.subscribe('blockchain.headers.subscribe', [], header_queue)\n        # 3. wait for results\n        while network.is_connected():\n            header = await header_queue.get()\n            print_msg(json_encode(header))\n    finally:\n        stopping_fut.set_result(1)\n\n# 2. send the subscription\nasyncio.run_coroutine_threadsafe(f(), loop)\nwhile loop_thread.is_alive():\n    loop_thread.join(1)\n"
  },
  {
    "path": "electrum/scripts/bruteforce_pw.py",
    "content": "#!/usr/bin/env python3\n#\n# This script is just a demonstration how one could go about bruteforcing an\n# Electrum wallet file password. As it is pure-python and runs in the CPU,\n# it is horribly slow. It could be changed to utilise multiple threads\n# but any serious attempt would need at least GPU acceleration.\n#\n# There are two main types of password encryption that need to be disambiguated\n# for Electrum wallets:\n# (1) keystore-encryption: The wallet file itself is mostly plaintext (json),\n#                          only the Bitcoin private keys themselves are encrypted.\n#                          (e.g. seed words, xprv are encrypted; addresses are not)\n#                          Even in memory (at runtime), the private keys are typically\n#                          stored encrypted, and only when needed the user is prompted\n#                          for their password to decrypt the keys briefly.\n# (2) storage-encryption: The file itself is encrypted. When opened in a text editor,\n#                         it is base64 ascii text. Normally storage-encrypted wallets\n#                         also have keystore-encryption (unless they don't have private keys).\n# Storage-encryption was introduced in Electrum 2.8, keystore-encryption predates that.\n# Newly created wallets in modern Electrum have storage-encryption enabled by default.\n#\n# Storage encryption uses a stronger KDF than keystore-encryption.\n# As is, this script can test around ~1000 passwords per second for storage-encryption.\n\nimport sys\nfrom string import digits, ascii_uppercase, ascii_lowercase\nfrom itertools import product\nfrom typing import Callable\nfrom functools import partial\n\nfrom electrum.wallet import Wallet, Abstract_Wallet\nfrom electrum.storage import WalletStorage\nfrom electrum.wallet_db import WalletDB\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.util import InvalidPassword\n\n\nALLOWED_CHARS = digits + ascii_uppercase + ascii_lowercase\nMAX_PASSWORD_LEN = 12\n\n\ndef test_password_for_storage_encryption(storage: WalletStorage, password: str) -> bool:\n    try:\n        storage.decrypt(password)\n    except InvalidPassword:\n        return False\n    else:\n        return True\n\n\ndef test_password_for_keystore_encryption(wallet: Abstract_Wallet, password: str) -> bool:\n    try:\n        wallet.check_password(password)\n    except InvalidPassword:\n        return False\n    else:\n        return True\n\n\ndef bruteforce_loop(test_password: Callable[[str], bool]) -> str:\n    num_tested = 0\n    for pw_len in range(1, MAX_PASSWORD_LEN + 1):\n        for pw_tuple in product(ALLOWED_CHARS, repeat=pw_len):\n            password = \"\".join(pw_tuple)\n            if test_password(password):\n                return password\n            num_tested += 1\n            if num_tested % 5000 == 0:\n                print(f\"> tested {num_tested} passwords so far... most recently tried: {password!r}\")\n\n\nif __name__ == '__main__':\n    if len(sys.argv) < 2:\n        print(\"ERROR. usage: bruteforce_pw.py <path_to_wallet_file>\")\n        sys.exit(1)\n    path = sys.argv[1]\n\n    config = SimpleConfig()\n    storage = WalletStorage(path)\n    if not storage.file_exists():\n        print(f\"ERROR. wallet file not found at path: {path}\")\n        sys.exit(1)\n    if storage.is_encrypted():\n        test_password = partial(test_password_for_storage_encryption, storage)\n        print(f\"wallet found: with storage encryption.\")\n    else:\n        db = WalletDB(storage.read(), storage=storage, upgrade=False)\n        wallet = Wallet(db, config=config)\n        if not wallet.has_password():\n            print(\"wallet found but it is not encrypted.\")\n            sys.exit(0)\n        test_password = partial(test_password_for_keystore_encryption, wallet)\n        print(f\"wallet found: with keystore encryption.\")\n    password = bruteforce_loop(test_password)\n    print(f\"====================\")\n    print(f\"password found: {password}\")\n"
  },
  {
    "path": "electrum/scripts/estimate_fee.py",
    "content": "#!/usr/bin/env python3\nimport json\nimport asyncio\nfrom statistics import median\nfrom numbers import Number\n\nfrom electrum.network import filter_protocol, Network\nfrom electrum.util import create_and_start_event_loop, log_exceptions\nfrom electrum.simple_config import SimpleConfig\n\n\nconfig = SimpleConfig()\n\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\nnetwork = Network(config)\nnetwork.start()\n\n@log_exceptions\nasync def f():\n    try:\n        peers = await network.get_peers()\n        peers = filter_protocol(peers)\n        results = await network.send_multiple_requests(peers, 'blockchain.estimatefee', [2])\n        print(json.dumps(results, indent=4))\n        feerate_estimates = filter(lambda x: isinstance(x, Number), results.values())\n        print(f\"median feerate: {median(feerate_estimates)}\")\n    finally:\n        stopping_fut.set_result(1)\n\nasyncio.run_coroutine_threadsafe(f(), loop)\nwhile loop_thread.is_alive():\n    loop_thread.join(1)\n"
  },
  {
    "path": "electrum/scripts/get_history.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nimport asyncio\n\nfrom electrum import bitcoin\nfrom electrum.network import Network\nfrom electrum.util import json_encode, print_msg, create_and_start_event_loop, log_exceptions\nfrom electrum.simple_config import SimpleConfig\n\n\ntry:\n    addr = sys.argv[1]\nexcept Exception:\n    print(\"usage: get_history <bitcoin_address>\")\n    sys.exit(1)\n\nconfig = SimpleConfig()\n\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\nnetwork = Network(config)\nnetwork.start()\n\n@log_exceptions\nasync def f():\n    try:\n        sh = bitcoin.address_to_scripthash(addr)\n        hist = await network.get_history_for_scripthash(sh)\n        print_msg(json_encode(hist))\n    finally:\n        stopping_fut.set_result(1)\n\nasyncio.run_coroutine_threadsafe(f(), loop)\nwhile loop_thread.is_alive():\n    loop_thread.join(1)\n"
  },
  {
    "path": "electrum/scripts/ln_features.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nScript to analyze the graph for Lightning features.\n\nhttps://github.com/lightningnetwork/lightning-rfc/blob/master/09-features.md\n\"\"\"\n\nimport asyncio\nimport os\nimport time\nfrom typing import Optional\n\nfrom aiorpcx import NetAddress\n\nfrom electrum.logging import get_logger, configure_logging\nfrom electrum.simple_config import SimpleConfig\nfrom electrum import constants, util\nfrom electrum.daemon import Daemon\nfrom electrum.wallet import create_new_wallet\nfrom electrum.util import create_and_start_event_loop, log_exceptions, bfh\nfrom electrum.lnutil import LnFeatures\n\nlogger = get_logger(__name__)\n\n\n# Configuration parameters\nIS_TESTNET = False\nTIMEOUT = 5  # for Lightning peer connections\nWORKERS = 30  # number of workers that concurrently fetch results for feature comparison\nNODES_PER_WORKER = 50\nVERBOSITY = ''  # for debugging set '*', otherwise ''\nFLAG = LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT  # chose the 'opt' flag\nPRESYNC = False  # should we sync the graph or take it from an already synced database?\n\n\nconfig = SimpleConfig({\"testnet\": IS_TESTNET, \"verbosity\": VERBOSITY})\nconfigure_logging(config)\n\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\n# avoid race condition when starting network, in debug starting the asyncio loop\n# takes some time\ntime.sleep(2)\n\nif IS_TESTNET:\n    constants.BitcoinTestnet.set_as_network()\ndaemon = Daemon(config, listen_jsonrpc=False)\nnetwork = daemon.network\nassert network.asyncio_loop.is_running()\n\n# create empty wallet\nwallet_dir = os.path.dirname(config.get_wallet_path())\nwallet_path = os.path.join(wallet_dir, \"ln_features_wallet_main\")\nif not os.path.exists(wallet_path):\n    create_new_wallet(path=wallet_path, config=config)\n\n# open wallet\nwallet = daemon.load_wallet(wallet_path, password=None, upgrade=True)\n\n\nasync def worker(work_queue: asyncio.Queue, results_queue: asyncio.Queue, flag):\n    \"\"\"Connects to a Lightning peer and checks whether the announced feature\n    from the gossip is equal to the feature in the init message.\n\n    Returns None if no connection could be made, True or False otherwise.\"\"\"\n    count = 0\n    while not work_queue.empty():\n        if count > NODES_PER_WORKER:\n            return\n        work = await work_queue.get()\n\n        # only check non-onion addresses\n        addr = None  # type: Optional[NetAddress]\n        for a in work['addrs']:  # type: NetAddress\n            if not str(a.host).endswith(\".onion\"):\n                addr = a\n        if not addr:\n            await results_queue.put(None)\n            continue\n\n        connect_str = f\"{work['pk'].hex()}@{addr}\"\n\n        print(f\"worker connecting to {connect_str}\")\n        try:\n            peer = await wallet.lnworker.lnpeermgr.add_peer(connect_str)\n            res = await util.wait_for2(peer.initialized, TIMEOUT)\n            if res:\n                if peer.features & flag == work['features'] & flag:\n                    await results_queue.put(True)\n                else:\n                    await results_queue.put(False)\n            else:\n                await results_queue.put(None)\n        except Exception as e:\n            await results_queue.put(None)\n\n\n@log_exceptions\nasync def node_flag_stats(opt_flag: LnFeatures, presync: False):\n    \"\"\"Determines statistics for feature advertisements by nodes on the Lighting\n    network by evaluation of the public graph.\n\n    opt_flag: The optional-flag for a feature.\n    presync: Sync the graph. Can take a long time and depends on the quality\n        of the peers. Better to use presynced graph from regular wallet use for\n        now.\n    \"\"\"\n    try:\n        await wallet.lnworker.channel_db.data_loaded.wait()\n\n        # optionally presync graph (not reliable)\n        if presync:\n            network.start_gossip()\n\n            # wait for the graph to be synchronized\n            while True:\n                await asyncio.sleep(5)\n\n                # logger.info(wallet.network.lngossip.get_sync_progress_estimate())\n                cur, tot, pct = wallet.network.lngossip.get_sync_progress_estimate()\n                print(f\"graph sync progress {cur}/{tot} ({pct}%) channels\")\n                if pct >= 100:\n                    break\n\n        with wallet.lnworker.channel_db.lock:\n            nodes = wallet.lnworker.channel_db._nodes.copy()\n\n        # check how many nodes advertise opt/req flag in the gossip\n        n_opt = 0\n        n_req = 0\n        print(f\"analyzing {len(nodes.keys())} nodes\")\n\n        # 1. statistics on graph\n        req_flag = LnFeatures(opt_flag >> 1)\n        for n, nv in nodes.items():\n            features = LnFeatures(nv.features)\n            if features & opt_flag:\n                n_opt += 1\n            if features & req_flag:\n                n_req += 1\n\n        # analyze numbers\n        print(\n            f\"opt: {n_opt} ({100 * n_opt/len(nodes)}%) \"\n            f\"req: {n_req} ({100 * n_req/len(nodes)}%)\")\n\n        # 2. compare announced and actual feature set\n        # put nodes into a work queue\n        work_queue = asyncio.Queue()\n        results_queue = asyncio.Queue()\n\n        # fill up work\n        for n, nv in nodes.items():\n            addrs = wallet.lnworker.channel_db._addresses[n]\n            await work_queue.put({'pk': n, 'addrs': addrs, 'features': nv.features})\n        tasks = [asyncio.create_task(worker(work_queue, results_queue, opt_flag)) for i in range(WORKERS)]\n        try:\n            await asyncio.gather(*tasks)\n        except Exception as e:\n            print(e)\n        # analyze results\n        n_true = 0\n        n_false = 0\n        n_tot = 0\n        while not results_queue.empty():\n            i = results_queue.get_nowait()\n            n_tot += 1\n            if i is True:\n                n_true += 1\n            elif i is False:\n                n_false += 1\n        print(f\"feature comparison - equal: {n_true} unequal: {n_false} total:{n_tot}\")\n\n    finally:\n        stopping_fut.set_result(1)\n\nasyncio.run_coroutine_threadsafe(\n    node_flag_stats(FLAG, presync=PRESYNC), loop)\nwhile loop_thread.is_alive():\n    loop_thread.join(1)\n"
  },
  {
    "path": "electrum/scripts/peers.py",
    "content": "#!/usr/bin/env python3\nimport asyncio\n\nfrom electrum.network import filter_protocol, Network\nfrom electrum.util import create_and_start_event_loop, log_exceptions\nfrom electrum.blockchain import hash_raw_header\nfrom electrum.simple_config import SimpleConfig\n\n\nconfig = SimpleConfig()\n\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\nnetwork = Network(config)\nnetwork.start()\n\n@log_exceptions\nasync def f():\n    try:\n        peers = await network.get_peers()\n        peers = filter_protocol(peers)\n        results = await network.send_multiple_requests(peers, 'blockchain.headers.subscribe', [])\n        for server, header in sorted(results.items(), key=lambda x: x[1].get('height')):\n            height = header.get('height')\n            blockhash = hash_raw_header(bytes.fromhex(header.get('hex')))\n            print(server, height, blockhash)\n    finally:\n        stopping_fut.set_result(1)\n\nasyncio.run_coroutine_threadsafe(f(), loop)\nwhile loop_thread.is_alive():\n    loop_thread.join(1)\n"
  },
  {
    "path": "electrum/scripts/quick_start.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport asyncio\n\nfrom electrum.simple_config import SimpleConfig\nfrom electrum import constants\nfrom electrum.daemon import Daemon\nfrom electrum.storage import WalletStorage\nfrom electrum.wallet import Wallet, create_new_wallet\nfrom electrum.wallet_db import WalletDB\nfrom electrum.commands import Commands\nfrom electrum.util import create_and_start_event_loop, log_exceptions\n\n\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\n\nconfig = SimpleConfig({\"testnet\": True})  # to use ~/.electrum/testnet as datadir\nconstants.BitcoinTestnet.set_as_network()  # to set testnet magic bytes\ndaemon = Daemon(config, listen_jsonrpc=False)\nnetwork = daemon.network\nassert network.asyncio_loop.is_running()\n\n# get wallet on disk\nwallet_dir = os.path.dirname(config.get_wallet_path())\nwallet_path = os.path.join(wallet_dir, \"test_wallet\")\nif not os.path.exists(wallet_path):\n    create_new_wallet(path=wallet_path, config=config)\n\n# open wallet\nwallet = daemon.load_wallet(wallet_path, password=None, upgrade=True)\n\n# you can use ~CLI commands by accessing command_runner\ncommand_runner = Commands(config=config, daemon=daemon, network=network)\nprint(\"balance\", network.run_from_another_thread(command_runner.getbalance(wallet=wallet)))\nprint(\"addr\",    network.run_from_another_thread(command_runner.getunusedaddress(wallet=wallet)))\nprint(\"gettx\",   network.run_from_another_thread(\n    command_runner.gettransaction(\"bd3a700b2822e10a034d110c11a596ee7481732533eb6aca7f9ca02911c70a4f\")))\n\n\n# but you might as well interact with the underlying methods directly\nprint(\"balance\", wallet.get_balance())\nprint(\"addr\",    wallet.get_unused_address())\nprint(\"gettx\",   network.run_from_another_thread(network.get_transaction(\"bd3a700b2822e10a034d110c11a596ee7481732533eb6aca7f9ca02911c70a4f\")))\n\nstopping_fut.set_result(1)  # to stop event loop\n"
  },
  {
    "path": "electrum/scripts/servers.py",
    "content": "#!/usr/bin/env python3\nimport json\nimport asyncio\n\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.network import filter_version, Network\nfrom electrum.util import create_and_start_event_loop, log_exceptions\nfrom electrum import constants\n\n# testnet?\n#constants.BitcoinTestnet.set_as_network()\nconfig = SimpleConfig({'testnet': False})\n\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\nnetwork = Network(config)\nnetwork.start()\n\n@log_exceptions\nasync def f():\n    try:\n        peers = await network.get_peers()\n        peers = filter_version(peers)\n        print(json.dumps(peers, sort_keys=True, indent=4))\n    finally:\n        stopping_fut.set_result(1)\n\nasyncio.run_coroutine_threadsafe(f(), loop)\nwhile loop_thread.is_alive():\n    loop_thread.join(1)\n"
  },
  {
    "path": "electrum/scripts/txbroadcast.py",
    "content": "#!/usr/bin/env python3\n#\n# Connect to lots of servers and broadcast a given tx to each.\n\nimport sys\nimport asyncio\n\nfrom electrum.network import filter_protocol, Network\nfrom electrum.util import create_and_start_event_loop, log_exceptions\nfrom electrum.simple_config import SimpleConfig\n\n\ntry:\n    rawtx = sys.argv[1]\nexcept Exception:\n    print(\"usage: txbroadcast rawtx\")\n    sys.exit(1)\n\nconfig = SimpleConfig()\n\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\nnetwork = Network(config)\nnetwork.start()\n\n@log_exceptions\nasync def f():\n    try:\n        peers = await network.get_peers()\n        peers = filter_protocol(peers)\n        results = await network.send_multiple_requests(peers, 'blockchain.transaction.broadcast', [rawtx])\n        for server, resp in results.items():\n            print(f\"result: server={server}, response={resp}\")\n    finally:\n        stopping_fut.set_result(1)\n\nasyncio.run_coroutine_threadsafe(f(), loop)\nwhile loop_thread.is_alive():\n    loop_thread.join(1)\n"
  },
  {
    "path": "electrum/scripts/txradar.py",
    "content": "#!/usr/bin/env python3\nimport sys\nimport asyncio\n\nfrom electrum.network import filter_protocol, Network\nfrom electrum.util import create_and_start_event_loop, log_exceptions\nfrom electrum.simple_config import SimpleConfig\n\n\ntry:\n    txid = sys.argv[1]\nexcept Exception:\n    print(\"usage: txradar txid\")\n    sys.exit(1)\n\nconfig = SimpleConfig()\n\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\nnetwork = Network(config)\nnetwork.start()\n\n@log_exceptions\nasync def f():\n    try:\n        peers = await network.get_peers()\n        peers = filter_protocol(peers)\n        results = await network.send_multiple_requests(peers, 'blockchain.transaction.get', [txid])\n        r1, r2 = [], []\n        for k, v in results.items():\n            (r1 if not isinstance(v, Exception) else r2).append(k)\n        print(f\"Received {len(results)} answers\")\n        try: propagation = len(r1) * 100. / (len(r1) + len(r2))\n        except ZeroDivisionError: propagation = 0\n        print(f\"Propagation rate: {propagation:.1f} percent\")\n    finally:\n        stopping_fut.set_result(1)\n\nasyncio.run_coroutine_threadsafe(f(), loop)\nwhile loop_thread.is_alive():\n    loop_thread.join(1)\n"
  },
  {
    "path": "electrum/scripts/update_default_servers.py",
    "content": "#!/usr/bin/env python3\n# This script prints a new \"servers.json\" to stdout.\n# It prunes the offline servers from the existing list (note: run with Tor proxy to keep .onions),\n# and adds new servers from provided file(s) of candidate servers.\n# A file of new candidate servers can be created via e.g.:\n# $ ./electrum/scripts/servers.py > reply.txt\n\nimport asyncio\nimport sys\nimport json\n\nfrom electrum.network import Network\nfrom electrum.util import create_and_start_event_loop, log_exceptions\nfrom electrum.simple_config import SimpleConfig\nfrom electrum import constants\n\ntry:\n    fname1 = sys.argv[1]\n    fname2 = sys.argv[2] if len(sys.argv) > 2 else None\nexcept Exception:\n    print(\"usage: update_default_servers.py <file1> [<file2>]\")\n    print(\"       - the file(s) should contain json hostmaps for new servers to be added\")\n    print(\"       - if two files are provided, their intersection is used (peers found in both).\\n\"\n          \"         file1 should have the newer data.\")\n    sys.exit(1)\n\n\ndef get_newly_added_servers(fname1, fname2=None):\n    with open(fname1) as f:\n        res_hostmap = json.loads(f.read())\n    if fname2 is not None:\n        with open(fname2) as f:\n            dict2 = json.loads(f.read())\n        common_set = set.intersection(set(res_hostmap), set(dict2))\n        res_hostmap = {k: v for k, v in res_hostmap.items() if k in common_set}\n    return res_hostmap\n\n\n# testnet?\n#constants.BitcoinTestnet.set_as_network()\nconfig = SimpleConfig({'testnet': False})\n\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\nnetwork = Network(config)\nnetwork.start()\n\n@log_exceptions\nasync def f():\n    try:\n        # prune existing servers\n        old_servers_all = constants.net.DEFAULT_SERVERS\n        old_servers_online = await network.prune_offline_servers(constants.net.DEFAULT_SERVERS)\n        # add new servers\n        newly_added_servers = get_newly_added_servers(fname1, fname2)\n        res_servers = {**old_servers_online, **newly_added_servers}\n\n        print(json.dumps(res_servers, indent=4, sort_keys=True))\n        print(f\"got reply from {len(old_servers_online)}/{len(old_servers_all)} old servers\", file=sys.stderr)\n        print(f\"len(newly_added_servers)={len(newly_added_servers)}. total: {len(res_servers)}\", file=sys.stderr)\n    finally:\n        stopping_fut.set_result(1)\n\nasyncio.run_coroutine_threadsafe(f(), loop)\nwhile loop_thread.is_alive():\n    loop_thread.join(1)\n"
  },
  {
    "path": "electrum/scripts/watch_address.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nimport asyncio\n\nfrom electrum.network import Network\nfrom electrum.util import print_msg, create_and_start_event_loop\nfrom electrum.synchronizer import SynchronizerBase\nfrom electrum.simple_config import SimpleConfig\n\n\ntry:\n    addr = sys.argv[1]\nexcept Exception:\n    print(\"usage: watch_address <bitcoin_address>\")\n    sys.exit(1)\n\nconfig = SimpleConfig()\n\n# start network\nloop, stopping_fut, loop_thread = create_and_start_event_loop()\nnetwork = Network(config)\nnetwork.start()\n\n\nclass Notifier(SynchronizerBase):\n    def __init__(self, network):\n        SynchronizerBase.__init__(self, network)\n        self.watched_addresses = set()\n        self.watch_queue = asyncio.Queue()\n\n    async def main(self):\n        # resend existing subscriptions if we were restarted\n        for addr in self.watched_addresses:\n            await self._add_address(addr)\n        # main loop\n        while True:\n            addr = await self.watch_queue.get()\n            self.watched_addresses.add(addr)\n            await self._add_address(addr)\n\n    async def _on_address_status(self, addr, status):\n        print_msg(f\"addr {addr}, status {status}\")\n\n\nnotifier = Notifier(network)\nasyncio.run_coroutine_threadsafe(notifier.watch_queue.put(addr), loop)\nwhile loop_thread.is_alive():\n    loop_thread.join(1)\n"
  },
  {
    "path": "electrum/segwit_addr.py",
    "content": "\n# Copyright (c) 2017 Pieter Wuille\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n\n\"\"\"Reference implementation for Bech32/Bech32m and segwit addresses.\"\"\"\n\nfrom enum import Enum\nfrom typing import Tuple, Optional, Sequence, NamedTuple, List, Mapping, Iterable\n\nCHARSET = \"qpzry9x8gf2tvdw0s3jn54khce6mua7l\"\nCHARSET_INVERSE = {c: i for (i, c) in enumerate(CHARSET)}  # type: Mapping[str, int]\n\nBECH32_CONST = 1\nBECH32M_CONST = 0x2bc830a3\n\n\nclass Encoding(Enum):\n    \"\"\"Enumeration type to list the various supported encodings.\"\"\"\n    BECH32 = 1\n    BECH32M = 2\n\n\nclass DecodedBech32(NamedTuple):\n    encoding: Optional[Encoding]\n    hrp: Optional[str]\n    data: Optional[Sequence[int]]  # 5-bit ints\n\n\ndef bech32_polymod(values):\n    \"\"\"Internal function that computes the Bech32 checksum.\"\"\"\n    generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]\n    chk = 1\n    for value in values:\n        top = chk >> 25\n        chk = (chk & 0x1ffffff) << 5 ^ value\n        for i in range(5):\n            chk ^= generator[i] if ((top >> i) & 1) else 0\n    return chk\n\n\ndef bech32_hrp_expand(hrp):\n    \"\"\"Expand the HRP into values for checksum computation.\"\"\"\n    return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]\n\n\ndef bech32_verify_checksum(hrp, data):\n    \"\"\"Verify a checksum given HRP and converted data characters.\"\"\"\n    check = bech32_polymod(bech32_hrp_expand(hrp) + data)\n    if check == BECH32_CONST:\n        return Encoding.BECH32\n    elif check == BECH32M_CONST:\n        return Encoding.BECH32M\n    else:\n        return None\n\n\ndef bech32_create_checksum(encoding: Encoding, hrp: str, data: List[int]) -> List[int]:\n    \"\"\"Compute the checksum values given HRP and data.\"\"\"\n    values = bech32_hrp_expand(hrp) + data\n    const = BECH32M_CONST if encoding == Encoding.BECH32M else BECH32_CONST\n    polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const\n    return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]\n\n\ndef bech32_encode(encoding: Encoding, hrp: str, data: List[int]) -> str:\n    \"\"\"Compute a Bech32 or Bech32m string given HRP and data values.\"\"\"\n    combined = data + bech32_create_checksum(encoding, hrp, data)\n    return hrp + '1' + ''.join([CHARSET[d] for d in combined])\n\n\ndef bech32_decode(bech: str, *, ignore_long_length=False) -> DecodedBech32:\n    \"\"\"Validate a Bech32/Bech32m string, and determine HRP and data.\"\"\"\n    bech_lower = bech.lower()\n    if bech_lower != bech and bech.upper() != bech:\n        return DecodedBech32(None, None, None)\n    pos = bech.rfind('1')\n    if pos < 1 or pos + 7 > len(bech) or (not ignore_long_length and len(bech) > 90):\n        return DecodedBech32(None, None, None)\n    # check that HRP only consists of sane ASCII chars\n    if any(ord(x) < 33 or ord(x) > 126 for x in bech[:pos+1]):\n        return DecodedBech32(None, None, None)\n    bech = bech_lower\n    hrp = bech[:pos]\n    try:\n        data = [CHARSET_INVERSE[x] for x in bech[pos + 1:]]\n    except KeyError:\n        return DecodedBech32(None, None, None)\n    encoding = bech32_verify_checksum(hrp, data)\n    if encoding is None:\n        return DecodedBech32(None, None, None)\n    return DecodedBech32(encoding=encoding, hrp=hrp, data=data[:-6])\n\n\ndef convertbits(data: Iterable[int], frombits: int, tobits: int, pad: bool = True) -> Optional[Sequence[int]]:\n    \"\"\"General power-of-2 base conversion.\"\"\"\n    acc = 0\n    bits = 0\n    ret = []\n    maxv = (1 << tobits) - 1\n    max_acc = (1 << (frombits + tobits - 1)) - 1\n    for value in data:\n        if value < 0 or (value >> frombits):\n            return None\n        acc = ((acc << frombits) | value) & max_acc\n        bits += frombits\n        while bits >= tobits:\n            bits -= tobits\n            ret.append((acc >> bits) & maxv)\n    if pad:\n        if bits:\n            ret.append((acc << (tobits - bits)) & maxv)\n    elif bits >= frombits or ((acc << (tobits - bits)) & maxv):\n        return None\n    return ret\n\n\ndef decode_segwit_address(hrp: str, addr: Optional[str]) -> Tuple[Optional[int], Optional[Sequence[int]]]:\n    \"\"\"Decode a segwit address.\"\"\"\n    if addr is None:\n        return (None, None)\n    encoding, hrpgot, data = bech32_decode(addr)\n    if hrpgot != hrp:\n        return (None, None)\n    decoded = convertbits(data[1:], 5, 8, False)\n    if decoded is None or len(decoded) < 2 or len(decoded) > 40:\n        return (None, None)\n    if data[0] > 16:\n        return (None, None)\n    if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:\n        return (None, None)\n    if (data[0] == 0 and encoding != Encoding.BECH32) or (data[0] != 0 and encoding != Encoding.BECH32M):\n        return (None, None)\n    return (data[0], decoded)\n\n\ndef encode_segwit_address(hrp: str, witver: int, witprog: bytes) -> Optional[str]:\n    \"\"\"Encode a segwit address.\"\"\"\n    encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M\n    ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5))\n    if decode_segwit_address(hrp, ret) == (None, None):\n        return None\n    return ret\n"
  },
  {
    "path": "electrum/simple_config.py",
    "content": "import json\nimport threading\nimport os\nimport stat\nfrom typing import Union, Optional, Dict, Sequence, Any, Set, Callable, AbstractSet, Type\nfrom functools import cached_property\n\nfrom copy import deepcopy\n\nfrom . import constants\nfrom . import util\nfrom . import invoices\nfrom .util import base_units, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, UnknownBaseUnit, DECIMAL_POINT_DEFAULT\nfrom .util import format_satoshis, format_fee_satoshis, os_chmod\nfrom .util import user_dir, make_dir\nfrom .util import is_valid_websocket_url\nfrom .lnutil import LN_MAX_FUNDING_SAT_LEGACY\nfrom .i18n import _\nfrom .logging import get_logger, Logger\n\n\n_logger = get_logger(__name__)\n\n\nFINAL_CONFIG_VERSION = 3\n\n\n_config_var_from_key = {}  # type: Dict[str, 'ConfigVar']\n\n\nclass ConfigVar(property):\n\n    def __init__(\n        self,\n        key: str,\n        *,\n        default: Union[Any, Callable[['SimpleConfig'], Any]],  # typically a literal, but can also be a callable\n        type_=None,\n        convert_getter: Callable[[Any], Any] = None,\n        convert_setter: Callable[[Any], Any] = None,\n        short_desc: Callable[[], str] = None,\n        long_desc: Callable[[], str] = None,\n        plugin: Optional[str] = None,\n    ):\n        self._key = key\n        self._default = default\n        self._type = type_\n        self._convert_getter = convert_getter\n        self._convert_setter = convert_setter\n        # note: the descriptions are callables instead of str literals, to delay evaluating the _() translations\n        #       until after the language is set.\n        assert short_desc is None or callable(short_desc)\n        assert long_desc is None or callable(long_desc)\n        self._short_desc = short_desc\n        self._long_desc = long_desc\n        if plugin:  # enforce \"key\" starts with 'plugins.<name of plugin>.'\n            pkg_prefix = \"electrum.plugins.\"  # for internal plugins\n            if plugin.startswith(pkg_prefix):\n                plugin = plugin[len(pkg_prefix):]\n            assert \".\" not in plugin, plugin\n            key_prefix = f'plugins.{plugin}.'\n            assert key.startswith(key_prefix), f\"ConfigVar {key=} must be prefixed with ({key_prefix})\"\n        property.__init__(self, self._get_config_value, self._set_config_value)\n        assert key not in _config_var_from_key, f\"duplicate config key str: {key!r}\"\n        _config_var_from_key[key] = self\n\n    def _get_config_value(self, config: 'SimpleConfig'):\n        with config.lock:\n            if config.is_set(self._key):\n                value = config.get(self._key)\n                # run converter\n                if self._convert_getter is not None:\n                    value = self._convert_getter(value)\n                # type-check\n                if self._type is not None:\n                    assert value is not None, f\"got None for key={self._key!r}\"\n                    try:\n                        value = self._type(value)\n                    except Exception as e:\n                        raise ValueError(\n                            f\"ConfigVar.get type-check and auto-conversion failed. \"\n                            f\"key={self._key!r}. type={self._type}. value={value!r}\") from e\n            else:\n                d = self._default\n                value = d(config) if callable(d) else d\n            return value\n\n    def _set_config_value(self, config: 'SimpleConfig', value, *, save=True):\n        # run converter\n        if self._convert_setter is not None and value is not None:\n            value = self._convert_setter(value)\n        # type-check\n        if self._type is not None and value is not None:\n            if not isinstance(value, self._type):\n                raise ValueError(\n                    f\"ConfigVar.set type-check failed. \"\n                    f\"key={self._key!r}. type={self._type}. value={value!r}\")\n        config.set_key(self._key, value, save=save)\n\n    def key(self) -> str:\n        return self._key\n\n    def get_default_value(self) -> Any:\n        return self._default\n\n    def get_short_desc(self) -> Optional[str]:\n        desc = self._short_desc\n        return desc() if desc else None\n\n    def get_long_desc(self) -> Optional[str]:\n        desc = self._long_desc\n        return desc() if desc else None\n\n    def __repr__(self):\n        return f\"<ConfigVar key={self._key!r}>\"\n\n    def __deepcopy__(self, memo):\n        # We can be considered ~stateless. State is stored in the config, which is external.\n        return self\n\n\nclass ConfigVarWithConfig:\n\n    def __init__(self, *, config: 'SimpleConfig', config_var: 'ConfigVar'):\n        self._config = config\n        self._config_var = config_var\n\n    def get(self) -> Any:\n        return self._config_var._get_config_value(self._config)\n\n    def set(self, value: Any, *, save=True) -> None:\n        self._config_var._set_config_value(self._config, value, save=save)\n\n    def key(self) -> str:\n        return self._config_var.key()\n\n    def get_default_value(self) -> Any:\n        return self._config_var.get_default_value()\n\n    def get_short_desc(self) -> Optional[str]:\n        return self._config_var.get_short_desc()\n\n    def get_long_desc(self) -> Optional[str]:\n        return self._config_var.get_long_desc()\n\n    def is_modifiable(self) -> bool:\n        return self._config.is_modifiable(self._config_var)\n\n    def is_set(self) -> bool:\n        return self._config.is_set(self._config_var)\n\n    def __repr__(self):\n        return f\"<ConfigVarWithConfig key={self.key()!r}>\"\n\n    def __eq__(self, other) -> bool:\n        if not isinstance(other, ConfigVarWithConfig):\n            return False\n        return self._config is other._config and self._config_var is other._config_var\n\n\nclass SimpleConfig(Logger):\n    \"\"\"\n    The SimpleConfig class is responsible for handling operations involving\n    configuration files.\n\n    There are two different sources of possible configuration values:\n        1. Command line options.\n        2. User configuration (in the user's config directory)\n    They are taken in order (1. overrides config options set in 2.)\n    \"\"\"\n\n    def __init__(self, options=None, read_user_config_function=None,\n                 read_user_dir_function=None):\n        if options is None:\n            options = {}\n        for config_key in options:\n            assert isinstance(config_key, str), f\"{config_key=!r} has type={type(config_key)}, expected str\"\n\n        Logger.__init__(self)\n\n        # This lock needs to be acquired for updating and reading the config in\n        # a thread-safe way.\n        self.lock = threading.RLock()\n\n        # The following two functions are there for dependency injection when\n        # testing.\n        if read_user_config_function is None:\n            read_user_config_function = read_user_config\n        if read_user_dir_function is None:\n            self.user_dir = user_dir\n        else:\n            self.user_dir = read_user_dir_function\n\n        # The command line options\n        self.cmdline_options = deepcopy(options)\n        # don't allow to be set on CLI:\n        self.cmdline_options.pop('config_version', None)\n\n        # Set self.path and read the user config\n        self.user_config = {}  # for self.get in electrum_path()\n        self.path = self.electrum_path()\n        self.user_config = read_user_config_function(self.path)\n        if not self.user_config:\n            # avoid new config getting upgraded\n            self.user_config = {'config_version': FINAL_CONFIG_VERSION}\n\n        self._not_modifiable_keys = set()  # type: Set[str]\n\n        # config \"upgrade\" - CLI options\n        self.rename_config_keys(\n            self.cmdline_options, {'auto_cycle': 'auto_connect'}, True)\n\n        # config upgrade - user config\n        if self.requires_upgrade():\n            self.upgrade()\n\n        self._check_dependent_keys()\n\n        # units and formatting\n        try:\n            decimal_point_to_base_unit_name(self.BTC_AMOUNTS_DECIMAL_POINT)\n        except UnknownBaseUnit:\n            self.BTC_AMOUNTS_DECIMAL_POINT = DECIMAL_POINT_DEFAULT\n        self.num_zeros = self.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT\n        self.amt_precision_post_satoshi = self.BTC_AMOUNTS_PREC_POST_SAT\n        self.amt_add_thousands_sep = self.BTC_AMOUNTS_ADD_THOUSANDS_SEP\n\n        self._init_done = True\n\n    def list_config_vars(self) -> Sequence[str]:\n        return list(sorted(_config_var_from_key.keys()))\n\n    def electrum_path_root(self):\n        # Read electrum_path from command line\n        # Otherwise use the user's default data directory.\n        path = self.get('electrum_path') or self.user_dir()\n        make_dir(path, allow_symlink=False)\n        return path\n\n    @classmethod\n    def set_chain_config_opt_based_on_android_packagename(cls, config_options: dict[str, Any]) -> None:\n        # ~hack for easier testnet builds. pkgname subject to change.\n        android_pkg_name = util.get_android_package_name()\n        for chain in constants.NETS_LIST:\n            if android_pkg_name == f\"org.electrum.{chain.cli_flag()}.electrum\":\n                config_options[chain.cli_flag()] = True\n\n    def get_selected_chain(self) -> Type[constants.AbstractNet]:\n        selected_chains = [\n            chain for chain in constants.NETS_LIST\n            if self.get(chain.config_key())]\n        if selected_chains:\n            # note: if multiple are selected, we just pick one deterministically random\n            return selected_chains[0]\n        return constants.BitcoinMainnet\n\n    def electrum_path(self):\n        path = self.electrum_path_root()\n        chain = self.get_selected_chain()\n        if subdir := chain.datadir_subdir():\n            path = os.path.join(path, subdir)\n            make_dir(path, allow_symlink=False)\n\n        self.logger.info(f\"electrum directory {path} (chain={chain.NET_NAME})\")\n        return path\n\n    def rename_config_keys(self, config, keypairs, deprecation_warning=False):\n        \"\"\"Migrate old key names to new ones\"\"\"\n        updated = False\n        for old_key, new_key in keypairs.items():\n            if old_key in config:\n                if new_key not in config:\n                    config[new_key] = config[old_key]\n                    if deprecation_warning:\n                        self.logger.warning('Note that the {} variable has been deprecated. '\n                                            'You should use {} instead.'.format(old_key, new_key))\n                del config[old_key]\n                updated = True\n        return updated\n\n    def set_key(self, key: Union[str, ConfigVar, ConfigVarWithConfig], value, *, save=True) -> None:\n        \"\"\"Set the value for an arbitrary string config key.\n        note: try to use explicit predefined ConfigVars instead of this method, whenever possible.\n              This method side-steps ConfigVars completely, and is mainly kept for situations\n              where the config key is dynamically constructed.\n        \"\"\"\n        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):\n            key = key.key()\n        assert isinstance(key, str), key\n        if not self.is_modifiable(key):\n            self.logger.warning(f\"not changing config key '{key}' set on the command line\")\n            return\n        try:\n            json.dumps(key)\n            json.dumps(value)\n        except Exception:\n            self.logger.info(f\"json error: cannot save {repr(key)} ({repr(value)})\")\n            return\n        self._set_key_in_user_config(key, value, save=save)\n\n    def _set_key_in_user_config(self, key: str, value, *, save=True) -> None:\n        assert isinstance(key, str), key\n        with self.lock:\n            if value is not None:\n                keypath = key.split('.')\n                d = self.user_config\n                for x in keypath[0:-1]:\n                    d2 = d.get(x)\n                    if not isinstance(d2, dict):\n                        d2 = d[x] = {}\n                    d = d2\n                d[keypath[-1]] = value\n            else:\n                def delete_key(d, key):\n                    if '.' not in key:\n                        d.pop(key, None)\n                    else:\n                        prefix, suffix = key.split('.', 1)\n                        d2 = d.get(prefix)\n                        empty = delete_key(d2, suffix)\n                        if empty:\n                            d.pop(prefix)\n                    return len(d) == 0\n                delete_key(self.user_config, key)\n            if save:\n                self.save_user_config()\n\n    def get(self, key: str, default=None) -> Any:\n        \"\"\"Get the value for an arbitrary string config key.\n        note: try to use explicit predefined ConfigVars instead of this method, whenever possible.\n              This method side-steps ConfigVars completely, and is mainly kept for situations\n              where the config key is dynamically constructed.\n        \"\"\"\n        assert isinstance(key, str), key\n        with self.lock:\n            out = self.cmdline_options.get(key)\n            if out is None:\n                d = self.user_config\n                path = key.split('.')\n                for key in path[0:-1]:\n                    d = d.get(key, {})\n                if not isinstance(d, dict):\n                    d = {}\n                out = d.get(path[-1], default)\n        return out\n\n    def is_set(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool:\n        \"\"\"Returns whether the config key has any explicit value set/defined.\"\"\"\n        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):\n            key = key.key()\n        assert isinstance(key, str), key\n        return self.get(key, default=...) is not ...\n\n    def is_plugin_enabled(self, name: str) -> bool:\n        return bool(self.get(f'plugins.{name}.enabled'))\n\n    def get_installed_plugins(self) -> AbstractSet[str]:\n        \"\"\"Returns all plugin names registered in the config.\"\"\"\n        return self.get('plugins', {}).keys()\n\n    def enable_plugin(self, name: str):\n        self.set_key(f'plugins.{name}.enabled', True, save=True)\n\n    def disable_plugin(self, name: str):\n        self.set_key(f'plugins.{name}.enabled', False, save=True)\n\n    def _check_dependent_keys(self) -> None:\n        if self.NETWORK_SERVERFINGERPRINT:\n            if not self.NETWORK_SERVER:\n                raise Exception(\n                    f\"config key {self.__class__.NETWORK_SERVERFINGERPRINT.key()!r} requires \"\n                    f\"{self.__class__.NETWORK_SERVER.key()!r} to also be set\")\n            self.make_key_not_modifiable(self.__class__.NETWORK_SERVER)\n\n    def requires_upgrade(self):\n        return self.get_config_version() < FINAL_CONFIG_VERSION\n\n    def upgrade(self):\n        with self.lock:\n            self.logger.info('upgrading config')\n\n            self.convert_version_2()\n            self.convert_version_3()\n\n            self.set_key('config_version', FINAL_CONFIG_VERSION, save=True)\n\n    def convert_version_2(self):\n        if not self._is_upgrade_method_needed(1, 1):\n            return\n\n        self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'})\n\n        try:\n            # change server string FROM host:port:proto TO host:port:s\n            server_str = self.user_config.get('server')\n            host, port, protocol = str(server_str).rsplit(':', 2)\n            assert protocol in ('s', 't')\n            int(port)  # Throw if cannot be converted to int\n            server_str = '{}:{}:s'.format(host, port)\n            self._set_key_in_user_config('server', server_str)\n        except BaseException:\n            self._set_key_in_user_config('server', None)\n\n        self.set_key('config_version', 2)\n\n    def convert_version_3(self):\n        if not self._is_upgrade_method_needed(2, 2):\n            return\n        base_unit = self.user_config.get('base_unit')\n        if isinstance(base_unit, str):\n            self._set_key_in_user_config('base_unit', None)\n            map_ = {'btc': 8, 'mbtc': 5, 'ubtc': 2, 'bits': 2, 'sat': 0}\n            decimal_point = map_.get(base_unit.lower())\n            self._set_key_in_user_config('decimal_point', decimal_point)\n        self.set_key('config_version', 3)\n\n    def _is_upgrade_method_needed(self, min_version, max_version):\n        cur_version = self.get_config_version()\n        if cur_version > max_version:\n            return False\n        elif cur_version < min_version:\n            raise Exception(\n                ('config upgrade: unexpected version %d (should be %d-%d)'\n                 % (cur_version, min_version, max_version)))\n        else:\n            return True\n\n    def get_config_version(self):\n        config_version = self.get('config_version', 1)\n        if config_version > FINAL_CONFIG_VERSION:\n            self.logger.warning('config version ({}) is higher than latest ({})'\n                                .format(config_version, FINAL_CONFIG_VERSION))\n        return config_version\n\n    def is_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool:\n        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):\n            key = key.key()\n        return (key not in self.cmdline_options\n                and key not in self._not_modifiable_keys)\n\n    def make_key_not_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> None:\n        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):\n            key = key.key()\n        assert isinstance(key, str), key\n        self._not_modifiable_keys.add(key)\n\n    def save_user_config(self):\n        if self.CONFIG_FORGET_CHANGES:\n            self.logger.warning(f\"not saving config changes to disk as {self.cv.CONFIG_FORGET_CHANGES.key()} is set\", only_once=True)\n            return\n        if not self.path:\n            return\n        path = os.path.join(self.path, \"config\")\n        s = json.dumps(self.user_config, indent=4, sort_keys=True)\n        try:\n            with open(path, \"w\", encoding='utf-8') as f:\n                os_chmod(path, stat.S_IREAD | stat.S_IWRITE)  # set restrictive perms *before* we write data\n                f.write(s)\n        except OSError:\n            # datadir probably deleted while running... e.g. portable exe running on ejected USB drive\n            # (in which case it is typically either FileNotFoundError or PermissionError,\n            #  but let's just catch the more generic OSError and test explicitly)\n            if os.path.exists(self.path):  # or maybe not?\n                raise\n\n    def get_backup_dir(self) -> Optional[str]:\n        # this is used to save wallet file backups (without active lightning channels)\n        # on Android, the export backup button uses android_backup_dir()\n        if 'ANDROID_DATA' in os.environ:\n            return None\n        else:\n            return self.WALLET_BACKUP_DIRECTORY\n\n    def maybe_complete_wallet_path(self, path: Optional[str]) -> str:\n        return self._complete_wallet_path(path) if path is not None else self.get_wallet_path()\n\n    def _complete_wallet_path(self, path: str) -> str:\n        \"\"\" add user wallets directory if needed \"\"\"\n        if os.path.split(path)[0] == '':\n            path = os.path.join(self.get_datadir_wallet_path(), path)\n        return path\n\n    def get_wallet_path(self) -> str:\n        \"\"\"Returns the wallet path.\"\"\"\n        # command line -w option\n        if path:= self.get('wallet_path'):\n            return self._complete_wallet_path(path)\n        # current wallet\n        path = self.CURRENT_WALLET\n        if path and os.path.exists(path):\n            return path\n        return self.get_fallback_wallet_path()\n\n    def get_datadir_wallet_path(self):\n        util.assert_datadir_available(self.path)\n        dirpath = os.path.join(self.path, \"wallets\")\n        make_dir(dirpath, allow_symlink=False)\n        return dirpath\n\n    def get_fallback_wallet_path(self):\n        return os.path.join(self.get_datadir_wallet_path(), \"default_wallet\")\n\n    def set_session_timeout(self, seconds):\n        self.logger.info(f\"session timeout -> {seconds} seconds\")\n        self.HWD_SESSION_TIMEOUT = seconds\n\n    def get_session_timeout(self):\n        return self.HWD_SESSION_TIMEOUT\n\n    def get_video_device(self):\n        device = self.VIDEO_DEVICE_PATH\n        if device == 'default':\n            device = ''\n        return device\n\n    def format_amount(\n        self,\n        amount_sat,\n        *,\n        is_diff=False,\n        whitespaces=False,\n        precision=None,\n        add_thousands_sep: bool = None,\n    ) -> str:\n        if precision is None:\n            precision = self.amt_precision_post_satoshi\n        if add_thousands_sep is None:\n            add_thousands_sep = self.amt_add_thousands_sep\n        return format_satoshis(\n            amount_sat,\n            num_zeros=self.num_zeros,\n            decimal_point=self.BTC_AMOUNTS_DECIMAL_POINT,\n            is_diff=is_diff,\n            whitespaces=whitespaces,\n            precision=precision,\n            add_thousands_sep=add_thousands_sep,\n        )\n\n    def format_amount_and_units(self, *args, **kwargs) -> str:\n        return self.format_amount(*args, **kwargs) + ' ' + self.get_base_unit()\n\n    def format_fee_rate(self, fee_rate) -> str:\n        \"\"\"fee_rate is in sat/kvByte.\"\"\"\n        return format_fee_satoshis(fee_rate/1000, num_zeros=self.num_zeros) + f\" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}\"\n\n    def get_base_unit(self):\n        return decimal_point_to_base_unit_name(self.BTC_AMOUNTS_DECIMAL_POINT)\n\n    def set_base_unit(self, unit):\n        assert unit in base_units.keys()\n        self.BTC_AMOUNTS_DECIMAL_POINT = base_unit_name_to_decimal_point(unit)\n\n    def get_nostr_relays(self) -> Sequence[str]:\n        relays = []\n        for url in self.NOSTR_RELAYS.split(','):\n            url = url.strip()\n            if url and is_valid_websocket_url(url):\n                relays.append(url)\n        return relays\n\n    def add_nostr_relay(self, relay: str):\n        l = self.get_nostr_relays()\n        if is_valid_websocket_url(relay) and relay not in l:\n            l.append(relay)\n            self.NOSTR_RELAYS = ','.join(l)\n\n    def remove_nostr_relay(self, relay: str):\n        l = self.get_nostr_relays()\n        if relay in l:\n            l.remove(relay)\n            self.NOSTR_RELAYS = ','.join(l)\n\n    def __setattr__(self, name, value):\n        \"\"\"Disallows setting instance attributes outside __init__.\n\n        The point is to make the following code raise:\n        >>> config.NETORK_AUTO_CONNECTT = False\n        (i.e. catch mistyped or non-existent ConfigVars)\n        \"\"\"\n        # If __init__ not finished yet, or this field already exists, set it:\n        if not getattr(self, \"_init_done\", False) or hasattr(self, name):\n            return super().__setattr__(name, value)\n        raise AttributeError(\n            f\"Tried to define new instance attribute for config: {name=!r}. \"\n            \"Did you perhaps mistype a ConfigVar?\"\n        )\n\n    @cached_property\n    def cv(config):\n        \"\"\"Allows getting a reference to a config variable without dereferencing it.\n\n        Compare:\n        >>> config.NETWORK_SERVER\n        'testnet.hsmiths.com:53012:s'\n        >>> config.cv.NETWORK_SERVER\n        <ConfigVarWithConfig key='server'>\n        \"\"\"\n        class CVLookupHelper:\n            def __getattribute__(self, name: str) -> ConfigVarWithConfig:\n                if name in (\"from_key\", ):  # don't apply magic, just use standard lookup\n                    return super().__getattribute__(name)\n                config_var = config.__class__.__getattribute__(type(config), name)\n                if not isinstance(config_var, ConfigVar):\n                    raise AttributeError()\n                return ConfigVarWithConfig(config=config, config_var=config_var)\n            def from_key(self, key: str) -> ConfigVarWithConfig:\n                try:\n                    config_var = _config_var_from_key[key]\n                except KeyError:\n                    raise KeyError(f\"No ConfigVar with key={key!r}\") from None\n                return ConfigVarWithConfig(config=config, config_var=config_var)\n            def __setattr__(self, name, value):\n                raise Exception(\n                    f\"Cannot assign value to config.cv.{name} directly. \"\n                    f\"Either use config.cv.{name}.set() or assign to config.{name} instead.\")\n        return CVLookupHelper()\n\n    # config variables ----->\n    NETWORK_AUTO_CONNECT = ConfigVar(\n        'auto_connect', default=True, type_=bool,\n        short_desc=lambda: _('Select server automatically'),\n        long_desc=lambda: _(\"If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain. \"\n                            \"If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.\"),\n    )\n    NETWORK_ONESERVER = ConfigVar(\n        'oneserver', default=False, type_=bool,\n        short_desc=lambda: _('Only connect to one server (full trust)'),\n        long_desc=lambda: _(\n            \"This is only intended for connecting to your own fully trusted server. \"\n            \"Using this option on a public server is a security risk and is discouraged.\"\n            \"\\n\\n\"\n            \"By default, Electrum tries to maintain connections to ~10 servers. \"\n            \"One of these nodes gets selected to be the history server and will learn the wallet addresses. \"\n            \"All the other nodes are *only* used for block header notifications. \"\n            \"\\n\\n\"\n            \"Getting block headers from multiple sources is useful to detect lagging servers, chain splits, and forks. \"\n            \"Chain split detection is security-critical for determining number of confirmations.\"\n        )\n    )\n    NETWORK_PROXY = ConfigVar('proxy', default=None, type_=str, convert_getter=lambda v: \"none\" if v is None else v)\n    NETWORK_PROXY_USER = ConfigVar('proxy_user', default=None, type_=str)\n    NETWORK_PROXY_PASSWORD = ConfigVar('proxy_password', default=None, type_=str)\n    NETWORK_PROXY_ENABLED = ConfigVar('enable_proxy', default=lambda config: config.NETWORK_PROXY not in [None, \"none\"], type_=bool)\n    NETWORK_SERVER = ConfigVar('server', default=None, type_=str)\n    NETWORK_NOONION = ConfigVar('noonion', default=False, type_=bool)\n    NETWORK_OFFLINE = ConfigVar('offline', default=False, type_=bool)\n    NETWORK_SKIPMERKLECHECK = ConfigVar('skipmerklecheck', default=False, type_=bool)\n    NETWORK_SERVERFINGERPRINT = ConfigVar('serverfingerprint', default=None, type_=str)\n    NETWORK_MAX_INCOMING_MSG_SIZE = ConfigVar('network_max_incoming_msg_size', default=8_100_000, type_=int)  # in bytes\n        # ^ the default is chosen so that the largest consensus-valid tx fits in a JSON-RPC message.\n        #   (so that if we request a tx from the server, we won't reject the response)\n        #   For Bitcoin, that is 4 M weight units, i.e. 4 MB on the p2p wire.\n        #   Double that due to our JSON-RPC hex-encoding, plus overhead, that's 8+ MB.\n    NETWORK_TIMEOUT = ConfigVar('network_timeout', default=None, type_=int)\n    NETWORK_BOOKMARKED_SERVERS = ConfigVar('network_bookmarked_servers', default=None)\n\n    WALLET_MERGE_DUPLICATE_OUTPUTS = ConfigVar(\n        'wallet_merge_duplicate_outputs', default=False, type_=bool,\n        short_desc=lambda: _('Merge duplicate outputs'),\n        long_desc=lambda: _('Merge transaction outputs that pay to the same address into '\n                            'a single output that pays the sum of the original amounts.'),\n    )\n    WALLET_SPEND_CONFIRMED_ONLY = ConfigVar(\n        'confirmed_only', default=False, type_=bool,\n        short_desc=lambda: _('Spend only confirmed coins'),\n        long_desc=lambda: _('Spend only confirmed inputs.'),\n    )\n    WALLET_COIN_CHOOSER_POLICY = ConfigVar('coin_chooser', default='Privacy', type_=str)\n    WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = ConfigVar(\n        'coin_chooser_output_rounding', default=True, type_=bool,\n        short_desc=lambda: _('Enable output value rounding'),\n        long_desc=lambda: (\n            _('Set the value of the change output so that it has similar precision to the other outputs.') + '\\n' +\n            _('This might improve your privacy somewhat.') + '\\n' +\n            _('If enabled, at most 100 satoshis might be lost due to this, per transaction.')),\n    )\n    WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT = ConfigVar('unconf_utxo_freeze_threshold', default=5_000, type_=int)\n    WALLET_PAYREQ_EXPIRY_SECONDS = ConfigVar('request_expiry', default=invoices.PR_DEFAULT_EXPIRATION_WHEN_CREATING, type_=int)\n    WALLET_SHOULD_USE_SINGLE_PASSWORD = ConfigVar('should_use_single_password', default=False, type_=bool)\n    # TODO: consider removing WALLET_DID_USE_SINGLE_PASSWORD once encrypted wallet file headers are available\n    WALLET_DID_USE_SINGLE_PASSWORD = ConfigVar('did_use_single_password', default=False, type_=bool)\n    WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION = ConfigVar('android_use_biometrics', default=False, type_=bool)\n    # this is the wrap key encrypted with a secret stored in AndroidKeyStore\n    WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY = ConfigVar('android_biometrics_encrypted_wrap_key', default='', type_=str)\n    # this is the \"unified wallet password\", encrypted with the wrap key\n    WALLET_ANDROID_BIOMETRIC_AUTH_WRAPPED_WALLET_PASSWORD = ConfigVar('android_biometrics_wrapped_wallet_password', default='', type_=str)\n    # note: 'use_change' and 'multiple_change' are per-wallet settings\n    WALLET_SEND_CHANGE_TO_LIGHTNING = ConfigVar(\n        'send_change_to_lightning', default=False, type_=bool,\n        short_desc=lambda: _('Send change to Lightning'),\n        long_desc=lambda: _('If possible, send the change of this transaction to your channels, with a submarine swap'),\n    )\n    WALLET_ENABLE_SUBMARINE_PAYMENTS = ConfigVar(\n        'enable_submarine_payments', default=False, type_=bool,\n        short_desc=lambda: _('Submarine Payments'),\n        long_desc=lambda: _('Send onchain payments directly from your Lightning balance with a '\n                            'submarine swap. This allows you to do onchain transactions even if your entire '\n                            'wallet balance is inside Lightning channels.')\n    )\n    WALLET_FREEZE_REUSED_ADDRESS_UTXOS = ConfigVar(\n        'wallet_freeze_reused_address_utxos', default=False, type_=bool,\n        short_desc=lambda: _('Avoid spending from used addresses'),\n        long_desc=lambda: _(\"\"\"Automatically freeze coins received to already used addresses.\nThis can eliminate a serious privacy issue where a malicious user can track your spends by sending small payments\nto a previously-paid address of yours that would then be included with unrelated inputs in your future payments.\"\"\"),\n    )\n    WALLET_PARTIAL_WRITES = ConfigVar(\n        'wallet_partial_writes', default=False, type_=bool,\n        long_desc=lambda: _(\"\"\"Allows partial updates to be written to disk for the wallet DB.\nIf disabled, the full wallet file is written to disk for every change. Experimental.\"\"\"),\n    )\n\n    FX_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool)\n    FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str)\n    FX_EXCHANGE = ConfigVar('use_exchange', default='CoinGecko', type_=str)  # default exchange should ideally provide historical rates\n    FX_HISTORY_RATES = ConfigVar(\n        'history_rates', default=False, type_=bool,\n        short_desc=lambda: _('Download historical rates'),\n    )\n    FX_HISTORY_RATES_CAPITAL_GAINS = ConfigVar(\n        'history_rates_capital_gains', default=False, type_=bool,\n        short_desc=lambda: _('Show Capital Gains'),\n    )\n    FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES = ConfigVar(\n        'fiat_address', default=False, type_=bool,\n        short_desc=lambda: _('Show Fiat balances'),\n    )\n\n    LIGHTNING_LISTEN = ConfigVar(\n        'lightning_listen', default=None, type_=str,\n        long_desc=lambda: _(\"\"\"By default the client does not listen on any port for incoming BOLT-08 transports.\nSet this to an interface:port combination, such as 'localhost:9735', to open a port and start listening.\n\nNote: if you open multiple lightning wallets, they will all try to bind the same port, conflict, and only the first will succeed.\"\"\"),\n    )\n    LIGHTNING_PEERS = ConfigVar('lightning_peers', default=None)\n    LIGHTNING_USE_GOSSIP = ConfigVar(\n        'use_gossip', default=False, type_=bool,\n        short_desc=lambda: _(\"Use trampoline routing\"),\n        long_desc=lambda: _(\"\"\"Lightning payments require finding a path through the Lightning Network. You may use trampoline routing, or local routing (gossip).\n\nDownloading the network gossip uses quite some bandwidth and storage, and is not recommended on mobile devices. If you use trampoline, you can only open channels with trampoline nodes.\"\"\"),\n    )\n    LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar(\n        'use_recoverable_channels', default=True, type_=bool,\n        short_desc=lambda: _(\"Create recoverable channels\"),\n        long_desc=lambda: _(\"\"\"Add extra data to your channel funding transactions, so that a static backup can be recovered from your seed.\n\nNote that static backups only allow you to request a force-close with the remote node. This assumes that the remote node is still online, did not lose its data, and accepts to force close the channel.\n\nIf this is enabled, other nodes cannot open a channel to you. Channel recovery data is encrypted, so that only your wallet can decrypt it. However, blockchain analysis will be able to tell that the transaction was probably created by Electrum.\"\"\"),\n    )\n    LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int)\n    LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=LN_MAX_FUNDING_SAT_LEGACY, type_=int)\n    LIGHTNING_MAX_HTLC_VALUE_IN_FLIGHT_MSAT = ConfigVar('lightning_max_htlc_value_in_flight_msat', default=None, type_=int)\n    INITIAL_TRAMPOLINE_FEE_LEVEL = ConfigVar('initial_trampoline_fee_level', default=1, type_=int)\n    LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS = ConfigVar(\n        'lightning_payment_fee_max_millionths', default=10_000,  # 1%\n        type_=int,\n        short_desc=lambda: _(\"Max lightning fees to pay\"),\n        long_desc=lambda: _(\"\"\"When sending lightning payments, this value is an upper bound for the fees we allow paying, proportional to the payment amount. The fees are paid in addition to the payment amount, by the sender.\n\nWarning: setting this to too low will result in lots of payment failures.\"\"\"),\n    )\n    LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT = ConfigVar(\n        'lightning_payment_fee_cutoff_msat', default=10_000,  # 10 sat\n        type_=int,\n        short_desc=lambda: _(\"Max lightning fees to pay for small payments\"),\n    )\n\n    LIGHTNING_NODE_ALIAS = ConfigVar('lightning_node_alias', default='', type_=str)\n    LIGHTNING_NODE_COLOR_RGB = ConfigVar('lightning_node_color_rgb', default='000000', type_=str)\n    EXPERIMENTAL_LN_FORWARD_PAYMENTS = ConfigVar('lightning_forward_payments', default=False, type_=bool)\n    EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = ConfigVar('lightning_forward_trampoline_payments', default=False, type_=bool)\n    TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = ConfigVar('test_fail_htlcs_with_temp_node_failure', default=False, type_=bool)\n    TEST_FAIL_HTLCS_AS_MALFORMED = ConfigVar('test_fail_malformed_htlc', default=False, type_=bool)\n    TEST_FORCE_MPP = ConfigVar('test_force_mpp', default=False, type_=bool)\n    TEST_FORCE_DISABLE_MPP = ConfigVar('test_force_disable_mpp', default=False, type_=bool)\n    TEST_SHUTDOWN_FEE = ConfigVar('test_shutdown_fee', default=None, type_=int)\n    TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None)\n    TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool)\n\n    # fee_policy is a dict: fee_policy_name -> fee_policy_descriptor\n    FEE_POLICY = ConfigVar('fee_policy.default', default='eta:2', type_=str)  # exposed to GUI\n    FEE_POLICY_LIGHTNING = ConfigVar('fee_policy.lnwatcher', default='eta:2', type_=str)  # for txbatcher (sweeping)\n    FEE_POLICY_SWAPS = ConfigVar('fee_policy.swaps', default='eta:2', type_=str)  # for txbatcher (sweeping and sending if we are a swapserver)\n    TEST_DISABLE_AUTOMATIC_FEE_ETA_UPDATE = ConfigVar('test_disable_automatic_fee_eta_update', default=False, type_=bool)\n\n    RPC_USERNAME = ConfigVar('rpcuser', default=None, type_=str)\n    RPC_PASSWORD = ConfigVar('rpcpassword', default=None, type_=str)\n    RPC_HOST = ConfigVar('rpchost', default='127.0.0.1', type_=str)\n    RPC_PORT = ConfigVar('rpcport', default=0, type_=int)\n    RPC_SOCKET_TYPE = ConfigVar('rpcsock', default='auto', type_=str)\n    RPC_SOCKET_FILEPATH = ConfigVar('rpcsockpath', default=None, type_=str)\n\n    GUI_NAME = ConfigVar('gui', default='qt', type_=str)\n    CURRENT_WALLET = ConfigVar('current_wallet', default=None, type_=str)\n\n    GUI_QT_COLOR_THEME = ConfigVar(\n        'qt_gui_color_theme', default='default', type_=str,\n        short_desc=lambda: _('Color theme'),\n    )\n    GUI_QT_DARK_TRAY_ICON = ConfigVar('dark_icon', default=False, type_=bool)\n    GUI_QT_WINDOW_IS_MAXIMIZED = ConfigVar('is_maximized', default=False, type_=bool)\n    GUI_QT_HIDE_ON_STARTUP = ConfigVar('hide_gui', default=False, type_=bool)\n    GUI_QT_HISTORY_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_history', default=False, type_=bool)\n    GUI_QT_ADDRESSES_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_addresses', default=False, type_=bool)\n    GUI_QT_TX_DIALOG_FETCH_TXIN_DATA = ConfigVar(\n        'tx_dialog_fetch_txin_data', default=False, type_=bool,\n        short_desc=lambda: _('Download missing data'),\n        long_desc=lambda: _(\n            'Download parent transactions from the network.\\n'\n            'Allows filling in missing fee and input details.'),\n    )\n    GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA = ConfigVar(\n        'gui_qt_tx_dialog_export_strip_sensitive_metadata', default=False, type_=bool,\n        short_desc=lambda: _('For CoinJoin; strip privates'),\n    )\n    GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS = ConfigVar(\n        'gui_qt_tx_dialog_export_include_global_xpubs', default=False, type_=bool,\n        short_desc=lambda: _('For hardware device; include xpubs'),\n    )\n    GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool)\n    GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar(\n        'show_tx_io', default=False, type_=bool,\n        short_desc=lambda: _('Show inputs and outputs'),\n    )\n    GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS = ConfigVar(\n        'show_tx_fee_details', default=False, type_=bool,\n        short_desc=lambda: _('Edit fees manually'),\n    )\n    GUI_QT_TX_EDITOR_SHOW_LOCKTIME = ConfigVar(\n        'show_tx_locktime', default=False, type_=bool,\n        short_desc=lambda: _('Edit Locktime'),\n    )\n    GUI_QT_SHOW_TAB_ADDRESSES = ConfigVar('show_addresses_tab', default=False, type_=bool)\n    GUI_QT_SHOW_TAB_CHANNELS = ConfigVar('show_channels_tab', default=False, type_=bool)\n    GUI_QT_SHOW_TAB_UTXO = ConfigVar('show_utxo_tab', default=False, type_=bool)\n    GUI_QT_SHOW_TAB_CONTACTS = ConfigVar('show_contacts_tab', default=False, type_=bool)\n    GUI_QT_SHOW_TAB_CONSOLE = ConfigVar('show_console_tab', default=False, type_=bool)\n    GUI_QT_SHOW_TAB_NOTES = ConfigVar('show_notes_tab', default=False, type_=bool)\n    GUI_QT_SCREENSHOT_PROTECTION = ConfigVar(\n        'screenshot_protection', default=True, type_=bool,\n        short_desc=lambda: _(\"Prevent screenshots\"),\n        # currently this option is Windows only, so the description can be specific to Windows\n        long_desc=lambda: _(\n            'Signals Windows to disallow recordings and screenshots of the application window. '\n            'There is no guarantee Windows will respect this signal.'),\n    )\n\n    GUI_QML_PREFERRED_REQUEST_TYPE = ConfigVar('preferred_request_type', default='bolt11', type_=str)\n    GUI_QML_USER_KNOWS_PRESS_AND_HOLD = ConfigVar('user_knows_press_and_hold', default=False, type_=bool)\n    GUI_QML_ADDRESS_LIST_SHOW_TYPE = ConfigVar('address_list_show_type', default=1, type_=int)\n    GUI_QML_ADDRESS_LIST_SHOW_USED = ConfigVar('address_list_show_used', default=False, type_=bool)\n    GUI_QML_ALWAYS_ALLOW_SCREENSHOTS = ConfigVar('android_always_allow_screenshots', default=False, type_=bool)\n    GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY = ConfigVar('android_set_max_brightness_on_qr_display', default=True, type_=bool)\n    GUI_QML_PAYMENT_AUTHENTICATION = ConfigVar('qml_payment_authentication', default=False, type_=bool)\n\n    BTC_AMOUNTS_DECIMAL_POINT = ConfigVar('decimal_point', default=DECIMAL_POINT_DEFAULT, type_=int)\n    BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = ConfigVar(\n        'num_zeros', default=0, type_=int,\n        short_desc=lambda: _('Zeros after decimal point'),\n        long_desc=lambda: _('Number of zeros displayed after the decimal point. For example, if this is set to 2, \"1.\" will be displayed as \"1.00\"'),\n    )\n    BTC_AMOUNTS_PREC_POST_SAT = ConfigVar(\n        'amt_precision_post_satoshi', default=0, type_=int,\n        short_desc=lambda: _(\"Show Lightning amounts with msat precision\"),\n    )\n    BTC_AMOUNTS_ADD_THOUSANDS_SEP = ConfigVar(\n        'amt_add_thousands_sep', default=False, type_=bool,\n        short_desc=lambda: _(\"Add thousand separators to bitcoin amounts\"),\n    )\n\n    BLOCK_EXPLORER = ConfigVar(\n        'block_explorer', default='Blockstream.info', type_=str,\n        short_desc=lambda: _('Online Block Explorer'),\n        long_desc=lambda: _('Choose which online block explorer to use for functions that open a web browser'),\n    )\n    BLOCK_EXPLORER_CUSTOM = ConfigVar('block_explorer_custom', default=None)\n    VIDEO_DEVICE_PATH = ConfigVar(\n        'video_device', default='default', type_=str,\n        short_desc=lambda: _('Video Device'),\n        long_desc=lambda: (_(\"For scanning QR codes.\") + \"\\n\" +\n                           _(\"Install the zbar package to enable this.\")),\n    )\n    OPENALIAS_ID = ConfigVar(\n        'alias', default=\"\", type_=str,\n        short_desc=lambda: 'OpenAlias',\n        long_desc=lambda: (\n            _('OpenAlias record, used to receive coins and to sign payment requests.') + '\\n\\n' +\n            _('The following alias providers are available:') + '\\n' +\n            '\\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\\n\\n' +\n            'For more information, see https://openalias.org'),\n    )\n    HWD_SESSION_TIMEOUT = ConfigVar('session_timeout', default=300, type_=int)\n    CLI_TIMEOUT = ConfigVar('timeout', default=60.0, type_=float, convert_setter=lambda v: float(v))\n    AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = ConfigVar(\n        'check_updates', default=False, type_=bool,\n        short_desc=lambda: _(\"Automatically check for software updates\"),\n    )\n    WRITE_LOGS_TO_DISK = ConfigVar(\n        'log_to_file', default=False, type_=bool,\n        short_desc=lambda: _(\"Write logs to file\"),\n        long_desc=lambda: _('Debug logs can be persisted to disk. These are useful for troubleshooting.'),\n    )\n    LOGS_NUM_FILES_KEEP = ConfigVar(\n        'logs_num_files_keep', default=30, type_=int,\n        long_desc=lambda: _(\"Old log files get deleted on startup, with only the newest few being kept.\"),\n    )\n    LOGS_MAX_TOTAL_SIZE_BYTES = ConfigVar(\n        'logs_max_total_size', default=200_000_000, type_=int,\n        long_desc=lambda: _(\n            \"Old log files get deleted on startup. \"\n            \"This value limits the max total size of the old log files kept, \"\n            \"and also separately the max size of the current log file. \"\n            \"Hence, the max disk usage will be twice this value.\"),\n    )\n    GUI_ENABLE_DEBUG_LOGS = ConfigVar('gui_enable_debug_logs', default=False, type_=bool)\n    LOCALIZATION_LANGUAGE = ConfigVar(\n        'language', default=\"\", type_=str,\n        short_desc=lambda: _(\"Language\"),\n        long_desc=lambda: _(\"Select which language is used in the GUI (after restart).\"),\n    )\n    BLOCKCHAIN_PREFERRED_BLOCK = ConfigVar('blockchain_preferred_block', default=None)\n    DONT_SHOW_TESTNET_WARNING = ConfigVar('dont_show_testnet_warning', default=False, type_=bool)\n    RECENTLY_OPEN_WALLET_FILES = ConfigVar('recently_open', default=None)\n    IO_DIRECTORY = ConfigVar('io_dir', default=os.path.expanduser('~'), type_=str)\n    WALLET_BACKUP_DIRECTORY = ConfigVar('backup_dir', default=None, type_=str)\n    QR_READER_FLIP_X = ConfigVar('qrreader_flip_x', default=True, type_=bool)\n    WIZARD_DONT_CREATE_SEGWIT = ConfigVar('nosegwit', default=False, type_=bool)\n    CONFIG_FORGET_CHANGES = ConfigVar('forget_config', default=False, type_=bool)\n    TERMS_OF_USE_ACCEPTED = ConfigVar('terms_of_use_accepted', default=0, type_=int)\n\n    # connect to remote submarine swap server\n    SWAPSERVER_URL = ConfigVar('swapserver_url', default='', type_=str)\n    TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool)\n    SWAPSERVER_NPUB = ConfigVar('swapserver_npub', default=None, type_=str)\n    SWAPSERVER_POW_TARGET = ConfigVar('swapserver_pow_target', default=30, type_=int)\n\n    # nostr\n    NOSTR_RELAYS = ConfigVar(\n        'nostr_relays',\n        default='wss://relay.getalby.com/v1,wss://nos.lol,wss://relay.damus.io,wss://brb.io,'\n                'wss://relay.primal.net,wss://ftp.halifax.rwth-aachen.de/nostr,'\n                'wss://eu.purplerelay.com,wss://nostr.einundzwanzig.space,wss://nostr.mom',\n        type_=str,\n        short_desc=lambda: _(\"Nostr relays\"),\n        long_desc=lambda: ' '.join([\n            _('Nostr relays are used to send and receive submarine swap offers.'),\n            _('These relays are also used for some plugins, e.g. Nostr Wallet Connect or Nostr Cosigner'),\n        ]),\n    )\n\n    # anchor outputs channels\n    ENABLE_ANCHOR_CHANNELS = ConfigVar('enable_anchor_channels', default=True, type_=bool)\n    # zeroconf channels\n    ACCEPT_ZEROCONF_CHANNELS = ConfigVar('accept_zeroconf_channels', default=False, type_=bool)\n    ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str)\n    ZEROCONF_MIN_OPENING_FEE = ConfigVar('zeroconf_min_opening_fee', default=5000, type_=int)\n    LN_UTXO_RESERVE = ConfigVar(\n        'ln_utxo_reserve',\n        default=10000,\n        type_=int,\n        short_desc=lambda: _(\"Amount that must be kept on-chain in order to sweep anchor output channels\"),\n        long_desc=lambda: _(\"Do not set this below dust limit\"),\n    )\n\n    # connect to remote WT\n    WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str)\n\n    PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int)\n\n\ndef read_user_config(path: Optional[str]) -> Dict[str, Any]:\n    \"\"\"Parse and store the user config settings in electrum.conf into user_config[].\"\"\"\n    if not path:\n        return {}\n    config_path = os.path.join(path, \"config\")\n    if not os.path.exists(config_path):\n        return {}\n    try:\n        with open(config_path, \"r\", encoding='utf-8') as f:\n            data = f.read()\n        result = json.loads(data)\n        assert isinstance(result, dict), \"config file is not a dict\"\n    except Exception as e:\n        raise ValueError(f\"Invalid config file at {config_path}: {str(e)}\")\n    return result\n"
  },
  {
    "path": "electrum/slip39.py",
    "content": "# Copyright (c) 2018 Andrew R. Kozlik\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy of\n# this software and associated documentation files (the \"Software\"), to deal in\n# the Software without restriction, including without limitation the rights to\n# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\n# of the Software, and to permit persons to whom the Software is furnished to do\n# so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n\n\"\"\"\nThis implements the high-level functions for SLIP-39, also called \"Shamir Backup\".\n\nSee https://github.com/satoshilabs/slips/blob/master/slip-0039.md.\n\"\"\"\n\nimport hmac\nfrom collections import defaultdict\nfrom hashlib import pbkdf2_hmac\nfrom typing import Dict, Iterable, List, Optional, Set, Tuple\n\nfrom .i18n import _\nfrom .mnemonic import Wordlist\n\nIndices = Tuple[int, ...]\nMnemonicGroups = Dict[int, Tuple[int, Set[Tuple[int, bytes]]]]\n\n\n\"\"\"\n## Simple helpers\n\"\"\"\n\n_RADIX_BITS = 10\n\"\"\"The length of the radix in bits.\"\"\"\n\n\ndef _bits_to_bytes(n: int) -> int:\n    return (n + 7) // 8\n\n\ndef _bits_to_words(n: int) -> int:\n    return (n + _RADIX_BITS - 1) // _RADIX_BITS\n\n\ndef _xor(a: bytes, b: bytes) -> bytes:\n    return bytes(x ^ y for x, y in zip(a, b))\n\n\n\"\"\"\n## Constants\n\"\"\"\n\n_ID_LENGTH_BITS = 15\n\"\"\"The length of the random identifier in bits.\"\"\"\n\n_ITERATION_EXP_LENGTH_BITS = 4\n\"\"\"The length of the iteration exponent in bits.\"\"\"\n\n_EXTENDABLE_BACKUP_FLAG_LENGTH_BITS = 1\n\"\"\"The length of the extendable backup flag in bits.\"\"\"\n\n_ID_EXP_LENGTH_WORDS = _bits_to_words(\n    _ID_LENGTH_BITS + _EXTENDABLE_BACKUP_FLAG_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS\n)\n\"\"\"The length of the random identifier, extendable backup flag and iteration exponent in words.\"\"\"\n\n_INDEX_LENGTH_BITS = 4\n\"\"\"The length of the group index, group threshold, group count, and member index in bits.\"\"\"\n\n_CHECKSUM_LENGTH_WORDS = 3\n\"\"\"The length of the RS1024 checksum in words.\"\"\"\n\n_DIGEST_LENGTH_BYTES = 4\n\"\"\"The length of the digest of the shared secret in bytes.\"\"\"\n\n_CUSTOMIZATION_STRING_NON_EXTENDABLE = b\"shamir\"\n\"\"\"The customization string used in the RS1024 checksum and in the PBKDF2 salt when extendable backup flag is not set.\"\"\"\n\n_CUSTOMIZATION_STRING_EXTENDABLE = b\"shamir_extendable\"\n\"\"\"The customization string used in the RS1024 checksum when extendable backup flag is set.\"\"\"\n\n_GROUP_PREFIX_LENGTH_WORDS = _ID_EXP_LENGTH_WORDS + 1\n\"\"\"The length of the prefix of the mnemonic that is common to a share group.\"\"\"\n\n_METADATA_LENGTH_WORDS = _ID_EXP_LENGTH_WORDS + 2 + _CHECKSUM_LENGTH_WORDS\n\"\"\"The length of the mnemonic in words without the share value.\"\"\"\n\n_MIN_STRENGTH_BITS = 128\n\"\"\"The minimum allowed entropy of the master secret.\"\"\"\n\n_MIN_MNEMONIC_LENGTH_WORDS = _METADATA_LENGTH_WORDS + _bits_to_words(_MIN_STRENGTH_BITS)\n\"\"\"The minimum allowed length of the mnemonic in words.\"\"\"\n\n_BASE_ITERATION_COUNT = 10000\n\"\"\"The minimum number of iterations to use in PBKDF2.\"\"\"\n\n_ROUND_COUNT = 4\n\"\"\"The number of rounds to use in the Feistel cipher.\"\"\"\n\n_SECRET_INDEX = 255\n\"\"\"The index of the share containing the shared secret.\"\"\"\n\n_DIGEST_INDEX = 254\n\"\"\"The index of the share containing the digest of the shared secret.\"\"\"\n\n\n\"\"\"\n# External API\n\"\"\"\n\n\nclass Slip39Error(RuntimeError):\n    pass\n\n\nclass Share:\n    \"\"\"\n    Represents a single mnemonic and offers its parsed metadata.\n    \"\"\"\n\n    def __init__(\n        self,\n        identifier: int,\n        extendable_backup_flag: bool,\n        iteration_exponent: int,\n        group_index: int,\n        group_threshold: int,\n        group_count: int,\n        member_index: int,\n        member_threshold: int,\n        share_value: bytes,\n    ):\n        self.index = None\n        self.identifier = identifier\n        self.extendable_backup_flag = extendable_backup_flag\n        self.iteration_exponent = iteration_exponent\n        self.group_index = group_index\n        self.group_threshold = group_threshold\n        self.group_count = group_count\n        self.member_index = member_index\n        self.member_threshold = member_threshold\n        self.share_value = share_value\n\n    def common_parameters(self) -> tuple:\n        \"\"\"Return the values that uniquely identify a matching set of shares.\"\"\"\n        return (\n            self.identifier,\n            self.extendable_backup_flag,\n            self.iteration_exponent,\n            self.group_threshold,\n            self.group_count,\n        )\n\n\nclass EncryptedSeed:\n    \"\"\"\n    Represents the encrypted master seed for BIP-32.\n    \"\"\"\n\n    def __init__(\n        self,\n        identifier: int,\n        extendable_backup_flag: bool,\n        iteration_exponent: int,\n        encrypted_master_secret: bytes,\n    ):\n        self.identifier = identifier\n        self.extendable_backup_flag = extendable_backup_flag\n        self.iteration_exponent = iteration_exponent\n        self.encrypted_master_secret = encrypted_master_secret\n\n    def decrypt(self, passphrase: str) -> bytes:\n        \"\"\"\n        Converts the Encrypted Master Secret to a Master Secret by applying the passphrase.\n        This is analogous to BIP-39 passphrase derivation. We do not use the term \"derive\"\n        here, because passphrase function is symmetric in SLIP-39. We are using the terms\n        \"encrypt\" and \"decrypt\" instead.\n        \"\"\"\n        passphrase = (passphrase or '').encode('utf-8')\n        ems_len = len(self.encrypted_master_secret)\n        l = self.encrypted_master_secret[: ems_len // 2]\n        r = self.encrypted_master_secret[ems_len // 2 :]\n        salt = _get_salt(self.identifier, self.extendable_backup_flag)\n        for i in reversed(range(_ROUND_COUNT)):\n            (l, r) = (\n                r,\n                _xor(l, _round_function(i, passphrase, self.iteration_exponent, salt, r)),\n            )\n        return r + l\n\n\ndef recover_ems(mnemonics: List[str]) -> EncryptedSeed:\n    \"\"\"\n    Combines mnemonic shares to obtain the encrypted master secret which was previously\n    split using Shamir's secret sharing scheme.\n    Returns identifier, iteration exponent and the encrypted master secret.\n    \"\"\"\n\n    if not mnemonics:\n        raise Slip39Error(\"The list of mnemonics is empty.\")\n\n    (\n        identifier,\n        extendable_backup_flag,\n        iteration_exponent,\n        group_threshold,\n        group_count,\n        groups,\n    ) = _decode_mnemonics(mnemonics)\n\n    # Use only groups that have at least the threshold number of shares.\n    groups = {group_index: group for group_index, group in groups.items() if len(group[1]) >= group[0]}\n\n    if len(groups) < group_threshold:\n        raise Slip39Error(\n            \"Insufficient number of mnemonic groups. Expected {} full groups, but {} were provided.\".format(\n                group_threshold, len(groups)\n            )\n        )\n\n    group_shares = [\n        (group_index, _recover_secret(group[0], list(group[1])))\n        for group_index, group in groups.items()\n    ]\n\n    encrypted_master_secret = _recover_secret(group_threshold, group_shares)\n    return EncryptedSeed(\n        identifier, extendable_backup_flag, iteration_exponent, encrypted_master_secret\n    )\n\n\ndef decode_mnemonic(mnemonic: str) -> Share:\n    \"\"\"Converts a share mnemonic to share data.\"\"\"\n\n    mnemonic_data = tuple(_mnemonic_to_indices(mnemonic))\n\n    if len(mnemonic_data) < _MIN_MNEMONIC_LENGTH_WORDS:\n        raise Slip39Error(_('Too short.'))\n\n    padding_len = (_RADIX_BITS * (len(mnemonic_data) - _METADATA_LENGTH_WORDS)) % 16\n    if padding_len > 8:\n        raise Slip39Error(_('Invalid length.'))\n\n    idExpExtInt = _int_from_indices(mnemonic_data[:_ID_EXP_LENGTH_WORDS])\n    identifier = idExpExtInt >> (\n        _EXTENDABLE_BACKUP_FLAG_LENGTH_BITS + _ITERATION_EXP_LENGTH_BITS\n    )\n    extendable_backup_flag = bool(\n        (idExpExtInt >> _ITERATION_EXP_LENGTH_BITS)\n        & ((1 << _EXTENDABLE_BACKUP_FLAG_LENGTH_BITS) - 1)\n    )\n    iteration_exponent = idExpExtInt & ((1 << _ITERATION_EXP_LENGTH_BITS) - 1)\n\n    if not _rs1024_verify_checksum(mnemonic_data, extendable_backup_flag):\n        raise Slip39Error(_('Invalid mnemonic checksum.'))\n\n    tmp = _int_from_indices(\n        mnemonic_data[_ID_EXP_LENGTH_WORDS : _ID_EXP_LENGTH_WORDS + 2]\n    )\n    (\n        group_index,\n        group_threshold,\n        group_count,\n        member_index,\n        member_threshold,\n    ) = _int_to_indices(tmp, 5, _INDEX_LENGTH_BITS)\n    value_data = mnemonic_data[_ID_EXP_LENGTH_WORDS + 2 : -_CHECKSUM_LENGTH_WORDS]\n\n    if group_count < group_threshold:\n        raise Slip39Error(_('Invalid mnemonic group threshold.'))\n\n    value_byte_count = _bits_to_bytes(_RADIX_BITS * len(value_data) - padding_len)\n    value_int = _int_from_indices(value_data)\n    if value_data[0] >= 1 << (_RADIX_BITS - padding_len):\n        raise Slip39Error(_('Invalid mnemonic padding.'))\n    value = value_int.to_bytes(value_byte_count, \"big\")\n\n    return Share(\n        identifier,\n        extendable_backup_flag,\n        iteration_exponent,\n        group_index,\n        group_threshold + 1,\n        group_count + 1,\n        member_index,\n        member_threshold + 1,\n        value,\n    )\n\n\ndef get_wordlist() -> Wordlist:\n    wordlist = Wordlist.from_file('slip39.txt')\n\n    required_words = 2**_RADIX_BITS\n    if len(wordlist) != required_words:\n        raise Slip39Error(\n            f\"The wordlist should contain {required_words} words, but it contains {len(wordlist)} words.\"\n        )\n\n    return wordlist\n\n\ndef process_mnemonics(mnemonics: List[str]) -> Tuple[Optional[EncryptedSeed], str]:\n    # Collect valid shares.\n    shares = []\n    for i, mnemonic in enumerate(mnemonics):\n        try:\n            share = decode_mnemonic(mnemonic)\n            share.index = i + 1\n            shares.append(share)\n        except Slip39Error:\n            pass\n\n    if not shares:\n        return None, _('No valid shares.')\n\n    # Sort shares into groups.\n    groups: Dict[int, Set[Share]] = defaultdict(set)  # group idx : shares\n    common_params = shares[0].common_parameters()\n    for share in shares:\n        if share.common_parameters() != common_params:\n            error_text = _(\"Share #{} is not part of the current set.\").format(share.index)\n            return None, _ERROR_STYLE % error_text\n        for other in groups[share.group_index]:\n            if share.member_index == other.member_index:\n                error_text = _(\"Share #{} is a duplicate of share #{}.\").format(share.index, other.index)\n                return None, _ERROR_STYLE % error_text\n        groups[share.group_index].add(share)\n\n    # Compile information about groups.\n    groups_completed = 0\n    for i, group in groups.items():\n        if group:\n            member_threshold = next(iter(group)).member_threshold\n            if len(group) >= member_threshold:\n                groups_completed += 1\n\n    identifier = shares[0].identifier\n    extendable_backup_flag = shares[0].extendable_backup_flag\n    iteration_exponent = shares[0].iteration_exponent\n    group_threshold = shares[0].group_threshold\n    group_count = shares[0].group_count\n    status = ''\n    if group_count > 1:\n        status += _('Completed {} of {} groups needed').format(f\"<b>{groups_completed}</b>\", f\"<b>{group_threshold}</b>\")\n        status += \":<br/>\"\n\n    for group_index in range(group_count):\n        group_prefix = _make_group_prefix(\n            identifier,\n            extendable_backup_flag,\n            iteration_exponent,\n            group_index,\n            group_threshold,\n            group_count,\n        )\n        status += _group_status(groups[group_index], group_prefix)\n\n    if groups_completed >= group_threshold:\n        if len(mnemonics) > len(shares):\n            status += _ERROR_STYLE % _('Some shares are invalid.')\n        else:\n            try:\n                encrypted_seed = recover_ems(mnemonics)\n                status += '<b>' + _('The set is complete!') + '</b>'\n            except Slip39Error as e:\n                encrypted_seed = None\n                status = _ERROR_STYLE % str(e)\n            return encrypted_seed, status\n\n    return None, status\n\n\n\"\"\"\n## Group status helpers\n\"\"\"\n\n_FINISHED = '<span style=\"color:green;\">&#x2714;</span>'\n_EMPTY = '<span style=\"color:red;\">&#x2715;</span>'\n_INPROGRESS = '<span style=\"color:orange;\">&#x26ab;</span>'\n_ERROR_STYLE = '<span style=\"color:red; font-weight:bold;\">' + _('Error') + ': %s</span>'\n\ndef _make_group_prefix(\n    identifier,\n    extendable_backup_flag,\n    iteration_exponent,\n    group_index,\n    group_threshold,\n    group_count,\n):\n    wordlist = get_wordlist()\n    val = identifier\n    val <<= _EXTENDABLE_BACKUP_FLAG_LENGTH_BITS\n    val += int(extendable_backup_flag)\n    val <<= _ITERATION_EXP_LENGTH_BITS\n    val += iteration_exponent\n    val <<= _INDEX_LENGTH_BITS\n    val += group_index\n    val <<= _INDEX_LENGTH_BITS\n    val += group_threshold - 1\n    val <<= _INDEX_LENGTH_BITS\n    val += group_count - 1\n    val >>= 2\n    prefix = ' '.join(wordlist[idx] for idx in _int_to_indices(val, _GROUP_PREFIX_LENGTH_WORDS, _RADIX_BITS))\n    return prefix\n\n\ndef _group_status(group: Set[Share], group_prefix) -> str:\n    len(group)\n    if not group:\n        return _EMPTY + _('{} shares from group {}').format('<b>0</b> ', f'<b>{group_prefix}</b>') + f'.<br/>'\n    else:\n        share = next(iter(group))\n        icon = _FINISHED if len(group) >= share.member_threshold else _INPROGRESS\n        return icon + _('{} of {} shares needed from group {}').format(f'<b>{len(group)}</b>', f'<b>{share.member_threshold}</b>', f'<b>{group_prefix}</b>') + f'.<br/>'\n\n\n\"\"\"\n## Convert mnemonics or integers to indices and back\n\"\"\"\n\n\ndef _int_from_indices(indices: Indices) -> int:\n    \"\"\"Converts a list of base 1024 indices in big endian order to an integer value.\"\"\"\n    value = 0\n    for index in indices:\n        value = (value << _RADIX_BITS) + index\n    return value\n\n\ndef _int_to_indices(value: int, output_length: int, bits: int) -> Iterable[int]:\n    \"\"\"Converts an integer value to indices in big endian order.\"\"\"\n    mask = (1 << bits) - 1\n    return ((value >> (i * bits)) & mask for i in reversed(range(output_length)))\n\n\ndef _mnemonic_to_indices(mnemonic: str) -> List[int]:\n    wordlist = get_wordlist()\n    indices = []\n    for word in mnemonic.split():\n        try:\n            indices.append(wordlist.index(word.lower()))\n        except ValueError:\n            if len(word) > 8:\n                word = word[:8] + '...'\n            raise Slip39Error(_('Invalid mnemonic word') + ' \"%s\".' % word) from None\n    return indices\n\n\n\"\"\"\n## Checksum functions\n\"\"\"\n\n\ndef _get_customization_string(extendable_backup_flag: bool) -> bytes:\n    if extendable_backup_flag:\n        return _CUSTOMIZATION_STRING_EXTENDABLE\n    else:\n        return _CUSTOMIZATION_STRING_NON_EXTENDABLE\n\n\ndef _rs1024_polymod(values: Indices) -> int:\n    GEN = (\n        0xE0E040,\n        0x1C1C080,\n        0x3838100,\n        0x7070200,\n        0xE0E0009,\n        0x1C0C2412,\n        0x38086C24,\n        0x3090FC48,\n        0x21B1F890,\n        0x3F3F120,\n    )\n    chk = 1\n    for v in values:\n        b = chk >> 20\n        chk = (chk & 0xFFFFF) << 10 ^ v\n        for i in range(10):\n            chk ^= GEN[i] if ((b >> i) & 1) else 0\n    return chk\n\n\ndef _rs1024_verify_checksum(data: Indices, extendable_backup_flag: bool) -> bool:\n    \"\"\"\n    Verifies a checksum of the given mnemonic, which was already parsed into Indices.\n    \"\"\"\n    return (\n        _rs1024_polymod(tuple(_get_customization_string(extendable_backup_flag)) + data)\n        == 1\n    )\n\n\n\"\"\"\n## Internal functions\n\"\"\"\n\n\ndef _precompute_exp_log() -> Tuple[List[int], List[int]]:\n    exp = [0 for i in range(255)]\n    log = [0 for i in range(256)]\n\n    poly = 1\n    for i in range(255):\n        exp[i] = poly\n        log[poly] = i\n\n        # Multiply poly by the polynomial x + 1.\n        poly = (poly << 1) ^ poly\n\n        # Reduce poly by x^8 + x^4 + x^3 + x + 1.\n        if poly & 0x100:\n            poly ^= 0x11B\n\n    return exp, log\n\n\n_EXP_TABLE, _LOG_TABLE = _precompute_exp_log()\n\n\ndef _interpolate(shares, x) -> bytes:\n    \"\"\"\n    Returns f(x) given the Shamir shares (x_1, f(x_1)), ... , (x_k, f(x_k)).\n    :param shares: The Shamir shares.\n    :type shares: A list of pairs (x_i, y_i), where x_i is an integer and y_i is an array of\n        bytes representing the evaluations of the polynomials in x_i.\n    :param int x: The x coordinate of the result.\n    :return: Evaluations of the polynomials in x.\n    :rtype: Array of bytes.\n    \"\"\"\n\n    x_coordinates = set(share[0] for share in shares)\n\n    if len(x_coordinates) != len(shares):\n        raise Slip39Error(\"Invalid set of shares. Share indices must be unique.\")\n\n    share_value_lengths = set(len(share[1]) for share in shares)\n    if len(share_value_lengths) != 1:\n        raise Slip39Error(\n            \"Invalid set of shares. All share values must have the same length.\"\n        )\n\n    if x in x_coordinates:\n        for share in shares:\n            if share[0] == x:\n                return share[1]\n\n    # Logarithm of the product of (x_i - x) for i = 1, ... , k.\n    log_prod = sum(_LOG_TABLE[share[0] ^ x] for share in shares)\n\n    result = bytes(share_value_lengths.pop())\n    for share in shares:\n        # The logarithm of the Lagrange basis polynomial evaluated at x.\n        log_basis_eval = (\n            log_prod\n            - _LOG_TABLE[share[0] ^ x]\n            - sum(_LOG_TABLE[share[0] ^ other[0]] for other in shares)\n        ) % 255\n\n        result = bytes(\n            intermediate_sum\n            ^ (\n                _EXP_TABLE[(_LOG_TABLE[share_val] + log_basis_eval) % 255]\n                if share_val != 0\n                else 0\n            )\n            for share_val, intermediate_sum in zip(share[1], result)\n        )\n\n    return result\n\n\ndef _round_function(i: int, passphrase: bytes, e: int, salt: bytes, r: bytes) -> bytes:\n    \"\"\"The round function used internally by the Feistel cipher.\"\"\"\n    return pbkdf2_hmac(\n        \"sha256\",\n        bytes([i]) + passphrase,\n        salt + r,\n        (_BASE_ITERATION_COUNT << e) // _ROUND_COUNT,\n        dklen=len(r),\n    )\n\n\ndef _get_salt(identifier: int, extendable_backup_flag: bool) -> bytes:\n    if extendable_backup_flag:\n        return bytes()\n    else:\n        return _CUSTOMIZATION_STRING_NON_EXTENDABLE + identifier.to_bytes(\n            _bits_to_bytes(_ID_LENGTH_BITS), \"big\"\n        )\n\n\ndef _create_digest(random_data: bytes, shared_secret: bytes) -> bytes:\n    return hmac.new(random_data, shared_secret, \"sha256\").digest()[:_DIGEST_LENGTH_BYTES]\n\n\ndef _recover_secret(threshold: int, shares: List[Tuple[int, bytes]]) -> bytes:\n    # If the threshold is 1, then the digest of the shared secret is not used.\n    if threshold == 1:\n        return shares[0][1]\n\n    shared_secret = _interpolate(shares, _SECRET_INDEX)\n    digest_share = _interpolate(shares, _DIGEST_INDEX)\n    digest = digest_share[:_DIGEST_LENGTH_BYTES]\n    random_part = digest_share[_DIGEST_LENGTH_BYTES:]\n\n    if digest != _create_digest(random_part, shared_secret):\n        raise Slip39Error(\"Invalid digest of the shared secret.\")\n\n    return shared_secret\n\n\ndef _decode_mnemonics(\n    mnemonics: List[str],\n) -> Tuple[int, int, int, int, MnemonicGroups]:\n    identifiers = set()\n    extendable_backup_flags = set()\n    iteration_exponents = set()\n    group_thresholds = set()\n    group_counts = set()\n\n    # { group_index : [threshold, set_of_member_shares] }\n    groups = {}  # type: MnemonicGroups\n    for mnemonic in mnemonics:\n        share = decode_mnemonic(mnemonic)\n        identifiers.add(share.identifier)\n        extendable_backup_flags.add(share.extendable_backup_flag)\n        iteration_exponents.add(share.iteration_exponent)\n        group_thresholds.add(share.group_threshold)\n        group_counts.add(share.group_count)\n        group = groups.setdefault(share.group_index, (share.member_threshold, set()))\n        if group[0] != share.member_threshold:\n            raise Slip39Error(\n                \"Invalid set of mnemonics. All mnemonics in a group must have the same member threshold.\"\n            )\n        group[1].add((share.member_index, share.share_value))\n\n    if (\n        len(identifiers) != 1\n        or len(extendable_backup_flags) != 1\n        or len(iteration_exponents) != 1\n    ):\n        raise Slip39Error(\n            \"Invalid set of mnemonics. All mnemonics must begin with the same {} words.\".format(\n                _ID_EXP_LENGTH_WORDS\n            )\n        )\n\n    if len(group_thresholds) != 1:\n        raise Slip39Error(\n            \"Invalid set of mnemonics. All mnemonics must have the same group threshold.\"\n        )\n\n    if len(group_counts) != 1:\n        raise Slip39Error(\n            \"Invalid set of mnemonics. All mnemonics must have the same group count.\"\n        )\n\n    for group_index, group in groups.items():\n        if len(set(share[0] for share in group[1])) != len(group[1]):\n            raise Slip39Error(\n                \"Invalid set of shares. Member indices in each group must be unique.\"\n            )\n\n    return (\n        identifiers.pop(),\n        extendable_backup_flags.pop(),\n        iteration_exponents.pop(),\n        group_thresholds.pop(),\n        group_counts.pop(),\n        groups,\n    )\n"
  },
  {
    "path": "electrum/sql_db.py",
    "content": "import os\nimport queue\nimport threading\nimport asyncio\nimport sqlite3\n\nfrom .logging import Logger\nfrom .util import test_read_write_permissions\n\n\ndef sql(func):\n    \"\"\"wrapper for sql methods\n\n    returns an awaitable asyncio.Future\n    \"\"\"\n    def wrapper(self: 'SqlDB', *args, **kwargs):\n        assert threading.current_thread() != self.sql_thread\n        f = self.asyncio_loop.create_future()\n        self.db_requests.put((f, func, args, kwargs))\n        return f\n    return wrapper\n\n\nclass SqlDB(Logger):\n\n    def __init__(self, asyncio_loop: asyncio.BaseEventLoop, path, commit_interval=None):\n        Logger.__init__(self)\n        self.asyncio_loop = asyncio_loop\n        self.stopping = False\n        self.stopped_event = asyncio.Event()\n        self.path = path\n        test_read_write_permissions(path)\n        self.commit_interval = commit_interval\n        self.db_requests = queue.Queue()\n        self.sql_thread = threading.Thread(target=self.run_sql)\n        self.sql_thread.start()\n\n    def stop(self):\n        self.stopping = True\n\n    def filesize(self):\n        return os.stat(self.path).st_size\n\n    def run_sql(self):\n        self.logger.info(\"SQL thread started\")\n        self.conn = sqlite3.connect(self.path)\n        self.logger.info(\"Creating database\")\n        self.create_database()\n        i = 0\n        while not self.stopping and self.asyncio_loop.is_running():\n            try:\n                future, func, args, kwargs = self.db_requests.get(timeout=0.1)\n            except queue.Empty:\n                continue\n            try:\n                result = func(self, *args, **kwargs)\n            except BaseException as e:\n                self.asyncio_loop.call_soon_threadsafe(future.set_exception, e)\n                continue\n            if not future.cancelled():\n                self.asyncio_loop.call_soon_threadsafe(future.set_result, result)\n            # note: in sweepstore session.commit() is called inside\n            # the sql-decorated methods, so committing to disk is awaited\n            if self.commit_interval:\n                i = (i + 1) % self.commit_interval\n                if i == 0:\n                    self.conn.commit()\n        # write\n        self.conn.commit()\n        self.conn.close()\n\n        self.logger.info(\"SQL thread terminated\")\n        self.asyncio_loop.call_soon_threadsafe(self.stopped_event.set)\n\n    def create_database(self):\n        raise NotImplementedError()\n"
  },
  {
    "path": "electrum/storage.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport os\nimport threading\nimport stat\nimport hashlib\nimport base64\nimport zlib\nfrom enum import IntEnum\nfrom typing import Optional\n\nimport electrum_ecc as ecc\n\nfrom . import crypto\nfrom .util import (profiler, InvalidPassword, WalletFileException, bfh, standardize_path,\n                   test_read_write_permissions, os_chmod)\n\nfrom .wallet_db import WalletDB\nfrom .logging import Logger\n\n\ndef get_derivation_used_for_hw_device_encryption():\n    return (\"m\"\n            \"/4541509'\"      # ascii 'ELE'  as decimal (\"BIP43 purpose\")\n            \"/1112098098'\")  # ascii 'BIE2' as decimal\n\n\nclass StorageEncryptionVersion(IntEnum):\n    PLAINTEXT = 0\n    USER_PASSWORD = 1\n    XPUB_PASSWORD = 2\n\n\nclass StorageReadWriteError(Exception): pass\n\n\nclass StorageOnDiskUnexpectedlyChanged(Exception): pass\n\n\n# TODO: Rename to Storage\nclass WalletStorage(Logger):\n\n    # TODO maybe split this into separate create() and open() classmethods, to prevent some bugs.\n    #      Until then, the onus is on the caller to check file_exists().\n    def __init__(\n        self,\n        path,\n        *,\n        allow_partial_writes: bool = False,\n    ):\n        Logger.__init__(self)\n        self.path = standardize_path(path)\n        self._file_exists = bool(self.path and os.path.exists(self.path))\n        self.logger.info(f\"wallet path {self.path}\")\n        self._allow_partial_writes = allow_partial_writes\n        self.pubkey = None\n        self.decrypted = ''\n        try:\n            test_read_write_permissions(self.path)\n        except IOError as e:\n            raise StorageReadWriteError(e) from e\n        if self.file_exists():\n            with open(self.path, \"rb\") as f:\n                self.raw = f.read().decode(\"utf-8\")\n                self.pos = f.seek(0, os.SEEK_END)\n                self.init_pos = self.pos\n            self._encryption_version = self._init_encryption_version()\n        else:\n            self.raw = ''\n            self._encryption_version = StorageEncryptionVersion.PLAINTEXT\n            self.pos = 0\n            self.init_pos = 0\n\n    def read(self):\n        return self.decrypted if self.is_encrypted() else self.raw\n\n    def write(self, data: str) -> None:\n        try:\n            mode = os.stat(self.path).st_mode\n        except FileNotFoundError:\n            mode = stat.S_IREAD | stat.S_IWRITE\n        s = self.encrypt_before_writing(data)\n        temp_path = \"%s.tmp.%s\" % (self.path, os.getpid())\n        with open(temp_path, \"wb\") as f:\n            try:\n                os_chmod(temp_path, mode)  # set restrictive perms *before* we write data\n            except PermissionError as e:  # tolerate NFS or similar weirdness?\n                self.logger.warning(f\"cannot chmod temp wallet file: {e!r}\")\n            f.write(s.encode(\"utf-8\"))\n            self.pos = f.seek(0, os.SEEK_END)\n            f.flush()\n            os.fsync(f.fileno())\n        # assert that wallet file does not exist, to prevent wallet corruption (see issue #5082)\n        if not self.file_exists():\n            assert not os.path.exists(self.path)\n        os.replace(temp_path, self.path)\n        self._file_exists = True\n        self.logger.info(f\"saved {self.path}\")\n\n    def append(self, data: str) -> None:\n        \"\"\" append data to file. for the moment, only non-encrypted file\"\"\"\n        assert self._allow_partial_writes\n        assert not self.is_encrypted()\n        with open(self.path, \"rb+\") as f:\n            pos = f.seek(0, os.SEEK_END)\n            if pos != self.pos:\n                raise StorageOnDiskUnexpectedlyChanged(f\"expected size {self.pos}, found {pos}\")\n            f.write(data.encode(\"utf-8\"))\n            self.pos = f.seek(0, os.SEEK_END)\n            f.flush()\n            os.fsync(f.fileno())\n\n    def _needs_consolidation(self):\n        return self.pos > 2 * self.init_pos\n\n    def should_do_full_write_next(self) -> bool:\n        \"\"\"If false, next action can be a partial-write ('append').\"\"\"\n        return (\n            not self.file_exists()\n            or self.is_encrypted()\n            or self._needs_consolidation()\n            or not self._allow_partial_writes\n        )\n\n    def file_exists(self) -> bool:\n        return self._file_exists\n\n    def is_past_initial_decryption(self) -> bool:\n        \"\"\"Return if storage is in a usable state for normal operations.\n\n        The value is True exactly\n            if encryption is disabled completely (self.is_encrypted() == False),\n            or if encryption is enabled but the contents have already been decrypted.\n        \"\"\"\n        return not self.is_encrypted() or bool(self.pubkey)\n\n    def is_encrypted(self) -> bool:\n        \"\"\"Return if storage encryption is currently enabled.\"\"\"\n        return self.get_encryption_version() != StorageEncryptionVersion.PLAINTEXT\n\n    def is_encrypted_with_user_pw(self) -> bool:\n        return self.get_encryption_version() == StorageEncryptionVersion.USER_PASSWORD\n\n    def is_encrypted_with_hw_device(self) -> bool:\n        return self.get_encryption_version() == StorageEncryptionVersion.XPUB_PASSWORD\n\n    def get_encryption_version(self):\n        \"\"\"Return the version of encryption used for this storage.\n\n        0: plaintext / no encryption\n\n        ECIES, private key derived from a password,\n        1: password is provided by user\n        2: password is derived from an xpub; used with hw wallets\n        \"\"\"\n        return self._encryption_version\n\n    def _init_encryption_version(self):\n        try:\n            magic = base64.b64decode(self.raw, validate=True)[0:4]\n            if magic == b'BIE1':\n                return StorageEncryptionVersion.USER_PASSWORD\n            elif magic == b'BIE2':\n                return StorageEncryptionVersion.XPUB_PASSWORD\n            else:\n                return StorageEncryptionVersion.PLAINTEXT\n        except Exception:\n            return StorageEncryptionVersion.PLAINTEXT\n\n    @staticmethod\n    def get_eckey_from_password(password):\n        if password is None:\n            password = \"\"\n        secret = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), b'', iterations=1024)\n        ec_key = ecc.ECPrivkey.from_arbitrary_size_secret(secret)\n        return ec_key\n\n    def _get_encryption_magic(self):\n        v = self._encryption_version\n        if v == StorageEncryptionVersion.USER_PASSWORD:\n            return b'BIE1'\n        elif v == StorageEncryptionVersion.XPUB_PASSWORD:\n            return b'BIE2'\n        else:\n            raise WalletFileException('no encryption magic for version: %s' % v)\n\n    def decrypt(self, password) -> None:\n        \"\"\"Raises an InvalidPassword exception on invalid password\"\"\"\n        if self.is_past_initial_decryption():\n            return\n        ec_key = self.get_eckey_from_password(password)\n        if self.raw:\n            enc_magic = self._get_encryption_magic()\n            s = zlib.decompress(crypto.ecies_decrypt_message(ec_key, self.raw, magic=enc_magic))\n            s = s.decode('utf8')\n        else:\n            s = ''\n        self.pubkey = ec_key.get_public_key_hex()\n        self.decrypted = s\n\n    def encrypt_before_writing(self, plaintext: str) -> str:\n        s = plaintext\n        if self.pubkey:\n            self.decrypted = plaintext\n            s = bytes(s, 'utf8')\n            c = zlib.compress(s, level=zlib.Z_BEST_SPEED)\n            enc_magic = self._get_encryption_magic()\n            public_key = ecc.ECPubkey(bfh(self.pubkey))\n            s = crypto.ecies_encrypt_message(public_key, c, magic=enc_magic)\n            s = s.decode('utf8')\n        return s\n\n    def check_password(self, password: Optional[str]) -> None:\n        \"\"\"Raises an InvalidPassword exception on invalid password\"\"\"\n        if not self.is_encrypted():\n            if password is not None:\n                raise InvalidPassword(\"password given but wallet has no password\")\n            return\n        if not self.is_past_initial_decryption():\n            self.decrypt(password)  # this sets self.pubkey\n        assert self.pubkey is not None\n        if self.pubkey != self.get_eckey_from_password(password).get_public_key_hex():\n            raise InvalidPassword()\n\n    def set_password(self, password, enc_version=None):\n        \"\"\"Set a password to be used for encrypting this storage.\"\"\"\n        if not self.is_past_initial_decryption():\n            raise Exception(\"storage needs to be decrypted before changing password\")\n        if enc_version is None:\n            enc_version = self._encryption_version\n        if password and enc_version != StorageEncryptionVersion.PLAINTEXT:\n            ec_key = self.get_eckey_from_password(password)\n            self.pubkey = ec_key.get_public_key_hex()\n            self._encryption_version = enc_version\n        else:\n            self.pubkey = None\n            self._encryption_version = StorageEncryptionVersion.PLAINTEXT\n\n    def basename(self) -> str:\n        return os.path.basename(self.path)\n\n"
  },
  {
    "path": "electrum/submarine_swaps.py",
    "content": "import asyncio\nimport json\nimport os\nimport ssl\nimport threading\nfrom typing import TYPE_CHECKING, Optional, Dict, Sequence, Tuple, Iterable, List\nfrom decimal import Decimal\nimport math\nimport time\n\nimport attr\nimport aiohttp\n\nfrom electrum_ecc import ECPrivkey\n\nimport electrum_aionostr as aionostr\nimport electrum_aionostr.key\nfrom electrum_aionostr.event import Event\nfrom electrum_aionostr.util import to_nip19\n\nfrom collections import defaultdict\n\n\nfrom .i18n import _\nfrom .logging import Logger\nfrom .crypto import sha256, ripemd\nfrom .bitcoin import (script_to_p2wsh, opcodes, dust_threshold, DummyAddress, construct_witness,\n                      construct_script, address_to_script)\nfrom . import bitcoin\nfrom .transaction import (\n    PartialTxInput, PartialTxOutput, PartialTransaction, Transaction, TxInput, TxOutpoint, script_GetOp,\n    match_script_against_template, OPPushDataGeneric, OPPushDataPubkey, TxOutput,\n)\nfrom .util import (\n    log_exceptions, ignore_exceptions, BelowDustLimit, OldTaskGroup, ca_path, gen_nostr_ann_pow,\n    get_nostr_ann_pow_amount, make_aiohttp_proxy_connector, get_running_loop, get_asyncio_loop, wait_for2,\n    run_sync_function_on_asyncio_thread, trigger_callback, NoDynamicFeeEstimates, UserFacingException,\n)\nfrom . import lnutil\nfrom .lnutil import hex_to_bytes, REDEEM_AFTER_DOUBLE_SPENT_DELAY, Keypair\nfrom .lnaddr import lndecode\nfrom .json_db import StoredObject, stored_in\nfrom . import constants\nfrom .address_synchronizer import (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE, TX_HEIGHT_UNCONFIRMED,\n                                   TX_HEIGHT_UNCONF_PARENT)\nfrom .fee_policy import FeePolicy\nfrom .invoices import Invoice, PR_PAID\nfrom .lnonion import OnionRoutingFailure, OnionFailureCode\nfrom .lnsweep import SweepInfo\n\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .wallet import Abstract_Wallet\n    from .lnwatcher import LNWatcher\n    from .lnworker import LNWallet\n    from .lnchannel import Channel\n    from .simple_config import SimpleConfig\n    from aiohttp_socks import ProxyConnector\n\n\nSWAP_TX_SIZE = 150  # default tx size, used for mining fee estimation\n\nMIN_SWAP_AMOUNT_SAT = 20_000\nMIN_LOCKTIME_DELTA = 60\nLOCKTIME_DELTA_REFUND = 70\nMAX_LOCKTIME_DELTA = 100\nMIN_FINAL_CLTV_DELTA_FOR_CLIENT = 3 * 144  # note: put in invoice, but is not enforced by receiver in lnpeer.py\nassert MIN_LOCKTIME_DELTA <= LOCKTIME_DELTA_REFUND <= MAX_LOCKTIME_DELTA\nassert MAX_LOCKTIME_DELTA < lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED\nassert MAX_LOCKTIME_DELTA < MIN_FINAL_CLTV_DELTA_FOR_CLIENT\n\n\n# The script of the reverse swaps has one extra check in it to verify\n# that the length of the preimage is 32. This is required because in\n# the reverse swaps the preimage is generated by the user and to\n# settle the hold invoice, you need a preimage with 32 bytes . If that\n# check wasn't there the user could generate a preimage with a\n# different length which would still allow for claiming the onchain\n# coins but the invoice couldn't be settled\n\n# Unified witness-script for all swaps.  Historically with Boltz-backend, this was the reverse-swap script.\nWITNESS_TEMPLATE_SWAP = [\n    opcodes.OP_SIZE,\n    OPPushDataGeneric(None),               # idx 1. length of preimage\n    opcodes.OP_EQUAL,\n    opcodes.OP_IF,\n    opcodes.OP_HASH160,\n    OPPushDataGeneric(lambda x: x == 20),  # idx 5. payment_hash\n    opcodes.OP_EQUALVERIFY,\n    OPPushDataPubkey,                      # idx 7. claim_pubkey\n    opcodes.OP_ELSE,\n    opcodes.OP_DROP,\n    OPPushDataGeneric(None),               # idx 10. locktime\n    opcodes.OP_CHECKLOCKTIMEVERIFY,\n    opcodes.OP_DROP,\n    OPPushDataPubkey,                      # idx 13. refund_pubkey\n    opcodes.OP_ENDIF,\n    opcodes.OP_CHECKSIG\n]\n\n\ndef _check_swap_scriptcode(\n    *,\n    redeem_script: bytes,\n    lockup_address: str,\n    payment_hash: bytes,\n    locktime: int,\n    refund_pubkey: Optional[bytes],   # note: We don't need to check the counterparty's key.\n    claim_pubkey: Optional[bytes],    #       Can use None in that case.\n) -> None:\n    assert (refund_pubkey is not None) or (claim_pubkey is not None), \"at least one pubkey must be set\"\n    parsed_script = [x for x in script_GetOp(redeem_script)]\n    if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_SWAP):\n        raise Exception(\"rswap check failed: scriptcode does not match template\")\n    if script_to_p2wsh(redeem_script) != lockup_address:\n        raise Exception(\"rswap check failed: inconsistent scriptcode and address\")\n    if ripemd(payment_hash) != parsed_script[5][1]:\n        raise Exception(\"rswap check failed: our preimage not in script\")\n    claim_pubkey = claim_pubkey or parsed_script[7][1]\n    if claim_pubkey != parsed_script[7][1]:\n        raise Exception(\"rswap check failed: our pubkey not in script\")\n    refund_pubkey = refund_pubkey or parsed_script[13][1]\n    if refund_pubkey != parsed_script[13][1]:\n        raise Exception(\"rswap check failed: our pubkey not in script\")\n    if locktime != int.from_bytes(parsed_script[10][1], byteorder='little'):\n        raise Exception(\"rswap check failed: inconsistent locktime and script\")\n    # let's just rebuild the full script from scratch...\n    if redeem_script != _construct_swap_scriptcode(\n        payment_hash=payment_hash,\n        locktime=locktime,\n        refund_pubkey=refund_pubkey,\n        claim_pubkey=claim_pubkey,\n    ):\n        raise Exception(\"failed to rebuild swap script from scratch\")\n\n\ndef _construct_swap_scriptcode(\n    payment_hash: bytes,\n    locktime: int,\n    refund_pubkey: bytes,\n    claim_pubkey: bytes,\n) -> bytes:\n    assert isinstance(payment_hash, bytes) and len(payment_hash) == 32\n    assert isinstance(locktime, int) and (0 <= locktime <= bitcoin.NLOCKTIME_BLOCKHEIGHT_MAX)\n    assert isinstance(refund_pubkey, bytes) and len(refund_pubkey) == 33\n    assert isinstance(claim_pubkey, bytes) and len(claim_pubkey) == 33\n    return construct_script(\n        WITNESS_TEMPLATE_SWAP,\n        values={1: 32, 5: ripemd(payment_hash), 7: claim_pubkey, 10: locktime, 13: refund_pubkey}\n    )\n\n\nclass SwapServerError(Exception):\n    def __init__(self, message=None):\n        self.message = message\n        super().__init__(message)\n\n    def __str__(self):\n        if self.message:\n            return self.message\n        return _(\"The swap server errored or is unreachable.\")\n\n\ndef now():\n    return int(time.time())\n\n\n@attr.s(frozen=True)\nclass SwapFees:\n    percentage = attr.ib(type=Decimal)\n    mining_fee = attr.ib(type=int)\n    min_amount = attr.ib(type=int)\n    max_forward = attr.ib(type=int)\n    max_reverse = attr.ib(type=int)\n\n\n@attr.frozen\nclass SwapOffer:\n    pairs = attr.ib(type=SwapFees)\n    relays = attr.ib(type=list[str])\n    pow_bits = attr.ib(type=int)\n    server_pubkey = attr.ib(type=str)\n    timestamp = attr.ib(type=int)\n\n    @property\n    def server_npub(self):\n        return to_nip19('npub', self.server_pubkey)\n\n\n@stored_in('submarine_swaps')\n@attr.s\nclass SwapData(StoredObject):\n    is_reverse = attr.ib(type=bool)  # for whoever is running code (PoV of client or server)\n    locktime = attr.ib(type=int)  # onchain, abs\n    onchain_amount = attr.ib(type=int)  # in sats\n    lightning_amount = attr.ib(type=int)  # in sats\n    redeem_script = attr.ib(type=bytes, converter=hex_to_bytes)\n    preimage = attr.ib(type=Optional[bytes], converter=hex_to_bytes)\n    prepay_hash = attr.ib(type=Optional[bytes], converter=hex_to_bytes)\n    privkey = attr.ib(type=bytes, converter=hex_to_bytes)\n    lockup_address = attr.ib(type=str)\n    claim_to_output = attr.ib(type=Optional[Tuple[str, int]])  # address, amount to claim the funding utxo to\n    funding_txid = attr.ib(type=Optional[str])\n    spending_txid = attr.ib(type=Optional[str])\n    is_redeemed = attr.ib(type=bool)\n\n    _funding_prevout = None  # type: Optional[TxOutpoint]  # for RBF\n    _payment_hash = None\n    _payment_pending = False # for forward swaps\n\n    @property\n    def payment_hash(self) -> bytes:\n        return self._payment_hash\n\n    def is_funded(self) -> bool:\n        return self._payment_pending or bool(self.funding_txid)\n\n\ndef pubkey_to_rgb_color(swapserver_pubkey: str) -> Tuple[int, int, int]:\n    assert isinstance(swapserver_pubkey, str), type(swapserver_pubkey)\n    assert len(swapserver_pubkey) == 64, len(swapserver_pubkey)\n    input_hash = int.from_bytes(sha256(swapserver_pubkey), byteorder=\"big\")\n    r = (input_hash & 0xFF0000) >> 16\n    g = (input_hash & 0x00FF00) >> 8\n    b = input_hash & 0x0000FF\n    return r, g, b\n\n\nclass SwapManager(Logger):\n\n    network: Optional['Network'] = None\n    lnwatcher: Optional['LNWatcher'] = None\n\n    def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'):\n        Logger.__init__(self)\n        self.mining_fee = None\n        self.percentage = None  # type: Optional[Decimal]\n        self._min_amount = None\n        self._max_forward = None\n        self._max_reverse = None\n\n        self.wallet = wallet\n        self.config = wallet.config\n        self.lnworker = lnworker\n        self.lnwatcher = self.lnworker.lnwatcher\n        self.config = wallet.config\n        self.taskgroup = OldTaskGroup()\n        self.dummy_address = DummyAddress.SWAP\n\n        # note: accessing swaps dicts (besides simple lookup) needs swaps_lock\n        self.swaps_lock = threading.Lock()\n        self._swaps = self.wallet.db.get_dict('submarine_swaps')  # type: Dict[str, SwapData]\n        self._swaps_by_funding_outpoint = {}  # type: Dict[TxOutpoint, SwapData]\n        self._swaps_by_lockup_address = {}  # type: Dict[str, SwapData]\n        for payment_hash_hex, swap in self._swaps.items():\n            payment_hash = bytes.fromhex(payment_hash_hex)\n            swap._payment_hash = payment_hash\n            self._add_or_reindex_swap(swap, is_new=False)\n            if not swap.is_reverse and not swap.is_redeemed and not self.lnworker.get_preimage(swap.payment_hash):\n                self.lnworker.register_hold_invoice(payment_hash, self.hold_invoice_callback)\n\n        self._prepayments = {}  # type: Dict[bytes, bytes] # fee_rhash -> rhash\n        for k, swap in self._swaps.items():\n            if swap.prepay_hash is not None:\n                self._prepayments[swap.prepay_hash] = bytes.fromhex(k)\n        self.is_server = False # overridden by swapserver plugin if enabled\n        self.is_initialized = asyncio.Event()\n        self.pairs_updated = asyncio.Event()\n\n    def start_network(self, network: 'Network'):\n        assert network\n        if self.network is not None:\n            self.logger.info('start_network: already started')\n            return\n        self.logger.info('start_network: starting main loop')\n        self.network = network\n        with self.swaps_lock:\n            swaps_items = list(self._swaps.items())\n        for k, swap in swaps_items:\n            if swap.is_redeemed:\n                continue\n            self.add_lnwatcher_callback(swap)\n        asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)\n\n    @log_exceptions\n    async def run_nostr_server(self):\n        await self.set_nostr_proof_of_work()\n\n        while self.wallet.has_password() and self.wallet.get_unlocked_password() is None:\n            self.logger.info(\"This wallet is password-protected. Please unlock it to start the swapserver plugin\")\n            await asyncio.sleep(10)\n\n        with NostrTransport(self.config, self, self.lnworker.nostr_keypair) as transport:\n            await transport.is_connected.wait()\n            self.logger.info(f'nostr is connected')\n            # will publish a new announcement if liquidity changed or every OFFER_UPDATE_INTERVAL_SEC\n            last_update = time.time()\n            while True:\n                await asyncio.sleep(transport.LIQUIDITY_UPDATE_INTERVAL_SEC)\n\n                previous_max_forward = self._max_forward\n                previous_max_reverse = self._max_reverse\n                previous_mining_fee = self.mining_fee\n                try:\n                    self.server_update_pairs()\n                except Exception:\n                    self.logger.exception(\"server_update_pairs failed\")\n                    continue\n\n                liquidity_changed = self._max_forward != previous_max_forward \\\n                                        or self._max_reverse != previous_max_reverse\n                mining_fees_changed = self.mining_fee != previous_mining_fee\n                if liquidity_changed or mining_fees_changed:\n                    self.logger.debug(f\"updating announcement: {liquidity_changed=}, {mining_fees_changed=}\")\n                elif time.time() - last_update < transport.OFFER_UPDATE_INTERVAL_SEC:\n                    continue\n\n                await transport.publish_offer(self)\n                last_update = time.time()\n\n    @log_exceptions\n    async def main_loop(self):\n        tasks = [self.pay_pending_invoices()]\n        if self.is_server:\n            # nostr and http are not mutually exclusive\n            if self.config.SWAPSERVER_PORT:\n                tasks.append(self.http_server.run())\n            if self.config.NOSTR_RELAYS:\n                tasks.append(self.run_nostr_server())\n\n        async with self.taskgroup as group:\n            for task in tasks:\n                await group.spawn(task)\n\n    async def stop(self):\n        await self.taskgroup.cancel_remaining()\n\n    def create_transport(self) -> 'SwapServerTransport':\n        from .lnutil import generate_random_keypair\n        if self.config.SWAPSERVER_URL:\n            return HttpTransport(self.config, self)\n        else:\n            keypair = self.lnworker.nostr_keypair if self.is_server else generate_random_keypair()\n            return NostrTransport(self.config, self, keypair)\n\n    async def set_nostr_proof_of_work(self) -> None:\n        current_pow = get_nostr_ann_pow_amount(\n            self.lnworker.nostr_keypair.pubkey[1:],\n            self.config.SWAPSERVER_ANN_POW_NONCE\n        )\n        if current_pow >= self.config.SWAPSERVER_POW_TARGET:\n            self.logger.debug(f\"Reusing existing PoW nonce for nostr announcement.\")\n            return\n\n        self.logger.info(f\"Generating PoW for nostr announcement. Target: {self.config.SWAPSERVER_POW_TARGET}\")\n        nonce, pow_amount = await gen_nostr_ann_pow(\n            self.lnworker.nostr_keypair.pubkey[1:],  # pubkey without prefix\n            self.config.SWAPSERVER_POW_TARGET,\n        )\n        self.logger.debug(f\"Found {pow_amount} bits of work for Nostr announcement.\")\n        self.config.SWAPSERVER_ANN_POW_NONCE = nonce\n\n    async def pay_invoice(self, key):\n        self.logger.info(f'trying to pay invoice {key}')\n        self.invoices_to_pay[key] = 1000000000000 # lock\n        try:\n            invoice = self.wallet.get_invoice(key)\n            success, log = await self.lnworker.pay_invoice(invoice)\n        except Exception as e:\n            self.logger.info(f'exception paying {key}, will not retry')\n            self.invoices_to_pay.pop(key, None)\n            return\n        if not success:\n            self.logger.info(f'failed to pay {key}, will retry in 10 minutes')\n            self.invoices_to_pay[key] = now() + 600\n        else:\n            self.logger.info(f'paid invoice {key}')\n            self.invoices_to_pay.pop(key, None)\n\n    async def pay_pending_invoices(self):\n        self.invoices_to_pay = {}\n        while True:\n            await asyncio.sleep(5)\n            for key, not_before in list(self.invoices_to_pay.items()):\n                if now() < not_before:\n                    continue\n                await self.taskgroup.spawn(self.pay_invoice(key))\n\n    def cancel_normal_swap(self, swap: SwapData):\n        \"\"\" we must not have broadcast the funding tx \"\"\"\n        if swap is None:\n            return\n        if swap.is_funded():\n            self.logger.info(f'cannot cancel swap {swap.payment_hash.hex()}: already funded')\n            return\n        self._fail_swap(swap, 'user cancelled')\n\n    def _fail_swap(self, swap: SwapData, reason: str):\n        self.logger.info(f'failing swap {swap.payment_hash.hex()}: {reason}')\n        if not swap.is_reverse and swap.payment_hash in self.lnworker.hold_invoice_callbacks:\n            # unregister_hold_invoice will fail pending htlcs if there is no preimage available\n            self.lnworker.unregister_hold_invoice(swap.payment_hash)\n            self.lnworker.delete_payment_info(swap.payment_hash.hex(), direction=lnutil.RECEIVED)\n            self.lnworker.clear_invoices_cache()\n        self.lnwatcher.remove_callback(swap.lockup_address)\n        if not swap.is_funded():\n            with self.swaps_lock:\n                if self._swaps.pop(swap.payment_hash.hex(), None) is None:\n                    self.logger.debug(f\"swap {swap.payment_hash.hex()} has already been deleted.\")\n                if swap._funding_prevout is not None:\n                    self._swaps_by_funding_outpoint.pop(swap._funding_prevout, None)\n                self._swaps_by_lockup_address.pop(swap.lockup_address, None)\n                if swap.prepay_hash is not None:\n                    self._prepayments.pop(swap.prepay_hash, None)\n                    if self.lnworker.get_payment_status(swap.prepay_hash, direction=lnutil.RECEIVED) != PR_PAID:\n                        self.lnworker.delete_payment_info(swap.prepay_hash.hex(), direction=lnutil.RECEIVED)\n                        self.lnworker.delete_payment_bundle(payment_hash=swap.payment_hash)\n                    if self.lnworker.get_payment_status(swap.prepay_hash, direction=lnutil.SENT) != PR_PAID:\n                        self.lnworker.delete_payment_info(swap.prepay_hash.hex(), direction=lnutil.SENT)\n                if self.lnworker.get_payment_status(swap.payment_hash, direction=lnutil.SENT) != PR_PAID:\n                    self.lnworker.delete_payment_info(swap.payment_hash.hex(), direction=lnutil.SENT)\n\n    @classmethod\n    def extract_preimage(cls, swap: SwapData, claim_tx: Transaction) -> Optional[bytes]:\n        for txin in claim_tx.inputs():\n            witness = txin.witness_elements()\n            if not witness or len(witness) < 2:\n                # tx may be unsigned\n                continue\n            preimage = witness[1]\n            if sha256(preimage) == swap.payment_hash:\n                return preimage\n        return None\n\n    @log_exceptions\n    async def _claim_swap(self, swap: SwapData) -> None:\n        assert self.network\n        assert self.lnwatcher\n        if not self.lnwatcher.adb.is_up_to_date():\n            return\n        current_height = self.network.get_local_height()\n        remaining_time = swap.locktime - current_height\n        txos = self.lnwatcher.adb.get_addr_outputs(swap.lockup_address)\n\n        for txin in txos.values():\n            if swap.is_reverse and txin.value_sats() < swap.onchain_amount:\n                # amount too low, we must not reveal the preimage\n                continue\n            break\n        else:\n            # swap not funded.\n            txin = None\n            # if it is a normal swap, we might have double spent the funding tx\n            # in that case we need to fail the HTLCs\n            if remaining_time <= 0:\n                self._fail_swap(swap, 'expired')\n\n        if txin:\n            # the swap is funded\n            # note: swap.funding_txid can change due to RBF, it will get updated here:\n            swap.funding_txid = txin.prevout.txid.hex()\n            swap._funding_prevout = txin.prevout\n            self._add_or_reindex_swap(swap, is_new=False)  # to update _swaps_by_funding_outpoint\n            funding_height = self.lnwatcher.adb.get_tx_height(txin.prevout.txid.hex())\n            spent_height = txin.spent_height\n            # set spending_txid (even if tx is local), for GUI grouping\n            swap.spending_txid = txin.spent_txid\n            # discard local spenders\n            if spent_height in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:\n                spent_height = None\n            if spent_height is not None:\n                if spent_height > 0 and swap.preimage:\n                    if current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY:\n                        self.logger.info(f'stop watching swap {swap.lockup_address}')\n                        swap.is_redeemed = True\n                        # cleanup\n                        self.lnwatcher.remove_callback(swap.lockup_address)\n                        if not swap.is_reverse:\n                            self.lnworker.delete_payment_bundle(payment_hash=swap.payment_hash)\n                            self.lnworker.unregister_hold_invoice(swap.payment_hash)\n\n            if not swap.is_reverse:\n                if swap.preimage is None and spent_height is not None:\n                    # extract the preimage, add it to lnwatcher\n                    claim_tx = self.lnwatcher.adb.get_transaction(txin.spent_txid)\n                    preimage = self.extract_preimage(swap, claim_tx)\n                    if preimage:\n                        swap.preimage = preimage\n                        self.logger.info(f'found preimage: {preimage.hex()}')\n                        self.lnworker.save_preimage(swap.payment_hash, preimage, mark_as_public=True)\n                    else:\n                        # this is our refund tx\n                        if spent_height > 0:\n                            self.logger.info(f'refund tx confirmed: {txin.spent_txid} {spent_height}')\n                            self._fail_swap(swap, 'refund tx confirmed')\n                            return\n                if remaining_time > 0:\n                    # too early for refund\n                    return\n                if swap.preimage:\n                    # we have been paid. do not try to get refund.\n                    return\n            else:\n                if swap.preimage is None:\n                    swap.preimage = self.lnworker.get_preimage(swap.payment_hash)\n                if swap.preimage is None:\n                    if funding_height.conf <= 0:\n                        return\n                    key = swap.payment_hash.hex()\n                    if remaining_time <= MIN_LOCKTIME_DELTA:\n                        if key in self.invoices_to_pay:\n                            # fixme: should consider cltv of ln payment\n                            self.logger.info(f'locktime too close {key} {remaining_time}')\n                            self.invoices_to_pay.pop(key, None)\n                        return\n                    if key not in self.invoices_to_pay:\n                        self.invoices_to_pay[key] = 0\n                    return\n\n                if self.network.config.TEST_SWAPSERVER_REFUND:\n                    # for testing: do not create claim tx\n                    return\n\n            if spent_height is not None and spent_height > 0:\n                return\n            txin, locktime = self.create_claim_txin(txin=txin, swap=swap)\n            if swap.is_reverse and swap.claim_to_output:\n                asyncio.create_task(self._claim_to_output(swap, txin))\n                return\n            # note: there is no csv in the script, we just set this so that txbatcher waits for one confirmation\n            name = 'swap claim' if swap.is_reverse else 'swap refund'\n            can_be_batched = True\n            sweep_info = SweepInfo(\n                txin=txin,\n                cltv_abs=locktime,\n                txout=None,\n                name=name,\n                can_be_batched=can_be_batched,\n                dust_override=False,\n            )\n            try:\n                self.wallet.txbatcher.add_sweep_input('swaps', sweep_info)\n            except BelowDustLimit:\n                self.logger.info('utxo value below dust threshold')\n                return\n            except NoDynamicFeeEstimates:\n                self.logger.info('got NoDynamicFeeEstimates')\n                return\n\n    async def _claim_to_output(self, swap: SwapData, claim_txin: PartialTxInput):\n        \"\"\"\n        Construct claim tx that spends exactly the funding utxo to the swap output, independent of the\n        current fee environment to guarantee the correct amount is being sent to the claim output which\n        might be an external address.\n        \"\"\"\n        assert swap.claim_to_output, swap\n        txout = PartialTxOutput.from_address_and_value(swap.claim_to_output[0], swap.claim_to_output[1])\n        tx = PartialTransaction.from_io([claim_txin], [txout])\n        funding_tx_confirmed = self.wallet.adb.get_tx_height(swap.funding_txid).height() > TX_HEIGHT_UNCONFIRMED\n        already_broadcast = self.wallet.adb.get_tx_height(tx.txid()).height() >= TX_HEIGHT_UNCONF_PARENT\n        self.logger.debug(f\"_claim_to_output: {funding_tx_confirmed=} {already_broadcast=}\")\n\n        # add tx to db so it can be shown as future tx\n        if not self.wallet.adb.get_transaction(tx.txid()):\n            try:\n                self.wallet.adb.add_transaction(tx)\n            except Exception:\n                self.logger.exception(\"\")\n                return\n            trigger_callback('wallet_updated', self)\n\n        # set or update future tx wanted height if it has not been broadcast yet\n        local_height = self.network.get_local_height()\n        wanted_height = local_height + claim_txin.get_block_based_relative_locktime()\n        if not already_broadcast and self.wallet.adb.future_tx.get(tx.txid(), 0) < wanted_height:\n            self.wallet.adb.set_future_tx(tx.txid(), wanted_height=wanted_height)\n\n        if funding_tx_confirmed and not already_broadcast:\n            tx = self.wallet.sign_transaction(tx, password=None, ignore_warnings=True)\n            assert tx and tx.is_complete(), tx\n            try:\n                await self.wallet.network.broadcast_transaction(tx)\n            except Exception:\n                self.logger.exception(f\"cannot broadcast swap to output claim tx\")\n\n    def get_fee_for_txbatcher(self):\n        return self._get_tx_fee(self.config.FEE_POLICY_SWAPS)\n\n    def _get_tx_fee(self, policy_descriptor: str):\n        fee_policy = FeePolicy(policy_descriptor)\n        return fee_policy.estimate_fee(SWAP_TX_SIZE, network=self.network, allow_fallback_to_static_rates=True)\n\n    def _sanity_check_swap_costs(\n        self,\n        *,\n        incoming_sat: int,\n        outgoing_sat: int,\n    ) -> None:\n        \"\"\"The user should have already seen the swap amounts, and hence the cost.\n        These are just some last-minute sanity checks that the cost of the swap is not insane.\n        \"\"\"\n        costs_abs = outgoing_sat - incoming_sat\n        costs_ratio = 1 - incoming_sat / outgoing_sat\n        if costs_abs < 10_000:  # \"small\" amounts are exempt from checks\n            return\n        exc = UserFacingException(_(\"Total swap costs are insane.\") + f\"\\n({costs_ratio=}, {costs_abs=} sat)\")\n        if costs_ratio > 0.25:\n            raise exc\n        if costs_abs > 1_000_000:\n            if costs_ratio > 0.15:\n                raise exc\n\n    def get_swap(self, payment_hash: bytes) -> Optional[SwapData]:\n        # for history\n        swap = self._swaps.get(payment_hash.hex())\n        if swap:\n            return swap\n        payment_hash = self._prepayments.get(payment_hash)\n        if payment_hash:\n            return self._swaps.get(payment_hash.hex())\n        return None\n\n    def add_lnwatcher_callback(self, swap: SwapData) -> None:\n        callback = lambda: self._claim_swap(swap)\n        self.lnwatcher.add_callback(swap.lockup_address, callback)\n\n    async def hold_invoice_callback(self, payment_hash: bytes) -> None:\n        # note: this assumes the wallet has been unlocked\n        key = payment_hash.hex()\n        if swap := self._swaps.get(key):\n            if not swap.is_funded():\n                output = self.create_funding_output(swap)\n                self.wallet.txbatcher.add_payment_output('swaps', output)\n                swap._payment_pending = True\n        else:\n            self.logger.info(f'key not in swaps {key}')\n\n    def create_normal_swap(self, *, lightning_amount_sat: int, payment_hash: bytes, their_pubkey: bytes = None):\n        \"\"\" server method \"\"\"\n        assert lightning_amount_sat\n        if payment_hash.hex() in self._swaps:\n            raise Exception(\"payment_hash already in use\")\n        locktime = self.network.get_local_height() + LOCKTIME_DELTA_REFUND\n        if self.network.blockchain().is_tip_stale():\n            raise Exception(\"our blockchain tip is stale\")\n        our_privkey = os.urandom(32)\n        our_pubkey = ECPrivkey(our_privkey).get_public_key_bytes(compressed=True)\n        onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True) # what the client is going to receive\n        if not onchain_amount_sat:\n            raise Exception(\"no onchain amount\")\n        redeem_script = _construct_swap_scriptcode(\n            payment_hash=payment_hash,\n            locktime=locktime,\n            refund_pubkey=our_pubkey,\n            claim_pubkey=their_pubkey,\n        )\n        swap, invoice, prepay_invoice = self.add_normal_swap(\n            redeem_script=redeem_script,\n            locktime=locktime,\n            onchain_amount_sat=onchain_amount_sat,\n            lightning_amount_sat=lightning_amount_sat,\n            payment_hash=payment_hash,\n            our_privkey=our_privkey,\n            prepay=True,\n        )\n        self.lnworker.register_hold_invoice(payment_hash, self.hold_invoice_callback)\n        return swap, invoice, prepay_invoice\n\n    def add_normal_swap(\n            self, *,\n            redeem_script: bytes,\n            locktime: int,  # onchain, abs\n            onchain_amount_sat: int,\n            lightning_amount_sat: int,\n            payment_hash: bytes,\n            our_privkey: bytes,\n            prepay: bool,\n            channels: Optional[Sequence['Channel']] = None,\n            min_final_cltv_expiry_delta: Optional[int] = None,\n    ) -> Tuple[SwapData, str, Optional[str]]:\n        \"\"\"creates a hold invoice\"\"\"\n        if payment_hash.hex() in self._swaps:\n            raise Exception(\"payment_hash already in use\")\n        if prepay:\n            # server requests 2 * the mining fee as instantly settled prepayment so that the mining\n            # fees of the funding tx and potential timeout refund tx are always covered\n            prepay_amount_sat = self.mining_fee * 2\n            invoice_amount_sat = lightning_amount_sat - prepay_amount_sat\n        else:\n            invoice_amount_sat = lightning_amount_sat\n\n        # add payment info to lnworker\n        self.lnworker.add_payment_info_for_hold_invoice(\n            payment_hash,\n            lightning_amount_sat=invoice_amount_sat,\n            min_final_cltv_delta=min_final_cltv_expiry_delta or lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED,\n            exp_delay=300,\n        )\n        info = self.lnworker.get_payment_info(payment_hash, direction=lnutil.RECEIVED)\n        lnaddr1, invoice = self.lnworker.get_bolt11_invoice(\n            payment_info=info,\n            message='Submarine swap',\n            fallback_address=None,\n            channels=channels,\n        )\n        margin_to_get_refund_tx_mined = MIN_LOCKTIME_DELTA\n        if not (locktime + margin_to_get_refund_tx_mined < self.network.get_local_height() + lnaddr1.get_min_final_cltv_delta()):\n            raise Exception(\n                f\"onchain locktime ({locktime}+{margin_to_get_refund_tx_mined}) \"\n                f\"too close to LN-htlc-expiry ({self.network.get_local_height()+lnaddr1.get_min_final_cltv_delta()})\")\n\n        if prepay:\n            prepay_hash = self.lnworker.create_payment_info(\n                amount_msat=prepay_amount_sat*1000,\n                min_final_cltv_delta=min_final_cltv_expiry_delta or lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED,\n                exp_delay=300,\n            )\n            info = self.lnworker.get_payment_info(prepay_hash, direction=lnutil.RECEIVED)\n            lnaddr2, prepay_invoice = self.lnworker.get_bolt11_invoice(\n                payment_info=info,\n                message='Submarine swap prepayment',\n                fallback_address=None,\n                channels=channels,\n            )\n            self.lnworker.bundle_payments([payment_hash, prepay_hash])\n            self._prepayments[prepay_hash] = payment_hash\n            assert lnaddr1.get_min_final_cltv_delta() == lnaddr2.get_min_final_cltv_delta()\n        else:\n            prepay_invoice = None\n            prepay_hash = None\n\n        lockup_address = script_to_p2wsh(redeem_script)\n        swap = SwapData(\n            redeem_script=redeem_script,\n            locktime=locktime,\n            privkey=our_privkey,\n            preimage=None,\n            prepay_hash=prepay_hash,\n            lockup_address=lockup_address,\n            onchain_amount=onchain_amount_sat,\n            claim_to_output=None,\n            lightning_amount=lightning_amount_sat,\n            is_reverse=False,\n            is_redeemed=False,\n            funding_txid=None,\n            spending_txid=None,\n        )\n        swap._payment_hash = payment_hash\n        self._add_or_reindex_swap(swap, is_new=True)\n        self.add_lnwatcher_callback(swap)\n        return swap, invoice, prepay_invoice\n\n    def create_reverse_swap(self, *, lightning_amount_sat: int, their_pubkey: bytes) -> SwapData:\n        \"\"\" server method. \"\"\"\n        assert lightning_amount_sat is not None\n        locktime = self.network.get_local_height() + LOCKTIME_DELTA_REFUND\n        if self.network.blockchain().is_tip_stale():\n            raise Exception(\"our blockchain tip is stale\")\n        privkey = os.urandom(32)\n        our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)\n        onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False)\n        if not onchain_amount_sat:\n            raise Exception(\"no onchain amount\")\n        preimage = os.urandom(32)\n        payment_hash = sha256(preimage)\n        redeem_script = _construct_swap_scriptcode(\n            payment_hash=payment_hash,\n            locktime=locktime,\n            refund_pubkey=their_pubkey,\n            claim_pubkey=our_pubkey,\n        )\n        swap = self.add_reverse_swap(\n            redeem_script=redeem_script,\n            locktime=locktime,\n            privkey=privkey,\n            preimage=preimage,\n            payment_hash=payment_hash,\n            prepay_hash=None,\n            onchain_amount_sat=onchain_amount_sat,\n            lightning_amount_sat=lightning_amount_sat)\n        return swap\n\n    def add_reverse_swap(\n        self,\n        *,\n        redeem_script: bytes,\n        locktime: int,  # onchain\n        privkey: bytes,\n        lightning_amount_sat: int,\n        onchain_amount_sat: int,\n        preimage: bytes,\n        payment_hash: bytes,\n        prepay_hash: Optional[bytes] = None,\n        claim_to_output: Optional[TxOutput] = None,\n    ) -> SwapData:\n        if payment_hash.hex() in self._swaps:\n            raise Exception(\"payment_hash already in use\")\n        assert sha256(preimage) == payment_hash\n        lockup_address = script_to_p2wsh(redeem_script)\n        if claim_to_output is not None:\n            # the claim_to_output value needs to be lower than the funding utxo value, otherwise\n            # there are no funds left for the fee of the claim tx\n            assert claim_to_output.value < onchain_amount_sat, f\"{claim_to_output=} >= {onchain_amount_sat=}\"\n            claim_to_output = (claim_to_output.address, claim_to_output.value)\n        swap = SwapData(\n            redeem_script=redeem_script,\n            locktime=locktime,\n            privkey=privkey,\n            preimage=preimage,\n            prepay_hash=prepay_hash,\n            lockup_address=lockup_address,\n            onchain_amount=onchain_amount_sat,\n            claim_to_output=claim_to_output,\n            lightning_amount=lightning_amount_sat,\n            is_reverse=True,\n            is_redeemed=False,\n            funding_txid=None,\n            spending_txid=None,\n        )\n        if prepay_hash:\n            if prepay_hash in self._prepayments:\n                raise Exception(\"prepay_hash already in use\")\n            self._prepayments[prepay_hash] = payment_hash\n        swap._payment_hash = payment_hash\n        self._add_or_reindex_swap(swap, is_new=True)\n        self.add_lnwatcher_callback(swap)\n        return swap\n\n    def server_add_swap_invoice(self, request: dict) -> dict:\n        \"\"\" server method.\n        (client-forward-swap phase2)\n        \"\"\"\n        invoice = request['invoice']\n        invoice = Invoice.from_bech32(invoice)\n        key = invoice.rhash\n        payment_hash = bytes.fromhex(key)\n        their_pubkey = bytes.fromhex(request['refundPublicKey'])\n        with self.swaps_lock:\n            assert key in self._swaps\n            swap = self._swaps[key]\n            assert swap.lightning_amount == int(invoice.get_amount_sat())\n            assert swap.is_reverse is True\n            # check that we have the preimage\n            assert sha256(swap.preimage) == payment_hash\n            assert swap.spending_txid is None\n            # check their_pubkey by recalculating redeem_script\n            our_pubkey = ECPrivkey(swap.privkey).get_public_key_bytes(compressed=True)\n            redeem_script = _construct_swap_scriptcode(\n                payment_hash=payment_hash, locktime=swap.locktime, refund_pubkey=their_pubkey, claim_pubkey=our_pubkey,\n            )\n            assert swap.redeem_script == redeem_script\n            assert key not in self.invoices_to_pay\n            self.invoices_to_pay[key] = 0\n            assert self.wallet.get_invoice(invoice.get_id()) is None\n            self.wallet.save_invoice(invoice)\n        return {}\n\n    async def normal_swap(\n            self,\n            *,\n            transport: 'SwapServerTransport',\n            lightning_amount_sat: int,\n            expected_onchain_amount_sat: int,\n            password,\n            tx: PartialTransaction = None,\n            channels = None,\n    ) -> Optional[str]:\n        \"\"\"send on-chain BTC, receive on Lightning\n\n        Old (removed) flow:\n        - User generates an LN invoice with RHASH, and knows preimage.\n        - User creates on-chain output locked to RHASH.\n        - Server pays LN invoice. User reveals preimage.\n        - Server spends the on-chain output using preimage.\n        cltv safety requirement: (onchain_locktime > LN_locktime),   otherwise server is vulnerable\n\n        New flow:\n         - User requests swap  (RPC 'createnormalswap')\n         - Server creates preimage, sends RHASH to user\n         - User creates hold invoice, sends it to server  (RPC 'addswapinvoice')\n         - Server sends HTLC, user holds it\n         - User creates on-chain output locked to RHASH\n         - Server spends the on-chain output using preimage (revealing the preimage)\n         - User fulfills HTLC using preimage\n        cltv safety requirement: (onchain_locktime < LN_locktime),   otherwise client is vulnerable\n        \"\"\"\n        assert self.network\n        assert self.lnwatcher\n        swap, invoice = await self.request_normal_swap(\n            transport=transport,\n            lightning_amount_sat=lightning_amount_sat,\n            expected_onchain_amount_sat=expected_onchain_amount_sat,\n            channels=channels,\n        )\n        tx = self.create_funding_tx(swap, tx, password=password)\n        return await self.wait_for_htlcs_and_broadcast(transport=transport, swap=swap, invoice=invoice, tx=tx)\n\n    async def request_normal_swap(\n            self,\n            *,\n            transport: 'SwapServerTransport',\n            lightning_amount_sat: int,\n            expected_onchain_amount_sat: int,\n            channels: Optional[Sequence['Channel']] = None,\n    ) -> Tuple[SwapData, str]:\n        self._sanity_check_swap_costs(\n            incoming_sat=lightning_amount_sat, outgoing_sat=expected_onchain_amount_sat)\n        await self.is_initialized.wait() # add timeout\n        refund_privkey = os.urandom(32)\n        refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True)\n        self.logger.info('requesting preimage hash for swap')\n        request_data = {\n            \"invoiceAmount\": lightning_amount_sat,\n            \"refundPublicKey\": refund_pubkey.hex()\n        }\n        data = await transport.send_request_to_server('createnormalswap', request_data)\n        try:\n            payment_hash = bytes.fromhex(data[\"preimageHash\"])\n            assert len(payment_hash) == 32, len(payment_hash)\n            onchain_amount = data[\"expectedAmount\"]\n            assert isinstance(onchain_amount, int), type(onchain_amount)\n            locktime = data[\"timeoutBlockHeight\"]\n            assert isinstance(locktime, int), type(locktime)\n            lockup_address = data[\"address\"]\n            assert isinstance(lockup_address, str), type(lockup_address)\n            assert bitcoin.is_address(lockup_address), lockup_address\n            redeem_script = bytes.fromhex(data[\"redeemScript\"])\n        except Exception as e:\n            self.logger.error(f\"failed to parse response from swapserver for createnormalswap: {e!r}\")\n            raise SwapServerError(\"failed to parse response from swapserver for createnormalswap\") from e\n        del data   # parsing done\n        # verify redeem_script is built with our pubkey and preimage\n        _check_swap_scriptcode(\n            redeem_script=redeem_script,\n            lockup_address=lockup_address,\n            payment_hash=payment_hash,\n            locktime=locktime,\n            refund_pubkey=refund_pubkey,\n            claim_pubkey=None,\n        )\n\n        # check that onchain_amount is not more than what we estimated\n        if onchain_amount > expected_onchain_amount_sat:\n            raise Exception(f\"fswap check failed: onchain_amount is more than what we estimated: \"\n                            f\"{onchain_amount} > {expected_onchain_amount_sat}\")\n        # verify that they are not locking up funds for too long\n        if locktime - self.network.get_local_height() > MAX_LOCKTIME_DELTA:\n            raise Exception(\"fswap check failed: locktime too far in future\")\n        if self.network.blockchain().is_tip_stale():\n            raise Exception(\"our blockchain tip is stale\")\n\n        swap, invoice, _ = self.add_normal_swap(\n            redeem_script=redeem_script,\n            locktime=locktime,\n            lightning_amount_sat=lightning_amount_sat,\n            onchain_amount_sat=onchain_amount,\n            payment_hash=payment_hash,\n            our_privkey=refund_privkey,\n            prepay=False,\n            channels=channels,\n            # When the client is doing a normal swap, we create a ln-invoice with larger than usual final_cltv_delta.\n            # If the user goes offline after broadcasting the funding tx (but before it is mined and\n            # the server claims it), they need to come back online before the held ln-htlc expires (see #8940).\n            # If the held ln-htlc expires, and the funding tx got confirmed, the server will have claimed the onchain\n            # funds, and the ln-htlc will be timed out onchain (and channel force-closed). i.e. the user loses the swap\n            # amount. Increasing the final_cltv_delta the user puts in the invoice extends this critical window.\n            min_final_cltv_expiry_delta=MIN_FINAL_CLTV_DELTA_FOR_CLIENT,\n        )\n        return swap, invoice\n\n    async def wait_for_htlcs_and_broadcast(\n            self,\n            *,\n            transport: 'SwapServerTransport',\n            swap: SwapData,\n            invoice: str,\n            tx: Transaction,\n    ) -> Optional[str]:\n        await transport.is_connected.wait()\n        payment_hash = swap.payment_hash\n        refund_pubkey = ECPrivkey(swap.privkey).get_public_key_bytes(compressed=True)\n        async def callback(payment_hash):\n            # FIXME what if this raises, e.g. TxBroadcastError?\n            #       We will never retry the hold-invoice-callback.\n            await self.broadcast_funding_tx(swap, tx)\n\n        self.lnworker.register_hold_invoice(payment_hash, callback)\n\n        # send invoice to server and wait for htlcs\n        # note: server will link this RPC to our previous 'createnormalswap' RPC\n        #       - using the RHASH from invoice, and using refundPublicKey\n        #       - FIXME it would be safer to use a proper session-secret?!\n        request_data = {\n            \"invoice\": invoice,\n            \"refundPublicKey\": refund_pubkey.hex(),\n        }\n        await transport.send_request_to_server('addswapinvoice', request_data)\n        # wait for funding tx\n        lnaddr = lndecode(invoice)\n        while swap.funding_txid is None and not lnaddr.is_expired():\n            await asyncio.sleep(0.1)\n        return swap.funding_txid\n\n    def create_funding_output(self, swap: SwapData) -> PartialTxOutput:\n        return PartialTxOutput.from_address_and_value(swap.lockup_address, swap.onchain_amount)\n\n    def create_funding_tx(\n            self,\n            swap: SwapData,\n            tx: Optional[PartialTransaction],\n            *,\n            password,\n    ) -> PartialTransaction:\n        # create funding tx\n        # use fee policy set by user (not using txbatcher)\n        fee_policy = FeePolicy(self.config.FEE_POLICY)\n        # note: rbf must not decrease payment\n        # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output\n        if tx is None:\n            funding_output = self.create_funding_output(swap)\n            tx = self.wallet.make_unsigned_transaction(\n                outputs=[funding_output],\n                rbf=True,\n                fee_policy=fee_policy,\n            )\n        else:\n            tx.replace_output_address(DummyAddress.SWAP, swap.lockup_address)\n            tx.set_rbf(True)\n        self.wallet.sign_transaction(tx, password)\n        return tx\n\n    @log_exceptions\n    async def request_swap_for_amount(\n        self,\n        *,\n        transport: 'SwapServerTransport',\n        onchain_amount: int,\n    ) -> Optional[Tuple[SwapData, str]]:\n        await self.is_initialized.wait()\n        lightning_amount_sat = self.get_recv_amount(onchain_amount, is_reverse=False)\n        if lightning_amount_sat is None:\n            raise SwapServerError(_(\"Swap amount outside of providers limits\") + \":\\n\"\n                                  + _(\"min\") + f\": {self.get_min_amount()}\\n\"\n                                  + _(\"max\") + f\": {self.get_provider_max_reverse_amount()}\")\n        swap, invoice = await self.request_normal_swap(\n            transport=transport,\n            lightning_amount_sat=lightning_amount_sat,\n            expected_onchain_amount_sat=onchain_amount)\n        return swap, invoice\n\n    @log_exceptions\n    async def broadcast_funding_tx(self, swap: SwapData, tx: Transaction) -> None:\n        swap.funding_txid = tx.txid()\n        await self.network.broadcast_transaction(tx)\n\n    async def reverse_swap(\n            self,\n            *,\n            transport: 'SwapServerTransport',\n            lightning_amount_sat: int,\n            expected_onchain_amount_sat: int,\n            prepayment_sat: int,\n            channels: Optional[Sequence['Channel']] = None,\n            claim_to_output: Optional[TxOutput] = None,\n    ) -> Optional[str]:\n        \"\"\"send on Lightning, receive on-chain\n\n        - User generates preimage, RHASH. Sends RHASH to server.  (RPC 'createswap')\n        - Server creates an LN invoice for RHASH.\n        - User pays LN invoice - except server needs to hold the HTLC as preimage is unknown.\n            - if the server requested a fee prepayment (using 'minerFeeInvoice'),\n              the server will have the preimage for that. The user will send HTLCs for both the main RHASH,\n              and for the fee prepayment. Once both MPP sets arrive at the server, the server will fulfill\n              the HTLCs for the fee prepayment (before creating the on-chain output).\n        - Server creates on-chain output locked to RHASH.\n        - User spends on-chain output, revealing preimage.\n        - Server fulfills HTLC using preimage.\n\n        cltv safety requirement: (onchain_locktime < LN_locktime),   otherwise server is vulnerable\n\n        Note: expected_onchain_amount_sat is BEFORE deducting the on-chain claim tx fee.\n        Note: prepayment_sat is passed as argument instead of accessing self.mining_fee to ensure\n        the mining fees the user sees in the GUI are also the values used for the checks performed here.\n        We commit to prepayment_sat as it limits the max fee pre-payment amt, which the server is trusted with.\n        \"\"\"\n        assert self.network\n        assert self.lnwatcher\n        self._sanity_check_swap_costs(\n            incoming_sat=expected_onchain_amount_sat, outgoing_sat=lightning_amount_sat)\n        privkey = os.urandom(32)\n        our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)\n        preimage = os.urandom(32)\n        payment_hash = sha256(preimage)\n        request_data = {\n            \"type\": \"reversesubmarine\",\n            \"pairId\": \"BTC/BTC\",\n            \"invoiceAmount\": lightning_amount_sat,\n            \"preimageHash\": payment_hash.hex(),\n            \"claimPublicKey\": our_pubkey.hex(),\n        }\n        self.logger.debug(f'rswap: sending request for {lightning_amount_sat}')\n        data = await transport.send_request_to_server('createswap', request_data)\n        try:\n            invoice = data['invoice']\n            assert isinstance(invoice, str), type(invoice)\n            fee_invoice = data.get('minerFeeInvoice')\n            assert fee_invoice is None or isinstance(fee_invoice, str), type(fee_invoice)\n            lockup_address = data['lockupAddress']\n            assert isinstance(lockup_address, str), type(lockup_address)\n            assert bitcoin.is_address(lockup_address), lockup_address\n            redeem_script = bytes.fromhex(data['redeemScript'])\n            locktime = data['timeoutBlockHeight']\n            assert isinstance(locktime, int), type(locktime)\n            onchain_amount = data[\"onchainAmount\"]\n            assert isinstance(onchain_amount, int), type(onchain_amount)\n            response_id = data['id']\n        except Exception as e:\n            self.logger.error(f\"failed to parse response from swapserver for createswap: {e!r}\")\n            raise SwapServerError(\"failed to parse response from swapserver for createswap\") from e\n        del data  # parsing done\n        self.logger.debug(f'rswap: {response_id=}')\n        # verify redeem_script is built with our pubkey and preimage\n        _check_swap_scriptcode(\n            redeem_script=redeem_script,\n            lockup_address=lockup_address,\n            payment_hash=payment_hash,\n            locktime=locktime,\n            refund_pubkey=None,\n            claim_pubkey=our_pubkey,\n        )\n        # check that the onchain amount is what we expected\n        if onchain_amount < expected_onchain_amount_sat:\n            raise Exception(f\"rswap check failed: onchain_amount is less than what we expected: \"\n                            f\"{onchain_amount} < {expected_onchain_amount_sat}\")\n        # verify that we will have enough time to get our tx confirmed\n        if locktime - self.network.get_local_height() <= MIN_LOCKTIME_DELTA:\n            raise Exception(\"rswap check failed: locktime too close\")\n        if self.network.blockchain().is_tip_stale():\n            raise Exception(\"our blockchain tip is stale\")\n        # verify invoice payment_hash\n        lnaddr = self.lnworker._check_bolt11_invoice(invoice)\n        invoice_amount = int(lnaddr.get_amount_sat())\n        if lnaddr.paymenthash != payment_hash:\n            raise Exception(\"rswap check failed: inconsistent RHASH and invoice\")\n        if fee_invoice:\n            fee_lnaddr = self.lnworker._check_bolt11_invoice(fee_invoice)\n            if fee_lnaddr.get_amount_sat() > prepayment_sat:\n                raise SwapServerError(_(\"Mining fee requested by swap-server larger \"\n                                        \"than what was announced in their offer.\"))\n            invoice_amount += fee_lnaddr.get_amount_sat()\n            prepay_hash = fee_lnaddr.paymenthash\n        else:\n            prepay_hash = None\n        # check that the lightning amount is what we requested\n        if int(invoice_amount) != lightning_amount_sat:\n            raise Exception(f\"rswap check failed: invoice_amount ({invoice_amount}) \"\n                            f\"not what we requested ({lightning_amount_sat})\")\n        # save swap data to wallet file\n        swap = self.add_reverse_swap(\n            redeem_script=redeem_script,\n            locktime=locktime,\n            privkey=privkey,\n            preimage=preimage,\n            payment_hash=payment_hash,\n            prepay_hash=prepay_hash,\n            onchain_amount_sat=onchain_amount,\n            lightning_amount_sat=lightning_amount_sat,\n            claim_to_output=claim_to_output,\n        )\n        # initiate fee payment.\n        if fee_invoice:\n            fee_invoice_obj = Invoice.from_bech32(fee_invoice)\n            asyncio.ensure_future(self.lnworker.pay_invoice(fee_invoice_obj))\n        # we return if we detect funding\n        async def wait_for_funding(swap):\n            while swap.funding_txid is None:\n                await asyncio.sleep(0.1)\n        # initiate main payment\n        invoice_obj = Invoice.from_bech32(invoice)\n        tasks = [asyncio.create_task(self.lnworker.pay_invoice(invoice_obj, channels=channels)), asyncio.create_task(wait_for_funding(swap))]\n        await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)\n        return swap.funding_txid\n\n    def _add_or_reindex_swap(self, swap: SwapData, *, is_new: bool) -> None:\n        with self.swaps_lock:\n            assert is_new == (swap.payment_hash.hex() not in self._swaps), is_new\n            if swap.payment_hash.hex() not in self._swaps:\n                self._swaps[swap.payment_hash.hex()] = swap\n            if swap._funding_prevout:\n                self._swaps_by_funding_outpoint[swap._funding_prevout] = swap\n            self._swaps_by_lockup_address[swap.lockup_address] = swap\n\n    def server_update_pairs(self) -> None:\n        \"\"\" for server \"\"\"\n        self.percentage = Decimal(self.config.SWAPSERVER_FEE_MILLIONTHS) / 10000  # type: ignore\n        self._min_amount = MIN_SWAP_AMOUNT_SAT\n        oc_balance_sat: int = self.wallet.get_spendable_balance_sat()\n        max_forward: int = min(int(self.lnworker.num_sats_can_receive()), oc_balance_sat, 10000000)\n        max_reverse: int = min(int(self.lnworker.num_sats_can_send()), 10000000)\n        self._max_forward: int = self._keep_leading_digits(max_forward, 2)\n        self._max_reverse: int = self._keep_leading_digits(max_reverse, 2)\n        new_mining_fee = self.get_fee_for_txbatcher()\n        if self.mining_fee is None \\\n                or abs(self.mining_fee - new_mining_fee) / self.mining_fee > 0.1:\n            self.mining_fee = new_mining_fee\n\n    @staticmethod\n    def _keep_leading_digits(num: int, digits: int) -> int:\n        \"\"\"Reduces precision of num to `digits` leading digits.\"\"\"\n        if num <= 0:\n            return 0\n        num_str = str(num)\n        zeroed_num_str = f\"{num_str[:digits]}{(len(num_str[digits:])) * '0'}\"\n        return int(zeroed_num_str)\n\n    def update_pairs(self, pairs: SwapFees):\n        self.logger.info(f'updating fees {pairs}')\n        self.mining_fee = pairs.mining_fee\n        self.percentage = pairs.percentage\n        self._min_amount = pairs.min_amount\n        self._max_forward = pairs.max_forward\n        self._max_reverse = pairs.max_reverse\n        self.trigger_pairs_updated_threadsafe()\n\n    def trigger_pairs_updated_threadsafe(self):\n        def trigger():\n            self.is_initialized.set()\n            self.pairs_updated.set()\n            self.pairs_updated.clear()\n\n        run_sync_function_on_asyncio_thread(trigger, block=True)\n\n    def get_provider_max_forward_amount(self) -> int:\n        \"\"\"in sat\"\"\"\n        return self._max_forward\n\n    def get_provider_max_reverse_amount(self) -> int:\n        \"\"\"in sat\"\"\"\n        return self._max_reverse\n\n    def get_min_amount(self) -> int:\n        \"\"\"in satoshis\"\"\"\n        return self._min_amount\n\n    def check_invoice_amount(self, x, is_reverse: bool) -> bool:\n        if is_reverse:\n            max_amount = self.get_provider_max_forward_amount()\n        else:\n            max_amount = self.get_provider_max_reverse_amount()\n        return self.get_min_amount() <= x <= max_amount\n\n    def _get_recv_amount(self, send_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:\n        \"\"\"For a given swap direction and amount we send, returns how much we will receive.\n\n        Note: in the reverse direction, the mining fee for the on-chain claim tx is NOT accounted for.\n        In the reverse direction, the result matches what the swap server returns as response[\"onchainAmount\"].\n        \"\"\"\n        if send_amount is None:\n            return None\n        x = Decimal(send_amount)\n        percentage = self.percentage\n        if is_reverse:\n            if not self.check_invoice_amount(x, is_reverse):\n                return None\n            # see/ref:\n            # https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L948\n            percentage_fee = math.ceil(percentage * x / 100)\n            base_fee = self.mining_fee\n            x -= percentage_fee + base_fee\n            x = math.floor(x)\n            if x < dust_threshold():\n                return None\n        else:\n            x -= self.mining_fee\n            percentage_fee = math.ceil(x * percentage / (100 + percentage))\n            x -= percentage_fee\n            if not self.check_invoice_amount(x, is_reverse):\n                return None\n        x = int(x)\n        return x\n\n    def _get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:\n        \"\"\"For a given swap direction and amount we want to receive, returns how much we will need to send.\n\n        Note: in the reverse direction, the mining fee for the on-chain claim tx is NOT accounted for.\n        In the forward direction, the result matches what the swap server returns as response[\"expectedAmount\"].\n        \"\"\"\n        if not recv_amount:\n            return None\n        x = Decimal(recv_amount)\n        percentage = self.percentage\n        if is_reverse:\n            # see/ref:\n            # https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L928\n            # https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L958\n            base_fee = self.mining_fee\n            x += base_fee\n            x = math.ceil(x / ((100 - percentage) / 100))\n            if not self.check_invoice_amount(x, is_reverse):\n                return None\n        else:\n            if not self.check_invoice_amount(x, is_reverse):\n                return None\n            # see/ref:\n            # https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L708\n            # https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/rates/FeeProvider.ts#L90\n            percentage_fee = math.ceil(percentage * x / 100)\n            x += percentage_fee + self.mining_fee\n        x = int(x)\n        return x\n\n    def get_recv_amount(self, send_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:\n        # first, add percentage fee\n        recv_amount = self._get_recv_amount(send_amount, is_reverse=is_reverse)\n        # sanity check calculation can be inverted\n        if recv_amount is not None:\n            inverted_send_amount = self._get_send_amount(recv_amount, is_reverse=is_reverse)\n            # accept off-by ones as amt_rcv = recv_amt(send_amt(amt_rcv)) only up to +-1\n            if abs(send_amount - inverted_send_amount) > 1:\n                raise Exception(f\"calc-invert-sanity-check failed. is_reverse={is_reverse}. \"\n                                f\"send_amount={send_amount} -> recv_amount={recv_amount} -> inverted_send_amount={inverted_send_amount}\")\n        # second, add on-chain claim tx fee\n        if is_reverse and recv_amount is not None:\n            recv_amount -= self.get_fee_for_txbatcher()\n        return recv_amount\n\n    def get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:\n        # first, add on-chain claim tx fee\n        if is_reverse and recv_amount is not None:\n            recv_amount += self.get_fee_for_txbatcher()\n        # second, add percentage fee\n        send_amount = self._get_send_amount(recv_amount, is_reverse=is_reverse)\n        # sanity check calculation can be inverted\n        if send_amount is not None:\n            inverted_recv_amount = self._get_recv_amount(send_amount, is_reverse=is_reverse)\n            if recv_amount != inverted_recv_amount:\n                raise Exception(f\"calc-invert-sanity-check failed. is_reverse={is_reverse}. \"\n                                f\"recv_amount={recv_amount} -> send_amount={send_amount} -> inverted_recv_amount={inverted_recv_amount}\")\n        return send_amount\n\n    def get_swaps_by_funding_tx(self, tx: Transaction) -> Iterable[SwapData]:\n        swaps = []\n        for txout_idx, _txo in enumerate(tx.outputs()):\n            prevout = TxOutpoint(txid=bytes.fromhex(tx.txid()), out_idx=txout_idx)\n            if swap := self._swaps_by_funding_outpoint.get(prevout):\n                swaps.append(swap)\n        return swaps\n\n    def get_swaps_by_claim_tx(self, tx: Transaction) -> Iterable[Tuple[int, SwapData]]:\n        swaps = []\n        for i, txin in enumerate(tx.inputs()):\n            if swap := self.get_swap_by_claim_txin(txin):\n                swaps.append((i, swap))\n        return swaps\n\n    def get_swap_by_claim_txin(self, txin: TxInput) -> Optional[SwapData]:\n        return self._swaps_by_funding_outpoint.get(txin.prevout)\n\n    def is_lockup_address_for_a_swap(self, addr: str) -> bool:\n        return bool(self._swaps_by_lockup_address.get(addr))\n\n    @classmethod\n    def add_txin_info(cls, swap, txin: PartialTxInput) -> None:\n        \"\"\"Add some info to a claim txin.\n        note: even without signing, this is useful for tx size estimation.\n        \"\"\"\n        preimage = swap.preimage if swap.is_reverse else 0\n        witness_script = swap.redeem_script\n        txin.script_sig = b''\n        txin.witness_script = witness_script\n        sig_dummy = b'\\x00' * 71  # DER-encoded ECDSA sig, with low S and low R\n        witness = [sig_dummy, preimage, witness_script]\n        txin.witness_sizehint = len(construct_witness(witness))\n        txin.nsequence = 1 if swap.is_reverse else 0xffffffff - 2\n\n    @classmethod\n    def create_claim_txin(\n            cls,\n            *,\n            txin: PartialTxInput,\n            swap: SwapData,\n    ) -> Tuple[PartialTxInput, Optional[int]]:\n        if swap.is_reverse:  # successful reverse swap\n            locktime = None\n            # preimage will be set in sign_tx\n        else:  # timing out forward swap\n            locktime = swap.locktime\n        cls.add_txin_info(swap, txin)\n        txin.privkey = swap.privkey\n        def make_witness(sig):\n            # preimae not known yet\n            preimage = swap.preimage if swap.is_reverse else 0\n            witness_script = swap.redeem_script\n            return construct_witness([sig, preimage, witness_script])\n        txin.make_witness = make_witness\n        return txin, locktime\n\n    def client_max_amount_forward_swap(self) -> Optional[int]:\n        \"\"\" returns None if we cannot swap \"\"\"\n        max_swap_amt_ln = self.get_provider_max_reverse_amount()\n        if max_swap_amt_ln is None:\n            return None\n        max_recv_amt_ln = int(self.lnworker.num_sats_can_receive())\n        max_amt_ln = int(min(max_swap_amt_ln, max_recv_amt_ln))\n        max_amt_oc = self.get_send_amount(max_amt_ln, is_reverse=False) or 0\n        min_amt_oc = self.get_send_amount(self.get_min_amount(), is_reverse=False) or 0\n        return max_amt_oc if max_amt_oc >= min_amt_oc else None\n\n    def client_max_amount_reverse_swap(self) -> Optional[int]:\n        \"\"\"Returns None if swap is not possible\"\"\"\n        provider_max = self.get_provider_max_forward_amount()\n        max_ln_send = int(self.lnworker.num_sats_can_send())\n        max_swap_size = min(max_ln_send, provider_max)\n        if max_swap_size < self.get_min_amount():\n            return None\n        return max_swap_size\n\n    def server_create_normal_swap(self, request):\n        # normal for client, reverse for server\n        #request = await r.json()\n        lightning_amount_sat = request['invoiceAmount']\n        their_pubkey = bytes.fromhex(request['refundPublicKey'])\n        assert len(their_pubkey) == 33\n        swap = self.create_reverse_swap(\n            lightning_amount_sat=lightning_amount_sat,\n            their_pubkey=their_pubkey,\n        )\n        response = {\n            \"id\": swap.payment_hash.hex(),\n            'preimageHash': swap.payment_hash.hex(),\n            \"acceptZeroConf\": False,\n            \"expectedAmount\": swap.onchain_amount,\n            \"timeoutBlockHeight\": swap.locktime,\n            \"address\": swap.lockup_address,\n            \"redeemScript\": swap.redeem_script.hex(),\n        }\n        return response\n\n    def server_create_swap(self, request):\n        # reverse for client, forward for server\n        # requesting a normal swap (old protocol) will raise an exception\n        #request = await r.json()\n        req_type = request['type']\n        assert request['pairId'] == 'BTC/BTC'\n        if req_type == 'reversesubmarine':\n            lightning_amount_sat=request['invoiceAmount']\n            payment_hash=bytes.fromhex(request['preimageHash'])\n            their_pubkey=bytes.fromhex(request['claimPublicKey'])\n            assert len(payment_hash) == 32\n            assert len(their_pubkey) == 33\n            swap, invoice, prepay_invoice = self.create_normal_swap(\n                lightning_amount_sat=lightning_amount_sat,\n                payment_hash=payment_hash,\n                their_pubkey=their_pubkey\n            )\n            response = {\n                'id': payment_hash.hex(),\n                'invoice': invoice,\n                'minerFeeInvoice': prepay_invoice,\n                'lockupAddress': swap.lockup_address,\n                'redeemScript': swap.redeem_script.hex(),\n                'timeoutBlockHeight': swap.locktime,\n                \"onchainAmount\": swap.onchain_amount,\n            }\n        elif req_type == 'submarine':\n            raise Exception('Deprecated API. Please upgrade your version of Electrum')\n        else:\n            raise Exception('unsupported request type:' + req_type)\n        return response\n\n    def get_groups_for_onchain_history(self):\n        current_height = self.wallet.adb.get_local_height()\n        d = {}\n        with self.swaps_lock:\n            swaps_items = list(self._swaps.items())\n        for payment_hash_hex, swap in swaps_items:\n            txid = swap.spending_txid if swap.is_reverse else swap.funding_txid\n            if txid is None:\n                continue\n\n            if swap.is_reverse and swap.claim_to_output:\n                group_label = 'Submarine Payment' + ' ' + self.config.format_amount_and_units(swap.claim_to_output[1])\n            elif swap.is_reverse:\n                group_label = 'Reverse swap' + ' ' + self.config.format_amount_and_units(swap.lightning_amount)\n            else:\n                group_label = 'Forward swap' + ' ' + self.config.format_amount_and_units(swap.onchain_amount)\n\n            label = _('Claim transaction') if swap.is_reverse else _('Funding transaction')\n            delta = current_height - swap.locktime\n            if self.wallet.adb.is_mine(swap.lockup_address):\n                tx_height = self.wallet.adb.get_tx_height(swap.funding_txid)\n                if swap.is_reverse and tx_height.height() <= 0:\n                    label += ' (%s)' % _('waiting for funding tx confirmation')\n                if not swap.is_reverse and not swap.is_redeemed and swap.spending_txid is None and delta < 0:\n                    label += f' (refundable in {-delta} blocks)' # fixme: only if unspent\n            d[txid] = {\n                'group_id': txid,\n                'label': label,\n                'group_label': group_label,\n            }\n            if not swap.is_reverse:\n                claim_tx = self.lnwatcher.adb.get_transaction(swap.spending_txid)\n                if claim_tx and not self.extract_preimage(swap, claim_tx):\n                    # if the spending_tx is in the wallet, this will add it\n                    # to the group (see wallet.get_full_history)\n                    d[swap.spending_txid] = {\n                        'group_id': txid,\n                        'group_label': group_label,\n                        'label': _('Refund transaction'),\n                    }\n                    self.wallet._accounting_addresses.add(swap.lockup_address)\n            elif swap.is_reverse and swap.claim_to_output:  # submarine payment\n                claim_tx = self.lnwatcher.adb.get_transaction(swap.spending_txid)\n                payee_spk = address_to_script(swap.claim_to_output[0])\n                if claim_tx and payee_spk not in (o.scriptpubkey for o in claim_tx.outputs()):\n                    # the swapserver must have refunded itself as the claim_tx did not spend\n                    # to the address we intended it to spend to, remove the funding\n                    # address again from accounting addresses so the refund tx is not incorrectly\n                    # shown in the wallet history as tx spending from this wallet\n                    self.wallet._accounting_addresses.discard(swap.lockup_address)\n                # add the funding tx to the group as the total amount of the group would\n                # otherwise be ~2x the actual payment as the claim tx gets counted as negative\n                # value (as it sends from the wallet/accounting address balance)\n                d[swap.funding_txid] = {\n                     'group_id': txid,\n                     'label': _('Funding transaction'),\n                     'group_label': group_label,\n                }\n                # add the lockup_address as the claim tx would otherwise not touch the wallet and\n                # wouldn't be shown in the history.\n                self.wallet._accounting_addresses.add(swap.lockup_address)\n\n        return d\n\n    def get_group_id_for_payment_hash(self, payment_hash: bytes) -> Optional[str]:\n        # add group_id to swap transactions\n        swap = self.get_swap(payment_hash)\n        if swap:\n            return swap.spending_txid if swap.is_reverse else swap.funding_txid\n        return None\n\n    def get_pending_swaps(self) -> List[SwapData]:\n        \"\"\"Returns a list of swaps with unconfirmed funding tx (which require us to stay online).\"\"\"\n        pending_swaps: List[SwapData] = []\n        with self.swaps_lock:\n            swaps = list(self._swaps.values())\n        for swap in swaps:\n            if swap.is_redeemed:\n                # adb data might have been removed after is_redeemed was set.\n                # in that case lnwatcher will no longer fetch the spending tx\n                # and adb will return TX_HEIGHT_LOCAL\n                continue\n            # note: adb.get_tx_height returns TX_HEIGHT_LOCAL if the txid is unknown\n            funding_height = self.lnworker.wallet.adb.get_tx_height(swap.funding_txid).height()\n            spending_height = self.lnworker.wallet.adb.get_tx_height(swap.spending_txid).height()\n            if funding_height > TX_HEIGHT_LOCAL and spending_height <= TX_HEIGHT_LOCAL:\n                pending_swaps.append(swap)\n        return pending_swaps\n\n\nclass SwapServerTransport(Logger):\n\n    def __init__(self, *, config: 'SimpleConfig', sm: 'SwapManager'):\n        Logger.__init__(self)\n        self.sm = sm\n        self.network = sm.network\n        self.config = config\n        self.is_connected = asyncio.Event()\n        self.connect_timeout = 10 if self.uses_proxy else 5\n\n    def __enter__(self):\n        pass\n\n    def __exit__(self, ex_type, ex, tb):\n        pass\n\n    async def __aenter__(self):\n        pass\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        pass\n\n    async def send_request_to_server(self, method: str, request_data: Optional[dict]) -> dict:\n        \"\"\"Might raise SwapServerError.\"\"\"\n        pass\n\n    @property\n    def uses_proxy(self):\n        return self.network.proxy and self.network.proxy.enabled\n\n\nclass HttpTransport(SwapServerTransport):\n\n    def __init__(self, config, sm):\n        SwapServerTransport.__init__(self, config=config, sm=sm)\n        self.api_url = config.SWAPSERVER_URL\n        self.is_connected.set()\n\n    def __enter__(self):\n        asyncio.run_coroutine_threadsafe(self.get_pairs_just_once(), self.network.asyncio_loop)\n        return self\n\n    def __exit__(self, ex_type, ex, tb):\n        pass\n\n    async def __aenter__(self):\n        asyncio.create_task(self.get_pairs_just_once())\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        pass\n\n    async def send_request_to_server(self, method, request_data):\n        try:\n            response = await self.network.async_send_http_on_proxy(\n                'post' if request_data else 'get',\n                self.api_url + '/' + method,\n                json=request_data,\n                timeout=30)\n        except aiohttp.ClientError as e:\n            self.logger.info(f\"Swap server errored: {e!r}\")\n            raise SwapServerError() from e\n        try:\n            parsed_json = json.loads(response)\n            if not isinstance(parsed_json, dict):\n                raise Exception(\"malformed response, not dict\")\n        except Exception as e:\n            self.logger.error(f\"failed to parse response from swapserver for {method=}: {e!r}\")\n            raise SwapServerError(f\"failed to parse response from swapserver for {method=}\") from e\n        return parsed_json\n\n    async def get_pairs_just_once(self) -> None:\n        \"\"\"Might raise SwapServerError.\"\"\"\n        response = await self.send_request_to_server('getpairs', None)\n        try:\n            assert response.get('htlcFirst') is True\n            fees = response['pairs']['BTC/BTC']['fees']\n            limits = response['pairs']['BTC/BTC']['limits']\n            pairs = SwapFees(\n                percentage=Decimal(str(fees['percentage'])),\n                mining_fee=fees['minerFees']['baseAsset']['mining_fee'],\n                min_amount=limits['minimal'],\n                max_forward=limits['max_forward_amount'],\n                max_reverse=limits['max_reverse_amount'],\n            )\n        except Exception as e:\n            self.logger.error(f\"failed to parse response from swapserver for getpairs: {e!r}\")\n            raise SwapServerError(\"failed to parse response from swapserver for getpairs\") from e\n        self.sm.update_pairs(pairs)\n\n\nclass NostrTransport(SwapServerTransport):\n    # uses nostr:\n    #  - to advertise servers\n    #  - for client-server RPCs (using DMs)\n    #     (todo: we should use onion messages for that)\n\n    EPHEMERAL_REQUEST = 25582\n    USER_STATUS_NIP38 = 30315\n    NOSTR_EVENT_VERSION = 5\n    OFFER_UPDATE_INTERVAL_SEC = 60 * 10\n    LIQUIDITY_UPDATE_INTERVAL_SEC = 30\n\n    def __init__(self, config, sm, keypair: Keypair):\n        SwapServerTransport.__init__(self, config=config, sm=sm)\n        self._offers = {}  # type: Dict[str, SwapOffer]\n        self.private_key = keypair.privkey\n        self.nostr_private_key = to_nip19('nsec', keypair.privkey.hex())\n        self.nostr_pubkey = keypair.pubkey.hex()[2:]\n        self.dm_replies = {}  # type: Dict[tuple[str, str], asyncio.Future[dict]]\n        self.ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path)\n        self.relay_manager = None  # type: Optional[aionostr.Manager]\n        self.taskgroup = OldTaskGroup()\n        self._last_swapserver_relays = self._load_last_swapserver_relays()  # type: Optional[Sequence[str]]\n        self._swap_server_requests = asyncio.Queue(maxsize=5)  # type: asyncio.Queue[dict]\n\n    def __enter__(self):\n        asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)\n        return self\n\n    def __exit__(self, ex_type, ex, tb):\n        fut = asyncio.run_coroutine_threadsafe(self.stop(), self.network.asyncio_loop)\n        fut.result(timeout=5)\n\n    async def __aenter__(self):\n        asyncio.create_task(self.main_loop())\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        await wait_for2(self.stop(), timeout=5)\n\n    @log_exceptions\n    async def main_loop(self):\n        self.logger.info(f'starting nostr transport with pubkey: {self.nostr_pubkey}')\n        self.logger.info(f'nostr relays: {self.relays}')\n        self.relay_manager = self.get_relay_manager()\n        await self.relay_manager.connect()\n        connected_relays = self.relay_manager.relays\n        self.logger.info(f'connected relays: {[relay.url for relay in connected_relays]}')\n        if connected_relays:\n            self.is_connected.set()\n        if self.sm.is_server:\n            tasks = [\n                self.check_direct_messages(),\n                self._handle_requests(),\n            ]\n        else:\n            tasks = [\n                self.check_direct_messages(),\n                self._get_pairs_loop(),\n                self.update_relays()\n            ]\n        try:\n            async with self.taskgroup as group:\n                for task in tasks:\n                    await group.spawn(task)\n        except Exception as e:\n            self.logger.exception(\"taskgroup died.\")\n        finally:\n            self.logger.info(\"taskgroup stopped.\")\n\n    @log_exceptions\n    async def stop(self):\n        self.logger.info(\"shutting down nostr transport\")\n        self.sm.is_initialized.clear()\n        self.is_connected.clear()\n        await self.taskgroup.cancel_remaining()\n        await self.relay_manager.close()\n        self.logger.info(\"nostr transport shut down\")\n\n    @property\n    def relays(self):\n        our_relays = self.config.NOSTR_RELAYS.split(',') if self.config.NOSTR_RELAYS else []\n        if self.sm.is_server:\n            return our_relays\n        last_swapserver_relays = self._last_swapserver_relays or []\n        return list(set(our_relays + last_swapserver_relays))\n\n    def get_relay_manager(self) -> aionostr.Manager:\n        assert get_running_loop() == get_asyncio_loop(), f\"this must be run on the asyncio thread!\"\n        if not self.relay_manager:\n            if self.uses_proxy:\n                proxy = make_aiohttp_proxy_connector(self.network.proxy, self.ssl_context)\n            else:\n                proxy: Optional['ProxyConnector'] = None\n            nostr_logger = self.logger.getChild('aionostr')\n            nostr_logger.setLevel('INFO')  # DEBUG is very verbose with aionostr\n            return aionostr.Manager(\n                self.relays,\n                private_key=self.nostr_private_key,\n                log=nostr_logger,\n                ssl_context=self.ssl_context,\n                proxy=proxy,\n                connect_timeout=self.connect_timeout\n            )\n        return self.relay_manager\n\n    def get_offer(self, pubkey: str) -> Optional[SwapOffer]:\n        return self._offers.get(pubkey)\n\n    def get_recent_offers(self) -> Sequence[SwapOffer]:\n        # filter to fresh timestamps\n        now = int(time.time())\n        recent_offers = [x for x in self._offers.values() if now - x.timestamp < 3600]\n        # sort by proof-of-work\n        recent_offers = sorted(recent_offers, key=lambda x: x.pow_bits, reverse=True)\n        # cap list size\n        recent_offers = recent_offers[:20]\n        return recent_offers\n\n    @ignore_exceptions\n    @log_exceptions\n    async def publish_offer(self, sm: 'SwapManager') -> None:\n        assert self.sm.is_server\n        if sm._max_forward < sm._min_amount and sm._max_reverse < sm._min_amount:\n            self.logger.warning(f\"not publishing swap offer, no liquidity available: {sm._max_forward=}, {sm._max_reverse=}\")\n            return\n        offer = {\n            'percentage_fee': float(sm.percentage),  # cast to float for <= 4.7.1 backwards compatibility\n            'mining_fee': sm.mining_fee,\n            'min_amount': sm._min_amount,\n            'max_forward_amount': sm._max_forward,\n            'max_reverse_amount': sm._max_reverse,\n            'relays': sm.config.NOSTR_RELAYS,\n            'pow_nonce': hex(sm.config.SWAPSERVER_ANN_POW_NONCE),\n        }\n        # the first value of a single letter tag is indexed and can be filtered for\n        tags = [['d', f'electrum-swapserver-{self.NOSTR_EVENT_VERSION}'],\n                ['r', 'net:' + constants.net.NET_NAME],\n                ['expiration', str(int(time.time() + self.OFFER_UPDATE_INTERVAL_SEC + 10))]]\n        try:\n            event_id = await aionostr._add_event(\n                self.relay_manager,\n                kind=self.USER_STATUS_NIP38,\n                tags=tags,\n                content=json.dumps(offer),\n                private_key=self.nostr_private_key)\n            self.logger.info(f\"published offer {event_id}\")\n        except asyncio.TimeoutError as e:\n            self.logger.warning(f\"failed to publish swap offer: {str(e)}\")\n\n    @ignore_exceptions\n    @log_exceptions\n    async def send_direct_message(self, pubkey: str, content: str, *, retries: int = 0) -> Optional[str]:\n        assert retries < 25, \"Use a sane retry amount\"\n        our_private_key = aionostr.key.PrivateKey(self.private_key)\n        recv_pubkey_hex = aionostr.util.from_nip19(pubkey)['object'].hex() if pubkey.startswith('npub') else pubkey\n        encrypted_msg = our_private_key.encrypt_message(content, recv_pubkey_hex)\n        try:\n            event_id = await aionostr._add_event(\n                self.relay_manager,\n                kind=self.EPHEMERAL_REQUEST,\n                content=encrypted_msg,\n                private_key=self.nostr_private_key,\n                tags=[['p', recv_pubkey_hex]],\n            )\n        except asyncio.TimeoutError:\n            self.logger.warning(f\"sending message to {pubkey} failed: timeout. {retries=}\")\n            if retries > 0:\n                return await self.send_direct_message(pubkey, content, retries=retries-1)\n            return None\n        return event_id\n\n    @log_exceptions\n    async def send_request_to_server(self, method: str, request_data: dict) -> dict:\n        self.logger.debug(f\"swapserver req: method: {method} relays: {self.relays}\")\n        request_data['method'] = method\n        server_npub = self.config.SWAPSERVER_NPUB\n        server_pubkey = aionostr.util.from_nip19(server_npub)['object'].hex()\n        event_id = await self.send_direct_message(server_pubkey, json.dumps(request_data), retries=1)\n        if not event_id:\n            raise SwapServerError()\n        self.dm_replies[(server_pubkey, event_id)] = fut = asyncio.Future()\n        response = await fut\n        assert isinstance(response, dict)\n        if 'error' in response:\n            self.logger.warning(f\"error from swap server [DO NOT TRUST THIS MESSAGE]: {response['error']}\")\n            raise SwapServerError()\n        return response\n\n    async def _get_pairs_loop(self):\n        await self.is_connected.wait()\n        query = {\n            \"kinds\": [self.USER_STATUS_NIP38],\n            \"limit\": 10,\n            \"#d\": [f\"electrum-swapserver-{self.NOSTR_EVENT_VERSION}\"],\n            \"#r\": [f\"net:{constants.net.NET_NAME}\"],\n            \"since\": int(time.time()) - 60 * 60,\n        }\n        async for event in self.relay_manager.get_events(query, single_event=False, only_stored=False):\n            try:\n                content = json.loads(event.content)\n                if not isinstance(content, dict):\n                    raise Exception(\"malformed content, not dict\")\n                tags = {k: v for k, v in event.tags}\n            except Exception as e:\n                self.logger.debug(f\"failed to parse event: {e}\")\n                continue\n            if tags.get('d') != f\"electrum-swapserver-{self.NOSTR_EVENT_VERSION}\":\n                continue\n            if tags.get('r') != f\"net:{constants.net.NET_NAME}\":\n                continue\n            if (event.created_at > int(time.time()) + 60 * 60\n                    or event.created_at < int(time.time()) - 60 * 60):\n                continue\n            # check if this is the most recent event for this pubkey\n            pubkey = event.pubkey\n            prev_offer = self._offers.get(to_nip19('npub', pubkey))\n            if prev_offer and event.created_at <= prev_offer.timestamp:\n                continue\n            try:\n                pow_nonce = int(content.get('pow_nonce', \"0\"), 16)  # type: int\n            except Exception:\n                continue\n            pow_bits = get_nostr_ann_pow_amount(bytes.fromhex(pubkey), pow_nonce)\n            if pow_bits < self.config.SWAPSERVER_POW_TARGET:\n                self.logger.debug(f\"too low pow: {pubkey}: pow: {pow_bits} nonce: {pow_nonce}\")\n                continue\n            try:\n                server_relays = content['relays'].split(',')\n            except Exception:\n                continue\n            try:\n                pairs = SwapFees(\n                    percentage=Decimal(str(content['percentage_fee'])),\n                    mining_fee=content['mining_fee'],\n                    min_amount=content['min_amount'],\n                    max_forward=content['max_forward_amount'],\n                    max_reverse=content['max_reverse_amount'],\n                )\n            except Exception:\n                self.logger.debug(f\"swap fees couldn't be parsed\", exc_info=True)\n                continue\n            offer = SwapOffer(\n                pairs=pairs,\n                relays=server_relays[:10],\n                timestamp=event.created_at,\n                server_pubkey=pubkey,\n                pow_bits=pow_bits,\n            )\n            self._offers[offer.server_npub] = offer\n            if self.config.SWAPSERVER_NPUB == offer.server_npub:\n                self.sm.update_pairs(pairs)\n            trigger_callback('swap_offers_changed', self.get_recent_offers())\n            # mirror event to other relays\n            await self.taskgroup.spawn(self.rebroadcast_event(event, server_relays))\n\n    async def update_relays(self):\n        \"\"\"\n        Update the relays when update_pairs is called.\n        This ensures we try to connect to the same relays as the ones announced by the swap server.\n        \"\"\"\n        while True:\n            previous_relays = self._last_swapserver_relays\n            await self.sm.pairs_updated.wait()\n            if (conf_swapserver_offer := self._offers.get(self.config.SWAPSERVER_NPUB)) is None:\n                self.logger.debug(\n                    f\"pairs updated but no pair for {self.config.SWAPSERVER_NPUB=} available? {self._offers=}\",\n                    stack_info=True,\n                )\n                continue\n            latest_known_relays = conf_swapserver_offer.relays\n            if latest_known_relays != previous_relays:\n                self.logger.debug(f\"swapserver relays changed, updating relay list.\")\n                # store the latest known relays to a file\n                self._store_last_swapserver_relays(latest_known_relays)\n                # update the relay manager\n                await self.relay_manager.update_relays(self.relays)\n\n    async def rebroadcast_event(self, event: Event, server_relays: Sequence[str]):\n        \"\"\"If the relays of the origin server are different from our relays we rebroadcast the\n        event to our relays so it gets spread more widely.\"\"\"\n        if not server_relays:\n            return\n        rebroadcast_relays = [relay for relay in self.relay_manager.relays if\n                              relay.url not in server_relays]\n        for relay in rebroadcast_relays:\n            try:\n                res = await relay.add_event(event, check_response=True)\n            except Exception as e:\n                self.logger.debug(f\"failed to rebroadcast event to {relay.url}: {e}\")\n                continue\n            self.logger.debug(f\"rebroadcasted event to {relay.url}: {res}\")\n\n    @log_exceptions\n    async def check_direct_messages(self):\n        privkey = aionostr.key.PrivateKey(self.private_key)\n        query = {\"kinds\": [self.EPHEMERAL_REQUEST], \"limit\":0, \"#p\": [self.nostr_pubkey]}\n        async for event in self.relay_manager.get_events(query, single_event=False, only_stored=False):\n            try:\n                content = privkey.decrypt_message(event.content, event.pubkey)\n                content = json.loads(content)\n                if not isinstance(content, dict):\n                    raise Exception(\"malformed content, not dict\")\n            except Exception:\n                continue\n            content['event_id'] = event.id\n            content['event_pubkey'] = event.pubkey\n            if not self.sm.is_server and 'reply_to' in content:\n                prev_event_id = content['reply_to']\n                server_pubkey = event.pubkey\n                fut = self.dm_replies.get((server_pubkey, prev_event_id))\n                if fut:\n                    fut.set_result(content)\n            elif self.sm.is_server and 'method' in content:\n                if self._swap_server_requests.full():\n                    self.logger.warning(f\"too many swap requests, dropping incoming request: {event.id[:10]}...\")\n                    continue\n                await self._swap_server_requests.put(content)\n            else:\n                self.logger.info(f'unknown message {content}')\n\n    @log_exceptions\n    async def _handle_requests(self) -> None:\n        assert self.sm.is_server\n        while True:\n            await asyncio.sleep(5)\n            request = await self._swap_server_requests.get()\n            event_id = request.pop('event_id')\n            event_pubkey = request.pop('event_pubkey')\n            try:\n                method = request.pop('method')\n                self.logger.info(f'handle_request: id={event_id} {method} {request}')\n                if method == 'addswapinvoice':  # client-forward-swap phase2\n                    r = self.sm.server_add_swap_invoice(request)\n                elif method == 'createswap':  # client-reverse-swap\n                    r = self.sm.server_create_swap(request)\n                elif method == 'createnormalswap':  # client-forward-swap phase1\n                    r = self.sm.server_create_normal_swap(request)\n                else:\n                    raise Exception(method)\n                r['reply_to'] = event_id\n                self.logger.debug(f'sending response id={event_id}')\n                await self.taskgroup.spawn(self.send_direct_message(event_pubkey, json.dumps(r), retries=2))\n            except Exception as e:\n                self.logger.exception(f\"failed to handle {request=}\")\n                error_response = json.dumps({\n                    \"error\": f\"Internal Server Error: {str(type(e))}\",\n                    \"reply_to\": event_id,\n                })\n                await self.taskgroup.spawn(self.send_direct_message(event_pubkey, error_response))\n\n    def _store_last_swapserver_relays(self, relays: Sequence[str]):\n        self._last_swapserver_relays = relays\n        if not self.config.path or not relays:\n            return\n        storage_path = os.path.join(self.config.path, 'recent_swapserver_relays')\n        try:\n            with open(storage_path, 'w', encoding=\"utf-8\") as f:\n                json.dump(relays, f, indent=4, sort_keys=True)  # type: ignore\n        except Exception:\n            self.logger.exception(f\"failed to write last swapserver relays to {storage_path}\")\n\n    def _load_last_swapserver_relays(self) -> Optional[Sequence[str]]:\n        storage_path = os.path.join(self.config.path, 'recent_swapserver_relays')\n        if not os.path.exists(storage_path):\n            return None\n        try:\n            with open(storage_path, 'r', encoding=\"utf-8\") as f:\n                relays = json.load(f)\n        except Exception:\n            self.logger.exception(f\"failed to read last swapserver relays from {storage_path}\")\n            return None\n        return relays\n"
  },
  {
    "path": "electrum/synchronizer.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2014 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport asyncio\nimport hashlib\nfrom typing import Dict, List, TYPE_CHECKING, Tuple, Set, Optional, Sequence\nfrom collections import defaultdict\nimport logging\n\nfrom aiorpcx import run_in_thread, RPCError\n\nfrom . import util\nfrom .transaction import Transaction, PartialTransaction\nfrom .util import make_aiohttp_session, NetworkJobOnDefaultServer, random_shuffled_copy, OldTaskGroup\nfrom .bitcoin import address_to_scripthash, is_address\nfrom .logging import Logger\nfrom .interface import GracefulDisconnect, NetworkTimeout\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .address_synchronizer import AddressSynchronizer\n\n\nclass SynchronizerFailure(Exception): pass\n\n\ndef history_status(h: Sequence[tuple[str, int]]) -> Optional[str]:\n    if not h:\n        return None\n    status = ''\n    for tx_hash, height in h:\n        status += tx_hash + ':%d:' % height\n    return hashlib.sha256(status.encode('ascii')).digest().hex()\n\n\nclass SynchronizerBase(NetworkJobOnDefaultServer):\n    \"\"\"Subscribe over the network to a set of addresses, and monitor their statuses.\n    Every time a status changes, run a coroutine provided by the subclass.\n    \"\"\"\n    def __init__(self, network: 'Network'):\n        self.asyncio_loop = network.asyncio_loop\n\n        NetworkJobOnDefaultServer.__init__(self, network)\n\n    def _reset(self):\n        super()._reset()\n        self._adding_addrs = set()\n        self.requested_addrs = set()\n        self._handling_addr_statuses = set()\n        self.scripthash_to_address = {}\n        self._processed_some_notifications = False  # so that we don't miss them\n        # Queues\n        self.status_queue = asyncio.Queue()\n\n    async def _run_tasks(self, *, taskgroup):\n        await super()._run_tasks(taskgroup=taskgroup)\n        try:\n            async with taskgroup as group:\n                await group.spawn(self.handle_status())\n                await group.spawn(self.main())\n        finally:\n            # we are being cancelled now\n            self.session.unsubscribe(self.status_queue)\n\n    def add(self, addr: str) -> None:\n        if not is_address(addr): raise ValueError(f\"invalid bitcoin address {addr}\")\n        self._adding_addrs.add(addr)  # this lets is_up_to_date already know about addr\n\n    async def _add_address(self, addr: str):\n        try:\n            if not is_address(addr): raise ValueError(f\"invalid bitcoin address {addr}\")\n            if addr in self.requested_addrs: return\n            self.requested_addrs.add(addr)\n            await self.taskgroup.spawn(self._subscribe_to_address, addr)\n        finally:\n            self._adding_addrs.discard(addr)  # ok for addr not to be present\n\n    async def _on_address_status(self, addr: str, status: Optional[str]):\n        \"\"\"Handle the change of the status of an address.\n        Should remove addr from self._handling_addr_statuses when done.\n        \"\"\"\n        raise NotImplementedError()  # implemented by subclasses\n\n    async def _subscribe_to_address(self, addr):\n        h = address_to_scripthash(addr)\n        self.scripthash_to_address[h] = addr\n        self._requests_sent += 1\n        try:\n            async with self._network_request_semaphore:\n                await self.session.subscribe('blockchain.scripthash.subscribe', [h], self.status_queue)\n        except RPCError as e:\n            if e.message == 'history too large':  # no unique error code\n                raise GracefulDisconnect(e, log_level=logging.ERROR) from e\n            raise\n        self._requests_answered += 1\n\n    async def handle_status(self):\n        while True:\n            h, status = await self.status_queue.get()\n            addr = self.scripthash_to_address[h]\n            self._handling_addr_statuses.add(addr)\n            self.requested_addrs.discard(addr)  # ok for addr not to be present\n            await self.taskgroup.spawn(self._on_address_status, addr, status)\n            self._processed_some_notifications = True\n\n    async def main(self):\n        raise NotImplementedError()  # implemented by subclasses\n\n\nclass Synchronizer(SynchronizerBase):\n    '''The synchronizer keeps the wallet up-to-date with its set of\n    addresses and their transactions.  It subscribes over the network\n    to wallet addresses, gets the wallet to generate new addresses\n    when necessary, requests the transaction history of any addresses\n    we don't have the full history of, and requests binary transaction\n    data of any transactions the wallet doesn't have.\n    '''\n    def __init__(self, adb: 'AddressSynchronizer'):\n        self.adb = adb\n        SynchronizerBase.__init__(self, adb.network)\n\n    def _reset(self):\n        super()._reset()\n        self._init_done = False\n        self.requested_tx = set()  # type: Set[str]\n        self.requested_histories = set()\n        self._stale_histories = dict()  # type: Dict[str, asyncio.Task]\n\n    def diagnostic_name(self):\n        return self.adb.diagnostic_name()\n\n    def is_up_to_date(self):\n        return (self._init_done\n                and not self._adding_addrs\n                and not self.requested_addrs\n                and not self._handling_addr_statuses\n                and not self.requested_histories\n                and not self.requested_tx\n                and not self._stale_histories\n                and self.status_queue.empty())\n\n    async def _maybe_request_history_for_addr(self, addr: str, *, ann_status: Optional[str]) -> List[dict]:\n        # First opportunistically try to guess the addr history. Might save us network requests.\n        old_history = self.adb.db.get_addr_history(addr)\n        def guess_height(old_height: int) -> int:\n            if old_height in (0, -1,):\n                return self.interface.tip  # maybe mempool tx got mined just now\n            return old_height\n        guessed_history = [(txid, guess_height(old_height)) for (txid, old_height) in old_history]\n        if history_status(guessed_history) == ann_status:\n            self.logger.debug(f\"managed to guess new history for {addr}. won't call 'blockchain.scripthash.get_history'.\")\n            return [{\"height\": height, \"tx_hash\": txid} for (txid, height) in guessed_history]\n        # request addr history from server\n        sh = address_to_scripthash(addr)\n        self._requests_sent += 1\n        async with self._network_request_semaphore:\n            result = await self.interface.get_history_for_scripthash(sh)\n        self._requests_answered += 1\n        self.logger.info(f\"receiving history {addr} {len(result)}\")\n        return result\n\n    async def _on_address_status(self, addr, status):\n        try:\n            old_history = self.adb.db.get_addr_history(addr)\n            if history_status(old_history) == status:\n                return\n            # No point in requesting history twice for the same announced status.\n            # However if we got announced a new status, we should request history again:\n            if (addr, status) in self.requested_histories:\n                return\n            # request address history\n            self.requested_histories.add((addr, status))\n            self._stale_histories.pop(addr, asyncio.Future()).cancel()\n        finally:\n            self._handling_addr_statuses.discard(addr)\n        result = await self._maybe_request_history_for_addr(addr, ann_status=status)\n        hist = list(map(lambda item: (item['tx_hash'], item['height']), result))\n        # tx_fees\n        tx_fees = [(item['tx_hash'], item.get('fee')) for item in result]\n        tx_fees = dict(filter(lambda x:x[1] is not None, tx_fees))\n        # Check that the status corresponds to what was announced\n        if history_status(hist) != status:\n            # could happen naturally if history changed between getting status and history (race)\n            self.logger.info(f\"error: status mismatch: {addr}. we'll wait a bit for status update.\")\n            # The server is supposed to send a new status notification, which will trigger a new\n            # get_history. We shall wait a bit for this to happen, otherwise we disconnect.\n            async def disconnect_if_still_stale():\n                timeout = self.network.get_network_timeout_seconds(NetworkTimeout.Generic)\n                await asyncio.sleep(timeout)\n                raise SynchronizerFailure(f\"timeout reached waiting for addr {addr}: history still stale\")\n            self._stale_histories[addr] = await self.taskgroup.spawn(disconnect_if_still_stale)\n        else:\n            self._stale_histories.pop(addr, asyncio.Future()).cancel()\n            # Store received history\n            self.adb.receive_history_callback(addr, hist, tx_fees)\n            # Request transactions we don't have\n            await self._request_missing_txs(hist)\n\n        # Remove request; this allows up_to_date to be True\n        self.requested_histories.discard((addr, status))\n\n    async def _request_missing_txs(self, hist, *, allow_server_not_finding_tx=False):\n        # \"hist\" is a list of [tx_hash, tx_height] lists\n        transaction_hashes = []\n        for tx_hash, _tx_height in hist:\n            if tx_hash in self.requested_tx:\n                continue\n            tx = self.adb.db.get_transaction(tx_hash)\n            if tx and not isinstance(tx, PartialTransaction):\n                continue  # already have complete tx\n            transaction_hashes.append(tx_hash)\n            # note: tx_height might change by the time we get the raw_tx\n            self.requested_tx.add(tx_hash)\n\n        if not transaction_hashes: return\n        async with OldTaskGroup() as group:\n            for tx_hash in transaction_hashes:\n                await group.spawn(self._get_transaction(tx_hash, allow_server_not_finding_tx=allow_server_not_finding_tx))\n\n    async def _get_transaction(self, tx_hash, *, allow_server_not_finding_tx=False):\n        self._requests_sent += 1\n        try:\n            async with self._network_request_semaphore:\n                raw_tx = await self.interface.get_transaction(tx_hash)\n        except RPCError as e:\n            # most likely, \"No such mempool or blockchain transaction\"\n            if allow_server_not_finding_tx:\n                self.requested_tx.remove(tx_hash)\n                return\n            else:\n                raise\n        finally:\n            self._requests_answered += 1\n        tx = Transaction(raw_tx)\n        if tx_hash != tx.txid():\n            raise SynchronizerFailure(f\"received tx does not match expected txid ({tx_hash} != {tx.txid()})\")\n        self.requested_tx.remove(tx_hash)\n        self.adb.receive_tx_callback(tx)\n        self.logger.info(f\"received tx {tx_hash}. bytes-len: {len(raw_tx)//2}\")\n\n    async def main(self):\n        self.adb.up_to_date_changed()\n        # request missing txns, if any\n        for addr in random_shuffled_copy(self.adb.db.get_history()):\n            history = self.adb.db.get_addr_history(addr)\n            # Old electrum servers returned ['*'] when all history for the address\n            # was pruned. This no longer happens but may remain in old wallets.\n            if history == ['*']: continue\n            await self._request_missing_txs(history, allow_server_not_finding_tx=True)\n        # add addresses to bootstrap\n        for addr in random_shuffled_copy(self.adb.get_addresses()):\n            await self._add_address(addr)\n        # main loop\n        self._init_done = True\n        prev_uptodate = False\n        while True:\n            await asyncio.sleep(0.1)\n            for addr in self._adding_addrs.copy(): # copy set to ensure iterator stability\n                await self._add_address(addr)\n            up_to_date = self.adb.is_up_to_date()\n            # see if status changed\n            if (up_to_date != prev_uptodate\n                    or up_to_date and self._processed_some_notifications):\n                self._processed_some_notifications = False\n                self.adb.up_to_date_changed()\n            prev_uptodate = up_to_date\n\n\nclass Notifier(SynchronizerBase):\n    \"\"\"Watch addresses. Every time the status of an address changes,\n    an HTTP POST is sent to the corresponding URL.\n    \"\"\"\n    def __init__(self, network):\n        SynchronizerBase.__init__(self, network)\n        self.watched_addresses = defaultdict(list)  # type: Dict[str, List[str]]\n        self._start_watching_queue = asyncio.Queue()  # type: asyncio.Queue[Tuple[str, str]]\n\n    async def main(self):\n        # resend existing subscriptions if we were restarted\n        for addr in self.watched_addresses:\n            await self._add_address(addr)\n        # main loop\n        while True:\n            addr, url = await self._start_watching_queue.get()\n            self.watched_addresses[addr].append(url)\n            await self._add_address(addr)\n\n    async def start_watching_addr(self, addr: str, url: str):\n        await self._start_watching_queue.put((addr, url))\n\n    async def stop_watching_addr(self, addr: str):\n        self.watched_addresses.pop(addr, None)\n        # TODO blockchain.scripthash.unsubscribe\n\n    async def _on_address_status(self, addr, status):\n        if addr not in self.watched_addresses:\n            return\n        self.logger.info(f'new status for addr {addr}')\n        headers = {'content-type': 'application/json'}\n        data = {'address': addr, 'status': status}\n        for url in self.watched_addresses[addr]:\n            try:\n                async with make_aiohttp_session(proxy=self.network.proxy, headers=headers) as session:\n                    async with session.post(url, json=data, headers=headers) as resp:\n                        await resp.text()\n            except Exception as e:\n                self.logger.info(repr(e))\n            else:\n                self.logger.info(f'Got Response for {addr}')\n"
  },
  {
    "path": "electrum/trampoline.py",
    "content": "import io\nimport os\nimport random\nimport dataclasses\nfrom typing import Mapping, Tuple, Optional, List, Iterable, Sequence, Set, Any\nfrom types import MappingProxyType\n\nfrom .lnutil import LnFeatures, PaymentFeeBudget, FeeBudgetExceeded\nfrom .lnonion import (\n    calc_hops_data_for_payment, new_onion_packet, OnionPacket, TRAMPOLINE_HOPS_DATA_SIZE, PER_HOP_HMAC_SIZE\n)\nfrom .lnrouter import TrampolineEdge, is_route_within_budget, LNPaymentTRoute\nfrom .lnutil import NoPathFound\nfrom .lntransport import LNPeerAddr\nfrom . import constants\nfrom .logging import get_logger\nfrom .util import random_shuffled_copy\n\n\n_logger = get_logger(__name__)\n\n# hardcoded list\n# TODO for some pubkeys, there are multiple network addresses we could try\nTRAMPOLINE_NODES_MAINNET = {\n    'ACINQ':                  LNPeerAddr(host='node.acinq.co',           port=9735, pubkey=bytes.fromhex('03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f')),\n    'Electrum trampoline':    LNPeerAddr(host='lightning.electrum.org',  port=9740, pubkey=bytes.fromhex('03ecef675be448b615e6176424070673ef8284e0fd19d8be062a6cb5b130a0a0d1')),\n    'trampoline hodlisterco': LNPeerAddr(host='trampoline.hodlister.co', port=9740, pubkey=bytes.fromhex('02ce014625788a61411398f83c945375663972716029ef9d8916719141dc109a1c')),\n}\n\nTRAMPOLINE_NODES_TESTNET = {\n    'endurance': LNPeerAddr(host='34.250.234.192', port=9735, pubkey=bytes.fromhex('03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134')),\n    'Electrum trampoline': LNPeerAddr(host='lightning.electrum.org', port=9739, pubkey=bytes.fromhex('02bf82e22f99dcd7ac1de4aad5152ce48f0694c46ec582567f379e0adbf81e2d0f')),\n}\n\nTRAMPOLINE_NODES_TESTNET4 = {}\n\nTRAMPOLINE_NODES_SIGNET = {\n    'eclair wakiyamap.dev': LNPeerAddr(host='signet-eclair.wakiyamap.dev', port=9735, pubkey=bytes.fromhex('0271cf3881e6eadad960f47125434342e57e65b98a78afa99f9b4191c02dd7ab3b')),\n}\n\n_TRAMPOLINE_NODES_UNITTESTS = {}  # used in unit tests\n\n\ndef hardcoded_trampoline_nodes() -> Mapping[str, LNPeerAddr]:\n    if _TRAMPOLINE_NODES_UNITTESTS:\n        return _TRAMPOLINE_NODES_UNITTESTS\n    elif constants.net.NET_NAME == \"mainnet\":\n        return TRAMPOLINE_NODES_MAINNET\n    elif constants.net.NET_NAME == \"testnet\":\n        return TRAMPOLINE_NODES_TESTNET\n    elif constants.net.NET_NAME == \"testnet4\":\n        return TRAMPOLINE_NODES_TESTNET4\n    elif constants.net.NET_NAME == \"signet\":\n        return TRAMPOLINE_NODES_SIGNET\n    else:\n        return {}\n\n\ndef trampolines_by_id():\n    return dict([(x.pubkey, x) for x in hardcoded_trampoline_nodes().values()])\n\n\ndef is_hardcoded_trampoline(node_id: bytes) -> bool:\n    return node_id in trampolines_by_id()\n\n\ndef encode_routing_info(r_tags: Sequence[Sequence[Sequence[Any]]]) -> List[bytes]:\n    routes = []\n    for route in r_tags:\n        result = bytes([len(route)])\n        for step in route:\n            pubkey, scid, feebase, feerate, cltv = step\n            result += pubkey\n            result += scid\n            result += int.to_bytes(feebase, length=4, byteorder=\"big\", signed=False)\n            result += int.to_bytes(feerate, length=4, byteorder=\"big\", signed=False)\n            result += int.to_bytes(cltv, length=2, byteorder=\"big\", signed=False)\n        routes.append(result)\n    return routes\n\n\ndef decode_routing_info(rinfo: bytes) -> Sequence[Sequence[Sequence[Any]]]:\n    if not rinfo:\n        return []\n    r_tags = []\n    with io.BytesIO(bytes(rinfo)) as s:\n        while True:\n            route = []\n            route_len = s.read(1)\n            if not route_len:\n                break\n            for step in range(route_len[0]):\n                pubkey = s.read(33)\n                scid = s.read(8)\n                feebase = int.from_bytes(s.read(4), byteorder=\"big\")\n                feerate = int.from_bytes(s.read(4), byteorder=\"big\")\n                cltv = int.from_bytes(s.read(2), byteorder=\"big\")\n                route.append((pubkey, scid, feebase, feerate, cltv))\n            r_tags.append(route)\n    return r_tags\n\n\ndef is_legacy_relay(invoice_features, r_tags) -> Tuple[bool, Set[bytes]]:\n    \"\"\"Returns if we deal with a legacy payment and the list of trampoline pubkeys in the invoice.\n    \"\"\"\n    invoice_features = LnFeatures(invoice_features)\n    # trampoline-supporting wallets:\n    if invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR)\\\n       or invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM):\n        # If there are no r_tags (routing hints) included, the wallet doesn't have\n        # private channels and is probably directly connected to a trampoline node.\n        # Any trampoline node should be able to figure out a path to the receiver and\n        # we can use an e2e payment.\n        if not r_tags:\n            return False, set()\n        else:\n            # - We choose one routing hint at random, and\n            #   use end-to-end trampoline if that node is a trampoline-forwarder (TF).\n            # - In case of e2e, the route will have either one or two TFs (one neighbour of sender,\n            #   and one neighbour of recipient; and these might coincide). Note that there are some\n            #   channel layouts where two TFs are needed for a payment to succeed, e.g. both\n            #   endpoints connected to T1 and T2, and sender only has send-capacity with T1, while\n            #   recipient only has recv-capacity with T2.\n            singlehop_r_tags = [x for x in r_tags if len(x) == 1]\n            invoice_trampolines = [x[0][0] for x in singlehop_r_tags]\n            invoice_trampolines = set(invoice_trampolines)\n            if invoice_trampolines:\n                return False, invoice_trampolines\n    # if trampoline receiving is not supported or the forwarder is not known as a trampoline,\n    # we send a legacy payment\n    return True, set()\n\n\nPLACEHOLDER_FEE = None\n\n\ndef _extend_trampoline_route(\n        route: List[TrampolineEdge],\n        *,\n        start_node: bytes = None,\n        end_node: bytes,\n        pay_fees: bool = True,\n):\n    \"\"\"Extends the route and modifies it in place.\"\"\"\n    if start_node is None:\n        assert route\n        start_node = route[-1].end_node\n    trampoline_features = LnFeatures.VAR_ONION_OPT\n    # get policy for *start_node*\n    # note: trampoline nodes are supposed to advertise their fee and cltv in node_update message.\n    #       However, in the temporary spec, they do not.\n    #       They also don't send their fee policy in the error message if we lowball the fee...\n    route.append(\n        TrampolineEdge(\n            start_node=start_node,\n            end_node=end_node,\n            fee_base_msat=PLACEHOLDER_FEE if pay_fees else 0,\n            fee_proportional_millionths=PLACEHOLDER_FEE if pay_fees else 0,\n            cltv_delta=576 if pay_fees else 0,\n            node_features=trampoline_features))\n\n\ndef _allocate_fee_along_route(\n    route: List[TrampolineEdge],\n    *,\n    budget: PaymentFeeBudget,\n    trampoline_fee_level: int,\n) -> None:\n    # calculate budget_to_use, based on given max available \"budget\"\n    if trampoline_fee_level == 0:\n        budget_to_use = 0\n    else:\n        assert trampoline_fee_level > 0\n        MAX_LEVEL = 6\n        if trampoline_fee_level > MAX_LEVEL:\n            raise FeeBudgetExceeded(\"highest trampoline fee level reached\")\n        budget_to_use = budget.fee_msat // (2 ** (MAX_LEVEL - trampoline_fee_level))\n    _logger.debug(f\"_allocate_fee_along_route(). {trampoline_fee_level=}, {budget.fee_msat=}, {budget_to_use=}\")\n    # replace placeholder fees\n    for edge in route:\n        assert edge.fee_base_msat in (0, PLACEHOLDER_FEE), edge.fee_base_msat\n        assert edge.fee_proportional_millionths in (0, PLACEHOLDER_FEE), edge.fee_proportional_millionths\n    edges_to_update = [\n        edge for edge in route\n        if edge.fee_base_msat == PLACEHOLDER_FEE]\n    for edge in edges_to_update:\n        edge.fee_base_msat = budget_to_use // len(edges_to_update)\n        edge.fee_proportional_millionths = 0\n\n\ndef _choose_second_trampoline(\n    my_trampoline: bytes,\n    trampolines: Iterable[bytes],\n    failed_routes: Iterable[Sequence[str]],\n) -> bytes:\n    trampolines = set(trampolines)\n    if my_trampoline in trampolines:\n        trampolines.discard(my_trampoline)\n    for r in failed_routes:\n        if len(r) > 2:\n            t2 = bytes.fromhex(r[1])\n            if t2 in trampolines:\n                trampolines.discard(t2)\n    if not trampolines:\n        raise NoPathFound('all routes have failed')\n    return random.choice(list(trampolines))\n\n\ndef create_trampoline_route(\n        *,\n        amount_msat: int,\n        min_final_cltv_delta: int,\n        invoice_pubkey: bytes,\n        invoice_features: int,\n        my_pubkey: bytes,\n        my_trampoline: bytes,  # the first trampoline in the path; which we are directly connected to\n        r_tags,\n        trampoline_fee_level: int,\n        use_two_trampolines: bool,\n        failed_routes: Iterable[Sequence[str]],\n        budget: PaymentFeeBudget,\n) -> LNPaymentTRoute:\n    # we decide whether to convert to a legacy payment\n    is_legacy, invoice_trampolines = is_legacy_relay(invoice_features, r_tags)\n    _logger.debug(f\"Creating trampoline route for invoice_pubkey={invoice_pubkey.hex()}, {is_legacy=}\")\n\n    # we build a route of trampoline hops and extend the route list in place\n    route = []\n\n    # our first trampoline hop is decided by the channel we use\n    _extend_trampoline_route(\n        route, start_node=my_pubkey, end_node=my_trampoline,\n        pay_fees=False,\n    )\n\n    if is_legacy:\n        # we add another different trampoline hop for privacy\n        if use_two_trampolines:\n            trampolines = trampolines_by_id()\n            second_trampoline = _choose_second_trampoline(my_trampoline, list(trampolines.keys()), failed_routes)\n            _extend_trampoline_route(route, end_node=second_trampoline)\n        # the last trampoline onion must contain routing hints for the last trampoline\n        # node to find the recipient\n        # Due to space constraints it is not guaranteed for all route hints to get included in the onion\n        invoice_routing_info: List[bytes] = encode_routing_info(r_tags)\n        assert invoice_routing_info == encode_routing_info(decode_routing_info(b''.join(invoice_routing_info)))\n        # lnwire invoice_features for trampoline is u64\n        invoice_features = invoice_features & 0xffffffffffffffff\n        route[-1].invoice_routing_info = invoice_routing_info\n        route[-1].invoice_features = invoice_features\n        route[-1].outgoing_node_id = invoice_pubkey\n    else:\n        if invoice_trampolines:\n            if my_trampoline in invoice_trampolines:\n                short_route = [my_trampoline.hex(), invoice_pubkey.hex()]\n                if short_route in failed_routes:\n                    add_trampoline = True\n                else:\n                    add_trampoline = False\n            else:\n                add_trampoline = True\n            if add_trampoline:\n                second_trampoline = _choose_second_trampoline(my_trampoline, invoice_trampolines, failed_routes)\n                _extend_trampoline_route(route, end_node=second_trampoline)\n\n    # Add final edge. note: eclair requires an encrypted t-onion blob even in legacy case.\n    # Also needed for fees for last TF!\n    if route[-1].end_node != invoice_pubkey:\n        _extend_trampoline_route(route, end_node=invoice_pubkey)\n\n    # replace placeholder fees in route\n    _allocate_fee_along_route(route, budget=budget, trampoline_fee_level=trampoline_fee_level)\n\n    # check that we can pay amount and fees\n    if not is_route_within_budget(\n        route=route,\n        budget=budget,\n        amount_msat_for_dest=amount_msat,\n        cltv_delta_for_dest=min_final_cltv_delta,\n    ):\n        raise FeeBudgetExceeded(f\"route exceeds budget: budget: {budget}\")\n    return route\n\n\ndef create_trampoline_onion(\n    *,\n    route: LNPaymentTRoute,\n    amount_msat: int,\n    final_cltv_abs: int,\n    total_msat: int,\n    payment_hash: bytes,\n    payment_secret: bytes,\n) -> Tuple[OnionPacket, int, int]:\n    # all edges are trampoline\n    hops_data, amount_msat, cltv_abs = calc_hops_data_for_payment(\n        route,\n        amount_msat,\n        final_cltv_abs=final_cltv_abs,\n        total_msat=total_msat,\n        payment_secret=payment_secret)\n    # detect trampoline hops.\n    payment_path_pubkeys = [x.node_id for x in route]\n    num_hops = len(payment_path_pubkeys)\n    routing_info_payload_index: Optional[int] = None\n    for i in range(num_hops):\n        route_edge = route[i]\n        assert route_edge.is_trampoline()\n        payload = dict(hops_data[i].payload)\n        if i < num_hops - 1:\n            payload.pop('short_channel_id')\n            next_edge = route[i+1]\n            assert next_edge.is_trampoline()\n            payload[\"outgoing_node_id\"] = {\"outgoing_node_id\": next_edge.node_id}\n        # only for final\n        if i == num_hops - 1:\n            payload[\"payment_data\"] = {\n                \"payment_secret\": payment_secret,\n                \"total_msat\": total_msat\n            }\n        # legacy\n        if i == num_hops - 2 and route_edge.invoice_features:\n            payload[\"invoice_features\"] = {\"invoice_features\": route_edge.invoice_features}\n            routing_info_payload_index = i\n            payload[\"payment_data\"] = {\n                \"payment_secret\": payment_secret,\n                \"total_msat\": total_msat\n            }\n        hops_data[i] = dataclasses.replace(hops_data[i], payload=payload)\n\n    if (index := routing_info_payload_index) is not None:\n        # fill the remaining payload space with available routing hints (r_tags)\n        payload = dict(hops_data[index].payload)\n        # try different r_tag order on each attempt\n        invoice_routing_info = random_shuffled_copy(route[index].invoice_routing_info)\n        remaining_payload_space = TRAMPOLINE_HOPS_DATA_SIZE \\\n                                  - sum(len(hop.to_bytes()) + PER_HOP_HMAC_SIZE for hop in hops_data)\n        routing_info_to_use = []\n        for encoded_r_tag in invoice_routing_info:\n            if remaining_payload_space < 50:\n                break  # no r_tag will fit here anymore\n            r_tag_size = len(encoded_r_tag)\n            if r_tag_size > remaining_payload_space:\n                continue\n            routing_info_to_use.append(encoded_r_tag)\n            remaining_payload_space -= r_tag_size\n        # add the chosen r_tags to the payload\n        payload[\"invoice_routing_info\"] = {\"invoice_routing_info\": b''.join(routing_info_to_use)}\n        hops_data[index] = dataclasses.replace(hops_data[index], payload=payload)\n        _logger.debug(f\"Using {len(routing_info_to_use)} of {len(invoice_routing_info)} r_tags\")\n\n    trampoline_session_key = os.urandom(32)\n    trampoline_onion = new_onion_packet(payment_path_pubkeys, trampoline_session_key, hops_data, associated_data=payment_hash, trampoline=True)\n    trampoline_onion = dataclasses.replace(\n        trampoline_onion,\n        _debug_hops_data=hops_data,\n        _debug_route=route,\n    )\n    return trampoline_onion, amount_msat, cltv_abs\n\n\ndef create_trampoline_route_and_onion(\n        *,\n        amount_msat: int,  # that final receiver gets\n        total_msat: int,\n        min_final_cltv_delta: int,\n        invoice_pubkey: bytes,\n        invoice_features,\n        my_pubkey: bytes,\n        node_id: bytes,\n        r_tags,\n        payment_hash: bytes,\n        payment_secret: bytes,\n        local_height: int,\n        trampoline_fee_level: int,\n        use_two_trampolines: bool,\n        failed_routes: Iterable[Sequence[str]],\n        budget: PaymentFeeBudget,\n) -> Tuple[LNPaymentTRoute, OnionPacket, int, int]:\n    # create route for the trampoline_onion\n    trampoline_route = create_trampoline_route(\n        amount_msat=amount_msat,\n        min_final_cltv_delta=min_final_cltv_delta,\n        my_pubkey=my_pubkey,\n        invoice_pubkey=invoice_pubkey,\n        invoice_features=invoice_features,\n        my_trampoline=node_id,\n        r_tags=r_tags,\n        trampoline_fee_level=trampoline_fee_level,\n        use_two_trampolines=use_two_trampolines,\n        failed_routes=failed_routes,\n        budget=budget,\n    )\n    # compute onion and fees\n    final_cltv_abs = local_height + min_final_cltv_delta\n    trampoline_onion, amount_with_fees, bucket_cltv_abs = create_trampoline_onion(\n        route=trampoline_route,\n        amount_msat=amount_msat,\n        final_cltv_abs=final_cltv_abs,\n        total_msat=total_msat,\n        payment_hash=payment_hash,\n        payment_secret=payment_secret)\n    bucket_cltv_delta = bucket_cltv_abs - local_height\n    return trampoline_route, trampoline_onion, amount_with_fees, bucket_cltv_delta\n"
  },
  {
    "path": "electrum/transaction.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2011 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n# Note: The deserialization code originally comes from ABE.\n\nimport struct\nimport io\nimport base64\nfrom typing import (\n    Sequence, Union, NamedTuple, Tuple, Optional, Iterable, Callable, List, Dict, Set, TYPE_CHECKING, Mapping, Any\n)\nfrom collections import defaultdict\nfrom enum import IntEnum\nimport itertools\nimport binascii\nimport copy\nimport re\n\nimport electrum_ecc as ecc\nfrom electrum_ecc.util import bip340_tagged_hash\n\nfrom . import bitcoin, bip32\nfrom .bip32 import BIP32Node\nfrom .util import to_bytes, bfh, chunks, is_hex_str, parse_max_spend\nfrom .bitcoin import (\n    TYPE_ADDRESS, TYPE_SCRIPT, hash_160, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, var_int,\n    TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, opcodes, base_decode, base_encode, construct_witness, construct_script,\n    taproot_tweak_seckey\n)\nfrom .crypto import sha256d, sha256\nfrom .logging import get_logger\nfrom .util import ShortID, OldTaskGroup\nfrom .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address, DUMMY_DER_SIG\n\nif TYPE_CHECKING:\n    from .wallet import Abstract_Wallet\n    from .network import Network\n    from .simple_config import SimpleConfig\n\n\n_logger = get_logger(__name__)\nDEBUG_PSBT_PARSING = False\n\n\n_NEEDS_RECALC = ...  # sentinel value\n\n\nclass SerializationError(Exception):\n    \"\"\" Thrown when there's a problem deserializing or serializing \"\"\"\n\n\nclass UnknownTxinType(Exception):\n    pass\n\n\nclass BadHeaderMagic(SerializationError):\n    pass\n\n\nclass UnexpectedEndOfStream(SerializationError):\n    pass\n\n\nclass PSBTInputConsistencyFailure(SerializationError):\n    pass\n\n\nclass MalformedBitcoinScript(Exception):\n    pass\n\n\nclass MissingTxInputAmount(Exception):\n    pass\n\n\nclass TxinDataFetchProgress(NamedTuple):\n    num_tasks_done: int\n    num_tasks_total: int\n    has_errored: bool\n    has_finished: bool\n\n\nclass Sighash(IntEnum):\n    # note: this is not an IntFlag, as ALL|NONE != SINGLE\n\n    DEFAULT = 0  # taproot only (bip-0341)\n    ALL = 1\n    NONE = 2\n    SINGLE = 3\n    ANYONECANPAY = 0x80\n\n    @classmethod\n    def is_valid(cls, sighash: int, *, is_taproot: bool = False) -> bool:\n        valid_flags = {\n            0x01, 0x02, 0x03,\n            0x81, 0x82, 0x83,\n        }\n        if is_taproot:\n            valid_flags.add(0x00)\n        return sighash in valid_flags\n\n    @classmethod\n    def to_sigbytes(cls, sighash: int) -> bytes:\n        if sighash == Sighash.DEFAULT:\n            return b\"\"\n        return sighash.to_bytes(length=1, byteorder=\"big\")\n\n\nclass TxOutput:\n    scriptpubkey: bytes\n    value: Union[int, str]\n\n    def __init__(self, *, scriptpubkey: bytes, value: Union[int, str]):\n        self.scriptpubkey = scriptpubkey\n        if not (isinstance(value, int) or parse_max_spend(value) is not None):\n            raise ValueError(f\"bad txout value: {value!r}\")\n        self.value = value  # int in satoshis; or spend-max-like str\n\n    @classmethod\n    def from_address_and_value(cls, address: str, value: Union[int, str]) -> Union['TxOutput', 'PartialTxOutput']:\n        return cls(scriptpubkey=bitcoin.address_to_script(address),\n                   value=value)\n\n    def serialize_to_network(self) -> bytes:\n        buf = int.to_bytes(self.value, 8, byteorder=\"little\", signed=False)\n        script = self.scriptpubkey\n        buf += var_int(len(script))\n        buf += script\n        return buf\n\n    @classmethod\n    def from_network_bytes(cls, raw: bytes) -> 'TxOutput':\n        vds = BCDataStream()\n        vds.write(raw)\n        txout = parse_output(vds)\n        if vds.can_read_more():\n            raise SerializationError('extra junk at the end of TxOutput bytes')\n        return txout\n\n    def to_legacy_tuple(self) -> Tuple[int, str, Union[int, str]]:\n        if self.address:\n            return TYPE_ADDRESS, self.address, self.value\n        return TYPE_SCRIPT, self.scriptpubkey.hex(), self.value\n\n    @classmethod\n    def from_legacy_tuple(cls, _type: int, addr: str, val: Union[int, str]) -> Union['TxOutput', 'PartialTxOutput']:\n        if _type == TYPE_ADDRESS:\n            return cls.from_address_and_value(addr, val)\n        if _type == TYPE_SCRIPT:\n            return cls(scriptpubkey=bfh(addr), value=val)\n        raise Exception(f\"unexpected legacy address type: {_type}\")\n\n    @property\n    def scriptpubkey(self) -> bytes:\n        return self._scriptpubkey\n\n    @scriptpubkey.setter\n    def scriptpubkey(self, scriptpubkey: bytes):\n        self._scriptpubkey = scriptpubkey\n        self._address = _NEEDS_RECALC\n\n    @property\n    def address(self) -> Optional[str]:\n        if self._address is _NEEDS_RECALC:\n            self._address = get_address_from_output_script(self._scriptpubkey)\n        return self._address\n\n    def get_ui_address_str(self) -> str:\n        addr = self.address\n        if addr is not None:\n            return addr\n        return f\"SCRIPT {self.scriptpubkey.hex()}\"\n\n    def __repr__(self):\n        return f\"<TxOutput script={self.scriptpubkey.hex()} address={self.address} value={self.value}>\"\n\n    def __eq__(self, other):\n        if not isinstance(other, TxOutput):\n            return False\n        return self.scriptpubkey == other.scriptpubkey and self.value == other.value\n\n    def __ne__(self, other):\n        return not (self == other)\n\n    def __hash__(self) -> int:\n        return hash((self.scriptpubkey, self.value))\n\n    def to_json(self):\n        d = {\n            'scriptpubkey': self.scriptpubkey.hex(),\n            'address': self.address,\n            'value_sats': self.value,\n        }\n        return d\n\n\nclass BIP143SharedTxDigestFields(NamedTuple):  # witness v0\n    hashPrevouts: bytes\n    hashSequence: bytes\n    hashOutputs: bytes\n\n    @classmethod\n    def from_tx(cls, tx: 'Transaction') -> 'BIP143SharedTxDigestFields':\n        inputs = tx.inputs()\n        outputs = tx.outputs()\n        hashPrevouts = sha256d(b''.join(txin.prevout.serialize_to_network() for txin in inputs))\n        hashSequence = sha256d(b''.join(\n            int.to_bytes(txin.nsequence, length=4, byteorder=\"little\", signed=False)\n            for txin in inputs))\n        hashOutputs = sha256d(b''.join(o.serialize_to_network() for o in outputs))\n        return BIP143SharedTxDigestFields(\n            hashPrevouts=hashPrevouts,\n            hashSequence=hashSequence,\n            hashOutputs=hashOutputs,\n        )\n\n\nclass BIP341SharedTxDigestFields(NamedTuple):  # witness v1\n    sha_prevouts: bytes\n    sha_amounts: bytes\n    sha_scriptpubkeys: bytes\n    sha_sequences: bytes\n    sha_outputs: bytes\n\n    @classmethod\n    def from_tx(cls, tx: 'Transaction') -> 'BIP341SharedTxDigestFields':\n        inputs = tx.inputs()\n        outputs = tx.outputs()\n        sha_prevouts = sha256(b''.join(txin.prevout.serialize_to_network() for txin in inputs))\n        sha_amounts = sha256(b''.join(\n            int.to_bytes(txin.value_sats(), length=8, byteorder=\"little\", signed=False)\n            for txin in inputs))\n        sha_scriptpubkeys = sha256(b''.join(\n            var_int(len(txin.scriptpubkey)) + txin.scriptpubkey\n            for txin in inputs))\n        sha_sequences = sha256(b''.join(\n            int.to_bytes(txin.nsequence, length=4, byteorder=\"little\", signed=False)\n            for txin in inputs))\n        sha_outputs = sha256(b''.join(o.serialize_to_network() for o in outputs))\n        return BIP341SharedTxDigestFields(\n            sha_prevouts=sha_prevouts,\n            sha_amounts=sha_amounts,\n            sha_scriptpubkeys=sha_scriptpubkeys,\n            sha_sequences=sha_sequences,\n            sha_outputs=sha_outputs,\n        )\n\n\nclass SighashCache:\n\n    def __init__(self):\n        self._witver0 = None  # type: Optional[BIP143SharedTxDigestFields]\n        self._witver1 = None  # type: Optional[BIP341SharedTxDigestFields]\n\n    def get_witver0_data_for_tx(self, tx: 'Transaction') -> BIP143SharedTxDigestFields:\n        if self._witver0 is None:\n            self._witver0 = BIP143SharedTxDigestFields.from_tx(tx)\n        return self._witver0\n\n    def get_witver1_data_for_tx(self, tx: 'Transaction') -> BIP341SharedTxDigestFields:\n        if self._witver1 is None:\n            self._witver1 = BIP341SharedTxDigestFields.from_tx(tx)\n        return self._witver1\n\n\nclass TxOutpoint(NamedTuple):\n    txid: bytes  # endianness same as hex string displayed; reverse of tx serialization order\n    out_idx: int\n\n    @classmethod\n    def from_str(cls, s: str) -> 'TxOutpoint':\n        hash_str, idx_str = s.split(':')\n        assert len(hash_str) == 64, f\"{hash_str} should be a sha256 hash\"\n        return TxOutpoint(txid=bfh(hash_str),\n                          out_idx=int(idx_str))\n\n    def __str__(self) -> str:\n        return f\"\"\"TxOutpoint(\"{self.to_str()}\")\"\"\"\n\n    def __repr__(self):\n        return f\"<{str(self)}>\"\n\n    def to_str(self) -> str:\n        return f\"{self.txid.hex()}:{self.out_idx}\"\n\n    def to_json(self):\n        return [self.txid.hex(), self.out_idx]\n\n    def serialize_to_network(self) -> bytes:\n        return self.txid[::-1] + int.to_bytes(self.out_idx, length=4, byteorder=\"little\", signed=False)\n\n    def is_coinbase(self) -> bool:\n        return self.txid == bytes(32)\n\n    def short_name(self):\n        return f\"{self.txid.hex()[0:10]}:{self.out_idx}\"\n\n\nclass TxInput:\n    prevout: TxOutpoint\n    script_sig: Optional[bytes]\n    nsequence: int\n    witness: Optional[bytes]\n    _is_coinbase_output: bool\n\n    def __init__(self, *,\n                 prevout: TxOutpoint,\n                 script_sig: bytes = None,\n                 nsequence: int = 0xffffffff - 1,\n                 witness: bytes = None,\n                 is_coinbase_output: bool = False):\n        self.prevout = prevout\n        self.script_sig = script_sig\n        self.nsequence = nsequence\n        self.witness = witness\n        self._is_coinbase_output = is_coinbase_output\n        # blockchain fields\n        self.block_height = None  # type: Optional[int]  # height at which the TXO is mined; None means unknown. SPV-ed.\n        self.block_txpos = None  # type: Optional[int]  # position of tx in block, if TXO is mined; otherwise None or -1\n        self.spent_height = None  # type: Optional[int]  # height at which the TXO got spent.  SPV-ed.\n        self.spent_txid = None  # type: Optional[str]  # txid of the spender\n        self._utxo = None  # type: Optional[Transaction]\n        self.__scriptpubkey = None  # type: Optional[bytes]\n        self.__address = None  # type: Optional[str]\n        self.__value_sats = None  # type: Optional[int]\n\n        self._is_taproot = None  # type: Optional[bool]  # None means unknown\n\n    def get_time_based_relative_locktime(self) -> Optional[int]:\n        # see bip 68\n        if self.nsequence & (1<<31):  # \"disable\" flag\n            return None\n        if self.nsequence & (1<<22):  # in units of 512 sec\n            return self.nsequence & 0xffff\n        return None\n\n    def get_block_based_relative_locktime(self) -> Optional[int]:\n        if self.nsequence & (1<<31):  # \"disable\" flag\n            return None\n        if not self.nsequence & (1<<22):  # in blocks\n            return self.nsequence & 0xffff\n        return None\n\n    @property\n    def short_id(self):\n        if self.block_txpos is not None and self.block_txpos >= 0:\n            return ShortID.from_components(self.block_height, self.block_txpos, self.prevout.out_idx)\n        else:\n            return self.prevout.short_name()\n\n    @property\n    def utxo(self):\n        return self._utxo\n\n    @utxo.setter\n    def utxo(self, tx: Optional['Transaction']):\n        if tx is None:\n            return\n        # note that tx might be a PartialTransaction\n        # serialize and de-serialize tx now. this might e.g. convert a complete PartialTx to a Tx\n        tx = tx_from_any(str(tx))\n        # 'utxo' field should not be a PSBT:\n        if not tx.is_complete():\n            return\n        self.validate_data(utxo=tx)\n        self._utxo = tx\n        # update derived fields\n        out_idx = self.prevout.out_idx\n        self.__scriptpubkey = self._utxo.outputs()[out_idx].scriptpubkey\n        self.__address = _NEEDS_RECALC\n        self.__value_sats = self._utxo.outputs()[out_idx].value\n\n    def validate_data(self, *, utxo: Optional['Transaction'] = None, **kwargs) -> None:\n        utxo = utxo or self.utxo\n        if utxo:\n            if self.prevout.txid.hex() != utxo.txid():\n                raise PSBTInputConsistencyFailure(f\"PSBT input validation: \"\n                                                  f\"If a non-witness UTXO is provided, its hash must match the hash specified in the prevout\")\n\n    def is_coinbase_input(self) -> bool:\n        \"\"\"Whether this is the input of a coinbase tx.\"\"\"\n        return self.prevout.is_coinbase()\n\n    def is_coinbase_output(self) -> bool:\n        \"\"\"Whether the coin being spent is an output of a coinbase tx.\n        This matters for coin maturity (and pretty much only for that!).\n        \"\"\"\n        return self._is_coinbase_output\n\n    def value_sats(self) -> Optional[int]:\n        return self.__value_sats\n\n    @property\n    def address(self) -> Optional[str]:\n        if self.__address is _NEEDS_RECALC:\n            self.__address = get_address_from_output_script(self.__scriptpubkey)\n        return self.__address\n\n    @property\n    def scriptpubkey(self) -> Optional[bytes]:\n        return self.__scriptpubkey\n\n    def to_json(self):\n        d = {\n            'prevout_hash': self.prevout.txid.hex(),\n            'prevout_n': self.prevout.out_idx,\n            'coinbase': self.is_coinbase_output(),\n            'nsequence': self.nsequence,\n        }\n        if self.script_sig is not None:\n            d['scriptSig'] = self.script_sig.hex()\n        if self.witness is not None:\n            d['witness'] = [x.hex() for x in self.witness_elements()]\n        return d\n\n    def serialize_to_network(self, *, script_sig: bytes = None) -> bytes:\n        if script_sig is None:\n            script_sig = self.script_sig\n        # Prev hash and index\n        s = self.prevout.serialize_to_network()\n        # Script length, script, sequence\n        s += var_int(len(script_sig))\n        s += script_sig\n        s += int.to_bytes(self.nsequence, length=4, byteorder=\"little\", signed=False)\n        return s\n\n    def witness_elements(self) -> Sequence[bytes]:\n        if not self.witness:\n            return []\n        vds = BCDataStream()\n        vds.write(self.witness)\n        n = vds.read_compact_size()\n        return list(vds.read_bytes(vds.read_compact_size()) for i in range(n))\n\n    def is_segwit(self, *, guess_for_address=False) -> bool:\n        if self.witness not in (b'\\x00', b'', None):\n            return True\n        return False\n\n    def is_taproot(self) -> Optional[bool]:\n        if self._is_taproot is None:\n            if self.address:\n                self._is_taproot = bitcoin.is_taproot_address(self.address)\n        return self._is_taproot\n\n    async def add_info_from_network(\n            self,\n            network: Optional['Network'],\n            *,\n            ignore_network_issues: bool = True,\n            timeout=None,\n    ) -> bool:\n        \"\"\"Returns True iff successful.\"\"\"\n        from .network import NetworkException\n        async def fetch_from_network(txid) -> Optional[Transaction]:\n            tx = None\n            if network and network.has_internet_connection():\n                try:\n                    raw_tx = await network.get_transaction(txid, timeout=timeout)\n                except NetworkException as e:\n                    _logger.info(f'got network error getting input txn. err: {repr(e)}. txid: {txid}. '\n                                 f'if you are intentionally offline, consider using the --offline flag')\n                    if not ignore_network_issues:\n                        raise e\n                else:\n                    tx = Transaction(raw_tx)\n            if not tx and not ignore_network_issues:\n                raise NetworkException('failed to get prev tx from network')\n            return tx\n\n        if self.utxo is None:\n            self.utxo = await fetch_from_network(txid=self.prevout.txid.hex())\n        return self.utxo is not None\n\n    def get_scriptcode_for_sighash(self) -> bytes:\n        \"\"\"Reconstructs the scriptcode part of the preimage for OP_CHECKSIG,\n        for an already complete txin, in order to *verify the signature*.\n        \"\"\"\n        scriptpubkey = self.scriptpubkey\n        if scriptpubkey is None:\n            raise Exception(\"missing scriptpubkey. 'utxo' not set?\")\n        script_type = get_script_type_from_output_script(scriptpubkey)\n        if script_type == \"p2wsh\":\n            wit_elems = self.witness_elements()\n            if not wit_elems:\n                raise Exception(f\"missing witness for {script_type=}\")\n            witness_script = wit_elems[-1]\n            if self.address != bitcoin.script_to_p2wsh(witness_script):\n                raise Exception(\"witness_script from witness does not match address\")\n            if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(witness_script)]:\n                raise Exception('OP_CODESEPARATOR black magic is not supported')\n            return witness_script\n        elif script_type == \"p2wpkh\":\n            pkh = scriptpubkey[-20:]\n            assert len(pkh) == 20\n            p2pkh_script = bitcoin.pubkeyhash_to_p2pkh_script(pkh)\n            return p2pkh_script\n        elif script_type == \"p2sh\":\n            if not self.script_sig:\n                raise Exception(f\"missing script_sig for {script_type=}\")\n            parsed_ss = list(script_GetOp(self.script_sig))\n            redeem_script = parsed_ss[-1][1]\n            if self.address != bitcoin.hash160_to_p2sh(hash_160(redeem_script)):\n                raise Exception(\"redeem_script from script_sig does not match address\")\n            if self.is_segwit():  # p2sh-wrapped-segwit\n                inner_script_type = get_script_type_from_output_script(redeem_script)\n                if inner_script_type == \"p2wsh\":\n                    wit_elems = self.witness_elements()\n                    witness_script = wit_elems[-1]\n                    if redeem_script != bitcoin.p2wsh_nested_script(witness_script):\n                        raise Exception(\"witness_script from witness does not match redeem_script\")\n                    if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(witness_script)]:\n                        raise Exception('OP_CODESEPARATOR black magic is not supported')\n                    return witness_script\n                elif inner_script_type == \"p2wpkh\":\n                    pkh = self.script_sig[-20:]\n                    assert len(pkh) == 20\n                    p2pkh_script = bitcoin.pubkeyhash_to_p2pkh_script(pkh)\n                    return p2pkh_script\n                else:\n                    raise Exception(f\"unexpected {inner_script_type=} wrapped in p2sh. script_sig={self.script_sig.hex()}\")\n            else:\n                if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(redeem_script)]:\n                    raise Exception('OP_CODESEPARATOR black magic is not supported')\n                return redeem_script\n        elif script_type in (\"p2pkh\", \"p2pk\"):\n            # For \"raw scriptpubkey\" case, it is usually the scriptPubKey that is signed.\n            # Complications for the general case:\n            # - signatures should be removed (\"FindAndDelete\")\n            # - OP_CODESEPARATOR black magic\n            return scriptpubkey\n        else:\n            raise Exception(f\"cannot handle {script_type=} ({scriptpubkey.hex()=})\")\n        raise Exception(\"should not get here\")\n\n\nclass BCDataStream(object):\n    \"\"\"Workalike python implementation of Bitcoin's CDataStream class.\"\"\"\n\n    def __init__(self):\n        self.input = None  # type: Optional[bytearray]\n        self.read_cursor = 0\n\n    def clear(self):\n        self.input = None\n        self.read_cursor = 0\n\n    def write(self, _bytes: Union[bytes, bytearray]):  # Initialize with string of _bytes\n        assert isinstance(_bytes, (bytes, bytearray))\n        if self.input is None:\n            self.input = bytearray(_bytes)\n        else:\n            self.input += bytearray(_bytes)\n\n    def read_string(self, encoding='ascii'):\n        # Strings are encoded depending on length:\n        # 0 to 252 :  1-byte-length followed by bytes (if any)\n        # 253 to 65,535 : byte'253' 2-byte-length followed by bytes\n        # 65,536 to 4,294,967,295 : byte '254' 4-byte-length followed by bytes\n        # ... and the Bitcoin client is coded to understand:\n        # greater than 4,294,967,295 : byte '255' 8-byte-length followed by bytes of string\n        # ... but I don't think it actually handles any strings that big.\n        if self.input is None:\n            raise SerializationError(\"call write(bytes) before trying to deserialize\")\n\n        length = self.read_compact_size()\n\n        return self.read_bytes(length).decode(encoding)\n\n    def write_string(self, string, encoding='ascii'):\n        string = to_bytes(string, encoding)\n        # Length-encoded as with read-string\n        self.write_compact_size(len(string))\n        self.write(string)\n\n    def read_bytes(self, length: int) -> bytes:\n        if self.input is None:\n            raise SerializationError(\"call write(bytes) before trying to deserialize\")\n        assert length >= 0\n        input_len = len(self.input)\n        read_begin = self.read_cursor\n        read_end = read_begin + length\n        if 0 <= read_begin <= read_end <= input_len:\n            result = self.input[read_begin:read_end]  # type: bytearray\n            self.read_cursor += length\n            return bytes(result)\n        else:\n            raise SerializationError('attempt to read past end of buffer')\n\n    def write_bytes(self, _bytes: Union[bytes, bytearray], length: int):\n        assert len(_bytes) == length, len(_bytes)\n        self.write(_bytes)\n\n    def can_read_more(self) -> bool:\n        if not self.input:\n            return False\n        return self.read_cursor < len(self.input)\n\n    def read_boolean(self) -> bool: return self.read_bytes(1) != b'\\x00'\n    def read_int16(self): return self._read_num('<h')\n    def read_uint16(self): return self._read_num('<H')\n    def read_int32(self): return self._read_num('<i')\n    def read_uint32(self): return self._read_num('<I')\n    def read_int64(self): return self._read_num('<q')\n    def read_uint64(self): return self._read_num('<Q')\n\n    def write_boolean(self, val): return self.write(b'\\x01' if val else b'\\x00')\n    def write_int16(self, val): return self._write_num('<h', val)\n    def write_uint16(self, val): return self._write_num('<H', val)\n    def write_int32(self, val): return self._write_num('<i', val)\n    def write_uint32(self, val): return self._write_num('<I', val)\n    def write_int64(self, val): return self._write_num('<q', val)\n    def write_uint64(self, val): return self._write_num('<Q', val)\n\n    def read_compact_size(self):\n        try:\n            size = self.input[self.read_cursor]\n            self.read_cursor += 1\n            if size == 253:\n                size = self._read_num('<H')\n            elif size == 254:\n                size = self._read_num('<I')\n            elif size == 255:\n                size = self._read_num('<Q')\n            return size\n        except IndexError as e:\n            raise SerializationError(\"attempt to read past end of buffer\") from e\n\n    def write_compact_size(self, size):\n        if size < 0:\n            raise SerializationError(\"attempt to write size < 0\")\n        elif size < 253:\n            self.write(bytes([size]))\n        elif size < 2**16:\n            self.write(b'\\xfd')\n            self._write_num('<H', size)\n        elif size < 2**32:\n            self.write(b'\\xfe')\n            self._write_num('<I', size)\n        elif size < 2**64:\n            self.write(b'\\xff')\n            self._write_num('<Q', size)\n        else:\n            raise Exception(f\"size {size} too large for compact_size\")\n\n    def _read_num(self, format):\n        try:\n            (i,) = struct.unpack_from(format, self.input, self.read_cursor)\n            self.read_cursor += struct.calcsize(format)\n        except Exception as e:\n            raise SerializationError(e) from e\n        return i\n\n    def _write_num(self, format, num):\n        s = struct.pack(format, num)\n        self.write(s)\n\n\ndef script_GetOp(_bytes : bytes):\n    i = 0\n    while i < len(_bytes):\n        vch = None\n        opcode = _bytes[i]\n        i += 1\n\n        if opcode <= opcodes.OP_PUSHDATA4:\n            nSize = opcode\n            if opcode == opcodes.OP_PUSHDATA1:\n                try: nSize = _bytes[i]\n                except IndexError: raise MalformedBitcoinScript()\n                i += 1\n            elif opcode == opcodes.OP_PUSHDATA2:\n                try: (nSize,) = struct.unpack_from('<H', _bytes, i)\n                except struct.error: raise MalformedBitcoinScript()\n                i += 2\n            elif opcode == opcodes.OP_PUSHDATA4:\n                try: (nSize,) = struct.unpack_from('<I', _bytes, i)\n                except struct.error: raise MalformedBitcoinScript()\n                i += 4\n            if i + nSize > len(_bytes):\n                raise MalformedBitcoinScript(\n                    f\"Push of data element that is larger than remaining data: {nSize} vs {len(_bytes) - i}\")\n            vch = _bytes[i:i + nSize]\n            i += nSize\n\n        yield opcode, vch, i\n\n\nclass OPPushDataGeneric:\n    def __init__(self, pushlen: Callable[[int], bool] | None = None):\n        if pushlen is not None:\n            self.check_data_len = pushlen\n\n    @classmethod\n    def check_data_len(cls, datalen: int) -> bool:\n        # Opcodes below OP_PUSHDATA4 all just push data onto stack, and are equivalent.\n        return opcodes.OP_PUSHDATA4 >= datalen >= 0\n\n    @classmethod\n    def is_instance(cls, item):\n        # accept objects that are instances of this class\n        # or other classes that are subclasses\n        return isinstance(item, cls) \\\n               or (isinstance(item, type) and issubclass(item, cls))\n\n\nclass OPGeneric:\n    def __init__(self, matcher: Callable[[Any], bool] | None = None):\n        if matcher is not None:\n            self.matcher = matcher\n\n    def match(self, op) -> bool:\n        return self.matcher(op)\n\n    @classmethod\n    def is_instance(cls, item) -> bool:\n        # accept objects that are instances of this class\n        # or other classes that are subclasses\n        return isinstance(item, cls) \\\n               or (isinstance(item, type) and issubclass(item, cls))\n\n\nOPPushDataPubkey = OPPushDataGeneric(lambda x: x in (33, 65))\nOP_ANYSEGWIT_VERSION = OPGeneric(lambda x: x in list(range(opcodes.OP_1, opcodes.OP_16 + 1)))\n\nSCRIPTPUBKEY_TEMPLATE_P2PK = [OPPushDataGeneric(lambda x: x in (33, 65)), opcodes.OP_CHECKSIG]\nSCRIPTPUBKEY_TEMPLATE_P2PKH = [opcodes.OP_DUP, opcodes.OP_HASH160,\n                               OPPushDataGeneric(lambda x: x == 20),\n                               opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG]\nSCRIPTPUBKEY_TEMPLATE_P2SH = [opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUAL]\nSCRIPTPUBKEY_TEMPLATE_WITNESS_V0 = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))]\nSCRIPTPUBKEY_TEMPLATE_P2WPKH = [opcodes.OP_0, OPPushDataGeneric(lambda x: x == 20)]\nSCRIPTPUBKEY_TEMPLATE_P2WSH = [opcodes.OP_0, OPPushDataGeneric(lambda x: x == 32)]\nSCRIPTPUBKEY_TEMPLATE_P2TR = [opcodes.OP_1, OPPushDataGeneric(lambda x: x == 32)]\nSCRIPTPUBKEY_TEMPLATE_ANYSEGWIT = [OP_ANYSEGWIT_VERSION, OPPushDataGeneric(lambda x: x in list(range(2, 40 + 1)))]\n\n\ndef check_scriptpubkey_template_and_dust(scriptpubkey, amount: Optional[int]):\n    if match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2PKH):\n        dust_limit = bitcoin.DUST_LIMIT_P2PKH\n    elif match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2SH):\n        dust_limit = bitcoin.DUST_LIMIT_P2SH\n    elif match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2WSH):\n        dust_limit = bitcoin.DUST_LIMIT_P2WSH\n    elif match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_P2WPKH):\n        dust_limit = bitcoin.DUST_LIMIT_P2WPKH\n    elif match_script_against_template(scriptpubkey, SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT):\n        dust_limit = bitcoin.DUST_LIMIT_UNKNOWN_SEGWIT\n    else:\n        raise Exception(f'scriptpubkey does not conform to any template: {scriptpubkey.hex()}')\n    if amount < dust_limit:\n        raise Exception(f'amount ({amount}) is below dust limit for scriptpubkey type ({dust_limit})')\n\n\ndef merge_duplicate_tx_outputs(outputs: Iterable['PartialTxOutput']) -> List['PartialTxOutput']:\n    \"\"\"Merges outputs that are paying to the same address by replacing them with a single larger output.\"\"\"\n    output_dict = {}\n    for output in outputs:\n        assert isinstance(output.value, int), \"tx outputs with spend-max-like str cannot be merged\"\n        if output.scriptpubkey in output_dict:\n            output_dict[output.scriptpubkey].value += output.value\n        else:\n            output_dict[output.scriptpubkey] = copy.copy(output)\n    return list(output_dict.values())\n\n\ndef match_script_against_template(script, template, debug=False) -> bool:\n    \"\"\"Returns whether 'script' matches 'template'.\"\"\"\n    if script is None:\n        return False\n    # optionally decode script now:\n    if isinstance(script, (bytes, bytearray)):\n        try:\n            script = [x for x in script_GetOp(script)]\n        except MalformedBitcoinScript:\n            if debug:\n                _logger.debug(f\"malformed script\")\n            return False\n    if debug:\n        _logger.debug(f\"match script against template: {script}\")\n    if len(script) != len(template):\n        if debug:\n            _logger.debug(f\"length mismatch {len(script)} != {len(template)}\")\n        return False\n    for i in range(len(script)):\n        template_item = template[i]\n        script_item = script[i]\n        if OPPushDataGeneric.is_instance(template_item) and template_item.check_data_len(script_item[0]):\n            continue\n        if OPGeneric.is_instance(template_item) and template_item.match(script_item[0]):\n            continue\n        if template_item != script_item[0]:\n            if debug:\n                _logger.debug(f\"item mismatch at position {i}: {template_item} != {script_item[0]}\")\n            return False\n    return True\n\n\ndef get_script_type_from_output_script(scriptpubkey: bytes) -> Optional[str]:\n    if scriptpubkey is None:\n        return None\n    try:\n        decoded = [x for x in script_GetOp(scriptpubkey)]\n    except MalformedBitcoinScript:\n        return None\n    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2PK):\n        return 'p2pk'\n    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2PKH):\n        return 'p2pkh'\n    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2SH):\n        return 'p2sh'\n    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2WPKH):\n        return 'p2wpkh'\n    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2WSH):\n        return 'p2wsh'\n    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2TR):\n        return 'p2tr'\n    return None\n\n\ndef get_address_from_output_script(_bytes: bytes, *, net=None) -> Optional[str]:\n    try:\n        decoded = [x for x in script_GetOp(_bytes)]\n    except MalformedBitcoinScript:\n        return None\n\n    # p2pkh\n    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2PKH):\n        return hash160_to_p2pkh(decoded[2][1], net=net)\n\n    # p2sh\n    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2SH):\n        return hash160_to_p2sh(decoded[1][1], net=net)\n\n    # segwit address (version 0)\n    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_WITNESS_V0):\n        return hash_to_segwit_addr(decoded[1][1], witver=0, net=net)\n\n    # segwit address (version 1-16)\n    future_witness_versions = list(range(opcodes.OP_1, opcodes.OP_16 + 1))\n    for witver, opcode in enumerate(future_witness_versions, start=1):\n        match = [opcode, OPPushDataGeneric(lambda x: 2 <= x <= 40)]\n        if match_script_against_template(decoded, match):\n            return hash_to_segwit_addr(decoded[1][1], witver=witver, net=net)\n\n    return None\n\n\ndef parse_input(vds: BCDataStream) -> TxInput:\n    prevout_hash = vds.read_bytes(32)[::-1]\n    prevout_n = vds.read_uint32()\n    prevout = TxOutpoint(txid=prevout_hash, out_idx=prevout_n)\n    script_sig = vds.read_bytes(vds.read_compact_size())\n    nsequence = vds.read_uint32()\n    return TxInput(prevout=prevout, script_sig=script_sig, nsequence=nsequence)\n\n\ndef parse_witness(vds: BCDataStream, txin: TxInput) -> None:\n    n = vds.read_compact_size()\n    witness_elements = list(vds.read_bytes(vds.read_compact_size()) for i in range(n))\n    txin.witness = construct_witness(witness_elements)\n\n\ndef parse_output(vds: BCDataStream) -> TxOutput:\n    value = vds.read_int64()\n    if value > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN:\n        raise SerializationError('invalid output amount (too large)')\n    if value < 0:\n        raise SerializationError('invalid output amount (negative)')\n    scriptpubkey = vds.read_bytes(vds.read_compact_size())\n    return TxOutput(value=value, scriptpubkey=scriptpubkey)\n\n\n# pay & redeem scripts\n\ndef multisig_script(public_keys: Sequence[str], m: int) -> bytes:\n    n = len(public_keys)\n    assert 1 <= m <= n <= 15, f'm {m}, n {n}'\n    return construct_script([m, *public_keys, n, opcodes.OP_CHECKMULTISIG])\n\n\nclass Transaction:\n    _cached_network_ser: Optional[str]\n\n    def __str__(self):\n        return self.serialize()\n\n    def __init__(self, raw):\n        if raw is None:\n            self._cached_network_ser = None\n        elif isinstance(raw, str):\n            self._cached_network_ser = raw.strip() if raw else None\n            assert is_hex_str(self._cached_network_ser)\n        elif isinstance(raw, (bytes, bytearray)):\n            self._cached_network_ser = raw.hex()\n        else:\n            raise Exception(f\"cannot initialize transaction from {raw}\")\n        self._inputs = None  # type: List[TxInput]\n        self._outputs = None  # type: List[TxOutput]\n        self._locktime = 0\n        self._version = 2\n\n        self._cached_txid = None  # type: Optional[str]\n\n    @property\n    def locktime(self):\n        self.deserialize()\n        return self._locktime\n\n    @locktime.setter\n    def locktime(self, value: int):\n        assert isinstance(value, int), f\"locktime must be int, not {value!r}\"\n        self._locktime = value\n        self.invalidate_ser_cache()\n\n    @property\n    def version(self):\n        self.deserialize()\n        return self._version\n\n    @version.setter\n    def version(self, value):\n        self._version = value\n        self.invalidate_ser_cache()\n\n    def to_json(self) -> dict:\n        d = {\n            'version': self.version,\n            'locktime': self.locktime,\n            'inputs': [txin.to_json() for txin in self.inputs()],\n            'outputs': [txout.to_json() for txout in self.outputs()],\n        }\n        return d\n\n    def inputs(self) -> Sequence[TxInput]:\n        if self._inputs is None:\n            self.deserialize()\n        return self._inputs\n\n    def outputs(self) -> Sequence[TxOutput]:\n        if self._outputs is None:\n            self.deserialize()\n        return self._outputs\n\n    def deserialize(self) -> None:\n        if self._cached_network_ser is None:\n            return\n        if self._inputs is not None:\n            return\n\n        raw_bytes = bfh(self._cached_network_ser)\n        vds = BCDataStream()\n        vds.write(raw_bytes)\n        self._version = vds.read_int32()\n        n_vin = vds.read_compact_size()\n        is_segwit = (n_vin == 0)\n        if is_segwit:\n            marker = vds.read_bytes(1)\n            if marker != b'\\x01':\n                raise SerializationError('invalid txn marker byte: {}'.format(marker))\n            n_vin = vds.read_compact_size()\n        if n_vin < 1:\n            raise SerializationError('tx needs to have at least 1 input')\n        txins = [parse_input(vds) for i in range(n_vin)]\n        n_vout = vds.read_compact_size()\n        if n_vout < 1:\n            raise SerializationError('tx needs to have at least 1 output')\n        self._outputs = [parse_output(vds) for i in range(n_vout)]\n        if is_segwit:\n            for txin in txins:\n                parse_witness(vds, txin)\n        self._inputs = txins  # only expose field after witness is parsed, for sanity\n        self._locktime = vds.read_uint32()\n        if vds.can_read_more():\n            raise SerializationError('extra junk at the end')\n\n    @classmethod\n    def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> bytes:\n        if txin.witness is not None:\n            return txin.witness\n        if txin.is_coinbase_input():\n            return b\"\"\n        assert isinstance(txin, PartialTxInput)\n\n        if not txin.is_segwit():\n            return construct_witness([])\n\n        if estimate_size and hasattr(txin, 'make_witness'):\n            txin.witness_sizehint = len(txin.make_witness(DUMMY_DER_SIG))\n\n        if estimate_size and txin.witness_sizehint is not None:\n            return bytes(txin.witness_sizehint)\n\n        dummy_desc = None\n        if estimate_size:\n            dummy_desc = create_dummy_descriptor_from_address(txin.address)\n        if desc := (txin.script_descriptor or dummy_desc):\n            sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.sigs_ecdsa)\n            if sol.witness is not None:\n                return sol.witness\n            return construct_witness([])\n        raise UnknownTxinType(\"cannot construct witness\")\n\n    @classmethod\n    def input_script(cls, txin: TxInput, *, estimate_size=False) -> bytes:\n        if txin.script_sig is not None:\n            return txin.script_sig\n        if txin.is_coinbase_input():\n            return b\"\"\n        assert isinstance(txin, PartialTxInput)\n\n        if txin.is_p2sh_segwit() and txin.redeem_script:\n            return construct_script([txin.redeem_script])\n        if txin.is_native_segwit():\n            return b\"\"\n\n        dummy_desc = None\n        if estimate_size:\n            dummy_desc = create_dummy_descriptor_from_address(txin.address)\n        if desc := (txin.script_descriptor or dummy_desc):\n            if desc.is_segwit():\n                if redeem_script := desc.expand().redeem_script:\n                    return construct_script([redeem_script])\n                return b\"\"\n            sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.sigs_ecdsa)\n            if sol.script_sig is not None:\n                return sol.script_sig\n            return b\"\"\n        raise UnknownTxinType(\"cannot construct scriptSig\")\n\n    def serialize_preimage(\n        self,\n        txin_index: int,\n        *,\n        sighash: Optional[int] = None,\n        sighash_cache: SighashCache = None,\n    ) -> bytes:\n        nVersion = int.to_bytes(self.version, length=4, byteorder=\"little\", signed=True)\n        nLocktime = int.to_bytes(self.locktime, length=4, byteorder=\"little\", signed=False)\n        inputs = self.inputs()\n        outputs = self.outputs()\n        txin = inputs[txin_index]\n        if isinstance(txin, PartialTxInput):\n            if sighash is None:\n                sighash = txin.sighash\n            if sighash is None:\n                sighash = Sighash.DEFAULT if txin.is_taproot() else Sighash.ALL\n        assert sighash is not None\n        if not Sighash.is_valid(sighash, is_taproot=txin.is_taproot()):\n            raise Exception(f\"SIGHASH_FLAG ({sighash}) not supported!\")\n        if sighash_cache is None:\n            sighash_cache = SighashCache()\n        if txin.is_segwit():\n            if txin.is_taproot():\n                scache = sighash_cache.get_witver1_data_for_tx(self)\n                sighash_epoch = b\"\\x00\"\n                hash_type = int.to_bytes(sighash, length=1, byteorder=\"little\", signed=False)\n                # txdata\n                preimage_txdata = bytearray()\n                preimage_txdata += nVersion\n                preimage_txdata += nLocktime\n                if sighash & 0x80 != Sighash.ANYONECANPAY:\n                    preimage_txdata += scache.sha_prevouts\n                    preimage_txdata += scache.sha_amounts\n                    preimage_txdata += scache.sha_scriptpubkeys\n                    preimage_txdata += scache.sha_sequences\n                if sighash & 3 not in (Sighash.NONE, Sighash.SINGLE):\n                    preimage_txdata += scache.sha_outputs\n                # inputdata\n                preimage_inputdata = bytearray()\n                spend_type = bytes([0])  # (ext_flag * 2) + annex_present\n                preimage_inputdata += spend_type\n                if sighash & 0x80 == Sighash.ANYONECANPAY:\n                    preimage_inputdata += txin.prevout.serialize_to_network()\n                    preimage_inputdata += int.to_bytes(txin.value_sats(), length=8, byteorder=\"little\", signed=False)\n                    preimage_inputdata += var_int(len(txin.scriptpubkey)) + txin.scriptpubkey\n                    preimage_inputdata += int.to_bytes(txin.nsequence, length=4, byteorder=\"little\", signed=False)\n                else:\n                    preimage_inputdata += int.to_bytes(txin_index, length=4, byteorder=\"little\", signed=False)\n                # TODO sha_annex\n                # outputdata\n                preimage_outputdata = bytearray()\n                if sighash & 3 == Sighash.SINGLE:\n                    try:\n                        txout = outputs[txin_index]\n                    except IndexError:\n                        raise Exception(\"Using SIGHASH_SINGLE without a corresponding output\") from None\n                    # note: we could cache this to avoid some potential DOS vectors:\n                    preimage_outputdata += sha256(txout.serialize_to_network())\n                return bytes(sighash_epoch + hash_type + preimage_txdata + preimage_inputdata + preimage_outputdata)\n            else:  # segwit (witness v0)\n                scache = sighash_cache.get_witver0_data_for_tx(self)\n                if not (sighash & Sighash.ANYONECANPAY):\n                    hashPrevouts = scache.hashPrevouts\n                else:\n                    hashPrevouts = bytes(32)\n                if not (sighash & Sighash.ANYONECANPAY) and (sighash & 0x1f) != Sighash.SINGLE and (sighash & 0x1f) != Sighash.NONE:\n                    hashSequence = scache.hashSequence\n                else:\n                    hashSequence = bytes(32)\n                if (sighash & 0x1f) != Sighash.SINGLE and (sighash & 0x1f) != Sighash.NONE:\n                    hashOutputs = scache.hashOutputs\n                elif (sighash & 0x1f) == Sighash.SINGLE and txin_index < len(outputs):\n                    # note: we could cache this to avoid some potential DOS vectors:\n                    hashOutputs = sha256d(outputs[txin_index].serialize_to_network())\n                else:\n                    hashOutputs = bytes(32)\n                outpoint = txin.prevout.serialize_to_network()\n                preimage_script = txin.get_scriptcode_for_sighash()\n                scriptCode = var_int(len(preimage_script)) + preimage_script\n                amount = int.to_bytes(txin.value_sats(), length=8, byteorder=\"little\", signed=False)\n                nSequence = int.to_bytes(txin.nsequence, length=4, byteorder=\"little\", signed=False)\n                nHashType = int.to_bytes(sighash, length=4, byteorder=\"little\", signed=False)\n                preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType\n                return preimage\n        else:  # legacy sighash (pre-segwit)\n            if sighash != Sighash.ALL:\n                raise Exception(f\"SIGHASH_FLAG ({sighash}) not supported! (for legacy sighash)\")\n            preimage_script = txin.get_scriptcode_for_sighash()\n            txins = var_int(len(inputs)) + b\"\".join(\n                txin.serialize_to_network(script_sig=preimage_script if txin_index==k else b\"\")\n                for k, txin in enumerate(inputs))\n            txouts = var_int(len(outputs)) + b\"\".join(o.serialize_to_network() for o in outputs)\n            nHashType = int.to_bytes(sighash, length=4, byteorder=\"little\", signed=False)\n            preimage = nVersion + txins + txouts + nLocktime + nHashType\n            return preimage\n        raise Exception(\"should not reach this\")\n\n    def verify_sig_for_txin(\n        self,\n        *,\n        txin_index: int,\n        pubkey_bytes: bytes,\n        sig: bytes,\n        sighash_cache: SighashCache = None,\n    ) -> bool:\n        txin = self.inputs()[txin_index]\n        if txin.is_taproot():\n            raise Exception(\"not implemented\")  # TODO\n        else:\n            der_sig, sighash = sig[:-1], sig[-1]\n            pre_hash = self.serialize_preimage(txin_index, sighash=sighash, sighash_cache=sighash_cache)\n            pubkey = ecc.ECPubkey(pubkey_bytes)\n            msg_hash = sha256d(pre_hash)\n            sig64 = ecc.ecdsa_sig64_from_der_sig(der_sig)\n            return pubkey.ecdsa_verify(sig64, msg_hash)\n\n    def is_segwit(self, *, guess_for_address=False):\n        return any(txin.is_segwit(guess_for_address=guess_for_address)\n                   for txin in self.inputs())\n\n    def invalidate_ser_cache(self):\n        self._cached_network_ser = None\n        self._cached_txid = None\n\n    def serialize(self) -> str:\n        if not self._cached_network_ser:\n            self._cached_network_ser = self.serialize_to_network(estimate_size=False, include_sigs=True)\n        return self._cached_network_ser\n\n    def serialize_as_bytes(self) -> bytes:\n        return bfh(self.serialize())\n\n    def serialize_to_network(self, *, estimate_size=False, include_sigs=True, force_legacy=False) -> str:\n        \"\"\"Serialize the transaction as used on the Bitcoin network, into hex.\n        `include_sigs` signals whether to include scriptSigs and witnesses.\n        `force_legacy` signals to use the pre-segwit format\n        note: (not include_sigs) implies force_legacy\n        \"\"\"\n        self.deserialize()\n        nVersion = int.to_bytes(self.version, length=4, byteorder=\"little\", signed=True).hex()\n        nLocktime = int.to_bytes(self.locktime, length=4, byteorder=\"little\", signed=False).hex()\n        inputs = self.inputs()\n        outputs = self.outputs()\n\n        def create_script_sig(txin: TxInput) -> bytes:\n            if include_sigs:\n                script_sig = self.input_script(txin, estimate_size=estimate_size)\n                return script_sig\n            return b\"\"\n        txins = var_int(len(inputs)).hex() + ''.join(\n            txin.serialize_to_network(script_sig=create_script_sig(txin)).hex()\n            for txin in inputs)\n        txouts = var_int(len(outputs)).hex() + ''.join(o.serialize_to_network().hex() for o in outputs)\n\n        use_segwit_ser_for_estimate_size = estimate_size and self.is_segwit(guess_for_address=True)\n        use_segwit_ser_for_actual_use = not estimate_size and self.is_segwit()\n        use_segwit_ser = use_segwit_ser_for_estimate_size or use_segwit_ser_for_actual_use\n        if include_sigs and not force_legacy and use_segwit_ser:\n            marker = '00'\n            flag = '01'\n            witness = ''.join(self.serialize_witness(x, estimate_size=estimate_size).hex() for x in inputs)\n            return nVersion + marker + flag + txins + txouts + witness + nLocktime\n        else:\n            return nVersion + txins + txouts + nLocktime\n\n    def to_qr_data(self) -> Tuple[str, bool]:\n        \"\"\"Returns (serialized_tx, is_complete). The tx is serialized to be put inside a QR code. No side-effects.\n        As space in a QR code is limited, some data might have to be omitted. This is signalled via is_complete=False.\n        \"\"\"\n        is_complete = True\n        tx = copy.deepcopy(self)  # make copy as we mutate tx\n        if isinstance(tx, PartialTransaction):\n            # this makes QR codes a lot smaller (or just possible in the first place!)\n            # note: will not apply if all inputs are taproot, due to new sighash.\n            tx.convert_all_utxos_to_witness_utxos()\n            is_complete = False\n        tx_bytes = tx.serialize_as_bytes()\n        return base_encode(tx_bytes, base=43), is_complete\n\n    def txid(self) -> Optional[str]:\n        if self._cached_txid is None:\n            self.deserialize()\n            all_segwit = all(txin.is_segwit() for txin in self.inputs())\n            if not all_segwit and not self.is_complete():\n                return None\n            try:\n                ser = self.serialize_to_network(force_legacy=True)\n            except UnknownTxinType:\n                # we might not know how to construct scriptSig for some scripts\n                return None\n            self._cached_txid = sha256d(bfh(ser))[::-1].hex()\n        return self._cached_txid\n\n    def wtxid(self) -> Optional[str]:\n        self.deserialize()\n        if not self.is_complete():\n            return None\n        try:\n            ser = self.serialize_to_network()\n        except UnknownTxinType:\n            # we might not know how to construct scriptSig/witness for some scripts\n            return None\n        return sha256d(bfh(ser))[::-1].hex()\n\n    def add_info_from_wallet(self, wallet: 'Abstract_Wallet', **kwargs) -> None:\n        # populate prev_txs\n        for txin in self.inputs():\n            wallet.add_input_info(txin)\n\n    async def add_info_from_network(\n        self,\n        network: Optional['Network'],\n        *,\n        ignore_network_issues: bool = True,\n        progress_cb: Callable[[TxinDataFetchProgress], None] = None,\n        timeout=None,\n    ) -> None:\n        \"\"\"note: it is recommended to call add_info_from_wallet first, as this can save some network requests\"\"\"\n        from .interface import NetworkException\n        if not self.is_missing_info_from_network():\n            return\n        if progress_cb is None:\n            progress_cb = lambda *args, **kwargs: None\n        num_tasks_done = 0\n        num_tasks_total = 0\n        has_errored = False\n        has_finished = False\n\n        async def add_info_to_txin(txin: TxInput):\n            nonlocal num_tasks_done, has_errored\n            progress_cb(TxinDataFetchProgress(num_tasks_done, num_tasks_total, has_errored, has_finished))\n            success = await txin.add_info_from_network(\n                network=network,\n                ignore_network_issues=ignore_network_issues,\n                timeout=timeout,\n            )\n            if success:\n                num_tasks_done += 1\n            else:\n                has_errored = True\n            progress_cb(TxinDataFetchProgress(num_tasks_done, num_tasks_total, has_errored, has_finished))\n\n        # schedule a network task for each txin\n        try:\n            async with OldTaskGroup() as group:\n                for txin in self.inputs():\n                    if txin.utxo is None:\n                        num_tasks_total += 1\n                        await group.spawn(add_info_to_txin(txin=txin))\n        except Exception as e:\n            has_errored = True\n            _logger.error(f\"tx.add_info_from_network() got exc: {e!r}\")\n            if isinstance(e, NetworkException) and not ignore_network_issues:\n                raise\n        finally:\n            has_finished = True\n            progress_cb(TxinDataFetchProgress(num_tasks_done, num_tasks_total, has_errored, has_finished))\n\n    def is_missing_info_from_network(self) -> bool:\n        return any(txin.utxo is None for txin in self.inputs())\n\n    def add_info_from_wallet_and_network(\n        self, *, wallet: 'Abstract_Wallet', show_error: Callable[[str], None],\n    ) -> bool:\n        \"\"\"Returns whether successful.\n        note: This is sort of a legacy hack... doing network requests in non-async code.\n              Relatedly, this should *not* be called from the network thread.\n        \"\"\"\n        # note side-effect: tx is being mutated\n        from .network import NetworkException, Network\n        self.add_info_from_wallet(wallet)\n        try:\n            if self.is_missing_info_from_network():\n                Network.run_from_another_thread(\n                    self.add_info_from_network(wallet.network, ignore_network_issues=False))\n        except NetworkException as e:\n            show_error(repr(e))\n            return False\n        return True\n\n    def get_time_based_relative_locktime(self) -> Optional[int]:\n        if self.version < 2:\n            return None\n        locktimes = list(filter(None, [txin.get_time_based_relative_locktime() for txin in self.inputs()]))\n        return max(locktimes) if locktimes else None\n\n    def get_block_based_relative_locktime(self) -> Optional[int]:\n        if self.version < 2:\n            return None\n        locktimes = list(filter(None, [txin.get_block_based_relative_locktime() for txin in self.inputs()]))\n        return max(locktimes) if locktimes else None\n\n    def is_rbf_enabled(self) -> bool:\n        \"\"\"Whether the tx explicitly signals BIP-0125 replace-by-fee.\"\"\"\n        return any([txin.nsequence < 0xffffffff - 1 for txin in self.inputs()])\n\n    def estimated_size(self) -> int:\n        \"\"\"Return an estimated virtual tx size in vbytes.\n        BIP-0141 defines 'Virtual transaction size' to be weight/4 rounded up.\n        This definition is only for humans, and has little meaning otherwise.\n        If we wanted sub-byte precision, fee calculation should use transaction\n        weights, but for simplicity we approximate that with (virtual_size)x4.\n        note: while we try to estimate as close to the true value as possible,\n            whenever that's not possible, we should over-estimate. E.g. ecdsa DER sig\n            sizes can be 71 or 72 bytes (even 73 though that is non-standard).\n            Over-estimating is preferred as the typical use-case is the user selecting\n            a target_feerate, and the code calculating abs fees as target_feerate*est_size.\n            If we over-estimate est_size there, that means the final true_feerate is going to\n            be higher than target_feerate, which is desirable especially near the min_relay_fee.\n        \"\"\"\n        weight = self.estimated_weight()\n        return self.virtual_size_from_weight(weight)\n\n    @classmethod\n    def estimated_input_weight(cls, txin: TxInput, is_segwit_tx: bool) -> int:\n        \"\"\"Return an estimate of serialized input weight in weight units.\"\"\"\n        script_sig = cls.input_script(txin, estimate_size=True)\n        input_size = len(txin.serialize_to_network(script_sig=script_sig))\n\n        if txin.is_segwit(guess_for_address=True):\n            witness_size = len(cls.serialize_witness(txin, estimate_size=True))\n        else:\n            witness_size = 1 if is_segwit_tx else 0\n\n        return 4 * input_size + witness_size\n\n    @classmethod\n    def estimated_output_size_for_address(cls, address: str) -> int:\n        \"\"\"Return an estimate of serialized output size in bytes.\"\"\"\n        script = bitcoin.address_to_script(address)\n        return cls.estimated_output_size_for_script(script)\n\n    @classmethod\n    def estimated_output_size_for_script(cls, script: bytes) -> int:\n        \"\"\"Return an estimate of serialized output size in bytes.\"\"\"\n        # 8 byte value + varint script len + script\n        script_len = len(script)\n        var_int_len = len(var_int(script_len))\n        return 8 + var_int_len + script_len\n\n    @classmethod\n    def virtual_size_from_weight(cls, weight: int) -> int:\n        return weight // 4 + (weight % 4 > 0)\n\n    @classmethod\n    def satperbyte_from_satperkw(cls, feerate_kw):\n        \"\"\"Converts feerate from sat/kw to sat/vbyte.\"\"\"\n        return feerate_kw * 4 / 1000\n\n    def estimated_total_size(self):\n        \"\"\"Return an estimated total transaction size in bytes.\"\"\"\n        if not self.is_complete() or self._cached_network_ser is None:\n            return len(self.serialize_to_network(estimate_size=True)) // 2\n        else:\n            return len(self._cached_network_ser) // 2  # ASCII hex string\n\n    def estimated_witness_size(self):\n        \"\"\"Return an estimate of witness size in bytes.\"\"\"\n        estimate = not self.is_complete()\n        if not self.is_segwit(guess_for_address=estimate):\n            return 0\n        inputs = self.inputs()\n        witness = b\"\".join(self.serialize_witness(x, estimate_size=estimate) for x in inputs)\n        witness_size = len(witness) + 2  # include marker and flag\n        return witness_size\n\n    def estimated_base_size(self):\n        \"\"\"Return an estimated base transaction size in bytes.\"\"\"\n        return self.estimated_total_size() - self.estimated_witness_size()\n\n    def estimated_weight(self):\n        \"\"\"Return an estimate of transaction weight.\"\"\"\n        total_tx_size = self.estimated_total_size()\n        base_tx_size = self.estimated_base_size()\n        return 3 * base_tx_size + total_tx_size\n\n    def is_complete(self) -> bool:\n        return True\n\n    def get_output_idxs_from_scriptpubkey(self, script: bytes) -> Set[int]:\n        \"\"\"Returns the set indices of outputs with given script.\"\"\"\n        assert isinstance(script, bytes)\n        # build cache if there isn't one yet\n        # note: can become stale and return incorrect data\n        #       if the tx is modified later; that's out of scope.\n        if not hasattr(self, '_script_to_output_idx'):\n            d = defaultdict(set)\n            for output_idx, o in enumerate(self.outputs()):\n                o_script = o.scriptpubkey\n                d[o_script].add(output_idx)\n            self._script_to_output_idx = d\n        return set(self._script_to_output_idx[script])  # copy\n\n    def get_output_idxs_from_address(self, addr: str) -> Set[int]:\n        script = bitcoin.address_to_script(addr)\n        return self.get_output_idxs_from_scriptpubkey(script)\n\n    def output_value_for_address(self, addr):\n        # assumes exactly one output has that address\n        for o in self.outputs():\n            if o.address == addr:\n                return o.value\n        else:\n            raise Exception('output not found', addr)\n\n    def input_value(self) -> int:\n        input_values = [txin.value_sats() for txin in self.inputs()]\n        if any([val is None for val in input_values]):\n            raise MissingTxInputAmount()\n        return sum(input_values)\n\n    def output_value(self) -> int:\n        return sum(o.value for o in self.outputs())\n\n    def get_fee(self) -> Optional[int]:\n        try:\n            return self.input_value() - self.output_value()\n        except MissingTxInputAmount:\n            return None\n\n    def get_input_idx_that_spent_prevout(self, prevout: TxOutpoint) -> Optional[int]:\n        # build cache if there isn't one yet\n        # note: can become stale and return incorrect data\n        #       if the tx is modified later; that's out of scope.\n        if not hasattr(self, '_prevout_to_input_idx'):\n            d = {}  # type: Dict[TxOutpoint, int]\n            for i, txin in enumerate(self.inputs()):\n                d[txin.prevout] = i\n            self._prevout_to_input_idx = d\n        idx = self._prevout_to_input_idx.get(prevout)\n        if idx is not None:\n            assert self.inputs()[idx].prevout == prevout\n        return idx\n\n\ndef convert_raw_tx_to_hex(raw: Union[str, bytes]) -> str:\n    \"\"\"Sanitizes tx-describing input (hex/base43/base64) into\n    raw tx hex string.\"\"\"\n    if not raw:\n        raise ValueError(\"empty string\")\n    raw_unstripped = raw\n    if isinstance(raw, str):\n        # remove all whitespace characters, anywhere, for convenience\n        # - leading/trailing whitespaces are quite common for user-input\n        # - newlines in the middle can also happen, e.g. when copying a raw tx from a pdf\n        # note: we don't do this for bytes-like inputs, as whitespace-looking bytes can appear\n        #       anywhere in a raw tx. Even leading/trailing pseudo-whitespace: consider that\n        #       the nVersion or the nLocktime might contain e.g. \"0a\" bytes\n        #       consider:  \"\\n\".encode().hex() == \"0a\"\n        #       For str, this is a non-issue and safe to do.\n        raw = re.sub(r'\\s', '', raw)\n    # try hex\n    try:\n        return binascii.unhexlify(raw).hex()\n    except Exception:\n        pass\n    # try base43\n    try:\n        return base_decode(raw, base=43).hex()\n    except Exception:\n        pass\n    # try base64\n    if raw[0:6] in ('cHNidP', b'cHNidP'):  # base64 psbt\n        try:\n            return base64.b64decode(raw, validate=True).hex()\n        except Exception:\n            pass\n    # raw bytes (do not strip whitespaces in this case)\n    if isinstance(raw_unstripped, bytes):\n        return raw_unstripped.hex()\n    raise ValueError(f\"failed to recognize transaction encoding for txt: {raw[:30]}...\")\n\n\ndef tx_from_any(raw: Union[str, bytes], *,\n                deserialize: bool = True) -> Union['PartialTransaction', 'Transaction']:\n    if isinstance(raw, bytearray):\n        raw = bytes(raw)\n    raw = convert_raw_tx_to_hex(raw)\n    try:\n        return PartialTransaction.from_raw_psbt(raw)\n    except BadHeaderMagic:\n        if raw[:10] == b'EPTF\\xff'.hex():\n            raise SerializationError(\"Partial transactions generated with old Electrum versions \"\n                                     \"(< 4.0) are no longer supported. Please upgrade Electrum on \"\n                                     \"the other machine where this transaction was created.\")\n    try:\n        tx = Transaction(raw)\n        if deserialize:\n            tx.deserialize()\n        return tx\n    except Exception as e:\n        raise SerializationError(f\"Failed to recognise tx encoding, or to parse transaction. \"\n                                 f\"raw: {raw[:30]!r}...\") from e\n\n\nclass PSBTGlobalType(IntEnum):\n    UNSIGNED_TX = 0\n    XPUB = 1\n    VERSION = 0xFB\n\n\nclass PSBTInputType(IntEnum):\n    NON_WITNESS_UTXO = 0\n    WITNESS_UTXO = 1\n    PARTIAL_SIG = 2\n    SIGHASH_TYPE = 3\n    REDEEM_SCRIPT = 4\n    WITNESS_SCRIPT = 5\n    BIP32_DERIVATION = 6\n    FINAL_SCRIPTSIG = 7\n    FINAL_SCRIPTWITNESS = 8\n    TAP_KEY_SIG = 0x13\n    TAP_MERKLE_ROOT = 0x18\n    SLIP19_OWNERSHIP_PROOF = 0x19\n\n\nclass PSBTOutputType(IntEnum):\n    REDEEM_SCRIPT = 0\n    WITNESS_SCRIPT = 1\n    BIP32_DERIVATION = 2\n\n\n# Serialization/deserialization tools\ndef deser_compact_size(f) -> Optional[int]:\n    # note: ~inverse of bitcoin.var_int\n    try:\n        nit = f.read(1)[0]\n    except IndexError:\n        return None     # end of file\n\n    if nit == 253:\n        nit = struct.unpack(\"<H\", f.read(2))[0]\n    elif nit == 254:\n        nit = struct.unpack(\"<I\", f.read(4))[0]\n    elif nit == 255:\n        nit = struct.unpack(\"<Q\", f.read(8))[0]\n    return nit\n\n\nclass PSBTSection:\n\n    def _populate_psbt_fields_from_fd(self, fd=None):\n        if not fd: return\n\n        while True:\n            try:\n                key_type, key, val = self.get_next_kv_from_fd(fd)\n            except StopIteration:\n                break\n            self.parse_psbt_section_kv(key_type, key, val)\n\n    @classmethod\n    def get_next_kv_from_fd(cls, fd) -> Tuple[int, bytes, bytes]:\n        key_size = deser_compact_size(fd)\n        if key_size == 0:\n            raise StopIteration()\n        if key_size is None:\n            raise UnexpectedEndOfStream()\n\n        full_key = fd.read(key_size)\n        key_type, key = cls.get_keytype_and_key_from_fullkey(full_key)\n\n        val_size = deser_compact_size(fd)\n        if val_size is None: raise UnexpectedEndOfStream()\n        val = fd.read(val_size)\n\n        return key_type, key, val\n\n    @classmethod\n    def create_psbt_writer(cls, fd):\n        def wr(key_type: int, val: bytes, key: bytes = b''):\n            full_key = cls.get_fullkey_from_keytype_and_key(key_type, key)\n            fd.write(var_int(len(full_key)))  # key_size\n            fd.write(full_key)  # key\n            fd.write(var_int(len(val)))  # val_size\n            fd.write(val)  # val\n        return wr\n\n    @classmethod\n    def get_keytype_and_key_from_fullkey(cls, full_key: bytes) -> Tuple[int, bytes]:\n        with io.BytesIO(full_key) as key_stream:\n            key_type = deser_compact_size(key_stream)\n            if key_type is None: raise UnexpectedEndOfStream()\n            key = key_stream.read()\n        return key_type, key\n\n    @classmethod\n    def get_fullkey_from_keytype_and_key(cls, key_type: int, key: bytes) -> bytes:\n        key_type_bytes = var_int(key_type)\n        return key_type_bytes + key\n\n    def _serialize_psbt_section(self, fd):\n        wr = self.create_psbt_writer(fd)\n        self.serialize_psbt_section_kvs(wr)\n        fd.write(b'\\x00')  # section-separator\n\n    def parse_psbt_section_kv(self, kt: int, key: bytes, val: bytes) -> None:\n        raise NotImplementedError()  # implemented by subclasses\n\n    def serialize_psbt_section_kvs(self, wr) -> None:\n        raise NotImplementedError()  # implemented by subclasses\n\n\nclass PartialTxInput(TxInput, PSBTSection):\n    def __init__(self, *args, **kwargs):\n        TxInput.__init__(self, *args, **kwargs)\n        self._witness_utxo = None  # type: Optional[TxOutput]\n        self.sigs_ecdsa = {}  # type: Dict[bytes, bytes]  # pubkey -> sig\n        self.tap_key_sig = None  # type: Optional[bytes]  # sig for taproot key-path-spending\n        self.sighash = None  # type: Optional[int]  # note: wrong abstraction level. should be per-signature\n        self.bip32_paths = {}  # type: Dict[bytes, Tuple[bytes, Sequence[int]]]  # pubkey -> (xpub_fingerprint, path)\n        self.redeem_script = None  # type: Optional[bytes]\n        self.witness_script = None  # type: Optional[bytes]\n        self.tap_merkle_root = None  # type: Optional[bytes]\n        self.slip_19_ownership_proof = None  # type: Optional[bytes]\n        self._unknown = {}  # type: Dict[bytes, bytes]\n\n        self._script_descriptor = None  # type: Optional[Descriptor]\n        self.is_mine = False  # type: bool  # whether the wallet considers the input to be ismine\n        self._trusted_value_sats = None  # type: Optional[int]\n        self._trusted_address = None  # type: Optional[str]\n        self._is_p2sh_segwit = None  # type: Optional[bool]  # None means unknown\n        self._is_native_segwit = None  # type: Optional[bool]  # None means unknown\n        self.witness_sizehint = None  # type: Optional[int]  # byte size of serialized complete witness, for tx size est\n\n    @property\n    def witness_utxo(self):\n        return self._witness_utxo\n\n    @witness_utxo.setter\n    def witness_utxo(self, value: Optional[TxOutput]):\n        self.validate_data(witness_utxo=value)\n        self._witness_utxo = value\n\n    @property\n    def pubkeys(self) -> Set[bytes]:\n        if desc := self.script_descriptor:\n            return desc.get_all_pubkeys()\n        return set()\n\n    @property\n    def script_descriptor(self):\n        return self._script_descriptor\n\n    @script_descriptor.setter\n    def script_descriptor(self, desc: Optional[Descriptor]):\n        self._script_descriptor = desc\n        if desc:\n            if self.redeem_script is None:\n                self.redeem_script = desc.expand().redeem_script\n            if self.witness_script is None:\n                self.witness_script = desc.expand().witness_script\n\n    def to_json(self):\n        d = super().to_json()\n        d.update({\n            'height': self.block_height,\n            'value_sats': self.value_sats(),\n            'address': self.address,\n            'desc': self.script_descriptor.to_string() if self.script_descriptor else None,\n            'utxo': str(self.utxo) if self.utxo else None,\n            'witness_utxo': self.witness_utxo.serialize_to_network().hex() if self.witness_utxo else None,\n            'sighash': self.sighash,\n            'redeem_script': self.redeem_script.hex() if self.redeem_script else None,\n            'witness_script': self.witness_script.hex() if self.witness_script else None,\n            'sigs_ecdsa': {pubkey.hex(): sig.hex() for pubkey, sig in self.sigs_ecdsa.items()},\n            'tap_key_sig': self.tap_key_sig.hex() if self.tap_key_sig else None,\n            'tap_merkle_root': self.tap_merkle_root.hex() if self.tap_merkle_root else None,\n            'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path))\n                            for pubkey, (xfp, path) in self.bip32_paths.items()},\n            'slip_19_ownership_proof': self.slip_19_ownership_proof.hex() if self.slip_19_ownership_proof else None,\n            'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()},\n        })\n        return d\n\n    @classmethod\n    def from_txin(cls, txin: TxInput, *, strip_witness: bool = True) -> 'PartialTxInput':\n        # FIXME: if strip_witness is True, res.is_segwit() will return False,\n        # and res.estimated_size() will return an incorrect value. These methods\n        # will return the correct values after we call add_input_info(). (see dscancel and bump_fee)\n        # This is very fragile: the value returned by estimate_size() depends on the calling order.\n        res = PartialTxInput(prevout=txin.prevout,\n                             script_sig=None if strip_witness else txin.script_sig,\n                             nsequence=txin.nsequence,\n                             witness=None if strip_witness else txin.witness,\n                             is_coinbase_output=txin.is_coinbase_output())\n        res.utxo = txin.utxo\n        return res\n\n    def validate_data(\n        self,\n        *,\n        for_signing=False,\n        # allow passing provisional fields for 'self', before setting them:\n        utxo: Optional[Transaction] = None,\n        witness_utxo: Optional[TxOutput] = None,\n    ) -> None:\n        utxo = utxo or self.utxo\n        witness_utxo = witness_utxo or self.witness_utxo\n        if utxo:\n            if self.prevout.txid.hex() != utxo.txid():\n                raise PSBTInputConsistencyFailure(f\"PSBT input validation: \"\n                                                  f\"If a non-witness UTXO is provided, its hash must match the hash specified in the prevout\")\n            if witness_utxo:\n                if utxo.outputs()[self.prevout.out_idx] != witness_utxo:\n                    raise PSBTInputConsistencyFailure(f\"PSBT input validation: \"\n                                                      f\"If both non-witness UTXO and witness UTXO are provided, they must be consistent\")\n        # The following test is disabled, so we are willing to sign non-segwit inputs\n        # without verifying the input amount. This means, given a maliciously modified PSBT,\n        # for non-segwit inputs, we might end up burning coins as miner fees.\n        if for_signing and False:\n            if not self.is_segwit() and witness_utxo:\n                raise PSBTInputConsistencyFailure(f\"PSBT input validation: \"\n                                                  f\"If a witness UTXO is provided, no non-witness signature may be created\")\n        if self.redeem_script and self.address:\n            addr = hash160_to_p2sh(hash_160(self.redeem_script))\n            if self.address != addr:\n                raise PSBTInputConsistencyFailure(f\"PSBT input validation: \"\n                                                  f\"If a redeemScript is provided, the scriptPubKey must be for that redeemScript\")\n        if self.witness_script:\n            if self.redeem_script:\n                if self.redeem_script != bitcoin.p2wsh_nested_script(self.witness_script):\n                    raise PSBTInputConsistencyFailure(f\"PSBT input validation: \"\n                                                      f\"If a witnessScript is provided, the redeemScript must be for that witnessScript\")\n            elif self.address:\n                if self.address != bitcoin.script_to_p2wsh(self.witness_script):\n                    raise PSBTInputConsistencyFailure(f\"PSBT input validation: \"\n                                                      f\"If a witnessScript is provided, the scriptPubKey must be for that witnessScript\")\n\n    def parse_psbt_section_kv(self, kt, key, val):\n        try:\n            kt = PSBTInputType(kt)\n        except ValueError:\n            pass  # unknown type\n        if DEBUG_PSBT_PARSING: print(f\"{repr(kt)} {key.hex()} {val.hex()}\")\n        if kt == PSBTInputType.NON_WITNESS_UTXO:\n            if self.utxo is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            self.utxo = Transaction(val)\n            self.utxo.deserialize()\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        elif kt == PSBTInputType.WITNESS_UTXO:\n            if self.witness_utxo is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            self.witness_utxo = TxOutput.from_network_bytes(val)\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        elif kt == PSBTInputType.PARTIAL_SIG:\n            if key in self.sigs_ecdsa:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            if len(key) not in (33, 65):\n                raise SerializationError(f\"key for {repr(kt)} has unexpected length: {len(key)}\")\n            self.sigs_ecdsa[key] = val\n        elif kt == PSBTInputType.TAP_KEY_SIG:\n            if self.tap_key_sig is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            if len(val) not in (64, 65):\n                raise SerializationError(f\"value for {repr(kt)} has unexpected length: {len(val)}\")\n            self.tap_key_sig = val\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        elif kt == PSBTInputType.TAP_MERKLE_ROOT:\n            if self.tap_merkle_root is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            if len(val) != 32:\n                raise SerializationError(f\"value for {repr(kt)} has unexpected length: {len(val)}\")\n            self.tap_merkle_root = val\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        elif kt == PSBTInputType.SIGHASH_TYPE:\n            if self.sighash is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            if len(val) != 4:\n                raise SerializationError(f\"value for {repr(kt)} has unexpected length: {len(val)}\")\n            self.sighash = struct.unpack(\"<I\", val)[0]\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        elif kt == PSBTInputType.BIP32_DERIVATION:\n            if key in self.bip32_paths:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            if len(key) not in (33, 65):\n                raise SerializationError(f\"key for {repr(kt)} has unexpected length: {len(key)}\")\n            self.bip32_paths[key] = unpack_bip32_root_fingerprint_and_int_path(val)\n        elif kt == PSBTInputType.REDEEM_SCRIPT:\n            if self.redeem_script is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            self.redeem_script = val\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        elif kt == PSBTInputType.WITNESS_SCRIPT:\n            if self.witness_script is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            self.witness_script = val\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        elif kt == PSBTInputType.FINAL_SCRIPTSIG:\n            if self.script_sig is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            self.script_sig = val\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        elif kt == PSBTInputType.FINAL_SCRIPTWITNESS:\n            if self.witness is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            self.witness = val\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        elif kt == PSBTInputType.SLIP19_OWNERSHIP_PROOF:\n            if self.slip_19_ownership_proof is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            self.slip_19_ownership_proof = val\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        else:\n            full_key = self.get_fullkey_from_keytype_and_key(kt, key)\n            if full_key in self._unknown:\n                raise SerializationError(f'duplicate key. PSBT input key for unknown type: {full_key}')\n            self._unknown[full_key] = val\n\n    def serialize_psbt_section_kvs(self, wr):\n        if self.witness_utxo:\n            wr(PSBTInputType.WITNESS_UTXO, self.witness_utxo.serialize_to_network())\n        if self.utxo:\n            wr(PSBTInputType.NON_WITNESS_UTXO, bfh(self.utxo.serialize_to_network(include_sigs=True)))\n        for pk, val in sorted(self.sigs_ecdsa.items()):\n            wr(PSBTInputType.PARTIAL_SIG, val, pk)\n        if self.tap_key_sig is not None:\n            wr(PSBTInputType.TAP_KEY_SIG, self.tap_key_sig)\n        if self.tap_merkle_root is not None:\n            wr(PSBTInputType.TAP_MERKLE_ROOT, self.tap_merkle_root)\n        if self.sighash is not None:\n            wr(PSBTInputType.SIGHASH_TYPE, struct.pack('<I', self.sighash))\n        if self.redeem_script is not None:\n            wr(PSBTInputType.REDEEM_SCRIPT, self.redeem_script)\n        if self.witness_script is not None:\n            wr(PSBTInputType.WITNESS_SCRIPT, self.witness_script)\n        for k in sorted(self.bip32_paths):\n            packed_path = pack_bip32_root_fingerprint_and_int_path(*self.bip32_paths[k])\n            wr(PSBTInputType.BIP32_DERIVATION, packed_path, k)\n        if self.script_sig is not None:\n            wr(PSBTInputType.FINAL_SCRIPTSIG, self.script_sig)\n        if self.witness is not None:\n            wr(PSBTInputType.FINAL_SCRIPTWITNESS, self.witness)\n        if self.slip_19_ownership_proof:\n            wr(PSBTInputType.SLIP19_OWNERSHIP_PROOF, self.slip_19_ownership_proof)\n        for full_key, val in sorted(self._unknown.items()):\n            key_type, key = self.get_keytype_and_key_from_fullkey(full_key)\n            wr(key_type, val, key=key)\n\n    def value_sats(self) -> Optional[int]:\n        if (val := super().value_sats()) is not None:\n            return val\n        if self._trusted_value_sats is not None:\n            return self._trusted_value_sats\n        if self.witness_utxo:\n            return self.witness_utxo.value\n        return None\n\n    @property\n    def address(self) -> Optional[str]:\n        if (addr := super().address) is not None:\n            return addr\n        if self._trusted_address is not None:\n            return self._trusted_address\n        if self.witness_utxo:\n            return self.witness_utxo.address\n        return None\n\n    @property\n    def scriptpubkey(self) -> Optional[bytes]:\n        if (spk := super().scriptpubkey) is not None:\n            return spk\n        if self._trusted_address is not None:\n            return bitcoin.address_to_script(self._trusted_address)\n        if self.witness_utxo:\n            return self.witness_utxo.scriptpubkey\n        return None\n\n    def is_complete(self) -> bool:\n        if self.script_sig is not None and self.witness is not None:\n            return True\n        if self.is_coinbase_input():\n            return True\n        if self.script_sig is not None and not self.is_segwit():\n            return True\n        if desc := self.script_descriptor:\n            try:\n                desc.satisfy(allow_dummy=False, sigdata=self.sigs_ecdsa)\n            except MissingSolutionPiece:\n                pass\n            else:\n                return True\n        return False\n\n    def get_satisfaction_progress(self) -> Tuple[int, int]:\n        if desc := self.script_descriptor:\n            return desc.get_satisfaction_progress(sigdata=self.sigs_ecdsa)\n        return 0, 0\n\n    def finalize(self) -> None:\n        def clear_fields_when_finalized():\n            # BIP-174: \"All other data except the UTXO and unknown fields in the\n            #           input key-value map should be cleared from the PSBT\"\n            self.sigs_ecdsa = {}\n            self.tap_key_sig = None\n            self.tap_merkle_root = None\n            self.sighash = None\n            self.bip32_paths = {}\n            self.redeem_script = None\n            # FIXME: side effect interferes with make_witness\n            # self.witness_script = None\n\n        if self.script_sig is not None and self.witness is not None:\n            clear_fields_when_finalized()\n            return  # already finalized\n        if self.is_complete():\n            self.script_sig = Transaction.input_script(self)\n            self.witness = Transaction.serialize_witness(self)\n            clear_fields_when_finalized()\n\n    def combine_with_other_txin(self, other_txin: 'TxInput') -> None:\n        assert self.prevout == other_txin.prevout\n        if other_txin.script_sig is not None:\n            self.script_sig = other_txin.script_sig\n        if other_txin.witness is not None:\n            self.witness = other_txin.witness\n        if isinstance(other_txin, PartialTxInput):\n            if other_txin.witness_utxo:\n                self.witness_utxo = other_txin.witness_utxo\n            if other_txin.utxo:\n                self.utxo = other_txin.utxo\n            self.sigs_ecdsa.update(other_txin.sigs_ecdsa)\n            if other_txin.sighash is not None:\n                self.sighash = other_txin.sighash\n            if other_txin.tap_key_sig is not None:\n                self.tap_key_sig = other_txin.tap_key_sig\n            if other_txin.tap_merkle_root is not None:\n                self.tap_merkle_root = other_txin.tap_merkle_root\n            self.bip32_paths.update(other_txin.bip32_paths)\n            if other_txin.redeem_script is not None:\n                self.redeem_script = other_txin.redeem_script\n            if other_txin.witness_script is not None:\n                self.witness_script = other_txin.witness_script\n            self._unknown.update(other_txin._unknown)\n        self.validate_data()\n        # try to finalize now\n        self.finalize()\n\n    def convert_utxo_to_witness_utxo(self) -> None:\n        if self.utxo:\n            self._witness_utxo = self.utxo.outputs()[self.prevout.out_idx]\n            self._utxo = None  # type: Optional[Transaction]\n\n    def is_native_segwit(self) -> Optional[bool]:\n        \"\"\"Whether this input is native segwit (any witness version). None means inconclusive.\"\"\"\n        if self._is_native_segwit is None:\n            if self.address:\n                self._is_native_segwit = bitcoin.is_segwit_address(self.address)\n        return self._is_native_segwit\n\n    def is_p2sh_segwit(self) -> Optional[bool]:\n        \"\"\"Whether this input is p2sh-embedded-segwit. None means inconclusive.\"\"\"\n        if self._is_p2sh_segwit is None:\n            def calc_if_p2sh_segwit_now():\n                if not (self.address and self.redeem_script):\n                    return None\n                if self.address != bitcoin.hash160_to_p2sh(hash_160(self.redeem_script)):\n                    # not p2sh address\n                    return False\n                try:\n                    decoded = [x for x in script_GetOp(self.redeem_script)]\n                except MalformedBitcoinScript:\n                    decoded = None\n                # witness version 0\n                if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_WITNESS_V0):\n                    return True\n                # witness version 1-16\n                future_witness_versions = list(range(opcodes.OP_1, opcodes.OP_16 + 1))\n                for witver, opcode in enumerate(future_witness_versions, start=1):\n                    match = [opcode, OPPushDataGeneric(lambda x: 2 <= x <= 40)]\n                    if match_script_against_template(decoded, match):\n                        return True\n                return False\n\n            self._is_p2sh_segwit = calc_if_p2sh_segwit_now()\n        return self._is_p2sh_segwit\n\n    def is_segwit(self, *, guess_for_address=False) -> bool:\n        \"\"\"Whether this input is segwit (any witness version).\"\"\"\n        if super().is_segwit():\n            return True\n        if self.is_native_segwit() or self.is_p2sh_segwit():\n            return True\n        if self.is_native_segwit() is False and self.is_p2sh_segwit() is False:\n            return False\n        if self.witness_script:\n            return True\n        if desc := self.script_descriptor:\n            return desc.is_segwit()\n        if guess_for_address:\n            dummy_desc = create_dummy_descriptor_from_address(self.address)\n            return dummy_desc.is_segwit()\n        return False  # can be false-negative\n\n    def is_taproot(self) -> Optional[bool]:\n        if (is_taproot := super().is_taproot()) is not None:\n            return is_taproot\n        if desc := self.script_descriptor:\n            return desc.is_taproot()\n        return None\n\n    def already_has_some_signatures(self) -> bool:\n        \"\"\"Returns whether progress has been made towards completing this input.\"\"\"\n        return (self.sigs_ecdsa\n                or self.tap_key_sig is not None\n                or self.script_sig is not None\n                or self.witness is not None)\n\n    def get_scriptcode_for_sighash(self) -> bytes:\n        \"\"\"Constructs the scriptcode part of the preimage for OP_CHECKSIG,\n        for a partial txin, to create a new signature.\n\n        Note: the base impl works by mimicking a consensus-verifier and extracting\n              the required fields from the already complete witness/scriptSig\n              and the scriptpubkey of the funding utxo.\n              In contrast, here we are the one constructing a spending txin,\n              and can have knowledge beyond what will be revealed onchain.\n        \"\"\"\n        if self.witness_script:\n            if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(self.witness_script)]:\n                raise Exception('OP_CODESEPARATOR black magic is not supported')\n            return self.witness_script\n        if not self.is_segwit() and self.redeem_script:\n            if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(self.redeem_script)]:\n                raise Exception('OP_CODESEPARATOR black magic is not supported')\n            return self.redeem_script\n\n        if desc := self.script_descriptor:\n            sc = desc.expand()\n            if script := sc.scriptcode_for_sighash:\n                return script\n            raise Exception(f\"don't know scriptcode for descriptor: {desc.to_string()}\")\n\n        raise UnknownTxinType(f'cannot construct preimage_script')\n\n\nclass PartialTxOutput(TxOutput, PSBTSection):\n    def __init__(self, *args, **kwargs):\n        TxOutput.__init__(self, *args, **kwargs)\n        self.redeem_script = None  # type: Optional[bytes]\n        self.witness_script = None  # type: Optional[bytes]\n        self.bip32_paths = {}  # type: Dict[bytes, Tuple[bytes, Sequence[int]]]  # pubkey -> (xpub_fingerprint, path)\n        self._unknown = {}  # type: Dict[bytes, bytes]\n\n        self._script_descriptor = None  # type: Optional[Descriptor]\n        self.is_mine = False  # type: bool  # whether the wallet considers the output to be ismine\n        self.is_change = False  # type: bool  # whether the wallet considers the output to be change\n        self.is_utxo_reserve = False  # type: bool  # whether this is a change output added to satisfy anchor channel requirements\n\n    @property\n    def pubkeys(self) -> Set[bytes]:\n        if desc := self.script_descriptor:\n            return desc.get_all_pubkeys()\n        return set()\n\n    @property\n    def script_descriptor(self):\n        return self._script_descriptor\n\n    @script_descriptor.setter\n    def script_descriptor(self, desc: Optional[Descriptor]):\n        self._script_descriptor = desc\n        if desc:\n            if self.redeem_script is None:\n                self.redeem_script = desc.expand().redeem_script\n            if self.witness_script is None:\n                self.witness_script = desc.expand().witness_script\n\n    def to_json(self):\n        d = super().to_json()\n        d.update({\n            'desc': self.script_descriptor.to_string() if self.script_descriptor else None,\n            'redeem_script': self.redeem_script.hex() if self.redeem_script else None,\n            'witness_script': self.witness_script.hex() if self.witness_script else None,\n            'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path))\n                            for pubkey, (xfp, path) in self.bip32_paths.items()},\n            'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()},\n        })\n        return d\n\n    @classmethod\n    def from_txout(cls, txout: TxOutput) -> 'PartialTxOutput':\n        res = PartialTxOutput(scriptpubkey=txout.scriptpubkey,\n                              value=txout.value)\n        return res\n\n    def parse_psbt_section_kv(self, kt, key, val):\n        try:\n            kt = PSBTOutputType(kt)\n        except ValueError:\n            pass  # unknown type\n        if DEBUG_PSBT_PARSING: print(f\"{repr(kt)} {key.hex()} {val.hex()}\")\n        if kt == PSBTOutputType.REDEEM_SCRIPT:\n            if self.redeem_script is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            self.redeem_script = val\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        elif kt == PSBTOutputType.WITNESS_SCRIPT:\n            if self.witness_script is not None:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            self.witness_script = val\n            if key: raise SerializationError(f\"key for {repr(kt)} must be empty\")\n        elif kt == PSBTOutputType.BIP32_DERIVATION:\n            if key in self.bip32_paths:\n                raise SerializationError(f\"duplicate key: {repr(kt)}\")\n            if len(key) not in (33, 65):\n                raise SerializationError(f\"key for {repr(kt)} has unexpected length: {len(key)}\")\n            self.bip32_paths[key] = unpack_bip32_root_fingerprint_and_int_path(val)\n        else:\n            full_key = self.get_fullkey_from_keytype_and_key(kt, key)\n            if full_key in self._unknown:\n                raise SerializationError(f'duplicate key. PSBT output key for unknown type: {full_key}')\n            self._unknown[full_key] = val\n\n    def serialize_psbt_section_kvs(self, wr):\n        if self.redeem_script is not None:\n            wr(PSBTOutputType.REDEEM_SCRIPT, self.redeem_script)\n        if self.witness_script is not None:\n            wr(PSBTOutputType.WITNESS_SCRIPT, self.witness_script)\n        for k in sorted(self.bip32_paths):\n            packed_path = pack_bip32_root_fingerprint_and_int_path(*self.bip32_paths[k])\n            wr(PSBTOutputType.BIP32_DERIVATION, packed_path, k)\n        for full_key, val in sorted(self._unknown.items()):\n            key_type, key = self.get_keytype_and_key_from_fullkey(full_key)\n            wr(key_type, val, key=key)\n\n    def combine_with_other_txout(self, other_txout: 'TxOutput') -> None:\n        assert self.scriptpubkey == other_txout.scriptpubkey\n        if not isinstance(other_txout, PartialTxOutput):\n            return\n        if other_txout.redeem_script is not None:\n            self.redeem_script = other_txout.redeem_script\n        if other_txout.witness_script is not None:\n            self.witness_script = other_txout.witness_script\n        self.bip32_paths.update(other_txout.bip32_paths)\n        self._unknown.update(other_txout._unknown)\n\n\nclass PartialTransaction(Transaction):\n\n    def __init__(self):\n        Transaction.__init__(self, None)\n        self.xpubs = {}  # type: Dict[BIP32Node, Tuple[bytes, Sequence[int]]]  # intermediate bip32node -> (xfp, der_prefix)\n        self._inputs = []  # type: List[PartialTxInput]\n        self._outputs = []  # type: List[PartialTxOutput]\n        self._unknown = {}  # type: Dict[bytes, bytes]\n        self.rbf_merge_txid = None\n\n    def to_json(self) -> dict:\n        d = super().to_json()\n        d.update({\n            'xpubs': {bip32node.to_xpub(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path))\n                      for bip32node, (xfp, path) in self.xpubs.items()},\n            'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()},\n        })\n        return d\n\n    @classmethod\n    def from_tx(cls, tx: Transaction, *, strip_witness: bool = True) -> 'PartialTransaction':\n        assert tx\n        res = cls()\n        res._inputs = [PartialTxInput.from_txin(txin, strip_witness=strip_witness)\n                       for txin in tx.inputs()]\n        res._outputs = [PartialTxOutput.from_txout(txout) for txout in tx.outputs()]\n        res.version = tx.version\n        res.locktime = tx.locktime\n        return res\n\n    @classmethod\n    def from_raw_psbt(cls, raw: Union[str, bytes, bytearray]) -> 'PartialTransaction':\n        # auto-detect and decode Base64 and Hex.\n        if raw[0:10].lower() == '70736274ff':  # hex (str)\n            raw = bytes.fromhex(raw)\n        elif raw[0:6] in (b'cHNidP', 'cHNidP'):  # base64\n            raw = base64.b64decode(raw, validate=True)\n        if not isinstance(raw, (bytes, bytearray)) or raw[0:5] != b'psbt\\xff':\n            raise BadHeaderMagic(\"bad magic\")\n\n        tx = None  # type: Optional[PartialTransaction]\n\n        # We parse the raw stream twice. The first pass is used to find the\n        # PSBT_GLOBAL_UNSIGNED_TX key in the global section and set 'tx'.\n        # The second pass does everything else.\n        with io.BytesIO(raw[5:]) as fd:  # parsing \"first pass\"\n            while True:\n                try:\n                    kt, key, val = PSBTSection.get_next_kv_from_fd(fd)\n                except StopIteration:\n                    break\n                try:\n                    kt = PSBTGlobalType(kt)\n                except ValueError:\n                    pass  # unknown type\n                if kt == PSBTGlobalType.UNSIGNED_TX:\n                    if tx is not None:\n                        raise SerializationError(f\"duplicate key: {repr(kt)}\")\n                    if key:\n                        raise SerializationError(f\"key for {repr(kt)} must be empty\")\n                    unsigned_tx = Transaction(val.hex())\n                    for txin in unsigned_tx.inputs():\n                        if txin.script_sig or txin.witness:\n                            raise SerializationError(f\"PSBT {repr(kt)} must have empty scriptSigs and witnesses\")\n                    tx = PartialTransaction.from_tx(unsigned_tx)\n\n        if tx is None:\n            raise SerializationError(f\"PSBT missing required global section PSBT_GLOBAL_UNSIGNED_TX\")\n\n        with io.BytesIO(raw[5:]) as fd:  # parsing \"second pass\"\n            # global section\n            while True:\n                try:\n                    kt, key, val = PSBTSection.get_next_kv_from_fd(fd)\n                except StopIteration:\n                    break\n                try:\n                    kt = PSBTGlobalType(kt)\n                except ValueError:\n                    pass  # unknown type\n                if DEBUG_PSBT_PARSING: print(f\"{repr(kt)} {key.hex()} {val.hex()}\")\n                if kt == PSBTGlobalType.UNSIGNED_TX:\n                    pass  # already handled during \"first\" parsing pass\n                elif kt == PSBTGlobalType.XPUB:\n                    bip32node = BIP32Node.from_bytes(key)\n                    if bip32node in tx.xpubs:\n                        raise SerializationError(f\"duplicate key: {repr(kt)}\")\n                    xfp, path = unpack_bip32_root_fingerprint_and_int_path(val)\n                    if bip32node.depth != len(path):\n                        raise SerializationError(f\"PSBT global xpub has mismatching depth ({bip32node.depth}) \"\n                                                 f\"and derivation prefix len ({len(path)})\")\n                    child_number_of_xpub = int.from_bytes(bip32node.child_number, 'big')\n                    if not ((bip32node.depth == 0 and child_number_of_xpub == 0)\n                            or (bip32node.depth != 0 and child_number_of_xpub == path[-1])):\n                        raise SerializationError(f\"PSBT global xpub has inconsistent child_number and derivation prefix\")\n                    tx.xpubs[bip32node] = xfp, path\n                elif kt == PSBTGlobalType.VERSION:\n                    if len(val) > 4:\n                        raise SerializationError(f\"value for {repr(kt)} has unexpected length: {len(val)} > 4\")\n                    psbt_version = int.from_bytes(val, byteorder='little', signed=False)\n                    if psbt_version > 0:\n                        raise SerializationError(f\"Only PSBTs with version 0 are supported. Found version: {psbt_version}\")\n                    if key:\n                        raise SerializationError(f\"key for {repr(kt)} must be empty\")\n                else:\n                    full_key = PSBTSection.get_fullkey_from_keytype_and_key(kt, key)\n                    if full_key in tx._unknown:\n                        raise SerializationError(f'duplicate key. PSBT global key for unknown type: {full_key}')\n                    tx._unknown[full_key] = val\n            try:\n                # inputs sections\n                for txin in tx.inputs():\n                    if DEBUG_PSBT_PARSING: print(\"-> new input starts\")\n                    txin._populate_psbt_fields_from_fd(fd)\n                # outputs sections\n                for txout in tx.outputs():\n                    if DEBUG_PSBT_PARSING: print(\"-> new output starts\")\n                    txout._populate_psbt_fields_from_fd(fd)\n            except UnexpectedEndOfStream:\n                raise UnexpectedEndOfStream('Unexpected end of stream. Num input and output maps provided does not match unsigned tx.') from None\n\n            if fd.read(1) != b'':\n                raise SerializationError(\"extra junk at the end of PSBT\")\n\n        for txin in tx.inputs():\n            txin.validate_data()\n\n        return tx\n\n    def requires_keystore(self):\n        \"\"\"\n        Returns True if signing will require private keys from the keystore\n        Called by txbatcher in order to know if a password is needed\n        \"\"\"\n        return not all(hasattr(txin, 'make_witness') for txin in self.inputs())\n\n    @classmethod\n    def from_io(\n            cls,\n            inputs: Sequence[PartialTxInput],\n            outputs: Sequence[PartialTxOutput],\n            *,\n            locktime: int = None,\n            version: int = None,\n            BIP69_sort: bool = True\n    ) -> 'PartialTransaction':\n        self = cls()\n        self._inputs = list(inputs)\n        self._outputs = list(outputs)\n        if locktime is not None:\n            self.locktime = locktime\n        if version is not None:\n            self.version = version\n        if BIP69_sort:\n            self.BIP69_sort()\n        return self\n\n    def _serialize_psbt(self, fd) -> None:\n        wr = PSBTSection.create_psbt_writer(fd)\n        fd.write(b'psbt\\xff')\n        # global section\n        wr(PSBTGlobalType.UNSIGNED_TX, bfh(self.serialize_to_network(include_sigs=False)))\n        for bip32node, (xfp, path) in sorted(self.xpubs.items()):\n            val = pack_bip32_root_fingerprint_and_int_path(xfp, path)\n            wr(PSBTGlobalType.XPUB, val, key=bip32node.to_bytes())\n        for full_key, val in sorted(self._unknown.items()):\n            key_type, key = PSBTSection.get_keytype_and_key_from_fullkey(full_key)\n            wr(key_type, val, key=key)\n        fd.write(b'\\x00')  # section-separator\n        # input sections\n        for inp in self._inputs:\n            inp._serialize_psbt_section(fd)\n        # output sections\n        for outp in self._outputs:\n            outp._serialize_psbt_section(fd)\n\n    def finalize_psbt(self) -> None:\n        for txin in self.inputs():\n            txin.finalize()\n\n    def combine_with_other_psbt(self, other_tx: 'Transaction') -> None:\n        \"\"\"Pulls in all data from other_tx we don't yet have (e.g. signatures).\n        other_tx must be concerning the same unsigned tx.\n        \"\"\"\n        if self.serialize_to_network(include_sigs=False) != other_tx.serialize_to_network(include_sigs=False):\n            raise Exception('A Combiner must not combine two different PSBTs.')\n        # BIP-174: \"The resulting PSBT must contain all of the key-value pairs from each of the PSBTs.\n        #           The Combiner must remove any duplicate key-value pairs, in accordance with the specification.\"\n        # global section\n        if isinstance(other_tx, PartialTransaction):\n            self.xpubs.update(other_tx.xpubs)\n            self._unknown.update(other_tx._unknown)\n        # input sections\n        for txin, other_txin in zip(self.inputs(), other_tx.inputs()):\n            txin.combine_with_other_txin(other_txin)\n        # output sections\n        for txout, other_txout in zip(self.outputs(), other_tx.outputs()):\n            txout.combine_with_other_txout(other_txout)\n        self.invalidate_ser_cache()\n\n    def join_with_other_psbt(self, other_tx: 'PartialTransaction', *, config: 'SimpleConfig') -> None:\n        \"\"\"Adds inputs and outputs from other_tx into this one.\"\"\"\n        if not isinstance(other_tx, PartialTransaction):\n            raise Exception('Can only join partial transactions.')\n        # make sure there are no duplicate prevouts\n        prevouts = set()\n        for txin in itertools.chain(self.inputs(), other_tx.inputs()):\n            prevout_str = txin.prevout.to_str()\n            if prevout_str in prevouts:\n                raise Exception(f\"Duplicate inputs! \"\n                                f\"Transactions that spend the same prevout cannot be joined.\")\n            prevouts.add(prevout_str)\n        # copy global PSBT section\n        self.xpubs.update(other_tx.xpubs)\n        self._unknown.update(other_tx._unknown)\n        # copy and add inputs and outputs\n        self.add_inputs(list(other_tx.inputs()))\n        self.add_outputs(list(other_tx.outputs()), merge_duplicates=config.WALLET_MERGE_DUPLICATE_OUTPUTS)\n        self.remove_signatures()\n        self.invalidate_ser_cache()\n\n    def inputs(self) -> Sequence[PartialTxInput]:\n        return self._inputs\n\n    def outputs(self) -> Sequence[PartialTxOutput]:\n        return self._outputs\n\n    def add_inputs(self, inputs: List[PartialTxInput], BIP69_sort=True) -> None:\n        self._inputs.extend(inputs)\n        if BIP69_sort:\n            self.BIP69_sort(outputs=False)\n        self.invalidate_ser_cache()\n\n    def add_outputs(self, outputs: List[PartialTxOutput], *, merge_duplicates: bool = False, BIP69_sort: bool = True) -> None:\n        self._outputs.extend(outputs)\n        if merge_duplicates:\n            self._outputs = merge_duplicate_tx_outputs(self._outputs)\n        if BIP69_sort:\n            self.BIP69_sort(inputs=False)\n        self.invalidate_ser_cache()\n\n    def replace_output_address(self, old_address: str, new_address: str) -> None:\n        idx = list(self.get_output_idxs_from_address(old_address))\n        assert len(idx) == 1\n        amount = self._outputs[idx[0]].value\n        funding_output = PartialTxOutput.from_address_and_value(new_address, amount)\n        old_output = PartialTxOutput.from_address_and_value(old_address, amount)\n        self._outputs.remove(old_output)\n        self.add_outputs([funding_output])\n        delattr(self, '_script_to_output_idx')\n\n    def get_change_outputs(self) -> Sequence[PartialTxOutput]:\n        return [o for o in self._outputs if o.is_change]\n\n    def has_change(self) -> bool:\n        return len(self.get_change_outputs()) > 0\n\n    def get_dummy_output(self, dummy_addr: str) -> Optional['PartialTxOutput']:\n        idxs = self.get_output_idxs_from_address(dummy_addr)\n        if not idxs:\n            return None\n        assert len(idxs) == 1\n        idx = list(idxs)[0]\n        return self.outputs()[idx]\n\n    def set_rbf(self, rbf: bool) -> None:\n        nSequence = 0xffffffff - (2 if rbf else 1)\n        for txin in self.inputs():\n            txin.nsequence = nSequence\n        self.invalidate_ser_cache()\n\n    def BIP69_sort(self, inputs=True, outputs=True):\n        # NOTE: other parts of the code rely on these sorts being *stable* sorts\n        if inputs:\n            self._inputs.sort(key = lambda i: (i.prevout.txid, i.prevout.out_idx))\n        if outputs:\n            self._outputs.sort(key = lambda o: (o.value, o.scriptpubkey))\n        self.invalidate_ser_cache()\n\n    def sign(self, keypairs: Mapping[bytes, bytes]) -> None:\n        # keypairs:  pubkey_bytes -> secret_bytes\n        sighash_cache = SighashCache()\n        for i, txin in enumerate(self.inputs()):\n            for pubkey in txin.pubkeys:\n                if txin.is_complete():\n                    break\n                if pubkey not in keypairs:\n                    continue\n                _logger.info(f\"adding signature for {pubkey.hex()}. spending utxo {txin.prevout.to_str()}\")\n                sec = keypairs[pubkey]\n                sig = self.sign_txin(i, sec, sighash_cache=sighash_cache)\n                self.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey, sig=sig)\n\n        _logger.debug(f\"tx.sign() finished. is_complete={self.is_complete()}\")\n        self.invalidate_ser_cache()\n\n    def sign_txin(\n        self,\n        txin_index: int,\n        privkey_bytes: bytes,\n        *,\n        sighash_cache: SighashCache = None,\n    ) -> bytes:\n        txin = self.inputs()[txin_index]\n        txin.validate_data(for_signing=True)\n        pre_hash = self.serialize_preimage(txin_index, sighash_cache=sighash_cache)\n        if txin.is_taproot():\n            # note: privkey_bytes is the internal key\n            merkle_root = txin.tap_merkle_root or bytes()\n            output_privkey_bytes = taproot_tweak_seckey(privkey_bytes, merkle_root)\n            output_privkey = ecc.ECPrivkey(output_privkey_bytes)\n            msg_hash = bip340_tagged_hash(b\"TapSighash\", pre_hash)\n            sig = output_privkey.schnorr_sign(msg_hash)\n            sighash = txin.sighash if txin.sighash is not None else Sighash.DEFAULT\n        else:\n            privkey = ecc.ECPrivkey(privkey_bytes)\n            msg_hash = sha256d(pre_hash)\n            sig = privkey.ecdsa_sign(msg_hash, sigencode=ecc.ecdsa_der_sig_from_r_and_s)\n            sighash = txin.sighash if txin.sighash is not None else Sighash.ALL\n        return sig + Sighash.to_sigbytes(sighash)\n\n    def is_complete(self) -> bool:\n        return all([txin.is_complete() for txin in self.inputs()])\n\n    def signature_count(self) -> Tuple[int, int]:\n        nhave, nreq = 0, 0\n        for txin in self.inputs():\n            a, b = txin.get_satisfaction_progress()\n            nhave += a\n            nreq += b\n        return nhave, nreq\n\n    def serialize(self) -> str:\n        \"\"\"Returns PSBT as base64 text, or raw hex of network tx (if complete).\"\"\"\n        self.finalize_psbt()  # FIXME this side-effects self\n        if self.is_complete():\n            return Transaction.serialize(self)\n        return self._serialize_as_base64()\n\n    def serialize_as_bytes(self, *, force_psbt: bool = False) -> bytes:\n        \"\"\"Returns PSBT as raw bytes, or raw bytes of network tx (if complete).\"\"\"\n        self.finalize_psbt()  # FIXME this side-effects self\n        if force_psbt or not self.is_complete():\n            with io.BytesIO() as fd:\n                self._serialize_psbt(fd)\n                return fd.getvalue()\n        else:\n            return Transaction.serialize_as_bytes(self)\n\n    def _serialize_as_base64(self) -> str:\n        raw_bytes = self.serialize_as_bytes()\n        return base64.b64encode(raw_bytes).decode('ascii')\n\n    def update_signatures(self, signatures: Sequence[Union[bytes, None]]) -> None:\n        \"\"\"Add new signatures to a transaction\n\n        `signatures` is expected to be a list of sigs with signatures[i]\n        intended for self._inputs[i].\n        This is used by the Trezor, KeepKey and Safe-T plugins.\n        \"\"\"\n        if self.is_complete():\n            return\n        if len(self.inputs()) != len(signatures):\n            raise Exception('expected {} signatures; got {}'.format(len(self.inputs()), len(signatures)))\n        for i, txin in enumerate(self.inputs()):\n            sig = signatures[i]\n            if sig is None:\n                continue\n            if sig in list(txin.sigs_ecdsa.values()):\n                continue\n            msg_hash = sha256d(self.serialize_preimage(i))\n            sig64 = ecc.ecdsa_sig64_from_der_sig(sig[:-1])\n            for recid in range(4):\n                try:\n                    public_key = ecc.ECPubkey.from_ecdsa_sig64(sig64, recid, msg_hash)\n                except ecc.InvalidECPointException:\n                    # the point might not be on the curve for some recid values\n                    continue\n                pubkey_bytes = public_key.get_public_key_bytes(compressed=True)\n                if pubkey_bytes in txin.pubkeys:\n                    if not public_key.ecdsa_verify(sig64, msg_hash):\n                        continue\n                    _logger.info(f\"adding sig: txin_idx={i}, signing_pubkey={pubkey_bytes.hex()}, sig={sig.hex()}\")\n                    self.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey_bytes, sig=sig)\n                    break\n        # redo raw\n        self.invalidate_ser_cache()\n\n    def add_signature_to_txin(self, *, txin_idx: int, signing_pubkey: bytes, sig: bytes) -> None:\n        txin = self._inputs[txin_idx]\n        txin.sigs_ecdsa[signing_pubkey] = sig\n        # force re-serialization\n        txin.script_sig = None\n        txin.witness = None\n        self.invalidate_ser_cache()\n\n    def add_info_from_wallet(\n            self,\n            wallet: 'Abstract_Wallet',\n            *,\n            include_xpubs: bool = False,\n    ) -> None:\n        if self.is_complete():\n            return\n        # only include xpubs for multisig wallets; currently only they need it in practice\n        # note: coldcard fw have a limitation that if they are included then all\n        #       inputs are assumed to be multisig... https://github.com/spesmilo/electrum/pull/5440#issuecomment-549504761\n        # note: trezor plugin needs xpubs included, if there are multisig inputs/change_outputs\n        from .wallet import Multisig_Wallet\n        if include_xpubs and isinstance(wallet, Multisig_Wallet):\n            from .keystore import Xpub\n            for ks in wallet.get_keystores():\n                if isinstance(ks, Xpub):\n                    fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(\n                        der_suffix=[], only_der_suffix=False)\n                    xpub = ks.get_xpub_to_be_used_in_partial_tx(only_der_suffix=False)\n                    bip32node = BIP32Node.from_xkey(xpub)\n                    self.xpubs[bip32node] = (fp_bytes, der_full)\n        for txin in self.inputs():\n            wallet.add_input_info(\n                txin,\n                only_der_suffix=False,\n            )\n        for txout in self.outputs():\n            wallet.add_output_info(\n                txout,\n                only_der_suffix=False,\n            )\n\n    def remove_xpubs_and_bip32_paths(self) -> None:\n        self.xpubs.clear()\n        for txin in self.inputs():\n            txin.bip32_paths.clear()\n        for txout in self.outputs():\n            txout.bip32_paths.clear()\n\n    def prepare_for_export_for_coinjoin(self) -> None:\n        \"\"\"Removes all sensitive details.\"\"\"\n        # globals\n        self.xpubs.clear()\n        self._unknown.clear()\n        # inputs\n        for txin in self.inputs():\n            txin.bip32_paths.clear()\n        # outputs\n        for txout in self.outputs():\n            txout.redeem_script = None\n            txout.witness_script = None\n            txout.bip32_paths.clear()\n            txout._unknown.clear()\n\n    async def prepare_for_export_for_hardware_device(self, wallet: 'Abstract_Wallet') -> None:\n        self.add_info_from_wallet(wallet, include_xpubs=True)\n        await self.add_info_from_network(wallet.network)\n        # log warning if PSBT_*_BIP32_DERIVATION fields cannot be filled with full path due to missing info\n        from .keystore import Xpub\n\n        def is_ks_missing_info(ks):\n            return (isinstance(ks, Xpub) and (ks.get_root_fingerprint() is None\n                                              or ks.get_derivation_prefix() is None))\n\n        if any([is_ks_missing_info(ks) for ks in wallet.get_keystores()]):\n            _logger.warning('PSBT was requested to be filled with full bip32 paths but '\n                            'some keystores lacked either the derivation prefix or the root fingerprint')\n\n    def convert_all_utxos_to_witness_utxos(self) -> None:\n        \"\"\"Replaces all NON-WITNESS-UTXOs with WITNESS-UTXOs.\n        This will likely make an exported PSBT invalid spec-wise,\n        but it makes e.g. QR codes significantly smaller.\n        \"\"\"\n        for txin in self.inputs():\n            txin.convert_utxo_to_witness_utxo()\n\n    def remove_signatures(self):\n        for txin in self.inputs():\n            txin.sigs_ecdsa = {}\n            txin.tap_key_sig = None\n            txin.script_sig = None\n            txin.witness = None\n        assert not self.is_complete()\n        self.invalidate_ser_cache()\n\n\ndef pack_bip32_root_fingerprint_and_int_path(xfp: bytes, path: Sequence[int]) -> bytes:\n    if len(xfp) != 4:\n        raise Exception(f'unexpected xfp length. xfp={xfp}')\n    return xfp + b''.join(i.to_bytes(4, byteorder='little', signed=False) for i in path)\n\n\ndef unpack_bip32_root_fingerprint_and_int_path(path: bytes) -> Tuple[bytes, Sequence[int]]:\n    if len(path) % 4 != 0:\n        raise Exception(f'unexpected packed path length. path={path.hex()}')\n    xfp = path[0:4]\n    int_path = [int.from_bytes(b, byteorder='little', signed=False) for b in chunks(path[4:], 4)]\n    return xfp, int_path\n"
  },
  {
    "path": "electrum/txbatcher.py",
    "content": "# This class batches outgoing payments and incoming utxo sweeps.\n# It ensures that we do not send a payment twice.\n#\n# Explanation of the problem:\n# Suppose we are asked to send two payments: first o1, and then o2.\n# We replace tx1(o1) (that pays to o1) with tx1'(o1,o2), that pays to o1 and o2.\n# tx1 and tx1' use the same inputs, so they cannot both be mined in the same blockchain.\n# If tx1 is mined instead of tx1', we now need to pay o2, so we will broadcast a new transaction tx2(o2).\n# However, tx1 may be removed from the blockchain, due to a reorg, and a chain with tx1' can become the valid one.\n# In that case, we might pay o2 twice: with tx1' and with tx2\n#\n# The following code prevents that by making tx2 a child of tx1.\n# This is denoted by tx2(tx1|o2).\n#\n# Example:\n#\n# output 1:     tx1(o1) ---------------\n#                                      \\\n# output 2:     tx1'(o1,o2)-------      ----> tx2(tx1|o2) ------\n#                                 \\     \\                       \\\n# output 3:     tx1''(o1,o2,o3)    \\     ---> tx2'(tx1|o2,o3)    ---->  tx3(tx2|o3)  (if tx2 is mined)\n#                                   \\\n#                                    -------------------------------->  tx3(tx1'|o3) (if tx1' is mined)\n#\n# In the above example, we have to make 3 payments.\n# Suppose we have broadcast tx1, tx1' and tx1''\n#  - if tx1 gets mined, we broadcast: tx2'(tx1|o2,o3)\n#  - if tx1' gets mined, we broadcast tx3(tx1'|o3)\n#\n# Note that there are two possible execution paths that may lead to the creation of tx3:\n#   - as a child of tx2\n#   - as a child of tx1'\n#\n# A batch is a set of incompatible txs, such as [tx1, tx1', tx1''].\n# Note that we do not persist older batches. We only persist the current batch in self.batch_txids.\n# Thus, if we need to broadcast tx2 or tx2', then self.batch_txids is reset, and the old batch is forgotten.\n#\n# If we cannot RBF a transaction (because the server returns an error), then we create a new batch,\n# as if the transaction had been mined.\n#   if cannot_rbf(tx1)  -> broadcast tx2(tx1,o2). The new base is now tx2(tx,o2)\n#   if cannot_rbf(tx1') -> broadcast tx3(tx1'|o3)\n#\n#\n# Notes:\n#\n# 1. CPFP:\n# When a batch is forgotten but not mined (because the server returned an error), we no longer bump its fee.\n# However, the current code does not treat the next batch as a CPFP when computing the fee.\n#\n# 2. Reorgs:\n# This code does not guarantee that a payment or a sweep will happen.\n# This is fine for sweeps; it is the responsibility of the caller (lnwatcher) to add them again.\n# To make payments reorg-safe, we would need to persist more data and redo failed payments.\n#\n# 3. batch_payments and batch_inputs are not persisted.\n# In the case of sweeps, lnwatcher ensures that SweepInfo is added again after a client restart.\n# In order to generalize that logic to payments, callers would need to pass a unique ID along with\n# the payment output, so that we can prevent paying twice.\n#\n# - nLocktime/CLTV values (bip-65) and nSequence/CSV values (bip-112) are either explicitly\n#   or implicitly block-height-based everywhere in this file.\n#   SCRIPT execution fails on height vs timestamp confusion, and\n#   it is not safe to do naive integer comparison between these values without establishing type.\n#   TODO review this is correct, and add checks.\n#    nLocktime/CLTV usage in particular seems dangerously *implicit* for being block-heights\n\nimport asyncio\nimport threading\nimport copy\nfrom typing import Dict, Sequence, Optional, TYPE_CHECKING, Mapping, Set, List, Tuple\n\nfrom . import util\nfrom .bitcoin import dust_threshold\nfrom .logging import Logger\nfrom .util import log_exceptions, NotEnoughFunds, BelowDustLimit, NoDynamicFeeEstimates, OldTaskGroup\nfrom .transaction import PartialTransaction, PartialTxOutput, Transaction, TxOutpoint, PartialTxInput\nfrom .address_synchronizer import TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE\nfrom .lnsweep import SweepInfo\nfrom .json_db import locked, StoredDict\nfrom .fee_policy import FeePolicy\n\nif TYPE_CHECKING:\n    from .wallet import Abstract_Wallet\n\n\nclass TxBatcher(Logger):\n\n    SLEEP_INTERVAL = 1\n\n    def __init__(self, wallet: 'Abstract_Wallet'):\n        Logger.__init__(self)\n        self.lock = threading.RLock()\n        self.storage = wallet.db.get_stored_item(\"tx_batches\", {})\n        self.tx_batches = {}  # type: Dict[str, TxBatch]\n        self.wallet = wallet\n        for key, item_storage in self.storage.items():\n            self.tx_batches[key] = TxBatch(self.wallet, item_storage)\n        self._legacy_htlcs = {}  # type: Dict[TxOutpoint, SweepInfo]\n        self.taskgroup = None  # type: Optional[OldTaskGroup]\n        self.password_future = None  # type: Optional[asyncio.Future[Optional[str]]]\n\n    @locked\n    def add_payment_output(self, key: str, output: 'PartialTxOutput') -> None:\n        batch = self._maybe_create_new_batch(key, fee_policy_name=key)\n        batch.add_payment_output(output)\n\n    @locked\n    def add_sweep_input(self, key: str, sweep_info: 'SweepInfo') -> None:\n        \"\"\"Can raise BelowDustLimit or NoDynamicFeeEstimates.\"\"\"\n        if sweep_info.txin and sweep_info.txout:\n            # detect legacy htlc using name and csv delay\n            if sweep_info.name in ['received-htlc', 'offered-htlc'] and sweep_info.csv_delay == 0:\n                if sweep_info.txin.prevout not in self._legacy_htlcs:\n                    self.logger.info(f'received {sweep_info.name}')\n                    self._legacy_htlcs[sweep_info.txin.prevout] = sweep_info\n                return\n        fee_policy_name = key\n        if not sweep_info.can_be_batched:\n            # create a batch only for that input\n            key = sweep_info.txin.prevout.to_str()\n        batch = self._maybe_create_new_batch(key, fee_policy_name)\n        batch.add_sweep_input(sweep_info)\n\n    def _maybe_create_new_batch(self, key: str, fee_policy_name: str) -> 'TxBatch':\n        assert util.get_running_loop() == util.get_asyncio_loop(), f\"this must be run on the asyncio thread!\"\n        if key not in self.storage:\n            self.logger.info(f'creating new batch: {key}')\n            self.storage[key] = { 'fee_policy_name': fee_policy_name, 'txids': [], 'prevout': None }\n            self.tx_batches[key] = batch = TxBatch(self.wallet, self.storage[key])\n            if self.taskgroup:\n                asyncio.ensure_future(self.taskgroup.spawn(self.run_batch(key, batch)))\n        return self.tx_batches[key]\n\n    @locked\n    def delete_batch(self, key: str) -> None:\n        self.logger.info(f'deleting TxBatch {key}')\n        self.storage.pop(key)\n        self.tx_batches.pop(key)\n\n    def find_batch_by_prevout(self, prevout: str) -> Optional['TxBatch']:\n        for k, v in self.tx_batches.items():\n            if v._prevout == prevout:\n                return v\n        return None\n\n    def find_batch_of_txid(self, txid: str) -> Optional[str]:\n        for k, v in self.tx_batches.items():\n            if v.is_mine(txid):\n                return k\n        return None\n\n    def is_mine(self, txid: str) -> bool:\n        # used to prevent GUI from interfering\n        return bool(self.find_batch_of_txid(txid))\n\n    async def run_batch(self, key: str, batch: 'TxBatch') -> None:\n        await batch.run()\n        self.delete_batch(key)\n\n    @log_exceptions\n    async def run(self):\n        self.taskgroup = OldTaskGroup()\n        for key, batch in self.tx_batches.items():\n            await self.taskgroup.spawn(self.run_batch(key, batch))\n        async with self.taskgroup as group:\n            await group.spawn(self.redeem_legacy_htlcs())\n\n    async def redeem_legacy_htlcs(self) -> None:\n        while True:\n            await asyncio.sleep(self.SLEEP_INTERVAL)\n            for sweep_info in self._legacy_htlcs.values():\n                await self._maybe_redeem_legacy_htlcs(sweep_info)\n\n    async def _maybe_redeem_legacy_htlcs(self, sweep_info: 'SweepInfo') -> None:\n        assert sweep_info.csv_delay == 0\n        local_height = self.wallet.network.get_local_height()\n        wanted_height = sweep_info.cltv_abs\n        if wanted_height - local_height > 0:\n            return\n        outpoint = sweep_info.txin.prevout.to_str()\n        prev_txid, index = outpoint.split(':')\n        if spender_txid := self.wallet.adb.db.get_spent_outpoint(prev_txid, int(index)):\n            tx_mined_status = self.wallet.adb.get_tx_height(spender_txid)\n            if tx_mined_status.height() > 0:\n                return\n            if tx_mined_status.height() not in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:\n                return\n        self.logger.info(f'will broadcast standalone tx {sweep_info.name}')\n        tx = PartialTransaction.from_io([sweep_info.txin], [sweep_info.txout], locktime=sweep_info.cltv_abs, version=2)\n        self.wallet.sign_transaction(tx, password=None, ignore_warnings=True)\n        if await self.wallet.network.try_broadcasting(tx, sweep_info.name):\n            self.wallet.adb.add_transaction(tx)\n\n    async def get_password(self, txid: str) -> Optional[str]:\n        # daemon, android have password in memory\n        password = self.wallet.get_unlocked_password()\n        if password:\n            return password\n        future = self.get_password_future(txid)\n        try:\n\n            await future\n        except asyncio.CancelledError as e:\n            return None\n        password = future.result()\n        return password\n\n    @locked\n    def set_password_future(self, password: Optional[str]) -> None:\n        if self.password_future is not None:\n            if password is not None:\n                self.password_future.set_result(password)\n            else:\n                self.password_future.cancel()\n            self.password_future = None\n            util.trigger_callback('password_not_required', self.wallet)\n\n    @locked\n    def get_password_future(self, txid: str):\n        if self.password_future is None:\n            self.password_future = asyncio.Future()\n            self.password_future.txids = []\n            self.logger.info(f'password required: {txid}')\n        self.password_future.txids.append(txid)\n        util.trigger_callback('password_required', self.wallet)\n        return self.password_future\n\n\nclass TxBatch(Logger):\n\n    def __init__(self, wallet: 'Abstract_Wallet', storage: StoredDict):\n        Logger.__init__(self)\n        self.wallet = wallet\n        self.storage = storage\n        self.lock = threading.RLock()\n        self.batch_payments = []  # type: List[PartialTxOutput]      # payments we need to make\n        self.batch_inputs = {}  # type: Dict[TxOutpoint, SweepInfo]  # inputs we need to sweep\n        # list of tx that were broadcast. Each tx is a RBF replacement of the previous one. Ony one can get mined.\n        self._prevout = storage.get('prevout')  # type: Optional[str]\n        self._batch_txids = storage['txids']  # type: List[str]\n        self._fee_policy_name = storage.get('fee_policy_name', 'default')  # type: str\n        self._base_tx = None  # type: Optional[PartialTransaction]   # current batch tx. last element of batch_txids\n        self._parent_tx = None  # type: Optional[PartialTransaction]\n        self._unconfirmed_sweeps = set()  # type: Set[TxOutpoint]  # inputs we are sweeping (until spending tx is confirmed)\n\n    @property\n    def fee_policy(self) -> FeePolicy:\n        # this assumes the descriptor is in config.fee_policy\n        cv_name = 'fee_policy' + '.' + self._fee_policy_name\n        descriptor = self.wallet.config.get(cv_name, 'eta:2')\n        return FeePolicy(descriptor)\n\n    @log_exceptions\n    async def run(self) -> None:\n        while not self.is_done():\n            await asyncio.sleep(self.wallet.txbatcher.SLEEP_INTERVAL)\n            if not (self.wallet.network and self.wallet.network.is_connected()):\n                continue\n            try:\n                await self.run_iteration()\n            except Exception as e:\n                self.logger.exception(f'TxBatch error: {repr(e)}')\n                break\n\n    def is_mine(self, txid: str) -> bool:\n        return txid in self._batch_txids\n\n    @locked\n    def add_payment_output(self, output: 'PartialTxOutput') -> None:\n        # todo: maybe we should raise NotEnoughFunds here\n        self.batch_payments.append(output)\n\n    def is_dust(self, sweep_info: SweepInfo) -> bool:\n        \"\"\"Can raise NoDynamicFeeEstimates.\"\"\"\n        if sweep_info.dust_override:\n            return False\n        if sweep_info.txout is not None:\n            return False\n        value = sweep_info.txin.value_sats()\n        witness_size = len(sweep_info.txin.make_witness(71*b'\\x00'))\n        tx_size_vbytes = 84 + witness_size//4     # assumes no batching, sweep to p2wpkh\n        fee = self.fee_policy.estimate_fee(tx_size_vbytes, network=self.wallet.network)\n        is_dust = value - fee <= dust_threshold()\n        self.logger.info(f'{sweep_info.name} size = {tx_size_vbytes}: {is_dust=}')\n        return is_dust\n\n    @locked\n    def add_sweep_input(self, sweep_info: 'SweepInfo') -> None:\n        \"\"\"Can raise BelowDustLimit or NoDynamicFeeEstimates.\"\"\"\n        if self.is_dust(sweep_info):\n            # note: this uses the current fee estimates. Just because something is dust\n            #       at the current fee levels, if fees go down, it might still become\n            #       worthwhile to sweep. So callers might want to retry later.\n            raise BelowDustLimit\n        txin = sweep_info.txin\n        if txin.prevout in self._unconfirmed_sweeps:\n            return\n        # early return if the spending tx is confirmed\n        # if its block is orphaned, the txin will be added again\n        prevout = txin.prevout.to_str()\n        prev_txid, index = prevout.split(':')\n        if spender_txid := self.wallet.adb.db.get_spent_outpoint(prev_txid, int(index)):\n            tx_mined_status = self.wallet.adb.get_tx_height(spender_txid)\n            if tx_mined_status.height() > 0:\n                return\n        self._unconfirmed_sweeps.add(txin.prevout)\n        self.logger.info(f'add_sweep_info: {sweep_info.name} {sweep_info.txin.prevout.to_str()}')\n        self.batch_inputs[txin.prevout] = sweep_info\n\n    @locked\n    def _to_pay_after(self, tx: Optional[PartialTransaction]) -> Sequence[PartialTxOutput]:\n        if not tx:\n            return self.batch_payments\n        # note: the below is equivalent to\n        #   to_pay = multiset(self.batch_payments) - multiset(tx.outputs())\n        to_pay = []\n        outputs = copy.deepcopy(tx.outputs())\n        for x in self.batch_payments:\n            if x not in outputs:\n                to_pay.append(x)\n            else:\n                outputs.remove(x)\n        return to_pay\n\n    @locked\n    def _to_sweep_after(self, tx: Optional[PartialTransaction]) -> Dict[TxOutpoint, SweepInfo]:\n        tx_prevouts = set(txin.prevout for txin in tx.inputs()) if tx else set()\n        result = []  # type: list[tuple[TxOutpoint, SweepInfo]]\n        for prevout, sweep_info in list(self.batch_inputs.items()):\n            assert prevout == sweep_info.txin.prevout\n            prev_txid, index = prevout.to_str().split(':')\n            if not (prev_tx := self.wallet.adb.db.get_transaction(prev_txid)):\n                continue\n            if sweep_info.is_anchor():\n                prev_tx_mined_status = self.wallet.adb.get_tx_height(prev_txid)\n                if prev_tx_mined_status.conf > 0:\n                    self.logger.info(f\"anchor not needed {prevout}\")\n                    self.batch_inputs.pop(prevout)  # note: if the input is already in a batch tx, this will trigger assert error\n                    continue\n                prev_tx_current_fee = self.wallet.adb.get_tx_fee(prev_txid)\n                try:\n                    prev_tx_target_fee = self.fee_policy.estimate_fee(\n                        prev_tx.estimated_size(),\n                        network=self.wallet.network,\n                    )\n                except NoDynamicFeeEstimates:\n                    prev_tx_target_fee = None\n                fees_available = prev_tx_current_fee and prev_tx_target_fee\n                if fees_available and prev_tx_current_fee > prev_tx_target_fee:\n                    self.logger.info(\n                        f\"not using anchor now, fee sufficient: \"\n                        f\"{prev_tx_current_fee=} > {prev_tx_target_fee=}\", only_once=True,\n                    )\n                    continue\n            if spender_txid := self.wallet.adb.db.get_spent_outpoint(prev_txid, int(index)):\n                tx_mined_status = self.wallet.adb.get_tx_height(spender_txid)\n                if tx_mined_status.height() not in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:\n                    continue\n            if prevout in tx_prevouts:\n                continue\n            result.append((prevout, sweep_info))\n        return dict(result)\n\n    def _should_bump_fee(self, base_tx: Optional[PartialTransaction]) -> bool:\n        if base_tx is None:\n            return False\n        if not self.is_mine(base_tx.txid()):\n            return False\n        base_tx_fee = base_tx.get_fee()\n        recommended_fee = self.fee_policy.estimate_fee(base_tx.estimated_size(), network=self.wallet.network)\n        should_bump_fee = base_tx_fee * 1.1 < recommended_fee\n        if should_bump_fee:\n            self.logger.info(f'base tx fee too low {base_tx_fee} < {recommended_fee}. we will bump the fee')\n        return should_bump_fee\n\n    def is_done(self):\n        # todo: require more than one confirmation\n        return len(self.batch_inputs) == 0 and len(self.batch_payments) == 0 and len(self._batch_txids) == 0\n\n    async def find_base_tx(self) -> Optional[PartialTransaction]:\n        if not self._prevout:\n            return None\n        prev_txid, index = self._prevout.split(':')\n        txid = self.wallet.adb.db.get_spent_outpoint(prev_txid, int(index))\n        tx = self.wallet.adb.get_transaction(txid) if txid else None\n        if not tx:\n            return None\n        tx = PartialTransaction.from_tx(tx)\n        tx.add_info_from_wallet(self.wallet)  # this sets is_change\n\n        if self.is_mine(txid):\n            if self._base_tx is None:\n                self.logger.info(f'found base_tx {txid}')\n            self._base_tx = tx\n        else:\n            self.logger.info(f'base tx was replaced by {tx.txid()}')\n            self._new_base_tx(tx)\n        # if tx is confirmed or local, we will start a new batch\n        tx_mined_status = self.wallet.adb.get_tx_height(txid)\n        if tx_mined_status.conf > 0:\n            self.logger.info(f'base tx confirmed {txid}')\n            self._clear_unconfirmed_sweeps(tx)\n            self._start_new_batch(tx)\n        if tx_mined_status.height() in [TX_HEIGHT_LOCAL]:\n            # this may happen if our Electrum server is unresponsive\n            # server could also be lying to us. Rebroadcasting might\n            # help, if we have switched to another server.\n            await self.wallet.network.try_broadcasting(tx, 'batch')\n\n        return self._base_tx\n\n    async def run_iteration(self) -> None:\n        base_tx = await self.find_base_tx()\n        try:\n            tx = self.create_next_transaction(base_tx)\n        except NoDynamicFeeEstimates:\n            self.logger.debug('no dynamic fee estimates available')\n            return\n        except Exception as e:\n            if base_tx:\n                self.logger.exception(f'Cannot create batch transaction: {repr(e)}')\n                self._start_new_batch(base_tx)\n                return\n            else:\n                # will be caught by txBatcher\n                raise\n\n        if tx is None:\n            # nothing to do\n            return\n\n        # add tx to wallet, in order to reserve utxos\n        # note: This saves the tx as local *unsigned*.\n        #       It will transition to local and signed, after we broadcast\n        #       the signed tx and get it back via the Synchronizer dance.\n        self.wallet.adb.add_transaction(tx)\n        # await password\n        if not await self.sign_transaction(tx):\n            self.wallet.adb.remove_transaction(tx.txid())\n            return\n\n        # save local base_tx\n        self._new_base_tx(tx)\n\n        if not await self.wallet.network.try_broadcasting(tx, 'batch'):\n            self.logger.info(f'cannot broadcast tx {tx.txid()}')\n            if base_tx:\n                # The most likely cause is that base_tx is not\n                # replaceable. This may be the case if it has children\n                # (because we don't pay enough fees to replace them)\n                # or if we are trying to sweep unconfirmed inputs\n                # (replacement-adds-unconfirmed error)\n\n                # it is OK to remove the transaction, because\n                # create_next_transaction will create a new tx that is\n                # incompatible with the one we remove here, so we\n                # cannot double pay.\n                self.wallet.adb.remove_transaction(tx.txid())\n                self.logger.info(f'starting new batch because could not broadcast')\n                self._start_new_batch(base_tx)\n            else:\n                # it is dangerous to remove the transaction if there\n                # is no base_tx. Indeed, the transaction might have\n                # been broadcast. So, we just keep the transaction as\n                # local, and we will try to rebroadcast it later (see\n                # above).\n                #\n                # FIXME: it should be possible to ensure that\n                # create_next_transaction creates transactions that\n                # spend the same coins, using self._prevout. This\n                # would make them incompatible, and safe to broadcast.\n                pass\n\n    async def sign_transaction(self, tx: PartialTransaction) -> Optional[PartialTransaction]:\n        tx.add_info_from_wallet(self.wallet)  # this adds input amounts\n        self.add_sweep_info_to_tx(tx)\n        pw_required = self.wallet.has_keystore_encryption() and tx.requires_keystore()\n        password = await self.wallet.txbatcher.get_password(tx.txid()) if pw_required else None\n        if password is None and pw_required:\n            return None\n        self.wallet.sign_transaction(tx, password)\n        assert tx.is_complete()\n        return tx\n\n    def create_next_transaction(self, base_tx: Optional[PartialTransaction]) -> Optional[PartialTransaction]:\n        to_pay = self._to_pay_after(base_tx)\n        to_sweep = self._to_sweep_after(base_tx)\n        to_sweep_now = []  # type: list[SweepInfo]\n        for k, v in to_sweep.items():\n            can_broadcast, wanted_height = self._can_broadcast(v, base_tx)\n            if can_broadcast:\n                to_sweep_now.append(v)\n            else:\n                self.wallet.add_future_tx(v, wanted_height)\n        while True:\n            if not to_pay and not to_sweep_now and not self._should_bump_fee(base_tx):\n                return None\n            try:\n                tx = self._create_batch_tx(base_tx=base_tx, to_sweep=to_sweep_now, to_pay=to_pay)\n            except NotEnoughFunds:\n                if to_pay:\n                    k = max(to_pay, key=lambda x: x.value)\n                    self.logger.info(f'Not enough funds, removing output {k}')\n                    to_pay.remove(k)\n                    continue\n                else:\n                    self.logger.info(f'Not enough funds, waiting')\n                    return None\n            # 100 kb max standardness rule\n            if tx.estimated_size() < 100_000:\n                break\n            to_sweep_now = to_sweep_now[0:len(to_sweep_now)//2]\n            to_pay = to_pay[0:len(to_pay)//2]\n\n        self.logger.info(f'created tx {tx.txid()} with {len(tx.inputs())} inputs and {len(tx.outputs())} outputs')\n        return tx\n\n    def add_sweep_info_to_tx(self, base_tx: PartialTransaction) -> None:\n        for txin in base_tx.inputs():\n            if sweep_info := self.batch_inputs.get(txin.prevout):\n                if hasattr(sweep_info.txin, 'make_witness'):\n                    txin.make_witness = sweep_info.txin.make_witness\n                    txin.privkey = sweep_info.txin.privkey\n                    txin.witness_script = sweep_info.txin.witness_script\n                    txin.script_sig = sweep_info.txin.script_sig\n\n    def _create_batch_tx(\n        self,\n        *,\n        base_tx: Optional[PartialTransaction],\n        to_sweep: Sequence[SweepInfo],\n        to_pay: Sequence[PartialTxOutput],\n    ) -> PartialTransaction:\n        self.logger.info(f'to_sweep: {[x.txin.prevout.to_str() for x in to_sweep]}')\n        self.logger.info(f'to_pay: {to_pay}')\n        inputs = []  # type: List[PartialTxInput]\n        outputs = []  # type: List[PartialTxOutput]\n        locktime = base_tx.locktime if base_tx else None\n        # sort inputs so that txin-txout pairs are first\n        for sweep_info in sorted(to_sweep, key=lambda x: not bool(x.txout)):\n            if sweep_info.cltv_abs is not None:\n                if locktime is None or locktime < sweep_info.cltv_abs:  # FIXME height vs timestamp confusion\n                    # nLockTime must be greater than or equal to the stack operand.\n                    locktime = sweep_info.cltv_abs\n            inputs.append(copy.deepcopy(sweep_info.txin))\n            if sweep_info.txout:\n                outputs.append(sweep_info.txout)\n        self.logger.info(f'locktime: {locktime}')\n        outputs += to_pay\n        inputs += self._create_inputs_from_tx_change(self._parent_tx) if self._parent_tx else []\n        # create tx\n        coins = self.wallet.get_spendable_coins(nonlocal_only=True)\n        tx = self.wallet.make_unsigned_transaction(\n            coins=coins,\n            fee_policy=self.fee_policy,\n            base_tx=base_tx,\n            inputs=inputs,\n            outputs=outputs,\n            locktime=locktime,\n            BIP69_sort=False,\n            merge_duplicate_outputs=False,\n        )\n        # this assert will fail if we merge duplicate outputs\n        for o in outputs: assert o in tx.outputs()\n        return tx\n\n    def _clear_unconfirmed_sweeps(self, tx: PartialTransaction) -> None:\n        # this ensures that we can accept an input again,\n        # in case the sweeping tx has been removed from the blockchain after a reorg\n        for txin in tx.inputs():\n            if txin.prevout in self._unconfirmed_sweeps:\n                self._unconfirmed_sweeps.remove(txin.prevout)\n\n    @locked\n    def _start_new_batch(self, tx: Optional[PartialTransaction]) -> None:\n        use_change = tx and tx.has_change() and any([txout in self.batch_payments for txout in tx.outputs()])\n        self.batch_payments = self._to_pay_after(tx)\n        self.batch_inputs = self._to_sweep_after(tx)\n        self._batch_txids.clear()\n        self._base_tx = None\n        self._parent_tx = tx if use_change else None\n        self._prevout = None\n\n    @locked\n    def _new_base_tx(self, tx: PartialTransaction) -> None:\n        self._prevout = tx.inputs()[0].prevout.to_str()\n        self.storage['prevout'] = self._prevout\n        if tx.has_change():\n            self._batch_txids.append(tx.txid())\n            self._base_tx = tx\n        else:\n            self.logger.info(f'starting new batch because current base tx does not have change')\n            self._start_new_batch(tx)\n\n    def _create_inputs_from_tx_change(self, parent_tx: PartialTransaction) -> List[PartialTxInput]:\n        inputs = []\n        for o in parent_tx.get_change_outputs():\n            coins = self.wallet.adb.get_addr_utxo(o.address)\n            inputs += list(coins.values())\n        for txin in inputs:\n            txin.nsequence = 0xffffffff - 2\n        return inputs\n\n    def _can_broadcast(self, sweep_info: 'SweepInfo', base_tx: 'Transaction') -> Tuple[bool, Optional[int]]:\n        prevout = sweep_info.txin.prevout.to_str()\n        name = sweep_info.name\n        prev_txid, index = prevout.split(':')\n        can_broadcast = True\n        wanted_height_cltv = None\n        wanted_height_csv = None\n        local_height = self.wallet.network.get_local_height()\n        if sweep_info.cltv_abs:\n            wanted_height_cltv = sweep_info.cltv_abs\n            if wanted_height_cltv - local_height > 0:\n                can_broadcast = False\n        prev_height = self.wallet.adb.get_tx_height(prev_txid).height()\n        if sweep_info.csv_delay:\n            if prev_height > 0:\n                wanted_height_csv = prev_height + sweep_info.csv_delay - 1\n                if wanted_height_csv - local_height > 0:\n                    can_broadcast = False\n            else:\n                can_broadcast = False\n                wanted_height_csv = local_height + sweep_info.csv_delay\n        if not can_broadcast:\n            wanted_height = max((wanted_height_csv or 0), (wanted_height_cltv or 0))\n        else:\n            wanted_height = None\n        if base_tx and prev_height <= 0:\n            # we cannot add unconfirmed inputs to existing base_tx (per RBF rules)\n            # thus, we will wait until the current batch is confirmed\n            if can_broadcast:\n                can_broadcast = False\n                wanted_height = local_height + 1\n        return can_broadcast, wanted_height\n\n"
  },
  {
    "path": "electrum/util.py",
    "content": "# Electrum - lightweight Bitcoin client\n# Copyright (C) 2011 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport concurrent.futures\nimport copy\nfrom dataclasses import dataclass\nimport logging\nimport os\nimport sys\nimport re\nfrom collections import defaultdict, OrderedDict\nfrom concurrent.futures.process import ProcessPoolExecutor\nfrom typing import (\n    NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any, Sequence, Dict, Generic, TypeVar, List, Iterable,\n    Set, Awaitable\n)\nfrom types import MappingProxyType\nfrom datetime import datetime, timezone, timedelta\nimport decimal\nfrom decimal import Decimal\nimport threading\nimport hmac\nimport hashlib\nimport stat\nimport asyncio\nimport builtins\nimport json\nimport time\nimport ssl\nimport ipaddress\nfrom ipaddress import IPv4Address, IPv6Address\nimport random\nimport secrets\nimport functools\nfrom functools import partial\nfrom abc import abstractmethod, ABC\nimport enum\nfrom contextlib import nullcontext, suppress\nimport traceback\nimport inspect\nimport weakref\n\nimport aiohttp\nfrom aiohttp_socks import ProxyConnector, ProxyType\nimport aiorpcx\nimport certifi\nimport dns.asyncresolver\n\nfrom .i18n import _\nfrom .logging import get_logger, Logger\n\nif TYPE_CHECKING:\n    from .network import Network, ProxySettings\n    from .interface import Interface\n    from .simple_config import SimpleConfig\n\n\n_logger = get_logger(__name__)\n\n\ndef inv_dict(d):\n    return {v: k for k, v in d.items()}\n\n\ndef all_subclasses(cls) -> Set:\n    \"\"\"Return all (transitive) subclasses of cls.\"\"\"\n    res = set(cls.__subclasses__())\n    for sub in res.copy():\n        res |= all_subclasses(sub)\n    return res\n\n\nca_path = certifi.where()\n\n\nbase_units = {'BTC':8, 'mBTC':5, 'bits':2, 'sat':0}\nbase_units_inverse = inv_dict(base_units)\nbase_units_list = ['BTC', 'mBTC', 'bits', 'sat']  # list(dict) does not guarantee order\n\nDECIMAL_POINT_DEFAULT = 5  # mBTC\n\n\nclass UnknownBaseUnit(Exception): pass\n\n\ndef decimal_point_to_base_unit_name(dp: int) -> str:\n    # e.g. 8 -> \"BTC\"\n    try:\n        return base_units_inverse[dp]\n    except KeyError:\n        raise UnknownBaseUnit(dp) from None\n\n\ndef base_unit_name_to_decimal_point(unit_name: str) -> int:\n    \"\"\"Returns the max number of digits allowed after the decimal point.\"\"\"\n    # e.g. \"BTC\" -> 8\n    try:\n        return base_units[unit_name]\n    except KeyError:\n        raise UnknownBaseUnit(unit_name) from None\n\ndef parse_max_spend(amt: Any) -> Optional[int]:\n    \"\"\"Checks if given amount is \"spend-max\"-like.\n    Returns None or the positive integer weight for \"max\". Never raises.\n\n    When creating invoices and on-chain txs, the user can specify to send \"max\".\n    This is done by setting the amount to '!'. Splitting max between multiple\n    tx outputs is also possible, and custom weights (positive ints) can also be used.\n    For example, to send 40% of all coins to address1, and 60% to address2:\n    ```\n    address1, 2!\n    address2, 3!\n    ```\n    \"\"\"\n    if not (isinstance(amt, str) and amt and amt[-1] == '!'):\n        return None\n    if amt == '!':\n        return 1\n    x = amt[:-1]\n    try:\n        x = int(x)\n    except ValueError:\n        return None\n    if x > 0:\n        return x\n    return None\n\nclass NotEnoughFunds(Exception):\n    def __str__(self):\n        return _(\"Insufficient funds\")\n\n\nclass UneconomicFee(Exception):\n    def __str__(self):\n        return _(\"The fee for the transaction is higher than the funds gained from it.\")\n\n\nclass NoDynamicFeeEstimates(Exception):\n    def __str__(self):\n        return _('Dynamic fee estimates not available')\n\n\nclass BelowDustLimit(Exception):\n    pass\n\n\nclass InvalidPassword(Exception):\n    def __init__(self, message: Optional[str] = None):\n        self.message = message\n\n    def __str__(self):\n        if self.message is None:\n            return _(\"Incorrect password\")\n        else:\n            return str(self.message)\n\n\nclass AddTransactionException(Exception):\n    pass\n\n\nclass UnrelatedTransactionException(AddTransactionException):\n    def __str__(self):\n        return _(\"Transaction is unrelated to this wallet.\")\n\n\nclass FileImportFailed(Exception):\n    def __init__(self, message=''):\n        self.message = str(message)\n\n    def __str__(self):\n        return _(\"Failed to import from file.\") + \"\\n\" + self.message\n\n\nclass FileExportFailed(Exception):\n    def __init__(self, message=''):\n        self.message = str(message)\n\n    def __str__(self):\n        return _(\"Failed to export to file.\") + \"\\n\" + self.message\n\n\nclass WalletFileException(Exception):\n    def __init__(self, message='', *, should_report_crash: bool = False):\n        Exception.__init__(self, message)\n        self.should_report_crash = should_report_crash\n\n\nclass BitcoinException(Exception): pass\n\n\nclass UserFacingException(Exception):\n    \"\"\"Exception that contains information intended to be shown to the user.\"\"\"\n\n\nclass InvoiceError(UserFacingException): pass\n\n\nclass NetworkOfflineException(UserFacingException):\n    \"\"\"Can be raised if we are running in offline mode (--offline flag)\n    and the user requests an operation that requires the network.\n    \"\"\"\n    def __str__(self):\n        return _(\"You are offline.\")\n\n\n# Throw this exception to unwind the stack like when an error occurs.\n# However unlike other exceptions the user won't be informed.\nclass UserCancelled(Exception):\n    '''An exception that is suppressed from the user'''\n    pass\n\n\ndef to_decimal(x: Union[str, float, int, Decimal]) -> Decimal:\n    # helper function mainly for float->Decimal conversion, i.e.:\n    #   >>> Decimal(41754.681)\n    #   Decimal('41754.680999999996856786310672760009765625')\n    #   >>> Decimal(\"41754.681\")\n    #   Decimal('41754.681')\n    if isinstance(x, Decimal):\n        return x\n    if isinstance(x, int):\n        return Decimal(x)\n    return Decimal(str(x))\n\n\n# note: this is not a NamedTuple as then its json encoding cannot be customized\nclass Satoshis(object):\n    __slots__ = ('value',)\n\n    def __new__(cls, value):\n        self = super(Satoshis, cls).__new__(cls)\n        # note: 'value' sometimes has msat precision\n        assert isinstance(value, (int, Decimal)), f\"unexpected type for {value=!r}\"\n        self.value = value\n        return self\n\n    def __repr__(self):\n        return f'Satoshis({self.value})'\n\n    def __str__(self):\n        # note: precision is truncated to satoshis here\n        return format_satoshis(self.value)\n\n    def __eq__(self, other):\n        return self.value == other.value\n\n    def __ne__(self, other):\n        return not (self == other)\n\n    def __add__(self, other):\n        return Satoshis(self.value + other.value)\n\n\n# note: this is not a NamedTuple as then its json encoding cannot be customized\nclass Fiat(object):\n    __slots__ = ('value', 'ccy')\n\n    def __new__(cls, value: Optional[Decimal], ccy: str):\n        self = super(Fiat, cls).__new__(cls)\n        self.ccy = ccy\n        if not isinstance(value, (Decimal, type(None))):\n            raise TypeError(f\"value should be Decimal or None, not {type(value)}\")\n        self.value = value\n        return self\n\n    def __repr__(self):\n        return 'Fiat(%s)'% self.__str__()\n\n    def __str__(self):\n        if self.value is None or self.value.is_nan():\n            return _('No Data')\n        else:\n            return \"{:.2f}\".format(self.value)\n\n    def to_ui_string(self):\n        if self.value is None or self.value.is_nan():\n            return _('No Data')\n        else:\n            return \"{:.2f}\".format(self.value) + ' ' + self.ccy\n\n    def __eq__(self, other):\n        if not isinstance(other, Fiat):\n            return False\n        if self.ccy != other.ccy:\n            return False\n        if isinstance(self.value, Decimal) and isinstance(other.value, Decimal) \\\n                and self.value.is_nan() and other.value.is_nan():\n            return True\n        return self.value == other.value\n\n    def __ne__(self, other):\n        return not (self == other)\n\n    def __add__(self, other):\n        assert self.ccy == other.ccy\n        return Fiat(self.value + other.value, self.ccy)\n\n\nclass MyEncoder(json.JSONEncoder):\n    def default(self, obj):\n        # note: this does not get called for namedtuples :(  https://bugs.python.org/issue30343\n        from .transaction import Transaction, TxOutput\n        if isinstance(obj, Transaction):\n            return obj.serialize()\n        if isinstance(obj, TxOutput):\n            return obj.to_legacy_tuple()\n        if isinstance(obj, Satoshis):\n            return str(obj)\n        if isinstance(obj, Fiat):\n            return str(obj)\n        if isinstance(obj, Decimal):\n            return str(obj)\n        if isinstance(obj, datetime):\n            # note: if there is a timezone specified, this will include the offset\n            return obj.isoformat(' ', timespec=\"minutes\")\n        if isinstance(obj, (set, frozenset)):\n            return list(obj)\n        if isinstance(obj, bytes): # for nametuples in lnchannel\n            return obj.hex()\n        if hasattr(obj, 'to_json') and callable(obj.to_json):\n            return obj.to_json()\n        return super(MyEncoder, self).default(obj)\n\n\nclass ThreadJob(Logger):\n    \"\"\"A job that is run periodically from a thread's main loop.  run() is\n    called from that thread's context.\n    \"\"\"\n\n    def __init__(self):\n        Logger.__init__(self)\n\n    def run(self):\n        \"\"\"Called periodically from the thread\"\"\"\n        pass\n\n\nclass DaemonThread(threading.Thread, Logger):\n    \"\"\" daemon thread that terminates cleanly \"\"\"\n\n    def __init__(self):\n        threading.Thread.__init__(self)\n        Logger.__init__(self)\n        self.parent_thread = threading.current_thread()\n        self.running = False\n        self.running_lock = threading.Lock()\n        self.job_lock = threading.Lock()\n        self.jobs = []\n        self.stopped_event = threading.Event()        # set when fully stopped\n        self.stopped_event_async = asyncio.Event()    # set when fully stopped\n        self.wake_up_event = threading.Event()  # for perf optimisation of polling in run()\n\n    def add_jobs(self, jobs):\n        with self.job_lock:\n            self.jobs.extend(jobs)\n\n    def run_jobs(self):\n        # Don't let a throwing job disrupt the thread, future runs of\n        # itself, or other jobs.  This is useful protection against\n        # malformed or malicious server responses\n        with self.job_lock:\n            for job in self.jobs:\n                start = time.perf_counter()\n                try:\n                    job.run()\n                except Exception as e:\n                    self.logger.exception('')\n                duration = time.perf_counter() - start\n                if duration > 0.5:\n                    self.logger.warning(f\"thread job {job} blocked {self} DaemonThread for {duration:.2f} s\")\n\n    def remove_jobs(self, jobs):\n        with self.job_lock:\n            for job in jobs:\n                self.jobs.remove(job)\n\n    def start(self):\n        with self.running_lock:\n            self.running = True\n        return threading.Thread.start(self)\n\n    def is_running(self):\n        with self.running_lock:\n            return self.running and self.parent_thread.is_alive()\n\n    def stop(self):\n        with self.running_lock:\n            self.running = False\n            self.wake_up_event.set()\n            self.wake_up_event.clear()\n\n    def on_stop(self):\n        if 'ANDROID_DATA' in os.environ:\n            import jnius\n            jnius.detach()\n            self.logger.info(\"jnius detach\")\n        self.logger.info(\"stopped\")\n        self.stopped_event.set()\n        loop = get_asyncio_loop()\n        loop.call_soon_threadsafe(self.stopped_event_async.set)\n\n\ndef print_stderr(*args):\n    args = [str(item) for item in args]\n    sys.stderr.write(\" \".join(args) + \"\\n\")\n    sys.stderr.flush()\n\n\ndef print_msg(*args):\n    # Stringify args\n    args = [str(item) for item in args]\n    sys.stdout.write(\" \".join(args) + \"\\n\")\n    sys.stdout.flush()\n\n\ndef json_encode(obj):\n    try:\n        s = json.dumps(obj, sort_keys = True, indent = 4, cls=MyEncoder)\n    except TypeError:\n        s = repr(obj)\n    return s\n\n\ndef json_decode(x):\n    try:\n        return json.loads(x, parse_float=Decimal)\n    except Exception:\n        return x\n\n\ndef json_normalize(x):\n    # note: The return value of commands, when going through the JSON-RPC interface,\n    #       is json-encoded. The encoder used there cannot handle some types, e.g. electrum.util.Satoshis.\n    # note: We should not simply do \"json_encode(x)\" here, as then later x would get doubly json-encoded.\n    # see #5868\n    return json_decode(json_encode(x))\n\n\n# taken from Django Source Code\ndef constant_time_compare(val1, val2):\n    \"\"\"Return True if the two strings are equal, False otherwise.\"\"\"\n    return hmac.compare_digest(to_bytes(val1, 'utf8'), to_bytes(val2, 'utf8'))\n\n\n_profiler_logger = _logger.getChild('profiler')\n\n\ndef profiler(func=None, *, min_threshold: Union[int, float, None] = None):\n    \"\"\"Function decorator that logs execution time.\n\n    min_threshold: if set, only log if time taken is higher than threshold\n    \"\"\"\n    if func is None:  # to make \"@profiler(...)\" work. (in addition to bare \"@profiler\")\n        return partial(profiler, min_threshold=min_threshold)\n    t0 = None  # type: Optional[float]\n\n    def timer_start():\n        nonlocal t0\n        t0 = time.time()\n\n    def timer_done():\n        t = time.time() - t0\n        if min_threshold is None or t > min_threshold:\n            _profiler_logger.debug(f\"{func.__qualname__} {t:,.4f} sec\")\n\n    if inspect.iscoroutinefunction(func):\n        async def do_profile(*args, **kw_args):\n            timer_start()\n            o = await func(*args, **kw_args)\n            timer_done()\n            return o\n    else:\n        def do_profile(*args, **kw_args):\n            timer_start()\n            o = func(*args, **kw_args)\n            timer_done()\n            return o\n    return do_profile\n\n\nclass AsyncHangDetector:\n    \"\"\"Context manager that logs every `n` seconds if encapsulated context still has not exited.\"\"\"\n\n    def __init__(\n        self,\n        *,\n        period_sec: int = 15,\n        message: str,\n        logger: logging.Logger = None,\n    ):\n        self.period_sec = period_sec\n        self.message = message\n        self.logger = logger or _logger\n\n    async def _monitor(self):\n        # note: this assumes that the event loop itself is not blocked\n        t0 = time.monotonic()\n        while True:\n            await asyncio.sleep(self.period_sec)\n            t1 = time.monotonic()\n            self.logger.info(f\"{self.message} (after {t1 - t0:.2f} sec)\")\n\n    async def __aenter__(self):\n        self.mtask = asyncio.create_task(self._monitor())\n\n    async def __aexit__(self, exc_type, exc, tb):\n        self.mtask.cancel()\n\n\ndef android_ext_dir():\n    from android.storage import primary_external_storage_path\n    return primary_external_storage_path()\n\n\ndef android_backup_dir():\n    pkgname = get_android_package_name()\n    d = os.path.join(android_ext_dir(), pkgname)\n    if not os.path.exists(d):\n        os.mkdir(d)\n    return d\n\n\ndef android_data_dir():\n    import jnius\n    PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity')\n    return PythonActivity.mActivity.getFilesDir().getPath() + '/data'\n\n\ndef ensure_sparse_file(filename):\n    # On modern Linux, no need to do anything.\n    # On Windows, need to explicitly mark file.\n    if os.name == \"nt\":\n        try:\n            os.system('fsutil sparse setflag \"{}\" 1'.format(filename))\n        except Exception as e:\n            _logger.info(f'error marking file {filename} as sparse: {e}')\n\n\ndef get_headers_dir(config):\n    return config.path\n\n\ndef assert_datadir_available(config_path):\n    path = config_path\n    if os.path.exists(path):\n        return\n    else:\n        raise FileNotFoundError(\n            'Electrum datadir does not exist. Was it deleted while running?' + '\\n' +\n            'Should be at {}'.format(path))\n\n\ndef assert_file_in_datadir_available(path, config_path):\n    if os.path.exists(path):\n        return\n    else:\n        assert_datadir_available(config_path)\n        raise FileNotFoundError(\n            'Cannot find file but datadir is there.' + '\\n' +\n            'Should be at {}'.format(path))\n\n\ndef standardize_path(path):\n    # note: os.path.realpath() is not used, as on Windows it can return non-working paths (see #8495).\n    #       This means that we don't resolve symlinks!\n    return os.path.normcase(\n                os.path.abspath(\n                    os.path.expanduser(\n                        path\n    )))\n\n\ndef get_new_wallet_name(wallet_folder: str) -> str:\n    \"\"\"Returns a file basename for a new wallet to be used.\n    Can raise OSError.\n    \"\"\"\n    i = 1\n    while True:\n        filename = \"wallet_%d\" % i\n        if filename in os.listdir(wallet_folder):\n            i += 1\n        else:\n            break\n    return filename\n\n\ndef is_android_debug_apk() -> bool:\n    is_android = 'ANDROID_DATA' in os.environ\n    if not is_android:\n        return False\n    from jnius import autoclass\n    pkgname = get_android_package_name()\n    build_config = autoclass(f\"{pkgname}.BuildConfig\")\n    return bool(build_config.DEBUG)\n\n\ndef get_android_package_name() -> str:\n    is_android = 'ANDROID_DATA' in os.environ\n    assert is_android\n    from jnius import autoclass\n    from android.config import ACTIVITY_CLASS_NAME\n    activity = autoclass(ACTIVITY_CLASS_NAME).mActivity\n    pkgname = str(activity.getPackageName())\n    return pkgname\n\n\ndef assert_bytes(*args):\n    \"\"\"\n    porting helper, assert args type\n    \"\"\"\n    try:\n        for x in args:\n            assert isinstance(x, (bytes, bytearray))\n    except Exception:\n        print('assert bytes failed', list(map(type, args)))\n        raise\n\n\ndef assert_str(*args):\n    \"\"\"\n    porting helper, assert args type\n    \"\"\"\n    for x in args:\n        assert isinstance(x, str)\n\n\ndef to_string(x, enc) -> str:\n    if isinstance(x, (bytes, bytearray)):\n        return x.decode(enc)\n    if isinstance(x, str):\n        return x\n    else:\n        raise TypeError(\"Not a string or bytes like object\")\n\n\ndef to_bytes(something, encoding='utf8') -> bytes:\n    \"\"\"\n    cast string to bytes() like object, but for python2 support it's bytearray copy\n    \"\"\"\n    if isinstance(something, bytes):\n        return something\n    if isinstance(something, str):\n        return something.encode(encoding)\n    elif isinstance(something, bytearray):\n        return bytes(something)\n    else:\n        raise TypeError(\"Not a string or bytes like object\")\n\n\nbfh = bytes.fromhex\n\n\ndef xor_bytes(a: bytes, b: bytes) -> bytes:\n    size = min(len(a), len(b))\n    return ((int.from_bytes(a[:size], \"big\") ^ int.from_bytes(b[:size], \"big\"))\n            .to_bytes(size, \"big\"))\n\n\ndef user_dir():\n    if \"ELECTRUMDIR\" in os.environ:\n        return os.environ[\"ELECTRUMDIR\"]\n    elif 'ANDROID_DATA' in os.environ:\n        return android_data_dir()\n    elif os.name == 'posix':\n        return os.path.join(os.environ[\"HOME\"], \".electrum\")\n    elif \"APPDATA\" in os.environ:\n        return os.path.join(os.environ[\"APPDATA\"], \"Electrum\")\n    elif \"LOCALAPPDATA\" in os.environ:\n        return os.path.join(os.environ[\"LOCALAPPDATA\"], \"Electrum\")\n    else:\n        #raise Exception(\"No home directory found in environment variables.\")\n        return\n\n\ndef resource_path(*parts):\n    return os.path.join(pkg_dir, *parts)\n\n\n# absolute path to python package folder of electrum (\"lib\")\npkg_dir = os.path.split(os.path.realpath(__file__))[0]\n\n\ndef is_valid_email(s):\n    regexp = r\"[^@]+@[^@]+\\.[^@]+\"\n    return re.match(regexp, s) is not None\n\n\ndef is_valid_websocket_url(url: str) -> bool:\n    \"\"\"\n    uses this django url validation regex:\n    https://github.com/django/django/blob/2c6906a0c4673a7685817156576724aba13ad893/django/core/validators.py#L45C1-L52C43\n    Note: this is not perfect, urls and their parsing can get very complex (see recent django code).\n    however its sufficient for catching weird user input in the gui dialog\n    \"\"\"\n    # stores the compiled regex in the function object itself to avoid recompiling it every call\n    if not hasattr(is_valid_websocket_url, \"regex\"):\n        is_valid_websocket_url.regex = re.compile(\n            r'^(?:ws|wss)://'  # ws:// or wss://\n            r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+(?:[A-Z]{2,6}\\.?|[A-Z0-9-]{2,}\\.?)|'  # domain...\n            r'localhost|'  # localhost...\n            r'\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|'  # ...or ipv4\n            r'\\[?[A-F0-9]*:[A-F0-9:]+\\]?)'  # ...or ipv6\n            r'(?::\\d+)?'  # optional port\n            r'(?:/?|[/?]\\S+)$', re.IGNORECASE)\n    try:\n        return re.match(is_valid_websocket_url.regex, url) is not None\n    except Exception:\n        return False\n\n\ndef is_hash256_str(text: Any) -> bool:\n    if not isinstance(text, str): return False\n    if len(text) != 64: return False\n    return is_hex_str(text)\n\n\ndef is_hex_str(text: Any) -> bool:\n    if not isinstance(text, str): return False\n    try:\n        b = bytes.fromhex(text)\n    except Exception:\n        return False\n    # forbid whitespaces in text:\n    if len(text) != 2 * len(b):\n        return False\n    return True\n\n\ndef is_integer(val: Any) -> bool:\n    return isinstance(val, int)\n\n\ndef is_non_negative_integer(val: Any) -> bool:\n    if is_integer(val):\n        return val >= 0\n    return False\n\n\ndef is_int_or_float(val: Any) -> bool:\n    return isinstance(val, (int, float))\n\n\ndef is_non_negative_int_or_float(val: Any) -> bool:\n    if is_int_or_float(val):\n        return val >= 0\n    return False\n\n\ndef chunks(items, size: int):\n    \"\"\"Break up items, an iterable, into chunks of length size.\"\"\"\n    if size < 1:\n        raise ValueError(f\"size must be positive, not {repr(size)}\")\n    for i in range(0, len(items), size):\n        yield items[i: i + size]\n\n\ndef format_satoshis_plain(\n        x: Union[int, float, Decimal, str],  # amount in satoshis,\n        *,\n        decimal_point: int = 8,  # how much to shift decimal point to left (default: sat->BTC)\n        is_max_allowed: bool = True,\n) -> str:\n    \"\"\"Display a satoshi amount scaled.  Always uses a '.' as a decimal\n    point and has no thousands separator\"\"\"\n    if is_max_allowed and parse_max_spend(x):\n        return f'max({x})'\n    assert isinstance(x, (int, float, Decimal)), f\"{x!r} should be a number\"\n    # TODO(ghost43) just hard-fail if x is a float. do we even use floats for money anywhere?\n    x = to_decimal(x)\n    scale_factor = pow(10, decimal_point)\n    return \"{:.8f}\".format(x / scale_factor).rstrip('0').rstrip('.')\n\n\n# Check that Decimal precision is sufficient.\n# We need at the very least ~20, as we deal with msat amounts, and\n# log10(21_000_000 * 10**8 * 1000) ~= 18.3\n# decimal.DefaultContext.prec == 28 by default, but it is mutable.\n# We enforce that we have at least that available.\nassert decimal.getcontext().prec >= 28, f\"PyDecimal precision too low: {decimal.getcontext().prec}\"\n\n# DECIMAL_POINT = locale.localeconv()['decimal_point']  # type: str\nDECIMAL_POINT = \".\"\nTHOUSANDS_SEP = \" \"\nassert len(DECIMAL_POINT) == 1, f\"DECIMAL_POINT has unexpected len. {DECIMAL_POINT!r}\"\nassert len(THOUSANDS_SEP) == 1, f\"THOUSANDS_SEP has unexpected len. {THOUSANDS_SEP!r}\"\n\n\ndef format_satoshis(\n        x: Union[int, float, Decimal, str, None],  # amount in satoshis\n        *,\n        num_zeros: int = 0,\n        decimal_point: int = 8,  # how much to shift decimal point to left (default: sat->BTC)\n        precision: int = 0,  # extra digits after satoshi precision\n        is_diff: bool = False,  # if True, enforce a leading sign (+/-)\n        whitespaces: bool = False,  # if True, add whitespaces, to align numbers in a column\n        add_thousands_sep: bool = False,  # if True, add whitespaces, for better readability of the numbers\n) -> str:\n    if x is None:\n        return 'unknown'\n    if parse_max_spend(x):\n        return f'max({x})'\n    assert isinstance(x, (int, float, Decimal)), f\"{x!r} should be a number\"\n    # TODO(ghost43) just hard-fail if x is a float. do we even use floats for money anywhere?\n    x = to_decimal(x)\n    # lose redundant precision\n    x = x.quantize(Decimal(10) ** (-precision))\n    # format string\n    overall_precision = decimal_point + precision  # max digits after final decimal point\n    decimal_format = \".\" + str(overall_precision) if overall_precision > 0 else \"\"\n    if is_diff:\n        decimal_format = '+' + decimal_format\n    # initial result\n    scale_factor = pow(10, decimal_point)\n    result = (\"{:\" + decimal_format + \"f}\").format(x / scale_factor)\n    if \".\" not in result: result += \".\"\n    result = result.rstrip('0')\n    # add extra decimal places (zeros)\n    integer_part, fract_part = result.split(\".\")\n    if len(fract_part) < num_zeros:\n        fract_part += \"0\" * (num_zeros - len(fract_part))\n    # add whitespaces as thousands' separator for better readability of numbers\n    if add_thousands_sep:\n        sign = integer_part[0] if integer_part[0] in (\"+\", \"-\") else \"\"\n        if sign == \"-\":\n            integer_part = integer_part[1:]\n        integer_part = \"{:,}\".format(int(integer_part)).replace(',', THOUSANDS_SEP)\n        integer_part = sign + integer_part\n        fract_part = THOUSANDS_SEP.join(fract_part[i:i+3] for i in range(0, len(fract_part), 3))\n    result = integer_part + DECIMAL_POINT + fract_part\n    # add leading/trailing whitespaces so that numbers can be aligned in a column\n    if whitespaces:\n        target_fract_len = overall_precision\n        target_integer_len = 14 - decimal_point  # should be enough for up to unsigned 999999 BTC\n        if add_thousands_sep:\n            target_fract_len += max(0, (target_fract_len - 1) // 3)\n            target_integer_len += max(0, (target_integer_len - 1) // 3)\n        # add trailing whitespaces\n        result += \" \" * (target_fract_len - len(fract_part))\n        # add leading whitespaces\n        target_total_len = target_integer_len + 1 + target_fract_len\n        result = \" \" * (target_total_len - len(result)) + result\n    return result\n\n\nFEERATE_PRECISION = 1  # num fractional decimal places for sat/byte fee rates\n_feerate_quanta = Decimal(10) ** (-FEERATE_PRECISION)\nUI_UNIT_NAME_FEERATE_SAT_PER_VBYTE = \"sat/vbyte\"\nUI_UNIT_NAME_FEERATE_SAT_PER_VB = \"sat/vB\"\nUI_UNIT_NAME_TXSIZE_VBYTES = \"vbytes\"\nUI_UNIT_NAME_MEMPOOL_MB = \"vMB\"\nUI_UNIT_NAME_FIXED_SAT = \"sat\"\n\n\ndef format_fee_satoshis(fee, *, num_zeros=0, precision=None):\n    if precision is None:\n        precision = FEERATE_PRECISION\n    num_zeros = min(num_zeros, FEERATE_PRECISION)  # no more zeroes than available prec\n    return format_satoshis(fee, num_zeros=num_zeros, decimal_point=0, precision=precision)\n\n\ndef quantize_feerate(fee) -> Union[None, Decimal, int]:\n    \"\"\"Strip sat/byte fee rate of excess precision.\"\"\"\n    if fee is None:\n        return None\n    return Decimal(fee).quantize(_feerate_quanta, rounding=decimal.ROUND_HALF_DOWN)\n\n\nDEFAULT_TIMEZONE = None  # type: timezone | None  # None means local OS timezone\ndef timestamp_to_datetime(timestamp: Union[int, float, None], *, utc: bool = False) -> Optional[datetime]:\n    if timestamp is None:\n        return None\n    tz = DEFAULT_TIMEZONE\n    if utc:\n        tz = timezone.utc\n    return datetime.fromtimestamp(timestamp, tz=tz)\n\n\ndef format_time(timestamp: Union[int, float, None]) -> str:\n    date = timestamp_to_datetime(timestamp)\n    return date.isoformat(' ', timespec=\"minutes\") if date else _(\"Unknown\")\n\n\ndef age(\n    from_date: Union[int, float, None],  # POSIX timestamp\n    *,\n    since_date: datetime = None,\n    target_tz=None,\n    include_seconds: bool = False,\n) -> str:\n    \"\"\"Takes a timestamp and returns a string with the approximation of the age\"\"\"\n    if from_date is None:\n        return _(\"Unknown\")\n    from_date = datetime.fromtimestamp(from_date)\n    if since_date is None:\n        since_date = datetime.now(target_tz)\n    distance_in_time = from_date - since_date\n    is_in_past = from_date < since_date\n    s = delta_time_str(distance_in_time, include_seconds=include_seconds)\n    return _(\"{} ago\").format(s) if is_in_past else _(\"in {}\").format(s)\n\n\ndef delta_time_str(distance_in_time: timedelta, *, include_seconds: bool = False) -> str:\n    distance_in_seconds = int(round(abs(distance_in_time.days * 86400 + distance_in_time.seconds)))\n    distance_in_minutes = int(round(distance_in_seconds / 60))\n    if distance_in_minutes == 0:\n        if include_seconds:\n            return _(\"{} seconds\").format(distance_in_seconds)\n        else:\n            return _(\"less than a minute\")\n    elif distance_in_minutes < 45:\n        return _(\"about {} minutes\").format(distance_in_minutes)\n    elif distance_in_minutes < 90:\n        return _(\"about 1 hour\")\n    elif distance_in_minutes < 1440:\n        return _(\"about {} hours\").format(round(distance_in_minutes / 60.0))\n    elif distance_in_minutes < 2880:\n        return _(\"about 1 day\")\n    elif distance_in_minutes < 43220:\n        return _(\"about {} days\").format(round(distance_in_minutes / 1440))\n    elif distance_in_minutes < 86400:\n        return _(\"about 1 month\")\n    elif distance_in_minutes < 525600:\n        return _(\"about {} months\").format(round(distance_in_minutes / 43200))\n    elif distance_in_minutes < 1051200:\n        return _(\"about 1 year\")\n    else:\n        return _(\"over {} years\").format(round(distance_in_minutes / 525600))\n\n\nmainnet_block_explorers = {\n    '3xpl.com': ('https://3xpl.com/bitcoin/',\n                        {'tx': 'transaction/', 'addr': 'address/'}),\n    'Bitflyer.jp': ('https://chainflyer.bitflyer.jp/',\n                        {'tx': 'Transaction/', 'addr': 'Address/'}),\n    'Blockchain.info': ('https://blockchain.com/btc/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'Blockstream.info': ('https://blockstream.info/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'Bitaps.com': ('https://btc.bitaps.com/',\n                        {'tx': '', 'addr': ''}),\n    'BTC.com': ('https://btc.com/',\n                        {'tx': '', 'addr': ''}),\n    'Chain.so': ('https://www.chain.so/',\n                        {'tx': 'tx/BTC/', 'addr': 'address/BTC/'}),\n    'Insight.is': ('https://insight.bitpay.com/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'BlockCypher.com': ('https://live.blockcypher.com/btc/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'Blockchair.com': ('https://blockchair.com/bitcoin/',\n                        {'tx': 'transaction/', 'addr': 'address/'}),\n    'blockonomics.co': ('https://www.blockonomics.co/',\n                        {'tx': 'api/tx?txid=', 'addr': '#/search?q='}),\n    'mempool.space': ('https://mempool.space/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'mempool.emzy.de': ('https://mempool.emzy.de/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'OXT.me': ('https://oxt.me/',\n                        {'tx': 'transaction/', 'addr': 'address/'}),\n    'mynode.local': ('http://mynode.local:3002/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'system default': ('blockchain:/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n}\n\ntestnet_block_explorers = {\n    'Bitaps.com': ('https://tbtc.bitaps.com/',\n                       {'tx': '', 'addr': ''}),\n    'BlockCypher.com': ('https://live.blockcypher.com/btc-testnet/',\n                       {'tx': 'tx/', 'addr': 'address/'}),\n    'Blockchain.info': ('https://www.blockchain.com/btc-testnet/',\n                       {'tx': 'tx/', 'addr': 'address/'}),\n    'Blockstream.info': ('https://blockstream.info/testnet/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'mempool.space': ('https://mempool.space/testnet/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'smartbit.com.au': ('https://testnet.smartbit.com.au/',\n                       {'tx': 'tx/', 'addr': 'address/'}),\n    'system default': ('blockchain://000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943/',\n                       {'tx': 'tx/', 'addr': 'address/'}),\n}\n\ntestnet4_block_explorers = {\n    'mempool.space': ('https://mempool.space/testnet4/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'wakiyamap.dev': ('https://testnet4-explorer.wakiyamap.dev/',\n                       {'tx': 'tx/', 'addr': 'address/'}),\n}\n\nsignet_block_explorers = {\n    'bc-2.jp': ('https://explorer.bc-2.jp/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'mempool.space': ('https://mempool.space/signet/',\n                        {'tx': 'tx/', 'addr': 'address/'}),\n    'bitcoinexplorer.org': ('https://signet.bitcoinexplorer.org/',\n                       {'tx': 'tx/', 'addr': 'address/'}),\n    'wakiyamap.dev': ('https://signet-explorer.wakiyamap.dev/',\n                       {'tx': 'tx/', 'addr': 'address/'}),\n    'ex.signet.bublina.eu.org': ('https://ex.signet.bublina.eu.org/',\n                       {'tx': 'tx/', 'addr': 'address/'}),\n    'system default': ('blockchain:/',\n                       {'tx': 'tx/', 'addr': 'address/'}),\n}\n\n_block_explorer_default_api_loc = {'tx': 'tx/', 'addr': 'address/'}\n\n\ndef block_explorer_info():\n    from . import constants\n    if constants.net.NET_NAME == \"testnet\":\n        return testnet_block_explorers\n    elif constants.net.NET_NAME == \"testnet4\":\n        return testnet4_block_explorers\n    elif constants.net.NET_NAME == \"signet\":\n        return signet_block_explorers\n    return mainnet_block_explorers\n\n\ndef block_explorer(config: 'SimpleConfig') -> Optional[str]:\n    \"\"\"Returns name of selected block explorer,\n    or None if a custom one (not among hardcoded ones) is configured.\n    \"\"\"\n    if config.BLOCK_EXPLORER_CUSTOM is not None:\n        return None\n    be_key = config.BLOCK_EXPLORER\n    be_tuple = block_explorer_info().get(be_key)\n    if be_tuple is None:\n        be_key = config.cv.BLOCK_EXPLORER.get_default_value()\n    assert isinstance(be_key, str), f\"{be_key!r} should be str\"\n    return be_key\n\n\ndef block_explorer_tuple(config: 'SimpleConfig') -> Optional[Tuple[str, dict]]:\n    custom_be = config.BLOCK_EXPLORER_CUSTOM\n    if custom_be:\n        if isinstance(custom_be, str):\n            return custom_be, _block_explorer_default_api_loc\n        if isinstance(custom_be, (tuple, list)) and len(custom_be) == 2:\n            return tuple(custom_be)\n        _logger.warning(f\"not using {config.cv.BLOCK_EXPLORER_CUSTOM.key()!r} from config. \"\n                        f\"expected a str or a pair but got {custom_be!r}\")\n        return None\n    else:\n        # using one of the hardcoded block explorers\n        return block_explorer_info().get(block_explorer(config))\n\n\ndef block_explorer_URL(config: 'SimpleConfig', kind: str, item: str) -> Optional[str]:\n    be_tuple = block_explorer_tuple(config)\n    if not be_tuple:\n        return\n    explorer_url, explorer_dict = be_tuple\n    kind_str = explorer_dict.get(kind)\n    if kind_str is None:\n        return\n    if explorer_url[-1] != \"/\":\n        explorer_url += \"/\"\n    url_parts = [explorer_url, kind_str, item]\n    return ''.join(url_parts)\n\n\n# Python bug (http://bugs.python.org/issue1927) causes raw_input\n# to be redirected improperly between stdin/stderr on Unix systems\n#TODO: py3\ndef raw_input(prompt=None):\n    if prompt:\n        sys.stdout.write(prompt)\n    return builtin_raw_input()\n\n\nbuiltin_raw_input = builtins.input\nbuiltins.input = raw_input\n\n\ndef parse_json(message):\n    # TODO: check \\r\\n pattern\n    n = message.find(b'\\n')\n    if n == -1:\n        return None, message\n    try:\n        j = json.loads(message[0:n].decode('utf8'))\n    except Exception:\n        j = None\n    return j, message[n+1:]\n\n\ndef setup_thread_excepthook():\n    \"\"\"\n    Workaround for `sys.excepthook` thread bug from:\n    http://bugs.python.org/issue1230540\n\n    Call once from the main thread before creating any threads.\n    \"\"\"\n\n    init_original = threading.Thread.__init__\n\n    def init(self, *args, **kwargs):\n\n        init_original(self, *args, **kwargs)\n        run_original = self.run\n\n        def run_with_except_hook(*args2, **kwargs2):\n            try:\n                run_original(*args2, **kwargs2)\n            except Exception:\n                sys.excepthook(*sys.exc_info())\n\n        self.run = run_with_except_hook\n\n    threading.Thread.__init__ = init\n\n\ndef send_exception_to_crash_reporter(e: BaseException):\n    from .base_crash_reporter import send_exception_to_crash_reporter\n    send_exception_to_crash_reporter(e)\n\n\ndef versiontuple(v):\n    return tuple(map(int, (v.split(\".\"))))\n\n\ndef read_json_file(path):\n    try:\n        with open(path, 'r', encoding='utf-8') as f:\n            data = json.loads(f.read())\n    except json.JSONDecodeError:\n        _logger.exception('')\n        raise FileImportFailed(_(\"Invalid JSON code.\"))\n    except BaseException as e:\n        _logger.exception('')\n        raise FileImportFailed(e)\n    return data\n\n\ndef write_json_file(path, data):\n    try:\n        with open(path, 'w+', encoding='utf-8') as f:\n            json.dump(data, f, indent=4, sort_keys=True, cls=MyEncoder)\n    except (IOError, os.error) as e:\n        _logger.exception('')\n        raise FileExportFailed(e)\n\n\ndef os_chmod(path, mode):\n    \"\"\"os.chmod aware of tmpfs\"\"\"\n    try:\n        os.chmod(path, mode)\n    except OSError as e:\n        xdg_runtime_dir = os.environ.get(\"XDG_RUNTIME_DIR\", None)\n        if xdg_runtime_dir and is_subpath(path, xdg_runtime_dir):\n            _logger.info(f\"Tried to chmod in tmpfs. Skipping... {e!r}\")\n        else:\n            raise\n\n\ndef make_dir(path, *, allow_symlink=True):\n    \"\"\"Makes directory if it does not yet exist.\n    Also sets sane 0700 permissions on the dir.\n    \"\"\"\n    if not os.path.exists(path):\n        if not allow_symlink and os.path.islink(path):\n            raise Exception('Dangling link: ' + path)\n        try:\n            os.mkdir(path)\n        except FileExistsError:\n            # this can happen in a multiprocess race, e.g. when an electrum daemon\n            # and an electrum cli command are launched in rapid fire\n            pass\n        os_chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)\n        assert os.path.exists(path)\n\n\ndef is_subpath(long_path: str, short_path: str) -> bool:\n    \"\"\"Returns whether long_path is a sub-path of short_path.\"\"\"\n    try:\n        common = os.path.commonpath([long_path, short_path])\n    except ValueError:\n        return False\n    short_path = standardize_path(short_path)\n    common     = standardize_path(common)\n    return short_path == common\n\n\ndef log_exceptions(func):\n    \"\"\"Decorator to log AND re-raise exceptions.\"\"\"\n    assert inspect.iscoroutinefunction(func), 'func needs to be a coroutine'\n\n    @functools.wraps(func)\n    async def wrapper(*args, **kwargs):\n        self = args[0] if len(args) > 0 else None\n        try:\n            return await func(*args, **kwargs)\n        except asyncio.CancelledError as e:\n            raise\n        except BaseException as e:\n            mylogger = self.logger if hasattr(self, 'logger') else _logger\n            try:\n                mylogger.exception(f\"Exception in {func.__name__}: {repr(e)}\")\n            except BaseException as e2:\n                print(f\"logging exception raised: {repr(e2)}... orig exc: {repr(e)} in {func.__name__}\")\n            raise\n    return wrapper\n\n\ndef ignore_exceptions(func):\n    \"\"\"Decorator to silently swallow all exceptions.\"\"\"\n    assert inspect.iscoroutinefunction(func), 'func needs to be a coroutine'\n\n    @functools.wraps(func)\n    async def wrapper(*args, **kwargs):\n        try:\n            return await func(*args, **kwargs)\n        except Exception as e:\n            pass\n    return wrapper\n\n\ndef with_lock(func):\n    \"\"\"Decorator to enforce a lock on a function call.\"\"\"\n    @functools.wraps(func)\n    def func_wrapper(self, *args, **kwargs):\n        with self.lock:\n            return func(self, *args, **kwargs)\n    return func_wrapper\n\n\n@dataclass(frozen=True, kw_only=True)\nclass TxMinedInfo:\n    _height: int                       # height of block that mined tx\n    conf: Optional[int] = None         # number of confirmations, SPV verified. >=0, or None (None means unknown)\n    timestamp: Optional[int] = None    # timestamp of block that mined tx\n    txpos: Optional[int] = None        # position of tx in serialized block\n    header_hash: Optional[str] = None  # hash of block that mined tx\n    wanted_height: Optional[int] = None  # in case of timelock, min abs block height\n\n    def height(self) -> int:\n        \"\"\"Treat unverified heights as unconfirmed.\"\"\"\n        h = self._height\n        if h > 0:\n            if self.conf is not None and self.conf >= 1:\n                return h\n            return 0  # treat it as unconfirmed until SPV-ed\n        else:  # h <= 0\n            return h\n\n    def short_id(self) -> Optional[str]:\n        if self.txpos is not None and self.txpos >= 0:\n            assert self.height() > 0\n            return f\"{self.height()}x{self.txpos}\"\n        return None\n\n    def is_local_like(self) -> bool:\n        \"\"\"Returns whether the tx is local-like (LOCAL/FUTURE).\"\"\"\n        from .address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT\n        if self.height() > 0:\n            return False\n        if self.height() in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):\n            return False\n        return True\n\n\nclass ShortID(bytes):\n\n    def __repr__(self):\n        return f\"<ShortID: {format_short_id(self)}>\"\n\n    def __str__(self):\n        return format_short_id(self)\n\n    @classmethod\n    def from_components(cls, block_height: int, tx_pos_in_block: int, output_index: int) -> 'ShortID':\n        bh = block_height.to_bytes(3, byteorder='big')\n        tpos = tx_pos_in_block.to_bytes(3, byteorder='big')\n        oi = output_index.to_bytes(2, byteorder='big')\n        return ShortID(bh + tpos + oi)\n\n    @classmethod\n    def from_str(cls, scid: str) -> 'ShortID':\n        \"\"\"Parses a formatted scid str, e.g. '643920x356x0'.\"\"\"\n        components = scid.split(\"x\")\n        if len(components) != 3:\n            raise ValueError(f\"failed to parse ShortID: {scid!r}\")\n        try:\n            components = [int(x) for x in components]\n        except ValueError:\n            raise ValueError(f\"failed to parse ShortID: {scid!r}\") from None\n        return ShortID.from_components(*components)\n\n    @classmethod\n    def normalize(cls, data: Union[None, str, bytes, 'ShortID']) -> Optional['ShortID']:\n        if isinstance(data, ShortID) or data is None:\n            return data\n        if isinstance(data, str):\n            assert len(data) == 16\n            return ShortID.fromhex(data)\n        if isinstance(data, (bytes, bytearray)):\n            assert len(data) == 8\n            return ShortID(data)\n\n    @property\n    def block_height(self) -> int:\n        return int.from_bytes(self[:3], byteorder='big')\n\n    @property\n    def txpos(self) -> int:\n        return int.from_bytes(self[3:6], byteorder='big')\n\n    @property\n    def output_index(self) -> int:\n        return int.from_bytes(self[6:8], byteorder='big')\n\n\ndef format_short_id(short_channel_id: Optional[bytes]):\n    if not short_channel_id:\n        return _('Not yet available')\n    return str(int.from_bytes(short_channel_id[:3], 'big')) \\\n        + 'x' + str(int.from_bytes(short_channel_id[3:6], 'big')) \\\n        + 'x' + str(int.from_bytes(short_channel_id[6:], 'big'))\n\n\ndef make_aiohttp_proxy_connector(proxy: 'ProxySettings', ssl_context: Optional[ssl.SSLContext] = None) -> ProxyConnector:\n    return ProxyConnector(\n        proxy_type=ProxyType.SOCKS5 if proxy.mode == 'socks5' else ProxyType.SOCKS4,\n        host=proxy.host,\n        port=int(proxy.port),\n        username=proxy.user,\n        password=proxy.password,\n        rdns=True,  # needed to prevent DNS leaks over proxy\n        ssl=ssl_context,\n    )\n\n\ndef make_aiohttp_session(proxy: Optional['ProxySettings'], headers=None, timeout=None):\n    if headers is None:\n        headers = {'User-Agent': 'Electrum'}\n    if timeout is None:\n        # The default timeout is high intentionally.\n        # DNS on some systems can be really slow, see e.g. #5337\n        timeout = aiohttp.ClientTimeout(total=45)\n    elif isinstance(timeout, (int, float)):\n        timeout = aiohttp.ClientTimeout(total=timeout)\n    ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path)\n\n    if proxy and proxy.enabled:\n        connector = make_aiohttp_proxy_connector(proxy, ssl_context)\n    else:\n        connector = aiohttp.TCPConnector(ssl=ssl_context)\n\n    return aiohttp.ClientSession(headers=headers, timeout=timeout, connector=connector)\n\n\nclass OldTaskGroup(aiorpcx.TaskGroup):\n    \"\"\"Automatically raises exceptions on join; as in aiorpcx prior to version 0.20.\n    That is, when using TaskGroup as a context manager, if any task encounters an exception,\n    we would like that exception to be re-raised (propagated out). For the wait=all case,\n    the OldTaskGroup class is emulating the following code-snippet:\n    ```\n    async with TaskGroup() as group:\n        await group.spawn(task1())\n        await group.spawn(task2())\n\n        async for task in group:\n            if not task.cancelled():\n                task.result()\n    ```\n    So instead of the above, one can just write:\n    ```\n    async with OldTaskGroup() as group:\n        await group.spawn(task1())\n        await group.spawn(task2())\n    ```\n    # TODO see if we can migrate to asyncio.timeout, introduced in python 3.11, and use stdlib instead of aiorpcx.curio...\n    \"\"\"\n    async def join(self):\n        if self._wait is all:\n            exc = False\n            try:\n                async for task in self:\n                    if not task.cancelled():\n                        task.result()\n            except BaseException:  # including asyncio.CancelledError\n                exc = True\n                raise\n            finally:\n                if exc:\n                    await self.cancel_remaining()\n                await super().join()\n        else:\n            await super().join()\n            if self.completed:\n                self.completed.result()\n\n\n# We monkey-patch aiorpcx TimeoutAfter (used by timeout_after and ignore_after API),\n# to fix a timing issue present in asyncio as a whole re timing out tasks.\n# To see the issue we are trying to fix, consider example:\n#     async def outer_task():\n#         async with timeout_after(0.1):\n#             await inner_task()\n# When the 0.1 sec timeout expires, inner_task will get cancelled by timeout_after (=internal cancellation).\n# If around the same time (in terms of event loop iterations) another coroutine\n# cancels outer_task (=external cancellation), there will be a race.\n# Both cancellations work by propagating a CancelledError out to timeout_after, which then\n# needs to decide (in TimeoutAfter.__aexit__) whether it's due to an internal or external cancellation.\n# AFAICT asyncio provides no reliable way of distinguishing between the two.\n# This patch tries to always give priority to external cancellations.\n# see https://github.com/kyuupichan/aiorpcX/issues/44\n# see https://github.com/aio-libs/async-timeout/issues/229\n# see https://bugs.python.org/issue42130 and https://bugs.python.org/issue45098\n# TODO see if we can migrate to asyncio.timeout, introduced in python 3.11, and use stdlib instead of aiorpcx.curio...\ndef _aiorpcx_monkeypatched_set_new_deadline(task, deadline):\n    def timeout_task():\n        task._orig_cancel()\n        task._timed_out = None if getattr(task, \"_externally_cancelled\", False) else deadline\n\n    def mycancel(*args, **kwargs):\n        task._orig_cancel(*args, **kwargs)\n        task._externally_cancelled = True\n        task._timed_out = None\n\n    if not hasattr(task, \"_orig_cancel\"):\n        task._orig_cancel = task.cancel\n        task.cancel = mycancel\n    task._deadline_handle = task._loop.call_at(deadline, timeout_task)\n\n\ndef _aiorpcx_monkeypatched_set_task_deadline(task, deadline):\n    ret = _aiorpcx_orig_set_task_deadline(task, deadline)\n    task._externally_cancelled = None\n    return ret\n\n\ndef _aiorpcx_monkeypatched_unset_task_deadline(task):\n    if hasattr(task, \"_orig_cancel\"):\n        task.cancel = task._orig_cancel\n        del task._orig_cancel\n    return _aiorpcx_orig_unset_task_deadline(task)\n\n\n_aiorpcx_orig_set_task_deadline    = aiorpcx.curio._set_task_deadline\n_aiorpcx_orig_unset_task_deadline  = aiorpcx.curio._unset_task_deadline\n\naiorpcx.curio._set_new_deadline    = _aiorpcx_monkeypatched_set_new_deadline\naiorpcx.curio._set_task_deadline   = _aiorpcx_monkeypatched_set_task_deadline\naiorpcx.curio._unset_task_deadline = _aiorpcx_monkeypatched_unset_task_deadline\n\n\nasync def wait_for2(fut: Awaitable, timeout: Union[int, float, None]):\n    \"\"\"Replacement for asyncio.wait_for,\n     due to bugs: https://bugs.python.org/issue42130 and https://github.com/python/cpython/issues/86296 ,\n     which are only fixed in python 3.12+.\n     \"\"\"\n    if sys.version_info[:3] >= (3, 12):\n        return await asyncio.wait_for(fut, timeout)\n    else:\n        async with async_timeout(timeout):\n            return await asyncio.ensure_future(fut, loop=get_running_loop())\n\n\nif hasattr(asyncio, 'timeout'):  # python 3.11+\n    async_timeout = asyncio.timeout\nelse:\n    class TimeoutAfterAsynciolike(aiorpcx.curio.TimeoutAfter):\n        async def __aexit__(self, exc_type, exc_value, tb):\n            try:\n                await super().__aexit__(exc_type, exc_value, tb)\n            except (aiorpcx.TaskTimeout, aiorpcx.UncaughtTimeoutError):\n                raise asyncio.TimeoutError from None\n            except aiorpcx.TimeoutCancellationError:\n                raise asyncio.CancelledError from None\n\n    def async_timeout(delay: Union[int, float, None]):\n        if delay is None:\n            return nullcontext()\n        return TimeoutAfterAsynciolike(delay)\n\n\nclass NetworkJobOnDefaultServer(Logger, ABC):\n    \"\"\"An abstract base class for a job that runs on the main network\n    interface. Every time the main interface changes, the job is\n    restarted, and some of its internals are reset.\n    \"\"\"\n    def __init__(self, network: 'Network'):\n        Logger.__init__(self)\n        self.network = network\n        self.interface = None  # type: Interface\n        self._restart_lock = asyncio.Lock()\n        # Ensure fairness between NetworkJobs. e.g. if multiple wallets\n        # are open, a large wallet's Synchronizer should not starve the small wallets:\n        self._network_request_semaphore = asyncio.Semaphore(100)\n\n        self._reset()\n        # every time the main interface changes, restart:\n        register_callback(self._restart, ['default_server_changed'])\n        # also schedule a one-off restart now, as there might already be a main interface:\n        asyncio.run_coroutine_threadsafe(self._restart(), network.asyncio_loop)\n\n    def _reset(self):\n        \"\"\"Initialise fields. Called every time the underlying\n        server connection changes.\n        \"\"\"\n        self.taskgroup = OldTaskGroup()\n        self.reset_request_counters()\n\n    async def _start(self, interface: 'Interface'):\n        self.logger.debug(f\"starting. interface.server={repr(str(interface.server))}\")\n        self.interface = interface\n\n        taskgroup = self.taskgroup\n\n        async def run_tasks_wrapper():\n            self.logger.debug(f\"starting taskgroup ({hex(id(taskgroup))}).\")\n            try:\n                await self._run_tasks(taskgroup=taskgroup)\n            except Exception as e:\n                self.logger.error(f\"taskgroup died ({hex(id(taskgroup))}). exc={e!r}\")\n                raise\n            finally:\n                self.logger.debug(f\"taskgroup stopped ({hex(id(taskgroup))}).\")\n        await interface.taskgroup.spawn(run_tasks_wrapper)\n\n    @abstractmethod\n    async def _run_tasks(self, *, taskgroup: OldTaskGroup) -> None:\n        \"\"\"Start tasks in taskgroup. Called every time the underlying\n        server connection changes.\n        \"\"\"\n        # If self.taskgroup changed, don't start tasks. This can happen if we have\n        # been restarted *just now*, i.e. after the _run_tasks coroutine object was created.\n        if taskgroup != self.taskgroup:\n            raise asyncio.CancelledError()\n\n    async def stop(self, *, full_shutdown: bool = True):\n        self.logger.debug(f\"stopping. {full_shutdown=}\")\n        if full_shutdown:\n            unregister_callback(self._restart)\n        await self.taskgroup.cancel_remaining()\n\n    @log_exceptions\n    async def _restart(self, *args):\n        interface = self.network.interface\n        if interface is None:\n            return  # we should get called again soon\n\n        async with self._restart_lock:\n            await self.stop(full_shutdown=False)\n            self._reset()\n            await self._start(interface)\n\n    def reset_request_counters(self):\n        self._requests_sent = 0\n        self._requests_answered = 0\n\n    def num_requests_sent_and_answered(self) -> Tuple[int, int]:\n        return self._requests_sent, self._requests_answered\n\n    @property\n    def session(self):\n        s = self.interface.session\n        assert s is not None\n        return s\n\n\nasync def detect_tor_socks_proxy() -> Optional[Tuple[str, int]]:\n    # Probable ports for Tor to listen at\n    candidates = [\n        (\"127.0.0.1\", 9050),\n        (\"127.0.0.1\", 9051),\n        (\"127.0.0.1\", 9150),\n    ]\n\n    proxy_addr = None\n\n    async def test_net_addr(net_addr):\n        is_tor = await is_tor_socks_port(*net_addr)\n        # set result, and cancel remaining probes\n        if is_tor:\n            nonlocal proxy_addr\n            proxy_addr = net_addr\n            await group.cancel_remaining()\n\n    async with OldTaskGroup() as group:\n        for net_addr in candidates:\n            await group.spawn(test_net_addr(net_addr))\n    return proxy_addr\n\n\n@log_exceptions\nasync def is_tor_socks_port(host: str, port: int) -> bool:\n    # mimic \"tor-resolve 0.0.0.0\".\n    # see https://github.com/spesmilo/electrum/issues/7317#issuecomment-1369281075\n    # > this is a socks5 handshake, followed by a socks RESOLVE request as defined in\n    # > [tor's socks extension spec](https://github.com/torproject/torspec/blob/7116c9cdaba248aae07a3f1d0e15d9dd102f62c5/socks-extensions.txt#L63),\n    # > resolving 0.0.0.0, which being an IP, tor resolves itself without needing to ask a relay.\n    writer = None\n    try:\n        async with async_timeout(10):\n            reader, writer = await asyncio.open_connection(host, port)\n            writer.write(b'\\x05\\x01\\x00\\x05\\xf0\\x00\\x03\\x070.0.0.0\\x00\\x00')\n            await writer.drain()\n            data = await reader.read(1024)\n            if data == b'\\x05\\x00\\x05\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00':\n                return True\n            return False\n    except (OSError, asyncio.TimeoutError):\n        return False\n    finally:\n        if writer:\n            writer.close()\n\n\nAS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP = False  # used by unit tests\n\n_asyncio_event_loop = None  # type: Optional[asyncio.AbstractEventLoop]\n\n\ndef get_asyncio_loop() -> asyncio.AbstractEventLoop:\n    \"\"\"Returns the global asyncio event loop we use.\"\"\"\n    if loop := _asyncio_event_loop:\n        return loop\n    if AS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP:\n        if loop := get_running_loop():\n            return loop\n    raise Exception(\"event loop not created yet\")\n\n\ndef create_and_start_event_loop() -> Tuple[asyncio.AbstractEventLoop,\n                                           asyncio.Future,\n                                           threading.Thread]:\n    global _asyncio_event_loop\n    if _asyncio_event_loop is not None:\n        raise Exception(\"there is already a running event loop\")\n\n    # asyncio.get_event_loop() became deprecated in python3.10. (see https://github.com/python/cpython/issues/83710)\n    # We set a custom event loop policy purely to be compatible with code that\n    # relies on asyncio.get_event_loop().\n    # - in python 3.8-3.9, asyncio.Event.__init__, asyncio.Lock.__init__,\n    #   and similar, calls get_event_loop. see https://github.com/python/cpython/pull/23420\n    class MyEventLoopPolicy(asyncio.DefaultEventLoopPolicy):\n        def get_event_loop(self):\n            # In case electrum is being used as a library, there might be other\n            # event loops in use besides ours. To minimise interfering with those,\n            # if there is a loop running in the current thread, return that:\n            running_loop = get_running_loop()\n            if running_loop is not None:\n                return running_loop\n            # Otherwise, return our global loop:\n            return get_asyncio_loop()\n    asyncio.set_event_loop_policy(MyEventLoopPolicy())\n\n    loop = asyncio.new_event_loop()\n    _asyncio_event_loop = loop\n\n    def on_exception(loop, context):\n        \"\"\"Suppress spurious messages it appears we cannot control.\"\"\"\n        SUPPRESS_MESSAGE_REGEX = re.compile('SSL handshake|Fatal read error on|'\n                                            'SSL error in data received')\n        message = context.get('message')\n        if message and SUPPRESS_MESSAGE_REGEX.match(message):\n            return\n        loop.default_exception_handler(context)\n\n    def run_event_loop():\n        try:\n            loop.run_until_complete(stopping_fut)\n        finally:\n            # clean-up\n            try:\n                pending_tasks = asyncio.gather(*asyncio.all_tasks(loop), return_exceptions=True)\n                pending_tasks.cancel()\n                with suppress(asyncio.CancelledError):\n                    loop.run_until_complete(pending_tasks)\n                loop.run_until_complete(loop.shutdown_asyncgens())\n                if isinstance(loop, asyncio.BaseEventLoop):\n                    loop.run_until_complete(loop.shutdown_default_executor())\n            except Exception as e:\n                _logger.debug(f\"exception when cleaning up asyncio event loop: {e}\")\n\n            global _asyncio_event_loop\n            _asyncio_event_loop = None\n            loop.close()\n\n    loop.set_exception_handler(on_exception)\n    _set_custom_task_factory(loop)\n    # loop.set_debug(True)\n    stopping_fut = loop.create_future()\n    loop_thread = threading.Thread(\n        target=run_event_loop,\n        name='EventLoop',\n    )\n    loop_thread.start()\n    # Wait until the loop actually starts.\n    # On a slow PC, or with a debugger attached, this can take a few dozens of ms,\n    # and if we returned without a running loop, weird things can happen...\n    t0 = time.monotonic()\n    while not loop.is_running():\n        time.sleep(0.01)\n        if time.monotonic() - t0 > 5:\n            raise Exception(\"been waiting for 5 seconds but asyncio loop would not start!\")\n    return loop, stopping_fut, loop_thread\n\n\n_running_asyncio_tasks = set()  # type: Set[asyncio.Future]\n\n\ndef _set_custom_task_factory(loop: asyncio.AbstractEventLoop):\n    \"\"\"Wrap task creation to track pending and running tasks.\n    When tasks are created, asyncio only maintains a weak reference to them.\n    Hence, the garbage collector might destroy the task mid-execution.\n    To avoid this, we store a strong reference for the task until it completes.\n\n    Without this, a lot of APIs are basically Heisenbug-generators... e.g.:\n    - \"asyncio.create_task\"\n    - \"loop.create_task\"\n    - \"asyncio.ensure_future\"\n    - \"asyncio.run_coroutine_threadsafe\"\n\n    related:\n        - https://bugs.python.org/issue44665\n        - https://github.com/python/cpython/issues/88831\n        - https://github.com/python/cpython/issues/91887\n        - https://textual.textualize.io/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/\n        - https://github.com/python/cpython/issues/91887#issuecomment-1434816045\n        - \"Task was destroyed but it is pending!\"\n    \"\"\"\n\n    platform_task_factory = loop.get_task_factory()\n\n    def factory(loop_, coro, **kwargs):\n        if platform_task_factory is not None:\n            task = platform_task_factory(loop_, coro, **kwargs)\n        else:\n            task = asyncio.Task(coro, loop=loop_, **kwargs)\n        _running_asyncio_tasks.add(task)\n        task.add_done_callback(_running_asyncio_tasks.discard)\n        return task\n\n    loop.set_task_factory(factory)\n\n\ndef run_sync_function_on_asyncio_thread(func: Callable[[], Any], *, block: bool) -> None:\n    \"\"\"Run a non-async fn on the asyncio thread. Can be called from any thread.\n\n    If the current thread is already the asyncio thread, func is guaranteed\n    to have been completed when this method returns.\n\n    For any other thread, we only wait for completion if `block` is True.\n    \"\"\"\n    assert not inspect.iscoroutinefunction(func), \"func must be a non-async function\"\n    asyncio_loop = get_asyncio_loop()\n    if get_running_loop() == asyncio_loop:  # we are running on the asyncio thread\n        func()\n    else:  # non-asyncio thread\n        async def wrapper():\n            return func()\n        fut = asyncio.run_coroutine_threadsafe(wrapper(), loop=asyncio_loop)\n        if block:\n            fut.result()\n        else:\n            # add explicit logging of exceptions, otherwise they might get lost\n            tb1 = traceback.format_stack()[:-1]\n            tb1_str = \"\".join(tb1)\n\n            def on_done(fut_: concurrent.futures.Future):\n                assert fut_.done()\n                if fut_.cancelled():\n                    _logger.debug(f\"func cancelled. {func=}.\")\n                elif exc := fut_.exception():\n                    # note: We explicitly log the first part of the traceback, tb1_str.\n                    #       The second part gets logged by setting \"exc_info\".\n                    _logger.error(\n                        f\"func errored. {func=}. {exc=}\"\n                        f\"\\n{tb1_str}\", exc_info=exc)\n            fut.add_done_callback(on_done)\n\n\nclass OrderedDictWithIndex(OrderedDict):\n    \"\"\"An OrderedDict that keeps track of the positions of keys.\n\n    Note: very inefficient to modify contents, except to add new items.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self._key_to_pos = {}\n        self._pos_to_key = {}\n\n    def _recalc_index(self):\n        self._key_to_pos = {key: pos for (pos, key) in enumerate(self.keys())}\n        self._pos_to_key = {pos: key for (pos, key) in enumerate(self.keys())}\n\n    def pos_from_key(self, key):\n        return self._key_to_pos[key]\n\n    def value_from_pos(self, pos):\n        key = self._pos_to_key[pos]\n        return self[key]\n\n    def popitem(self, *args, **kwargs):\n        ret = super().popitem(*args, **kwargs)\n        self._recalc_index()\n        return ret\n\n    def move_to_end(self, *args, **kwargs):\n        ret = super().move_to_end(*args, **kwargs)\n        self._recalc_index()\n        return ret\n\n    def clear(self):\n        ret = super().clear()\n        self._recalc_index()\n        return ret\n\n    def pop(self, *args, **kwargs):\n        ret = super().pop(*args, **kwargs)\n        self._recalc_index()\n        return ret\n\n    def update(self, *args, **kwargs):\n        ret = super().update(*args, **kwargs)\n        self._recalc_index()\n        return ret\n\n    def __delitem__(self, *args, **kwargs):\n        ret = super().__delitem__(*args, **kwargs)\n        self._recalc_index()\n        return ret\n\n    def __setitem__(self, key, *args, **kwargs):\n        is_new_key = key not in self\n        ret = super().__setitem__(key, *args, **kwargs)\n        if is_new_key:\n            pos = len(self) - 1\n            self._key_to_pos[key] = pos\n            self._pos_to_key[pos] = key\n        return ret\n\n\ndef make_object_immutable(obj):\n    \"\"\"Makes the passed object immutable recursively.\"\"\"\n    allowed_types = (\n        dict, MappingProxyType, list, tuple, set, frozenset, str, int, float, bool, bytes, type(None)\n    )\n    assert isinstance(obj, allowed_types), f\"{type(obj)=} cannot be made immutable\"\n    if isinstance(obj, (dict, MappingProxyType)):\n        return MappingProxyType({k: make_object_immutable(v) for k, v in obj.items()})\n    elif isinstance(obj, (list, tuple)):\n        return tuple(make_object_immutable(item) for item in obj)\n    elif isinstance(obj, (set, frozenset)):\n        return frozenset(make_object_immutable(item) for item in obj)\n    return obj\n\n\ndef multisig_type(wallet_type):\n    \"\"\"If wallet_type is mofn multi-sig, return [m, n],\n    otherwise return None.\"\"\"\n    if not wallet_type:\n        return None\n    match = re.match(r'(\\d+)of(\\d+)', wallet_type)\n    if match:\n        match = [int(x) for x in match.group(1, 2)]\n    return match\n\n\ndef is_ip_address(x: Union[str, bytes]) -> bool:\n    if isinstance(x, bytes):\n        x = x.decode(\"utf-8\")\n    try:\n        ipaddress.ip_address(x)\n        return True\n    except ValueError:\n        return False\n\n\ndef is_localhost(host: str) -> bool:\n    if str(host) in ('localhost', 'localhost.',):\n        return True\n    if host[0] == '[' and host[-1] == ']':  # IPv6\n        host = host[1:-1]\n    try:\n        ip_addr = ipaddress.ip_address(host)  # type: Union[IPv4Address, IPv6Address]\n        return ip_addr.is_loopback\n    except ValueError:\n        pass  # not an IP\n    return False\n\n\ndef is_private_netaddress(host: str) -> bool:\n    if is_localhost(host):\n        return True\n    if host[0] == '[' and host[-1] == ']':  # IPv6\n        host = host[1:-1]\n    try:\n        ip_addr = ipaddress.ip_address(host)  # type: Union[IPv4Address, IPv6Address]\n        return ip_addr.is_private\n    except ValueError:\n        pass  # not an IP\n    return False\n\n\ndef list_enabled_bits(x: int) -> Sequence[int]:\n    \"\"\"e.g. 77 (0b1001101) --> (0, 2, 3, 6)\"\"\"\n    binary = bin(x)[2:]\n    rev_bin = reversed(binary)\n    return tuple(i for i, b in enumerate(rev_bin) if b == '1')\n\n\nasync def resolve_dns_srv(host: str):\n    # FIXME this method is not using the network proxy. (although the proxy might not support UDP?)\n    srv_records = await dns.asyncresolver.resolve(host, 'SRV')\n    # priority: prefer lower\n    # weight: tie breaker; prefer higher\n    srv_records = sorted(srv_records, key=lambda x: (x.priority, -x.weight))\n\n    def dict_from_srv_record(srv):\n        return {\n            'host': str(srv.target),\n            'port': srv.port,\n        }\n    return [dict_from_srv_record(srv) for srv in srv_records]\n\n\ndef randrange(bound: int) -> int:\n    \"\"\"Return a random integer k such that 1 <= k < bound, uniformly\n    distributed across that range.\n    This is guaranteed to be cryptographically strong.\n    \"\"\"\n    # secrets.randbelow(bound) returns a random int: 0 <= r < bound,\n    # hence transformations:\n    return secrets.randbelow(bound - 1) + 1\n\n\nclass CallbackManager(Logger):\n    # callbacks set by the GUI or any thread\n    # guarantee: the callbacks will always get triggered from the asyncio thread.\n\n    # FIXME: There should be a way to prevent circular callbacks.\n    # At the very least, we need a distinction between callbacks that\n    # are for the GUI and callbacks between wallet components\n\n    def __init__(self):\n        Logger.__init__(self)\n        self.callback_lock = threading.RLock()\n        self._wcallbacks = defaultdict(set)  # type: Dict[str, Set[weakref.ref[Callable]]]  # note: needs self.callback_lock\n\n    @staticmethod\n    def _wcb_from_any_callback(cb: Callable) -> weakref.ref[Callable]:\n        assert callable(cb), type(cb)\n        if isinstance(cb, weakref.ref):  # no-op\n            return cb\n        elif inspect.ismethod(cb):  # instance method, such as for a subclass of EventListener\n            return WeakMethodProper(cb)\n        else:  # proper function? e.g. used by lnpeer unit tests\n            return weakref.ref(cb)\n\n    def register_callback(self, cb: Callable, events: Sequence[str]) -> None:\n        wcb = self._wcb_from_any_callback(cb)\n        with self.callback_lock:\n            for event in events:\n                self._wcallbacks[event].add(wcb)\n\n    def unregister_callback(self, cb: Callable) -> None:\n        wcb = self._wcb_from_any_callback(cb)\n        with self.callback_lock:\n            # note: ^ callback_lock needs to be re-entrant, as we can now trigger __del__, which also takes the lock\n            for callbacks in self._wcallbacks.values():\n                if wcb in callbacks:\n                    callbacks.remove(wcb)\n\n    def count_all_callbacks(self) -> int:\n        with self.callback_lock:\n            return sum(len(cbs) for cbs in self._wcallbacks.values())\n\n    def clear_all_callbacks(self) -> None:\n        with self.callback_lock:\n            self._wcallbacks.clear()\n\n    def trigger_callback(self, event: str, *args) -> None:\n        \"\"\"Trigger a callback with given arguments.\n        Can be called from any thread. The callback itself will get scheduled\n        on the event loop.\n        \"\"\"\n        loop = get_asyncio_loop()\n        assert loop.is_running(), \"event loop not running\"\n        with self.callback_lock:\n            wcallbacks = copy.copy(self._wcallbacks[event])\n        for wcb in wcallbacks:\n            callback = wcb()\n            if callback is None:\n                continue\n            if inspect.iscoroutinefunction(callback):  # async cb\n                fut = asyncio.run_coroutine_threadsafe(callback(*args), loop)\n\n                def on_done(fut_: concurrent.futures.Future):\n                    assert fut_.done()\n                    if fut_.cancelled():\n                        self.logger.debug(f\"cb cancelled. {event=}.\")\n                    elif exc := fut_.exception():\n                        self.logger.error(f\"cb errored. {event=}. {exc=}\", exc_info=exc)\n                fut.add_done_callback(on_done)\n            else:  # non-async cb\n                run_sync_function_on_asyncio_thread(partial(callback, *args), block=False)\n\n\ncallback_mgr = CallbackManager()\ntrigger_callback = callback_mgr.trigger_callback\nregister_callback = callback_mgr.register_callback\nunregister_callback = callback_mgr.unregister_callback\n_event_listeners = defaultdict(set)  # type: Dict[str, Set[str]]\n\n\nclass WeakMethodProper(weakref.WeakMethod):\n    \"\"\"Unlike weakref.WeakMethod, this class has an __eq__ I can trust.\"\"\"\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        meth = self()\n        self._my_id = (id(meth.__self__), id(meth.__func__))\n\n    def __hash__(self):\n        return hash(self._my_id)\n\n    def __eq__(self, other):\n        if not isinstance(other, WeakMethodProper):\n            return False\n        return self._my_id == other._my_id\n\n\nclass EventListener:\n    \"\"\"Use as a mixin for a class that has methods to be triggered on events.\n    - Methods that receive the callbacks should be named \"on_event_*\" and decorated with @event_listener.\n    - register_callbacks() should be called once per instance of EventListener, e.g. in __init__\n    - unregister_callbacks() should be called at least once, e.g. when the instance is destroyed\n        - as fallback, __del__() also calls unregister_callbacks()\n    \"\"\"\n\n    def _list_callbacks(self):\n        for c in self.__class__.__mro__:\n            classpath = f\"{c.__module__}.{c.__name__}\"\n            for method_name in _event_listeners[classpath]:\n                method = getattr(self, method_name)\n                assert callable(method)\n                assert method_name.startswith('on_event_')\n                yield method_name[len('on_event_'):], method\n\n    def register_callbacks(self):\n        for name, method in self._list_callbacks():\n            #_logger.debug(f'registering callback {method}')\n            register_callback(method, [name])\n\n    def unregister_callbacks(self):\n        for name, method in self._list_callbacks():\n            #_logger.debug(f'unregistering callback {method}')\n            unregister_callback(method)\n\n    def __del__(self):\n        self.unregister_callbacks()\n\n\ndef event_listener(func):\n    \"\"\"To be used in subclasses of EventListener only. (how to enforce this programmatically?)\"\"\"\n    classname, method_name = func.__qualname__.split('.')\n    assert method_name.startswith('on_event_')\n    classpath = f\"{func.__module__}.{classname}\"\n    _event_listeners[classpath].add(method_name)\n    return func\n\n\n_NetAddrType = TypeVar(\"_NetAddrType\")\n# requirements for _NetAddrType:\n# - reasonable __hash__() implementation (e.g. based on host/port of remote endpoint)\n\n\nclass NetworkRetryManager(Generic[_NetAddrType]):\n    \"\"\"Truncated Exponential Backoff for network connections.\"\"\"\n\n    def __init__(\n            self, *,\n            max_retry_delay_normal: float,\n            init_retry_delay_normal: float,\n            max_retry_delay_urgent: float = None,\n            init_retry_delay_urgent: float = None,\n    ):\n        self._last_tried_addr = {}  # type: Dict[_NetAddrType, Tuple[float, int]]  # (unix ts, num_attempts)\n\n        # note: these all use \"seconds\" as unit\n        if max_retry_delay_urgent is None:\n            max_retry_delay_urgent = max_retry_delay_normal\n        if init_retry_delay_urgent is None:\n            init_retry_delay_urgent = init_retry_delay_normal\n        self._max_retry_delay_normal = max_retry_delay_normal\n        self._init_retry_delay_normal = init_retry_delay_normal\n        self._max_retry_delay_urgent = max_retry_delay_urgent\n        self._init_retry_delay_urgent = init_retry_delay_urgent\n\n    def _trying_addr_now(self, addr: _NetAddrType) -> None:\n        last_time, num_attempts = self._last_tried_addr.get(addr, (0, 0))\n        # we add up to 1 second of noise to the time, so that clients are less likely\n        # to get synchronised and bombard the remote in connection waves:\n        cur_time = time.time() + random.random()\n        self._last_tried_addr[addr] = cur_time, num_attempts + 1\n\n    def _on_connection_successfully_established(self, addr: _NetAddrType) -> None:\n        self._last_tried_addr[addr] = time.time(), 0\n\n    def _can_retry_addr(self, addr: _NetAddrType, *,\n                        now: float = None, urgent: bool = False) -> bool:\n        if now is None:\n            now = time.time()\n        last_time, num_attempts = self._last_tried_addr.get(addr, (0, 0))\n        if urgent:\n            max_delay = self._max_retry_delay_urgent\n            init_delay = self._init_retry_delay_urgent\n        else:\n            max_delay = self._max_retry_delay_normal\n            init_delay = self._init_retry_delay_normal\n        delay = self.__calc_delay(multiplier=init_delay, max_delay=max_delay, num_attempts=num_attempts)\n        next_time = last_time + delay\n        return next_time < now\n\n    @classmethod\n    def __calc_delay(cls, *, multiplier: float, max_delay: float,\n                     num_attempts: int) -> float:\n        num_attempts = min(num_attempts, 100_000)\n        try:\n            res = multiplier * 2 ** num_attempts\n        except OverflowError:\n            return max_delay\n        return max(0, min(max_delay, res))\n\n    def _clear_addr_retry_times(self) -> None:\n        self._last_tried_addr.clear()\n\n\nclass ESocksProxy(aiorpcx.SOCKSProxy):\n    # note: proxy will not leak DNS as create_connection()\n    # sets (local DNS) resolve=False by default\n\n    async def open_connection(self, host=None, port=None, **kwargs):\n        loop = asyncio.get_running_loop()\n        reader = asyncio.StreamReader(loop=loop)\n        protocol = asyncio.StreamReaderProtocol(reader, loop=loop)\n        transport, _ = await self.create_connection(\n            lambda: protocol, host, port, **kwargs)\n        writer = asyncio.StreamWriter(transport, protocol, reader, loop)\n        return reader, writer\n\n    @classmethod\n    def from_network_settings(cls, network: Optional['Network']) -> Optional['ESocksProxy']:\n        if not network or not network.proxy or not network.proxy.enabled:\n            return None\n        proxy = network.proxy\n        username, pw = proxy.user, proxy.password\n        if not username or not pw:\n            # is_proxy_tor is tri-state; None indicates it is still probing the proxy to test for TOR\n            if network.is_proxy_tor:\n                auth = aiorpcx.socks.SOCKSRandomAuth()\n            else:\n                auth = None\n        else:\n            auth = aiorpcx.socks.SOCKSUserAuth(username, pw)\n        addr = aiorpcx.NetAddress(proxy.host, proxy.port)\n        if proxy.mode == \"socks4\":\n            ret = cls(addr, aiorpcx.socks.SOCKS4a, auth)\n        elif proxy.mode == \"socks5\":\n            ret = cls(addr, aiorpcx.socks.SOCKS5, auth)\n        else:\n            raise NotImplementedError  # http proxy not available with aiorpcx\n        return ret\n\n\nclass JsonRPCError(Exception):\n\n    class Codes(enum.IntEnum):\n        # application-specific error codes\n        USERFACING = 1\n        INTERNAL = 2\n\n    def __init__(self, *, code: int, message: str, data: Optional[dict] = None):\n        Exception.__init__(self)\n        self.code = code\n        self.message = message\n        self.data = data\n\n\nclass JsonRPCClient:\n\n    def __init__(self, session: aiohttp.ClientSession, url: str):\n        self.session = session\n        self.url = url\n        self._id = 0\n\n    async def request(self, endpoint, *args):\n        \"\"\"Send request to server, parse and return result.\n        note: parsing code is naive, the server is assumed to be well-behaved.\n              Up to the caller to handle exceptions, including those arising from parsing errors.\n        \"\"\"\n        self._id += 1\n        data = ('{\"jsonrpc\": \"2.0\", \"id\":\"%d\", \"method\": \"%s\", \"params\": %s }'\n                % (self._id, endpoint, json.dumps(args)))\n        async with self.session.post(self.url, data=data) as resp:\n            if resp.status == 200:\n                r = await resp.json()\n                result = r.get('result')\n                error = r.get('error')\n                if error:\n                    raise JsonRPCError(code=error[\"code\"], message=error[\"message\"], data=error.get(\"data\"))\n                else:\n                    return result\n            else:\n                text = await resp.text()\n                return 'Error: ' + str(text)\n\n    def add_method(self, endpoint):\n        async def coro(*args):\n            return await self.request(endpoint, *args)\n        setattr(self, endpoint, coro)\n\n\nT = TypeVar('T')\n\n\ndef random_shuffled_copy(x: Iterable[T]) -> List[T]:\n    \"\"\"Returns a shuffled copy of the input.\"\"\"\n    x_copy = list(x)  # copy\n    random.shuffle(x_copy)  # shuffle in-place\n    return x_copy\n\n\ndef test_read_write_permissions(path) -> None:\n    # note: There might already be a file at 'path'.\n    #       Make sure we do NOT overwrite/corrupt that!\n    temp_path = \"%s.tmptest.%s\" % (path, os.getpid())\n    echo = \"fs r/w test\"\n    try:\n        # test READ permissions for actual path\n        if os.path.exists(path):\n            with open(path, \"rb\") as f:\n                f.read(1)  # read 1 byte\n        # test R/W sanity for \"similar\" path\n        with open(temp_path, \"w\", encoding='utf-8') as f:\n            f.write(echo)\n        with open(temp_path, \"r\", encoding='utf-8') as f:\n            echo2 = f.read()\n        os.remove(temp_path)\n    except Exception as e:\n        raise IOError(e) from e\n    if echo != echo2:\n        raise IOError('echo sanity-check failed')\n\n\nclass classproperty(property):\n    \"\"\"~read-only class-level @property\n    from https://stackoverflow.com/a/13624858 by denis-ryzhkov\n    \"\"\"\n    def __get__(self, owner_self, owner_cls):\n        return self.fget(owner_cls)\n\n\ndef sticky_property(val):\n    \"\"\"Creates a 'property' whose value cannot be changed and that cannot be deleted.\n    Attempts to change the value are silently ignored.\n\n    >>> class C: pass\n    ...\n    >>> setattr(C, 'x', sticky_property(3))\n    >>> c = C()\n    >>> c.x\n    3\n    >>> c.x = 2\n    >>> c.x\n    3\n    >>> del c.x\n    >>> c.x\n    3\n    \"\"\"\n    return property(\n        fget=lambda self: val,\n        fset=lambda *args, **kwargs: None,\n        fdel=lambda *args, **kwargs: None,\n    )\n\n\ndef get_running_loop() -> Optional[asyncio.AbstractEventLoop]:\n    \"\"\"Returns the asyncio event loop that is *running in this thread*, if any.\"\"\"\n    try:\n        return asyncio.get_running_loop()\n    except RuntimeError:\n        return None\n\n\ndef error_text_str_to_safe_str(err: str, *, max_len: Optional[int] = 500) -> str:\n    \"\"\"Converts an untrusted error string to a sane printable ascii str.\n    Never raises.\n    \"\"\"\n    text = error_text_bytes_to_safe_str(\n        err.encode(\"ascii\", errors='backslashreplace'),\n        max_len=None)\n    return truncate_text(text, max_len=max_len)\n\n\ndef error_text_bytes_to_safe_str(err: bytes, *, max_len: Optional[int] = 500) -> str:\n    \"\"\"Converts an untrusted error bytes text to a sane printable ascii str.\n    Never raises.\n\n    Note that naive ascii conversion would be insufficient. Fun stuff:\n    >>> b = b\"my_long_prefix_blabla\" + 21 * b\"\\x08\" + b\"malicious_stuff\"\n    >>> s = b.decode(\"ascii\")\n    >>> print(s)\n    malicious_stuffblabla\n    \"\"\"\n    # convert to ascii, to get rid of unicode stuff\n    ascii_text = err.decode(\"ascii\", errors='backslashreplace')\n    # do repr to handle ascii special chars (especially when printing/logging the str)\n    text = repr(ascii_text)\n    return truncate_text(text, max_len=max_len)\n\n\ndef truncate_text(text: str, *, max_len: Optional[int]) -> str:\n    if max_len is None or len(text) <= max_len:\n        return text\n    else:\n        return text[:max_len] + f\"... (truncated. orig_len={len(text)})\"\n\n\ndef nostr_pow_worker(nonce, nostr_pubk, target_bits, hash_function, hash_len_bits, shutdown):\n    \"\"\"Function to generate PoW for Nostr, to be spawned in a ProcessPoolExecutor.\"\"\"\n    hash_preimage = b'electrum-' + nostr_pubk\n    while True:\n        # we cannot check is_set on each iteration as it has a lot of overhead, this way we can check\n        # it with low overhead (just the additional range counter)\n        for i in range(1000000):\n            digest = hash_function(hash_preimage + nonce.to_bytes(32, 'big')).digest()\n            if int.from_bytes(digest, 'big') < (1 << (hash_len_bits - target_bits)):\n                shutdown.set()\n                return hash, nonce\n            nonce += 1\n        if shutdown.is_set():\n            return None, None\n\n\nasync def gen_nostr_ann_pow(nostr_pubk: bytes, target_bits: int) -> Tuple[int, int]:\n    \"\"\"Generate a PoW for a Nostr announcement. The PoW is hash[b'electrum-'+pubk+nonce]\"\"\"\n    import multiprocessing  # not available on Android, so we import it here\n    hash_function = hashlib.sha256\n    hash_len_bits = 256\n    max_nonce: int = (1 << (32 * 8)) - 1  # 32-byte nonce\n    start_nonce = 0\n\n    max_workers = max(multiprocessing.cpu_count() - 1, 1)  # use all but one CPU\n    manager = multiprocessing.Manager()\n    shutdown = manager.Event()\n    with ProcessPoolExecutor(max_workers=max_workers) as executor:\n        tasks = []\n        loop = asyncio.get_running_loop()\n        for task in range(0, max_workers):\n            task = loop.run_in_executor(\n                executor,\n                nostr_pow_worker,\n                start_nonce,\n                nostr_pubk,\n                target_bits,\n                hash_function,\n                hash_len_bits,\n                shutdown\n            )\n            tasks.append(task)\n            start_nonce += max_nonce // max_workers  # split the nonce range between the processes\n            if start_nonce > max_nonce:  # make sure we don't go over the max_nonce\n                start_nonce = random.randint(0, int(max_nonce * 0.75))\n\n        done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)\n        hash_res, nonce_res = done.pop().result()\n        executor.shutdown(wait=False, cancel_futures=True)\n\n    return nonce_res, get_nostr_ann_pow_amount(nostr_pubk, nonce_res)\n\n\ndef get_nostr_ann_pow_amount(nostr_pubk: bytes, nonce: Optional[int]) -> int:\n    \"\"\"Return the amount of leading zero bits for a nostr announcement PoW.\"\"\"\n    if not nonce:\n        return 0\n    hash_function = hashlib.sha256\n    hash_len_bits = 256\n    hash_preimage = b'electrum-' + nostr_pubk\n\n    digest = hash_function(hash_preimage + nonce.to_bytes(32, 'big')).digest()\n    digest = int.from_bytes(digest, 'big')\n    return hash_len_bits - digest.bit_length()\n\n\nclass OnchainHistoryItem(NamedTuple):\n    txid: str\n    amount_sat: int\n    fee_sat: int\n    balance_sat: int\n    tx_mined_status: TxMinedInfo\n    group_id: Optional[str]\n    label: Optional[str]\n    monotonic_timestamp: int\n    group_id: Optional[str]\n    def to_dict(self):\n        return {\n            'txid': self.txid,\n            'amount_sat': self.amount_sat,\n            'fee_sat': self.fee_sat,\n            'height': self.tx_mined_status.height(),\n            'confirmations': self.tx_mined_status.conf,\n            'timestamp': self.tx_mined_status.timestamp,\n            'monotonic_timestamp': self.monotonic_timestamp,\n            'incoming': True if self.amount_sat>0 else False,\n            'bc_value': Satoshis(self.amount_sat),\n            'bc_balance': Satoshis(self.balance_sat),\n            'date': timestamp_to_datetime(self.tx_mined_status.timestamp),\n            'txpos_in_block': self.tx_mined_status.txpos,\n            'wanted_height': self.tx_mined_status.wanted_height,\n            'label': self.label,\n            'group_id': self.group_id,\n        }\n\n\nclass LightningHistoryItem(NamedTuple):\n    payment_hash: Optional[str]\n    preimage: Optional[str]\n    amount_msat: int\n    fee_msat: Optional[int]\n    type: str\n    group_id: Optional[str]\n    timestamp: int\n    label: Optional[str]\n    direction: Optional[int]\n    def to_dict(self):\n        return {\n            'type': self.type,\n            'label': self.label,\n            'timestamp': self.timestamp or 0,\n            'date': timestamp_to_datetime(self.timestamp),\n            'amount_msat': self.amount_msat,\n            'fee_msat': self.fee_msat,\n            'payment_hash': self.payment_hash,\n            'preimage': self.preimage,\n            'group_id': self.group_id,\n            'ln_value': Satoshis(Decimal(self.amount_msat) / 1000),\n            'direction': self.direction,\n        }\n\n\n@dataclass(kw_only=True, slots=True)\nclass ChoiceItem:\n    key: Any\n    label: str  # user facing string\n    extra_data: Any = None\n"
  },
  {
    "path": "electrum/utils/__init__.py",
    "content": ""
  },
  {
    "path": "electrum/utils/memory_leak.py",
    "content": "import asyncio\nfrom collections import defaultdict\nimport datetime\nimport os\nimport time\nfrom typing import Sequence, Mapping, TypeVar, Optional\nimport weakref\n\nfrom electrum import util\nfrom electrum.util import ThreadJob\n\n\n_U = TypeVar('_U')\n\ndef count_objects_in_memory(mclasses: Sequence[type[_U]]) -> Mapping[type[_U], Sequence[weakref.ref[_U]]]:\n    import gc\n    gc.collect()\n    objmap = defaultdict(list)\n    for obj in gc.get_objects():\n        for class_ in mclasses:\n            try:\n                _isinstance = isinstance(obj, class_)\n            except AttributeError:\n                _isinstance = False\n            if _isinstance:\n                objmap[class_].append(weakref.ref(obj))\n    return objmap\n\n\nclass DebugMem(ThreadJob):\n    '''A handy class for debugging GC memory leaks\n\n    In Qt console:\n    >>> from electrum.utils.memory_leak import DebugMem\n    >>> from electrum.wallet import Abstract_Wallet\n    >>> plugins.add_jobs([DebugMem([Abstract_Wallet,], interval=5)])\n    '''\n    def __init__(self, classes, interval=30):\n        ThreadJob.__init__(self)\n        self.next_time = 0\n        self.classes = classes\n        self.interval = interval\n\n    def mem_stats(self):\n        self.logger.info(\"Start memscan\")\n        objmap = count_objects_in_memory(self.classes)\n        for class_, objs in objmap.items():\n            self.logger.info(f\"{class_.__name__}: {len(objs)}\")\n        self.logger.info(\"Finish memscan\")\n\n    def run(self):\n        if time.time() > self.next_time:\n            self.mem_stats()\n            self.next_time = time.time() + self.interval\n\n\nasync def wait_until_obj_is_garbage_collected(wr: weakref.ref) -> None:\n    \"\"\"Async wait until the object referenced by `wr` is GC-ed.\"\"\"\n    obj = wr()\n    if obj is None:\n        return\n    evt_gc = asyncio.Event()  # set when obj is finally GC-ed.\n    wr2 = weakref.ref(obj, lambda _x: util.run_sync_function_on_asyncio_thread(evt_gc.set, block=False))\n    del obj\n    while True:\n        try:\n            async with util.async_timeout(0.01):\n                await evt_gc.wait()\n        except asyncio.TimeoutError:\n            import gc\n            gc.collect()\n        else:\n            break\n    assert evt_gc.is_set()\n\n\ndef debug_memusage_list_all_objects(limit: int = 50) -> list[tuple[str, int]]:\n    \"\"\"Return a string listing the most common types in memory.\"\"\"\n    import objgraph  # 3rd-party dependency\n    return objgraph.most_common_types(\n        limit=limit,\n        shortnames=False,\n    )\n\n\ndef debug_memusage_dump_random_backref_chain(objtype: str) -> Optional[str]:\n    \"\"\"Writes a dotfile to cwd, containing the backref chain\n    for a randomly selected object of type objtype.\n\n    Warning: very slow!\n\n    In Qt console:\n    >>> debug_memusage_dump_random_backref_chain(\"Standard_Wallet\")\n\n    To convert to image:\n    $ dot -Tps filename.dot -o outfile.ps\n    \"\"\"\n    import objgraph  # 3rd-party dependency\n    import random\n    timestamp = datetime.datetime.now(datetime.timezone.utc).strftime(\"%Y%m%dT%H%M%SZ\")\n    fpath = os.path.abspath(f\"electrum_backref_chain_{timestamp}.dot\")\n    objects = objgraph.by_type(objtype)\n    if not objects:\n        return None\n    random_obj = random.choice(objects)\n    with open(fpath, \"w\") as f:\n        objgraph.show_chain(\n            objgraph.find_backref_chain(\n                random_obj,\n                objgraph.is_proper_module),\n            output=f)\n    return fpath\n"
  },
  {
    "path": "electrum/utils/stacktracer.py",
    "content": "#!/usr/bin/env python\n#\n# Copyright (C) 2010 Laszlo Nagy (nagylzs)\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n#\n# Taken from: https://code.activestate.com/recipes/577334-how-to-debug-deadlocked-multi-threaded-programs/\n\n\n\"\"\"Stack tracer for multi-threaded applications.\nUseful for debugging deadlocks and hangs.\n\nUsage:\n    import stacktracer\n    stacktracer.trace_start(\"trace.html\", interval=5)\n    ...\n    stacktracer.trace_stop()\n\nThis will create a file named \"trace.html\" showing the stack traces of all threads,\nupdated every 5 seconds.\n\"\"\"\n\nimport os\nimport sys\nimport threading\nimport time\nimport traceback\nfrom typing import Optional\n\n# 3rd-party dependency:\nfrom pygments import highlight\nfrom pygments.lexers import PythonLexer\nfrom pygments.formatters import HtmlFormatter\n\n\ndef _thread_from_id(ident) -> Optional[threading.Thread]:\n    return threading._active.get(ident)\n\n\ndef stacktraces():\n    \"\"\"Taken from http://bzimmer.ziclix.com/2008/12/17/python-thread-dumps/\"\"\"\n    code = []\n    for thread_id, stack in sys._current_frames().items():\n        thread = _thread_from_id(thread_id)\n        code.append(f\"\\n# thread_id={thread_id}. thread={thread}\")\n        for filename, lineno, name, line in traceback.extract_stack(stack):\n            code.append(f'File: \"{filename}\", line {lineno}, in {name}')\n            if line:\n                code.append(\"  %s\" % (line.strip()))\n\n    return highlight(\"\\n\".join(code), PythonLexer(), HtmlFormatter(\n        full=False,\n        # style=\"native\",\n        noclasses=True,\n    ))\n\n\nclass TraceDumper(threading.Thread):\n    \"\"\"Dump stack traces into a given file periodically.\n\n    # written by nagylzs\n    \"\"\"\n\n    def __init__(self, fpath, interval, auto):\n        \"\"\"\n        @param fpath: File path to output HTML (stack trace file)\n        @param auto: Set flag (True) to update trace continuously.\n            Clear flag (False) to update only if file not exists.\n            (Then delete the file to force update.)\n        @param interval: In seconds: how often to update the trace file.\n        \"\"\"\n        assert (interval > 0.1)\n        self.auto = auto\n        self.interval = interval\n        self.fpath = os.path.abspath(fpath)\n        self.stop_requested = threading.Event()\n        threading.Thread.__init__(self)\n\n    def run(self):\n        while not self.stop_requested.is_set():\n            time.sleep(self.interval)\n            if self.auto or not os.path.isfile(self.fpath):\n                self.dump_stacktraces()\n\n    def stop(self):\n        self.stop_requested.set()\n        self.join()\n        try:\n            if os.path.isfile(self.fpath):\n                os.unlink(self.fpath)\n        except OSError:\n            pass\n\n    def dump_stacktraces(self):\n        with open(self.fpath, \"w+\") as fout:\n            fout.write(stacktraces())\n\n\n_tracer = None  # type: Optional[TraceDumper]\n\n\ndef trace_start(fpath, interval=5, *, auto=True):\n    \"\"\"Start tracing into the given file.\"\"\"\n    global _tracer\n    if _tracer is None:\n        _tracer = TraceDumper(fpath, interval, auto)\n        _tracer.daemon = True\n        _tracer.start()\n    else:\n        raise Exception(\"Already tracing to %s\" % _tracer.fpath)\n\n\ndef trace_stop():\n    \"\"\"Stop tracing.\"\"\"\n    global _tracer\n    if _tracer is None:\n        raise Exception(\"Not tracing, cannot stop.\")\n    else:\n        _tracer.stop()\n        _tracer = None\n"
  },
  {
    "path": "electrum/verifier.py",
    "content": "# Electrum - Lightweight Bitcoin Client\n# Copyright (c) 2012 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport asyncio\nfrom typing import Sequence, Optional, TYPE_CHECKING\n\nimport aiorpcx\n\nfrom .util import TxMinedInfo, NetworkJobOnDefaultServer\nfrom .crypto import sha256d\nfrom .bitcoin import hash_decode, hash_encode\nfrom .transaction import Transaction\nfrom .blockchain import hash_header\nfrom .interface import GracefulDisconnect\nfrom . import constants\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .address_synchronizer import AddressSynchronizer\n\n\nclass MerkleVerificationFailure(Exception): pass\nclass MissingBlockHeader(MerkleVerificationFailure): pass\nclass MerkleRootMismatch(MerkleVerificationFailure): pass\nclass InnerNodeOfSpvProofIsValidTx(MerkleVerificationFailure): pass\n\n\nclass SPV(NetworkJobOnDefaultServer):\n    \"\"\" Simple Payment Verification \"\"\"\n\n    def __init__(self, network: 'Network', wallet: 'AddressSynchronizer'):\n        self.wallet = wallet\n        NetworkJobOnDefaultServer.__init__(self, network)\n\n    def _reset(self):\n        super()._reset()\n        self.merkle_roots = {}  # txid -> merkle root (once it has been verified)\n        self.requested_merkle = set()  # txid set of pending requests\n\n    async def _run_tasks(self, *, taskgroup):\n        await super()._run_tasks(taskgroup=taskgroup)\n        async with taskgroup as group:\n            await group.spawn(self.main)\n\n    def diagnostic_name(self):\n        return self.wallet.diagnostic_name()\n\n    async def main(self):\n        self.blockchain = self.network.blockchain()\n        while True:\n            await self._maybe_undo_verifications()\n            await self._request_proofs()\n            await asyncio.sleep(0.1)\n\n    async def _request_proofs(self):\n        local_height = self.blockchain.height()\n        unverified = self.wallet.get_unverified_txs()\n\n        for tx_hash, tx_height in unverified.items():\n            # do not request merkle branch if we already requested it\n            if tx_hash in self.requested_merkle or tx_hash in self.merkle_roots:\n                continue\n            # or before headers are available\n            if not (0 < tx_height <= local_height):\n                continue\n            # if it's in the checkpoint region, we still might not have the header\n            header = self.blockchain.read_header(tx_height)\n            if header is None:\n                if tx_height <= constants.net.max_checkpoint():\n                    # FIXME these requests are not counted (self._requests_sent += 1)\n                    await self.taskgroup.spawn(self.interface.request_chunk_below_max_checkpoint(height=tx_height))\n                continue\n            # request now\n            self.logger.info(f'requested merkle {tx_hash}')\n            self.requested_merkle.add(tx_hash)\n            await self.taskgroup.spawn(self._request_and_verify_single_proof, tx_hash, tx_height)\n\n    async def _request_and_verify_single_proof(self, tx_hash, tx_height):\n        try:\n            self._requests_sent += 1\n            async with self._network_request_semaphore:\n                merkle = await self.interface.get_merkle_for_transaction(tx_hash, tx_height)\n        except aiorpcx.jsonrpc.RPCError:\n            self.logger.info(f'tx {tx_hash} not at height {tx_height}')\n            self.wallet.remove_unverified_tx(tx_hash, tx_height)\n            self.requested_merkle.discard(tx_hash)\n            return\n        finally:\n            self._requests_answered += 1\n        # Verify the hash of the server-provided merkle branch to a\n        # transaction matches the merkle root of its block\n        if tx_height != merkle.get('block_height'):\n            self.logger.info('requested tx_height {} differs from received tx_height {} for txid {}'\n                             .format(tx_height, merkle.get('block_height'), tx_hash))\n        tx_height = merkle.get('block_height')\n        pos = merkle.get('pos')\n        merkle_branch = merkle.get('merkle')\n        # we need to wait if header sync/reorg is still ongoing, hence lock:\n        async with self.network.bhi_lock:\n            header = self.network.blockchain().read_header(tx_height)\n        try:\n            verify_tx_is_in_block(tx_hash, merkle_branch, pos, header, tx_height)\n        except MerkleVerificationFailure as e:\n            if self.network.config.NETWORK_SKIPMERKLECHECK:\n                self.logger.info(f\"skipping merkle proof check {tx_hash}\")\n            else:\n                self.logger.info(repr(e))\n                raise GracefulDisconnect(e) from e\n        # we passed all the tests\n        self.merkle_roots[tx_hash] = header.get('merkle_root')\n        self.requested_merkle.discard(tx_hash)\n        self.logger.info(f\"verified {tx_hash}\")\n        header_hash = hash_header(header)\n        tx_info = TxMinedInfo(_height=tx_height,\n                              timestamp=header.get('timestamp'),\n                              txpos=pos,\n                              header_hash=header_hash)\n        self.wallet.add_verified_tx(tx_hash, tx_info)\n\n    @classmethod\n    def hash_merkle_root(cls, merkle_branch: Sequence[str], tx_hash: str, leaf_pos_in_tree: int):\n        \"\"\"Return calculated merkle root.\"\"\"\n        try:\n            h = hash_decode(tx_hash)\n            merkle_branch_bytes = [hash_decode(item) for item in merkle_branch]\n            leaf_pos_in_tree = int(leaf_pos_in_tree)  # raise if invalid\n        except Exception as e:\n            raise MerkleVerificationFailure(e)\n        if leaf_pos_in_tree < 0:\n            raise MerkleVerificationFailure('leaf_pos_in_tree must be non-negative')\n        index = leaf_pos_in_tree\n        for item in merkle_branch_bytes:\n            if len(item) != 32:\n                raise MerkleVerificationFailure('all merkle branch items have to be 32 bytes long')\n            inner_node = (item + h) if (index & 1) else (h + item)\n            cls._raise_if_valid_tx(inner_node.hex())\n            h = sha256d(inner_node)\n            index >>= 1\n        if index != 0:\n            raise MerkleVerificationFailure(f'leaf_pos_in_tree too large for branch')\n        return hash_encode(h)\n\n    @classmethod\n    def _raise_if_valid_tx(cls, raw_tx: str):\n        # If an inner node of the merkle proof is also a valid tx, chances are, this is an attack.\n        # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-June/016105.html\n        # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/attachments/20180609/9f4f5b1f/attachment-0001.pdf\n        # https://bitcoin.stackexchange.com/questions/76121/how-is-the-leaf-node-weakness-in-merkle-trees-exploitable/76122#76122\n        tx = Transaction(raw_tx)\n        try:\n            tx.deserialize()\n        except Exception:\n            pass\n        else:\n            raise InnerNodeOfSpvProofIsValidTx()\n\n    async def _maybe_undo_verifications(self):\n        old_chain = self.blockchain\n        cur_chain = self.network.blockchain()\n        if cur_chain != old_chain:\n            self.blockchain = cur_chain\n            above_height = cur_chain.get_height_of_last_common_block_with_chain(old_chain)\n            self.logger.info(f\"undoing verifications above height {above_height}\")\n            tx_hashes = self.wallet.undo_verifications(self.blockchain, above_height)\n            for tx_hash in tx_hashes:\n                self.logger.info(f\"redoing {tx_hash}\")\n                self.remove_spv_proof_for_tx(tx_hash)\n\n    def remove_spv_proof_for_tx(self, tx_hash):\n        self.merkle_roots.pop(tx_hash, None)\n        self.requested_merkle.discard(tx_hash)\n\n    def is_up_to_date(self):\n        return (not self.requested_merkle\n                and not self.wallet.unverified_tx)\n\n\ndef verify_tx_is_in_block(tx_hash: str, merkle_branch: Sequence[str],\n                          leaf_pos_in_tree: int, block_header: Optional[dict],\n                          block_height: int) -> None:\n    \"\"\"Raise MerkleVerificationFailure if verification fails.\"\"\"\n    if not block_header:\n        raise MissingBlockHeader(\"merkle verification failed for {} (missing header {})\"\n                                 .format(tx_hash, block_height))\n    if len(merkle_branch) > 30:\n        raise MerkleVerificationFailure(f\"merkle branch too long: {len(merkle_branch)}\")\n    calc_merkle_root = SPV.hash_merkle_root(merkle_branch, tx_hash, leaf_pos_in_tree)\n    if block_header.get('merkle_root') != calc_merkle_root:\n        raise MerkleRootMismatch(\"merkle verification failed for {} ({} != {})\".format(\n            tx_hash, block_header.get('merkle_root'), calc_merkle_root))\n"
  },
  {
    "path": "electrum/version.py",
    "content": "ELECTRUM_VERSION = '4.7.1'       # version of the client package\n\nPROTOCOL_VERSION_MIN = '1.4'     # electrum protocol\nPROTOCOL_VERSION_MAX = '1.6'\n\n# The hash of the mnemonic seed must begin with this\nSEED_PREFIX        = '01'      # Standard wallet\nSEED_PREFIX_SW     = '100'     # Segwit wallet\nSEED_PREFIX_2FA    = '101'     # Two-factor authentication\nSEED_PREFIX_2FA_SW = '102'     # Two-factor auth, using segwit\n\n\ndef seed_prefix(seed_type):\n    if seed_type == 'standard':\n        return SEED_PREFIX\n    elif seed_type == 'segwit':\n        return SEED_PREFIX_SW\n    elif seed_type == '2fa':\n        return SEED_PREFIX_2FA\n    elif seed_type == '2fa_segwit':\n        return SEED_PREFIX_2FA_SW\n    raise Exception(f\"unknown seed_type: {seed_type!r}\")\n"
  },
  {
    "path": "electrum/wallet.py",
    "content": "# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n# Wallet classes:\n#   - Imported_Wallet: imported addresses or single keys, 0 or 1 keystore\n#   - Standard_Wallet: one HD keystore, P2PKH-like scripts\n#   - Multisig_Wallet: several HD keystores, M-of-N OP_CHECKMULTISIG scripts\n\nimport os\nimport random\nimport time\nimport copy\nimport math\nfrom functools import partial\nfrom collections import defaultdict\nfrom decimal import Decimal\nfrom typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any, Set, Iterable, Mapping\nfrom abc import ABC, abstractmethod\nimport itertools\nimport threading\nimport enum\nimport asyncio\nfrom dataclasses import dataclass\n\nimport electrum_ecc as ecc\nfrom aiorpcx import ignore_after, run_in_thread\n\nfrom . import util, keystore, transaction, bitcoin, coinchooser, bip32, descriptor\nfrom .i18n import _\nfrom .bip32 import BIP32Node, convert_bip32_intpath_to_strpath, convert_bip32_strpath_to_intpath\nfrom .logging import get_logger, Logger\nfrom .util import (\n    NotEnoughFunds, UserCancelled, profiler, OldTaskGroup, format_fee_satoshis,\n    WalletFileException, BitcoinException, InvalidPassword, format_time, timestamp_to_datetime,\n    Satoshis, Fiat, TxMinedInfo, quantize_feerate, OrderedDictWithIndex, multisig_type, parse_max_spend,\n    OnchainHistoryItem, read_json_file, write_json_file, UserFacingException, FileImportFailed, EventListener,\n    event_listener\n)\nfrom .bitcoin import COIN, is_address, is_minikey, relayfee, dust_threshold, DummyAddress, DummyAddressUsedInTxException\nfrom .keystore import (\n    load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, AddressIndexGeneric, CannotDerivePubkey\n)\nfrom .simple_config import SimpleConfig\nfrom .fee_policy import FeePolicy, FixedFeePolicy, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE\nfrom .storage import StorageEncryptionVersion, WalletStorage\nfrom .wallet_db import WalletDB\nfrom .transaction import (\n    Transaction, TxInput, TxOutput, PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint, Sighash\n)\nfrom .plugin import run_hook\nfrom .address_synchronizer import (\n    AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE,\n    TX_TIMESTAMP_INF\n)\nfrom .invoices import BaseInvoice, Invoice, Request, PR_PAID, PR_UNPAID, PR_EXPIRED, PR_UNCONFIRMED\nfrom .contacts import Contacts\nfrom .mnemonic import Mnemonic\nfrom .lnworker import LNWallet\nfrom .lnutil import MIN_FUNDING_SAT, RECEIVED, SENT\nfrom .lntransport import extract_nodeid\nfrom .descriptor import Descriptor\nfrom .txbatcher import TxBatcher\nfrom .submarine_swaps import MIN_SWAP_AMOUNT_SAT\n\nif TYPE_CHECKING:\n    from .network import Network\n    from .exchange_rate import FxThread\n    from .submarine_swaps import SwapData\n    from .lnchannel import AbstractChannel\n    from .lnsweep import SweepInfo\n\n\n_logger = get_logger(__name__)\n\nTX_STATUS = [\n    _('Unconfirmed'),\n    _('Unconfirmed parent'),\n    _('Not Verified'),\n    _('Local'),\n]\n\n\nasync def _append_utxos_to_inputs(\n    *,\n    inputs: List[PartialTxInput],\n    network: 'Network',\n    script_descriptor: 'descriptor.Descriptor',\n    imax: int,\n) -> None:\n    script = script_descriptor.expand().output_script\n    scripthash = bitcoin.script_to_scripthash(script)\n\n    async def append_single_utxo(item):\n        prev_tx_raw = await network.get_transaction(item['tx_hash'])\n        prev_tx = Transaction(prev_tx_raw)\n        prev_txout = prev_tx.outputs()[item['tx_pos']]\n        if scripthash != bitcoin.script_to_scripthash(prev_txout.scriptpubkey):\n            raise Exception('scripthash mismatch when sweeping')\n        prevout_str = item['tx_hash'] + ':%d' % item['tx_pos']\n        prevout = TxOutpoint.from_str(prevout_str)\n        txin = PartialTxInput(prevout=prevout)\n        txin.utxo = prev_tx\n        txin.block_height = int(item['height'])\n        txin.script_descriptor = script_descriptor\n        inputs.append(txin)\n\n    u = await network.listunspent_for_scripthash(scripthash)\n    async with OldTaskGroup() as group:\n        for item in u:\n            if len(inputs) >= imax:\n                break\n            await group.spawn(append_single_utxo(item))\n\n\nasync def sweep_preparations(\n    privkeys: Iterable[str], network: 'Network', imax=100,\n) -> Tuple[Sequence[PartialTxInput], Mapping[bytes, bytes]]:\n\n    async def find_utxos_for_privkey(txin_type: str, privkey: bytes, compressed: bool):\n        pubkey = ecc.ECPrivkey(privkey).get_public_key_bytes(compressed=compressed)\n        try:\n            desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type)\n        except descriptor.NotLegacySinglesigScriptType:\n            raise UserFacingException(_(\"Unsupported script-type ({}) for sweeping.\").format(txin_type)) from None\n        await _append_utxos_to_inputs(\n            inputs=inputs,\n            network=network,\n            script_descriptor=desc,\n            imax=imax)\n        keypairs[pubkey] = privkey\n\n    inputs = []  # type: List[PartialTxInput]\n    keypairs = {}  # type: Dict[bytes, bytes]\n    async with OldTaskGroup() as group:\n        for sec in privkeys:\n            txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)\n            await group.spawn(find_utxos_for_privkey(txin_type, privkey, compressed))\n            # do other lookups to increase support coverage\n            if is_minikey(sec):\n                # minikeys don't have a compressed byte\n                # we lookup both compressed and uncompressed pubkeys\n                await group.spawn(find_utxos_for_privkey(txin_type, privkey, not compressed))\n            elif txin_type == 'p2pkh':\n                # WIF serialization does not distinguish p2pkh and p2pk\n                # we also search for pay-to-pubkey outputs\n                await group.spawn(find_utxos_for_privkey('p2pk', privkey, compressed))\n    if not inputs:\n        raise UserFacingException(_('No inputs found.'))\n    return inputs, keypairs\n\n\nasync def sweep(\n        privkeys: Iterable[str],\n        *,\n        network: 'Network',\n        to_address: str,\n        fee_policy: FeePolicy,\n        imax=100,\n        locktime=None,\n        tx_version=None\n) -> PartialTransaction:\n    inputs, keypairs = await sweep_preparations(privkeys, network, imax)\n    total = sum(txin.value_sats() for txin in inputs)\n    outputs = [PartialTxOutput(scriptpubkey=bitcoin.address_to_script(to_address), value=total)]\n    tx = PartialTransaction.from_io(inputs, outputs)\n    fee = fee_policy.estimate_fee(tx.estimated_size(), network=network)\n    if total - fee < 0:\n        raise Exception(_('Not enough funds on address.') + '\\nTotal: %d satoshis\\nFee: %d' % (total, fee))\n    if total - fee < dust_threshold(network):\n        raise Exception(_('Not enough funds on address.') +\n                        '\\nTotal: %d satoshis\\nFee: %d\\nDust Threshold: %d' % (total, fee, dust_threshold(network)))\n    outputs = [PartialTxOutput(scriptpubkey=bitcoin.address_to_script(to_address), value=total - fee)]\n    if locktime is None:\n        locktime = get_locktime_for_new_transaction(network)\n    tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime, version=tx_version)\n    tx.set_rbf(True)\n    tx.sign(keypairs)\n    return tx\n\n\ndef get_locktime_for_new_transaction(\n    network: 'Network',\n    *,\n    include_random_component: bool = True,\n) -> int:\n    # if no network or not up to date, just set locktime to zero\n    if not network:\n        return 0\n    chain = network.blockchain()\n    if chain.is_tip_stale():\n        return 0\n    # figure out current block height\n    chain_height = chain.height()  # learnt from all connected servers, SPV-checked\n    server_height = network.get_server_height()  # height claimed by main server, unverified\n    # note: main server might be lagging (either is slow, is malicious, or there is an SPV-invisible-hard-fork)\n    #       - if it's lagging too much, it is the network's job to switch away\n    if server_height < chain_height - 10:\n        # the diff is suspiciously large... give up and use something non-fingerprintable\n        return 0\n    # discourage \"fee sniping\"\n    locktime = min(chain_height, server_height)\n    # sometimes pick locktime a bit further back, to help privacy\n    # of setups that need more time (offline/multisig/coinjoin/...)\n    if include_random_component:\n        if random.randint(0, 9) == 0:\n            locktime = max(0, locktime - random.randint(0, 99))\n    locktime = max(0, locktime)\n    return locktime\n\n\nclass CannotRBFTx(Exception): pass\nclass TransactionPotentiallyDangerousException(Exception): pass\nclass TransactionDangerousException(TransactionPotentiallyDangerousException): pass\n\n\nclass CannotBumpFee(CannotRBFTx):\n    def __str__(self):\n        return _('Cannot bump fee') + ':\\n\\n' + Exception.__str__(self)\n\n\nclass CannotDoubleSpendTx(CannotRBFTx):\n    def __str__(self):\n        return _('Cannot cancel transaction') + ':\\n\\n' + Exception.__str__(self)\n\n\nclass CannotCPFP(Exception):\n    def __str__(self):\n        return _('Cannot create child transaction') + ':\\n\\n' + Exception.__str__(self)\n\n\nclass InternalAddressCorruption(Exception):\n    def __str__(self):\n        return _(\"Wallet file corruption detected. \"\n                 \"Please restore your wallet from seed, and compare the addresses in both files\")\n\n\nclass TxSighashRiskLevel(enum.IntEnum):\n    # higher value -> more risk\n    SAFE = 0\n    FEE_WARNING_SKIPCONFIRM = 1  # show warning icon (ignored for CLI)\n    FEE_WARNING_NEEDCONFIRM = 2  # prompt user for confirmation\n    WEIRD_SIGHASH = 3            # prompt user for confirmation\n    INSANE_SIGHASH = 4           # reject\n\n\nclass TxSighashDanger:\n\n    def __init__(\n        self,\n        *,\n        risk_level: TxSighashRiskLevel = TxSighashRiskLevel.SAFE,\n        short_message: str = None,\n        messages: List[str] = None,\n    ):\n        self.risk_level = risk_level\n        self.short_message = short_message\n        self._messages = messages or []\n\n    def needs_confirm(self) -> bool:\n        \"\"\"If True, the user should be prompted for explicit confirmation before signing.\"\"\"\n        return self.risk_level >= TxSighashRiskLevel.FEE_WARNING_NEEDCONFIRM\n\n    def needs_reject(self) -> bool:\n        \"\"\"If True, the transaction should be rejected, i.e. abort signing.\"\"\"\n        return self.risk_level >= TxSighashRiskLevel.INSANE_SIGHASH\n\n    def get_long_message(self) -> str:\n        \"\"\"Returns a description of the potential dangers of signing the tx that can be shown to the user.\n        Empty string if there are none.\n        \"\"\"\n        if self.short_message:\n            header = [self.short_message]\n        else:\n            header = []\n        return \"\\n\".join(header + self._messages)\n\n    def combine(*args: 'TxSighashDanger') -> 'TxSighashDanger':\n        max_danger = max(args, key=lambda sighash_danger: sighash_danger.risk_level)  # type: TxSighashDanger\n        messages = [msg for sighash_danger in args for msg in sighash_danger._messages]\n        return TxSighashDanger(\n            risk_level=max_danger.risk_level,\n            short_message=max_danger.short_message,\n            messages=messages,\n        )\n\n    def __repr__(self):\n        return (f\"<{self.__class__.__name__} risk_level={self.risk_level} \"\n                f\"short_message={self.short_message!r} _messages={self._messages!r}>\")\n\n\nclass BumpFeeStrategy(enum.Enum):\n    PRESERVE_PAYMENT = enum.auto()\n    DECREASE_PAYMENT = enum.auto()\n\n    @classmethod\n    def all(cls) -> Sequence['BumpFeeStrategy']:\n        return list(BumpFeeStrategy.__members__.values())\n\n    def text(self) -> str:\n        if self == self.PRESERVE_PAYMENT:\n            return _('Preserve payment')\n        elif self == self.DECREASE_PAYMENT:\n            return _('Decrease payment')\n        else:\n            raise Exception(f\"unknown strategy: {self=}\")\n\n\nclass ReceiveRequestHelp(NamedTuple):\n    # help texts (warnings/errors):\n    address_help: str\n    URI_help: str\n    ln_help: str\n    # whether the texts correspond to an error (or just a warning):\n    address_is_error: bool\n    URI_is_error: bool\n    ln_is_error: bool\n\n    ln_swap_suggestion: Optional[Any] = None\n    ln_rebalance_suggestion: Optional[Any] = None\n    ln_zeroconf_suggestion: bool = False\n\n    def can_swap(self) -> bool:\n        return bool(self.ln_swap_suggestion)\n\n    def can_rebalance(self) -> bool:\n        return bool(self.ln_rebalance_suggestion)\n\n    def can_zeroconf(self) -> bool:\n        return self.ln_zeroconf_suggestion\n\n\nclass TxWalletDelta(NamedTuple):\n    is_relevant: bool  # \"related to wallet?\"\n    is_any_input_ismine: bool\n    is_all_input_ismine: bool\n    delta: int\n    fee: Optional[int]\n\n\nclass TxWalletDetails(NamedTuple):\n    txid: Optional[str]\n    status: str\n    label: str\n    can_broadcast: bool\n    can_bump: bool\n    can_cpfp: bool\n    can_dscancel: bool  # whether user can double-spend to self\n    can_save_as_local: bool\n    amount: Optional[int]\n    fee: Optional[int]\n    tx_mined_status: TxMinedInfo\n    mempool_depth_bytes: Optional[int]\n    can_remove: bool  # whether user should be allowed to delete tx\n    is_lightning_funding_tx: bool\n    is_related_to_wallet: bool\n\n\n@dataclass(kw_only=True, slots=True, frozen=True)\nclass PiechartBalance:\n    confirmed: int    # confirmed and matured and NOT frozen\n    unconfirmed: int  # unconfirmed and NOT frozen\n    unmatured: int    # unmatured and NOT frozen\n    frozen: int       # on-chain\n    lightning: Decimal\n    lightning_frozen: Decimal\n\n    def total(self) -> Decimal:\n        return self.confirmed + self.unconfirmed + self.unmatured + self.frozen + self.lightning + self.lightning_frozen\n\n\nclass Abstract_Wallet(ABC, Logger, EventListener):\n    \"\"\"\n    Wallet classes are created to handle various address generation methods.\n    Completion states (watching-only, single account, no seed, etc) are handled inside classes.\n    \"\"\"\n\n    max_change_outputs = 3\n    gap_limit_for_change = None  # type: int | None\n\n    txin_type: str\n    wallet_type: str\n    lnworker: Optional['LNWallet']\n    network: Optional['Network']\n\n    def __init__(self, db: WalletDB, *, config: SimpleConfig):\n        self.config = config\n        assert self.config is not None, \"config must not be None\"\n        self.db = db\n        self.storage = db.storage  # type: Optional[WalletStorage]\n        # load addresses needs to be called before constructor for sanity checks\n        db.load_addresses(self.wallet_type)\n        self.keystore = None  # type: Optional[KeyStore]  # will be set by load_keystore\n        self._password_in_memory = None  # see self.unlock\n        Logger.__init__(self)\n\n        self.network = None\n        self.adb = AddressSynchronizer(db, config, name=self.diagnostic_name())\n        for addr in self.get_addresses():\n            self.adb.add_address(addr)\n        self.lock = self.adb.lock\n        self._last_full_history = None\n        self._tx_parents_cache = {}\n        self._default_labels = {}\n        self._accounting_addresses = set()  # addresses counted as ours after successful sweep\n\n        self.taskgroup = None\n\n        # saved fields\n        self.use_change            = db.get('use_change', True)\n        self.multiple_change       = db.get('multiple_change', False)\n        self._labels               = db.get_dict('labels')\n        self._frozen_addresses     = set(db.get('frozen_addresses', []))\n        self._frozen_coins         = db.get_dict('frozen_coins')  # type: Dict[str, bool]\n        self.fiat_value            = db.get_dict('fiat_value')\n        self._receive_requests     = db.get_dict('payment_requests')  # type: Dict[str, Request]\n        self._invoices             = db.get_dict('invoices')  # type: Dict[str, Invoice]\n        self._reserved_addresses   = set(db.get('reserved_addresses', []))\n        self._num_parents          = db.get_dict('num_parents')\n\n        self._freeze_lock = threading.RLock()  # for mutating/iterating frozen_{addresses,coins}\n\n        self.load_keystore()\n        self.txbatcher = TxBatcher(self)\n        self._init_lnworker()\n        self._init_requests_rhash_index()\n        self._prepare_onchain_invoice_paid_detection()\n        self._calc_unused_change_addresses()\n        # save wallet type the first time\n        if self.db.get('wallet_type') is None:\n            self.db.put('wallet_type', self.wallet_type)\n        self.contacts = Contacts(self.db)\n        self._coin_price_cache = {}\n\n        # true when synchronized. this is stricter than adb.is_up_to_date():\n        # to-be-generated (HD) addresses are also considered here (gap-limit-roll-forward)\n        self._up_to_date = False\n        self.up_to_date_changed_event = asyncio.Event()\n\n        self.test_addresses_sanity()\n        if self.storage and self.has_storage_encryption():\n            if (se := self.storage.get_encryption_version()) not in (ae := self.get_available_storage_encryption_versions()):\n                raise WalletFileException(f\"unexpected storage encryption type. found: {se!r}. allowed: {ae!r}\")\n\n        self.register_callbacks()\n\n    def _init_lnworker(self):\n        self.lnworker = None\n\n    async def main_loop(self):\n        self.logger.info(f\"starting taskgroup ({hex(id(self.taskgroup))}).\")\n        try:\n            async with self.taskgroup as group:\n                await group.spawn(asyncio.Event().wait)  # run forever (until cancel)\n                await group.spawn(self.do_synchronize_loop())\n                await group.spawn(self.txbatcher.run())\n        except Exception as e:\n            self.logger.exception(\"taskgroup died.\")\n        finally:\n            util.trigger_callback('wallet_updated', self)\n            self.logger.info(\"taskgroup stopped.\")\n\n    async def do_synchronize_loop(self):\n        \"\"\"Generates new deterministic addresses if needed (gap limit roll-forward),\n        and sets up_to_date.\n        \"\"\"\n        while True:\n            # polling.\n            # TODO if adb had \"up_to_date_changed\" asyncio.Event(), we could *also* trigger on that.\n            #      The polling would still be useful as often need to gen new addrs while adb.is_up_to_date() is False\n            await asyncio.sleep(0.1)\n            # note: we only generate new HD addresses if the existing ones\n            #       have history that are mined and SPV-verified.\n            await run_in_thread(self.synchronize)\n\n    def save_db(self):\n        if self.db.storage:\n            self.db.write()\n\n    def save_backup(self, backup_dir):\n        new_path = os.path.join(backup_dir, self.basename() + '.backup')\n        new_storage = WalletStorage(new_path)\n        new_storage._encryption_version = self.storage._encryption_version\n        new_storage.pubkey = self.storage.pubkey\n\n        new_db = WalletDB(self.db.dump(), storage=new_storage, upgrade=True)\n        if self.lnworker:\n            channel_backups = new_db.get_dict('imported_channel_backups')\n            for chan_id, chan in self.lnworker.channels.items():\n                channel_backups[chan_id.hex()] = self.lnworker.create_channel_backup(chan_id)\n            new_db.put('channels', None)\n            new_db.put('lightning_privkey2', None)\n        new_db.set_modified(True)\n        new_db.write()\n        return new_path\n\n    def has_lightning(self) -> bool:\n        return bool(self.lnworker)\n\n    def has_channels(self):\n        return self.lnworker is not None and len(self.lnworker._channels) > 0\n\n    def can_have_lightning(self) -> bool:\n        \"\"\" whether this wallet can create new channels \"\"\"\n        # we want static_remotekey to be a wallet address\n        if not self.txin_type == 'p2wpkh':\n            return False\n        if self.config.ENABLE_ANCHOR_CHANNELS:\n            if not self.keystore:\n                return False\n            if self.keystore.is_watching_only():\n                return False\n            # exclude hardware wallets\n            if not self.keystore.may_have_password():\n                return False\n        return True\n\n    def can_have_deterministic_lightning(self) -> bool:\n        if not self.can_have_lightning():\n            return False\n        return self.keystore.can_have_deterministic_lightning_xprv()\n\n    def init_lightning(self, *, password) -> None:\n        assert self.can_have_lightning()\n        assert self.db.get('lightning_xprv') is None\n        assert self.db.get('lightning_privkey2') is None\n        if self.can_have_deterministic_lightning():\n            assert isinstance(self.keystore, keystore.BIP32_KeyStore)\n            ln_xprv = self.keystore.get_lightning_xprv(password)\n            self.db.put('lightning_xprv', ln_xprv)\n        else:\n            seed = os.urandom(32)\n            node = BIP32Node.from_rootseed(seed, xtype='standard')\n            ln_xprv = node.to_xprv()\n            self.db.put('lightning_privkey2', ln_xprv)\n        self.lnworker = LNWallet(self, ln_xprv)\n        self.save_db()\n        if self.network:\n            self._start_network_lightning()\n\n    async def stop(self):\n        \"\"\"Stop all networking and save DB to disk.\"\"\"\n        self.unregister_callbacks()\n        try:\n            async with ignore_after(5):\n                if self.lnworker:\n                    await self.lnworker.stop()\n                    self.lnworker = None\n                if self.network:\n                    self.network = None\n                if self.taskgroup:\n                    await self.taskgroup.cancel_remaining()\n                    self.taskgroup = None\n                await self.adb.stop()\n        finally:  # even if we get cancelled\n            if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]):\n                self.save_keystore()\n            self.db.prune_uninstalled_plugin_data(self.config.get_installed_plugins())\n            self.save_db()\n\n    def is_up_to_date(self) -> bool:\n        if self.taskgroup and self.taskgroup.joined:  # either stop() was called, or the taskgroup died\n            return False\n        return self._up_to_date\n\n    def tx_is_related(self, tx):\n        is_mine = any([self.is_mine(out.address) for out in tx.outputs()])\n        is_mine |= any([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()])\n        return is_mine\n\n    def clear_tx_parents_cache(self):\n        with self.lock:\n            self._tx_parents_cache.clear()\n            self._num_parents.clear()\n            self._last_full_history = None\n\n    @event_listener\n    async def on_event_adb_set_up_to_date(self, adb):\n        if self.adb != adb:\n            return\n        num_new_addrs = await run_in_thread(self.synchronize)\n        up_to_date = self.adb.is_up_to_date() and num_new_addrs == 0\n        with self.lock:\n            status_changed = self._up_to_date != up_to_date\n            self._up_to_date = up_to_date\n        if up_to_date:\n            self.adb.reset_netrequest_counters()  # sync progress indicator\n            self.save_db()\n        # fire triggers\n        if status_changed or up_to_date:  # suppress False->False transition, as it is spammy\n            if self.lnworker:\n                await self.lnworker.lnwatcher.trigger_callbacks()\n            util.trigger_callback('wallet_updated', self)\n            util.trigger_callback('status')\n            self.up_to_date_changed_event.set()\n            self.up_to_date_changed_event.clear()\n        if status_changed:\n            self.logger.info(f'set_up_to_date: {up_to_date}')\n\n    @event_listener\n    def on_event_adb_added_tx(self, adb, tx_hash: str, tx: Transaction):\n        if self.adb != adb:\n            return\n        if not self.tx_is_related(tx):\n            return\n        self.clear_tx_parents_cache()\n        if self.lnworker:\n            self.lnworker.maybe_add_backup_from_tx(tx)\n        self._update_invoices_and_reqs_touched_by_tx(tx_hash)\n        util.trigger_callback('new_transaction', self, tx)\n\n    @event_listener\n    def on_event_adb_removed_tx(self, adb, txid: str, tx: Transaction):\n        if self.adb != adb:\n            return\n        if not tx or not self.tx_is_related(tx):\n            return\n        self.clear_tx_parents_cache()\n        util.trigger_callback('removed_transaction', self, tx)\n\n    @event_listener\n    def on_event_adb_added_verified_tx(self, adb, tx_hash):\n        if adb != self.adb:\n            return\n        self._update_invoices_and_reqs_touched_by_tx(tx_hash)\n        tx_mined_status = self.adb.get_tx_height(tx_hash)\n        util.trigger_callback('verified', self, tx_hash, tx_mined_status)\n\n    @event_listener\n    def on_event_adb_removed_verified_tx(self, adb, tx_hash):\n        if adb != self.adb:\n            return\n        self._update_invoices_and_reqs_touched_by_tx(tx_hash)\n\n    def clear_history(self):\n        self.adb.clear_history()\n        self.save_db()\n\n    def start_network(self, network: 'Network'):\n        assert self.network is None, \"already started\"\n        self.taskgroup = OldTaskGroup()\n        self.network = network\n        if network:\n            asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)\n            self.adb.start_network(network)\n            if self.lnworker:\n                self._start_network_lightning()\n\n    def _start_network_lightning(self):\n        assert self.lnworker\n        assert self.lnworker.network is None, 'lnworker network already initialized'\n        self.lnworker.start_network(self.network)\n        # only start gossiping when we already have channels\n        if self.db.get('channels'):\n            self.network.start_gossip()\n\n    @abstractmethod\n    def load_keystore(self) -> None:\n        pass\n\n    def diagnostic_name(self):\n        return self.basename()\n\n    def __str__(self):\n        return self.basename()\n\n    def get_master_public_key(self):\n        return None\n\n    def get_master_public_keys(self):\n        return []\n\n    def basename(self) -> str:\n        return self.storage.basename() if self.storage else 'no_name'\n\n    def test_addresses_sanity(self) -> None:\n        addrs = self.get_receiving_addresses()\n        if len(addrs) > 0:\n            addr = str(addrs[0])\n            if not bitcoin.is_address(addr):\n                neutered_addr = addr[:5] + '..' + addr[-2:]\n                raise WalletFileException(f'The addresses in this wallet are not bitcoin addresses.\\n'\n                                          f'e.g. {neutered_addr} (length: {len(addr)})')\n\n    def check_returned_address_for_corruption(func):\n        def wrapper(self, *args, **kwargs):\n            addr = func(self, *args, **kwargs)\n            self.check_address_for_corruption(addr)\n            return addr\n        return wrapper\n\n    def _calc_unused_change_addresses(self) -> Sequence[str]:\n        \"\"\"Returns a list of change addresses to choose from, for usage in e.g. new transactions.\n        The caller should give priority to earlier ones in the list.\n        \"\"\"\n        with self.lock:\n            # We want a list of unused change addresses.\n            # As a performance optimisation, to avoid checking all addresses every time,\n            # we maintain a list of \"not old\" addresses (\"old\" addresses have deeply confirmed history),\n            # and only check those.\n            if not hasattr(self, '_not_old_change_addresses'):\n                self._not_old_change_addresses = self.get_change_addresses()\n            self._not_old_change_addresses = [addr for addr in self._not_old_change_addresses\n                                              if not self.adb.address_is_old(addr)]\n            unused_addrs = [addr for addr in self._not_old_change_addresses\n                            if not self.adb.is_used(addr) and not self.is_address_reserved(addr)]\n            return unused_addrs\n\n    def is_deterministic(self) -> bool:\n        return self.keystore.is_deterministic()\n\n    def _set_label(self, key: str, value: Optional[str]) -> None:\n        with self.lock:\n            if value is None:\n                self._labels.pop(key, None)\n            else:\n                self._labels[key] = value\n\n    def set_label(self, name: str, text: str = None) -> bool:\n        if not name:\n            return False\n        changed = False\n        with self.lock:\n            old_text = self._labels.get(name)\n            if text:\n                text = text.replace(\"\\n\", \" \")\n                if old_text != text:\n                    self._labels[name] = text\n                    changed = True\n            else:\n                if old_text is not None:\n                    self._labels.pop(name)\n                    changed = True\n        if changed:\n            run_hook('set_label', self, name, text)\n        return changed\n\n    def import_labels(self, path):\n        data = read_json_file(path)\n        for key, value in data.items():\n            self.set_label(key, value)\n\n    def export_labels(self, path):\n        write_json_file(path, self.get_all_labels())\n\n    def set_fiat_value(self, txid, ccy, text, fx, value_sat):\n        if not self.db.get_transaction(txid):\n            return\n        # since fx is inserting the thousands separator,\n        # and not util, also have fx remove it\n        text = fx.remove_thousands_separator(text)\n        def_fiat = self.default_fiat_value(txid, fx, value_sat)\n        formatted = fx.ccy_amount_str(def_fiat, add_thousands_sep=False)\n        def_fiat_rounded = Decimal(formatted)\n        reset = not text\n        if not reset:\n            try:\n                text_dec = Decimal(text)\n                text_dec_rounded = Decimal(fx.ccy_amount_str(text_dec, add_thousands_sep=False))\n                reset = text_dec_rounded == def_fiat_rounded\n            except Exception:\n                # garbage. not resetting, but not saving either\n                return False\n        if reset:\n            d = self.fiat_value.get(ccy, {})\n            if d and txid in d:\n                d.pop(txid)\n            else:\n                # avoid saving empty dict\n                return True\n        else:\n            if ccy not in self.fiat_value:\n                self.fiat_value[ccy] = {}\n            self.fiat_value[ccy][txid] = text\n        return reset\n\n    def get_fiat_value(self, txid, ccy):\n        fiat_value = self.fiat_value.get(ccy, {}).get(txid)\n        try:\n            return Decimal(fiat_value)\n        except Exception:\n            return\n\n    def is_mine(self, address) -> bool:\n        if not address: return False\n        return bool(self.get_address_index(address))\n\n    def is_change(self, address) -> bool:\n        if not self.is_mine(address):\n            return False\n        return self.get_address_index(address)[0] == 1\n\n    @abstractmethod\n    def get_addresses(self) -> Sequence[str]:\n        pass\n\n    @abstractmethod\n    def get_address_index(self, address: str) -> Optional[AddressIndexGeneric]:\n        pass\n\n    @abstractmethod\n    def get_address_path_str(self, address: str) -> Optional[str]:\n        \"\"\"Returns derivation path str such as \"m/0/5\" to address,\n        or None if not applicable.\n        \"\"\"\n        pass\n\n    def get_redeem_script(self, address: str) -> Optional[str]:\n        desc = self.get_script_descriptor_for_address(address)\n        if desc is None: return None\n        redeem_script = desc.expand().redeem_script\n        if redeem_script:\n            return redeem_script.hex()\n\n    def get_witness_script(self, address: str) -> Optional[str]:\n        desc = self.get_script_descriptor_for_address(address)\n        if desc is None: return None\n        witness_script = desc.expand().witness_script\n        if witness_script:\n            return witness_script.hex()\n\n    @abstractmethod\n    def get_txin_type(self, address: str) -> str:\n        \"\"\"Return script type of wallet address.\"\"\"\n        pass\n\n    def export_private_key(self, address: str, password: Optional[str]) -> str:\n        if self.is_watching_only():\n            raise UserFacingException(_(\"This is a watching-only wallet\"))\n        if not is_address(address):\n            raise UserFacingException(_('Invalid bitcoin address: {}').format(address))\n        if not self.is_mine(address):\n            raise UserFacingException(_('Address not in wallet: {}').format(address))\n        index = self.get_address_index(address)\n        pk, compressed = self.keystore.get_private_key(index, password)\n        txin_type = self.get_txin_type(address)\n        serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type)\n        return serialized_privkey\n\n    def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str:\n        raise UserFacingException(\"this wallet is not deterministic\")\n\n    @abstractmethod\n    def get_public_keys(self, address: str) -> Sequence[str]:\n        pass\n\n    def get_public_keys_with_deriv_info(self, address: str) -> Dict[bytes, Tuple[KeyStoreWithMPK, Sequence[int]]]:\n        \"\"\"Returns a map: pubkey -> (keystore, derivation_suffix)\"\"\"\n        return {}\n\n    def is_lightning_funding_tx(self, txid: Optional[str]) -> bool:\n        if not self.lnworker or txid is None:\n            return False\n        if any([chan.funding_outpoint.txid == txid\n                for chan in self.lnworker.channels.values()]):\n            return True\n        if any([chan.funding_outpoint.txid == txid\n                for chan in self.lnworker.channel_backups.values()]):\n            return True\n        return False\n\n    def get_swaps_by_claim_tx(self, tx: Transaction) -> Iterable['SwapData']:\n        return self.lnworker.swap_manager.get_swaps_by_claim_tx(tx) if self.lnworker else []\n\n    def get_swaps_by_funding_tx(self, tx: Transaction) -> Iterable['SwapData']:\n        return self.lnworker.swap_manager.get_swaps_by_funding_tx(tx) if self.lnworker else []\n\n    def is_accounting_address(self, addr):\n        \"\"\"\n        Addresses from which we have been able to sweep funds.\n        We consider them 'ours' for accounting purposes, so that the\n        wallet history does not show funds going in and out of the wallet.\n        \"\"\"\n        # must be a sweep utxo AND we swept (spending tx is a wallet tx)\n        return addr in self._accounting_addresses\n\n    def get_wallet_delta(self, tx: Transaction) -> TxWalletDelta:\n        \"\"\"Return the effect a transaction has on the wallet.\n        This method must use self.is_mine, not self.adb.is_mine()\n        \"\"\"\n        is_relevant = False  # \"related to wallet?\"\n        num_input_ismine = 0\n        v_in = v_in_mine = v_out = v_out_mine = 0\n        with self.lock:\n            for txin in tx.inputs():\n                addr = self.adb.get_txin_address(txin)\n                value = self.adb.get_txin_value(txin, address=addr)\n                if self.is_mine(addr) or self.is_accounting_address(addr):\n                    num_input_ismine += 1\n                    is_relevant = True\n                    assert value is not None\n                    v_in_mine += value\n                if value is None:\n                    v_in = None\n                elif v_in is not None:\n                    v_in += value\n            for txout in tx.outputs():\n                v_out += txout.value\n                if self.is_mine(txout.address) or self.is_accounting_address(txout.address):\n                    v_out_mine += txout.value\n                    is_relevant = True\n        delta = v_out_mine - v_in_mine\n        if v_in is not None:\n            fee = v_in - v_out\n        else:\n            fee = None\n        if fee is None and isinstance(tx, PartialTransaction):\n            fee = tx.get_fee()\n        return TxWalletDelta(\n            is_relevant=is_relevant,\n            is_any_input_ismine=num_input_ismine > 0,\n            is_all_input_ismine=num_input_ismine == len(tx.inputs()),\n            delta=delta,\n            fee=fee,\n        )\n\n    def get_tx_info(self, tx: Transaction) -> TxWalletDetails:\n        tx_wallet_delta = self.get_wallet_delta(tx)\n        is_relevant = tx_wallet_delta.is_relevant\n        is_any_input_ismine = tx_wallet_delta.is_any_input_ismine\n        is_swap = bool(self.get_swaps_by_claim_tx(tx))\n        fee = tx_wallet_delta.fee\n        exp_n = None\n        can_broadcast = False\n        can_bump = False\n        can_cpfp = False\n        tx_hash = tx.txid()  # note: txid can be None! e.g. when called from GUI tx dialog\n        is_lightning_funding_tx = self.is_lightning_funding_tx(tx_hash)\n        tx_we_already_have_in_db = self.adb.db.get_transaction(tx_hash)\n        can_save_as_local = (is_relevant and tx.txid() is not None\n                             and (tx_we_already_have_in_db is None or not tx_we_already_have_in_db.is_complete()))\n        label = ''\n        tx_mined_status = self.adb.get_tx_height(tx_hash)\n        can_remove = ((tx_mined_status.height() in [TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL])\n                      # otherwise 'height' is unreliable (typically LOCAL):\n                      and is_relevant\n                      # don't offer during common signing flow, e.g. when watch-only wallet starts creating a tx:\n                      and bool(tx_we_already_have_in_db))\n        can_dscancel = False\n        if tx.is_complete():\n            if tx_we_already_have_in_db:\n                label = self.get_label_for_txid(tx_hash)\n                if tx_mined_status.height() > 0:\n                    if tx_mined_status.conf:\n                        status = _(\"{} confirmations\").format(tx_mined_status.conf)\n                    else:\n                        status = _('Not verified')\n                elif tx_mined_status.height() in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED):\n                    status = _('Unconfirmed')\n                    if fee is None:\n                        fee = self.adb.get_tx_fee(tx_hash)\n                    if fee and self.network and self.network.has_fee_mempool():\n                        size = tx.estimated_size()\n                        fee_per_byte = fee / size\n                        exp_n = self.network.mempool_fees.fee_to_depth(fee_per_byte)\n                    can_bump = (is_any_input_ismine or is_swap) and self.can_rbf_tx(tx)\n                    can_dscancel = (is_any_input_ismine and self.can_rbf_tx(tx, is_dscancel=True)\n                                    and not all([self.is_mine(txout.address) for txout in tx.outputs()]))\n                    try:\n                        self.cpfp(tx, 0)\n                        can_cpfp = True\n                    except Exception:\n                        can_cpfp = False\n                else:\n                    status = _('Local')\n                    if tx_mined_status.height() == TX_HEIGHT_FUTURE:\n                        num_blocks_remainining = tx_mined_status.wanted_height - self.adb.get_local_height()\n                        num_blocks_remainining = max(0, num_blocks_remainining)\n                        status = _('Local (future: {})').format(_('in {} blocks').format(num_blocks_remainining))\n                    can_broadcast = self.network is not None\n                    can_bump = (is_any_input_ismine or is_swap) and self.can_rbf_tx(tx)\n            else:\n                status = _(\"Signed\")\n                can_broadcast = self.network is not None\n        else:\n            assert isinstance(tx, PartialTransaction)\n            s, r = tx.signature_count()\n            status = _(\"Unsigned\") if s == 0 else _('Partially signed') + ' (%d/%d)' % (s, r)\n\n        if is_relevant:\n            if tx_wallet_delta.is_all_input_ismine:\n                assert fee is not None\n                amount = tx_wallet_delta.delta + fee\n            else:\n                amount = tx_wallet_delta.delta\n        else:\n            amount = None\n\n        if is_lightning_funding_tx:\n            assert not can_bump  # would change txid\n\n        return TxWalletDetails(\n            txid=tx_hash,\n            status=status,\n            label=label,\n            can_broadcast=can_broadcast,\n            can_bump=can_bump,\n            can_cpfp=can_cpfp,\n            can_dscancel=can_dscancel,\n            can_save_as_local=can_save_as_local,\n            amount=amount,\n            fee=fee,\n            tx_mined_status=tx_mined_status,\n            mempool_depth_bytes=exp_n,\n            can_remove=can_remove,\n            is_lightning_funding_tx=is_lightning_funding_tx,\n            is_related_to_wallet=is_relevant,\n        )\n\n    def get_num_parents(self, txid: str) -> Optional[int]:\n        if not self.is_up_to_date():\n            return\n        if txid not in self._num_parents:\n            self._num_parents[txid] = len(self.get_tx_parents(txid))\n        return self._num_parents[txid]\n\n    def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]:\n        \"\"\"\n        returns a flat dict:\n        txid -> list of parent txids\n        \"\"\"\n        with self.lock:\n            if self._last_full_history is None:\n                self._last_full_history = self.get_onchain_history()\n                # populate cache in chronological order (confirmed tx only)\n                # todo: get_full_history should return unconfirmed tx topologically sorted\n                for _txid, tx_item in self._last_full_history.items():\n                    if tx_item.tx_mined_status.height() > 0:\n                        self.get_tx_parents(_txid)\n\n            result = self._tx_parents_cache.get(txid, None)\n            if result is not None:\n                return result\n            result = {}   # type: Dict[str, Tuple[List[str], List[str]]]\n            parents = []  # type: List[str]\n            uncles = []   # type: List[str]\n            tx = self.adb.get_transaction(txid)\n            assert tx, f\"cannot find {txid} in db\"\n            for i, txin in enumerate(tx.inputs()):\n                _txid = txin.prevout.txid.hex()\n                parents.append(_txid)\n                # detect address reuse\n                addr = self.adb.get_txin_address(txin)\n                if addr is None:\n                    continue\n                received, sent = self.adb.get_addr_io(addr)\n                if len(sent) > 1:\n                    my_txid, my_height, my_pos = sent[txin.prevout.to_str()]\n                    assert my_txid == txid\n                    for k, v in sent.items():\n                        if k != txin.prevout.to_str():\n                            reuse_txid, reuse_height, reuse_pos = v\n                            if reuse_height <= 0:  # exclude not-yet-mined (we need topological ordering)\n                                continue\n                            if (reuse_height, reuse_pos) < (my_height, my_pos):\n                                uncle_txid, uncle_index = k.split(':')\n                                uncles.append(uncle_txid)\n\n            for _txid in parents + uncles:\n                if _txid in self._last_full_history.keys():\n                    result.update(self.get_tx_parents(_txid))\n            result[txid] = parents, uncles\n            self._tx_parents_cache[txid] = result\n            return result\n\n    def get_balance(self, **kwargs):\n        \"\"\"Note: intended for display-purposes.\n        Do not use for NotEnoughFunds checks. Use get_spendable_balance_sat() instead.\n        \"\"\"\n        domain = self.get_addresses()\n        return self.adb.get_balance(domain, **kwargs)\n\n    def anchor_reserve(self) -> int:\n        if self.lnworker is None or not isinstance(self.lnworker, LNWallet):\n            return 0\n        if not self.lnworker.has_anchor_channels():\n            return 0\n        return self.config.LN_UTXO_RESERVE\n\n    def get_spendable_balance_sat(\n        self,\n        deduct_anchor_reserve: bool = True,\n        **kwargs\n    ) -> int:\n        anchor_reserve = self.anchor_reserve() if deduct_anchor_reserve else 0\n        spendable_coins = self.get_spendable_coins(**kwargs)\n        oc_balance = sum([coin.value_sats() for coin in spendable_coins]) - anchor_reserve\n        return max(0, oc_balance)\n\n    def get_addr_balance(self, address) -> tuple[int, int, int]:\n        return self.adb.get_balance([address])\n\n    def get_utxos(\n            self,\n            domain: Optional[Iterable[str]] = None,\n            **kwargs,\n    ) -> Sequence[PartialTxInput]:\n        if domain is None:\n            domain = self.get_addresses()\n        return self.adb.get_utxos(domain=domain, **kwargs)\n\n    def get_spendable_coins(\n            self,\n            domain: Optional[Iterable[str]] = None,\n            *,\n            nonlocal_only: bool = False,\n            confirmed_only: bool = None,\n    ) -> Sequence[PartialTxInput]:\n        with self._freeze_lock:\n            frozen_addresses = self._frozen_addresses.copy()\n        if confirmed_only is None:\n            confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY\n        utxos = self.get_utxos(\n            domain=domain,\n            excluded_addresses=frozen_addresses,\n            mature_only=True,\n            confirmed_funding_only=confirmed_only,\n            nonlocal_only=nonlocal_only,\n        )\n        utxos = [utxo for utxo in utxos if not self.is_frozen_coin(utxo)]\n        return utxos\n\n    @abstractmethod\n    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:\n        pass\n\n    @abstractmethod\n    def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:\n        pass\n\n    def dummy_address(self) -> str:\n        # first receiving address\n        return self.get_receiving_addresses(slice_start=0, slice_stop=1)[0]\n\n    def get_frozen_balance(self) -> tuple[int, int, int]:\n        with self._freeze_lock:\n            frozen_addresses = self._frozen_addresses.copy()\n        # note: for coins, use is_frozen_coin instead of _frozen_coins,\n        #       as latter only contains *manually* frozen ones\n        frozen_coins = {utxo.prevout.to_str() for utxo in self.get_utxos()\n                        if self.is_frozen_coin(utxo)}\n        if not frozen_coins:  # shortcut\n            return self.adb.get_balance(frozen_addresses)\n        c1, u1, x1 = self.get_balance()\n        c2, u2, x2 = self.get_balance(\n            excluded_addresses=frozen_addresses,\n            excluded_coins=frozen_coins,\n        )\n        return c1-c2, u1-u2, x1-x2\n\n    def get_balances_for_piechart(self) -> PiechartBalance:\n        \"\"\"Note: intended for display-purposes.\n        Do not use for NotEnoughFunds checks. Use get_spendable_balance_sat() instead.\n        \"\"\"\n        # return only positive values\n        c, u, x = self.get_balance()\n        fc, fu, fx = self.get_frozen_balance()\n        lightning = self.lnworker.get_balance() if self.has_lightning() else 0\n        f_lightning = self.lnworker.get_balance(frozen=True) if self.has_lightning() else 0\n        # subtract frozen funds\n        cc = c - fc\n        uu = u - fu\n        xx = x - fx\n        frozen = fc + fu + fx\n        return PiechartBalance(\n            confirmed=cc,\n            unconfirmed=uu,\n            unmatured=xx,\n            frozen=frozen,\n            lightning=lightning - f_lightning,\n            lightning_frozen=f_lightning,\n        )\n\n    def balance_at_timestamp(self, domain, target_timestamp):\n        # we assume that get_history returns items ordered by block height\n        # we also assume that block timestamps are monotonic (which is false...!)\n        h = self.adb.get_history(domain=domain)\n        balance = 0\n        for hist_item in h:\n            balance = hist_item.balance\n            if hist_item.tx_mined_status.timestamp is None or hist_item.tx_mined_status.timestamp > target_timestamp:\n                return balance - hist_item.delta\n        # return last balance\n        return balance\n\n    def get_onchain_history(\n            self, *,\n            domain=None,\n            from_timestamp=None,\n            to_timestamp=None,\n            from_height=None,  # [from_height, to_height[\n            to_height=None,\n    ) -> Dict[str, OnchainHistoryItem]:\n        # sanity check\n        if (from_timestamp is not None or to_timestamp is not None) \\\n                and (from_height is not None or to_height is not None):\n            raise UserFacingException('timestamp and block height based filtering cannot be used together')\n        # call lnworker first, because it adds accounting addresses\n        groups = self.lnworker.get_groups_for_onchain_history() if self.lnworker else {}\n        if domain is None:\n            domain = self.get_addresses()\n            domain += list(self._accounting_addresses)\n\n        now = time.time()\n        transactions = OrderedDictWithIndex()\n        monotonic_timestamp = 0\n        for hist_item in self.adb.get_history(domain=domain):\n            timestamp = (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF)\n            height = hist_item.tx_mined_status.height()\n            if from_timestamp and (timestamp or now) < from_timestamp:\n                continue\n            if to_timestamp and (timestamp or now) >= to_timestamp:\n                continue\n            if from_height is not None and from_height > height > 0:\n                continue\n            if to_height is not None and (height >= to_height or height <= 0):\n                continue\n            monotonic_timestamp = max(monotonic_timestamp, timestamp)\n            txid = hist_item.txid\n            group_id = groups.get(txid)\n            label = self.get_label_for_txid(txid)\n            tx_item = OnchainHistoryItem(\n                txid=hist_item.txid,\n                amount_sat=hist_item.delta,\n                fee_sat=hist_item.fee,\n                balance_sat=hist_item.balance,\n                tx_mined_status=hist_item.tx_mined_status,\n                label=label,\n                monotonic_timestamp=monotonic_timestamp,\n                group_id=group_id,\n            )\n            transactions[hist_item.txid] = tx_item\n\n        return transactions\n\n    def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:\n        height = self.adb.get_local_height()\n        if pr:\n            return Invoice.from_bip70_payreq(pr, height=height)\n        amount_msat = 0\n        for x in outputs:\n            if parse_max_spend(x.value):\n                amount_msat = '!'\n                break\n            else:\n                assert isinstance(x.value, int), f\"{x.value!r}\"\n                amount_msat += x.value * 1000\n        timestamp = None\n        exp = None\n        if URI:\n            timestamp = URI.get('time')\n            exp = URI.get('exp')\n        timestamp = timestamp or int(Invoice._get_cur_time())\n        exp = exp or 0\n        invoice = Invoice(\n            amount_msat=amount_msat,\n            message=message,\n            time=timestamp,\n            exp=exp,\n            outputs=outputs,\n            bip70=None,\n            height=height,\n            lightning_invoice=None,\n        )\n        return invoice\n\n    def save_invoice(self, invoice: Invoice, *, write_to_disk: bool = True) -> None:\n        key = invoice.get_id()\n        if not invoice.is_lightning():\n            if self.is_onchain_invoice_paid(invoice)[0]:\n                _logger.info(\"saving invoice... but it is already paid!\")\n            with self.lock:\n                for txout in invoice.get_outputs():\n                    self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)\n        self._invoices[key] = invoice\n        if write_to_disk:\n            self.save_db()\n\n    def clear_invoices(self):\n        self._invoices.clear()\n        self.save_db()\n\n    def clear_requests(self):\n        self._receive_requests.clear()\n        self._requests_addr_to_key.clear()\n        self.save_db()\n\n    def get_invoices(self) -> List[Invoice]:\n        out = list(self._invoices.values())\n        out.sort(key=lambda x: x.time)\n        return out\n\n    def get_unpaid_invoices(self) -> List[Invoice]:\n        invoices = self.get_invoices()\n        return [x for x in invoices if self.get_invoice_status(x) != PR_PAID]\n\n    def get_invoice(self, invoice_id):\n        return self._invoices.get(invoice_id)\n\n    def import_requests(self, path):\n        data = read_json_file(path)\n        for x in data:\n            try:\n                req = Request(**x)\n            except Exception:\n                raise FileImportFailed(_(\"Invalid invoice format\"))\n            self.add_payment_request(req, write_to_disk=False)\n        self.save_db()\n\n    def export_requests(self, path):\n        # note: this does not export preimages for LN bolt11 invoices\n        write_json_file(path, list(self._receive_requests.values()))\n\n    def import_invoices(self, path):\n        data = read_json_file(path)\n        for x in data:\n            try:\n                invoice = Invoice(**x)\n            except Exception:\n                raise FileImportFailed(_(\"Invalid invoice format\"))\n            self.save_invoice(invoice, write_to_disk=False)\n        self.save_db()\n\n    def export_invoices(self, path):\n        write_json_file(path, list(self._invoices.values()))\n\n    def get_relevant_invoices_for_tx(self, tx_hash: Optional[str]) -> Sequence[Invoice]:\n        if not tx_hash:\n            return []\n        invoice_keys = self._invoices_from_txid_map.get(tx_hash, set())\n        invoices = [self.get_invoice(key) for key in invoice_keys]\n        invoices = [inv for inv in invoices if inv]  # filter out None\n        for inv in invoices:\n            assert isinstance(inv, Invoice), f\"unexpected type {type(inv)}\"\n        return invoices\n\n    def _init_requests_rhash_index(self):\n        # self._requests_addr_to_key may contain addresses that can be reused\n        # this is checked in get_request_by_address\n        self._requests_addr_to_key = defaultdict(set)  # type: Dict[str, Set[str]]\n        for req in self._receive_requests.values():\n            if addr := req.get_address():\n                self._requests_addr_to_key[addr].add(req.get_id())\n\n    def _prepare_onchain_invoice_paid_detection(self):\n        self._invoices_from_txid_map = defaultdict(set)  # type: Dict[str, Set[str]]\n        self._invoices_from_scriptpubkey_map = defaultdict(set)  # type: Dict[bytes, Set[str]]\n        self._update_onchain_invoice_paid_detection(self._invoices.keys())\n\n    def _update_onchain_invoice_paid_detection(self, invoice_keys: Iterable[str]) -> None:\n        for invoice_key in invoice_keys:\n            invoice = self._invoices.get(invoice_key)\n            if not invoice:\n                continue\n            if invoice.is_lightning() and not invoice.get_address():\n                continue\n            if invoice.is_lightning() and self.lnworker and self.lnworker.get_invoice_status(invoice) == PR_PAID:\n                continue\n            is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)\n            if is_paid:\n                for txid in relevant_txs:\n                    self._invoices_from_txid_map[txid].add(invoice_key)\n            for txout in invoice.get_outputs():\n                self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)\n            # update invoice status\n            status = self.get_invoice_status(invoice)\n            util.trigger_callback('invoice_status', self, invoice_key, status)\n\n    def _is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int], Sequence[str]]:\n        \"\"\"Returns whether on-chain invoice/request is satisfied, num confs required txs have,\n        and list of relevant TXIDs.\n        \"\"\"\n        outputs = invoice.get_outputs()\n        if not outputs:  # e.g. lightning-only\n            return False, None, []\n        invoice_amounts = defaultdict(int)  # type: Dict[bytes, int]  # scriptpubkey -> value_sats\n        for txo in outputs:  # type: PartialTxOutput\n            invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value\n        relevant_txs = set()\n        is_paid = True\n        conf_needed = None  # type: Optional[int]\n        with self.lock:\n            for invoice_scriptpubkey, invoice_amt in invoice_amounts.items():\n                scripthash = bitcoin.script_to_scripthash(invoice_scriptpubkey)\n                prevouts_and_values = self.db.get_prevouts_by_scripthash(scripthash)\n                confs_and_values = []\n                for prevout, v in prevouts_and_values:\n                    relevant_txs.add(prevout.txid.hex())\n                    tx_height = self.adb.get_tx_height(prevout.txid.hex())\n                    if 0 < tx_height.height() <= invoice.height:  # exclude txs older than invoice\n                        continue\n                    confs_and_values.append((tx_height.conf or 0, v))\n                # check that there is at least one TXO, and that they pay enough.\n                # note: \"at least one TXO\" check is needed for zero amount invoice (e.g. OP_RETURN)\n                vsum = 0\n                for conf, v in reversed(sorted(confs_and_values)):\n                    vsum += v\n                    if vsum >= invoice_amt:\n                        conf_needed = min(conf_needed, conf) if conf_needed is not None else conf\n                        break\n                else:\n                    is_paid = False\n        return is_paid, conf_needed, list(relevant_txs)\n\n    def is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int]]:\n        is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)\n        return is_paid, conf_needed\n\n    @profiler\n    def get_full_history(\n            self,\n            *,\n            fx: 'FxThread' = None,  # used for fiat values if set\n            onchain_domain=None,\n            include_lightning=True,\n    ) -> OrderedDictWithIndex:\n        \"\"\"\n        includes both onchain and lightning\n        includes grouping information\n        \"\"\"\n        include_fiat = fx is not None and fx.has_history()\n        transactions_tmp = OrderedDictWithIndex()\n        # add on-chain txns\n        onchain_history = self.get_onchain_history(domain=onchain_domain)\n        for tx_item in onchain_history.values():\n            txid = tx_item.txid\n            transactions_tmp[txid] = tx_item.to_dict()\n            transactions_tmp[txid]['lightning'] = False\n\n        # add lightning_transactions\n        lightning_history = self.lnworker.get_lightning_history() if self.lnworker and include_lightning else {}\n        for tx_item in lightning_history.values():\n            key = tx_item.payment_hash or 'ln:' + tx_item.group_id\n            transactions_tmp[key] = tx_item.to_dict()\n            transactions_tmp[key]['lightning'] = True\n\n        # sort on-chain and LN stuff into new dict, by timestamp\n        # (we rely on this being a *stable* sort)\n        def sort_key(x):\n            txid, tx_item = x\n            ts = tx_item.get('monotonic_timestamp') or tx_item.get('timestamp') or float('inf')\n            height = self.adb.tx_height_to_sort_height(tx_item.get('height'))\n            return ts, height\n        # create groups\n        transactions = OrderedDictWithIndex()\n        for k, tx_item in sorted(list(transactions_tmp.items()), key=sort_key):\n            if 'ln_value' not in tx_item:\n                tx_item['ln_value'] = Satoshis(0)\n            if 'bc_value' not in tx_item:\n                tx_item['bc_value'] = Satoshis(0)\n            group_id = tx_item.get('group_id')\n            if not group_id:\n                transactions[k] = tx_item\n            else:\n                key = 'group:' + group_id\n                parent = transactions.get(key)\n                group_label = self.get_label_for_group(group_id)\n                if parent is None:\n                    parent = {\n                        'label': group_label,\n                        'bc_value': Satoshis(0),\n                        'ln_value': Satoshis(0),\n                        'value': Satoshis(0),\n                        'children': [],\n                        'timestamp': 0,\n                        'date': timestamp_to_datetime(0),\n                        'fee_sat': 0,\n                        # fixme: there is no guarantee that there will be an onchain tx in the group\n                        'height': 0,\n                        'confirmations': 0,\n                        'txid': '----',\n                    }\n                    if include_fiat:\n                        parent['fiat_value'] = Fiat(Decimal(0), fx.ccy)\n                    transactions[key] = parent\n                parent['bc_value'] += tx_item['bc_value']\n                parent['ln_value'] += tx_item['ln_value']\n                parent['value'] = parent['bc_value'] + parent['ln_value']\n                if 'fiat_value' in tx_item:\n                    parent['fiat_value'] += tx_item['fiat_value']\n                if tx_item.get('txid') == group_id:\n                    parent['lightning'] = False\n                    parent['txid'] = tx_item['txid']\n                    parent['timestamp'] = tx_item['timestamp']\n                    parent['date'] = timestamp_to_datetime(tx_item['timestamp'])\n                    parent['height'] = tx_item['height']\n                    parent['confirmations'] = tx_item['confirmations']\n                    parent['wanted_height'] = tx_item.get('wanted_height')\n                parent['children'].append(tx_item)\n\n        now = time.time()\n        for key, item in transactions.items():\n            children = item.get('children', [])\n            if len(children) == 1:\n                transactions[key] = children[0]\n            # add on-chain and lightning values\n            # note: 'value' has msat precision (as LN has msat precision)\n            item['value'] = item.get('bc_value', Satoshis(0)) + item.get('ln_value', Satoshis(0))\n            for child in item.get('children', []):\n                child['value'] = child.get('bc_value', Satoshis(0)) + child.get('ln_value', Satoshis(0))\n            if not include_fiat:\n                continue\n            # add fiat values to both the root item and its children\n            for add_fiat_item in [item] + children:\n                value = add_fiat_item['value'].value\n                txid = add_fiat_item.get('txid')\n                if not add_fiat_item.get('lightning') and txid:\n                    fiat_fields = self.get_tx_item_fiat(tx_hash=txid, amount_sat=value, fx=fx, tx_fee=add_fiat_item['fee_sat'])\n                    add_fiat_item.update(fiat_fields)\n                else:\n                    timestamp = add_fiat_item['timestamp'] or now\n                    fiat_value = value / Decimal(bitcoin.COIN) * fx.timestamp_rate(timestamp)\n                    add_fiat_item['fiat_value'] = Fiat(fiat_value, fx.ccy)\n                    add_fiat_item['fiat_default'] = True\n        return transactions\n\n    @profiler\n    def get_onchain_capital_gains(self, fx, **kwargs):\n        # History with capital gains, using utxo pricing\n        # FIXME: Lightning capital gains would requires FIFO\n        from_timestamp = kwargs.get('from_timestamp')\n        to_timestamp = kwargs.get('to_timestamp')\n        history = self.get_onchain_history(**kwargs)\n        show_fiat = fx and fx.is_enabled() and fx.has_history()\n        out = []\n        income = 0\n        expenditures = 0\n        capital_gains = Decimal(0)\n        fiat_income = Decimal(0)\n        fiat_expenditures = Decimal(0)\n        for txid, hitem in history.items():\n            item = hitem.to_dict()\n            if item['bc_value'].value == 0:\n                continue\n            timestamp = item['timestamp']\n            tx_hash = item['txid']\n            tx_fee = item['fee_sat']\n            # fixme: use in and out values\n            value = item['bc_value'].value\n            if value < 0:\n                expenditures += -value\n            else:\n                income += value\n            # fiat computations\n            if show_fiat:\n                fiat_fields = self.get_tx_item_fiat(tx_hash=tx_hash, amount_sat=value, fx=fx, tx_fee=tx_fee)\n                fiat_value = fiat_fields['fiat_value'].value\n                if value < 0:\n                    capital_gains += fiat_fields['capital_gain'].value\n                    fiat_expenditures += -fiat_value\n                else:\n                    fiat_income += fiat_value\n            out.append(item)\n        # add summary\n        if out:\n            first_item = out[0]\n            last_item = out[-1]\n            start_height = first_item['height'] - 1\n            end_height = last_item['height']\n\n            b = first_item['bc_balance'].value\n            v = first_item['bc_value'].value\n            start_balance = None if b is None or v is None else b - v\n            end_balance = last_item['bc_balance'].value\n\n            if from_timestamp is not None and to_timestamp is not None:\n                start_timestamp = from_timestamp\n                end_timestamp = to_timestamp\n            else:\n                start_timestamp = first_item['timestamp']\n                end_timestamp = last_item['timestamp']\n\n            start_coins = self.get_utxos(\n                block_height=start_height,\n                confirmed_funding_only=True,\n                confirmed_spending_only=True,\n                nonlocal_only=True)\n            end_coins = self.get_utxos(\n                block_height=end_height,\n                confirmed_funding_only=True,\n                confirmed_spending_only=True,\n                nonlocal_only=True)\n\n            def summary_point(timestamp, height, balance, coins):\n                date = timestamp_to_datetime(timestamp)\n                out = {\n                    'date': date,\n                    'block_height': height,\n                    'BTC_balance': Satoshis(balance),\n                }\n                if show_fiat:\n                    ap = self.acquisition_price(coins, fx.timestamp_rate, fx.ccy)\n                    lp = self.liquidation_price(coins, fx.timestamp_rate, timestamp)\n                    out['acquisition_price'] = Fiat(ap, fx.ccy)\n                    out['liquidation_price'] = Fiat(lp, fx.ccy)\n                    out['unrealized_gains'] = Fiat(lp - ap, fx.ccy)\n                    out['fiat_balance'] = Fiat(fx.historical_value(balance, date), fx.ccy)\n                    out['BTC_fiat_price'] = Fiat(fx.historical_value(COIN, date), fx.ccy)\n                return out\n\n            summary_start = summary_point(start_timestamp, start_height, start_balance, start_coins)\n            summary_end = summary_point(end_timestamp, end_height, end_balance, end_coins)\n            flow = {\n                'BTC_incoming': Satoshis(income),\n                'BTC_outgoing': Satoshis(expenditures)\n            }\n            if show_fiat:\n                flow['fiat_currency'] = fx.ccy\n                flow['fiat_incoming'] = Fiat(fiat_income, fx.ccy)\n                flow['fiat_outgoing'] = Fiat(fiat_expenditures, fx.ccy)\n                flow['realized_capital_gains'] = Fiat(capital_gains, fx.ccy)\n            summary = {\n                'begin': summary_start,\n                'end': summary_end,\n                'flow': flow,\n            }\n\n        else:\n            summary = {}\n        return summary\n\n    def acquisition_price(self, coins, price_func, ccy):\n        return Decimal(sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.adb.get_txin_value(coin)) for coin in coins))\n\n    def liquidation_price(self, coins, price_func, timestamp):\n        p = price_func(timestamp)\n        return sum([coin.value_sats() for coin in coins]) * p / Decimal(COIN)\n\n    def default_fiat_value(self, tx_hash, fx, value_sat):\n        return value_sat / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate)\n\n    def get_tx_item_fiat(\n            self,\n            *,\n            tx_hash: str,\n            amount_sat: int,\n            fx: 'FxThread',\n            tx_fee: Optional[int],\n    ) -> Dict[str, Any]:\n        item = {}\n        fiat_value = self.get_fiat_value(tx_hash, fx.ccy)\n        fiat_default = fiat_value is None\n        fiat_rate = self.price_at_timestamp(tx_hash, fx.timestamp_rate)\n        fiat_value = fiat_value if fiat_value is not None else self.default_fiat_value(tx_hash, fx, amount_sat)\n        fiat_fee = tx_fee / Decimal(COIN) * fiat_rate if tx_fee is not None else None\n        item['fiat_currency'] = fx.ccy\n        item['fiat_rate'] = Fiat(fiat_rate, fx.ccy)\n        item['fiat_value'] = Fiat(fiat_value, fx.ccy)\n        item['fiat_fee'] = Fiat(fiat_fee, fx.ccy) if fiat_fee is not None else None\n        item['fiat_default'] = fiat_default\n        if amount_sat < 0:\n            acquisition_price = - amount_sat / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy)\n            liquidation_price = - fiat_value\n            item['acquisition_price'] = Fiat(acquisition_price, fx.ccy)\n            cg = liquidation_price - acquisition_price\n            item['capital_gain'] = Fiat(cg, fx.ccy)\n        return item\n\n    def _get_label(self, key: str) -> str:\n        # key is typically: address / txid / LN-payment-hash-hex\n        return self._labels.get(key) or ''\n\n    def get_label_for_address(self, addr: str) -> str:\n        label = self._labels.get(addr) or ''\n        if not label and (request := self.get_request_by_addr(addr)):\n            label = request.get_message()\n        return label\n\n    def set_default_label(self, key: str, value: str):\n        self._default_labels[key] = value\n\n    def get_label_for_outpoint(self, outpoint: str) -> str:\n        return self._labels.get(outpoint) or self._get_default_label_for_outpoint(outpoint)\n\n    def _get_default_label_for_outpoint(self, outpoint: str) -> str:\n        return self._default_labels.get(outpoint)\n\n    def get_label_for_group(self, group_id: str) -> str:\n        return self._default_labels.get('group:' + group_id)\n\n    def set_group_label(self, group_id: str, label: str):\n        self._default_labels['group:' + group_id] = label\n\n    def get_label_for_txid(self, tx_hash: str) -> str:\n        assert tx_hash, f\"expected a txid, got {tx_hash!r}\"\n        return self._labels.get(tx_hash) or self._get_default_label_for_txid(tx_hash) or \"\"\n\n    def _get_default_label_for_txid(self, tx_hash: str) -> str:\n        if label := self._default_labels.get(tx_hash):\n            return label\n        labels = []\n        tx = self.adb.get_transaction(tx_hash)\n        if tx:\n            for txin in tx.inputs():\n                outpoint = txin.prevout.to_str()\n                if label := self.get_label_for_outpoint(outpoint):\n                    labels.append('sweep ' + label)\n            if not labels:\n                for i in range(len(tx.outputs())):\n                    outpoint = tx_hash + f':{i}'\n                    if label := self.get_label_for_outpoint(outpoint):\n                        labels.append(label)\n\n        # note: we don't deserialize tx as the history calls us for every tx, and that would be slow\n        if not self.db.get_txi_addresses(tx_hash):\n            # no inputs are ismine -> likely incoming payment -> concat labels of output addresses\n            for addr in self.db.get_txo_addresses(tx_hash):\n                label = self.get_label_for_address(addr)\n                if label:\n                    labels.append(label)\n        else:\n            # some inputs are ismine -> likely outgoing payment\n            for invoice in self.get_relevant_invoices_for_tx(tx_hash):\n                if invoice.message:\n                    labels.append(invoice.message)\n        #if not labels and self.lnworker and (label:= self.lnworker.get_label_for_txid(tx_hash)):\n        #    labels.append(label)\n        return ', '.join(labels)\n\n    def _get_default_label_for_rhash(self, rhash: str) -> str:\n        req = self.get_request(rhash)\n        return req.get_message() if req else ''\n\n    def get_label_for_rhash(self, rhash: str) -> str:\n        return self._labels.get(rhash) or self._get_default_label_for_rhash(rhash)\n\n    def get_all_labels(self) -> Dict[str, str]:\n        with self.lock:\n            return copy.copy(self._labels)\n\n    def get_tx_status(self, tx_hash: str, tx_mined_info: TxMinedInfo):\n        extra = []\n        height = tx_mined_info.height()\n        conf = tx_mined_info.conf\n        timestamp = tx_mined_info.timestamp\n        if height == TX_HEIGHT_FUTURE:\n            num_blocks_remainining = tx_mined_info.wanted_height - self.adb.get_local_height()\n            num_blocks_remainining = max(0, num_blocks_remainining)\n            return 2, _('in {} blocks').format(num_blocks_remainining)\n        if conf == 0:\n            tx = self.db.get_transaction(tx_hash)\n            if not tx:\n                return 2, _(\"unknown\")\n            if not tx.is_complete():\n                tx.add_info_from_wallet(self)  # needed for estimated_size(), for txin size calc\n            fee = self.adb.get_tx_fee(tx_hash)\n            if fee is not None:\n                size = tx.estimated_size()\n                fee_per_byte = Decimal(fee) / size\n                extra.append(format_fee_satoshis(fee_per_byte) + f\" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VB}\")\n            if fee is not None and height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) \\\n               and self.network and self.network.has_fee_mempool():\n                exp_n = self.network.mempool_fees.fee_to_depth(fee_per_byte)\n                if exp_n is not None:\n                    extra.append(FeePolicy.get_depth_mb_str(exp_n))\n            if height == TX_HEIGHT_LOCAL:\n                status = 3\n            elif height == TX_HEIGHT_UNCONF_PARENT:\n                status = 1\n            elif height == TX_HEIGHT_UNCONFIRMED:\n                status = 0\n            else:\n                status = 2  # not SPV verified\n        else:\n            status = 3 + min(conf, 6)\n        time_str = format_time(timestamp) if timestamp else _(\"unknown\")\n        status_str = TX_STATUS[status] if status < 4 else time_str\n        if extra:\n            status_str += ' [%s]' % (', '.join(extra))\n        return status, status_str\n\n    def relayfee(self):\n        return relayfee(self.network)\n\n    def dust_threshold(self):\n        return dust_threshold(self.network)\n\n    def get_candidates_for_batching(\n        self,\n        outputs: Sequence[PartialTxOutput],\n        *,\n        coins: Sequence[PartialTxInput],\n    ) -> Sequence[Transaction]:\n        \"\"\"\n        coins: utxos available to add as inputs into the final tx. If empty, the set of candidates is restricted to\n               base txs with large enough change outputs to cover paying for all the `outputs`.\n        \"\"\"\n        # do not batch if we spend max (not supported by make_unsigned_transaction)\n        if any([parse_max_spend(o.value) is not None for o in outputs]):\n            return []\n        candidates = []\n        domain = self.get_addresses()\n        for hist_item in self.adb.get_history(domain):\n            # tx should not be mined yet\n            if hist_item.tx_mined_status.conf > 0: continue\n            # conservative future proofing of code: only allow known unconfirmed types\n            if hist_item.tx_mined_status.height() not in (\n                    TX_HEIGHT_UNCONFIRMED,\n                    TX_HEIGHT_UNCONF_PARENT,\n                    TX_HEIGHT_LOCAL):\n                continue\n            # tx should be \"outgoing\" from wallet\n            if hist_item.delta >= 0:\n                continue\n            tx = self.db.get_transaction(hist_item.txid)\n            if not tx:\n                continue\n            txid = tx.txid()\n            # tx should not belong to tx batcher\n            if self.txbatcher.is_mine(txid):\n                continue\n            # is_mine outputs should not be spent yet\n            # to avoid cancelling our own dependent transactions\n            if any([self.is_mine(o.address) and self.db.get_spent_outpoint(txid, output_idx)\n                    for output_idx, o in enumerate(tx.outputs())]):\n                continue\n            # all inputs should be is_mine\n            if not all([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()]):\n                continue\n            # tx must have opted-in for RBF (even if local, for consistency)\n            if not self.can_rbf_tx(tx):\n                continue\n            # reject merge if we need to spend outputs from the base tx\n            remaining_amount = sum(c.value_sats() for c in coins if c.prevout.txid.hex() != tx.txid())\n            change_amount = sum(o.value for o in tx.outputs() if self.is_change(o.address))\n            output_amount = sum(o.value for o in outputs)\n            if output_amount > remaining_amount + change_amount:\n                continue\n            candidates.append(tx)\n        return candidates\n\n    def get_change_addresses_for_new_transaction(\n            self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,\n    ) -> List[str]:\n        \"\"\"note: might return an empty list! (e.g. if use_change is disabled, or allow_reuse is False)\"\"\"\n        change_addrs = []\n        if preferred_change_addr:\n            if isinstance(preferred_change_addr, (list, tuple)):\n                change_addrs = list(preferred_change_addr)\n            else:\n                change_addrs = [preferred_change_addr]\n        elif self.use_change:\n            change_addrs = self._get_change_addresses_we_can_use_now(allow_reuse=allow_reusing_used_change_addrs)\n        for addr in change_addrs:\n            assert is_address(addr), f\"not valid bitcoin address: {addr}\"\n            # note that change addresses are not necessarily ismine\n            # in which case this is a no-op\n            self.check_address_for_corruption(addr)\n        max_change = self.max_change_outputs if self.multiple_change else 1\n        return change_addrs[:max_change]\n\n    def get_single_change_address_for_new_transaction(\n            self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,\n    ) -> Optional[str]:\n        addrs = self.get_change_addresses_for_new_transaction(\n            preferred_change_addr=preferred_change_addr,\n            allow_reusing_used_change_addrs=allow_reusing_used_change_addrs,\n        )\n        if addrs:\n            return addrs[0]\n        return None\n\n    def get_new_sweep_address_for_channel(self) -> str:\n        addrs = self._get_change_addresses_we_can_use_now(allow_reuse=True)\n        if addrs:\n            return addrs[0]\n        # fallback for e.g. imported wallets\n        return self.get_receiving_address()\n\n    def _get_change_addresses_we_can_use_now(\n        self,\n        *,\n        allow_reuse: bool = True,\n    ) -> Sequence[str]:\n        # Recalc and get unused change addresses\n        addrs = self._calc_unused_change_addresses()\n        # New change addresses are created only after a few\n        # confirmations.\n        if addrs:\n            # if there are any unused, select all\n            change_addrs = addrs\n        else:\n            # if there are none, take one randomly from the last few\n            if not allow_reuse:\n                return []\n            gap_limit = self.gap_limit_for_change or 0\n            addrs = self.get_change_addresses(slice_start=-gap_limit)\n            change_addrs = [random.choice(addrs)] if addrs else []\n        for addr in change_addrs:\n            assert is_address(addr), f\"not valid bitcoin address: {addr}\"\n            # note that change addresses are not necessarily ismine\n            # in which case this is a no-op\n            self.check_address_for_corruption(addr)\n        return change_addrs\n\n    def should_keep_reserve_utxo(\n            self,\n            tx_inputs: Sequence[PartialTxInput],\n            tx_outputs: Sequence[PartialTxOutput],\n            is_anchor_channel_opening: bool,\n    ) -> bool:\n        channels_need_reserve = self.lnworker and self.lnworker.has_anchor_channels()\n        # note: is_anchor_channel_opening is used in unit tests, without lnworker\n        is_reserve_needed = is_anchor_channel_opening or channels_need_reserve\n        if not is_reserve_needed:\n            return False\n\n        coins_in_wallet = self.get_spendable_coins(nonlocal_only=False, confirmed_only=False)\n        prevout_coins_in_wallet = set(c.prevout for c in coins_in_wallet)\n        amount_in_wallet = sum(c.value_sats() for c in coins_in_wallet)\n\n        amount_consumed = sum(c.value_sats() for c in tx_inputs if c.prevout in prevout_coins_in_wallet)\n        amount_retained = sum(o.value for o in tx_outputs if self.is_mine(o.address))\n        to_be_spent_sat = amount_consumed - amount_retained\n\n        assert amount_in_wallet - to_be_spent_sat >= 0\n        if amount_in_wallet - to_be_spent_sat >= self.config.LN_UTXO_RESERVE:\n            # there will be enough remaining after we send\n            return False\n        # we will need to subtract the reserve\n        self.logger.info(f'we should keep a reserve: {to_be_spent_sat=}, {amount_in_wallet=}')\n        return True\n\n    def is_low_reserve(self) -> bool:\n        return self.should_keep_reserve_utxo([], [], False)\n\n    def tx_keeps_ln_utxo_reserve(self, tx, *, gui_spend_max: bool) -> Optional[int]:\n        if reserve_output_amount := sum(txo.value for txo in tx.outputs() if txo.is_utxo_reserve):\n            # tx has a reserve change output\n            return reserve_output_amount\n        if gui_spend_max:  # user tried to spend max amount\n            coins_in_wallet = self.get_spendable_coins(nonlocal_only=False, confirmed_only=False)\n            amount_in_wallet = sum(c.value_sats() for c in coins_in_wallet)\n            tx_spend_amount = tx.output_value() + tx.get_fee()\n            if amount_in_wallet - tx_spend_amount == self.config.LN_UTXO_RESERVE:\n                # tx keeps exactly LN_UTXO_RESERVE amount sats in the wallet\n                return self.config.LN_UTXO_RESERVE\n        return None\n\n    @profiler(min_threshold=0.1)\n    def make_unsigned_transaction(\n            self, *,\n            coins: Optional[Sequence[PartialTxInput]] = None,\n            outputs: List[PartialTxOutput],\n            inputs: Optional[List[PartialTxInput]] = None,\n            fee_policy: FeePolicy,\n            change_addr: str = None,\n            is_sweep: bool = False,  # used by Wallet_2fa subclass\n            rbf: bool = True,\n            BIP69_sort: Optional[bool] = True,\n            base_tx: Optional[Transaction] = None,\n            send_change_to_lightning: bool = False,\n            merge_duplicate_outputs: bool = False,\n            locktime: Optional[int] = None,\n            tx_version: Optional[int] = None,\n            is_anchor_channel_opening: bool = False,\n    ) -> PartialTransaction:\n        \"\"\"Can raise NotEnoughFunds or NoDynamicFeeEstimates.\"\"\"\n\n        if coins is None:\n            coins = self.get_spendable_coins()\n        if not inputs and not coins:  # any bitcoin tx must have at least 1 input by consensus\n            raise NotEnoughFunds()\n        if any([c.already_has_some_signatures() for c in coins]):\n            raise Exception(\"Some inputs already contain signatures!\")\n        if inputs is None:\n            inputs = []\n        # make sure inputs and coins do not overlap\n        if inputs:\n            input_set = set(txin.prevout for txin in inputs)\n            coins = [coin for coin in coins if (coin.prevout not in input_set)]\n\n        # prevent side-effect with '!'\n        outputs = copy.deepcopy(outputs)\n\n        # check outputs for \"max\" amount\n        i_max = []\n        i_max_sum = 0\n        for i, o in enumerate(outputs):\n            weight = parse_max_spend(o.value)\n            if weight:\n                i_max_sum += weight\n                i_max.append((weight, i))\n\n        for txin in coins:\n            self.add_input_info(txin)\n            nSequence = 0xffffffff - (2 if rbf else 1)\n            txin.nsequence = nSequence\n\n        fee_estimator = partial(fee_policy.estimate_fee, network=self.network)\n\n        # set if we merge with another transaction\n        rbf_merge_txid = None\n\n        if len(i_max) == 0:\n            # Let the coin chooser select the coins to spend\n            coin_chooser = coinchooser.get_coin_chooser(self.config)\n            # If there is an unconfirmed RBF tx, merge with it\n            if base_tx:\n                # make sure we don't try to spend change from the tx-to-be-replaced:\n                coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()]\n                is_local = self.adb.get_tx_height(base_tx.txid()).height() == TX_HEIGHT_LOCAL\n                # estimate base tx fee before stripping tx for more accurate estimate\n                base_tx_fee = base_tx.get_fee()\n                base_feerate = Decimal(base_tx_fee)/base_tx.estimated_size()\n                relayfeerate = Decimal(self.relayfee()) / 1000\n                original_fee_estimator = fee_estimator\n                if not isinstance(base_tx, PartialTransaction):\n                    base_tx = PartialTransaction.from_tx(base_tx)\n                    base_tx.add_info_from_wallet(self)\n                else:\n                    # don't cast PartialTransaction, because it removes make_witness\n                    base_tx.remove_signatures()\n                def fee_estimator(size: Union[int, float, Decimal]) -> int:\n                    size = Decimal(size)\n                    lower_bound_relayfee = int(base_tx_fee + round(size * relayfeerate)) if not is_local else 0\n                    lower_bound_feerate = int(base_feerate * size) + 1\n                    lower_bound = max(lower_bound_feerate, lower_bound_relayfee)\n                    return max(lower_bound, original_fee_estimator(size))\n                txi = base_tx.inputs() + list(inputs)\n                txo = list(filter(lambda o: not self.is_change(o.address), base_tx.outputs())) + list(outputs)\n                old_change_addrs = [o.address for o in base_tx.outputs() if self.is_change(o.address)]\n                rbf_merge_txid = base_tx.txid()\n            else:\n                txi = list(inputs)\n                txo = list(outputs)\n                old_change_addrs = []\n            # change address. if empty, coin_chooser will set it\n            change_addrs = self.get_change_addresses_for_new_transaction(change_addr or old_change_addrs)\n            if merge_duplicate_outputs:\n                txo = transaction.merge_duplicate_tx_outputs(txo)\n            if len(txo) == 0 or (self.lnworker and send_change_to_lightning):\n                # even if the option use multiple change outputs is enabled there should be only\n                # one change address if there are 0 txos as this is a sweep tx, or if we want to swap change to ln\n                change_addrs = change_addrs[0:1]\n            tx = coin_chooser.make_tx(\n                coins=coins,\n                inputs=txi,\n                outputs=txo,\n                change_addrs=change_addrs,\n                fee_estimator_vb=fee_estimator,\n                dust_threshold=self.dust_threshold(),\n                BIP69_sort=BIP69_sort)\n            if send_change_to_lightning and self.lnworker and self.lnworker.swap_manager.is_initialized.is_set():\n                sm = self.lnworker.swap_manager\n                change = tx.get_change_outputs()\n                if len(change) == 1:\n                    amount = change[0].value\n                    min_swap_amount = sm.get_min_amount()\n                    max_swap_amount = sm.client_max_amount_forward_swap() or 0\n                    if min_swap_amount <= amount <= max_swap_amount:\n                        tx.replace_output_address(change[0].address, DummyAddress.SWAP)\n            if self.should_keep_reserve_utxo(tx.inputs(), tx.outputs(), is_anchor_channel_opening):\n                raise NotEnoughFunds()\n            self.logger.debug(f'coinchooser returned tx with {len(tx.inputs())} inputs and {len(tx.outputs())} outputs')\n\n        else:\n            # \"spend max\" branch\n            # note: This *will* spend inputs with negative effective value (if there are any).\n            #       Given as the user is spending \"max\", and so might be abandoning the wallet,\n            #       try to include all UTXOs, otherwise leftover might remain in the UTXO set\n            #       forever. see #5433\n            # note: Actually, it might be the case that not all UTXOs from the wallet are\n            #       being spent if the user manually selected UTXOs.\n            def distribute_amount(amount):\n                if amount < 0:\n                    raise NotEnoughFunds()\n                distr_amount = 0\n                for (weight, i) in i_max:\n                    # fixme: this does not check that value >= dust_threshold\n                    val = int((amount/i_max_sum) * weight)\n                    outputs[i].value = val\n                    distr_amount += val\n                (x, i) = i_max[-1]\n                outputs[i].value += (amount - distr_amount)\n\n            tx_inputs = inputs + coins  # these do not overlap, see above\n            distribute_amount(0)\n            tx = PartialTransaction.from_io(list(tx_inputs), list(outputs))\n            fee = fee_estimator(tx.estimated_size())\n\n            input_amount = sum(c.value_sats() for c in tx_inputs)  # may change if reserve is needed\n            allocated_amount = sum(o.value for o in outputs if not parse_max_spend(o.value))\n            to_distribute = input_amount - allocated_amount\n            distribute_amount(to_distribute - fee)\n\n            if self.should_keep_reserve_utxo(tx_inputs, outputs, is_anchor_channel_opening):\n                # check if any input of the tx is == LN_UTXO_RESERVE, then we can just remove the input\n                reserve_sized_input = None\n                for tx_input in tx_inputs:\n                    if tx_input.value_sats() and tx_input.value_sats() == self.config.LN_UTXO_RESERVE:\n                        reserve_sized_input = tx_input\n                        break\n\n                if reserve_sized_input:\n                    self.logger.debug(f'Removing LN_UTXO_RESERVE sized input to keep utxo reserve')\n                    tx_inputs.remove(reserve_sized_input)\n                    to_distribute -= reserve_sized_input.value_sats()\n                else:\n                    self.logger.info(f'Adding change output to meet utxo reserve requirements')\n                    change_addrs = self.get_change_addresses_for_new_transaction(change_addr)\n                    change_addr = change_addrs[0] if change_addrs else tx_inputs[0].address\n                    change = PartialTxOutput.from_address_and_value(change_addr, self.config.LN_UTXO_RESERVE)\n                    change.is_utxo_reserve = True  # for GUI\n                    outputs.append(change)\n                    to_distribute -= change.value\n\n                assert not self.should_keep_reserve_utxo(tx_inputs, outputs, is_anchor_channel_opening)\n                tx = PartialTransaction.from_io(list(tx_inputs), list(outputs))\n                fee = fee_estimator(tx.estimated_size())\n                distribute_amount(to_distribute - fee)\n\n            tx = PartialTransaction.from_io(list(tx_inputs), list(outputs))\n\n        assert len(tx.outputs()) > 0, \"any bitcoin tx must have at least 1 output by consensus\"\n        if locktime is None:\n            # Timelock tx to current height.\n            locktime = get_locktime_for_new_transaction(self.network)\n        tx.locktime = locktime\n        if tx_version is not None:\n            tx.version = tx_version\n        tx.rbf_merge_txid = rbf_merge_txid\n        tx.add_info_from_wallet(self)\n        run_hook('make_unsigned_transaction', self, tx)\n        return tx\n\n    def is_frozen_address(self, addr: str) -> bool:\n        return addr in self._frozen_addresses\n\n    def is_frozen_coin(self, utxo: PartialTxInput) -> bool:\n        prevout_str = utxo.prevout.to_str()\n        frozen = self._frozen_coins.get(prevout_str, None)\n        # note: there are three possible states for 'frozen':\n        #       True/False if the user explicitly set it,\n        #       None otherwise\n        if frozen is not None:  # user has explicitly set the state\n            return bool(frozen)\n        # State not set. We implicitly mark certain coins as frozen:\n        tx_mined_status = self.adb.get_tx_height(utxo.prevout.txid.hex())\n        if tx_mined_status.height() == TX_HEIGHT_FUTURE:\n            return True\n        if self._is_coin_small_and_unconfirmed(utxo):\n            return True\n        addr = utxo.address\n        assert addr is not None\n        if self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS and self.adb.is_used_as_from_address(addr):\n            return True\n        return False\n\n    def _is_coin_small_and_unconfirmed(self, utxo: PartialTxInput) -> bool:\n        \"\"\"If true, the coin should not be spent.\n        The idea here is that an attacker might send us a UTXO in a\n        large low-fee unconfirmed tx that will ~never confirm. If we\n        spend it as part of a tx ourselves, that too will not confirm\n        (unless we use a high fee, but that might not be worth it for\n        a small value UTXO).\n        In particular, this test triggers for large \"dusting transactions\"\n        that are used for advertising purposes by some entities.\n        see #6960\n        \"\"\"\n        # confirmed UTXOs are fine; check this first for performance:\n        block_height = utxo.block_height\n        assert block_height is not None\n        if block_height > 0:\n            return False\n        # exempt large value UTXOs\n        value_sats = utxo.value_sats()\n        assert value_sats is not None\n        threshold = self.config.WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT\n        if value_sats >= threshold:\n            return False\n        # if funding tx has any is_mine input, then UTXO is fine\n        funding_tx = self.db.get_transaction(utxo.prevout.txid.hex())\n        if funding_tx is None:\n            # we should typically have the funding tx available;\n            # might not have it e.g. while not up_to_date\n            return True\n        if any(self.is_mine(self.adb.get_txin_address(txin))\n               for txin in funding_tx.inputs()):\n            return False\n        return True\n\n    def set_frozen_state_of_addresses(\n        self,\n        addrs: Iterable[str],\n        freeze: bool,\n        *,\n        write_to_disk: bool = True,\n    ) -> bool:\n        \"\"\"Set frozen state of the addresses to FREEZE, True or False\"\"\"\n        if all(self.is_mine(addr) for addr in addrs):\n            with self._freeze_lock:\n                if freeze:\n                    self._frozen_addresses |= set(addrs)\n                else:\n                    self._frozen_addresses -= set(addrs)\n                self.db.put('frozen_addresses', list(self._frozen_addresses))\n            util.trigger_callback('status')\n            if write_to_disk:\n                self.save_db()\n            return True\n        return False\n\n    def set_frozen_state_of_coins(\n        self,\n        utxos: Iterable[str],\n        freeze: Optional[bool],  # tri-state\n        *,\n        write_to_disk: bool = True,\n    ) -> None:\n        \"\"\"Set frozen state of the utxos to `freeze`, True or False (or None).\n        A value of True/False means the user explicitly set if the coin should be frozen.\n        In contrast, None is the default \"unset\" state. If unset, is_frozen_coin()\n        can decide whether a coin should be frozen.\n        \"\"\"\n        # basic sanity check that input is not garbage: (see if raises)\n        [TxOutpoint.from_str(utxo) for utxo in utxos]\n        assert freeze in (None, False, True), f\"{freeze=!r}\"\n        with self._freeze_lock:\n            for utxo in utxos:\n                if freeze is None:\n                    self._frozen_coins.pop(utxo, None)\n                else:\n                    self._frozen_coins[utxo] = bool(freeze)\n        util.trigger_callback('status')\n        if write_to_disk:\n            self.save_db()\n\n    def is_address_reserved(self, addr: str) -> bool:\n        # note: atm 'reserved' status is only taken into consideration for 'change addresses'\n        return addr in self._reserved_addresses\n\n    def set_reserved_state_of_address(self, addr: str, *, reserved: bool) -> None:\n        if not self.is_mine(addr):\n            # silently ignore non-ismine addresses\n            return\n        with self.lock:\n            has_changed = (addr in self._reserved_addresses) != reserved\n            if reserved:\n                self._reserved_addresses.add(addr)\n            else:\n                self._reserved_addresses.discard(addr)\n            if has_changed:\n                self.db.put('reserved_addresses', list(self._reserved_addresses))\n\n    def set_reserved_addresses_for_chan(self, chan: 'AbstractChannel', *, reserved: bool) -> None:\n        for addr in chan.get_wallet_addresses_channel_might_want_reserved():\n            self.set_reserved_state_of_address(addr, reserved=reserved)\n\n    def can_export(self):\n        return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key')\n\n    def get_bumpfee_strategies_for_tx(\n        self,\n        *,\n        tx: Transaction,\n    ) -> Tuple[Sequence[BumpFeeStrategy], int]:\n        \"\"\"Returns tuple(list of available strategies, idx of recommended option among those).\"\"\"\n        all_strats = BumpFeeStrategy.all()\n        # are we paying max?\n        invoices = self.get_relevant_invoices_for_tx(tx.txid())\n        if len(invoices) == 1 and len(invoices[0].outputs) == 1:\n            if invoices[0].outputs[0].value == '!':\n                return all_strats, all_strats.index(BumpFeeStrategy.DECREASE_PAYMENT)\n        # do not decrease payment if it is a swap\n        if self.get_swaps_by_funding_tx(tx):\n            return [BumpFeeStrategy.PRESERVE_PAYMENT], 0\n        # default\n        return all_strats, all_strats.index(BumpFeeStrategy.PRESERVE_PAYMENT)\n\n    def bump_fee(\n            self,\n            *,\n            tx: Transaction,\n            new_fee_rate: Union[int, float, Decimal],\n            coins: Sequence[PartialTxInput] = None,\n            strategy: BumpFeeStrategy = BumpFeeStrategy.PRESERVE_PAYMENT,\n    ) -> PartialTransaction:\n        \"\"\"Increase the miner fee of 'tx'.\n        'new_fee_rate' is the target min rate in sat/vbyte\n        'coins' is a list of UTXOs we can choose from as potential new inputs to be added\n\n        note: it is the caller's responsibility to have already called tx.add_info_from_network().\n              Without that, all txins must be ismine.\n        \"\"\"\n        assert tx\n        old_tx_size = tx.estimated_size()  # estimate before stripping tx for more accurate estimate\n        if not isinstance(tx, PartialTransaction):\n            tx = PartialTransaction.from_tx(tx)\n        assert isinstance(tx, PartialTransaction)\n        tx.remove_signatures()\n        if not self.can_rbf_tx(tx):\n            raise CannotBumpFee(_('Transaction is final'))\n        new_fee_rate = quantize_feerate(new_fee_rate)  # strip excess precision\n        tx.add_info_from_wallet(self)\n        if tx.is_missing_info_from_network():\n            raise Exception(\"tx missing info from network\")\n        old_fee = tx.get_fee()\n        assert old_fee is not None\n        old_fee_rate = old_fee / old_tx_size  # sat/vbyte\n        if new_fee_rate <= old_fee_rate:\n            raise CannotBumpFee(_(\"The new fee rate needs to be higher than the old fee rate.\"))\n\n        if strategy == BumpFeeStrategy.PRESERVE_PAYMENT:\n            # FIXME: we should try decreasing change first,\n            # but it requires updating a bunch of unit tests\n            try:\n                tx_new = self._bump_fee_through_coinchooser(\n                    tx=tx,\n                    new_fee_rate=new_fee_rate,\n                    coins=coins,\n                )\n            except CannotBumpFee as e:\n                tx_new = self._bump_fee_through_decreasing_change(\n                    tx=tx, new_fee_rate=new_fee_rate)\n        elif strategy == BumpFeeStrategy.DECREASE_PAYMENT:\n            tx_new = self._bump_fee_through_decreasing_payment(\n                tx=tx, new_fee_rate=new_fee_rate)\n        else:\n            raise Exception(f\"unknown strategy: {strategy=}\")\n\n        target_min_fee = new_fee_rate * tx_new.estimated_size()\n        actual_fee = tx_new.get_fee()\n        if actual_fee + 1 < target_min_fee:\n            raise CannotBumpFee(\n                f\"bump_fee fee target was not met. \"\n                f\"got {actual_fee}, expected >={target_min_fee}. \"\n                f\"target rate was {new_fee_rate}\")\n        tx_new.locktime = get_locktime_for_new_transaction(self.network)\n        tx_new.add_info_from_wallet(self)\n        return tx_new\n\n    def _bump_fee_through_coinchooser(\n            self,\n            *,\n            tx: PartialTransaction,\n            new_fee_rate: Union[int, Decimal],\n            coins: Sequence[PartialTxInput] = None,\n    ) -> PartialTransaction:\n        \"\"\"Increase the miner fee of 'tx'.\n\n        - keeps all inputs\n        - keeps all not is_mine outputs,\n        - allows adding new inputs\n        \"\"\"\n        tx = copy.deepcopy(tx)\n        tx.add_info_from_wallet(self)\n        assert tx.get_fee() is not None\n        old_inputs = list(tx.inputs())\n        old_outputs = list(tx.outputs())\n        # change address\n        old_change_addrs = [o.address for o in old_outputs if self.is_change(o.address)]\n        change_addrs = self.get_change_addresses_for_new_transaction(old_change_addrs)\n        # which outputs to keep?\n        if old_change_addrs:\n            fixed_outputs = list(filter(lambda o: not self.is_change(o.address), old_outputs))\n        else:\n            if all(self.is_mine(o.address) for o in old_outputs):\n                # all outputs are is_mine and none of them are change.\n                # we bail out as it's unclear what the user would want!\n                # the coinchooser bump fee method is probably not a good idea in this case\n                raise CannotBumpFee(_('All outputs are non-change is_mine'))\n            old_not_is_mine = list(filter(lambda o: not self.is_mine(o.address), old_outputs))\n            if old_not_is_mine:\n                fixed_outputs = old_not_is_mine\n            else:\n                fixed_outputs = old_outputs\n        if not fixed_outputs:\n            raise CannotBumpFee(_('Could not figure out which outputs to keep'))\n\n        if coins is None:\n            coins = self.get_spendable_coins(None)\n        # make sure we don't try to spend output from the tx-to-be-replaced:\n        coins = [c for c in coins\n                 if c.prevout.txid.hex() not in self.adb.get_conflicting_transactions(tx, include_self=True)]\n        for item in coins:\n            item.nsequence = 0xffffffff - 2\n            self.add_input_info(item)\n        def fee_estimator(size):\n            return FeePolicy.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)\n        coin_chooser = coinchooser.get_coin_chooser(self.config)\n        try:\n            return coin_chooser.make_tx(\n                coins=coins,\n                inputs=old_inputs,\n                outputs=fixed_outputs,\n                change_addrs=change_addrs,\n                fee_estimator_vb=fee_estimator,\n                dust_threshold=self.dust_threshold())\n        except NotEnoughFunds as e:\n            raise CannotBumpFee(e)\n\n    def _bump_fee_through_decreasing_change(\n            self,\n            *,\n            tx: PartialTransaction,\n            new_fee_rate: Union[int, Decimal],\n    ) -> PartialTransaction:\n        \"\"\"Increase the miner fee of 'tx'.\n\n        - keeps all inputs\n        - no new inputs are added\n        - change outputs are decreased or removed\n        \"\"\"\n        tx = copy.deepcopy(tx)\n        tx.add_info_from_wallet(self)\n        assert tx.get_fee() is not None\n        inputs = tx.inputs()\n        outputs = tx._outputs  # note: we will mutate this directly\n\n        # use own outputs\n        s = list(filter(lambda o: self.is_mine(o.address), outputs))\n        if not s:\n            raise CannotBumpFee('No suitable output')\n\n        # prioritize low value outputs, to get rid of dust\n        s = sorted(s, key=lambda o: o.value)\n        for o in s:\n            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))\n            delta = target_fee - tx.get_fee()\n            if delta <= 0:\n                break\n            i = outputs.index(o)\n            if o.value - delta >= self.dust_threshold():\n                new_output_value = o.value - delta\n                assert isinstance(new_output_value, int)\n                outputs[i].value = new_output_value\n                delta = 0\n                break\n            else:\n                del outputs[i]\n                # note: we mutated the outputs of tx, which will affect\n                #       tx.estimated_size() in the next iteration\n        else:\n            # recompute delta if there was no next iteration\n            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))\n            delta = target_fee - tx.get_fee()\n\n        if delta > 0:\n            raise CannotBumpFee(_('Could not find suitable outputs'))\n\n        return PartialTransaction.from_io(inputs, outputs)\n\n    def _bump_fee_through_decreasing_payment(\n            self,\n            *,\n            tx: PartialTransaction,\n            new_fee_rate: Union[int, Decimal],\n    ) -> PartialTransaction:\n        \"\"\"\n        Increase the miner fee of 'tx' by decreasing amount paid.\n        This should be used for transactions that pay \"Max\".\n\n        - keeps all inputs\n        - no new inputs are added\n        - Each non-ismine output is decreased proportionally to their byte-size.\n        \"\"\"\n        tx = copy.deepcopy(tx)\n        tx.add_info_from_wallet(self)\n        assert tx.get_fee() is not None\n        inputs = tx.inputs()\n        outputs = tx.outputs()\n\n        # select non-ismine outputs\n        s = [(idx, out) for (idx, out) in enumerate(outputs)\n             if not self.is_mine(out.address)]\n        s = [(idx, out) for (idx, out) in s if self._is_rbf_allowed_to_touch_tx_output(out)]\n        if not s:\n            raise CannotBumpFee(\"Cannot find payment output\")\n\n        del_out_idxs = set()\n        tx_size = tx.estimated_size()\n        cur_fee = tx.get_fee()\n        # Main loop. Each iteration decreases value of all selected outputs.\n        # The number of iterations is bounded by len(s) as only the final iteration\n        # can *not remove* any output.\n        for __ in range(len(s) + 1):\n            target_fee = int(math.ceil(tx_size * new_fee_rate))\n            delta_total = target_fee - cur_fee\n            if delta_total <= 0:\n                break\n            out_size_total = sum(Transaction.estimated_output_size_for_script(out.scriptpubkey)\n                                 for (idx, out) in s if idx not in del_out_idxs)\n            if out_size_total == 0:  # no outputs left to decrease\n                raise CannotBumpFee(_('Could not find suitable outputs'))\n            for idx, out in s:\n                out_size = Transaction.estimated_output_size_for_script(out.scriptpubkey)\n                delta = int(math.ceil(delta_total * out_size / out_size_total))\n                if out.value - delta >= self.dust_threshold():\n                    new_output_value = out.value - delta\n                    assert isinstance(new_output_value, int)\n                    outputs[idx].value = new_output_value\n                    cur_fee += delta\n                else:  # remove output\n                    tx_size -= out_size\n                    cur_fee += out.value\n                    del_out_idxs.add(idx)\n        if delta_total > 0:\n            raise CannotBumpFee(_('Could not find suitable outputs'))\n\n        outputs = [out for (idx, out) in enumerate(outputs) if idx not in del_out_idxs]\n        return PartialTransaction.from_io(inputs, outputs)\n\n    def _is_rbf_allowed_to_touch_tx_output(self, txout: TxOutput) -> bool:\n        # 2fa fee outputs if present, should not be removed or have their value decreased\n        if self.is_billing_address(txout.address):\n            return False\n        # submarine swap funding outputs must not be decreased\n        if self.lnworker and self.lnworker.swap_manager.is_lockup_address_for_a_swap(txout.address):\n            return False\n        return True\n\n    def can_rbf_tx(self, tx: Transaction, *, is_dscancel: bool = False) -> bool:\n        # do not mutate LN funding txs, as that would change their txid\n        if not is_dscancel and self.is_lightning_funding_tx(tx.txid()):\n            return False\n        return tx.is_rbf_enabled()\n\n    def cpfp(self, tx: Transaction, fee: int) -> Optional[PartialTransaction]:\n        assert tx\n        txid = tx.txid()\n        for i, o in enumerate(tx.outputs()):\n            address, value = o.address, o.value\n            if self.is_mine(address):\n                break\n        else:\n            raise CannotCPFP(_(\"Could not find suitable output\"))\n        coins = self.adb.get_addr_utxo(address)\n        item = coins.get(TxOutpoint.from_str(txid + ':%d' % i))\n        if not item:\n            raise CannotCPFP(_(\"Could not find coins for output\"))\n        inputs = [item]\n        out_address = (self.get_single_change_address_for_new_transaction(allow_reusing_used_change_addrs=False)\n                       or self.get_unused_address()\n                       or address)\n        output_value = value - fee\n        if output_value < self.dust_threshold():\n            raise CannotCPFP(_(\"The output value remaining after fee is too low.\"))\n        outputs = [PartialTxOutput.from_address_and_value(out_address, output_value)]\n        locktime = get_locktime_for_new_transaction(self.network)\n        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)\n        tx_new.set_rbf(True)\n        tx_new.add_info_from_wallet(self)\n        return tx_new\n\n    def dscancel(\n            self, *, tx: Transaction, new_fee_rate: Union[int, float, Decimal]\n    ) -> PartialTransaction:\n        \"\"\"Double-Spend-Cancel: cancel an unconfirmed tx by double-spending\n        its inputs, paying ourselves.\n        'new_fee_rate' is the target min rate in sat/vbyte\n\n        note: it is the caller's responsibility to have already called tx.add_info_from_network().\n              Without that, all txins must be ismine.\n        \"\"\"\n        assert tx\n        old_tx_size = tx.estimated_size()  # estimate before stripping tx for more accurate estimate\n        if not isinstance(tx, PartialTransaction):\n            tx = PartialTransaction.from_tx(tx)\n        assert isinstance(tx, PartialTransaction)\n        tx.remove_signatures()\n\n        if not self.can_rbf_tx(tx, is_dscancel=True):\n            raise CannotDoubleSpendTx(_('Transaction is final'))\n        new_fee_rate = quantize_feerate(new_fee_rate)  # strip excess precision\n        tx.add_info_from_wallet(self)\n        if tx.is_missing_info_from_network():\n            raise Exception(\"tx missing info from network\")\n        old_fee = tx.get_fee()\n        assert old_fee is not None\n        old_fee_rate = old_fee / old_tx_size  # sat/vbyte\n        if new_fee_rate <= old_fee_rate:\n            raise CannotDoubleSpendTx(_(\"The new fee rate needs to be higher than the old fee rate.\"))\n        # grab all ismine inputs\n        inputs = [txin for txin in tx.inputs()\n                  if self.is_mine(self.adb.get_txin_address(txin))]\n        value = sum([txin.value_sats() for txin in inputs])\n        # figure out output address\n        old_change_addrs = [o.address for o in tx.outputs() if self.is_mine(o.address)]\n        out_address = (self.get_single_change_address_for_new_transaction(old_change_addrs)\n                       or self.get_receiving_address())\n        locktime = get_locktime_for_new_transaction(self.network)\n        outputs = [PartialTxOutput.from_address_and_value(out_address, value)]\n        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)\n        new_tx_size = tx_new.estimated_size()\n        new_fee = max(\n            new_fee_rate * new_tx_size,\n            old_fee + self.relayfee() * new_tx_size / Decimal(1000),  # BIP-125 rules 3 and 4\n        )\n        new_fee = int(math.ceil(new_fee))\n        output_value = value - new_fee\n        if output_value < self.dust_threshold():\n            raise CannotDoubleSpendTx(_(\"The output value remaining after fee is too low.\"))\n        outputs = [PartialTxOutput.from_address_and_value(out_address, value - new_fee)]\n        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)\n        tx_new.set_rbf(True)\n        tx_new.add_info_from_wallet(self)\n        return tx_new\n\n    def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput],\n                                     address: str, *, only_der_suffix: bool) -> None:\n        pass  # implemented by subclasses\n\n    def _add_input_utxo_info(\n            self,\n            txin: PartialTxInput,\n            *,\n            address: str = None,\n    ) -> None:\n        # - We prefer to include UTXO (full tx), even for segwit inputs (see #6198).\n        # - For witness v0 inputs, we include *both* UTXO and WITNESS_UTXO. UTXO is a strict superset,\n        #   so this is redundant, but it is (implied to be) \"expected\" from bip-0174 (see #8039).\n        #   Regardless, this might improve compatibility with some other software.\n        # - For witness v1, witness_utxo will be enough though (bip-0341 sighash fixes known prior issues).\n        # - We cannot include UTXO if the prev tx is not signed yet (chain of unsigned txs).\n        address = address or txin.address\n        # add witness_utxo\n        if txin.witness_utxo is None and txin.is_segwit() and address:\n            received, spent = self.adb.get_addr_io(address)\n            item = received.get(txin.prevout.to_str())\n            if item:\n                txin_value = item[2]\n                txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value)\n        # add utxo\n        if txin.utxo is None:\n            txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())\n        # Maybe remove witness_utxo. witness_utxo should not be present for non-segwit inputs.\n        # If it is present, it might be because another electrum instance added it when sharing the psbt via QR code.\n        # If we have the full utxo available, we can remove it without loss of information.\n        if txin.witness_utxo and not txin.is_segwit() and txin.utxo:\n            txin.witness_utxo = None\n\n    def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput],\n                                                        address: str) -> bool:\n        \"\"\"Tries to learn the derivation path for an address (potentially beyond gap limit)\n        using data available in given txin/txout.\n        Returns whether the address was found to be is_mine.\n        \"\"\"\n        return False  # implemented by subclasses\n\n    def add_input_info(\n            self,\n            txin: TxInput,\n            *,\n            only_der_suffix: bool = False,\n    ) -> None:\n        \"\"\"Populates the txin, using info the wallet already has.\n        That is, network requests are *not* done to fetch missing prev txs!\n        For that, use txin.add_info_from_network.\n        \"\"\"\n        # note: we add input utxos regardless of is_mine\n        if txin.utxo is None:\n            txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())\n        if not isinstance(txin, PartialTxInput):\n            return\n        address = self.adb.get_txin_address(txin)\n        self._add_input_utxo_info(txin, address=address)\n        is_mine = self.is_mine(address)\n        if not is_mine:\n            is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address)\n        if not is_mine:\n            return\n        txin.script_descriptor = self.get_script_descriptor_for_address(address)\n        txin.is_mine = True\n        self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)\n        txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height()\n\n    def has_support_for_slip_19_ownership_proofs(self) -> bool:\n        return False\n\n    def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:\n        raise NotImplementedError()\n\n    def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]:\n        if not self.is_mine(address):\n            return None\n        script_type = self.get_txin_type(address)\n        if script_type in ('address', 'unknown'):\n            return None\n        addr_index = self.get_address_index(address)\n        if addr_index is None:\n            return None\n        pubkeys = [ks.get_pubkey_provider(addr_index) for ks in self.get_keystores()]\n        if not pubkeys:\n            return None\n        if script_type == 'p2pk':\n            return descriptor.PKDescriptor(pubkey=pubkeys[0])\n        elif script_type == 'p2pkh':\n            return descriptor.PKHDescriptor(pubkey=pubkeys[0])\n        elif script_type == 'p2wpkh':\n            return descriptor.WPKHDescriptor(pubkey=pubkeys[0])\n        elif script_type == 'p2wpkh-p2sh':\n            wpkh = descriptor.WPKHDescriptor(pubkey=pubkeys[0])\n            return descriptor.SHDescriptor(subdescriptor=wpkh)\n        elif script_type == 'p2sh':\n            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)\n            return descriptor.SHDescriptor(subdescriptor=multi)\n        elif script_type == 'p2wsh':\n            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)\n            return descriptor.WSHDescriptor(subdescriptor=multi)\n        elif script_type == 'p2wsh-p2sh':\n            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)\n            wsh = descriptor.WSHDescriptor(subdescriptor=multi)\n            return descriptor.SHDescriptor(subdescriptor=wsh)\n        else:\n            raise NotImplementedError(f\"unexpected {script_type=}\")\n\n    def can_sign(self, tx: Transaction) -> bool:\n        if not isinstance(tx, PartialTransaction):\n            return False\n        if tx.is_complete():\n            return False\n        # add info to inputs if we can; otherwise we might return a false negative:\n        tx.add_info_from_wallet(self)\n        for txin in tx.inputs():\n            # note: is_mine check needed to avoid false positives.\n            #       just because keystore could sign, txin does not necessarily belong to wallet.\n            #       Example: we have p2pkh-like addresses and txin is a multisig that involves our pubkey.\n            if not self.is_mine(txin.address):\n                continue\n            for k in self.get_keystores():\n                if k.can_sign_txin(txin):\n                    return True\n        return False\n\n    def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = False) -> None:\n        address = txout.address\n        if not self.is_mine(address):\n            is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address)\n            if not is_mine:\n                return\n        txout.script_descriptor = self.get_script_descriptor_for_address(address)\n        txout.is_mine = True\n        txout.is_change = self.is_change(address)\n        self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix)\n\n    def sign_transaction(\n            self,\n            tx: Transaction,\n            password,\n            *,\n            ignore_warnings: bool = False\n    ) -> Optional[PartialTransaction]:\n        \"\"\" returns tx if successful else None \"\"\"\n        if self.is_watching_only():\n            return\n        if not isinstance(tx, PartialTransaction):\n            return\n        if any(DummyAddress.is_dummy_address(txout.address) for txout in tx.outputs()):\n            raise DummyAddressUsedInTxException(\"tried to sign tx with dummy address!\")\n\n        # check if signing is dangerous\n        sh_danger = self.check_sighash(tx)\n        if sh_danger.needs_reject():\n            raise TransactionDangerousException('Not signing transaction:\\n' + sh_danger.get_long_message())\n        if sh_danger.needs_confirm() and not ignore_warnings:\n            raise TransactionPotentiallyDangerousException('Not signing transaction:\\n' + sh_danger.get_long_message())\n\n        # find out if we are replacing a txbatcher transaction\n        prevout_str = tx.inputs()[0].prevout.to_str()\n        batch = self.txbatcher.find_batch_by_prevout(prevout_str)\n        if batch:\n            batch.add_sweep_info_to_tx(tx)\n\n        # sign with make_witness\n        for i, txin in enumerate(tx.inputs()):\n            if hasattr(txin, 'make_witness'):\n                self.logger.info(f'sign_transaction: adding witness using make_witness')\n                privkey = txin.privkey\n                sig = tx.sign_txin(i, privkey)\n                txin.script_sig = b''\n                txin.witness = txin.make_witness(sig)\n                assert txin.is_complete()\n\n        # add info to a temporary tx copy; including xpubs\n        # and full derivation paths as hw keystores might want them\n        tmp_tx = copy.deepcopy(tx)\n        tmp_tx.add_info_from_wallet(self, include_xpubs=True)\n        # sign. start with ready keystores.\n        # note: ks.ready_to_sign() side-effect: we trigger pairings with potential hw devices.\n        #       We only do this once, before the loop, however we could rescan after each iteration,\n        #       to see if the user connected/disconnected devices in the meantime.\n        for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True):\n            try:\n                if k.can_sign(tmp_tx):\n                    k.sign_transaction(tmp_tx, password)\n            except UserCancelled:\n                continue\n        # remove sensitive info; then copy back details from temporary tx\n        tmp_tx.remove_xpubs_and_bip32_paths()\n        tx.combine_with_other_psbt(tmp_tx)\n        tx.add_info_from_wallet(self, include_xpubs=False)\n        return tx\n\n    def try_detecting_internal_addresses_corruption(self) -> None:\n        pass\n\n    def check_address_for_corruption(self, addr: str) -> None:\n        pass\n\n    def get_unused_addresses(self) -> Sequence[str]:\n        domain = self.get_receiving_addresses()\n        return [addr for addr in domain if not self.adb.is_used(addr) and not self.get_request_by_addr(addr)]\n\n    @check_returned_address_for_corruption\n    def get_unused_address(self) -> Optional[str]:\n        \"\"\"Get an unused receiving address, if there is one.\n        Note: there might NOT be one available!\n        \"\"\"\n        addrs = self.get_unused_addresses()\n        if addrs:\n            return addrs[0]\n\n    @check_returned_address_for_corruption\n    def get_receiving_address(self) -> str:\n        \"\"\"Get a receiving address. Guaranteed to always return an address.\"\"\"\n        unused_addr = self.get_unused_address()\n        if unused_addr:\n            return unused_addr\n        domain = self.get_receiving_addresses()\n        if not domain:\n            raise Exception(\"no receiving addresses in wallet?!\")\n        choice = domain[0]\n        for addr in domain:\n            if not self.adb.is_used(addr):\n                if self.get_request_by_addr(addr) is None:\n                    return addr\n                else:\n                    choice = addr\n        return choice\n\n    def create_new_address(self, for_change: bool = False):\n        raise UserFacingException(\"this wallet cannot generate new addresses\")\n\n    def import_address(self, address: str) -> str:\n        raise UserFacingException(\"this wallet cannot import addresses\")\n\n    def import_addresses(self, addresses: List[str], *,\n                         write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:\n        raise UserFacingException(\"this wallet cannot import addresses\")\n\n    def delete_address(self, address: str) -> None:\n        raise UserFacingException(\"this wallet cannot delete addresses\")\n\n    def get_request_URI(self, req: Request) -> Optional[str]:\n        return req.get_bip21_URI(lightning_invoice=None)\n\n    def check_expired_status(self, r: BaseInvoice, status):\n        #if r.is_lightning() and r.exp == 0:\n        #    status = PR_EXPIRED  # for BOLT-11 invoices, exp==0 means 0 seconds\n        if status == PR_UNPAID and r.has_expired():\n            status = PR_EXPIRED\n        return status\n\n    def get_invoice_status(self, invoice: BaseInvoice):\n        \"\"\"Returns status of (incoming) request or (outgoing) invoice.\"\"\"\n        # lightning invoices can be paid onchain\n        if invoice.is_lightning() and self.lnworker:\n            status = self.lnworker.get_invoice_status(invoice)\n            if status != PR_UNPAID:\n                return self.check_expired_status(invoice, status)\n        paid, conf = self.is_onchain_invoice_paid(invoice)\n        if not paid:\n            if isinstance(invoice, Invoice):\n                if status := invoice.get_broadcasting_status():\n                    return status\n            status = PR_UNPAID\n        elif conf == 0:\n            status = PR_UNCONFIRMED\n        else:\n            assert conf >= 1, conf\n            status = PR_PAID\n        return self.check_expired_status(invoice, status)\n\n    def get_request_by_addr(self, addr: str) -> Optional[Request]:\n        \"\"\"Returns a relevant request for address, from an on-chain PoV.\n        (One that has been paid on-chain or is pending)\n\n        Called in get_label_for_address and update_invoices_and_reqs_touched_by_tx\n        Returns None if the address can be reused (i.e. was paid by lightning or has expired)\n        \"\"\"\n        keys = self._requests_addr_to_key.get(addr) or []\n        reqs = [self._receive_requests.get(key) for key in keys]\n        reqs = [req for req in reqs if req]  # filter None\n        if not reqs:\n            return\n        # filter out expired\n        reqs = [req for req in reqs if self.get_invoice_status(req) != PR_EXPIRED]\n        # filter out paid-with-lightning\n        if self.lnworker:\n            reqs = [req for req in reqs\n                    if not req.is_lightning() or self.lnworker.get_invoice_status(req) == PR_UNPAID]\n        if not reqs:\n            return None\n        # note: There typically should not be more than one relevant request for an address.\n        #       If there's multiple, return the one created last (see #8113). Consider:\n        #       - there is an old expired req1, and a newer unpaid req2, reusing the same addr (and same amount),\n        #       - now req2 gets paid. however, get_invoice_status will say both req1 and req2 are PAID. (see #8061)\n        #       - as a workaround, we return the request with the larger creation time.\n        reqs.sort(key=lambda req: req.get_time())\n        return reqs[-1]\n\n    def get_request(self, request_id: str) -> Optional[Request]:\n        return self._receive_requests.get(request_id)\n\n    def get_formatted_request(self, request_id):\n        x = self.get_request(request_id)\n        if x:\n            return self.export_request(x)\n\n    def export_request(self, x: Request) -> Dict[str, Any]:\n        key = x.get_id()\n        status = self.get_invoice_status(x)\n        d = x.as_dict(status)\n        d['request_id'] = d.pop('id')\n        if x.is_lightning():\n            d['rhash'] = x.rhash\n            d['lightning_invoice'] = self.get_bolt11_invoice(x)\n            if self.lnworker:\n                if status == PR_UNPAID:\n                    d['can_receive'] = self.lnworker.can_receive_invoice(x)\n                elif status == PR_PAID and (preimage := self.lnworker.get_preimage(x.payment_hash)):\n                    d['preimage'] = preimage.hex()\n        if address := x.get_address():\n            d['address'] = address\n            d['URI'] = self.get_request_URI(x)\n            # if request was paid onchain, add relevant fields\n            # note: addr is reused when getting paid on LN! so we check for that.\n            _, conf, tx_hashes = self._is_onchain_invoice_paid(x)\n            if not x.is_lightning() or not self.lnworker or self.lnworker.get_invoice_status(x) != PR_PAID:\n                if conf is not None:\n                    d['confirmations'] = conf\n                d['tx_hashes'] = tx_hashes\n        run_hook('wallet_export_request', d, key)\n        return d\n\n    def export_invoice(self, x: Invoice) -> Dict[str, Any]:\n        key = x.get_id()\n        status = self.get_invoice_status(x)\n        d = x.as_dict(status)\n        d['invoice_id'] = d.pop('id')\n        if x.is_lightning():\n            d['lightning_invoice'] = x.lightning_invoice\n            if self.lnworker and status == PR_UNPAID:\n                d['can_pay'] = self.lnworker.can_pay_invoice(x)\n            if self.lnworker and status == PR_PAID:\n                payment_hash = bytes.fromhex(d['invoice_id'])\n                preimage = self.lnworker.get_preimage(payment_hash)\n                d['preimage'] = preimage.hex() if preimage else None\n        else:\n            amount_sat = x.get_amount_sat()\n            assert isinstance(amount_sat, (int, str, type(None)))\n            d['outputs'] = [y.to_legacy_tuple() for y in x.get_outputs()]\n            if x.bip70:\n                d['bip70'] = x.bip70\n        return d\n\n    def get_invoices_and_requests_touched_by_tx(self, tx):\n        request_keys = set()\n        invoice_keys = set()\n        with self.lock:\n            for txo in tx.outputs():\n                addr = txo.address\n                if request := self.get_request_by_addr(addr):\n                    request_keys.add(request.get_id())\n                for invoice_key in self._invoices_from_scriptpubkey_map.get(txo.scriptpubkey, set()):\n                    invoice_keys.add(invoice_key)\n        return request_keys, invoice_keys\n\n    def _update_invoices_and_reqs_touched_by_tx(self, tx_hash: str) -> None:\n        # FIXME in some cases if tx2 replaces unconfirmed tx1 in the mempool, we are not called.\n        #       For a given receive request, if tx1 touches it but tx2 does not, then\n        #       we were called when tx1 was added, but we will not get called when tx2 replaces tx1.\n        tx = self.db.get_transaction(tx_hash)\n        if tx is None:\n            return\n        request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx)\n        for key in request_keys:\n            request = self.get_request(key)\n            if not request:\n                continue\n            status = self.get_invoice_status(request)\n            util.trigger_callback('request_status', self, request.get_id(), status)\n        self._update_onchain_invoice_paid_detection(invoice_keys)\n\n    def set_broadcasting(self, tx: Transaction, *, broadcasting_status: Optional[int]):\n        request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx)\n        for key in invoice_keys:\n            invoice = self._invoices.get(key)\n            if not invoice:\n                continue\n            invoice._broadcasting_status = broadcasting_status\n            status = self.get_invoice_status(invoice)\n            util.trigger_callback('invoice_status', self, key, status)\n\n    def get_bolt11_invoice(self, req: Request) -> str:\n        if not self.lnworker:\n            return ''\n        if (payment_hash := req.payment_hash) is None:  # e.g. req might have been generated before enabling LN\n            return ''\n        amount_msat = req.get_amount_msat() or None\n        assert (amount_msat is None or amount_msat > 0), amount_msat\n        info = self.lnworker.get_payment_info(payment_hash, direction=RECEIVED)\n        assert info.amount_msat == amount_msat, f\"{info.amount_msat=} != {amount_msat=}\"  # info.amount_msat or None\n        lnaddr, invoice = self.lnworker.get_bolt11_invoice(\n            payment_info=info,\n            message=req.message,\n            fallback_address=None)\n        return invoice\n\n    def create_request(self, amount_sat: Optional[int], message: Optional[str], exp_delay: Optional[int], address: Optional[str]):\n        \"\"\" will create a lightning request if address is None \"\"\"\n        # for receiving\n        amount_sat = amount_sat or 0\n        assert isinstance(amount_sat, int), f\"{amount_sat!r}\"\n        amount_msat = None if not amount_sat else amount_sat * 1000  # amount_sat in [None, 0] implies undefined.\n        message = message or ''\n        address = address or None  # converts \"\" to None\n        exp_delay = exp_delay or 0\n        timestamp = int(Request._get_cur_time())\n        if address is None:\n            assert self.has_lightning()\n            payment_hash = self.lnworker.create_payment_info(\n                amount_msat=amount_msat,\n                exp_delay=exp_delay,\n                write_to_disk=False,\n            )\n        else:\n            payment_hash = None\n        outputs = [PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []\n        height = self.adb.get_local_height()\n        req = Request(\n            outputs=outputs,\n            message=message,\n            time=timestamp,\n            amount_msat=amount_msat,\n            exp=exp_delay,\n            height=height,\n            bip70=None,\n            payment_hash=payment_hash,\n        )\n        key = self.add_payment_request(req)\n        return key\n\n    def add_payment_request(self, req: Request, *, write_to_disk: bool = True):\n        request_id = req.get_id()\n        self._receive_requests[request_id] = req\n        if addr := req.get_address():\n            self._requests_addr_to_key[addr].add(request_id)\n        if write_to_disk:\n            self.save_db()\n        return request_id\n\n    def delete_request(self, request_id, *, write_to_disk: bool = True):\n        \"\"\" lightning or on-chain \"\"\"\n        req = self.get_request(request_id)\n        if req is None:\n            return\n        self._receive_requests.pop(request_id, None)\n        if addr := req.get_address():\n            self._requests_addr_to_key[addr].discard(request_id)\n        if req.is_lightning() and self.lnworker:\n            self.lnworker.delete_payment_info(req.rhash, direction=RECEIVED)\n        if write_to_disk:\n            self.save_db()\n\n    def delete_invoice(self, invoice_id, *, write_to_disk: bool = True):\n        \"\"\" lightning or on-chain \"\"\"\n        inv = self._invoices.pop(invoice_id, None)\n        if inv is None:\n            return\n        if inv.is_lightning() and self.lnworker:\n            self.lnworker.delete_payment_info(inv.rhash, direction=SENT)\n        if write_to_disk:\n            self.save_db()\n\n    def get_requests(self) -> List[Request]:\n        out = [self.get_request(x) for x in self._receive_requests.keys()]\n        out = [x for x in out if x is not None]\n        return out\n\n    def get_sorted_requests(self) -> List[Request]:\n        \"\"\" sorted by timestamp \"\"\"\n        out = self.get_requests()\n        out.sort(key=lambda x: x.time)\n        return out\n\n    def get_unpaid_requests(self) -> List[Request]:\n        out = [x for x in self._receive_requests.values() if self.get_invoice_status(x) != PR_PAID]\n        out.sort(key=lambda x: x.time)\n        return out\n\n    def delete_expired_requests(self):\n        keys = [k for k, v in self._receive_requests.items() if self.get_invoice_status(v) == PR_EXPIRED]\n        self.delete_requests(keys)\n        return keys\n\n    def delete_requests(self, keys):\n        for key in keys:\n            self.delete_request(key, write_to_disk=False)\n        if keys:\n            self.save_db()\n\n    @abstractmethod\n    def get_fingerprint(self) -> str:\n        \"\"\"Returns a string that can be used to identify this wallet.\n        Used e.g. by Labels plugin, and LN channel backups.\n        Returns empty string \"\" for wallets that don't have an ID.\n        \"\"\"\n        pass\n\n    def can_import_privkey(self):\n        return False\n\n    def can_import_address(self):\n        return False\n\n    def can_delete_address(self):\n        return False\n\n    def has_password(self) -> bool:\n        return self.has_keystore_encryption() or self.has_storage_encryption() #and self.storage.is_encrypted_with_user_pw())\n\n    def can_have_keystore_encryption(self):\n        return self.keystore and self.keystore.may_have_password()\n\n    def get_available_storage_encryption_versions(self) -> Sequence[StorageEncryptionVersion]:\n        \"\"\"Returns the type of storage encryption offered to the user.\n\n        A wallet file (storage) is either encrypted with this version\n        or is stored in plaintext.\n        \"\"\"\n        out = [StorageEncryptionVersion.USER_PASSWORD]\n        if isinstance(self.keystore, Hardware_KeyStore):\n            out.append(StorageEncryptionVersion.XPUB_PASSWORD)\n        return out\n\n    def has_keystore_encryption(self) -> bool:\n        \"\"\"Returns whether encryption is enabled for the keystore.\n\n        If True, e.g. signing a transaction will require a password.\n        \"\"\"\n        if self.can_have_keystore_encryption():\n            return bool(self.db.get('use_encryption', False))\n        return False\n\n    def has_storage_encryption(self) -> bool:\n        \"\"\"Returns whether encryption is enabled for the wallet file on disk.\"\"\"\n        return bool(self.storage) and self.storage.is_encrypted()\n\n    @classmethod\n    def may_have_password(cls):\n        return True\n\n    def check_password(self, password: Optional[str]) -> None:\n        \"\"\"Raises an InvalidPassword exception on invalid password\"\"\"\n        if not self.has_password():\n            if password is not None:\n                raise InvalidPassword(\"password given but wallet has no password\")\n            return\n        if self.has_keystore_encryption():\n            self.keystore.check_password(password)\n        if self.has_storage_encryption():\n            self.storage.check_password(password)\n\n    def update_password(self, old_pw, new_pw, *, encrypt_storage: bool = True, xpub_encrypt: bool = False):\n        if old_pw is None and self.has_password():\n            raise InvalidPassword()\n        self.check_password(old_pw)\n        if self.storage:\n            if encrypt_storage:\n                enc_version = StorageEncryptionVersion.XPUB_PASSWORD if xpub_encrypt else StorageEncryptionVersion.USER_PASSWORD\n                assert enc_version in self.get_available_storage_encryption_versions()\n            else:\n                enc_version = StorageEncryptionVersion.PLAINTEXT\n            self.storage.set_password(new_pw, enc_version)\n        # make sure next storage.write() saves changes\n        self.db.set_modified(True)\n\n        # note: Encrypting storage with a hw device is currently only\n        #       allowed for non-multisig wallets. Further,\n        #       Hardware_KeyStore.may_have_password() == False.\n        #       If these were not the case,\n        #       extra care would need to be taken when encrypting keystores.\n        self._update_password_for_keystore(old_pw, new_pw)\n        encrypt_keystore = self.can_have_keystore_encryption()\n        self.db.set_keystore_encryption(bool(new_pw) and encrypt_keystore)\n        # save changes. force full rewrite to rm remnants of old password\n        if self.storage and self.storage.file_exists():\n            self.db.write_and_force_consolidation()\n        # if wallet was previously unlocked, reset password_in_memory\n        self.lock_wallet()\n\n    @abstractmethod\n    def _update_password_for_keystore(self, old_pw: Optional[str], new_pw: Optional[str]) -> None:\n        pass\n\n    def sign_message(self, address: str, message: str, password) -> bytes:\n        index = self.get_address_index(address)\n        script_type = self.get_txin_type(address)\n        assert script_type != \"address\"\n        return self.keystore.sign_message(index, message, password, script_type=script_type)\n\n    def decrypt_message(self, pubkey: str, message, password) -> bytes:\n        addr = self.pubkeys_to_address([pubkey])\n        index = self.get_address_index(addr)\n        return self.keystore.decrypt_message(index, message, password)\n\n    @abstractmethod\n    def pubkeys_to_address(self, pubkeys: Sequence[str]) -> Optional[str]:\n        pass\n\n    def price_at_timestamp(self, txid, price_func):\n        \"\"\"Returns fiat price of bitcoin at the time tx got confirmed.\"\"\"\n        timestamp = self.adb.get_tx_height(txid).timestamp\n        return price_func(timestamp if timestamp else time.time())\n\n    def average_price(self, txid, price_func, ccy) -> Decimal:\n        \"\"\" Average acquisition price of the inputs of a transaction \"\"\"\n        input_value = 0\n        total_price = 0\n        txi_addresses = self.db.get_txi_addresses(txid)\n        if not txi_addresses:\n            return Decimal('NaN')\n        for addr in txi_addresses:\n            d = self.db.get_txi_addr(txid, addr)\n            for ser, v in d:\n                input_value += v\n                total_price += self.coin_price(ser.split(':')[0], price_func, ccy, v)\n        return total_price / (input_value/Decimal(COIN))\n\n    def clear_coin_price_cache(self):\n        self._coin_price_cache = {}\n\n    def coin_price(self, txid, price_func, ccy, txin_value) -> Decimal:\n        \"\"\"\n        Acquisition price of a coin.\n        This assumes that either all inputs are mine, or no input is mine.\n        \"\"\"\n        if txin_value is None:\n            return Decimal('NaN')\n        cache_key = \"{}:{}:{}\".format(str(txid), str(ccy), str(txin_value))\n        result = self._coin_price_cache.get(cache_key, None)\n        if result is not None:\n            return result\n        if self.db.get_txi_addresses(txid):\n            result = self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN)\n            self._coin_price_cache[cache_key] = result\n            return result\n        else:\n            fiat_value = self.get_fiat_value(txid, ccy)\n            if fiat_value is not None:\n                return fiat_value\n            else:\n                p = self.price_at_timestamp(txid, price_func)\n                return p * txin_value/Decimal(COIN)\n\n    def is_billing_address(self, addr):\n        # overridden for TrustedCoin wallets\n        return False\n\n    @abstractmethod\n    def is_watching_only(self) -> bool:\n        pass\n\n    def get_keystore(self) -> Optional[KeyStore]:\n        return self.keystore\n\n    def get_keystores(self) -> Sequence[KeyStore]:\n        return [self.keystore] if self.keystore else []\n\n    @abstractmethod\n    def save_keystore(self):\n        pass\n\n    def can_enable_disable_keystore(self, ks: KeyStore) -> bool:\n        \"\"\"Whether the wallet is capable of disabling/enabling the given keystore.\n        This is a necessary but not sufficient check: e.g. if wallet has LN channels, we should not allow disabling.\n        \"\"\"\n        return False\n\n    def enable_keystore(self, keystore: KeyStore, is_hardware_keystore: bool, password) -> None:\n        raise NotImplementedError()\n\n    def disable_keystore(self, keystore: KeyStore) -> None:\n        raise NotImplementedError()\n\n    def _update_keystore(self, keystore: KeyStore) -> None:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def has_seed(self) -> bool:\n        pass\n\n    def get_seed_type(self) -> Optional[str]:\n        return None\n\n    @abstractmethod\n    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:\n        pass\n\n    def _check_risk_of_burning_coins_as_fees(self, tx: 'PartialTransaction') -> TxSighashDanger:\n        \"\"\"Helper method to check if there is risk of burning coins as fees if we sign.\n        Note that if not all inputs are ismine, e.g. coinjoin, the risk is not just about fees.\n\n        Note:\n            - legacy sighash does not commit to any input amounts\n            - BIP-0143 sighash only commits to the *corresponding* input amount\n            - BIP-taproot sighash commits to *all* input amounts\n        \"\"\"\n        assert isinstance(tx, PartialTransaction)\n        rl = TxSighashRiskLevel\n        short_message = _(\"Warning\") + \": \" + _(\"The fee could not be verified!\")\n        # check that all inputs use SIGHASH_ALL\n        if not all(txin.sighash in (None, Sighash.ALL) for txin in tx.inputs()):\n            messages = [(_(\"Warning\") + \": \"\n                         + _(\"Some inputs use non-default sighash flags, which might affect the fee.\"))]\n            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)\n        # if we have all full previous txs, we *know* all the input amounts -> fine\n        if all([txin.utxo for txin in tx.inputs()]):\n            return TxSighashDanger(risk_level=rl.SAFE)\n        # a single segwit input -> fine\n        if len(tx.inputs()) == 1 and tx.inputs()[0].is_segwit() and tx.inputs()[0].witness_utxo:\n            return TxSighashDanger(risk_level=rl.SAFE)\n        # coinjoin or similar\n        if any([not self.is_mine(txin.address) for txin in tx.inputs()]):\n            messages = [(_(\"Warning\") + \": \"\n                         + _(\"The input amounts could not be verified as the previous transactions are missing.\\n\"\n                             \"The amount of money being spent CANNOT be verified.\"))]\n            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)\n        # some inputs are legacy\n        if any([not txin.is_segwit() for txin in tx.inputs()]):\n            messages = [(_(\"Warning\") + \": \"\n                         + _(\"The fee could not be verified. Signing non-segwit inputs is risky:\\n\"\n                             \"if this transaction was maliciously modified before you sign,\\n\"\n                             \"you might end up paying a higher mining fee than displayed.\"))]\n            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)\n        # all inputs are segwit\n        # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-August/014843.html\n        messages = [(_(\"Warning\") + \": \"\n                     + _(\"If you received this transaction from an untrusted device, \"\n                         \"do not accept to sign it more than once,\\n\"\n                         \"otherwise you could end up paying a different fee.\"))]\n        return TxSighashDanger(risk_level=rl.FEE_WARNING_SKIPCONFIRM, short_message=short_message, messages=messages)\n\n    def check_sighash(self, tx: 'PartialTransaction') -> TxSighashDanger:\n        \"\"\"Checks the Sighash for my inputs and considers if the tx is safe to sign.\"\"\"\n        assert isinstance(tx, PartialTransaction)\n        rl = TxSighashRiskLevel\n        hintmap = {\n            0:                    (rl.SAFE,           None),\n            Sighash.NONE:         (rl.INSANE_SIGHASH, _('Input {} is marked SIGHASH_NONE.')),\n            Sighash.SINGLE:       (rl.WEIRD_SIGHASH,  _('Input {} is marked SIGHASH_SINGLE.')),\n            Sighash.ALL:          (rl.SAFE,           None),\n            Sighash.ANYONECANPAY: (rl.WEIRD_SIGHASH,  _('Input {} is marked SIGHASH_ANYONECANPAY.')),\n        }\n        sighash_danger = TxSighashDanger()\n        for txin_idx, txin in enumerate(tx.inputs()):\n            if txin.sighash in (None, Sighash.ALL):\n                continue  # None will get converted to Sighash.ALL, so these values are safe\n            # found interesting sighash flag\n            addr = self.adb.get_txin_address(txin)\n            if self.is_mine(addr):\n                sh_base = txin.sighash & (Sighash.ANYONECANPAY ^ 0xff)\n                sh_acp = txin.sighash & Sighash.ANYONECANPAY\n                for sh in [sh_base, sh_acp]:\n                    if msg := hintmap[sh][1]:\n                        risk_level = hintmap[sh][0]\n                        header = _('Fatal') if TxSighashDanger(risk_level=risk_level).needs_reject() else _('Warning')\n                        shd = TxSighashDanger(\n                            risk_level=risk_level,\n                            short_message=_('Danger! This transaction uses non-default sighash flags!'),\n                            messages=[f\"{header}: {msg.format(txin_idx)}\"],\n                        )\n                        sighash_danger = sighash_danger.combine(shd)\n        if sighash_danger.needs_reject():  # no point for further tests\n            return sighash_danger\n        # if we show any fee to the user, check now how reliable that is:\n        if self.get_wallet_delta(tx).fee is not None:\n            shd = self._check_risk_of_burning_coins_as_fees(tx)\n            sighash_danger = sighash_danger.combine(shd)\n        return sighash_danger\n\n    def get_tx_fee_warning(\n            self, *,\n            invoice_amt: int,\n            tx_size: int,\n            fee: int,\n            txid: Optional[str]) -> Optional[Tuple[bool, str, str]]:\n\n        assert invoice_amt >= 0, f\"{invoice_amt=!r} must be non-negative satoshis\"\n        assert fee >= 0, f\"{fee=!r} must be non-negative satoshis\"\n        is_future_tx = txid is not None and txid in self.adb.future_tx\n        feerate = Decimal(fee) / tx_size  # sat/byte\n        fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 0\n        long_warning = None\n        short_warning = None\n        allow_send = True\n        if feerate < Decimal(self.relayfee()) / 1000 and not is_future_tx:\n            long_warning = ' '.join([\n                _(\"This transaction requires a higher fee, or it will not be propagated by your current server.\"),\n                _(\"Try to raise your transaction fee, or use a server with a lower relay fee.\")\n            ])\n            short_warning = _(\"below relay fee\") + \"!\"\n            allow_send = False\n        elif fee_ratio >= FEE_RATIO_HIGH_WARNING:\n            long_warning = ' '.join([\n                _(\"The fee for this transaction seems unusually high.\"),\n                _(\"({}% of amount)\").format(f'{fee_ratio*100:.2f}')\n            ])\n            short_warning = _(\"high fee ratio\") + \"!\"\n        elif feerate > FEERATE_WARNING_HIGH_FEE / 1000:\n            long_warning = ' '.join([\n                _(\"The fee for this transaction seems unusually high.\"),\n                _(\"(feerate: {})\").format(self.config.format_fee_rate(1000 * feerate))\n            ])\n            short_warning = _(\"high fee rate\") + \"!\"\n        if long_warning is None:\n            return None\n        else:\n            return allow_send, long_warning, short_warning\n\n    def get_help_texts_for_receive_request(self, req: Request) -> ReceiveRequestHelp:\n        key = req.get_id()\n        addr = req.get_address() or ''\n        amount_sat = req.get_amount_sat() or 0\n        address_help = ''\n        URI_help = ''\n        ln_help = ''\n        address_is_error = False\n        URI_is_error = False\n        ln_is_error = False\n        ln_swap_suggestion = None\n        ln_rebalance_suggestion = None\n        ln_zeroconf_suggestion = False\n        URI = self.get_request_URI(req) or ''\n        lightning_has_channels = (\n            self.lnworker and len([chan for chan in self.lnworker.channels.values() if chan.is_open()]) > 0\n        )\n        lightning_online = self.lnworker and self.lnworker.lnpeermgr.num_peers() > 0\n        num_sats_can_receive = self.lnworker.num_sats_can_receive() if self.lnworker else 0\n        can_receive_lightning = self.lnworker and num_sats_can_receive > 0 and amount_sat <= num_sats_can_receive\n        try:\n            zeroconf_nodeid = extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)[0]\n        except Exception:\n            zeroconf_nodeid = None\n        can_get_zeroconf_channel = (self.lnworker and self.config.ACCEPT_ZEROCONF_CHANNELS\n                                    and self.lnworker.lnpeermgr.get_peer_by_pubkey(zeroconf_nodeid) is not None)\n        status = self.get_invoice_status(req)\n\n        if status == PR_EXPIRED:\n            address_help = URI_help = ln_help = _('This request has expired')\n\n        is_amt_too_small_for_onchain = amount_sat and amount_sat < self.dust_threshold()\n        if not addr:\n            address_is_error = True\n            address_help = _('This request cannot be paid on-chain')\n            if is_amt_too_small_for_onchain:\n                address_help = _('Amount too small to be received onchain')\n        if not URI:\n            URI_is_error = True\n            URI_help = _('This request cannot be paid on-chain')\n            if is_amt_too_small_for_onchain:\n                URI_help = _('Amount too small to be received onchain')\n        if not req.is_lightning():\n            ln_is_error = True\n            ln_help = _('This request does not have a Lightning invoice.')\n\n        if status == PR_UNPAID:\n            if self.adb.is_used(addr):\n                address_help = URI_help = (_(\"This address has already been used. \"\n                                             \"For better privacy, do not reuse it for new payments.\"))\n            if req.is_lightning():\n                if not lightning_has_channels and not can_get_zeroconf_channel:\n                    ln_is_error = True\n                    ln_help = _(\"You must have an open Lightning channel to receive payments.\")\n                elif not lightning_online:\n                    ln_is_error = True\n                    ln_help = _('You must be online to receive Lightning payments.')\n                elif not can_receive_lightning or (amount_sat <= 0 and not lightning_has_channels):\n                    ln_rebalance_suggestion = self.lnworker.suggest_rebalance_to_receive(amount_sat)\n                    ln_swap_suggestion = self.lnworker.suggest_swap_to_receive(max(amount_sat, MIN_SWAP_AMOUNT_SAT))\n                    # prefer to use swaps over JIT channels if possible\n                    if can_get_zeroconf_channel and not bool(ln_rebalance_suggestion) and not bool(ln_swap_suggestion):\n                        if amount_sat < MIN_FUNDING_SAT:\n                            ln_is_error = True\n                            ln_help = (_('Cannot receive this payment. Request at least {} '\n                                       'to purchase a Lightning channel from your service provider.')\n                                       .format(self.config.format_amount_and_units(amount_sat=MIN_FUNDING_SAT)))\n                        else:\n                            ln_zeroconf_suggestion = True\n                            ln_help = _(f'Receiving this payment will purchase a payment channel from your '\n                                        f'service provider. Service fees are deducted from the incoming payment.')\n                    else:\n                        ln_is_error = True\n                        ln_help = _('You do not have enough capacity to receive with Lightning.')\n                        if bool(ln_rebalance_suggestion):\n                            ln_help += '\\n\\n' + _('You may have enough capacity if you rebalance your channels.')\n                        elif bool(ln_swap_suggestion):\n                            ln_help += '\\n\\n' + _('You may have enough capacity if you swap some of your funds.')\n                # for URI that has LN part but no onchain part, copy error:\n                if not addr and ln_is_error:\n                    URI_is_error = ln_is_error\n                    URI_help = ln_help\n        return ReceiveRequestHelp(\n            address_help=address_help,\n            URI_help=URI_help,\n            ln_help=ln_help,\n            address_is_error=address_is_error,\n            URI_is_error=URI_is_error,\n            ln_is_error=ln_is_error,\n            ln_rebalance_suggestion=ln_rebalance_suggestion,\n            ln_swap_suggestion=ln_swap_suggestion,\n            ln_zeroconf_suggestion=ln_zeroconf_suggestion\n        )\n\n    def synchronize(self) -> int:\n        \"\"\"Returns the number of new addresses we generated.\"\"\"\n        return 0\n\n    def unlock(self, password: Optional[str]) -> None:\n        self.logger.info(f'unlocking wallet')\n        password = password or None\n        self.check_password(password)\n        self._password_in_memory = password\n\n    def lock_wallet(self):\n        self._password_in_memory = None\n\n    def get_unlocked_password(self) -> Optional[str]:\n        pw = self._password_in_memory\n        if not self.is_unlocked():\n            return None\n        try:\n            self.check_password(pw)\n        except InvalidPassword as e:\n            raise Exception(\"inconsistent _password_in_memory\") from e\n        return pw\n\n    def is_unlocked(self) -> bool:\n        return self._password_in_memory is not None or not self.has_password()\n\n    def get_text_not_enough_funds_mentioning_frozen(\n            self,\n            *,\n            for_amount: Optional[Union[int, str]] = None,\n            hint: Optional[str] = None\n    ) -> str:\n        \"\"\"Generate 'Not enough funds' text.\n        Include mention of frozen coins (and append optional hint), iff unfreezing would satisfy for_amount\n        \"\"\"\n        text = _('Not enough funds')\n        if for_amount is not None:\n            if frozen_bal := sum(self.get_frozen_balance()):\n                frozen_str = None\n                if isinstance(for_amount, int):\n                    if frozen_bal + self.get_spendable_balance_sat() > for_amount:\n                        frozen_str = self.config.format_amount_and_units(frozen_bal)\n                elif for_amount == '!':\n                    frozen_str = self.config.format_amount_and_units(frozen_bal)\n                if frozen_str:\n                    text = _('Not enough funds') + \" \" + _('({} are frozen)').format(frozen_str)\n                if hint:\n                    text += '. ' + hint\n        return text\n\n    def get_frozen_balance_str(self) -> Optional[str]:\n        frozen_bal = sum(self.get_frozen_balance())\n        if not frozen_bal:\n            return None\n        return self.config.format_amount_and_units(frozen_bal)\n\n    def add_future_tx(self, sweep_info: 'SweepInfo', wanted_height: int):\n        \"\"\" add local tx to provide user feedback \"\"\"\n        txin = copy.deepcopy(sweep_info.txin)\n        prevout = txin.prevout.to_str()\n        prev_txid, index = prevout.split(':')\n        if txid := self.adb.db.get_spent_outpoint(prev_txid, int(index)):\n            # set future tx of existing spender because it is not persisted\n            # (and wanted_height can change if input of CSV was not mined before)\n            self.adb.set_future_tx(txid, wanted_height=wanted_height)\n            return\n        name = sweep_info.name\n        # outputs = [] will send coins to a change address\n        tx = self.make_unsigned_transaction(\n            inputs=[txin],\n            outputs=[],\n            fee_policy=FixedFeePolicy(0),\n        )\n        try:\n            self.adb.add_transaction(tx)\n        except Exception as e:\n            self.logger.info(f'could not add future tx: {name}. prevout: {prevout} {str(e)}')\n            return\n        self.logger.info(f'added future tx: {name}. prevout: {prevout}')\n        util.trigger_callback('wallet_updated', self)\n        self.adb.set_future_tx(tx.txid(), wanted_height=wanted_height)\n\n    def export_history_to_file(self, *, fx: Optional['FxThread'], file_path: str, is_csv: bool):\n        \"\"\"Create a file containing the wallet history in either json or csv format, e.g. for bookkeeping.\"\"\"\n        if run_hook('export_history_to_file', self, fx, file_path, is_csv):\n            return  # allow for plugins to create history fancy export\n        txns = self.get_full_history(fx=fx)\n        # remove unconfirmed/local tx as their ordering is not deterministic, and they don't seem\n        # useful for a wallet export (can't do accounting on a tx that hasn't happened yet)\n        txns = {k: v for k, v in txns.items() if v['timestamp'] not in (None, 0)}\n\n        def get_all_fees_paid_by_item(h_item: dict) -> Tuple[int, Optional[Fiat]]:\n            # gets all fees paid in an item (or group), as the outer group doesn't contain the\n            # transaction fees paid by the children\n            fees_sat = 0\n            fees_fiat = Fiat(ccy=fx.ccy, value=Decimal()) if fx else None\n            for child in h_item.get('children', []):\n                fees_sat += child['fee_sat'] or 0 if 'fee_sat' in child \\\n                    else (child.get('fee_msat', 0) or 0) // 1000  # FIXME: loses msat precision\n                if fees_fiat is not None and (child_fiat_fee := child.get('fiat_fee')):\n                    fees_fiat += child_fiat_fee\n\n            fees_sat += h_item['fee_sat'] or 0 if 'fee_sat' in h_item \\\n                else (h_item.get('fee_msat', 0) or 0) // 1000  # FIXME: loses msat precision\n            if fees_fiat is not None and (h_item_fiat_fee := h_item.get('fiat_fee')):\n                fees_fiat += h_item_fiat_fee\n\n            fiat_value = h_item.get('fiat_value')\n            if fees_fiat is not None and isinstance(fiat_value, Fiat) \\\n                    and (fiat_value.value is None or fiat_value.value.is_nan()):\n                # ensure that str(fees_fiat) == 'No Data' if str(fiat_value) == 'No Data'\n                fees_fiat = Fiat(ccy=fx.ccy, value=None)\n\n            return fees_sat, fees_fiat\n\n        lines = []\n        if is_csv:\n            # sort by timestamp so the generated csv is more understandable on first sight\n            txns = dict(sorted(txns.items(), key=lambda h_item: h_item[1]['timestamp']))\n            for item in txns.values():\n                # tx groups will are shown as single element\n                fees_sat, fees_fiat = get_all_fees_paid_by_item(item)\n                # users are sensitive to changes of these fields as they have scripts/spreadsheets\n                # depending on them. E.g. https://github.com/spesmilo/electrum/issues/10445\n                assert str(fees_fiat) == 'No Data' if str(item.get('fiat_value')) == 'No Data' else True\n                line = [\n                    item.get('txid', ''),\n                    item.get('payment_hash', ''),\n                    item.get('label', ''),\n                    item.get('confirmations', ''),\n                    item['bc_value'],\n                    item['ln_value'],\n                    item.get('fiat_value', ''),\n                    util.format_satoshis(fees_sat),\n                    str(fees_fiat or ''),\n                    item['date']\n                ]\n                lines.append(line)\n\n        with open(file_path, \"w+\", encoding='utf-8') as f:\n            if is_csv:\n                import csv\n                transaction = csv.writer(f, lineterminator='\\n')\n                transaction.writerow([\"oc_transaction_hash\",\n                                      \"ln_payment_hash\",\n                                      \"label\",\n                                      \"confirmations\",\n                                      \"amount_chain_bc\",\n                                      \"amount_lightning_bc\",\n                                      \"fiat_value\",\n                                      \"network_fee_bc\",\n                                      \"fiat_fee\",\n                                      \"timestamp\"])\n                for line in lines:\n                    transaction.writerow(line)\n            else:\n                f.write(util.json_encode(txns))\n\n    def get_user_notifications_for_new_txns(self, txns: Sequence[Transaction]) -> Sequence[str]:\n        notifications = []\n        # Combine the transactions if there are at least three\n        if len(txns) >= 3:\n            total_amount = 0\n            total_debit = 0\n            total_credit = 0\n            for tx in txns:\n                tx_wallet_delta = self.get_wallet_delta(tx)\n                if not tx_wallet_delta.is_relevant:\n                    continue\n                if tx_wallet_delta.delta < 0:\n                    total_debit += -tx_wallet_delta.delta\n                else:\n                    total_credit += tx_wallet_delta.delta\n                total_amount += tx_wallet_delta.delta\n            message = _('{} new transactions:').format(len(txns))\n            if total_debit:\n                message += '\\n' + _('Total amount sent {}').format(self.config.format_amount_and_units(total_debit))\n            if total_credit:\n                message += '\\n' + _('Total amount received {}').format(self.config.format_amount_and_units(total_credit))\n            if total_debit and total_credit:\n                message += '\\n' + _('Total balance change: {}').format(self.config.format_amount_and_units(total_amount))\n            notifications.append(message)\n        else:\n            for tx in txns:\n                tx_wallet_delta = self.get_wallet_delta(tx)\n                if not tx_wallet_delta.is_relevant:\n                    continue\n                if tx_wallet_delta.delta < 0:\n                    message = _('sent {}').format(self.config.format_amount_and_units(-tx_wallet_delta.delta))\n                else:\n                    message = _('received {}').format(self.config.format_amount_and_units(tx_wallet_delta.delta))\n                message = _(\"New transaction: {}\").format(message)\n                notifications.append(message)\n        return notifications\n\n\nclass Simple_Wallet(Abstract_Wallet):\n    # wallet with a single keystore\n\n    def is_watching_only(self):\n        return self.keystore.is_watching_only()\n\n    def _update_password_for_keystore(self, old_pw, new_pw):\n        if self.keystore and self.keystore.may_have_password():\n            self.keystore.update_password(old_pw, new_pw)\n            self.save_keystore()\n\n    def save_keystore(self):\n        self.db.put('keystore', self.keystore.dump())\n\n    @abstractmethod\n    def get_public_key(self, address: str) -> Optional[str]:\n        pass\n\n    def get_public_keys(self, address: str) -> Sequence[str]:\n        pk = self.get_public_key(address)\n        return [pk] if pk else []\n\n\nclass Imported_Wallet(Simple_Wallet):\n    # wallet made of imported addresses\n\n    wallet_type = 'imported'\n    txin_type = 'address'\n\n    def __init__(self, db, *, config):\n        Abstract_Wallet.__init__(self, db, config=config)\n        self.use_change = db.get('use_change', False)\n\n    def is_watching_only(self):\n        return self.keystore is None\n\n    def can_import_privkey(self):\n        return bool(self.keystore)\n\n    def load_keystore(self):\n        self.keystore = load_keystore(self.db, 'keystore') if self.db.get('keystore') else None\n\n    def save_keystore(self):\n        self.db.put('keystore', self.keystore.dump())\n\n    def can_import_address(self):\n        return self.is_watching_only()\n\n    def can_delete_address(self):\n        return True\n\n    def has_seed(self):\n        return False\n\n    def is_deterministic(self):\n        return False\n\n    def is_change(self, address):\n        return False\n\n    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:\n        return set()\n\n    def get_fingerprint(self):\n        return ''\n\n    def get_addresses(self):\n        # note: overridden so that the history can be cleared\n        return self.db.get_imported_addresses()\n\n    def get_receiving_addresses(self, **kwargs):\n        return self.get_addresses()\n\n    def get_change_addresses(self, **kwargs):\n        return self.get_addresses()\n\n    def import_addresses(self, addresses: List[str], *,\n                         write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:\n        good_addr = []  # type: List[str]\n        bad_addr = []  # type: List[Tuple[str, str]]\n        for address in addresses:\n            if not bitcoin.is_address(address):\n                bad_addr.append((address, _('invalid address')))\n                continue\n            if self.db.has_imported_address(address):\n                bad_addr.append((address, _('address already in wallet')))\n                continue\n            good_addr.append(address)\n            self.db.add_imported_address(address, {})\n            self.adb.add_address(address)\n        if write_to_disk:\n            self.save_db()\n        return good_addr, bad_addr\n\n    def import_address(self, address: str) -> str:\n        good_addr, bad_addr = self.import_addresses([address])\n        if good_addr and good_addr[0] == address:\n            return address\n        else:\n            raise BitcoinException(str(bad_addr[0][1]))\n\n    def delete_address(self, address: str) -> None:\n        if not self.db.has_imported_address(address):\n            return\n        with self.lock:\n            if len(self.get_addresses()) <= 1:  # check this inside lock\n                raise UserFacingException(_('Cannot delete last remaining address from wallet'))\n            transactions_to_remove = set()  # only referred to by this address\n            transactions_new = set()  # txs that are not only referred to by address\n            # rm txs from history\n            for addr in self.db.get_history():\n                details = self.adb.get_address_history(addr).items()\n                if addr == address:\n                    for tx_hash, height in details:\n                        transactions_to_remove.add(tx_hash)\n                else:\n                    for tx_hash, height in details:\n                        transactions_new.add(tx_hash)\n            transactions_to_remove -= transactions_new\n            self.db.remove_addr_history(address)\n            for tx_hash in transactions_to_remove:\n                self.adb._remove_transaction(tx_hash)\n            # rm label for addr\n            # TODO rm label for txids?\n            self.set_label(address, None)\n            # rm receive requests for addr\n            if req := self.get_request_by_addr(address):\n                self.delete_request(req.get_id())\n            self.set_frozen_state_of_addresses([address], False, write_to_disk=False)\n            # rm corresponding key from keystore\n            pubkey = self.get_public_key(address)\n            self.db.remove_imported_address(address)\n            if pubkey:\n                # delete key iff no other address uses it (e.g. p2pkh and p2wpkh for same key)\n                for txin_type in bitcoin.WIF_SCRIPT_TYPES.keys():\n                    try:\n                        addr2 = bitcoin.pubkey_to_address(txin_type, pubkey)\n                    except descriptor.NotLegacySinglesigScriptType:\n                        pass\n                    else:\n                        if self.db.has_imported_address(addr2):\n                            break\n                else:\n                    self.keystore.delete_imported_key(pubkey)\n                    self.save_keystore()\n            self.save_db()\n\n    def get_change_addresses_for_new_transaction(self, *args, **kwargs) -> List[str]:\n        # for an imported wallet, if all \"change addresses\" are already used,\n        # it is probably better to send change back to the \"from address\", than to\n        # send it to another random used address and link them together, hence\n        # we force \"allow_reusing_used_change_addrs=False\"\n        return super().get_change_addresses_for_new_transaction(\n            *args,\n            **{**kwargs, \"allow_reusing_used_change_addrs\": False},\n        )\n\n    def _calc_unused_change_addresses(self) -> Sequence[str]:\n        with self.lock:\n            unused_addrs = [addr for addr in self.get_change_addresses()\n                            if not self.adb.is_used(addr) and not self.is_address_reserved(addr)]\n            return unused_addrs\n\n    def is_mine(self, address) -> bool:\n        if not address:\n            return False\n        return self.db.has_imported_address(address)\n\n    def get_address_index(self, address) -> Optional[str]:\n        # Return pubkey for address if we know it.\n        # If we don't know it, return None, which might happen:\n        # - if address is not is_mine\n        # - if this is an \"imported address\", we don't have the pubkey for. (watch-only imported wallet)\n        return self.get_public_key(address)\n\n    def get_address_path_str(self, address):\n        return None\n\n    def get_public_key(self, address) -> Optional[str]:\n        x = self.db.get_imported_address(address)\n        return x.get('pubkey') if x else None\n\n    def import_private_keys(self, keys: Sequence[str], password: Optional[str], *,\n                            write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:\n        good_addr = []  # type: List[str]\n        bad_keys = []  # type: List[Tuple[str, str]]\n        for key in keys:\n            try:\n                txin_type, pubkey = self.keystore.import_privkey(key, password)\n            except Exception as e:\n                bad_keys.append((key, _('invalid private key') + f': {e}'))\n                continue\n            if txin_type not in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):\n                bad_keys.append((key, _('not implemented type') + f': {txin_type}'))\n                continue\n            addr = bitcoin.pubkey_to_address(txin_type, pubkey)\n            good_addr.append(addr)\n            self.db.add_imported_address(addr, {'type': txin_type, 'pubkey': pubkey})\n            self.adb.add_address(addr)\n        self.save_keystore()\n        if write_to_disk:\n            self.save_db()\n        return good_addr, bad_keys\n\n    def import_private_key(self, key: str, password: Optional[str]) -> str:\n        good_addr, bad_keys = self.import_private_keys([key], password=password)\n        if good_addr:\n            return good_addr[0]\n        else:\n            raise BitcoinException(str(bad_keys[0][1]))\n\n    def get_txin_type(self, address):\n        return self.db.get_imported_address(address).get('type', 'address')\n\n    @profiler\n    def try_detecting_internal_addresses_corruption(self):\n        # we check only a random sample, for performance\n        addresses_all = self.get_addresses()\n        # some random *used* addresses (note: we likely have not synced yet)\n        addresses_used = [addr for addr in addresses_all if self.adb.is_used(addr)]\n        sample1 = random.sample(addresses_used, min(len(addresses_used), 10))\n        # some random *unused* addresses\n        addresses_unused = [addr for addr in addresses_all if not self.adb.is_used(addr)]\n        sample2 = random.sample(addresses_unused, min(len(addresses_unused), 10))\n        for addr_found in itertools.chain(sample1, sample2):\n            self.check_address_for_corruption(addr_found)\n\n    def check_address_for_corruption(self, addr):\n        if addr and self.is_mine(addr):\n            pubkey = self.get_public_key(addr)\n            if not pubkey:\n                return\n            txin_type = self.get_txin_type(addr)\n            if txin_type == 'address':\n                return\n            if addr != bitcoin.pubkey_to_address(txin_type, pubkey):\n                raise InternalAddressCorruption()\n\n    def pubkeys_to_address(self, pubkeys):\n        pubkey = pubkeys[0]\n        # FIXME This is slow.\n        #       Ideally we would re-derive the address from the pubkey and the txin_type,\n        #       but we don't know the txin_type, and we only have an addr->txin_type map.\n        #       so instead a linear search of reverse-lookups is done...\n        for addr in self.db.get_imported_addresses():\n            if self.db.get_imported_address(addr)['pubkey'] == pubkey:\n                return addr\n        return None\n\n    def decrypt_message(self, pubkey: str, message, password) -> bytes:\n        # this is significantly faster than the implementation in the superclass\n        return self.keystore.decrypt_message(pubkey, message, password)\n\n\nclass Deterministic_Wallet(Abstract_Wallet):\n    gap_limit_for_change: int\n\n    def __init__(self, db, *, config):\n        self._ephemeral_addr_to_addr_index = {}  # type: Dict[str, Sequence[int]]\n        Abstract_Wallet.__init__(self, db, config=config)\n        self.gap_limit = db.get('gap_limit', 20)\n        self.gap_limit_for_change = db.get('gap_limit_for_change', 10)\n        # generate addresses now. note that without libsecp this might block\n        # for a few seconds!\n        self.synchronize()\n\n    def _init_lnworker(self):\n        # lightning_privkey2 is not deterministic (legacy wallets, bip39)\n        ln_xprv = self.db.get('lightning_xprv') or self.db.get('lightning_privkey2')\n        # lnworker can only be initialized once receiving addresses are available\n        # therefore we instantiate lnworker in DeterministicWallet\n        self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None\n\n    def has_seed(self):\n        return self.keystore.has_seed()\n\n    def get_addresses(self):\n        # note: overridden so that the history can be cleared.\n        # addresses are ordered based on derivation\n        out = self.get_receiving_addresses()\n        out += self.get_change_addresses()\n        return out\n\n    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None):\n        return self.db.get_receiving_addresses(slice_start=slice_start, slice_stop=slice_stop)\n\n    def get_change_addresses(self, *, slice_start=None, slice_stop=None):\n        return self.db.get_change_addresses(slice_start=slice_start, slice_stop=slice_stop)\n\n    @profiler\n    def try_detecting_internal_addresses_corruption(self):\n        addresses_all = self.get_addresses()\n        # first few addresses\n        nfirst_few = 10\n        sample1 = addresses_all[:nfirst_few]\n        # some random *used* addresses (note: we likely have not synced yet)\n        addresses_used = [addr for addr in addresses_all[nfirst_few:] if self.adb.is_used(addr)]\n        sample2 = random.sample(addresses_used, min(len(addresses_used), 10))\n        # some random *unused* addresses\n        addresses_unused = [addr for addr in addresses_all[nfirst_few:] if not self.adb.is_used(addr)]\n        sample3 = random.sample(addresses_unused, min(len(addresses_unused), 10))\n        for addr_found in itertools.chain(sample1, sample2, sample3):\n            self.check_address_for_corruption(addr_found)\n\n    def check_address_for_corruption(self, addr):\n        if addr and self.is_mine(addr):\n            if addr != self.derive_address(*self.get_address_index(addr)):\n                raise InternalAddressCorruption()\n\n    def get_seed(self, password):\n        return self.keystore.get_seed(password)\n\n    def get_seed_type(self) -> Optional[str]:\n        if not self.has_seed():\n            return None\n        assert isinstance(self.keystore, keystore.Deterministic_KeyStore), type(self.keystore)\n        return self.keystore.get_seed_type()\n\n    def change_gap_limit(self, value):\n        \"\"\"This method is not called in the code, it is kept for console use\"\"\"\n        value = int(value)\n        if value >= self.min_acceptable_gap():\n            self.gap_limit = value\n            self.db.put('gap_limit', self.gap_limit)\n            self.save_db()\n            return True\n        else:\n            return False\n\n    def num_unused_trailing_addresses(self, addresses):\n        k = 0\n        for addr in addresses[::-1]:\n            if self.db.get_addr_history(addr):\n                break\n            k += 1\n        return k\n\n    def min_acceptable_gap(self) -> int:\n        # fixme: this assumes wallet is synchronized\n        n = 0\n        nmax = 0\n        addresses = self.get_receiving_addresses()\n        k = self.num_unused_trailing_addresses(addresses)\n        for addr in addresses[0:-k]:\n            if self.adb.address_is_old(addr):\n                n = 0\n            else:\n                n += 1\n                nmax = max(nmax, n)\n        return nmax + 1\n\n    @abstractmethod\n    def derive_pubkeys(self, c: int, i: int) -> Sequence[str]:\n        pass\n\n    def derive_address(self, for_change: int, n: int) -> str:\n        for_change = int(for_change)\n        pubkeys = self.derive_pubkeys(for_change, n)\n        return self.pubkeys_to_address(pubkeys)\n\n    def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str:\n        if isinstance(path, str):\n            path = convert_bip32_strpath_to_intpath(path)\n        pk, compressed = self.keystore.get_private_key(path, password)\n        txin_type = self.get_txin_type()  # assumes no mixed-scripts in wallet\n        return bitcoin.serialize_privkey(pk, compressed, txin_type)\n\n    def get_public_keys_with_deriv_info(self, address: str):\n        der_suffix = self.get_address_index(address)\n        der_suffix = [int(x) for x in der_suffix]\n        return {k.derive_pubkey(*der_suffix): (k, der_suffix)\n                for k in self.get_keystores()}\n\n    def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix):\n        if not self.is_mine(address):\n            return\n        pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)\n        for pubkey in pubkey_deriv_info:\n            ks, der_suffix = pubkey_deriv_info[pubkey]\n            fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix,\n                                                                                   only_der_suffix=only_der_suffix)\n            txinout.bip32_paths[pubkey] = (fp_bytes, der_full)\n\n    def create_new_address(self, for_change: bool = False):\n        assert type(for_change) is bool\n        with self.lock:\n            n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()\n            address = self.derive_address(int(for_change), n)\n            self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address)\n            self.adb.add_address(address)\n            if for_change:\n                # note: if it's actually \"old\", it will get filtered later\n                self._not_old_change_addresses.append(address)\n            return address\n\n    def synchronize_sequence(self, for_change: bool) -> int:\n        count = 0  # num new addresses we generated\n        limit = self.gap_limit_for_change if for_change else self.gap_limit\n        while True:\n            num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()\n            if num_addr < limit:\n                count += 1\n                self.create_new_address(for_change)\n                continue\n            if for_change:\n                last_few_addresses = self.get_change_addresses(slice_start=-limit)\n            else:\n                last_few_addresses = self.get_receiving_addresses(slice_start=-limit)\n            if any(map(self.adb.address_is_old, last_few_addresses)):\n                count += 1\n                self.create_new_address(for_change)\n            else:\n                break\n        return count\n\n    def synchronize(self):\n        count = 0\n        with self.lock:\n            count += self.synchronize_sequence(False)\n            count += self.synchronize_sequence(True)\n        return count\n\n    def get_all_known_addresses_beyond_gap_limit(self):\n        # note that we don't stop at first large gap\n        found = set()\n\n        def process_addresses(addrs, gap_limit):\n            rolling_num_unused = 0\n            for addr in addrs:\n                if self.db.get_addr_history(addr):\n                    rolling_num_unused = 0\n                else:\n                    if rolling_num_unused >= gap_limit:\n                        found.add(addr)\n                    rolling_num_unused += 1\n\n        process_addresses(self.get_receiving_addresses(), self.gap_limit)\n        process_addresses(self.get_change_addresses(), self.gap_limit_for_change)\n        return found\n\n    def get_address_index(self, address) -> Optional[Sequence[int]]:\n        return self.db.get_address_index(address) or self._ephemeral_addr_to_addr_index.get(address)\n\n    def get_address_path_str(self, address):\n        intpath = self.get_address_index(address)\n        if intpath is None:\n            return None\n        return convert_bip32_intpath_to_strpath(intpath)\n\n    def _learn_derivation_path_for_address_from_txinout(self, txinout, address):\n        for ks in self.get_keystores():\n            pubkey, der_suffix = ks.find_my_pubkey_in_txinout(txinout, only_der_suffix=True)\n            if der_suffix is not None:\n                # note: we already know the pubkey belongs to the keystore,\n                #       but the script template might be different\n                if len(der_suffix) != 2:\n                    continue\n                try:\n                    my_address = self.derive_address(*der_suffix)\n                except CannotDerivePubkey:\n                    my_address = None\n                if my_address == address:\n                    self._ephemeral_addr_to_addr_index[address] = list(der_suffix)\n                    return True\n        return False\n\n    def get_master_public_keys(self):\n        return [self.get_master_public_key()]\n\n    def get_fingerprint(self):\n        return self.get_master_public_key()\n\n    def get_txin_type(self, address=None):\n        return self.txin_type\n\n    def can_enable_disable_keystore(self, ks: KeyStore) -> bool:\n        return True\n\n    def enable_keystore(self, keystore: KeyStore, is_hardware_keystore: bool, password) -> None:\n        assert self.can_enable_disable_keystore(keystore)\n        if not is_hardware_keystore and self.storage.is_encrypted_with_user_pw():\n            keystore.update_password(None, password)\n            self.db.put('use_encryption', True)\n        self._update_keystore(keystore)\n\n    def disable_keystore(self, keystore: KeyStore) -> None:\n        assert self.can_enable_disable_keystore(keystore)\n        assert not self.has_channels()\n        assert keystore in self.get_keystores()\n        if hasattr(keystore, 'thread') and keystore.thread:\n            keystore.thread.stop()\n        if self.storage.is_encrypted_with_hw_device():\n            password = keystore.get_password_for_storage_encryption()\n            self.update_password(password, None, encrypt_storage=False)\n        new = keystore.watching_only_keystore()\n        self._update_keystore(new)\n\n\nclass Standard_Wallet(Simple_Wallet, Deterministic_Wallet):\n    \"\"\" Deterministic Wallet with a single pubkey per address \"\"\"\n    wallet_type = 'standard'\n\n    def __init__(self, db, *, config):\n        Deterministic_Wallet.__init__(self, db, config=config)\n\n    def get_public_key(self, address):\n        sequence = self.get_address_index(address)\n        pubkeys = self.derive_pubkeys(*sequence)\n        return pubkeys[0]\n\n    def load_keystore(self):\n        self.keystore = load_keystore(self.db, 'keystore')  # type: KeyStoreWithMPK\n        try:\n            xtype = bip32.xpub_type(self.keystore.xpub)\n        except Exception:\n            xtype = 'standard'\n        self.txin_type = 'p2pkh' if xtype == 'standard' else xtype\n\n    def get_master_public_key(self):\n        return self.keystore.get_master_public_key()\n\n    def derive_pubkeys(self, c, i):\n        return [self.keystore.derive_pubkey(c, i).hex()]\n\n    def pubkeys_to_address(self, pubkeys):\n        pubkey = pubkeys[0]\n        return bitcoin.pubkey_to_address(self.txin_type, pubkey)\n\n    def has_support_for_slip_19_ownership_proofs(self) -> bool:\n        return self.keystore.has_support_for_slip_19_ownership_proofs()\n\n    def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:\n        tx.add_info_from_wallet(self)\n        self.keystore.add_slip_19_ownership_proofs_to_tx(tx=tx, password=None)\n\n    def _update_keystore(self, keystore):\n        if self.keystore.get_master_public_key() != keystore.get_master_public_key():\n            raise Exception(\"mismatching xpubs\")\n        self.keystore = keystore\n        self.save_keystore()\n\n\n\nclass Multisig_Wallet(Deterministic_Wallet):\n    # generic m of n\n\n    def __init__(self, db, *, config):\n        self.wallet_type = db.get('wallet_type')\n        self.m, self.n = multisig_type(self.wallet_type)\n        Deterministic_Wallet.__init__(self, db, config=config)\n        # sanity checks\n        for ks in self.get_keystores():\n            if not isinstance(ks, keystore.Xpub):\n                raise Exception(f\"unexpected keystore type={type(ks)} in multisig\")\n            if bip32.xpub_type(self.keystore.xpub) != bip32.xpub_type(ks.xpub):\n                raise Exception(f\"multisig wallet needs to have homogeneous xpub types\")\n        bip32_nodes = set({ks.get_bip32_node_for_xpub() for ks in self.get_keystores()})\n        if len(bip32_nodes) != len(self.get_keystores()):\n            raise Exception(f\"duplicate xpubs in multisig\")\n\n    def get_public_keys(self, address):\n        return [pk.hex() for pk in self.get_public_keys_with_deriv_info(address)]\n\n    def pubkeys_to_address(self, pubkeys):\n        redeem_script = self.pubkeys_to_scriptcode(pubkeys)\n        return bitcoin.redeem_script_to_address(self.txin_type, redeem_script)\n\n    def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> bytes:\n        return transaction.multisig_script(sorted(pubkeys), self.m)\n\n    def derive_pubkeys(self, c, i):\n        return [k.derive_pubkey(c, i).hex() for k in self.get_keystores()]\n\n    def load_keystore(self):\n        self.keystores = {}  # type: Dict[str, KeyStore]\n        for i in range(self.n):\n            name = 'x%d' % (i+1)\n            self.keystores[name] = load_keystore(self.db, name)\n        self.keystore = self.keystores['x1']\n        xtype = bip32.xpub_type(self.keystore.xpub)\n        self.txin_type = 'p2sh' if xtype == 'standard' else xtype\n\n    def save_keystore(self):\n        for name, k in self.keystores.items():\n            self.db.put(name, k.dump())\n\n    def get_keystore(self):\n        return self.keystores.get('x1')\n\n    def get_keystores(self):\n        return [self.keystores[i] for i in sorted(self.keystores.keys())]\n\n    def _update_keystore(self, keystore):\n        for name, k in self.keystores.items():\n            if k.xpub == keystore.xpub:\n                break\n        else:\n            raise Exception('keystore not found')\n        self.keystores[name] = keystore\n        self.keystore = keystore\n        self.save_keystore()\n\n    def can_have_keystore_encryption(self):\n        return any([k.may_have_password() for k in self.get_keystores()])\n\n    def _update_password_for_keystore(self, old_pw, new_pw):\n        for name, keystore in self.keystores.items():\n            if keystore.may_have_password():\n                keystore.update_password(old_pw, new_pw)\n                self.db.put(name, keystore.dump())\n\n    def check_password(self, password):\n        for name, keystore in self.keystores.items():\n            if keystore.may_have_password():\n                keystore.check_password(password)\n        if self.has_storage_encryption():\n            self.storage.check_password(password)\n\n    def get_available_storage_encryption_versions(self) -> Sequence[StorageEncryptionVersion]:\n        # multisig wallets are not offered hw device encryption\n        return [StorageEncryptionVersion.USER_PASSWORD]\n\n    def has_seed(self):\n        return self.keystore.has_seed()\n\n    def is_watching_only(self):\n        return all([k.is_watching_only() for k in self.get_keystores()])\n\n    def get_master_public_key(self):\n        return self.keystore.get_master_public_key()\n\n    def get_master_public_keys(self):\n        return [k.get_master_public_key() for k in self.get_keystores()]\n\n    def get_fingerprint(self):\n        return ''.join(sorted(self.get_master_public_keys()))\n\n\nwallet_types = ['standard', 'multisig', 'imported']\n\n\ndef register_wallet_type(category):\n    wallet_types.append(category)\n\n\nwallet_constructors = {\n    'standard': Standard_Wallet,\n    'old': Standard_Wallet,\n    'xpub': Standard_Wallet,\n    'imported': Imported_Wallet\n}\n\n\ndef register_constructor(wallet_type, constructor):\n    wallet_constructors[wallet_type] = constructor\n\n\n# former WalletFactory\nclass Wallet(object):\n    \"\"\"The main wallet \"entry point\".\n    This class is actually a factory that will return a wallet of the correct\n    type when passed a WalletStorage instance.\"\"\"\n\n    def __new__(cls, db: 'WalletDB', *, config: SimpleConfig) -> Abstract_Wallet:\n        wallet_type = db.get('wallet_type')\n        WalletClass = cls.wallet_class(wallet_type)\n        wallet = WalletClass(db, config=config)\n        return wallet\n\n    @staticmethod\n    def wallet_class(wallet_type):\n        if multisig_type(wallet_type):\n            return Multisig_Wallet\n        if wallet_type in wallet_constructors:\n            return wallet_constructors[wallet_type]\n        raise WalletFileException(\"Unknown wallet type: \" + str(wallet_type))\n\n\ndef create_new_wallet(\n    *,\n    path,\n    config: SimpleConfig,\n    passphrase: Optional[str] = None,\n    password: Optional[str] = None,\n    encrypt_file: bool = True,\n    seed_type: Optional[str] = None,\n    gap_limit: Optional[int] = None,\n    gap_limit_for_change: Optional[int] = None,\n) -> dict:\n    \"\"\"Create a new wallet\"\"\"\n    storage = WalletStorage(path, allow_partial_writes=config.WALLET_PARTIAL_WRITES)\n    if storage.file_exists():\n        raise UserFacingException(\"Remove the existing wallet first!\")\n    db = WalletDB('', storage=storage, upgrade=True)\n\n    seed = Mnemonic('en').make_seed(seed_type=seed_type)\n    k = keystore.from_seed(seed, passphrase=passphrase)\n    db.put('keystore', k.dump())\n    db.put('wallet_type', 'standard')\n    if k.can_have_deterministic_lightning_xprv():\n        db.put('lightning_xprv', k.get_lightning_xprv(None))\n    if gap_limit is not None:\n        db.put('gap_limit', gap_limit)\n    if gap_limit_for_change is not None:\n        db.put('gap_limit_for_change', gap_limit_for_change)\n    wallet = Wallet(db, config=config)\n    wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)\n    wallet.synchronize()\n    msg = \"Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet.\"\n    wallet.save_db()\n    return {'seed': seed, 'wallet': wallet, 'msg': msg}\n\n\ndef restore_wallet_from_text(\n    text: str,\n    *,\n    path: Optional[str],\n    config: SimpleConfig,\n    passphrase: Optional[str] = None,\n    password: Optional[str] = None,\n    encrypt_file: Optional[bool] = None,\n    gap_limit: Optional[int] = None,\n    gap_limit_for_change: Optional[int] = None,\n    wallet_factory = Wallet,  # used in tests\n) -> dict:\n    \"\"\"Restore a wallet from text. Text can be a seed phrase, a master\n    public key, a master private key, a list of bitcoin addresses\n    or bitcoin private keys.\"\"\"\n    if path is None:  # create wallet in-memory\n        storage = None\n    else:\n        storage = WalletStorage(path, allow_partial_writes=config.WALLET_PARTIAL_WRITES)\n        if storage.file_exists():\n            raise UserFacingException(\"Remove the existing wallet first!\")\n    if encrypt_file is None:\n        encrypt_file = True\n    db = WalletDB('', storage=storage, upgrade=True)\n    text = text.strip()\n    if keystore.is_address_list(text):\n        wallet = Imported_Wallet(db, config=config)\n        addresses = text.split()\n        good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False)\n        # FIXME tell user about bad_inputs\n        if not good_inputs:\n            raise UserFacingException(\"None of the given addresses can be imported\")\n    elif keystore.is_private_key_list(text, allow_spaces_inside_key=False):\n        k = keystore.Imported_KeyStore({})\n        db.put('keystore', k.dump())\n        wallet = Imported_Wallet(db, config=config)\n        keys = keystore.get_private_keys(text, allow_spaces_inside_key=False)\n        good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False)\n        # FIXME tell user about bad_inputs\n        if not good_inputs:\n            raise UserFacingException(\"None of the given privkeys can be imported\")\n    else:\n        if keystore.is_master_key(text):\n            k = keystore.from_master_key(text)\n        elif keystore.is_seed(text):\n            k = keystore.from_seed(text, passphrase=passphrase)\n            if k.can_have_deterministic_lightning_xprv():\n                db.put('lightning_xprv', k.get_lightning_xprv(None))\n        else:\n            raise UserFacingException(\"Seed or key not recognized\")\n        db.put('keystore', k.dump())\n        db.put('wallet_type', 'standard')\n        if gap_limit is not None:\n            db.put('gap_limit', gap_limit)\n        if gap_limit_for_change is not None:\n            db.put('gap_limit_for_change', gap_limit_for_change)\n        wallet = wallet_factory(db, config=config)\n    if db.storage:\n        assert not db.storage.file_exists(), \"file was created too soon! plaintext keys might have been written to disk\"\n    wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)\n    wallet.synchronize()\n    msg = (\"This wallet was restored offline. It may contain more addresses than displayed. \"\n           \"Start a daemon and use load_wallet to sync its history.\")\n    wallet.save_db()\n    return {'wallet': wallet, 'msg': msg}\n"
  },
  {
    "path": "electrum/wallet_db.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2015 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport datetime\nimport json\nimport copy\nfrom collections import defaultdict\nfrom typing import (Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Sequence, TYPE_CHECKING,\n                    Union, AbstractSet)\nimport time\nfrom functools import partial\n\nimport attr\n\nfrom . import bitcoin\nfrom .util import profiler, WalletFileException, multisig_type, TxMinedInfo, MyEncoder\nfrom .keystore import bip44_derivation\nfrom .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput, BadHeaderMagic\nfrom .logging import Logger\n\nfrom .lnutil import HTLCOwner, ChannelType, RecvMPPResolution\nfrom . import json_db\nfrom .json_db import JsonDB, locked, modifier, StoredObject, stored_in, stored_as\nfrom .plugin import run_hook, plugin_loaders\nfrom .version import ELECTRUM_VERSION\nfrom .i18n import _\n\nif TYPE_CHECKING:\n    from .storage import WalletStorage\n\n\nclass WalletRequiresUpgrade(WalletFileException):\n    pass\n\n\nclass WalletRequiresSplit(WalletFileException):\n    def __init__(self, split_data):\n        super().__init__()\n        self._split_data = split_data\n\n\nclass WalletUnfinished(WalletFileException):\n    def __init__(self, wallet_db: 'WalletDB'):\n        super().__init__()\n        self._wallet_db = wallet_db\n\n\n# seed_version is now used for the version of the wallet file\nOLD_SEED_VERSION = 4        # electrum versions < 2.0\nNEW_SEED_VERSION = 11       # electrum versions >= 2.0\nFINAL_SEED_VERSION = 70     # electrum >= 2.7 will set this to prevent\n                            # old versions from overwriting new format\n\n\n@stored_in('tx_fees', tuple)\nclass TxFeesValue(NamedTuple):\n    fee: Optional[int] = None\n    is_calculated_by_us: bool = False\n    num_inputs: Optional[int] = None\n\n\n@stored_as('db_metadata')\n@attr.s\nclass DBMetadata(StoredObject):\n    creation_timestamp = attr.ib(default=None, type=int)\n    first_electrum_version_used = attr.ib(default=None, type=str)\n\n    def to_str(self) -> str:\n        ts = self.creation_timestamp\n        ver = self.first_electrum_version_used\n        if ts is None or ver is None:\n            return \"unknown\"\n        date_str = datetime.date.fromtimestamp(ts).isoformat()\n        return f\"using {ver}, on {date_str}\"\n\n\n# note: subclassing WalletFileException for some specific cases\n#       allows the crash reporter to distinguish them and open\n#       separate tracking issues\nclass WalletFileExceptionVersion51(WalletFileException): pass\n\n\n# register dicts that require value conversions not handled by constructor\njson_db.register_dict('transactions', lambda x: tx_from_any(x, deserialize=False), None)\njson_db.register_dict('data_loss_protect_remote_pcp', lambda x: bytes.fromhex(x), None)\njson_db.register_dict('contacts', tuple, None)\n# register dicts that require key conversion\nfor key in [\n        'adds', 'locked_in', 'settles', 'fails', 'fee_updates', 'buckets',\n        'unacked_updates', 'unfulfilled_htlcs', 'onion_keys']:\n    json_db.register_dict_key(key, int)\nfor key in ['log']:\n    json_db.register_dict_key(key, lambda x: HTLCOwner(int(x)))\nfor key in ['locked_in', 'fails', 'settles']:\n    json_db.register_parent_key(key, lambda x: HTLCOwner(int(x)))\n\n\nclass WalletDBUpgrader(Logger):\n    def __init__(self, data: dict):\n        Logger.__init__(self)\n        self.data = data\n        # self.data must be in-memory dict (not a StoredDict or similar),\n        # so a failed, partial upgrade won't get commited to disk\n        assert type(self.data) == dict, type(self.data)\n\n    def get(self, key, default=None):\n        return self.data.get(key, default)\n\n    def put(self, key, value):\n        if value is not None:\n            self.data[key] = value\n        else:\n            self.data.pop(key, None)\n\n    def requires_split(self):\n        d = self.get('accounts', {})\n        return len(d) > 1\n\n    def get_split_accounts(self):\n        result = []\n        # backward compatibility with old wallets\n        d = self.get('accounts', {})\n        if len(d) < 2:\n            return\n        wallet_type = self.get('wallet_type')\n        if wallet_type == 'old':\n            assert len(d) == 2\n            data1 = copy.deepcopy(self.data)\n            data1['accounts'] = {'0': d['0']}\n            data1['suffix'] = 'deterministic'\n            data2 = copy.deepcopy(self.data)\n            data2['accounts'] = {'/x': d['/x']}\n            data2['seed'] = None\n            data2['seed_version'] = None\n            data2['master_public_key'] = None\n            data2['wallet_type'] = 'imported'\n            data2['suffix'] = 'imported'\n            result = [data1, data2]\n\n        # note: do not add new hardware types here, this code is for converting legacy wallets\n        elif wallet_type in ['bip44', 'trezor', 'keepkey', 'ledger', 'btchip']:\n            mpk = self.get('master_public_keys')\n            for k in d.keys():\n                i = int(k)\n                x = d[k]\n                if x.get(\"pending\"):\n                    continue\n                xpub = mpk[\"x/%d'\"%i]\n                new_data = copy.deepcopy(self.data)\n                # save account, derivation and xpub at index 0\n                new_data['accounts'] = {'0': x}\n                new_data['master_public_keys'] = {\"x/0'\": xpub}\n                new_data['derivation'] = bip44_derivation(k)\n                new_data['suffix'] = k\n                result.append(new_data)\n        else:\n            raise WalletFileException(f'Unsupported wallet type for split: {wallet_type}')\n        return result\n\n    def requires_upgrade(self):\n        return self.get_seed_version() < FINAL_SEED_VERSION\n\n    @profiler\n    def upgrade(self):\n        self.logger.info('upgrading wallet format')\n        self._convert_imported()\n        self._convert_wallet_type()\n        self._convert_account()\n        self._convert_version_13_b()\n        self._convert_version_14()\n        self._convert_version_15()\n        self._convert_version_16()\n        self._convert_version_17()\n        self._convert_version_18()\n        self._convert_version_19()\n        self._convert_version_20()\n        self._convert_version_21()\n        self._convert_version_22()\n        self._convert_version_23()\n        self._convert_version_24()\n        self._convert_version_25()\n        self._convert_version_26()\n        self._convert_version_27()\n        self._convert_version_28()\n        self._convert_version_29()\n        self._convert_version_30()\n        self._convert_version_31()\n        self._convert_version_32()\n        self._convert_version_33()\n        self._convert_version_34()\n        self._convert_version_35()\n        self._convert_version_36()\n        self._convert_version_37()\n        self._convert_version_38()\n        self._convert_version_39()\n        self._convert_version_40()\n        self._convert_version_41()\n        self._convert_version_42()\n        self._convert_version_43()\n        self._convert_version_44()\n        self._convert_version_45()\n        self._convert_version_46()\n        self._convert_version_47()\n        self._convert_version_48()\n        self._convert_version_49()\n        self._convert_version_50()\n        self._convert_version_51()\n        self._convert_version_52()\n        self._convert_version_53()\n        self._convert_version_54()\n        self._convert_version_55()\n        self._convert_version_56()\n        self._convert_version_57()\n        self._convert_version_58()\n        self._convert_version_59()\n        self._convert_version_60()\n        self._convert_version_61()\n        self._convert_version_62()\n        self._convert_version_63()\n        self._convert_version_64()\n        self._convert_version_65()\n        self._convert_version_66()\n        self._convert_version_67()\n        self._convert_version_68()\n        self._convert_version_69()\n        self._convert_version_70()\n        self.put('seed_version', FINAL_SEED_VERSION)  # just to be sure\n\n    def _convert_wallet_type(self):\n        if not self._is_upgrade_method_needed(0, 13):\n            return\n\n        wallet_type = self.get('wallet_type')\n        if wallet_type == 'btchip': wallet_type = 'ledger'\n        if self.get('keystore') or self.get('x1/') or wallet_type=='imported':\n            return False\n        assert not self.requires_split()\n        seed_version = self.get_seed_version()\n        seed = self.get('seed')\n        xpubs = self.get('master_public_keys')\n        xprvs = self.get('master_private_keys', {})\n        mpk = self.get('master_public_key')\n        keypairs = self.get('keypairs')\n        key_type = self.get('key_type')\n        if seed_version == OLD_SEED_VERSION or wallet_type == 'old':\n            d = {\n                'type': 'old',\n                'seed': seed,\n                'mpk': mpk,\n            }\n            self.put('wallet_type', 'standard')\n            self.put('keystore', d)\n\n        elif key_type == 'imported':\n            d = {\n                'type': 'imported',\n                'keypairs': keypairs,\n            }\n            self.put('wallet_type', 'standard')\n            self.put('keystore', d)\n\n        elif wallet_type in ['xpub', 'standard']:\n            xpub = xpubs[\"x/\"]\n            xprv = xprvs.get(\"x/\")\n            d = {\n                'type': 'bip32',\n                'xpub': xpub,\n                'xprv': xprv,\n                'seed': seed,\n            }\n            self.put('wallet_type', 'standard')\n            self.put('keystore', d)\n\n        elif wallet_type in ['bip44']:\n            xpub = xpubs[\"x/0'\"]\n            xprv = xprvs.get(\"x/0'\")\n            d = {\n                'type': 'bip32',\n                'xpub': xpub,\n                'xprv': xprv,\n            }\n            self.put('wallet_type', 'standard')\n            self.put('keystore', d)\n\n        # note: do not add new hardware types here, this code is for converting legacy wallets\n        elif wallet_type in ['trezor', 'keepkey', 'ledger']:\n            xpub = xpubs[\"x/0'\"]\n            derivation = self.get('derivation', bip44_derivation(0))\n            d = {\n                'type': 'hardware',\n                'hw_type': wallet_type,\n                'xpub': xpub,\n                'derivation': derivation,\n            }\n            self.put('wallet_type', 'standard')\n            self.put('keystore', d)\n\n        elif (wallet_type == '2fa') or multisig_type(wallet_type):\n            for key in xpubs.keys():\n                d = {\n                    'type': 'bip32',\n                    'xpub': xpubs[key],\n                    'xprv': xprvs.get(key),\n                }\n                if key == 'x1/' and seed:\n                    d['seed'] = seed\n                self.put(key, d)\n        else:\n            raise WalletFileException('Unable to tell wallet type. Is this even a wallet file?')\n        # remove junk\n        self.put('master_public_key', None)\n        self.put('master_public_keys', None)\n        self.put('master_private_keys', None)\n        self.put('derivation', None)\n        self.put('seed', None)\n        self.put('keypairs', None)\n        self.put('key_type', None)\n\n    def _convert_version_13_b(self):\n        # version 13 is ambiguous, and has an earlier and a later structure\n        if not self._is_upgrade_method_needed(0, 13):\n            return\n\n        if self.get('wallet_type') == 'standard':\n            if self.get('keystore').get('type') == 'imported':\n                pubkeys = self.get('keystore').get('keypairs').keys()\n                d = {'change': []}\n                receiving_addresses = []\n                for pubkey in pubkeys:\n                    addr = bitcoin.pubkey_to_address('p2pkh', pubkey)\n                    receiving_addresses.append(addr)\n                d['receiving'] = receiving_addresses\n                self.put('addresses', d)\n                self.put('pubkeys', None)\n\n        self.put('seed_version', 13)\n\n    def _convert_version_14(self):\n        # convert imported wallets for 3.0\n        if not self._is_upgrade_method_needed(13, 13):\n            return\n\n        if self.get('wallet_type') =='imported':\n            addresses = self.get('addresses')\n            if type(addresses) is list:\n                addresses = dict([(x, None) for x in addresses])\n                self.put('addresses', addresses)\n        elif self.get('wallet_type') == 'standard':\n            if self.get('keystore').get('type')=='imported':\n                addresses = set(self.get('addresses').get('receiving'))\n                pubkeys = self.get('keystore').get('keypairs').keys()\n                assert len(addresses) == len(pubkeys)\n                d = {}\n                for pubkey in pubkeys:\n                    addr = bitcoin.pubkey_to_address('p2pkh', pubkey)\n                    assert addr in addresses\n                    d[addr] = {\n                        'pubkey': pubkey,\n                        'redeem_script': None,\n                        'type': 'p2pkh'\n                    }\n                self.put('addresses', d)\n                self.put('pubkeys', None)\n                self.put('wallet_type', 'imported')\n        self.put('seed_version', 14)\n\n    def _convert_version_15(self):\n        if not self._is_upgrade_method_needed(14, 14):\n            return\n        if self.get('seed_type') == 'segwit':\n            # should not get here; get_seed_version should have caught this\n            raise Exception('unsupported derivation (development segwit, v14)')\n        self.put('seed_version', 15)\n\n    def _convert_version_16(self):\n        # fixes issue #3193 for Imported_Wallets with addresses\n        # also, previous versions allowed importing any garbage as an address\n        #       which we now try to remove, see pr #3191\n        if not self._is_upgrade_method_needed(15, 15):\n            return\n\n        def remove_address(addr):\n            def remove_from_dict(dict_name):\n                d = self.get(dict_name, None)\n                if d is not None:\n                    d.pop(addr, None)\n                    self.put(dict_name, d)\n\n            def remove_from_list(list_name):\n                lst = self.get(list_name, None)\n                if lst is not None:\n                    s = set(lst)\n                    s -= {addr}\n                    self.put(list_name, list(s))\n\n            # note: we don't remove 'addr' from self.get('addresses')\n            remove_from_dict('addr_history')\n            remove_from_dict('labels')\n            remove_from_dict('payment_requests')\n            remove_from_list('frozen_addresses')\n\n        if self.get('wallet_type') == 'imported':\n            addresses = self.get('addresses')\n            assert isinstance(addresses, dict)\n            addresses_new = dict()\n            for address, details in addresses.items():\n                if not bitcoin.is_address(address):\n                    remove_address(address)\n                    continue\n                if details is None:\n                    addresses_new[address] = {}\n                else:\n                    addresses_new[address] = details\n            self.put('addresses', addresses_new)\n\n        self.put('seed_version', 16)\n\n    def _convert_version_17(self):\n        # delete pruned_txo; construct spent_outpoints\n        if not self._is_upgrade_method_needed(16, 16):\n            return\n\n        self.put('pruned_txo', None)\n\n        transactions = self.get('transactions', {})  # txid -> raw_tx\n        spent_outpoints = defaultdict(dict)\n        for txid, raw_tx in transactions.items():\n            tx = Transaction(raw_tx)\n            for txin in tx.inputs():\n                if txin.is_coinbase_input():\n                    continue\n                prevout_hash = txin.prevout.txid.hex()\n                prevout_n = txin.prevout.out_idx\n                spent_outpoints[prevout_hash][str(prevout_n)] = txid\n        self.put('spent_outpoints', spent_outpoints)\n\n        self.put('seed_version', 17)\n\n    def _convert_version_18(self):\n        # delete verified_tx3 as its structure changed\n        if not self._is_upgrade_method_needed(17, 17):\n            return\n        self.put('verified_tx3', None)\n        self.put('seed_version', 18)\n\n    def _convert_version_19(self):\n        # delete tx_fees as its structure changed\n        if not self._is_upgrade_method_needed(18, 18):\n            return\n        self.put('tx_fees', None)\n        self.put('seed_version', 19)\n\n    def _convert_version_20(self):\n        # store 'derivation' (prefix) and 'root_fingerprint' in all xpub-based keystores.\n        # store explicit None values if we cannot retroactively determine them\n        if not self._is_upgrade_method_needed(19, 19):\n            return\n\n        from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath\n        # note: This upgrade method reimplements bip32.root_fp_and_der_prefix_from_xkey.\n        #       This is done deliberately, to avoid introducing that method as a dependency to this upgrade.\n        for ks_name in ('keystore', *['x{}/'.format(i) for i in range(1, 16)]):\n            ks = self.get(ks_name, None)\n            if ks is None: continue\n            xpub = ks.get('xpub', None)\n            if xpub is None: continue\n            bip32node = BIP32Node.from_xkey(xpub)\n            # derivation prefix\n            derivation_prefix = ks.get('derivation', None)\n            if derivation_prefix is None:\n                assert bip32node.depth >= 0, bip32node.depth\n                if bip32node.depth == 0:\n                    derivation_prefix = 'm'\n                elif bip32node.depth == 1:\n                    child_number_int = int.from_bytes(bip32node.child_number, 'big')\n                    derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int])\n                ks['derivation'] = derivation_prefix\n            # root fingerprint\n            root_fingerprint = ks.get('ckcc_xfp', None)\n            if root_fingerprint is not None:\n                root_fingerprint = root_fingerprint.to_bytes(4, byteorder=\"little\", signed=False).hex().lower()\n            if root_fingerprint is None:\n                if bip32node.depth == 0:\n                    root_fingerprint = bip32node.calc_fingerprint_of_this_node().hex().lower()\n                elif bip32node.depth == 1:\n                    root_fingerprint = bip32node.fingerprint.hex()\n            ks['root_fingerprint'] = root_fingerprint\n            ks.pop('ckcc_xfp', None)\n            self.put(ks_name, ks)\n\n        self.put('seed_version', 20)\n\n    def _convert_version_21(self):\n        if not self._is_upgrade_method_needed(20, 20):\n            return\n        channels = self.get('channels')\n        if channels:\n            for channel in channels:\n                channel['state'] = 'OPENING'\n            self.put('channels', channels)\n        self.put('seed_version', 21)\n\n    def _convert_version_22(self):\n        # construct prevouts_by_scripthash\n        if not self._is_upgrade_method_needed(21, 21):\n            return\n\n        from .bitcoin import script_to_scripthash\n        transactions = self.get('transactions', {})  # txid -> raw_tx\n        prevouts_by_scripthash = defaultdict(list)\n        for txid, raw_tx in transactions.items():\n            tx = Transaction(raw_tx)\n            for idx, txout in enumerate(tx.outputs()):\n                outpoint = f\"{txid}:{idx}\"\n                scripthash = script_to_scripthash(txout.scriptpubkey)\n                prevouts_by_scripthash[scripthash].append((outpoint, txout.value))\n        self.put('prevouts_by_scripthash', prevouts_by_scripthash)\n\n        self.put('seed_version', 22)\n\n    def _convert_version_23(self):\n        if not self._is_upgrade_method_needed(22, 22):\n            return\n        channels = self.get('channels', [])\n        LOCAL = 1\n        REMOTE = -1\n        for c in channels:\n            # move revocation store from remote_config\n            r = c['remote_config'].pop('revocation_store')\n            c['revocation_store'] = r\n            # convert fee updates\n            log = c.get('log', {})\n            for sub in LOCAL, REMOTE:\n                l = log[str(sub)]['fee_updates']\n                d = {}\n                for i, fu in enumerate(l):\n                    d[str(i)] = {\n                        'rate':fu['rate'],\n                        'ctn_local':fu['ctns'][str(LOCAL)],\n                        'ctn_remote':fu['ctns'][str(REMOTE)]\n                    }\n                log[str(int(sub))]['fee_updates'] = d\n        self.data['channels'] = channels\n\n        self.data['seed_version'] = 23\n\n    def _convert_version_24(self):\n        if not self._is_upgrade_method_needed(23, 23):\n            return\n        channels = self.get('channels', [])\n        for c in channels:\n            # convert revocation store to dict\n            r = c['revocation_store']\n            d = {}\n            for i in range(49):\n                v = r['buckets'][i]\n                if v is not None:\n                    d[str(i)] = v\n            r['buckets'] = d\n            c['revocation_store'] = r\n        # convert channels to dict\n        self.data['channels'] = {x['channel_id']: x for x in channels}\n        # convert txi & txo\n        txi = self.get('txi', {})\n        for tx_hash, d in list(txi.items()):\n            d2 = {}\n            for addr, l in d.items():\n                d2[addr] = {}\n                for ser, v in l:\n                    d2[addr][ser] = v\n            txi[tx_hash] = d2\n        self.data['txi'] = txi\n        txo = self.get('txo', {})\n        for tx_hash, d in list(txo.items()):\n            d2 = {}\n            for addr, l in d.items():\n                d2[addr] = {}\n                for n, v, cb in l:\n                    d2[addr][str(n)] = (v, cb)\n            txo[tx_hash] = d2\n        self.data['txo'] = txo\n\n        self.data['seed_version'] = 24\n\n    def _convert_version_25(self):\n        from .crypto import sha256\n        if not self._is_upgrade_method_needed(24, 24):\n            return\n        # add 'type' field to onchain requests\n        PR_TYPE_ONCHAIN = 0\n        requests = self.data.get('payment_requests', {})\n        for k, r in list(requests.items()):\n            if r.get('address') == k:\n                requests[k] = {\n                    'address': r['address'],\n                    'amount': r.get('amount'),\n                    'exp': r.get('exp'),\n                    'id': r.get('id'),\n                    'memo': r.get('memo'),\n                    'time': r.get('time'),\n                    'type': PR_TYPE_ONCHAIN,\n                }\n        # delete bip70 invoices\n        # note: this upgrade was changed ~2 years after-the-fact to delete instead of converting\n        invoices = self.data.get('invoices', {})\n        for k, r in list(invoices.items()):\n            data = r.get(\"hex\")\n            pr_id = sha256(bytes.fromhex(data))[0:16].hex()\n            if pr_id != k:\n                continue\n            del invoices[k]\n        self.data['seed_version'] = 25\n\n    def _convert_version_26(self):\n        if not self._is_upgrade_method_needed(25, 25):\n            return\n        channels = self.data.get('channels', {})\n        channel_timestamps = self.data.pop('lightning_channel_timestamps', {})\n        for channel_id, c in channels.items():\n            item = channel_timestamps.get(channel_id)\n            if item:\n                funding_txid, funding_height, funding_timestamp, closing_txid, closing_height, closing_timestamp = item\n                if funding_txid:\n                    c['funding_height'] = funding_txid, funding_height, funding_timestamp\n                if closing_txid:\n                    c['closing_height'] = closing_txid, closing_height, closing_timestamp\n        self.data['seed_version'] = 26\n\n    def _convert_version_27(self):\n        if not self._is_upgrade_method_needed(26, 26):\n            return\n        channels = self.data.get('channels', {})\n        for channel_id, c in channels.items():\n            c['local_config']['htlc_minimum_msat'] = 1\n        self.data['seed_version'] = 27\n\n    def _convert_version_28(self):\n        if not self._is_upgrade_method_needed(27, 27):\n            return\n        channels = self.data.get('channels', {})\n        for channel_id, c in channels.items():\n            c['local_config']['channel_seed'] = None\n        self.data['seed_version'] = 28\n\n    def _convert_version_29(self):\n        if not self._is_upgrade_method_needed(28, 28):\n            return\n        PR_TYPE_ONCHAIN = 0\n        requests = self.data.get('payment_requests', {})\n        invoices = self.data.get('invoices', {})\n        for d in [invoices, requests]:\n            for key, r in list(d.items()):\n                _type = r.get('type', 0)\n                item = {\n                    'type': _type,\n                    'message': r.get('message') or r.get('memo', ''),\n                    'amount': r.get('amount'),\n                    'exp': r.get('exp') or 0,\n                    'time': r.get('time', 0),\n                }\n                if _type == PR_TYPE_ONCHAIN:\n                    address = r.pop('address', None)\n                    if address:\n                        outputs = [(0, address, r.get('amount'))]\n                    else:\n                        outputs = r.get('outputs')\n                    item.update({\n                        'outputs': outputs,\n                        'id': r.get('id'),\n                        'bip70': r.get('bip70'),\n                        'requestor': r.get('requestor'),\n                    })\n                else:\n                    item.update({\n                        'rhash': r['rhash'],\n                        'invoice': r['invoice'],\n                    })\n                d[key] = item\n        self.data['seed_version'] = 29\n\n    def _convert_version_30(self):\n        if not self._is_upgrade_method_needed(29, 29):\n            return\n        PR_TYPE_ONCHAIN = 0\n        PR_TYPE_LN = 2\n        requests = self.data.get('payment_requests', {})\n        invoices = self.data.get('invoices', {})\n        for d in [invoices, requests]:\n            for key, item in list(d.items()):\n                _type = item['type']\n                if _type == PR_TYPE_ONCHAIN:\n                    item['amount_sat'] = item.pop('amount')\n                elif _type == PR_TYPE_LN:\n                    amount_sat = item.pop('amount')\n                    item['amount_msat'] = 1000 * amount_sat if amount_sat is not None else None\n                    item.pop('exp')\n                    item.pop('message')\n                    item.pop('rhash')\n                    item.pop('time')\n                else:\n                    raise Exception(f\"unknown invoice type: {_type}\")\n        self.data['seed_version'] = 30\n\n    def _convert_version_31(self):\n        if not self._is_upgrade_method_needed(30, 30):\n            return\n        PR_TYPE_ONCHAIN = 0\n        requests = self.data.get('payment_requests', {})\n        invoices = self.data.get('invoices', {})\n        for d in [invoices, requests]:\n            for key, item in list(d.items()):\n                if item['type'] == PR_TYPE_ONCHAIN:\n                    item['amount_sat'] = item['amount_sat'] or 0\n                    item['exp'] = item['exp'] or 0\n                    item['time'] = item['time'] or 0\n        self.data['seed_version'] = 31\n\n    def _convert_version_32(self):\n        if not self._is_upgrade_method_needed(31, 31):\n            return\n        PR_TYPE_ONCHAIN = 0\n        invoices_old = self.data.get('invoices', {})\n        invoices_new = {k: item for k, item in invoices_old.items()\n                        if not (item['type'] == PR_TYPE_ONCHAIN and item['outputs'] is None)}\n        self.data['invoices'] = invoices_new\n        self.data['seed_version'] = 32\n\n    def _convert_version_33(self):\n        if not self._is_upgrade_method_needed(32, 32):\n            return\n        PR_TYPE_ONCHAIN = 0\n        requests = self.data.get('payment_requests', {})\n        invoices = self.data.get('invoices', {})\n        for d in [invoices, requests]:\n            for key, item in list(d.items()):\n                if item['type'] == PR_TYPE_ONCHAIN:\n                    item['height'] = item.get('height') or 0\n        self.data['seed_version'] = 33\n\n    def _convert_version_34(self):\n        if not self._is_upgrade_method_needed(33, 33):\n            return\n        channels = self.data.get('channels', {})\n        for key, item in channels.items():\n            item['local_config']['upfront_shutdown_script'] = \\\n                item['local_config'].get('upfront_shutdown_script') or \"\"\n            item['remote_config']['upfront_shutdown_script'] = \\\n                item['remote_config'].get('upfront_shutdown_script') or \"\"\n        self.data['seed_version'] = 34\n\n    def _convert_version_35(self):\n        # same as 32, but for payment_requests\n        if not self._is_upgrade_method_needed(34, 34):\n            return\n        PR_TYPE_ONCHAIN = 0\n        requests_old = self.data.get('payment_requests', {})\n        requests_new = {k: item for k, item in requests_old.items()\n                        if not (item['type'] == PR_TYPE_ONCHAIN and item['outputs'] is None)}\n        self.data['payment_requests'] = requests_new\n        self.data['seed_version'] = 35\n\n    def _convert_version_36(self):\n        if not self._is_upgrade_method_needed(35, 35):\n            return\n        old_frozen_coins = self.data.get('frozen_coins', [])\n        new_frozen_coins = {coin: True for coin in old_frozen_coins}\n        self.data['frozen_coins'] = new_frozen_coins\n        self.data['seed_version'] = 36\n\n    def _convert_version_37(self):\n        if not self._is_upgrade_method_needed(36, 36):\n            return\n        payments = self.data.get('lightning_payments', {})\n        for k, v in list(payments.items()):\n            amount_sat, direction, status = v\n            amount_msat = amount_sat * 1000 if amount_sat is not None else None\n            payments[k] = amount_msat, direction, status\n        self.data['lightning_payments'] = payments\n        self.data['seed_version'] = 37\n\n    def _convert_version_38(self):\n        if not self._is_upgrade_method_needed(37, 37):\n            return\n        PR_TYPE_ONCHAIN = 0\n        PR_TYPE_LN = 2\n        from .bitcoin import TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN\n        max_sats = TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN\n        requests = self.data.get('payment_requests', {})\n        invoices = self.data.get('invoices', {})\n        for d in [invoices, requests]:\n            for key, item in list(d.items()):\n                if item['type'] == PR_TYPE_ONCHAIN:\n                    amount_sat = item['amount_sat']\n                    if amount_sat == '!':\n                        continue\n                    if not (isinstance(amount_sat, int) and 0 <= amount_sat <= max_sats):\n                        del d[key]\n                elif item['type'] == PR_TYPE_LN:\n                    amount_msat = item['amount_msat']\n                    if not amount_msat:\n                        continue\n                    if not (isinstance(amount_msat, int) and 0 <= amount_msat <= max_sats * 1000):\n                        del d[key]\n        self.data['seed_version'] = 38\n\n    def _convert_version_39(self):\n        # this upgrade prevents initialization of lightning_privkey2 after lightning_xprv has been set\n        if not self._is_upgrade_method_needed(38, 38):\n            return\n        self.data['imported_channel_backups'] = self.data.pop('channel_backups', {})\n        self.data['seed_version'] = 39\n\n    def _convert_version_40(self):\n        # put 'seed_type' into keystores\n        if not self._is_upgrade_method_needed(39, 39):\n            return\n        for ks_name in ('keystore', *['x{}/'.format(i) for i in range(1, 16)]):\n            ks = self.data.get(ks_name, None)\n            if ks is None: continue\n            seed = ks.get('seed')\n            if not seed: continue\n            seed_type = None\n            xpub = ks.get('xpub') or None\n            if xpub:\n                assert isinstance(xpub, str)\n                if xpub[0:4] in ('xpub', 'tpub'):\n                    seed_type = 'standard'\n                elif xpub[0:4] in ('zpub', 'Zpub', 'vpub', 'Vpub'):\n                    seed_type = 'segwit'\n            elif ks.get('type') == 'old':\n                seed_type = 'old'\n            if seed_type is not None:\n                ks['seed_type'] = seed_type\n        self.data['seed_version'] = 40\n\n    def _convert_version_41(self):\n        # this is a repeat of upgrade 39, to fix wallet backup files (see #7339)\n        if not self._is_upgrade_method_needed(40, 40):\n            return\n        imported_channel_backups = self.data.pop('channel_backups', {})\n        imported_channel_backups.update(self.data.get('imported_channel_backups', {}))\n        self.data['imported_channel_backups'] = imported_channel_backups\n        self.data['seed_version'] = 41\n\n    def _convert_version_42(self):\n        # in OnchainInvoice['outputs'], convert values from None to 0\n        if not self._is_upgrade_method_needed(41, 41):\n            return\n        PR_TYPE_ONCHAIN = 0\n        requests = self.data.get('payment_requests', {})\n        invoices = self.data.get('invoices', {})\n        for d in [invoices, requests]:\n            for key, item in list(d.items()):\n                if item['type'] == PR_TYPE_ONCHAIN:\n                    item['outputs'] = [(_type, addr, (val or 0))\n                                       for _type, addr, val in item['outputs']]\n        self.data['seed_version'] = 42\n\n    def _convert_version_43(self):\n        if not self._is_upgrade_method_needed(42, 42):\n            return\n        channels = self.data.pop('channels', {})\n        for k, c in channels.items():\n            log = c['log']\n            c['fail_htlc_reasons'] = log.pop('fail_htlc_reasons', {})\n            c['unfulfilled_htlcs'] = log.pop('unfulfilled_htlcs', {})\n            log[\"1\"]['unacked_updates'] = log.pop('unacked_local_updates2', {})\n        self.data['channels'] = channels\n        self.data['seed_version'] = 43\n\n    def _convert_version_44(self):\n        if not self._is_upgrade_method_needed(43, 43):\n            return\n        channels = self.data.get('channels', {})\n        for key, item in channels.items():\n            if bool(item.get('static_remotekey_enabled')):\n                channel_type = ChannelType.OPTION_STATIC_REMOTEKEY\n            else:\n                channel_type = ChannelType(0)\n            item.pop('static_remotekey_enabled', None)\n            item['channel_type'] = channel_type\n        self.data['seed_version'] = 44\n\n    def _convert_version_45(self):\n        from .lnaddr import lndecode\n        if not self._is_upgrade_method_needed(44, 44):\n            return\n        swaps = self.data.get('submarine_swaps', {})\n        for key, item in swaps.items():\n            item['receive_address'] = None\n        # note: we set height to zero\n        # the new key for all requests is a wallet address, not done here\n        for name in ['invoices', 'payment_requests']:\n            invoices = self.data.get(name, {})\n            for key, item in invoices.items():\n                is_lightning = item['type'] == 2\n                lightning_invoice = item['invoice'] if is_lightning else None\n                outputs = item['outputs'] if not is_lightning else None\n                bip70 = item['bip70'] if not is_lightning else None\n                if is_lightning:\n                    lnaddr = lndecode(item['invoice'])\n                    amount_msat = lnaddr.get_amount_msat()\n                    timestamp = lnaddr.date\n                    exp_delay = lnaddr.get_expiry()\n                    message = lnaddr.get_description()\n                    height = 0\n                else:\n                    amount_sat = item['amount_sat']\n                    amount_msat = amount_sat * 1000 if amount_sat not in [None, '!'] else amount_sat\n                    message = item['message']\n                    timestamp = item['time']\n                    exp_delay = item['exp']\n                    height = item['height']\n\n                invoices[key] = {\n                    'amount_msat':amount_msat,\n                    'message':message,\n                    'time':timestamp,\n                    'exp':exp_delay,\n                    'height':height,\n                    'outputs':outputs,\n                    'bip70':bip70,\n                    'lightning_invoice':lightning_invoice,\n                }\n        self.data['seed_version'] = 45\n\n    def _convert_invoices_keys(self, invoices):\n        # recalc keys of outgoing on-chain invoices\n        from .crypto import sha256d\n        def get_id_from_onchain_outputs(raw_outputs, timestamp):\n            outputs = [PartialTxOutput.from_legacy_tuple(*output) for output in raw_outputs]\n            outputs_str = \"\\n\".join(f\"{txout.scriptpubkey.hex()}, {txout.value}\" for txout in outputs)\n            return sha256d(outputs_str + \"%d\" % timestamp).hex()[0:10]\n        for key, item in list(invoices.items()):\n            is_lightning = item['lightning_invoice'] is not None\n            if is_lightning:\n                continue\n            outputs_raw = item['outputs']\n            assert outputs_raw, outputs_raw\n            timestamp = item['time']\n            newkey = get_id_from_onchain_outputs(outputs_raw, timestamp)\n            if newkey != key:\n                invoices[newkey] = item\n                del invoices[key]\n\n    def _convert_version_46(self):\n        if not self._is_upgrade_method_needed(45, 45):\n            return\n        invoices = self.data.get('invoices', {})\n        self._convert_invoices_keys(invoices)\n        self.data['seed_version'] = 46\n\n    def _convert_version_47(self):\n        from .lnaddr import lndecode\n        if not self._is_upgrade_method_needed(46, 46):\n            return\n        # recalc keys of requests\n        requests = self.data.get('payment_requests', {})\n        for key, item in list(requests.items()):\n            lnaddr = item.get('lightning_invoice')\n            if lnaddr:\n                lnaddr = lndecode(lnaddr)\n                rhash = lnaddr.paymenthash.hex()\n                if key != rhash:\n                    requests[rhash] = item\n                    del requests[key]\n        self.data['seed_version'] = 47\n\n    def _convert_version_48(self):\n        # fix possible corruption of invoice amounts, see #7774\n        if not self._is_upgrade_method_needed(47, 47):\n            return\n        invoices = self.data.get('invoices', {})\n        for key, item in list(invoices.items()):\n            if item['amount_msat'] == 1000 * \"!\":\n                item['amount_msat'] = \"!\"\n        self.data['seed_version'] = 48\n\n    def _convert_version_49(self):\n        if not self._is_upgrade_method_needed(48, 48):\n            return\n        channels = self.data.get('channels', {})\n        legacy_chans = [chan_dict for chan_dict in channels.values()\n                        if chan_dict['channel_type'] == ChannelType.OPTION_LEGACY_CHANNEL]\n        if legacy_chans:\n            raise WalletFileException(\n                f\"This wallet contains {len(legacy_chans)} lightning channels of type 'LEGACY'. \"\n                f\"These channels were created using unreleased development versions of Electrum \"\n                f\"before the first lightning-capable release of 4.0, and are not supported anymore. \"\n                f\"Please use Electrum 4.3.0 to open this wallet, close the channels, \"\n                f\"and delete them from the wallet.\"\n            )\n        self.data['seed_version'] = 49\n\n    def _convert_version_50(self):\n        if not self._is_upgrade_method_needed(49, 49):\n            return\n        requests = self.data.get('payment_requests', {})\n        self._convert_invoices_keys(requests)\n        self.data['seed_version'] = 50\n\n    def _convert_version_51(self):\n        from .lnaddr import lndecode\n        if not self._is_upgrade_method_needed(50, 50):\n            return\n        requests = self.data.get('payment_requests', {})\n        for key, item in list(requests.items()):\n            lightning_invoice = item.pop('lightning_invoice')\n            if lightning_invoice is None:\n                payment_hash = None\n            else:\n                lnaddr = lndecode(lightning_invoice)\n                payment_hash = lnaddr.paymenthash.hex()\n            item['payment_hash'] = payment_hash\n        self.data['seed_version'] = 51\n\n    def _detect_insane_version_51(self) -> int:\n        \"\"\"Returns 0 if file okay,\n        error code 1: multisig wallet has old_mpk\n        error code 2: multisig wallet has mixed Ypub/Zpub\n        \"\"\"\n        assert self.get('seed_version') == 51\n        xpub_type = None\n        for ks_name in ['x{}/'.format(i) for i in range(1, 16)]:  # having any such field <=> multisig wallet\n            ks = self.data.get(ks_name, None)\n            if ks is None: continue\n            ks_type = ks.get('type')\n            if ks_type == \"old\":\n                return 1  # error\n            assert ks_type in (\"bip32\", \"hardware\"), f\"unexpected {ks_type=}\"\n            xpub = ks.get('xpub') or None\n            assert xpub is not None\n            assert isinstance(xpub, str)\n            if xpub_type is None:  # first iter\n                xpub_type = xpub[0:4]\n            if xpub[0:4] != xpub_type:\n                return 2  # error\n        # looks okay\n        return 0\n\n    def _convert_version_52(self):\n        if not self._is_upgrade_method_needed(51, 51):\n            return\n        if (error_code := self._detect_insane_version_51()) != 0:\n            # should not get here; get_seed_version should have caught this\n            raise Exception(f'unsupported wallet file: version_51 with error {error_code}')\n        self.data['seed_version'] = 52\n\n    def _convert_version_53(self):\n        if not self._is_upgrade_method_needed(52, 52):\n            return\n        cbs = self.data.get('imported_channel_backups', {})\n        for channel_id, cb in list(cbs.items()):\n            if 'local_payment_pubkey' not in cb:\n                cb['local_payment_pubkey'] = None\n        self.data['seed_version'] = 53\n\n    def _convert_version_54(self):\n        # note: similar to convert_version_38\n        if not self._is_upgrade_method_needed(53, 53):\n            return\n        from .bitcoin import TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN\n        max_sats = TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN\n        requests = self.data.get('payment_requests', {})\n        invoices = self.data.get('invoices', {})\n        for d in [invoices, requests]:\n            for key, item in list(d.items()):\n                amount_msat = item['amount_msat']\n                if amount_msat == '!':\n                    continue\n                if not (isinstance(amount_msat, int) and 0 <= amount_msat <= max_sats * 1000):\n                    del d[key]\n        self.data['seed_version'] = 54\n\n    def _convert_version_55(self):\n        if not self._is_upgrade_method_needed(54, 54):\n            return\n        # do not use '/' in dict keys\n        for key in list(self.data.keys()):\n            if key.endswith('/'):\n                self.data[key[:-1]] = self.data.pop(key)\n        self.data['seed_version'] = 55\n\n    def _convert_version_56(self):\n        if not self._is_upgrade_method_needed(55, 55):\n            return\n        channels = self.data.get('channels', {})\n        for key, item in channels.items():\n            item['constraints']['flags'] = 0\n            for c in ['local_config', 'remote_config']:\n                item[c]['announcement_node_sig'] = ''\n                item[c]['announcement_bitcoin_sig'] = ''\n            item['local_config'].pop('was_announced')\n        self.data['seed_version'] = 56\n\n    def _convert_version_57(self):\n        if not self._is_upgrade_method_needed(56, 56):\n            return\n        # The 'seed_type' field could be present both at the top-level and inside keystores.\n        # We delete the one that is top-level.\n        self.data.pop('seed_type', None)\n        self.data['seed_version'] = 57\n\n    def _convert_version_58(self):\n        # re-construct prevouts_by_scripthash\n        # new structure:  scripthash -> outpoint -> value\n        if not self._is_upgrade_method_needed(57, 57):\n            return\n        from .bitcoin import script_to_scripthash\n        transactions = self.get('transactions', {})  # txid -> raw_tx\n        prevouts_by_scripthash = {}\n        for txid, raw_tx in transactions.items():\n            try:\n                tx = PartialTransaction.from_raw_psbt(raw_tx)\n            except BadHeaderMagic:\n                tx = Transaction(raw_tx)\n            for idx, txout in enumerate(tx.outputs()):\n                outpoint = f\"{txid}:{idx}\"\n                scripthash = script_to_scripthash(txout.scriptpubkey)\n                if scripthash not in prevouts_by_scripthash:\n                    prevouts_by_scripthash[scripthash] = {}\n                prevouts_by_scripthash[scripthash][outpoint] = txout.value\n        self.put('prevouts_by_scripthash', prevouts_by_scripthash)\n        self.data['seed_version'] = 58\n\n    def _convert_version_59(self):\n        if not self._is_upgrade_method_needed(58, 58):\n            return\n        channels = self.data.get('channels', {})\n        for _key, chan in channels.items():\n            chan.pop('fail_htlc_reasons', {})\n            unfulfilled_htlcs = {}\n            for htlc_id, (local_ctn, remote_ctn, onion_packet_hex, forwarding_key) in chan['unfulfilled_htlcs'].items():\n                unfulfilled_htlcs[htlc_id] = (onion_packet_hex, forwarding_key or None)\n            chan['unfulfilled_htlcs'] = unfulfilled_htlcs\n        self.data['channels'] = channels\n        self.data['seed_version'] = 59\n\n    def _convert_version_60(self):\n        if not self._is_upgrade_method_needed(59, 59):\n            return\n        cbs = self.data.get('imported_channel_backups', {})\n        for channel_id, cb in list(cbs.items()):\n            if 'multisig_funding_privkey' not in cb:\n                cb['multisig_funding_privkey'] = None\n        self.data['seed_version'] = 60\n\n    def _convert_version_61(self):\n        if not self._is_upgrade_method_needed(60, 60):\n            return\n        # adding additional fields to PaymentInfo\n        lightning_payments = self.data.get('lightning_payments', {})\n        expiry_never = 100 * 365 * 24 * 60 * 60\n        migration_time = int(time.time())\n        for rhash, (amount_msat, direction, is_paid) in list(lightning_payments.items()):\n            new = (amount_msat, direction, is_paid, 147, expiry_never, migration_time)\n            lightning_payments[rhash] = new\n        self.data['seed_version'] = 61\n\n    def _convert_version_62(self):\n        if not self._is_upgrade_method_needed(61, 61):\n            return\n        swaps = self.data.get('submarine_swaps', {})\n        # remove unused receive_address field which is getting replaced by a claim_to_output field\n        # which also allows specifying an amount\n        for swap in swaps.values():\n            del swap['receive_address']\n            swap['claim_to_output'] = None\n        self.data['seed_version'] = 62\n\n    def _convert_version_63(self):\n        if not self._is_upgrade_method_needed(62, 62):\n            return\n        # Old ReceivedMPPStatus:\n        #   class ReceivedMPPStatus(NamedTuple):\n        #      resolution: RecvMPPResolution\n        #      expected_msat: int\n        #      htlc_set: Set[Tuple[ShortChannelID, UpdateAddHtlc]]\n        #\n        # New ReceivedMPPStatus:\n        #   class ReceivedMPPStatus(NamedTuple):\n        #       resolution: RecvMPPResolution\n        #       htlcs: set[ReceivedMPPHtlc]\n        #\n        #   class ReceivedMPPHtlc(NamedTuple):\n        #       scid: ShortChannelID\n        #       htlc: UpdateAddHtlc\n        #       unprocessed_onion: str\n\n        # previously chan.unfulfilled_htlcs went through 4 stages:\n        # - 1. not forwarded yet: (onion_packet_hex, None)\n        # - 2. forwarded: (onion_packet_hex, forwarding_key)\n        # - 3. processed: (None, forwarding_key), not irrevocably removed yet\n        # - 4. done: (None, forwarding_key), irrevocably removed\n        channels = self.data.get('channels', {})\n        def _move_unprocessed_onion(short_channel_id: str, htlc_id: Optional[int]) -> Optional[Tuple[str, Optional[str]]]:\n            if htlc_id is None:\n                return None\n            for chan_ in channels.values():\n                if chan_['short_channel_id'] != short_channel_id:\n                    continue\n                unfulfilled_htlcs_ = chan_.get('unfulfilled_htlcs', {})\n                htlc_data = unfulfilled_htlcs_.get(str(htlc_id))\n                if htlc_data is None:\n                    return None\n                stored_onion_packet, htlc_forwarding_key = htlc_data\n                if stored_onion_packet is not None:\n                    htlc_data[0] = None  # overwrite the onion so it is not processed again in htlc_switch\n                    return stored_onion_packet, htlc_forwarding_key\n            return None\n\n        mpp_sets = self.data.get('received_mpp_htlcs', {})\n        for payment_key, recv_mpp_status in list(mpp_sets.items()):\n            assert isinstance(recv_mpp_status, list), f\"{recv_mpp_status=}\"\n            del recv_mpp_status[1]  # remove expected_msat\n\n            new_type_htlcs = []\n            forwarding_key = None\n            for scid, update_add_htlc in recv_mpp_status[1]:  # htlc_set\n                htlc_info_from_chan = _move_unprocessed_onion(scid, update_add_htlc[3])\n                if htlc_info_from_chan is None:\n                    # if there is no onion packet for the htlc it is dropped as it was already\n                    # processed in the old htlc_switch\n                    continue\n                onion_packet_hex = htlc_info_from_chan[0]\n                forwarding_key = htlc_info_from_chan[1] if htlc_info_from_chan[1] else forwarding_key\n                new_type_htlcs.append([\n                    scid,\n                    update_add_htlc,\n                    onion_packet_hex,\n                ])\n\n            if len(new_type_htlcs) == 0:\n                self.logger.debug(f\"_convert_version_63: dropping mpp set {payment_key=}.\")\n                del mpp_sets[payment_key]\n            else:\n                recv_mpp_status[1] = new_type_htlcs\n                self.logger.debug(f\"_convert_version_63: migrated mpp set {payment_key=}\")\n                if forwarding_key is not None:\n                    # if the forwarding key is set for the old mpp set it was either a forwarding\n                    # or a swap hold invoice. Assuming users of 4.6.2 don't use forwarding this update\n                    # most likely happens during a swap waiting for the preimage. Setting the mpp set\n                    # to SETTLING prevents us from accidentally failing the htlc set after the update,\n                    # however it carries the risk of the channel getting force closed if the swap fails\n                    # as the htlcs won't get failed due to the new SETTLING state\n                    # unless a forwarding error is set.\n                    recv_mpp_status[0] = 4  # RecvMPPResolution.SETTLING\n\n        # replace Tuple[onion, forwarding_key] with just the onion in chan['unfulfilled_htlcs']\n        for chan in channels.values():\n            unfulfilled_htlcs = chan.get('unfulfilled_htlcs', {})\n            for htlc_id, (unprocessed_onion, forwarding_key) in list(unfulfilled_htlcs.items()):\n                if unprocessed_onion is None:\n                    # delete all unfulfilled_htlcs with empty onion as they are already processed\n                    del unfulfilled_htlcs[htlc_id]\n                else:\n                    unfulfilled_htlcs[htlc_id] = unprocessed_onion\n\n        self.data['seed_version'] = 63\n\n    def _convert_version_64(self):\n        \"\"\"Key payment_info by \"rhash:direction\" instead of just rhash to allow storing a PaymentInfo\n        for each direction\"\"\"\n        if not self._is_upgrade_method_needed(63, 63):\n            return\n\n        new_payment_infos = {}\n        old_payment_infos = self.data.get('lightning_payments', {})\n        for payment_hash, old_values in old_payment_infos.items():\n            amount_msat, direction, status, min_final_cltv_expiry, expiry, creation_ts = old_values\n            # drop direction\n            new_values = (amount_msat, status, min_final_cltv_expiry, expiry, creation_ts)\n            new_key = f\"{payment_hash}:{direction}\"\n            new_payment_infos[new_key] = new_values  # save new entry\n\n        self.data['lightning_payments'] = new_payment_infos\n        self.data['seed_version'] = 64\n\n    def _convert_version_65(self):\n        \"\"\"Store channel_id instead of short_channel_id in ReceivedMPPHtlc\"\"\"\n        if not self._is_upgrade_method_needed(64, 64):\n            return\n\n        channels = self.data.get('channels', {})\n        def scid_to_channel_id(scid):\n            for channel_id, channel_data in channels.items():\n                if scid == channel_data.get('short_channel_id'):\n                    return channel_id\n            raise KeyError(f\"missing {scid=} in channels\")\n\n        mpp_sets = self.data.get('received_mpp_htlcs', {})\n        new_mpp_sets = {}\n        for payment_key, mpp_set in mpp_sets.items():\n            if len(mpp_set) == 2:\n                # if the db has received_mpp_htlcs pre version 65 we cannot assume they have parent_set_key\n                # as _convert_version_63 doesn't set it\n                resolution, htlc_list = mpp_set\n                parent_set_key = None\n            else:\n                resolution, htlc_list, parent_set_key = mpp_set\n            new_htlc_list = []\n            for htlc_data_tuple in htlc_list:\n                scid, update_add_htlc, onion = htlc_data_tuple\n                channel_id = scid_to_channel_id(scid)\n                new_htlc_list.append((channel_id, update_add_htlc, onion))\n            new_mpp_sets[payment_key] = (resolution, new_htlc_list, parent_set_key)\n\n        self.data['received_mpp_htlcs'] = new_mpp_sets\n        self.data['seed_version'] = 65\n\n    def _convert_version_66(self):\n        \"\"\"Add invoice features to PaymentInfo\"\"\"\n        if not self._is_upgrade_method_needed(65, 65):\n            return\n\n        new_payment_infos = {}\n        old_payment_infos = self.data.get('lightning_payments', {})\n        for key, old_v in old_payment_infos.items():\n            amount_msat, status, min_final_cltv_expiry, expiry, creation_ts = old_v\n            invoice_features = 0x24100  # <VAR_ONION_REQ|PAYMENT_SECRET_REQ|BASIC_MPP_OPT>\n            new_v = (amount_msat, status, min_final_cltv_expiry, expiry, creation_ts, invoice_features)\n            new_payment_infos[key] = new_v\n\n        self.data['lightning_payments'] = new_payment_infos\n        self.data['seed_version'] = 66\n\n    def _convert_version_67(self):\n        if not self._is_upgrade_method_needed(66, 66):\n            return\n        channels = self.data.get('channels', {})\n        for _key, chan in channels.items():\n            is_initiator = chan['constraints']['is_initiator']\n            key = '-1' if is_initiator else '1'\n            assert len(chan['log'][key]['fee_updates']) == 1, chan['log'][key]['fee_updates']\n            chan['log'][key]['fee_updates'] = {}\n        self.data['channels'] = channels\n        self.data['seed_version'] = 67\n\n    def _convert_version_68(self):\n        if not self._is_upgrade_method_needed(67, 67):\n            return\n        old_preimages = self.data.get('lightning_preimages', {})\n        new_preimages = {}\n        for _hash, preimage in old_preimages.items():\n            new_preimages[_hash] = (preimage, False)\n        self.data['lightning_preimages'] = new_preimages\n        self.data['seed_version'] = 68\n\n    def _convert_version_69(self):\n        \"\"\"Convert PaymentInfo amounts from 0 to None\"\"\"\n        if not self._is_upgrade_method_needed(68, 68):\n            return\n        new_payment_infos = {}\n        old_payment_infos = self.data.get('lightning_payments', {})\n        for key, old_v in old_payment_infos.items():\n            #amount_msat, status, min_final_cltv_delta, expiry_delay, creation_ts, invoice_features = old_v\n            amount_msat = old_v[0]\n            rhash, direction = key.split(\":\")  # key is \"RHASH:direction\"\n            direction = int(direction)\n            if direction == 1:  # RECEIVED\n                if amount_msat == 0:\n                    amount_msat = None\n            new_v = (amount_msat, *old_v[1:])\n            new_payment_infos[key] = new_v\n        self.data['lightning_payments'] = new_payment_infos\n        self.data['seed_version'] = 69\n\n    def _convert_version_70(self):\n        \"\"\"\n        Converts spending budget values of nwc plugin from sat to msat.\n        \"\"\"\n        if not self._is_upgrade_method_needed(69, 69):\n            return\n        nwc_connections = self.data.get('plugin_data', {}).get('nwc', {}).get('connections', {})\n        for pubkey, connection in nwc_connections.items():\n            new_budget_spends = []\n            for amount_sat, timestamp in connection.get('budget_spends', []):\n                new_budget_spends.append([amount_sat * 1000, timestamp])\n            connection['budget_spends'] = new_budget_spends\n        self.data['seed_version'] = 70\n\n    def _convert_imported(self):\n        if not self._is_upgrade_method_needed(0, 13):\n            return\n\n        # '/x' is the internal ID for imported accounts\n        d = self.get('accounts', {}).get('/x', {}).get('imported',{})\n        if not d:\n            return False\n        addresses = []\n        keypairs = {}\n        for addr, v in d.items():\n            pubkey, privkey = v\n            if privkey:\n                keypairs[pubkey] = privkey\n            else:\n                addresses.append(addr)\n        if addresses and keypairs:\n            raise WalletFileException('mixed addresses and privkeys')\n        elif addresses:\n            self.put('addresses', addresses)\n            self.put('accounts', None)\n        elif keypairs:\n            self.put('wallet_type', 'standard')\n            self.put('key_type', 'imported')\n            self.put('keypairs', keypairs)\n            self.put('accounts', None)\n        else:\n            raise WalletFileException('no addresses or privkeys')\n\n    def _convert_account(self):\n        if not self._is_upgrade_method_needed(0, 13):\n            return\n        self.put('accounts', None)\n\n    def _is_upgrade_method_needed(self, min_version, max_version):\n        assert min_version <= max_version\n        cur_version = self.get_seed_version()\n        if cur_version > max_version:\n            return False\n        elif cur_version < min_version:\n            raise WalletFileException(\n                'storage upgrade: unexpected version {} (should be {}-{})'\n                .format(cur_version, min_version, max_version))\n        else:\n            return True\n\n    def get_seed_version(self):\n        seed_version = self.get('seed_version')\n        if not seed_version:\n            seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION\n        if seed_version > FINAL_SEED_VERSION:\n            raise WalletFileException('This version of Electrum ({}) is too old to open this wallet.\\n'\n                                      '(highest supported storage version: {}, version of this file: {})'\n                                      .format(ELECTRUM_VERSION, FINAL_SEED_VERSION, seed_version))\n        if seed_version == 14 and self.get('seed_type') == 'segwit':\n            self._raise_unsupported_version(seed_version)\n        if seed_version == 51 and self._detect_insane_version_51():\n            self._raise_unsupported_version(seed_version)\n        if seed_version >= 12:\n            return seed_version\n        if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]:\n            self._raise_unsupported_version(seed_version)\n        return seed_version\n\n    def _raise_unsupported_version(self, seed_version):\n        msg = f\"Your wallet has an unsupported seed version: {seed_version}.\"\n        if seed_version in [5, 7, 8, 9, 10, 14]:\n            msg += \"\\n\\nTo open this wallet, try 'git checkout seed_v%d'\"%seed_version\n        if seed_version == 6:\n            # version 1.9.8 created v6 wallets when an incorrect seed was entered in the restore dialog\n            msg += '\\n\\nThis file was created because of a bug in version 1.9.8.'\n            if self.get('master_public_keys') is None and self.get('master_private_keys') is None and self.get('imported_keys') is None:\n                # pbkdf2 (at that time an additional dependency) was not included with the binaries, and wallet creation aborted.\n                msg += \"\\nIt does not contain any keys, and can safely be removed.\"\n            else:\n                # creation was complete if electrum was run from source\n                msg += \"\\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet.\"\n        if seed_version == 51:\n            error_code = self._detect_insane_version_51()\n            assert error_code != 0\n            msg += f\" ({error_code=})\"\n            if error_code == 1:\n                msg += \"\\nThis is a multisig wallet containing an old_mpk (pre-bip32 master public key).\"\n                msg += \"\\nPlease contact us to help recover it by opening an issue on GitHub.\"\n            elif error_code == 2:\n                msg += (\"\\nThis is a multisig wallet containing mixed xpub/Ypub/Zpub.\"\n                        \"\\nThe script type is determined by the type of the first keystore.\"\n                        \"\\nTo recover, you should re-create the wallet with matching type \"\n                        \"(converted if needed) master keys.\"\n                        \"\\nOr you can contact us to help recover it by opening an issue on GitHub.\")\n            else:\n                raise Exception(f\"unexpected {error_code=}\")\n            raise WalletFileExceptionVersion51(msg, should_report_crash=True)\n        # generic exception\n        raise WalletFileException(msg)\n\n\ndef upgrade_wallet_db(data: dict, do_upgrade: bool) -> Tuple[dict, bool]:\n    was_upgraded = False\n\n    if len(data) == 0:\n        # create new DB\n        data['seed_version'] = FINAL_SEED_VERSION\n        # store this for debugging purposes\n        v = DBMetadata(\n            creation_timestamp=int(time.time()),\n            first_electrum_version_used=ELECTRUM_VERSION,\n        )\n        assert data.get(\"db_metadata\", None) is None\n        data[\"db_metadata\"] = v.to_json()\n        was_upgraded = True\n\n    dbu = WalletDBUpgrader(data)\n    if dbu.requires_split():\n        raise WalletRequiresSplit(dbu.get_split_accounts())\n    if dbu.requires_upgrade() and do_upgrade:\n        dbu.upgrade()\n        was_upgraded = True\n    if dbu.requires_upgrade():\n        raise WalletRequiresUpgrade()\n    return dbu.data, was_upgraded\n\n\nclass WalletDB(JsonDB):\n\n    def __init__(\n        self,\n        s: str,\n        *,\n        storage: Optional['WalletStorage'] = None,\n        upgrade: bool = False,\n    ):\n        JsonDB.__init__(\n            self,\n            s,\n            storage=storage,\n            encoder=MyEncoder,\n            upgrader=partial(upgrade_wallet_db, do_upgrade=upgrade),\n        )\n        # create pointers\n        self.load_transactions()\n        # load plugins that are conditional on wallet type\n        self.load_plugins()\n\n    @locked\n    def get_seed_version(self):\n        return self.get('seed_version')\n\n    def get_db_metadata(self) -> Optional[DBMetadata]:\n        # field only present for wallet files created with ver 4.4.0 or later\n        return self.get(\"db_metadata\")\n\n    @locked\n    def get_txi_addresses(self, tx_hash: str) -> List[str]:\n        \"\"\"Returns list of is_mine addresses that appear as inputs in tx.\"\"\"\n        assert isinstance(tx_hash, str)\n        return list(self.txi.get(tx_hash, {}).keys())\n\n    @locked\n    def get_txo_addresses(self, tx_hash: str) -> List[str]:\n        \"\"\"Returns list of is_mine addresses that appear as outputs in tx.\"\"\"\n        assert isinstance(tx_hash, str)\n        return list(self.txo.get(tx_hash, {}).keys())\n\n    @locked\n    def get_txi_addr(self, tx_hash: str, address: str) -> Iterable[Tuple[str, int]]:\n        \"\"\"Returns an iterable of (prev_outpoint, value).\"\"\"\n        assert isinstance(tx_hash, str)\n        assert isinstance(address, str)\n        d = self.txi.get(tx_hash, {}).get(address, {})\n        return list(d.items())\n\n    @locked\n    def get_txo_addr(self, tx_hash: str, address: str) -> Dict[int, Tuple[int, bool]]:\n        \"\"\"Returns a dict: output_index -> (value, is_coinbase).\"\"\"\n        assert isinstance(tx_hash, str)\n        assert isinstance(address, str)\n        d = self.txo.get(tx_hash, {}).get(address, {})\n        return {int(n): (v, cb) for (n, (v, cb)) in d.items()}\n\n    @modifier\n    def add_txi_addr(self, tx_hash: str, addr: str, ser: str, v: int) -> None:\n        assert isinstance(tx_hash, str)\n        assert isinstance(addr, str)\n        assert isinstance(ser, str)\n        assert isinstance(v, int)\n        if tx_hash not in self.txi:\n            self.txi[tx_hash] = {}\n        d = self.txi[tx_hash]\n        if addr not in d:\n            d[addr] = {}\n        d[addr][ser] = v\n\n    @modifier\n    def add_txo_addr(self, tx_hash: str, addr: str, n: Union[int, str], v: int, is_coinbase: bool) -> None:\n        n = str(n)\n        assert isinstance(tx_hash, str)\n        assert isinstance(addr, str)\n        assert isinstance(n, str)\n        assert isinstance(v, int)\n        assert isinstance(is_coinbase, bool)\n        if tx_hash not in self.txo:\n            self.txo[tx_hash] = {}\n        d = self.txo[tx_hash]\n        if addr not in d:\n            d[addr] = {}\n        d[addr][n] = (v, is_coinbase)\n\n    @locked\n    def list_txi(self) -> Sequence[str]:\n        return list(self.txi.keys())\n\n    @locked\n    def list_txo(self) -> Sequence[str]:\n        return list(self.txo.keys())\n\n    @modifier\n    def remove_txi(self, tx_hash: str) -> None:\n        assert isinstance(tx_hash, str)\n        self.txi.pop(tx_hash, None)\n\n    @modifier\n    def remove_txo(self, tx_hash: str) -> None:\n        assert isinstance(tx_hash, str)\n        self.txo.pop(tx_hash, None)\n\n    @locked\n    def list_spent_outpoints(self) -> Sequence[Tuple[str, str]]:\n        return [(h, n)\n                for h in self.spent_outpoints.keys()\n                for n in self.get_spent_outpoints(h)\n        ]\n\n    @locked\n    def get_spent_outpoints(self, prevout_hash: str) -> Sequence[str]:\n        assert isinstance(prevout_hash, str)\n        return list(self.spent_outpoints.get(prevout_hash, {}).keys())\n\n    @locked\n    def get_spent_outpoint(self, prevout_hash: str, prevout_n: Union[int, str]) -> Optional[str]:\n        assert isinstance(prevout_hash, str)\n        prevout_n = str(prevout_n)\n        return self.spent_outpoints.get(prevout_hash, {}).get(prevout_n)\n\n    @modifier\n    def remove_spent_outpoint(self, prevout_hash: str, prevout_n: Union[int, str]) -> None:\n        assert isinstance(prevout_hash, str)\n        prevout_n = str(prevout_n)\n        self.spent_outpoints[prevout_hash].pop(prevout_n, None)\n        if not self.spent_outpoints[prevout_hash]:\n            self.spent_outpoints.pop(prevout_hash)\n\n    @modifier\n    def set_spent_outpoint(self, prevout_hash: str, prevout_n: Union[int, str], tx_hash: str) -> None:\n        assert isinstance(prevout_hash, str)\n        assert isinstance(tx_hash, str)\n        prevout_n = str(prevout_n)\n        if prevout_hash not in self.spent_outpoints:\n            self.spent_outpoints[prevout_hash] = {}\n        self.spent_outpoints[prevout_hash][prevout_n] = tx_hash\n\n    @modifier\n    def add_prevout_by_scripthash(self, scripthash: str, *, prevout: TxOutpoint, value: int) -> None:\n        assert isinstance(scripthash, str)\n        assert isinstance(prevout, TxOutpoint)\n        assert isinstance(value, int)\n        if scripthash not in self._prevouts_by_scripthash:\n            self._prevouts_by_scripthash[scripthash] = dict()\n        self._prevouts_by_scripthash[scripthash][prevout.to_str()] = value\n\n    @modifier\n    def remove_prevout_by_scripthash(self, scripthash: str, *, prevout: TxOutpoint, value: int) -> None:\n        assert isinstance(scripthash, str)\n        assert isinstance(prevout, TxOutpoint)\n        assert isinstance(value, int)\n        self._prevouts_by_scripthash[scripthash].pop(prevout.to_str(), None)\n        if not self._prevouts_by_scripthash[scripthash]:\n            self._prevouts_by_scripthash.pop(scripthash)\n\n    @locked\n    def get_prevouts_by_scripthash(self, scripthash: str) -> Set[Tuple[TxOutpoint, int]]:\n        assert isinstance(scripthash, str)\n        prevouts_and_values = self._prevouts_by_scripthash.get(scripthash, {})\n        return {(TxOutpoint.from_str(prevout), value) for prevout, value in prevouts_and_values.items()}\n\n    @modifier\n    def add_transaction(self, tx_hash: str, tx: Transaction) -> None:\n        assert isinstance(tx_hash, str)\n        assert isinstance(tx, Transaction), tx\n        # note that tx might be a PartialTransaction\n        # serialize and de-serialize tx now. this might e.g. convert a complete PartialTx to a Tx\n        tx = tx_from_any(str(tx))\n        if not tx_hash:\n            raise Exception(\"trying to add tx to db without txid\")\n        if tx_hash != tx.txid():\n            raise Exception(f\"trying to add tx to db with inconsistent txid: {tx_hash} != {tx.txid()}\")\n        # don't allow overwriting complete tx with partial tx\n        tx_we_already_have = self.transactions.get(tx_hash, None)\n        if tx_we_already_have is None or isinstance(tx_we_already_have, PartialTransaction):\n            self.transactions[tx_hash] = tx\n\n    @modifier\n    def remove_transaction(self, tx_hash: str) -> Optional[Transaction]:\n        assert isinstance(tx_hash, str)\n        return self.transactions.pop(tx_hash, None)\n\n    @locked\n    def get_transaction(self, tx_hash: Optional[str]) -> Optional[Transaction]:\n        if tx_hash is None:\n            return None\n        assert isinstance(tx_hash, str)\n        return self.transactions.get(tx_hash)\n\n    @locked\n    def list_transactions(self) -> Sequence[str]:\n        return list(self.transactions.keys())\n\n    @locked\n    def get_history(self) -> Sequence[str]:\n        return list(self.history.keys())\n\n    def is_addr_in_history(self, addr: str) -> bool:\n        # does not mean history is non-empty!\n        assert isinstance(addr, str)\n        return addr in self.history\n\n    @locked\n    def get_addr_history(self, addr: str) -> Sequence[Tuple[str, int]]:\n        assert isinstance(addr, str)\n        return self.history.get(addr, [])\n\n    @modifier\n    def set_addr_history(self, addr: str, hist) -> None:\n        assert isinstance(addr, str)\n        self.history[addr] = hist\n\n    @modifier\n    def remove_addr_history(self, addr: str) -> None:\n        assert isinstance(addr, str)\n        self.history.pop(addr, None)\n\n    @locked\n    def list_verified_tx(self) -> Sequence[str]:\n        return list(self.verified_tx.keys())\n\n    @locked\n    def get_verified_tx(self, txid: str) -> Optional[TxMinedInfo]:\n        assert isinstance(txid, str)\n        if txid not in self.verified_tx:\n            return None\n        height, timestamp, txpos, header_hash = self.verified_tx[txid]\n        return TxMinedInfo(_height=height,\n                           conf=None,\n                           timestamp=timestamp,\n                           txpos=txpos,\n                           header_hash=header_hash)\n\n    @modifier\n    def add_verified_tx(self, txid: str, info: TxMinedInfo):\n        assert isinstance(txid, str)\n        assert isinstance(info, TxMinedInfo)\n        height = info._height  # number of conf is dynamic and might not be set here\n        assert height > 0, height\n        self.verified_tx[txid] = (height, info.timestamp, info.txpos, info.header_hash)\n\n    @modifier\n    def remove_verified_tx(self, txid: str):\n        assert isinstance(txid, str)\n        self.verified_tx.pop(txid, None)\n\n    def is_in_verified_tx(self, txid: str) -> bool:\n        assert isinstance(txid, str)\n        return txid in self.verified_tx\n\n    @modifier\n    def add_tx_fee_from_server(self, txid: str, fee_sat: Optional[int]) -> None:\n        assert isinstance(txid, str)\n        # note: when called with (fee_sat is None), rm currently saved value\n        if txid not in self.tx_fees:\n            self.tx_fees[txid] = TxFeesValue()\n        tx_fees_value = self.tx_fees[txid]\n        if tx_fees_value.is_calculated_by_us:\n            return\n        self.tx_fees[txid] = tx_fees_value._replace(fee=fee_sat, is_calculated_by_us=False)\n\n    @modifier\n    def add_tx_fee_we_calculated(self, txid: str, fee_sat: Optional[int]) -> None:\n        assert isinstance(txid, str)\n        if fee_sat is None:\n            return\n        assert isinstance(fee_sat, int)\n        if txid not in self.tx_fees:\n            self.tx_fees[txid] = TxFeesValue()\n        self.tx_fees[txid] = self.tx_fees[txid]._replace(fee=fee_sat, is_calculated_by_us=True)\n\n    @locked\n    def get_tx_fee(self, txid: str, *, trust_server: bool = False) -> Optional[int]:\n        assert isinstance(txid, str)\n        \"\"\"Returns tx_fee.\"\"\"\n        tx_fees_value = self.tx_fees.get(txid)\n        if tx_fees_value is None:\n            return None\n        if not trust_server and not tx_fees_value.is_calculated_by_us:\n            return None\n        return tx_fees_value.fee\n\n    @modifier\n    def add_num_inputs_to_tx(self, txid: str, num_inputs: int) -> None:\n        assert isinstance(txid, str)\n        assert isinstance(num_inputs, int)\n        if txid not in self.tx_fees:\n            self.tx_fees[txid] = TxFeesValue()\n        self.tx_fees[txid] = self.tx_fees[txid]._replace(num_inputs=num_inputs)\n\n    @locked\n    def get_num_all_inputs_of_tx(self, txid: str) -> Optional[int]:\n        assert isinstance(txid, str)\n        tx_fees_value = self.tx_fees.get(txid)\n        if tx_fees_value is None:\n            return None\n        return tx_fees_value.num_inputs\n\n    @locked\n    def get_num_ismine_inputs_of_tx(self, txid: str) -> int:\n        assert isinstance(txid, str)\n        txins = self.txi.get(txid, {})\n        return sum([len(tupls) for addr, tupls in txins.items()])\n\n    @modifier\n    def remove_tx_fee(self, txid: str) -> None:\n        assert isinstance(txid, str)\n        self.tx_fees.pop(txid, None)\n\n    @locked\n    def num_change_addresses(self) -> int:\n        return len(self.change_addresses)\n\n    @locked\n    def num_receiving_addresses(self) -> int:\n        return len(self.receiving_addresses)\n\n    @locked\n    def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> List[str]:\n        # note: slicing makes a shallow copy\n        return self.change_addresses[slice_start:slice_stop]\n\n    @locked\n    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> List[str]:\n        # note: slicing makes a shallow copy\n        return self.receiving_addresses[slice_start:slice_stop]\n\n    @modifier\n    def add_change_address(self, addr: str) -> None:\n        assert isinstance(addr, str)\n        self._addr_to_addr_index[addr] = (1, len(self.change_addresses))\n        self.change_addresses.append(addr)\n\n    @modifier\n    def add_receiving_address(self, addr: str) -> None:\n        assert isinstance(addr, str)\n        self._addr_to_addr_index[addr] = (0, len(self.receiving_addresses))\n        self.receiving_addresses.append(addr)\n\n    @locked\n    def get_address_index(self, address: str) -> Optional[Sequence[int]]:\n        assert isinstance(address, str)\n        return self._addr_to_addr_index.get(address)\n\n    @modifier\n    def add_imported_address(self, addr: str, d: dict) -> None:\n        assert isinstance(addr, str)\n        self.imported_addresses[addr] = d\n\n    @modifier\n    def remove_imported_address(self, addr: str) -> None:\n        assert isinstance(addr, str)\n        self.imported_addresses.pop(addr)\n\n    @locked\n    def has_imported_address(self, addr: str) -> bool:\n        assert isinstance(addr, str)\n        return addr in self.imported_addresses\n\n    @locked\n    def get_imported_addresses(self) -> Sequence[str]:\n        return list(sorted(self.imported_addresses.keys()))\n\n    @locked\n    def get_imported_address(self, addr: str) -> Optional[dict]:\n        assert isinstance(addr, str)\n        return self.imported_addresses.get(addr)\n\n    def load_addresses(self, wallet_type):\n        \"\"\" called from Abstract_Wallet.__init__ \"\"\"\n        if wallet_type == 'imported':\n            self.imported_addresses = self.get_dict('addresses')  # type: Dict[str, dict]\n        else:\n            self.get_dict('addresses')\n            for name in ['receiving', 'change']:\n                if name not in self.data['addresses']:\n                    self.data['addresses'][name] = []\n            self.change_addresses = self.data['addresses']['change']\n            self.receiving_addresses = self.data['addresses']['receiving']\n            self._addr_to_addr_index = {}  # type: Dict[str, Sequence[int]]  # key: address, value: (is_change, index)\n            for i, addr in enumerate(self.receiving_addresses):\n                self._addr_to_addr_index[addr] = (0, i)\n            for i, addr in enumerate(self.change_addresses):\n                self._addr_to_addr_index[addr] = (1, i)\n\n    @profiler\n    def load_transactions(self):\n        # references in self.data\n        # TODO make all these private\n        # txid -> address -> prev_outpoint -> value\n        self.txi = self.get_dict('txi')                          # type: Dict[str, Dict[str, Dict[str, int]]]\n        # txid -> address -> output_index -> (value, is_coinbase)\n        self.txo = self.get_dict('txo')                          # type: Dict[str, Dict[str, Dict[str, Tuple[int, bool]]]]\n        self.transactions = self.get_dict('transactions')        # type: Dict[str, Transaction]\n        self.spent_outpoints = self.get_dict('spent_outpoints')  # txid -> output_index -> next_txid\n        self.history = self.get_dict('addr_history')             # address -> list of (txid, height)\n        self.verified_tx = self.get_dict('verified_tx3')         # txid -> (height, timestamp, txpos, header_hash)\n        self.tx_fees = self.get_dict('tx_fees')                  # type: Dict[str, TxFeesValue]\n        # scripthash -> outpoint -> value\n        self._prevouts_by_scripthash = self.get_dict('prevouts_by_scripthash')  # type: Dict[str, Dict[str, int]]\n        # remove unreferenced tx\n        for tx_hash in list(self.transactions.keys()):\n            if not self.get_txi_addresses(tx_hash) and not self.get_txo_addresses(tx_hash):\n                self.logger.info(f\"removing unreferenced tx: {tx_hash}\")\n                self.transactions.pop(tx_hash)\n        # remove unreferenced outpoints\n        for prevout_hash in self.spent_outpoints.keys():\n            d = self.spent_outpoints[prevout_hash]\n            for prevout_n, spending_txid in list(d.items()):\n                if spending_txid not in self.transactions:\n                    self.logger.info(\"removing unreferenced spent outpoint\")\n                    d.pop(prevout_n)\n\n    @modifier\n    def clear_history(self):\n        self.txi.clear()\n        self.txo.clear()\n        self.spent_outpoints.clear()\n        self.transactions.clear()\n        self.history.clear()\n        self.verified_tx.clear()\n        self.tx_fees.clear()\n        self._prevouts_by_scripthash.clear()\n\n    def _should_convert_to_stored_dict(self, key) -> bool:\n        if key == 'keystore':\n            return False\n        multisig_keystore_names = [('x%d' % i) for i in range(1, 16)]\n        if key in multisig_keystore_names:\n            return False\n        return True\n\n    @classmethod\n    def split_accounts(klass, root_path, split_data):\n        from .storage import WalletStorage\n        file_list = []\n        for data in split_data:\n            path = root_path + '.' + data['suffix']\n            item_storage = WalletStorage(path)\n            db = WalletDB(json.dumps(data), storage=item_storage, upgrade=True)\n            db.write()\n            file_list.append(path)\n        return file_list\n\n    def get_action(self):\n        action = run_hook('get_action', self)\n        return action\n\n    def load_plugins(self):\n        wallet_type = self.get('wallet_type')\n        if wallet_type in plugin_loaders:\n            plugin_loaders[wallet_type]()\n\n    def get_plugin_storage(self) -> dict:\n        return self.get_dict('plugin_data')\n\n    def prune_uninstalled_plugin_data(self, installed_plugins: AbstractSet[str]) -> None:\n        \"\"\"Remove plugin data for plugins that are not installed anymore.\"\"\"\n        plugin_storage = self.get_plugin_storage()\n        for name in list(plugin_storage.keys()):\n            if name not in installed_plugins:\n                plugin_storage.pop(name)\n                self.logger.info(f\"deleting plugin data: {name=}\")\n\n    def set_keystore_encryption(self, enable):\n        self.put('use_encryption', enable)\n"
  },
  {
    "path": "electrum/wizard.py",
    "content": "import copy\nimport os\n\nfrom typing import List, NamedTuple, Any, Dict, Optional, Tuple, TYPE_CHECKING\n\nfrom electrum.gui.messages import TERMS_OF_USE_LATEST_VERSION\n\nfrom electrum.i18n import _\nfrom electrum.interface import ServerAddr\nfrom electrum.keystore import hardware_keystore\nfrom electrum.logging import get_logger\nfrom electrum.network import ProxySettings\nfrom electrum.plugin import run_hook\nfrom electrum.slip39 import EncryptedSeed\nfrom electrum.storage import WalletStorage, StorageEncryptionVersion, StorageReadWriteError\nfrom electrum.util import UserFacingException\nfrom electrum.wallet_db import WalletDB\nfrom electrum.bip32 import normalize_bip32_derivation, xpub_type\nfrom electrum import keystore, mnemonic, bitcoin\nfrom electrum.mnemonic import is_any_2fa_seed_type, can_seed_have_passphrase\nfrom electrum.util import multisig_type\n\nif TYPE_CHECKING:\n    from electrum.daemon import Daemon\n    from electrum.plugin import Plugins\n    from electrum.keystore import Hardware_KeyStore\n    from electrum.simple_config import SimpleConfig\n\n\nclass WizardViewState(NamedTuple):\n    view: Optional[str]\n    wizard_data: Dict[str, Any]\n    params: Dict[str, Any]\n\n\nclass AbstractWizard:\n    # serve as a base for all UIs, so no qt\n    # encapsulate wizard state\n    # encapsulate navigation decisions, UI agnostic\n    # encapsulate stack, go backwards\n    # allow extend/override flow in subclasses e.g.\n    # - override: replace 'next' value to own fn\n    # - extend: add new keys to navmap, wire up flow by override\n\n    _logger = get_logger(__name__)\n\n    def __init__(self):\n        self.navmap = {}\n\n        self._current = WizardViewState(None, {}, {})\n        self._stack = []  # type: List[WizardViewState]\n\n    def navmap_merge(self, additional_navmap: dict):\n        # NOTE: only merges one level deep. Deeper dict levels will overwrite\n        for k, v in additional_navmap.items():\n            if k in self.navmap:\n                self.navmap[k].update(v)\n            else:\n                self.navmap[k] = v\n\n    # from current view and wizard_data, resolve the new view\n    # returns WizardViewState tuple (view name, wizard_data, view params)\n    # view name is the string id of the view in the nav map\n    # wizard data is the (stacked) wizard data dict containing user input and choices\n    # view params are transient, meant for extra configuration of a view (e.g. info\n    #   msg in a generic choice dialog)\n    # exception: stay on this view\n    def resolve_next(self, view: str, wizard_data: dict) -> WizardViewState:\n        assert view, f'view not defined: {repr(self.sanitize_stack_item(wizard_data))}'\n        self._logger.debug(f'view={view}')\n        assert view in self.navmap\n\n        nav = self.navmap[view]\n\n        if 'accept' in nav:\n            # allow python scope to append to wizard_data before\n            # adding to stack or finishing\n            view_accept = nav['accept']\n            if callable(view_accept):\n                view_accept(wizard_data)\n            else:\n                raise Exception(f'accept handler for view {view} is not callable')\n\n        # make a clone for next view\n        wizard_data = copy.deepcopy(wizard_data)\n\n        if 'next' not in nav:\n            new_view = WizardViewState(None, wizard_data, {})\n        else:\n            view_next = nav['next']\n            if isinstance(view_next, str):\n                # string literal\n                new_view = WizardViewState(view_next, wizard_data, {})\n            elif callable(view_next):\n                # handler fn based\n                nv = view_next(wizard_data)\n                self._logger.debug(repr(nv))\n\n                # append wizard_data and params if not returned\n                if isinstance(nv, str):\n                    new_view = WizardViewState(nv, wizard_data, {})\n                elif len(nv) == 1:\n                    new_view = WizardViewState(nv[0], wizard_data, {})\n                elif len(nv) == 2:\n                    new_view = WizardViewState(nv[0], nv[1], {})\n                else:\n                    new_view = nv\n            else:\n                raise Exception(f'next handler for view {view} is not callable nor a string literal')\n\n            if 'params' in self.navmap[new_view.view]:\n                params = self.navmap[new_view.view]['params']\n                assert isinstance(params, dict), 'params is not a dict'\n                new_view.params.update(params)\n\n            self._logger.debug(f'resolve_next view is {new_view.view}')\n\n        self._stack.append(copy.deepcopy(self._current))\n        self._current = new_view\n\n        self.log_stack()\n\n        return new_view\n\n    def resolve_prev(self):\n        self._current = self._stack.pop()\n\n        self._logger.debug(f'resolve_prev view is \"{self._current.view}\"')\n        self.log_stack()\n\n        return self._current\n\n    # check if this view is the final view\n    def is_last_view(self, view: str, wizard_data: dict) -> bool:\n        assert view, f'view not defined: {repr(self.sanitize_stack_item(wizard_data))}'\n        assert view in self.navmap\n\n        nav = self.navmap[view]\n\n        if 'last' not in nav:\n            return False\n\n        view_last = nav['last']\n        if isinstance(view_last, bool):\n            # bool literal\n            self._logger.debug(f'view \"{view}\" last: {view_last}')\n            return view_last\n        elif callable(view_last):\n            # handler fn based\n            is_last = view_last(wizard_data)\n            self._logger.debug(f'view \"{view}\" last: {is_last}')\n            return is_last\n        else:\n            raise Exception(f'last handler for view {view} is not callable nor a bool literal')\n\n    def reset(self):\n        self._stack = []\n        self._current = WizardViewState(None, {}, {})\n\n    def log_stack(self):\n        logstr = 'wizard stack:'\n        i = 0\n        for item in self._stack:\n            ssi = self.sanitize_stack_item(item.wizard_data)\n            logstr += f'\\n{i}: {hex(id(item.wizard_data))} - {repr(ssi)}'\n            i += 1\n        sci = self.sanitize_stack_item(self._current.wizard_data)\n        logstr += f'\\nc: {hex(id(self._current.wizard_data))} - {repr(sci)}'\n        self._logger.debug(logstr)\n\n    def sanitize_stack_item(self, _stack_item) -> dict:\n        whitelist = [\n            \"wallet_name\", \"wallet_exists\", \"wallet_is_open\", \"wallet_needs_hw_unlock\",\n            \"wallet_type\", \"keystore_type\", \"seed_variant\", \"seed_type\", \"seed_extend\",\n            \"script_type\", \"derivation_path\", \"encrypt\",\n            # hardware devices:\n            \"hardware_device\", \"hw_type\", \"label\", \"soft_device_id\", \"xpub_encrypt\",\n            # inside keystore:\n            \"type\", \"pw_hash_version\", \"derivation\", \"root_fingerprint\",\n            # multisig:\n            \"multisig_participants\", \"multisig_signatures\", \"multisig_current_cosigner\", \"cosigner_keystore_type\",\n            # trustedcoin:\n            \"trustedcoin_keepordisable\", \"trustedcoin_go_online\",\n        ]\n\n        def sanitize(_dict):\n            result = {}\n            for item in _dict:\n                if isinstance(_dict[item], dict):\n                    result[item] = sanitize(_dict[item])\n                else:\n                    if item in whitelist:\n                        result[item] = _dict[item]\n                    else:\n                        result[item] = '<redacted>'\n            return result\n        return sanitize(_stack_item)\n\n    def get_wizard_data(self) -> dict:\n        return copy.deepcopy(self._current.wizard_data)\n\n\nclass KeystoreWizard(AbstractWizard):\n\n    _logger = get_logger(__name__)\n\n    def __init__(self, plugins: 'Plugins'):\n        AbstractWizard.__init__(self)\n        self.plugins = plugins\n        self.navmap = {\n            'keystore_type': {\n                'next': self.on_keystore_type\n            },\n            'enter_seed': {\n                'next': lambda d: 'enter_ext' if self.wants_ext(d) else 'script_and_derivation',\n                'accept': lambda d: None if (self.wants_ext(d) or self.needs_derivation_path(d)) else self.update_keystore(d),\n                'last': lambda d: not self.wants_ext(d) and not self.needs_derivation_path(d),\n            },\n            'enter_ext': {\n                'next': 'script_and_derivation',\n                'accept': lambda d: None if self.needs_derivation_path(d) else self.update_keystore(d),\n                'last': lambda d: not self.needs_derivation_path(d)\n            },\n            'script_and_derivation': {\n                'accept': self.update_keystore,\n                'last': True\n            },\n            'choose_hardware_device': {\n                'next': self.on_hardware_device,\n            },\n            'wallet_password': {\n                'last': True\n            },\n            'wallet_password_hardware': {\n                'last': True\n            },\n        }\n\n    def maybe_master_pubkey(self, wizard_data):\n        self.update_keystore(wizard_data)\n\n    def check_multisig_constraints(self, wizard_data: dict) -> Tuple[bool, str]:\n        # called by GUI. overloaded in NewWalletWizard\n        return True, ''\n\n    def update_keystore(self, wizard_data):\n        wallet_type = wizard_data['wallet_type']\n        keystore = self.keystore_from_data(wallet_type, wizard_data)\n        self._result = keystore, (wizard_data['keystore_type'] == 'hardware')\n\n    def on_keystore_type(self, wizard_data: dict) -> str:\n        t = wizard_data['keystore_type']\n        return {\n            'haveseed': 'enter_seed',\n            'hardware': 'choose_hardware_device'\n        }.get(t)\n\n    def last_cosigner(self, wizard_data: dict) -> bool:\n        # one at a time\n        return True\n\n    def _convert_wallet_type(self, wizard_data: dict) -> None:\n        assert 'wallet_type' in wizard_data\n        if multisig_type(wizard_data['wallet_type']):\n            wizard_data['wallet_type'] = 'multisig'  # convert from e.g. \"2of2\" to \"multisig\"\n            wizard_data['multisig_participants'] = 2\n            wizard_data['multisig_signatures'] = 2\n            wizard_data['multisig_cosigner_data'] = {}\n\n    def start(self, *, start_viewstate: WizardViewState = None) -> WizardViewState:\n        self.reset()\n        if start_viewstate is None:\n            start_view = 'keystore_type'\n            params = self.navmap[start_view].get('params', {})\n            self._current = WizardViewState(start_view, {}, params)\n        else:\n            self._current = start_viewstate\n        self._convert_wallet_type(self._current.wizard_data)  # mutating in-place\n        return self._current\n\n    # returns (sub)dict of current cosigner (or root if first)\n    def current_cosigner(self, wizard_data: dict) -> dict:\n        wdata = wizard_data\n        if wizard_data.get('wallet_type') == 'multisig' and 'multisig_current_cosigner' in wizard_data:\n            cosigner = wizard_data['multisig_current_cosigner']\n            wdata = wizard_data['multisig_cosigner_data'][str(cosigner)]\n        return wdata\n\n    def needs_derivation_path(self, wizard_data: dict) -> bool:\n        wdata = self.current_cosigner(wizard_data)\n        return 'seed_variant' in wdata and wdata['seed_variant'] in ['bip39', 'slip39']\n\n    def wants_ext(self, wizard_data: dict) -> bool:\n        wdata = self.current_cosigner(wizard_data)\n        return 'seed_variant' in wdata and wdata['seed_extend']\n\n    def is_multisig(self, wizard_data: dict) -> bool:\n        return wizard_data['wallet_type'] == 'multisig'\n\n    def is_hardware(self, wizard_data: dict) -> bool:\n        return wizard_data['keystore_type'] == 'hardware'\n\n    def wallet_password_view(self, wizard_data: dict) -> str:\n        if self.is_hardware(wizard_data) and wizard_data['wallet_type'] == 'standard':\n            return 'wallet_password_hardware'\n        return 'wallet_password'\n\n    def on_hardware_device(self, wizard_data: dict, new_wallet=True) -> str:\n        current_cosigner = self.current_cosigner(wizard_data)\n        _type, _info = current_cosigner['hardware_device']\n        plugin = self.plugins.get_plugin(_type)\n        run_hook('init_wallet_wizard', self)  # TODO: currently only used for hww, hook name might be confusing\n        return plugin.wizard_entry_for_device(_info, new_wallet=new_wallet)\n\n    def validate_seed(self, seed: str, seed_variant: str, wallet_type: str) -> Tuple[bool, str, str, bool]:\n        seed_type = ''\n        seed_valid = False\n        validation_message = ''\n        can_passphrase = True\n\n        if seed_variant == 'electrum':\n            seed_type = mnemonic.calc_seed_type(seed)\n            if seed_type != '':\n                seed_valid = True\n                can_passphrase = can_seed_have_passphrase(seed)\n        elif seed_variant == 'bip39':\n            is_checksum, is_wordlist = keystore.bip39_is_checksum_valid(seed)\n            validation_message = ('' if is_checksum else _('BIP39 checksum failed')) if is_wordlist else _('Unknown BIP39 wordlist')\n            if not bool(seed):\n                validation_message = ''\n            seed_type = 'bip39'\n            # bip39 always valid, even if checksum failed, see #8720\n            # however, reject empty string\n            seed_valid = bool(seed)\n        elif seed_variant == 'slip39':\n            # seed shares should be already validated by wizard page, we have a combined encrypted seed\n            if seed and isinstance(seed, EncryptedSeed):\n                seed_valid = True\n                seed_type = 'slip39'\n            else:\n                seed_valid = False\n        else:\n            raise Exception(f'unknown seed variant {seed_variant}')\n\n        # check if seed matches wallet type\n        if wallet_type == '2fa' and not is_any_2fa_seed_type(seed_type):\n            seed_valid = False\n        elif wallet_type == 'standard' and seed_type not in ['old', 'standard', 'segwit', 'bip39', 'slip39']:\n            seed_valid = False\n        elif wallet_type == 'multisig' and seed_type not in ['standard', 'segwit', 'bip39', 'slip39']:\n            seed_valid = False\n\n        self._logger.debug(f'seed verified: {seed_valid}, type={seed_type!r}, validation_message={validation_message}')\n\n        return seed_valid, seed_type, validation_message, can_passphrase\n\n    def keystore_from_data(self, wallet_type: str, data: dict):\n        if data['keystore_type'] in ['createseed', 'haveseed'] and 'seed' in data:\n            seed_extension = data.get('seed_extra_words', '')\n            if data['seed_variant'] == 'electrum':\n                for_multisig = wallet_type in ['multisig']\n                return keystore.from_seed(data['seed'], passphrase=seed_extension, for_multisig=for_multisig)\n            elif data['seed_variant'] == 'bip39':\n                root_seed = keystore.bip39_to_seed(data['seed'], passphrase=seed_extension)\n                derivation = normalize_bip32_derivation(data['derivation_path'])\n                if wallet_type == 'multisig':\n                    script = data['script_type'] if data['script_type'] != 'p2sh' else 'standard'\n                else:\n                    script = data['script_type'] if data['script_type'] != 'p2pkh' else 'standard'\n                return keystore.from_bip43_rootseed(root_seed, derivation=derivation, xtype=script)\n            elif data['seed_variant'] == 'slip39':\n                root_seed = data['seed'].decrypt(seed_extension)\n                derivation = normalize_bip32_derivation(data['derivation_path'])\n                if wallet_type == 'multisig':\n                    script = data['script_type'] if data['script_type'] != 'p2sh' else 'standard'\n                else:\n                    script = data['script_type'] if data['script_type'] != 'p2pkh' else 'standard'\n                return keystore.from_bip43_rootseed(root_seed, derivation=derivation, xtype=script)\n            else:\n                raise Exception('Unsupported seed variant %s' % data['seed_variant'])\n        elif data['keystore_type'] == 'masterkey' and 'master_key' in data:\n            return keystore.from_master_key(data['master_key'])\n        elif data['keystore_type'] == 'hardware':\n            return self.hw_keystore(data)\n        else:\n            raise Exception('no seed or master_key in data')\n\n    def hw_keystore(self, data: dict) -> 'Hardware_KeyStore':\n        return hardware_keystore({\n            'type': 'hardware',\n            'hw_type': data['hw_type'],\n            'derivation': data['derivation_path'],\n            'root_fingerprint': data['root_fingerprint'],\n            'xpub': data['master_key'],\n            'label': data['label'],\n            'soft_device_id': data['soft_device_id']\n        })\n\n\nclass NewWalletWizard(KeystoreWizard):\n\n    _logger = get_logger(__name__)\n\n    def __init__(self, daemon: 'Daemon', plugins: 'Plugins'):\n        KeystoreWizard.__init__(self, plugins)\n        self.navmap = {\n            'wallet_name': {\n                'next': lambda d: 'hw_unlock' if d.get('wallet_needs_hw_unlock') else 'wallet_type',\n            },\n            'hw_unlock': {\n                'next': lambda d: self.on_hardware_device(d, new_wallet=False),\n            },\n            'wallet_type': {\n                'next': self.on_wallet_type\n            },\n            'keystore_type': {\n                'next': self.on_keystore_type\n            },\n            'create_seed': {\n                'next': lambda d: 'create_ext' if self.wants_ext(d) else 'confirm_seed',\n            },\n            'create_ext': {\n                'next': 'confirm_seed',\n            },\n            'confirm_seed': {\n                'next': lambda d: 'confirm_ext' if self.wants_ext(d) else self.on_have_or_confirm_seed(d),\n                'accept': lambda d: None if self.wants_ext(d) else self.maybe_master_pubkey(d),\n                'last': lambda d: self.is_single_password() and not self.is_multisig(d) and not self.wants_ext(d),\n            },\n            'confirm_ext': {\n                'next': self.on_have_or_confirm_seed,\n                'accept': self.maybe_master_pubkey,\n                'last': lambda d: self.is_single_password() and not self.is_multisig(d)\n            },\n            'have_seed': {\n                'next': lambda d: 'have_ext' if self.wants_ext(d) else self.on_have_or_confirm_seed(d),\n                'accept': lambda d: None if self.wants_ext(d) else self.maybe_master_pubkey(d),\n                'last': lambda d: self.is_single_password() and not\n                                    (self.needs_derivation_path(d) or self.is_multisig(d) or self.wants_ext(d)),\n            },\n            'have_ext': {\n                'next': self.on_have_or_confirm_seed,\n                'accept': self.maybe_master_pubkey,\n                'last': lambda d: self.is_single_password() and not\n                                  (self.needs_derivation_path(d) or self.is_multisig(d))\n            },\n            'choose_hardware_device': {\n                'next': self.on_hardware_device,\n            },\n            'script_and_derivation': {\n                'next': lambda d: self.wallet_password_view(d) if not self.is_multisig(d) else 'multisig_cosigner_keystore',\n                'accept': self.maybe_master_pubkey,\n                'last': lambda d: self.is_single_password() and not self.is_multisig(d)\n            },\n            'have_master_key': {\n                'next': lambda d: self.wallet_password_view(d) if not self.is_multisig(d) else 'multisig_cosigner_keystore',\n                'accept': self.maybe_master_pubkey,\n                'last': lambda d: self.is_single_password() and not self.is_multisig(d)\n            },\n            'multisig': {\n                'next': 'keystore_type'\n            },\n            'multisig_cosigner_keystore': {  # this view should set 'multisig_current_cosigner'\n                'next': self.on_cosigner_keystore_type\n            },\n            'multisig_cosigner_key': {\n                'next': lambda d: self.wallet_password_view(d) if self.last_cosigner(d) else 'multisig_cosigner_keystore',\n                'last': lambda d: self.is_single_password() and self.last_cosigner(d)\n            },\n            'multisig_cosigner_seed': {\n                'next': lambda d: 'multisig_cosigner_have_ext' if self.wants_ext(d) else self.on_have_cosigner_seed(d),\n                'last': lambda d: self.is_single_password() and self.last_cosigner(d) and not\n                                  (self.needs_derivation_path(d) or self.wants_ext(d)),\n            },\n            'multisig_cosigner_have_ext': {\n                'next': self.on_have_cosigner_seed,\n                'last': lambda d: self.is_single_password() and self.last_cosigner(d) and not self.needs_derivation_path(d)\n            },\n            'multisig_cosigner_hardware': {\n                'next': self.on_hardware_device,\n            },\n            'multisig_cosigner_script_and_derivation': {\n                'next': lambda d: self.wallet_password_view(d) if self.last_cosigner(d) else 'multisig_cosigner_keystore',\n                'last': lambda d: self.is_single_password() and self.last_cosigner(d)\n            },\n            'imported': {\n                'next': 'wallet_password',\n                'last': lambda d: self.is_single_password()\n            },\n            'wallet_password': {\n                'last': True\n            },\n            'wallet_password_hardware': {\n                'last': True\n            }\n        }\n        self._daemon = daemon\n        self.plugins = plugins\n        # todo: load only if needed, like hw plugins\n        self.plugins.load_plugin_by_name('trustedcoin')\n\n    def start(self, *, start_viewstate: WizardViewState = None) -> WizardViewState:\n        self.reset()\n        if start_viewstate is None:\n            start_view = 'wallet_name'\n            params = self.navmap[start_view].get('params', {})\n            self._current = WizardViewState(start_view, {}, params)\n        else:\n            self._current = start_viewstate\n        return self._current\n\n    def is_single_password(self) -> bool:\n        raise NotImplementedError()\n\n    def on_wallet_type(self, wizard_data: dict) -> str:\n        t = wizard_data['wallet_type']\n        return {\n            'standard': 'keystore_type',\n            '2fa': 'trustedcoin_start',\n            'multisig': 'multisig',\n            'imported': 'imported'\n        }.get(t)\n\n    def on_keystore_type(self, wizard_data: dict) -> str:\n        t = wizard_data['keystore_type']\n        return {\n            'createseed': 'create_seed',\n            'haveseed': 'have_seed',\n            'masterkey': 'have_master_key',\n            'hardware': 'choose_hardware_device'\n        }.get(t)\n\n    def on_have_or_confirm_seed(self, wizard_data: dict) -> str:\n        if self.needs_derivation_path(wizard_data):\n            return 'script_and_derivation'\n        elif self.is_multisig(wizard_data):\n            return 'multisig_cosigner_keystore'\n        else:\n            return 'wallet_password'\n\n    def maybe_master_pubkey(self, wizard_data: dict):\n        self._logger.debug('maybe_master_pubkey')\n        if self.needs_derivation_path(wizard_data) and 'derivation_path' not in wizard_data:\n            self._logger.debug('deferred, missing derivation_path')\n            return\n\n        wizard_data['multisig_master_pubkey'] = self.keystore_from_data(wizard_data['wallet_type'], wizard_data).get_master_public_key()\n\n    def on_cosigner_keystore_type(self, wizard_data: dict) -> str:\n        t = wizard_data['cosigner_keystore_type']\n        return {\n            'masterkey': 'multisig_cosigner_key',\n            'haveseed': 'multisig_cosigner_seed',\n            'hardware': 'multisig_cosigner_hardware'\n        }.get(t)\n\n    def on_have_cosigner_seed(self, wizard_data: dict) -> str:\n        current_cosigner = self.current_cosigner(wizard_data)\n        if self.needs_derivation_path(wizard_data) and 'derivation_path' not in current_cosigner:\n            return 'multisig_cosigner_script_and_derivation'\n        elif self.last_cosigner(wizard_data):\n            return 'wallet_password'\n        else:\n            return 'multisig_cosigner_keystore'\n\n    def last_cosigner(self, wizard_data: dict) -> bool:\n        # check if we have the final number of cosigners. Doesn't check if cosigner data itself is complete\n        # (should be validated by wizardcomponents)\n        if not self.is_multisig(wizard_data):\n            return True\n\n        if len(wizard_data['multisig_cosigner_data']) < (wizard_data['multisig_participants'] - 1):\n            return False\n\n        return True\n\n    def has_duplicate_masterkeys(self, wizard_data: dict) -> bool:\n        \"\"\"Multisig wallets need distinct master keys. If True, need to prevent wallet-creation.\"\"\"\n        xpubs = [self.keystore_from_data(wizard_data['wallet_type'], wizard_data).get_master_public_key()]\n        for cosigner in wizard_data['multisig_cosigner_data']:\n            data = wizard_data['multisig_cosigner_data'][cosigner]\n            xpubs.append(self.keystore_from_data(wizard_data['wallet_type'], data).get_master_public_key())\n        assert xpubs\n        return len(xpubs) != len(set(xpubs))\n\n    def has_heterogeneous_masterkeys(self, wizard_data: dict) -> bool:\n        \"\"\"Multisig wallets need homogeneous master keys.\n        All master keys need to be bip32, and e.g. Ypub cannot be mixed with Zpub.\n        If True, need to prevent wallet-creation.\n        \"\"\"\n        xpubs = [self.keystore_from_data(wizard_data['wallet_type'], wizard_data).get_master_public_key()]\n        for cosigner in wizard_data['multisig_cosigner_data']:\n            data = wizard_data['multisig_cosigner_data'][cosigner]\n            xpubs.append(self.keystore_from_data(wizard_data['wallet_type'], data).get_master_public_key())\n        assert xpubs\n        try:\n            k_xpub_type = xpub_type(xpubs[0])\n        except Exception:\n            return True  # maybe old_mpk?\n        for xpub in xpubs:\n            try:\n                my_xpub_type = xpub_type(xpub)\n            except Exception:\n                return True  # maybe old_mpk?\n            if my_xpub_type != k_xpub_type:\n                return True\n        return False\n\n    def is_current_cosigner_hardware(self, wizard_data: dict) -> bool:\n        cosigner_data = self.current_cosigner(wizard_data)\n        cosigner_is_hardware = cosigner_data == wizard_data and wizard_data['keystore_type'] == 'hardware'\n        if 'cosigner_keystore_type' in wizard_data and wizard_data['cosigner_keystore_type'] == 'hardware':\n            cosigner_is_hardware = True\n        return cosigner_is_hardware\n\n    def check_multisig_constraints(self, wizard_data: dict) -> Tuple[bool, str]:\n        if not self.is_multisig(wizard_data):\n            return True, ''\n\n        # current cosigner might be incomplete. In that case, return valid\n        cosigner_data = self.current_cosigner(wizard_data)\n        if self.needs_derivation_path(wizard_data):\n            if 'derivation_path' not in cosigner_data:\n                self._logger.debug('defer multisig check: missing derivation_path')\n                return True, ''\n        if self.wants_ext(wizard_data):\n            if 'seed_extra_words' not in cosigner_data:\n                self._logger.debug('defer multisig check: missing extra words')\n                return True, ''\n        if self.is_current_cosigner_hardware(wizard_data):\n            if 'master_key' not in cosigner_data:\n                self._logger.debug('defer multisig check: missing master_key')\n                return True, ''\n\n        user_info = ''\n\n        if self.has_duplicate_masterkeys(wizard_data):\n            self._logger.debug('Duplicate master keys!')\n            user_info = _('Duplicate master keys')\n            multisig_keys_valid = False\n        elif self.has_heterogeneous_masterkeys(wizard_data):\n            self._logger.debug('Heterogenous master keys!')\n            user_info = _('Heterogenous master keys')\n            multisig_keys_valid = False\n        else:\n            multisig_keys_valid = True\n\n        return multisig_keys_valid, user_info\n\n    def validate_master_key(self, key: str, wallet_type: str):\n        # TODO: deduplicate with master key check in create_storage()\n        validation_message = ''\n        key_valid = False\n\n        if not keystore.is_master_key(key):\n            validation_message = _('Not a master key')\n        else:\n            k = keystore.from_master_key(key)\n            if wallet_type == 'standard':\n                if isinstance(k, keystore.Xpub):  # has bip32 xpub\n                    t1 = xpub_type(k.xpub)\n                    if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:  # disallow Ypub/Zpub\n                        validation_message = '%s: %s' % (_('Wrong key type'), t1)\n                    else:\n                        key_valid = True\n                elif isinstance(k, keystore.Old_KeyStore):\n                    key_valid = True\n                else:\n                    self._logger.error(f\"unexpected keystore type: {type(keystore)}\")\n            elif wallet_type == 'multisig':\n                if not isinstance(k, keystore.Xpub):  # old mpk?\n                    validation_message = '%s: %s' % (_('Wrong key type'), \"not bip32\")\n                t1 = xpub_type(k.xpub)\n                if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']:  # disallow ypub/zpub\n                    validation_message = '%s: %s' % (_('Wrong key type'), t1)\n                else:\n                    key_valid = True\n            else:\n                validation_message = '%s: %s' % (_('Unsupported wallet type'), wallet_type)\n                self._logger.error(f'Unsupported wallet type: {wallet_type}')\n\n        return key_valid, validation_message\n\n    def create_storage(self, path: str, data: dict):\n        assert data['wallet_type'] in ['standard', '2fa', 'imported', 'multisig']\n\n        if os.path.exists(path):\n            raise UserFacingException(_('File already exists at path: {}').format(path))\n        try:\n            storage = WalletStorage(path)\n        except StorageReadWriteError as e:\n            raise UserFacingException(e)\n\n        # TODO: refactor using self.keystore_from_data\n        k = None\n        if 'keystore_type' not in data:\n            assert data['wallet_type'] == 'imported'\n            addresses = {}\n            if 'private_key_list' in data:\n                k = keystore.Imported_KeyStore({})\n                keys = keystore.get_private_keys(data['private_key_list'])\n                for pk in keys:\n                    assert bitcoin.is_private_key(pk)\n                    txin_type, pubkey = k.import_privkey(pk, None)\n                    addr = bitcoin.pubkey_to_address(txin_type, pubkey)\n                    addresses[addr] = {'type': txin_type, 'pubkey': pubkey}\n            elif 'address_list' in data:\n                for addr in data['address_list'].split():\n                    assert isinstance(addr, str)\n                    assert bitcoin.is_address(addr), f\"expected bitcoin addr. got {addr[:5] + '..' + addr[-2:]}\"\n                    # note: we do not normalize addresses. :/\n                    #       In particular, bech32 addresses can be either all-lowercase or all-uppercase.\n                    #       TODO we should normalize them, but it only makes sense if we also do a walletDB-upgrade.\n                    addresses[addr] = {}\n        elif data['keystore_type'] in ['createseed', 'haveseed']:\n            seed_extension = data['seed_extra_words'] if data['seed_extend'] else ''\n            if data['seed_type'] in ['old', 'standard', 'segwit']:\n                self._logger.debug('creating keystore from electrum seed')\n                k = keystore.from_seed(data['seed'], passphrase=seed_extension, for_multisig=data['wallet_type'] == 'multisig')\n            elif data['seed_type'] in ['bip39', 'slip39']:\n                self._logger.debug('creating keystore from %s seed' % data['seed_type'])\n                if data['seed_type'] == 'bip39':\n                    root_seed = keystore.bip39_to_seed(data['seed'], passphrase=seed_extension)\n                else:\n                    root_seed = data['seed'].decrypt(seed_extension)\n                derivation = normalize_bip32_derivation(data['derivation_path'])\n                if data['wallet_type'] == 'multisig':\n                    script = data['script_type'] if data['script_type'] != 'p2sh' else 'standard'\n                else:\n                    script = data['script_type'] if data['script_type'] != 'p2pkh' else 'standard'\n                k = keystore.from_bip43_rootseed(root_seed, derivation=derivation, xtype=script)\n            elif is_any_2fa_seed_type(data['seed_type']):\n                self._logger.debug('creating keystore from 2fa seed')\n                k = keystore.from_xprv(data['x1']['xprv'])\n            else:\n                raise NotImplementedError('unsupported/unknown seed_type %s' % data['seed_type'])\n        elif data['keystore_type'] == 'masterkey':\n            k = keystore.from_master_key(data['master_key'])\n            if isinstance(k, keystore.Xpub):  # has xpub\n                t1 = xpub_type(k.xpub)\n                if data['wallet_type'] == 'multisig':\n                    if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']:\n                        raise UserFacingException(_('Wrong key type {}').format(t1))\n                else:\n                    if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:\n                        raise UserFacingException(_('Wrong key type {}').format(t1))\n            elif isinstance(k, keystore.Old_KeyStore):\n                pass\n            else:\n                raise TypeError(f'unexpected keystore type: {type(k)}')\n        elif data['keystore_type'] == 'hardware':\n            k = self.hw_keystore(data)\n            if isinstance(k, keystore.Xpub):  # has xpub\n                t1 = xpub_type(k.xpub)\n                if data['wallet_type'] == 'multisig':\n                    if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']:\n                        raise UserFacingException(_('Wrong key type {}').format(t1))\n                else:\n                    if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:\n                        raise UserFacingException(_('Wrong key type {}').format(t1))\n            else:\n                raise TypeError(f'unexpected keystore type: {type(k)}')\n        else:\n            raise NotImplementedError('unsupported/unknown keystore_type %s' % data['keystore_type'])\n\n        if data['password']:\n            if k and k.may_have_password():\n                k.update_password(None, data['password'])\n\n        if data['encrypt']:\n            if data.get('xpub_encrypt'):\n                assert data.get('keystore_type') == 'hardware' and data['wallet_type'] == 'standard'\n                enc_version = StorageEncryptionVersion.XPUB_PASSWORD\n            else:\n                enc_version = StorageEncryptionVersion.USER_PASSWORD\n            storage.set_password(data['password'], enc_version=enc_version)\n\n        db = WalletDB('', storage=storage, upgrade=True)\n        db.set_keystore_encryption(bool(data['password']))\n\n        db.put('wallet_type', data['wallet_type'])\n\n        if data['wallet_type'] == 'standard':\n            db.put('keystore', k.dump())\n        elif data['wallet_type'] == '2fa':\n            db.put('x1', k.dump())\n            if 'trustedcoin_keepordisable' in data and data['trustedcoin_keepordisable'] == 'disable':\n                k2 = keystore.from_xprv(data['x2']['xprv'])\n                if data['encrypt'] and k2.may_have_password():\n                    k2.update_password(None, data['password'])\n                db.put('x2', k2.dump())\n            else:\n                db.put('x2', data['x2'])\n            if 'x3' in data:\n                db.put('x3', data['x3'])\n            db.put('use_trustedcoin', True)\n        elif data['wallet_type'] == 'multisig':\n            if not isinstance(k, keystore.Xpub):\n                raise TypeError(f'unexpected keystore(main) type={type(k)} in multisig. not bip32.')\n            k_xpub_type = xpub_type(k.xpub)\n            db.put('wallet_type', '%dof%d' % (data['multisig_signatures'], data['multisig_participants']))\n            db.put('x1', k.dump())\n            for cosigner in data['multisig_cosigner_data']:\n                cosigner_keystore = self.keystore_from_data('multisig', data['multisig_cosigner_data'][cosigner])\n                if not isinstance(cosigner_keystore, keystore.Xpub):\n                    raise TypeError(f'unexpected keystore(cosigner) type={type(cosigner_keystore)} in multisig. not bip32.')\n                if k_xpub_type != xpub_type(cosigner_keystore.xpub):\n                    raise UserFacingException(_('Multisig wallet needs to have homogeneous xpub types.'))\n                if data['encrypt'] and cosigner_keystore.may_have_password():\n                    cosigner_keystore.update_password(None, data['password'])\n                db.put(f'x{cosigner}', cosigner_keystore.dump())\n        elif data['wallet_type'] == 'imported':\n            if k:\n                db.put('keystore', k.dump())\n            db.put('addresses', addresses)\n\n        if k and k.can_have_deterministic_lightning_xprv():\n            db.put('lightning_xprv', k.get_lightning_xprv(data['password']))\n\n        db.load_plugins()\n        db.write()\n\n\nclass ServerConnectWizard(AbstractWizard):\n\n    _logger = get_logger(__name__)\n\n    def __init__(self, daemon: 'Daemon'):\n        AbstractWizard.__init__(self)\n        self.navmap = {\n            'welcome': {\n                'next': lambda d: 'proxy_config' if d['want_proxy'] else 'server_config',\n                'accept': lambda d: self.do_enable_autoconnect(d) if d['autoconnect'] else None,\n                'last': lambda d: bool(d['autoconnect'] and not d['want_proxy'])\n            },\n            'proxy_config': {\n                'next': 'server_config',\n                'accept': self.do_configure_proxy,\n                'last': lambda d: bool(d['autoconnect'])\n            },\n            'server_config': {\n                'accept': self.do_configure_server,\n                'last': True\n            }\n        }\n        self._daemon = daemon\n\n    def do_configure_proxy(self, wizard_data: dict):\n        proxy_settings = wizard_data['proxy']\n        if not self._daemon.network:\n            self._logger.debug('not configuring proxy, electrum config wants offline mode')\n            return\n        self._logger.debug(f'configuring proxy: {proxy_settings!r}')\n        net_params = self._daemon.network.get_parameters()\n        proxy = ProxySettings.from_dict(proxy_settings)\n        net_params = net_params._replace(proxy=proxy, auto_connect=bool(wizard_data['autoconnect']))\n        self._daemon.network.run_from_another_thread(self._daemon.network.set_parameters(net_params))\n\n    def do_configure_server(self, wizard_data: dict):\n        self._logger.debug(f'configuring server: {wizard_data!r}')\n        net_params = self._daemon.network.get_parameters()\n        server = None\n        oneserver = wizard_data.get('one_server', False)\n        if not wizard_data['autoconnect']:\n            server = ServerAddr.from_str_with_inference(wizard_data.get('server', ''))\n            if not server:\n                self._logger.warn('failed to parse server %s' % wizard_data.get('server', ''))\n                return  # Network._start() will set autoconnect and default server\n        net_params = net_params._replace(\n            server=server or net_params.server,\n            auto_connect=wizard_data['autoconnect'],\n            oneserver=oneserver,\n        )\n        self._daemon.network.run_from_another_thread(self._daemon.network.set_parameters(net_params))\n\n    def do_enable_autoconnect(self, wizard_data: dict):\n        # NETWORK_AUTO_CONNECT will only get explicitly set True, 'autoconnect': False means\n        # the user requested manual server configuration\n        self._logger.debug(f'enabling autoconnect: {wizard_data!r}')\n        assert wizard_data.get('autoconnect'), wizard_data\n        if self._daemon.config.cv.NETWORK_AUTO_CONNECT.is_modifiable():\n            self._daemon.config.NETWORK_AUTO_CONNECT = True\n\n    def start(self, *, start_viewstate: WizardViewState = None) -> WizardViewState:\n        self.reset()\n        if start_viewstate is None:\n            start_view = 'welcome'\n            params = self.navmap[start_view].get('params', {})\n            self._current = WizardViewState(start_view, {}, params)\n        else:\n            self._current = start_viewstate\n        return self._current\n\n\nclass TermsOfUseWizard(AbstractWizard):\n\n    _logger = get_logger(__name__)\n\n    def __init__(self, config: 'SimpleConfig'):\n        AbstractWizard.__init__(self)\n        self._config = config\n        self.navmap = {\n            'terms_of_use': {\n                'accept': self.accept_terms_of_use,\n                'last': True,\n            },\n        }\n\n    def accept_terms_of_use(self, _):\n        self._config.TERMS_OF_USE_ACCEPTED = TERMS_OF_USE_LATEST_VERSION\n\n    def start(self, *, start_viewstate: WizardViewState = None) -> WizardViewState:\n        self.reset()\n        if start_viewstate is None:\n            start_view = 'terms_of_use'\n            params = self.navmap[start_view].get('params', {})\n            self._current = WizardViewState(start_view, {}, params)\n        else:\n            self._current = start_viewstate\n        return self._current\n"
  },
  {
    "path": "electrum/wordlist/chinese_simplified.txt",
    "content": "的\n一\n是\n在\n不\n了\n有\n和\n人\n这\n中\n大\n为\n上\n个\n国\n我\n以\n要\n他\n时\n来\n用\n们\n生\n到\n作\n地\n于\n出\n就\n分\n对\n成\n会\n可\n主\n发\n年\n动\n同\n工\n也\n能\n下\n过\n子\n说\n产\n种\n面\n而\n方\n后\n多\n定\n行\n学\n法\n所\n民\n得\n经\n十\n三\n之\n进\n着\n等\n部\n度\n家\n电\n力\n里\n如\n水\n化\n高\n自\n二\n理\n起\n小\n物\n现\n实\n加\n量\n都\n两\n体\n制\n机\n当\n使\n点\n从\n业\n本\n去\n把\n性\n好\n应\n开\n它\n合\n还\n因\n由\n其\n些\n然\n前\n外\n天\n政\n四\n日\n那\n社\n义\n事\n平\n形\n相\n全\n表\n间\n样\n与\n关\n各\n重\n新\n线\n内\n数\n正\n心\n反\n你\n明\n看\n原\n又\n么\n利\n比\n或\n但\n质\n气\n第\n向\n道\n命\n此\n变\n条\n只\n没\n结\n解\n问\n意\n建\n月\n公\n无\n系\n军\n很\n情\n者\n最\n立\n代\n想\n已\n通\n并\n提\n直\n题\n党\n程\n展\n五\n果\n料\n象\n员\n革\n位\n入\n常\n文\n总\n次\n品\n式\n活\n设\n及\n管\n特\n件\n长\n求\n老\n头\n基\n资\n边\n流\n路\n级\n少\n图\n山\n统\n接\n知\n较\n将\n组\n见\n计\n别\n她\n手\n角\n期\n根\n论\n运\n农\n指\n几\n九\n区\n强\n放\n决\n西\n被\n干\n做\n必\n战\n先\n回\n则\n任\n取\n据\n处\n队\n南\n给\n色\n光\n门\n即\n保\n治\n北\n造\n百\n规\n热\n领\n七\n海\n口\n东\n导\n器\n压\n志\n世\n金\n增\n争\n济\n阶\n油\n思\n术\n极\n交\n受\n联\n什\n认\n六\n共\n权\n收\n证\n改\n清\n美\n再\n采\n转\n更\n单\n风\n切\n打\n白\n教\n速\n花\n带\n安\n场\n身\n车\n例\n真\n务\n具\n万\n每\n目\n至\n达\n走\n积\n示\n议\n声\n报\n斗\n完\n类\n八\n离\n华\n名\n确\n才\n科\n张\n信\n马\n节\n话\n米\n整\n空\n元\n况\n今\n集\n温\n传\n土\n许\n步\n群\n广\n石\n记\n需\n段\n研\n界\n拉\n林\n律\n叫\n且\n究\n观\n越\n织\n装\n影\n算\n低\n持\n音\n众\n书\n布\n复\n容\n儿\n须\n际\n商\n非\n验\n连\n断\n深\n难\n近\n矿\n千\n周\n委\n素\n技\n备\n半\n办\n青\n省\n列\n习\n响\n约\n支\n般\n史\n感\n劳\n便\n团\n往\n酸\n历\n市\n克\n何\n除\n消\n构\n府\n称\n太\n准\n精\n值\n号\n率\n族\n维\n划\n选\n标\n写\n存\n候\n毛\n亲\n快\n效\n斯\n院\n查\n江\n型\n眼\n王\n按\n格\n养\n易\n置\n派\n层\n片\n始\n却\n专\n状\n育\n厂\n京\n识\n适\n属\n圆\n包\n火\n住\n调\n满\n县\n局\n照\n参\n红\n细\n引\n听\n该\n铁\n价\n严\n首\n底\n液\n官\n德\n随\n病\n苏\n失\n尔\n死\n讲\n配\n女\n黄\n推\n显\n谈\n罪\n神\n艺\n呢\n席\n含\n企\n望\n密\n批\n营\n项\n防\n举\n球\n英\n氧\n势\n告\n李\n台\n落\n木\n帮\n轮\n破\n亚\n师\n围\n注\n远\n字\n材\n排\n供\n河\n态\n封\n另\n施\n减\n树\n溶\n怎\n止\n案\n言\n士\n均\n武\n固\n叶\n鱼\n波\n视\n仅\n费\n紧\n爱\n左\n章\n早\n朝\n害\n续\n轻\n服\n试\n食\n充\n兵\n源\n判\n护\n司\n足\n某\n练\n差\n致\n板\n田\n降\n黑\n犯\n负\n击\n范\n继\n兴\n似\n余\n坚\n曲\n输\n修\n故\n城\n夫\n够\n送\n笔\n船\n占\n右\n财\n吃\n富\n春\n职\n觉\n汉\n画\n功\n巴\n跟\n虽\n杂\n飞\n检\n吸\n助\n升\n阳\n互\n初\n创\n抗\n考\n投\n坏\n策\n古\n径\n换\n未\n跑\n留\n钢\n曾\n端\n责\n站\n简\n述\n钱\n副\n尽\n帝\n射\n草\n冲\n承\n独\n令\n限\n阿\n宣\n环\n双\n请\n超\n微\n让\n控\n州\n良\n轴\n找\n否\n纪\n益\n依\n优\n顶\n础\n载\n倒\n房\n突\n坐\n粉\n敌\n略\n客\n袁\n冷\n胜\n绝\n析\n块\n剂\n测\n丝\n协\n诉\n念\n陈\n仍\n罗\n盐\n友\n洋\n错\n苦\n夜\n刑\n移\n频\n逐\n靠\n混\n母\n短\n皮\n终\n聚\n汽\n村\n云\n哪\n既\n距\n卫\n停\n烈\n央\n察\n烧\n迅\n境\n若\n印\n洲\n刻\n括\n激\n孔\n搞\n甚\n室\n待\n核\n校\n散\n侵\n吧\n甲\n游\n久\n菜\n味\n旧\n模\n湖\n货\n损\n预\n阻\n毫\n普\n稳\n乙\n妈\n植\n息\n扩\n银\n语\n挥\n酒\n守\n拿\n序\n纸\n医\n缺\n雨\n吗\n针\n刘\n啊\n急\n唱\n误\n训\n愿\n审\n附\n获\n茶\n鲜\n粮\n斤\n孩\n脱\n硫\n肥\n善\n龙\n演\n父\n渐\n血\n欢\n械\n掌\n歌\n沙\n刚\n攻\n谓\n盾\n讨\n晚\n粒\n乱\n燃\n矛\n乎\n杀\n药\n宁\n鲁\n贵\n钟\n煤\n读\n班\n伯\n香\n介\n迫\n句\n丰\n培\n握\n兰\n担\n弦\n蛋\n沉\n假\n穿\n执\n答\n乐\n谁\n顺\n烟\n缩\n征\n脸\n喜\n松\n脚\n困\n异\n免\n背\n星\n福\n买\n染\n井\n概\n慢\n怕\n磁\n倍\n祖\n皇\n促\n静\n补\n评\n翻\n肉\n践\n尼\n衣\n宽\n扬\n棉\n希\n伤\n操\n垂\n秋\n宜\n氢\n套\n督\n振\n架\n亮\n末\n宪\n庆\n编\n牛\n触\n映\n雷\n销\n诗\n座\n居\n抓\n裂\n胞\n呼\n娘\n景\n威\n绿\n晶\n厚\n盟\n衡\n鸡\n孙\n延\n危\n胶\n屋\n乡\n临\n陆\n顾\n掉\n呀\n灯\n岁\n措\n束\n耐\n剧\n玉\n赵\n跳\n哥\n季\n课\n凯\n胡\n额\n款\n绍\n卷\n齐\n伟\n蒸\n殖\n永\n宗\n苗\n川\n炉\n岩\n弱\n零\n杨\n奏\n沿\n露\n杆\n探\n滑\n镇\n饭\n浓\n航\n怀\n赶\n库\n夺\n伊\n灵\n税\n途\n灭\n赛\n归\n召\n鼓\n播\n盘\n裁\n险\n康\n唯\n录\n菌\n纯\n借\n糖\n盖\n横\n符\n私\n努\n堂\n域\n枪\n润\n幅\n哈\n竟\n熟\n虫\n泽\n脑\n壤\n碳\n欧\n遍\n侧\n寨\n敢\n彻\n虑\n斜\n薄\n庭\n纳\n弹\n饲\n伸\n折\n麦\n湿\n暗\n荷\n瓦\n塞\n床\n筑\n恶\n户\n访\n塔\n奇\n透\n梁\n刀\n旋\n迹\n卡\n氯\n遇\n份\n毒\n泥\n退\n洗\n摆\n灰\n彩\n卖\n耗\n夏\n择\n忙\n铜\n献\n硬\n予\n繁\n圈\n雪\n函\n亦\n抽\n篇\n阵\n阴\n丁\n尺\n追\n堆\n雄\n迎\n泛\n爸\n楼\n避\n谋\n吨\n野\n猪\n旗\n累\n偏\n典\n馆\n索\n秦\n脂\n潮\n爷\n豆\n忽\n托\n惊\n塑\n遗\n愈\n朱\n替\n纤\n粗\n倾\n尚\n痛\n楚\n谢\n奋\n购\n磨\n君\n池\n旁\n碎\n骨\n监\n捕\n弟\n暴\n割\n贯\n殊\n释\n词\n亡\n壁\n顿\n宝\n午\n尘\n闻\n揭\n炮\n残\n冬\n桥\n妇\n警\n综\n招\n吴\n付\n浮\n遭\n徐\n您\n摇\n谷\n赞\n箱\n隔\n订\n男\n吹\n园\n纷\n唐\n败\n宋\n玻\n巨\n耕\n坦\n荣\n闭\n湾\n键\n凡\n驻\n锅\n救\n恩\n剥\n凝\n碱\n齿\n截\n炼\n麻\n纺\n禁\n废\n盛\n版\n缓\n净\n睛\n昌\n婚\n涉\n筒\n嘴\n插\n岸\n朗\n庄\n街\n藏\n姑\n贸\n腐\n奴\n啦\n惯\n乘\n伙\n恢\n匀\n纱\n扎\n辩\n耳\n彪\n臣\n亿\n璃\n抵\n脉\n秀\n萨\n俄\n网\n舞\n店\n喷\n纵\n寸\n汗\n挂\n洪\n贺\n闪\n柬\n爆\n烯\n津\n稻\n墙\n软\n勇\n像\n滚\n厘\n蒙\n芳\n肯\n坡\n柱\n荡\n腿\n仪\n旅\n尾\n轧\n冰\n贡\n登\n黎\n削\n钻\n勒\n逃\n障\n氨\n郭\n峰\n币\n港\n伏\n轨\n亩\n毕\n擦\n莫\n刺\n浪\n秘\n援\n株\n健\n售\n股\n岛\n甘\n泡\n睡\n童\n铸\n汤\n阀\n休\n汇\n舍\n牧\n绕\n炸\n哲\n磷\n绩\n朋\n淡\n尖\n启\n陷\n柴\n呈\n徒\n颜\n泪\n稍\n忘\n泵\n蓝\n拖\n洞\n授\n镜\n辛\n壮\n锋\n贫\n虚\n弯\n摩\n泰\n幼\n廷\n尊\n窗\n纲\n弄\n隶\n疑\n氏\n宫\n姐\n震\n瑞\n怪\n尤\n琴\n循\n描\n膜\n违\n夹\n腰\n缘\n珠\n穷\n森\n枝\n竹\n沟\n催\n绳\n忆\n邦\n剩\n幸\n浆\n栏\n拥\n牙\n贮\n礼\n滤\n钠\n纹\n罢\n拍\n咱\n喊\n袖\n埃\n勤\n罚\n焦\n潜\n伍\n墨\n欲\n缝\n姓\n刊\n饱\n仿\n奖\n铝\n鬼\n丽\n跨\n默\n挖\n链\n扫\n喝\n袋\n炭\n污\n幕\n诸\n弧\n励\n梅\n奶\n洁\n灾\n舟\n鉴\n苯\n讼\n抱\n毁\n懂\n寒\n智\n埔\n寄\n届\n跃\n渡\n挑\n丹\n艰\n贝\n碰\n拔\n爹\n戴\n码\n梦\n芽\n熔\n赤\n渔\n哭\n敬\n颗\n奔\n铅\n仲\n虎\n稀\n妹\n乏\n珍\n申\n桌\n遵\n允\n隆\n螺\n仓\n魏\n锐\n晓\n氮\n兼\n隐\n碍\n赫\n拨\n忠\n肃\n缸\n牵\n抢\n博\n巧\n壳\n兄\n杜\n讯\n诚\n碧\n祥\n柯\n页\n巡\n矩\n悲\n灌\n龄\n伦\n票\n寻\n桂\n铺\n圣\n恐\n恰\n郑\n趣\n抬\n荒\n腾\n贴\n柔\n滴\n猛\n阔\n辆\n妻\n填\n撤\n储\n签\n闹\n扰\n紫\n砂\n递\n戏\n吊\n陶\n伐\n喂\n疗\n瓶\n婆\n抚\n臂\n摸\n忍\n虾\n蜡\n邻\n胸\n巩\n挤\n偶\n弃\n槽\n劲\n乳\n邓\n吉\n仁\n烂\n砖\n租\n乌\n舰\n伴\n瓜\n浅\n丙\n暂\n燥\n橡\n柳\n迷\n暖\n牌\n秧\n胆\n详\n簧\n踏\n瓷\n谱\n呆\n宾\n糊\n洛\n辉\n愤\n竞\n隙\n怒\n粘\n乃\n绪\n肩\n籍\n敏\n涂\n熙\n皆\n侦\n悬\n掘\n享\n纠\n醒\n狂\n锁\n淀\n恨\n牲\n霸\n爬\n赏\n逆\n玩\n陵\n祝\n秒\n浙\n貌\n役\n彼\n悉\n鸭\n趋\n凤\n晨\n畜\n辈\n秩\n卵\n署\n梯\n炎\n滩\n棋\n驱\n筛\n峡\n冒\n啥\n寿\n译\n浸\n泉\n帽\n迟\n硅\n疆\n贷\n漏\n稿\n冠\n嫩\n胁\n芯\n牢\n叛\n蚀\n奥\n鸣\n岭\n羊\n凭\n串\n塘\n绘\n酵\n融\n盆\n锡\n庙\n筹\n冻\n辅\n摄\n袭\n筋\n拒\n僚\n旱\n钾\n鸟\n漆\n沈\n眉\n疏\n添\n棒\n穗\n硝\n韩\n逼\n扭\n侨\n凉\n挺\n碗\n栽\n炒\n杯\n患\n馏\n劝\n豪\n辽\n勃\n鸿\n旦\n吏\n拜\n狗\n埋\n辊\n掩\n饮\n搬\n骂\n辞\n勾\n扣\n估\n蒋\n绒\n雾\n丈\n朵\n姆\n拟\n宇\n辑\n陕\n雕\n偿\n蓄\n崇\n剪\n倡\n厅\n咬\n驶\n薯\n刷\n斥\n番\n赋\n奉\n佛\n浇\n漫\n曼\n扇\n钙\n桃\n扶\n仔\n返\n俗\n亏\n腔\n鞋\n棱\n覆\n框\n悄\n叔\n撞\n骗\n勘\n旺\n沸\n孤\n吐\n孟\n渠\n屈\n疾\n妙\n惜\n仰\n狠\n胀\n谐\n抛\n霉\n桑\n岗\n嘛\n衰\n盗\n渗\n脏\n赖\n涌\n甜\n曹\n阅\n肌\n哩\n厉\n烃\n纬\n毅\n昨\n伪\n症\n煮\n叹\n钉\n搭\n茎\n笼\n酷\n偷\n弓\n锥\n恒\n杰\n坑\n鼻\n翼\n纶\n叙\n狱\n逮\n罐\n络\n棚\n抑\n膨\n蔬\n寺\n骤\n穆\n冶\n枯\n册\n尸\n凸\n绅\n坯\n牺\n焰\n轰\n欣\n晋\n瘦\n御\n锭\n锦\n丧\n旬\n锻\n垄\n搜\n扑\n邀\n亭\n酯\n迈\n舒\n脆\n酶\n闲\n忧\n酚\n顽\n羽\n涨\n卸\n仗\n陪\n辟\n惩\n杭\n姚\n肚\n捉\n飘\n漂\n昆\n欺\n吾\n郎\n烷\n汁\n呵\n饰\n萧\n雅\n邮\n迁\n燕\n撒\n姻\n赴\n宴\n烦\n债\n帐\n斑\n铃\n旨\n醇\n董\n饼\n雏\n姿\n拌\n傅\n腹\n妥\n揉\n贤\n拆\n歪\n葡\n胺\n丢\n浩\n徽\n昂\n垫\n挡\n览\n贪\n慰\n缴\n汪\n慌\n冯\n诺\n姜\n谊\n凶\n劣\n诬\n耀\n昏\n躺\n盈\n骑\n乔\n溪\n丛\n卢\n抹\n闷\n咨\n刮\n驾\n缆\n悟\n摘\n铒\n掷\n颇\n幻\n柄\n惠\n惨\n佳\n仇\n腊\n窝\n涤\n剑\n瞧\n堡\n泼\n葱\n罩\n霍\n捞\n胎\n苍\n滨\n俩\n捅\n湘\n砍\n霞\n邵\n萄\n疯\n淮\n遂\n熊\n粪\n烘\n宿\n档\n戈\n驳\n嫂\n裕\n徙\n箭\n捐\n肠\n撑\n晒\n辨\n殿\n莲\n摊\n搅\n酱\n屏\n疫\n哀\n蔡\n堵\n沫\n皱\n畅\n叠\n阁\n莱\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": "electrum/wordlist/english.txt",
    "content": "abandon\nability\nable\nabout\nabove\nabsent\nabsorb\nabstract\nabsurd\nabuse\naccess\naccident\naccount\naccuse\nachieve\nacid\nacoustic\nacquire\nacross\nact\naction\nactor\nactress\nactual\nadapt\nadd\naddict\naddress\nadjust\nadmit\nadult\nadvance\nadvice\naerobic\naffair\nafford\nafraid\nagain\nage\nagent\nagree\nahead\naim\nair\nairport\naisle\nalarm\nalbum\nalcohol\nalert\nalien\nall\nalley\nallow\nalmost\nalone\nalpha\nalready\nalso\nalter\nalways\namateur\namazing\namong\namount\namused\nanalyst\nanchor\nancient\nanger\nangle\nangry\nanimal\nankle\nannounce\nannual\nanother\nanswer\nantenna\nantique\nanxiety\nany\napart\napology\nappear\napple\napprove\napril\narch\narctic\narea\narena\nargue\narm\narmed\narmor\narmy\naround\narrange\narrest\narrive\narrow\nart\nartefact\nartist\nartwork\nask\naspect\nassault\nasset\nassist\nassume\nasthma\nathlete\natom\nattack\nattend\nattitude\nattract\nauction\naudit\naugust\naunt\nauthor\nauto\nautumn\naverage\navocado\navoid\nawake\naware\naway\nawesome\nawful\nawkward\naxis\nbaby\nbachelor\nbacon\nbadge\nbag\nbalance\nbalcony\nball\nbamboo\nbanana\nbanner\nbar\nbarely\nbargain\nbarrel\nbase\nbasic\nbasket\nbattle\nbeach\nbean\nbeauty\nbecause\nbecome\nbeef\nbefore\nbegin\nbehave\nbehind\nbelieve\nbelow\nbelt\nbench\nbenefit\nbest\nbetray\nbetter\nbetween\nbeyond\nbicycle\nbid\nbike\nbind\nbiology\nbird\nbirth\nbitter\nblack\nblade\nblame\nblanket\nblast\nbleak\nbless\nblind\nblood\nblossom\nblouse\nblue\nblur\nblush\nboard\nboat\nbody\nboil\nbomb\nbone\nbonus\nbook\nboost\nborder\nboring\nborrow\nboss\nbottom\nbounce\nbox\nboy\nbracket\nbrain\nbrand\nbrass\nbrave\nbread\nbreeze\nbrick\nbridge\nbrief\nbright\nbring\nbrisk\nbroccoli\nbroken\nbronze\nbroom\nbrother\nbrown\nbrush\nbubble\nbuddy\nbudget\nbuffalo\nbuild\nbulb\nbulk\nbullet\nbundle\nbunker\nburden\nburger\nburst\nbus\nbusiness\nbusy\nbutter\nbuyer\nbuzz\ncabbage\ncabin\ncable\ncactus\ncage\ncake\ncall\ncalm\ncamera\ncamp\ncan\ncanal\ncancel\ncandy\ncannon\ncanoe\ncanvas\ncanyon\ncapable\ncapital\ncaptain\ncar\ncarbon\ncard\ncargo\ncarpet\ncarry\ncart\ncase\ncash\ncasino\ncastle\ncasual\ncat\ncatalog\ncatch\ncategory\ncattle\ncaught\ncause\ncaution\ncave\nceiling\ncelery\ncement\ncensus\ncentury\ncereal\ncertain\nchair\nchalk\nchampion\nchange\nchaos\nchapter\ncharge\nchase\nchat\ncheap\ncheck\ncheese\nchef\ncherry\nchest\nchicken\nchief\nchild\nchimney\nchoice\nchoose\nchronic\nchuckle\nchunk\nchurn\ncigar\ncinnamon\ncircle\ncitizen\ncity\ncivil\nclaim\nclap\nclarify\nclaw\nclay\nclean\nclerk\nclever\nclick\nclient\ncliff\nclimb\nclinic\nclip\nclock\nclog\nclose\ncloth\ncloud\nclown\nclub\nclump\ncluster\nclutch\ncoach\ncoast\ncoconut\ncode\ncoffee\ncoil\ncoin\ncollect\ncolor\ncolumn\ncombine\ncome\ncomfort\ncomic\ncommon\ncompany\nconcert\nconduct\nconfirm\ncongress\nconnect\nconsider\ncontrol\nconvince\ncook\ncool\ncopper\ncopy\ncoral\ncore\ncorn\ncorrect\ncost\ncotton\ncouch\ncountry\ncouple\ncourse\ncousin\ncover\ncoyote\ncrack\ncradle\ncraft\ncram\ncrane\ncrash\ncrater\ncrawl\ncrazy\ncream\ncredit\ncreek\ncrew\ncricket\ncrime\ncrisp\ncritic\ncrop\ncross\ncrouch\ncrowd\ncrucial\ncruel\ncruise\ncrumble\ncrunch\ncrush\ncry\ncrystal\ncube\nculture\ncup\ncupboard\ncurious\ncurrent\ncurtain\ncurve\ncushion\ncustom\ncute\ncycle\ndad\ndamage\ndamp\ndance\ndanger\ndaring\ndash\ndaughter\ndawn\nday\ndeal\ndebate\ndebris\ndecade\ndecember\ndecide\ndecline\ndecorate\ndecrease\ndeer\ndefense\ndefine\ndefy\ndegree\ndelay\ndeliver\ndemand\ndemise\ndenial\ndentist\ndeny\ndepart\ndepend\ndeposit\ndepth\ndeputy\nderive\ndescribe\ndesert\ndesign\ndesk\ndespair\ndestroy\ndetail\ndetect\ndevelop\ndevice\ndevote\ndiagram\ndial\ndiamond\ndiary\ndice\ndiesel\ndiet\ndiffer\ndigital\ndignity\ndilemma\ndinner\ndinosaur\ndirect\ndirt\ndisagree\ndiscover\ndisease\ndish\ndismiss\ndisorder\ndisplay\ndistance\ndivert\ndivide\ndivorce\ndizzy\ndoctor\ndocument\ndog\ndoll\ndolphin\ndomain\ndonate\ndonkey\ndonor\ndoor\ndose\ndouble\ndove\ndraft\ndragon\ndrama\ndrastic\ndraw\ndream\ndress\ndrift\ndrill\ndrink\ndrip\ndrive\ndrop\ndrum\ndry\nduck\ndumb\ndune\nduring\ndust\ndutch\nduty\ndwarf\ndynamic\neager\neagle\nearly\nearn\nearth\neasily\neast\neasy\necho\necology\neconomy\nedge\nedit\neducate\neffort\negg\neight\neither\nelbow\nelder\nelectric\nelegant\nelement\nelephant\nelevator\nelite\nelse\nembark\nembody\nembrace\nemerge\nemotion\nemploy\nempower\nempty\nenable\nenact\nend\nendless\nendorse\nenemy\nenergy\nenforce\nengage\nengine\nenhance\nenjoy\nenlist\nenough\nenrich\nenroll\nensure\nenter\nentire\nentry\nenvelope\nepisode\nequal\nequip\nera\nerase\nerode\nerosion\nerror\nerupt\nescape\nessay\nessence\nestate\neternal\nethics\nevidence\nevil\nevoke\nevolve\nexact\nexample\nexcess\nexchange\nexcite\nexclude\nexcuse\nexecute\nexercise\nexhaust\nexhibit\nexile\nexist\nexit\nexotic\nexpand\nexpect\nexpire\nexplain\nexpose\nexpress\nextend\nextra\neye\neyebrow\nfabric\nface\nfaculty\nfade\nfaint\nfaith\nfall\nfalse\nfame\nfamily\nfamous\nfan\nfancy\nfantasy\nfarm\nfashion\nfat\nfatal\nfather\nfatigue\nfault\nfavorite\nfeature\nfebruary\nfederal\nfee\nfeed\nfeel\nfemale\nfence\nfestival\nfetch\nfever\nfew\nfiber\nfiction\nfield\nfigure\nfile\nfilm\nfilter\nfinal\nfind\nfine\nfinger\nfinish\nfire\nfirm\nfirst\nfiscal\nfish\nfit\nfitness\nfix\nflag\nflame\nflash\nflat\nflavor\nflee\nflight\nflip\nfloat\nflock\nfloor\nflower\nfluid\nflush\nfly\nfoam\nfocus\nfog\nfoil\nfold\nfollow\nfood\nfoot\nforce\nforest\nforget\nfork\nfortune\nforum\nforward\nfossil\nfoster\nfound\nfox\nfragile\nframe\nfrequent\nfresh\nfriend\nfringe\nfrog\nfront\nfrost\nfrown\nfrozen\nfruit\nfuel\nfun\nfunny\nfurnace\nfury\nfuture\ngadget\ngain\ngalaxy\ngallery\ngame\ngap\ngarage\ngarbage\ngarden\ngarlic\ngarment\ngas\ngasp\ngate\ngather\ngauge\ngaze\ngeneral\ngenius\ngenre\ngentle\ngenuine\ngesture\nghost\ngiant\ngift\ngiggle\nginger\ngiraffe\ngirl\ngive\nglad\nglance\nglare\nglass\nglide\nglimpse\nglobe\ngloom\nglory\nglove\nglow\nglue\ngoat\ngoddess\ngold\ngood\ngoose\ngorilla\ngospel\ngossip\ngovern\ngown\ngrab\ngrace\ngrain\ngrant\ngrape\ngrass\ngravity\ngreat\ngreen\ngrid\ngrief\ngrit\ngrocery\ngroup\ngrow\ngrunt\nguard\nguess\nguide\nguilt\nguitar\ngun\ngym\nhabit\nhair\nhalf\nhammer\nhamster\nhand\nhappy\nharbor\nhard\nharsh\nharvest\nhat\nhave\nhawk\nhazard\nhead\nhealth\nheart\nheavy\nhedgehog\nheight\nhello\nhelmet\nhelp\nhen\nhero\nhidden\nhigh\nhill\nhint\nhip\nhire\nhistory\nhobby\nhockey\nhold\nhole\nholiday\nhollow\nhome\nhoney\nhood\nhope\nhorn\nhorror\nhorse\nhospital\nhost\nhotel\nhour\nhover\nhub\nhuge\nhuman\nhumble\nhumor\nhundred\nhungry\nhunt\nhurdle\nhurry\nhurt\nhusband\nhybrid\nice\nicon\nidea\nidentify\nidle\nignore\nill\nillegal\nillness\nimage\nimitate\nimmense\nimmune\nimpact\nimpose\nimprove\nimpulse\ninch\ninclude\nincome\nincrease\nindex\nindicate\nindoor\nindustry\ninfant\ninflict\ninform\ninhale\ninherit\ninitial\ninject\ninjury\ninmate\ninner\ninnocent\ninput\ninquiry\ninsane\ninsect\ninside\ninspire\ninstall\nintact\ninterest\ninto\ninvest\ninvite\ninvolve\niron\nisland\nisolate\nissue\nitem\nivory\njacket\njaguar\njar\njazz\njealous\njeans\njelly\njewel\njob\njoin\njoke\njourney\njoy\njudge\njuice\njump\njungle\njunior\njunk\njust\nkangaroo\nkeen\nkeep\nketchup\nkey\nkick\nkid\nkidney\nkind\nkingdom\nkiss\nkit\nkitchen\nkite\nkitten\nkiwi\nknee\nknife\nknock\nknow\nlab\nlabel\nlabor\nladder\nlady\nlake\nlamp\nlanguage\nlaptop\nlarge\nlater\nlatin\nlaugh\nlaundry\nlava\nlaw\nlawn\nlawsuit\nlayer\nlazy\nleader\nleaf\nlearn\nleave\nlecture\nleft\nleg\nlegal\nlegend\nleisure\nlemon\nlend\nlength\nlens\nleopard\nlesson\nletter\nlevel\nliar\nliberty\nlibrary\nlicense\nlife\nlift\nlight\nlike\nlimb\nlimit\nlink\nlion\nliquid\nlist\nlittle\nlive\nlizard\nload\nloan\nlobster\nlocal\nlock\nlogic\nlonely\nlong\nloop\nlottery\nloud\nlounge\nlove\nloyal\nlucky\nluggage\nlumber\nlunar\nlunch\nluxury\nlyrics\nmachine\nmad\nmagic\nmagnet\nmaid\nmail\nmain\nmajor\nmake\nmammal\nman\nmanage\nmandate\nmango\nmansion\nmanual\nmaple\nmarble\nmarch\nmargin\nmarine\nmarket\nmarriage\nmask\nmass\nmaster\nmatch\nmaterial\nmath\nmatrix\nmatter\nmaximum\nmaze\nmeadow\nmean\nmeasure\nmeat\nmechanic\nmedal\nmedia\nmelody\nmelt\nmember\nmemory\nmention\nmenu\nmercy\nmerge\nmerit\nmerry\nmesh\nmessage\nmetal\nmethod\nmiddle\nmidnight\nmilk\nmillion\nmimic\nmind\nminimum\nminor\nminute\nmiracle\nmirror\nmisery\nmiss\nmistake\nmix\nmixed\nmixture\nmobile\nmodel\nmodify\nmom\nmoment\nmonitor\nmonkey\nmonster\nmonth\nmoon\nmoral\nmore\nmorning\nmosquito\nmother\nmotion\nmotor\nmountain\nmouse\nmove\nmovie\nmuch\nmuffin\nmule\nmultiply\nmuscle\nmuseum\nmushroom\nmusic\nmust\nmutual\nmyself\nmystery\nmyth\nnaive\nname\nnapkin\nnarrow\nnasty\nnation\nnature\nnear\nneck\nneed\nnegative\nneglect\nneither\nnephew\nnerve\nnest\nnet\nnetwork\nneutral\nnever\nnews\nnext\nnice\nnight\nnoble\nnoise\nnominee\nnoodle\nnormal\nnorth\nnose\nnotable\nnote\nnothing\nnotice\nnovel\nnow\nnuclear\nnumber\nnurse\nnut\noak\nobey\nobject\noblige\nobscure\nobserve\nobtain\nobvious\noccur\nocean\noctober\nodor\noff\noffer\noffice\noften\noil\nokay\nold\nolive\nolympic\nomit\nonce\none\nonion\nonline\nonly\nopen\nopera\nopinion\noppose\noption\norange\norbit\norchard\norder\nordinary\norgan\norient\noriginal\norphan\nostrich\nother\noutdoor\nouter\noutput\noutside\noval\noven\nover\nown\nowner\noxygen\noyster\nozone\npact\npaddle\npage\npair\npalace\npalm\npanda\npanel\npanic\npanther\npaper\nparade\nparent\npark\nparrot\nparty\npass\npatch\npath\npatient\npatrol\npattern\npause\npave\npayment\npeace\npeanut\npear\npeasant\npelican\npen\npenalty\npencil\npeople\npepper\nperfect\npermit\nperson\npet\nphone\nphoto\nphrase\nphysical\npiano\npicnic\npicture\npiece\npig\npigeon\npill\npilot\npink\npioneer\npipe\npistol\npitch\npizza\nplace\nplanet\nplastic\nplate\nplay\nplease\npledge\npluck\nplug\nplunge\npoem\npoet\npoint\npolar\npole\npolice\npond\npony\npool\npopular\nportion\nposition\npossible\npost\npotato\npottery\npoverty\npowder\npower\npractice\npraise\npredict\nprefer\nprepare\npresent\npretty\nprevent\nprice\npride\nprimary\nprint\npriority\nprison\nprivate\nprize\nproblem\nprocess\nproduce\nprofit\nprogram\nproject\npromote\nproof\nproperty\nprosper\nprotect\nproud\nprovide\npublic\npudding\npull\npulp\npulse\npumpkin\npunch\npupil\npuppy\npurchase\npurity\npurpose\npurse\npush\nput\npuzzle\npyramid\nquality\nquantum\nquarter\nquestion\nquick\nquit\nquiz\nquote\nrabbit\nraccoon\nrace\nrack\nradar\nradio\nrail\nrain\nraise\nrally\nramp\nranch\nrandom\nrange\nrapid\nrare\nrate\nrather\nraven\nraw\nrazor\nready\nreal\nreason\nrebel\nrebuild\nrecall\nreceive\nrecipe\nrecord\nrecycle\nreduce\nreflect\nreform\nrefuse\nregion\nregret\nregular\nreject\nrelax\nrelease\nrelief\nrely\nremain\nremember\nremind\nremove\nrender\nrenew\nrent\nreopen\nrepair\nrepeat\nreplace\nreport\nrequire\nrescue\nresemble\nresist\nresource\nresponse\nresult\nretire\nretreat\nreturn\nreunion\nreveal\nreview\nreward\nrhythm\nrib\nribbon\nrice\nrich\nride\nridge\nrifle\nright\nrigid\nring\nriot\nripple\nrisk\nritual\nrival\nriver\nroad\nroast\nrobot\nrobust\nrocket\nromance\nroof\nrookie\nroom\nrose\nrotate\nrough\nround\nroute\nroyal\nrubber\nrude\nrug\nrule\nrun\nrunway\nrural\nsad\nsaddle\nsadness\nsafe\nsail\nsalad\nsalmon\nsalon\nsalt\nsalute\nsame\nsample\nsand\nsatisfy\nsatoshi\nsauce\nsausage\nsave\nsay\nscale\nscan\nscare\nscatter\nscene\nscheme\nschool\nscience\nscissors\nscorpion\nscout\nscrap\nscreen\nscript\nscrub\nsea\nsearch\nseason\nseat\nsecond\nsecret\nsection\nsecurity\nseed\nseek\nsegment\nselect\nsell\nseminar\nsenior\nsense\nsentence\nseries\nservice\nsession\nsettle\nsetup\nseven\nshadow\nshaft\nshallow\nshare\nshed\nshell\nsheriff\nshield\nshift\nshine\nship\nshiver\nshock\nshoe\nshoot\nshop\nshort\nshoulder\nshove\nshrimp\nshrug\nshuffle\nshy\nsibling\nsick\nside\nsiege\nsight\nsign\nsilent\nsilk\nsilly\nsilver\nsimilar\nsimple\nsince\nsing\nsiren\nsister\nsituate\nsix\nsize\nskate\nsketch\nski\nskill\nskin\nskirt\nskull\nslab\nslam\nsleep\nslender\nslice\nslide\nslight\nslim\nslogan\nslot\nslow\nslush\nsmall\nsmart\nsmile\nsmoke\nsmooth\nsnack\nsnake\nsnap\nsniff\nsnow\nsoap\nsoccer\nsocial\nsock\nsoda\nsoft\nsolar\nsoldier\nsolid\nsolution\nsolve\nsomeone\nsong\nsoon\nsorry\nsort\nsoul\nsound\nsoup\nsource\nsouth\nspace\nspare\nspatial\nspawn\nspeak\nspecial\nspeed\nspell\nspend\nsphere\nspice\nspider\nspike\nspin\nspirit\nsplit\nspoil\nsponsor\nspoon\nsport\nspot\nspray\nspread\nspring\nspy\nsquare\nsqueeze\nsquirrel\nstable\nstadium\nstaff\nstage\nstairs\nstamp\nstand\nstart\nstate\nstay\nsteak\nsteel\nstem\nstep\nstereo\nstick\nstill\nsting\nstock\nstomach\nstone\nstool\nstory\nstove\nstrategy\nstreet\nstrike\nstrong\nstruggle\nstudent\nstuff\nstumble\nstyle\nsubject\nsubmit\nsubway\nsuccess\nsuch\nsudden\nsuffer\nsugar\nsuggest\nsuit\nsummer\nsun\nsunny\nsunset\nsuper\nsupply\nsupreme\nsure\nsurface\nsurge\nsurprise\nsurround\nsurvey\nsuspect\nsustain\nswallow\nswamp\nswap\nswarm\nswear\nsweet\nswift\nswim\nswing\nswitch\nsword\nsymbol\nsymptom\nsyrup\nsystem\ntable\ntackle\ntag\ntail\ntalent\ntalk\ntank\ntape\ntarget\ntask\ntaste\ntattoo\ntaxi\nteach\nteam\ntell\nten\ntenant\ntennis\ntent\nterm\ntest\ntext\nthank\nthat\ntheme\nthen\ntheory\nthere\nthey\nthing\nthis\nthought\nthree\nthrive\nthrow\nthumb\nthunder\nticket\ntide\ntiger\ntilt\ntimber\ntime\ntiny\ntip\ntired\ntissue\ntitle\ntoast\ntobacco\ntoday\ntoddler\ntoe\ntogether\ntoilet\ntoken\ntomato\ntomorrow\ntone\ntongue\ntonight\ntool\ntooth\ntop\ntopic\ntopple\ntorch\ntornado\ntortoise\ntoss\ntotal\ntourist\ntoward\ntower\ntown\ntoy\ntrack\ntrade\ntraffic\ntragic\ntrain\ntransfer\ntrap\ntrash\ntravel\ntray\ntreat\ntree\ntrend\ntrial\ntribe\ntrick\ntrigger\ntrim\ntrip\ntrophy\ntrouble\ntruck\ntrue\ntruly\ntrumpet\ntrust\ntruth\ntry\ntube\ntuition\ntumble\ntuna\ntunnel\nturkey\nturn\nturtle\ntwelve\ntwenty\ntwice\ntwin\ntwist\ntwo\ntype\ntypical\nugly\numbrella\nunable\nunaware\nuncle\nuncover\nunder\nundo\nunfair\nunfold\nunhappy\nuniform\nunique\nunit\nuniverse\nunknown\nunlock\nuntil\nunusual\nunveil\nupdate\nupgrade\nuphold\nupon\nupper\nupset\nurban\nurge\nusage\nuse\nused\nuseful\nuseless\nusual\nutility\nvacant\nvacuum\nvague\nvalid\nvalley\nvalve\nvan\nvanish\nvapor\nvarious\nvast\nvault\nvehicle\nvelvet\nvendor\nventure\nvenue\nverb\nverify\nversion\nvery\nvessel\nveteran\nviable\nvibrant\nvicious\nvictory\nvideo\nview\nvillage\nvintage\nviolin\nvirtual\nvirus\nvisa\nvisit\nvisual\nvital\nvivid\nvocal\nvoice\nvoid\nvolcano\nvolume\nvote\nvoyage\nwage\nwagon\nwait\nwalk\nwall\nwalnut\nwant\nwarfare\nwarm\nwarrior\nwash\nwasp\nwaste\nwater\nwave\nway\nwealth\nweapon\nwear\nweasel\nweather\nweb\nwedding\nweekend\nweird\nwelcome\nwest\nwet\nwhale\nwhat\nwheat\nwheel\nwhen\nwhere\nwhip\nwhisper\nwide\nwidth\nwife\nwild\nwill\nwin\nwindow\nwine\nwing\nwink\nwinner\nwinter\nwire\nwisdom\nwise\nwish\nwitness\nwolf\nwoman\nwonder\nwood\nwool\nword\nwork\nworld\nworry\nworth\nwrap\nwreck\nwrestle\nwrist\nwrite\nwrong\nyard\nyear\nyellow\nyou\nyoung\nyouth\nzebra\nzero\nzone\nzoo\n"
  },
  {
    "path": "electrum/wordlist/japanese.txt",
    "content": "あいこくしん\nあいさつ\nあいだ\nあおぞら\nあかちゃん\nあきる\nあけがた\nあける\nあこがれる\nあさい\nあさひ\nあしあと\nあじわう\nあずかる\nあずき\nあそぶ\nあたえる\nあたためる\nあたりまえ\nあたる\nあつい\nあつかう\nあっしゅく\nあつまり\nあつめる\nあてな\nあてはまる\nあひる\nあぶら\nあぶる\nあふれる\nあまい\nあまど\nあまやかす\nあまり\nあみもの\nあめりか\nあやまる\nあゆむ\nあらいぐま\nあらし\nあらすじ\nあらためる\nあらゆる\nあらわす\nありがとう\nあわせる\nあわてる\nあんい\nあんがい\nあんこ\nあんぜん\nあんてい\nあんない\nあんまり\nいいだす\nいおん\nいがい\nいがく\nいきおい\nいきなり\nいきもの\nいきる\nいくじ\nいくぶん\nいけばな\nいけん\nいこう\nいこく\nいこつ\nいさましい\nいさん\nいしき\nいじゅう\nいじょう\nいじわる\nいずみ\nいずれ\nいせい\nいせえび\nいせかい\nいせき\nいぜん\nいそうろう\nいそがしい\nいだい\nいだく\nいたずら\nいたみ\nいたりあ\nいちおう\nいちじ\nいちど\nいちば\nいちぶ\nいちりゅう\nいつか\nいっしゅん\nいっせい\nいっそう\nいったん\nいっち\nいってい\nいっぽう\nいてざ\nいてん\nいどう\nいとこ\nいない\nいなか\nいねむり\nいのち\nいのる\nいはつ\nいばる\nいはん\nいびき\nいひん\nいふく\nいへん\nいほう\nいみん\nいもうと\nいもたれ\nいもり\nいやがる\nいやす\nいよかん\nいよく\nいらい\nいらすと\nいりぐち\nいりょう\nいれい\nいれもの\nいれる\nいろえんぴつ\nいわい\nいわう\nいわかん\nいわば\nいわゆる\nいんげんまめ\nいんさつ\nいんしょう\nいんよう\nうえき\nうえる\nうおざ\nうがい\nうかぶ\nうかべる\nうきわ\nうくらいな\nうくれれ\nうけたまわる\nうけつけ\nうけとる\nうけもつ\nうける\nうごかす\nうごく\nうこん\nうさぎ\nうしなう\nうしろがみ\nうすい\nうすぎ\nうすぐらい\nうすめる\nうせつ\nうちあわせ\nうちがわ\nうちき\nうちゅう\nうっかり\nうつくしい\nうったえる\nうつる\nうどん\nうなぎ\nうなじ\nうなずく\nうなる\nうねる\nうのう\nうぶげ\nうぶごえ\nうまれる\nうめる\nうもう\nうやまう\nうよく\nうらがえす\nうらぐち\nうらない\nうりあげ\nうりきれ\nうるさい\nうれしい\nうれゆき\nうれる\nうろこ\nうわき\nうわさ\nうんこう\nうんちん\nうんてん\nうんどう\nえいえん\nえいが\nえいきょう\nえいご\nえいせい\nえいぶん\nえいよう\nえいわ\nえおり\nえがお\nえがく\nえきたい\nえくせる\nえしゃく\nえすて\nえつらん\nえのぐ\nえほうまき\nえほん\nえまき\nえもじ\nえもの\nえらい\nえらぶ\nえりあ\nえんえん\nえんかい\nえんぎ\nえんげき\nえんしゅう\nえんぜつ\nえんそく\nえんちょう\nえんとつ\nおいかける\nおいこす\nおいしい\nおいつく\nおうえん\nおうさま\nおうじ\nおうせつ\nおうたい\nおうふく\nおうべい\nおうよう\nおえる\nおおい\nおおう\nおおどおり\nおおや\nおおよそ\nおかえり\nおかず\nおがむ\nおかわり\nおぎなう\nおきる\nおくさま\nおくじょう\nおくりがな\nおくる\nおくれる\nおこす\nおこなう\nおこる\nおさえる\nおさない\nおさめる\nおしいれ\nおしえる\nおじぎ\nおじさん\nおしゃれ\nおそらく\nおそわる\nおたがい\nおたく\nおだやか\nおちつく\nおっと\nおつり\nおでかけ\nおとしもの\nおとなしい\nおどり\nおどろかす\nおばさん\nおまいり\nおめでとう\nおもいで\nおもう\nおもたい\nおもちゃ\nおやつ\nおやゆび\nおよぼす\nおらんだ\nおろす\nおんがく\nおんけい\nおんしゃ\nおんせん\nおんだん\nおんちゅう\nおんどけい\nかあつ\nかいが\nがいき\nがいけん\nがいこう\nかいさつ\nかいしゃ\nかいすいよく\nかいぜん\nかいぞうど\nかいつう\nかいてん\nかいとう\nかいふく\nがいへき\nかいほう\nかいよう\nがいらい\nかいわ\nかえる\nかおり\nかかえる\nかがく\nかがし\nかがみ\nかくご\nかくとく\nかざる\nがぞう\nかたい\nかたち\nがちょう\nがっきゅう\nがっこう\nがっさん\nがっしょう\nかなざわし\nかのう\nがはく\nかぶか\nかほう\nかほご\nかまう\nかまぼこ\nかめれおん\nかゆい\nかようび\nからい\nかるい\nかろう\nかわく\nかわら\nがんか\nかんけい\nかんこう\nかんしゃ\nかんそう\nかんたん\nかんち\nがんばる\nきあい\nきあつ\nきいろ\nぎいん\nきうい\nきうん\nきえる\nきおう\nきおく\nきおち\nきおん\nきかい\nきかく\nきかんしゃ\nききて\nきくばり\nきくらげ\nきけんせい\nきこう\nきこえる\nきこく\nきさい\nきさく\nきさま\nきさらぎ\nぎじかがく\nぎしき\nぎじたいけん\nぎじにってい\nぎじゅつしゃ\nきすう\nきせい\nきせき\nきせつ\nきそう\nきぞく\nきぞん\nきたえる\nきちょう\nきつえん\nぎっちり\nきつつき\nきつね\nきてい\nきどう\nきどく\nきない\nきなが\nきなこ\nきぬごし\nきねん\nきのう\nきのした\nきはく\nきびしい\nきひん\nきふく\nきぶん\nきぼう\nきほん\nきまる\nきみつ\nきむずかしい\nきめる\nきもだめし\nきもち\nきもの\nきゃく\nきやく\nぎゅうにく\nきよう\nきょうりゅう\nきらい\nきらく\nきりん\nきれい\nきれつ\nきろく\nぎろん\nきわめる\nぎんいろ\nきんかくじ\nきんじょ\nきんようび\nぐあい\nくいず\nくうかん\nくうき\nくうぐん\nくうこう\nぐうせい\nくうそう\nぐうたら\nくうふく\nくうぼ\nくかん\nくきょう\nくげん\nぐこう\nくさい\nくさき\nくさばな\nくさる\nくしゃみ\nくしょう\nくすのき\nくすりゆび\nくせげ\nくせん\nぐたいてき\nくださる\nくたびれる\nくちこみ\nくちさき\nくつした\nぐっすり\nくつろぐ\nくとうてん\nくどく\nくなん\nくねくね\nくのう\nくふう\nくみあわせ\nくみたてる\nくめる\nくやくしょ\nくらす\nくらべる\nくるま\nくれる\nくろう\nくわしい\nぐんかん\nぐんしょく\nぐんたい\nぐんて\nけあな\nけいかく\nけいけん\nけいこ\nけいさつ\nげいじゅつ\nけいたい\nげいのうじん\nけいれき\nけいろ\nけおとす\nけおりもの\nげきか\nげきげん\nげきだん\nげきちん\nげきとつ\nげきは\nげきやく\nげこう\nげこくじょう\nげざい\nけさき\nげざん\nけしき\nけしごむ\nけしょう\nげすと\nけたば\nけちゃっぷ\nけちらす\nけつあつ\nけつい\nけつえき\nけっこん\nけつじょ\nけっせき\nけってい\nけつまつ\nげつようび\nげつれい\nけつろん\nげどく\nけとばす\nけとる\nけなげ\nけなす\nけなみ\nけぬき\nげねつ\nけねん\nけはい\nげひん\nけぶかい\nげぼく\nけまり\nけみかる\nけむし\nけむり\nけもの\nけらい\nけろけろ\nけわしい\nけんい\nけんえつ\nけんお\nけんか\nげんき\nけんげん\nけんこう\nけんさく\nけんしゅう\nけんすう\nげんそう\nけんちく\nけんてい\nけんとう\nけんない\nけんにん\nげんぶつ\nけんま\nけんみん\nけんめい\nけんらん\nけんり\nこあくま\nこいぬ\nこいびと\nごうい\nこうえん\nこうおん\nこうかん\nごうきゅう\nごうけい\nこうこう\nこうさい\nこうじ\nこうすい\nごうせい\nこうそく\nこうたい\nこうちゃ\nこうつう\nこうてい\nこうどう\nこうない\nこうはい\nごうほう\nごうまん\nこうもく\nこうりつ\nこえる\nこおり\nごかい\nごがつ\nごかん\nこくご\nこくさい\nこくとう\nこくない\nこくはく\nこぐま\nこけい\nこける\nここのか\nこころ\nこさめ\nこしつ\nこすう\nこせい\nこせき\nこぜん\nこそだて\nこたい\nこたえる\nこたつ\nこちょう\nこっか\nこつこつ\nこつばん\nこつぶ\nこてい\nこてん\nことがら\nことし\nことば\nことり\nこなごな\nこねこね\nこのまま\nこのみ\nこのよ\nごはん\nこひつじ\nこふう\nこふん\nこぼれる\nごまあぶら\nこまかい\nごますり\nこまつな\nこまる\nこむぎこ\nこもじ\nこもち\nこもの\nこもん\nこやく\nこやま\nこゆう\nこゆび\nこよい\nこよう\nこりる\nこれくしょん\nころっけ\nこわもて\nこわれる\nこんいん\nこんかい\nこんき\nこんしゅう\nこんすい\nこんだて\nこんとん\nこんなん\nこんびに\nこんぽん\nこんまけ\nこんや\nこんれい\nこんわく\nざいえき\nさいかい\nさいきん\nざいげん\nざいこ\nさいしょ\nさいせい\nざいたく\nざいちゅう\nさいてき\nざいりょう\nさうな\nさかいし\nさがす\nさかな\nさかみち\nさがる\nさぎょう\nさくし\nさくひん\nさくら\nさこく\nさこつ\nさずかる\nざせき\nさたん\nさつえい\nざつおん\nざっか\nざつがく\nさっきょく\nざっし\nさつじん\nざっそう\nさつたば\nさつまいも\nさてい\nさといも\nさとう\nさとおや\nさとし\nさとる\nさのう\nさばく\nさびしい\nさべつ\nさほう\nさほど\nさます\nさみしい\nさみだれ\nさむけ\nさめる\nさやえんどう\nさゆう\nさよう\nさよく\nさらだ\nざるそば\nさわやか\nさわる\nさんいん\nさんか\nさんきゃく\nさんこう\nさんさい\nざんしょ\nさんすう\nさんせい\nさんそ\nさんち\nさんま\nさんみ\nさんらん\nしあい\nしあげ\nしあさって\nしあわせ\nしいく\nしいん\nしうち\nしえい\nしおけ\nしかい\nしかく\nじかん\nしごと\nしすう\nじだい\nしたうけ\nしたぎ\nしたて\nしたみ\nしちょう\nしちりん\nしっかり\nしつじ\nしつもん\nしてい\nしてき\nしてつ\nじてん\nじどう\nしなぎれ\nしなもの\nしなん\nしねま\nしねん\nしのぐ\nしのぶ\nしはい\nしばかり\nしはつ\nしはらい\nしはん\nしひょう\nしふく\nじぶん\nしへい\nしほう\nしほん\nしまう\nしまる\nしみん\nしむける\nじむしょ\nしめい\nしめる\nしもん\nしゃいん\nしゃうん\nしゃおん\nじゃがいも\nしやくしょ\nしゃくほう\nしゃけん\nしゃこ\nしゃざい\nしゃしん\nしゃせん\nしゃそう\nしゃたい\nしゃちょう\nしゃっきん\nじゃま\nしゃりん\nしゃれい\nじゆう\nじゅうしょ\nしゅくはく\nじゅしん\nしゅっせき\nしゅみ\nしゅらば\nじゅんばん\nしょうかい\nしょくたく\nしょっけん\nしょどう\nしょもつ\nしらせる\nしらべる\nしんか\nしんこう\nじんじゃ\nしんせいじ\nしんちく\nしんりん\nすあげ\nすあし\nすあな\nずあん\nすいえい\nすいか\nすいとう\nずいぶん\nすいようび\nすうがく\nすうじつ\nすうせん\nすおどり\nすきま\nすくう\nすくない\nすける\nすごい\nすこし\nずさん\nすずしい\nすすむ\nすすめる\nすっかり\nずっしり\nずっと\nすてき\nすてる\nすねる\nすのこ\nすはだ\nすばらしい\nずひょう\nずぶぬれ\nすぶり\nすふれ\nすべて\nすべる\nずほう\nすぼん\nすまい\nすめし\nすもう\nすやき\nすらすら\nするめ\nすれちがう\nすろっと\nすわる\nすんぜん\nすんぽう\nせあぶら\nせいかつ\nせいげん\nせいじ\nせいよう\nせおう\nせかいかん\nせきにん\nせきむ\nせきゆ\nせきらんうん\nせけん\nせこう\nせすじ\nせたい\nせたけ\nせっかく\nせっきゃく\nぜっく\nせっけん\nせっこつ\nせっさたくま\nせつぞく\nせつだん\nせつでん\nせっぱん\nせつび\nせつぶん\nせつめい\nせつりつ\nせなか\nせのび\nせはば\nせびろ\nせぼね\nせまい\nせまる\nせめる\nせもたれ\nせりふ\nぜんあく\nせんい\nせんえい\nせんか\nせんきょ\nせんく\nせんげん\nぜんご\nせんさい\nせんしゅ\nせんすい\nせんせい\nせんぞ\nせんたく\nせんちょう\nせんてい\nせんとう\nせんぬき\nせんねん\nせんぱい\nぜんぶ\nぜんぽう\nせんむ\nせんめんじょ\nせんもん\nせんやく\nせんゆう\nせんよう\nぜんら\nぜんりゃく\nせんれい\nせんろ\nそあく\nそいとげる\nそいね\nそうがんきょう\nそうき\nそうご\nそうしん\nそうだん\nそうなん\nそうび\nそうめん\nそうり\nそえもの\nそえん\nそがい\nそげき\nそこう\nそこそこ\nそざい\nそしな\nそせい\nそせん\nそそぐ\nそだてる\nそつう\nそつえん\nそっかん\nそつぎょう\nそっけつ\nそっこう\nそっせん\nそっと\nそとがわ\nそとづら\nそなえる\nそなた\nそふぼ\nそぼく\nそぼろ\nそまつ\nそまる\nそむく\nそむりえ\nそめる\nそもそも\nそよかぜ\nそらまめ\nそろう\nそんかい\nそんけい\nそんざい\nそんしつ\nそんぞく\nそんちょう\nぞんび\nぞんぶん\nそんみん\nたあい\nたいいん\nたいうん\nたいえき\nたいおう\nだいがく\nたいき\nたいぐう\nたいけん\nたいこ\nたいざい\nだいじょうぶ\nだいすき\nたいせつ\nたいそう\nだいたい\nたいちょう\nたいてい\nだいどころ\nたいない\nたいねつ\nたいのう\nたいはん\nだいひょう\nたいふう\nたいへん\nたいほ\nたいまつばな\nたいみんぐ\nたいむ\nたいめん\nたいやき\nたいよう\nたいら\nたいりょく\nたいる\nたいわん\nたうえ\nたえる\nたおす\nたおる\nたおれる\nたかい\nたかね\nたきび\nたくさん\nたこく\nたこやき\nたさい\nたしざん\nだじゃれ\nたすける\nたずさわる\nたそがれ\nたたかう\nたたく\nただしい\nたたみ\nたちばな\nだっかい\nだっきゃく\nだっこ\nだっしゅつ\nだったい\nたてる\nたとえる\nたなばた\nたにん\nたぬき\nたのしみ\nたはつ\nたぶん\nたべる\nたぼう\nたまご\nたまる\nだむる\nためいき\nためす\nためる\nたもつ\nたやすい\nたよる\nたらす\nたりきほんがん\nたりょう\nたりる\nたると\nたれる\nたれんと\nたろっと\nたわむれる\nだんあつ\nたんい\nたんおん\nたんか\nたんき\nたんけん\nたんご\nたんさん\nたんじょうび\nだんせい\nたんそく\nたんたい\nだんち\nたんてい\nたんとう\nだんな\nたんにん\nだんねつ\nたんのう\nたんぴん\nだんぼう\nたんまつ\nたんめい\nだんれつ\nだんろ\nだんわ\nちあい\nちあん\nちいき\nちいさい\nちえん\nちかい\nちから\nちきゅう\nちきん\nちけいず\nちけん\nちこく\nちさい\nちしき\nちしりょう\nちせい\nちそう\nちたい\nちたん\nちちおや\nちつじょ\nちてき\nちてん\nちぬき\nちぬり\nちのう\nちひょう\nちへいせん\nちほう\nちまた\nちみつ\nちみどろ\nちめいど\nちゃんこなべ\nちゅうい\nちゆりょく\nちょうし\nちょさくけん\nちらし\nちらみ\nちりがみ\nちりょう\nちるど\nちわわ\nちんたい\nちんもく\nついか\nついたち\nつうか\nつうじょう\nつうはん\nつうわ\nつかう\nつかれる\nつくね\nつくる\nつけね\nつける\nつごう\nつたえる\nつづく\nつつじ\nつつむ\nつとめる\nつながる\nつなみ\nつねづね\nつのる\nつぶす\nつまらない\nつまる\nつみき\nつめたい\nつもり\nつもる\nつよい\nつるぼ\nつるみく\nつわもの\nつわり\nてあし\nてあて\nてあみ\nていおん\nていか\nていき\nていけい\nていこく\nていさつ\nていし\nていせい\nていたい\nていど\nていねい\nていひょう\nていへん\nていぼう\nてうち\nておくれ\nてきとう\nてくび\nでこぼこ\nてさぎょう\nてさげ\nてすり\nてそう\nてちがい\nてちょう\nてつがく\nてつづき\nでっぱ\nてつぼう\nてつや\nでぬかえ\nてぬき\nてぬぐい\nてのひら\nてはい\nてぶくろ\nてふだ\nてほどき\nてほん\nてまえ\nてまきずし\nてみじか\nてみやげ\nてらす\nてれび\nてわけ\nてわたし\nでんあつ\nてんいん\nてんかい\nてんき\nてんぐ\nてんけん\nてんごく\nてんさい\nてんし\nてんすう\nでんち\nてんてき\nてんとう\nてんない\nてんぷら\nてんぼうだい\nてんめつ\nてんらんかい\nでんりょく\nでんわ\nどあい\nといれ\nどうかん\nとうきゅう\nどうぐ\nとうし\nとうむぎ\nとおい\nとおか\nとおく\nとおす\nとおる\nとかい\nとかす\nときおり\nときどき\nとくい\nとくしゅう\nとくてん\nとくに\nとくべつ\nとけい\nとける\nとこや\nとさか\nとしょかん\nとそう\nとたん\nとちゅう\nとっきゅう\nとっくん\nとつぜん\nとつにゅう\nとどける\nととのえる\nとない\nとなえる\nとなり\nとのさま\nとばす\nどぶがわ\nとほう\nとまる\nとめる\nともだち\nともる\nどようび\nとらえる\nとんかつ\nどんぶり\nないかく\nないこう\nないしょ\nないす\nないせん\nないそう\nなおす\nながい\nなくす\nなげる\nなこうど\nなさけ\nなたでここ\nなっとう\nなつやすみ\nななおし\nなにごと\nなにもの\nなにわ\nなのか\nなふだ\nなまいき\nなまえ\nなまみ\nなみだ\nなめらか\nなめる\nなやむ\nならう\nならび\nならぶ\nなれる\nなわとび\nなわばり\nにあう\nにいがた\nにうけ\nにおい\nにかい\nにがて\nにきび\nにくしみ\nにくまん\nにげる\nにさんかたんそ\nにしき\nにせもの\nにちじょう\nにちようび\nにっか\nにっき\nにっけい\nにっこう\nにっさん\nにっしょく\nにっすう\nにっせき\nにってい\nになう\nにほん\nにまめ\nにもつ\nにやり\nにゅういん\nにりんしゃ\nにわとり\nにんい\nにんか\nにんき\nにんげん\nにんしき\nにんずう\nにんそう\nにんたい\nにんち\nにんてい\nにんにく\nにんぷ\nにんまり\nにんむ\nにんめい\nにんよう\nぬいくぎ\nぬかす\nぬぐいとる\nぬぐう\nぬくもり\nぬすむ\nぬまえび\nぬめり\nぬらす\nぬんちゃく\nねあげ\nねいき\nねいる\nねいろ\nねぐせ\nねくたい\nねくら\nねこぜ\nねこむ\nねさげ\nねすごす\nねそべる\nねだん\nねつい\nねっしん\nねつぞう\nねったいぎょ\nねぶそく\nねふだ\nねぼう\nねほりはほり\nねまき\nねまわし\nねみみ\nねむい\nねむたい\nねもと\nねらう\nねわざ\nねんいり\nねんおし\nねんかん\nねんきん\nねんぐ\nねんざ\nねんし\nねんちゃく\nねんど\nねんぴ\nねんぶつ\nねんまつ\nねんりょう\nねんれい\nのいず\nのおづま\nのがす\nのきなみ\nのこぎり\nのこす\nのこる\nのせる\nのぞく\nのぞむ\nのたまう\nのちほど\nのっく\nのばす\nのはら\nのべる\nのぼる\nのみもの\nのやま\nのらいぬ\nのらねこ\nのりもの\nのりゆき\nのれん\nのんき\nばあい\nはあく\nばあさん\nばいか\nばいく\nはいけん\nはいご\nはいしん\nはいすい\nはいせん\nはいそう\nはいち\nばいばい\nはいれつ\nはえる\nはおる\nはかい\nばかり\nはかる\nはくしゅ\nはけん\nはこぶ\nはさみ\nはさん\nはしご\nばしょ\nはしる\nはせる\nぱそこん\nはそん\nはたん\nはちみつ\nはつおん\nはっかく\nはづき\nはっきり\nはっくつ\nはっけん\nはっこう\nはっさん\nはっしん\nはったつ\nはっちゅう\nはってん\nはっぴょう\nはっぽう\nはなす\nはなび\nはにかむ\nはぶらし\nはみがき\nはむかう\nはめつ\nはやい\nはやし\nはらう\nはろうぃん\nはわい\nはんい\nはんえい\nはんおん\nはんかく\nはんきょう\nばんぐみ\nはんこ\nはんしゃ\nはんすう\nはんだん\nぱんち\nぱんつ\nはんてい\nはんとし\nはんのう\nはんぱ\nはんぶん\nはんぺん\nはんぼうき\nはんめい\nはんらん\nはんろん\nひいき\nひうん\nひえる\nひかく\nひかり\nひかる\nひかん\nひくい\nひけつ\nひこうき\nひこく\nひさい\nひさしぶり\nひさん\nびじゅつかん\nひしょ\nひそか\nひそむ\nひたむき\nひだり\nひたる\nひつぎ\nひっこし\nひっし\nひつじゅひん\nひっす\nひつぜん\nぴったり\nぴっちり\nひつよう\nひてい\nひとごみ\nひなまつり\nひなん\nひねる\nひはん\nひびく\nひひょう\nひほう\nひまわり\nひまん\nひみつ\nひめい\nひめじし\nひやけ\nひやす\nひよう\nびょうき\nひらがな\nひらく\nひりつ\nひりょう\nひるま\nひるやすみ\nひれい\nひろい\nひろう\nひろき\nひろゆき\nひんかく\nひんけつ\nひんこん\nひんしゅ\nひんそう\nぴんち\nひんぱん\nびんぼう\nふあん\nふいうち\nふうけい\nふうせん\nぷうたろう\nふうとう\nふうふ\nふえる\nふおん\nふかい\nふきん\nふくざつ\nふくぶくろ\nふこう\nふさい\nふしぎ\nふじみ\nふすま\nふせい\nふせぐ\nふそく\nぶたにく\nふたん\nふちょう\nふつう\nふつか\nふっかつ\nふっき\nふっこく\nぶどう\nふとる\nふとん\nふのう\nふはい\nふひょう\nふへん\nふまん\nふみん\nふめつ\nふめん\nふよう\nふりこ\nふりる\nふるい\nふんいき\nぶんがく\nぶんぐ\nふんしつ\nぶんせき\nふんそう\nぶんぽう\nへいあん\nへいおん\nへいがい\nへいき\nへいげん\nへいこう\nへいさ\nへいしゃ\nへいせつ\nへいそ\nへいたく\nへいてん\nへいねつ\nへいわ\nへきが\nへこむ\nべにいろ\nべにしょうが\nへらす\nへんかん\nべんきょう\nべんごし\nへんさい\nへんたい\nべんり\nほあん\nほいく\nぼうぎょ\nほうこく\nほうそう\nほうほう\nほうもん\nほうりつ\nほえる\nほおん\nほかん\nほきょう\nぼきん\nほくろ\nほけつ\nほけん\nほこう\nほこる\nほしい\nほしつ\nほしゅ\nほしょう\nほせい\nほそい\nほそく\nほたて\nほたる\nぽちぶくろ\nほっきょく\nほっさ\nほったん\nほとんど\nほめる\nほんい\nほんき\nほんけ\nほんしつ\nほんやく\nまいにち\nまかい\nまかせる\nまがる\nまける\nまこと\nまさつ\nまじめ\nますく\nまぜる\nまつり\nまとめ\nまなぶ\nまぬけ\nまねく\nまほう\nまもる\nまゆげ\nまよう\nまろやか\nまわす\nまわり\nまわる\nまんが\nまんきつ\nまんぞく\nまんなか\nみいら\nみうち\nみえる\nみがく\nみかた\nみかん\nみけん\nみこん\nみじかい\nみすい\nみすえる\nみせる\nみっか\nみつかる\nみつける\nみてい\nみとめる\nみなと\nみなみかさい\nみねらる\nみのう\nみのがす\nみほん\nみもと\nみやげ\nみらい\nみりょく\nみわく\nみんか\nみんぞく\nむいか\nむえき\nむえん\nむかい\nむかう\nむかえ\nむかし\nむぎちゃ\nむける\nむげん\nむさぼる\nむしあつい\nむしば\nむじゅん\nむしろ\nむすう\nむすこ\nむすぶ\nむすめ\nむせる\nむせん\nむちゅう\nむなしい\nむのう\nむやみ\nむよう\nむらさき\nむりょう\nむろん\nめいあん\nめいうん\nめいえん\nめいかく\nめいきょく\nめいさい\nめいし\nめいそう\nめいぶつ\nめいれい\nめいわく\nめぐまれる\nめざす\nめした\nめずらしい\nめだつ\nめまい\nめやす\nめんきょ\nめんせき\nめんどう\nもうしあげる\nもうどうけん\nもえる\nもくし\nもくてき\nもくようび\nもちろん\nもどる\nもらう\nもんく\nもんだい\nやおや\nやける\nやさい\nやさしい\nやすい\nやすたろう\nやすみ\nやせる\nやそう\nやたい\nやちん\nやっと\nやっぱり\nやぶる\nやめる\nややこしい\nやよい\nやわらかい\nゆうき\nゆうびんきょく\nゆうべ\nゆうめい\nゆけつ\nゆしゅつ\nゆせん\nゆそう\nゆたか\nゆちゃく\nゆでる\nゆにゅう\nゆびわ\nゆらい\nゆれる\nようい\nようか\nようきゅう\nようじ\nようす\nようちえん\nよかぜ\nよかん\nよきん\nよくせい\nよくぼう\nよけい\nよごれる\nよさん\nよしゅう\nよそう\nよそく\nよっか\nよてい\nよどがわく\nよねつ\nよやく\nよゆう\nよろこぶ\nよろしい\nらいう\nらくがき\nらくご\nらくさつ\nらくだ\nらしんばん\nらせん\nらぞく\nらたい\nらっか\nられつ\nりえき\nりかい\nりきさく\nりきせつ\nりくぐん\nりくつ\nりけん\nりこう\nりせい\nりそう\nりそく\nりてん\nりねん\nりゆう\nりゅうがく\nりよう\nりょうり\nりょかん\nりょくちゃ\nりょこう\nりりく\nりれき\nりろん\nりんご\nるいけい\nるいさい\nるいじ\nるいせき\nるすばん\nるりがわら\nれいかん\nれいぎ\nれいせい\nれいぞうこ\nれいとう\nれいぼう\nれきし\nれきだい\nれんあい\nれんけい\nれんこん\nれんさい\nれんしゅう\nれんぞく\nれんらく\nろうか\nろうご\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": "electrum/wordlist/portuguese.txt",
    "content": "# Copyright (c) 2014, The Monero Project\n# \n# All rights reserved.\n# \n# Redistribution and use in source and binary forms, with or without modification, are\n# permitted provided that the following conditions are met:\n# \n# 1. Redistributions of source code must retain the above copyright notice, this list of\n#    conditions and the following disclaimer.\n# \n# 2. Redistributions in binary form must reproduce the above copyright notice, this list\n#    of conditions and the following disclaimer in the documentation and/or other\n#    materials provided with the distribution.\n# \n# 3. Neither the name of the copyright holder nor the names of its contributors may be\n#    used to endorse or promote products derived from this software without specific\n#    prior written permission.\n# \n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY\n# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL\n# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF\n# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nabaular\nabdominal\nabeto\nabissinio\nabjeto\nablucao\nabnegar\nabotoar\nabrutalhar\nabsurdo\nabutre\nacautelar\naccessorios\nacetona\nachocolatado\nacirrar\nacne\nacovardar\nacrostico\nactinomicete\nacustico\nadaptavel\nadeus\nadivinho\nadjunto\nadmoestar\nadnominal\nadotivo\nadquirir\nadriatico\nadsorcao\nadutora\nadvogar\naerossol\nafazeres\nafetuoso\nafixo\nafluir\nafortunar\nafrouxar\naftosa\nafunilar\nagentes\nagito\naglutinar\naiatola\naimore\naino\naipo\nairoso\najeitar\najoelhar\najudante\najuste\nalazao\nalbumina\nalcunha\nalegria\nalexandre\nalforriar\nalguns\nalhures\nalivio\nalmoxarife\nalotropico\nalpiste\nalquimista\nalsaciano\naltura\naluviao\nalvura\namazonico\nambulatorio\nametodico\namizades\namniotico\namovivel\namurada\nanatomico\nancorar\nanexo\nanfora\naniversario\nanjo\nanotar\nansioso\nanturio\nanuviar\nanverso\nanzol\naonde\napaziguar\napito\naplicavel\napoteotico\naprimorar\naprumo\napto\napuros\naquoso\narauto\narbusto\narduo\naresta\narfar\narguto\naritmetico\narlequim\narmisticio\naromatizar\narpoar\narquivo\narrumar\narsenio\narturiano\naruaque\narvores\nasbesto\nascorbico\naspirina\nasqueroso\nassustar\nastuto\natazanar\nativo\natletismo\natmosferico\natormentar\natroz\naturdir\naudivel\nauferir\naugusto\naula\naumento\naurora\nautuar\navatar\navexar\navizinhar\navolumar\navulso\naxiomatico\nazerbaijano\nazimute\nazoto\nazulejo\nbacteriologista\nbadulaque\nbaforada\nbaixote\nbajular\nbalzaquiana\nbambuzal\nbanzo\nbaoba\nbaqueta\nbarulho\nbastonete\nbatuta\nbauxita\nbavaro\nbazuca\nbcrepuscular\nbeato\nbeduino\nbegonia\nbehaviorista\nbeisebol\nbelzebu\nbemol\nbenzido\nbeocio\nbequer\nberro\nbesuntar\nbetume\nbexiga\nbezerro\nbiatlon\nbiboca\nbicuspide\nbidirecional\nbienio\nbifurcar\nbigorna\nbijuteria\nbimotor\nbinormal\nbioxido\nbipolarizacao\nbiquini\nbirutice\nbisturi\nbituca\nbiunivoco\nbivalve\nbizarro\nblasfemo\nblenorreia\nblindar\nbloqueio\nblusao\nboazuda\nbofete\nbojudo\nbolso\nbombordo\nbonzo\nbotina\nboquiaberto\nbostoniano\nbotulismo\nbourbon\nbovino\nboximane\nbravura\nbrevidade\nbritar\nbroxar\nbruno\nbruxuleio\nbubonico\nbucolico\nbuda\nbudista\nbueiro\nbuffer\nbugre\nbujao\nbumerangue\nburundines\nbusto\nbutique\nbuzios\ncaatinga\ncabuqui\ncacunda\ncafuzo\ncajueiro\ncamurca\ncanudo\ncaquizeiro\ncarvoeiro\ncasulo\ncatuaba\ncauterizar\ncebolinha\ncedula\nceifeiro\ncelulose\ncerzir\ncesto\ncetro\nceus\ncevar\nchavena\ncheroqui\nchita\nchovido\nchuvoso\nciatico\ncibernetico\ncicuta\ncidreira\ncientistas\ncifrar\ncigarro\ncilio\ncimo\ncinzento\ncioso\ncipriota\ncirurgico\ncisto\ncitrico\nciumento\ncivismo\nclavicula\nclero\nclitoris\ncluster\ncoaxial\ncobrir\ncocota\ncodorniz\ncoexistir\ncogumelo\ncoito\ncolusao\ncompaixao\ncomutativo\ncontentamento\nconvulsivo\ncoordenativa\ncoquetel\ncorreto\ncorvo\ncostureiro\ncotovia\ncovil\ncozinheiro\ncretino\ncristo\ncrivo\ncrotalo\ncruzes\ncubo\ncucuia\ncueiro\ncuidar\ncujo\ncultural\ncunilingua\ncupula\ncurvo\ncustoso\ncutucar\nczarismo\ndablio\ndacota\ndados\ndaguerreotipo\ndaiquiri\ndaltonismo\ndamista\ndantesco\ndaquilo\ndarwinista\ndasein\ndativo\ndeao\ndebutantes\ndecurso\ndeduzir\ndefunto\ndegustar\ndejeto\ndeltoide\ndemover\ndenunciar\ndeputado\ndeque\ndervixe\ndesvirtuar\ndeturpar\ndeuteronomio\ndevoto\ndextrose\ndezoito\ndiatribe\ndicotomico\ndidatico\ndietista\ndifuso\ndigressao\ndiluvio\ndiminuto\ndinheiro\ndinossauro\ndioxido\ndiplomatico\ndique\ndirimivel\ndisturbio\ndiurno\ndivulgar\ndizivel\ndoar\ndobro\ndocura\ndodoi\ndoer\ndogue\ndoloso\ndomo\ndonzela\ndoping\ndorsal\ndossie\ndote\ndoutro\ndoze\ndravidico\ndreno\ndriver\ndropes\ndruso\ndubnio\nducto\ndueto\ndulija\ndundum\nduodeno\nduquesa\ndurou\nduvidoso\nduzia\nebano\nebrio\neburneo\necharpe\neclusa\necossistema\nectoplasma\necumenismo\neczema\neden\neditorial\nedredom\nedulcorar\nefetuar\nefigie\nefluvio\negiptologo\negresso\negua\neinsteiniano\neira\neivar\neixos\nejetar\nelastomero\neldorado\nelixir\nelmo\neloquente\nelucidativo\nemaranhar\nembutir\nemerito\nemfa\nemitir\nemotivo\nempuxo\nemulsao\nenamorar\nencurvar\nenduro\nenevoar\nenfurnar\nenguico\nenho\nenigmista\nenlutar\nenormidade\nenpreendimento\nenquanto\nenriquecer\nenrugar\nentusiastico\nenunciar\nenvolvimento\nenxuto\nenzimatico\neolico\nepiteto\nepoxi\nepura\nequivoco\nerario\nerbio\nereto\nerguido\nerisipela\nermo\nerotizar\nerros\nerupcao\nervilha\nesburacar\nescutar\nesfuziante\nesguio\nesloveno\nesmurrar\nesoterismo\nesperanca\nespirito\nespurio\nessencialmente\nesturricar\nesvoacar\netario\neterno\netiquetar\netnologo\netos\netrusco\neuclidiano\neuforico\neugenico\neunuco\neuropio\neustaquio\neutanasia\nevasivo\neventualidade\nevitavel\nevoluir\nexaustor\nexcursionista\nexercito\nexfoliado\nexito\nexotico\nexpurgo\nexsudar\nextrusora\nexumar\nfabuloso\nfacultativo\nfado\nfagulha\nfaixas\nfajuto\nfaltoso\nfamoso\nfanzine\nfapesp\nfaquir\nfartura\nfastio\nfaturista\nfausto\nfavorito\nfaxineira\nfazer\nfealdade\nfebril\nfecundo\nfedorento\nfeerico\nfeixe\nfelicidade\nfelipe\nfeltro\nfemur\nfenotipo\nfervura\nfestivo\nfeto\nfeudo\nfevereiro\nfezinha\nfiasco\nfibra\nficticio\nfiduciario\nfiesp\nfifa\nfigurino\nfijiano\nfiltro\nfinura\nfiorde\nfiquei\nfirula\nfissurar\nfitoteca\nfivela\nfixo\nflavio\nflexor\nflibusteiro\nflotilha\nfluxograma\nfobos\nfoco\nfofura\nfoguista\nfoie\nfoliculo\nfominha\nfonte\nforum\nfosso\nfotossintese\nfoxtrote\nfraudulento\nfrevo\nfrivolo\nfrouxo\nfrutose\nfuba\nfucsia\nfugitivo\nfuinha\nfujao\nfulustreco\nfumo\nfunileiro\nfurunculo\nfustigar\nfuturologo\nfuxico\nfuzue\ngabriel\ngado\ngaelico\ngafieira\ngaguejo\ngaivota\ngajo\ngalvanoplastico\ngamo\nganso\ngarrucha\ngastronomo\ngatuno\ngaussiano\ngaviao\ngaxeta\ngazeteiro\ngear\ngeiser\ngeminiano\ngeneroso\ngenuino\ngeossinclinal\ngerundio\ngestual\ngetulista\ngibi\ngigolo\ngilete\nginseng\ngiroscopio\nglaucio\nglacial\ngleba\nglifo\nglote\nglutonia\ngnostico\ngoela\ngogo\ngoitaca\ngolpista\ngomo\ngonzo\ngorro\ngostou\ngoticula\ngourmet\ngoverno\ngozo\ngraxo\ngrevista\ngrito\ngrotesco\ngruta\nguaxinim\ngude\ngueto\nguizo\nguloso\ngume\nguru\ngustativo\ngustavo\ngutural\nhabitue\nhaitiano\nhalterofilista\nhamburguer\nhanseniase\nhappening\nharpista\nhastear\nhaveres\nhebreu\nhectometro\nhedonista\nhegira\nhelena\nhelminto\nhemorroidas\nhenrique\nheptassilabo\nhertziano\nhesitar\nheterossexual\nheuristico\nhexagono\nhiato\nhibrido\nhidrostatico\nhieroglifo\nhifenizar\nhigienizar\nhilario\nhimen\nhino\nhippie\nhirsuto\nhistoriografia\nhitlerista\nhodometro\nhoje\nholograma\nhomus\nhonroso\nhoquei\nhorto\nhostilizar\nhotentote\nhuguenote\nhumilde\nhuno\nhurra\nhutu\niaia\nialorixa\niambico\niansa\niaque\niara\niatista\niberico\nibis\nicar\niceberg\nicosagono\nidade\nideologo\nidiotice\nidoso\niemenita\niene\nigarape\niglu\nignorar\nigreja\niguaria\niidiche\nilativo\niletrado\nilharga\nilimitado\nilogismo\nilustrissimo\nimaturo\nimbuzeiro\nimerso\nimitavel\nimovel\nimputar\nimutavel\ninaveriguavel\nincutir\ninduzir\ninextricavel\ninfusao\ningua\ninhame\niniquo\ninjusto\ninning\ninoxidavel\ninquisitorial\ninsustentavel\nintumescimento\ninutilizavel\ninvulneravel\ninzoneiro\niodo\niogurte\nioio\nionosfera\nioruba\niota\nipsilon\nirascivel\niris\nirlandes\nirmaos\niroques\nirrupcao\nisca\nisento\nislandes\nisotopo\nisqueiro\nisraelita\nisso\nisto\niterbio\nitinerario\nitrio\niuane\niugoslavo\njabuticabeira\njacutinga\njade\njagunco\njainista\njaleco\njambo\njantarada\njapones\njaqueta\njarro\njasmim\njato\njaula\njavel\njazz\njegue\njeitoso\njejum\njenipapo\njeova\njequitiba\njersei\njesus\njetom\njiboia\njihad\njilo\njingle\njipe\njocoso\njoelho\njoguete\njoio\njojoba\njorro\njota\njoule\njoviano\njubiloso\njudoca\njugular\njuizo\njujuba\njuliano\njumento\njunto\njururu\njusto\njuta\njuventude\nlabutar\nlaguna\nlaico\nlajota\nlanterninha\nlapso\nlaquear\nlastro\nlauto\nlavrar\nlaxativo\nlazer\nleasing\nlebre\nlecionar\nledo\nleguminoso\nleitura\nlele\nlemure\nlento\nleonardo\nleopardo\nlepton\nleque\nleste\nletreiro\nleucocito\nlevitico\nlexicologo\nlhama\nlhufas\nliame\nlicoroso\nlidocaina\nliliputiano\nlimusine\nlinotipo\nlipoproteina\nliquidos\nlirismo\nlisura\nliturgico\nlivros\nlixo\nlobulo\nlocutor\nlodo\nlogro\nlojista\nlombriga\nlontra\nloop\nloquaz\nlorota\nlosango\nlotus\nlouvor\nluar\nlubrificavel\nlucros\nlugubre\nluis\nluminoso\nluneta\nlustroso\nluto\nluvas\nluxuriante\nluzeiro\nmaduro\nmaestro\nmafioso\nmagro\nmaiuscula\nmajoritario\nmalvisto\nmamute\nmanutencao\nmapoteca\nmaquinista\nmarzipa\nmasturbar\nmatuto\nmausoleu\nmavioso\nmaxixe\nmazurca\nmeandro\nmecha\nmedusa\nmefistofelico\nmegera\nmeirinho\nmelro\nmemorizar\nmenu\nmequetrefe\nmertiolate\nmestria\nmetroviario\nmexilhao\nmezanino\nmiau\nmicrossegundo\nmidia\nmigratorio\nmimosa\nminuto\nmiosotis\nmirtilo\nmisturar\nmitzvah\nmiudos\nmixuruca\nmnemonico\nmoagem\nmobilizar\nmodulo\nmoer\nmofo\nmogno\nmoita\nmolusco\nmonumento\nmoqueca\nmorubixaba\nmostruario\nmotriz\nmouse\nmovivel\nmozarela\nmuarra\nmuculmano\nmudo\nmugir\nmuitos\nmumunha\nmunir\nmuon\nmuquira\nmurros\nmusselina\nnacoes\nnado\nnaftalina\nnago\nnaipe\nnaja\nnalgum\nnamoro\nnanquim\nnapolitano\nnaquilo\nnascimento\nnautilo\nnavios\nnazista\nnebuloso\nnectarina\nnefrologo\nnegus\nnelore\nnenufar\nnepotismo\nnervura\nneste\nnetuno\nneutron\nnevoeiro\nnewtoniano\nnexo\nnhenhenhem\nnhoque\nnigeriano\nniilista\nninho\nniobio\nniponico\nniquelar\nnirvana\nnisto\nnitroglicerina\nnivoso\nnobreza\nnocivo\nnoel\nnogueira\nnoivo\nnojo\nnominativo\nnonuplo\nnoruegues\nnostalgico\nnoturno\nnouveau\nnuanca\nnublar\nnucleotideo\nnudista\nnulo\nnumismatico\nnunquinha\nnupcias\nnutritivo\nnuvens\noasis\nobcecar\nobeso\nobituario\nobjetos\noblongo\nobnoxio\nobrigatorio\nobstruir\nobtuso\nobus\nobvio\nocaso\noccipital\noceanografo\nocioso\noclusivo\nocorrer\nocre\noctogono\nodalisca\nodisseia\nodorifico\noersted\noeste\nofertar\nofidio\noftalmologo\nogiva\nogum\noigale\noitavo\noitocentos\nojeriza\nolaria\noleoso\nolfato\nolhos\noliveira\nolmo\nolor\nolvidavel\nombudsman\nomeleteira\nomitir\nomoplata\nonanismo\nondular\noneroso\nonomatopeico\nontologico\nonus\nonze\nopalescente\nopcional\noperistico\nopio\noposto\noprobrio\noptometrista\nopusculo\noratorio\norbital\norcar\norfao\norixa\norla\nornitologo\norquidea\nortorrombico\norvalho\nosculo\nosmotico\nossudo\nostrogodo\notario\notite\nouro\nousar\noutubro\nouvir\novario\novernight\noviparo\novni\novoviviparo\novulo\noxala\noxente\noxiuro\noxossi\nozonizar\npaciente\npactuar\npadronizar\npaete\npagodeiro\npaixao\npajem\npaludismo\npampas\npanturrilha\npapudo\npaquistanes\npastoso\npatua\npaulo\npauzinhos\npavoroso\npaxa\npazes\npeao\npecuniario\npedunculo\npegaso\npeixinho\npejorativo\npelvis\npenuria\npequno\npetunia\npezada\npiauiense\npictorico\npierro\npigmeu\npijama\npilulas\npimpolho\npintura\npiorar\npipocar\npiqueteiro\npirulito\npistoleiro\npituitaria\npivotar\npixote\npizzaria\nplistoceno\nplotar\npluviometrico\npneumonico\npoco\npodridao\npoetisa\npogrom\npois\npolvorosa\npomposo\nponderado\npontudo\npopuloso\npoquer\nporvir\nposudo\npotro\npouso\npovoar\nprazo\nprezar\nprivilegios\nproximo\nprussiano\npseudopode\npsoriase\npterossauros\nptialina\nptolemaico\npudor\npueril\npufe\npugilista\npuir\npujante\npulverizar\npumba\npunk\npurulento\npustula\nputsch\npuxe\nquatrocentos\nquetzal\nquixotesco\nquotizavel\nrabujice\nracista\nradonio\nrafia\nragu\nrajado\nralo\nrampeiro\nranzinza\nraptor\nraquitismo\nraro\nrasurar\nratoeira\nravioli\nrazoavel\nreavivar\nrebuscar\nrecusavel\nreduzivel\nreexposicao\nrefutavel\nregurgitar\nreivindicavel\nrejuvenescimento\nrelva\nremuneravel\nrenunciar\nreorientar\nrepuxo\nrequisito\nresumo\nreturno\nreutilizar\nrevolvido\nrezonear\nriacho\nribossomo\nricota\nridiculo\nrifle\nrigoroso\nrijo\nrimel\nrins\nrios\nriqueza\nriquixa\nrissole\nritualistico\nrivalizar\nrixa\nrobusto\nrococo\nrodoviario\nroer\nrogo\nrojao\nrolo\nrompimento\nronronar\nroqueiro\nrorqual\nrosto\nrotundo\nrouxinol\nroxo\nroyal\nruas\nrucula\nrudimentos\nruela\nrufo\nrugoso\nruivo\nrule\nrumoroso\nrunico\nruptura\nrural\nrustico\nrutilar\nsaariano\nsabujo\nsacudir\nsadomasoquista\nsafra\nsagui\nsais\nsamurai\nsantuario\nsapo\nsaquear\nsartriano\nsaturno\nsaude\nsauva\nsaveiro\nsaxofonista\nsazonal\nscherzo\nscript\nseara\nseborreia\nsecura\nseduzir\nsefardim\nseguro\nseja\nselvas\nsempre\nsenzala\nsepultura\nsequoia\nsestercio\nsetuplo\nseus\nseviciar\nsezonismo\nshalom\nsiames\nsibilante\nsicrano\nsidra\nsifilitico\nsignos\nsilvo\nsimultaneo\nsinusite\nsionista\nsirio\nsisudo\nsituar\nsivan\nslide\nslogan\nsoar\nsobrio\nsocratico\nsodomizar\nsoerguer\nsoftware\nsogro\nsoja\nsolver\nsomente\nsonso\nsopro\nsoquete\nsorveteiro\nsossego\nsoturno\nsousafone\nsovinice\nsozinho\nsuavizar\nsubverter\nsucursal\nsudoriparo\nsufragio\nsugestoes\nsuite\nsujo\nsultao\nsumula\nsuntuoso\nsuor\nsupurar\nsuruba\nsusto\nsuturar\nsuvenir\ntabuleta\ntaco\ntadjique\ntafeta\ntagarelice\ntaitiano\ntalvez\ntampouco\ntanzaniano\ntaoista\ntapume\ntaquion\ntarugo\ntascar\ntatuar\ntautologico\ntavola\ntaxionomista\ntchecoslovaco\nteatrologo\ntectonismo\ntedioso\nteflon\ntegumento\nteixo\ntelurio\ntemporas\ntenue\nteosofico\ntepido\ntequila\nterrorista\ntestosterona\ntetrico\nteutonico\nteve\ntexugo\ntiara\ntibia\ntiete\ntifoide\ntigresa\ntijolo\ntilintar\ntimpano\ntintureiro\ntiquete\ntiroteio\ntisico\ntitulos\ntive\ntoar\ntoboga\ntofu\ntogoles\ntoicinho\ntolueno\ntomografo\ntontura\ntoponimo\ntoquio\ntorvelinho\ntostar\ntoto\ntouro\ntoxina\ntrazer\ntrezentos\ntrivialidade\ntrovoar\ntruta\ntuaregue\ntubular\ntucano\ntudo\ntufo\ntuiste\ntulipa\ntumultuoso\ntunisino\ntupiniquim\nturvo\ntutu\nucraniano\nudenista\nufanista\nufologo\nugaritico\nuiste\nuivo\nulceroso\nulema\nultravioleta\numbilical\numero\numido\numlaut\nunanimidade\nunesco\nungulado\nunheiro\nunivoco\nuntuoso\nurano\nurbano\nurdir\nuretra\nurgente\nurinol\nurna\nurologo\nurro\nursulina\nurtiga\nurupe\nusavel\nusbeque\nusei\nusineiro\nusurpar\nutero\nutilizar\nutopico\nuvular\nuxoricidio\nvacuo\nvadio\nvaguear\nvaivem\nvalvula\nvampiro\nvantajoso\nvaporoso\nvaquinha\nvarziano\nvasto\nvaticinio\nvaudeville\nvazio\nveado\nvedico\nveemente\nvegetativo\nveio\nveja\nveludo\nvenusiano\nverdade\nverve\nvestuario\nvetusto\nvexatorio\nvezes\nviavel\nvibratorio\nvictor\nvicunha\nvidros\nvietnamita\nvigoroso\nvilipendiar\nvime\nvintem\nvioloncelo\nviquingue\nvirus\nvisualizar\nvituperio\nviuvo\nvivo\nvizir\nvoar\nvociferar\nvodu\nvogar\nvoile\nvolver\nvomito\nvontade\nvortice\nvosso\nvoto\nvovozinha\nvoyeuse\nvozes\nvulva\nvupt\nwestern\nxadrez\nxale\nxampu\nxango\nxarope\nxaual\nxavante\nxaxim\nxenonio\nxepa\nxerox\nxicara\nxifopago\nxiita\nxilogravura\nxinxim\nxistoso\nxixi\nxodo\nxogum\nxucro\nzabumba\nzagueiro\nzambiano\nzanzar\nzarpar\nzebu\nzefiro\nzeloso\nzenite\nzumbi\n"
  },
  {
    "path": "electrum/wordlist/slip39.txt",
    "content": "academic\nacid\nacne\nacquire\nacrobat\nactivity\nactress\nadapt\nadequate\nadjust\nadmit\nadorn\nadult\nadvance\nadvocate\nafraid\nagain\nagency\nagree\naide\naircraft\nairline\nairport\najar\nalarm\nalbum\nalcohol\nalien\nalive\nalpha\nalready\nalto\naluminum\nalways\namazing\nambition\namount\namuse\nanalysis\nanatomy\nancestor\nancient\nangel\nangry\nanimal\nanswer\nantenna\nanxiety\napart\naquatic\narcade\narena\nargue\narmed\nartist\nartwork\naspect\nauction\naugust\naunt\naverage\naviation\navoid\naward\naway\naxis\naxle\nbeam\nbeard\nbeaver\nbecome\nbedroom\nbehavior\nbeing\nbelieve\nbelong\nbenefit\nbest\nbeyond\nbike\nbiology\nbirthday\nbishop\nblack\nblanket\nblessing\nblimp\nblind\nblue\nbody\nbolt\nboring\nborn\nboth\nboundary\nbracelet\nbranch\nbrave\nbreathe\nbriefing\nbroken\nbrother\nbrowser\nbucket\nbudget\nbuilding\nbulb\nbulge\nbumpy\nbundle\nburden\nburning\nbusy\nbuyer\ncage\ncalcium\ncamera\ncampus\ncanyon\ncapacity\ncapital\ncapture\ncarbon\ncards\ncareful\ncargo\ncarpet\ncarve\ncategory\ncause\nceiling\ncenter\nceramic\nchampion\nchange\ncharity\ncheck\nchemical\nchest\nchew\nchubby\ncinema\ncivil\nclass\nclay\ncleanup\nclient\nclimate\nclinic\nclock\nclogs\ncloset\nclothes\nclub\ncluster\ncoal\ncoastal\ncoding\ncolumn\ncompany\ncorner\ncostume\ncounter\ncourse\ncover\ncowboy\ncradle\ncraft\ncrazy\ncredit\ncricket\ncriminal\ncrisis\ncritical\ncrowd\ncrucial\ncrunch\ncrush\ncrystal\ncubic\ncultural\ncurious\ncurly\ncustody\ncylinder\ndaisy\ndamage\ndance\ndarkness\ndatabase\ndaughter\ndeadline\ndeal\ndebris\ndebut\ndecent\ndecision\ndeclare\ndecorate\ndecrease\ndeliver\ndemand\ndensity\ndeny\ndepart\ndepend\ndepict\ndeploy\ndescribe\ndesert\ndesire\ndesktop\ndestroy\ndetailed\ndetect\ndevice\ndevote\ndiagnose\ndictate\ndiet\ndilemma\ndiminish\ndining\ndiploma\ndisaster\ndiscuss\ndisease\ndish\ndismiss\ndisplay\ndistance\ndive\ndivorce\ndocument\ndomain\ndomestic\ndominant\ndough\ndowntown\ndragon\ndramatic\ndream\ndress\ndrift\ndrink\ndrove\ndrug\ndryer\nduckling\nduke\nduration\ndwarf\ndynamic\nearly\nearth\neasel\neasy\necho\neclipse\necology\nedge\neditor\neducate\neither\nelbow\nelder\nelection\nelegant\nelement\nelephant\nelevator\nelite\nelse\nemail\nemerald\nemission\nemperor\nemphasis\nemployer\nempty\nending\nendless\nendorse\nenemy\nenergy\nenforce\nengage\nenjoy\nenlarge\nentrance\nenvelope\nenvy\nepidemic\nepisode\nequation\nequip\neraser\nerode\nescape\nestate\nestimate\nevaluate\nevening\nevidence\nevil\nevoke\nexact\nexample\nexceed\nexchange\nexclude\nexcuse\nexecute\nexercise\nexhaust\nexotic\nexpand\nexpect\nexplain\nexpress\nextend\nextra\neyebrow\nfacility\nfact\nfailure\nfaint\nfake\nfalse\nfamily\nfamous\nfancy\nfangs\nfantasy\nfatal\nfatigue\nfavorite\nfawn\nfiber\nfiction\nfilter\nfinance\nfindings\nfinger\nfirefly\nfirm\nfiscal\nfishing\nfitness\nflame\nflash\nflavor\nflea\nflexible\nflip\nfloat\nfloral\nfluff\nfocus\nforbid\nforce\nforecast\nforget\nformal\nfortune\nforward\nfounder\nfraction\nfragment\nfrequent\nfreshman\nfriar\nfridge\nfriendly\nfrost\nfroth\nfrozen\nfumes\nfunding\nfurl\nfused\ngalaxy\ngame\ngarbage\ngarden\ngarlic\ngasoline\ngather\ngeneral\ngenius\ngenre\ngenuine\ngeology\ngesture\nglad\nglance\nglasses\nglen\nglimpse\ngoat\ngolden\ngraduate\ngrant\ngrasp\ngravity\ngray\ngreatest\ngrief\ngrill\ngrin\ngrocery\ngross\ngroup\ngrownup\ngrumpy\nguard\nguest\nguilt\nguitar\ngums\nhairy\nhamster\nhand\nhanger\nharvest\nhave\nhavoc\nhawk\nhazard\nheadset\nhealth\nhearing\nheat\nhelpful\nherald\nherd\nhesitate\nhobo\nholiday\nholy\nhome\nhormone\nhospital\nhour\nhuge\nhuman\nhumidity\nhunting\nhusband\nhush\nhusky\nhybrid\nidea\nidentify\nidle\nimage\nimpact\nimply\nimprove\nimpulse\ninclude\nincome\nincrease\nindex\nindicate\nindustry\ninfant\ninform\ninherit\ninjury\ninmate\ninsect\ninside\ninstall\nintend\nintimate\ninvasion\ninvolve\niris\nisland\nisolate\nitem\nivory\njacket\njerky\njewelry\njoin\njudicial\njuice\njump\njunction\njunior\njunk\njury\njustice\nkernel\nkeyboard\nkidney\nkind\nkitchen\nknife\nknit\nladen\nladle\nladybug\nlair\nlamp\nlanguage\nlarge\nlaser\nlaundry\nlawsuit\nleader\nleaf\nlearn\nleaves\nlecture\nlegal\nlegend\nlegs\nlend\nlength\nlevel\nliberty\nlibrary\nlicense\nlift\nlikely\nlilac\nlily\nlips\nliquid\nlisten\nliterary\nliving\nlizard\nloan\nlobe\nlocation\nlosing\nloud\nloyalty\nluck\nlunar\nlunch\nlungs\nluxury\nlying\nlyrics\nmachine\nmagazine\nmaiden\nmailman\nmain\nmakeup\nmaking\nmama\nmanager\nmandate\nmansion\nmanual\nmarathon\nmarch\nmarket\nmarvel\nmason\nmaterial\nmath\nmaximum\nmayor\nmeaning\nmedal\nmedical\nmember\nmemory\nmental\nmerchant\nmerit\nmethod\nmetric\nmidst\nmild\nmilitary\nmineral\nminister\nmiracle\nmixed\nmixture\nmobile\nmodern\nmodify\nmoisture\nmoment\nmorning\nmortgage\nmother\nmountain\nmouse\nmove\nmuch\nmule\nmultiple\nmuscle\nmuseum\nmusic\nmustang\nnail\nnational\nnecklace\nnegative\nnervous\nnetwork\nnews\nnuclear\nnumb\nnumerous\nnylon\noasis\nobesity\nobject\nobserve\nobtain\nocean\noften\nolympic\nomit\noral\norange\norbit\norder\nordinary\norganize\nounce\noven\noverall\nowner\npaces\npacific\npackage\npaid\npainting\npajamas\npancake\npants\npapa\npaper\nparcel\nparking\nparty\npatent\npatrol\npayment\npayroll\npeaceful\npeanut\npeasant\npecan\npenalty\npencil\npercent\nperfect\npermit\npetition\nphantom\npharmacy\nphoto\nphrase\nphysics\npickup\npicture\npiece\npile\npink\npipeline\npistol\npitch\nplains\nplan\nplastic\nplatform\nplayoff\npleasure\nplot\nplunge\npractice\nprayer\npreach\npredator\npregnant\npremium\nprepare\npresence\nprevent\npriest\nprimary\npriority\nprisoner\nprivacy\nprize\nproblem\nprocess\nprofile\nprogram\npromise\nprospect\nprovide\nprune\npublic\npulse\npumps\npunish\npuny\npupal\npurchase\npurple\npython\nquantity\nquarter\nquick\nquiet\nrace\nracism\nradar\nrailroad\nrainbow\nraisin\nrandom\nranked\nrapids\nraspy\nreaction\nrealize\nrebound\nrebuild\nrecall\nreceiver\nrecover\nregret\nregular\nreject\nrelate\nremember\nremind\nremove\nrender\nrepair\nrepeat\nreplace\nrequire\nrescue\nresearch\nresident\nresponse\nresult\nretailer\nretreat\nreunion\nrevenue\nreview\nreward\nrhyme\nrhythm\nrich\nrival\nriver\nrobin\nrocky\nromantic\nromp\nroster\nround\nroyal\nruin\nruler\nrumor\nsack\nsafari\nsalary\nsalon\nsalt\nsatisfy\nsatoshi\nsaver\nsays\nscandal\nscared\nscatter\nscene\nscholar\nscience\nscout\nscramble\nscrew\nscript\nscroll\nseafood\nseason\nsecret\nsecurity\nsegment\nsenior\nshadow\nshaft\nshame\nshaped\nsharp\nshelter\nsheriff\nshort\nshould\nshrimp\nsidewalk\nsilent\nsilver\nsimilar\nsimple\nsingle\nsister\nskin\nskunk\nslap\nslavery\nsled\nslice\nslim\nslow\nslush\nsmart\nsmear\nsmell\nsmirk\nsmith\nsmoking\nsmug\nsnake\nsnapshot\nsniff\nsociety\nsoftware\nsoldier\nsolution\nsoul\nsource\nspace\nspark\nspeak\nspecies\nspelling\nspend\nspew\nspider\nspill\nspine\nspirit\nspit\nspray\nsprinkle\nsquare\nsqueeze\nstadium\nstaff\nstandard\nstarting\nstation\nstay\nsteady\nstep\nstick\nstilt\nstory\nstrategy\nstrike\nstyle\nsubject\nsubmit\nsugar\nsuitable\nsunlight\nsuperior\nsurface\nsurprise\nsurvive\nsweater\nswimming\nswing\nswitch\nsymbolic\nsympathy\nsyndrome\nsystem\ntackle\ntactics\ntadpole\ntalent\ntask\ntaste\ntaught\ntaxi\nteacher\nteammate\nteaspoon\ntemple\ntenant\ntendency\ntension\nterminal\ntestify\ntexture\nthank\nthat\ntheater\ntheory\ntherapy\nthorn\nthreaten\nthumb\nthunder\nticket\ntidy\ntimber\ntimely\nting\ntofu\ntogether\ntolerate\ntotal\ntoxic\ntracks\ntraffic\ntraining\ntransfer\ntrash\ntraveler\ntreat\ntrend\ntrial\ntricycle\ntrip\ntriumph\ntrouble\ntrue\ntrust\ntwice\ntwin\ntype\ntypical\nugly\nultimate\numbrella\nuncover\nundergo\nunfair\nunfold\nunhappy\nunion\nuniverse\nunkind\nunknown\nunusual\nunwrap\nupgrade\nupstairs\nusername\nusher\nusual\nvalid\nvaluable\nvampire\nvanish\nvarious\nvegan\nvelvet\nventure\nverdict\nverify\nvery\nveteran\nvexed\nvictim\nvideo\nview\nvintage\nviolence\nviral\nvisitor\nvisual\nvitamins\nvocal\nvoice\nvolume\nvoter\nvoting\nwalnut\nwarmth\nwarn\nwatch\nwavy\nwealthy\nweapon\nwebcam\nwelcome\nwelfare\nwestern\nwidth\nwildlife\nwindow\nwine\nwireless\nwisdom\nwithdraw\nwits\nwolf\nwoman\nwork\nworthy\nwrap\nwrist\nwriting\nwrote\nyear\nyelp\nyield\nyoga\nzero\n"
  },
  {
    "path": "electrum/wordlist/spanish.txt",
    "content": "ábaco\nabdomen\nabeja\nabierto\nabogado\nabono\naborto\nabrazo\nabrir\nabuelo\nabuso\nacabar\nacademia\nacceso\nacción\naceite\nacelga\nacento\naceptar\nácido\naclarar\nacné\nacoger\nacoso\nactivo\nacto\nactriz\nactuar\nacudir\nacuerdo\nacusar\nadicto\nadmitir\nadoptar\nadorno\naduana\nadulto\naéreo\nafectar\nafición\nafinar\nafirmar\nágil\nagitar\nagonía\nagosto\nagotar\nagregar\nagrio\nagua\nagudo\náguila\naguja\nahogo\nahorro\naire\naislar\najedrez\najeno\najuste\nalacrán\nalambre\nalarma\nalba\nálbum\nalcalde\naldea\nalegre\nalejar\nalerta\naleta\nalfiler\nalga\nalgodón\naliado\naliento\nalivio\nalma\nalmeja\nalmíbar\naltar\nalteza\naltivo\nalto\naltura\nalumno\nalzar\namable\namante\namapola\namargo\namasar\námbar\námbito\nameno\namigo\namistad\namor\namparo\namplio\nancho\nanciano\nancla\nandar\nandén\nanemia\nángulo\nanillo\nánimo\nanís\nanotar\nantena\nantiguo\nantojo\nanual\nanular\nanuncio\nañadir\nañejo\naño\napagar\naparato\napetito\napio\naplicar\napodo\naporte\napoyo\naprender\naprobar\napuesta\napuro\narado\naraña\narar\nárbitro\nárbol\narbusto\narchivo\narco\narder\nardilla\narduo\nárea\nárido\naries\narmonía\narnés\naroma\narpa\narpón\narreglo\narroz\narruga\narte\nartista\nasa\nasado\nasalto\nascenso\nasegurar\naseo\nasesor\nasiento\nasilo\nasistir\nasno\nasombro\náspero\nastilla\nastro\nastuto\nasumir\nasunto\natajo\nataque\natar\natento\nateo\nático\natleta\nátomo\natraer\natroz\natún\naudaz\naudio\nauge\naula\naumento\nausente\nautor\naval\navance\navaro\nave\navellana\navena\navestruz\navión\naviso\nayer\nayuda\nayuno\nazafrán\nazar\nazote\nazúcar\nazufre\nazul\nbaba\nbabor\nbache\nbahía\nbaile\nbajar\nbalanza\nbalcón\nbalde\nbambú\nbanco\nbanda\nbaño\nbarba\nbarco\nbarniz\nbarro\nbáscula\nbastón\nbasura\nbatalla\nbatería\nbatir\nbatuta\nbaúl\nbazar\nbebé\nbebida\nbello\nbesar\nbeso\nbestia\nbicho\nbien\nbingo\nblanco\nbloque\nblusa\nboa\nbobina\nbobo\nboca\nbocina\nboda\nbodega\nboina\nbola\nbolero\nbolsa\nbomba\nbondad\nbonito\nbono\nbonsái\nborde\nborrar\nbosque\nbote\nbotín\nbóveda\nbozal\nbravo\nbrazo\nbrecha\nbreve\nbrillo\nbrinco\nbrisa\nbroca\nbroma\nbronce\nbrote\nbruja\nbrusco\nbruto\nbuceo\nbucle\nbueno\nbuey\nbufanda\nbufón\nbúho\nbuitre\nbulto\nburbuja\nburla\nburro\nbuscar\nbutaca\nbuzón\ncaballo\ncabeza\ncabina\ncabra\ncacao\ncadáver\ncadena\ncaer\ncafé\ncaída\ncaimán\ncaja\ncajón\ncal\ncalamar\ncalcio\ncaldo\ncalidad\ncalle\ncalma\ncalor\ncalvo\ncama\ncambio\ncamello\ncamino\ncampo\ncáncer\ncandil\ncanela\ncanguro\ncanica\ncanto\ncaña\ncañón\ncaoba\ncaos\ncapaz\ncapitán\ncapote\ncaptar\ncapucha\ncara\ncarbón\ncárcel\ncareta\ncarga\ncariño\ncarne\ncarpeta\ncarro\ncarta\ncasa\ncasco\ncasero\ncaspa\ncastor\ncatorce\ncatre\ncaudal\ncausa\ncazo\ncebolla\nceder\ncedro\ncelda\ncélebre\nceloso\ncélula\ncemento\nceniza\ncentro\ncerca\ncerdo\ncereza\ncero\ncerrar\ncerteza\ncésped\ncetro\nchacal\nchaleco\nchampú\nchancla\nchapa\ncharla\nchico\nchiste\nchivo\nchoque\nchoza\nchuleta\nchupar\nciclón\nciego\ncielo\ncien\ncierto\ncifra\ncigarro\ncima\ncinco\ncine\ncinta\nciprés\ncirco\nciruela\ncisne\ncita\nciudad\nclamor\nclan\nclaro\nclase\nclave\ncliente\nclima\nclínica\ncobre\ncocción\ncochino\ncocina\ncoco\ncódigo\ncodo\ncofre\ncoger\ncohete\ncojín\ncojo\ncola\ncolcha\ncolegio\ncolgar\ncolina\ncollar\ncolmo\ncolumna\ncombate\ncomer\ncomida\ncómodo\ncompra\nconde\nconejo\nconga\nconocer\nconsejo\ncontar\ncopa\ncopia\ncorazón\ncorbata\ncorcho\ncordón\ncorona\ncorrer\ncoser\ncosmos\ncosta\ncráneo\ncráter\ncrear\ncrecer\ncreído\ncrema\ncría\ncrimen\ncripta\ncrisis\ncromo\ncrónica\ncroqueta\ncrudo\ncruz\ncuadro\ncuarto\ncuatro\ncubo\ncubrir\ncuchara\ncuello\ncuento\ncuerda\ncuesta\ncueva\ncuidar\nculebra\nculpa\nculto\ncumbre\ncumplir\ncuna\ncuneta\ncuota\ncupón\ncúpula\ncurar\ncurioso\ncurso\ncurva\ncutis\ndama\ndanza\ndar\ndardo\ndátil\ndeber\ndébil\ndécada\ndecir\ndedo\ndefensa\ndefinir\ndejar\ndelfín\ndelgado\ndelito\ndemora\ndenso\ndental\ndeporte\nderecho\nderrota\ndesayuno\ndeseo\ndesfile\ndesnudo\ndestino\ndesvío\ndetalle\ndetener\ndeuda\ndía\ndiablo\ndiadema\ndiamante\ndiana\ndiario\ndibujo\ndictar\ndiente\ndieta\ndiez\ndifícil\ndigno\ndilema\ndiluir\ndinero\ndirecto\ndirigir\ndisco\ndiseño\ndisfraz\ndiva\ndivino\ndoble\ndoce\ndolor\ndomingo\ndon\ndonar\ndorado\ndormir\ndorso\ndos\ndosis\ndragón\ndroga\nducha\nduda\nduelo\ndueño\ndulce\ndúo\nduque\ndurar\ndureza\nduro\nébano\nebrio\nechar\neco\necuador\nedad\nedición\nedificio\neditor\neducar\nefecto\neficaz\neje\nejemplo\nelefante\nelegir\nelemento\nelevar\nelipse\nélite\nelixir\nelogio\neludir\nembudo\nemitir\nemoción\nempate\nempeño\nempleo\nempresa\nenano\nencargo\nenchufe\nencía\nenemigo\nenero\nenfado\nenfermo\nengaño\nenigma\nenlace\nenorme\nenredo\nensayo\nenseñar\nentero\nentrar\nenvase\nenvío\népoca\nequipo\nerizo\nescala\nescena\nescolar\nescribir\nescudo\nesencia\nesfera\nesfuerzo\nespada\nespejo\nespía\nesposa\nespuma\nesquí\nestar\neste\nestilo\nestufa\netapa\neterno\nética\netnia\nevadir\nevaluar\nevento\nevitar\nexacto\nexamen\nexceso\nexcusa\nexento\nexigir\nexilio\nexistir\néxito\nexperto\nexplicar\nexponer\nextremo\nfábrica\nfábula\nfachada\nfácil\nfactor\nfaena\nfaja\nfalda\nfallo\nfalso\nfaltar\nfama\nfamilia\nfamoso\nfaraón\nfarmacia\nfarol\nfarsa\nfase\nfatiga\nfauna\nfavor\nfax\nfebrero\nfecha\nfeliz\nfeo\nferia\nferoz\nfértil\nfervor\nfestín\nfiable\nfianza\nfiar\nfibra\nficción\nficha\nfideo\nfiebre\nfiel\nfiera\nfiesta\nfigura\nfijar\nfijo\nfila\nfilete\nfilial\nfiltro\nfin\nfinca\nfingir\nfinito\nfirma\nflaco\nflauta\nflecha\nflor\nflota\nfluir\nflujo\nflúor\nfobia\nfoca\nfogata\nfogón\nfolio\nfolleto\nfondo\nforma\nforro\nfortuna\nforzar\nfosa\nfoto\nfracaso\nfrágil\nfranja\nfrase\nfraude\nfreír\nfreno\nfresa\nfrío\nfrito\nfruta\nfuego\nfuente\nfuerza\nfuga\nfumar\nfunción\nfunda\nfurgón\nfuria\nfusil\nfútbol\nfuturo\ngacela\ngafas\ngaita\ngajo\ngala\ngalería\ngallo\ngamba\nganar\ngancho\nganga\nganso\ngaraje\ngarza\ngasolina\ngastar\ngato\ngavilán\ngemelo\ngemir\ngen\ngénero\ngenio\ngente\ngeranio\ngerente\ngermen\ngesto\ngigante\ngimnasio\ngirar\ngiro\nglaciar\nglobo\ngloria\ngol\ngolfo\ngoloso\ngolpe\ngoma\ngordo\ngorila\ngorra\ngota\ngoteo\ngozar\ngrada\ngráfico\ngrano\ngrasa\ngratis\ngrave\ngrieta\ngrillo\ngripe\ngris\ngrito\ngrosor\ngrúa\ngrueso\ngrumo\ngrupo\nguante\nguapo\nguardia\nguerra\nguía\nguiño\nguion\nguiso\nguitarra\ngusano\ngustar\nhaber\nhábil\nhablar\nhacer\nhacha\nhada\nhallar\nhamaca\nharina\nhaz\nhazaña\nhebilla\nhebra\nhecho\nhelado\nhelio\nhembra\nherir\nhermano\nhéroe\nhervir\nhielo\nhierro\nhígado\nhigiene\nhijo\nhimno\nhistoria\nhocico\nhogar\nhoguera\nhoja\nhombre\nhongo\nhonor\nhonra\nhora\nhormiga\nhorno\nhostil\nhoyo\nhueco\nhuelga\nhuerta\nhueso\nhuevo\nhuida\nhuir\nhumano\nhúmedo\nhumilde\nhumo\nhundir\nhuracán\nhurto\nicono\nideal\nidioma\nídolo\niglesia\niglú\nigual\nilegal\nilusión\nimagen\nimán\nimitar\nimpar\nimperio\nimponer\nimpulso\nincapaz\níndice\ninerte\ninfiel\ninforme\ningenio\ninicio\ninmenso\ninmune\ninnato\ninsecto\ninstante\ninterés\níntimo\nintuir\ninútil\ninvierno\nira\niris\nironía\nisla\nislote\njabalí\njabón\njamón\njarabe\njardín\njarra\njaula\njazmín\njefe\njeringa\njinete\njornada\njoroba\njoven\njoya\njuerga\njueves\njuez\njugador\njugo\njuguete\njuicio\njunco\njungla\njunio\njuntar\njúpiter\njurar\njusto\njuvenil\njuzgar\nkilo\nkoala\nlabio\nlacio\nlacra\nlado\nladrón\nlagarto\nlágrima\nlaguna\nlaico\nlamer\nlámina\nlámpara\nlana\nlancha\nlangosta\nlanza\nlápiz\nlargo\nlarva\nlástima\nlata\nlátex\nlatir\nlaurel\nlavar\nlazo\nleal\nlección\nleche\nlector\nleer\nlegión\nlegumbre\nlejano\nlengua\nlento\nleña\nleón\nleopardo\nlesión\nletal\nletra\nleve\nleyenda\nlibertad\nlibro\nlicor\nlíder\nlidiar\nlienzo\nliga\nligero\nlima\nlímite\nlimón\nlimpio\nlince\nlindo\nlínea\nlingote\nlino\nlinterna\nlíquido\nliso\nlista\nlitera\nlitio\nlitro\nllaga\nllama\nllanto\nllave\nllegar\nllenar\nllevar\nllorar\nllover\nlluvia\nlobo\nloción\nloco\nlocura\nlógica\nlogro\nlombriz\nlomo\nlonja\nlote\nlucha\nlucir\nlugar\nlujo\nluna\nlunes\nlupa\nlustro\nluto\nluz\nmaceta\nmacho\nmadera\nmadre\nmaduro\nmaestro\nmafia\nmagia\nmago\nmaíz\nmaldad\nmaleta\nmalla\nmalo\nmamá\nmambo\nmamut\nmanco\nmando\nmanejar\nmanga\nmaniquí\nmanjar\nmano\nmanso\nmanta\nmañana\nmapa\nmáquina\nmar\nmarco\nmarea\nmarfil\nmargen\nmarido\nmármol\nmarrón\nmartes\nmarzo\nmasa\nmáscara\nmasivo\nmatar\nmateria\nmatiz\nmatriz\nmáximo\nmayor\nmazorca\nmecha\nmedalla\nmedio\nmédula\nmejilla\nmejor\nmelena\nmelón\nmemoria\nmenor\nmensaje\nmente\nmenú\nmercado\nmerengue\nmérito\nmes\nmesón\nmeta\nmeter\nmétodo\nmetro\nmezcla\nmiedo\nmiel\nmiembro\nmiga\nmil\nmilagro\nmilitar\nmillón\nmimo\nmina\nminero\nmínimo\nminuto\nmiope\nmirar\nmisa\nmiseria\nmisil\nmismo\nmitad\nmito\nmochila\nmoción\nmoda\nmodelo\nmoho\nmojar\nmolde\nmoler\nmolino\nmomento\nmomia\nmonarca\nmoneda\nmonja\nmonto\nmoño\nmorada\nmorder\nmoreno\nmorir\nmorro\nmorsa\nmortal\nmosca\nmostrar\nmotivo\nmover\nmóvil\nmozo\nmucho\nmudar\nmueble\nmuela\nmuerte\nmuestra\nmugre\nmujer\nmula\nmuleta\nmulta\nmundo\nmuñeca\nmural\nmuro\nmúsculo\nmuseo\nmusgo\nmúsica\nmuslo\nnácar\nnación\nnadar\nnaipe\nnaranja\nnariz\nnarrar\nnasal\nnatal\nnativo\nnatural\nnáusea\nnaval\nnave\nnavidad\nnecio\nnéctar\nnegar\nnegocio\nnegro\nneón\nnervio\nneto\nneutro\nnevar\nnevera\nnicho\nnido\nniebla\nnieto\nniñez\nniño\nnítido\nnivel\nnobleza\nnoche\nnómina\nnoria\nnorma\nnorte\nnota\nnoticia\nnovato\nnovela\nnovio\nnube\nnuca\nnúcleo\nnudillo\nnudo\nnuera\nnueve\nnuez\nnulo\nnúmero\nnutria\noasis\nobeso\nobispo\nobjeto\nobra\nobrero\nobservar\nobtener\nobvio\noca\nocaso\nocéano\nochenta\nocho\nocio\nocre\noctavo\noctubre\noculto\nocupar\nocurrir\nodiar\nodio\nodisea\noeste\nofensa\noferta\noficio\nofrecer\nogro\noído\noír\nojo\nola\noleada\nolfato\nolivo\nolla\nolmo\nolor\nolvido\nombligo\nonda\nonza\nopaco\nopción\nópera\nopinar\noponer\noptar\nóptica\nopuesto\noración\norador\noral\nórbita\norca\norden\noreja\nórgano\norgía\norgullo\noriente\norigen\norilla\noro\norquesta\noruga\nosadía\noscuro\nosezno\noso\nostra\notoño\notro\noveja\nóvulo\nóxido\noxígeno\noyente\nozono\npacto\npadre\npaella\npágina\npago\npaís\npájaro\npalabra\npalco\npaleta\npálido\npalma\npaloma\npalpar\npan\npanal\npánico\npantera\npañuelo\npapá\npapel\npapilla\npaquete\nparar\nparcela\npared\nparir\nparo\npárpado\nparque\npárrafo\nparte\npasar\npaseo\npasión\npaso\npasta\npata\npatio\npatria\npausa\npauta\npavo\npayaso\npeatón\npecado\npecera\npecho\npedal\npedir\npegar\npeine\npelar\npeldaño\npelea\npeligro\npellejo\npelo\npeluca\npena\npensar\npeñón\npeón\npeor\npepino\npequeño\npera\npercha\nperder\npereza\nperfil\nperico\nperla\npermiso\nperro\npersona\npesa\npesca\npésimo\npestaña\npétalo\npetróleo\npez\npezuña\npicar\npichón\npie\npiedra\npierna\npieza\npijama\npilar\npiloto\npimienta\npino\npintor\npinza\npiña\npiojo\npipa\npirata\npisar\npiscina\npiso\npista\npitón\npizca\nplaca\nplan\nplata\nplaya\nplaza\npleito\npleno\nplomo\npluma\nplural\npobre\npoco\npoder\npodio\npoema\npoesía\npoeta\npolen\npolicía\npollo\npolvo\npomada\npomelo\npomo\npompa\nponer\nporción\nportal\nposada\nposeer\nposible\nposte\npotencia\npotro\npozo\nprado\nprecoz\npregunta\npremio\nprensa\npreso\nprevio\nprimo\npríncipe\nprisión\nprivar\nproa\nprobar\nproceso\nproducto\nproeza\nprofesor\nprograma\nprole\npromesa\npronto\npropio\npróximo\nprueba\npúblico\npuchero\npudor\npueblo\npuerta\npuesto\npulga\npulir\npulmón\npulpo\npulso\npuma\npunto\npuñal\npuño\npupa\npupila\npuré\nquedar\nqueja\nquemar\nquerer\nqueso\nquieto\nquímica\nquince\nquitar\nrábano\nrabia\nrabo\nración\nradical\nraíz\nrama\nrampa\nrancho\nrango\nrapaz\nrápido\nrapto\nrasgo\nraspa\nrato\nrayo\nraza\nrazón\nreacción\nrealidad\nrebaño\nrebote\nrecaer\nreceta\nrechazo\nrecoger\nrecreo\nrecto\nrecurso\nred\nredondo\nreducir\nreflejo\nreforma\nrefrán\nrefugio\nregalo\nregir\nregla\nregreso\nrehén\nreino\nreír\nreja\nrelato\nrelevo\nrelieve\nrelleno\nreloj\nremar\nremedio\nremo\nrencor\nrendir\nrenta\nreparto\nrepetir\nreposo\nreptil\nres\nrescate\nresina\nrespeto\nresto\nresumen\nretiro\nretorno\nretrato\nreunir\nrevés\nrevista\nrey\nrezar\nrico\nriego\nrienda\nriesgo\nrifa\nrígido\nrigor\nrincón\nriñón\nrío\nriqueza\nrisa\nritmo\nrito\nrizo\nroble\nroce\nrociar\nrodar\nrodeo\nrodilla\nroer\nrojizo\nrojo\nromero\nromper\nron\nronco\nronda\nropa\nropero\nrosa\nrosca\nrostro\nrotar\nrubí\nrubor\nrudo\nrueda\nrugir\nruido\nruina\nruleta\nrulo\nrumbo\nrumor\nruptura\nruta\nrutina\nsábado\nsaber\nsabio\nsable\nsacar\nsagaz\nsagrado\nsala\nsaldo\nsalero\nsalir\nsalmón\nsalón\nsalsa\nsalto\nsalud\nsalvar\nsamba\nsanción\nsandía\nsanear\nsangre\nsanidad\nsano\nsanto\nsapo\nsaque\nsardina\nsartén\nsastre\nsatán\nsauna\nsaxofón\nsección\nseco\nsecreto\nsecta\nsed\nseguir\nseis\nsello\nselva\nsemana\nsemilla\nsenda\nsensor\nseñal\nseñor\nseparar\nsepia\nsequía\nser\nserie\nsermón\nservir\nsesenta\nsesión\nseta\nsetenta\nsevero\nsexo\nsexto\nsidra\nsiesta\nsiete\nsiglo\nsigno\nsílaba\nsilbar\nsilencio\nsilla\nsímbolo\nsimio\nsirena\nsistema\nsitio\nsituar\nsobre\nsocio\nsodio\nsol\nsolapa\nsoldado\nsoledad\nsólido\nsoltar\nsolución\nsombra\nsondeo\nsonido\nsonoro\nsonrisa\nsopa\nsoplar\nsoporte\nsordo\nsorpresa\nsorteo\nsostén\nsótano\nsuave\nsubir\nsuceso\nsudor\nsuegra\nsuelo\nsueño\nsuerte\nsufrir\nsujeto\nsultán\nsumar\nsuperar\nsuplir\nsuponer\nsupremo\nsur\nsurco\nsureño\nsurgir\nsusto\nsutil\ntabaco\ntabique\ntabla\ntabú\ntaco\ntacto\ntajo\ntalar\ntalco\ntalento\ntalla\ntalón\ntamaño\ntambor\ntango\ntanque\ntapa\ntapete\ntapia\ntapón\ntaquilla\ntarde\ntarea\ntarifa\ntarjeta\ntarot\ntarro\ntarta\ntatuaje\ntauro\ntaza\ntazón\nteatro\ntecho\ntecla\ntécnica\ntejado\ntejer\ntejido\ntela\nteléfono\ntema\ntemor\ntemplo\ntenaz\ntender\ntener\ntenis\ntenso\nteoría\nterapia\nterco\ntérmino\nternura\nterror\ntesis\ntesoro\ntestigo\ntetera\ntexto\ntez\ntibio\ntiburón\ntiempo\ntienda\ntierra\ntieso\ntigre\ntijera\ntilde\ntimbre\ntímido\ntimo\ntinta\ntío\ntípico\ntipo\ntira\ntirón\ntitán\ntítere\ntítulo\ntiza\ntoalla\ntobillo\ntocar\ntocino\ntodo\ntoga\ntoldo\ntomar\ntono\ntonto\ntopar\ntope\ntoque\ntórax\ntorero\ntormenta\ntorneo\ntoro\ntorpedo\ntorre\ntorso\ntortuga\ntos\ntosco\ntoser\ntóxico\ntrabajo\ntractor\ntraer\ntráfico\ntrago\ntraje\ntramo\ntrance\ntrato\ntrauma\ntrazar\ntrébol\ntregua\ntreinta\ntren\ntrepar\ntres\ntribu\ntrigo\ntripa\ntriste\ntriunfo\ntrofeo\ntrompa\ntronco\ntropa\ntrote\ntrozo\ntruco\ntrueno\ntrufa\ntubería\ntubo\ntuerto\ntumba\ntumor\ntúnel\ntúnica\nturbina\nturismo\nturno\ntutor\nubicar\núlcera\numbral\nunidad\nunir\nuniverso\nuno\nuntar\nuña\nurbano\nurbe\nurgente\nurna\nusar\nusuario\nútil\nutopía\nuva\nvaca\nvacío\nvacuna\nvagar\nvago\nvaina\nvajilla\nvale\nválido\nvalle\nvalor\nválvula\nvampiro\nvara\nvariar\nvarón\nvaso\nvecino\nvector\nvehículo\nveinte\nvejez\nvela\nvelero\nveloz\nvena\nvencer\nvenda\nveneno\nvengar\nvenir\nventa\nvenus\nver\nverano\nverbo\nverde\nvereda\nverja\nverso\nverter\nvía\nviaje\nvibrar\nvicio\nvíctima\nvida\nvídeo\nvidrio\nviejo\nviernes\nvigor\nvil\nvilla\nvinagre\nvino\nviñedo\nviolín\nviral\nvirgo\nvirtud\nvisor\nvíspera\nvista\nvitamina\nviudo\nvivaz\nvivero\nvivir\nvivo\nvolcán\nvolumen\nvolver\nvoraz\nvotar\nvoto\nvoz\nvuelo\nvulgar\nyacer\nyate\nyegua\nyema\nyerno\nyeso\nyodo\nyoga\nyogur\nzafiro\nzanja\nzapato\nzarza\nzona\nzorro\nzumo\nzurdo\n"
  },
  {
    "path": "electrum/x509.py",
    "content": "#!/usr/bin/env python\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2014 Thomas Voegtlin\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nimport hashlib\nimport time\n\nfrom .util import profiler, timestamp_to_datetime\nfrom .logging import get_logger\n\n\n_logger = get_logger(__name__)\n\n\n# algo OIDs\nALGO_RSA_SHA1 = '1.2.840.113549.1.1.5'\nALGO_RSA_SHA256 = '1.2.840.113549.1.1.11'\nALGO_RSA_SHA384 = '1.2.840.113549.1.1.12'\nALGO_RSA_SHA512 = '1.2.840.113549.1.1.13'\nALGO_ECDSA_SHA256 = '1.2.840.10045.4.3.2'\n\n# prefixes, see http://stackoverflow.com/questions/3713774/c-sharp-how-to-calculate-asn-1-der-encoding-of-a-particular-hash-algorithm\nPREFIX_RSA_SHA256 = bytearray(\n    [0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20])\nPREFIX_RSA_SHA384 = bytearray(\n    [0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30])\nPREFIX_RSA_SHA512 = bytearray(\n    [0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40])\n\n# types used in ASN1 structured data\nASN1_TYPES = {\n    'BOOLEAN'          : 0x01,\n    'INTEGER'          : 0x02,\n    'BIT STRING'       : 0x03,\n    'OCTET STRING'     : 0x04,\n    'NULL'             : 0x05,\n    'OBJECT IDENTIFIER': 0x06,\n    'SEQUENCE'         : 0x70,\n    'SET'              : 0x71,\n    'PrintableString'  : 0x13,\n    'IA5String'        : 0x16,\n    'UTCTime'          : 0x17,\n    'GeneralizedTime'  : 0x18,\n    'ENUMERATED'       : 0x0A,\n    'UTF8String'       : 0x0C,\n}\n\n\nclass CertificateError(Exception):\n    pass\n\n\n# helper functions\ndef bitstr_to_bytestr(s):\n    if s[0] != 0x00:\n        raise TypeError('no padding')\n    return s[1:]\n\n\ndef bytestr_to_int(s):\n    i = 0\n    for char in s:\n        i <<= 8\n        i |= char\n    return i\n\n\ndef decode_OID(s):\n    r = []\n    r.append(s[0] // 40)\n    r.append(s[0] % 40)\n    k = 0\n    for i in s[1:]:\n        if i < 128:\n            r.append(i + 128 * k)\n            k = 0\n        else:\n            k = (i - 128) + 128 * k\n    return '.'.join(map(str, r))\n\n\ndef encode_OID(oid):\n    x = [int(i) for i in oid.split('.')]\n    s = chr(x[0] * 40 + x[1])\n    for i in x[2:]:\n        ss = chr(i % 128)\n        while i > 128:\n            i //= 128\n            ss = chr(128 + i % 128) + ss\n        s += ss\n    return s\n\n\nclass ASN1_Node(bytes):\n    def get_node(self, ix):\n        # return index of first byte, first content byte and last byte.\n        first = self[ix + 1]\n        if (first & 0x80) == 0:\n            length = first\n            ixf = ix + 2\n            ixl = ixf + length - 1\n        else:\n            lengthbytes = first & 0x7F\n            length = bytestr_to_int(self[ix + 2:ix + 2 + lengthbytes])\n            ixf = ix + 2 + lengthbytes\n            ixl = ixf + length - 1\n        return ix, ixf, ixl\n\n    def root(self):\n        return self.get_node(0)\n\n    def next_node(self, node):\n        ixs, ixf, ixl = node\n        return self.get_node(ixl + 1)\n\n    def first_child(self, node):\n        ixs, ixf, ixl = node\n        if self[ixs] & 0x20 != 0x20:\n            raise TypeError('Can only open constructed types.', hex(self[ixs]))\n        return self.get_node(ixf)\n\n    @staticmethod\n    def is_child_of(node1, node2):\n        ixs, ixf, ixl = node1\n        jxs, jxf, jxl = node2\n        return ((ixf <= jxs) and (jxl <= ixl)) or ((jxf <= ixs) and (ixl <= jxl))\n\n    def get_all(self, node):\n        # return type + length + value\n        ixs, ixf, ixl = node\n        return self[ixs:ixl + 1]\n\n    def get_value_of_type(self, node, asn1_type):\n        # verify type byte and return content\n        ixs, ixf, ixl = node\n        if ASN1_TYPES[asn1_type] != self[ixs]:\n            raise TypeError('Wrong type:', hex(self[ixs]), hex(ASN1_TYPES[asn1_type]))\n        return self[ixf:ixl + 1]\n\n    def get_value(self, node):\n        ixs, ixf, ixl = node\n        return self[ixf:ixl + 1]\n\n    def get_children(self, node):\n        nodes = []\n        ii = self.first_child(node)\n        nodes.append(ii)\n        while ii[2] < node[2]:\n            ii = self.next_node(ii)\n            nodes.append(ii)\n        return nodes\n\n    def get_sequence(self):\n        return list(map(lambda j: self.get_value(j), self.get_children(self.root())))\n\n    def get_dict(self, node):\n        p = {}\n        for ii in self.get_children(node):\n            for iii in self.get_children(ii):\n                iiii = self.first_child(iii)\n                oid = decode_OID(self.get_value_of_type(iiii, 'OBJECT IDENTIFIER'))\n                iiii = self.next_node(iiii)\n                value = self.get_value(iiii)\n                p[oid] = value\n        return p\n\n    def decode_time(self, ii):\n        GENERALIZED_TIMESTAMP_FMT = '%Y%m%d%H%M%SZ'\n        UTCTIME_TIMESTAMP_FMT = '%y%m%d%H%M%SZ'\n\n        try:\n            return time.strptime(self.get_value_of_type(ii, 'UTCTime').decode('ascii'), UTCTIME_TIMESTAMP_FMT)\n        except TypeError:\n            return time.strptime(self.get_value_of_type(ii, 'GeneralizedTime').decode('ascii'), GENERALIZED_TIMESTAMP_FMT)\n\n\nclass X509(object):\n    def __init__(self, b):\n\n        self.bytes = bytearray(b)\n\n        der = ASN1_Node(b)\n        root = der.root()\n        cert = der.first_child(root)\n        # data for signature\n        self.data = der.get_all(cert)\n\n        # optional version field\n        if der.get_value(cert)[0] == 0xa0:\n            version = der.first_child(cert)\n            serial_number = der.next_node(version)\n        else:\n            serial_number = der.first_child(cert)\n        self.serial_number = bytestr_to_int(der.get_value_of_type(serial_number, 'INTEGER'))\n\n        # signature algorithm\n        sig_algo = der.next_node(serial_number)\n        ii = der.first_child(sig_algo)\n        self.sig_algo = decode_OID(der.get_value_of_type(ii, 'OBJECT IDENTIFIER'))\n\n        # issuer\n        issuer = der.next_node(sig_algo)\n        self.issuer = der.get_dict(issuer)\n\n        # validity\n        validity = der.next_node(issuer)\n        ii = der.first_child(validity)\n        self.notBefore = der.decode_time(ii)\n        ii = der.next_node(ii)\n        self.notAfter = der.decode_time(ii)\n\n        # subject\n        subject = der.next_node(validity)\n        self.subject = der.get_dict(subject)\n        subject_pki = der.next_node(subject)\n        public_key_algo = der.first_child(subject_pki)\n        ii = der.first_child(public_key_algo)\n        self.public_key_algo = decode_OID(der.get_value_of_type(ii, 'OBJECT IDENTIFIER'))\n\n        if self.public_key_algo != '1.2.840.10045.2.1':  # for non EC public key\n            # pubkey modulus and exponent\n            subject_public_key = der.next_node(public_key_algo)\n            spk = der.get_value_of_type(subject_public_key, 'BIT STRING')\n            spk = ASN1_Node(bitstr_to_bytestr(spk))\n            r = spk.root()\n            modulus = spk.first_child(r)\n            exponent = spk.next_node(modulus)\n            rsa_n = spk.get_value_of_type(modulus, 'INTEGER')\n            rsa_e = spk.get_value_of_type(exponent, 'INTEGER')\n            self.modulus = int.from_bytes(rsa_n, byteorder='big', signed=False)\n            self.exponent = int.from_bytes(rsa_e, byteorder='big', signed=False)\n        else:\n            subject_public_key = der.next_node(public_key_algo)\n            spk = der.get_value_of_type(subject_public_key, 'BIT STRING')\n            self.ec_public_key = spk\n\n        # extensions\n        self.CA = False\n        self.AKI = None\n        self.SKI = None\n        i = subject_pki\n        while i[2] < cert[2]:\n            i = der.next_node(i)\n            d = der.get_dict(i)\n            for oid, value in d.items():\n                value = ASN1_Node(value)\n                if oid == '2.5.29.19':\n                    # Basic Constraints\n                    self.CA = bool(value)\n                elif oid == '2.5.29.14':\n                    # Subject Key Identifier\n                    r = value.root()\n                    value = value.get_value_of_type(r, 'OCTET STRING')\n                    self.SKI = value.hex()\n                elif oid == '2.5.29.35':\n                    # Authority Key Identifier\n                    self.AKI = value.get_sequence()[0].hex()\n                else:\n                    pass\n\n        # cert signature\n        cert_sig_algo = der.next_node(cert)\n        ii = der.first_child(cert_sig_algo)\n        self.cert_sig_algo = decode_OID(der.get_value_of_type(ii, 'OBJECT IDENTIFIER'))\n        cert_sig = der.next_node(cert_sig_algo)\n        self.signature = der.get_value(cert_sig)[1:]\n\n    def get_keyID(self):\n        # http://security.stackexchange.com/questions/72077/validating-an-ssl-certificate-chain-according-to-rfc-5280-am-i-understanding-th\n        return self.SKI if self.SKI else repr(self.subject)\n\n    def get_issuer_keyID(self):\n        return self.AKI if self.AKI else repr(self.issuer)\n\n    def get_common_name(self):\n        return self.subject.get('2.5.4.3', b'unknown').decode()\n\n    def get_signature(self):\n        return self.cert_sig_algo, self.signature, self.data\n\n    def check_ca(self):\n        return self.CA\n\n    def check_date(self):\n        now = time.gmtime()\n        if self.notBefore > now:\n            raise CertificateError('Certificate has not entered its valid date range. (%s)' % self.get_common_name())\n        if self.notAfter <= now:\n            dt = timestamp_to_datetime(time.mktime(self.notAfter), utc=True)\n            raise CertificateError(f'Certificate ({self.get_common_name()}) has expired (at {dt} UTC).')\n\n    def getFingerprint(self):\n        return hashlib.sha1(self.bytes).digest()\n\n\n@profiler\ndef load_certificates(ca_path):\n    from . import pem\n    ca_list = {}\n    ca_keyID = {}\n    # ca_path = '/tmp/tmp.txt'\n    with open(ca_path, 'r', encoding='utf-8') as f:\n        s = f.read()\n    bList = pem.dePemList(s, \"CERTIFICATE\")\n    for b in bList:\n        try:\n            x = X509(b)\n            x.check_date()\n        except BaseException as e:\n            # with open('/tmp/tmp.txt', 'w') as f:\n            #     f.write(pem.pem(b, 'CERTIFICATE').decode('ascii'))\n            _logger.info(f\"cert error: {e}\")\n            continue\n\n        fp = x.getFingerprint()\n        ca_list[fp] = x\n        ca_keyID[x.get_keyID()] = fp\n\n    return ca_list, ca_keyID\n\n\nif __name__ == \"__main__\":\n    import certifi\n\n    ca_path = certifi.where()\n    ca_list, ca_keyID = load_certificates(ca_path)\n"
  },
  {
    "path": "electrum-env",
    "content": "#!/usr/bin/env bash\n#\n# This script creates a virtualenv named 'env' and installs all pinned\n# python dependencies before activating the env and running Electrum.\n# If 'env' already exists, it is activated and Electrum is started\n# without any installations (unless the pins have changed).\n#\n# By default, not all optional dependencies are installed.\n# E.g. for hardware wallet support, do:\n# $ source ./env/bin/activate\n# $ pip install -r contrib/deterministic-build/requirements-hw.txt\n# $ deactivate\n\nset -e\n\ncd \"$(dirname \"$0\")\"\nif [ -e ./env/bin/activate ]; then  # existing venv\n    source ./env/bin/activate\nelse  # create new venv\n    echo \"Creating new venv.\"\n    python3 -m venv env\n    source ./env/bin/activate\n    pip install -r contrib/deterministic-build/requirements.txt\n    pip install -r contrib/deterministic-build/requirements-binaries.txt\n    pip install --no-dependencies -e .\n    echo \"Done creating venv.\"\nfi\n\n# This might be an old directory and our requirements might have changed in the meantime:\nDEPS_CHANGED_TIME=$(stat --printf %Y contrib/deterministic-build/requirements.txt)\nif [ \"$DEPS_CHANGED_TIME\" -gt \"$(stat --printf %Y env)\" ] ; then\n    echo \"Detected changed requirements.txt. Updating dependencies now...\"\n    pip install -r contrib/deterministic-build/requirements.txt\n    pip install -r contrib/deterministic-build/requirements-binaries.txt\n    touch env\n    echo \"Done updating deps.\"\nfi\n\n./run_electrum \"$@\"\n"
  },
  {
    "path": "electrum.desktop",
    "content": "# If you want Electrum to appear in a Linux app launcher (\"start menu\"), install this by doing:\n# sudo desktop-file-install electrum.desktop\n# Note: This assumes $HOME/.local/bin is in your $PATH\n\n[Desktop Entry]\nComment=Lightweight Bitcoin Client\nExec=electrum %u\nGenericName[en_US]=Bitcoin Wallet\nGenericName=Bitcoin Wallet\nIcon=electrum\nName[en_US]=Electrum Bitcoin Wallet\nName=Electrum Bitcoin Wallet\nCategories=Finance;Network;\nStartupNotify=true\nStartupWMClass=electrum\nTerminal=false\nType=Application\nMimeType=x-scheme-handler/bitcoin;x-scheme-handler/lightning;\nActions=Testnet;\nKeywords=crypto;currency;BTC\n\n[Desktop Action Testnet]\nExec=electrum --testnet %u\nName=Testnet mode\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "Electrum is a libre self-custodial Bitcoin wallet with support for the Lightning Network.\nIt's secure, feature rich and trusted by the Bitcoin community since 2011.\n\nFeatures:\n• Safe: Your private keys are encrypted and never leave your device.\n• Open-source: MIT-licensed free/libre open-source software, with reproducible builds.\n• Forgiving: Your wallet can be recovered from a secret phrase.\n• Instant On: Electrum uses servers that index the Bitcoin blockchain making it fast.\n• No Lock-In: You can export your private keys and use them in other Bitcoin clients.\n• No Downtimes: Electrum servers are decentralized and redundant. Your wallet is never down.\n• Proof Checking: Electrum Wallet verifies all the transactions in your history using SPV.\n• Cold Storage: Keep your private keys offline and go online with a watching-only wallet.\n\nLinks:\n• Website: https://electrum.org  (with documentation and FAQ)\n• Source code: https://github.com/spesmilo/electrum\n• Help us with translations: https://crowdin.com/project/electrum\n• Support: Please use GitHub (preferred) or email electrumdev@gmail.com to report bugs rather than the app rating system.\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "Fast and self-custodial wallet for Bitcoin and the Lightning Network\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/title.txt",
    "content": "Electrum Bitcoin Wallet"
  },
  {
    "path": "org.electrum.electrum.metainfo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!--\n  Copyright 2023-2024 Soren Stoutner <soren@debian.org>.\n\n  This file can be validated using `appstreamcli validate org.electrum.electrum.metainfo.xml`.\n\n  Distributed under the MIT software license, see the accompanying file LICENCE or http://www.opensource.org/licenses/mit-license.php.\n-->\n\n<component type=\"desktop-application\">\n  <id>org.electrum.electrum</id>\n\n  <name>Electrum</name>\n  <summary>Bitcoin Wallet</summary>\n\n  <metadata_license>MIT</metadata_license>\n  <project_license>MIT</project_license>\n\n  <description>\n    <p>\n      Electrum is a lightweight Bitcoin wallet focused on speed, with low resource usage and simplifying Bitcoin.\n      Startup times are instant because it operates in conjunction with high-performance servers that handle the most complicated parts of the Bitcoin system.\n    </p>\n  </description>\n\n  <url type=\"homepage\">https://www.electrum.org/</url>\n  <developer id=\"org.electrum\">\n      <name>The Electrum developers</name>\n  </developer>\n\n  <launchable type=\"desktop-id\">electrum.desktop</launchable>\n\n  <content_rating type=\"oars-1.1\" />\n</component>\n"
  },
  {
    "path": "pubkeys/Animazing.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: SKS 1.1.4\nComment: Hostname: keys.fedoraproject.org\n\nmQENBFD1gzgBCADGVp8wVMk0viUTR3FBzCfdGGULYcyiKFNdGlKxORyrdWJF6/g5JuYPU0NX\nCRJ7hqX4hopOiID+oxE7p/vP/i/Sm6B6xZMq8bJ+PiJ2h8ZqnourgL8tkAqlDV+zLana+XeQ\nPsPhJPeARAQDtl5QhQbvWm0idMyd1zuWdt4OVIYnhJ7w7Mw0CdUmBbkTc1P23J8vwqiyyuHq\nV3JimkNJLm4vyWGFig6ElgRMbV5YWci41OTH3x8qkWHFdB1h0ODP/28bBwpVVXqnObrp/Lsr\n9aQSP5VujGIL/cOdel/7xD2Dc5qS/HXYZgJUk6x2WkLmO47NSpW6rone3W2A82gkKRwXABEB\nAAG0H0FuaW1hemluZyA8YW5pbWF6aW5nQGdtYWlsLmNvbT6JATgEEwECACIFAlD1gzgCGwMG\nCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJECJFMARpVQb95i8IAISo2NvZW8Zln4ivuo5l\n6PirIrzSIlec3Xa/xmkgrHTfBVT3WYeGp54NkEcpgarD1qjt5ZFF7wExn9WLErGlsIkPmegO\nZjx2mn3ZyB+Y5y0cJeGPM72aD0Z3jaBi+htv37GMb2mV6yO700dG1x3vwS1YYyRLgR6pVwlR\nw+0l+5B/5zHsKdRvpRcuTpmrg8rtgTxZOsRyORn6ck6w7lUbno3+1XMBTFTbVf4/SymMU3Wl\n9eru2agYRfkB3puXd8DMYlSzZiiUWTHV6bOsB0N001sJtmFf6KmM1xF8Q/eqMlWevS2sd0XG\nOWFQu4RGyAJqN7h1Z3cym278WVCMhYzJn/SJAhwEEAECAAYFAlHaodMACgkQuW8jAK0Ry+6x\nohAAj1Cka1AoIDmPcU5IlPLPwsYoyfLmkAbcpeNKZA6Yf4pm96i3tKXbLgCxSrejSmraVIYZ\noNGFtmAx+rpwdZtDWyFWGlMkDhjRY1rQ6nlkO8CtPU/brbyZJci2J67U7hUsJt+O5gaftclG\n2RcC61BQz3Ee0Fl8JKSE5V2KqOD2W1BTCTYrnpY5YdMB8qSalR8txZaSqkA3xpSrIwJ04EHT\n1uyQAJiacQ0ynHpcU/xfVBnzj3Hc0mRxBTI5Nznk8NAHleY5OSiZX5YJb3mvycdV34mGXnna\nyk9S8HotWs5kLpMQ2TpXOiZK4/lJBlp+q3ksj+01qoqTrRXijI+6z9QRS6y7AkSWO0PaV9Ui\nuARH5bBV5qzzT2q9vtYTEkoGHj1IyoW70l2TxyfQLQBVNcYp864+TQJmxiVIVUEMSsJ7Vnoi\nfzIvgBz8/d62POQH7zvmnWA7VvYD8Pn3dXVOF7sSOxqJ+YLxcOkjpgqI8ef0Jiypa0w5Tguj\npJ/SVWpeP410SlK4w+ICmxc3shR/pdJfVhgRGS6e8A4GCHnDHg8ksLJ8qoUmPlOWJjdfjakz\nQD5vvAV0KwWQ0mu9ca3C3NjivYSWZdiHzeQE5hXqVV8wlghshFKOcm81vdB/hsay5hthilSS\nL9trmnNdYNppZHc0bV6PYpYpHWaD1NpWQSbIeiW5AQ0EUPWDOAEIAMzeN8NjFRB53Fid/Iag\nQxxQZeD1Y9cn9S6fJ+nEoZWMPQ+iVCictH3gmqm+eZ0qOrqSTxkXgyIh7Uiu2lwjm7nmeBK3\n28k2ei1e3bpp0I2oMiShVAehxbXx5Jh29nlPDS8US/xehj6WnUeojR2qXVOSAtK7+V7taSyi\ngOpYr/+5fcq+/8Z+d+NaVp97MUmoH5LVNU2jCR+qyIM07qX2G4Vx+hU4tjc79Kl6m1equ6/s\nHwnkmMIsGvLHZG+Pq+1wHIZwt1p39MhFvob9B2RFqbFtihTal36D0NBQMdlp9PtKtQZSQR5h\nWsmWG+m17/vyux7ZtwnTRbR+p/adFx6fsTcAEQEAAYkBHwQYAQIACQUCUPWDOAIbDAAKCRAi\nRTAEaVUG/aARB/9pYfkB4rDe4OulrUXBusTxOGKm1yPjZaXveOqQkD8/2Vwxu7tYFMVMLDKY\ntvxpmrUdb6oz9s8XrTkMIW7ZM3FmhNQhxsfEj9g6UGJnCXcf+Zw9wbqmzONNCupfN5wPOtoG\nd+KFUR5dVl3+BsUoIEcAcPR5AdP9gbqDSbrDyuk8+j1CfiLVAQXud3u2rkt8GWbTJ/LzKtF1\nUM0X3YnE7fxi61DDgX6A1EJfss3aFj2lwmb38Yp041WKik2ZZEQpyQ8rXeYG8Wl0wSEYaMPD\nedEHRDoIAYRshRHglZYCD80LF7c31koGLGzVWio3erJgUYUwhy1QUltdVL+5PcVyUlm1\n=4eNp\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "pubkeys/Emzy.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBF2wShgBEAC0wL07eYOkG/A8coMWLTFz+MLRsTojJZp2VfDQZNPOck9uddsy\nrjY1+O8JVONcnuA8OJ4USiRjR4w8ImwDAVo5NxCRngxWtajFV+g1g8QtvLyIz4HT\nNzhCyHHTQbTyTQcUnaMnt10O5J4yljuV5tot/XDwIZ2zY82Oz3v12NqOJZYhaORD\nOP2Mxqbt0RclmUDIb3OPssztJDJJLFQ5m9PRfgdnlp6gPXL171XZe4A94q2WOA5F\naV6xioGTg+TtXCNvJffXOuIBfVy0iKR6wMb1Qi0MxQUieGePq9bqx0vpey5b/Q9L\nZDJ8x5RvkQgjxXNll6RuBa59azPFOemcjiVMKufXOkWdvx6KFLGRm3dAwWQ59Foz\n5vf2KzTOngepyH/p6EXF/6v+q8jhKOuG08nZPR3cNElaF2w8HeBQgR7tdDh+QLm3\nFr5m5+okpUkKVLnF+FJ6cTltlT5lKYdM8Im7Yxy4x7KsPZ8NJNn9V0eI7AMf3/py\nGJavyWUSDUWik/OvvRM/odiZuwbchqO6oJWV9VlE4I9mb78PHH8M8wNEHioMCWp7\n3APs0/mYKxZwJYE9gnAPF3Pt1ksnuoqHJpMwUatiOrURlWpOAxGx6PYaDFoJBM6e\nT0wq//oFEviB3fnz18L0gcpKbC5O+n9BmZ4Kekwv6RZWQnIw0+li8kFqYwARAQAB\ntCBTdGVwaGFuIE9lc3RlIChpdCkgPGl0QG9lc3RlLmRlPokCTAQTAQoANhYhBJ7a\n/4DggGWWBPSnay67BW/YR/inBQJdsE0fAhsBBAsJCAcEFQoJCAUWAgMBAAIeAQIX\ngAAKCRAuuwVv2Ef4p9q4EACnm9gmgWrj39PGqsfqL68ZqLnKqMkD1FxrkO98dXKy\nXHCCt1bWOVqttFrT+Tb334AP5Xh7rw0adGFj13drywPGg3+lVCIVRG7QGMPqXA2o\n1ApNa+EPMPmF8g0IvHt6/nKFAxYs03esUsWgD1egn+gfeROiJjRrBgOXWIdHp83a\nJSmu3nugjh1nSgeuRSX2rNlr16h0WE2HNs1KTnVyVL4eRr/HnUajAJcvs5pw29No\nDrOcuFzdTZ8Kck7SThLut0CxOgxlGQ/HNhKHYJMI/nhue9V7hm0b+OFoc690Jm/F\nkmu4Rzd/A34FkqApPBfKhIXIcnlcAGY6XBozmc+j7UNyOyGl7r2EvkQlKQJV41fK\nIbvTNwbIQTQ2XfSXUISdRvp4oo+dJ8JIwGfqnCHEkOyuSLlu+oVecZo6V2xPjbXj\njin+0gOAplqj/M/nZCun1sx3nEPirVFNxwUbUSN89HhQvqLahT9L9ZwfRsay4cL0\nbIHxmp6rQ2BUklCEX6fMLsBT/AZucCfSuTom8GBjnx9aK+cUua0uu7+Mm7msL6dG\nPQdPN9Dp5Vg8SJcaP7ivVm7Kc7xK+c1oU5jgAmq5pIMcwQ/m2Le7/zasl1kbjIKy\ny2ZlIFSZdBB5aDKzx1/8lX6B/15ZpHkfcYznzYid9q2FsrjYBnz8GiaAjp+7+KLm\nT4hdBBARCgAdFiEEhnNFAmtnY+iwfuc6tnNxFzl/XE8FAl2wX5AACgkQtnNxFzl/\nXE8BKQCeIUwTgHlwCQxR9gIuxZI86GnEVd0AmgIad2n3N+8XMzDpnnseGKB9ium7\niQEzBBABCAAdFiEE1mYXKq8/S6cFwe8/7PuqmjrDf5sFAmI7IDEACgkQ7PuqmjrD\nf5v7eQf9Hnp3lGmRXU5S3uLOB5q4NHmE67cJTZan4owt5iX9HQAz3A2JdYO7A/8a\n7XswY41334CRy1eqKSKt0datjcggMwd9tFm6X41D58yCVzOvyShyqsZlEdFSV0JV\nVVbwfBeAsjMKnFFl1Dwc7OBRXGJYcRUfiYwSP9uFM7lFKYHm+2/hkD50pOQjMvUR\n4Fjkfi4QUyuhB9ii2NFuXLLwZ/QLiy3UREShgUTZc9HYqc+NH4d0QKLFJ83mfnsV\nBQ5MvX24Q8FwqSxjjNIPCnU15tTl2u911JPgZCB2wVaN7Bt/iDoo2zRTrcea3JF+\nv9667OLpoyHo2lBn8GQKHedh9RS/4IkBMwQQAQgAHRYhBHeJAfkT+2ZycGvMj1uM\nXFao9yPkBQJiUabdAAoJEFuMXFao9yPkLlcIAMK6NgY79hZUrgXnUpLYjVyvgbcP\n9gg6ZnMXxQzpeBT7Naxp60l7sc2c2oyTqjxxNHEjKtEDKwhtCZIHVdwyf2IonWQV\nMYx/DMG5j8qGFIiM65JG/pVRy1TL9zZ7zKC+uM58x8hjWfG27eFbjoVOy+6qksfR\nmP1s15+9fUutJmW7u93PNFYr/S3khlxZI1iN1g1sTldG5oWZfwoDxMthxpZZX/gR\nyukgJYnep69aAIHUYMomUDJ7raNhGOh2W/CY6nc7d7gkfaAvQVv+kmJSsRSZNE8n\nqvfYmgLfzVIjtPZoZM5cVRR6C0Tjy7ivVTn3oi4Z8tgOJL4ALZZ9LY4G2a6JAjME\nEAEIAB0WIQSzcaI8xElwP/G/CSec9kY8v3U36AUCYzaeIgAKCRCc9kY8v3U36Grm\nD/42oOca/R1odryNsOqhIs1Knon1bGbX473QhsUv2XelFcp0lIqPqCIXwnuIXKEF\nQ70yZWI4+vy5rl6OfnBP8rDz0YWwOxgU0qqjhRAirHHscZqzURVMxPAxZ0lnSy0c\nvM2xqduG1GSjiA7v/pwSnsro9pNx9xo2EYtOOsXGr7HFREeboDIMocgmM6VCbJE/\naT8cB+8jz1BnfHnEkJ3u7jptXO1VjF0W2ObwFKSGmshvDUIRJr+G3Ri+SvrOCHKv\nl5H4u2tULFMRsSqTB9NLKJBXbRgkcp7jZQT6BuiM3BMseziTgLS7JccgGFExbH6r\nQz3zFqmDOGvgq1x/G3C8+DHYlTlNdRHTU8ODBG3lrJnErEUgrvacnImp2elXHJxy\n6Aaom2gX2j7tbGY2AZwXEx+ietaYeGP3Knu6SbRGtwVCzjjiqdptUriCescXrz4Y\ntzvWxj4Z37l/4Hj8RqS41LM+E4JFvwIZLWrPWFTNxlw28ZLzJPkCZ4GCv0of6z1Z\n0dhePKdUi+rn7J53FF/GAzvjt5VX+LtSGoyOMTkuji3Ng1g3O3gr1fa/3cNF8ufG\nbfiAnNclGshSHBspQsUjTda3G3X65ebOECbUwMzZEXY8CGv7ZXgi2N6NBmH568KA\nJn0VYpQ3ydFY/QN8hLL5S0hJEiAsU9MkmWKh+AXJoOdMa7QtU3RlcGhhbiBPZXN0\nZSAoTWFzdGVyLWtleSkgPHN0ZXBoYW5Ab2VzdGUuZGU+iQJMBBMBCgA2FiEEntr/\ngOCAZZYE9KdrLrsFb9hH+KcFAl2wShgCGwEECwkIBwQVCgkIBRYCAwEAAh4BAheA\nAAoJEC67BW/YR/inVg0QAJGPCzjLbAh7BLFHwpJwLDS+gWZCDvgUlGxdnqgSZU52\niXGSPsKY4Q7sQRdWHfbOMD6+KNDqzKsc93Wilq0LM+0IMvFYe0Azz3StlTNvySyv\nz/T0k1bO3rF0FJSsG8trWHd6CzN1/Yz8ZnMSbyzA6bNmc7Jbvv7+D+Vae6Ytqpj+\nMnNd3wwgXMa2bclcKCjV9J+f0cxA/GTrLy9CSDykSF3oOKgMU1kfJVnGZkNQYcp0\nmU+1nyXRo4W5plJFzsbqbcHQiShkx73h75ehRURUb2Ob+7WHId4F7jwXWaLVw85w\nzfwOTu0LCeu/HGWMfkkAwXu1LDQuvmmRga+TAYACtvYx8W/iPnvmqA86Vp2QnEEi\nJSo4GLE36J/ZKn7qjSZmeeNXyHihTmJQnwBhlaAKRVpD1Q4XD5NvNNJILU5KgmP0\nEOD3nnAwfGoQwcDXiUXi3gm6dK6E3VDdhTMeECPAvCN8XGtzZPvzJFcFU3FS3R7S\n/kwhIavMi9TPv2X3788+N04PGAiIID+Z5DnPhfrGLbSRSse9DKfx2JVcb/lO5CMG\nOuy3PTcPGzW9s/2FGRbOaIvajuRtXOeSch3lDE28Pa3K350w/UHan3D3F+F3WrCb\nfwAOPI0/2xQdwFh28s43+8BSsuxqfLCMw4dOx+jhNw/iSNJd7cQbmUjumXhKI0u/\niF0EEBEKAB0WIQSGc0UCa2dj6LB+5zq2c3EXOX9cTwUCXbBflwAKCRC2c3EXOX9c\nT7cqAJ4pIFCjx0Z3em+MjqEKCMlc9wU3FACeMAg2+zo4c5pR8a9cqiqTGEh/IEeJ\nATMEEAEIAB0WIQTWZhcqrz9LpwXB7z/s+6qaOsN/mwUCYjsgMwAKCRDs+6qaOsN/\nm9uwB/9H+QZ6jMe2R5W610X0cpIzOHutipBuSbfx5d07COiJzDyI1P0cJ+flLhmp\nRIzxqd6nMehCmNaHH0uUtfH3RbLDhJpRazww20bp+czxXK5HQaYWvgwPIGMo0pMx\n1lGbzPh/5ZvDnYTQJxViksDivi4WyhmRCx3UB+dUaUMk+bEli4xZ9K57L6TFOf2h\nCWe/2uR5ZxHT6/KoZlCpo/Ht5/GCEd3wStlQKlyZkQde2AsUjRjwy/KiAMgruabs\nspRQxFszccJGo4NgyWB8+RpeHLGntLyklQ5woxWi4TNV9ubObCJ9ZGzUWkPGPk1p\nSw6XtFejKntBot7Wqz6obRPRX9bdiQEzBBABCAAdFiEEd4kB+RP7ZnJwa8yPW4xc\nVqj3I+QFAmJRpuQACgkQW4xcVqj3I+TaXAgApZ4uI6tVWtnkSbAxMt31EALZN+8l\ndJJq1sczqwZr+SjwVID28vdMvTwqsiHf2ijc3eQI29ACwjsBqAx17yCS0HODu7aG\nyrTosnXv3J30FAN9PUhMMQgo2+W2SbA8TsKZKr3EKVdz9XNNuSATOCioazEU8U7U\nZH2CfyHXNXYhlwOjK/3CoKOrTB3fu6ohh92WNtDajSxmyH6+y8kccYy4lHqPdLOm\nyDRIMUyLbMI2ryPeorhYC6OmcZttTd1gBPnV525wUnz261tP84W7eE/PcSDd2+Hh\n2M0mTQqiKszOOTDyQwlIfjgwwEmhMXoEhh323jcxo++x4XSCly9VYZZPbokCMwQQ\nAQgAHRYhBLNxojzESXA/8b8JJ5z2Rjy/dTfoBQJjNp4jAAoJEJz2Rjy/dTforHsP\n/i0xPnqfYt4Yff4YsDtA5qXB5MnWOa07to7xoz5r44wzUv9FuF1UxS/+hh670lnG\n5tloPta6F+kXdqCrOuaB2np7JnE2F1BGfy2ajhs80RTDSprgJDO7Q2/lcbj4hO+6\n5m3OHkuKVMTZC5X11dWnLS632699S7pxzqS4wpKHap/7JIHDBUaLTLttxmX6iWpJ\nvpZ+mD3+ScK78qHCK6bT2nRYEBIugAIntUZVq/yh4j2jrn83GPTvTCmtwyR5uIOQ\ntbL80koT/jrxwgORYuoyf2BJC7MOI9T/utvB8l3H8DOcONHOUUlxKo4HT3shkKXS\n9K66AyBzTG+B9q+fFMPUZ1neNgk4Coee2Lg/T1wD4V75JsUyLprlOAKNMlBK9mb7\nRyNg+tCO2rm0q/9q/eE44GbnDPFoXqJPsSDfFr68qpcaPrm22wdsO1SHDWtGkoGY\nbQXIM173+Fft7gF5M5vZqHyYlHcaj0dt7AlMnvjZacUgfvxPyU2HMFLj+QuYLzn0\nTCkwDnB+bLjDx/jIzHVzq1u4p5rT4OomRnwmFopoTaszZCaADRjZkQvbrimLRqMT\nGUnf917gPiMtqvtOhGh1j5t02oRIWpGJftKFSb80fbZ/kAW6lKkqYNzYn6mNECFM\na8M0kKbqYA4w1Clnfkzeeddn7qJV2OJmgrwuuA0wtu/jtB1FbXp5IEUuIChlbXp5\nKSA8ZW16eUBlbXp5LmRlPokCTAQTAQoANhYhBJ7a/4DggGWWBPSnay67BW/YR/in\nBQJdsEziAhsBBAsJCAcEFQoJCAUWAgMBAAIeAQIXgAAKCRAuuwVv2Ef4p8KGEACQ\n8pmwhrXWPN7C8lKF+Q+X+p+prdeeXN9RQJ7YqJMSmj3Ips81MscsNMjOVeUUXRkI\nSr2VCXZZ6Z1QA7SpxD04KPPMfEmhYZ6k704NFrL5RmGarQeSc9hAAcOQog5yoJhh\n2g8xum7Gjs2TNweepaoOpVnAs1++whWItGRGISjJfzpdsMAxp/VgwkaqWc2LH26E\nbnFYI7txGmu5SUzX1oEKqd6bYhDrn71B76DSzww2LQzMayhGT0jInwt6tKJdWaGp\nsyKHxFOL7GswZujEU1MnxnZTKBqzFt/mxFPtyRElLZ0wcUB2zDFRTT0TJF54kRUc\ncCo1h2Knh/ZxMuMNR6mQ/01ouIo/MeuvV2/dHMtzH1YJTlOVVkEk9mSUCNYMrxB5\nS4aqMEho3Cfmo5jrwDL1XDK8AYmR1byGexoywSXOHWJAdQNwD6E+4FCXLB5XSTkU\nXweD78CQmqy6F5ziHbjf0Wu/pvKhQDIP1wzSsdOirrMdep7rnybyXQ4jnl51xrN1\nmOGouGOHTHPqUCilxpKfAkrl5Z9idezWrxpU71DwQnwfDVwCwsBMZKwPPhEFE3aJ\nQJeLssBAwEPvWGLi9ELA0wXmStPA7oA+bkpUs2ZT3m9LBLOKmwjvWizEBqlqI021\nJ51lzXAGxDMyHVVZh7vYBeaPVJ/h1Ce6ucdrBTPd1ohdBBARCgAdFiEEhnNFAmtn\nY+iwfuc6tnNxFzl/XE8FAl2wX5cACgkQtnNxFzl/XE/QRwCfcEvnT4VyKbXPnX8c\nVx+zwTnQP9kAn3tAHpc7iuGGQMJc3A3LK9H5S7njiQEzBBABCAAdFiEE1mYXKq8/\nS6cFwe8/7PuqmjrDf5sFAmI7IDMACgkQ7PuqmjrDf5sXgQf9FfRbX43itEdA+CGe\nVnjCd6AUls/hMP6jeMR27eSkPbUFT4JEG/WcUAYj4IStruYEqmJJJwiQFlQscuR3\naS39QW+qv3qQ2LI7APh9cHRGdaDyZ2jh4wHHB1yBhrAUYFxbP0J37jofJrjbEs17\nvg79ve31bimRRrywNB8WiKVSZdq8LKBahPYOutAaylNFSdCIWzhGpoxABbWg3jWN\nEg9dH2QRvy8Ibj95E8tQ0RX2X2BlBrPMfaBPwGdPgc1G1qG0fQoheJl6LAk65aSo\nIKDFK90f/9lybViKAatrKRpNRi8IWrs1T9hjF1h77c5DUu3VK9eg/2MyDAwW8pBw\n01rJDYkBMwQQAQgAHRYhBHeJAfkT+2ZycGvMj1uMXFao9yPkBQJiUabkAAoJEFuM\nXFao9yPkj/AH/2J1vhWc0P/k24FwJwF9T6pP0fvVUF5jIpyhWsbDiDiEw1IcGm3K\nIgel+/RVSvpjSBkLDRgTwAKx4cJ+HX79X6OBKVr1obVVc4NnY6Ow+I0z/dhzXFv9\nb/I+KbsMfYlVg6vCEkgY8YZSYg56/qfsgeGoVLTcCEijM8vYadzA9fhz1PehBIma\n4EC/2l4BcXfLTIcRQv10o7xI/vx0nEzfTyO7IVLJygq3loMECetIwGlIzVzE3WCD\nDcuNOSsZDuFe3qP79498JnF1X4hF8shmehgZkTonE8iwrUTGqERgzHN78ClSaO6M\ndrjEMq2oLvlJeQ+qG7PafV77OavhF4dHu/yJAjMEEAEIAB0WIQSzcaI8xElwP/G/\nCSec9kY8v3U36AUCYzaeJAAKCRCc9kY8v3U36D/LD/4+Sakr573/bn1VkQIVM42E\nbW5bsE8QfamH7H91AIj+uuA1cGmNiSquiUOpr8OWA4lPmDBWS01Ar9Vk35MF2aml\nr9cuMIjZSwawTz70zcCmE/8YBY1NEy9Z6YzuWB/61M1oephYk5Prrjc95EI2HfCj\nLYjnoIgU4FAhnltnxhx03ukwatPR/UVfnedkzSAKXH24QV2nNVxHVC2lmqRczTtv\nDXpSnC3Zb0lNv0YQ+t9UT+3DNpAV0i4YbqMPq2BNmM5TBLJxhENlDnZwgBzLPKeX\n3WH8WFdXtbOVdl49aVMG1e9Gw2177MwQzbBXaBCaNzL1rqzoesy1OMgAMF7O/CBE\nj5x5DHFbUF2Ku9NZMY3uLYmLr9BZzQ1uWGIAt0FZtYfBpiAVZgV+aWUmF9o7fGA1\nEhYiK40bTA9qnrAG+EebGzZndE392q7l1RF1m0vVFVleOhEXtvETMirhYEg+eqiP\nMysWoGh/hsv+h5+c7q4kUHtFBfc5vXF6beP+XG7qmzBjD5btUIyTlY7rhnkQ0WmD\n2kHbuDPXcaX0JvvaTJqScWfx4cG9mj4J4qMOARt4s+A/3z4xJKBd0fth+zYmjIyn\nVqj2gmhccx5bks3C8W5subw7HGJBKb+hyPgaEUnUMeYvwuWFqg5GUf4/gLL7J1dZ\nQWIJzT+uqueW0/IoEAEbVLkCDQRdsEtJARAAuQBqw216oGG016/eADvkCzSIhwBR\nHtvVw4kqA18z4F1eTvjGgpWD7H8tYfDxqpXCXkYHpgMZueyF7G6ekVmJeZ6fUByZ\nDFAjvRTQPcSmLN426UqzF4syANpFeQ2kb2+mWfIJdddY+4+XrE2EggpsxzqhPjVj\nybzkYEu02/SGFLocKizbAXCHJhBJzdG5kB6HX2HERt1PozI+7I3lRvjKtB/VkR9o\nquUUIm596+ZJ6cZ2oc/i6Jy9rbrnE3iE1hIUBOR1CEXg4cDRBIqmiS07YsKLV7rX\n4cYDUGfAlTrXvVRfzBNQVaQi0KOQ9dVnKtG0gUD4U/I+3dgH79DXXhutRf/MW56v\n/Wn9AZvLUEyWejIcF+8J2c6jR+wGvnReCrc9PrfXDHbV94l2joewGZ1+dLFkk36K\npGkXb/s/DSkCfCxjmFr25qnVkfJ7azzMQcAlJvtC/5BmL/wGFg6syGwHqL3M9Uo9\n26bKbO8iLk/U+ZJwRJ4BloTimKNfN0XFyB9rO2T1Z1wPJ2gIwBO/yJBfPfx7AC74\nAdTjUsDddRxUg1UmhNom/Ebxppc8pfqN2vv+5hI1SiswGsCcFrqWtPxClsCLzPPB\nOyKaHa5aWlo7yf0jmc6zYX/vD4/Aw9+oKP3+EeyuwoyzhotwKpAMSbgAc9eZwb9C\nmbrpaSd5xyvMcFMAEQEAAYkEcgQYAQoAJhYhBJ7a/4DggGWWBPSnay67BW/YR/in\nBQJdsEtJAhsCBQkFo5qAAkAJEC67BW/YR/inwXQgBBkBCgAdFiEEY32x4jNw+Er/\niMzgMVI0fQfaYnwFAl2wS0kACgkQMVI0fQfaYnyqxhAApqUJs4FOBaC3oXYLRcSm\n7qLFol4dFMXwSo/QA91vnZd7Gir8dxy8OgGjvEy/XbLH7oaKV3XRXt5ke4epEYZU\nYJBwR7pO74GnylAVm6NO1EX8S6Ab9w7ukP9G4lwdQV7SJ06wecUdPe325/ikAA2G\nNtd72+9YX3o/lr+XPR7ZvkrcxGovIeduc3WbS24KqSKaQRZ5XwbxPN2TAi1Uo2QJ\nfN7TCiN+K2lUitgHirbSSoh0vmsmGLOxKiqRHhzFfsobqsjEzSjrQCoqXAkzF6CO\nos0iRcgStugcxOyZQP+dcyi0lyQB/yjsCv48V8X69QqnMMf+iYjVD8MqGmofeRsE\nH3DFyZB683DwP8xmak/XCQ6z0QdC4UIr61aBeGlhI5mh7t43J1c6E5TCuz22dQ6t\nXleZBFQ+a3L1QdbzG0QMrPCByAzIdil9Lvgv9L7IsB2AaXj6Jbh2+3BjZ9BE4bdu\nKSlnep43/v1jT9Ywfy+dE4Hvs+1TbeyamQmXdLUhTpiQ7c4RVARPxkUc5lcchh1e\n0SZSjJJegdRdpsXRXKHT9AZpFppbWXUDf0TWOkMjP/e4rwTdysFBLTyVIxNx0A0I\n6fAVUNPQd3vd4lH32AZLIjI7RDALN2VLjXJrKaa77VkY+6yeZapFXAa+uVutX5ag\nf8QSAi4nyt3wHM2vRcy6kSOOlRAArqW6Ys7uH5naeBx9cWGnaMW9XqEoa5lK/xt6\n5pSjDkArd8wjxRXjxECVjYjp4nTmxU7AYK+JStYd0qZWzEd7dQ2461EIsNAd6BeR\nznRj9oruvoVo26YfZWrpod8BgGV7e0cdC94yQveQw9sg61OPpPnRThgVc8ZSkek+\nplf06EmuEj/tRd+mSjRwxCIo9s+4AnN42IqdLWWEFufGDrbTbOgBbfnZbAdzpb9D\nhZRGDBL2yfc7BeBikGQadGDD4ITGr8RIxMC6mEk9yvTqcdeSnRLkK3tPnuWf6MI8\nFMfLc/YmEdLnO+DOd2zYqHvJRNZ1EDZJeLJlZ3U4Ve9ETOfkoTvSqVaFkVSY6zeJ\nPp4pH/z70/O07TU2geI1yNPHh0j9Cx47Ar1KwTFXNboqrA4OzwxTQGi+dR363eqX\n7xelcv53Ga38jrjamI3V0m2fQral4/N+4PiSDH+SGtbaDQGxMDe0IxtM5lBci3XG\n/0qsskj3VKEiibKZUn9Lp1R+atwqOcpz7R6pkeDCFMdolTgcMgdcQ723AKF0PPxm\no+fX1W7yhyW2WtZNA27/KkBRphNIVhpLEj2MdQKKJQlP4gFuhXACvdyr28fHfb5k\nNU88Sw3I+SHcCqEcID69R9GHADtmvURdmTXDhuS+wGfQ6xXjV6chF0WQCNqovtFb\n1GHZMpuJBHIEGAEKACYCGwIWIQSe2v+A4IBllgT0p2suuwVv2Ef4pwUCY0gFqwUJ\nDRyIYgJAwXQgBBkBCgAdFiEEY32x4jNw+Er/iMzgMVI0fQfaYnwFAl2wS0kACgkQ\nMVI0fQfaYnyqxhAApqUJs4FOBaC3oXYLRcSm7qLFol4dFMXwSo/QA91vnZd7Gir8\ndxy8OgGjvEy/XbLH7oaKV3XRXt5ke4epEYZUYJBwR7pO74GnylAVm6NO1EX8S6Ab\n9w7ukP9G4lwdQV7SJ06wecUdPe325/ikAA2GNtd72+9YX3o/lr+XPR7ZvkrcxGov\nIeduc3WbS24KqSKaQRZ5XwbxPN2TAi1Uo2QJfN7TCiN+K2lUitgHirbSSoh0vmsm\nGLOxKiqRHhzFfsobqsjEzSjrQCoqXAkzF6COos0iRcgStugcxOyZQP+dcyi0lyQB\n/yjsCv48V8X69QqnMMf+iYjVD8MqGmofeRsEH3DFyZB683DwP8xmak/XCQ6z0QdC\n4UIr61aBeGlhI5mh7t43J1c6E5TCuz22dQ6tXleZBFQ+a3L1QdbzG0QMrPCByAzI\ndil9Lvgv9L7IsB2AaXj6Jbh2+3BjZ9BE4bduKSlnep43/v1jT9Ywfy+dE4Hvs+1T\nbeyamQmXdLUhTpiQ7c4RVARPxkUc5lcchh1e0SZSjJJegdRdpsXRXKHT9AZpFppb\nWXUDf0TWOkMjP/e4rwTdysFBLTyVIxNx0A0I6fAVUNPQd3vd4lH32AZLIjI7RDAL\nN2VLjXJrKaa77VkY+6yeZapFXAa+uVutX5agf8QSAi4nyt3wHM2vRcy6kSMJEC67\nBW/YR/inWdwP/0NU/O05mEirF9KgIBiWDl3XDK6xsr7TtE40B3j99uNCeQsWUFly\n+lr4J829OyUnR4WhncL486/IeKw3dP50N+7bZZ+9EqWMxSc0S9qcGjoV3L/vChR6\nNbc/os3CuWpcE0ILw6KTmXNqHU3YAUkTmIG3qhWKEXc8UdtXrB8k/OjbsOTAYBkI\ns/WlWLOsrXswkr26azrunQjTo/4d7RXtchqkHEjjUa8ggRelWiCmQ0LcGxPlbovz\nU9v70bOe5qWYlGsTyPpAahV+WBAsHpegKeUFtswuVWQ4vpBZzqJJvp9ZYJwksVQ4\njpj9TuWxouC9PId7i5sLtUIS7Mse6rgvD3woYGBJ2YZ/5R7JkrHpKuQf9eV7nK4Q\n3V7hZfGLqmNyJcqtZM8OCSdLmH2RksxudtLGK8vjPMNV9PmXxyqhkr3OarTGLAyS\nFqokwo7FHzP9z5c/oCxet02ubA6BGfRhmAfBX17oVpg4VL6h7ClZjJ2iDSjqA11k\nf4jq+YMgU4csRzKvOnGWN9JExvVoh+bae3+WCPuWLHf6+eAwEYl7YsQyPszazOwa\nhxD4n8grMv7VGzpgjeVKzWF+dsLNzAU1dJDKB9PHL+t9KuNTp+3JU4iWIFj04Bkt\nUkdJIKCOZHV7f5y15l1dSCN39XahId8vGxZXAalWtVx2Z+osZVLn1fKbuQINBF2w\nTAwBEAC4xSq2PqOMDB4WdiNKzUqzLPtmcSKpxljw/DuMZOeLD/HHL2EXLXaRSuoQ\nWbH6dQLm6ejBjw1KCqe5T5dZoFyQkR9PF2j6yARz7bh8OFeFplya+OUUIQ249fV6\nN1bKVEbcQX/ijozRHtWDu4iom/6Zv6hOoSpJ2G0ocmUBjZoxBFgu5WaWwPjGaT6P\nq2hL/VTZfmYHDfNRVYDa+23F9RcK9JntDJUR+Nck/ukYJSdjyQGL56GpohMO8L4H\nNFl+foKBC3dq0zNCai4U/zsxXZYMV901L6n6lRd/ek/TaPSyDmtBBHh96j3CZoSp\noGHdu6bvCtsBlsxe0TiC72aNkt9doru43TCdajtch/DLtXv/tv/BxXDCSXeyEViq\n2/FSEoLXeFfz3mUFZc2nTEOFUFpOpJWQZ2VCU/ZED9W/vNNH5SrViq+ek830dg8n\n43dhnVU0L0Ax+RwnQufHW++rkd8jnLj3nnVwstJNG4qkFt50Y/hcK84EKkf5OPd/\n+DsQk7k51Ndgy0gh597JWdzj59d7EJl1wPJ+Ls3P+Dg5Cx7uuv+/AfT+5BxjCQ+V\nbmGU6JyutJObbyQaHz82yz8KPScxJ6eZvtEupTDbcRZ+YBthRmusWcYRJYtll2a1\nNteQYxoVtbO2ftwymeMSBWJlsYUl1aVDs7YeM2B/87I9z53WcQARAQABiQI8BBgB\nCgAmFiEEntr/gOCAZZYE9KdrLrsFb9hH+KcFAl2wTAwCGwwFCQWjmoAACgkQLrsF\nb9hH+KdIMQ//bJk1LCi++ZiRTZG5nGd1hXPJdMFlEbATGE4Ma1OsVEr9aeAP6tZf\nJNIHfzSAnVy3qi5+QFq1dUzjoLtJ+xvJ0TcN2FW0Ujk1rf05wClsIwNVN1+C0zSi\nRn+/kaRH2XgsY3JKWEKOb4WG/QStz/mJYSYjjvSWzx2lnKPvOJf4BZpCS3tzEeMG\ne195gMyuGBi3UncnLWmKTLdxBliv4xOde3L/IkQ3Yq3s3TSjVez0W6Hogm2/+vIv\nA+xo7FO99ZBG8GIovoSfJNFCvibxFn38S9FLwCm9FrutEa7Rz63+R6/qfFMcnBXI\nw82MijXWpDOK+JYRuSU250qXyrATZEAJ1Z/59jLzka8+L7O+lgrfMbloVi6nVT2T\njvPG8poJIGnfEtncysioye4tSY0l6Mmibwy/ao4iegLmVTO1KFmf4lHp7WGRocJ3\nPpgEUPVja1u5sDsaHPpHSRehImo2dRWRYRBPOK/ssQ17zjSovMj6LDA/+VjKOm3z\n0bFeV4FujX6OFsN+kSvP6qFlgpNctbhzAJPDoaOl3bKBvVxf398LlI+5j59u6f6y\njbNqlPzyp8ZabBZxry8B+ayM4R3Qf8iQ3rFWYPHCctTnirMzUs7TmjiQIiGjRPN8\nkPGo41lf7rZG2/KgOxeMizvQXfU1Rl5ioIahXjC/jYU+XteaS9fzF+2JAjwEGAEK\nACYCGwwWIQSe2v+A4IBllgT0p2suuwVv2Ef4pwUCY0gFtQUJDRyHnwAKCRAuuwVv\n2Ef4p2nyD/9wwAsfDZMZC1hnlBgOHbkhbNtgyRq4PN9vKAKsg3AouUdHwhEG5gPo\nBEvIELaH5ITzE2EyZ9sqgiXZl9gJylaYKUJsX3qxd7nc18/ZPd1dqcHw0TLGPiie\nymOsxFORI4xNXi9F0FjMmo+77Uew0qg1oUVOgkfMY+DVtCyRbU/hY2WogoqsWcX5\nPRxiMWdNZqGi52+fopEIzWhjvMBZJPcF+9jFMZwWtN+/eHevr/csh/fPRRoTySkQ\ndE+wzERgqiEFzHRIaKISGWtN+Wq2KuJMbpCKjFZzPkCr0qZ/42629TSy87eR/fA5\nY9K8dHVPp/tz+OySbItBgv98GZJsnG4eOjUkv969lE0z0tmWgH0a5c/HUnJNwKn8\nL5NSffQ8uhMInbjyjlz0v+pLaBSBwg1huJNd6tqRt4hXQURY2d9Mb5VikDqbpDhC\nTow/RnpLZpYR/d8GoJZQAIqXnwruO+uZetDTzchrbD5PaNJkgO3/Y9Yh/ZWpWDv5\nH3CctWWLkhHbHWAeJTR+R05apXmIeoRLqbsEGvIrwxjDXzWlfZzJT5+TW8BHbKyK\n1W5U3oW2B0BHbxZT3ge6F6tCUatPkRprsVMh6cy/33Hfhgdb+DQFeSoPECoN/w6Y\n2FYhbUpFIjo6dVh8n5d0gwDcDd/u+CGTzk1i8n24OgJFFDcGYslePbkCDQRdsEx2\nARAAuzLJr7jH37QEVQ4n1ota9QKEIZhhPtJH0oez3eOrIQbtohGQlA/Q34GY9aZD\nB5UcM/gr/Zv0qr/1waWoxyzAelU5YeBNaqhL/1r2rke62m0c+j8Qg35smEXqagFF\nFhRKwl6uhjgMEuRyMdfYL/5ybOBVH13T+kjuwIcYkPoCo0/i5eDeq9+857hn8K2D\nnd58sllAmQalmSAFr5q6tEHKjb7PArjBa0kG3xbWH1Mjfi/WjnOHW7RKVj+Koowb\nNMEh4W8ZnJY3LyMz3s9QY9l6mYyQeDkAH4Cf5+g2emjY4rsH63Z0nTMm8Vvt2nPj\nM5XYUTio3lOH4Zyj4mtZzJOIp8WiOc2l4bbS51IcLi4WunxfuunITUYZn4mQtgVB\no+ZOU0HP4MJRwvIknG6E4JCUhIqs66n/XyP/+WGJPd8JW04VCY/slYfCH96win4/\namXLhWCUDX7GHtFBqppot0QOkTF5lj4VVFTvMYp9ShT83+mvEordjgR85tGCVShL\nkj8dG1vWG84RTgNV4KhuuADiAHdgqF/p8eTMZVsI7MEdr2Wms/98zz+PfWgQQcgt\nX+RLq0ak5vumXCy7YB27eGDBmx9RUyDuylm90/PuLP5aIew7Mzax/oFqmYHWrz5U\n5zNiaIol0RYa7s/Tbzup5nXDSkGfaZo8xqWnDNU4m1O4tekAEQEAAYkCPAQYAQoA\nJhYhBJ7a/4DggGWWBPSnay67BW/YR/inBQJdsEx2AhsgBQkFo5qAAAoJEC67BW/Y\nR/inp2wP/3Q3xmkdGCtRlw+MFyTj3qaKEN3gKZrgAcJeuglTUaYoRJS3mnR8zo2d\n2g6sZsgOzXfSXYq10el1vYUwmU9Sacn5wo9ElOlQgbdDbU5ri2ggzq77JFhrJHPG\nY9DlVJApaU2GhfI+fZvxAd3XYXXSFD9TBn+VqBg98C8Tu1h20Cvl13a+4g+JVG6s\nbFwTyK4PRQQqH3sB9YDIMrvOGwmWhMOojOBrdvv+9n/YBbTFo7ikoZPPXtoZL7Wc\n/kRyaBCLapaZPhKS6DVvIEulOt3oBoyzpii2n7qMhew0Om+5arTSksRJZQy5nj5t\nKoniKF2VJRxp3m9tT6hp7z1QiOY4hHu9WTj6WW3I+YCojiMERfjBN6SXQ8UVson9\n8jYAI4SOa3g7ppc0h2PZo6aBUwAtSeohbXHa+M9mpKtVTmEC8E7uP+adEvqf0lWj\nBHfLONOvvtJiONXPHp8ULLd3vyxkWEjiTliWq8AOM/AsPkNsoIE4nVPGpfS5h/kw\nAGffk8LXS2YDjP6w8KX9ltKMrSCzFU1ZUDQIijFupinQYqn2+9pmcLyi+gp5nzi9\n3uEXRtaDZRymi5lGN1bevNIJe2EGo6eYgXZFSggaGcm7reNfO3pzo0Et4fri5AZ0\nYKMOgazg517Bsss4zxxKnz7NHoxoR17kHmJiaJXdykPTO4hueQiqiQI8BBgBCgAm\nAhsgFiEEntr/gOCAZZYE9KdrLrsFb9hH+KcFAmNIBbYFCQ0chzUACgkQLrsFb9hH\n+KfHRhAAoopk7OSdxg4bisR/jX0AwlmVVCS3O3pE0tM2zMCxDP6KwdO355gaFs1a\numuNdLXNNtK1V4u82qaOeZxklrHOku2bQsNB3C/CRYNaDBKA6f5i9PRLzV2/1rgl\nXKxiJ/pJpPPHl3v3dG8bLFdvOFFvmqJB+WjK23ASlw7fNzfJCQ/k5SfPFKvEqrms\npuX900N8u8tEMVvyxpAfNBVAGyGJfmMujpMrNjyv/GscsDIMIvQEsT+JPopDGWRj\nkt0PL3g1T/VhuL7s3SLMVsjhTF1YvfPkua8s38OVRchO5zTx/jUZhPr4nzqdIMtk\nL5xF5iI7KLGfggbCU+ttN2HSjcy+rTQAlQ+Xow04rvSnqb6IEVRyRel/zxAtg+8D\ngDvmcwNX5mm3VtmfExIAot9KcAxTcSEo3ZtXGSqXCc7aXUyrVlwNq0P4H0hxhi/Z\nT6IiQD9/P7iaFVeqFi8tmSkt26FRzXLD6H18MjLQMKlaAYQUkU6EXWVGj6k8V4kz\nv2MNkSu7TFy8G8PO7UlLATB9Y3rlyFRaTnftDtAtNQoHQVyfrF8cWMU8yx/iULEz\nI4Ixswjrh4sfxDRlqDUfl+fpeam7qhhimcRZekQbm2uQjR19hcCOPsHXfYiy5Ivc\njQJGON9XMchQRYTeVMl3keCJId5JCfoZJYGZ1K8ILGREVq47dnQ=\n=qsoI\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "pubkeys/ThomasV.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBE34z9wBEACT31iv9i8Jx/6MhywWmytSGWojS7aJwGiH/wlHQcjeleGnW8HF\nZ8R73ICgvpcWM2mfx0R/YIzRIbbT+E2PJ+iTw0BTGU7irRKrdLXReH130K3bDg05\n+DaYFf0qY/t/e4WDXRVnr8L28hRQ4/9SnvgNcUBzd0IDOUiicZvhkIm6TikL+xSr\n5Gcn/PaJFS1VpbWklXaLfvci9l4fINL3vMyLiV/75b1laSP5LPEvbfd7W9T6HeCX\n63epTHmGBmB4ycGqkwOgq6NxxaLHxRWlfylRXRWpI/9B66x8vOUd70jjjyqG+mhQ\n+1+qfydeSW3R6Dr2vzDyDrBXbdVMTL2VFXqNG03FYcv191H7zJgPlJGyaO4IZxj+\n+O8LaoJuFqAr8/+NX4K4UfWPvcrJ2i+eUkbkDJHo4GQK712/DtSLAA+YGeIF9HAn\nzKvaMkZDMwY8z3gBSE/jMV2IcONvpUUOFPQgTmCvlJZAFTPeLTDv+HX8GfhmjAJY\nT5rTcvyPEkoq9fWhQiFp5HRpYrD36yLVrpznh2Mx7B1Iy8Rq/7avadwVn87C6scJ\nouPu+0PF3IeVmYfCScbfxtx1FaEczm8wGBlaB/jkDEhx0RR8PYKKTIEM7T2LH2p6\ns/+Ei4V7mqkcveF/DPnScMPBprJwuoGNFdx2qKmgCKLycWlSnwec+hdyTwARAQAB\ntBlUaG9tYXNWIDx0aG9tYXN2MUBnbXguZGU+iQI4BBMBAgAiBQJN+M/cAhsDBgsJ\nCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAr1YJLf5Rw5hlhD/9T4I/sBCleS9nH\nnjTJqcOnG28c9C3CRYIizjEui/pKmXz9fB1N9QrCaruPUQx2UacDVCl6dKxac+7s\ns3/a6lsjaRn0/2OM/sCVLScyxNPNPQs2b6jkodSNPIM8zv51g+flhwtfrO6h6B4j\nIhZgSjFdvqtZd5jaly9rA0uMX045CC4K6HGnq8n4F2p31z0L0LaHBf5EcsCM0MMp\nQVkY0aUrNg9uVMGXBHn3osHnOtQaODqcIbpa/OG+Tlt6pVOiDJ7i8TkpQKT7sOaM\nVdL//TEoDIOC7qVCN82q2q/gtiBXbziaERVs/eU0O52aX5qUhXu3VIjXTp/riRim\nR/f9BPB1dgDZbF2aPZ/rJm26v82ft7gP1Sf52E9MrAaZATTfI0/TUHXeBzN93EA9\nxb6/ENAMTX74u+NjlynWPD+hl64eBzJ2ionZF1bJFTgBkMfRYnhllvleCjcq9YfX\nmd5HKCwtxfygBIujUQSwyUzn0f5DbVCJ7/B19bKdvHGSSBgBEjxqXWQskm2wc0In\nww63goZAGDQliKhIT8xnwOBbLkqSobq4tD9zpQyxvMA2rhy7/gfFRp7TTak7MZHf\nlTJ37S5LvcWHm/ccWUZDUN7akoEDc+m6jX3uIEPMD3PQvcHhWv0amco3zDr1qb/+\nrXM7TJKd7DPX0E2dRzKu6aYRMTbklbQhVGhvbWFzIFZvZWd0bGluIDx0aG9tYXN2\nMUBnbXguZGU+iQI4BBMBAgAiBQJTQDaRAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIe\nAQIXgAAKCRAr1YJLf5Rw5hOBD/9o/NqHLvjhrCfy6/SblSC/udV9ujFnvhZZZprb\nr8Oe6GdMwfw+ktZd2nYb09KjxXYmGoZeZKmvCb0LoMKSVWgisH1rgzDzI6UzFL4b\npV2+PqCSiWaekfnBm+oHbGgJCuAXebGXjVL8JsvhAl0HQZzTA1RX0u8TEAHOxOI5\nl+mXSN+cwVZuDMpt5v+JDyPGHM/KqaXCw1WJY50mqlan6/15XHilmvY/CaxmbXNH\nZOXucmPxyCTeiQTqyhHsIBb4RxWYCaUXv9+svriotv2HZpQ110NN09ml1K1kDlNL\nZh3jNqMsbImFArbN8GikjqhRBV3K77Np4lccnsBPllQMqqQULG7UshcQTatkmTMb\nj2TQ0oQWEZt0uJmnmxgz18ijs6m2fJZhlH0QYVYOwUvK6GfAFluHwOZHIXonv8Ck\nuTW+P90lOB/9ZnREZeYb2wlvV6fCTMHxptIbT31kbLTzu4KEI6+ShQXT+YAKiC5S\nJC9heheaeApH3wcLiZJcCKYv6ubY+3Uf/EoXcqWywwpS/nWkSpMSYjq+V9xCcGHI\nMZ4vZkiZ6OS5Mu739rgGfP7Yi3pqUYLIpUa5QiNOMEhPtWbj/oH5ldaZowwgZ4MK\n2Mzxex8IhFppPtZgqJfu9NZQLICpxcd2hUe3XWvB+jcvboZ1p7RO7ax3Vo9zy1fy\nYEFML7Q9VGhvbWFzIFZvZWd0bGluIChodHRwczovL2VsZWN0cnVtLm9yZykgPHRo\nb21hc3ZAZWxlY3RydW0ub3JnPokCOAQTAQIAIgUCVMYFygIbAwYLCQgHAwIGFQgC\nCQoLBBYCAwECHgECF4AACgkQK9WCS3+UcOZ7BQ//VJuRmM7kQd5DcJS76BKpMtKt\ngUNV3hi2h8kNGtkIeKhpeiK+PeweFJCb0nQDiEYsg5Xd/l5ZwN34cqlhgaQ8uWBY\nrmNnSYGECLrxejx6WTWHp2AtD9BXrj73HEox2abC0Bdky39aCTyuRhSzbFnV2unh\nL7IarKqr5bat6ywFZWsOcaisEjWXlTSD/hYqnkRX8vnBZRnRgHyi1yOvHsXGFB3x\nO+P7JUb4E7BVzVRDJzMgcBhY5vTZ4Mnc8eIplNVI1TaF2hmhmnezvRF6XNYV1Ew9\nt2/HE85+DqIBikUWYPTTxJiWUOwxXP9dVOEmNTcAgVThvMN7W+WoF7//qcNKmbPI\nDyGU5xb/MLNrM+MWfavtkHNqcY0+cFf27z4mOxd2eEMDVxN/Fhq0HipugMEawaZ0\nG9xsF/rZBzKgpu7+SvqRqxUn36vNz59vDlBYEXSng6nJobUdNb6iHo/rpZ6ZYHKx\nmzrK5ROpmKs6zpPTOn8Hw29jxx07auzEIVEa8hzZaiqTfwI9yBwzhFQwNxmNaKRE\nadxosvU1VyTvaEVmMmTx227MF1qhwq9yrSXtmKZJGiHRzyL4B4vAGrf9uK9GwzS2\nTlyksRdjapw6Cqp8sUB2PUzHqYNWs0wSsZuxwVt6JSD4N8vpYTTF00LONKe2oLhj\nGNxpH+BV3SqMHXQl9Ki5Ag0ETfjP3AEQAL5LYJiX5S4PG891TMihejh5KVgc36/R\nzgWYJkE26K855t+WdAa6spHKR1RmpTTsnaTXaC/bNxJZq+0vi9GKlw94twEueu0v\nCniinpy6AFeydveCi+qdr5XQ4hx1DY11kntGBL2wMOtrZ4oAeFnntHYcAMYaMBY5\np8gd3WVR2dgIvpOcezQBLwhoMHnN6A+JEQ27ZHcolwDO9ic+t4YAtl552DP1xKbc\nT4D1JD0J6W6FbUJElOXReSjNGCuSLZZTsCzMg0P6RHwWUKtDvRKrK/M3Nh/L2EsW\n5mAQnYps6a+hyVkVd9kLsogtHPE4xv33pzbDB5Yj+2zqdjYUqO/ODfkP+HjNRvyj\nuHL6W3bjU6FnuJQXX4llskls4hlKDPawa3cuWnsdafouAZOxWwBlGysRZ7BaHOFE\nTOlAeUN1EYfFrckcfkYzTX7NDA0S99aX730z/c9XrnqM52OO9LrSFRnYZ+K3M8z2\nFFvo9/ZtqqTDH0/oH+ay0CwtowSovZUoljAQ8zmmi8CtPDFHg4srae8YxW4fetn7\nQtP6rOVRwQCyP12LztC7oYGOectU5G9GkVDubNW48Vuex0/upP9RORjKN8atBroS\ncmomR5hShxmgdJBy4I/TDkVFbZq/hRPSTAHgnciEC67TYhszzXP3nTn5/Ah0wCGC\nd3HfiNX6G7MdABEBAAGJAh8EGAECAAkFAk34z9wCGwwACgkQK9WCS3+UcOaJRA//\ndLHRBjeAkNbRsY5GNTWUZzXr3VC5vNqpwpP9rK4QTAmpl3iU5F+wsgMG78iS2XOV\n+ijZA8KvishletQJoNMxS1PU4sA4Y34hYb61ptHs+PmwNpcdgjAX+mCh9xQ0816G\nyIaXtxtxacJJW3K07fqKIkJjISPOyTLSd+wl1LtRE2fA67pMmpMHG8t+RPq1dp/e\n3qp6L7jc6X3U+bn2m7u2cgEVbuAnSaKGoMSMnsd71Ltf1b6/DwvZz/HBttEgcgSm\nPleHUVyBD4LDrcjTDK7zdEMw7b/cPBnu6CmTcogFEqvB4n9Yodo+4ij7AndUTz4J\nj1p8vFlnHvhRg82MDfGUPJ+ujBjbYXROs+WAmaCQ8TgjZ3dAFNFrOqAbYu6QlY2x\nfu7vj+ruc6ArdmBrOlsJFmNsxFRJfgdUug5JFIUN77GbjisHjWem8cY3szuyEke8\nH2pi803CAuVtkaoNmNDHsEBieft34Zo0V+A/q2wkix3S9vyRjOKqhGrW30qxnV6Z\nFexueWuO3qOQ0ZU5/TIH0kft2n45/RexeBq/Ip52zE1vEvTkQmBCfCGZmqTu+9Ro\n8qsjecxVNxyVPlwhlimryiQ+dPaJYaOSfiwEEMh2MyV5c6t6qN9n6jFdiCLOlmmH\nZFA8xDodsofQEmlv+I/xyEZ7na6nxbpZVuPC3B0JFtY=\n=sUYl\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "pubkeys/bauerj.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBFRL5aABCACgnvbQOPgPeyBolejlFaY279tVUWaBeFEYQ17xfI3xo87Ywb7E\nDOq1xsQx6RNGOiriKFWyM41S8lcIu7fOAtfkilWiqUCoapn7bQlDyTl7LPKOQgNA\ntxIKibKyfmDJ1xyMAcyF8kV+Gav3JgucpBlYjmTdNC3MvI/6MGd4GdxG1l/4aGLc\n1xV9a38RvjZnDD0HOfyUGbqE1dY5nEVla0sgMp1h7mSyBebjLkOareidXJxK5N7v\no+/yFidN2BiyKSQLzpftx4OIJx2hWfaTRbn+l1WF35Bu6iYhBtsvrZFZBK1bjc/A\nxHTu15kJsS+GuP3v8qH/QB5fcGah44QjM7FdABEBAAG0IEpvaGFubiBCYXVlciA8\nYmF1ZXJqQHR2NHVzZXIuZGU+iQE/BBMBAgApBQJUS/v2AhsDBQkB4MKABwsJCAcD\nAgEGFQgCCQoLBBYCAwECHgECF4AACgkQhPG/klsfSE2JAQf7BE7GHWifVHMjiciN\nbvS0SQ/hx33hn42Yd/jwYsXsIBuJcJ/81s0sq+O/JRXrhZxSrOx4ekKQ+8tQURvw\n42MAXN8QTp9lXno3jPvyTHPLlmW3Ig1wQ31Kh5daKv/dmRTrsgP2aBH0YRLQ28Qr\ngRiCEK8Ea1ujoUq6PzmmcRB3waKJm1eIUwEj1iP2rFB5MV+ESDfKXTyUiDpRRma1\nbgj4mKv6vDO0839Ho3tLyGnRYksCcS3XUqYU1nhsROzW+91YWQiD8zfTmnQ+q/t6\nVxXW9aRgq9EY8KZUy7I94f5ETRokhszOxxdv5zZRTKpWyKUt1e8zeLss2krUtJzl\nT3GWtokBPwQTAQIAKQIbAwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheABQJWlU5z\nBQkEKpxPAAoJEITxv5JbH0hNycIH+wYbhniOrfrmWhgyjWKFqvdhNA9Z1t6DPAqJ\nDi4Ow4GBEp6N4RmRrv6WateG/Mva+Fy1x/Rj6PgrJti+9CZUuvrlhCJ3SPQN6Ajr\ncwih0QyiFAPRXZ8FVOds93GUKyMy4SzLU/d/OOJ/0MxPCjbWnz6J+0snwzYAykuL\nWeB3PIeq3n97MM2XRSDMY3a5/6XpKBK+JPb95MwMbSeh6czqp1Xa96S2iW14Wa/v\n4shHXwBgC32Sk6CUu4qidi+w2eGK/tVWRKAffONULFB7cT5sFgm1l4gScxH4GrBH\nSsZWilFckkUXxxogh/FY5i60FJ58rLdGntZ8x7sO5lcdHTy5Uo6JAT8EEwECACkC\nGwMHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAUCWKW/OgUJCBxAjAAKCRCE8b+S\nWx9ITZGmB/9CPtyBSOv9hMhf3NouFrrIZfVHW3RDvr0zPtF7Z1JQdQzccXMdboyc\nm9kAP4OzkG2uRhJtaTvGuiCd/B9X7xsbI2JkQo67rgQiesByZIuBHwugg/nmGerM\nvpApTqljTqd3yVxy68377mFRd2DU9byCyghPGyFMS8RAo5lMEEpk4kicfjSL75la\n9W4MAcHM1HZ1h0roqN3Nxwhn4RsD6ssOiGEO4LQUhzsaU4LSYk1OjHb2zvd7UHsV\nRNRLlSsj66y7nLuQFcJX0/YyqHWwhyUTKDRN24ifpCO3/HlD4PmO84FdF35b21DG\nSE5ZOywtpPSqP6R3gF1qxvSXFLxI7nePiQE/BBMBAgApAhsDBwsJCAcDAgEGFQgC\nCQoLBBYCAwECHgECF4AFAlilwkUFCQgcQ5gACgkQhPG/klsfSE0b8Qf+KBY+HW+z\nlvZbEzsZ9s/4Er/0InGSHWD8o9K1V2M2woThXlbiZZjvnJQaEzXXjvgdqd2BhAp4\nfPwcd28ww7mVBycDMqffGq4M1xKzwXSXC8oSC+zqP5po7cFppYZi0QnwATtJDdS1\nqBOCx4r6+TXndMP8wlXOAIYVPFPgvsAICOhBfFz/BPx7V/gEWj03TC6P4+chbPfW\nB9bFKUUlsW7IqM5nps9GHs/jkCArb29f2UiKEbMSlPzB30uHxqw1cma9CPvYjpXu\n5Rnw+nIThBdOhuTcAqBwgBRwI4StMAd2mBEeCUJ8OrR/tQ7BDHXWdgNrQJdybeS1\ntuEwSDm6f52vHbQgSm9oYW5uIEJhdWVyIDxqaG5uLmJyQGdtYWlsLmNvbT6JAT8E\nEwECACkFAlRL8mcCGwMFCQHgwoAHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRCE8b+SWx9ITYN9B/sHBt/PZ26zsHYu+b8mLGENm7lw2jYgYsde03NWf+dT7a8p\nW5c1rt2ENmG2N68a8+aAgMxcn8ZJsXOF/APMmRbHfpHdshGTUMBs2wYaizlAwjYv\nnerBfSOvWSZpk7VqI2/+0Q+sYn5w1MjRu60upyEGQVM+ZIftwwrp0FolJdkDgihM\nzXcJuwxCSscqF6NsVukSxo1A5gKjJ1V9jvcXi4yUaYhfSw/hUSAjHo4hXeXbJNuA\naBjLiTq+QMQ7d9dAflZCAvd+KsG3BBXuG8IQIz+OxTtdDnFvQQxTPzlcIq5KHI7O\n6IdXC+T7Fmf9x0h6QkhFuVS6OB81E0I000d2TMcViQE/BBMBAgApAhsDBwsJCAcD\nAgEGFQgCCQoLBBYCAwECHgECF4AFAlaVTnMFCQQqnE8ACgkQhPG/klsfSE177AgA\nhUXVzFWHpUXJbsMsdzuZ9d9ts72+NUY/0ilNaL3t6X1GFvKfTDxuc72ivP2W6Eo4\naYWAHBYQb5a7SphvrknQetIwCM7ll5LZFlvkff0xb8DjLSLfVj4BBiT7N4pBJRsl\n2VQoqhdcul+EilXb7bYcPQGIU0ZK2epBbm8VfO0hetQtb4DxT6viuSOmkntMcgHG\n7zSgvhOkyZHjlw3sMqAr999xyV0hZRE3vUEHeO3f9L/nZ0msLpLrfKvczKrlHkNI\nIHzG80Tm5JzmVtmnc3nVGbskqZgTLgR8sIdNdTBN9j6I03wwvve8BqNaeh3W6I3P\nxgVgWxwF7ULLutld6z4mGokBPwQTAQIAKQIbAwcLCQgHAwIBBhUIAgkKCwQWAgMB\nAh4BAheABQJYpb86BQkIHECMAAoJEITxv5JbH0hNBxIH/jCr/qpjflwuWAIojmLQ\ni/HOZTssUym36zseOW0BN0pMdbqrinrzSXxrn7C+Yzf/1EZTy1bgE3tI0fmcPOJS\ndOCIIqeuMbF3uZ82imYg3aX1t4eaGF2/hnJWn8W054FCmR8iRO0/Ge8bPT8ZO79Z\npvZzY1w31qnOVIflFNJla0+fXhi+2Bys6WpvEdAo6PfUh775RE2bRGO7i08nyJUP\n3fLuuWiF7rIrO14lCTBkwBYQUEfN2JbIFfckFJBieZPyirB+EHdHJG3qMZCeefee\no8vkSIX4NfLkHB5qXkdYYwBKlXuVTXwZpD2FyIAuKRcbWJgJ8Uw0sLSRyYDXdlKz\nlgSJAT8EEwECACkCGwMHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAUCWKXCRQUJ\nCBxDmAAKCRCE8b+SWx9ITQtfB/4gzmhMaFp3RE1Swel2G5dMbgfnU+RkutdHWUtN\nQPZFzRE7aKDY5dNXU/NyjNgiD9EIrJwgalXo7m9TCBR4jwLqdFwLSQ1IgPNGoyRj\nx6IVudLX2apzR2ZDnJCFaJKNxxLH9pIouORk30XsBVPRSyVYJJaksdR8nyae3jNl\nLNgHTb9P+mMuMBErrFf9tEWOb4hqO52zTnKCeMdMneL7r1ZZthJhk4nKV7FUWjwZ\n+8HEIhiJo2HgTUqdQlgJ+NKQw/FnO4XIJp+97eKD38W3rFjYKLH+gx+a6Ftxn2Hz\nrcwKvn59/P3BbkaS+m48nROy2lOIzolNGel8L60OkIAkX8EHtB9Kb2hhbm4gQmF1\nZXIgPGJhdWVyakBiYXVlcmouZXU+iQE/BBMBAgApAhsDBwsJCAcDAgEGFQgCCQoL\nBBYCAwECHgECF4AFAlRL5gMFCQHgwoAACgkQhPG/klsfSE0DBggAkVZPbh84VxGs\nlLqhj6FLOJFEP52TPbmNWhKe3C5KT+tWawuBQDcnlmyly9A+fVcW8BE5JnAn/Q+q\nbwBZUZCF2tqgR0SHL3f1GOrpwWJ3VbCCodoeG/UFa3XSW9C1klre0m9vISl/NB4L\nga/ILmXy9Y7M4igHGgzxEGdn0jo9X9o0tp3iPwLlO5nAZwL74YlH5ay1e7RsZQ/0\nRJDvrATd9Fuqog5vXFq4xJay9p8/KsMMMeJwh11BsN48DDW/JytB1juTGoTAG4UT\n0N8KFOsfKdEuEFJddyQAtS6ZtHKmmDDubYoAHPW0zXzkUXTFNM53xkjJOl0LwVPt\nZ/7u7TU7sIkBPwQTAQIAKQIbAwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheABQJW\nlU5zBQkEKpxPAAoJEITxv5JbH0hNPcEIAJwDysT3uBCsaoVQvxJB4HOussnvz8hA\nxuvB/GoUMF5lg9WUpImNM0iUEoCFWtYUBspPhP6XdVOHOwAUINqJTi+tEQZgRJvv\nPD3Y+oXhIV9SzXhVRzPvkRhcU6VVQKd7DqDyZceGGn6CRahRMdDhDWZuEBjb/Std\nOv04GDwNYWSwpz+iU3pP5Ab2dT6oDrxKCLogu3LV2TuhTXypvOhTeFpspfGRacyf\nbcVezL+kHT/EbWVp/qZnh5v4AdqxYQulzW3JWzWt2LTdPDO4AsE+2UAse2vyPgGP\n//69RXfvrVoW9gilmP5sLuozP1AZ4KnFwOvTrv8BP/sSzUJumUdChR+JAT8EEwEC\nACkCGwMHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAUCWKW/OgUJCBxAjAAKCRCE\n8b+SWx9ITdmHB/44opEJEEEboquNtYHiyjcvU6PI1jJPIRocE93klBDHfo91UbE3\nNwDp0TfeS6ooje+8Q+nWcTb19EdL+kDLRIj2i8O6amQ4p42ypd/6A04C0MJHM4Mw\n9zamihy25+ORtl25BG+qhF57jWn+r828TGgx3PWQbdenacjXm4bkyb7f67HkaEAD\naiB1D0U2lrBaKoVYc4qTSC8mgcdh7hSB6iBMPsuqtriGqTeFsRs3Kl/P3IfWtbdN\nVAE9Le5dcllAX0OORolXgvQBBVRWz0LcqdRitRIevcZ902P4Jl4trMq4bel0Spqy\nPqNcn+Cswq95nSyLTEwlb+shK1vDs5icNiFriQE/BBMBAgApAhsDBwsJCAcDAgEG\nFQgCCQoLBBYCAwECHgECF4AFAlilwkUFCQgcQ5gACgkQhPG/klsfSE10Sgf+JPsZ\n5/dW/sDx+W3G0rfRU8PKiKgxvkmm7U4R9UuF1FoPv1iMrBMh/sdOeEwD6A6kZFmB\nrXgb+gPToc8Vavmo9QumbNVW6msj403H0oReGxxbbQ++XimTGrGQjLsjGIdmDWJm\no1sZC1bVHMlRUEyaCRtBc5wJUGdo+m6zE6308XiSg9EcFw6ZQo15imevmiSdGSQ3\novlA9aJe878bJRy7MbilsDabXeasvUtCZ02zu46VfkbdlH5oDP/tKY2FdinVOED2\n94r2JJUid0chDb2FQW6cZ1WzidBfmJmwUKyMDx/Igmu4pNcYxt5q9KLuvoRMBbRg\nylmG9Uyo0r8dXZCgObQqSm9oYW5uIEJhdWVyIDxqb2hhbm4uYmF1ZXJAdW5pLXJv\nc3RvY2suZGU+iQE/BBMBAgApBQJUS/JAAhsDBQkB4MKABwsJCAcDAgEGFQgCCQoL\nBBYCAwECHgECF4AACgkQhPG/klsfSE2+dQf9GmR7T30orDcptqjVA+63hiNR8RKi\njJXRi8VsvX0gKacJ3E9o6MBMGWMuJAQ/oR2YYzS8T3vUbtLuvEOq3lkedyu032XO\nvDwCuEzs751Y/6YR2mitats3ze7Ey280hqYbq+NjZthFe1Ezr//ZsDYeOBhRGB/Z\nSBt7uhVmwc/17AbdrS5xJb+a2VmC5DdYTeR0bdE4A0TRKNQ/9kt9SIQ4aJ8b0ueh\n8tXO8PgFUlsvO07N/k9UkAkwWC8kd3FTVNZt5zabRUoy98ygOIiL3YlfIjaBK2xp\nn3DF5KRsmKmDtBXKs929KCgAolV8QjMJuZLe+UdynXA35E0gyUDT1j+hhYkBPwQT\nAQIAKQIbAwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheABQJWlU5zBQkEKpxPAAoJ\nEITxv5JbH0hN2LQH/0nJgXlfI1YAf+mD5JmY4FThzcnud2PpYuIUAZ5bzgMp9KGC\nidiuHa0m6HCGvZiQPJ+MEfVfZN0zvysrJhoo5uk6slf9hIaKgWQxaCSkw1pGj+2F\n8Qbg9Lx49Be04DKnk8C9KCqzA2vpaD3p6aiXYJ05FB7b19GxT4v0FAQNmI3tR9fu\nwrxMK/kl3lQok+I8fwVeWIvwia+DLJJa+Pf2bOrQginXPOrSr/Ysw0ZOJoDvrtXm\nI/RVGQnR3kJW29wJXIeQzwFFgjHI3qC9jiQqij6SCgaunGKrfdZ56qe7SwfXcXlp\n4C+FmA4tvPHwMHnrb9jXJutY1ECL3darU9QX5iGJAT8EEwECACkCGwMHCwkIBwMC\nAQYVCAIJCgsEFgIDAQIeAQIXgAUCWKW/OgUJCBxAjAAKCRCE8b+SWx9ITcAxB/9/\nZc52sOSeyoyITBJlz2uCXcpvBuQBN7GoVEDmQEP7EBYBy3o5xs7TFbep6dVamzIF\nbp0V1TcW8aKk37Jac2WVbpdfBTu44AdLAYuOnLVuSu6sTGGct3tK41Op72RXXVYN\n1l8JAFXpHtP32z4t6tq3Tc6Rgr4G2aozYQjOzbgmBcPeZRSz5ubMTIsTDaVZILku\nYT8fwvBbRiiOoYfVThWlJxWtz7Xs23TFKwVdBYDyKQWQyvBnpIPKusd+GIjIAR4a\n+P1Wujsxu88Mruhxp1iSB1gnbN7hum0MOu/ncEg4r2locX3133LU6t7fbAmleZ66\nuyYofllRyxY3FJrdBtsziQE/BBMBAgApAhsDBwsJCAcDAgEGFQgCCQoLBBYCAwEC\nHgECF4AFAlilwkUFCQgcQ5gACgkQhPG/klsfSE34hQf/SPzpAjbpghUnPvYgUsRI\nAuQbGZANBgSBNj6K65RNNCz78M/eUdNSqyx/n/wMPLewNW1aJzZDV533ADzckvd5\nl1qfsE6iJlQlTwjlfirmVJ3eKYAS/7gn6Yrked7KjKMzL7E0Ytz6idzSXkDPyPWb\nNl06Q70sU+kEKSEP5Q1W0u3BUOU3t0v4GsMeWK/OlIMUOxoEpj1sVnUFT8RtZBKp\nQ9VKZTdOX3TBeEx9O9NjbjTt62SSB1WCH34d0o2GAYLJOEhFNKt92lzaygytfOAt\nFY/TBJl/gnqY7CzMFtKgUHttrz98XdI2ze+GqZ2KRMCTfhWStAnwkxgMK0X++jIF\nEbQfSm9oYW5uIEJhdWVyIDxiYXVlcmpAYmF1ZXJqLmRlPokBPwQTAQIAKQUCVEwi\nOwIbAwUJAeDCgAcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJEITxv5JbH0hN\n5OoH/0kWbdL7R4sznsrstkU+Z1Gi795M6tzk/1/oSkR8j9tf4B8RX2bSs6tVmHQP\nByTVdKV48b16//k4MmznziJuQmjs8rJvMsxKleD6UTncH0DNzYUxpxhsAGj9ekf9\nUB7uRtQ00DuK+6z+aqfbBh2FgnxtpQrpsLbHvW9WI2DX0zvKmec3WlrhU4lsVwBp\nRWUyvAv++PB5ivkm4TBea10nVAy1RvLeBqPolniAW3nE+pTljQeMOMK0L5sDuMvA\nfiIiBAjMq1WUGirRmZDWRbgzD86BaVnY3+IB8pCjnG/uxX3lrpz5n+hYYeNt6q5h\nP3zixFFrA3W1+h/hBGBZtDV4iAiJAT8EEwECACkCGwMHCwkIBwMCAQYVCAIJCgsE\nFgIDAQIeAQIXgAUCVpVObwUJBCqcTwAKCRCE8b+SWx9ITXIqB/oD66hPC7m2g7NA\ncAe4sEp0qplr723lhn7fcJ3mBvCHUxUl01lQoKCSGIQX1ilVgd+xjPytPRhUy1Rr\nO1z0pldDyJfVazYP7VSq8qwbYNcAeU/efVuE57hlQJ1mlhJ+h3j1qkYL0k9pf23m\nJs1amiGb2FO7d0MSClERno4gJJ/BWSa47ZTtM/YJfvp2CV5mOD+LseEsCP4U+Uzd\nONP0mTV4WgX0jdH5kAl7PvXb3g2n72kWuRV3QrTF1PV+3Et1BJinhGU3+YJb4/OB\nLnF0cufGiL8DR6A13pbskaFRBxqZs7x90E0lpAqGIz2Z/hy5KnqATUTF/TeDG0zg\ngoqxX9fxiQE/BBMBAgApAhsDBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AFAlil\nvzoFCQgcQIwACgkQhPG/klsfSE2ViAf/a+Ayp4MdDT6zfRIt7RbAx4bdpYe3pWU6\n0jH3b4UJ36LtmqukPvoQzhfQBazwPPmOxnvo4Ias0XTgCx8lbNmLl9tlRbxYvgNx\nNk6/Wtz6h/y9i2TPtzDe9xmeH9/nK0HvaDxWfFTp94LfJqlpYLwpalK6uC7uczh0\nkEl6Y/3pYuEtXb/hk6XjiZWj73gKkrienktHj9lQBsfph8Jjuweym7zRacZaycd0\nCiDOWBStHvq1gDqy1lggne7OPRhWN2Ttp+gEmkSboL1dV+7BDvBhzZ1efhE/DSfk\n+2BR8MROCgaAGA8FoZvxlwfKJBCLygCmXUG1pcCvxbmcgN7OK+iw6okBPwQTAQIA\nKQIbAwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheABQJYpcJFBQkIHEOYAAoJEITx\nv5JbH0hNer0H/j1XV3GiMAzbEQje2oGWss381CJyVnqJFVklpssUgNjRikfbnj4w\n06H4BCg3c5rzxVTd4aK4hyWWGH8tHwVhN7tfxLzr3OxnZOI8ftujvdwyOwHSXGJ7\noad1Nsov7glzHFhkzBCjY1U9UERQ3T+9u+SJOZkhyTsipUIK7JPI0r4r2/A07jsJ\nAj09yREC+Jz+sdtXrEWo+dz1ewamHPkha3HHfkgnw4yWRQ3BxRoxb5xaotlzOuVD\nz47oB8Y33FxdpXYZikajTZBPeX28zHjI5FPmkQBQ97sbyZTw5rg59Wg1A1gXV/jQ\nN//Q34fhExbcLyeVv4drUkFL5mDXYzFCB/u5AQ0EVEvloAEIAK/PFf19cxUVxu6a\nF5GXTqZXvhEszCWfurhPEiloSpoaH0aH31oFgi58KmivH2tworyUG8PeIBOcoUGm\nQUFJrXPsnNu3hdFIEkI2eeT1FBezF+newY0S3oOQG5aISgzLu7r3vvbY4JW3AUFA\ngVVwJmatBplNPrnoLwG+Nn8oBtOdMMvkOOaHnW3z62I4JLwCnFRG2eDDFYCWsxh5\nEkh0DgJEdYGXSKIsHPm+UD/18WNG78C8zC9GyUmbsZ3zibc6GmdW3Sh08lNdraAR\nS3V6Ty2aKXq6jdi682ehKzAeSvqtr0LEUPsmD5s6g2PhfXCX0Dc/9czmaGPVs05Z\nX/3Y/skAEQEAAYkBHwQYAQIACQUCVEvloAIbDAAKCRCE8b+SWx9ITffQB/9Q5AMw\nElZu2g0cE5tfhh0dydN5D9Z3T892lYG3R2EQ/puCrLV8xg9R1/Oe3LYvpxavAeKQ\nafmj8BIHYzuGYwMmNRRQEOGTlkisQlFmuAVgPniOf2AEgjwly0Me4eib7CHVIEP+\ntHTU7FzcVw4PPl3PbHKyPNi7MF/LL68xaJthIgzKCQkl7vGkChHJFRwphFinNHAZ\n57und85/CMrDMK6/BHAkI+ShwxVGgZIwzOq9pKbaBUVeNWhvAQWl1JBRh+e/CCJT\n9hnJJGKUTdUMjIDNfH9mEFEYkAYMH+SATTwTDumdS8ixmMVaSX3E1zblogE3NO2P\nT2vtGNK2jhXLDcGeiQElBBgBAgAPAhsMBQJYpcJTBQkIHEOwAAoJEITxv5JbH0hN\nmUMH/2roD8oBNjQrhzkT2N0amWa8Wlg0Kyc1qbkEdi57b9PVEAuTmR6AGzIlLcJG\n7s8qZHMdyY/Rg62aJkJ+ma1YNF7cK4ALVW0LUjXNiyfTnUSBgwx/QobtMUcE3K+z\n4DRLa4QYE28qaweNAA7VKeHSzC9G86BnxGIKvZolRASPW6hwDiUZfHLLdt6jLVwf\nb/b7f/2fLQDzQmxm/nwMN+qLAkv/4+vhcKDcMNfAhz5DmuAAg3OrkZEghX54troN\ntpb9QxdWdhrgTZ6OocAloqc5aFOsTY5CFqmc5lQupMsVzpXhqLiYA2OXRbh7eQIA\n402TZWn+BlhGAFxa+Wzl46MVavI=\n=bDjo\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "pubkeys/felixb_f321x.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmFIEaE/7mBMIKoZIzj0DAQcCAwSWKI0AntQ4Vb3SiaRcWTyW7ZDL9ub2Y13SGt4s\n2UOhvwqwm746j3VCKKll6/IHkOE4SS+5UXQqm2bmNiM2CkNytBZGZWxpeCBCLiA8\nZkBmMzIxeC5jb20+iIAEExMIABwFAmhP+5gCCwkCGwMEFQgJCgQWAgMBAheAAh4B\nABYJEH7Y2Cs3GSaICxpUUkVaT1ItR1BHb3cA/0xm4L1c7q49HwpS1jQTSkmn0yE0\nqX0IqOIq1+OiLIcyAP9UVMTtWKXN5SIKQACAddMnulQiqfyV/8EGOP5AmhzxTtHU\nC9QJARAAAQEAAAAAAAAAAAAAAAD/2P/gABBKRklGAAEBAAABAAEAAP/hAEBFeGlm\nAABJSSoACAAAAAEAaYcEAAEAAAAaAAAAAAAAAAIAAqAJAAEAAADwAAAAA6AJAAEA\nAADwAAAAAAAAAP/bAEMAKh0gJSAaKiUiJS8tKjI/aUQ/Ojo/gVxhTGmZhqCeloaT\nkai98s2os+W1kZPS/9Xl+v////+jy///////8v/////bAEMBLS8vPzc/fEREfP+u\nk67/////////////////////////////////////////////////////////////\n///////AABEIAPAA8AMBIgACEQEDEQH/xAAaAAACAwEBAAAAAAAAAAAAAAAAAwEC\nBAUG/8QAMhAAAgIBAwQBBAECBgIDAAAAAQIAAxEEEiETMUFRIgUyYXEUI5EzQlJy\ngaGxwRU0Yv/EABcBAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAcEQEBAQEBAQEBAQAA\nAAAAAAAAAREhEgIxQWH/2gAMAwEAAhEDEQA/AG/XP8BP904eeJ0vrV5e8VD7U/8A\nM5kAhCEAhCEDbp/6WjstX784Bka9crTbjl15lNPeiVPVaCUbnjwZa3Vqyqi1goow\nN3eYy61/E2Z1GiFv+ao7WPsSuhcCw1P9lo2mUq1T1bgoXawwVI4kdc7gwVcj8Som\nmk/yxU47NgzY+29L02AdLsZlOssa1XYL8TnAEbZqqQlnRVt1nfPiSyrMaRhLqaAq\nlGU54mBLP4+qYgfEMRg+szQusqwljBuqgwAO0y2X7wMqM5zmJKVOrq6N5x9rfJf1\nG3Y1GlW5VAdPi4H/AEZRtWzqqvWhC9uO0rXqmqLbVTDDBGJeo00EU0VHYpLtg5EX\nqcafW7kAHY4k06mrYotBBRsjbF2avqM7GtSW4BPiSS6vBrkCaklBhWAYYl7wLNBT\nbj5hipx5ixqn6QrZUYAYBI5lk1ZqJFagofB9+5eoVWWww/EW/wB5/ckuSxbPJ7wT\nbu+ecfiUaq8ajQsmP6lXyH68yaMU6Nrtqli2BkRVepFG4UqMMMZbvG1aip6DXd8R\nnI2iZurGhtOjatW2gDbuIx3MTbtv0dlmwKyNxgeIfzl/k7sHp42ylt9KUNVRkhzk\n5kkq8Gmxbp7aW5IG5f3F6OrqX/LICjJg2qPWWxFC7RjHuOp1SvqSXAQMpWaupx3N\nI/U06MfIjojSALp0UEHAxkR8s/ErzGuO7V2H2xmeO1OOqx9kxGZUEIS9SdSxUzjJ\nxApCbn0dWy3ZYxesZOYjT1JcGTJFmCV9GTVwiEZSKzYBaSF/EnUUmm41nn0fcqFQ\nm/8AhV7NoY9TbmZ9NSlxdSxD4+I9mTYuEQl66zZaqDuTia7tFWtTslmWTuI2Qxhh\nHipW0psUnep+Q/ENNQLixY4VRkxphEJpv0wTpmsllftmObQoEYLZmxRkjxHqGMEI\n+mgW02MGwyePcXUoe1VY4BOMy6ikIy+o03Mh5wZop0avWrPZtL/aJNXGOEclBOp6\nLHHOCZS1Onayf6SRLqKQjmoxpluByCcEepSmsW3KhO3ccZjRSEvbWa7mr7kHE0jQ\n5G3qDqYztxJbIuMcB3minS9RWZ2CKpwSY5dCy6hVzuXvmNhjs/TwV0iAzTKVJsrV\nfQl5UeZ1qbLWHpiJlnV+rVbbmPhhmcqARlDBL0Y9gYuEDquUpTUOzKRb2APMyVbd\nO62hwxHiZYTOLrTZXU7b6nwDyQfEvqbKbrFcOcjA7THCXDXXG1beszAIK8ZmAEVC\nu1PuzmIycYycSJJ8mt7WUDXV2V8BuT+I01LQl7swIfsJy5OSe5jya2UFKLyrtlLF\nw3EvQiBrqqrN2RwZz5IJB4MWGulay0pp63I3KRn8R2zZddaxARkGDOOSSck5klmI\nwWOPWZPK6eirS6MLVJzyMeJbUVVNYXqtXaef+ZkhmaxNbWWvU1qxtCuq4OR3j1qL\n/wAdlYYXgzlywsdRgMQPQMl+asrXeuLbrA3OcAyNTWLgl1ePkPkPRmTJPcw3EDAJ\nxGJsbdOAKLq7uasbsj3MwpcqHXkeIvJxjPEkOwGAxA/cuU433FbNPTfbxZkD/cI7\nY3/yDOR8dvecksTjJJxLdazZt3tt9ZkvzVlbj/V0dorGTv7D9zp6YDcue4E88lj1\nnKMV/U7P0gM+bHbcSPcT56WupCEJplzvq65rRv2JwJ6X6ku7SN+OZ5qASY/R7DeF\nsAKv8f1mTVUa9aK3HKtiTVJaqxV3MhA94ldpIzg4nV3M66pXOQvAHqZdG/zNTfZZ\nx/zJKYyAEnAGTDEchai7IxuU4jNao6osUYDjdLpjKQR3EMHGcTY5Op0pYj51Hx6l\ndHbtfpNzXZwY0xkk4x3jGDUXkf5kMfqib6K9Rjn7Wx7jRkwYBSxwoJP4myo/yNG9\nR++sblP48ydB8ar3H3BeItMYmUqcMCD+ZJRwu4qQPeJ0bB1l0jvyTwfzL7zY+prb\nlVHAk9LjlYkYmvRuMtQw+NnGfRikLafUZxyhl1CZM0a1MaliB8W5EZTo1dFLFgWj\nTGOEatYW/ZYcAHBIk6ino3FAcjuD7EaYRCWxIlREJMiAT0f0pQNGjY5M85PT/TlK\n6GoH1A0whCAnVDNDCeWIwxHqdv6vq3p21KBhxkkzi5ycwGVGteWDFh6OJe282anr\nBcHI4iQZbJPkyK1PqyyMBUAW7mKNpO34gFZC0ufGP3LjTn2JMXUtfvbcyJmQLj0w\nrKrY7Zlv4/8A+v8AqQaDjvJi6ql5rzsAAPf8yBdtYMiKCJPROeSBKtSw9GXE0NeX\nJNihs95K6oqpQIvTPdYso3gGUOR3jIadXqelZvrrUcY5lqtZh26iAqwwQoxM3EvV\nXW4bfaEI7DHeXImnWazc9excJX2BjG1qAFq68O33EzEq5YAnA9y9iqrkI24e5Mhq\n/W/q9RUAMu+pNoy6KW7ZxM4l1Rm+0GMi6b/IJrVGVTjyRHh6iylrMbTkATOKXPfA\nlhpz/qEmGjUdN7GdW59SbGSzTV5bDp8cexDoEeZQ0mUK2j3IK/kR3RyODKGsyoUV\n/I/vIwZdkK9xKSoZp0D3oGYKueSTjienrevaFR1IHAwZ5OSCQQQcEQPXwi6WLUVs\neSVBP9oyBxvra7r6ATjORn1zMx0dSsQdSnHOeOf+5p+vfdT+j/6nJEB9taIV6dm/\nP4xiWT+m0XSMt+o8qDzIpu7d2lolTgxoMlWLScdpetNxjnoIUHExa1jGwzKA44Mc\nwxFsuZqVLBwc4ibUzGcLILr5mmGNlwZWPtAIJERKCWUZMrGV94DOmFwe8dW/GDKj\nt+JGNsinCWEorZEuvMlqyJA5kGOSsnx4lWQiZ1rGfGJbKn9yWGZTaAe81GarYu6Z\nXXBmveAee3uKt2sDiaZVotrRCHXJzxwJF9tbjCJt59eOYkxtGlv1H+FWWHvxA9Np\n/wD69X+wf+IyUqUpUinuFAl4Cr6a7UxYisB2yJwNbp0pfNfC+p6JuxnG+or3gYqP\numnEyUnD49zXniQVP6Esj/gSJHbtJWpW6iwKRkD+012XLs/c5KWRpckD9TGN8Wds\nnsP7RcCYZmpGbVWESyAx+cypXiaZZXyqkeIiP1B8RKglsAZlREspwZr1emJsFlQB\nVgCQPBmVlKMVbgiTVxrT7RJIEXS24Y8iNgUGAfM0V7TjvEmCtgzNjUdjT7NsRqdm\n7iZ6riPPiVZ93eYxr/UHGZVlBhmBM6RilOgiWTb+pqxmUsXgzTLLTV1tSlXbc2J6\netFrQIgwoGAJw/pKb9eW8ICf/U70AkwhAg9pyfqC8Gdac/XplTA4YODNNdgPfgzO\n4wxkAyK2yDF1v4JjQMmBQ8S4fgZgRFtxiTF04GBPqJWzwYwHEqLgGDsFBJlDaB5m\ne2zd2lQuxtzEyqsVzjyMQMiBIZgeCZJYscscmVhIpiMVbImpWDDImMS6MQeIGqVg\nrAiWA4g1CsQZYNkSpEXuKmTF0/MO5i1bdLg4lS0wCVuYCpj5xI3gTNc7WWFF5zgD\nEqOl9ErxVZYR9xwP+J1InS1dDTpX6HP7joEwkSYETPqq9yzRKuMrA5doTqDTlBtI\n7+Zy+kesax3BInZ1K1pZ1WblR29zmWIvUNouXOc4mJxq9IDESwtYeYxmpP8AVzlj\n/k/Mcq0u1dpsRcDlcy6Yz9Z5DWMcZjq2rt6oZ1Qlsgn1C1q63qVWD7TkkRpjPk5x\njmBZsecTaBStpt6qHI7ZiaxXdpwpsVGU+eJNMZ3DKcMCD+Za6pqiobB3DcCI7UdO\n247bVAVQM+5Jau/TrW7hXr4BPkS6YU9SfxVtQsTu2sDGadETTNfYu/BwAYUmpFem\n1wVfkMPBl6jU1D6c2Ac5DHzJaQWaVG1FW3hLBuIHiWsrquWxUQI1fYjzJbUVpfSA\nwYKpBMHeqiu1hYrtYeAJOrxVaqkWutlybfPqJs0zIXIIKrNCPVatNhsVTX3Bkdeq\n17ay20N2YxtOMpWxFDEEA9jAO4GcnE0WtUa1pNg+IGGld9TUil7MbTkMBLqYULHP\nbmV3Fj7MdQ6ae3O8MDwcSP6NViutm75dsdhLphQ3gnAOR3k4sLBTuye2ZsL01l7O\nop3eAeZWuyq1a2ZwrJ7k2mQhdNa55GBnGSZ1dBoUpHUPyf2fEVpnrsbHf5GdMDAl\nltSpEmEJpEwhCASJMIGHWVZBnCvTaxnpb1ys4urqy+B5MDnwjb6HocLYOSMiA09p\noNoXKDvzAXCXppe5itYBOM4zKFSG2kYPbEAhNDaG9ay5UcDOM8ytOltuXci8Zxkm\nTYuEQjDU4t6RGHzjEi2p6X2WDBlRSEY9FiVLYw+LdjmTTp7LgemM4/MaFQllRmcK\noyxOMRtukuqTeyjHnB7SaESYz+PZ0OsFynvMimiy8kVrkj8yikJO07tuOc4xLW0v\nS22xcEjMCkI6rSW2puVRj8nvKpRZZYUVfkO/4k2LhcBGXaeyggOO/bEdXorsBivH\nfGeY2GN302rgGdaZdFXsrE1yoiTCEAhCEAhCECrjKzla2s4yBmdY9pi1QIBwcSUc\n2yt7dGqFf6tZyB5Kxeh3JftcEIwKtmLvLpZkMQfYMU9tj/cxMmVeLtVbp78YIYHg\nia9VUx1iW7fidu4j3MXWsxjef7yBY4xhjwc94ynHUqJOvtznbs/4iGyNJp9ueX8R\nT6+16ymFGeCRK0a2ylNgwR4z4mfNXYb9RRjqSyjsByPcLksv0ddhUl0O0/kTK19j\nMWLHLd5AtcHIc/3msqcatKrW020P6yufBiqurprg21hg8xRscnO45km6wjBckRlO\nN9dHT1obgo2SPxCvc9OpByfkZgS6xHVgxO3tkxt2tstXbgKPOPMmXV2GaQNtspfj\nevxDdsylVN1OqrGCDkHI9RDWu2NzE47SevZjG8y5U4dq62a6y5FyhY8iNsqe7R0+\nbVzkHviY0tdAQrEA9xAW2B9wchvYMZTjoEsr6QDIB7iXfAXUlfu/H6mOrXWVptwG\nx2J8SlWqsrsZ+G3dwfMz5rWxtUb9Ppt/J3ef+ZqpRv5hbnGZyn1Nl1idl29gJ2tG\n7OAWUZ9x5p6bFAA4kwhOjAhCEAhCEAhCEAmTUcgzXEWJ8xntA4mrTmYSJ3rcaiq5\nCoBXtEBlGoGl2LsKzPprHIhOjWo0ulewAF92Of3F68Bba7AoG4ZIj1qYxQmvXKCK\nrVAAdece4+krRRQAgJsPOZbSRzYTTqwKtYSgA7HAltcquK70GFcYOPBjRkhNZPW0\nGcDNRA49S+nKUaXrFA7E45i0kYYTdrQqPVaigZ5I8ReuVT0rUXAsTJx2zEumMsJs\nqC26GxQg314IPkjPMpoXVNSquAVf4nMajNCNtQ0ahkPOxo7VqLUTU1qArcMB4aNG\nSE6iEUmmoIuHHORKLWNMbnCg4PAPqT015ZdKm6yei0qba5iqrWw12qoBYczpqMKB\nLLqWYmEISoIQhAIQhAIQhAJBGZMIGHWY09Lsiks05/8AJ04sF53dQDG2dx1DqVYZ\nBnF+o/TjXmyrlfI9TPmLpNepqsqau/IGcgiRdqarbftOwLtHExQjzDWqnVJXV07K\nRYAeM+I2nUUuiC0lTWcjEwQl8w1ru1FNtjM1ZOTwcyE1SitqnrBrPYejMsIyGtVO\npWqxtqDYwwQfMZXqKHrauxdik5GJhhF+Ya23ammxwChZF7eJWrVJsau2vdXnIA/y\nzJCPMNak1QpdTSmFHfPmQ9tAJauohj2BPAmaEZDWt9TVeoN6HqAfcvn9wTVIEatq\nx0z69+JkhHmGuimoos2WWNtdPEiq9NQbEsO3ceDOeBmdLQaUsQcSeYvp1NMq7VC/\naomqUqrFa4EvLJjIhCEoIQhAIQhAIQhAIQhAJBAIIPYyYQOTqvpIa3fWcKe49TkP\nWyWmsjkHE9bOfr9D1nS6sfNTkj2JBzm0CBXAsJsVckYmfT0C8WAHDquQPc6b1sl1\n9rcK1eBOfSH011dhHBmZWrCKazbaqDjM026NVpd633FDhhG109P6lx9pyVltjDT6\nkEcs/H5i/XSRjegDTLcrZ5ww9GGnoF6WfLDKMgY7zRpaz/V01nBdcrz5iaBbRZ1S\nCAvfPmXUwisA2KGOATgmX1VPQvavuB2P4jdTpj1d1Q3VvyMeIyxDqdOuP8arhh7E\numEHTj+GL1bJ3YYeoaWgahyhba2Mr+THaQbd9N3xW0cZ9xa1Xaa4WFcbDnOe8mmM\n+MNhuMHBjtRR0rAFO5WAKn3Nlv0+zU6g2046dnyyT2nVo0iVVVqwDsgwCRL+jnaT\n6XuSt3JGRkidaqpal2oMS8JUEIQgEIQgEIQgEIQgEIQgEIQgEIQgEIQgZdboxqq8\nByjeMHg/ucHU1aihtl27Hg5yDPUSrIrqVZQQfBgeT6j5B3tkdue0s11jABnYgfmd\nnU/R6ny1LdM+jyJytRo79Of6iHb/AKhyIwJLsW3Fjn3BrHYYZif2ZWEC4tsUYDsB\n6zIDsDuDEH3LU6e29ttSFv8AwJ1tN9GUYbUPk/6V7QOVXXdqbMIGdp2NN9L+IOqc\nuR2XPAnQrqSpdtaBR+BLwIVQqhVAAHYCTCEAhCEAhCEAhCEAhCEAhCED/9mIkwQT\nEwgAOxYhBKoLxoJLOXu6mXduFX7Y2Cs3GSaIBQJoT/+kAhsDBQsJCAcCAiICBhUK\nCQgLAgQWAgMBAh4HAheAAAoJEH7Y2Cs3GSaIQIkA/iXXiurCqVHwJrHq1oUUV78L\nQpouD72BSyWDcukJdiB+AP9HtqU+152mqCkPuo72AsXFRmHQQFO8BkKCjzUNACRm\n6LhWBGhP+5gSCCqGSM49AwEHAgMEQWJGgsdQkVz3cVsjJOh+nn4TW30T48YT3Hxe\ndJfRlOpNoUP9f6bvj5Q+34CcP0bNFQUW3qw0puvkt3x19aKAogMBCAeIbQQYEwgA\nCQUCaE/7mAIbDAAWCRB+2NgrNxkmiAsaVFJFWk9SLUdQRxy/AP4zn19UMMggM+FV\naRe30XCkcMME4hJrDeGvZr8jKR+/3wD/bEBhEzioZGDpV8A+i1VfcE9OUUdRF8Hd\nKqMDD6LbkUY=\n=uJCT\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "pubkeys/kyuupichan.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v1\n\nmQGiBD7CrZkRBADjXAk5ONxSGO01JMqUjwQFZAwGynWcEVF093g6SezPXsMN/HEP\nPoxndqVhtOOI5M7ZezusYzUEH4RJNEDc/JmrQh9d6Q+CanaLuOn3c84XvgKVAmID\nogMrxeQ7biCn9NyZf6EjqP4QGBwWQdsrb2CvK+NO8h2oLo2Aycv+hciDlwCgoLPl\nscZLBvfwNRdcajG45g5NfmMEANqZLvax6e5Wr9lHpfZFQch3ai5hP5dDetA0jeR4\nnzeWdbwD4Aph3AXrZt4c64qwkSi3ElS5UKzNRsO6APF3+COR1VXJHyN5ve6jxpuY\n69zq9ZM3KwYHztoKfHlXJqTtOx9U0DSK/xwphI+Q1neMBr3RyL8muKQvlESNuN+3\nn47+A/9+5pWs8m060jRcs7W1zAYOBCv5fb3Zeot6TgkprDrrRFitcyXbB6k1zyMh\niqUMce5Ht785AWYZuCTjvbxJ2yuT37s26OswyXDoBGwwcPgaFyEO3SBRGaoxvTrW\nNQ+gpiW8ILjXCBoJdgPcOlxjj+mhRp3OQ1TJZojMBgio56IWbbQtTmVpbCBCb290\naCAoYWtpaGFiYXJhKSA8bmVpbEBkYWlrb2t1eWEuY28udWs+iFkEExECABkFAj7C\nrZkECwcDAgMVAgMDFgIBAh4BAheAAAoJEHV+VfRE0xInLkgAn0c/8n77dUl71F7q\naMolNZ8KmxZcAJ93ESJQ6UWhnyUxHm8l4OFdXXu4RohGBBMRAgAGBQI+0it6AAoJ\nEMXAxcchjRjXF7MAoM88e9oNMPdUpeu/hUMmZw6AL+AqAKCPgRmICbdH2fQYR99E\nLyw1wGwii4hGBBMRAgAGBQI+0jp/AAoJEDiaVjzCcqEmYpoAn1kwGm12ovyG4PwS\n5rN+Z46FE5wUAJ9tQkDrxF4yhTkCU6KVOje/tufBdohGBBMRAgAGBQI+0la2AAoJ\nELfOmxk3oYfG1yoAoIhyQvVnzA3AUPmpJZP/sfCqzRrNAJ9LmVN+b63BfgyXkJbu\nK0w792EDS4hGBBIRAgAGBQI+0pzOAAoJECIYyB6OfAP/1G0An0XtPhXIf+Z8VHrb\nu9d+e7tJodQ1AJ4v1rH9v/NopQtHdcnxFvI9alEPH4hGBBMRAgAGBQI+06DGAAoJ\nEC4s9nt3lqYLLuoAnRm+RnRj3RJJxE42yTzLP7GslzazAKDO3CHEtgPbj8DseXKS\n2jiwi8g9RYhGBBMRAgAGBQI+1BTBAAoJEElFpTfXe0P7LQcAn23wIm2Pg6nO+dBO\n8oBrmARV0+ImAKCcO5k/5ByeqUMHy7lKx3HVzOET8YhGBBIRAgAGBQI+1K4rAAoJ\nENGVGa1MfyvuLTIAoITJh0RbLqqsoyKMIPA3DWWs8iUOAJ4g4HO/rL4foavPOyzn\nBDaHoDBJ1IhGBBIRAgAGBQI+1Y1MAAoJEFC7KXQtWafSrP8An17bXzC3iyywvnC1\nW7RfvjohMzzQAJ9v6BJn9xRORSIHY4ifqfU6BPtVUIhGBBMRAgAGBQI+1M8FAAoJ\nEEXlkGj5G7efrxoAoIKXAOjFCrxhlNSIFUpDkgtxaPfyAJ9LYM88b7Jpz/octbEH\n6o6lx8B5lIhGBBMRAgAGBQI+1lmrAAoJEFI0hF3yuSD1WDgAn0CjIP3eHGFoNTMF\nBTefl5KQWFjlAJ0bt40eHF2hp46HKS6Bbl2p09FFyohGBBMRAgAGBQI+1mfTAAoJ\nEG4Dj17go4N34MMAn0GSwAMasmCfBUhFLRxy4uYSy2mhAKDZ2uklcaBauLQyjFoL\nM1NMhMCJ/ohGBBMRAgAGBQI+1r14AAoJECTxPj/mjACSpH8AoIxJsUJr3iUoYCzZ\n0220/CbBO9kyAJ49dc3iiBArycmnGjbkSLnktpiqwYhGBBIRAgAGBQI+1itWAAoJ\nECn45GVniJZfNQoAoIfjMXEsjsnaHI83sNdrBmN4knQDAJ9NAsEIxzaGFfQmc33E\nMhxRpo2cE4hGBBMRAgAGBQI+2BXeAAoJEFlRJ0yBj+NA09QAoLN99g2hp7h7onqW\nMIc4T9WYWKgiAKCD/4r1f7vrqR/sSYKHrahI5PbysYhGBBARAgAGBQI+3GFvAAoJ\nEGcvIifCwHAobfQAn01wzHEdfIeWy1X44PF3EDA1izBAAJ9oLQ3ES3Tv7R1rPPlo\nrSCPocO324hGBBARAgAGBQI+4mQfAAoJEHFzfab4xNFPBgUAoOk8J3Xe3ko2SPjM\nd84JfXbIijW+AKD5b7NfeVAkz9mNhYePqU53SpQvQYhGBBMRAgAGBQI+2U6qAAoJ\nEFHGMyB5fcdf47MAnAprssI6tke7MQuiqf+xOPO2XxS7AKDOn8YOQDEJtlm8qRW/\nSGG0DJ5MBIhGBBMRAgAGBQI+44T5AAoJEN5HUcxjjSIaSpcAn0pWC2xioUmTdjnJ\nOVJPV6m3h7TYAKCm3tIRQ/n1e3+SjrZ6B6u9arKRiYhGBBMRAgAGBQI/ASS3AAoJ\nEDC3rnBH3fqFUAoAnA5k7w/Y6FCkgSBDqjC8Mz9e+DQ7AJ4npy3wXIW6gk9UD484\n6CFTk7rwHIhGBBMRAgAGBQI/ATFzAAoJEF1s/WZ+hdAzbf4AoMjm6YMw4TfxTASb\n85EfShS6LdH5AJ4zDq7QAvGkPjTY2JLlwHph3afyHIhGBBMRAgAGBQI/K8oEAAoJ\nEHx6uUUZG8DsJYQAn0VwLcYxa3f7Ovk6m+xJUzcpcMMIAJ9F0JRKAS2w//tkMPab\n8YZ+sYECv7QhTmVpbCBCb290aCA8a3l1dXBpY2hhbkBnbWFpbC5jb20+iGIEExEC\nACIFAk/485MCGyMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEHV+VfRE0xIn\nmTYAnid43IhUSJp4f35yw7V48uWI/PHiAJ9O0+8kcnVukXQOLpKgQekH8GIevbkB\nDQQ+wq2lEAQAlZjNXxfrUYxJ8ewd0LIGmig3HSKjREHc7vuN76P56KzH7pdEENaa\nrhY7XggGQ826MyFgkpRp7LLNR8LNJb6JR4tPeeUFpS6Xyz2tj3UHIkLTbLeub3em\nW3/oinoO3bbzuZ2hVx2GyR2yVlM5hbjUFayne0D5KfSHzCEJ5SLMn0cABAsD/i6c\n62u9tAkMRsrWAjcd8z5PLf+Gp5MuviE396gP0CCdcwo3K7RAYcUyiJRObv/zktbN\ncE+ZwYclB5zJT7kiJlaDmMfwyHBDYond4bV5VKeveGcr8Xy/cEh+cN7CceWoPC8P\nwjcnylxM5UZHMrTBlaoLqKvNq/JZk1sDAau/9g9kiEYEGBECAAYFAj7CraUACgkQ\ndX5V9ETTEidutgCdERuNxOlRNFORwpSVJcvOgeItgMsAn3RblJxgwKC8S+z1NpV/\nq9K8C944\n=MRTd\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "pubkeys/sombernight.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFq/xaYBEADLjN4eABFUTJH0OGRE6mtfPz4J2WYU8B3VI0KSKnqSNwGKm6MP\ndB394HIxpxO2aKW2IbxULQvbsaMYmqYYXzndlGgQIcqX6FSNfoOfmsTW9zzLI7CU\nEzsh4lvV5PIxCMaJdG1qwM4vEEQh2AZkqdGLCNjS1nDywDFUqC11R+XmbcpDh6pn\n/T8f7oWBEBpfYKCPWSy35oRIxse9o6pc84JxGRv57eVMofApkZkhDVf+r+Z3mjbj\niDVmRo/cGTaEPod3332z5dh+Ee/7Tg8RiVpqrJXkDlL+VhHH3ARECw/PeQEVd7B7\nRxv9Ss/At2Js0Ah6cQSc67+S2Fuk/c7CahItAEJO9Us8T5LhhBNkc8QD9VoFkHBz\nT7S6Xhvq2MbRdbpIWLg32IqilARYUOSDOfa1GR//5lRW6Z9XX9GEjkKNAHbeQVTQ\nkwTVjN34cHMv03PhNy3a3FVaHzcIW+NTS5d7yuyinmTR+a3DTH3Uoo2d1TEItb6J\nNJsMaq4mC7XoXSg0JflqNU3sCjC22V/fFdGbdtLGJmv1pDX8g10XZ0REIZUX3vfl\nzsUsS3P0MD8aHst6nQzsDiUSjouuMdyO66rSKUZSM54rUH4DIj9EKq+yA8qfq6jJ\nUUvOo4AXpBsdQmNBj5FLksFKIEatC0ov+/U/Nj1G884UkWXJv9GqmcGvjQARAQAB\ntClTb21iZXJOaWdodCA8c29tYmVyLm5pZ2h0QHByb3Rvbm1haWwuY29tPokCTgQT\nAQgAOBYhBErWQznfoF4gs/atUee3SM2vXl7ZBQJav8WmAhsDBQsJCAcCBhUKCQgL\nAgQWAgMBAh4BAheAAAoJEOe3SM2vXl7ZPp0P/3yPNOgatCImwQq3eoXBNOonXazI\n7y4E/mcCNz0PJQmpxrzrbM/JEKx0ARXQuRw/ai22+tPM9XNkD1ewySs6iNMmeT4T\nYDQajNCI7V+aXlvLdbRnSwHaXC1gT/LA65vraTRuzkiCGkPFEmvUFO8ax1pdMwz2\njxpXLRo04NtQf1BHAi3iM0BAd0+MY5xd7Dcqzdp96Kg/KY89g09UscQ7O6HKfM5k\nDQyOKfUgGJJ/rbON7OuFEJCrAbIPp+xOysIz+7ay602lpPnbNnmjgDcg3V4U3O7T\nvZQqXD7AHUUA03PDUUtAfpgNzSFqbh0T3UDjQI/Yv/LOQmHqvD6XxEiIb42Ttj0C\nhmWbamXV/Q2Fkt5QSvmXeTAEI8R9IW/Ui8+WvnHwxck51P5PnH/U2raBgU8nYpCv\nL78m9GFsScgrf6MMWRfh4L53E7eS3Mb4YTUgIgCdb5PYlDLq9zlyQiDFf9tW9V6A\nb7db/BVUWQ4wu9lulumnbooUY98DaodWL7TmAbOgqmMPX0KBOhf1vNXIuF8puIoK\njowV9FIYD0muVICHY4U0uMO9hqnvdRjQXssTY1u4io3bfG4l49wnCNup2rRMS172\nuef+xIl5rle0MsGBlcjf48Fuxi1FnFNzeRrVP8s8NEof4/men+3a0bnB0GywHtSr\nD9HurGkl0mOJOt/piQEiBBABAgAMBQJav8wABQMAEnUAAAoJEJcQuJvKV618UgUI\nAKi/tyFRSXqef3qUpdLG+2l6TTw8TMejQGtifKAKZ/sBWtIY3UruKVaKlHqb9vXR\n/ZFMezQZoW+Z4mA79NS+T6yIH8AnaN6lIIbXEQ8lzJHV744Wp+vWlluqLD30534m\nbfGRFmPj8jCCV5k/GHsSUL3GTFlfxOOI8wQu/xENqRX6HjhH1pKOQfYqOBbxhaOv\nIrRYlL3Ia6CVJpHzJbQVKOwu8reij8MZ5hE1xr+Dj9i7GGYVlz2ZPRfOIzM7mIN8\nv8nMXsrb7sEMNga9tDD4/YV93rDodBk2bEk+yVR11/uX8BC1bpHoAncd7qbd42Ie\nE1ZZ2scYrMPz7YBkTEh6YumJASIEEAECAAwFAlsFV+UFAwASdQAACgkQlxC4m8pX\nrXxqTggAh+DkMbATIUrmoo5GD4iU2KLCfJFVe4YFGgUXIctjdE9TvPTPAPA1Vbeb\nIhctTEbgB4B/7IFyYrq1xUSRU99foWOEqgONbVdtNr97z9a0i3anF03zouIb0weU\nb0LS2jVdieU32RtDsF/bDr7ZbwNEihKVDgbQ+6VRnc5M2aHDZeC/Kd6EmUBacaUm\nK7cgfGpgBW6lpWv1cUBKVvrcEVTDngi2W7Qd6e96NO+Ljov1q2u9dU3LZe86oQEJ\nW/zDFnPTyl94Jrgr14/zzspjL5Pyw/1jZe65g5VbbjQiXbiL8ahQxlFFJlFLFzso\n+uAf6XC36lVh4gbYZD2v3ljerNrnGIkBIgQQAQIADAUCW10DuAUDABJ1AAAKCRCX\nELibyletfM9vB/95y51Tzt8vn0By4yIxQb3IIeFdHoVfhijt41OQXRjKIPS9vklb\nO5+sboF9qpZuP5cnzNhRdIorGXgM9l/iuYW6pcr16UJn+Z3rSJOb7z2fyjwUn+lf\nLwYd10JbRahq8VQP9rK2cCNXcovZr6svKqRR7vKAapRQgV8+MXf1Id+KeaPgBOo8\nHi3ahaPyvyBDxBxQ9zpBjoL0SnGjaqbG2YG73F3Q9hhWpEGtyfvejdwQlFvFrSVp\nJ8FqkMhlavAlL3G45wIZ8y3BpgWsryRuH5t0F8RYXUimFNCmKVVtF012m6iilZM1\niuk7WcPjMP3uz37/dhhgvxLLd7XcnxgGDB34iQEiBBABAgAMBQJbbs/uBQMAEnUA\nAAoJEJcQuJvKV618dIkH+gL5ye20ZiiVHrrImrXOc6sq8dhpPMMlogsoUaWY4BZ1\n2cbB9LmOBbsdLNuSRXTut2CwPb2nLT5C05We+B8A7KmYxlYmYYvhN8OwP0O14JxB\na8w5esY/CFt1i2lLWQDJB/n9YyaFbk/bJ/vLhknHyYBvSORRhs8aRbB8TPCRu2r3\nzyGKBPtovAEeFRz7zcZ08Jq5BO526RT5VTdh0RYlwPipkOtampRSjpioSu75MV2F\nD0HPp/WmfNBCD1mvclo6S4X4xK6tG/mfIVXJIW1zwfamrUF/r9wGCWa3MA04ayZK\ntqrO3Nncw3XSHmSZILcQYcyOpTsZDOBtGKqw/a40CNGJAnAEEgEIAFoWIQSThqL7\nLanQ0x+vCBjAwHYTL/p2lQUCXNXHsQWDB4TOADYaaHR0cHM6Ly9qb25hdGhhbmNy\nb3NzLmNvbS9DMEMwNzYxMzJGRkE3Njk1LnBvbGljeS50eHQACgkQwMB2Ey/6dpV5\nNBAAkqNHMkuX5IIWe4aMqciFJkQDS9svkCwpruv+FVG/onBMbASYY9kdGNxwZyS5\nLvBZzHC0KBLCXv0oezFKvB/d75HoX/CXeHxo21ODNHpPlyRagB6n581XWGEVuhDW\nn/isJHeJACdStL62p9tgXVYDvY214ACUANuUKmi0rYu0GGtfWcIXcZDRFfjxrZy6\ngUjI9JMwgK1zWv2Dv5aD/lqoMO8KsObgfZEjoEYs75pS24+jpgrij5+QTxZhqpIh\nM5GEJ2vlf37Zzpuy/FR1qpPYt91rdPEI8ulsZv1atXIofe1e6THQxjoA3Kirs0Bt\nhme43EL92vBme89v/4kisSiQ25WSEv25SmbkjxrgDUIVdVs+1pNx00AKIDjaSpFF\nVqojK6B6kHAeyq9c7bK2yBalsWh9YZtesWZZoj6hxUFkd1NIHI0qFpbZfmjR3GAY\n6TeyBGFOBZZq0ypUfffZLexkJrNMkkwDdMmwCrbCALmwLOaMMxdq8IxUNf16kxX6\nyavlY+mwQ2a7QDP22LJLeXIHg3oTeBtbgm7m9tHDGfEgHTb+I2hTu8jDznIK+BuA\nDj60qx1LcaZzBDoFscRB33uLy3gpOv/BrsB68bSnl7DssJT88ZV77Dq0CA4FAa27\nn9J87G9oMzUggLkl3rIfeHcKWQ5gnE9AZ7/rWXw94TYylzKJAjMEEAEIAB0WIQSh\nYrP2R4Rpi+qvKcPRbfMARm4M4gUCXR8zLQAKCRDRbfMARm4M4g1FD/44o6VVD2yW\nlUHZHvXCOCe+qNQo7dImGp7IRAPHTInawNDJIUWxzas9DzD48el85fS4ADU/dWfA\nGYmIptpm6eISe4Uz0wQx2EbDee1Xn9qnlYV6J3cnzwNrHbguP3zkNtyBdYBSIwR9\nIuen0f7PbVbPsBi9GtdJHpNafCh+Vpw/6FJoh9ZFP64G+NuEPw2VeL4LIgn3UuBu\nTBumFVQR+KIV96nChQGfy2eqVE0WWd1NEvv7d0rHIRJmOr5YyVbIFaOrUbTYrYlz\nENraDuH6ALJ0uJ1I4aOMs6bIHKi9BQmklMpqEfQr2xnwR7gJxE9RRHUzZVlyTiXs\nsx2kGRb5NTGhcpAwBvocOp4/DDgwICnNCFtnfHByLCZ77OVDvl49W2Ne1G1sdpNp\nnAgRsBV7Sn/68WlvoheRR14s+EftiBAEXArwI8mX8fdqgMXAoTow7rnN9lHGb009\nL5epnx+dhhZy7WWAPkgh2ShYE/msW1kApizLpcn6xWtlPyCrzA+O/vGuEOuitOdq\nh7WM4DPSnQhXyH+MYh9MP2rZdBPYwp+EUdnzheC3TORktoAStAe9TBM3jwUAWhWA\nyfOlMU+dnzfxDXHuK/yVLsH7A1Y0ti9L9m/hoMOIL0HMWjYtuviGVHeVDpvAHbJC\n5+gIzNzuwWyXJx/zbYtbG2hDQC4U8OW2TIkCSgQQAQgANBYhBA7tz9XK+0WQZzSb\nI8qe7sQ9+RHcBQJgyf+9FhSAAAAAAA0AAHJlbUBnbnVwZy5vcmcACgkQyp7uxD35\nEdxyiw/7Bs/eN7UoBJPR4uGi398ivkAR5kfyYv/DAmSr87NM3dL5u7upYkUuJ8aT\nf6KjpkeecXF46yPMaNc+JjIpbHbtKOiFmaU2iB6mWYNdpqIYaUsGzctj2MqhfPmV\nlURS5CHAjtkGc3/M172phCB2heYtRgkCP+jdRicEAEvkOlfGlmRRAps6rzG37DUh\nenBqtwBbudFmAnhws89ndDL0AFyI6PoeymBMf/ZH2iWjQi3/3dNdZwUqnTziJMem\nuGPhrkQgoF0Bh9eDg25nXjCVae2UEtnGk+uOLuJUpqJg3fsyN1vpPUFkQb0KaqKI\nFXcVcHjMTZeca7Cg9EAlkKr6aFhrZ7k2RE65uKGdN52sUg855xBxDp68YQyomhD7\nI0u0Lcng17A47Xx0gcOvMX7lxP4+i2zeOjJQPC3Q/F6T1EbPxRTOtozala9dExDU\ncCn04MFxsPd14/N9GK+fqb65fRhzGgB06/6h/9OYmpr3Bbj38T84U2gXvo9c9hah\nS/CmoDcnpUp+5am1HhPNgOPXnKKZ7pakRp2xtStw0bEL8ESGqkolj/KFMu1+dlnB\nT8l4llRsBfLusqgJUX3ouSL9kKp5LbktN6XUD5NmkdaD5RcGjX5F2Fv17FVvYD0P\nHVYOK5lwNB/i93xPTuJgrco2f23W4MHyCOeDdO4kUGsjlt8OPE6JAjMEEAEKAB0W\nIQRmlNjee+juVjG+2VAr1YJLf5Rw5gUCYMn/fgAKCRAr1YJLf5Rw5iHdD/9IqUwM\nnmw5AtDhKRimJmEzCyftlWEd+gjWqntccBaVnPaR976Yt5Z1U/YihJq2Tt/glW/1\nElhzn2YMKYYHDiR/pp1MHgdv6ondWoJV5yzjRAhlg5kcZQC+3xtrf/bOHTHW0B9M\nlF59i70n11j4jiSAzBEpV9Fh4P3MJxtU7REJ5vLj08Je4BHrZ64Dv/nX6G3t8JGB\nIaoz3nqluAb3jLFiGLiZ8K8LHMjXemY5o4ZO0e/fVSnklg2BM2TAcE9mdOpZq6pc\nl7a+fo8xFkR4bO/0EGJ6Y3iU8w/+2BCwKaVQGAgyAvEwzytjqh8kC1H4Vb28zT1z\n+tgdN2TD8rPnHalNz6LyHrSQ3PHPSC/iLh+4aZ6qOCLnIe30sCaPgOLS2kbcjK+s\nLca50TPpzTvxKAi4vKCD7fVE0rm5xiRDxHlKU68KCli29bLz8akqMNHiw6oaNfpD\n4m2NUFke0oQySPtwTHx25uoXHHYjtQ3Cm7dmK/eHyGmBZPxfT+yAqDhck/t3TV5m\n+o0XzE32Idx7M6QSnVAhbLkhq0fyRlECeYqQfMjSY6EUTbC9d4EsuzOawqywS0g0\nvuJgOXMEe6wqlagjQUqHMRlE1rP29ovJJS5FCC7JoMP3So1Iv9d4hUvtKzXdPJHt\nJkvXGjXmZ5Xhf3ug+ZwDjQOgCtQDMeD5KAK1s7kCDQRav8WmARAArNUpnNdDSVNe\n34H1j7A+IF8aVK408UR/6OAwSLCNYUb8bjukOpR6NMei+D8vpYDYxZbmxL019xKy\nroW6ydJFigcfUGN6v4eUgYmnXTabWZ28yNX/YLAbtW/96bPOC7LXncYWmaw/vRoa\nqeja9eKXZnX+MrHx5IRsehb+fmckar0TennRnJEPLRlAeVmCmhTT7SpsxBrCjgMp\nOB8MQbA8u37z6lHh0XZPljGiZ/qTY03qXbG4msiXnu2lqc8GTAL3+ostmnDD+V5f\nWDgaRDHt/uSJzUWMI8OlJgZchOSRPbneUdeLLe20kZPbM/CC8rG6S2yP1Zvm+lzn\nk0f2KcoKc9cpsWbfiY5+YUaVnZQmfjAbc5CBswO19SckWjQv62GODtIMNetrs0ey\nGayWnT5i8F9W22fQLfdsUJRzbqFRSuRx6YCxmCmEQ4nppgyTYDd1wWPKW3/AgPiM\ndWGl2F2RuoGED/YEkvqj1+m20Tv6gGIzmKFIKWQCmlGTnWguKz4R6/bwoyWW9ewN\nrsdFKSunoAVhofJTamc4CZO87E3r6gQkZLW8p8K7g7LrhNDcKzslc/iyDAXyl3ku\nhg+w7uF9vAYw3rZ8SKXXty4DQvPrHAUcEh0kV9JsmO/YCZns//bbRtRb2M2HZWiX\n0Kb0l9NCwBjjeYVBrjIwt13cXd298IUAEQEAAYkCNgQYAQgAIBYhBErWQznfoF4g\ns/atUee3SM2vXl7ZBQJav8WmAhsMAAoJEOe3SM2vXl7ZRHoQAJHWqiid4bmicvyw\nGGNVbtNG4zHkXTYUkn3Qopwj5s5SmpIcVYAUuaO0yOpUjMW7P4x9p6voq0Kcs7tE\nC0I3FO9nk3+p4bREfapD8RM8ABISfpoIBEO1cMbrtPUY1pOjlAhiDqKkPXBv+m2R\nGoPTIrhj31opzLS7Ex51zXRckNd814YxZCgfCfU8CU4ZBNu3gk5Bsf9UQLz80Os8\nMB3Z/DaggLaCLShNJshjikp4U63kXRQiQPNy1HexPK8te4fujRrNFMh29WiCi443\noO2LNB/+LaqL/n+0Jx7wmSxHNR/neU0MGPiAsy22/v1UG1N/kXl7mafLk3kmZpN2\nSl1m0PCtA3V8AyawgUM0Pt8h1b/u7ADXXJN5igkbFwYeIM1Wp1ZxLOV4vS6E4dlO\n9ux/oe7vjePcv+MkyqWV/tbis8fHc6MsLh0KBTEDeCDanKVhI6V1AYTgjUkktb0t\nrl/zI2+Nc6Jzfo6scodk87lo4ZGqAucnAO88TJRJzgIKUoM3Rr/2BKBptKOhBnXa\nQ8DvepMjjmicxgijZzXL0rO8ZVOsNt8Uw3OOBIZ+JIFJLpZiz2IjLQqoWvLdRlaL\n4/n15r5xU9w6tw3anvNslo+KMm28p4idPnWKdOO8lPHTricAHt2AViSWHMK6bI6B\nWXhz5mSf1GX6Hd4u1y0lHqjEi9UOuQINBFq/xyQBEACn7dkYvJ+3aQKUsk1jL01N\nM5RmptPgWiQCqnPf2FF23XQZxX3xPJZL59o4d/astdDk4wwWUl0Sey9TgMNA0xjW\npSZpOERsW6lnNr2oG6pp/B5x9AAshGyy8OdJWZxFq5Ak59UKQWIaPH29h5FPtWBb\nJyod6Ho24dDvA0GLtNbU9vVfWpfbF91k9dMh7KbkPZGFYmcIvPbXk/LFnrKnBSas\nooG4xj9hGpxr1UQATE5zJgVuzy6OiOowcIfOJ1lg9CiYKngmhjoOCxeWnljtPEVA\nOMLjXW+tYSBdCXyxHaJxFv99ZCmftyfwRs0mAm98AfsQfb3nsVullqvaqXfb0ag1\nlABC/r400qAR3ON/UGbRf65XJr27dkGyGuer/DMjYcdndRbjdkp23nji6z4qOi5F\nEtoU8O8ouGgVr6JUUF7rX555sSSFYG0pda+tG1G2B2dtadS7tvWiglgT+z07Ue0+\nRE1q/q/qnSdYkItv+OE6IzdOtpDoPSdnHA1FqKmUKOKdxeU/zuVo2OGaSKLJRhx6\nmYzhfYuPWCyZtaYpO3tMi+PiJUuHM2PtgI8fr20AKc3wPkbG+diBudZOPeKkHbfq\nLW0AhMkGRt/3V8zFYj0a6XbMh/Ry96WGhUTvbMdFE2WfnK1S8I2UHDwznFQjHn7G\nw6lunzGfF19rZtrbsYtl8QARAQABiQRsBBgBCAAgFiEEStZDOd+gXiCz9q1R57dI\nza9eXtkFAlq/xyQCGwICQAkQ57dIza9eXtnBdCAEGQEIAB0WIQRteiEW2pCeAKwh\nEIuzO18jLGJx6QUCWr/HJAAKCRCzO18jLGJx6QX1D/9K9lwWGjBh9BIGBc2XVDUJ\nlDCVqkOYjbGa50KEKaDfDi5EsyEt6agdp7o5ZqKv2RgbCELUZec7AruqJgukYqMK\nV4269wSsJmzohRbcVGgtpVpEMAzDkVey/Dzf6gebl25cuIIVIggB0iB9xaueQQYb\nw7PobCBn1yAZFQqxq0O0ZqnNSU0SsyCZMlyWYcfHAvBsP6q4uIXWJlccaTpoO18y\n0uyw8VrdcGJGQe7oZkyKjjPQVBVpbhdIAQwFY71yHuZ04YzcAxDzI8QvGZZAHGu6\ny4JJjWEwxeMSNTwMGV54F24dmuymFwOgCUTbBjxwiJmCXjUrnIEJfk3772qnBIKy\n44rey6J3OoI8sJTLt5jT/hSituY0egIg+Xmg5kIgv7O/zUNMoYUZ31sHvuCBUQbN\nP6wM2+uZ66/AZjB7xXtMD2BrGHg/X6ovA3lRKpJ67bsCGtfDQeDKbe8tQFREss9X\nFfoc9qkmFiw/A9HIWXIkthWgGnhjiThgh6qJY8Jz5rWeVN+8r84kO+8LM0VmHkrb\npyy9mcGuiu2ZRq7aTjMiD8MwmfgVbC95DUc0qbIIptBYJ6EKvapr5A4bpH4U79PE\nVEKxY1ptvtuWRFXVA0E6FUanqDm+slAvNzaQD2QRlGUch6la5ynE4Gw4+45BHn+B\nwz1GFDzQe5gZj6R+QFwKJOpKEACvXwql8OZIwGeDgNfZYlWvVpNnee0l4JjwXRP2\ncQ3+s89nZH/hwQHyhtqjIoBhjNoW1G6MnL4i8p8MT232pqsZu8Rq7L3t1TpxuETI\nIap/DAOjpYm/DHSR2Xwh82pgnsQmI22jHVns6CJ0fTCY4drFGZfi3k+efLcVHnMJ\n8Cvxt/l76/3g4tZIFXffo+FeFm22gLMKllNXnlk1XzE/LwAPSxOk4nn3DUWNnIYo\n6a2PMGokWsXL4Q0ldbST59Jv1ks5GWAlHdvEjZDctWj4LoeXa9bcD0GCpfBP1xg9\nb7kumrWJLRa4bPcHf6NlIWRdgVgWk0+aLy7V7Ei0qXffWrG7yDlHhEJF5CGYvSh/\n6QwH3SwqODaCf8DZ1U5GNUrYhsfLd+0UKKS7lKKorVCz1T9lrCwHF+4mkTZvExZJ\nn2pKvkR/rbbBN8oR4mpIE7gwfTgkUPlySKKaD7gpp3UeJ6Dks6Bq+idVX3G9UyCz\nFA2l+g8VhRNqQ3RpsMwrEDVvpHaDEi++nnQz0nu38Geo+ozXjxDbQKCbUbUArzPw\n37ZxfbaFhybK3bEsTB9ZqiNUWnnXz7oZPyGbCCNE5QYLc5YzoNqr8X+N6P+wjv4x\n3jS8jgGfy3iB0QCThvwkqoSGVP+fK+VLnKd6Ciycsgw2Wo7T0SXTClEdMmjFv3nX\n8Y8wzw==\n=/wYk\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "pubkeys/sombernight_releasekey.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGDI71QBEACpSQg2/ZronvXY/Jhe9N2vfasExcVGEfBX0P6IqUGy/WTERmxr\nHN+D4wRXqUtVCE2phLfDmQPR7vJZBlULQg6pviraTRlYchlUvvAqNBl/crS6Ms8d\nytFvhMvBPyePfTsy4xeDz7I4my9dpndcdONPJkxzFs77nqe/2RO78ELTtyRvOW1s\nLl6b7lCinPZ65IOWwqWfIplxEWzNUMJn45Ay5Qhu+pgBn6076W06Sgc4nUSFROXn\numTF9wqtQGl/A6cd3ZKwh1Ypo4BgglUmPJHXm3AZ9jMlPPlNMdVdffBoD2JJg+DY\n2hAmlzduLNJ1Q4takn+4ZWuTvrRtLROtGZkxltxacNK4nRPz17XWe52YjpvxsODc\nQFxxjPfd4HF6qnO8Fni+cn6BrIo62s/AsLQ26/OtTLO+Tx5w1ch0uKUBHdZEfFbG\nS9vXI+So2EJxo48WlyN62B5MqC8GYZnz3/zW543E6/3uB8SIvsp3Dm7SxID8vBYi\n1IUmCT+Tm5VNVatILiluHc/LVWfMDo0WSCpw5WRwEqgkquRwJ56X1XKta4YSXqqt\naDubWgR9i/m4hdik/caQJwLuoZj0aZMB8dIoZLpG6ZbqLLivjRFqWlWbMQDKkHP6\nqTxKZRFUyfAXUkyvjlDioxATFnFOQ2keqoFpFL1P2LeiplbiIiQeaLPeSwARAQAB\ntFBTb21iZXJOaWdodC9naG9zdDQzIChFbGVjdHJ1bSBSRUxFQVNFIHNpZ25pbmcg\na2V5KSA8c29tYmVyLm5pZ2h0QHByb3Rvbm1haWwuY29tPokCTgQTAQgAOBYhBA7t\nz9XK+0WQZzSbI8qe7sQ9+RHcBQJgyPIBAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4B\nAheAAAoJEMqe7sQ9+RHcFYsP/R2boaEfx22rwVjqxKlPbLD7Ruc6D5d7i9Zy/pAd\noM5w7PKK9HQUzgRrqcvnIhA/Tq/sdpKTMrfKjRpEUKESIFi7aXkcfk8EvFq/sAIw\n5ukbQCKiZr/jNNygG2RZTI4BCs6SabRmUt4O48glUC2hBUIUsyRyQSJ6gE3iwHhb\nnepOItftoOnze+zpEgduhMi0OxQsUrnvd2XMhHd5DCb4rgIH+4Bf4QwcbDf2zdXU\nbrM6xRlL/oAV5qjytXY6g5hrYLm1tVgY9Qu0N3PyAiuXbl04P5DPwa29YVay4kCf\n4J9jOR/2+fnZVa3lrgGluLqmQL3PFxzbp5DPX1oP3K+h1RC6nllgFBaBPY9IRgi4\nHcxdqCk13E6Ev1B31HyzCFryLeiJ2DljiVRxfxcqVRI2vmbLIFQ8yGx34ONZS/kl\nQBTZQx4jZ8DKmb1A+E1aExHCgb0ON2IZrKtPqqrEkr9SM2MTuIcgzVAphUsB9E7x\nPvxNeLc/Mh0h6owilMVwnZgbaLCTBnrqzzrduv9QjNfB/cBpyZGYwmpm+FNuzCk6\nw+wyJ5A0WuEyqwKfegt66WDstZCzD+gvundIBjAJzyex6xLkSXZmgrHO9eYJxvMe\nZJkImX/Rpq3/zYsdVdoI4Zyguv5ExLzDJrEZdoyJO/NZ5IK2+ac9abndtl7sPa3C\n6AVniQJKBBABCAA0FiEEStZDOd+gXiCz9q1R57dIza9eXtkFAmDI8qwWFIAAAAAA\nDQAAcmVtQGdudXBnLm9yZwAKCRDnt0jNr15e2Yd9D/9PYFEGWyxNmtCAAvSs7UzU\nz161lgNo6Ldi5VZcieADZ5f9DAadvk8F7NE/klU8FulToV6o+eJy24Mip44cdQGv\nglw7X0mQVlUHS5bIzlXHXTFAJcBgw1RDOFOP1Zz9CnbiYAaT/yQqxxpqDt2/RmPk\nwdxpTwdhNDYqSEGe3eq52L+FxNNlx7TKN4LMA3MVnmyXB4u/cEQd2v9gGoGUMQkt\nGnxyuJO8RRZZsOpK13ZZSpb2adW58t6xqptvdPkfLtPOeyeXyKfOOJgZ54e19u09\nWS5RFtccEIzddp0E6G7SORWY/q33LOQaZpxfUnickdu1ca72ZveBCSMg+ckFnUyS\nQ36Mrj6bVx4Zu/ke4ilGFtnNYbqdd6NGFE2gqcegvpp+Wsfe20/P/gjYDVafIGQc\na78HRPNIGY2Kg7s/ap6d8fHveDZM5kKpxtNP0ljDSKsLdBqqLqExnab3aSao9L7C\nVH09fBaMeBjUzRl0yDkW6N2cCJucLWoBoCmL7LsZc37j/fpMKD/zE8P154wzWcyc\nnsdrtU13qk2Nw4S0gfpBm4nDd8mjVV/xt3Qk4A7L6YACXKf17hmaVR0U0RpvNyMi\neGDE4YSnJ5U0KXwgIBkBQCwx5FfLeatD0gxCZQaBe0ul3GZ0JmXWOzTt2xTzjmrG\n7VcQnGg4LAOaYrlV6FJHLokCMwQQAQoAHRYhBGaU2N576O5WMb7ZUCvVgkt/lHDm\nBQJgygHkAAoJECvVgkt/lHDmCzcP/3rX4ltWaAYbCH7tvgel4ti1NwAAgkmNpzUz\nny2BDTJnwaydgzCQ3+wah0wTWQBzHCoD+RLsS3EKCnfK8X4MaQ5vyNPkg8jautRX\n9dn2v+F5y3/y+8cXBfWl3HG4hHqvr9XZXWfdiTSM9XA8Tozrqigd8lPMeIsWTnuU\n44CL0H4wwT9ncA24cbLaSu26zdISVoPJbxrsNZu+iA/uwVNH7HjKrd6vY7F/l+WY\nVxl5ryjK87b2I6vzH9/oy9MNKZAtPVbtSTp5WuYKwexe+UE9ThiMty92xbnAleJL\nFlwcHViBBCqGU8g2+ZNYfh1QQRrAR3mEH73ZJNicUny2/2zX/v0zyfR3GJVpO2zn\n7zSZn6UYNOdkWCJfZIoNGdKfMOCfMFnE/Nw1+wlohpcm/2/o7zdJjyyHWk2xySBx\nBOvZisM5FOQCiZGxWsBfrFPRcQ6E3l5x/JTxLdCrCjXBZAT6FZwa6Nco7zgnY2nr\nkcFbMaOGBUJ0S3cDe2GDYOPYs9TXii4RmT5WUJsLbrCanoltMYr5m7/SFuCjFaJc\nI7adkKYEeoOi6AZgiPz0J+MZuGN+Goyhx0eoDzIG8+QPPvssPvzF4hNTua2MRe5B\n/wbNkSbv2eGgFlTPhxBDZzwA3Kk5mnF3YZfBZzA7lyLUm97IaRW+ipwmugOIgEJy\n0GZsSaso\n=zSe2\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "pubkeys/wozz.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: GPGTools - http://gpgtools.org\n\nmQINBFFI4SABEAC7U8hKaLjTBQb05k067LzsT+CTfEVvCA675fxp2413SKCLJUL5\nsfBZF+2fsXGqZA3SoA79/y83414YYBogOUxM8OFhIQknSSbukpELcbJ2f6w4GQze\nxVDak0FH58mLepu4dx27cP+txvC0JCSXFETe4yvXd0nM0MPj/bbHyhWDZ8qp11qW\np1GtOyFL7Xtkt9Wcbhx2/1pA92H1TYnnfsEqXWgH+Og294Vsjp6novaNerjJsRYD\nUQucpG6scNw7DNQgRJE/tdWvj8n9Q/RSy60ZekZlLpFd3reNN67nV0ojI25Y5zOu\nH0yz0XSFx4ZZakkP3d8oDsMdWPDycFhQjJGUTj4TWfBKo0cn4DzeoPBlFxPoyO8g\nruemw01fawlOA9YXLe6y3J50l3aZOvME0JzCGh5M32MvWmAywVlc47cjr6YA0uOS\nuNuQzO4XGIPwVbfIHUz1+9BBffgpeBA97mm60e+UtuAM50fTWx5ddiGlkERXCdps\nNCBfptvsM5b0Efa7v4fhRuAj0lOBE5KbgWaOUGLOAnGJ6ZxkCjzmCcGKPICufxLU\n2errH6xk9z1poRsJvVH3l5SsIBbFmuntJwiKCHRy/tPzdnGuFHcQLYihcrjSXaer\nsiiPcOEHjgAezUvNvAy6MnIW7Erl+zYYUS1k0QqAslul6iPZ61JW2hjwrQARAQAB\ntBtNaWNoYWVsIFdvem5pYWsgPG13QGtvaC5tcz6JAkAEEwEKACoCGy8FCQeGH4AF\nCwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAlFI4tkCGQEACgkQA4wJ9GLCT8c5XxAA\ntWNVR1pQhpsBq8J2H0xkCxfeRtuNb5gEq9fmusPW+7Dad/CL+KVIfpyfUSii1+Te\nzE9cNCb6I7xf7jGWPUrKohl5IPLHo+bYDWIL27EjoDYIW05v0RYyTHA53fsHNX37\nmWd39xyu6DZLV2PDfAqILra3VLH/luDI7kpYfvuHAr/AqFAGByKgMr/ZLLOoDcfV\nsI9k9jiyp77VHbPjvTIHan9hhTT7Ee8oCSmxTsZ/ySJcMf1ROKUeCq4hGTXchHSX\nFXQNveCKChEL862nVuCQ0betcXYKPolQG3DJVfihMaEpQYP3bkyerSTpE1TRbZTt\ndoeNvwFSvhXfzhOC8UcuwuD4ET9dS+brq/r2IvM8FrcZt8dEfReiu4NbIXGb+V0A\n/fqKatYs5cIyy7hY0U/aILUI7WEaxA1EQ4wOcLudgM6Y4hF/df17RMzofCrXrh/2\neYLVVdVMj9pfj/MHdlwJX7K9OCmzsN2e7PNjwu/wgqY//aG2tyRdhjmeI/ybwLYj\ny5sJ8GJkekCrnZbkiSgfBxfO0wNPzq+JGF95qVB4RrhMPiOzLOHyU1RUyP8cJqFr\nSt1fyHtfNa9XN++r7nRTIMFWQL5nyQ9czJEOSKnBMbADtcSs9kxlFYTAMnfpETGu\nXLAONS3wufi9qEN4vIwG/xRqc7I4f6CUPMtSwj7AzQaJASIEEAECAAwFAlF1UFQF\nAwASdQAACgkQlxC4m8pXrXyCHggAp8qekPNR3N78WrCiEVqama9iqoeHZxhzoNaB\n9ELyH2Y8PP8FAcpFSQK+OmoAOwCPBX9+b+jTXwZYhjoMI7yrSdxcUDvE0PUpO3q2\nVV5A2fKQuljSmzYuhnjUgA5ZKbj1lwPjbWpXUR6q8schnu4zDzasuz53bqYdDVlb\nqO2IBaPhss5Hc98vTIRMHp9rxbET3Ict9a6XtMYhTKye5jf4O5JUOsNPLZWZeSn1\nQGxaxbJ4ko+/NXvHN2ZdNoFyZEm4Am2v2m/tKq2JLMVF5SXUtS1c8MCEOO9byjsH\nCE/X7GHQoC3IVxR32wdkwzZ2uZcVG7Zm+RvLLzy14WSPjjRNPIkCHAQSAQIABgUC\nUYc4zwAKCRAyidZ6Uw5GxjE2EACNU7LHVUpAQFKjxKN03IM0cd/C6coHObvGmpXY\nsMFMta5iyXafmo+MGrR52kEr8xrQ5xz7Ozw2RMC83PyBN3P9nEirQT7gm+gA4m8I\n3a/jg/py7iwRh49XXY5Xh7eMAlkvy12DMsmkCHx9yQHVv+Rdt5/N+ELvgeL4aZR9\nN8/J+6Lf/MV5ZapxXyxessdOQPF7Ik/EMi8iAPZYkjXlA5qsfGONqS/hs8sJJ4sF\nrwndgqMTtsqmYbqwKnZgvSPpMj1CclbXorSVjzBn49/9zsCiv2twsVFr2kS7HnsX\na/Xu+4OLdOCVQ7w4kHkrIIuRQ9Xuz5uFNOTADvpjvwbxeu7q1+SORd+kEbPIoT17\necQoM6/POXpUv7Xcja67PCL1O8xeLLjsxco68uFCtKGSmnztOGtw95+KrLgzLC2a\nron9D6BcXSOrWwlyrxLP+0CWBnXBDRY39yfXF5RZ1tIlKPRBJEyBPJgIPoS5Fxlq\nrN+J3TzvVK8bK5QXTi7rGnSxrTM6teE88jOESBuUfT3KS7Lof3k8n+GWUOdqbf/k\nK2fb3YG49w0grQq2cBCs5gaaJLao37xQMPAb9ub0dWQMImEvwJmVn5m9ekcM2KcY\nKb+qpRPZcwJEZOPW7nyl4DbqCKpuiL8BSl5vg2T/+8BdlRIVqj8oH5Fwmbz7OR4f\n+LEchokBIgQQAQIADAUCUYeHuQUDABJ1AAAKCRCXELibyletfMv9CACcBOWrLaV7\ncXnb3NcBt9EBUchTH8J3JCI2zCE4oFNw3VSVzBA/ocLAm51llvmjdyR1A3/6RmOZ\nmF9pLKU0opOXBLjZ9paD3D05xlaOesuTNm++lPgolnMjFXBiPX3t5ulUMW998OHP\nguQYaxXIOBIGqqZ3SP8p0OMZcpZCNKwfexELmFknyLW2RcHnax5GcCIB+yDl39JU\nEbWx85jGcIwGgwSXtxINnhYYjz5zJMjGZfb4h7MdnV3fQxt3+nNVvA5ePTeS7v9E\nsQmnjYd6BFekr8crb6z7GQNrJk8DUjipe+PgA93EskHDMjiYrHAhIS+VV6MVGn5F\nMJrKjx9homCliQIcBBIBCAAGBQJRlYDaAAoJEO+dUAE5sB2JlIcP/2KY5EDXlXwo\niSe6OhGZNIyC7qU8SIz06WWAPj9xlhjyjl54sWce1yOrwv1ZeytAEsVyXEQKvNGZ\nIcTto/RJqiSag0f7J7mZDcw1s48y2EPYHDk5yV96AT5uSaRUkmMpWPMK8Key0hH6\nEMfhZp4H/ziEn9VnKgpIpS9nQ/K5a6JjDcwZiM0TsEtfaDEC1/SFtrUA6Rf+yzK3\nxLHXhbPnLRlphM/fUoXqS/Zr0ft2eDwUlhbB6dP1ZfrxCbndNq97dOnxnmgPIxZ2\nRVX1pek7XOyi+qgWC+TpJwfzGX5d91do9tPjuC6+l8tR+CGrslHW7Bd3i8AcekdX\nL74JbQr5CXUUTEEFPZuKY6iOWGoPHyEy/o8VYztuOeWcZ4dBgrvQMBFGbu0wciPe\nQ/d6mZw57KuYfBBs8JpBmzi8MfCCh37VAPBMkYMfWk0FuaelTW0eL7prjEn+4Zlg\n+iXDOWFp4OtY06aPMc+h0GXK64HBX3YJsYTS7iZNtQxSJK7Li3toREPy5kkhsYxE\n3dRTUdM5px5ekZWEGrRORsxOenQ+TStDGqfQwbCj5noUTy3WLBNwNeC7vxpb2NYI\nVkNPv/GS/29NBIkBbwZGIS5j82+SaeEo2/9ZfXiYXV2Va2lbH8OTIyhXLFXS6FQo\nBzkCFbsCyLICe5PLtwASnYKa4po+nKcWiQEcBBABAgAGBQJRrggZAAoJENtpSKdF\nkwASi9gH/3CIAuxg+oBYUDly4tM+oeFiWknDEUcroY6i2BdHq6n0O2mhvmd0+BuM\nOGHi2ilYZ9SeKazgFvGBIlD0wdpApW0EWnS6b4CNa87+ckTpBDqXTXNTyWhKuGS/\nbWnUDkAxAT5TxfX0jmTDVsjUT5XjL0acgL7TVxJZXbDFRMS4Qt8z6V2Q8JUY7wwD\nArWri5QaYF9OnMm8rj0a+XnAdLvgTIbAy3w12aq0MEiI45++r47iyL0kXxqAFpRo\nYywSthtwvbeMDlGsz0HQfNLUszxWaroAyqARBtHvUlYWXmO9Vhr8ycFuZwrZ/GLm\nF56CHTUCQUXN37qq6jN0tkBAzN8Z+EqIRgQQEQIABgUCUbytgAAKCRAD0kzvlgTT\n5Mv9AJ0dknWViyDyho0xBvdU8otET/DlIwCfZYMlpofCbtxM3k9NY0FClstdBg2I\nRgQTEQIABgUCUbywoQAKCRAoUZ/piO1lEM/sAJwO4ULlzNfIKurfByPELQq5kEzU\nuQCeJmBPZJZouZgJIZxWtRKwku+ORKqJARwEEAECAAYFAlG8tIkACgkQS6iSTApa\nKQMkSQf8DKe+KKk5H0X4r3c5FSsaU4FchP+5BhiE1zhB//US80kJGOcdxLOgY6Wx\nQID0csneIhz/RrPwmHBoamlKlBEwMJKVPHeTlEJ1tBS62DyBsfTMZdPLxts9Z3da\nPy/sKSuG1bHuDOlm6quzm3h4gEmjCvMnJmWnBGBq/rONz/r8AY2qsAvVWba9lLVw\n9Ih5wVLTtUwG7O7D/TYNRPpVNQpm0VOuNImAiAwj8A3iHtYLN0Zq1c8hrlJQ5eF4\naWaX1F/kSNIL4DL1bQMojVd/wmMp41gzI0WcqodFuqLalWC/hxMxKjJ1hP/CGUA1\n9jjJYKbZPryIaQzzlqHXe5EaZmvSP4kCIgQTAQIADAUCUbywVAWDB4YfgAAKCRC5\nnwhOJxzwubV0EACgd3vRfbH+QCDeB6y2JS6UldPahx4XGCtH24SAhqGwBNSrLKnu\nJ/8gl9UXc9xRoa5as3/dVR3wvalmQ9If0q2CzzpvhDjr+xFH3DI7oVr31EjzIrs2\nU558QBhKL0SnUre0Tzg0gSYGhBeVcaN3hc/iaYdn+qjDQKIbm1MPqion6zPt0EhP\nNRjXR6ttHJmr4os0bCG4Dl/GMKYTdfQRiKlhy1R4d1XvaoK4VrFXggt4gpw+YHxx\nYW5Mxk4M8+bxBHbLF63v6x1U/Op9v7xHJvUfysCHaZyi4QYy4H93fbbzeYeYEFS5\nrNX+/Y0pIbhvLDdhe+mtMSCpFJrssXSISmxkmPYSKZ6BwF8l9UYWBWF5Xwv+d2m5\ngyuaqi3QPQRhZKraMpomUbZjr8TZspDdQG3PCxFuu/H23QxijM7it9LOBzxUXe88\njTOKMtqwAHYZWDfvuGQSlkU0O0yz8OJH+sM1+FyAsjp9RM8nCJJNRFzTyxQmad7U\nnclErOWOKeV3wTpPS6iMOSv3wOqFIjSvF7xPvHNtL2Qfr8j3zxCMt62X6k8WJVok\n/2CyrbN3dXRuUiZZymIez9R4tF3Y+xf1NhqvEh2sew1WkMjtzhBsHWCMjePgqwod\nQtdQvP6c08H9tpXrKagph2WVnPFj4hQfGNI9Hc8bH166hDDrEwo/LWDQo4kCIgQT\nAQIADAUCUbywVAWDB4YfgAAKCRC5nwhOJxzwubV0EACgd3vRfbH+QCDeB6y2JS6U\nldPahx4XGCtH24SAhqGwBNSrLKnuJ/8gl9UXc9xRoa5as3/dVR3wvalmQ9If0q2C\nzzpvhDjr+xFH3DI7oVr31EjzIrs2U558QBhKL0SnUre0Tzg0gSYGhBeVcaN3hc/i\naYdn+qjDQKIbm1MPqion6zPt0EhPNRjXR6ttHJmr4os0bCG4Dl/GMKYTdfQRiKlh\ny1R4d1XvaoK4VrFXggt4gpw+YHxxYW5Mxk4M8+bxBHbLF63v6x1U/Op9v7xHJvUf\nysCHaZz/////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////\n/////////////////////4kCHAQQAQIABgUCUcM1sAAKCRDofrZNT3C3NcoFD/9O\nLaxOr5SjwOEdEWGcfnIYrM9W3JTnPv55Sz5zyKkgDd6p3fONnrSVz6UYMEFnkMPF\njn8KodOnyFvqVrkwko8FP8BctI4ztyNG6i9xv3xu9iOqt2n49ANRZM/tUtwTyb7q\nBXVQwcMmhVJ/i8MR1yYMraFA8YM6ovMHUszY3gx/x2Yr97S/xAtuauaRO0sZasA1\nUyxaZxWHGrek+/tinuuWp0Y1b/iSyW7tC0WV/tFO0C8Iv5tRcel/4b1EsV8he1EB\n8qOLBrAXcrJy3eC8H11h9rkfwL85qUf81oHYYg/vfmR4Y151VglclOZM6Y9kLC/X\n8yIhraa6dUG2prLxi2xEnl9eovs8A5xhcxPy2CHcJLRwR5xHA1IsrPK4aZsRC6cw\nRt+oLFxLCvGK8MjW8O2qMjn7u/xeBTnkEEOJpe3NQH+1jql3Tt/3ZkPtANR/Oly0\n/7tj/CyR37GUWxM6pIXIElhQ19u+5bGwgxdhD9Htwbe6j2LNqD0e3MUrQphojzIk\npjbmvPU8Xpk6upYFAu04+X/DfANWv61MZdiEHhEiySxIlrg2sthRooOURmmXld0L\ngzOCwJzMOjcXkaVDt3nubJGhfetHT4K36cAWBfRURMuC0hUQw+WgCBRzNT5nXBDi\n6Ng2l79fXlrY3/AQXDf9bH2EYpHBYXLySqBnxwBWS4hGBBARAgAGBQJRzJUrAAoJ\nEHbCDUAgGgwVAHgAn2Lm4q4fsoPwvy56i5NBWItY0ReGAJ4jpm+rsXdK+mW8gp4V\nr4RQD4bjO4kCHAQQAQIABgUCUcyVjQAKCRAoOKHZD9LVht9TEACeUgvjTWf/C9j3\n8fHj8zd5OrTy9PwXXfGvAZ17THgnzZNy1/U6zIhLqdUUwvyqtRU9EYkZ4YFquCPS\nqqZbyNBzpjdFARpoxL41xXSATjaaitHrywYYeWTb7USaUkAjOz/5krsGTuVsM2YG\nCLrYn2OVFobLZ0alO7D4mrAwLzTyat+WXuJA8V6wJW11r8E7awf3qdoI/VWGbQch\n7d3pYgfp6Q6ZRwt0dZDLkKNUYx41x5saM2wo8d/TgcHWuACnVV5eX9v80GToXUsm\n12ruI1ctRqEqsgiTLYpBdrevkzZgHTrY/VLTg5MZxWayUvSLn6abKalYNTvs6HFH\n2FuqgY0dSS88ozFbt/WzVG8u/2LPJ5loqYBREawJmdYjZCS7TI0gjq0rpL9Ppwhz\n3kkNGYKOlXpSiJVMBL2yiBkqLG7KYnzaJrFpG/aupxnxGPHjzdoLOd+Kpi/K+tnK\nFoTngfVBJxPdNG8z2a9TqxeLtXNPkJmwKQUQPibVFzKvvVkbSp7wQ0dfyuxPcHXu\nO0Jf3D+/AXl/V0BqrjIsoJBWhONavt3RnyiX1a2UDhTE3HFqblctg0E8wpG649dv\nvnZW57/a2PAqV6B7ztZ33tDUtLHnMPOT/kWQRdU+x3WKUi+j1GM4dWksLr2FeTUf\nC/5Tekmf0w7TjH32rHU3l4Pty8hhpIhGBBARAgAGBQJRzJW+AAoJEAqchymq1vv2\n/hsAoKBSCdynoKY1+RY8WF76WJJO0GxgAJ9p+/nu5kaId8j/mcVQfSULBl5MdYhr\nBBARAgArBQJR0eauBYMB4oUAHhpodHRwOi8vd3d3LmNhY2VydC5vcmcvY3BzLnBo\ncAAKCRDSuw0BZdD9WIHTAJ972DSGqdJ+MlRGxjhWtteRFdrJcgCeLLzVFbi6C380\nOb0T1kEaPnloUrGJAo0EEwEIAHcFAlIz079wGmh0dHA6Ly91bmRlcmdyaWQubmV0\nL2xlZ2FsL2dwZy9wb2xpY3kvMjAxMTEyMjQvNjdkYmUxM2I1YThkNDIwNDFmMGIw\nOWUyYjdkMjQ0Zjg0MmZjY2I4Y2EzYmZjNzBiNzkzZWVlM2U2NTI4NmRjMQAKCRAV\n0KYu0B4ZDKl1EACgPo7uNemY3DIuOpUfuR1gyxRLOy93/dyHZYyrjCA6qqFEfU7g\ni/2i2ZGqualW+ot/AtyHztY32e4KQFJ1msnLk9RKwrQUHJCF7HgNo1Hn3dSrmYxH\nvAaMUJ6MRSQF2kKJjgbTa754FzRO5BzMqKcgffn1Dl6JVhjNpVk9RYBx0sRGP8tf\nqggsSbEYTtf/s08IhZlSnBriUBFU96eabYzbuGdLTGDQusf4OgtkTcu0mhmNv+hV\nFgvB8/sMToZzhLC2LyklA9t3W4S+syFnhI7qutLdzgVZfdXe7ZyYT/SN0PZhZQwR\nEhci/hczbE2mO8k10HvUiOo6TDrOBxWGg4EAAXsRTKLvAwcbLuDDSdfALnPx4agq\ndX7pNYFMG11ct218p5AdwO8mUxIIvioN9TAs3ZFw2/OMA/I2L5Wm3etvmPHFWUgV\nZ++cxXGV/i0rD5VWUxdqtuw+l3Dx5MddzROlCyggzrTE7G8lkxGv/NoxE257/dJh\noeyCnvkdEfZeQJQweeUP14EHcm322AZpgQIqQMYcsrlVJJVEWXitocbfUuGbhIKo\nyNi+7y6zbixcNV+OCxhqeEVXJrC8ZVTaIOPuQiPYJCDWQEuClnLQ46lpBVfwPOu1\nVVgg2tBiI12jYlOTL6XaZt+Gkm2JRg8kX9CkwbUhtWh7GmYp9jFhUeZQV4kCjQQT\nAQgAdwUCUjPT1XAaaHR0cDovL3VuZGVyZ3JpZC5uZXQvbGVnYWwvZ3BnL3BvbGlj\neS8yMDExMTIyNC82N2RiZTEzYjVhOGQ0MjA0MWYwYjA5ZTJiN2QyNDRmODQyZmNj\nYjhjYTNiZmM3MGI3OTNlZWUzZTY1Mjg2ZGMxAAoJEP/OHJpPrfGXAr0P/3fJas1n\nEGTYJQDSyvVRREklecwqlEsP5tVru+yW7eswSeFPZnZMab+4Bl+D5pLmp9zZ3oFc\n2x6ARb/e9uUNd9SkpcOovvw+TxKRFHeRbIokJz82aLEPK8ascYdbHLG+0LMPFyfA\nxFYLQD7i18QX1HGN8FdoVuWUVp7/UO7qMcBc436RZhbRMfmSjsLaB9HDny7DOlIq\nScQYtHbQ/qraBUOrGgI0K/APN/17E/7tt0Amzg944YlfOXi54OwekB/i8g0+fIEU\nFQ4b8sjSWbOZwUiE6WC2g6TwYTzSmnKunBjuMXGH5se96VkRPdFi5yHYjt77TzEd\nb9xIFztpLwRlOEFD+fzzJ2zqqvd319RBn3IG0rwVOC9lr+pRHEYMpd8R7yAd/v1/\nkOQppzgHEqzcfNMQgbUlIyoXqnhFbMH+NIkOTH2Lda3GkFC93p+TOqADbwYOBo8g\nYfsUnEU/dASA18D/hshRg/Wjlq3sdJkLQfgrqk0CxVOgaIy6iDz4MerXxiLJG4uH\nchgmGJLuhEvle9QB6uGNEa48DOETMUDGwPeSPqgUsk5zxgdCq/qOGH2+76S/Tc1K\nDwI8MTf+/04dUbbj1whDuH7tfKdEHrCl3UoPV4gwKbmsoMoZG/sfaHqehkW1XlZy\nFtHEVf0XDmOEbruyBMjhRLkQc9apAIqqeVxPiLcEExEIAHcFAlIz099wGmh0dHA6\nLy91bmRlcmdyaWQubmV0L2xlZ2FsL2dwZy9wb2xpY3kvMjAxMTEyMjQvNjdkYmUx\nM2I1YThkNDIwNDFmMGIwOWUyYjdkMjQ0Zjg0MmZjY2I4Y2EzYmZjNzBiNzkzZWVl\nM2U2NTI4NmRjMQAKCRDVc9WxKatM3fF0AJ9FSrSWq9OUE8LRcA+QxbtoZB+DFQCf\nV2NIf0wyR8HWLjwLXiG8beEbj2mItwQTEQgAdwUCUjPT6XAaaHR0cDovL3VuZGVy\nZ3JpZC5uZXQvbGVnYWwvZ3BnL3BvbGljeS8yMDExMTIyNC82N2RiZTEzYjVhOGQ0\nMjA0MWYwYjA5ZTJiN2QyNDRmODQyZmNjYjhjYTNiZmM3MGI3OTNlZWUzZTY1Mjg2\nZGMxAAoJEFRMSGhi299iPBYAn3F7fsXWvWPXwQFM7/BBxU/PUvayAJ42XPHSSVWf\nZEXDT/gkpYG0xhYJ6bkCDQRRSOEgARAAyWjtpoXnq/G8veD10U0ZSG3OMLsHxLsk\nokLKigPxWMHSgFwRrdT+t7OQ+T5f4ETerfPBeripsYqtlcajSVhXEciUJRqXMCXN\nl1tyawCOvE4Vs+cQN780XzfxfiwUD/NeC5YzEcUBVMfdqNoVJVYtte/niv24PZaV\ndRKRWICX1J9wZj3oA8WS0JdKnvYjzQSzldrJ6iIrY7Hyb/Y4a1hCpEelJ9LombG3\n5O1f8bi1RNv5fHHLzJLU2Ngwj9ghb9XGD4n0NJAEqOrLpUm4VZ4UyJOj6kvEsOa1\nGlKzzizWblSelFtXvwXwtVgOK3kbpJ3rCYN8omckqk7T23rEAYy+4AO2Yqq+xdYL\n1qgloAEFtUCUFunnVAx1j5gqqd7TcjBPDqNOgHkfgeVhZ3GmbB/uUsK2PGocGLd/\nYTlXf3EGaJpuwYts4mHOF2Vovhww1+LswX5fKKnJ5CzbfC52y+7HMpO4kLUQYkF9\nsTXElNaUSh+3qNwsuUevI4QeFsyt21AEjUj+gcLSrbI7xqsJWG1gyfnfVuOj1KDC\nYL6hkq+/XmZSXOHbI/7TeCyEsgl6JGtfV6bhZ3Lgd5nEctaou8byDDL+yL9/aie4\njEtC3tWdHWWZxNqROyQEdCXtgEoRXC95MyrcWA0QADtv7KHBh7lekcLh7uF1gLEN\ngOUnLVB1r10AEQEAAYkERAQYAQoADwUCUUjhIAIbLgUJB4YfgAIpCRADjAn0YsJP\nx8FdIAQZAQoABgUCUUjhIAAKCRC9XExgS3u4A8pXEACd+Q6zyerRstasztj11Jyi\nNbAvaGJ/jIk6CEPttBQOrH4/OgeeaaabaVrQuzYtuYJJ9FwemHiRHKQ7xxg9L67k\na5tHwXuxpA0tjilZABsF0/VuENMH6yf4Z2vPrtmJtuL+ZzeXANijWwuzNfOsUMQS\nKK20XeHxUXI5WpboZzRBB9ZjYVgEYg2Yy4RuIV903wy90TFrfkCiNyxEctTbr5PB\nuq/Sbvv2cfa2n5K6UUWjWAMAJfeePyrEPjDZusUNonIFyQ+tUtpQCzFDSeLKi9MR\nHOplTa77BjWSkdcT58EtOJLYYRkJEHjw/Tv8MDMOjMOHAeo4toaWNNOadYV8Ml0a\nO7rULVQuTDeJJqZ5YAtcCl/TxAFv4mIVBKs7919KQobPPHUa68F6MQ/N65l27ly0\nVdc1V+O923NcP37Y59f960FmECKDHwrBwMtCKzLtmXDtP+F63s0Nh1S+Oxe47t7N\ng4W8qgILwzSqXyST0RzOafYpJWfvj3bhW7h0Sax7tCeDWltTagaxNKhi7Tzjqph7\nJAAauIeRzBSisPTdEJw1ww9mI7LlXz4HGp1rV+ynvraDyhC81tZ+xhIng9/Hj/7v\nC4oFOuZABohYXjg5sahFYuR2l+kYRz9hA0fKSFNUo07xqL+rDt0Nah0uhEhJG3MS\n8HL5UYRXD6gCqUiwimOF1jj5EACiIL1tjlgvftkcaCyLOFbPegUnbwrgcXNoGo0T\nD6D2ir5swV6GL2cLxPTGTwXjQ74zNna3vrnRb0p6cgjk0fVUuGvI2gBWewJ55mDO\ntTMZv5u06zN3VfnUVlKp+aiNpEhOwoMBhufOMCRk7U0xzV755BxJs2XjO9Fwv3fq\n7jnqNlK/sLPOd6KANJyHLNAtkpkUlF3KiMwHCdNZxzila8x7CBfzlfjJz9tMb1Ug\n7MSMZ3A4fOp9Uz7vWEie5gu8gPm4rr+ksAylWeNLd6K+ZlkK5Z+jQ3qRU9YBr3BJ\ndXePk5WqGuzav5Tmh2S1avEDfD1V4RzW8bdwDowsfsjqXz1dNeD8ahoZN32PwFiN\n7dgHKQtUs4NIbXCqeTaD14oNjmR/lCBjP/Vxdahmh7WuQcnKEx1/ZUa8a2JYWxFW\neol1U/rrXuX/CiTs2U4vYDQzpcDJPZCVs6+731q2qWo4HBG9jkQKjuNyxTGhBoTn\nkiBEwGug84/fXHq/+kcdYhsyekU8tR45ILH3FVuRuV89JhPGYorC7ThJOu72dYSv\nYDvc+GHc7v/f2cqHGUc/ZBWmuD41lZq3hbZMzypCYTBQglnqGj3ttGM6ZhlIW7KR\nt/zwAbMPLlFLnrMb7FkvSvHWH3BNAcuhqr4lC0sUd30jtaVsLt0Mwvi9sv+wZ1Gg\nrSxUQQ==\n=B9ld\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "run_electrum",
    "content": "#!/usr/bin/env python3\n# -*- mode: python -*-\n#\n# Electrum - lightweight Bitcoin client\n# Copyright (C) 2011 thomasv@gitorious\n#\n# Permission is hereby granted, free of charge, to any person\n# obtaining a copy of this software and associated documentation files\n# (the \"Software\"), to deal in the Software without restriction,\n# including without limitation the rights to use, copy, modify, merge,\n# publish, distribute, sublicense, and/or sell copies of the Software,\n# and to permit persons to whom the Software is furnished to do so,\n# subject to 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\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport os\nimport sys\n\n\nMIN_PYTHON_VERSION = \"3.10.0\"  # FIXME duplicated from setup.py\n_min_python_version_tuple = tuple(map(int, (MIN_PYTHON_VERSION.split(\".\"))))\n\n\nif sys.version_info[:3] < _min_python_version_tuple:\n    sys.exit(\"Error: Electrum requires Python version >= %s...\" % MIN_PYTHON_VERSION)\n\n\nimport warnings\nimport asyncio\nfrom typing import TYPE_CHECKING, Optional, Dict\n\n\nscript_dir = os.path.dirname(os.path.realpath(__file__))\nis_pyinstaller = getattr(sys, 'frozen', False)\nis_android = 'ANDROID_DATA' in os.environ\nis_appimage = 'APPIMAGE' in os.environ\nis_binary_distributable = is_pyinstaller or is_android or is_appimage\n# is_local: unpacked tar.gz but not pip installed, or git clone\nis_local = (not is_binary_distributable\n            and os.path.exists(os.path.join(script_dir, \"electrum.desktop\")))\nis_git_clone = is_local and os.path.exists(os.path.join(script_dir, \".git\"))\n\nif is_git_clone:\n    # developers should probably see all deprecation warnings unless explicitly overruled\n    if not any(['DeprecationWarning' in x for x in sys.warnoptions]):\n        warnings.simplefilter('default', DeprecationWarning)\n\nif is_local or is_android:\n    sys.path.insert(0, os.path.join(script_dir, 'packages'))\n\nif is_pyinstaller:\n    # Keep an open file handle for the binary that started us. On Windows, this\n    # prevents users from moving or renaming the exe file while running (doing which\n    # causes ImportErrors and other runtime failures). (see #4072)\n    _file = open(sys.executable, 'rb')\n\n\n# when running from source, on Windows, also search for DLLs in inner 'electrum' folder\nif is_local and os.name == 'nt':  # fixme: duplicated between main script and __init__.py :(\n    os.add_dll_directory(os.path.join(os.path.dirname(__file__), 'electrum'))\n\n\ndef check_imports():\n    # pure-python dependencies need to be imported here for pyinstaller\n    try:\n        import dns\n        import certifi\n        import qrcode\n        import google.protobuf\n        import aiorpcx\n        import aiohttp\n        import aiohttp_socks\n        import electrum_ecc\n        import jsonpatch\n        import electrum_aionostr\n    except ImportError as e:\n        sys.exit(f\"Error: {str(e)}. Some dependencies are missing. Have you read the README? Or just try '$ python3 -m pip install -r contrib/requirements/requirements.txt'\")\n    if not ((0, 25, 0) <= aiorpcx._version < (0, 26)):\n        raise RuntimeError(f'aiorpcX version {aiorpcx._version} does not match required: 0.25.0<=ver<0.26')\n    # the following imports are for pyinstaller\n    from google.protobuf import descriptor\n    from google.protobuf import message\n    from google.protobuf import reflection\n    from google.protobuf import descriptor_pb2\n    # make sure that certificates are here\n    assert os.path.exists(certifi.where())\n\n\nif not is_android:\n    check_imports()\n\n\nif is_android:\n    # hack to make pycryptodomex work on Android\n    # from https://github.com/kivy/python-for-android/issues/1866#issuecomment-927157780\n    import ctypes\n    ctypes.pythonapi = ctypes.PyDLL(\"libpython%d.%d.so\" % sys.version_info[:2])  # replaces ctypes.PyDLL(None)\n\n\nsys._ELECTRUM_RUNNING_VIA_RUNELECTRUM = True  # used by logging.py\n\nfrom electrum.logging import get_logger, configure_logging  # import logging submodule first\nfrom electrum import util\nfrom electrum.payment_identifier import PaymentIdentifier\nfrom electrum import SimpleConfig\nfrom electrum.wallet_db import WalletDB\nfrom electrum.wallet import Wallet\nfrom electrum.storage import WalletStorage\nfrom electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled\nfrom electrum.util import InvalidPassword\nfrom electrum.plugin import Plugins\nfrom electrum.commands import get_parser, get_simple_parser, known_commands, Commands, config_variables\nfrom electrum import daemon\nfrom electrum.util import create_and_start_event_loop, UserFacingException, JsonRPCError\nfrom electrum.i18n import set_language\n\nif TYPE_CHECKING:\n    import threading\n\n_logger = get_logger(__name__)\n\n\n# get password routine\ndef prompt_password(prompt: str, *, confirm: bool = True) -> Optional[str]:\n    import getpass\n    password = getpass.getpass(prompt, stream=None)\n    if password and confirm:\n        password2 = getpass.getpass(\"Confirm: \")\n        if password != password2:\n            sys.exit(\"Error: Passwords do not match.\")\n    if not password:\n        password = None\n    return password\n\n\ndef init_cmdline(config_options, wallet_path, *, rpcserver: bool, config: 'SimpleConfig'):\n    cmdname = config.get('cmd')\n    cmd = known_commands[cmdname]\n\n    if cmdname in ['payto', 'paytomany'] and config.get('unsigned'):\n        cmd.requires_password = False\n\n    if cmdname in ['payto', 'paytomany'] and config.get('broadcast'):\n        cmd.requires_network = True\n\n    if cmd.requires_wallet and not wallet_path:\n        print_msg(\"wallet path not provided.\")\n        sys_exit(1)\n\n    # instantiate wallet for command-line\n    storage = WalletStorage(wallet_path, allow_partial_writes=config.WALLET_PARTIAL_WRITES) if wallet_path else None\n\n    if cmd.requires_wallet and not storage.file_exists():\n        print_msg(\"Error: Wallet file not found.\")\n        print_msg(\"Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option\")\n        sys_exit(1)\n\n    # important warning\n    if cmd.name in ['getprivatekeys']:\n        print_stderr(\"WARNING: ALL your private keys are secret.\")\n        print_stderr(\"Exposing a single private key can compromise your entire wallet!\")\n        print_stderr(\"In particular, DO NOT use 'redeem private key' services proposed by third parties.\")\n\n    # commands needing password\n    if ((cmd.requires_wallet and storage.is_encrypted() and not rpcserver)\n            or (cmdname == 'load_wallet' and storage.is_encrypted())\n            or (cmdname in ['password', 'unlock'])\n            or (cmd.requires_password and not rpcserver)):\n        if storage.is_encrypted_with_hw_device():\n            # this case is handled later in the control flow\n            password = None\n        elif config.get('password') is not None:\n            password = config.get('password')\n            if password == '':\n                password = None\n        else:\n            password = prompt_password('Password:', confirm=False)\n    else:\n        password = None\n\n    config_options['password'] = config_options.get('password') or password\n\n    if cmd.name == 'password' and 'new_password' not in config_options:\n        new_password = prompt_password('New password:')\n        config_options['new_password'] = new_password\n\n\ndef get_connected_hw_devices(plugins: 'Plugins'):\n    supported_plugins = plugins.get_hardware_support()\n    # scan devices\n    devices = []\n    devmgr = plugins.device_manager\n    for splugin in supported_plugins:\n        name, plugin = splugin.name, splugin.plugin\n        if not plugin:\n            e = splugin.exception\n            _logger.error(f\"{name}: error during plugin init: {repr(e)}\")\n            continue\n        try:\n            u = devmgr.list_pairable_device_infos(handler=None, plugin=plugin)\n        except Exception as e:\n            _logger.error(f'error getting device infos for {name}: {repr(e)}')\n            continue\n        devices += list(map(lambda x: (name, x), u))\n    return devices\n\n\ndef get_password_for_hw_device_encrypted_storage(plugins: 'Plugins') -> str:\n    devices = get_connected_hw_devices(plugins)\n    if len(devices) == 0:\n        raise UserFacingException(\"Error: No connected hw device found. Cannot decrypt this wallet.\")\n    elif len(devices) > 1:\n        print_msg(\"Warning: multiple hardware devices detected. \"\n                  \"The first one will be used to decrypt the wallet.\")\n    # FIXME we use the \"first\" device, in case of multiple ones\n    name, device_info = devices[0]\n    devmgr = plugins.device_manager\n    try:\n        client = devmgr.client_by_id(device_info.device.id_)\n        client.handler = client.plugin.create_handler(None)\n        return client.get_password_for_storage_encryption()\n    except UserCancelled:\n        raise\n\n\nasync def run_offline_command(config: 'SimpleConfig', config_options: dict, wallet_path: str, plugins: 'Plugins'):\n    cmdname = config.get('cmd')\n    cmd = known_commands[cmdname]\n    password = config_options.get('password')\n    if 'wallet_path' in cmd.options and config_options.get('wallet_path') is None:\n        config_options['wallet_path'] = wallet_path\n    if cmd.requires_wallet:\n        storage = WalletStorage(wallet_path, allow_partial_writes=config.WALLET_PARTIAL_WRITES)\n        if storage.is_encrypted():\n            if storage.is_encrypted_with_hw_device():\n                password = get_password_for_hw_device_encrypted_storage(plugins)\n                config_options['password'] = password\n            storage.decrypt(password)\n        db = WalletDB(storage.read(), storage=storage, upgrade=True)\n        wallet = Wallet(db, config=config)\n        config_options['wallet'] = wallet\n    else:\n        wallet = None\n    # check password\n    if cmd.requires_password and wallet.has_password():\n        try:\n            wallet.check_password(password)\n        except InvalidPassword:\n            print_msg(\"Error: This password does not decode this wallet.\")\n            raise\n    if cmd.requires_network:\n        print_msg(\"Warning: running command offline\")\n    # arguments passed to function\n    args = [config.get(x) for x in cmd.params]\n    # decode json arguments\n    if cmdname not in ('setconfig',):\n        args = list(map(json_decode, args))\n    # options\n    kwargs = {}\n    for x in cmd.options:\n        kwargs[x] = (config_options.get(x) if x in ['wallet_path', 'wallet', 'password', 'new_password'] else config.get(x))\n    cmd_runner = Commands(config=config)\n    func = getattr(cmd_runner, cmd.name)\n    result = await func(*args, **kwargs)\n    # save wallet\n    if wallet:\n        wallet.save_db()\n    return result\n\n\nloop = None  # type: Optional[asyncio.AbstractEventLoop]\nstop_loop = None  # type: Optional[asyncio.Future]\nloop_thread = None  # type: Optional[threading.Thread]\n\n\ndef sys_exit(i):\n    # stop event loop and exit\n    if loop:\n        loop.call_soon_threadsafe(stop_loop.set_result, 1)\n        loop_thread.join(timeout=1)\n    sys.exit(i)\n\n\ndef read_config(config_options: dict) -> SimpleConfig:\n    \"\"\"\n    Reads the config file and returns SimpleConfig, on failure it will potentially\n    show a GUI error dialog if a gui is available, and then re-raise the exception.\n    \"\"\"\n    try:\n        return SimpleConfig(config_options)\n    except Exception as config_error:\n        # parse full cmd to find out which UI is being used\n        full_config_options = parse_command_line(simple_parser=False)\n        if full_config_options.get(\"cmd\") == 'gui':\n            gui_name = full_config_options.get(SimpleConfig.GUI_NAME.key(), 'qt')\n            try:\n                gui = __import__(f'electrum.gui.{gui_name}', fromlist=['electrum'])\n                gui.standalone_exception_dialog(config_error)  # type: ignore\n            except Exception as e:\n                print_stderr(f\"Error showing standalone gui dialog: {e}\")\n        raise\n\n\ndef parse_command_line(simple_parser=False) -> Dict:\n    # parse command line from sys.argv\n    if simple_parser:\n        parser = get_simple_parser()\n        options, args = parser.parse_args()\n        config_options = options.__dict__\n        config_options['cmd'] = 'gui'\n    else:\n        parser = get_parser()\n        args = parser.parse_args()\n        config_options = args.__dict__\n        f = lambda key: config_options[key] is not None and key not in config_variables.get(args.cmd, {}).keys()\n        config_options = {key: config_options[key] for key in filter(f, config_options.keys())}\n        if config_options.get(SimpleConfig.NETWORK_SERVER.key()):\n            config_options[SimpleConfig.NETWORK_AUTO_CONNECT.key()] = False\n        if config_options.get(SimpleConfig.NETWORK_PROXY.key()):\n            config_options[SimpleConfig.NETWORK_PROXY_ENABLED.key()] = True\n\n    config_options['cwd'] = cwd = os.getcwd()\n\n    # fixme: this can probably be achieved with a runtime hook (pyinstaller)\n    if is_pyinstaller and os.path.exists(os.path.join(sys._MEIPASS, 'is_portable')):\n        config_options['portable'] = True\n\n    if config_options.get('portable'):\n        if is_local:\n            # running from git clone or local source: put datadir next to main script\n            datadir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'electrum_data')\n        else:\n            # Running a binary or installed source. The most generic but still reasonable thing\n            # is to use the current working directory. (see #7732)\n            # note: The main script is often unpacked to a temporary directory from a bundled executable,\n            #       and we don't want to put the datadir inside a temp dir.\n            # note: Re the portable .exe on Windows, when the user double-clicks it, CWD gets set\n            #       to the parent dir, i.e. we will put the datadir next to the exe.\n            datadir = os.path.join(os.path.realpath(cwd), 'electrum_data')\n        config_options['electrum_path'] = datadir\n\n    if not config_options.get('verbosity'):\n        warnings.simplefilter('ignore', DeprecationWarning)\n    return config_options\n\n\ndef main():\n    global loop, stop_loop, loop_thread\n    # The hook will only be used in the Qt GUI right now\n    util.setup_thread_excepthook()\n    # on macOS, delete Process Serial Number arg generated for apps launched in Finder\n    sys.argv = list(filter(lambda x: not x.startswith('-psn'), sys.argv))\n\n    # old 'help' syntax\n    if len(sys.argv) > 1 and sys.argv[1] == 'help':\n        sys.argv.remove('help')\n        sys.argv.append('-h')\n\n    # old '-v' syntax\n    # Due to this workaround that keeps old -v working,\n    # more advanced usages of -v need to use '-v='.\n    # e.g. -v=debug,network=warning,interface=error\n    try:\n        i = sys.argv.index('-v')\n    except ValueError:\n        pass\n    else:\n        sys.argv[i] = '-v*'\n\n    # read arguments from stdin pipe and prompt\n    for i, arg in enumerate(sys.argv):\n        if arg == '-':\n            if not sys.stdin.isatty():\n                sys.argv[i] = sys.stdin.read()\n                break\n            else:\n                raise Exception('Cannot get argument from stdin')\n        elif arg == '?':\n            sys.argv[i] = input(\"Enter argument:\")\n        elif arg == ':':\n            sys.argv[i] = prompt_password('Enter argument (will not echo):', confirm=False)\n\n    # config is an object passed to the various constructors (wallet, interface, gui)\n    if is_android:\n        import importlib.util\n        config_options = {\n            'verbosity': '*' if util.is_android_debug_apk() else '',\n            'cmd': 'gui',\n            SimpleConfig.GUI_NAME.key(): 'qml',\n            SimpleConfig.WALLET_SHOULD_USE_SINGLE_PASSWORD.key(): True,\n        }\n        SimpleConfig.set_chain_config_opt_based_on_android_packagename(config_options)\n    else:\n        # save sys args for next parser\n        saved_sys_argv = sys.argv[:]\n        # disable help, the next parser will display it\n        for x in sys.argv:\n            if x in ['-h', '--help']:\n                sys.argv.remove(x)\n        # parse first without plugins\n        config_options = parse_command_line(simple_parser=True)\n        tmp_config = read_config(config_options)\n        # load (only) the commands modules of plugins so their commands are registered\n        _plugin_commands = Plugins(tmp_config, cmd_only=True)\n        # re-parse command line\n        sys.argv = saved_sys_argv[:]\n        config_options = parse_command_line()\n\n    config = read_config(config_options)\n    cmdname = config.get('cmd')\n\n    # set language as early as possible\n    # Note: we are already too late for strings that are declared in the global scope\n    #       of an already imported module. However, the GUI and the plugins at least have\n    #       not been imported yet. (see #4621)\n    # Note: it is ok to call set_language() again later, but note that any call only applies\n    #       to not-yet-evaluated strings.\n    # Note: the CLI is intentionally always non-localized.\n    # Note: Some unit tests might rely on the default non-localized strings.\n    if cmdname == 'gui':\n        gui_name = config.GUI_NAME\n        lang = config.LOCALIZATION_LANGUAGE\n        if not lang:\n            try:\n                from electrum.gui.default_lang import get_default_language\n                lang = get_default_language(gui_name=gui_name)\n                _logger.info(f\"get_default_language: detected default as {lang=!r}\")\n            except ImportError as e:\n                _logger.info(f\"get_default_language: failed. got exc={e!r}\")\n        set_language(lang)\n\n    chain = config.get_selected_chain()\n    chain.set_as_network()\n\n    # check if we received a valid payment identifier\n    uri = config_options.get('url')\n    if uri and not PaymentIdentifier(None, uri).is_valid():\n        print_stderr('unknown command:', uri)\n        sys.exit(1)\n\n    if sys.platform == \"linux\" and not is_android:\n        import electrum.harden_memory_linux\n        electrum.harden_memory_linux.set_dumpable_safe(False)\n\n    if cmdname == 'daemon' and config.get(\"detach\"):\n        # detect lockfile.\n        # This is not as good as get_file_descriptor, but that would require the asyncio loop\n        lockfile = daemon.get_lockfile(config)\n        if os.path.exists(lockfile):\n            print_stderr(\"Daemon already running (lockfile detected).\")\n            print_stderr(\"Run 'electrum stop' to stop the daemon.\")\n            sys.exit(1)\n        # Initialise rpc credentials to random if not set yet. This would normally be done\n        # later anyway, but we need to avoid the two sides of the fork setting conflicting random creds.\n        daemon.get_rpc_credentials(config)  # inits creds as side-effect\n        # fork before creating the asyncio event loop\n        try:\n            pid = os.fork()\n        except AttributeError as e:\n            print_stderr(f\"Error: {e!r}\")\n            print_stderr(\"Running daemon in detached mode (-d) is not supported on this platform.\")\n            print_stderr(\"Try running the daemon in the foreground (without -d).\")\n            sys.exit(1)\n        if pid:\n            print_stderr(\"starting daemon (PID %d)\" % pid)\n            loop, stop_loop, loop_thread = create_and_start_event_loop()\n            ready = daemon.wait_until_daemon_becomes_ready(config=config, timeout=5)\n            if ready:\n                sys_exit(0)\n            else:\n                print_stderr(\"timed out waiting for daemon to get ready\")\n                sys_exit(1)\n        else:\n            # redirect standard file descriptors\n            sys.stdout.flush()\n            sys.stderr.flush()\n            si = open(os.devnull, 'r')\n            so = open(os.devnull, 'w')\n            se = open(os.devnull, 'w')\n            os.dup2(si.fileno(), sys.stdin.fileno())\n            os.dup2(so.fileno(), sys.stdout.fileno())\n            os.dup2(se.fileno(), sys.stderr.fileno())\n\n    loop, stop_loop, loop_thread = create_and_start_event_loop()\n\n    try:\n        handle_cmd(\n            cmdname=cmdname,\n            config=config,\n            config_options=config_options,\n        )\n    except Exception:\n        _logger.exception(\"\")\n        sys_exit(1)\n\n\ndef handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict):\n    if cmdname == 'gui':\n        configure_logging(config)\n        fd = daemon.get_file_descriptor(config)\n        if fd is not None:\n            d = daemon.Daemon(config, fd, start_network=False)\n            try:\n                d.run_gui()\n            except BaseException as e:\n                _logger.exception('daemon.run_gui errored')\n                sys_exit(1)\n            else:\n                sys_exit(0)\n        else:\n            try:\n                result = daemon.request(config, 'gui', (config_options,))\n            except JsonRPCError as e:\n                if e.code == JsonRPCError.Codes.USERFACING:\n                    print_stderr(e.message)\n                elif e.code == JsonRPCError.Codes.INTERNAL:\n                    print_stderr(\"(inside daemon): \" + e.data[\"traceback\"])\n                    print_stderr(e.message)\n                else:\n                    raise Exception(f\"unknown error code {e.code}\")\n                sys_exit(1)\n\n    elif cmdname == 'daemon':\n        configure_logging(config)\n        fd = daemon.get_file_descriptor(config)\n        if fd is not None:\n            # run daemon\n            d = daemon.Daemon(config, fd)\n            d.run_daemon()\n            sys_exit(0)\n        else:\n            # FIXME this message is lost in detached mode (parent process already exited after forking)\n            print_msg(\"Daemon already running\")\n            sys_exit(1)\n    else:\n        # command line\n        configure_logging(config, log_to_file=False)  # don't spam logfiles for each client-side RPC, but support \"-v\"\n        cmd = known_commands[cmdname]\n        wallet_path = config.get_wallet_path()\n        if cmd.requires_wallet and not wallet_path:\n            print_stderr('wallet path not provided')\n            sys_exit(1)\n        if not config.NETWORK_OFFLINE:\n            init_cmdline(config_options, wallet_path, rpcserver=True, config=config)\n            timeout = config.CLI_TIMEOUT\n            try:\n                result = daemon.request(config, 'run_cmdline', (config_options,), timeout)\n            except daemon.DaemonNotRunning:\n                print_msg(\"Daemon not running; try 'electrum daemon -d'\")\n                if not cmd.requires_network:\n                    print_msg(\"To run this command without a daemon, use --offline\")\n                if cmd.name == \"stop\":  # remove lockfile if it exists, as daemon looks dead\n                    lockfile = daemon.get_lockfile(config)\n                    if os.path.exists(lockfile):\n                        print_msg(\"Found lingering lockfile for daemon. Removing.\")\n                        daemon.remove_lockfile(lockfile)\n                sys_exit(1)\n            except JsonRPCError as e:\n                if e.code == JsonRPCError.Codes.USERFACING:\n                    print_stderr(e.message)\n                elif e.code == JsonRPCError.Codes.INTERNAL:\n                    print_stderr(\"(inside daemon): \" + e.data[\"traceback\"])\n                    print_stderr(e.message)\n                else:\n                    raise Exception(f\"unknown error code {e.code}\")\n                sys_exit(1)\n            except Exception as e:\n                _logger.exception(\"error running command (with daemon)\")\n                sys_exit(1)\n        else:\n            if cmd.requires_network:\n                print_msg(\"This command cannot be run offline\")\n                sys_exit(1)\n            lockfile = daemon.get_lockfile(config)\n            if os.path.exists(lockfile):\n                print_stderr(\"Daemon already running (lockfile detected)\")\n                print_stderr(\"Run 'electrum stop' to stop the daemon.\")\n                print_stderr(\"Run this command without --offline to interact with the daemon\")\n                sys_exit(1)\n            init_cmdline(config_options, wallet_path, rpcserver=False, config=config)\n            plugins = Plugins(config, 'cmdline')\n            coro = run_offline_command(config, config_options, wallet_path, plugins)\n            fut = asyncio.run_coroutine_threadsafe(coro, loop)\n            try:\n                try:\n                    result = fut.result()\n                finally:\n                    plugins.stop()\n                    plugins.stopped_event.wait(1)\n            except UserFacingException as e:\n                print_stderr(str(e))\n                sys_exit(1)\n            except InvalidPassword:\n                print_stderr(\"Invalid password\")\n                sys_exit(1)\n            except UserCancelled:\n                print_stderr(\"Aborted by user\")\n                sys_exit(1)\n            except Exception as e:\n                _logger.exception(\"error running command (without daemon)\")\n                sys_exit(1)\n    # print result\n    if isinstance(result, str):\n        print_msg(result)\n    elif result is not None:\n        print_msg(json_encode(result))\n    sys_exit(0)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "setup.cfg",
    "content": "[easy_install]\n# We don't want setuptools sneakily installing dependencies, invisible to pip.\n# see https://pip.pypa.io/en/stable/reference/pip_install/#controlling-setup-requires\n# see https://github.com/pypa/setuptools/issues/1916#issuecomment-743350566\nindex_url = ''\nfind_links = ''\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python3\n\n# python setup.py sdist --format=zip,gztar\n\nimport os\nimport sys\nimport platform\nimport importlib.util\nimport argparse\nimport subprocess\n\nfrom setuptools import setup, find_packages\nfrom setuptools.command.install import install\n\nMIN_PYTHON_VERSION = \"3.10.0\"\n_min_python_version_tuple = tuple(map(int, (MIN_PYTHON_VERSION.split(\".\"))))\n\n\nif sys.version_info[:3] < _min_python_version_tuple:\n    sys.exit(\"Error: Electrum requires Python version >= %s...\" % MIN_PYTHON_VERSION)\n\nwith open('contrib/requirements/requirements.txt') as f:\n    requirements = f.read().splitlines()\n\nwith open('contrib/requirements/requirements-hw.txt') as f:\n    requirements_hw = f.read().splitlines()\n\n# load version.py; needlessly complicated alternative to \"imp.load_source\":\nversion_spec = importlib.util.spec_from_file_location('version', 'electrum/version.py')\nversion_module = version = importlib.util.module_from_spec(version_spec)\nversion_spec.loader.exec_module(version_module)\n\ndata_files = []\n\nif platform.system() in ['Linux', 'FreeBSD', 'DragonFly']:\n    # note: we can't use absolute paths here. see #7787\n    data_files += [\n        (os.path.join('share', 'applications'),               ['electrum.desktop']),\n        (os.path.join('share', 'pixmaps'),                    ['electrum/gui/icons/electrum.png']),\n        (os.path.join('share', 'icons/hicolor/128x128/apps'), ['electrum/gui/icons/electrum.png']),\n    ]\n\nextras_require = {\n    'hardware': requirements_hw,\n    'gui': ['pyqt6'],\n    'crypto': ['cryptography>=2.6'],\n    'tests': ['pycryptodomex>=3.7', 'cryptography>=2.6', 'pyaes>=0.1a1'],\n    'qml_gui': ['pyqt6<6.6', 'pyqt6-qt6<6.6']\n}\n# 'full' extra that tries to grab everything an enduser would need (except for libsecp256k1...)\nextras_require['full'] = [pkg for sublist in\n                          (extras_require['hardware'], extras_require['gui'], extras_require['crypto'])\n                          for pkg in sublist]\n# legacy. keep 'fast' extra working\nextras_require['fast'] = extras_require['crypto']\n\n\nsetup(\n    name=\"Electrum\",\n    version=version.ELECTRUM_VERSION,\n    python_requires='>={}'.format(MIN_PYTHON_VERSION),\n    install_requires=requirements,\n    extras_require=extras_require,\n    packages=(['electrum',]\n              + [('electrum.'+pkg) for pkg in\n                 find_packages('electrum', exclude=[\"tests\"])]),\n    package_dir={\n        'electrum': 'electrum'\n    },\n    # Note: MANIFEST.in lists what gets included in the tar.gz, and the\n    # package_data kwarg lists what gets put in site-packages when pip installing the tar.gz.\n    # By specifying include_package_data=True, MANIFEST.in becomes responsible for both.\n    include_package_data=True,\n    scripts=['electrum/electrum'],\n    data_files=data_files,\n    description=\"Lightweight Bitcoin Wallet\",\n    author=\"Thomas Voegtlin\",\n    author_email=\"thomasv@electrum.org\",\n    license=\"MIT Licence\",\n    url=\"https://electrum.org\",\n    long_description=\"\"\"Lightweight Bitcoin Wallet\"\"\",\n)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "import asyncio\nimport os\nimport unittest\nimport threading\nimport tempfile\nimport shutil\nimport functools\nimport inspect\nfrom typing import TYPE_CHECKING, List\n\nimport electrum\nimport electrum.logging\nfrom electrum import constants\nfrom electrum import util\nfrom electrum.util import OldTaskGroup\nfrom electrum.logging import Logger\nfrom electrum.wallet import restore_wallet_from_text\n\nif TYPE_CHECKING:\n    from .test_lnpeer import MockLNWallet\n\n\n# Set this locally to make the test suite run faster.\n# If set, unit tests that would normally test functions with multiple implementations,\n# will only be run once, using the fastest implementation.\n# e.g. libsecp256k1 vs python-ecdsa. pycryptodomex vs pyaes.\nFAST_TESTS = False\n\n\nelectrum.logging._configure_stderr_logging(verbosity=\"*\")\n\nelectrum.util.AS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP = True\n\n\nclass ElectrumTestCase(unittest.IsolatedAsyncioTestCase, Logger):\n    \"\"\"Base class for our unit tests.\"\"\"\n\n    TESTNET = False  # there is also an @as_testnet decorator to run single tests in testnet mode\n    REGTEST = False\n    TEST_ANCHOR_CHANNELS = False\n    WALLET_FILES_DIR = os.path.join(os.path.dirname(__file__), \"test_storage_upgrade\")\n    # maxDiff = None  # for debugging\n\n    # some unit tests are modifying globals... so we run sequentially:\n    _test_lock = threading.Lock()\n\n    def __init__(self, *args, **kwargs):\n        Logger.__init__(self)\n        unittest.IsolatedAsyncioTestCase.__init__(self, *args, **kwargs)\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        assert not (cls.REGTEST and cls.TESTNET), \"regtest and testnet are mutually exclusive\"\n        if cls.REGTEST:\n            constants.BitcoinRegtest.set_as_network()\n        elif cls.TESTNET:\n            constants.BitcoinTestnet.set_as_network()\n\n    @classmethod\n    def tearDownClass(cls):\n        super().tearDownClass()\n        if cls.TESTNET or cls.REGTEST:\n            constants.BitcoinMainnet.set_as_network()\n\n    def setUp(self):\n        have_lock = self._test_lock.acquire(timeout=0.1)\n        if not have_lock:\n            # This can happen when trying to run the tests in parallel,\n            # or if a prior test raised  during `setUp` or `asyncSetUp` and never released the lock.\n            raise Exception(\"timed out waiting for test_lock\")\n        super().setUp()\n        self.unittest_base_path = tempfile.mkdtemp(prefix=\"electrum-unittest-base-\")\n        self.electrum_path = os.path.join(self.unittest_base_path, \"electrum\")\n        util.make_dir(self.electrum_path)\n        assert util._asyncio_event_loop is None, \"global event loop already set?!\"\n        self._lnworkers_created = []  # type: List[MockLNWallet]\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        loop = util.get_asyncio_loop()\n        # IsolatedAsyncioTestCase creates event loops with debug=True, which makes the tests take ~4x time\n        if not (os.environ.get(\"PYTHONASYNCIODEBUG\") or os.environ.get(\"PYTHONDEVMODE\")):\n            loop.set_debug(False)\n        util._asyncio_event_loop = loop\n\n    async def asyncTearDown(self):\n        # clean up lnworkers\n        async with OldTaskGroup() as group:\n            for lnworker in self._lnworkers_created:\n                await group.spawn(lnworker.stop())\n        self._lnworkers_created.clear()\n        await super().asyncTearDown()\n\n    def tearDown(self):\n        util.callback_mgr.clear_all_callbacks()\n        shutil.rmtree(self.unittest_base_path)\n        super().tearDown()\n        util._asyncio_event_loop = None  # cleared here, at the ~last possible moment. asyncTearDown is too early.\n        self._test_lock.release()\n\n    def create_mock_lnwallet(\n        self,\n        *,\n        name: str,\n        has_anchors: bool,\n    ) -> 'MockLNWallet':\n        from .test_lnpeer import _create_mock_lnwallet\n        data_dir = tempfile.mkdtemp(prefix=\"lnwallet-\", dir=self.unittest_base_path)\n        lnwallet = _create_mock_lnwallet(name=name, has_anchors=has_anchors, data_dir=data_dir)\n        self._lnworkers_created.append(lnwallet)\n        return lnwallet\n\n    def get_wallet_file_path(self, wallet_name: str) -> str:\n        return os.path.join(self.WALLET_FILES_DIR, wallet_name)\n\n\ndef as_testnet(func):\n    \"\"\"Function decorator to run a single unit test in testnet mode.\n\n    NOTE: this is inherently sequential; tests running in parallel would break things\n    \"\"\"\n    old_net = constants.net\n    if inspect.iscoroutinefunction(func):\n        async def run_test(*args, **kwargs):\n            try:\n                constants.BitcoinTestnet.set_as_network()\n                return await func(*args, **kwargs)\n            finally:\n                constants.net = old_net\n    else:\n        def run_test(*args, **kwargs):\n            try:\n                constants.BitcoinTestnet.set_as_network()\n                return func(*args, **kwargs)\n            finally:\n                constants.net = old_net\n    return run_test\n\n\n@functools.wraps(restore_wallet_from_text)\ndef restore_wallet_from_text__for_unittest(*args, gap_limit=2, gap_limit_for_change=1, **kwargs):\n    \"\"\"much lower default gap limits (to save compute time)\"\"\"\n    return restore_wallet_from_text(\n        *args,\n        gap_limit=gap_limit,\n        gap_limit_for_change=gap_limit_for_change,\n        **kwargs,\n    )\n"
  },
  {
    "path": "tests/anchor-vectors.json",
    "content": "[\n  {\n    \"Name\": \"simple commitment tx with no HTLCs\",\n    \"LocalBalance\": 7000000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 15000,\n    \"UseTestHtlcs\": false,\n    \"HtlcDescs\": [],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80044a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994c0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a508b6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004830450221008266ac6db5ea71aac3c95d97b0e172ff596844851a3216eb88382a8dddfd33d2022050e240974cfd5d708708b4365574517c18e7ae535ef732a3484d43d0d82be9f701483045022100f89034eba16b2be0e5581f750a0a6309192b75cce0f202f0ee2b4ec0cc394850022076c65dc507fe42276152b7a3d90e961e678adbe966e916ecfe85e64d430e75f301475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"3045022100f89034eba16b2be0e5581f750a0a6309192b75cce0f202f0ee2b4ec0cc394850022076c65dc507fe42276152b7a3d90e961e678adbe966e916ecfe85e64d430e75f3\"\n  },\n  {\n    \"Name\": \"simple commitment tx with no HTLCs and single anchor\",\n    \"LocalBalance\": 7000000000,\n    \"RemoteBalance\": 0,\n    \"FeePerKw\": 15000,\n    \"UseTestHtlcs\": false,\n    \"HtlcDescs\": [],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80024a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f508b6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100da5310620e72bc23dc57af25d18102cc75479aea0258ab89fe1a66ca176033ec0220339efb450c12872e134c8bda986bb92f3e4eebcaa2d0fee5d9a2b1257d12f12a0147304402200dc30542c9b8b2ff4b8d98f46798b3218a088a07e97b9e786177287dc6a5347b02203d23b1c2bf17262362fdb4cdcc36dbb449a9efcdb10051ad52cfa09fc76842b001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"304402200dc30542c9b8b2ff4b8d98f46798b3218a088a07e97b9e786177287dc6a5347b02203d23b1c2bf17262362fdb4cdcc36dbb449a9efcdb10051ad52cfa09fc76842b0\"\n  },\n  {\n    \"Name\": \"commitment tx with seven outputs untrimmed (maximum feerate)\",\n    \"LocalBalance\": 6988000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 644,\n    \"UseTestHtlcs\": true,\n    \"HtlcDescs\": [\n      {\n        \"RemoteSigHex\": \"304402205912d91c58016f593d9e46fefcdb6f4125055c41a17b03101eaaa034b9028ab60220520d4d239c85c66e4c75c5b413620b62736e227659d7821b308e2b8ced3e728e\",\n        \"ResolutionTxHex\": \"02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a0200000000010000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402205912d91c58016f593d9e46fefcdb6f4125055c41a17b03101eaaa034b9028ab60220520d4d239c85c66e4c75c5b413620b62736e227659d7821b308e2b8ced3e728e834730440220473166a5adcca68550bab80403f410a726b5bd855030527e3fefa8c1e4b4fd7b02203b1dc91d8d69039473036cb5c34398b99e8eb90ae500c22130a557b62294b188012000000000000000000000000000000000000000000000000000000000000000008d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6851b2756800000000\"\n      },\n      {\n        \"RemoteSigHex\": \"3045022100c6b4113678039ee1e43a6cba5e3224ed2355ffc05e365a393afe8843dc9a76860220566d01fd52d65a89ba8595023884f9e8f2e9a310a6b9b85281c0bce06863430c\",\n        \"ResolutionTxHex\": \"02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a0300000000010000000124060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100c6b4113678039ee1e43a6cba5e3224ed2355ffc05e365a393afe8843dc9a76860220566d01fd52d65a89ba8595023884f9e8f2e9a310a6b9b85281c0bce06863430c83483045022100d0d86307ea55d5daa80f453ad6d64b78fe8a6504aac25407c73e8502c0702c1602206a0809a02aa00c8dc4a53d976bb05d4605d8bb0b7b26b973a5c4e2734d8afbb401008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6851b27568f6010000\"\n      },\n      {\n        \"RemoteSigHex\": \"304402203c3a699fb80a38112aafd73d6e3a9b7d40bc2c3ed8b7fbc182a20f43b215172202204e71821b984d1af52c4b8e2cd4c572578c12a965866130c2345f61e4c2d3fef4\",\n        \"ResolutionTxHex\": \"02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a040000000001000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402203c3a699fb80a38112aafd73d6e3a9b7d40bc2c3ed8b7fbc182a20f43b215172202204e71821b984d1af52c4b8e2cd4c572578c12a965866130c2345f61e4c2d3fef48347304402205bcfa92f83c69289a412b0b6dd4f2a0fe0b0fc2d45bd74706e963257a09ea24902203783e47883e60b86240e877fcbf33d50b1742f65bc93b3162d1be26583b367ee012001010101010101010101010101010101010101010101010101010101010101018d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6851b2756800000000\"\n      },\n      {\n        \"RemoteSigHex\": \"304402200f089bcd20f25475216307d32aa5b6c857419624bfba1da07335f51f6ba4645b02206ce0f7153edfba23b0d4b2afc26bb3157d404368cb8ea0ca7cf78590dcdd28cf\",\n        \"ResolutionTxHex\": \"02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a050000000001000000010c0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402200f089bcd20f25475216307d32aa5b6c857419624bfba1da07335f51f6ba4645b02206ce0f7153edfba23b0d4b2afc26bb3157d404368cb8ea0ca7cf78590dcdd28cf83483045022100e4516da08f72c7a4f7b2f37aa84a0feb54ae2cc5b73f0da378e81ae0ca8119bf02207751b2628d8e2f62b4b9abccda4866246c1bfcc82e3d416ad562fd212102c28f01008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000\"\n      },\n      {\n        \"RemoteSigHex\": \"3045022100aa72cfaf0965020c73a12c77276c6411ca68c4de36ac1998adf86c917a899a43022060da0a159fecfe0bed37c3962d767f12f90e30fed8a8f34b1301775c21a2bd3a\",\n        \"ResolutionTxHex\": \"02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a06000000000100000001da0d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100aa72cfaf0965020c73a12c77276c6411ca68c4de36ac1998adf86c917a899a43022060da0a159fecfe0bed37c3962d767f12f90e30fed8a8f34b1301775c21a2bd3a8347304402203cd12065c2a42963c762e6b1a981e17695616ecb6f9fb33d8b0717cdd7ca0ee4022065500005c491c1dcf2fe9c4024f74b1c90785d572527055a491278f901143904012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000\"\n      }\n    ],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80094a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994e80300000000000022002010f88bf09e56f14fb4543fd26e47b0db50ea5de9cf3fc46434792471082621aed0070000000000002200203e68115ae0b15b8de75b6c6bc9af5ac9f01391544e0870dae443a1e8fe7837ead007000000000000220020fe0598d74fee2205cc3672e6e6647706b4f3099713b4661b62482c3addd04a5eb80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a4f996a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100ef82a405364bfc4007e63a7cc82925a513d79065bdbc216d60b6a4223a323f8a02200716730b8561f3c6d362eaf47f202e99fb30d0557b61b92b5f9134f8e2de368101483045022100e0106830467a558c07544a3de7715610c1147062e7d091deeebe8b5c661cda9402202ad049c1a6d04834317a78483f723c205c9f638d17222aafc620800cc1b6ae3501475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"3045022100e0106830467a558c07544a3de7715610c1147062e7d091deeebe8b5c661cda9402202ad049c1a6d04834317a78483f723c205c9f638d17222aafc620800cc1b6ae35\"\n  },\n  {\n    \"Name\": \"commitment tx with six outputs untrimmed (minimum feerate)\",\n    \"LocalBalance\": 6988000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 645,\n    \"UseTestHtlcs\": true,\n    \"HtlcDescs\": [\n      {\n        \"RemoteSigHex\": \"30440220446f9e5c375db6a61d6eeee8b59219a30a4a37372afc2670a1a2889c78e9b943022061895f6088fb48b490ab2140a4842c277b64bf25ff591625dd0356e0c96ab7a8\",\n        \"ResolutionTxHex\": \"02000000000101104f394af4c4fad78337f95e3e9f802f4c0d86ab231853af09b28534856132000200000000010000000123060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220446f9e5c375db6a61d6eeee8b59219a30a4a37372afc2670a1a2889c78e9b943022061895f6088fb48b490ab2140a4842c277b64bf25ff591625dd0356e0c96ab7a883483045022100c1621ba26a99c263fd885feff5fda5ca2cc73df080b3a49ecf15164ee244d2a5022037f4cc7fd4441af39a83a0e44c3b1db7d64a4c8080e8697f9e952f85421a34d801008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6851b27568f6010000\"\n      },\n      {\n        \"RemoteSigHex\": \"3044022027a3ffcb8a007e3349d75382efbd4b3fb99fcbd479a18555e58697bd1278d5c402205c8303d46211c3ae8975fe84a0df08b4623119fecd03bc93b49d7f7a0c64c710\",\n        \"ResolutionTxHex\": \"02000000000101104f394af4c4fad78337f95e3e9f802f4c0d86ab231853af09b28534856132000300000000010000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022027a3ffcb8a007e3349d75382efbd4b3fb99fcbd479a18555e58697bd1278d5c402205c8303d46211c3ae8975fe84a0df08b4623119fecd03bc93b49d7f7a0c64c71083483045022100b697aca55c6fb15e5348bb7387b584815fd15e8dd306afe0c477cb550d0c2d40022050b0f7e370f7604d2fec781fefe86715dbe95dff4dab88d628f509d62f854de1012001010101010101010101010101010101010101010101010101010101010101018d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6851b2756800000000\"\n      },\n      {\n        \"RemoteSigHex\": \"30440220013975ae356e6daf22a86a29f21c4f35aca82ed8f731a1103c60c74f5ed1c5aa02200350d4e5455cdbcacb7ccf174db5bed8286019e509a113f6b4c5e606ee12c9d7\",\n        \"ResolutionTxHex\": \"02000000000101104f394af4c4fad78337f95e3e9f802f4c0d86ab231853af09b2853485613200040000000001000000010b0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220013975ae356e6daf22a86a29f21c4f35aca82ed8f731a1103c60c74f5ed1c5aa02200350d4e5455cdbcacb7ccf174db5bed8286019e509a113f6b4c5e606ee12c9d783483045022100e69a29f78779577830e73f327073c93168896f1b89432124b7846f5def9cd9cb02204433db3697e6ed7ac89574ca066a749640e0c9e114ac2e0ee4545741fcf7b7e901008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000\"\n      },\n      {\n        \"RemoteSigHex\": \"304402205257017423644c7e831f30bc0c334eecfe66e9a6d2e92d157c5bece576b2be4f022047b21cf8e955e22b7471940563922d1a5852fb95459ca32905c7d46a19141664\",\n        \"ResolutionTxHex\": \"02000000000101104f394af4c4fad78337f95e3e9f802f4c0d86ab231853af09b285348561320005000000000100000001d90d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402205257017423644c7e831f30bc0c334eecfe66e9a6d2e92d157c5bece576b2be4f022047b21cf8e955e22b7471940563922d1a5852fb95459ca32905c7d46a191416648347304402204f5de65a624e3f757adffb678bd887eb4e656538c5ea7044922f6ee3eed8a06202206ff6f7bfe73b565343cae76131ac658f1a9c60d3ca2343358cda60b9e35f94c8012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000\"\n      }\n    ],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80084a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994d0070000000000002200203e68115ae0b15b8de75b6c6bc9af5ac9f01391544e0870dae443a1e8fe7837ead007000000000000220020fe0598d74fee2205cc3672e6e6647706b4f3099713b4661b62482c3addd04a5eb80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994abc996a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100d57697c707b6f6d053febf24b98e8989f186eea42e37e9e91663ec2c70bb8f70022079b0715a472118f262f43016a674f59c015d9cafccec885968e76d9d9c5d005101473044022025d97466c8049e955a5afce28e322f4b34d2561118e52332fb400f9b908cc0a402205dc6fba3a0d67ee142c428c535580cd1f2ff42e2f89b47e0c8a01847caffc31201475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"3044022025d97466c8049e955a5afce28e322f4b34d2561118e52332fb400f9b908cc0a402205dc6fba3a0d67ee142c428c535580cd1f2ff42e2f89b47e0c8a01847caffc312\"\n  },\n  {\n    \"Name\": \"commitment tx with six outputs untrimmed (maximum feerate)\",\n    \"LocalBalance\": 6988000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 2060,\n    \"UseTestHtlcs\": true,\n    \"HtlcDescs\": [\n      {\n        \"RemoteSigHex\": \"30440220011f999016570bbab9f3125377d0f35096b4dbe155f97c20f71829ead2817d1602201f23f7e17f6928734601c5d8613431eed5c90aa41c3106e8c1cb02ce32aacb5d\",\n        \"ResolutionTxHex\": \"02000000000101e7f364cf3a554b670767e723ef14b2af7a3eac70bd79dbde9256f384369c062d0200000000010000000175020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220011f999016570bbab9f3125377d0f35096b4dbe155f97c20f71829ead2817d1602201f23f7e17f6928734601c5d8613431eed5c90aa41c3106e8c1cb02ce32aacb5d83473044022017da96dfb0eb4061fa0162dc6fa6b2e07ecc5040ab5e6cb07be59838460b3e58022079371ffc95002cc1dc2891ec38198c9c25aca8164304fe114f1b55e2ffd1ddd501008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6851b27568f6010000\"\n      },\n      {\n        \"RemoteSigHex\": \"304402202d2d9681409b0a0987bd4a268ffeb112df85c4c988ac2a3a2475cb00a61912c302206aa4f4d1388b7d3282bc847871af3cca30766cc8f1064e3a41ec7e82221e10f7\",\n        \"ResolutionTxHex\": \"02000000000101e7f364cf3a554b670767e723ef14b2af7a3eac70bd79dbde9256f384369c062d0300000000010000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402202d2d9681409b0a0987bd4a268ffeb112df85c4c988ac2a3a2475cb00a61912c302206aa4f4d1388b7d3282bc847871af3cca30766cc8f1064e3a41ec7e82221e10f78347304402206426d67911aa6ff9b1cb147b093f3f65a37831a86d7c741d999afc0666e1773d022000bb71821650c70ea58d9bcdd03af736c41a5a8159d436c3ee0408a07394dcce012001010101010101010101010101010101010101010101010101010101010101018d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6851b2756800000000\"\n      },\n      {\n        \"RemoteSigHex\": \"3045022100f51cdaa525b7d4304548c642bb7945215eb5ae7d32874517cde67ca23ab0a12202206286d59e4b19926c6ac844be6f3ab8149a1ddb9c70f5026b7e83e40a6c08e6e1\",\n        \"ResolutionTxHex\": \"02000000000101e7f364cf3a554b670767e723ef14b2af7a3eac70bd79dbde9256f384369c062d040000000001000000015d060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100f51cdaa525b7d4304548c642bb7945215eb5ae7d32874517cde67ca23ab0a12202206286d59e4b19926c6ac844be6f3ab8149a1ddb9c70f5026b7e83e40a6c08e6e18348304502210091b16b1ac63b867e7a5ca0344f7b2aa1cdd49d4b72eac86a31e7ec6f069e20640220402bfb571ba3a9c49e3b0061c89303453803d0241059d899222aaac4799b507601008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000\"\n      },\n      {\n        \"RemoteSigHex\": \"304402202f058d99cb5a54f90773d43ba4e7a0089efd9f8269ef2da1b85d48a3e230555402205acc4bd6561830867d45cd7b84bba9fa35ad2b345016471c1737142bc99782c4\",\n        \"ResolutionTxHex\": \"02000000000101e7f364cf3a554b670767e723ef14b2af7a3eac70bd79dbde9256f384369c062d05000000000100000001f2090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402202f058d99cb5a54f90773d43ba4e7a0089efd9f8269ef2da1b85d48a3e230555402205acc4bd6561830867d45cd7b84bba9fa35ad2b345016471c1737142bc99782c48347304402202913f9cacea54efd2316cffa91219def9e0e111977216c1e76e9da80befab14f022000a9a69e8f37ebe4a39107ab50fab0dde537334588f8f412bbaca57b179b87a6012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000\"\n      }\n    ],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80084a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994d0070000000000002200203e68115ae0b15b8de75b6c6bc9af5ac9f01391544e0870dae443a1e8fe7837ead007000000000000220020fe0598d74fee2205cc3672e6e6647706b4f3099713b4661b62482c3addd04a5eb80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994ab88f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402201ce37a44b95213358c20f44404d6db7a6083bea6f58de6c46547ae41a47c9f8202206db1d45be41373e92f90d346381febbea8c78671b28c153e30ad1db3441a94970147304402206208aeb34e404bd052ce3f298dfa832891c9d42caec99fe2a0d2832e9690b94302201b034bfcc6fa9faec667a9b7cbfe0b8d85e954aa239b66277887b5088aff08c301475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"304402206208aeb34e404bd052ce3f298dfa832891c9d42caec99fe2a0d2832e9690b94302201b034bfcc6fa9faec667a9b7cbfe0b8d85e954aa239b66277887b5088aff08c3\"\n  },\n  {\n    \"Name\": \"commitment tx with five outputs untrimmed (minimum feerate)\",\n    \"LocalBalance\": 6988000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 2061,\n    \"UseTestHtlcs\": true,\n    \"HtlcDescs\": [\n      {\n        \"RemoteSigHex\": \"3045022100e10744f572a2cd1d787c969e894b792afaed21217ee0480df0112d2fa3ef96ea02202af4f66eb6beebc36d8e98719ed6b4be1b181659fcb561fc491d8cfebff3aa85\",\n        \"ResolutionTxHex\": \"02000000000101cf32732fe2d1387ed4e2335f69ddd3c0f337dabc03269e742531f89d35e161d10200000000010000000174020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e10744f572a2cd1d787c969e894b792afaed21217ee0480df0112d2fa3ef96ea02202af4f66eb6beebc36d8e98719ed6b4be1b181659fcb561fc491d8cfebff3aa8583483045022100c3dc3ea50a0ca20e350f97b50c52c5514717cfa36cb9600918caac5cb556842b022049af018d676dde0c8e28ecf325f3ff5c1594261c4f7511d501f9d62d0594d2a201008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6851b27568f6010000\"\n      },\n      {\n        \"RemoteSigHex\": \"3045022100e1f51fb72fec604b029b348a3bb6363454e1869f5b1e24fd736f860c8039f8070220030a2c90186437d8c9b47d4897798c024521b1274991c4cdc125970b346094b1\",\n        \"ResolutionTxHex\": \"02000000000101cf32732fe2d1387ed4e2335f69ddd3c0f337dabc03269e742531f89d35e161d1030000000001000000015c060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e1f51fb72fec604b029b348a3bb6363454e1869f5b1e24fd736f860c8039f8070220030a2c90186437d8c9b47d4897798c024521b1274991c4cdc125970b346094b183483045022100ec7ade6037e531629f24390ca9713782a04d648065d17fbe6b015981cdb296c202202d61049a6ecba2fb5314f3edcda2361cad187a89bea6e5d15185354d80c0c08501008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000\"\n      },\n      {\n        \"RemoteSigHex\": \"304402203479f81a1d83c516957679dc98bf91d35deada967739a8e3869e3e8db08246130220053c8e154b97e3019048dcec3d51bfaf396f36861fbda6d33f0e2a57155c8b9f\",\n        \"ResolutionTxHex\": \"02000000000101cf32732fe2d1387ed4e2335f69ddd3c0f337dabc03269e742531f89d35e161d104000000000100000001f1090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402203479f81a1d83c516957679dc98bf91d35deada967739a8e3869e3e8db08246130220053c8e154b97e3019048dcec3d51bfaf396f36861fbda6d33f0e2a57155c8b9f83483045022100a558eb5caa04e35a4417c1f0123ac12eec5f6badee28f5764dc6b69486e594f802201589b12784e242f205832d2d032149bd4e79433ec304c05394241fc7dcba5a71012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000\"\n      }\n    ],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80074a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994d0070000000000002200203e68115ae0b15b8de75b6c6bc9af5ac9f01391544e0870dae443a1e8fe7837eab80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a18916a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402204ab07c659412dd2cd6043b1ad811ab215e901b6b5653e08cb3d2fe63d3e3dc57022031c7b3d130f9380ef09581f4f5a15cb6f359a2e0a597146b96c3533a26d6f4cd01483045022100a2faf2ad7e323b2a82e07dc40b6847207ca6ad7b089f2c21dea9a4d37e52d59d02204c9480ce0358eb51d92a4342355a97e272e3cc45f86c612a76a3fe32fc3c4cb401475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"3045022100a2faf2ad7e323b2a82e07dc40b6847207ca6ad7b089f2c21dea9a4d37e52d59d02204c9480ce0358eb51d92a4342355a97e272e3cc45f86c612a76a3fe32fc3c4cb4\"\n  },\n  {\n    \"Name\": \"commitment tx with five outputs untrimmed (maximum feerate)\",\n    \"LocalBalance\": 6988000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 2184,\n    \"UseTestHtlcs\": true,\n    \"HtlcDescs\": [\n      {\n        \"RemoteSigHex\": \"304402202e03ba1390998b3487e9a7fefcb66814c09abea0ef1bcc915dbaefbcf310569a02206bd10493a105ac69048e9bcedcb8e3301ef81b55018d911a4afd297297f98d30\",\n        \"ResolutionTxHex\": \"020000000001015b03043e20eb467029305a22af4c3b915e793743f192c5d225cf1d3c6e8c03010200000000010000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402202e03ba1390998b3487e9a7fefcb66814c09abea0ef1bcc915dbaefbcf310569a02206bd10493a105ac69048e9bcedcb8e3301ef81b55018d911a4afd297297f98d308347304402200c3952ca04be0c60dcc0b7873a0829f560607524943554ae4a27d8d967166199022021a68657b88e22f9bf9ac6065be412685aff643d17049f04f2e99e86197dabb101008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6851b27568f6010000\"\n      },\n      {\n        \"RemoteSigHex\": \"304402201f8a6adda2403bc400c919ea69d72d315337291e00d02cde085ea32953dbc50002202d65230da98df7af8ebefd2b60b457d0945232988ee2d7460a94a77d414a9acc\",\n        \"ResolutionTxHex\": \"020000000001015b03043e20eb467029305a22af4c3b915e793743f192c5d225cf1d3c6e8c0301030000000001000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402201f8a6adda2403bc400c919ea69d72d315337291e00d02cde085ea32953dbc50002202d65230da98df7af8ebefd2b60b457d0945232988ee2d7460a94a77d414a9acc83483045022100ea69c9273b8914ac62b5b7082d6ac1da2b7b065ebf2ef3cd6403f5305ce3f26802203d98736ea97638895a898dfcc5ee0d0c55eb496b3964df0bb25d223688ea8b8701008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000\"\n      },\n      {\n        \"RemoteSigHex\": \"3045022100ea6e4c9b8f56dd9cf5799492a201cdd65b8bc9bc089c3cff34107896ae313f90022034760f7760975cc68e8917a7f62894e25583da7be11af557c4fc402661d0cbf8\",\n        \"ResolutionTxHex\": \"020000000001015b03043e20eb467029305a22af4c3b915e793743f192c5d225cf1d3c6e8c0301040000000001000000019b090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100ea6e4c9b8f56dd9cf5799492a201cdd65b8bc9bc089c3cff34107896ae313f90022034760f7760975cc68e8917a7f62894e25583da7be11af557c4fc402661d0cbf8834730440220717012f2f7ef6cac590aaf66c2109132c93ffba245959ac62d82e394ba80191302203f00fd9cb37c92c6b0ad4b33e62c3e55b04e5c2cfa0adcca5a9bc49774eeca8a012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000\"\n      }\n    ],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80074a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994d0070000000000002200203e68115ae0b15b8de75b6c6bc9af5ac9f01391544e0870dae443a1e8fe7837eab80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a4f906a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220555c05261f72c5b4702d5c83a608630822b473048724b08640d6e75e345094250220448950b74a96a56963928ba5db8b457661a742c855e69d239b3b6ab73de307a301473044022013d326f80ff7607cf366c823fcbbcb7a2b10322484825f151e6c4c756af24b8f02201ba05b9d8beb7cea2947f9f4d9e03f90435e93db2dd48b32eb9ca3f3dd042c7901475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"3044022013d326f80ff7607cf366c823fcbbcb7a2b10322484825f151e6c4c756af24b8f02201ba05b9d8beb7cea2947f9f4d9e03f90435e93db2dd48b32eb9ca3f3dd042c79\"\n  },\n  {\n    \"Name\": \"commitment tx with four outputs untrimmed (minimum feerate)\",\n    \"LocalBalance\": 6988000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 2185,\n    \"UseTestHtlcs\": true,\n    \"HtlcDescs\": [\n      {\n        \"RemoteSigHex\": \"304502210094480e38afb41d10fae299224872f19c53abe23c7033a1c0642c48713e7863a10220726dd9456407682667dc4bd9c66975acb3744961770b5002f7eb9c0df9ef2f3e\",\n        \"ResolutionTxHex\": \"02000000000101ac13a7715f80b8e52dda43c6929cade5521bdced3a405da02b443f1ffb1e33cc0200000000010000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050048304502210094480e38afb41d10fae299224872f19c53abe23c7033a1c0642c48713e7863a10220726dd9456407682667dc4bd9c66975acb3744961770b5002f7eb9c0df9ef2f3e8347304402203148dac61513dc0361738cba30cb341a1e580f8acd5ab0149bf65bd670688cf002207e5d9a0fcbbea2c263bc714fa9e9c44d7f582ea447f366119fc614a23de32f1f01008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000\"\n      },\n      {\n        \"RemoteSigHex\": \"304402200dbde868dbc20c6a2433fe8979ba5e3f966b1c2d1aeb615f1c42e9c938b3495402202eec5f663c8b601c2061c1453d35de22597c137d1907a2feaf714d551035cb6e\",\n        \"ResolutionTxHex\": \"02000000000101ac13a7715f80b8e52dda43c6929cade5521bdced3a405da02b443f1ffb1e33cc030000000001000000019a090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402200dbde868dbc20c6a2433fe8979ba5e3f966b1c2d1aeb615f1c42e9c938b3495402202eec5f663c8b601c2061c1453d35de22597c137d1907a2feaf714d551035cb6e83483045022100b896bded41d7feac7af25c19e35c53037c53b50e73cfd01eb4ba139c7fdf231602203a3be049d3d89396c4dc766d82ce31e237da8bc3a93e2c7d35992d1932d9cfeb012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000\"\n      }\n    ],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80064a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994b80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994ac5916a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100cd8479cfe1edb1e5a1d487391e0451a469c7171e51e680183f19eb4321f20e9b02204eab7d5a6384b1b08e03baa6e4d9748dfd2b5ab2bae7e39604a0d0055bbffdd501473044022040f63a16148cf35c8d3d41827f5ae7f7c3746885bb64d4d1b895892a83812b3e02202fcf95c2bf02c466163b3fa3ced6a24926fbb4035095a96842ef516e86ba54c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"3044022040f63a16148cf35c8d3d41827f5ae7f7c3746885bb64d4d1b895892a83812b3e02202fcf95c2bf02c466163b3fa3ced6a24926fbb4035095a96842ef516e86ba54c0\"\n  },\n  {\n    \"Name\": \"commitment tx with four outputs untrimmed (maximum feerate)\",\n    \"LocalBalance\": 6988000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 3686,\n    \"UseTestHtlcs\": true,\n    \"HtlcDescs\": [\n      {\n        \"RemoteSigHex\": \"304402202cfe6618926ca9f1574f8c4659b425e9790b4677ba2248d77901290806130ffe02204ab37bb0287abcdb8b750b018d41a09effe37cb65ff801fa70d3f1a416599841\",\n        \"ResolutionTxHex\": \"020000000001012c32e55722e4b96324d8e5b398d583a20780b25202816adc32dc3157dee731c90200000000010000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402202cfe6618926ca9f1574f8c4659b425e9790b4677ba2248d77901290806130ffe02204ab37bb0287abcdb8b750b018d41a09effe37cb65ff801fa70d3f1a41659984183473044022030b318139715e3b34f19be852cc01c1c0e1599e8b926a73df2bfb70dd186ddee022062a2b7398aed9f563b4014da04a1a99debd0ff663ceece68a547df5982dc2d7201008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000\"\n      },\n      {\n        \"RemoteSigHex\": \"30440220687af8544d335376620a6f4b5412bfd0da48de047c1785674f26e669d4a3ff82022058591c1e3a6c50017427d38a8f756eb685bdab88ec73838eed3530048861f9d5\",\n        \"ResolutionTxHex\": \"020000000001012c32e55722e4b96324d8e5b398d583a20780b25202816adc32dc3157dee731c90300000000010000000176050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220687af8544d335376620a6f4b5412bfd0da48de047c1785674f26e669d4a3ff82022058591c1e3a6c50017427d38a8f756eb685bdab88ec73838eed3530048861f9d5834730440220109f1a62b5a13d28d5b7634dd7693b1d5994eb404c4bb4a9a80aa540d3984d170220307251107ff8499a23e99abce7dda4f1c707c98abddb9405a83de0081cde8ace012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000\"\n      }\n    ],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80064a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994b80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a29896a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100c268496aad5c3f97f25cf41c1ba5483a12982de29b222051b6de3daa2229413b02207f3c82d77a2c14f0096ed9bb4c34649483bb20fa71f819f71af44de6593e8bb2014730440220784485cf7a0ad7979daf2c858ffdaf5298d0020cea7aea466843e7948223bd9902206031b81d25e02a178c64e62f843577fdcdfc7a1decbbfb54cd895de692df85ca01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"30440220784485cf7a0ad7979daf2c858ffdaf5298d0020cea7aea466843e7948223bd9902206031b81d25e02a178c64e62f843577fdcdfc7a1decbbfb54cd895de692df85ca\"\n  },\n  {\n    \"Name\": \"commitment tx with three outputs untrimmed (minimum feerate)\",\n    \"LocalBalance\": 6988000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 3687,\n    \"UseTestHtlcs\": true,\n    \"HtlcDescs\": [\n      {\n        \"RemoteSigHex\": \"3045022100b287bb8e079a62dcb3aaa8b6c67c0f434a87ebf64ab0bcfb2fc14b55576b859f02206d37c2eb5fd04cfc9eb0534c76a28a98da251b84a931377cce307af39dfaed74\",\n        \"ResolutionTxHex\": \"02000000000101542562b326c08e3a076d9cfca2be175041366591da334d8d513ff1686fd95a600200000000010000000175050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100b287bb8e079a62dcb3aaa8b6c67c0f434a87ebf64ab0bcfb2fc14b55576b859f02206d37c2eb5fd04cfc9eb0534c76a28a98da251b84a931377cce307af39dfaed7483483045022100a497c64faea286ec4221f48628086dc6403fd7b60a23c4176e8ebbca15ae70dc0220754e20e968e96cf6421fd2a672c8c26d3bc6e19218cfc8fc2aa51fce026c14b1012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000\"\n      }\n    ],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80054a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994aa28b6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100c970799bcb33f43179eb43b3378a0a61991cf2923f69b36ef12548c3df0e6d500220413dc27d2e39ee583093adfcb7799be680141738babb31cc7b0669a777a31f5d01483045022100ad6c71569856b2d7ff42e838b4abe74a713426b37f22fa667a195a4c88908c6902202b37272b02a42dc6d9f4f82cab3eaf84ac882d9ed762859e1e75455c2c22837701475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"3045022100ad6c71569856b2d7ff42e838b4abe74a713426b37f22fa667a195a4c88908c6902202b37272b02a42dc6d9f4f82cab3eaf84ac882d9ed762859e1e75455c2c228377\"\n  },\n  {\n    \"Name\": \"commitment tx with three outputs untrimmed (maximum feerate)\",\n    \"LocalBalance\": 6988000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 4893,\n    \"UseTestHtlcs\": true,\n    \"HtlcDescs\": [\n      {\n        \"RemoteSigHex\": \"30450221008db80f8531104820b3e894492b4463f074f965b542e1b5c153ddfb108a5ea642022030b203d857a2b3581c2087a7bf17c95d04fadc1c6cdae88c620477f2dccb1ee4\",\n        \"ResolutionTxHex\": \"02000000000101d515a15e9175fd315bb8d4e768f28684801a9e5a9acdfeba34f7b3b3b3a9ba1d0200000000010000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221008db80f8531104820b3e894492b4463f074f965b542e1b5c153ddfb108a5ea642022030b203d857a2b3581c2087a7bf17c95d04fadc1c6cdae88c620477f2dccb1ee483483045022100e5fbae857c47dbfc050a05924bd449fc9804798bd6442002c578437dc34450810220296589bc387645512345299e307116aaac4ce9fc752abcd1936b802d03526312012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000\"\n      }\n    ],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80054a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a87856a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220086288faceab47461eb2d808e9e9b0cb3ffc24a03c2f18db7198247d38f10e58022031d1c2782a58c8c6ce187d0019eb47a83babdf3040e2caff299ab48f7e12b1fa01483045022100a8771147109e4d3f44a5976c3c3de98732bbb77308d21444dbe0d76faf06480e02200b4e916e850c3d1f918de87bbbbb07843ffea1d4658dfe060b6f9ccd96d34be801475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"3045022100a8771147109e4d3f44a5976c3c3de98732bbb77308d21444dbe0d76faf06480e02200b4e916e850c3d1f918de87bbbbb07843ffea1d4658dfe060b6f9ccd96d34be8\"\n  },\n  {\n    \"Name\": \"commitment tx with two outputs untrimmed (minimum feerate)\",\n    \"LocalBalance\": 6988000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 4894,\n    \"UseTestHtlcs\": true,\n    \"HtlcDescs\": [],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80044a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994c0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994ad0886a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004830450221009f16ac85d232e4eddb3fcd750a68ebf0b58e3356eaada45d3513ede7e817bf4c02207c2b043b4e5f971261975406cb955219fa56bffe5d834a833694b5abc1ce4cfd01483045022100e784a66b1588575801e237d35e510fd92a81ae3a4a2a1b90c031ad803d07b3f3022021bc5f16501f167607d63b681442da193eb0a76b4b7fd25c2ed4f8b28fd35b9501475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"3045022100e784a66b1588575801e237d35e510fd92a81ae3a4a2a1b90c031ad803d07b3f3022021bc5f16501f167607d63b681442da193eb0a76b4b7fd25c2ed4f8b28fd35b95\"\n  },\n  {\n    \"Name\": \"commitment tx with one output untrimmed (minimum feerate)\",\n    \"LocalBalance\": 6988000000,\n    \"RemoteBalance\": 3000000000,\n    \"FeePerKw\": 6216010,\n    \"UseTestHtlcs\": true,\n    \"HtlcDescs\": [],\n    \"ExpectedCommitmentTxHex\": \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80024a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994c0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a04004830450221009ad80792e3038fe6968d12ff23e6888a565c3ddd065037f357445f01675d63f3022018384915e5f1f4ae157e15debf4f49b61c8d9d2b073c7d6f97c4a68caa3ed4c1014830450221008fd5dbff02e4b59020d4cd23a3c30d3e287065fda75a0a09b402980adf68ccda022001e0b8b620cd915ddff11f1de32addf23d81d51b90e6841b2cb8dcaf3faa5ecf01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\",\n    \"RemoteSigHex\": \"30450221008fd5dbff02e4b59020d4cd23a3c30d3e287065fda75a0a09b402980adf68ccda022001e0b8b620cd915ddff11f1de32addf23d81d51b90e6841b2cb8dcaf3faa5ecf\"\n  }\n]"
  },
  {
    "path": "tests/bip-0341/wallet-test-vectors.json",
    "content": "{\n    \"version\": 1,\n    \"scriptPubKey\": [\n        {\n            \"given\": {\n                \"internalPubkey\": \"d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d\",\n                \"scriptTree\": null\n            },\n            \"intermediary\": {\n                \"merkleRoot\": null,\n                \"tweak\": \"b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70\",\n                \"tweakedPubkey\": \"53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343\"\n            },\n            \"expected\": {\n                \"scriptPubKey\": \"512053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343\",\n                \"bip350Address\": \"bc1p2wsldez5mud2yam29q22wgfh9439spgduvct83k3pm50fcxa5dps59h4z5\"\n            }\n        },\n        {\n            \"given\": {\n                \"internalPubkey\": \"187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27\",\n                \"scriptTree\": {\n                    \"id\": 0,\n                    \"script\": \"20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac\",\n                    \"leafVersion\": 192\n                }\n            },\n            \"intermediary\": {\n                \"leafHashes\": [\n                    \"5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21\"\n                ],\n                \"merkleRoot\": \"5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21\",\n                \"tweak\": \"cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001\",\n                \"tweakedPubkey\": \"147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3\"\n            },\n            \"expected\": {\n                \"scriptPubKey\": \"5120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3\",\n                \"bip350Address\": \"bc1pz37fc4cn9ah8anwm4xqqhvxygjf9rjf2resrw8h8w4tmvcs0863sa2e586\",\n                \"scriptPathControlBlocks\": [\n                    \"c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27\"\n                ]\n            }\n        },\n        {\n            \"given\": {\n                \"internalPubkey\": \"93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820\",\n                \"scriptTree\": {\n                    \"id\": 0,\n                    \"script\": \"20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac\",\n                    \"leafVersion\": 192\n                }\n            },\n            \"intermediary\": {\n                \"leafHashes\": [\n                    \"c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b\"\n                ],\n                \"merkleRoot\": \"c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b\",\n                \"tweak\": \"6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30\",\n                \"tweakedPubkey\": \"e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e\"\n            },\n            \"expected\": {\n                \"scriptPubKey\": \"5120e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e\",\n                \"bip350Address\": \"bc1punvppl2stp38f7kwv2u2spltjuvuaayuqsthe34hd2dyy5w4g58qqfuag5\",\n                \"scriptPathControlBlocks\": [\n                    \"c093478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820\"\n                ]\n            }\n        },\n        {\n            \"given\": {\n                \"internalPubkey\": \"ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592\",\n                \"scriptTree\": [\n                    {\n                        \"id\": 0,\n                        \"script\": \"20387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48ac\",\n                        \"leafVersion\": 192\n                    },\n                    {\n                        \"id\": 1,\n                        \"script\": \"06424950333431\",\n                        \"leafVersion\": 250\n                    }\n                ]\n            },\n            \"intermediary\": {\n                \"leafHashes\": [\n                    \"8ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7\",\n                    \"f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a\"\n                ],\n                \"merkleRoot\": \"6c2dc106ab816b73f9d07e3cd1ef2c8c1256f519748e0813e4edd2405d277bef\",\n                \"tweak\": \"9e0517edc8259bb3359255400b23ca9507f2a91cd1e4250ba068b4eafceba4a9\",\n                \"tweakedPubkey\": \"712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5\"\n            },\n            \"expected\": {\n                \"scriptPubKey\": \"5120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5\",\n                \"bip350Address\": \"bc1pwyjywgrd0ffr3tx8laflh6228dj98xkjj8rum0zfpd6h0e930h6saqxrrm\",\n                \"scriptPathControlBlocks\": [\n                    \"c0ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a\",\n                    \"faee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf37865928ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7\"\n                ]\n            }\n        },\n        {\n            \"given\": {\n                \"internalPubkey\": \"f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8\",\n                \"scriptTree\": [\n                    {\n                        \"id\": 0,\n                        \"script\": \"2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac\",\n                        \"leafVersion\": 192\n                    },\n                    {\n                        \"id\": 1,\n                        \"script\": \"07546170726f6f74\",\n                        \"leafVersion\": 192\n                    }\n                ]\n            },\n            \"intermediary\": {\n                \"leafHashes\": [\n                    \"64512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89\",\n                    \"2cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb\"\n                ],\n                \"merkleRoot\": \"ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc\",\n                \"tweak\": \"639f0281b7ac49e742cd25b7f188657626da1ad169209078e2761cefd91fd65e\",\n                \"tweakedPubkey\": \"77e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220\"\n            },\n            \"expected\": {\n                \"scriptPubKey\": \"512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220\",\n                \"bip350Address\": \"bc1pwl3s54fzmk0cjnpl3w9af39je7pv5ldg504x5guk2hpecpg2kgsqaqstjq\",\n                \"scriptPathControlBlocks\": [\n                    \"c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd82cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb\",\n                    \"c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd864512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89\"\n                ]\n            }\n        },\n        {\n            \"given\": {\n                \"internalPubkey\": \"e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f\",\n                \"scriptTree\": [\n                    {\n                        \"id\": 0,\n                        \"script\": \"2072ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69ac\",\n                        \"leafVersion\": 192\n                    },\n                    [\n                        {\n                            \"id\": 1,\n                            \"script\": \"202352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8ac\",\n                            \"leafVersion\": 192\n                        },\n                        {\n                            \"id\": 2,\n                            \"script\": \"207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac\",\n                            \"leafVersion\": 192\n                        }\n                    ]\n                ]\n            },\n            \"intermediary\": {\n                \"leafHashes\": [\n                    \"2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817\",\n                    \"ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c\",\n                    \"9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf6\"\n                ],\n                \"merkleRoot\": \"ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2\",\n                \"tweak\": \"b57bfa183d28eeb6ad688ddaabb265b4a41fbf68e5fed2c72c74de70d5a786f4\",\n                \"tweakedPubkey\": \"91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605\"\n            },\n            \"expected\": {\n                \"scriptPubKey\": \"512091b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605\",\n                \"bip350Address\": \"bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e\",\n                \"scriptPathControlBlocks\": [\n                    \"c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fffe578e9ea769027e4f5a3de40732f75a88a6353a09d767ddeb66accef85e553\",\n                    \"c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817\",\n                    \"c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817\"\n                ]\n            }\n        },\n        {\n            \"given\": {\n                \"internalPubkey\": \"55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d\",\n                \"scriptTree\": [\n                    {\n                        \"id\": 0,\n                        \"script\": \"2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac\",\n                        \"leafVersion\": 192\n                    },\n                    [\n                        {\n                            \"id\": 1,\n                            \"script\": \"20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac\",\n                            \"leafVersion\": 192\n                        },\n                        {\n                            \"id\": 2,\n                            \"script\": \"20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac\",\n                            \"leafVersion\": 192\n                        }\n                    ]\n                ]\n            },\n            \"intermediary\": {\n                \"leafHashes\": [\n                    \"f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d\",\n                    \"737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711\",\n                    \"d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7\"\n                ],\n                \"merkleRoot\": \"2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def\",\n                \"tweak\": \"6579138e7976dc13b6a92f7bfd5a2fc7684f5ea42419d43368301470f3b74ed9\",\n                \"tweakedPubkey\": \"75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831\"\n            },\n            \"expected\": {\n                \"scriptPubKey\": \"512075169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831\",\n                \"bip350Address\": \"bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe\",\n                \"scriptPathControlBlocks\": [\n                    \"c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d3cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91\",\n                    \"c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312dd7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d\",\n                    \"c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d\"\n                ]\n            }\n        }\n    ],\n    \"keyPathSpending\": [\n        {\n            \"given\": {\n                \"rawUnsignedTx\": \"02000000097de20cbff686da83a54981d2b9bab3586f4ca7e48f57f5b55963115f3b334e9c010000000000000000d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd990000000000fffffffff8e1f583384333689228c5d28eac13366be082dc57441760d957275419a418420000000000fffffffff0689180aa63b30cb162a73c6d2a38b7eeda2a83ece74310fda0843ad604853b0100000000feffffffaa5202bdf6d8ccd2ee0f0202afbbb7461d9264a25e5bfd3c5a52ee1239e0ba6c0000000000feffffff956149bdc66faa968eb2be2d2faa29718acbfe3941215893a2a3446d32acd050000000000000000000e664b9773b88c09c32cb70a2a3e4da0ced63b7ba3b22f848531bbb1d5d5f4c94010000000000000000e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf0000000000ffffffffa778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af10100000000ffffffff0200ca9a3b000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac807840cb0000000020ac9a87f5594be208f8532db38cff670c450ed2fea8fcdefcc9a663f78bab962b0065cd1d\",\n                \"utxosSpent\": [\n                    {\n                        \"scriptPubKey\": \"512053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343\",\n                        \"amountSats\": 420000000\n                    },\n                    {\n                        \"scriptPubKey\": \"5120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3\",\n                        \"amountSats\": 462000000\n                    },\n                    {\n                        \"scriptPubKey\": \"76a914751e76e8199196d454941c45d1b3a323f1433bd688ac\",\n                        \"amountSats\": 294000000\n                    },\n                    {\n                        \"scriptPubKey\": \"5120e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e\",\n                        \"amountSats\": 504000000\n                    },\n                    {\n                        \"scriptPubKey\": \"512091b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605\",\n                        \"amountSats\": 630000000\n                    },\n                    {\n                        \"scriptPubKey\": \"00147dd65592d0ab2fe0d0257d571abf032cd9db93dc\",\n                        \"amountSats\": 378000000\n                    },\n                    {\n                        \"scriptPubKey\": \"512075169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831\",\n                        \"amountSats\": 672000000\n                    },\n                    {\n                        \"scriptPubKey\": \"5120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5\",\n                        \"amountSats\": 546000000\n                    },\n                    {\n                        \"scriptPubKey\": \"512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220\",\n                        \"amountSats\": 588000000\n                    }\n                ]\n            },\n            \"intermediary\": {\n                \"hashAmounts\": \"58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde6\",\n                \"hashOutputs\": \"a2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc5\",\n                \"hashPrevouts\": \"e3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f\",\n                \"hashScriptPubkeys\": \"23ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e21\",\n                \"hashSequences\": \"18959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e\"\n            },\n            \"inputSpending\": [\n                {\n                    \"given\": {\n                        \"txinIndex\": 0,\n                        \"internalPrivkey\": \"6b973d88838f27366ed61c9ad6367663045cb456e28335c109e30717ae0c6baa\",\n                        \"merkleRoot\": null,\n                        \"hashType\": 3\n                    },\n                    \"intermediary\": {\n                        \"internalPubkey\": \"d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d\",\n                        \"tweak\": \"b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70\",\n                        \"tweakedPrivkey\": \"2405b971772ad26915c8dcdf10f238753a9b837e5f8e6a86fd7c0cce5b7296d9\",\n                        \"sigMsg\": \"0003020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e0000000000d0418f0e9a36245b9a50ec87f8bf5be5bcae434337b87139c3a5b1f56e33cba0\",\n                        \"precomputedUsed\": [\n                            \"hashAmounts\",\n                            \"hashPrevouts\",\n                            \"hashScriptPubkeys\",\n                            \"hashSequences\"\n                        ],\n                        \"sigHash\": \"2514a6272f85cfa0f45eb907fcb0d121b808ed37c6ea160a5a9046ed5526d555\"\n                    },\n                    \"expected\": {\n                        \"witness\": [\n                            \"ed7c1647cb97379e76892be0cacff57ec4a7102aa24296ca39af7541246d8ff14d38958d4cc1e2e478e4d4a764bbfd835b16d4e314b72937b29833060b87276c03\"\n                        ]\n                    }\n                },\n                {\n                    \"given\": {\n                        \"txinIndex\": 1,\n                        \"internalPrivkey\": \"1e4da49f6aaf4e5cd175fe08a32bb5cb4863d963921255f33d3bc31e1343907f\",\n                        \"merkleRoot\": \"5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21\",\n                        \"hashType\": 131\n                    },\n                    \"intermediary\": {\n                        \"internalPubkey\": \"187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27\",\n                        \"tweak\": \"cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001\",\n                        \"tweakedPrivkey\": \"ea260c3b10e60f6de018455cd0278f2f5b7e454be1999572789e6a9565d26080\",\n                        \"sigMsg\": \"0083020000000065cd1d00d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd9900000000808f891b00000000225120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3ffffffffffcef8fb4ca7efc5433f591ecfc57391811ce1e186a3793024def5c884cba51d\",\n                        \"precomputedUsed\": [],\n                        \"sigHash\": \"325a644af47e8a5a2591cda0ab0723978537318f10e6a63d4eed783b96a71a4d\"\n                    },\n                    \"expected\": {\n                        \"witness\": [\n                            \"052aedffc554b41f52b521071793a6b88d6dbca9dba94cf34c83696de0c1ec35ca9c5ed4ab28059bd606a4f3a657eec0bb96661d42921b5f50a95ad33675b54f83\"\n                        ]\n                    }\n                },\n                {\n                    \"given\": {\n                        \"txinIndex\": 3,\n                        \"internalPrivkey\": \"d3c7af07da2d54f7a7735d3d0fc4f0a73164db638b2f2f7c43f711f6d4aa7e64\",\n                        \"merkleRoot\": \"c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b\",\n                        \"hashType\": 1\n                    },\n                    \"intermediary\": {\n                        \"internalPubkey\": \"93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820\",\n                        \"tweak\": \"6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30\",\n                        \"tweakedPrivkey\": \"97323385e57015b75b0339a549c56a948eb961555973f0951f555ae6039ef00d\",\n                        \"sigMsg\": \"0001020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957ea2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc50003000000\",\n                        \"precomputedUsed\": [\n                            \"hashAmounts\",\n                            \"hashOutputs\",\n                            \"hashPrevouts\",\n                            \"hashScriptPubkeys\",\n                            \"hashSequences\"\n                        ],\n                        \"sigHash\": \"bf013ea93474aa67815b1b6cc441d23b64fa310911d991e713cd34c7f5d46669\"\n                    },\n                    \"expected\": {\n                        \"witness\": [\n                            \"ff45f742a876139946a149ab4d9185574b98dc919d2eb6754f8abaa59d18b025637a3aa043b91817739554f4ed2026cf8022dbd83e351ce1fabc272841d2510a01\"\n                        ]\n                    }\n                },\n                {\n                    \"given\": {\n                        \"txinIndex\": 4,\n                        \"internalPrivkey\": \"f36bb07a11e469ce941d16b63b11b9b9120a84d9d87cff2c84a8d4affb438f4e\",\n                        \"merkleRoot\": \"ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2\",\n                        \"hashType\": 0\n                    },\n                    \"intermediary\": {\n                        \"internalPubkey\": \"e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f\",\n                        \"tweak\": \"b57bfa183d28eeb6ad688ddaabb265b4a41fbf68e5fed2c72c74de70d5a786f4\",\n                        \"tweakedPrivkey\": \"a8e7aa924f0d58854185a490e6c41f6efb7b675c0f3331b7f14b549400b4d501\",\n                        \"sigMsg\": \"0000020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957ea2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc50004000000\",\n                        \"precomputedUsed\": [\n                            \"hashAmounts\",\n                            \"hashOutputs\",\n                            \"hashPrevouts\",\n                            \"hashScriptPubkeys\",\n                            \"hashSequences\"\n                        ],\n                        \"sigHash\": \"4f900a0bae3f1446fd48490c2958b5a023228f01661cda3496a11da502a7f7ef\"\n                    },\n                    \"expected\": {\n                        \"witness\": [\n                            \"b4010dd48a617db09926f729e79c33ae0b4e94b79f04a1ae93ede6315eb3669de185a17d2b0ac9ee09fd4c64b678a0b61a0a86fa888a273c8511be83bfd6810f\"\n                        ]\n                    }\n                },\n                {\n                    \"given\": {\n                        \"txinIndex\": 6,\n                        \"internalPrivkey\": \"415cfe9c15d9cea27d8104d5517c06e9de48e2f986b695e4f5ffebf230e725d8\",\n                        \"merkleRoot\": \"2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def\",\n                        \"hashType\": 2\n                    },\n                    \"intermediary\": {\n                        \"internalPubkey\": \"55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d\",\n                        \"tweak\": \"6579138e7976dc13b6a92f7bfd5a2fc7684f5ea42419d43368301470f3b74ed9\",\n                        \"tweakedPrivkey\": \"241c14f2639d0d7139282aa6abde28dd8a067baa9d633e4e7230287ec2d02901\",\n                        \"sigMsg\": \"0002020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e0006000000\",\n                        \"precomputedUsed\": [\n                            \"hashAmounts\",\n                            \"hashPrevouts\",\n                            \"hashScriptPubkeys\",\n                            \"hashSequences\"\n                        ],\n                        \"sigHash\": \"15f25c298eb5cdc7eb1d638dd2d45c97c4c59dcaec6679cfc16ad84f30876b85\"\n                    },\n                    \"expected\": {\n                        \"witness\": [\n                            \"a3785919a2ce3c4ce26f298c3d51619bc474ae24014bcdd31328cd8cfbab2eff3395fa0a16fe5f486d12f22a9cedded5ae74feb4bbe5351346508c5405bcfee002\"\n                        ]\n                    }\n                },\n                {\n                    \"given\": {\n                        \"txinIndex\": 7,\n                        \"internalPrivkey\": \"c7b0e81f0a9a0b0499e112279d718cca98e79a12e2f137c72ae5b213aad0d103\",\n                        \"merkleRoot\": \"6c2dc106ab816b73f9d07e3cd1ef2c8c1256f519748e0813e4edd2405d277bef\",\n                        \"hashType\": 130\n                    },\n                    \"intermediary\": {\n                        \"internalPubkey\": \"ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592\",\n                        \"tweak\": \"9e0517edc8259bb3359255400b23ca9507f2a91cd1e4250ba068b4eafceba4a9\",\n                        \"tweakedPrivkey\": \"65b6000cd2bfa6b7cf736767a8955760e62b6649058cbc970b7c0871d786346b\",\n                        \"sigMsg\": \"0082020000000065cd1d00e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf00000000804c8b2000000000225120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5ffffffff\",\n                        \"precomputedUsed\": [],\n                        \"sigHash\": \"cd292de50313804dabe4685e83f923d2969577191a3e1d2882220dca88cbeb10\"\n                    },\n                    \"expected\": {\n                        \"witness\": [\n                            \"ea0c6ba90763c2d3a296ad82ba45881abb4f426b3f87af162dd24d5109edc1cdd11915095ba47c3a9963dc1e6c432939872bc49212fe34c632cd3ab9fed429c482\"\n                        ]\n                    }\n                },\n                {\n                    \"given\": {\n                        \"txinIndex\": 8,\n                        \"internalPrivkey\": \"77863416be0d0665e517e1c375fd6f75839544eca553675ef7fdf4949518ebaa\",\n                        \"merkleRoot\": \"ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc\",\n                        \"hashType\": 129\n                    },\n                    \"intermediary\": {\n                        \"internalPubkey\": \"f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8\",\n                        \"tweak\": \"639f0281b7ac49e742cd25b7f188657626da1ad169209078e2761cefd91fd65e\",\n                        \"tweakedPrivkey\": \"ec18ce6af99f43815db543f47b8af5ff5df3b2cb7315c955aa4a86e8143d2bf5\",\n                        \"sigMsg\": \"0081020000000065cd1da2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc500a778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af101000000002b0c230000000022512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220ffffffff\",\n                        \"precomputedUsed\": [\n                            \"hashOutputs\"\n                        ],\n                        \"sigHash\": \"cccb739eca6c13a8a89e6e5cd317ffe55669bbda23f2fd37b0f18755e008edd2\"\n                    },\n                    \"expected\": {\n                        \"witness\": [\n                            \"bbc9584a11074e83bc8c6759ec55401f0ae7b03ef290c3139814f545b58a9f8127258000874f44bc46db7646322107d4d86aec8e73b8719a61fff761d75b5dd981\"\n                        ]\n                    }\n                }\n            ],\n            \"auxiliary\": {\n                \"fullySignedTx\": \"020000000001097de20cbff686da83a54981d2b9bab3586f4ca7e48f57f5b55963115f3b334e9c010000000000000000d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd990000000000fffffffff8e1f583384333689228c5d28eac13366be082dc57441760d957275419a41842000000006b4830450221008f3b8f8f0537c420654d2283673a761b7ee2ea3c130753103e08ce79201cf32a022079e7ab904a1980ef1c5890b648c8783f4d10103dd62f740d13daa79e298d50c201210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff0689180aa63b30cb162a73c6d2a38b7eeda2a83ece74310fda0843ad604853b0100000000feffffffaa5202bdf6d8ccd2ee0f0202afbbb7461d9264a25e5bfd3c5a52ee1239e0ba6c0000000000feffffff956149bdc66faa968eb2be2d2faa29718acbfe3941215893a2a3446d32acd050000000000000000000e664b9773b88c09c32cb70a2a3e4da0ced63b7ba3b22f848531bbb1d5d5f4c94010000000000000000e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf0000000000ffffffffa778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af10100000000ffffffff0200ca9a3b000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac807840cb0000000020ac9a87f5594be208f8532db38cff670c450ed2fea8fcdefcc9a663f78bab962b0141ed7c1647cb97379e76892be0cacff57ec4a7102aa24296ca39af7541246d8ff14d38958d4cc1e2e478e4d4a764bbfd835b16d4e314b72937b29833060b87276c030141052aedffc554b41f52b521071793a6b88d6dbca9dba94cf34c83696de0c1ec35ca9c5ed4ab28059bd606a4f3a657eec0bb96661d42921b5f50a95ad33675b54f83000141ff45f742a876139946a149ab4d9185574b98dc919d2eb6754f8abaa59d18b025637a3aa043b91817739554f4ed2026cf8022dbd83e351ce1fabc272841d2510a010140b4010dd48a617db09926f729e79c33ae0b4e94b79f04a1ae93ede6315eb3669de185a17d2b0ac9ee09fd4c64b678a0b61a0a86fa888a273c8511be83bfd6810f0247304402202b795e4de72646d76eab3f0ab27dfa30b810e856ff3a46c9a702df53bb0d8cc302203ccc4d822edab5f35caddb10af1be93583526ccfbade4b4ead350781e2f8adcd012102f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90141a3785919a2ce3c4ce26f298c3d51619bc474ae24014bcdd31328cd8cfbab2eff3395fa0a16fe5f486d12f22a9cedded5ae74feb4bbe5351346508c5405bcfee0020141ea0c6ba90763c2d3a296ad82ba45881abb4f426b3f87af162dd24d5109edc1cdd11915095ba47c3a9963dc1e6c432939872bc49212fe34c632cd3ab9fed429c4820141bbc9584a11074e83bc8c6759ec55401f0ae7b03ef290c3139814f545b58a9f8127258000874f44bc46db7646322107d4d86aec8e73b8719a61fff761d75b5dd9810065cd1d\"\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "tests/blinded-onion-message-onion-test.json",
    "content": "{\n  \"comment\": \"Test vector creating an onionmessage, including joining an existing one\",\n  \"generate\": {\n    \"comment\": \"This sections contains test data for Dave's blinded path Bob->Dave; sender has to prepend a hop to Alice to reach Bob\",\n    \"session_key\": \"0303030303030303030303030303030303030303030303030303030303030303\",\n    \"hops\": [\n      {\n        \"alias\": \"Alice\",\n        \"comment\": \"Alice->Bob: note next_path_key_override to match that give by Dave for Bob\",\n        \"path_key_secret\": \"6363636363636363636363636363636363636363636363636363636363636363\",\n        \"tlvs\": {\n          \"next_node_id\": \"0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c\",\n          \"next_path_key_override\": \"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f\",\n          \"path_key_override_secret\": \"0101010101010101010101010101010101010101010101010101010101010101\"\n        },\n        \"encrypted_data_tlv\": \"04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c0821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f\",\n        \"ss\": \"c04d2a4c518241cb49f2800eea92554cb543f268b4c73f85693541e86d649205\",\n        \"HMAC256('blinded_node_id', ss)\": \"bc5388417c8db33af18ab7ba43f6a5641861f7b0ecb380e501a739af446a7bf4\",\n        \"blinded_node_id\": \"02d1c3d73f8cac67e7c5b6ec517282d5ba0a52b06a29ec92ff01e12decf76003c1\",\n        \"E\": \"031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd99\",\n        \"H(E || ss)\": \"83377bd6096f82df3a46afec20d68f3f506168f2007f6e86c2dc267417de9e34\",\n        \"next_e\": \"bf3e8999518c0bb6e876abb0ae01d44b9ba211720048099a2ba5a83afd730cad01\",\n        \"rho\": \"6926df9d4522b26ad4330a51e3481208e4816edd9ae4feaf311ea0342eb90c44\",\n        \"encrypted_recipient_data\": \"49531cf38d3280b7f4af6d6461a2b32e3df50acfd35176fc61422a1096eed4dfc3806f29bf74320f712a61c766e7f7caac0c42f86040125fbaeec0c7613202b206dbdd31fda56394367b66a711bfd7d5bedbe20bed1b\"\n      },\n      {\n        \"alias\": \"Bob\",\n        \"comment\": \"Bob->Carol\",\n        \"path_key_secret\": \"0101010101010101010101010101010101010101010101010101010101010101\",\n        \"tlvs\": {\n          \"next_node_id\": \"027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007\",\n          \"unknown_tag_561\": \"123456\"\n        },\n        \"encrypted_data_tlv\": \"0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007fd023103123456\",\n        \"ss\": \"196f1f3e0be9d65f88463c1ab63e07f41b4e7c0368c28c3e6aa290cc0d22eaed\",\n        \"HMAC256('blinded_node_id', ss)\": \"c331d35827bdd509a02f1e64d48c7f0d7b2603355abbb1a3733c86e50135608e\",\n        \"blinded_node_id\": \"03f1465ca5cf3ec83f16f9343d02e6c24b76993a93e1dea2398f3147a9be893d7a\",\n        \"E\": \"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f\",\n        \"H(E || ss)\": \"1889a6cf337d9b34f80bb23a91a2ca194e80d7614f0728bdbda153da85e46b69\",\n        \"next_e\": \"f7ab6dca6152f7b6b0c9d7c82d716af063d72d8eef8816dfc51a8ae828fa7dce01\",\n        \"rho\": \"db991242ce366ab44272f38383476669b713513818397a00d4808d41ea979827\",\n        \"encrypted_recipient_data\": \"adf6771d3983b7f543d1b3d7a12b440b2bd3e1b3b8d6ec1023f6dec4f0e7548a6f57f6dbe9573b0a0f24f7c5773a7dd7a7bdb6bd0ee686d759f5\"\n      },\n      {\n        \"alias\": \"Carol\",\n        \"comment\": \"Carol->Dave\",\n        \"path_key_secret\": \"f7ab6dca6152f7b6b0c9d7c82d716af063d72d8eef8816dfc51a8ae828fa7dce\",\n        \"tlvs\": {\n          \"padding\": \"0000000000\",\n          \"next_node_id\": \"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991\"\n        },\n        \"encrypted_data_tlv\": \"010500000000000421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991\",\n        \"ss\": \"c7b33d74a723e26331a91c15ae5bc77db28a18b801b6bc5cd5bba98418303a9d\",\n        \"HMAC256('blinded_node_id', ss)\": \"a684c7495444a8cc2a6dfdecdf0819f3cdf4e86b81cc14e39825a40872ecefff\",\n        \"blinded_node_id\": \"035dbc0493aa4e7eea369d6a06e8013fd03e66a5eea91c455ed65950c4942b624b\",\n        \"E\": \"02b684babfd400c8dd48b367e9754b8021a3594a34dc94d7101776c7f6a86d0582\",\n        \"H(E || ss)\": \"2d80c5619a5a68d22dd3d784cab584c2718874922735d36cb36a179c10a796ca\",\n        \"next_e\": \"5de52bb427cc148bf23e509fdc18012004202517e80abcfde21612ae408e6cea01\",\n        \"rho\": \"739851e89b61cab34ee9ba7d5f3c342e4adc8b91a72991664026f68a685f0bdc\",\n        \"encrypted_recipient_data\": \"d8903df7a79ac799a0b59f4ba22f6a599fa32e7ff1a8325fc22b88d278ce3e4840af02adfb82d6145a189ba50c2219c9e4351e634d198e0849ac\"\n      },\n      {\n        \"alias\": \"Dave\",\n        \"comment\": \"Dave is final node, hence path_id\",\n        \"path_key_secret\": \"5de52bb427cc148bf23e509fdc18012004202517e80abcfde21612ae408e6cea\",\n        \"tlvs\": {\n          \"padding\": \"\",\n          \"path_id\": \"deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0\",\n          \"unknown_tag_65535\": \"06c1\"\n        },\n        \"encrypted_data_tlv\": \"01000620deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0fdffff0206c1\",\n        \"ss\": \"024955ed0d4ebbfab13498f5d7aacd00bf096c8d9ed0473cdfc96d90053c86b7\",\n        \"HMAC256('blinded_node_id', ss)\": \"3f5612df60f050ac571aeaaf76655e138529bea6d23293ebe15659f2588cd039\",\n        \"blinded_node_id\": \"0237bf019fa0fbecde8b4a1c7b197c9c1c76f9a23d67dd55bb5e42e1f50bb771a6\",\n        \"E\": \"025aaca62db7ce6b46386206ef9930daa32e979a35cb185a41cb951aa7d254b03c\",\n        \"H(E || ss)\": \"db5719e79919d706eab17eebaad64bd691e56476a42f0e26ae60caa9082f56fa\",\n        \"next_e\": \"ae31d2fbbf2f59038542c13287b9b624ea1a212c82be87c137c3d92aa30a185d01\",\n        \"rho\": \"c47cde57edc790df7b9b6bf921aff5e5eee43f738ab8fa9103ef675495f3f50e\",\n        \"encrypted_recipient_data\": \"bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6\"\n      }\n    ]\n  },\n  \"route\": {\n    \"comment\": \"The resulting blinded route Alice to Dave.\",\n    \"first_node_id\": \"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619\",\n    \"first_path_key\": \"031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd99\",\n    \"hops\": [\n      {\n        \"blinded_node_id\": \"02d1c3d73f8cac67e7c5b6ec517282d5ba0a52b06a29ec92ff01e12decf76003c1\",\n        \"encrypted_recipient_data\": \"49531cf38d3280b7f4af6d6461a2b32e3df50acfd35176fc61422a1096eed4dfc3806f29bf74320f712a61c766e7f7caac0c42f86040125fbaeec0c7613202b206dbdd31fda56394367b66a711bfd7d5bedbe20bed1b\"\n      },\n      {\n        \"blinded_node_id\": \"03f1465ca5cf3ec83f16f9343d02e6c24b76993a93e1dea2398f3147a9be893d7a\",\n        \"encrypted_recipient_data\": \"adf6771d3983b7f543d1b3d7a12b440b2bd3e1b3b8d6ec1023f6dec4f0e7548a6f57f6dbe9573b0a0f24f7c5773a7dd7a7bdb6bd0ee686d759f5\"\n      },\n      {\n        \"blinded_node_id\": \"035dbc0493aa4e7eea369d6a06e8013fd03e66a5eea91c455ed65950c4942b624b\",\n        \"encrypted_recipient_data\": \"d8903df7a79ac799a0b59f4ba22f6a599fa32e7ff1a8325fc22b88d278ce3e4840af02adfb82d6145a189ba50c2219c9e4351e634d198e0849ac\"\n      },\n      {\n        \"blinded_node_id\": \"0237bf019fa0fbecde8b4a1c7b197c9c1c76f9a23d67dd55bb5e42e1f50bb771a6\",\n        \"encrypted_recipient_data\": \"bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6\"\n      }\n    ]\n  },\n  \"onionmessage\": {\n    \"comment\": \"An onion message which sends a 'hello' to Dave\",\n    \"unknown_tag_1\": \"68656c6c6f\",\n    \"onion_message_packet\": \"0002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33793b828776d70aabbd8cef1a5b52d5a397ae1a20f20435ff6057cd8be339d5aee226660ef73b64afa45dbf2e6e8e26eb96a259b2db5aeecda1ce2e768bbc35d389d7f320ca3d2bd14e2689bef2f5ac0307eaaabc1924eb972c1563d4646ae131accd39da766257ed35ea36e4222527d1db4fa7b2000aab9eafcceed45e28b5560312d4e2299bd8d1e7fe27d10925966c28d497aec400b4630485e82efbabc00550996bdad5d6a9a8c75952f126d14ad2cff91e16198691a7ef2937de83209285f1fb90944b4e46bca7c856a9ce3da10cdf2a7d00dc2bf4f114bc4d3ed67b91cbde558ce9af86dc81fbdc37f8e301b29e23c1466659c62bdbf8cff5d4c20f0fb0851ec72f5e9385dd40fdd2e3ed67ca4517117825665e50a3e26f73c66998daf18e418e8aef9ce2d20da33c3629db2933640e03e7b44c2edf49e9b482db7b475cfd4c617ae1d46d5c24d697846f9f08561eac2b065f9b382501f6eabf07343ed6c602f61eab99cdb52adf63fd44a8db2d3016387ea708fc1c08591e19b4d9984ebe31edbd684c2ea86526dd8c7732b1d8d9117511dc1b643976d356258fce8313b1cb92682f41ab72dedd766f06de375f9edacbcd0ca8c99b865ea2b7952318ea1fd20775a28028b5cf59dece5de14f615b8df254eee63493a5111ea987224bea006d8f1b60d565eef06ac0da194dba2a6d02e79b2f2f34e9ca6e1984a507319d86e9d4fcaeea41b4b9144e0b1826304d4cc1da61cfc5f8b9850697df8adc5e9d6f3acb3219b02764b4909f2b2b22e799fd66c383414a84a7d791b899d4aa663770009eb122f90282c8cb9cda16aba6897edcf9b32951d0080c0f52be3ca011fbec3fb16423deb47744645c3b05fdbd932edf54ba6efd26e65340a8e9b1d1216582e1b30d64524f8ca2d6c5ba63a38f7120a3ed71bed8960bcac2feee2dd41c90be48e3c11ec518eb3d872779e4765a6cc28c6b0fa71ab57ced73ae963cc630edae4258cba2bf25821a6ae049fec2fca28b5dd1bb004d92924b65701b06dcf37f0ccd147a13a03f9bc0f98b7d78fe9058089756931e2cd0e0ed92ec6759d07b248069526c67e9e6ce095118fd3501ba0f858ef030b76c6f6beb11a09317b5ad25343f4b31aef02bc555951bc7791c2c289ecf94d5544dcd6ad3021ed8e8e3db34b2a73e1eedb57b578b068a5401836d6e382110b73690a94328c404af25e85a8d6b808893d1b71af6a31fadd8a8cc6e31ecc0d9ff7e6b91fd03c274a5c1f1ccd25b61150220a3fddb04c91012f5f7a83a5c90deb2470089d6e38cd5914b9c946eca6e9d31bbf8667d36cf87effc3f3ff283c21dd4137bd569fe7cf758feac94053e4baf7338bb592c8b7c291667fadf4a9bf9a2a154a18f612cbc7f851b3f8f2070e0a9d180622ee4f8e81b0ab250d504cef24116a3ff188cc829fcd8610b56343569e8dc997629410d1967ca9dd1d27eec5e01e4375aad16c46faba268524b154850d0d6fe3a76af2c6aa3e97647c51036049ac565370028d6a439a2672b6face56e1b171496c0722cfa22d9da631be359661617c5d5a2d286c5e19db9452c1e21a0107b6400debda2decb0c838f342dd017cdb2dccdf1fe97e3df3f881856b546997a3fed9e279c720145101567dd56be21688fed66bf9759e432a9aa89cbbd225d13cdea4ca05f7a45cfb6a682a3d5b1e18f7e6cf934fae5098108bae9058d05c3387a01d8d02a656d2bfff67e9f46b2d8a6aac28129e52efddf6e552214c3f8a45bc7a912cca9a7fec1d7d06412c6972cb9e3dc518983f56530b8bffe7f92c4b6eb47d4aef59fb513c4653a42de61bc17ad7728e7fc7590ff05a9e991de03f023d0aaf8688ed6170def5091c66576a424ac1cb\"\n  },\n  \"decrypt\": {\n    \"comment\": \"This section contains the internal values generated by intermediate nodes when decrypting the onion.\",\n    \"hops\": [\n      {\n        \"alias\": \"Alice\",\n        \"privkey\": \"4141414141414141414141414141414141414141414141414141414141414141\",\n        \"onion_message\": \"0201031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd9905560002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33793b828776d70aabbd8cef1a5b52d5a397ae1a20f20435ff6057cd8be339d5aee226660ef73b64afa45dbf2e6e8e26eb96a259b2db5aeecda1ce2e768bbc35d389d7f320ca3d2bd14e2689bef2f5ac0307eaaabc1924eb972c1563d4646ae131accd39da766257ed35ea36e4222527d1db4fa7b2000aab9eafcceed45e28b5560312d4e2299bd8d1e7fe27d10925966c28d497aec400b4630485e82efbabc00550996bdad5d6a9a8c75952f126d14ad2cff91e16198691a7ef2937de83209285f1fb90944b4e46bca7c856a9ce3da10cdf2a7d00dc2bf4f114bc4d3ed67b91cbde558ce9af86dc81fbdc37f8e301b29e23c1466659c62bdbf8cff5d4c20f0fb0851ec72f5e9385dd40fdd2e3ed67ca4517117825665e50a3e26f73c66998daf18e418e8aef9ce2d20da33c3629db2933640e03e7b44c2edf49e9b482db7b475cfd4c617ae1d46d5c24d697846f9f08561eac2b065f9b382501f6eabf07343ed6c602f61eab99cdb52adf63fd44a8db2d3016387ea708fc1c08591e19b4d9984ebe31edbd684c2ea86526dd8c7732b1d8d9117511dc1b643976d356258fce8313b1cb92682f41ab72dedd766f06de375f9edacbcd0ca8c99b865ea2b7952318ea1fd20775a28028b5cf59dece5de14f615b8df254eee63493a5111ea987224bea006d8f1b60d565eef06ac0da194dba2a6d02e79b2f2f34e9ca6e1984a507319d86e9d4fcaeea41b4b9144e0b1826304d4cc1da61cfc5f8b9850697df8adc5e9d6f3acb3219b02764b4909f2b2b22e799fd66c383414a84a7d791b899d4aa663770009eb122f90282c8cb9cda16aba6897edcf9b32951d0080c0f52be3ca011fbec3fb16423deb47744645c3b05fdbd932edf54ba6efd26e65340a8e9b1d1216582e1b30d64524f8ca2d6c5ba63a38f7120a3ed71bed8960bcac2feee2dd41c90be48e3c11ec518eb3d872779e4765a6cc28c6b0fa71ab57ced73ae963cc630edae4258cba2bf25821a6ae049fec2fca28b5dd1bb004d92924b65701b06dcf37f0ccd147a13a03f9bc0f98b7d78fe9058089756931e2cd0e0ed92ec6759d07b248069526c67e9e6ce095118fd3501ba0f858ef030b76c6f6beb11a09317b5ad25343f4b31aef02bc555951bc7791c2c289ecf94d5544dcd6ad3021ed8e8e3db34b2a73e1eedb57b578b068a5401836d6e382110b73690a94328c404af25e85a8d6b808893d1b71af6a31fadd8a8cc6e31ecc0d9ff7e6b91fd03c274a5c1f1ccd25b61150220a3fddb04c91012f5f7a83a5c90deb2470089d6e38cd5914b9c946eca6e9d31bbf8667d36cf87effc3f3ff283c21dd4137bd569fe7cf758feac94053e4baf7338bb592c8b7c291667fadf4a9bf9a2a154a18f612cbc7f851b3f8f2070e0a9d180622ee4f8e81b0ab250d504cef24116a3ff188cc829fcd8610b56343569e8dc997629410d1967ca9dd1d27eec5e01e4375aad16c46faba268524b154850d0d6fe3a76af2c6aa3e97647c51036049ac565370028d6a439a2672b6face56e1b171496c0722cfa22d9da631be359661617c5d5a2d286c5e19db9452c1e21a0107b6400debda2decb0c838f342dd017cdb2dccdf1fe97e3df3f881856b546997a3fed9e279c720145101567dd56be21688fed66bf9759e432a9aa89cbbd225d13cdea4ca05f7a45cfb6a682a3d5b1e18f7e6cf934fae5098108bae9058d05c3387a01d8d02a656d2bfff67e9f46b2d8a6aac28129e52efddf6e552214c3f8a45bc7a912cca9a7fec1d7d06412c6972cb9e3dc518983f56530b8bffe7f92c4b6eb47d4aef59fb513c4653a42de61bc17ad7728e7fc7590ff05a9e991de03f023d0aaf8688ed6170def5091c66576a424ac1cb\",\n        \"next_node_id\": \"0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c\"\n      },\n      {\n        \"alias\": \"Bob\",\n        \"privkey\": \"4242424242424242424242424242424242424242424242424242424242424242\",\n        \"onion_message\": \"0201031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f05560002536d53f93796cad550b6c68662dca41f7e8c221c31022c64dd1a627b2df3982b25eac261e88369cfc66e1e3b6d9829cb3dcd707046e68a7796065202a7904811bf2608c5611cf74c9eb5371c7eb1a4428bb39a041493e2a568ddb0b2482a6cc6711bc6116cef144ebf988073cb18d9dd4ce2d3aa9de91a7dc6d7c6f11a852024626e66b41ba1158055505dff9cb15aa51099f315564d9ee3ed6349665dc3e209eedf9b5805ee4f69d315df44c80e63d0e2efbdab60ec96f44a3447c6a6ddb1efb6aa4e072bde1dab974081646bfddf3b02daa2b83847d74dd336465e76e9b8fecc2b0414045eeedfc39939088a76820177dd1103c99939e659beb07197bab9f714b30ba8dc83738e9a6553a57888aaeda156c68933a2f4ff35e3f81135076b944ed9856acbfee9c61299a5d1763eadd14bf5eaf71304c8e165e590d7ecbcd25f1650bf5b6c2ad1823b2dc9145e168974ecf6a2273c94decff76d94bc6708007a17f22262d63033c184d0166c14f41b225a956271947aae6ce65890ed8f0d09c6ffe05ec02ee8b9de69d7077a0c5adeb813aabcc1ba8975b73ab06ddea5f4db3c23a1de831602de2b83f990d4133871a1a81e53f86393e6a7c3a7b73f0c099fa72afe26c3027bb9412338a19303bd6e6591c04fb4cde9b832b5f41ae199301ea8c303b5cef3aca599454273565de40e1148156d1f97c1aa9e58459ab318304075e034f5b7899c12587b86776a18a1da96b7bcdc22864fccc4c41538ebce92a6f054d53bf46770273a70e75fe0155cd6d2f2e937465b0825ce3123b8c206fac4c30478fa0f08a97ade7216dce11626401374993213636e93545a31f500562130f2feb04089661ad8c34d5a4cbd2e4e426f37cb094c786198a220a2646ecadc38c04c29ee67b19d662c209a7b30bfecc7fe8bf7d274de0605ee5df4db490f6d32234f6af639d3fce38a2801bcf8d51e9c090a6c6932355a83848129a378095b34e71cb8f51152dc035a4fe8e802fec8de221a02ba5afd6765ce570bef912f87357936ea0b90cb2990f56035e89539ec66e8dbd6ed50835158614096990e019c3eba3d7dd6a77147641c6145e8b17552cd5cf7cd163dd40b9eaeba8c78e03a2cd8c0b7997d6f56d35f38983a202b4eb8a54e14945c4de1a6dde46167e11708b7a5ff5cb9c0f7fc12fae49a012aa90bb1995c038130b749c48e6f1ffb732e92086def42af10fbc460d94abeb7b2fa744a5e9a491d62a08452be8cf2fdef573deedc1fe97098bce889f98200b26f9bb99da9aceddda6d793d8e0e44a2601ef4590cfbb5c3d0197aac691e3d31c20fd8e38764962ca34dabeb85df28feabaf6255d4d0df3d814455186a84423182caa87f9673df770432ad8fdfe78d4888632d460d36d2719e8fa8e4b4ca10d817c5d6bc44a8b2affab8c2ba53b8bf4994d63286c2fad6be04c28661162fa1a67065ecda8ba8c13aee4a8039f4f0110e0c0da2366f178d8903e19136dad6df9d8693ce71f3a270f9941de2a93d9b67bc516207ac1687bf6e00b29723c42c7d9c90df9d5e599dbeb7b73add0a6a2b7aba82f98ac93cb6e60494040445229f983a81c34f7f686d166dfc98ec23a6318d4a02a311ac28d655ea4e0f9c3014984f31e621ef003e98c373561d9040893feece2e0fa6cd2dd565e6fbb2773a2407cb2c3273c306cf71f427f2e551c4092e067cf9869f31ac7c6c80dd52d4f85be57a891a41e34be0d564e39b4af6f46b85339254a58b205fb7e10e7d0470ee73622493f28c08962118c23a1198467e72c4ae1cd482144b419247a5895975ea90d135e2a46ef7e5794a1551a447ff0a0d299b66a7f565cd86531f5e7af5408d85d877ce95b1df12b88b7d5954903a5296325ba478ba1e1a9d1f30a2d5052b2e2889bbd64f72c72bc71d8817288a2\",\n        \"next_node_id\": \"027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007\"\n      },\n      {\n        \"alias\": \"Carol\",\n        \"privkey\": \"4343434343434343434343434343434343434343434343434343434343434343\",\n        \"onion_message\": \"020102b684babfd400c8dd48b367e9754b8021a3594a34dc94d7101776c7f6a86d0582055600029a77e8523162efa1f4208f4f2050cd5c386ddb6ce6d36235ea569d217ec52209fb85fdf7dbc4786c373eebdba0ddc184cfbe6da624f610e93f62c70f2c56be1090b926359969f040f932c03f53974db5656233bd60af375517d4323002937d784c2c88a564bcefe5c33d3fc21c26d94dfacab85e2e19685fd2ff4c543650958524439b6da68779459aee5ffc9dc543339acec73ff43be4c44ddcbe1c11d50e2411a67056ba9db7939d780f5a86123fdd3abd6f075f7a1d78ab7daf3a82798b7ec1e9f1345bc0d1e935098497067e2ae5a51ece396fcb3bb30871ad73aee51b2418b39f00c8e8e22be4a24f4b624e09cb0414dd46239de31c7be035f71e8da4f5a94d15b44061f46414d3f355069b5c5b874ba56704eb126148a22ec873407fe118972127e63ff80e682e410f297f23841777cec0517e933eaf49d7e34bd203266b42081b3a5193b51ccd34b41342bc67cf73523b741f5c012ba2572e9dda15fbe131a6ac2ff24dc2a7622d58b9f3553092cfae7fae3c8864d95f97aa49ec8edeff5d9f5782471160ee412d82ff6767030fc63eec6a93219a108cd41433834b26676a39846a944998796c79cd1cc460531b8ded659cedfd8aecefd91944f00476f1496daafb4ea6af3feacac1390ea510709783c2aa81a29de27f8959f6284f4684102b17815667cbb0645396ac7d542b878d90c42a1f7f00c4c4eedb2a22a219f38afadb4f1f562b6e000a94e75cc38f535b43a3c0384ccef127fde254a9033a317701c710b2b881065723486e3f4d3eea5e12f374a41565fe43fa137c1a252c2153dde055bb343344c65ad0529010ece29bbd405effbebfe3ba21382b94a60ac1a5ffa03f521792a67b30773cb42e862a8a02a8bbd41b842e115969c87d1ff1f8c7b5726b9f20772dd57fe6e4ea41f959a2a673ffad8e2f2a472c4c8564f3a5a47568dd75294b1c7180c500f7392a7da231b1fe9e525ea2d7251afe9ca52a17fe54a116cb57baca4f55b9b6de915924d644cba9dade4ccc01939d7935749c008bafc6d3ad01cd72341ce5ddf7a5d7d21cf0465ab7a3233433aef21f9acf2bfcdc5a8cc003adc4d82ac9d72b36eb74e05c9aa6ccf439ac92e6b84a3191f0764dd2a2e0b4cc3baa08782b232ad6ecd3ca6029bc08cc094aef3aebddcaddc30070cb6023a689641de86cfc6341c8817215a4650f844cd2ca60f2f10c6e44cfc5f23912684d4457bf4f599879d30b79bf12ef1ab8d34dddc15672b82e56169d4c770f0a2a7a960b1e8790773f5ff7fce92219808f16d061cc85e053971213676d28fb48925e9232b66533dbd938458eb2cc8358159df7a2a2e4cf87500ede2afb8ce963a845b98978edf26a6948d4932a6b95d022004556d25515fe158092ce9a913b4b4a493281393ca731e8d8e5a3449b9d888fc4e73ffcbb9c6d6d66e88e03cf6e81a0496ede6e4e4172b08c000601993af38f80c7f68c9d5fff9e0e215cff088285bf039ca731744efcb7825a272ca724517736b4890f47e306b200aa2543c363e2c9090bcf3cf56b5b86868a62471c7123a41740392fc1d5ab28da18dca66618e9af7b42b62b23aba907779e73ca03ec60e6ab9e0484b9cae6578e0fddb6386cb3468506bf6420298bf4a690947ab582255551d82487f271101c72e19e54872ab47eae144db66bc2f8194a666a5daec08d12822cb83a61946234f2dfdbd6ca7d8763e6818adee7b401fcdb1ac42f9df1ac5cc5ac131f2869013c8d6cd29d4c4e3d05bccd34ca83366d616296acf854fa05149bfd763a25b9938e96826a037fdcb85545439c76df6beed3bdbd01458f9cf984997cc4f0a7ac3cc3f5e1eeb59c09cadcf5a537f16e444149c8f17d4bdaef16c9fbabc5ef06eb0f0bf3a07a1beddfeacdaf1df5582d6dbd6bb808d6ab31bc22e5d7\",\n        \"next_node_id\": \"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991\"\n      },\n      {\n        \"alias\": \"Dave\",\n        \"privkey\": \"4444444444444444444444444444444444444444444444444444444444444444\",\n        \"onion_message\": \"0201025aaca62db7ce6b46386206ef9930daa32e979a35cb185a41cb951aa7d254b03c055600025550b2910294fa73bda99b9de9c851be9cbb481e23194a1743033630efba546b86e7d838d0f6e9cc0ed088dbf6889f0dceca3bfc745bd77d013a31311fa932a8bf1d28387d9ff521eabc651dee8f861fed609a68551145a451f017ec44978addeee97a423c08445531da488fd1ddc998e9cdbfcea59517b53fbf1833f0bbe6188dba6ca773a247220ec934010daca9cc185e1ceb136803469baac799e27a0d82abe53dc48a06a55d1f643885cc7894677dd20a4e4152577d1ba74b870b9279f065f9b340cedb3ca13b7df218e853e10ccd1b59c42a2acf93f489e170ee4373d30ab158b60fc20d3ba73a1f8c750951d69fb5b9321b968ddc8114936412346aff802df65516e1c09c51ef19849ff36c0199fd88c8bec301a30fef0c7cb497901c038611303f64e4174b5daf42832aa5586b84d2c9b95f382f4269a5d1bd4be898618dc78dfd451170f72ca16decac5b03e60702112e439cadd104fb3bbb3d5023c9b80823fdcd0a212a7e1aaa6eeb027adc7f8b3723031d135a09a979a4802788bb7861c6cc85501fb91137768b70aeab309b27b885686604ffc387004ac4f8c44b101c39bc0597ef7fd957f53fc5051f534b10eb3852100962b5e58254e5558689913c26ad6072ea41f5c5db10077cfc91101d4ae393be274c74297da5cc381cd88d54753aaa7df74b2f9da8d88a72bc9218fcd1f19e4ff4aace182312b9509c5175b6988f044c5756d232af02a451a02ca752f3c52747773acff6fd07d2032e6ce562a2c42105d106eba02d0b1904182cdc8c74875b082d4989d3a7e9f0e73de7c75d357f4af976c28c0b206c5e8123fc2391d078592d0d5ff686fd245c0a2de2e535b7cca99c0a37d432a8657393a9e3ca53eec1692159046ba52cb9bc97107349d8673f74cbc97e231f1108005c8d03e24ca813cea2294b39a7a493bcc062708f1f6cf0074e387e7d50e0666ce784ef4d31cb860f6cad767438d9ea5156ff0ae86e029e0247bf94df75ee0cda4f2006061455cb2eaff513d558863ae334cef7a3d45f55e7cc13153c6719e9901c1d4db6c03f643b69ea4860690305651794284d9e61eb848ccdf5a77794d376f0af62e46d4835acce6fd9eef5df73ebb8ea3bb48629766967f446e744ecc57ff3642c4aa1ccee9a2f72d5caa75fa05787d08b79408fce792485fdecdc25df34820fb061275d70b84ece540b0fc47b2453612be34f2b78133a64e812598fbe225fd85415f8ffe5340ce955b5fd9d67dd88c1c531dde298ed25f96df271558c812c26fa386966c76f03a6ebccbca49ac955916929bd42e134f982dde03f924c464be5fd1ba44f8dc4c3cbc8162755fd1d8f7dc044b15b1a796c53df7d8769bb167b2045b49cc71e08908796c92c16a235717cabc4bb9f60f8f66ff4fff1f9836388a99583acebdff4a7fb20f48eedcd1f4bdcc06ec8b48e35307df51d9bc81d38a94992dd135b30079e1f592da6e98dff496cb1a7776460a26b06395b176f585636ebdf7eab692b227a31d6979f5a6141292698e91346b6c806b90c7c6971e481559cae92ee8f4136f2226861f5c39ddd29bbdb118a35dece03f49a96804caea79a3dacfbf09d65f2611b5622de51d98e18151acb3bb84c09caaa0cc80edfa743a4679f37d6167618ce99e73362fa6f213409931762618a61f1738c071bba5afc1db24fe94afb70c40d731908ab9a505f76f57a7d40e708fd3df0efc5b7cbb2a7b75cd23449e09684a2f0e2bfa0d6176c35f96fe94d92fc9fa4103972781f81cb6e8df7dbeb0fc529c600d768bed3f08828b773d284f69e9a203459d88c12d6df7a75be2455fec128f07a497a2b2bf626cc6272d0419ca663e9dc66b8224227eb796f0246dcae9c5b0b6cfdbbd40c3245a610481c92047c968c9fc92c04b89cc41a0c15355a8f\",\n        \"tlvs\": {\n          \"unknown_tag_1\": \"68656c6c6f\",\n          \"encrypted_recipient_data\": \"bdc03f088764c6224c8f939e321bf096f363b2092db381fc8787f891c8e6dc9284991b98d2a63d9f91fe563065366dd406cd8e112cdaaa80d0e6\"\n        }\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "tests/cause_carbon_wallet.json",
    "content": "{\n    \"seed\": \"cause carbon luggage air humble mistake melt paper supreme sense gravity void\",\n    \"funding_tx\": \"020000000001021798e10f8b7220c57ea0d605316a52453ca9b3eed99996b5b7bdf4699548bb520000000000fdffffff277d82678d238ca45dd3490ac9fbb49272f0980b093b9197ff70ec8eb082cfb00100000000fdffffff028c360100000000001600147a9bfd90821be827275023849dd91ee80d494957a08601000000000016001476efaaa243327bf3a2c0f5380cb3914099448cec024730440220354b2a74f5ac039cca3618f7ff98229d243b89ac40550c8b027894f2c5cb88ff022064cb5ab1539b4c5367c2e01a8362e0aa12c2732bc8d08c3fce6eab9e56b7fe19012103e0a1499cb3d8047492c60466722c435dfbcffae8da9b83e758fbd203d12728f502473044022073cef8b0cfb093aed5b8eaacbb58c2fa6a69405a8e266cd65e76b726c9151d7602204d5820b23ab96acc57c272aac96d94740a20a6b89c016aa5aed7c06d1e6b9100012102f09e50a265c6a0dcf7c87153ea73d7b12a0fbe9d7d0bbec5db626b2402c1e85c02fa2400\",\n    \"outgoing_address\": \"tb1qkfn0fude7z789uys2u7sf80kd4805zpvs3na0h\",\n    \"to_self_address\": \"tb1qyfnv3y866ufedugxxxfksyratv4pz3h78g9dad\"\n}\n"
  },
  {
    "path": "tests/fiat_fx_data/BitFinex_EUR",
    "content": "{\n    \"2017-05-19\": \"1775.9515356\",\n    \"2017-05-21\": \"1809.3\",\n    \"2017-05-22\": \"1812.642783\",\n    \"2017-05-23\": \"2000.6\",\n    \"2017-05-24\": \"2137.2\",\n    \"2017-05-25\": \"2022.7\",\n    \"2017-05-26\": \"1883.3\",\n    \"2017-05-27\": \"1619.455649\",\n    \"2017-05-28\": \"1941.9\",\n    \"2017-05-29\": \"1962.2\",\n    \"2017-05-30\": \"1908.1\",\n    \"2017-05-31\": \"1950.9\",\n    \"2017-06-01\": \"2058.5\",\n    \"2017-06-02\": \"2138.4\",\n    \"2017-06-03\": \"2197.4\",\n    \"2017-06-04\": \"2200.1\",\n    \"2017-06-05\": \"2343.3\",\n    \"2017-06-06\": \"2523.3\",\n    \"2017-06-07\": \"2350.6\",\n    \"2017-06-08\": \"2469.7\",\n    \"2017-06-09\": \"2506.3\",\n    \"2017-06-10\": \"2508.5\",\n    \"2017-06-11\": \"2614.5\",\n    \"2017-06-12\": \"2292.2\",\n    \"2017-06-13\": \"2386.6\",\n    \"2017-06-14\": \"2134.7\",\n    \"2017-06-15\": \"2131.3\",\n    \"2017-06-16\": \"2184\",\n    \"2017-06-17\": \"2344.1\",\n    \"2017-06-18\": \"2229.9\",\n    \"2017-06-19\": \"2296.5661376\",\n    \"2017-06-20\": \"2437.3\",\n    \"2017-06-21\": \"2347.8\",\n    \"2017-06-22\": \"2395.6\",\n    \"2017-06-23\": \"2390.7\",\n    \"2017-06-24\": \"2249.9\",\n    \"2017-06-25\": \"2216.8\",\n    \"2017-06-26\": \"2159.6\",\n    \"2017-06-27\": \"2202.5\",\n    \"2017-06-28\": \"2224.4\",\n    \"2017-06-29\": \"2161.4\",\n    \"2017-06-30\": \"2121.7\",\n    \"2017-07-01\": \"2060.6\",\n    \"2017-07-02\": \"2150.4\",\n    \"2017-07-03\": \"2242.85639568\",\n    \"2017-07-04\": \"2275.1\",\n    \"2017-07-05\": \"2282.1\",\n    \"2017-07-06\": \"2271.3\",\n    \"2017-07-07\": \"2176.1\",\n    \"2017-07-08\": \"2227.7\",\n    \"2017-07-09\": \"2170.2\",\n    \"2017-07-10\": \"2037.6\",\n    \"2017-07-11\": \"1989\",\n    \"2017-07-12\": \"2077.9\",\n    \"2017-07-13\": \"2042.3\",\n    \"2017-07-14\": \"1931.1\",\n    \"2017-07-15\": \"1730.67616479\",\n    \"2017-07-16\": \"1683.49716771\",\n    \"2017-07-17\": \"1926.9\",\n    \"2017-07-18\": \"1991.6\",\n    \"2017-07-19\": \"1949.4\",\n    \"2017-07-20\": \"2432.98874936\",\n    \"2017-07-21\": \"2276.2\",\n    \"2017-07-22\": \"2429.16002653\",\n    \"2017-07-23\": \"2361.6\",\n    \"2017-07-24\": \"2384.46446801\",\n    \"2017-07-26\": \"2161.8\",\n    \"2017-07-27\": \"2258.7\",\n    \"2017-07-28\": \"2376.2\",\n    \"2017-07-30\": \"2305.76616028\",\n    \"2017-07-31\": \"2360.21517013\",\n    \"2017-08-01\": \"2339.22642008\",\n    \"2017-08-02\": \"2330.56465809\",\n    \"2017-08-03\": \"2302.30886099\",\n    \"2017-08-04\": \"2402.27735489\",\n    \"2017-08-05\": \"2673.64542721\",\n    \"2017-08-06\": \"2714.87084196\",\n    \"2017-08-07\": \"2770.71893214\",\n    \"2017-08-10\": \"2883\",\n    \"2017-08-11\": \"2982.51149311\",\n    \"2017-08-14\": \"3647\",\n    \"2017-08-15\": \"3561.3\",\n    \"2017-08-16\": \"3486.3\",\n    \"2017-08-17\": \"3650.8\",\n    \"2017-08-18\": \"3468.5\",\n    \"2017-08-19\": \"3371.3\",\n    \"2017-08-20\": \"3430.2\",\n    \"2017-08-21\": \"3411.4\",\n    \"2017-08-22\": \"3466.27008096\",\n    \"2017-08-23\": \"3512.8\",\n    \"2017-08-24\": \"3694.4\",\n    \"2017-08-25\": \"3629.4592806\",\n    \"2017-08-28\": \"3642.42272465\",\n    \"2017-08-29\": \"3782\",\n    \"2017-08-30\": \"3832.3\",\n    \"2017-08-31\": \"3969.2\",\n    \"2017-09-01\": \"4041.8\",\n    \"2017-09-02\": \"3734.8\",\n    \"2017-09-04\": \"3513.7\",\n    \"2017-09-05\": \"3721.2\",\n    \"2017-09-06\": \"3832.2\",\n    \"2017-09-07\": \"3835.38075543\",\n    \"2017-09-08\": \"3581.7\",\n    \"2017-09-09\": \"3604.22472779\",\n    \"2017-09-10\": \"3408.77401831\",\n    \"2017-09-11\": \"3516\",\n    \"2017-09-12\": \"3471.4\",\n    \"2017-09-13\": \"3188.4\",\n    \"2017-09-14\": \"2692.2\",\n    \"2017-09-15\": \"3114.4\",\n    \"2017-09-16\": \"3109.1\",\n    \"2017-09-17\": \"3144.2\",\n    \"2017-09-18\": \"3411.24525273\",\n    \"2017-09-19\": \"3281.85694217\",\n    \"2017-09-20\": \"3259\",\n    \"2017-09-21\": \"3026.4\",\n    \"2017-09-22\": \"3027.07716509\",\n    \"2017-09-23\": \"3166.33676611\",\n    \"2017-09-24\": \"3069.5659803\",\n    \"2017-09-25\": \"3157\",\n    \"2017-09-26\": \"3283.7\",\n    \"2017-09-27\": \"3585.06760042\",\n    \"2017-09-28\": \"3564.4\",\n    \"2017-09-29\": \"3541\",\n    \"2017-09-30\": \"3699.63326052\",\n    \"2017-10-01\": \"3736.90656899\",\n    \"2017-10-02\": \"3715.3\",\n    \"2017-10-03\": \"3638.3\",\n    \"2017-10-04\": \"3571.2\",\n    \"2017-10-05\": \"3679.2\",\n    \"2017-10-06\": \"3712.8\",\n    \"2017-10-07\": \"3830.5\",\n    \"2017-10-08\": \"3917.8\",\n    \"2017-10-09\": \"4100.3\",\n    \"2017-10-10\": \"4032.5\",\n    \"2017-10-11\": \"4050.2\",\n    \"2017-10-12\": \"4496.32810127\",\n    \"2017-10-13\": \"4728.5\",\n    \"2017-10-14\": \"4913.7\",\n    \"2017-10-15\": \"4833.75919838\",\n    \"2017-10-16\": \"4889.82878214\",\n    \"2017-10-17\": \"4797.77258034\",\n    \"2017-10-18\": \"4724\",\n    \"2017-10-19\": \"4815.7\",\n    \"2017-10-20\": \"5075.30461194\",\n    \"2017-10-21\": \"5162.62450494\",\n    \"2017-10-22\": \"4954.17157316\",\n    \"2017-10-23\": \"5022.5\",\n    \"2017-10-24\": \"4661.52597414\",\n    \"2017-10-25\": \"4846.2\",\n    \"2017-10-26\": \"5031.6\",\n    \"2017-10-27\": \"4950.38602678\",\n    \"2017-10-28\": \"4951\",\n    \"2017-10-29\": \"5358.38071816\",\n    \"2017-10-30\": \"5238.2\",\n    \"2017-10-31\": \"5526.9\",\n    \"2017-11-01\": \"5633.8300408\",\n    \"2017-11-02\": \"6000\",\n    \"2017-11-03\": \"6145.3\",\n    \"2017-11-04\": \"6331.3\",\n    \"2017-11-05\": \"6465.9\",\n    \"2017-11-06\": \"6011.9\",\n    \"2017-11-07\": \"6130.8\",\n    \"2017-11-08\": \"6190.3\",\n    \"2017-11-09\": \"6060\",\n    \"2017-11-10\": \"5639.8\",\n    \"2017-11-11\": \"5431\",\n    \"2017-11-12\": \"5020.9\",\n    \"2017-11-13\": \"5550.4\",\n    \"2017-11-14\": \"5457.8\",\n    \"2017-11-15\": \"6156.1\",\n    \"2017-11-16\": \"6690.26971749\",\n    \"2017-11-17\": \"6553.96141606\",\n    \"2017-11-18\": \"6586.4\",\n    \"2017-11-19\": \"6803.2\",\n    \"2017-11-20\": \"6979.5\",\n    \"2017-11-21\": \"6906.2\",\n    \"2017-11-22\": \"7014\",\n    \"2017-11-23\": \"6797.7\",\n    \"2017-11-24\": \"6882.01056252\",\n    \"2017-11-25\": \"7368.4\",\n    \"2017-11-26\": \"7827.9\",\n    \"2017-11-27\": \"8158.31541442\",\n    \"2017-11-28\": \"8319.13670164\",\n    \"2017-11-29\": \"8247.4\",\n    \"2017-11-30\": \"8337.22594629\",\n    \"2017-12-01\": \"9130.8\",\n    \"2017-12-02\": \"9161.13049578\",\n    \"2017-12-03\": \"9390.8\",\n    \"2017-12-04\": \"9761.07482\",\n    \"2017-12-05\": \"9801.070776\",\n    \"2017-12-06\": \"11441.64602\",\n    \"2017-12-07\": \"14059\",\n    \"2017-12-08\": \"13440\",\n    \"2017-12-09\": \"12471.337116\",\n    \"2017-12-10\": \"12693\",\n    \"2017-12-11\": \"14231\",\n    \"2017-12-12\": \"14392.0062\",\n    \"2017-12-13\": \"13670\",\n    \"2017-12-14\": \"13906.928427\",\n    \"2017-12-15\": \"14875.503728\",\n    \"2017-12-16\": \"16317.786008\",\n    \"2017-12-17\": \"16083\",\n    \"2017-12-18\": \"16075.2092\",\n    \"2017-12-19\": \"14643.521856\",\n    \"2017-12-20\": \"13810\",\n    \"2017-12-21\": \"13204\",\n    \"2017-12-22\": \"11105.365888\",\n    \"2017-12-23\": \"11827.906016\",\n    \"2017-12-24\": \"11423\",\n    \"2017-12-25\": \"11471.6062\",\n    \"2017-12-26\": \"13202\",\n    \"2017-12-27\": \"12919.627875\",\n    \"2017-12-28\": \"11997\",\n    \"2017-12-29\": \"11920\",\n    \"2017-12-30\": \"10303.855384\",\n    \"2017-12-31\": \"11467\",\n    \"2018-01-01\": \"11133\",\n    \"2018-01-02\": \"12190\",\n    \"2018-01-03\": \"12611.93038\",\n    \"2018-01-04\": \"12553.152741\",\n    \"2018-01-05\": \"14061.1968\",\n    \"2018-01-06\": \"14278\",\n    \"2018-01-07\": \"13470\",\n    \"2018-01-08\": \"12472\",\n    \"2018-01-09\": \"12074\",\n    \"2018-01-10\": \"12450\",\n    \"2018-01-11\": \"11014\",\n    \"2018-01-12\": \"11339.379872\",\n    \"2018-01-13\": \"11643.948448\",\n    \"2018-01-14\": \"11128.798575\",\n    \"2018-01-15\": \"11094.0568\",\n    \"2018-01-16\": \"9011.35813\",\n    \"2018-01-17\": \"9106.4289\",\n    \"2018-01-18\": \"9025\",\n    \"2018-01-19\": \"9383\",\n    \"2018-01-20\": \"10409.734128\",\n    \"2018-01-21\": \"9390\",\n    \"2018-01-22\": \"8787.0125\",\n    \"2018-01-23\": \"8780.737444\",\n    \"2018-01-24\": \"9193\",\n    \"2018-01-25\": \"8992.570508\",\n    \"2018-01-26\": \"8908.5\",\n    \"2018-01-27\": \"9215.7\",\n    \"2018-01-28\": \"9522.1\",\n    \"2018-01-29\": \"9055\",\n    \"2018-01-30\": \"8198\",\n    \"2018-01-31\": \"8270\",\n    \"2018-02-01\": \"7340.28945\",\n    \"2018-02-02\": \"7136.2\",\n    \"2018-02-03\": \"7405\",\n    \"2018-02-04\": \"6588\",\n    \"2018-02-05\": \"5612.4411296\",\n    \"2018-02-06\": \"6202\",\n    \"2018-02-07\": \"6188\",\n    \"2018-02-08\": \"6714.0852651\",\n    \"2018-02-09\": \"7076.2\",\n    \"2018-02-10\": \"6992.4\",\n    \"2018-02-11\": \"6584.34528\",\n    \"2018-02-12\": \"7237.868799\",\n    \"2018-02-13\": \"6881.4\",\n    \"2018-02-14\": \"7597.583895\",\n    \"2018-02-15\": \"7992\",\n    \"2018-02-16\": \"8187.737064\",\n    \"2018-02-17\": \"8902.5\",\n    \"2018-02-18\": \"8374.1\",\n    \"2018-02-19\": \"9010.4\",\n    \"2018-02-20\": \"9090.7\",\n    \"2018-02-21\": \"8511\",\n    \"2018-02-22\": \"7968.5029524\",\n    \"2018-02-23\": \"8247.313872\",\n    \"2018-02-24\": \"7867.6\",\n    \"2018-02-25\": \"7793\",\n    \"2018-02-26\": \"8376.7\",\n    \"2018-02-27\": \"8653\",\n    \"2018-02-28\": \"8459.7\",\n    \"2018-03-01\": \"8884.3\",\n    \"2018-03-02\": \"8947\",\n    \"2018-03-03\": \"9294.6\",\n    \"2018-03-04\": \"9342.250799\",\n    \"2018-03-05\": \"9242\",\n    \"2018-03-06\": \"8622.9\",\n    \"2018-03-07\": \"7967\",\n    \"2018-03-08\": \"7555\",\n    \"2018-03-09\": \"7487.8464276\",\n    \"2018-03-10\": \"7114.9529916\",\n    \"2018-03-11\": \"7736.9\",\n    \"2018-03-12\": \"7394\",\n    \"2018-03-13\": \"7381\",\n    \"2018-03-14\": \"6612.524688\",\n    \"2018-03-15\": \"6701\",\n    \"2018-03-16\": \"6708.474344\",\n    \"2018-03-17\": \"6369\",\n    \"2018-03-18\": \"6671.7\",\n    \"2018-03-19\": \"6967.4045196\",\n    \"2018-03-20\": \"7270.988592\",\n    \"2018-03-21\": \"7236\",\n    \"2018-03-22\": \"7060.634205\",\n    \"2018-03-23\": \"7213.6\",\n    \"2018-03-24\": \"6897\",\n    \"2018-03-25\": \"6820.3527358\",\n    \"2018-03-26\": \"6529.3\",\n    \"2018-03-27\": \"6273.6\",\n    \"2018-03-28\": \"6449.5910391\",\n    \"2018-03-29\": \"5757.1\",\n    \"2018-03-30\": \"5547\",\n    \"2018-03-31\": \"5619.2\",\n    \"2018-04-01\": \"5534.6\",\n    \"2018-04-02\": \"5731.4\",\n    \"2018-04-03\": \"6036\",\n    \"2018-04-04\": \"5521.8354138\",\n    \"2018-04-05\": \"5517.5\",\n    \"2018-04-06\": \"5382.1\",\n    \"2018-04-07\": \"5612\",\n    \"2018-04-08\": \"5721\",\n    \"2018-04-09\": \"5493.88885\",\n    \"2018-04-10\": \"5521.817256\",\n    \"2018-04-11\": \"5608.1\",\n    \"2018-04-12\": \"6414\",\n    \"2018-04-13\": \"6385.1\",\n    \"2018-04-14\": \"6488\",\n    \"2018-04-15\": \"6774.2\",\n    \"2018-04-16\": \"6502.9807524\",\n    \"2018-04-17\": \"6373.4296604\",\n    \"2018-04-18\": \"6595.1683072\",\n    \"2018-04-19\": \"6701.79528\",\n    \"2018-04-20\": \"7214.5437752\",\n    \"2018-04-21\": \"7258.2\",\n    \"2018-04-22\": \"7166.7728\",\n    \"2018-04-23\": \"7319\",\n    \"2018-04-24\": \"7878.6209835\",\n    \"2018-04-25\": \"7285\",\n    \"2018-04-26\": \"7656.4\",\n    \"2018-04-27\": \"7350.4368\",\n    \"2018-04-28\": \"7704.774\",\n    \"2018-04-29\": \"7753.0918\",\n    \"2018-04-30\": \"7642.8542778\",\n    \"2018-05-01\": \"7558.4\",\n    \"2018-05-02\": \"7725.086735\",\n    \"2018-05-03\": \"8139\",\n    \"2018-05-04\": \"8119.700064\",\n    \"2018-05-05\": \"8231.0994072\",\n    \"2018-05-06\": \"8076.8\",\n    \"2018-05-07\": \"7855.1\",\n    \"2018-05-08\": \"7743.0238215\",\n    \"2018-05-09\": \"7861.3263301\",\n    \"2018-05-10\": \"7564.3\",\n    \"2018-05-11\": \"7035.6\",\n    \"2018-05-12\": \"7074.0984408\",\n    \"2018-05-13\": \"7249.1797742\",\n    \"2018-05-14\": \"7259.03432\",\n    \"2018-05-15\": \"7168.960954\",\n    \"2018-05-16\": \"7062.9\",\n    \"2018-05-17\": \"6823.7887376\",\n    \"2018-05-18\": \"6998.1546156\",\n    \"2018-05-19\": \"6991.3\",\n    \"2018-05-20\": \"7242.5367478\",\n    \"2018-05-21\": \"7122.9\",\n    \"2018-05-22\": \"6768.4\",\n    \"2018-05-23\": \"6397.5\",\n    \"2018-05-24\": \"6458.8961184\",\n    \"2018-05-25\": \"6409.9\",\n    \"2018-05-26\": \"6274.3\",\n    \"2018-05-27\": \"6278.3720403\",\n    \"2018-05-28\": \"6105.1\",\n    \"2018-05-29\": \"6459.893312\",\n    \"2018-05-30\": \"6323.508309\",\n    \"2018-05-31\": \"6400\",\n    \"2018-06-01\": \"6447.6313268\",\n    \"2018-06-02\": \"6552.2\",\n    \"2018-06-03\": \"6615.10361538\",\n    \"2018-06-04\": \"6400.2799632\",\n    \"2018-06-05\": \"6494.3\",\n    \"2018-06-06\": \"6492.5812908\",\n    \"2018-06-07\": \"6516.3350611\",\n    \"2018-06-08\": \"6469.9043808\",\n    \"2018-06-09\": \"6366.082772\",\n    \"2018-06-10\": \"5732.8595335\",\n    \"2018-06-11\": \"5829.837029\",\n    \"2018-06-12\": \"5574.1\",\n    \"2018-06-13\": \"5330.7256013\",\n    \"2018-06-14\": \"5732.9118555\",\n    \"2018-06-15\": \"5499.3\",\n    \"2018-06-16\": \"5595.9919464\",\n    \"2018-06-17\": \"5552.3\",\n    \"2018-06-18\": \"5766.5\",\n    \"2018-06-19\": \"5810.9\",\n    \"2018-06-20\": \"5838.9\",\n    \"2018-06-21\": \"5781.4\",\n    \"2018-06-22\": \"5184.8\",\n    \"2018-06-23\": \"5278.6\",\n    \"2018-06-24\": \"5269.2732322\",\n    \"2018-06-25\": \"5333.251902\",\n    \"2018-06-26\": \"5217\",\n    \"2018-06-27\": \"5302.6\",\n    \"2018-06-28\": \"5055.1\",\n    \"2018-06-29\": \"5314.6\",\n    \"2018-06-30\": \"5467.7\",\n    \"2018-07-01\": \"5437.0285396\",\n    \"2018-07-02\": \"5678.5380714\",\n    \"2018-07-03\": \"5576.45824\",\n    \"2018-07-04\": \"5648.948\",\n    \"2018-07-05\": \"5585.9\",\n    \"2018-07-06\": \"5617.6837836\",\n    \"2018-07-07\": \"5754.8\",\n    \"2018-07-08\": \"5706.66543525\",\n    \"2018-07-09\": \"5663\",\n    \"2018-07-10\": \"5369.48372056\",\n    \"2018-07-11\": \"5448.1\",\n    \"2018-07-12\": \"5337\",\n    \"2018-07-13\": \"5329.78990422\",\n    \"2018-07-14\": \"5350.4\",\n    \"2018-07-15\": \"5432.5\",\n    \"2018-07-16\": \"5760.2\",\n    \"2018-07-17\": \"6280\",\n    \"2018-07-18\": \"6318.00002262\",\n    \"2018-07-19\": \"6412.98048002\",\n    \"2018-07-20\": \"6250.11570027\",\n    \"2018-07-21\": \"6316\",\n    \"2018-07-22\": \"6307\",\n    \"2018-07-23\": \"6598.55215324\",\n    \"2018-07-24\": \"7182.3\",\n    \"2018-07-25\": \"6960.9\",\n    \"2018-07-26\": \"6826.7\",\n    \"2018-07-27\": \"7016.37316492\",\n    \"2018-07-28\": \"7081.07420651\",\n    \"2018-07-29\": \"7052.21140166\",\n    \"2018-07-30\": \"6999.1\",\n    \"2018-07-31\": \"6613.99998531\",\n    \"2018-08-01\": \"6516.88162769\",\n    \"2018-08-02\": \"6512\",\n    \"2018-08-03\": \"6406\",\n    \"2018-08-04\": \"6055\",\n    \"2018-08-05\": \"6088.9\",\n    \"2018-08-06\": \"6006.9783088\",\n    \"2018-08-07\": \"5789.65276181\",\n    \"2018-08-08\": \"5410.1\",\n    \"2018-08-09\": \"5668.5\",\n    \"2018-08-10\": \"5380.95806897\",\n    \"2018-08-11\": \"5449.3\",\n    \"2018-08-12\": \"5539.84785361\",\n    \"2018-08-13\": \"5483.57276619\",\n    \"2018-08-14\": \"5453.6\",\n    \"2018-08-15\": \"5528.2\",\n    \"2018-08-16\": \"5553.5\",\n    \"2018-08-17\": \"5751\",\n    \"2018-08-18\": \"5582.90279605\",\n    \"2018-08-19\": \"5673.7\",\n    \"2018-08-20\": \"5444.7\",\n    \"2018-08-21\": \"5600.1\",\n    \"2018-08-22\": \"5490.50001981\",\n    \"2018-08-23\": \"5654.28488408\",\n    \"2018-08-24\": \"5757.4\",\n    \"2018-08-25\": \"5789.19998727\",\n    \"2018-08-26\": \"5757.79997952\",\n    \"2018-08-27\": \"5913.64719144\",\n    \"2018-08-28\": \"6052.33003974\",\n    \"2018-08-29\": \"6011.60000256\",\n    \"2018-08-30\": \"5989.9705665\",\n    \"2018-08-31\": \"6034.10001638\",\n    \"2018-09-01\": \"6193.2\",\n    \"2018-09-02\": \"6296.4\",\n    \"2018-09-03\": \"6257.6\",\n    \"2018-09-04\": \"6352.25937755\",\n    \"2018-09-05\": \"5763.59998635\",\n    \"2018-09-06\": \"5597.4\",\n    \"2018-09-07\": \"5527.2\",\n    \"2018-09-08\": \"5344.58305918\",\n    \"2018-09-09\": \"5399\",\n    \"2018-09-10\": \"5448.80113956\",\n    \"2018-09-11\": \"5423.41636333\",\n    \"2018-09-12\": \"5446.1\",\n    \"2018-09-13\": \"5546.9\",\n    \"2018-09-14\": \"5571\",\n    \"2018-09-15\": \"5596.4\",\n    \"2018-09-16\": \"5589.39232507\",\n    \"2018-09-17\": \"5359.3\",\n    \"2018-09-18\": \"5417.60001257\",\n    \"2018-09-19\": \"5477.1\",\n    \"2018-09-20\": \"5514\",\n    \"2018-09-21\": \"5747.91607879\",\n    \"2018-09-22\": \"5712.31553427\",\n    \"2018-09-23\": \"5710.87426634\",\n    \"2018-09-24\": \"5603.4\",\n    \"2018-09-25\": \"5476.4\",\n    \"2018-09-26\": \"5499.4\",\n    \"2018-09-27\": \"5748.4\",\n    \"2018-09-28\": \"5710.9\",\n    \"2018-09-29\": \"5674.4\",\n    \"2018-09-30\": \"5706.4\",\n    \"2018-10-01\": \"5701.3\",\n    \"2018-10-02\": \"5662.6\",\n    \"2018-10-03\": \"5666.50872195\",\n    \"2018-10-04\": \"5722.5\",\n    \"2018-10-05\": \"5756.7\",\n    \"2018-10-06\": \"5717.8\",\n    \"2018-10-07\": \"5731.2445615\",\n    \"2018-10-08\": \"5806.42496545\",\n    \"2018-10-09\": \"5793.7\",\n    \"2018-10-10\": \"5749.7\",\n    \"2018-10-11\": \"5393.86550684\",\n    \"2018-10-12\": \"5436.7\",\n    \"2018-10-13\": \"5465.43172531\",\n    \"2018-10-14\": \"5489\",\n    \"2018-10-15\": \"5824.6\",\n    \"2018-10-16\": \"5834.9\",\n    \"2018-10-17\": \"5857.01016627\",\n    \"2018-10-18\": \"5772\",\n    \"2018-10-19\": \"5658.99083085\",\n    \"2018-10-20\": \"5709.5\",\n    \"2018-10-21\": \"5728.5\",\n    \"2018-10-22\": \"5728.49058146\",\n    \"2018-10-23\": \"5708.80000127\",\n    \"2018-10-24\": \"5750.3\",\n    \"2018-10-25\": \"5740.0265113\",\n    \"2018-10-26\": \"5717.1\",\n    \"2018-10-27\": \"5685.0833935\",\n    \"2018-10-28\": \"5695.40001708\",\n    \"2018-10-29\": \"5578.4\",\n    \"2018-10-30\": \"5579.0721231\",\n    \"2018-10-31\": \"5627.73099395\",\n    \"2018-11-01\": \"5617.66922491\",\n    \"2018-11-02\": \"5643.30022452\",\n    \"2018-11-03\": \"5609.1408947\",\n    \"2018-11-04\": \"5695.3\",\n    \"2018-11-05\": \"5670.3\",\n    \"2018-11-06\": \"5703.8\",\n    \"2018-11-07\": \"5748.5\",\n    \"2018-11-08\": \"5699.10001532\",\n    \"2018-11-09\": \"5661.4\",\n    \"2018-11-10\": \"5672.33490601\",\n    \"2018-11-11\": \"5696.05948459\",\n    \"2018-11-12\": \"5744.4\",\n    \"2018-11-13\": \"5713.7\",\n    \"2018-11-14\": \"5235\",\n    \"2018-11-15\": \"5077.5\",\n    \"2018-11-16\": \"4956.4\",\n    \"2018-11-17\": \"4926.82292799\",\n    \"2018-11-18\": \"4957.90001695\",\n    \"2018-11-19\": \"4294.0275314\",\n    \"2018-11-20\": \"4021\",\n    \"2018-11-21\": \"4097.5\",\n    \"2018-11-22\": \"3826.28642109\",\n    \"2018-11-23\": \"3899.2292183\",\n    \"2018-11-24\": \"3465.19784738\",\n    \"2018-11-25\": \"3610.05881076\",\n    \"2018-11-26\": \"3409.6\",\n    \"2018-11-27\": \"3437.6\",\n    \"2018-11-28\": \"3787.9\",\n    \"2018-11-29\": \"3792.59292037\",\n    \"2018-11-30\": \"3569.7\",\n    \"2018-12-01\": \"3752.1436987\",\n    \"2018-12-02\": \"3678.20207085\",\n    \"2018-12-03\": \"3437.1\",\n    \"2018-12-04\": \"3514.9\",\n    \"2018-12-05\": \"3317.6\",\n    \"2018-12-06\": \"3103.9\",\n    \"2018-12-07\": \"3039.8\",\n    \"2018-12-08\": \"3079.5\",\n    \"2018-12-09\": \"3198.9\",\n    \"2018-12-10\": \"3103.6\",\n    \"2018-12-11\": \"3033.7\",\n    \"2018-12-12\": \"3108.7\",\n    \"2018-12-13\": \"2950.6\",\n    \"2018-12-14\": \"2901.3\",\n    \"2018-12-15\": \"2906.50312368\",\n    \"2018-12-16\": \"2917.7\",\n    \"2018-12-17\": \"3194.23369142\",\n    \"2018-12-18\": \"3319.12346694\",\n    \"2018-12-19\": \"3345.55338417\",\n    \"2018-12-20\": \"3681.2\",\n    \"2018-12-21\": \"3487.85743511\",\n    \"2018-12-22\": \"3636.49371307\",\n    \"2018-12-23\": \"3581.5\",\n    \"2018-12-24\": \"3631.6\",\n    \"2018-12-25\": \"3425.17087526\",\n    \"2018-12-26\": \"3460.5\",\n    \"2018-12-27\": \"3247.8\",\n    \"2018-12-28\": \"3525\",\n    \"2018-12-29\": \"3398.19030586\",\n    \"2018-12-30\": \"3472.6\",\n    \"2018-12-31\": \"3331.5\",\n    \"2019-01-01\": \"3458\",\n    \"2019-01-02\": \"3577.19535794\",\n    \"2019-01-03\": \"3441.5\",\n    \"2019-01-04\": \"3476.1\",\n    \"2019-01-05\": \"3432.47814418\",\n    \"2019-01-06\": \"3653.9\",\n    \"2019-01-07\": \"3583.7\",\n    \"2019-01-08\": \"3578.1\",\n    \"2019-01-09\": \"3535.3\",\n    \"2019-01-10\": \"3226.8056184\",\n    \"2019-01-11\": \"3231.7\",\n    \"2019-01-12\": \"3228\",\n    \"2019-01-13\": \"3129.45436251\",\n    \"2019-01-14\": \"3268.1\",\n    \"2019-01-15\": \"3201.8\",\n    \"2019-01-16\": \"3224.3\",\n    \"2019-01-17\": \"3265.58412287\",\n    \"2019-01-18\": \"3232.1\",\n    \"2019-01-19\": \"3309.2\",\n    \"2019-01-20\": \"3155.3\",\n    \"2019-01-21\": \"3149.65065044\",\n    \"2019-01-22\": \"3202.2\",\n    \"2019-01-23\": \"3179.4\",\n    \"2019-01-24\": \"3228.9\",\n    \"2019-01-25\": \"3184.3\",\n    \"2019-01-26\": \"3184.77438556\",\n    \"2019-01-27\": \"3152.3\",\n    \"2019-01-28\": \"3063.8\",\n    \"2019-01-29\": \"3041.42005904\",\n    \"2019-01-30\": \"3073.94120765\",\n    \"2019-01-31\": \"3059.1\",\n    \"2019-02-01\": \"3090.47981224\",\n    \"2019-02-02\": \"3110.9\",\n    \"2019-02-03\": \"3056.3\",\n    \"2019-02-04\": \"3052\",\n    \"2019-02-05\": \"3077.7\",\n    \"2019-02-06\": \"3033.7\",\n    \"2019-02-07\": \"3033.2\",\n    \"2019-02-08\": \"3285.25692698\",\n    \"2019-02-09\": \"3280.2284725\",\n    \"2019-02-10\": \"3309.5\",\n    \"2019-02-11\": \"3263.9\",\n    \"2019-02-12\": \"3259.5\",\n    \"2019-02-13\": \"3254.543614\",\n    \"2019-02-14\": \"3238.3\",\n    \"2019-02-15\": \"3247\",\n    \"2019-02-16\": \"3254.5\",\n    \"2019-02-17\": \"3308.8\",\n    \"2019-02-18\": \"3515.98401145\",\n    \"2019-02-19\": \"3518.7\",\n    \"2019-02-20\": \"3578.38945029\",\n    \"2019-02-21\": \"3535.6\",\n    \"2019-02-22\": \"3578.2\",\n    \"2019-02-23\": \"3713.95991052\",\n    \"2019-02-24\": \"3378.78172863\",\n    \"2019-02-25\": \"3452.02628715\",\n    \"2019-02-26\": \"3420.416385\",\n    \"2019-02-27\": \"3428.2\",\n    \"2019-02-28\": \"3424.1\",\n    \"2019-03-01\": \"3442.397642\",\n    \"2019-03-02\": \"3438.5\",\n    \"2019-03-03\": \"3415.8\",\n    \"2019-03-04\": \"3352.11161509\",\n    \"2019-03-05\": \"3494.8\",\n    \"2019-03-06\": \"3493\",\n    \"2019-03-07\": \"3537.359325\",\n    \"2019-03-08\": \"3505.5\",\n    \"2019-03-09\": \"3574.3\",\n    \"2019-03-10\": \"3560.95998496\",\n    \"2019-03-11\": \"3508\",\n    \"2019-03-12\": \"3503.6\",\n    \"2019-03-13\": \"3482.5\",\n    \"2019-03-14\": \"3493.4\",\n    \"2019-03-15\": \"3528.7\",\n    \"2019-03-16\": \"3603.5\",\n    \"2019-03-17\": \"3595\",\n    \"2019-03-18\": \"3593.6\",\n    \"2019-03-19\": \"3615.1\",\n    \"2019-03-20\": \"3611.7\",\n    \"2019-03-21\": \"3555.48032664\",\n    \"2019-03-22\": \"3571.1\",\n    \"2019-03-23\": \"3589.2\",\n    \"2019-03-24\": \"3577.6\",\n    \"2019-03-25\": \"3508.4\",\n    \"2019-03-26\": \"3541.9\",\n    \"2019-03-27\": \"3646.2\",\n    \"2019-03-28\": \"3644.8\",\n    \"2019-03-29\": \"3720.86167147\",\n    \"2019-03-30\": \"3708.4\",\n    \"2019-03-31\": \"3713.5\",\n    \"2019-04-01\": \"3744.8\",\n    \"2019-04-02\": \"4394.25493868\",\n    \"2019-04-03\": \"4425.99634073\",\n    \"2019-04-04\": \"4400.3\",\n    \"2019-04-05\": \"4523.6\",\n    \"2019-04-06\": \"4531.4\",\n    \"2019-04-07\": \"4669.29473352\",\n    \"2019-04-08\": \"4719.8\",\n    \"2019-04-09\": \"4631.2\",\n    \"2019-04-10\": \"4714.8\",\n    \"2019-04-11\": \"4496.2\",\n    \"2019-04-12\": \"4520.4\",\n    \"2019-04-13\": \"4508.8\",\n    \"2019-04-14\": \"4595.68289107\",\n    \"2019-04-15\": \"4460.9\",\n    \"2019-04-16\": \"4638.4\",\n    \"2019-04-17\": \"4650.8\",\n    \"2019-04-18\": \"4727.2\",\n    \"2019-04-19\": \"4725.1\",\n    \"2019-04-20\": \"4748.2\",\n    \"2019-04-21\": \"4733.16482049\",\n    \"2019-04-22\": \"4810.2\",\n    \"2019-04-23\": \"4940.5\",\n    \"2019-04-24\": \"4913.6\",\n    \"2019-04-25\": \"4687.6674672\",\n    \"2019-04-26\": \"4846.9\",\n    \"2019-04-27\": \"4839.8\",\n    \"2019-04-28\": \"4920.7\",\n    \"2019-04-29\": \"4899.2\",\n    \"2019-04-30\": \"4993.1\",\n    \"2019-05-01\": \"5026.11250973\",\n    \"2019-05-02\": \"5132.9\",\n    \"2019-05-03\": \"5364\",\n    \"2019-05-04\": \"5439.2\",\n    \"2019-05-05\": \"5400.5\",\n    \"2019-05-06\": \"5383.6\",\n    \"2019-05-07\": \"5443.8\",\n    \"2019-05-08\": \"5533.4\",\n    \"2019-05-09\": \"5574.1\",\n    \"2019-05-10\": \"5730.7\",\n    \"2019-05-11\": \"6355.20591974\",\n    \"2019-05-12\": \"6214.9\",\n    \"2019-05-13\": \"6915.8\",\n    \"2019-05-14\": \"7089.7\",\n    \"2019-05-15\": \"7255.7\",\n    \"2019-05-16\": \"7031.9\",\n    \"2019-05-17\": \"6640.3\",\n    \"2019-05-18\": \"6503.3\",\n    \"2019-05-19\": \"7335.36575004\",\n    \"2019-05-20\": \"7149.1\",\n    \"2019-05-21\": \"7101.9\",\n    \"2019-05-22\": \"6824\",\n    \"2019-05-23\": \"7045.5\",\n    \"2019-05-24\": \"7134.2\",\n    \"2019-05-25\": \"7188.2\",\n    \"2019-05-26\": \"7787.7\",\n    \"2019-05-27\": \"7872.01788304\",\n    \"2019-05-28\": \"7837.6\",\n    \"2019-05-29\": \"7796.1\",\n    \"2019-05-30\": \"7429.6\",\n    \"2019-05-31\": \"7619.5\",\n    \"2019-06-01\": \"7630.51409457\",\n    \"2019-06-02\": \"7823.4505809\",\n    \"2019-06-03\": \"7205.9\",\n    \"2019-06-04\": \"6831\",\n    \"2019-06-05\": \"6936.8\",\n    \"2019-06-06\": \"6938.1\",\n    \"2019-06-07\": \"7058.3\",\n    \"2019-06-08\": \"6986.7\",\n    \"2019-06-09\": \"6747.9\",\n    \"2019-06-10\": \"7080.8\",\n    \"2019-06-11\": \"6963.3\",\n    \"2019-06-12\": \"7241\",\n    \"2019-06-13\": \"7289.3\",\n    \"2019-06-14\": \"7732.2\",\n    \"2019-06-15\": \"7882\",\n    \"2019-06-16\": \"8012.9\",\n    \"2019-06-17\": \"8336.5\",\n    \"2019-06-18\": \"8123.2\",\n    \"2019-06-19\": \"8282.7122342\",\n    \"2019-06-20\": \"8461.7\",\n    \"2019-06-21\": \"8962.3\",\n    \"2019-06-22\": \"9429\",\n    \"2019-06-23\": \"9593.2\",\n    \"2019-06-24\": \"9704.29768985\",\n    \"2019-06-25\": \"10331\",\n    \"2019-06-26\": \"11325.42600958\",\n    \"2019-06-27\": \"9847.1\",\n    \"2019-06-28\": \"10844\",\n    \"2019-06-29\": \"10456\",\n    \"2019-06-30\": \"9511.1\",\n    \"2019-07-01\": \"9426.2\",\n    \"2019-07-02\": \"9653.258\",\n    \"2019-07-03\": \"10618.47029971\",\n    \"2019-07-04\": \"9917.3\",\n    \"2019-07-05\": \"9772.4\",\n    \"2019-07-06\": \"10016\",\n    \"2019-07-07\": \"10243\",\n    \"2019-07-08\": \"10982\",\n    \"2019-07-09\": \"11208\",\n    \"2019-07-10\": \"10732\",\n    \"2019-07-11\": \"10079\",\n    \"2019-07-12\": \"10432\",\n    \"2019-07-13\": \"10031\",\n    \"2019-07-14\": \"9054.9\",\n    \"2019-07-15\": \"9609.1\",\n    \"2019-07-16\": \"8399.7\",\n    \"2019-07-17\": \"8616.1811226\",\n    \"2019-07-18\": \"9433.2\",\n    \"2019-07-19\": \"9363\",\n    \"2019-07-20\": \"9557.2\",\n    \"2019-07-21\": \"9421.7\",\n    \"2019-07-22\": \"9219.3\",\n    \"2019-07-23\": \"8837.8703475\",\n    \"2019-07-24\": \"8767.6\",\n    \"2019-07-25\": \"8870.9\",\n    \"2019-07-26\": \"8829.3\",\n    \"2019-07-27\": \"8499.6\",\n    \"2019-07-28\": \"8552.4936775\",\n    \"2019-07-29\": \"8534.8\",\n    \"2019-07-30\": \"8592.7\",\n    \"2019-07-31\": \"9113.4\",\n    \"2019-08-01\": \"9386.3\",\n    \"2019-08-02\": \"9452.3\",\n    \"2019-08-03\": \"9722.9\",\n    \"2019-08-04\": \"9883.5\",\n    \"2019-08-05\": \"10514\",\n    \"2019-08-06\": \"10242\",\n    \"2019-08-07\": \"10701\",\n    \"2019-08-08\": \"10779.77071446\",\n    \"2019-08-09\": \"10514\",\n    \"2019-08-10\": \"9999.9\",\n    \"2019-08-11\": \"10320\",\n    \"2019-08-12\": \"10176\",\n    \"2019-08-13\": \"9756.2\",\n    \"2019-08-14\": \"9031.9\",\n    \"2019-08-15\": \"9299.4\",\n    \"2019-08-16\": \"9355.1116559\",\n    \"2019-08-17\": \"9217.2\",\n    \"2019-08-18\": \"9319.6\",\n    \"2019-08-19\": \"9865.6\",\n    \"2019-08-20\": \"9724.2\",\n    \"2019-08-21\": \"9148.9\",\n    \"2019-08-22\": \"9126.9\",\n    \"2019-08-23\": \"9261\",\n    \"2019-08-24\": \"9019.5\",\n    \"2019-08-25\": \"9092.1\",\n    \"2019-08-26\": \"9342.3\",\n    \"2019-08-27\": \"9183.7\",\n    \"2019-08-28\": \"8787.3\",\n    \"2019-08-29\": \"8591.3\",\n    \"2019-08-30\": \"8716.96189656\",\n    \"2019-08-31\": \"8741.1\",\n    \"2019-09-01\": \"8897.22073737\",\n    \"2019-09-02\": \"9473.918328\",\n    \"2019-09-03\": \"9697.4\",\n    \"2019-09-04\": \"9610.4\",\n    \"2019-09-05\": \"9592.1\",\n    \"2019-09-06\": \"9261.5\",\n    \"2019-09-07\": \"9417.4626544\",\n    \"2019-09-08\": \"9460\",\n    \"2019-09-09\": \"9339.07975\",\n    \"2019-09-10\": \"9161.2\",\n    \"2019-09-11\": \"9239.1\",\n    \"2019-09-12\": \"9451.8\",\n    \"2019-09-13\": \"9294.4\",\n    \"2019-09-14\": \"9303.2\",\n    \"2019-09-15\": \"9312.4\",\n    \"2019-09-16\": \"9360.7\",\n    \"2019-09-17\": \"9222.5\",\n    \"2019-09-18\": \"9234.02655\",\n    \"2019-09-19\": \"9324.7\",\n    \"2019-09-20\": \"9258.3\",\n    \"2019-09-21\": \"9096.7\",\n    \"2019-09-22\": \"9122.4\",\n    \"2019-09-23\": \"8826.2\",\n    \"2019-09-24\": \"7770.400909\",\n    \"2019-09-25\": \"7739.5\",\n    \"2019-09-26\": \"7415.2\",\n    \"2019-09-27\": \"7506.9\",\n    \"2019-09-28\": \"7527.9\",\n    \"2019-09-29\": \"7394.9\",\n    \"2019-09-30\": \"7643.4\",\n    \"2019-10-01\": \"7626\",\n    \"2019-10-02\": \"7675.2\",\n    \"2019-10-03\": \"7523.6\",\n    \"2019-10-04\": \"7446\",\n    \"2019-10-05\": \"7432.9\",\n    \"2019-10-06\": \"7177\",\n    \"2019-10-07\": \"7497.8\",\n    \"2019-10-08\": \"7498.1\",\n    \"2019-10-09\": \"7833.8\",\n    \"2019-10-10\": \"7807.6\",\n    \"2019-10-11\": \"7510.9\",\n    \"2019-10-12\": \"7538.3\",\n    \"2019-10-13\": \"7532.3\",\n    \"2019-10-14\": \"7591.6\",\n    \"2019-10-15\": \"7426.2\",\n    \"2019-10-16\": \"7237.68861591\",\n    \"2019-10-17\": \"7277.2\",\n    \"2019-10-18\": \"7155.66451191\",\n    \"2019-10-19\": \"7155.7\",\n    \"2019-10-20\": \"7406.2\",\n    \"2019-10-21\": \"7393.2\",\n    \"2019-10-22\": \"7242.1\",\n    \"2019-10-23\": \"6731.44085918\",\n    \"2019-10-24\": \"6712.7\",\n    \"2019-10-25\": \"7832.4\",\n    \"2019-10-26\": \"8370.3\",\n    \"2019-10-27\": \"8641.8\",\n    \"2019-10-28\": \"8315.5\",\n    \"2019-10-29\": \"8501\",\n    \"2019-10-30\": \"8238.4\",\n    \"2019-10-31\": \"8238\",\n    \"2019-11-01\": \"8305.4\",\n    \"2019-11-02\": \"8356.5\",\n    \"2019-11-03\": \"8265.7\",\n    \"2019-11-04\": \"8487.2\",\n    \"2019-11-05\": \"8427.7\",\n    \"2019-11-06\": \"8445.8\",\n    \"2019-11-07\": \"8331.3\",\n    \"2019-11-08\": \"7970.2\",\n    \"2019-11-09\": \"8011.7\",\n    \"2019-11-10\": \"8239.4\",\n    \"2019-11-11\": \"7919\",\n    \"2019-11-12\": \"8016.4\",\n    \"2019-11-13\": \"7993.7\",\n    \"2019-11-14\": \"7854.2\",\n    \"2019-11-15\": \"7686.8\",\n    \"2019-11-16\": \"7710\",\n    \"2019-11-17\": \"7730.5\",\n    \"2019-11-18\": \"7423.3\",\n    \"2019-11-19\": \"7370.8\",\n    \"2019-11-20\": \"7336.7\",\n    \"2019-11-21\": \"6932.8\",\n    \"2019-11-22\": \"6645.1\",\n    \"2019-11-23\": \"6671.3\",\n    \"2019-11-24\": \"6323.5\",\n    \"2019-11-25\": \"6508.7\",\n    \"2019-11-26\": \"6532.6\",\n    \"2019-11-27\": \"6851.8\",\n    \"2019-11-28\": \"6775.3\",\n    \"2019-11-29\": \"7061.3\",\n    \"2019-11-30\": \"6897.6\",\n    \"2019-12-01\": \"6764.2\",\n    \"2019-12-02\": \"6637.9\",\n    \"2019-12-03\": \"6619.8\",\n    \"2019-12-04\": \"6530.3\",\n    \"2019-12-05\": \"6702.2\",\n    \"2019-12-06\": \"6861.1\",\n    \"2019-12-07\": \"6825.1\",\n    \"2019-12-08\": \"6840\",\n    \"2019-12-09\": \"6669.7\",\n    \"2019-12-10\": \"6540.1\",\n    \"2019-12-11\": \"6493.9959416\",\n    \"2019-12-12\": \"6456\",\n    \"2019-12-13\": \"6549.3\",\n    \"2019-12-14\": \"6387.3\",\n    \"2019-12-15\": \"6432.3\",\n    \"2019-12-16\": \"6221.4\",\n    \"2019-12-17\": \"5979\",\n    \"2019-12-18\": \"6588.4\",\n    \"2019-12-19\": \"6457.5\",\n    \"2019-12-20\": \"6511\",\n    \"2019-12-21\": \"6468.8\",\n    \"2019-12-22\": \"6801.8\",\n    \"2019-12-23\": \"6625\",\n    \"2019-12-24\": \"6563\",\n    \"2019-12-25\": \"6507.548331\",\n    \"2019-12-26\": \"6518.085216\",\n    \"2019-12-27\": \"6522.1\",\n    \"2019-12-28\": \"6562.9\",\n    \"2019-12-29\": \"6631.4\",\n    \"2019-12-30\": \"6482\",\n    \"2019-12-31\": \"6425.6\",\n    \"2020-01-01\": \"6429.7212745\",\n    \"2020-01-02\": \"6256.8\",\n    \"2020-01-03\": \"6605.31758712\",\n    \"2020-01-04\": \"6607.8\",\n    \"2020-01-05\": \"6598.193025\",\n    \"2020-01-06\": \"6929.6\",\n    \"2020-01-07\": \"7307.5\",\n    \"2020-01-08\": \"7230.5\",\n    \"2020-01-09\": \"7037.5\",\n    \"2020-01-10\": \"7354.3\",\n    \"2020-01-11\": \"7200.1\",\n    \"2020-01-12\": \"7353.5\",\n    \"2020-01-13\": \"7269.1\",\n    \"2020-01-14\": \"7894.1\",\n    \"2020-01-15\": \"7897.010319\",\n    \"2020-01-16\": \"7830.64535442\",\n    \"2020-01-17\": \"8005.1\",\n    \"2020-01-18\": \"8017\",\n    \"2020-01-19\": \"7845.7\",\n    \"2020-01-20\": \"7788.2\",\n    \"2020-01-21\": \"7878.4\",\n    \"2020-01-22\": \"7801.8\",\n    \"2020-01-23\": \"7601.7\",\n    \"2020-01-24\": \"7661.8\",\n    \"2020-01-25\": \"7576.78083328\",\n    \"2020-01-26\": \"7815.3\",\n    \"2020-01-27\": \"8086.1\",\n    \"2020-01-28\": \"8521.3\",\n    \"2020-01-29\": \"8440.7\",\n    \"2020-01-30\": \"8624.7\",\n    \"2020-01-31\": \"8444.2\",\n    \"2020-02-01\": \"8483.9\",\n    \"2020-02-02\": \"8436.5\",\n    \"2020-02-03\": \"8418.3\",\n    \"2020-02-04\": \"8326.23948\",\n    \"2020-02-05\": \"8738.3\",\n    \"2020-02-06\": \"8874.7\",\n    \"2020-02-07\": \"8958.20723882\",\n    \"2020-02-08\": \"9029.8\",\n    \"2020-02-09\": \"9270.5\",\n    \"2020-02-10\": \"9039.42967513\",\n    \"2020-02-11\": \"9385\",\n    \"2020-02-12\": \"9516.9\",\n    \"2020-02-13\": \"9428.6\",\n    \"2020-02-14\": \"9541.2\",\n    \"2020-02-15\": \"9132.34386728\",\n    \"2020-02-16\": \"9160.9\",\n    \"2020-02-17\": \"8959.26121826\",\n    \"2020-02-18\": \"9433.5\",\n    \"2020-02-19\": \"8875.4\",\n    \"2020-02-20\": \"8908\",\n    \"2020-02-21\": \"8938.7\",\n    \"2020-02-22\": \"8911.5\",\n    \"2020-02-23\": \"9213.5\",\n    \"2020-02-24\": \"8916.8\",\n    \"2020-02-25\": \"8574.1\",\n    \"2020-02-26\": \"8097.2\",\n    \"2020-02-27\": \"8024.54439871\",\n    \"2020-02-28\": \"7917.5\",\n    \"2020-02-29\": \"7763\",\n    \"2020-03-01\": \"7749.7784569\",\n    \"2020-03-02\": \"8004.77235195\",\n    \"2020-03-03\": \"7866.2\",\n    \"2020-03-04\": \"7891\",\n    \"2020-03-05\": \"8094.2\",\n    \"2020-03-06\": \"8132.9\",\n    \"2020-03-07\": \"7901.4\",\n    \"2020-03-08\": \"7090.1\",\n    \"2020-03-09\": \"6990.389502\",\n    \"2020-03-10\": \"6989.86385832\",\n    \"2020-03-11\": \"7080.3\",\n    \"2020-03-12\": \"4362.2\",\n    \"2020-03-13\": \"5034.59914792\",\n    \"2020-03-14\": \"4675.6\",\n    \"2020-03-15\": \"4826.95495908\",\n    \"2020-03-16\": \"4525.2\",\n    \"2020-03-17\": \"4853.81575872\",\n    \"2020-03-18\": \"4940.1\",\n    \"2020-03-19\": \"5791.9\",\n    \"2020-03-20\": \"5803.8\",\n    \"2020-03-21\": \"5791.9\",\n    \"2020-03-22\": \"5443.1\",\n    \"2020-03-23\": \"6029.8\",\n    \"2020-03-24\": \"6257.1\",\n    \"2020-03-25\": \"6142.8\",\n    \"2020-03-26\": \"6110.9\",\n    \"2020-03-27\": \"5709.6\",\n    \"2020-03-28\": \"5597.1\",\n    \"2020-03-29\": \"5290.435206\",\n    \"2020-03-30\": \"5810.1\",\n    \"2020-03-31\": \"5827.8\",\n    \"2020-04-01\": \"6096.4\",\n    \"2020-04-02\": \"6272.8\",\n    \"2020-04-03\": \"6239.2\",\n    \"2020-04-04\": \"6367.5\",\n    \"2020-04-05\": \"6290.1\",\n    \"2020-04-06\": \"6801.6\",\n    \"2020-04-07\": \"6611.2\",\n    \"2020-04-08\": \"6779.4\",\n    \"2020-04-09\": \"6666.2\",\n    \"2020-04-10\": \"6285\",\n    \"2020-04-11\": \"6295.9\",\n    \"2020-04-12\": \"6326.97432254\",\n    \"2020-04-13\": \"6270.1\",\n    \"2020-04-14\": \"6252.5\",\n    \"2020-04-15\": \"6085.6\",\n    \"2020-04-16\": \"6548.8\",\n    \"2020-04-17\": \"6478.9\",\n    \"2020-04-18\": \"6682.2\",\n    \"2020-04-19\": \"6563.1\",\n    \"2020-04-20\": \"6294.9\",\n    \"2020-04-21\": \"6310.46903423\",\n    \"2020-04-22\": \"6600\",\n    \"2020-04-23\": \"6944.4319755\",\n    \"2020-04-24\": \"6932.6\",\n    \"2020-04-25\": \"6967.2\",\n    \"2020-04-26\": \"7113.5\",\n    \"2020-04-27\": \"7185.3\",\n    \"2020-04-28\": \"7153.741086\",\n    \"2020-04-29\": \"8077.5\",\n    \"2020-04-30\": \"7886.9\",\n    \"2020-05-01\": \"8036.3\",\n    \"2020-05-02\": \"8181.5\",\n    \"2020-05-03\": \"8126.1\",\n    \"2020-05-04\": \"8132.848856\",\n    \"2020-05-05\": \"8322.6\",\n    \"2020-05-06\": \"8475.6\",\n    \"2020-05-07\": \"9200.2\",\n    \"2020-05-08\": \"8929.04200903\",\n    \"2020-05-09\": \"8795.5\",\n    \"2020-05-10\": \"8046.4\",\n    \"2020-05-11\": \"7918.93993942\",\n    \"2020-05-12\": \"8121.7\",\n    \"2020-05-13\": \"8604\",\n    \"2020-05-14\": \"9060.5\",\n    \"2020-05-15\": \"8600.3\",\n    \"2020-05-16\": \"8675.2\",\n    \"2020-05-17\": \"8935\",\n    \"2020-05-18\": \"8901.5\",\n    \"2020-05-19\": \"8941.35127547\",\n    \"2020-05-20\": \"8656\",\n    \"2020-05-21\": \"8277.69524985\",\n    \"2020-05-22\": \"8404\",\n    \"2020-05-23\": \"8413.2\",\n    \"2020-05-24\": \"7985.9\",\n    \"2020-05-25\": \"8160.6\",\n    \"2020-05-26\": \"8056.3\",\n    \"2020-05-27\": \"8361.1\",\n    \"2020-05-28\": \"8648.7\",\n    \"2020-05-29\": \"8486.5\",\n    \"2020-05-30\": \"8738.9\",\n    \"2020-05-31\": \"8502\",\n    \"2020-06-01\": \"9185.6\",\n    \"2020-06-02\": \"8517.3\",\n    \"2020-06-03\": \"8614.7\",\n    \"2020-06-04\": \"8632.7\",\n    \"2020-06-05\": \"8521.3\",\n    \"2020-06-06\": \"8563.4\",\n    \"2020-06-07\": \"8611.6\",\n    \"2020-06-08\": \"8656.5\",\n    \"2020-06-09\": \"8622.43936701\",\n    \"2020-06-10\": \"8702.8\",\n    \"2020-06-11\": \"8210.9\",\n    \"2020-06-12\": \"8411\",\n    \"2020-06-13\": \"8423.4\",\n    \"2020-06-14\": \"8300.2\",\n    \"2020-06-15\": \"8321.6\",\n    \"2020-06-16\": \"8464.3\",\n    \"2020-06-17\": \"8427.97820087\",\n    \"2020-06-18\": \"8387.4\",\n    \"2020-06-19\": \"8334.5\",\n    \"2020-06-20\": \"8383.1\",\n    \"2020-06-21\": \"8320.1\",\n    \"2020-06-22\": \"8601.4\",\n    \"2020-06-23\": \"8517.2\",\n    \"2020-06-24\": \"8269.5\",\n    \"2020-06-25\": \"8251.50162662\",\n    \"2020-06-26\": \"8176.8\",\n    \"2020-06-27\": \"8035.7\",\n    \"2020-06-28\": \"8130.6\",\n    \"2020-06-29\": \"8173.3\",\n    \"2020-06-30\": \"8145.1\",\n    \"2020-07-01\": \"8210\",\n    \"2020-07-02\": \"8097.90801492\",\n    \"2020-07-03\": \"8074.1\",\n    \"2020-07-04\": \"8134.90324448\",\n    \"2020-07-05\": \"8077.9\",\n    \"2020-07-06\": \"8274.88998909\",\n    \"2020-07-07\": \"8219.7\",\n    \"2020-07-08\": \"8336.1\",\n    \"2020-07-09\": \"8193.8\",\n    \"2020-07-10\": \"8238.1\",\n    \"2020-07-11\": \"8184.5110563\",\n    \"2020-07-12\": \"8236.98378309\",\n    \"2020-07-13\": \"8151\",\n    \"2020-07-14\": \"8118.66037766\",\n    \"2020-07-15\": \"8071.3\",\n    \"2020-07-16\": \"8032.77068646\",\n    \"2020-07-17\": \"8021.5\",\n    \"2020-07-18\": \"8042.1\",\n    \"2020-07-19\": \"8064.8\",\n    \"2020-07-20\": \"8011.60266033\",\n    \"2020-07-21\": \"8151.76905\",\n    \"2020-07-22\": \"8262.57428348\",\n    \"2020-07-23\": \"8291.83177387\",\n    \"2020-07-24\": \"8200.5\",\n    \"2020-07-25\": \"8332.9\",\n    \"2020-07-26\": \"8535.8\",\n    \"2020-07-27\": \"9384\",\n    \"2020-07-28\": \"9328.023505\",\n    \"2020-07-29\": \"9432.9\",\n    \"2020-07-30\": \"9368.33865\",\n    \"2020-07-31\": \"9626.7\",\n    \"2020-08-01\": \"10021\",\n    \"2020-08-02\": \"9382\",\n    \"2020-08-03\": \"9544.8\",\n    \"2020-08-04\": \"9465.858\",\n    \"2020-08-05\": \"9884.9\",\n    \"2020-08-06\": \"9908.5\",\n    \"2020-08-07\": \"9826.2\",\n    \"2020-08-08\": \"9976.4\",\n    \"2020-08-09\": \"9910\",\n    \"2020-08-10\": \"10131.6867\",\n    \"2020-08-11\": \"9696.3\",\n    \"2020-08-12\": \"9820.588635\",\n    \"2020-08-13\": \"9979.9\",\n    \"2020-08-14\": \"9927.8\",\n    \"2020-08-15\": \"10036\",\n    \"2020-08-16\": \"10067\",\n    \"2020-08-17\": \"10357\",\n    \"2020-08-18\": \"10012\",\n    \"2020-08-19\": \"9944.1\",\n    \"2020-08-20\": \"10011.32975\",\n    \"2020-08-21\": \"9762.9\",\n    \"2020-08-22\": \"9905.2\",\n    \"2020-08-23\": \"9886.3\",\n    \"2020-08-24\": \"9983.5\",\n    \"2020-08-25\": \"9583.2\",\n    \"2020-08-26\": \"9687.2595\",\n    \"2020-08-27\": \"9579.70766833\",\n    \"2020-08-28\": \"9693.4\",\n    \"2020-08-29\": \"9652.7\",\n    \"2020-08-30\": \"9842\",\n    \"2020-08-31\": \"9776.05551876\",\n    \"2020-09-01\": \"10016\",\n    \"2020-09-02\": \"9623\",\n    \"2020-09-03\": \"8584.9\",\n    \"2020-09-04\": \"8858.8\",\n    \"2020-09-05\": \"8613.2\",\n    \"2020-09-06\": \"8683.4\",\n    \"2020-09-07\": \"8801\",\n    \"2020-09-08\": \"8624.3\",\n    \"2020-09-09\": \"8673\",\n    \"2020-09-10\": \"8756.6\",\n    \"2020-09-11\": \"8782.44429478\",\n    \"2020-09-12\": \"8827.2\",\n    \"2020-09-13\": \"8721.985672\",\n    \"2020-09-14\": \"9003.1\",\n    \"2020-09-15\": \"9116.4\",\n    \"2020-09-16\": \"9293.320715\",\n    \"2020-09-17\": \"9249.45357772\",\n    \"2020-09-18\": \"9254.1\",\n    \"2020-09-19\": \"9372.8\",\n    \"2020-09-20\": \"9229.3\",\n    \"2020-09-21\": \"8869.5\",\n    \"2020-09-22\": \"9009.20502\",\n    \"2020-09-23\": \"8797.9\",\n    \"2020-09-24\": \"9209.36378297\",\n    \"2020-09-25\": \"9200.6\",\n    \"2020-09-26\": \"9247.2989264\",\n    \"2020-09-27\": \"9286.4\",\n    \"2020-09-28\": \"9169.79543\",\n    \"2020-09-29\": \"9245.1\",\n    \"2020-09-30\": \"9199.9\",\n    \"2020-10-01\": \"9043.8\",\n    \"2020-10-02\": \"9038.34262093\",\n    \"2020-10-03\": \"9011.3\",\n    \"2020-10-04\": \"9119.8\",\n    \"2020-10-05\": \"9171.34256862\",\n    \"2020-10-06\": \"9044.3\",\n    \"2020-10-07\": \"9080.925579\",\n    \"2020-10-08\": \"9311\",\n    \"2020-10-09\": \"9367.0879637\",\n    \"2020-10-10\": \"9557.3\",\n    \"2020-10-11\": \"9632.4\",\n    \"2020-10-12\": \"9787\",\n    \"2020-10-13\": \"9744.63651\",\n    \"2020-10-14\": \"9741.446\",\n    \"2020-10-15\": \"9827.2\",\n    \"2020-10-16\": \"9665.93281539\",\n    \"2020-10-17\": \"9709.8\",\n    \"2020-10-18\": \"9832.1\",\n    \"2020-10-19\": \"9986.8\",\n    \"2020-10-20\": \"10083\",\n    \"2020-10-21\": \"10810\",\n    \"2020-10-22\": \"10986.33690728\",\n    \"2020-10-23\": \"10905\",\n    \"2020-10-24\": \"11066\",\n    \"2020-10-25\": \"11002\",\n    \"2020-10-26\": \"11059.5954\",\n    \"2020-10-27\": \"11565\",\n    \"2020-10-28\": \"11296.66188\",\n    \"2020-10-29\": \"11507\",\n    \"2020-10-30\": \"11621.04628011\",\n    \"2020-10-31\": \"11807\",\n    \"2020-11-01\": \"11812\",\n    \"2020-11-02\": \"11650.142295\",\n    \"2020-11-03\": \"11935\",\n    \"2020-11-04\": \"12060\",\n    \"2020-11-05\": \"13188\",\n    \"2020-11-06\": \"13115\",\n    \"2020-11-07\": \"12501\",\n    \"2020-11-08\": \"13034\",\n    \"2020-11-09\": \"12970\",\n    \"2020-11-10\": \"12957\",\n    \"2020-11-11\": \"13335\",\n    \"2020-11-12\": \"13806\",\n    \"2020-11-13\": \"13780\",\n    \"2020-11-14\": \"13567\",\n    \"2020-11-15\": \"13475\",\n    \"2020-11-16\": \"14091\",\n    \"2020-11-17\": \"14893\",\n    \"2020-11-18\": \"14997\",\n    \"2020-11-19\": \"15015\",\n    \"2020-11-20\": \"15746\",\n    \"2020-11-21\": \"15738\",\n    \"2020-11-22\": \"15501\",\n    \"2020-11-23\": \"15511.829415\",\n    \"2020-11-24\": \"16081\",\n    \"2020-11-25\": \"15713\",\n    \"2020-11-26\": \"14446\",\n    \"2020-11-27\": \"14353\",\n    \"2020-11-28\": \"14833.1360792\",\n    \"2020-11-29\": \"15200\",\n    \"2020-11-30\": \"16502.1975\",\n    \"2020-12-01\": \"15573\",\n    \"2020-12-02\": \"15884\",\n    \"2020-12-03\": \"16017\",\n    \"2020-12-04\": \"15389\",\n    \"2020-12-05\": \"15803\",\n    \"2020-12-06\": \"16013\",\n    \"2020-12-07\": \"15848\",\n    \"2020-12-08\": \"15146\",\n    \"2020-12-09\": \"15367\",\n    \"2020-12-10\": \"15042\",\n    \"2020-12-11\": \"14903\",\n    \"2020-12-12\": \"15537\",\n    \"2020-12-13\": \"15798\",\n    \"2020-12-14\": \"15872\",\n    \"2020-12-15\": \"16008.19387853\",\n    \"2020-12-16\": \"17479\",\n    \"2020-12-17\": \"18584\",\n    \"2020-12-18\": \"18867\",\n    \"2020-12-19\": \"19418\",\n    \"2020-12-20\": \"19183\",\n    \"2020-12-21\": \"18557\",\n    \"2020-12-22\": \"19535\",\n    \"2020-12-23\": \"19039\",\n    \"2020-12-24\": \"19481.78293239\",\n    \"2020-12-25\": \"20274\",\n    \"2020-12-26\": \"21652.58755023\",\n    \"2020-12-27\": \"21508\",\n    \"2020-12-28\": \"22139\",\n    \"2020-12-29\": \"22297.7403996\",\n    \"2020-12-30\": \"23472\",\n    \"2020-12-31\": \"23690\",\n    \"2021-01-01\": \"24027\",\n    \"2021-01-02\": \"26330\",\n    \"2021-01-03\": \"26938\",\n    \"2021-01-04\": \"26140\",\n    \"2021-01-05\": \"27633\",\n    \"2021-01-06\": \"29800\",\n    \"2021-01-07\": \"32100\",\n    \"2021-01-08\": \"33136\",\n    \"2021-01-09\": \"32852.35523174\",\n    \"2021-01-10\": \"31351\",\n    \"2021-01-11\": \"29119.3621636\",\n    \"2021-01-12\": \"27980\",\n    \"2021-01-13\": \"30716\",\n    \"2021-01-14\": \"32240\",\n    \"2021-01-15\": \"30427\",\n    \"2021-01-16\": \"29834\",\n    \"2021-01-17\": \"29746\",\n    \"2021-01-18\": \"30387.44200515\",\n    \"2021-01-19\": \"29547\",\n    \"2021-01-20\": \"29320.74679072\",\n    \"2021-01-21\": \"25341.18102776\",\n    \"2021-01-22\": \"27167\",\n    \"2021-01-23\": \"26379\",\n    \"2021-01-24\": \"26540.02372226\",\n    \"2021-01-25\": \"26575.73258597\",\n    \"2021-01-26\": \"26734\",\n    \"2021-01-27\": \"25143\",\n    \"2021-01-28\": \"27527\",\n    \"2021-01-29\": \"28323\",\n    \"2021-01-30\": \"28219\",\n    \"2021-01-31\": \"27326\",\n    \"2021-02-01\": \"27804\",\n    \"2021-02-02\": \"29530\",\n    \"2021-02-03\": \"31294.70445874\",\n    \"2021-02-04\": \"30890.34991168\",\n    \"2021-02-05\": \"31755\",\n    \"2021-02-06\": \"32562\",\n    \"2021-02-07\": \"32252.36996814\",\n    \"2021-02-08\": \"38522\",\n    \"2021-02-09\": \"38367\",\n    \"2021-02-10\": \"37003\",\n    \"2021-02-11\": \"39543\",\n    \"2021-02-12\": \"39099\",\n    \"2021-02-13\": \"38924\",\n    \"2021-02-14\": \"40071\",\n    \"2021-02-15\": \"39515\",\n    \"2021-02-16\": \"40706.97222073\",\n    \"2021-02-17\": \"43227.84574981\",\n    \"2021-02-18\": \"42639\",\n    \"2021-02-19\": \"46076\",\n    \"2021-02-20\": \"46154.37301358\",\n    \"2021-02-21\": \"47330\",\n    \"2021-02-22\": \"44476\",\n    \"2021-02-23\": \"40212\",\n    \"2021-02-24\": \"40827\",\n    \"2021-02-25\": \"38774.15894126\",\n    \"2021-02-26\": \"38399\",\n    \"2021-02-27\": \"38227\",\n    \"2021-02-28\": \"37484\",\n    \"2021-03-01\": \"41173\",\n    \"2021-03-02\": \"40174\",\n    \"2021-03-03\": \"41823\",\n    \"2021-03-04\": \"40449\",\n    \"2021-03-05\": \"40983\",\n    \"2021-03-06\": \"41010\",\n    \"2021-03-07\": \"42767\",\n    \"2021-03-08\": \"44212\",\n    \"2021-03-09\": \"46179\",\n    \"2021-03-10\": \"46915\",\n    \"2021-03-11\": \"48294\",\n    \"2021-03-12\": \"47954.49588332\",\n    \"2021-03-13\": \"51178\",\n    \"2021-03-14\": \"49375.87194011\",\n    \"2021-03-15\": \"46658.32101958\",\n    \"2021-03-16\": \"47834\",\n    \"2021-03-17\": \"49129\",\n    \"2021-03-18\": \"48364\",\n    \"2021-03-19\": \"48591\",\n    \"2021-03-20\": \"48638\",\n    \"2021-03-21\": \"48297\",\n    \"2021-03-22\": \"45320\",\n    \"2021-03-23\": \"45904\",\n    \"2021-03-24\": \"44292\",\n    \"2021-03-25\": \"43637\",\n    \"2021-03-26\": \"46627\",\n    \"2021-03-27\": \"47263\",\n    \"2021-03-28\": \"47307\",\n    \"2021-03-29\": \"49006.49344536\",\n    \"2021-03-30\": \"50192\",\n    \"2021-03-31\": \"50035\",\n    \"2021-04-01\": \"49863\",\n    \"2021-04-02\": \"50164.53467394\",\n    \"2021-04-03\": \"48462.08233237\",\n    \"2021-04-04\": \"49501\",\n    \"2021-04-05\": \"50070\",\n    \"2021-04-06\": \"48834\",\n    \"2021-04-07\": \"47138\",\n    \"2021-04-08\": \"48756\",\n    \"2021-04-09\": \"48852\",\n    \"2021-04-10\": \"50268\",\n    \"2021-04-11\": \"50444.64934969\",\n    \"2021-04-12\": \"50262.00107621\",\n    \"2021-04-13\": \"53144\",\n    \"2021-04-14\": \"52502\",\n    \"2021-04-15\": \"52801\",\n    \"2021-04-16\": \"51277\",\n    \"2021-04-17\": \"50015\",\n    \"2021-04-18\": \"46933\",\n    \"2021-04-19\": \"46185\",\n    \"2021-04-20\": \"46928\",\n    \"2021-04-21\": \"44676\",\n    \"2021-04-22\": \"43062\",\n    \"2021-04-23\": \"42268\",\n    \"2021-04-24\": \"41442\",\n    \"2021-04-25\": \"40628\",\n    \"2021-04-26\": \"44746\",\n    \"2021-04-27\": \"45531\",\n    \"2021-04-28\": \"45203\",\n    \"2021-04-29\": \"44180.26183656\",\n    \"2021-04-30\": \"47940\",\n    \"2021-05-01\": \"48065\",\n    \"2021-05-02\": \"47053\",\n    \"2021-05-03\": \"47351\",\n    \"2021-05-04\": \"44315\",\n    \"2021-05-05\": \"47853.63564834\",\n    \"2021-05-06\": \"46789\",\n    \"2021-05-07\": \"47147\",\n    \"2021-05-08\": \"48431\",\n    \"2021-05-09\": \"47866.75182028\",\n    \"2021-05-10\": \"46008.5313306\",\n    \"2021-05-11\": \"46689\",\n    \"2021-05-12\": \"41039\",\n    \"2021-05-13\": \"41228\",\n    \"2021-05-14\": \"41102\",\n    \"2021-05-15\": \"38550\",\n    \"2021-05-16\": \"38340\",\n    \"2021-05-17\": \"35790\",\n    \"2021-05-18\": \"35122\",\n    \"2021-05-19\": \"30163\",\n    \"2021-05-20\": \"33199\",\n    \"2021-05-21\": \"30699\",\n    \"2021-05-22\": \"30833\",\n    \"2021-05-23\": \"28521\",\n    \"2021-05-24\": \"31810\",\n    \"2021-05-25\": \"31351\",\n    \"2021-05-26\": \"32224\",\n    \"2021-05-27\": \"31607\",\n    \"2021-05-28\": \"29248\",\n    \"2021-05-29\": \"28362\",\n    \"2021-05-30\": \"29227\",\n    \"2021-05-31\": \"30474\",\n    \"2021-06-01\": \"30016\",\n    \"2021-06-02\": \"30764\",\n    \"2021-06-03\": \"32333\",\n    \"2021-06-04\": \"30296\",\n    \"2021-06-05\": \"29207\",\n    \"2021-06-06\": \"29403.146421\",\n    \"2021-06-07\": \"27542\",\n    \"2021-06-08\": \"27441\",\n    \"2021-06-09\": \"30686\",\n    \"2021-06-10\": \"30100\",\n    \"2021-06-11\": \"30836\",\n    \"2021-06-12\": \"29311\",\n    \"2021-06-13\": \"32215\",\n    \"2021-06-14\": \"33410\",\n    \"2021-06-15\": \"33098\",\n    \"2021-06-16\": \"31972\",\n    \"2021-06-17\": \"31955\",\n    \"2021-06-18\": \"30215\",\n    \"2021-06-19\": \"29980\",\n    \"2021-06-20\": \"30015\",\n    \"2021-06-21\": \"26691\",\n    \"2021-06-22\": \"27278\",\n    \"2021-06-23\": \"28250\",\n    \"2021-06-24\": \"29040\",\n    \"2021-06-25\": \"26466\",\n    \"2021-06-26\": \"27105\",\n    \"2021-06-27\": \"29103\",\n    \"2021-06-28\": \"28920\",\n    \"2021-06-29\": \"30129\",\n    \"2021-06-30\": \"29555\",\n    \"2021-07-01\": \"28327\",\n    \"2021-07-02\": \"28514\",\n    \"2021-07-03\": \"29234\",\n    \"2021-07-04\": \"29755\",\n    \"2021-07-05\": \"28386\",\n    \"2021-07-06\": \"28967\",\n    \"2021-07-07\": \"28665.915036\",\n    \"2021-07-08\": \"27747\",\n    \"2021-07-09\": \"28423.72351\",\n    \"2021-07-10\": \"28199\",\n    \"2021-07-11\": \"28829\",\n    \"2021-07-12\": \"27885\",\n    \"2021-07-13\": \"27797\",\n    \"2021-07-14\": \"27733\",\n    \"2021-07-15\": \"26976\",\n    \"2021-07-16\": \"26621\",\n    \"2021-07-17\": \"26730\",\n    \"2021-07-18\": \"26942.35752\",\n    \"2021-07-19\": \"26140\",\n    \"2021-07-20\": \"25319\",\n    \"2021-07-21\": \"27265\",\n    \"2021-07-22\": \"27480\",\n    \"2021-07-23\": \"28585\",\n    \"2021-07-24\": \"29117\",\n    \"2021-07-25\": \"30090\",\n    \"2021-07-26\": \"31568\",\n    \"2021-07-27\": \"33471.01890053\",\n    \"2021-07-28\": \"33782\",\n    \"2021-07-29\": \"33662\",\n    \"2021-07-30\": \"35529\",\n    \"2021-07-31\": \"34887\",\n    \"2021-08-01\": \"33600\",\n    \"2021-08-02\": \"32974\",\n    \"2021-08-03\": \"32136\",\n    \"2021-08-04\": \"33556\",\n    \"2021-08-05\": \"34530\",\n    \"2021-08-06\": \"36430\",\n    \"2021-08-07\": \"37888.950384\",\n    \"2021-08-08\": \"37260.0632724\",\n    \"2021-08-09\": \"39511\",\n    \"2021-08-10\": \"38885.488776\",\n    \"2021-08-11\": \"38770\",\n    \"2021-08-12\": \"37869.258098\",\n    \"2021-08-13\": \"40550.160015\",\n    \"2021-08-14\": \"39891\",\n    \"2021-08-15\": \"39907.120202\",\n    \"2021-08-16\": \"39103\",\n    \"2021-08-17\": \"38153.542692\",\n    \"2021-08-18\": \"38201\",\n    \"2021-08-19\": \"40060\",\n    \"2021-08-20\": \"42161\",\n    \"2021-08-21\": \"41745\",\n    \"2021-08-22\": \"42116\",\n    \"2021-08-23\": \"42167\",\n    \"2021-08-24\": \"40596.01743\",\n    \"2021-08-25\": \"41613\",\n    \"2021-08-26\": \"39834\",\n    \"2021-08-27\": \"41613\",\n    \"2021-08-28\": \"41468\",\n    \"2021-08-29\": \"41361.993475\",\n    \"2021-08-30\": \"39871.998641\",\n    \"2021-08-31\": \"39913\",\n    \"2021-09-01\": \"41234\",\n    \"2021-09-02\": \"41492\",\n    \"2021-09-03\": \"42121\",\n    \"2021-09-04\": \"41990\",\n    \"2021-09-05\": \"43563\",\n    \"2021-09-06\": \"44363\",\n    \"2021-09-07\": \"39475\",\n    \"2021-09-08\": \"38983\",\n    \"2021-09-09\": \"39227\",\n    \"2021-09-10\": \"37984.20678\",\n    \"2021-09-11\": \"38231\",\n    \"2021-09-12\": \"38973\",\n    \"2021-09-13\": \"38066\",\n    \"2021-09-14\": \"39956\",\n    \"2021-09-15\": \"40756\",\n    \"2021-09-16\": \"40620\",\n    \"2021-09-17\": \"40336\",\n    \"2021-09-18\": \"41227\",\n    \"2021-09-19\": \"40237\",\n    \"2021-09-20\": \"36691\",\n    \"2021-09-21\": \"34759\",\n    \"2021-09-22\": \"37289\",\n    \"2021-09-23\": \"38237\",\n    \"2021-09-24\": \"36580\",\n    \"2021-09-25\": \"36433\",\n    \"2021-09-26\": \"36840.5283\",\n    \"2021-09-27\": \"36066.02043\",\n    \"2021-09-28\": \"35132\",\n    \"2021-09-29\": \"35819\",\n    \"2021-09-30\": \"37800.21875\",\n    \"2021-10-01\": \"41525.615004\",\n    \"2021-10-02\": \"41093\",\n    \"2021-10-03\": \"41557\",\n    \"2021-10-04\": \"42377.4252\",\n    \"2021-10-05\": \"44431\",\n    \"2021-10-06\": \"47940\",\n    \"2021-10-07\": \"46546\",\n    \"2021-10-08\": \"46589.478816\",\n    \"2021-10-09\": \"47444\",\n    \"2021-10-10\": \"47295\",\n    \"2021-10-11\": \"49770\",\n    \"2021-10-12\": \"48502\",\n    \"2021-10-13\": \"49474\",\n    \"2021-10-14\": \"49467.464853\",\n    \"2021-10-15\": \"53202.35085\",\n    \"2021-10-16\": \"52476\",\n    \"2021-10-17\": \"53023\",\n    \"2021-10-18\": \"53410.8943\",\n    \"2021-10-19\": \"55230\",\n    \"2021-10-20\": \"56636\",\n    \"2021-10-21\": \"53494\",\n    \"2021-10-22\": \"52126\",\n    \"2021-10-23\": \"52664\",\n    \"2021-10-24\": \"52315\",\n    \"2021-10-25\": \"54348\",\n    \"2021-10-26\": \"51974.7055\",\n    \"2021-10-27\": \"50390\",\n    \"2021-10-28\": \"51841.917754\",\n    \"2021-10-29\": \"53863\",\n    \"2021-10-30\": \"53520.328125\",\n    \"2021-10-31\": \"53062\",\n    \"2021-11-01\": \"52499\",\n    \"2021-11-02\": \"54632\",\n    \"2021-11-03\": \"54211.72638\",\n    \"2021-11-04\": \"53160\",\n    \"2021-11-05\": \"52816\",\n    \"2021-11-06\": \"53230\",\n    \"2021-11-07\": \"54691\",\n    \"2021-11-08\": \"58275\",\n    \"2021-11-09\": \"57733\",\n    \"2021-11-10\": \"56514\",\n    \"2021-11-11\": \"56608\",\n    \"2021-11-12\": \"56050\",\n    \"2021-11-13\": \"56283\",\n    \"2021-11-14\": \"57221\",\n    \"2021-11-15\": \"55932\",\n    \"2021-11-16\": \"53105\",\n    \"2021-11-17\": \"53368\",\n    \"2021-11-18\": \"50076.857124\",\n    \"2021-11-19\": \"51528\",\n    \"2021-11-20\": \"52957\",\n    \"2021-11-21\": \"52047\",\n    \"2021-11-22\": \"50097\",\n    \"2021-11-23\": \"51204.01264\",\n    \"2021-11-24\": \"51046\",\n    \"2021-11-25\": \"52584\",\n    \"2021-11-26\": \"47579\",\n    \"2021-11-27\": \"48412\",\n    \"2021-11-28\": \"50806\",\n    \"2021-11-29\": \"51217\",\n    \"2021-11-30\": \"50246\",\n    \"2021-12-01\": \"50579.663247\",\n    \"2021-12-02\": \"50007\",\n    \"2021-12-03\": \"47502\",\n    \"2021-12-04\": \"43530\",\n    \"2021-12-05\": \"43765\",\n    \"2021-12-06\": \"44829\",\n    \"2021-12-07\": \"44919.144964\",\n    \"2021-12-08\": \"44516\",\n    \"2021-12-09\": \"42154\",\n    \"2021-12-10\": \"41742\",\n    \"2021-12-11\": \"43674\",\n    \"2021-12-12\": \"44311\",\n    \"2021-12-13\": \"41421\",\n    \"2021-12-14\": \"42989\",\n    \"2021-12-15\": \"43295\",\n    \"2021-12-16\": \"42049\",\n    \"2021-12-17\": \"41093\",\n    \"2021-12-18\": \"41709\",\n    \"2021-12-19\": \"41555\",\n    \"2021-12-20\": \"41590\",\n    \"2021-12-21\": \"43347.490305\",\n    \"2021-12-22\": \"42916\",\n    \"2021-12-23\": \"44880\",\n    \"2021-12-24\": \"44821.215516\",\n    \"2021-12-25\": \"44474\",\n    \"2021-12-26\": \"44844\",\n    \"2021-12-27\": \"44734.07707\",\n    \"2021-12-28\": \"42022\",\n    \"2021-12-29\": \"40916\",\n    \"2021-12-30\": \"41638.812345\",\n    \"2021-12-31\": \"40643.87811\",\n    \"2022-01-01\": \"42017\",\n    \"2022-01-02\": \"41600\",\n    \"2022-01-03\": \"41105.915025\",\n    \"2022-01-04\": \"40620\",\n    \"2022-01-05\": \"38410\",\n    \"2022-01-06\": \"38182\",\n    \"2022-01-07\": \"36552\",\n    \"2022-01-08\": \"36702\",\n    \"2022-01-09\": \"36872\",\n    \"2022-01-10\": \"36896\",\n    \"2022-01-11\": \"37600\",\n    \"2022-01-12\": \"38372\",\n    \"2022-01-13\": \"37143\",\n    \"2022-01-14\": \"37745\",\n    \"2022-01-15\": \"37731\",\n    \"2022-01-16\": \"37767.96877\",\n    \"2022-01-17\": \"37028\",\n    \"2022-01-18\": \"37405\",\n    \"2022-01-19\": \"36739\",\n    \"2022-01-20\": \"35977\",\n    \"2022-01-21\": \"32188\",\n    \"2022-01-22\": \"30944\",\n    \"2022-01-23\": \"32019\",\n    \"2022-01-24\": \"32419\",\n    \"2022-01-25\": \"32727\",\n    \"2022-01-26\": \"32770\",\n    \"2022-01-27\": \"33387\",\n    \"2022-01-28\": \"33898.80927358\",\n    \"2022-01-29\": \"34278\",\n    \"2022-01-30\": \"34011\",\n    \"2022-01-31\": \"34319\",\n    \"2022-02-01\": \"34341\",\n    \"2022-02-02\": \"32689\",\n    \"2022-02-03\": \"32671\",\n    \"2022-02-04\": \"36315\",\n    \"2022-02-05\": \"36194\",\n    \"2022-02-06\": \"37058\",\n    \"2022-02-07\": \"38349\",\n    \"2022-02-08\": \"38580\",\n    \"2022-02-09\": \"38916\",\n    \"2022-02-10\": \"38166\",\n    \"2022-02-11\": \"37357.17466814\",\n    \"2022-02-12\": \"37217\",\n    \"2022-02-13\": \"37031\",\n    \"2022-02-14\": \"37635\",\n    \"2022-02-15\": \"39237\",\n    \"2022-02-16\": \"38604\",\n    \"2022-02-17\": \"35682\",\n    \"2022-02-18\": \"35359\",\n    \"2022-02-19\": \"35453\",\n    \"2022-02-20\": \"33935\",\n    \"2022-02-21\": \"32782\",\n    \"2022-02-22\": \"33785\",\n    \"2022-02-23\": \"32971\",\n    \"2022-02-24\": \"34298\",\n    \"2022-02-25\": \"34813\",\n    \"2022-02-26\": \"34707\",\n    \"2022-02-27\": \"33726\",\n    \"2022-02-28\": \"38523\",\n    \"2022-03-01\": \"39930\",\n    \"2022-03-02\": \"39553\",\n    \"2022-03-03\": \"38384.298573\",\n    \"2022-03-04\": \"35778\",\n    \"2022-03-05\": \"36015\",\n    \"2022-03-06\": \"35374\",\n    \"2022-03-07\": \"34993\",\n    \"2022-03-08\": \"35586\",\n    \"2022-03-09\": \"37895\",\n    \"2022-03-10\": \"35833\",\n    \"2022-03-11\": \"35500\",\n    \"2022-03-12\": \"35572\",\n    \"2022-03-13\": \"34542\",\n    \"2022-03-14\": \"36289\",\n    \"2022-03-15\": \"35849\",\n    \"2022-03-16\": \"37357\",\n    \"2022-03-17\": \"36895\",\n    \"2022-03-18\": \"37762\",\n    \"2022-03-19\": \"38167.23678047\",\n    \"2022-03-20\": \"37363\",\n    \"2022-03-21\": \"37219\",\n    \"2022-03-22\": \"38415\",\n    \"2022-03-23\": \"38967.30222\",\n    \"2022-03-24\": \"39973\",\n    \"2022-03-25\": \"40366\",\n    \"2022-03-26\": \"40547\",\n    \"2022-03-27\": \"42629\",\n    \"2022-03-28\": \"42833\",\n    \"2022-03-29\": \"42757\",\n    \"2022-03-30\": \"42121\",\n    \"2022-03-31\": \"41128\",\n    \"2022-04-01\": \"41907\",\n    \"2022-04-02\": \"41475\",\n    \"2022-04-03\": \"41993\",\n    \"2022-04-04\": \"42476\",\n    \"2022-04-05\": \"41697\",\n    \"2022-04-06\": \"39637\",\n    \"2022-04-07\": \"39976\",\n    \"2022-04-08\": \"38847.6075\",\n    \"2022-04-09\": \"39354\",\n    \"2022-04-10\": \"38743\",\n    \"2022-04-11\": \"36325\",\n    \"2022-04-12\": \"37025\",\n    \"2022-04-13\": \"37787\",\n    \"2022-04-14\": \"36906\",\n    \"2022-04-15\": \"37530\",\n    \"2022-04-16\": \"37386\",\n    \"2022-04-17\": \"36726.3781\",\n    \"2022-04-18\": \"37880\",\n    \"2022-04-19\": \"38464\",\n    \"2022-04-20\": \"38136.39021\",\n    \"2022-04-21\": \"37382\",\n    \"2022-04-22\": \"36781\",\n    \"2022-04-23\": \"36531.88719853\",\n    \"2022-04-24\": \"36530\",\n    \"2022-04-25\": \"37774.424674\",\n    \"2022-04-26\": \"35836\",\n    \"2022-04-27\": \"37199\",\n    \"2022-04-28\": \"37879\",\n    \"2022-04-29\": \"36619.5869\",\n    \"2022-04-30\": \"35717\",\n    \"2022-05-01\": \"36521\",\n    \"2022-05-02\": \"36657\",\n    \"2022-05-03\": \"35871\",\n    \"2022-05-04\": \"37372\",\n    \"2022-05-05\": \"34710\",\n    \"2022-05-06\": \"34184.42632\",\n    \"2022-05-07\": \"33663\",\n    \"2022-05-08\": \"32338\",\n    \"2022-05-09\": \"28518\",\n    \"2022-05-10\": \"29507.74594981\",\n    \"2022-05-11\": \"27646\",\n    \"2022-05-12\": \"27988\",\n    \"2022-05-13\": \"28174\",\n    \"2022-05-14\": \"29009\",\n    \"2022-05-15\": \"30108\",\n    \"2022-05-16\": \"28626\",\n    \"2022-05-17\": \"28862.80196911\",\n    \"2022-05-18\": \"27379\",\n    \"2022-05-19\": \"28671\",\n    \"2022-05-20\": \"27647\",\n    \"2022-05-21\": \"27872\",\n    \"2022-05-22\": \"28639\",\n    \"2022-05-23\": \"27229\",\n    \"2022-05-24\": \"27627\",\n    \"2022-05-25\": \"27647.57064\",\n    \"2022-05-26\": \"27213\",\n    \"2022-05-27\": \"26650\",\n    \"2022-05-28\": \"27051\",\n    \"2022-05-29\": \"27492\",\n    \"2022-05-30\": \"29460\",\n    \"2022-05-31\": \"29615\",\n    \"2022-06-01\": \"28002\",\n    \"2022-06-02\": \"28346.89879\",\n    \"2022-06-03\": \"27722.05512176\",\n    \"2022-06-04\": \"27850\",\n    \"2022-06-05\": \"27897\",\n    \"2022-06-06\": \"29321\",\n    \"2022-06-07\": \"29112\",\n    \"2022-06-08\": \"28199.4908\",\n    \"2022-06-09\": \"28353\",\n    \"2022-06-10\": \"27663\",\n    \"2022-06-11\": \"27033\",\n    \"2022-06-12\": \"25402.56441\",\n    \"2022-06-13\": \"21606.8175\",\n    \"2022-06-14\": \"21247.87104\",\n    \"2022-06-15\": \"21618.7776\",\n    \"2022-06-16\": \"19339\",\n    \"2022-06-17\": \"19445\",\n    \"2022-06-18\": \"18080.408704\",\n    \"2022-06-19\": \"19592.0728\",\n    \"2022-06-20\": \"19535\",\n    \"2022-06-21\": \"19635\",\n    \"2022-06-22\": \"18878.19264\",\n    \"2022-06-23\": \"20044.04005\",\n    \"2022-06-24\": \"20092.55869486\",\n    \"2022-06-25\": \"20341.2465\",\n    \"2022-06-26\": \"19930\",\n    \"2022-06-27\": \"19578.41905\",\n    \"2022-06-28\": \"19255.33042826\",\n    \"2022-06-29\": \"19240.725\",\n    \"2022-06-30\": \"19024.88225\",\n    \"2022-07-01\": \"18473\",\n    \"2022-07-02\": \"18439\",\n    \"2022-07-03\": \"18506\",\n    \"2022-07-04\": \"19374.75978\",\n    \"2022-07-05\": \"19644.67238689\",\n    \"2022-07-06\": \"20186\",\n    \"2022-07-07\": \"21279\",\n    \"2022-07-08\": \"21204.81825\",\n    \"2022-07-09\": \"21182\",\n    \"2022-07-10\": \"20474\",\n    \"2022-07-11\": \"19783\",\n    \"2022-07-12\": \"19267\",\n    \"2022-07-13\": \"20204.79832\",\n    \"2022-07-14\": \"20513.690325\",\n    \"2022-07-15\": \"20636.75739693\",\n    \"2022-07-16\": \"21034\",\n    \"2022-07-17\": \"20583\",\n    \"2022-07-18\": \"22117.08269662\",\n    \"2022-07-19\": \"22867\",\n    \"2022-07-20\": \"22786\",\n    \"2022-07-21\": \"22645.624995\",\n    \"2022-07-22\": \"22200.60991558\",\n    \"2022-07-23\": \"21974\",\n    \"2022-07-24\": \"22131\",\n    \"2022-07-25\": \"20829.30384299\",\n    \"2022-07-26\": \"21004\",\n    \"2022-07-27\": \"22500\",\n    \"2022-07-28\": \"23393.6956\",\n    \"2022-07-29\": \"23240.0038\",\n    \"2022-07-30\": \"23114\",\n    \"2022-07-31\": \"22822.2965\",\n    \"2022-08-01\": \"22691\",\n    \"2022-08-02\": \"22633\",\n    \"2022-08-03\": \"22477\",\n    \"2022-08-04\": \"22066\",\n    \"2022-08-05\": \"22900\",\n    \"2022-08-06\": \"22508\",\n    \"2022-08-07\": \"22785\",\n    \"2022-08-08\": \"23355\",\n    \"2022-08-09\": \"22661\",\n    \"2022-08-10\": \"23263.590025\",\n    \"2022-08-11\": \"23204\",\n    \"2022-08-12\": \"23770\",\n    \"2022-08-13\": \"23820.2976\",\n    \"2022-08-14\": \"23692\",\n    \"2022-08-15\": \"23712\",\n    \"2022-08-16\": \"23459\",\n    \"2022-08-17\": \"22930\",\n    \"2022-08-18\": \"22952.2366\",\n    \"2022-08-19\": \"20748.20670566\",\n    \"2022-08-20\": \"21055\",\n    \"2022-08-21\": \"21446\",\n    \"2022-08-22\": \"21537\",\n    \"2022-08-23\": \"21599\",\n    \"2022-08-24\": \"21435\",\n    \"2022-08-25\": \"21629\",\n    \"2022-08-26\": \"20320\",\n    \"2022-08-27\": \"20108\",\n    \"2022-08-28\": \"19685\",\n    \"2022-08-29\": \"20297\",\n    \"2022-08-30\": \"19748\",\n    \"2022-08-31\": \"19980.17558275\",\n    \"2022-09-01\": \"20235.566505\",\n    \"2022-09-02\": \"20058\",\n    \"2022-09-03\": \"19915\",\n    \"2022-09-04\": \"20174\",\n    \"2022-09-05\": \"19894\",\n    \"2022-09-06\": \"18983\",\n    \"2022-09-07\": \"19284\",\n    \"2022-09-08\": \"19303\",\n    \"2022-09-09\": \"21055.44375\",\n    \"2022-09-10\": \"21571\",\n    \"2022-09-11\": \"21682.126305\",\n    \"2022-09-12\": \"22106.502\",\n    \"2022-09-13\": \"20220\",\n    \"2022-09-14\": \"20262.902925\",\n    \"2022-09-15\": \"19737\",\n    \"2022-09-16\": \"19783\",\n    \"2022-09-17\": \"20101\",\n    \"2022-09-18\": \"19383\",\n    \"2022-09-19\": \"19485\",\n    \"2022-09-20\": \"18943\",\n    \"2022-09-21\": \"18788\",\n    \"2022-09-22\": \"19731\",\n    \"2022-09-23\": \"19915.82186857\",\n    \"2022-09-24\": \"19550.00000001\",\n    \"2022-09-25\": \"19395.08887714\",\n    \"2022-09-26\": \"20004\",\n    \"2022-09-27\": \"19904\",\n    \"2022-09-28\": \"20004.99999999\",\n    \"2022-09-29\": \"19941.715875\",\n    \"2022-09-30\": \"19815\",\n    \"2022-10-01\": \"19702\",\n    \"2022-10-02\": \"19447.663\",\n    \"2022-10-03\": \"19959\",\n    \"2022-10-04\": \"20379.2663303\",\n    \"2022-10-05\": \"20354\",\n    \"2022-10-06\": \"20387\",\n    \"2022-10-07\": \"20059\",\n    \"2022-10-08\": \"19935\",\n    \"2022-10-09\": \"19981\",\n    \"2022-10-10\": \"19690.00000001\",\n    \"2022-10-11\": \"19639.00000001\",\n    \"2022-10-12\": \"19735\",\n    \"2022-10-13\": \"19850\",\n    \"2022-10-14\": \"19732\",\n    \"2022-10-15\": \"19637\",\n    \"2022-10-16\": \"19780\",\n    \"2022-10-17\": \"19861\",\n    \"2022-10-18\": \"19601\",\n    \"2022-10-19\": \"19587\",\n    \"2022-10-20\": \"19480\",\n    \"2022-10-21\": \"19442\",\n    \"2022-10-22\": \"19489\",\n    \"2022-10-23\": \"19852\",\n    \"2022-10-24\": \"19559\",\n    \"2022-10-25\": \"20172\",\n    \"2022-10-26\": \"20594\",\n    \"2022-10-27\": \"20360\",\n    \"2022-10-28\": \"20676\",\n    \"2022-10-29\": \"20895\",\n    \"2022-10-30\": \"20732\",\n    \"2022-10-31\": \"20735\",\n    \"2022-11-01\": \"20736\",\n    \"2022-11-02\": \"20532\",\n    \"2022-11-03\": \"20729\",\n    \"2022-11-04\": \"21224\",\n    \"2022-11-05\": \"21374\",\n    \"2022-11-06\": \"21067\",\n    \"2022-11-07\": \"20558\",\n    \"2022-11-08\": \"18417\",\n    \"2022-11-09\": \"15864\",\n    \"2022-11-10\": \"17236\",\n    \"2022-11-11\": \"16416\",\n    \"2022-11-12\": \"16191\",\n    \"2022-11-13\": \"15817\",\n    \"2022-11-14\": \"16077\",\n    \"2022-11-15\": \"16293\",\n    \"2022-11-16\": \"16025\",\n    \"2022-11-17\": \"16103\",\n    \"2022-11-18\": \"16151\",\n    \"2022-11-19\": \"16157\",\n    \"2022-11-20\": \"15742\",\n    \"2022-11-21\": \"15374\",\n    \"2022-11-22\": \"15719\",\n    \"2022-11-23\": \"15928\",\n    \"2022-11-24\": \"15940\",\n    \"2022-11-25\": \"15854\",\n    \"2022-11-26\": \"15799\",\n    \"2022-11-27\": \"15838\",\n    \"2022-11-28\": \"15660\",\n    \"2022-11-29\": \"15915\",\n    \"2022-11-30\": \"16457\",\n    \"2022-12-01\": \"16130\",\n    \"2022-12-02\": \"16223\",\n    \"2022-12-03\": \"16037\",\n    \"2022-12-04\": \"16236\",\n    \"2022-12-05\": \"16165\",\n    \"2022-12-06\": \"16314\",\n    \"2022-12-07\": \"16014\",\n    \"2022-12-08\": \"16318\",\n    \"2022-12-09\": \"16243\",\n    \"2022-12-10\": \"16240\",\n    \"2022-12-11\": \"16234\",\n    \"2022-12-12\": \"16316\",\n    \"2022-12-13\": \"16681\",\n    \"2022-12-14\": \"16643\",\n    \"2022-12-15\": \"16291\",\n    \"2022-12-16\": \"15677\",\n    \"2022-12-17\": \"15831\",\n    \"2022-12-18\": \"15792\",\n    \"2022-12-19\": \"15471\",\n    \"2022-12-20\": \"15878\",\n    \"2022-12-21\": \"15850\",\n    \"2022-12-22\": \"15858\",\n    \"2022-12-23\": \"15775\",\n    \"2022-12-24\": \"15823\",\n    \"2022-12-25\": \"15855\",\n    \"2022-12-26\": \"15896\",\n    \"2022-12-27\": \"15700\",\n    \"2022-12-28\": \"15560\",\n    \"2022-12-29\": \"15599\",\n    \"2022-12-30\": \"15508\",\n    \"2022-12-31\": \"15422\",\n    \"2023-01-01\": \"15521\",\n    \"2023-01-02\": \"15595\",\n    \"2023-01-03\": \"15798\",\n    \"2023-01-04\": \"15882\",\n    \"2023-01-05\": \"15992\",\n    \"2023-01-06\": \"15916\",\n    \"2023-01-07\": \"15909\",\n    \"2023-01-08\": \"16069\",\n    \"2023-01-09\": \"16006\",\n    \"2023-01-10\": \"16244\",\n    \"2023-01-11\": \"16662\",\n    \"2023-01-12\": \"17354\",\n    \"2023-01-13\": \"18395\",\n    \"2023-01-14\": \"19322\",\n    \"2023-01-15\": \"19285\",\n    \"2023-01-16\": \"19571\",\n    \"2023-01-17\": \"19591\",\n    \"2023-01-18\": \"19161\",\n    \"2023-01-19\": \"19464\",\n    \"2023-01-20\": \"20872\",\n    \"2023-01-21\": \"20988\",\n    \"2023-01-22\": \"20915\",\n    \"2023-01-23\": \"21085\",\n    \"2023-01-24\": \"20784\",\n    \"2023-01-25\": \"21097\",\n    \"2023-01-26\": \"21128\",\n    \"2023-01-27\": \"21239\",\n    \"2023-01-28\": \"21200\",\n    \"2023-01-29\": \"21856\",\n    \"2023-01-30\": \"21043\",\n    \"2023-01-31\": \"21295\",\n    \"2023-02-01\": \"21550\",\n    \"2023-02-02\": \"21540\",\n    \"2023-02-03\": \"21698\",\n    \"2023-02-04\": \"21602\",\n    \"2023-02-05\": \"21274\",\n    \"2023-02-06\": \"21224\",\n    \"2023-02-07\": \"21667\",\n    \"2023-02-08\": \"21440\",\n    \"2023-02-09\": \"20298\",\n    \"2023-02-10\": \"20269\",\n    \"2023-02-11\": \"20497\",\n    \"2023-02-12\": \"20421\",\n    \"2023-02-13\": \"20311\",\n    \"2023-02-14\": \"20695\",\n    \"2023-02-15\": \"22754\",\n    \"2023-02-16\": \"22068\",\n    \"2023-02-17\": \"22974\",\n    \"2023-02-18\": \"23026\",\n    \"2023-02-19\": \"22731\",\n    \"2023-02-20\": \"23250\",\n    \"2023-02-21\": \"22954\",\n    \"2023-02-22\": \"22802\",\n    \"2023-02-23\": \"22603\",\n    \"2023-02-24\": \"22000\",\n    \"2023-02-25\": \"21983\",\n    \"2023-02-26\": \"22330\",\n    \"2023-02-27\": \"22154\",\n    \"2023-02-28\": \"21877\",\n    \"2023-03-01\": \"22165\",\n    \"2023-03-02\": \"22152\",\n    \"2023-03-03\": \"21033\",\n    \"2023-03-04\": \"21026\",\n    \"2023-03-05\": \"21119\",\n    \"2023-03-06\": \"20980\",\n    \"2023-03-07\": \"21051\",\n    \"2023-03-08\": \"20585\",\n    \"2023-03-09\": \"19248\",\n    \"2023-03-10\": \"19003\",\n    \"2023-03-11\": \"19315\",\n    \"2023-03-12\": \"20710\",\n    \"2023-03-13\": \"22516\",\n    \"2023-03-14\": \"23053\",\n    \"2023-03-15\": \"22996\",\n    \"2023-03-16\": \"23598\",\n    \"2023-03-17\": \"25680\",\n    \"2023-03-18\": \"25271\",\n    \"2023-03-19\": \"26241\",\n    \"2023-03-20\": \"25928\",\n    \"2023-03-21\": \"26147\",\n    \"2023-03-22\": \"25124\",\n    \"2023-03-23\": \"26146\",\n    \"2023-03-24\": \"25554\",\n    \"2023-03-25\": \"25551\",\n    \"2023-03-26\": \"25979\",\n    \"2023-03-27\": \"25118\",\n    \"2023-03-28\": \"25167\",\n    \"2023-03-29\": \"26163\",\n    \"2023-03-30\": \"25722\",\n    \"2023-03-31\": \"26273\",\n    \"2023-04-01\": \"26266\",\n    \"2023-04-02\": \"26111\",\n    \"2023-04-03\": \"25521\",\n    \"2023-04-04\": \"25745\",\n    \"2023-04-05\": \"25884\",\n    \"2023-04-06\": \"25714\",\n    \"2023-04-07\": \"25609\",\n    \"2023-04-08\": \"25677\",\n    \"2023-04-09\": \"25990\",\n    \"2023-04-10\": \"27311\",\n    \"2023-04-11\": \"27701\",\n    \"2023-04-12\": \"27218\",\n    \"2023-04-13\": \"27535\",\n    \"2023-04-14\": \"27739\",\n    \"2023-04-15\": \"27608\",\n    \"2023-04-16\": \"27647\",\n    \"2023-04-17\": \"26958\",\n    \"2023-04-18\": \"27726\",\n    \"2023-04-19\": \"26363\",\n    \"2023-04-20\": \"25800\",\n    \"2023-04-21\": \"24832\",\n    \"2023-04-22\": \"25387\",\n    \"2023-04-23\": \"25163\",\n    \"2023-04-24\": \"24923\",\n    \"2023-04-25\": \"25820\",\n    \"2023-04-26\": \"25771\",\n    \"2023-04-27\": \"26774\",\n    \"2023-04-28\": \"26652\",\n    \"2023-04-29\": \"26549\",\n    \"2023-04-30\": \"26576\",\n    \"2023-05-01\": \"25640\",\n    \"2023-05-02\": \"26056\",\n    \"2023-05-03\": \"26246\",\n    \"2023-05-04\": \"26195\",\n    \"2023-05-05\": \"26824\",\n    \"2023-05-06\": \"26252\",\n    \"2023-05-07\": \"25854\",\n    \"2023-05-08\": \"25220\",\n    \"2023-05-09\": \"25279\",\n    \"2023-05-10\": \"25198\",\n    \"2023-05-11\": \"24804\",\n    \"2023-05-12\": \"24710\",\n    \"2023-05-13\": \"24709\",\n    \"2023-05-14\": \"24832\",\n    \"2023-05-15\": \"25018\",\n    \"2023-05-16\": \"24901\",\n    \"2023-05-17\": \"25301\",\n    \"2023-05-18\": \"24912\",\n    \"2023-05-19\": \"24883\",\n    \"2023-05-20\": \"25123\",\n    \"2023-05-21\": \"24741\",\n    \"2023-05-22\": \"24859\",\n    \"2023-05-23\": \"25298\",\n    \"2023-05-24\": \"24507\",\n    \"2023-05-25\": \"24703\",\n    \"2023-05-26\": \"24917\",\n    \"2023-05-27\": \"25051\",\n    \"2023-05-28\": \"26225\",\n    \"2023-05-29\": \"25957\",\n    \"2023-05-30\": \"25834\",\n    \"2023-05-31\": \"25466\",\n    \"2023-06-01\": \"24920\",\n    \"2023-06-02\": \"25455\",\n    \"2023-06-03\": \"25299\",\n    \"2023-06-04\": \"25367\",\n    \"2023-06-05\": \"24077\",\n    \"2023-06-06\": \"25467\",\n    \"2023-06-07\": \"24660\",\n    \"2023-06-08\": \"24600\",\n    \"2023-06-09\": \"24673\",\n    \"2023-06-10\": \"24151\",\n    \"2023-06-11\": \"24193\",\n    \"2023-06-12\": \"24118\",\n    \"2023-06-13\": \"24081\",\n    \"2023-06-14\": \"23225\",\n    \"2023-06-15\": \"23397\",\n    \"2023-06-16\": \"24110\",\n    \"2023-06-17\": \"24273\",\n    \"2023-06-18\": \"24137\",\n    \"2023-06-19\": \"24587\",\n    \"2023-06-20\": \"25965\",\n    \"2023-06-21\": \"27325\",\n    \"2023-06-22\": \"27313\",\n    \"2023-06-23\": \"28180\",\n    \"2023-06-24\": \"28032\",\n    \"2023-06-25\": \"27958\",\n    \"2023-06-26\": \"27759\",\n    \"2023-06-27\": \"28061\",\n    \"2023-06-28\": \"27605\",\n    \"2023-06-29\": \"28057\",\n    \"2023-06-30\": \"27969\",\n    \"2023-07-01\": \"28077\",\n    \"2023-07-02\": \"28115\",\n    \"2023-07-03\": \"28589\",\n    \"2023-07-04\": \"28331\",\n    \"2023-07-05\": \"28146\",\n    \"2023-07-06\": \"27517\",\n    \"2023-07-07\": \"27763\",\n    \"2023-07-08\": \"27735\",\n    \"2023-07-09\": \"27575\",\n    \"2023-07-10\": \"27681\",\n    \"2023-07-11\": \"27879\",\n    \"2023-07-12\": \"27341\",\n    \"2023-07-13\": \"28113\",\n    \"2023-07-14\": \"27045\",\n    \"2023-07-15\": \"27017\",\n    \"2023-07-16\": \"26955\",\n    \"2023-07-17\": \"26845\",\n    \"2023-07-18\": \"26613\",\n    \"2023-07-19\": \"26744\",\n    \"2023-07-20\": \"26782\",\n    \"2023-07-21\": \"26917\",\n    \"2023-07-22\": \"26782\",\n    \"2023-07-23\": \"27084\",\n    \"2023-07-24\": \"26403\",\n    \"2023-07-25\": \"26495\",\n    \"2023-07-26\": \"26505\",\n    \"2023-07-27\": \"26650\",\n    \"2023-07-28\": \"26662\",\n    \"2023-07-29\": \"26700\",\n    \"2023-07-30\": \"26599\",\n    \"2023-07-31\": \"26579\",\n    \"2023-08-01\": \"27030\",\n    \"2023-08-02\": \"26676\",\n    \"2023-08-03\": \"26704\",\n    \"2023-08-04\": \"26433\",\n    \"2023-08-05\": \"26464\",\n    \"2023-08-06\": \"26489\",\n    \"2023-08-07\": \"26531\",\n    \"2023-08-08\": \"27197\",\n    \"2023-08-09\": \"26969\",\n    \"2023-08-10\": \"26856\",\n    \"2023-08-11\": \"26918\",\n    \"2023-08-12\": \"26945\",\n    \"2023-08-13\": \"26830\",\n    \"2023-08-14\": \"27014\",\n    \"2023-08-15\": \"26787\",\n    \"2023-08-16\": \"26446\",\n    \"2023-08-17\": \"24519\",\n    \"2023-08-18\": \"24001\",\n    \"2023-08-19\": \"24119\",\n    \"2023-08-20\": \"24143\",\n    \"2023-08-21\": \"24013\",\n    \"2023-08-22\": \"24052\",\n    \"2023-08-23\": \"24387\",\n    \"2023-08-24\": \"24295\",\n    \"2023-08-25\": \"24190\",\n    \"2023-08-26\": \"24123\",\n    \"2023-08-27\": \"24195\",\n    \"2023-08-28\": \"24144\",\n    \"2023-08-29\": \"25540\",\n    \"2023-08-30\": \"25035\",\n    \"2023-08-31\": \"23940\",\n    \"2023-09-01\": \"23979\",\n    \"2023-09-02\": \"24024\",\n    \"2023-09-03\": \"24102\",\n    \"2023-09-04\": \"23953\",\n    \"2023-09-05\": \"24048\",\n    \"2023-09-06\": \"24078\",\n    \"2023-09-07\": \"24522\",\n    \"2023-09-08\": \"24222\",\n    \"2023-09-09\": \"24216\",\n    \"2023-09-10\": \"24149\",\n    \"2023-09-11\": \"23440\",\n    \"2023-09-12\": \"24072\",\n    \"2023-09-13\": \"24473\",\n    \"2023-09-14\": \"24988\",\n    \"2023-09-15\": \"25052\",\n    \"2023-09-16\": \"24922\",\n    \"2023-09-17\": \"24894\",\n    \"2023-09-18\": \"25041\",\n    \"2023-09-19\": \"25510\",\n    \"2023-09-20\": \"25508\",\n    \"2023-09-21\": \"24936\",\n    \"2023-09-22\": \"24972\",\n    \"2023-09-23\": \"24956\",\n    \"2023-09-24\": \"24659\",\n    \"2023-09-25\": \"24826\",\n    \"2023-09-26\": \"24797\",\n    \"2023-09-27\": \"25113\",\n    \"2023-09-28\": \"25612\",\n    \"2023-09-29\": \"25475\",\n    \"2023-09-30\": \"25545\",\n    \"2023-10-01\": \"26497\",\n    \"2023-10-02\": \"26289\",\n    \"2023-10-03\": \"26224\",\n    \"2023-10-04\": \"26470\",\n    \"2023-10-05\": \"26016\",\n    \"2023-10-06\": \"26388\",\n    \"2023-10-07\": \"26406\",\n    \"2023-10-08\": \"26440\",\n    \"2023-10-09\": \"26079\",\n    \"2023-10-10\": \"25861\",\n    \"2023-10-11\": \"25305\",\n    \"2023-10-12\": \"25402\",\n    \"2023-10-13\": \"25578\",\n    \"2023-10-14\": \"25587\",\n    \"2023-10-15\": \"25849\",\n    \"2023-10-16\": \"27046\",\n    \"2023-10-17\": \"26918\",\n    \"2023-10-18\": \"26916\",\n    \"2023-10-19\": \"27176\",\n    \"2023-10-20\": \"28076\",\n    \"2023-10-21\": \"28252\",\n    \"2023-10-22\": \"28376\",\n    \"2023-10-23\": \"31057\",\n    \"2023-10-24\": \"32025\",\n    \"2023-10-25\": \"32680\",\n    \"2023-10-26\": \"32380\",\n    \"2023-10-27\": \"32107\",\n    \"2023-10-28\": \"32345\",\n    \"2023-10-29\": \"32687\",\n    \"2023-10-30\": \"32521\",\n    \"2023-10-31\": \"32775\",\n    \"2023-11-01\": \"33496\",\n    \"2023-11-02\": \"32907\",\n    \"2023-11-03\": \"32354\",\n    \"2023-11-04\": \"32771\",\n    \"2023-11-05\": \"32707\",\n    \"2023-11-06\": \"32709\",\n    \"2023-11-07\": \"33197\",\n    \"2023-11-08\": \"33363\",\n    \"2023-11-09\": \"34536\",\n    \"2023-11-10\": \"35024\",\n    \"2023-11-11\": \"34845\",\n    \"2023-11-12\": \"34745\",\n    \"2023-11-13\": \"34231\",\n    \"2023-11-14\": \"32735\",\n    \"2023-11-15\": \"34896\",\n    \"2023-11-16\": \"33414\",\n    \"2023-11-17\": \"33580\",\n    \"2023-11-18\": \"33620\",\n    \"2023-11-19\": \"34402\",\n    \"2023-11-20\": \"34391\",\n    \"2023-11-21\": \"32854\",\n    \"2023-11-22\": \"34465\",\n    \"2023-11-23\": \"34283\",\n    \"2023-11-24\": \"34523\",\n    \"2023-11-25\": \"34591\",\n    \"2023-11-26\": \"34327\",\n    \"2023-11-27\": \"34058\",\n    \"2023-11-28\": \"34349\",\n    \"2023-11-29\": \"34567\",\n    \"2023-11-30\": \"34668\",\n    \"2023-12-01\": \"35603\",\n    \"2023-12-02\": \"36316\",\n    \"2023-12-03\": \"36753\",\n    \"2023-12-04\": \"38747\",\n    \"2023-12-05\": \"40834\",\n    \"2023-12-06\": \"40665\",\n    \"2023-12-07\": \"40156\",\n    \"2023-12-08\": \"41106\",\n    \"2023-12-09\": \"40651\",\n    \"2023-12-10\": \"40729\",\n    \"2023-12-11\": \"38383\",\n    \"2023-12-12\": \"38456\",\n    \"2023-12-13\": \"39428\",\n    \"2023-12-14\": \"39140\",\n    \"2023-12-15\": \"38552\",\n    \"2023-12-16\": \"38779\",\n    \"2023-12-17\": \"38032\",\n    \"2023-12-18\": \"39122\",\n    \"2023-12-19\": \"38499\",\n    \"2023-12-20\": \"39917\",\n    \"2023-12-21\": \"39873\",\n    \"2023-12-22\": \"39998\",\n    \"2023-12-23\": \"39802\",\n    \"2023-12-24\": \"39139\",\n    \"2023-12-25\": \"39621\",\n    \"2023-12-26\": \"38590\",\n    \"2023-12-27\": \"39167\",\n    \"2023-12-28\": \"38501\",\n    \"2023-12-29\": \"38099\",\n    \"2023-12-30\": \"38264\",\n    \"2023-12-31\": \"38320\",\n    \"2024-01-01\": \"39904\",\n    \"2024-01-02\": \"41025\",\n    \"2024-01-03\": \"39163\",\n    \"2024-01-04\": \"40329\",\n    \"2024-01-05\": \"40212\",\n    \"2024-01-06\": \"40109\",\n    \"2024-01-07\": \"40006\",\n    \"2024-01-08\": \"42866\",\n    \"2024-01-09\": \"42152\",\n    \"2024-01-10\": \"42538\",\n    \"2024-01-11\": \"42022\",\n    \"2024-01-12\": \"39159\",\n    \"2024-01-13\": \"39096\",\n    \"2024-01-14\": \"38276\",\n    \"2024-01-15\": \"38900\",\n    \"2024-01-16\": \"39688\",\n    \"2024-01-17\": \"39296\",\n    \"2024-01-18\": \"37908\",\n    \"2024-01-19\": \"38240\",\n    \"2024-01-20\": \"38247\",\n    \"2024-01-21\": \"38177\",\n    \"2024-01-22\": \"36321\",\n    \"2024-01-23\": \"36674\",\n    \"2024-01-24\": \"36905\",\n    \"2024-01-25\": \"36860\",\n    \"2024-01-26\": \"38599\",\n    \"2024-01-27\": \"38834\",\n    \"2024-01-28\": \"38797\",\n    \"2024-01-29\": \"39974\",\n    \"2024-01-30\": \"39599\",\n    \"2024-01-31\": \"39462\",\n    \"2024-02-01\": \"39599\",\n    \"2024-02-02\": \"40032\",\n    \"2024-02-03\": \"39853\",\n    \"2024-02-04\": \"39514\",\n    \"2024-02-05\": \"39738\",\n    \"2024-02-06\": \"40145\",\n    \"2024-02-07\": \"41173\",\n    \"2024-02-08\": \"42114\",\n    \"2024-02-09\": \"43693\",\n    \"2024-02-10\": \"44278\",\n    \"2024-02-11\": \"44675\",\n    \"2024-02-12\": \"46340\",\n    \"2024-02-13\": \"46475\",\n    \"2024-02-14\": \"48299\",\n    \"2024-02-15\": \"48237\",\n    \"2024-02-16\": \"48464\",\n    \"2024-02-17\": \"48024\",\n    \"2024-02-18\": \"48415\",\n    \"2024-02-19\": \"48157\",\n    \"2024-02-20\": \"48453\",\n    \"2024-02-21\": \"48033\",\n    \"2024-02-22\": \"47532\",\n    \"2024-02-23\": \"46986\",\n    \"2024-02-24\": \"47752\",\n    \"2024-02-25\": \"47910\",\n    \"2024-02-26\": \"50273\",\n    \"2024-02-27\": \"52695\",\n    \"2024-02-28\": \"57694\",\n    \"2024-02-29\": \"56655\",\n    \"2024-03-01\": \"57643\",\n    \"2024-03-02\": \"57366\",\n    \"2024-03-03\": \"58389\",\n    \"2024-03-04\": \"63000\",\n    \"2024-03-05\": \"58731\",\n    \"2024-03-06\": \"60694\",\n    \"2024-03-07\": \"61184\",\n    \"2024-03-08\": \"62502\",\n    \"2024-03-09\": \"62709\",\n    \"2024-03-10\": \"63203\",\n    \"2024-03-11\": \"65987\",\n    \"2024-03-12\": \"65415\",\n    \"2024-03-13\": \"66766\",\n    \"2024-03-14\": \"65773\",\n    \"2024-03-15\": \"63921\",\n    \"2024-03-16\": \"60067\",\n    \"2024-03-17\": \"63062\",\n    \"2024-03-18\": \"62307\",\n    \"2024-03-19\": \"57133\",\n    \"2024-03-20\": \"62243\",\n    \"2024-03-21\": \"60369\",\n    \"2024-03-22\": \"59039\",\n    \"2024-03-23\": \"59299\",\n    \"2024-03-24\": \"62240\",\n    \"2024-03-25\": \"64544\",\n    \"2024-03-26\": \"64710\",\n    \"2024-03-27\": \"64250\",\n    \"2024-03-28\": \"65614\",\n    \"2024-03-29\": \"64778\",\n    \"2024-03-30\": \"64623\",\n    \"2024-03-31\": \"66063\",\n    \"2024-04-01\": \"64942\",\n    \"2024-04-02\": \"60828\",\n    \"2024-04-03\": \"61033\",\n    \"2024-04-04\": \"63312\",\n    \"2024-04-05\": \"62629\",\n    \"2024-04-06\": \"63723\",\n    \"2024-04-07\": \"64148\",\n    \"2024-04-08\": \"65980\",\n    \"2024-04-09\": \"63711\",\n    \"2024-04-10\": \"65709\",\n    \"2024-04-11\": \"65245\",\n    \"2024-04-12\": \"63122\",\n    \"2024-04-13\": \"60040\",\n    \"2024-04-14\": \"61948\",\n    \"2024-04-15\": \"59684\",\n    \"2024-04-16\": \"60087\",\n    \"2024-04-17\": \"57472\",\n    \"2024-04-18\": \"59601\",\n    \"2024-04-19\": \"59883\",\n    \"2024-04-20\": \"60854\",\n    \"2024-04-21\": \"60938\",\n    \"2024-04-22\": \"62769\",\n    \"2024-04-23\": \"62074\",\n    \"2024-04-24\": \"60054\",\n    \"2024-04-25\": \"60120\",\n    \"2024-04-26\": \"59657\",\n    \"2024-04-27\": \"59422\",\n    \"2024-04-28\": \"58976\",\n    \"2024-04-29\": \"59531\",\n    \"2024-04-30\": \"56887\",\n    \"2024-05-01\": \"54361\",\n    \"2024-05-02\": \"55093\",\n    \"2024-05-03\": \"58453\",\n    \"2024-05-04\": \"59330\",\n    \"2024-05-05\": \"59510\",\n    \"2024-05-06\": \"58699\",\n    \"2024-05-07\": \"57983\",\n    \"2024-05-08\": \"56872\",\n    \"2024-05-09\": \"58527\",\n    \"2024-05-10\": \"56461\",\n    \"2024-05-11\": \"56477\",\n    \"2024-05-12\": \"57109\",\n    \"2024-05-13\": \"58218\",\n    \"2024-05-14\": \"56946\",\n    \"2024-05-15\": \"60829\",\n    \"2024-05-16\": \"60024\",\n    \"2024-05-17\": \"61683\",\n    \"2024-05-18\": \"61595\",\n    \"2024-05-19\": \"60987\",\n    \"2024-05-20\": \"65575\",\n    \"2024-05-21\": \"64573\",\n    \"2024-05-22\": \"63834\",\n    \"2024-05-23\": \"62816\",\n    \"2024-05-24\": \"63265\",\n    \"2024-05-25\": \"63858\",\n    \"2024-05-26\": \"63161\",\n    \"2024-05-27\": \"63888\",\n    \"2024-05-28\": \"62930\",\n    \"2024-05-29\": \"62578\",\n    \"2024-05-30\": \"63119\",\n    \"2024-05-31\": \"62266\",\n    \"2024-06-01\": \"62433\",\n    \"2024-06-02\": \"62462\",\n    \"2024-06-03\": \"63066\",\n    \"2024-06-04\": \"64807\",\n    \"2024-06-05\": \"65295\",\n    \"2024-06-06\": \"64939\",\n    \"2024-06-07\": \"64215\",\n    \"2024-06-08\": \"64169\",\n    \"2024-06-09\": \"64618\",\n    \"2024-06-10\": \"64472\",\n    \"2024-06-11\": \"62692\",\n    \"2024-06-12\": \"63053\",\n    \"2024-06-13\": \"62112\",\n    \"2024-06-14\": \"61702\",\n    \"2024-06-15\": \"61797\",\n    \"2024-06-16\": \"62218\",\n    \"2024-06-17\": \"61892\",\n    \"2024-06-18\": \"60677\",\n    \"2024-06-19\": \"60529\",\n    \"2024-06-20\": \"60584\",\n    \"2024-06-21\": \"59953\",\n    \"2024-06-22\": \"60061\",\n    \"2024-06-23\": \"59173\",\n    \"2024-06-24\": \"56176\",\n    \"2024-06-25\": \"57708\",\n    \"2024-06-26\": \"56938\",\n    \"2024-06-27\": \"57552\",\n    \"2024-06-28\": \"56357\",\n    \"2024-06-29\": \"56868\",\n    \"2024-06-30\": \"58451\",\n    \"2024-07-01\": \"58614\",\n    \"2024-07-02\": \"57768\",\n    \"2024-07-03\": \"55854\",\n    \"2024-07-04\": \"52893\",\n    \"2024-07-05\": \"52307\",\n    \"2024-07-06\": \"53774\",\n    \"2024-07-07\": \"51620\",\n    \"2024-07-08\": \"52419\",\n    \"2024-07-09\": \"53817\",\n    \"2024-07-10\": \"53543\",\n    \"2024-07-11\": \"52917\",\n    \"2024-07-12\": \"53210\",\n    \"2024-07-13\": \"54529\",\n    \"2024-07-14\": \"56064\",\n    \"2024-07-15\": \"59685\",\n    \"2024-07-16\": \"59811\",\n    \"2024-07-17\": \"58686\",\n    \"2024-07-18\": \"58818\",\n    \"2024-07-19\": \"61323\",\n    \"2024-07-20\": \"61751\",\n    \"2024-07-21\": \"62562\",\n    \"2024-07-22\": \"62104\",\n    \"2024-07-23\": \"60820\",\n    \"2024-07-24\": \"60388\",\n    \"2024-07-25\": \"60642\",\n    \"2024-07-26\": \"62611\",\n    \"2024-07-27\": \"62562\",\n    \"2024-07-28\": \"62927\",\n    \"2024-07-29\": \"61860\",\n    \"2024-07-30\": \"61432\",\n    \"2024-07-31\": \"59923\",\n    \"2024-08-01\": \"60726\",\n    \"2024-08-02\": \"56538\",\n    \"2024-08-03\": \"55914\",\n    \"2024-08-04\": \"53346\",\n    \"2024-08-05\": \"49351\",\n    \"2024-08-06\": \"51392\",\n    \"2024-08-07\": \"50512\",\n    \"2024-08-08\": \"56639\",\n    \"2024-08-09\": \"55908\",\n    \"2024-08-10\": \"55886\",\n    \"2024-08-11\": \"53933\",\n    \"2024-08-12\": \"54367\",\n    \"2024-08-13\": \"55241\",\n    \"2024-08-14\": \"53472\",\n    \"2024-08-15\": \"52537\",\n    \"2024-08-16\": \"53599\",\n    \"2024-08-17\": \"54095\",\n    \"2024-08-18\": \"53253\",\n    \"2024-08-19\": \"53949\",\n    \"2024-08-20\": \"53138\",\n    \"2024-08-21\": \"54936\",\n    \"2024-08-22\": \"54439\",\n    \"2024-08-23\": \"57411\",\n    \"2024-08-24\": \"57400\",\n    \"2024-08-25\": \"57715\",\n    \"2024-08-26\": \"56372\",\n    \"2024-08-27\": \"53325\",\n    \"2024-08-28\": \"53166\",\n    \"2024-08-29\": \"53675\",\n    \"2024-08-30\": \"53601\",\n    \"2024-08-31\": \"53427\",\n    \"2024-09-01\": \"51990\",\n    \"2024-09-02\": \"53538\",\n    \"2024-09-03\": \"52067\",\n    \"2024-09-04\": \"52406\",\n    \"2024-09-05\": \"50666\",\n    \"2024-09-06\": \"48827\",\n    \"2024-09-07\": \"49032\",\n    \"2024-09-08\": \"49683\",\n    \"2024-09-09\": \"51804\",\n    \"2024-09-10\": \"52422\",\n    \"2024-09-11\": \"52245\",\n    \"2024-09-12\": \"52646\",\n    \"2024-09-13\": \"54874\",\n    \"2024-09-14\": \"54435\",\n    \"2024-09-15\": \"53642\",\n    \"2024-09-16\": \"52513\",\n    \"2024-09-17\": \"54298\",\n    \"2024-09-18\": \"55648\",\n    \"2024-09-19\": \"56482\",\n    \"2024-09-20\": \"56583\",\n    \"2024-09-21\": \"56843\",\n    \"2024-09-22\": \"56970\",\n    \"2024-09-23\": \"56998\",\n    \"2024-09-24\": \"57447\",\n    \"2024-09-25\": \"56733\",\n    \"2024-09-26\": \"58156\",\n    \"2024-09-27\": \"58895\",\n    \"2024-09-28\": \"58784\",\n    \"2024-09-29\": \"58717\",\n    \"2024-09-30\": \"56757\",\n    \"2024-10-01\": \"55021\",\n    \"2024-10-02\": \"54932\",\n    \"2024-10-03\": \"55048\",\n    \"2024-10-04\": \"56496\",\n    \"2024-10-05\": \"56500\",\n    \"2024-10-06\": \"57277\",\n    \"2024-10-07\": \"56683\",\n    \"2024-10-08\": \"56598\",\n    \"2024-10-09\": \"55299\",\n    \"2024-10-10\": \"54957\",\n    \"2024-10-11\": \"56941\",\n    \"2024-10-12\": \"57365\",\n    \"2024-10-13\": \"57343\",\n    \"2024-10-14\": \"60402\",\n    \"2024-10-15\": \"61407\",\n    \"2024-10-16\": \"62054\",\n    \"2024-10-17\": \"62061\",\n    \"2024-10-18\": \"62727\",\n    \"2024-10-19\": \"62783\",\n    \"2024-10-20\": \"63292\",\n    \"2024-10-21\": \"62245\",\n    \"2024-10-22\": \"62400\",\n    \"2024-10-23\": \"61841\",\n    \"2024-10-24\": \"62988\",\n    \"2024-10-25\": \"61834\",\n    \"2024-10-26\": \"62024\",\n    \"2024-10-27\": \"62505\",\n    \"2024-10-28\": \"63912\",\n    \"2024-10-29\": \"66653\",\n    \"2024-10-30\": \"66358\",\n    \"2024-10-31\": \"64352\",\n    \"2024-11-01\": \"63666\",\n    \"2024-11-02\": \"63565\",\n    \"2024-11-03\": \"62758\",\n    \"2024-11-04\": \"61919\",\n    \"2024-11-05\": \"63239\",\n    \"2024-11-06\": \"70336\",\n    \"2024-11-07\": \"69913\",\n    \"2024-11-08\": \"71093\",\n    \"2024-11-09\": \"71267\",\n    \"2024-11-10\": \"74688\",\n    \"2024-11-11\": \"82909\",\n    \"2024-11-12\": \"82573\",\n    \"2024-11-13\": \"85455\",\n    \"2024-11-14\": \"82576\",\n    \"2024-11-15\": \"86059\",\n    \"2024-11-16\": \"85746\",\n    \"2024-11-17\": \"85069\",\n    \"2024-11-18\": \"85370\",\n    \"2024-11-19\": \"86697\",\n    \"2024-11-20\": \"89126\",\n    \"2024-11-21\": \"93717\",\n    \"2024-11-22\": \"94800\",\n    \"2024-11-23\": \"93224\",\n    \"2024-11-24\": \"93481\",\n    \"2024-11-25\": \"88863\",\n    \"2024-11-26\": \"87615\",\n    \"2024-11-27\": \"90774\",\n    \"2024-11-28\": \"90658\",\n    \"2024-11-29\": \"92089\",\n    \"2024-11-30\": \"91109\",\n    \"2024-12-01\": \"92160\",\n    \"2024-12-02\": \"91317\",\n    \"2024-12-03\": \"91403\",\n    \"2024-12-04\": \"93973\",\n    \"2024-12-05\": \"91646\",\n    \"2024-12-06\": \"94416\",\n    \"2024-12-07\": \"94290\",\n    \"2024-12-08\": \"95879\",\n    \"2024-12-09\": \"92056\",\n    \"2024-12-10\": \"91397\",\n    \"2024-12-11\": \"96225\",\n    \"2024-12-12\": \"95444\",\n    \"2024-12-13\": \"95972\",\n    \"2024-12-14\": \"96614\",\n    \"2024-12-15\": \"99046\",\n    \"2024-12-16\": \"100450\",\n    \"2024-12-17\": \"101010\",\n    \"2024-12-18\": \"96679\",\n    \"2024-12-19\": \"94033\",\n    \"2024-12-20\": \"93825\",\n    \"2024-12-21\": \"93020\",\n    \"2024-12-22\": \"91243\",\n    \"2024-12-23\": \"91086\",\n    \"2024-12-24\": \"94770\",\n    \"2024-12-25\": \"95519\",\n    \"2024-12-26\": \"91769\",\n    \"2024-12-27\": \"90475\",\n    \"2024-12-28\": \"91150\",\n    \"2024-12-29\": \"89777\",\n    \"2024-12-30\": \"89076\",\n    \"2024-12-31\": \"90102\",\n    \"2025-01-01\": \"91271\",\n    \"2025-01-02\": \"94302\",\n    \"2025-01-03\": \"95250\",\n    \"2025-01-04\": \"95279\",\n    \"2025-01-05\": \"95444\",\n    \"2025-01-06\": \"98463\",\n    \"2025-01-07\": \"93817\",\n    \"2025-01-08\": \"92319\",\n    \"2025-01-09\": \"89925\",\n    \"2025-01-10\": \"92552\",\n    \"2025-01-11\": \"92303\",\n    \"2025-01-12\": \"92294\",\n    \"2025-01-13\": \"92171\",\n    \"2025-01-14\": \"93806\",\n    \"2025-01-15\": \"97666\",\n    \"2025-01-16\": \"97231\",\n    \"2025-01-17\": \"101470\",\n    \"2025-01-18\": \"101830\",\n    \"2025-01-19\": \"98463\",\n    \"2025-01-20\": \"98196\",\n    \"2025-01-21\": \"102130\",\n    \"2025-01-22\": \"99902\",\n    \"2025-01-23\": \"99989\",\n    \"2025-01-24\": \"99894\",\n    \"2025-01-25\": \"99940\",\n    \"2025-01-26\": \"98222\",\n    \"2025-01-27\": \"97906\",\n    \"2025-01-28\": \"97285\",\n    \"2025-01-29\": \"99670\",\n    \"2025-01-30\": \"100890\",\n    \"2025-01-31\": \"98820\",\n    \"2025-02-01\": \"97477\",\n    \"2025-02-02\": \"95444\",\n    \"2025-02-03\": \"98584\",\n    \"2025-02-04\": \"94544\",\n    \"2025-02-05\": \"92990\",\n    \"2025-02-06\": \"93146\",\n    \"2025-02-07\": \"93525\",\n    \"2025-02-08\": \"93780\",\n    \"2025-02-09\": \"93726\",\n    \"2025-02-10\": \"94539\",\n    \"2025-02-11\": \"92643\",\n    \"2025-02-12\": \"94033\",\n    \"2025-02-13\": \"92183\",\n    \"2025-02-14\": \"92994\",\n    \"2025-02-15\": \"93000\",\n    \"2025-02-16\": \"91623\",\n    \"2025-02-17\": \"91397\",\n    \"2025-02-18\": \"91409\",\n    \"2025-02-19\": \"92830\",\n    \"2025-02-20\": \"93906\",\n    \"2025-02-21\": \"91894\",\n    \"2025-02-22\": \"92406\",\n    \"2025-02-23\": \"92000\",\n    \"2025-02-24\": \"87573\",\n    \"2025-02-25\": \"84267\",\n    \"2025-02-26\": \"80258\",\n    \"2025-02-27\": \"81456\",\n    \"2025-02-28\": \"81416\",\n    \"2025-03-01\": \"83021\",\n    \"2025-03-02\": \"90658\",\n    \"2025-03-03\": \"82127\",\n    \"2025-03-04\": \"82238\",\n    \"2025-03-05\": \"83925\",\n    \"2025-03-06\": \"83472\",\n    \"2025-03-07\": \"80041\",\n    \"2025-03-08\": \"79499\",\n    \"2025-03-09\": \"74240\",\n    \"2025-03-10\": \"72411\",\n    \"2025-03-11\": \"75925\",\n    \"2025-03-12\": \"76990\",\n    \"2025-03-13\": \"74600\",\n    \"2025-03-14\": \"77198\",\n    \"2025-03-15\": \"77462\",\n    \"2025-03-16\": \"75912\",\n    \"2025-03-17\": \"76976\",\n    \"2025-03-18\": \"75617\",\n    \"2025-03-19\": \"79575\",\n    \"2025-03-20\": \"77381\",\n    \"2025-03-21\": \"77589\",\n    \"2025-03-22\": \"77325\",\n    \"2025-03-23\": \"79400\",\n    \"2025-03-24\": \"80554\",\n    \"2025-03-25\": \"80750\",\n    \"2025-03-26\": \"80348\",\n    \"2025-03-27\": \"80718\",\n    \"2025-03-28\": \"77862\",\n    \"2025-03-29\": \"76377\",\n    \"2025-03-30\": \"76086\",\n    \"2025-03-31\": \"76131\",\n    \"2025-04-01\": \"78883\",\n    \"2025-04-02\": \"75651\",\n    \"2025-04-03\": \"75266\",\n    \"2025-04-04\": \"76005\",\n    \"2025-04-05\": \"76131\",\n    \"2025-04-06\": \"71244\",\n    \"2025-04-07\": \"72313\",\n    \"2025-04-08\": \"69435\",\n    \"2025-04-09\": \"75311\",\n    \"2025-04-10\": \"70754\",\n    \"2025-04-11\": \"73498\",\n    \"2025-04-12\": \"75107\",\n    \"2025-04-13\": \"73826\",\n    \"2025-04-14\": \"74304\",\n    \"2025-04-15\": \"74009\",\n    \"2025-04-16\": \"73570\",\n    \"2025-04-17\": \"74631\",\n    \"2025-04-18\": \"74031\",\n    \"2025-04-19\": \"74702\",\n    \"2025-04-20\": \"74181\",\n    \"2025-04-21\": \"75925\",\n    \"2025-04-22\": \"82294\",\n    \"2025-04-23\": \"82254\",\n    \"2025-04-24\": \"82488\",\n    \"2025-04-25\": \"83111\",\n    \"2025-04-26\": \"83432\",\n    \"2025-04-27\": \"82605\",\n    \"2025-04-28\": \"83172\",\n    \"2025-04-29\": \"82508\",\n    \"2025-04-30\": \"83074\",\n    \"2025-05-01\": \"85153\",\n    \"2025-05-02\": \"85640\",\n    \"2025-05-03\": \"84940\",\n    \"2025-05-04\": \"83133\",\n    \"2025-05-05\": \"83807\",\n    \"2025-05-06\": \"85412\",\n    \"2025-05-07\": \"85783\",\n    \"2025-05-08\": \"91722\",\n    \"2025-05-09\": \"91357\",\n    \"2025-05-10\": \"92992\",\n    \"2025-05-11\": \"92700\",\n    \"2025-05-12\": \"92853\",\n    \"2025-05-13\": \"93171\",\n    \"2025-05-14\": \"92394\",\n    \"2025-05-15\": \"92643\",\n    \"2025-05-16\": \"92644\",\n    \"2025-05-17\": \"92392\",\n    \"2025-05-18\": \"95196\",\n    \"2025-05-19\": \"94088\",\n    \"2025-05-20\": \"94544\",\n    \"2025-05-21\": \"96544\",\n    \"2025-05-22\": \"98900\",\n    \"2025-05-23\": \"94161\",\n    \"2025-05-24\": \"94700\",\n    \"2025-05-25\": \"95832\",\n    \"2025-05-26\": \"96092\",\n    \"2025-05-27\": \"96300\",\n    \"2025-05-28\": \"95923\",\n    \"2025-05-29\": \"92894\",\n    \"2025-05-30\": \"91740\",\n    \"2025-05-31\": \"92300\",\n    \"2025-06-01\": \"93242\",\n    \"2025-06-02\": \"92500\",\n    \"2025-06-03\": \"92598\",\n    \"2025-06-04\": \"91645\",\n    \"2025-06-05\": \"88818\",\n    \"2025-06-06\": \"91648\",\n    \"2025-06-07\": \"92727\",\n    \"2025-06-08\": \"92643\",\n    \"2025-06-09\": \"96485\",\n    \"2025-06-10\": \"96353\",\n    \"2025-06-11\": \"94479\",\n    \"2025-06-12\": \"91200\",\n    \"2025-06-13\": \"91894\",\n    \"2025-06-14\": \"91333\",\n    \"2025-06-15\": \"91535\",\n    \"2025-06-16\": \"92483\",\n    \"2025-06-17\": \"91118\",\n    \"2025-06-18\": \"91274\",\n    \"2025-06-19\": \"91069\",\n    \"2025-06-20\": \"89588\",\n    \"2025-06-21\": \"88715\",\n    \"2025-06-22\": \"87755\",\n    \"2025-06-23\": \"91086\",\n    \"2025-06-24\": \"91308\",\n    \"2025-06-25\": \"91854\",\n    \"2025-06-26\": \"91739\",\n    \"2025-06-27\": \"91522\",\n    \"2025-06-28\": \"91822\",\n    \"2025-06-29\": \"92563\",\n    \"2025-06-30\": \"91065\",\n    \"2025-07-01\": \"89652\",\n    \"2025-07-02\": \"92518\",\n    \"2025-07-03\": \"93396\",\n    \"2025-07-04\": \"92043\",\n    \"2025-07-05\": \"92026\",\n    \"2025-07-06\": \"92971\",\n    \"2025-07-07\": \"92206\",\n    \"2025-07-08\": \"92968\",\n    \"2025-07-09\": \"94832\",\n    \"2025-07-10\": \"98946\",\n    \"2025-07-11\": \"100270\",\n    \"2025-07-12\": \"99905\",\n    \"2025-07-13\": \"101740\",\n    \"2025-07-14\": \"102630\",\n    \"2025-07-15\": \"101300\",\n    \"2025-07-16\": \"101930\",\n    \"2025-07-17\": \"102680\",\n    \"2025-07-18\": \"101440\",\n    \"2025-07-19\": \"101310\",\n    \"2025-07-20\": \"100900\",\n    \"2025-07-21\": \"100350\",\n    \"2025-07-22\": \"102200\",\n    \"2025-07-23\": \"100750\",\n    \"2025-07-24\": \"100690\",\n    \"2025-07-25\": \"100220\",\n    \"2025-07-26\": \"100530\",\n    \"2025-07-27\": \"101570\",\n    \"2025-07-28\": \"101700\",\n    \"2025-07-29\": \"102130\",\n    \"2025-07-30\": \"102940\",\n    \"2025-07-31\": \"101200\",\n    \"2025-08-01\": \"97769\",\n    \"2025-08-02\": \"97229\",\n    \"2025-08-03\": \"98703\",\n    \"2025-08-04\": \"99400\",\n    \"2025-08-05\": \"98596\",\n    \"2025-08-06\": \"98659\",\n    \"2025-08-07\": \"100760\",\n    \"2025-08-08\": \"100360\",\n    \"2025-08-09\": \"99728\",\n    \"2025-08-10\": \"102310\",\n    \"2025-08-11\": \"101800\",\n    \"2025-08-12\": \"102830\",\n    \"2025-08-13\": \"104980\",\n    \"2025-08-14\": \"101470\"\n}"
  },
  {
    "path": "tests/plugins/__init__.py",
    "content": ""
  },
  {
    "path": "tests/plugins/test_revealer.py",
    "content": "from electrum.plugins.revealer.revealer import RevealerPlugin\n\nfrom .. import ElectrumTestCase\n\n\nclass TestRevealer(ElectrumTestCase):\n\n    def test_version_0_noisemap(self):\n        versioned_seed = RevealerPlugin.get_versioned_seed_from_user_input('03b0c557d6d0d4308a3393851d78bd8c7861')\n        noise_map = RevealerPlugin.get_noise_map(versioned_seed)\n        bigint = 0\n        for (x, y), pixel in noise_map.items():\n            if pixel:\n                bigint |= 1 << (y*RevealerPlugin.SIZE[1]+x)\n        self.assertEqual(0x541dde00b20ac7d320510e943d7ed9ffff5ff6b431c915353fbeffbc1beb737ff3a59c032a39ff8cbd532dffe42655bccbbef4f777ffeff8ec90e64aacbff5f4ff37ef4f32ac1d7240ed2bbb37dfeff459c7c2e2e0bfddffff7fffc7fd27eeb84a5ceafcf6bf9ffaff632367f97fffbf9fbfecff2b3a11a1c5befdfbfe7f125fba2c3e5d4ded591f9fbbbadbeed2220fb4337df9e4c7bfbe6ce4ad7b18ad57f3d75dfe7b6deb7350478bdbf7b7bfdf776eb301217d1f5c7f7ffffeefffe2070f52dbedfff2fef7f27f7d27f80b6a7bfb7f67bcbf7faf11f6b577dfbefebd44ffffe7bf5ee17ba4fb3377e1fcffeded781eca37a5bff3ebefdccbe1538c16129aed7fadfd7eb3bab55bcbdaee7e5d5b9fff57bd662333923b27af4f4da5ffd8bb15b58effed8bbeeff9ab7ecb75b62b977fdd88f3fbaeef6997a999b4dfffbfa375bf9e9c12b6011e2fde9fef7f66efc1155cc4fedfeefffeeff6ded645712b12bfe2b35df796f7ca05e0f12afbff6fefd1dd7f736bb9a567dff5797eafc1bfa0cf6cd090ddddfbfb79fd9f7f17bba2197e5dd3fb7fd9ff7579f0b6e28f7df3bdfe6fa8efd5a0a2e48f4d6efff79bf5efebc2638ff7eefffbbdfdb5bfac80426052df6fe6fd33eff5336a3c87c9fcff797b6bddbf91fea62e635333ffd7bfdd35f5c365432f5dfe7fd8bfb6c6e7cc90e6b5796d1dfeef567fdf390124a4bfeefd7efd1eee7f88ca45658fbf5cabffbfebf9fefe2c9f73dbffd36d7df77d73665c5f1dfa7b5b7fffafb6ea18bf9396e37b77fffffffb6aefdb0635ff7e43cfeef77fd8a527741dd3fffef1eddedf7f4259cc4253ffd9dffffb3bfbb0632d3d7fbf7bfbbfedfff2a7589be7624faffffbdbd7dfb5b5189b66fde8abf8bfbba7f440b80c2ba86de5fdfdf7ffba25625877fb9fdbf6f39333fe20e7710cdffadef8e7fb727d059237ef3dfceb9bd7ffef7b041565f2bb7ffdfefffff1ba3c7abb9a0fcf3fdf78fee7efb5da83dd1ffadebde675ff36d725426027dfffd9f76df3f7605b7f6fa4f75ff6e3df57ffd1a56c6239feffebffeecffdbd1ecc69f99ffea7f5ec759e2b2a99977b467edbffafae5faef9e719b7bff73fe9fed753ddc20ba23d8e7fdf4adfdbefadbb6a6775f7ef7eddffdfffeead7be0b38dcefeeb6afffef3d272d1b0492e733fff15dc3bfa2bfb83b9fefbfdf853fbddfbdbdf354868fd6dedf93edffca29130013bfcfbe27f4feffc86bbaa925bffdeed3fede76b321dc0abb57fe367df5adaaf30cc615c1efef7fbffe3993e583ff3721bdfbe66edef8faef24697f311ff6ff57ffefbebff9b90325bda76f77daeabfbcb9abd45c0bffe576bc3fffffc96911d477ddbbc3feef7f63a4510ec1265e6e1fe765eaafbca10400876bff7bffdfdffcc9920f60119beedfffd57e6ff383e6c3637def9fdffb7bfffb3339f94eb3fdf5bd7bbdfdf621d8f008dad195dffd6ffbff57a1ce166e5ef9f85febdde28a4a013987bf7ffebffffb56cf7aa522589bdafdf51ff4f39ed386097667fafdbfffff7ef9379dbc136bedfb9ff7aefffc3f081be97bf4e7ecfbde35cea3018d1bf1bffbfaeebefb9fac072f05bac77f7fdffeffe2eb1bd4d90a6fddafd7c2ff7bf9ba80d6f6df77ce727ff9a97fb41f03dcfbc557b3fbcc80b,\n                         bigint)\n\n    def test_version_1_noisemap(self):\n        versioned_seed = RevealerPlugin.get_versioned_seed_from_user_input('125Df05b7ccf079ce2978Ae18e99219868cd')\n        noise_map = RevealerPlugin.get_noise_map(versioned_seed)\n        bigint = 0\n        for (x, y), pixel in noise_map.items():\n            if pixel:\n                bigint |= 1 << (y*RevealerPlugin.SIZE[1]+x)\n        self.assertEqual(0x36fde1eece10b3f674227ea76f7ababbcbf87dfba1eddf2edebfeefec3dffff719ee1cbd477b9be7cf6fcdaff924ff05a26ff2fb7bfdbbdef1a2f90c097d7cdadfbb9d1ef592c27c85efffffff7ffc8dff60d6de87f71c9fe77f7f5372cfdb1dc0eb9e959dbed42197c7ee4288f7fbf73b65fdfbd5e153ede49bb957edaefe6f7dee4a72502eef77babfa7fff7d0a3fc6f5ffefb3b7b67aab66118a56eb1fffe1ddafbeefefa96b26d715bb8e5fafbbb2ffcf64e8df2bdffeffed39ffdfdef986491a7fbf97bffb7fafee072640b7af6edf8da2f7cdff268ccd52b75f53f9afef77b4be4db9c5f9debffeff5f7f1f7b1882cb4eed67f757b37ebf0b2c7f849bd73f4737f3ffb5a3f75ac537ff5fff8edcdfeb6be63d3147ffeefa9caf7ebf740989520c1eddedabdfd73f7f821fc3977fdfbe9fbee7d6e8ca9f16b8b8f1decbffef17f806ade988d77fef5775cff3f7bd9759675f4773fff6fefaff385fe807fdbfcbbffefa6d7c4ed54a0d1959cefeecffdffe8cd539451dfdfbff71ff7e97cf37aec8069efffcf7536ebbc515e991cf293ff97feffd72cebe110d44bf787f1efecda7306ac88cd49dfff257cbd9ff7ff8fd1686eedf2dfeb373fbe2a10ed81f7d9c979316efceeea745926377ffcffba7edf67fc79cace0eeef5ff5ceffeeeff94d20c4dadd53efee6f97bfed2f8ae059ff7ffbfedfbf17f2d45bc8afdebf1dff7856ebd02c39ae22ef7befc9e97cbb7f31af5bd57e3feffafc4d7fef9ae222e9fdcf5a2dfbffefd50399d7d095d22fd7bf9fdef6d5d5044bafffdfd57fff7cbe6af91096b3f5fffe3fff7fb97fa930d316db6dfbf236ddbc8abb3bbea6edf9deafd39b8efac7ae014e7ffbdd97cfebe7ec84a72a7b323fe77afffd7f1c8eaec48e6ff3fdf7da9fffaf2d57e961d7f5debc9afe7ff32d72ff374d3ff57fff2fbffdbe9833405fcbabff8beb8ff1e55f53b2d6e96bdf7e7fbfd0fb6b130071c13cf5de5be5ef8ade46a2dd53d77ff69eff7ff946ad4e32febddff87b73fceeaf2ce94adafbb9fddeff8fbf11f4161ad6cb29dbe5f2ffe6ee2023ceeb79c76d7fff7bbffaf4485b6f6f3b7f97f9f75ce372c173177fedd65e5fb76fdc5bbf7a737f9bdddefefff1f7a5533dd1efde7ffafabcd96e1193e3cadb93e76fcdfdb4fa533bc3d3a7eff5eebc9f3ffce91aa51bbf5e7f6bcfd7dfbfd1928c0726f3f26ef8f3ffe2eb8843cbb1dfd2eabfe4f7ffec47a95263e5c65737affe73e3e3735d61e8cbffdbf75e37fc04a991ad7ff7feffa6fafdfed988b50fdf379ffdfff2f7eeb6738c0ceffff77f32fe7f2b22c866514db75f7c3df6e5fd210a70bb4bbf31bcfb6d325f4a00b06ed7d34c9dbbdffff3fd6fd8ed570d7def1dcf789ff5ae040339ff35deb5e37bedf889d83bcf5feffb77e57ffcfdd8edfd91bb8bffd3b7dbd8fea3083734c3d7dedd9ffefcb78c5d87e1919f5655bf5bf1bfb3dd65fb64ee2fffd777afef18965d03872d73bbfb6fa7df7250f3e8d6ef7ff9ffffeadff5e39abf8727fde93febddfffee3096ca1779ceabbf7ff7bda2f756353be9dfabf2bcff6feec1cad233fffbf9ecefffffa21b7b8b17ffded7ff7fef56ee44b02d9bdf3d3cf42aa777fd90ba9b08af4cfd5b797dadb3694bf3282abcb39fb2d760f9,\n                         bigint)\n\n    def test_version_1_noisemap_indexerror(self):\n        versioned_seed = RevealerPlugin.get_versioned_seed_from_user_input('1A082CBDC627FFA37ABD154A64AD2565D725')\n        noise_map = RevealerPlugin.get_noise_map(versioned_seed)\n        bigint = 0\n        for (x, y), pixel in noise_map.items():\n            if pixel:\n                bigint |= 1 << (y*RevealerPlugin.SIZE[1]+x)\n        self.assertEqual(0x20bdba94d80b604107d92e4bb77fbdff66cbd769d14d31cc26fdbb77fffff237db49c1cb6bf5cfd4fbb7e169dfc0213b57ffbf7e7fb3dffbdce1f92bbb595efffbefeeb06e00cd6fbfcf5572e6d4f376843fed920475dfdad7dbfffedf1f5fb9de7f67ff77aefefb753daebce8eeb77ff7e35eaeefaf48fe37c7fd71ecfa96faf6d49bd1b60e29cbff7ff2f5fe7daa83d231efdfdfc2ffd17887d9aa79b1713f16dfcf75f7fa1e64c574b7abbea7abfff9c0e27b9f55cbdedffbff3eff95508796eb8bbf9dfffc367f9b6b1b59337ffebf3ff9fecdffd61a6febbc3aafff379dbafcfc814c56bfaffdd1fb7ffeb35a1c293e9f393e35b5d70fbeca50020427db9ecabaf3fff9a2211ee279fdffb53973fbbefa0d2d9270fe2fef5ffbaffefd753cc251ebff7ff352e3f77fdd50bd3975ee3ffcdff6f5fe199284436d7adfbffbbfaf9efabde086fe79bfdf21fb7efba8079b7eae57ffbabff97b3e7463300e53ff5fd5f295ffe7340062f84f689dadfd3ff13feaef33d83ef7fa9aea65fff1f53021f5baef9ffddfbfbf5dff0ad38328f03ebfadfcffdffdc45e2a9a1ef64fbe4f1b7f7fb567039baafffdffed87f7bb8784f28e7dffd7d7fbfff2e7c0d13cf0d7ff77baffefbf73f6fe851607ef9fb5bf7fd5e8fe0d07d6e9bf7de7effd3b7cbd8e55336e7fef8fcbc67fb9a74ba6481d7dfdc6f3fedd3ffe87e7184216dcffd7dfef7b7a6024b2c4d5ffc72d99d7cd5ff6da6717ecf2ffffceffb6e686a4fb4d57fbcebf7ef9b3ae4c5a553340bafff7f7377bffbe3dae1fb43b738ac7ebd5dfed0c8537fe4b0ddc277b7ffbeff63c618bebcdffe9bdff6f9e52be359a5feac691ffbcdf6abb62102b09fa66f9ffafd4b7f73d88a81e3ff2be4b7f7feba2a2566203f7af53ef7253ebf64ac799ff9e7dcff7fadadbffd23fc9b2fd9efecaeaffcff04efa1e0866fe1f9379ffaed4b9792af1b63fffceed58fcff21bac450bb7ffd9aebeffef5d6525510a55b5ffffef78f7bfa1c94ff7f3d7fcbe3df7d3fbc5a0f710157befdeff762edf2bcfed7fb7ef96ff17dd6bddb9dcf01126dfee1f5b33bf1ef3c4c21c13ffbfbfd985fbfd3804539d95ba5def9dfcdceb7b17c6bf29df6dbefbe7fb9fd9f4fd39a8fb4727b87ee579d88721c9bb3f7ffffb7ffd9df369daca81d57d5ffd0bfdeffaf1434ffcb5b6af7f7eddebdbe403b7d4abfec6f719f6b9fee205da43d79fdef7c7dbfdef2f0f61b98fb39efb43cf7eeee082d36101abb73d9effbbf77dc474f6f47fbbebb1f36b76df0dffa846efedbb7affdfbf77222153dff7fdd7dffff5e81f3b5a8ee07ff3bdfe9b77ef1fecabd51c4ef272feffebeefea7334e38d7fc5effce7ffaccffdd18506bee13bffddefbd4f092fbf6e57dddfbfd7fefdf78303790e7b1befc77f6bbcbf9348c44b37fbee7feebffb57217373b0febef97effeb29ff79daad6df17f7fedfde7eeff727c6b54217fcfbb56ffeeffbc492064ad04cff6ff9b7c2efe308364110873875bff4bb73f5c2666500afbdbbc75ff9bebc5be4465eafbbffb7ff7b7fdc8bb7dee0747df7fcfd76a4bfe5e698aa3f37fe2bffbffb65b780150337efb7a21fdfb747d14fd95ceefd7f7cfe0f6db3c0ca5eb52fb7fe82ff7f7acdea3f9ddabf48ceec4ce41bb13,\n                         bigint)\n"
  },
  {
    "path": "tests/plugins/test_timelock_recovery/default_wallet",
    "content": "{\n    \"active_forwardings\": {},\n    \"addr_history\": {\n        \"tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq\": [\n            [\n                \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\",\n                1455208\n            ],\n            [\n                \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\",\n                1568264\n            ]\n        ],\n        \"tb1q02g5nde0heaed0y24rztkedh9nvswknw50h7fx\": [],\n        \"tb1q069xqa4lej2tljmd8fcvvfedav54nmspvjnfs2\": [],\n        \"tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5\": [\n            [\n                \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\",\n                1346957\n            ],\n            [\n                \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\",\n                1346978\n            ]\n        ],\n        \"tb1q0j2gt4ap2s08cz5vzm5jg87fdeps7x8v4djgrm\": [],\n        \"tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4\": [\n            [\n                \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\",\n                1414311\n            ],\n            [\n                \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\",\n                1414562\n            ]\n        ],\n        \"tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl\": [\n            [\n                \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\",\n                1454548\n            ],\n            [\n                \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\",\n                1455208\n            ]\n        ],\n        \"tb1q3s4hkssd34tyxdlhafthv4muckjtgwuhltu37t\": [],\n        \"tb1q3t0xcpmzreece8xdxq8k5aaxrt3r623tqldp8n\": [],\n        \"tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg\": [],\n        \"tb1q7mhmnxal53vtc5flh69nph44vah5j56eyesjx9\": [],\n        \"tb1q8evsj0vkzfak2y5qnqx4yf9lty462l7yfhegyd\": [],\n        \"tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5\": [\n            [\n                \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                2579583\n            ],\n            [\n                \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\",\n                2579584\n            ]\n        ],\n        \"tb1q99hkhlswfnj8r5wy2xu9a9m0vy8mvffwzhrx6n\": [],\n        \"tb1q9u3ufsm9ksql8utguap40ch8zpnw83fgzf4z97\": [],\n        \"tb1qad28sgvvrxjnxdnfjxcuepgzzhzlapgxcwuj0k\": [],\n        \"tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6\": [\n            [\n                \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\",\n                2579584\n            ]\n        ],\n        \"tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9\": [\n            [\n                \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\",\n                2415285\n            ],\n            [\n                \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\",\n                2415285\n            ]\n        ],\n        \"tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07\": [],\n        \"tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r\": [\n            [\n                \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n                2528392\n            ]\n        ],\n        \"tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3\": [\n            [\n                \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\",\n                1457134\n            ],\n            [\n                \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\",\n                1568264\n            ]\n        ],\n        \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\": [\n            [\n                \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\",\n                2349374\n            ],\n            [\n                \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\",\n                2349377\n            ],\n            [\n                \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\",\n                2406297\n            ]\n        ],\n        \"tb1qdgscjnjm6w59chq0xgghwtq42vfhhn0murqx6hrfz2yaf2yx9v9skh03as\": [\n            [\n                \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\",\n                2406297\n            ],\n            [\n                \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\",\n                2415285\n            ]\n        ],\n        \"tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l\": [\n            [\n                \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\",\n                1414311\n            ],\n            [\n                \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\",\n                1414311\n            ]\n        ],\n        \"tb1qf03zdjdnzxwztxs9d3g9ynsvvs5rmjhvtmln35\": [],\n        \"tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp\": [\n            [\n                \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n                2425096\n            ],\n            [\n                \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n                2528392\n            ]\n        ],\n        \"tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8\": [\n            [\n                \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\",\n                2404107\n            ],\n            [\n                \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\",\n                2406297\n            ]\n        ],\n        \"tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z\": [\n            [\n                \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\",\n                1583044\n            ],\n            [\n                \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\",\n                1583044\n            ]\n        ],\n        \"tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e\": [\n            [\n                \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\",\n                2415285\n            ],\n            [\n                \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\",\n                2418761\n            ]\n        ],\n        \"tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5\": [],\n        \"tb1qh0csljush2tad6t0s4qgx4r5t0rzcw9729l7kx\": [],\n        \"tb1qh878je0rfut79fkudf4mkl8m4cn8uzfsluersy\": [],\n        \"tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu\": [\n            [\n                \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\",\n                1346978\n            ],\n            [\n                \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\",\n                1414310\n            ]\n        ],\n        \"tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg\": [\n            [\n                \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\",\n                2418761\n            ],\n            [\n                \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n                2425096\n            ]\n        ],\n        \"tb1qjv7a78ea0jp9d793d5ra7mtzkjzezwldz5zvr6\": [],\n        \"tb1qkneqe450eqxtpr3r5z8aw4234sjmpknm0gxsae\": [],\n        \"tb1qkp86pkt75ds257snenp3q7vs29pf4g6cmhy2hw\": [],\n        \"tb1qlclgzsp2tktdl66xuk3je7ztztstxvjatly8wy\": [],\n        \"tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd\": [\n            [\n                \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\",\n                1455781\n            ],\n            [\n                \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\",\n                1568264\n            ]\n        ],\n        \"tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c\": [\n            [\n                \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\",\n                1455208\n            ],\n            [\n                \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\",\n                1457134\n            ]\n        ],\n        \"tb1qndaru6pfal030ev296uxwuulxrezaj0j70ceje\": [],\n        \"tb1qplsf242vay6vavy4eguef855fx3klmp9505g88\": [\n            [\n                \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\",\n                2579582\n            ],\n            [\n                \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                2579583\n            ]\n        ],\n        \"tb1qq0gdz0vz02ypa3cawlljstrx8cxydhvalcv8wc\": [],\n        \"tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu\": [\n            [\n                \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\",\n                1346870\n            ],\n            [\n                \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\",\n                1346957\n            ]\n        ],\n        \"tb1qr7mjlxgc6at67tx0s8ypa5efx8clc47xh6yjqg\": [],\n        \"tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3\": [\n            [\n                \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\",\n                2579584\n            ]\n        ],\n        \"tb1qs087qkcawefutkkv8pg037t6txldk5szfntamj\": [],\n        \"tb1qs3j3j05rjefnjqf0mlpztszg8acz868wnxgz6j\": [],\n        \"tb1qsd2c4xwg47hnngn2uqg66y5rxz2hp074u9rq6r\": [],\n        \"tb1qssypacgyt40r8t95myqgrhrdhq93f5y4jmgmqe53w4tlydphcnaqmpm8kr\": [\n            [\n                \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                2579583\n            ]\n        ],\n        \"tb1qt339ksrha0n5a6lwpql778erkm272hxgamdc0u\": [],\n        \"tb1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sak7ncg\": [],\n        \"tb1qtqqddqrlg4xj3dzvjnea8wh5zy2fdf3jxl7qhs\": [],\n        \"tb1qucj6lx6eatgm5396fe539x53cp8zr0yzgclk6q\": [],\n        \"tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj\": [\n            [\n                \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\",\n                1414310\n            ],\n            [\n                \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\",\n                1414311\n            ]\n        ],\n        \"tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk\": [\n            [\n                \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\",\n                1775825\n            ],\n            [\n                \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\",\n                1892447\n            ]\n        ],\n        \"tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct\": [\n            [\n                \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\",\n                1414562\n            ],\n            [\n                \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\",\n                1454548\n            ]\n        ],\n        \"tb1qvwwxv48k9vch5ddmf83g4fhd0tnx3mt8jp6rka\": [],\n        \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\": [\n            [\n                \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\",\n                1583044\n            ],\n            [\n                \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\",\n                1583044\n            ]\n        ],\n        \"tb1qym6srwn87eu2sa5prkgd2lqva0nh0tr2xkeftp\": [],\n        \"tb1qzyaz308030saay93zqma0at032vfqa9y0gfge3\": []\n    },\n    \"addresses\": {\n        \"change\": [\n            \"tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5\",\n            \"tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu\",\n            \"tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj\",\n            \"tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l\",\n            \"tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4\",\n            \"tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct\",\n            \"tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl\",\n            \"tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq\",\n            \"tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3\",\n            \"tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9\",\n            \"tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e\",\n            \"tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg\",\n            \"tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r\",\n            \"tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5\",\n            \"tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5\",\n            \"tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6\",\n            \"tb1q02g5nde0heaed0y24rztkedh9nvswknw50h7fx\",\n            \"tb1qzyaz308030saay93zqma0at032vfqa9y0gfge3\",\n            \"tb1q3s4hkssd34tyxdlhafthv4muckjtgwuhltu37t\",\n            \"tb1qjv7a78ea0jp9d793d5ra7mtzkjzezwldz5zvr6\",\n            \"tb1qndaru6pfal030ev296uxwuulxrezaj0j70ceje\",\n            \"tb1qsd2c4xwg47hnngn2uqg66y5rxz2hp074u9rq6r\",\n            \"tb1qtqqddqrlg4xj3dzvjnea8wh5zy2fdf3jxl7qhs\",\n            \"tb1q9u3ufsm9ksql8utguap40ch8zpnw83fgzf4z97\",\n            \"tb1qlclgzsp2tktdl66xuk3je7ztztstxvjatly8wy\",\n            \"tb1q7mhmnxal53vtc5flh69nph44vah5j56eyesjx9\"\n        ],\n        \"receiving\": [\n            \"tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu\",\n            \"tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd\",\n            \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\",\n            \"tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c\",\n            \"tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk\",\n            \"tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z\",\n            \"tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8\",\n            \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\",\n            \"tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07\",\n            \"tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg\",\n            \"tb1qplsf242vay6vavy4eguef855fx3klmp9505g88\",\n            \"tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3\",\n            \"tb1qt339ksrha0n5a6lwpql778erkm272hxgamdc0u\",\n            \"tb1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sak7ncg\",\n            \"tb1qf03zdjdnzxwztxs9d3g9ynsvvs5rmjhvtmln35\",\n            \"tb1qq0gdz0vz02ypa3cawlljstrx8cxydhvalcv8wc\",\n            \"tb1qkp86pkt75ds257snenp3q7vs29pf4g6cmhy2hw\",\n            \"tb1q3t0xcpmzreece8xdxq8k5aaxrt3r623tqldp8n\",\n            \"tb1qr7mjlxgc6at67tx0s8ypa5efx8clc47xh6yjqg\",\n            \"tb1qkneqe450eqxtpr3r5z8aw4234sjmpknm0gxsae\",\n            \"tb1qs087qkcawefutkkv8pg037t6txldk5szfntamj\",\n            \"tb1q069xqa4lej2tljmd8fcvvfedav54nmspvjnfs2\",\n            \"tb1q0j2gt4ap2s08cz5vzm5jg87fdeps7x8v4djgrm\",\n            \"tb1qs3j3j05rjefnjqf0mlpztszg8acz868wnxgz6j\",\n            \"tb1qh878je0rfut79fkudf4mkl8m4cn8uzfsluersy\",\n            \"tb1qvwwxv48k9vch5ddmf83g4fhd0tnx3mt8jp6rka\",\n            \"tb1q8evsj0vkzfak2y5qnqx4yf9lty462l7yfhegyd\",\n            \"tb1q99hkhlswfnj8r5wy2xu9a9m0vy8mvffwzhrx6n\",\n            \"tb1qh0csljush2tad6t0s4qgx4r5t0rzcw9729l7kx\",\n            \"tb1qad28sgvvrxjnxdnfjxcuepgzzhzlapgxcwuj0k\",\n            \"tb1qym6srwn87eu2sa5prkgd2lqva0nh0tr2xkeftp\",\n            \"tb1qucj6lx6eatgm5396fe539x53cp8zr0yzgclk6q\"\n        ]\n    },\n    \"channels\": {},\n    \"fiat_value\": {},\n    \"forwarding_failures\": {},\n    \"frozen_addresses\": [\n        \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\",\n        \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\",\n        \"tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07\"\n    ],\n    \"frozen_coins\": {\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e:0\": true\n    },\n    \"imported_channel_backups\": {},\n    \"invoices\": {},\n    \"keystore\": {\n        \"derivation\": \"m/0h\",\n        \"pw_hash_version\": 1,\n        \"root_fingerprint\": \"535e473f\",\n        \"seed\": \"9dk\",\n        \"seed_type\": \"segwit\",\n        \"type\": \"bip32\",\n        \"xprv\": \"vprv9FrABTX8HFeSYL9aaMnLRcEkHBbJnBu9foDJaTvcF8SLvHx6uKqL8rtt7kTd66V4QPLfWPaCJMVZa3h9zuzLr7YFZd1uoEevqqyxp66oSbN\",\n        \"xpub\": \"vpub5UqWay427dCjkpE3gPKLnkBUqDRoBed1328uNrLDoTyKo6HFSs9agfDMy1VXbVtcuBVRiAZQsPPsPdu1Ge8m8qvNZPyzJ4ecPsf6U1ieW4x\"\n    },\n    \"labels\": {\n        \"bcrt1q069xqa4lej2tljmd8fcvvfedav54nmspwm2y8r\": \"\",\n        \"bcrt1q07ulrxeuu45uqen0clqe85v5en6rf77cypla9a\": \"\",\n        \"bcrt1q0quewquwhlfgahhsdg0q3r5lmyzufrtpnqmkwu\": \"\",\n        \"bcrt1q3t0xcpmzreece8xdxq8k5aaxrt3r623tzk5vs6\": \"\",\n        \"bcrt1q6k5h4cz6ra8nzhg90xm9wldvadgh0fptfqj60p\": \"dshsdu\",\n        \"bcrt1qchyc02y9mv4xths4je9puc4yzuxt8rfmgnqych\": \"\",\n        \"bcrt1qd7tjvgttaxzkszzh5ty4yq97r8wscgtemm9au3\": \"\",\n        \"bcrt1qdy4xwmgklqmyrfj336g4f54582zxtm2y4k35sk\": \"\",\n        \"bcrt1qf03zdjdnzxwztxs9d3g9ynsvvs5rmjhvfjx7xa\": \"\",\n        \"bcrt1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxp7gsyw\": \"\",\n        \"bcrt1qgcgk7j9kpt2mygmhmnu4zep79cd289t6lsxfft\": \"dsadsa\",\n        \"bcrt1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s477hsl4\": \"\",\n        \"bcrt1qkneqe450eqxtpr3r5z8aw4234sjmpknmdpla2s\": \"\",\n        \"bcrt1qkp86pkt75ds257snenp3q7vs29pf4g6ce7a8q8\": \"\",\n        \"bcrt1qm7ckcjsed98zhvhv3dr56a22w3fehlkxx7vrly\": \"\",\n        \"bcrt1qn7d2x7272lznt5hhk9s07q3cqnrqljnwladrd3\": \"dsadsaddsdsa\",\n        \"bcrt1qplsf242vay6vavy4eguef855fx3klmp9kxd9sw\": \"a\",\n        \"bcrt1qq0gdz0vz02ypa3cawlljstrx8cxydhvaa342e3\": \"\",\n        \"bcrt1qq2tmmcngng78nllq2pvrkchcdukemtj5jnxz44\": \"\",\n        \"bcrt1qr7mjlxgc6at67tx0s8ypa5efx8clc47x4nalhp\": \"donke\",\n        \"bcrt1qrdzfu6mlgrxpupd4syxrv77ncku89a0yd95n7c\": \"\",\n        \"bcrt1qs087qkcawefutkkv8pg037t6txldk5szt6jsvm\": \"\",\n        \"bcrt1qt339ksrha0n5a6lwpql778erkm272hxglj54c4\": \"\",\n        \"bcrt1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sll870p\": \"\",\n        \"bcrt1quhk94rhlsflc4wgxl9qzd6p6wszt30uxfuyznm\": \"\",\n        \"bcrt1qusm48zmlzwr32csxdw4ar7atw260h22c9ten9l\": \"hihi hi4655\",\n        \"bcrt1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpwp9f0z\": \"\",\n        \"bcrt1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsf57wq4\": \"ddddd\",\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": \"Open channel\",\n        \"ce0bea6a71ef5d8649e67a3235a1d1f5f8611e47725d54cb16d5f9ebabd92c8a\": \"Miner fee for sending to BTC address\",\n        \"d0ae4c76f769dccfa0187633839d704ae803e810fd592e4f2d14178aaacc94d6:1\": \"JOHN\",\n        \"d8a07501eb18302f5b4fa0535c1a7a71eee058de1013f0c2e2ceb53ca50bb99c\": \"dsadsa\",\n        \"eaa745cef67b98bcabd604598406cd782cfafc99a419c2c7a8a1dde163cf7dab\": \"44\",\n        \"fdf0d67d412785e337b2883cf56784345e215513331d966c5ee875ff6d48b38d\": \"dsjaiodsa\",\n        \"tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r\": \"at some point there was some change here\",\n        \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\": \"old address 95645\"\n    },\n    \"lightning_payments\": {},\n    \"lightning_preimages\": {},\n    \"lightning_xprv\": \"vprv9HAix419EKycrbTEJxT1zKCLURqA9LWRHeohrddxQ35QuvfH8fqMurdF4mseJ5oytUCJH5VvhEXSmCTs5SaoT7jEr2hcy7e8uCFLJtuDSme\",\n    \"notes_text\": \"\",\n    \"num_parents\": {\n        \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": 1,\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": 9,\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": 3\n    },\n    \"onchain_channel_backups\": {},\n    \"payment_requests\": {},\n    \"prevouts_by_scripthash\": {\n        \"08cf1ff744d66d4699c5654e162654bacc04558035de45a2f613abe5329d4286\": {\n            \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d:1\": 500000\n        },\n        \"0dbcae29aaa4fde8a0542b606d89c6ae46350f0fc7a3a957337f4d7ef03b5dd3\": {\n            \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:1\": 99810\n        },\n        \"0eaed6a38625ac78bde7585d061872cb55609f8d05c2a4b1f4230f1ea0f78a90\": {\n            \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535:1\": 7596590\n        },\n        \"102dcf626801074a661c851fcc3536c2101eaa2ec03ed03a11b9bb9c19741237\": {\n            \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269:0\": 99811\n        },\n        \"143f42f87521ff360780106377c98d2c27599b6028b7f2858fc71636f003eb9e\": {\n            \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c:1\": 8798\n        },\n        \"241de66b34b781feb863a601c2d49a05ebea8ad0c0f6760ea73e502fafba5bf7\": {\n            \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3:1\": 9598000\n        },\n        \"244501478da81fb6a3ffd88d222708dcb6194931136947873cc0fccc55f24661\": {\n            \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f:0\": 113600\n        },\n        \"2a06d09e67dc4c49523796cf899d2e4002429d9e1280456264b79d1c7caac727\": {\n            \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d:0\": 381400\n        },\n        \"3248021fc00b3f196a362dc9f940a25d488a040498a2d08b7a60aee9ac18626c\": {\n            \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e:0\": 3190000\n        },\n        \"327923b903a21b12aa85f694c8c0de952805a78e4a0164631850c5dc607360df\": {\n            \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3:0\": 200000\n        },\n        \"3641e700a31ec0763da7ecdab2809abd7e6c7668414902ea13958f12c5ca9ed1\": {\n            \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea:0\": 1387695\n        },\n        \"3658a8d8b2614ec7814d2973305e174a12f9d4e9cacf84f81b09cf2fa90d6fe2\": {\n            \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f:0\": 99889\n        },\n        \"374127d2991c7bf24c117cc4d73058d2f56987ce4591930271ae5b87599a40a2\": {\n            \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2:1\": 654165\n        },\n        \"385645f17d282282564fc60e25d026d1258b7c23074ce18127577fc6e1e5ede1\": {\n            \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69:1\": 404711\n        },\n        \"39c770e551fa2d861b8bf9be640933abbde97b6fd3d5b86e21e815b493745448\": {\n            \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba:1\": 8597295\n        },\n        \"3b64546ad58f7d7d5632bd6ecc6bd8d69bfab794674713dac2a4a260406aa792\": {\n            \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661:0\": 100000\n        },\n        \"3fea64e654f5b0ebf560cd7def07419823d048251e5cba13b9bc1d19fc5570f9\": {\n            \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce:0\": 403476\n        },\n        \"4940877cf9e1587732a4379592b0490273e92379e93e7a024656f02690a7b955\": {\n            \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4:0\": 5100\n        },\n        \"513018b2d303921ccf5194decd9e9c1f2a7f93f687ef301254fcd3a20dea0c71\": {\n            \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe:1\": 302547\n        },\n        \"54f31d22030e3b6ff2ffc9d6fef4a9184ff9bd37285c99bd405b662a4fb4b99c\": {\n            \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c:1\": 1148200\n        },\n        \"5dea1555deacfbc9698f32073bb26ac2b3a6462752816a727af8e2e288d80857\": {\n            \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f:0\": 17898379\n        },\n        \"6abaf5808033282b0d182f1393cd89d8e73245b317ee42896449206f6d67f009\": {\n            \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c:0\": 1000\n        },\n        \"6c9528c720baace52d4daf3af546fc33be00b981618807a99e088d148e3569e9\": {\n            \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1:0\": 404573\n        },\n        \"6d6a947e3b89b4901842539aa1059a8f44dafe5029c39d1bbad63bfdbf98d4f4\": {\n            \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c:0\": 10000000\n        },\n        \"76bbdc72fdc74fbb0b049d21b0f80045f2a227cde1cd57a00925a437638984b5\": {\n            \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f:0\": 3099663\n        },\n        \"80c018b3fbc6a3d64e21c8a53a4f5b7eb39cb62998272bd6e0f5a5a5b5947bf2\": {\n            \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6:1\": 6595885\n        },\n        \"846dc81dba7a3df5a44c955bc73afc9b51ab7ef56e3170b835ae9d37967e1323\": {\n            \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c:1\": 6394885\n        },\n        \"969c34a4c63b26dada2c5042db47236fe4bf0d1b16736bc644596b9e1a0863f0\": {\n            \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c:0\": 200000\n        },\n        \"9d905269b1d499af60d605b3a3c16afe21eea08dec1b2013f4707bde14b40c97\": {\n            \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c:0\": 14695224\n        },\n        \"a8445ee64c4775b070c64df9b8006788787da9734502ede664c0d17db65f9efc\": {\n            \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2:0\": 627994\n        },\n        \"af4144146cb7abcd1dd2c812bdcc9661b0611a6a70f682167bb683ea0e91b1de\": {\n            \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f:1\": 135000\n        },\n        \"b77873e2123e43f37054e972ae4685ff203b9d35c14989da321d242f8a0d0799\": {\n            \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:2\": 400000\n        },\n        \"b81fe544d6d281c5aa695c99dba16ba1fd2bd66f5d342ace52ead7c168b36e2f\": {\n            \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69:0\": 0\n        },\n        \"ba80fd5d5ce4a19c5b2750ec73e621dcc46637090dbae62d3f5bc5bb68565360\": {\n            \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4:1\": 94500\n        },\n        \"bc42e29c85e9d6096a1fd59582e91eccd045acaa7683326ea4dc10d579c0cbc4\": {\n            \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe:0\": 0\n        },\n        \"ceb18da9abab996544f4120b0b7fc221581fdf81a7ac4fc1d311ff9c3dd8571a\": {\n            \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535:0\": 1000000\n        },\n        \"cf1356579698ef6bad07be8da12e82f69687505dddffd1f31f70a626347e69bb\": {\n            \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39:1\": 9799000\n        },\n        \"d20f43d6c9c52327eb9ac64c2b9f4b2b54ef197a485936a2bb46b2a68b37168e\": {\n            \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b:1\": 988000\n        },\n        \"d47dd057642f30739fe81d4c56edcbd658ba87445ecf763edfb508b4c615581c\": {\n            \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6:0\": 1000000\n        },\n        \"d5f4d828cfea4006e972fab9e4c23176761621eb98f8b35a4b4a12671ee04453\": {\n            \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b:0\": 3199293\n        },\n        \"dadfddf343dd5eefc2dedbea6f4ad1f48e85852e60d739593ea7d821e59c0893\": {\n            \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:0\": 0\n        },\n        \"e4dbc5351a813f40d2a14dad58b8337eb7cca5071d54978f0aaae195bcd3dd20\": {\n            \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39:0\": 200000\n        },\n        \"e690891dd2a7bfa84bb93d56a9aef8fb6b4e8cdca9014b1bcfb16ab69d05f5f0\": {\n            \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e:0\": 302364\n        },\n        \"e7f95bc81c2de2279d0193d4bc6f5950fa0df01be599f5aeb4ffb63b29d7d1f7\": {\n            \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661:1\": 302700\n        },\n        \"eac5af8eeb1ff53507f99263ccfc8b8b2a85bf947634293a33c5eb70178d5875\": {\n            \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:1\": 3194603\n        },\n        \"ebc0e58e3591ec18b2bb8cce5e58dabfa7d121e026ce0683a02ca4990505aafb\": {\n            \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e:1\": 3204744\n        },\n        \"f04bf26fc16a27abaa2f10540da7a24e369e6ec408f2d901202a7c0cd4a6112d\": {\n            \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:0\": 10000\n        },\n        \"f38506e05a767c8f162b2271551c63a5ddae94e6eb395e441993de38e921fea6\": {\n            \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b:0\": 160000,\n            \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c:0\": 110000\n        },\n        \"f58156a910255e946f7cf2c7daf08ce3faeaf0deda0c9f300679ad9517ef7a68\": {\n            \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba:0\": 1000000\n        },\n        \"faf2c38001ff18046f939975176e9ea069090ed1ef4959625f24d31b3368d68f\": {\n            \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea:1\": 100000\n        },\n        \"fe0e7e43ca025de6ec189c121b245ac207f40177385e829fa725dff68e24e2ec\": {\n            \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c:1\": 15313760\n        }\n    },\n    \"qt-console-history\": [],\n    \"reserved_addresses\": [\n        \"tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r\",\n        \"tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5\"\n    ],\n    \"seed_version\": 57,\n    \"spent_outpoints\": {\n        \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": {\n            \"0\": \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\"\n        },\n        \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": {\n            \"1\": \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\"\n        },\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": {\n            \"1\": \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\"\n        },\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": {\n            \"1\": \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\"\n        },\n        \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": {\n            \"0\": \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\"\n        },\n        \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": {\n            \"1\": \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\"\n        },\n        \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": {\n            \"1\": \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\"\n        },\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": {\n            \"0\": \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\",\n            \"1\": \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\"\n        },\n        \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": {\n            \"0\": \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\"\n        },\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": {\n            \"1\": \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\"\n        },\n        \"4a87be118635dc782e9dd8be26a5be0fa6a1c5267b9db8303f0af85fbf6173ba\": {\n            \"1\": \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\"\n        },\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": {\n            \"0\": \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\",\n            \"1\": \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\"\n        },\n        \"60850b8952f6bf52f1fca76e051888637cb38c1c8a85301c7fa5359f76d1703d\": {\n            \"1\": \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\"\n        },\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": {\n            \"0\": \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\"\n        },\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": {\n            \"1\": \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\"\n        },\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": {\n            \"1\": \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\"\n        },\n        \"820fb2b07ba74b4ac3974d2ccaaf8096290614fe7ec83266ae028d6d857b1e41\": {\n            \"1\": \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\"\n        },\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": {\n            \"1\": \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\"\n        },\n        \"8aed48c4e4ab646c1c5692487c2f1a4414221fef1805463fb21f7549b41a0ff0\": {\n            \"1\": \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\"\n        },\n        \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": {\n            \"0\": \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\"\n        },\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": {\n            \"1\": \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\"\n        },\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": {\n            \"1\": \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\"\n        },\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": {\n            \"1\": \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\"\n        },\n        \"983bbe3d0593b37996821733e53417fd06bcf559adcacbf339dd7729cb19673d\": {\n            \"0\": \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\"\n        },\n        \"a3ee0b514e9af4735097072dd06beba9b9ba02a072e242d1cfe0fce6bfad4bad\": {\n            \"1\": \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\"\n        },\n        \"aff92372faa2717819781f1b9a8771abd3fa20f5b504b682d264cc9575acca19\": {\n            \"0\": \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\"\n        },\n        \"b30af3bfa76e031e5aea6c2c5cb7cbcd57a145697b74e820ba2a3a5858bdc98c\": {\n            \"1\": \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\"\n        },\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": {\n            \"1\": \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\"\n        },\n        \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": {\n            \"0\": \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\"\n        },\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": {\n            \"1\": \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\"\n        },\n        \"ca675c39c9f64bce9ede8f551d2b5871da2aa4747322a2f75878ad76b0b645d9\": {\n            \"1\": \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\"\n        },\n        \"d1d575990837cfb5b93d92ba4e9b0454b385fe3b865b98088d68b1778b8855e8\": {\n            \"0\": \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\"\n        },\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": {\n            \"0\": \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\"\n        },\n        \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": {\n            \"0\": \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\",\n            \"1\": \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\"\n        },\n        \"f2dbe14e9f10e068c6bd4a534d8c8a2945c6bcd69cc85b4d5eb37aca4d3b5384\": {\n            \"0\": \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\"\n        }\n    },\n    \"stored_height\": 2579588,\n    \"submarine_swaps\": {},\n    \"transactions\": {\n        \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": \"01000000000101e855888b77b1688d08985b863bfe85b354049b4eba923db9b5cf37089975d5d10000000000fdffffff0280969800000000001600140297bde2689a3c79ffe050583b62f86f2d9dae5460abe9000000000016001472df47551b6e7e0c8428814d2e572bc5ac773dda024730440220383efa2f0f5b87f8ce5d6b6eaf48cba03bf522b23fbb23b2ac54ff9d9a8f6a8802206f67d1f909f3c7a22ac0308ac4c19853ffca3a9317e1d7e0c88cc3a86853aaac0121035061949222555a0df490978fe6e7ebbaa96332ecb5c266918fd800c0eef736e7358d1400\",\n        \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": \"0200000000010119caac7595cc64d282b604b5f520fad3ab71879a1b1f78197871a2fa7223f9af0000000000fdffffff02d8d105000000000016001440bb0029ce2482a73fff367f0f7ea2390d7a1c5f20a10700000000001600140fe095554ce934ceb095ca39949e9449a36fec25024730440220582ac0428398fb55846cb570e26fd78566b4bb67febfc3c835057e74f1e03ee0022022aa084ba38768a4ce451face547fd9217684e5fefebc29853ffc38ee9eed0ee0121031284572b5ebec99319a87ec710d162a9d21e0afa2a89f8a5cbe290f95369a22e7d5c2700\",\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": \"01000000000101399d64669c44e99825ddef70e299d85c9235030f9d75c2ac3103d23616c9f10e0100000000feffffff02400d0300000000002200205208fb0ad888e82e4452ab6cbb06a51c09ca36c6f2a86e5e81884de1860cf400307492000000000016001496ea3197df375c6a62188cf9119aee984c0cc6150247304402203e535dd045e4e7d062af6d11888643ddce48649e123abecf6a42f6048f892f18022076b9e21191096e9c1800904e24fce6758a7d93e8795d35c161a334b83e1286860121030644479c1b8c106e2f0a495fe18dc45572b3413bda2db919c1fcf0f77f43241ca18d1400\",\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": \"010000000001012ca35f85d1189b6cdc8d0c95fe637c23928fb23dbc492f9d078f85e9ea6444020000000000feffffff02400d0300000000002200203bf21a5a90ce63776ecf8216ad92b0afc831c1d5fd1d213d29f916181aac74a958859500000000001600147fb9f19b3ce569c0666fc7c193d194ccf434fbd80247304402200571cebd2f7845524862e773661d08d3e14b0a1fd109db2f325ae405e25dd83d02200a92d4ecfe49919389eb9754cc60b5af058a167690b876dad6f09f15f1b69b19012102a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc8c8d1400\",\n        \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\": \"02000000000101eafcb255247f0572767b990a52274f705dbb4cdc2d7675b1f7cae7b7e4ebdd250100000000fdffffff01318601000000000017a914bff6c01dc206f8fb7062e314e378e9aeae017a5c870247304402203cd1869eb6ce787f4f27e471b58acfd288d57ca55c8c996aba89d1865647408902200f57bf52775d397b4f02ac584bf6bab96335e89a2b2bfa0bb8f4aa98ee62dd66012102fb265cce2019e555fe23fb45e4cbc3d966bb399a52b2e93b808ad7961b426de15ee01c00\",\n        \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": \"0200000001f00f1ab449751fb23f460518ef1f2214441a2f7c4892561c6c64abe4c448ed8a010000006b48304502210092443dd340a1caa7791351324aee87b35b6057a7d9237fc95e1f1d029dd2fe2402200cd3cd13551313d186726bd6beb6d386f79d0a978972e5f923303df1700c957d0121030638cdea69f69ff45ed0d97d8a5c41ce7bbe0d774be898143f7a73cb73ad72b0fdffffff01e38501000000000016001446116f48b60ad5b22377dcf951643e2e1aa3957ac3271800\",\n        \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": \"02000000000101411e7b856d8d02ae6632c87efe1406299680afca2c4d97c34a4ba77bb0b20f820100000000feffffff02c0bb01000000000016001455d5618cfc2d731d7c3d80b92bd2c87835c1234c580f0200000000001600144f28f480d74dbfa2cf5c1c85cbe29368cba5f3a60247304402201f857d44cbf474275010d4fc996bf78e15b5631ea6557f842c3a3aebdd7fc8be02205e21cabbf5247ad08c62bea14adcde8f0bad722de3473435f500a8fba7ea6c900121031abaf20c4146bdee1ad87cbc56c892cbe93a92fd11614d379f80be5f60b1f3d50aaf2400\",\n        \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": \"02000000000101ad4badbfe6fce0cfd142e272a002bab9a9eb6bd02d07975073f49a4e510beea301000000171600142d886eecf1d36b2a317f74ae3ae0088944017d27feffffff02af2c150000000000160014e866546b96bddfa46ae298c8363527531e03573ba086010000000000160014e437538b7f13871562066babd1fbab72b4fba9580247304402202ce3a50013652650b1b93b89aed5d36bab4adc13ab7054aedb908735b0d2923d02202a4a5bd091f88a186b2d9e92ca99f3129c6cd15861eba06d4b6677601fa1fc9a012102c821655842477341331fb3faa44a04547c5c039b8f44f48b1b7c45266f545ea7d0181b00\",\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": \"010000000001016ef7ba954c29f9d1aa21d5fb1ff44e2e36d04536ce17ebd15306130f81f2bd4d0100000000fdffffff0210270000000000001600149f9aa3795e57c535d2f7b160ff023804c60fca6eebbe3000000000001600147bcf38d8a69071c21abbfe0532cb6aeacaf5c2940248304502210086c464555baeb19f60d9a8284bbca0c77009e6864e751bd4ca87105d0f9d1acf02207b6d4f3d5142af6fe7636e60967aac294155bdea469bd5c0ac18612e8ed8456d012103cd68ad8bddacdc82a39a82efd7a045f2d9513a9de31049286b696c87a8efbe8767341600\",\n        \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\": \"02000000000103e00607297fdfc9808759f2042adfe27912e79031bbff6deb627583a65538bc300100000000fdffffff6c4bec256f089c29f5b6bc5c4cd40fdeca1729cc2106a2cc6e0630c071e429400000000000fdffffff5c22c57d029a8b261ebd9290e2ddd58283fd0fe0a8bef084eb39ebc42e3624b40100000000fdffffff018b1b110100000000160014ef8dfaec6a39b7efc77828fc3840a26da11bfe7502473044022073382b79c486c708f16a1e53c8327060c193ba8db376df3c40abd7b2cf0c39c7022028f210de787f1bdc9cf0060e612b1d71b7f36b1f86956b43a53d516f1aeb2657012103ff475c0005da39a068516cbbfe5f0b5fcc126aba81e405728adab8bdd572d9a3024730440220243989034479fd8de480207d59aec4106ff2277f11f7a982509d309ae90e108f02203282db9a7f38d43c2672abe64f0bfd5d28f60b053fcb1b87671d5dbaa987e12301210242c23a20435cd52192581537b2017288b8cdf3fef349100c11a125d9529017e10247304402207712fbcfac6e0011f404945aa1480332da34eeb5ac47623e341a4f9692caa49802202c6ec5e739aa52f675ffefbf4c057ab6b4a357f66902d96b6af5327206c9367f012103834e1e34194508e3fc7db19d3c6964149efd58f8c67548cc765c3284cad24d6707ee1700\",\n        \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": \"010000000001036ef7ba954c29f9d1aa21d5fb1ff44e2e36d04536ce17ebd15306130f81f2bd4d0000000000fdffffff3d70d1769f35a57f1c30858a1c8cb37c638818056ea7fcf152bff652890b85600100000000fdffffff84533b4dca7ab35e4d5bc89cd6bcc645298a8c4d534abdc668e0109f4ee1dbf20000000000fdffffff01383be00000000000160014dfb16c4a19694e2bb2ec8b474d754a74539bfec602483045022100dfbf8dde6a9070a13a808d317e4fb6d35906a3b0ba4f53543ddababfa0df631e02204bc0b692c63c500d517ee96df74ac13e0d680898d761895ea80ed7458031dbc5012102462df8e9cd6196186dd4d5444b6a4c7acd006a7d4c59b8b16e7517828fb8cfc302483045022100b59c985153d9a2a27bdac393ec238dfca3dc2dd63bb181c778ad41568ae2017802204b3c71b2e90dbfb054f432ea95c6fefe29d8c2c36ee725a7916b0241ba6409130121030ea1026664c47391f82ff1bb693b1bfc959ec3b3cbc80a8dfd31fc637cad765702483045022100d056da28e2cd316f5e86aaa56298f0f5d1a313cbbad95f65712b9a4a0be1448f022059d8719fece4677da694a7ca1b61a9ee386eb108e58bdf648dbde0db704ad40f01210208b66c5067eb109735768b92a2800aa6844ae7575911298e0fd6ced709987d04a4361600\",\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": \"01000000000101a6c78418b9383dfcf1cf93f1b084f4e5106f862c82ac0b602efa2568195778730100000000feffffff02400d03000000000022002005b05ff139f7a25173a8ada54eaf24c7d9764409c829ca792726fb7aff1eb8360594610000000000160014641837eaea0de6ed898e30ff6e093c566d790521024730440220736f7035d58d08d3e7be11edcdbf3d91cbd86cda16b19ec48b12e5257c21c6bb02202987a4976bbc5e759ba8d5dc7dedc56eadb449966c0e2520f9a1392c2342cb6e0121039c25ce87cddec7e7b0a7a4541047cba7554d6d4093965869787ad2eca4be8a55a1951500\",\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": \"010000000001013cbef1e09f39646ab1e2ba273edcafa11f9da5ea25ac0fd194ccec51fa4a28410100000000fdffffff02f0ac3000000000001600144616df751811e5114e045a6b7cad889463555c0488e6300000000000160014553a3b97c2cc7d3d4edeace3281ff038bd06762902473044022070000f29d154f419ebe2dc740c80eb89dc0d571d96d0469f629761d7fb13cad9022008c94670cb3e682a5f076c4cd6634b8ec32168cd648c3f6c89597c105cf5368a0121030bd1ee5d34698af47ee3ceb954b3009ecc0614bb64b54d669adfb233a36d2ae8d3311600\",\n        \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": \"020000000001013d6719cb2977dd39f3cbcaad59f5bc06fd1734e53317829679b393053dbe3b980000000000fdffffff021a950900000000001600141f81d08aedb71cd788380d642b755ecb961a1edc55fb0900000000001600141b449e6b7f40cc1e05b5810c367bd3c5b872f5e402473044022064462613a5a80a83ed4badc54ff4e6e85e06781a55ebccc1eb253dc865c478cb02204c4bd331205a9cf90094ca60ba8f1367f5246b92595a361b40fafce522ba4dbf01210251700930db16fbda3de7141685dfae05fda67b234ff7c4861562a3ce07f592497f5c2700\",\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": \"02000000000101fe13acd33700e2dc135c833b790eb75e1ed71429773b36192315f3fede7f76970100000000aa771980011c9d040000000000160014c6c1e628487ca44a22beb15e9035f7f76bbc103e04004730440220196ae31431605048d0af586af779d903c813abada54fcf35877b2b4569ee27e502204fa7c6a675369fe1ccc3d5d2e658fc4be7fb480d9f3bd6d1049a7c67264189c401483045022100bd1fdd55c6db44fff54bebf3acd649a5e135254f187561761c150521555e3713022042035709c099da8956af5e6806726170f0104cf5f145ec4632a46d806f3c447601475221022aab353e6b25226a3cb38e81dafe8fca3416642847dab7d38aaafe00eb8294ee2103a5bf1debe9c97bb56c7a4316b45bf2f8d35612db2964253ae8efd0f7ca8e1f7052ae1388d520\",\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": \"02000000000101e1c8758e0a1e9728001f7c49b49493384fca2257594de3868cebc779a3b77bd50000000000fdffffff0114280600000000001600144343eef6273442ab066a852a7d6ecc61950b2e7b0247304402205f8a63b3c273abd010a9c095d9591767b6a628452c93acb30809a7989fc3bfa602204b5f812b1c7151e279d58dffa29942552dcb2fad9d14dc39790785fa158b6adf012102c0c89554ca035a1d840eb87ae679146d5cca0fac2a57d078811894ed9e3a4b5ab4da2400\",\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": \"010000000001013545008007e0d26a52ab3b64217909a77ab1dd06ada0520b66b6710c04162d970100000000fdffffff0240420f00000000001600142e61106d61d1ad4f1e44e79c8d5bc5907d48d3552da5640000000000160014783997038ebfd28edef06a1e088e9fd905c48d610247304402207878752ca1cc79758db2959062b2fd0ae0682e83fb258385a340d636c9b85810022049a0275f3e8dde8ddaffaab5298c44195c1066ebb414fc3d942b57728058ff6a012102be2e344c2f49afa8d734a2f79a09827b038d2f8f6bfa77d5a2e077c22a3fd80aa6941500\",\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": \"01000000000101d3b8ef348489a948e31003be75291a6bfe0cf7288a695a9f9931f2ab3249eb030100000000fdffffff0240420f0000000000160014eb23be36ca7b46f3508a042a121b17f36384c4222f2f830000000000160014e5ec5a8eff827f8ab906f94026e83a7404b8bf8602483045022100c61749c026980911afde50838cb72d3484cdd9b9f419d8e6c7647574a52f0a070220471133c1fb6d21a139f91f01e6fd927463d1f5017dd248f2ca6f97363de396d8012103a23dcf3edc18180b0d82b0be8eb1c30ae61597e99e298246e05109b7ba897637a5941500\",\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": \"02000000000101384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bc90100000000fdffffff02ec130000000000001976a9147f20306730683c828f9a3a990193007106aaca9788ac2471010000000000160014edc62a3c999159ea7aa02f4798f06e2020dc35ad0247304402205f8aa5c02cc6d949f11dc6f61b10ffb37a9fbbd5a4dcc840e1048c257a55ae080220153a6e02eb8b12de1f943d0071cef4f710c3406c8a67174ddf491ea94f4104b0012103555bc1ad91fea0ace08ef29d9e1eeaaa6b9fc45d225c60e30d7b8a6ee37ed03e7f5c2700\",\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": \"02000000000101ce08f7d7b4b76a57ba5693a31219e4068e02c5fa8e7e57bc1cf50c47625cf56f0000000000fdffffff02a0860100000000002200204915b90ffa7ef166e0818b75a206d7d179efa14af39c23781a083c749aa651a46c9e040000000000160014905190f141d2089497a554d5b74175929d2b2c7d0247304402203c9cbdd54e067f9a45c7795ae570fc49c706bc4174255f934afbaa6bf8e5411f02203b019772e113869efc8573fed181b14e33ea9bfdeb3f495d3ae1e4f8844e3290012103a85bd8a32a699000359752570dd3bc027d1ca9865fe5fa39c7d4aaab785b50cb48e82400\",\n        \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": \"020000000001013cf21d22012066ba1c566dad36c1e5d76e4c95acc95b9e357e4d58e0862acaef0100000000feffffff0200710200000000001600146f9726216be985680857a2c95200be19dd0c217960130f0000000000160014a96ad868b42a6944f6adc75b68a4b30a158ad175024730440220235d1cddad375f7d7612c50fa08b2ec1d4772b333795b6cb98754225626757b902200f4a784e44a2a23fd811f9f28e82ee0333068156ba969556aed1f6337337823f0121030ee45e829f5f95c3a0a184897c7fdb687307301164ed55d98411368d962510bd40d92300\",\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": \"020000000001037fbdcc64290f40401515f0cf75d761e9d2b80acf70118d6fa923976b0a49b5230100000000feffffff8bf2c1baabcc788c47c5d2da265a823ddecf97250ba0ddebca1f234e98f8098c0000000000feffffff3cf21d22012066ba1c566dad36c1e5d76e4c95acc95b9e357e4d58e0862acaef0000000000feffffff020000000000000000166a14b837667ddbc6acf0464c1eb35b8a2ba485364cd7e72c0600000000002200206a21894e5bd3a85c5c0f3211772c1553137bcdfbe0c06d5c691289d4a8862b0b0247304402206bfc98da12ba0095c12a9929e7b39132ed9f37f8ba3beaf32a479a738f12a66f0220678fa3ac65fbe79801a39931a4d83255bd75c62a76045ddd7f611ad109538b8a012102bb8e9193021c1f704d7ce8577c5fdc7ba22dd05a03b5b5ecd6ef6307dd8a60bc024730440220730470f1b831e706a633091f2bdaa0944e8b80a61db84dfc41aae5e9265809dd022055362b2618d4dee152819fd259db76aca5c7e7cb7f56d7cd6da8d006b44ca3bf01210319d5b935f6f1cd4a1b84645c3f88db404a93baf78451da144a0dc00aaa1d978c024730440220205c9e119f1b2a6951129f8f4a917572ee4c5c7ed55042b1a6e4d787c8d65bdd02204ced0cbfb7bbdb9e89938ebaca90a9a15727c21a03d5d758617aa25077eba4ed01210319d5b935f6f1cd4a1b84645c3f88db404a93baf78451da144a0dc00aaa1d978c98b72400\",\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": \"01000000000101ba709d2dafe9a11fdc2c1b235587455baf0415a2cf302f9cb9c046129a9326770100000000fdffffff0240420f0000000000160014e278e5edb47b274f91d137e767190b4d02b09a172eea730000000000160014692a676d16f83641a6518e9154d2b43a8465ed4402473044022074b4e39c36785a2751fa3b944ed1430bab6461714549d32a195132aace1ca507022043c545d8a1d349ecb6cd3d87fd6c73eb5b68e0d2ba99891d72fce278dce090d0012103e0e2b67a3e8244900fa916decb6a9d45787ebe15b882c7a065cd0151d298fb4ea6941500\",\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": \"020000000001016136fe76bf2a9528c3eeff3200dd1c84dbbde4d3df9c5571e0f189a0a7f5db870100000000feffffff020000000000000000166a1492e1315ad9862cb68d93417085728e974f2f8bbed39d0400000000002200204c0dffbbeacb7180f46597605740cdab2f8cbbe603b737b95eb7076de632659a0247304402204b95fa4019d2d5427646c04a8b740111da39b91ab8cd8e7976aa99912d1cf7d60220458db68cf3d977cfc5ecb38c193b890a3beae714d3485233f28b52b4db5f7563012102576b988dc3260d1dc5e579d604f6d3663287c933cde4ccd60d6875e9ced8fc5607012500\",\n        \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\": \"020000000001026962bc75f03d744a58dc43f17c61ea1f5ebf2aa9e80ede831eb2e097e1e142200000000000fdffffff1f657881066bcd13a189eed6825e30061c1062c473124fcbeaa4e8cde5bc48ba0000000000fdffffff013dd13000000000001976a914fb08dfdd325a10708693d8565b93d2eb38373c0188ac0247304402205039ecdcbbd57fb7093b38bfbd599aeed5c085a0812d7d977f1325d63733ea710220281af83a941ca7e1dd99381904905d095413a654f1d9e9997060bd5f61adf8be01210379da8156b704d5dfe4432bc829588c643ec65c5a409763dccc3da211479f4bb702483045022100e0c07bb37d8da22ea0036b835d86512ecaf584f00534b2548855b02c1d49f59802200cd94644caa3e90349a0e22de080037290b75e2a5906333cb46ef5fb8f36ba7e0121027ba4d3ee6471a985307a37d09eb9a0a73c1a31b57616fe3e53a3d6d454002519c3271800\",\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": \"02000000000101e00607297fdfc9808759f2042adfe27912e79031bbff6deb627583a65538bc300000000000fdffffff02e803000000000000536a4c504a61637175656c696e652c2049206c6f766520796f75206d6f7265207468616e20616e797468696e672e20596f75206172652065766572797468696e6720746f206d652e204c6f76652c2044796c616e5e22000000000000160014c388b18dd1771046fecb85f44642ffb7a4efb62702483045022100fa3523ed4f12e036b952c8c7aa88dfc663086bd6a951d14a7faaa3fd28587294022017c7984bdcfeed7e25de571f020f65d30ff03e0dce4c6c2514c2ffc208280c80012103c3172a9f8820681c62b8bf28961988a4642b33f4920b9da14b06965c7fff83fded3b1600\",\n        \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": \"0200000002ba7361bf5ff80a3f30b89d7b26c5a1a60fbea526bed89d2e78dc358611be874a010000006b483045022100e2886246bf9d98bae1ed94dddb3da345456d4848c02dcd90e0e9357ed609836f0220344890230f0748c7389645e2a71cf693aeb84617822114bc01e15e17960b36cc01210348d667e58824a00bfc719645c0ff685513243f3476b6b05e4f46cf9b8c3146b0fdffffffd945b6b076ad7858f7a2227374a42ada71582b1d558fde9ece4bf6c9395c67ca010000006a473044022047dcdee44123175b7301882c628da72810ff585e30b94c8403aa641ffd6d8825022046ff6818ed60b5dffbe19534ea4b7a275e48aaee183a14eeb66e36d136880c6b012103ef48fb7c170e02b9f6f481e4eb541947ce89709163c4cdef0af65bf8b64b2d8dfdffffff010f4c2f00000000001600147714df0e53290ddfa3d893b41b8056f7be98a770c3271800\",\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": \"020000000001017d6f488442fa311ff6e3a1124ff3cf6a974aa13867306f0456c8e73ca5c7be030100000000feffffff030000000000000000166a14cbfdb55e747e0cbdc2328518ae6363cc5f5c3fc0e2850100000000001600143ece1158a80cbbb40dd12688732d3b77a959e5bf801a06000000000022002084081ee1045d5e33acb4d90081dc6db80b14d09596d1b066917557f23437c4fa0247304402205113e656225c09f8b05fb5bef8c48196e2520a988bc89832f8cc806c64dc3dce02204fbb91805640038fb7e73eb2bf5a28a3a015982a1d9dcd591e6bb9c8c103bab9012102fe3de4d6b6bf207134f40ada206ff6e2ed088169d2413db656c8de963a15c6e17e5c2700\",\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": \"0200000000010169fb54298cd7f5490eb80c10e80bb8720390f7d96a9382c91c74827edee9fc940100000000ffffffff015d2c060000000000160014edb4b55f1f8c33ac8597fa53d08d8997131f1b93040047304402202033a46f390d2ed27d1e7aaca2c29cc6af20b92a7535ce7bfa9355480a1a83d202204a94016d878e7e25647738ff28884046d27e4ca83287a0fd4e51ad7d899e47ea0147304402201bddd59748ba0a79d73fda2747b9688b86eef9cad625201d26d6591ceebe18ae022014ea8ecb3722b3ab4873476ca77b2d6ae366d2e61af42ead40419221d6192ba301475221031c9cd5530ea64a7e411c21701d5bcac74e55420924110c93706c12ef7184f0a42103672361cd8f92dfac7fb1d1bb8974bf826cf8cc0eaa6fcb23175be9312d5aced652ae00000000\",\n        \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": \"020000000001018cc9bd58583a2aba20e8747b6945a157cdcbb75c2c6cea5a1e036ea7bff30ab30100000000feffffff02b0ad0100000000001600146f9726216be985680857a2c95200be19dd0c2179288511000000000016001418d1896a5e9360279ab7d07a65ecc992d9b514c50247304402206941c4f8cdd0f4a907931f4400ed37a310b00425a2cf218f11affd53d70268e50220412b7e34545d3aabe2b9f00b9fa18c84a2d030b2f4244127dca69738abec6fe10121033399307690d676c7faf57ac454ca6399017f45ef5aa3bebac07b8691bb572a073dd92300\"\n    },\n    \"tx_fees\": {\n        \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": [\n            null,\n            false,\n            1\n        ],\n        \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": [\n            null,\n            false,\n            1\n        ],\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": [\n            1000,\n            true,\n            1\n        ],\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": [\n            1000,\n            true,\n            1\n        ],\n        \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\": [\n            111,\n            true,\n            1\n        ],\n        \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": [\n            null,\n            false,\n            1\n        ],\n        \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": [\n            null,\n            false,\n            1\n        ],\n        \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": [\n            null,\n            false,\n            1\n        ],\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": [\n            141,\n            true,\n            1\n        ],\n        \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\": [\n            246,\n            true,\n            3\n        ],\n        \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": [\n            null,\n            false,\n            3\n        ],\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": [\n            1000,\n            true,\n            1\n        ],\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": [\n            141,\n            true,\n            1\n        ],\n        \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": [\n            null,\n            false,\n            1\n        ],\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": [\n            183,\n            true,\n            1\n        ],\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": [\n            1097,\n            true,\n            1\n        ],\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": [\n            705,\n            true,\n            1\n        ],\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": [\n            705,\n            true,\n            1\n        ],\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": [\n            210,\n            true,\n            1\n        ],\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": [\n            776,\n            true,\n            1\n        ],\n        \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": [\n            null,\n            false,\n            1\n        ],\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": [\n            289,\n            true,\n            3\n        ],\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": [\n            705,\n            true,\n            1\n        ],\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": [\n            153,\n            true,\n            1\n        ],\n        \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\": [\n            181,\n            true,\n            2\n        ],\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": [\n            202,\n            true,\n            1\n        ],\n        \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": [\n            null,\n            false,\n            2\n        ],\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": [\n            190,\n            true,\n            1\n        ],\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": [\n            138,\n            true,\n            1\n        ],\n        \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": [\n            null,\n            false,\n            1\n        ]\n    },\n    \"txi\": {\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": {\n            \"tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5\": {\n                \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39:1\": 9799000\n            }\n        },\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": {\n            \"tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu\": {\n                \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c:0\": 10000000\n            }\n        },\n        \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\": {\n            \"tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk\": {\n                \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea:1\": 100000\n            }\n        },\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": {\n            \"tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl\": {\n                \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e:1\": 3204744\n            }\n        },\n        \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\": {\n            \"tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq\": {\n                \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:1\": 3194603\n            },\n            \"tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3\": {\n                \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c:1\": 8798\n            },\n            \"tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd\": {\n                \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c:0\": 14695224\n            }\n        },\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": {\n            \"tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4\": {\n                \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6:1\": 6595885\n            }\n        },\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": {\n            \"tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct\": {\n                \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c:1\": 6394885\n            }\n        },\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": {\n            \"tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp\": {\n                \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe:1\": 302547\n            }\n        },\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": {\n            \"tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9\": {\n                \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1:0\": 404573\n            }\n        },\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": {\n            \"tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l\": {\n                \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535:1\": 7596590\n            }\n        },\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": {\n            \"tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu\": {\n                \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3:1\": 9598000\n            }\n        },\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": {\n            \"tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5\": {\n                \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:1\": 99810\n            }\n        },\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": {\n            \"tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e\": {\n                \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce:0\": 403476\n            }\n        },\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": {\n            \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\": {\n                \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b:0\": 160000,\n                \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c:0\": 110000\n            },\n            \"tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8\": {\n                \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f:1\": 135000\n            }\n        },\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": {\n            \"tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj\": {\n                \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba:1\": 8597295\n            }\n        },\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": {\n            \"tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg\": {\n                \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661:1\": 302700\n            }\n        },\n        \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\": {\n            \"tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z\": {\n                \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269:0\": 99811\n            },\n            \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\": {\n                \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f:0\": 3099663\n            }\n        },\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": {\n            \"tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c\": {\n                \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:0\": 10000\n            }\n        },\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": {\n            \"tb1qplsf242vay6vavy4eguef855fx3klmp9505g88\": {\n                \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d:1\": 500000\n            }\n        },\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": {\n            \"tb1qdgscjnjm6w59chq0xgghwtq42vfhhn0murqx6hrfz2yaf2yx9v9skh03as\": {\n                \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69:1\": 404711\n            }\n        }\n    },\n    \"txo\": {\n        \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": {\n            \"tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu\": {\n                \"0\": [\n                    10000000,\n                    false\n                ]\n            }\n        },\n        \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": {\n            \"tb1qplsf242vay6vavy4eguef855fx3klmp9505g88\": {\n                \"1\": [\n                    500000,\n                    false\n                ]\n            }\n        },\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": {\n            \"tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu\": {\n                \"1\": [\n                    9598000,\n                    false\n                ]\n            }\n        },\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": {\n            \"tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5\": {\n                \"1\": [\n                    9799000,\n                    false\n                ]\n            }\n        },\n        \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": {\n            \"tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z\": {\n                \"0\": [\n                    99811,\n                    false\n                ]\n            }\n        },\n        \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": {\n            \"tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8\": {\n                \"1\": [\n                    135000,\n                    false\n                ]\n            }\n        },\n        \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": {\n            \"tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk\": {\n                \"1\": [\n                    100000,\n                    false\n                ]\n            }\n        },\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": {\n            \"tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq\": {\n                \"1\": [\n                    3194603,\n                    false\n                ]\n            },\n            \"tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c\": {\n                \"0\": [\n                    10000,\n                    false\n                ]\n            }\n        },\n        \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": {\n            \"tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd\": {\n                \"0\": [\n                    14695224,\n                    false\n                ]\n            }\n        },\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": {\n            \"tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct\": {\n                \"1\": [\n                    6394885,\n                    false\n                ]\n            }\n        },\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": {\n            \"tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl\": {\n                \"1\": [\n                    3204744,\n                    false\n                ]\n            }\n        },\n        \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": {\n            \"tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3\": {\n                \"1\": [\n                    654165,\n                    false\n                ]\n            }\n        },\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": {\n            \"tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r\": {\n                \"0\": [\n                    302364,\n                    false\n                ]\n            }\n        },\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": {\n            \"tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e\": {\n                \"0\": [\n                    403476,\n                    false\n                ]\n            }\n        },\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": {\n            \"tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4\": {\n                \"1\": [\n                    6595885,\n                    false\n                ]\n            }\n        },\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": {\n            \"tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj\": {\n                \"1\": [\n                    8597295,\n                    false\n                ]\n            }\n        },\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": {\n            \"tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6\": {\n                \"1\": [\n                    94500,\n                    false\n                ]\n            }\n        },\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": {\n            \"tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg\": {\n                \"1\": [\n                    302700,\n                    false\n                ]\n            }\n        },\n        \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": {\n            \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\": {\n                \"0\": [\n                    160000,\n                    false\n                ]\n            }\n        },\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": {\n            \"tb1qdgscjnjm6w59chq0xgghwtq42vfhhn0murqx6hrfz2yaf2yx9v9skh03as\": {\n                \"1\": [\n                    404711,\n                    false\n                ]\n            }\n        },\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": {\n            \"tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l\": {\n                \"1\": [\n                    7596590,\n                    false\n                ]\n            }\n        },\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": {\n            \"tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp\": {\n                \"1\": [\n                    302547,\n                    false\n                ]\n            }\n        },\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": {\n            \"tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3\": {\n                \"1\": [\n                    8798,\n                    false\n                ]\n            }\n        },\n        \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": {\n            \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\": {\n                \"0\": [\n                    3099663,\n                    false\n                ]\n            }\n        },\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": {\n            \"tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5\": {\n                \"1\": [\n                    99810,\n                    false\n                ]\n            },\n            \"tb1qssypacgyt40r8t95myqgrhrdhq93f5y4jmgmqe53w4tlydphcnaqmpm8kr\": {\n                \"2\": [\n                    400000,\n                    false\n                ]\n            }\n        },\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": {\n            \"tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9\": {\n                \"0\": [\n                    404573,\n                    false\n                ]\n            }\n        },\n        \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": {\n            \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\": {\n                \"0\": [\n                    110000,\n                    false\n                ]\n            }\n        }\n    },\n    \"use_change\": true,\n    \"use_encryption\": false,\n    \"verified_tx3\": {\n        \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": [\n            1346870,\n            1530271747,\n            11,\n            \"0000000000000be4817e499bebdcebd9c792f68551bfedbf14fa058fd8db4e2d\"\n        ],\n        \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": [\n            2579582,\n            1708966959,\n            675,\n            \"0000000000002b0be5f214bcdcd765f6e4cefa1226a6d3a4e1b482da9707603a\"\n        ],\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": [\n            1346978,\n            1530282040,\n            3,\n            \"00000000000004f73116c1b9d17dafc409710362110c7cd9e52f1e8703147238\"\n        ],\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": [\n            1346957,\n            1530280346,\n            10,\n            \"00000000000004e05e5f82a8f3db5f37cf8b3c8ad4d44a80556c63173f2fdb8d\"\n        ],\n        \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\": [\n            1892447,\n            1605658960,\n            42,\n            \"000000000000009e8f7f68acdbf0d5d20d920794916249d7300d3d4ddea01a7a\"\n        ],\n        \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": [\n            1583044,\n            1571148168,\n            273,\n            \"00000000691cd68ef23f24f9bd245aaf85f8dec4abe26d1f9ff97681af9eb222\"\n        ],\n        \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": [\n            2404107,\n            1667575669,\n            18,\n            \"00000000000000dd5c68b0c41dcd77382c18fada64632c0475b906f0f6a936d7\"\n        ],\n        \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": [\n            1775825,\n            1594136749,\n            81,\n            \"000000007583aa1310c8368737317e3443990a03c2bdcc95513f82c21e7e8609\"\n        ],\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": [\n            1455208,\n            1549129020,\n            23,\n            \"000000000000002bbb81515fa47a4aaa4af7b324b606df83b90be6018172ba99\"\n        ],\n        \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\": [\n            1568264,\n            1562691216,\n            85,\n            \"000000000000027001cae7dc4b5b14a66f2219d13a620e1ed12bf5a7a9f444fd\"\n        ],\n        \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": [\n            1455781,\n            1549379178,\n            61,\n            \"000000000000006f773a59309e17ce987f302945141d657b6e310bf04a5a11d2\"\n        ],\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": [\n            1414562,\n            1538150440,\n            81,\n            \"0000000085ee7b5accad0063fc1a368d905cbf704223bccac1b9d2e351a4578d\"\n        ],\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": [\n            1454548,\n            1548893536,\n            44,\n            \"000000000000004bfd6914c088814a418c4308cc0f6b87b993455b1a2eca7531\"\n        ],\n        \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": [\n            2579584,\n            1708968409,\n            1814,\n            \"00000000000000120fe076771544efb54ec2ed49c0ad1278bb49f9c9efa51944\"\n        ],\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": [\n            2528392,\n            1696346138,\n            155,\n            \"00000000f7d4a4dcb86d377009f654832d52f9f94299c51434eab00038e4ab6a\"\n        ],\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": [\n            2415285,\n            1673173967,\n            17,\n            \"00000000000000105c5d1016f327809add5b052aa5032e34cab26050535143ff\"\n        ],\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": [\n            1414311,\n            1537898497,\n            45,\n            \"0000000000000046df1be1854387a550e40df1f487218b6b977b7323db986c45\"\n        ],\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": [\n            1414310,\n            1537897872,\n            48,\n            \"0000000050c14b6289103b0a666e76bb178e757f0411947e9461ae82483df756\"\n        ],\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": [\n            2579584,\n            1708968409,\n            284,\n            \"00000000000000120fe076771544efb54ec2ed49c0ad1278bb49f9c9efa51944\"\n        ],\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": [\n            2418761,\n            1675433925,\n            56,\n            \"00000000000037c0576afb47f7f757544ad44710a08c7829dcfc56710d2216d8\"\n        ],\n        \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": [\n            2349377,\n            1664914506,\n            58,\n            \"000000003309f969ede680d3aea4ca8643912b4b4b793172de15a127a790f778\"\n        ],\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": [\n            2406297,\n            1668461654,\n            24,\n            \"000000000000001c4dd10ac70eb8f0af901aec27be9469354f98f15beeebdc03\"\n        ],\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": [\n            1414311,\n            1537898497,\n            44,\n            \"0000000000000046df1be1854387a550e40df1f487218b6b977b7323db986c45\"\n        ],\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": [\n            2425096,\n            1679252503,\n            21,\n            \"000000000000001221ae1721de7f87c9313912720a43b867546e1c93f5d94945\"\n        ],\n        \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\": [\n            1583044,\n            1571148168,\n            286,\n            \"00000000691cd68ef23f24f9bd245aaf85f8dec4abe26d1f9ff97681af9eb222\"\n        ],\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": [\n            1457134,\n            1550165277,\n            52,\n            \"0000000000000057c116d32f1edf128ec4f81a5e734a4095cda3c9ef1d6cb7f7\"\n        ],\n        \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": [\n            1583044,\n            1571148168,\n            191,\n            \"00000000691cd68ef23f24f9bd245aaf85f8dec4abe26d1f9ff97681af9eb222\"\n        ],\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": [\n            2579583,\n            1708967515,\n            479,\n            \"000000000000001f81c50f287422a15099c2bc5cd4464d9962f84e8c1a7b94bb\"\n        ],\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": [\n            2415285,\n            1673173967,\n            16,\n            \"00000000000000105c5d1016f327809add5b052aa5032e34cab26050535143ff\"\n        ],\n        \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": [\n            2349374,\n            1664911683,\n            13,\n            \"0000000000000012fd19ff17ac6890f0911be5791042eb2a6243e3a6b4817e94\"\n        ]\n    },\n    \"wallet_nonce\": 129,\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        344,\n        374,\n        1336,\n        899\n    ]\n}"
  },
  {
    "path": "tests/plugins/test_timelock_recovery.py",
    "content": "from io import StringIO\nimport os\nimport sys\n\nfrom electrum.bitcoin import address_to_script\nfrom electrum.fee_policy import FixedFeePolicy\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.storage import WalletStorage\nfrom electrum.transaction import PartialTxOutput\nfrom electrum.wallet import Wallet\nfrom electrum.wallet_db import WalletDB\n\nfrom electrum.plugins.timelock_recovery.timelock_recovery import TimelockRecoveryContext, TimelockRecoveryPlugin\n\nfrom .. import ElectrumTestCase\n\n\nclass TestTimelockRecovery(ElectrumTestCase):\n    TESTNET = True\n\n    def setUp(self):\n        super(TestTimelockRecovery, self).setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n        self.wallet_path = os.path.join(self.electrum_path, \"timelock_recovery_wallet\")\n\n        self._saved_stdout = sys.stdout\n        self._stdout_buffer = StringIO()\n        sys.stdout = self._stdout_buffer\n\n    def tearDown(self):\n        super(TestTimelockRecovery, self).tearDown()\n        # Restore the \"real\" stdout\n        sys.stdout = self._saved_stdout\n\n    def _create_default_wallet(self):\n        with open(os.path.join(os.path.dirname(__file__), \"test_timelock_recovery\", \"default_wallet\"), \"r\") as f:\n            wallet_str = f.read()\n        storage = WalletStorage(self.wallet_path)\n        db = WalletDB(wallet_str, storage=storage, upgrade=True)\n        wallet = Wallet(db, config=self.config)\n        return wallet\n\n    async def test_get_alert_address(self):\n        wallet = self._create_default_wallet()\n\n        context = TimelockRecoveryContext(wallet)\n        alert_address = context.get_alert_address()\n        self.assertEqual(alert_address, 'tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07')\n\n    async def test_get_cancellation_address(self):\n        wallet = self._create_default_wallet()\n\n        context = TimelockRecoveryContext(wallet)\n        context.get_alert_address()\n        cancellation_address = context.get_cancellation_address()\n        self.assertEqual(cancellation_address, 'tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg')\n\n    async def test_make_unsigned_alert_tx(self):\n        wallet = self._create_default_wallet()\n\n        context = TimelockRecoveryContext(wallet)\n        context.outputs = [\n            PartialTxOutput(scriptpubkey=address_to_script('tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd'), value='!'),\n        ]\n\n        alert_tx = context.make_unsigned_alert_tx(fee_policy=FixedFeePolicy(5000))\n        self.assertEqual(alert_tx.version, 2)\n        alert_tx_inputs = [tx_input.prevout.to_str() for tx_input in alert_tx.inputs()]\n        self.assertEqual(alert_tx_inputs, [\n            '59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2:1',\n            '778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4:1',\n        ])\n        alert_tx_outputs = [(tx_output.address, tx_output.value) for tx_output in alert_tx.outputs()]\n        self.assertEqual(alert_tx_outputs, [\n            ('tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd', 600),\n            ('tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07', 743065),\n        ])\n        self.assertEqual(alert_tx.txid(), '01c227f136c4490ec7cb0fe2ba5e44c436f58906b7fc29a83cb865d7e3bfaa60')\n\n    async def test_make_unsigned_recovery_tx(self):\n        wallet = self._create_default_wallet()\n\n        context = TimelockRecoveryContext(wallet)\n        context.outputs = [\n            PartialTxOutput(scriptpubkey=address_to_script('tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd'), value='!'),\n        ]\n        context.alert_tx = context.make_unsigned_alert_tx(fee_policy=FixedFeePolicy(5000))\n        context.timelock_days = 90\n\n        recovery_tx = context.make_unsigned_recovery_tx(fee_policy=FixedFeePolicy(5000))\n        self.assertEqual(recovery_tx.version, 2)\n        recovery_tx_inputs = [tx_input.prevout.to_str() for tx_input in recovery_tx.inputs()]\n        self.assertEqual(recovery_tx_inputs, [\n            '01c227f136c4490ec7cb0fe2ba5e44c436f58906b7fc29a83cb865d7e3bfaa60:1',\n        ])\n        self.assertEqual(recovery_tx.inputs()[0].nsequence, 0x00403b54)\n\n        recovery_tx_outputs = [(tx_output.address, tx_output.value) for tx_output in recovery_tx.outputs()]\n        self.assertEqual(recovery_tx_outputs, [\n            ('tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd', 738065),\n        ])\n\n    async def test_make_unsigned_cancellation_tx(self):\n        wallet = self._create_default_wallet()\n\n        context = TimelockRecoveryContext(wallet)\n        context.outputs = [\n            PartialTxOutput(scriptpubkey=address_to_script('tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd'), value='!'),\n        ]\n        context.alert_tx = context.make_unsigned_alert_tx(fee_policy=FixedFeePolicy(5000))\n\n        cancellation_tx = context.make_unsigned_cancellation_tx(fee_policy=FixedFeePolicy(6000))\n        self.assertEqual(cancellation_tx.version, 2)\n        cancellation_tx_inputs = [tx_input.prevout.to_str() for tx_input in cancellation_tx.inputs()]\n        self.assertEqual(cancellation_tx_inputs, [\n            '01c227f136c4490ec7cb0fe2ba5e44c436f58906b7fc29a83cb865d7e3bfaa60:1',\n        ])\n        self.assertEqual(cancellation_tx.inputs()[0].nsequence, 0xfffffffd)\n        cancellation_tx_outputs = [(tx_output.address, tx_output.value) for tx_output in cancellation_tx.outputs()]\n        self.assertEqual(cancellation_tx_outputs, [\n            ('tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg', 737065),\n        ])\n\n    def test_checksum_non_ascii(self):\n        # Non-ASCII characters must be serialized as-is (ensure_ascii=False),\n        # not escaped as \\uXXXX sequences, before hashing.\n        json_data = {\"wallet_name\": \"Ωmega Wörld Ñoño 日本語 中文 עברית العربية\", \"id\": \"abc-123\"}\n        result = TimelockRecoveryPlugin.json_checksum(json_data)\n        self.assertEqual(result, \"74674eca\")\n\n    def test_checksum_bip_example(self):\n        # test vector from https://github.com/bitcoin/bips/blob/b3827283792882ed0176a12033944fd63c5d398b/bip-0128.mediawiki#reference-implementation\n        json_data = {\n          \"kind\": \"timelock-recovery-plan\",\n          \"id\": \"exported-692452189b301b561ed57cbe\",\n          \"name\": \"Recovery Plan ac300e72-7612-497e-96b0-df2fdeda59ea\",\n          \"description\": \"RITREK APP 1.1.0: Trezor Account #1\",\n          \"created_at\": \"2025-11-24T12:39:53.532Z\",\n          \"plugin_version\": \"1.0.1\",\n          \"wallet_version\": \"1.0.1\",\n          \"wallet_name\": \"RITREK Service\",\n          \"wallet_kind\": \"RITREK BACKEND\",\n          \"timelock_days\": 2,\n          \"anchor_amount_sats\": 600,\n          \"anchor_addresses\": [\n            \"bc1qnda6x2gxdh3yujd2zjpsd7qzx3awxmlaf9wwlk\"\n          ],\n          \"alert_address\": \"bc1qj0f9sjenwyjs0u7mlgvptjp05z3syzq7mru3ep\",\n          \"alert_inputs\": [\n            \"a265a485df4c6417019b91379257eb387bceeda96f7bb6311794b8ed358cf104:0\",\n            \"2f621c2151f33173983133cbc1000e3b603b8a18423b0379feffe8513171d5d3:0\"\n          ],\n          \"alert_tx\": \"0200000000010204F18C35EDB8941731B67B6FA9EDCE7B38EB579237919B0117644CDF85A465A20000000000FDFFFFFFD3D5713151E8FFFE79033B42188A3B603B0E00C1CB3331987331F351211C622F0000000000FDFFFFFF0258020000000000001600149B7BA329066DE24E49AA148306F802347AE36FFD205600000000000016001493D2584B33712507F3DBFA1815C82FA0A302081E02483045022100DCDBAE77C35EB4A0B3ED0DE5484206AB6B07041BE99B2BBAF0243C125916523C0220396959C3C52B2B1F9E472AEEE7C5D9540531B131C3221DE942754C6D0941397D012103C08FF3ADBA14B742646572BCA6F07AEB910666FB28E4DDDC40E33755E7C869D30248304502210089084472FDA3CF82D6ABC11BF1A5E77C9B423617C8B840F58C02746035B3BA6302203942AA1FA13F952F49FB114D48130A9AAF70151E7D09036D15734DB1F41A8B6001210397064EDED7DAD7D662290DC2847E87C5C27DA8865B89DDB58FDE9A006BA7DB3900000000\",\n          \"alert_txid\": \"f1413fedadaf30697820bcd8f6a393fcc73ea00a15bea3253f89d5658690d2f7\",\n          \"alert_fee\": 231,\n          \"alert_weight\": 834,\n          \"recovery_tx\": \"02000000000101F7D2908665D5893F25A3BE150AA03EC7FC93A3F6D8BC20786930AFADED3F41F101000000005201400001A6550000000000001600149B7BA329066DE24E49AA148306F802347AE36FFD0247304402204AFF87C2127F5697F300C6522067A8D5E5290CA8D140D2E5BCEF4A36606C5FE5022056673BEC5BB459DFFBD4D266EE95AEF0D701383ED80BD433A02C3C486A826D76012102774DBCD59F2D08EFF718BC09972ADC609FBC31C26B551B3E4EA30A1D43EEDB9700000000\",\n          \"recovery_txid\": \"bc304610e8f282036345e87163d4cba5b16488a3bf2e4d738379d7bda3a0bca3\",\n          \"recovery_fee\": 122,\n          \"recovery_weight\": 437,\n          \"recovery_outputs\": [\n            [\n              \"bc1qnda6x2gxdh3yujd2zjpsd7qzx3awxmlaf9wwlk\",\n              21926,\n              \"My Backup Wallet\"\n            ]\n          ],\n          \"metadata\": \"sig:825d6b3858c175c7fc16da3134030e095c4f9089c3c89722247eeedc08a7ef4f\",\n        }\n        result = TimelockRecoveryPlugin.json_checksum(json_data)\n        self.assertEqual(result, \"92f8b3da\")\n"
  },
  {
    "path": "tests/qml/__init__.py",
    "content": ""
  },
  {
    "path": "tests/qml/qt_util.py",
    "content": "import threading\nimport traceback\nimport unittest\nfrom functools import wraps, partial\nfrom unittest import SkipTest\n\nfrom PyQt6.QtCore import QCoreApplication, QMetaObject, Qt, pyqtSlot, QObject\n\n\nclass TestQCoreApplication(QCoreApplication):\n    @pyqtSlot()\n    def doInvoke(self):\n        getattr(self._instance, self._method)()\n\n\nclass QEventReceiver(QObject):\n    def __init__(self, *signals):\n        super().__init__()\n        self.received = []\n        self.signals = []\n        for signal in signals:\n            self.signals.append(signal)\n            signal.connect(partial(self.doReceive, signal))\n\n    # intentionally no pyqtSlot decorator, to catch all\n    def doReceive(self, signal, *args):\n        self.received.append((signal, args))\n\n    def receivedForSignal(self, signal):\n        return list(filter(lambda x: x[0] == signal, self.received))\n\n    def clear(self):\n        self.received.clear()\n\n\nclass QETestCase(unittest.TestCase):\n\n    def setUp(self):\n        super().setUp()\n        self.app = None\n        self._e = None\n        self._testcase_event = threading.Event()\n        self._app_ready_event = threading.Event()\n\n        def start_qt_task():\n            try:\n                assert self.app is None\n                self.app = TestQCoreApplication([])\n                self._app_ready_event.set()\n                self.app.exec()\n                self.app = None\n            except Exception as e:\n                print(f'Problem starting QCoreApplication: {str(e)}')\n\n        self._qt_thread = threading.Thread(target=start_qt_task)\n        self._qt_thread.start()\n\n    def tearDown(self):\n        self.app.exit()\n        if self._qt_thread.is_alive():\n            self._qt_thread.join()\n\n\ndef qt_test(func):\n    @wraps(func)\n    def decorator(self, *args):\n        if threading.current_thread().name == 'MainThread':\n            res = self._app_ready_event.wait(3)\n            if not res:\n                raise Exception('app not ready in time')\n            self._testcase_event.clear()\n            self.app._instance = self\n            self.app._method = func.__name__\n            QMetaObject.invokeMethod(self.app, 'doInvoke', Qt.ConnectionType.QueuedConnection)\n            res = self._testcase_event.wait(15)\n            if not res:\n                self._e = Exception('testcase timed out')\n            if self._e:\n                print(\"\".join(traceback.format_exception(self._e)))\n                # deallocate stored exception from qt thread otherwise we SEGV garbage collector\n                # instead, re-create using the exception message, special casing AssertionError and SkipTest\n                e = None\n                if isinstance(self._e, AssertionError):\n                    e = AssertionError(str(self._e))\n                elif isinstance(self._e, SkipTest):\n                    e = SkipTest(str(self._e))\n                else:\n                    e = Exception(str(self._e))\n                self._e = None\n                raise e\n            return\n        try:\n            func(self, *args)\n        except Exception as e:\n            self._e = e\n        finally:\n            self._testcase_event.set()\n    return decorator\n"
  },
  {
    "path": "tests/qml/test_qml_qeconfig.py",
    "content": "from typing import TYPE_CHECKING\n\nfrom electrum import SimpleConfig\nfrom electrum.gui.qml.qeconfig import QEConfig\n\nfrom .qt_util import QETestCase, qt_test\n\nif TYPE_CHECKING:\n    from PyQt6.QtCore import QRegularExpression\n\n\nclass TestConfig(QETestCase):\n    @classmethod\n    def setUpClass(cls):\n        QEConfig(SimpleConfig())\n\n    def setUp(self):\n        super().setUp()\n        self.q: QEConfig = QEConfig.instance\n        # raise Exception()  # NOTE: exceptions in setUp() will block the test\n\n    @qt_test\n    def test_satstounits(self):\n        self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 5\n        self.assertEqual(self.q.satsToUnits(100_000), 1.0)\n        self.assertEqual(self.q.satsToUnits(1), 0.00001)\n        self.assertEqual(self.q.satsToUnits(0.001), 0.00000001)\n\n    @qt_test\n    def test_unitstosats(self):\n        qa = self.q.unitsToSats('')\n        self.assertTrue(qa.isEmpty)\n        qa = self.q.unitsToSats('0')\n        self.assertTrue(qa.isEmpty)\n        qa = self.q.unitsToSats('0.000')\n        self.assertTrue(qa.isEmpty)\n\n        self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 5\n\n        qa = self.q.unitsToSats('1')\n        self.assertFalse(qa.isEmpty)\n        self.assertEqual(qa.satsInt, 100_000)\n        self.assertEqual(qa.msatsInt, 100_000_000)\n\n        qa = self.q.unitsToSats('1.001')\n        self.assertFalse(qa.isEmpty)\n        self.assertEqual(qa.satsInt, 100_100)\n        self.assertEqual(qa.msatsInt, 100_100_000)\n\n        qa = self.q.unitsToSats('1.000001')\n        self.assertFalse(qa.isEmpty)\n        self.assertEqual(qa.satsInt, 100_000)\n        self.assertEqual(qa.msatsInt, 100_000_100)\n\n        self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 0\n\n        qa = self.q.unitsToSats('1.001')\n        self.assertFalse(qa.isEmpty)\n        self.assertEqual(qa.satsInt, 1)\n        self.assertEqual(qa.msatsInt, 1001)\n\n        qa = self.q.unitsToSats('1.0001')  # outside msat precision\n        self.assertFalse(qa.isEmpty)\n        self.assertEqual(qa.satsInt, 1)\n        self.assertEqual(qa.msatsInt, 1000)\n\n        self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 8\n\n        qa = self.q.unitsToSats('0.00000001001')\n        self.assertFalse(qa.isEmpty)\n        self.assertEqual(qa.satsInt, 1)\n        self.assertEqual(qa.msatsInt, 1001)\n\n    @qt_test\n    def test_btc_amount_regexes(self):\n        self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 8\n\n        a: 'QRegularExpression' = self.q.btcAmountRegex\n        b: 'QRegularExpression' = self.q.btcAmountRegexMsat\n\n        self.assertTrue(a.isValid())\n        self.assertTrue(b.isValid())\n\n        self.assertTrue(a.match('1').hasMatch())\n        self.assertTrue(a.match('1.').hasMatch())\n        self.assertTrue(a.match('1.00000000').hasMatch())\n        self.assertFalse(a.match('1.000000000').hasMatch())\n        self.assertTrue(a.match('21000000').hasMatch())\n        self.assertFalse(a.match('121000000').hasMatch())\n\n        self.assertTrue(b.match('1').hasMatch())\n        self.assertTrue(b.match('1.').hasMatch())\n        self.assertTrue(b.match('1.00000000').hasMatch())\n        self.assertTrue(b.match('1.00000000000').hasMatch())\n        self.assertFalse(b.match('1.000000000000').hasMatch())\n        self.assertTrue(b.match('21000000').hasMatch())\n        self.assertFalse(b.match('121000000').hasMatch())\n\n        self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 5\n\n        a: 'QRegularExpression' = self.q.btcAmountRegex\n        b: 'QRegularExpression' = self.q.btcAmountRegexMsat\n\n        self.assertTrue(a.isValid())\n        self.assertTrue(b.isValid())\n\n        self.assertTrue(a.match('1').hasMatch())\n        self.assertTrue(a.match('1.').hasMatch())\n        self.assertTrue(a.match('1.00000').hasMatch())\n        self.assertFalse(a.match('1.000000').hasMatch())\n        self.assertTrue(a.match('21000000000').hasMatch())\n        self.assertFalse(a.match('121000000000').hasMatch())\n\n        self.assertTrue(b.match('1').hasMatch())\n        self.assertTrue(b.match('1.').hasMatch())\n        self.assertTrue(b.match('1.0000000').hasMatch())\n        self.assertTrue(b.match('1.00000000').hasMatch())\n        self.assertFalse(b.match('1.000000000000').hasMatch())\n        self.assertTrue(b.match('21000000000').hasMatch())\n        self.assertFalse(b.match('121000000000').hasMatch())\n\n        self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 0\n\n        a: 'QRegularExpression' = self.q.btcAmountRegex\n        b: 'QRegularExpression' = self.q.btcAmountRegexMsat\n\n        self.assertTrue(a.isValid())\n        self.assertTrue(b.isValid())\n\n        self.assertTrue(a.match('1').hasMatch())\n        self.assertFalse(a.match('1.').hasMatch())\n        self.assertTrue(a.match('2100000000000000').hasMatch())\n        self.assertFalse(a.match('12100000000000000').hasMatch())\n\n        self.assertTrue(b.match('1').hasMatch())\n        self.assertTrue(b.match('1.').hasMatch())\n        self.assertTrue(b.match('1.000').hasMatch())\n        self.assertFalse(b.match('1.0000').hasMatch())\n        self.assertTrue(b.match('2100000000000000').hasMatch())\n        self.assertFalse(b.match('12100000000000000').hasMatch())\n"
  },
  {
    "path": "tests/qml/test_qml_qetransactionlistmodel.py",
    "content": "from datetime import datetime\nfrom unittest.mock import patch\n\nfrom electrum.gui.qml.qetransactionlistmodel import QETransactionListModel\n\nfrom .. import ElectrumTestCase\n\n\nclass TestQETransactionListModel(ElectrumTestCase):\n\n    def test_get_section_by_timestamp(self):\n        f = QETransactionListModel.get_section_by_timestamp\n\n        mock_today = datetime(2023, 6, 15, 0, 0, 0, 0)\n        with patch('electrum.gui.qml.qetransactionlistmodel.datetime') as mock_dt:\n            mock_dt.today.return_value = mock_today\n            mock_dt.fromtimestamp = datetime.fromtimestamp\n\n            today_ts = datetime(2023, 6, 15, 10, 30, 0).timestamp()\n            self.assertEqual(f(today_ts), 'today')\n\n            today_edge_ts = datetime(2023, 6, 15, 0, 0, 1).timestamp()\n            self.assertEqual(f(today_edge_ts), 'today')\n\n            yesterday_ts = datetime(2023, 6, 14, 15, 0, 0).timestamp()\n            self.assertEqual(f(yesterday_ts), 'yesterday')\n\n            yesterday_edge_ts = datetime(2023, 6, 13, 23, 59, 59).timestamp()\n            self.assertEqual(f(yesterday_edge_ts), 'lastweek')\n\n            lastweek_ts = datetime(2023, 6, 12, 12, 0, 0).timestamp()\n            self.assertEqual(f(lastweek_ts), 'lastweek')\n\n            lastweek_boundary_ts = datetime(2023, 6, 8, 12, 0, 0).timestamp()\n            self.assertEqual(f(lastweek_boundary_ts), 'lastweek')\n\n            lastmonth_ts = datetime(2023, 6, 5, 9, 0, 0).timestamp()\n            self.assertEqual(f(lastmonth_ts), 'lastmonth')\n\n            lastmonth_boundary_ts = datetime(2023, 5, 15, 8, 0, 0).timestamp()\n            self.assertEqual(f(lastmonth_boundary_ts), 'lastmonth')\n\n            older_ts = datetime(2023, 5, 14, 10, 0, 0).timestamp()\n            self.assertEqual(f(older_ts), 'older')\n\n            much_older_ts = datetime(2022, 1, 1, 0, 0, 0).timestamp()\n            self.assertEqual(f(much_older_ts), 'older')\n\n    def test_format_date_by_section(self):\n        f = QETransactionListModel.format_date_by_section\n\n        test_date = datetime(2023, 6, 15, 14, 30, 45)\n\n        result = f('today', test_date)\n        self.assertEqual(result, '14:30')\n\n        result = f('yesterday', test_date)\n        self.assertEqual(result, '14:30')\n\n        result = f('lastweek', test_date)\n        self.assertEqual(result, 'Thu, 14:30')\n\n        result = f('lastmonth', test_date)\n        self.assertEqual(result, 'Thu 15, 14:30')\n\n        result = f('older', test_date)\n        self.assertEqual(result, '2023-06-15 14:30')\n\n        result = f('unknown_section', test_date)\n        self.assertEqual(result, '2023-06-15 14:30')\n\n"
  },
  {
    "path": "tests/qml/test_qml_types.py",
    "content": "import shutil\nimport tempfile\n\nfrom electrum import SimpleConfig\nfrom electrum.gui.qml.qetypes import QEAmount\nfrom electrum.invoices import Invoice, LN_EXPIRY_NEVER\nfrom electrum.transaction import PartialTxOutput\n\nfrom .qt_util import QETestCase, QEventReceiver, qt_test\n\n\nclass WalletMock:\n    def __init__(self, electrum_path):\n        self.config = SimpleConfig({\n            'electrum_path': electrum_path,\n            'decimal_point': 5\n        })\n        self.contacts = None\n\n\nclass TestTypes(QETestCase):\n\n    def setUp(self):\n        super().setUp()\n        self.electrum_path = tempfile.mkdtemp()\n        self.wallet = WalletMock(self.electrum_path)\n\n    def tearDown(self):\n        super().tearDown()\n        shutil.rmtree(self.electrum_path)\n\n    @qt_test\n    def test_qeamount(self):\n        a = QEAmount()\n        self.assertTrue(a.isEmpty)\n        a_er = QEventReceiver(a.valueChanged)\n        a.satsInt = 1\n        self.assertTrue(bool(a_er.received))\n        self.assertFalse(a.isEmpty)\n        self.assertEqual('1', a.satsStr)\n\n        a_er.clear()\n        a.clear()\n        self.assertTrue(a.isEmpty)\n        self.assertTrue(bool(a_er.received))\n        self.assertEqual('0', a.satsStr)\n\n        a.clear()\n        a_er.clear()\n        a.isMax = True\n        self.assertTrue(a.isMax)\n        self.assertFalse(a.isEmpty)\n        self.assertTrue(bool(a_er.received))\n        self.assertEqual('0', a.satsStr)\n\n        a.clear()\n        a_er.clear()\n        a.msatsInt = 1\n        self.assertTrue(bool(a_er.received))\n        self.assertFalse(a.isEmpty)\n        self.assertEqual('1', a.msatsStr)\n\n    @qt_test\n    def test_qeamount_copy(self):\n        a = QEAmount()\n        b = QEAmount()\n        b.satsInt = 1\n        c = QEAmount()\n        c.msatsInt = 1\n        d = QEAmount()\n        d.isMax = True\n\n        t = QEAmount()\n        t_er = QEventReceiver(t.valueChanged)\n\n        t.copyFrom(a)\n        self.assertTrue(t.isEmpty)\n        self.assertEqual(0, len(t_er.received))\n\n        t.clear()\n        t_er.clear()\n        t.copyFrom(b)\n        self.assertFalse(t.isEmpty)\n        self.assertEqual(t.satsInt, 1)\n        self.assertEqual(1, len(t_er.received))\n\n        t.clear()\n        t_er.clear()\n        t.copyFrom(c)\n        self.assertFalse(t.isEmpty)\n        self.assertEqual(t.msatsInt, 1)\n        self.assertEqual(1, len(t_er.received))\n\n        t.clear()\n        t_er.clear()\n        t.copyFrom(d)\n        self.assertFalse(t.isEmpty)\n        self.assertTrue(t.isMax)\n        self.assertEqual(1, len(t_er.received))\n\n    @qt_test\n    def test_qeamount_frominvoice(self):\n        amount_sat = 10_000\n        outputs = [PartialTxOutput.from_address_and_value('bc1qj3zx2zc4rpv3npzmznxhdxzn0wm7pzqp8p2293', amount_sat)]\n        invoice = Invoice(\n            amount_msat=amount_sat * 1000,\n            message=\"mymsg\",\n            time=1692716965,\n            exp=LN_EXPIRY_NEVER,\n            outputs=outputs,\n            bip70=None,\n            height=0,\n            lightning_invoice=None,\n        )\n        a = QEAmount(from_invoice=invoice)\n        self.assertEqual(10_000, a.satsInt)\n        self.assertEqual(10_000_000, a.msatsInt)\n        self.assertFalse(a.isMax)\n\n        outputs = [PartialTxOutput.from_address_and_value('bc1qj3zx2zc4rpv3npzmznxhdxzn0wm7pzqp8p2293', '!')]\n        invoice = Invoice(\n            amount_msat='!',\n            message=\"mymsg\",\n            time=1692716965,\n            exp=LN_EXPIRY_NEVER,\n            outputs=outputs,\n            bip70=None,\n            height=0,\n            lightning_invoice=None,\n        )\n        a = QEAmount(from_invoice=invoice)\n        self.assertTrue(a.isMax)\n        self.assertEqual(0, a.satsInt)\n        self.assertEqual(0, a.msatsInt)\n\n        bolt11 = 'lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj'\n        invoice = Invoice.from_bech32(bolt11)\n        a = QEAmount(from_invoice=invoice)\n        self.assertEqual(2_000_000, a.satsInt)\n        self.assertEqual(2_000_000_000, a.msatsInt)\n        self.assertFalse(a.isMax)\n"
  },
  {
    "path": "tests/regtest/regtest.sh",
    "content": "#!/usr/bin/env bash\nexport HOME=~\nset -eu\n\nTEST_ANCHOR_CHANNELS=True\n\n# alice -> bob -> carol\n\nalice=\"./run_electrum --regtest -D /tmp/alice\"\nbob=\"./run_electrum --regtest -D /tmp/bob\"\ncarol=\"./run_electrum --regtest -D /tmp/carol\"\n\nbitcoin_cli=\"bitcoin-cli -rpcuser=doggman -rpcpassword=donkey -rpcport=18554 -regtest\"\n\nfunction new_blocks()\n{\n    printf \"mining $1 blocks\\n\"\n    $bitcoin_cli generatetoaddress $1 $($bitcoin_cli getnewaddress) > /dev/null\n}\n\nfunction wait_until_htlcs_settled()\n{\n    msg=\"wait until $1's local_unsettled_sent is zero\"\n    cmd=\"./run_electrum --regtest -D /tmp/$1\"\n    declare -i timeout_sec=30\n    declare -i elapsed_sec=0\n\n    while unsettled=$($cmd list_channels | jq '.[] | .local_unsettled_sent') && [ $unsettled != \"0\" ]; do\n        if ((elapsed_sec > timeout_sec)); then\n            printf \"Timeout of %i s exceeded\\n\" \"$elapsed_sec\"\n            exit 1\n        fi\n\n        sleep 1\n        elapsed_sec=$((elapsed_sec + 1))\n        msg=\"$msg.\"\n        printf \"$msg\\r\"\n    done\n    printf \"\\n\"\n}\n\n\nfunction wait_for_balance()\n{\n    msg=\"wait until $1's balance reaches $2\"\n    cmd=\"./run_electrum --regtest -D /tmp/$1\"\n    declare -i timeout_sec=30\n    declare -i elapsed_sec=0\n\n    while balance=$($cmd getbalance | jq '[.confirmed, .unconfirmed] | to_entries | map(select(.value != null).value) | map(tonumber) | add ') && (( $(echo \"$balance < $2\" | bc -l) )); do\n        if ((elapsed_sec > timeout_sec)); then\n            printf \"Timeout of %i s exceeded\\n\" \"$elapsed_sec\"\n            exit 1\n        fi\n\n        sleep 1\n        elapsed_sec=$((elapsed_sec + 1))\n        msg=\"$msg.\"\n        printf \"$msg\\r\"\n    done\n    printf \"\\n\"\n}\n\nfunction wait_until_channel_open()\n{\n    msg=\"wait until $1 sees channel open\"\n    cmd=\"./run_electrum --regtest -D /tmp/$1\"\n    declare -i timeout_sec=30\n    declare -i elapsed_sec=0\n\n    while channel_state=$($cmd list_channels | jq '.[0] | .state' | tr -d '\"') && [ $channel_state != \"OPEN\" ]; do\n        if ((elapsed_sec > timeout_sec)); then\n            printf \"Timeout of %i s exceeded\\n\" \"$elapsed_sec\"\n            exit 1\n        fi\n\n        sleep 1\n        elapsed_sec=$((elapsed_sec + 1))\n        msg=\"$msg.\"\n        printf \"$msg\\r\"\n    done\n    printf \"\\n\"\n}\n\nfunction wait_until_channel_closed()\n{\n    msg=\"wait until $1 sees channel closed\"\n    cmd=\"./run_electrum --regtest -D /tmp/$1\"\n    declare -i timeout_sec=30\n    declare -i elapsed_sec=0\n\n    while [[ $($cmd list_channels | jq '.[0].state' | tr -d '\"') != \"CLOSED\" ]]; do\n        if ((elapsed_sec > timeout_sec)); then\n            printf \"Timeout of %i s exceeded\\n\" \"$elapsed_sec\"\n            exit 1\n        fi\n\n        sleep 1\n        elapsed_sec=$((elapsed_sec + 1))\n        msg=\"$msg.\"\n        printf \"$msg\\r\"\n    done\n    printf \"\\n\"\n}\n\nfunction wait_until_preimage()\n{\n    msg=\"wait until $1 has preimage for $2\"\n    cmd=\"./run_electrum --regtest -D /tmp/$1\"\n    declare -i timeout_sec=30\n    declare -i elapsed_sec=0\n\n    while [[ $($cmd get_invoice $2 | jq '.preimage' | tr -d '\"') == \"null\" ]]; do\n        if ((elapsed_sec > timeout_sec)); then\n            printf \"Timeout of %i s exceeded\\n\" \"$elapsed_sec\"\n            exit 1\n        fi\n\n        sleep 1\n        elapsed_sec=$((elapsed_sec + 1))\n        msg=\"$msg.\"\n        printf \"$msg\\r\"\n    done\n    printf \"\\n\"\n}\n\nfunction wait_until_spent()\n{\n    msg=\"wait until $1:$2 is spent\"\n    declare -i timeout_sec=30\n    declare -i elapsed_sec=0\n\n    while [[ $($bitcoin_cli gettxout $1 $2) ]]; do\n        if ((elapsed_sec > timeout_sec)); then\n            printf \"Timeout of %i s exceeded\\n\" \"$elapsed_sec\"\n            exit 1\n        fi\n\n        sleep 1\n        elapsed_sec=$((elapsed_sec + 1))\n        msg=\"$msg.\"\n        printf \"$msg\\r\"\n    done\n    printf \"\\n\"\n}\n\nfunction assert_utxo_exists()\n{\n    utxo=$($bitcoin_cli gettxout $1 $2)\n    if [[ -z \"$utxo\" ]]; then\n        echo \"utxo $1:$2 does not exist\"\n        exit 1\n    fi\n}\n\nif [[ $# -eq 0 ]]; then\n    echo \"syntax: init|start|open|status|pay|close|stop\"\n    exit 1\nfi\n\nif [[ $1 == \"new_block\" ]]; then\n    new_blocks 1\nfi\n\nif [[ $1 == \"init\" ]]; then\n    echo \"initializing $2\"\n    rm -rf /tmp/$2/\n    agent=\"./run_electrum --regtest -D /tmp/$2\"\n    $agent create --offline > /dev/null\n    $agent setconfig --offline enable_anchor_channels $TEST_ANCHOR_CHANNELS\n    $agent setconfig --offline log_to_file True\n    $agent setconfig --offline use_gossip True\n    $agent setconfig --offline server 127.0.0.1:51001:t\n    $agent setconfig --offline lightning_to_self_delay 144\n    $agent setconfig --offline test_force_disable_mpp True\n    echo \"funding $2\"\n    # note: changing the funding amount affects all tests, as they rely on \"wait_for_balance\"\n    $bitcoin_cli sendtoaddress $($agent getunusedaddress -o -w \"/tmp/$2/regtest/wallets/default_wallet\") 1\nfi\n\nif [[ $1 == \"setconfig\" ]]; then\n    # use this to set config vars that need to be set before the daemon is started\n    agent=\"./run_electrum --regtest -D /tmp/$2\"\n    $agent setconfig --offline $3 $4\nfi\n\n# start daemons. Bob is started first because he is listening\nif [[ $1 == \"start\" ]]; then\n    agent=\"./run_electrum --regtest -D /tmp/$2\"\n    $agent daemon -d\n    $agent load_wallet\n    $agent wait_for_sync\nfi\n\nif [[ $1 == \"stop\" ]]; then\n    agent=\"./run_electrum --regtest -D /tmp/$2\"\n    $agent stop || true\nfi\n\n\n# alice sends two payments, then broadcast ctx after first payment.\n# thus, bob needs to redeem both to_local and to_remote\n\n\nif [[ $1 == \"breach\" ]]; then\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    channel=$($alice open_channel $bob_node 0.15 --password='')\n    new_blocks 3\n    wait_until_channel_open alice\n    request=$($bob add_request 0.01 --lightning | jq -r \".lightning_invoice\")\n    echo \"alice pays\"\n    $alice lnpay $request\n    sleep 2\n    ctx=$($alice get_channel_ctx $channel --iknowwhatimdoing)\n    request=$($bob add_request 0.01 --lightning | jq -r \".lightning_invoice\")\n    echo \"alice pays again\"\n    $alice lnpay $request\n    echo \"alice broadcasts old ctx\"\n    $bitcoin_cli sendrawtransaction $ctx\n    new_blocks 1\n    wait_until_channel_closed bob\n    new_blocks 1\n    wait_for_balance bob 1.14\n    $bob getbalance\nfi\n\n\nif [[ $1 == \"backup\" ]]; then\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    channel1=$($alice open_channel $bob_node 0.15 --password='')\n    new_blocks 1  # cannot open multiple chans with same node in same block\n    $alice setconfig use_recoverable_channels False\n    channel2=$($alice open_channel $bob_node 0.15 --password='')\n    new_blocks 3\n    wait_until_channel_open alice\n    backup=$($alice export_channel_backup $channel2)\n    seed=$($alice getseed --password='')\n    $alice stop\n    mv /tmp/alice/regtest/wallets/default_wallet /tmp/alice/regtest/wallets/default_wallet.old\n    $alice -o restore \"$seed\"\n    $alice daemon -d\n    $alice load_wallet\n    $alice import_channel_backup $backup\n    $alice wait_for_sync\n    echo \"request force close $channel1\"\n    $alice request_force_close $channel1\n    echo \"request force close $channel2\"\n    $alice request_force_close $channel2\n    new_blocks 1\n    wait_for_balance alice 0.997\nfi\n\n\nif [[ $1 == \"backup_local_forceclose\" ]]; then\n    # Alice does a local-force-close, and then restores from seed before sweeping CSV-locked coins\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    $alice setconfig use_recoverable_channels False\n    channel=$($alice open_channel $bob_node 0.15 --password='')\n    new_blocks 3\n    wait_until_channel_open alice\n    backup=$($alice export_channel_backup $channel)\n    echo \"local force close $channel\"\n    $alice close_channel $channel --force\n    sleep 0.5\n    seed=$($alice getseed --password='')\n    $alice stop\n    mv /tmp/alice/regtest/wallets/default_wallet /tmp/alice/regtest/wallets/default_wallet.old\n    new_blocks 150\n    $alice -o restore \"$seed\"\n    $alice daemon -d\n    $alice load_wallet\n    $alice import_channel_backup $backup\n    wait_for_balance alice 0.998\nfi\n\n\nif [[ $1 == \"collaborative_close\" ]]; then\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    channel=$($alice open_channel $bob_node 0.15 --password='')\n    new_blocks 3\n    wait_until_channel_open alice\n    echo \"alice closes channel\"\n    request=$($bob close_channel $channel)\nfi\n\n\nif [[ $1 == \"swapserver_success\" ]]; then\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    channel=$($alice open_channel $bob_node 0.15 --password='')\n    new_blocks 3\n    wait_until_channel_open alice\n    echo \"alice initiates swap\"\n    dryrun=$($alice reverse_swap 0.02 dryrun)\n    onchain_amount=$(echo $dryrun| jq -r \".onchain_amount\")\n    prepayment=$(echo $dryrun| jq -r \".prepayment\")\n    swap=$($alice reverse_swap 0.02 $onchain_amount --prepayment $prepayment)\n    echo $swap | jq\n    funding_txid=$(echo $swap| jq -r \".funding_txid\")\n    new_blocks 1\n    wait_until_spent $funding_txid 0\n    wait_until_htlcs_settled alice\nfi\n\n\nif [[ $1 == \"swapserver_forceclose\" ]]; then\n    # Alice starts reverse-swap with Bob.\n    # Alice sends hold-HTLCs via LN, Bob funds locking script onchain.\n    # Bob force-closes the channel, before swap-funding-tx gets mined.\n    # After swap-funding-tx gets mined, Alice broadcasts onchain claim tx, revealing preimage.\n    # Bob finds preimage onchain, and creates HTLC-success tx to spend own ctx htlc output onchain.\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    channel=$($alice open_channel $bob_node 0.15 --password='')\n    new_blocks 3\n    wait_until_channel_open alice\n    echo \"alice initiates swap\"\n    dryrun=$($alice reverse_swap 0.02 dryrun)\n    onchain_amount=$(echo $dryrun| jq -r \".onchain_amount\")\n    prepayment=$(echo $dryrun| jq -r \".prepayment\")\n    swap=$($alice reverse_swap 0.02 $onchain_amount --prepayment $prepayment)\n    echo $swap | jq\n    funding_txid=$(echo $swap| jq -r \".funding_txid\")\n    ctx_id=$($bob close_channel --force $channel)\n    new_blocks 1\n    wait_until_spent $funding_txid 0 # alice reveals preimage\n    new_blocks 1\n    if [ $TEST_ANCHOR_CHANNELS = True ] ; then\n        output_index=3  # received_htlc_output in bob's ctx. FIXME index depends on Alice not using MPP\n    else\n        output_index=1\n    fi\n    # wait until Bob finds preimage onchain and uses it to create an htlc_success tx\n    wait_until_spent $ctx_id $output_index\n    new_blocks 144\n    wait_for_balance bob 0.999\n    # check that the closing tx is in alice's onchain_history. Since this tx does not\n    # touch alice's wallet addresses, this test requires accounting_addresses to be set\n    $alice stop\n    if [[ ! $($alice -o onchain_history| jq --arg txid $ctx_id '.[]|select(.txid == $txid)') ]]; then\n       echo \"accounting_address not set\"\n       exit 1\n    fi\nfi\n\n\nif [[ $1 == \"swapserver_refund\" ]]; then\n    # Alice starts reverse-swap with Bob.\n    # Alice sends hold-HTLCs via LN, Bob funds locking script onchain.\n    # Alice never broadcasts onchain claim tx. Bob will use timeout path onchain.\n    # Then Bob fails hold-HTLCs via LN.\n    # Channel stays open.\n    $alice setconfig test_swapserver_refund true\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    channel=$($alice open_channel $bob_node 0.15 --password='')\n    new_blocks 3\n    wait_until_channel_open alice\n    echo \"alice initiates swap\"\n    dryrun=$($alice reverse_swap 0.02 dryrun)\n    onchain_amount=$(echo $dryrun| jq -r \".onchain_amount\")\n    prepayment=$(echo $dryrun| jq -r \".prepayment\")\n    swap=$($alice reverse_swap 0.02 $onchain_amount --prepayment $prepayment)\n    echo $swap | jq\n    funding_txid=$(echo $swap| jq -r \".funding_txid\")\n    new_blocks 140\n    wait_until_spent $funding_txid 0\n    new_blocks 1\n    wait_until_htlcs_settled alice\nfi\n\n\nif [[ $1 == \"lnwatcher_waits_until_fees_go_down\" ]]; then\n    # Alice sends two HTLCs to Bob (one for small invoice, one for large invoice), which Bob will hold.\n    # Alice requests Bob to force-close the channel, while the HTLCs are pending. Bob force-closes.\n    # Fee levels rise, to the point where the small HTLC is not economical to claim.\n    #                  Alice sweeps the large HTLC (via onchain timeout), but not the small one.\n    # Then, fee levels go back down, and Alice sweeps the small HTLC.\n    # This test checks Alice does not abandon channel outputs that are temporarily ~dust due to\n    # mempool spikes, and keeps watching the channel in hope of fees going down.\n    $alice setconfig test_force_disable_mpp true\n    $alice setconfig test_force_mpp false\n    wait_for_balance alice 1\n    $alice setconfig test_disable_automatic_fee_eta_update true\n    $alice test_inject_fee_etas \"{2:1000}\"\n    $bob test_inject_fee_etas \"{2:1000}\"\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    channel=$($alice open_channel $bob_node 0.15 --password='')\n    chan_funding_txid=$(echo \"$channel\" | cut -d \":\" -f 1)\n    chan_funding_outidx=$(echo \"$channel\" | cut -d \":\" -f 2)\n    new_blocks 3\n    wait_until_channel_open alice\n    # Alice sends an HTLC to Bob, which Bob will hold indefinitely. Alice's lnpay will time out.\n    invoice1=$($bob add_hold_invoice deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1 \\\n                    --amount 0.0004 --min_final_cltv_expiry_delta 300 | jq -r \".invoice\")\n    invoice2=$($bob add_hold_invoice deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee2 \\\n                    --amount 0.04 --min_final_cltv_expiry_delta 300 | jq -r \".invoice\")\n    set +e\n    $alice lnpay $invoice1 --timeout 3\n    $alice lnpay $invoice2 --timeout 3\n    set -e\n    # After a while, Alice gets impatient and gets Bob to close the channel.\n    new_blocks 20\n    $alice request_force_close $channel\n    wait_until_spent $chan_funding_txid $chan_funding_outidx\n    $bob stop  # bob closes and then disappears. FIXME this is a hack to prevent Bob claiming the fake-hold-invoice-htlc onchain\n    new_blocks 1\n    wait_until_channel_closed alice\n    ctx_id=$($alice list_channels | jq -r \".[0].closing_txid\")\n    if [ $TEST_ANCHOR_CHANNELS = True ] ; then\n        htlc_output_index1=2\n        htlc_output_index2=3\n        to_alice_index=4  # Bob's to_remote\n        wait_until_spent $ctx_id $to_alice_index\n    else\n        htlc_output_index1=0\n        htlc_output_index2=1\n        to_alice_index=2\n    fi\n    new_blocks 1\n    assert_utxo_exists $ctx_id $htlc_output_index1\n    assert_utxo_exists $ctx_id $htlc_output_index2\n    # fee levels rise. now small htlc is ~dust\n    $alice test_inject_fee_etas \"{2:300000}\"\n    new_blocks 300  # this goes past the CLTV of the HTLC-output in ctx\n    wait_until_spent $ctx_id $htlc_output_index2\n    assert_utxo_exists $ctx_id $htlc_output_index1\n    new_blocks 24  # note: >20 blocks depth is considered \"DEEP\" by lnwatcher\n    sleep 1  # give time for Alice to make mistakes, such as abandoning the channel. which it should NOT do.\n    new_blocks 1\n    # Alice goes offline and comes back later, 1\n    $alice stop\n    $alice daemon -d\n    $alice test_inject_fee_etas \"{2:300000}\"\n    $alice load_wallet\n    $alice wait_for_sync\n    new_blocks 1\n    sleep 1  # give time for Alice to make mistakes\n    # Alice goes offline and comes back later, 2\n    $alice stop\n    $alice daemon -d\n    $alice test_inject_fee_etas \"{2:300000}\"\n    $alice load_wallet\n    $alice wait_for_sync\n    new_blocks 1\n    sleep 1  # give time for Alice to make mistakes\n    # fee levels go down. time to claim the small htlc\n    $alice test_inject_fee_etas \"{2:1000}\"\n    new_blocks 1\n    wait_until_spent $ctx_id $htlc_output_index1\n    new_blocks 1\n    wait_for_balance alice 0.9995\nfi\n\n\nif [[ $1 == \"extract_preimage\" ]]; then\n    # Alice sends htlc1 to Bob.  Bob sends htlc2 to Alice.\n    # Neither one of them settles, they hold the htlcs, and Bob force-closes.\n    # Bob's ctx contains two htlc outputs: \"received\" htlc1, and \"offered\" htlc2.\n    # Bob also broadcasts an HTLC-success tx for received htlc1, revealing the preimage.\n    # Alice broadcasts a direct-spend of the offered htlc2, revealing the preimage.\n    # This test checks that\n    # - Alice successfully extracts the preimage for htlc1 from Bob's HTLC-success tx, and\n    # - Bob successfully extracts the preimage for htlc2 from Alice's direct spend tx\n    # note: actually, due to MPP, there will be more htlcs in the ctx:\n    #       we force alice to use MPP, but force bob NOT to use MPP\n    $alice setconfig test_force_disable_mpp false\n    $alice setconfig test_force_mpp true\n    $bob setconfig test_force_disable_mpp true\n    $bob setconfig test_force_mpp false\n    $alice enable_htlc_settle false\n    $bob enable_htlc_settle false\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    $alice open_channel $bob_node 0.15 --password='' --push_amount=0.075\n    new_blocks 3\n    wait_until_channel_open alice\n    chan_id=$($alice list_channels | jq -r \".[0].channel_point\")\n    # alice pays bob\n    request1=$($bob add_request 0.04 --lightning --memo \"test1\")\n    invoice1=$(echo $request1 | jq -r \".lightning_invoice\")\n    rhash1=$(echo $request1 | jq -r \".rhash\")\n    screen -S alice_payment -dm -L -Logfile /tmp/alice/screen1.log $alice lnpay $invoice1 --timeout=600\n    sleep 1\n    unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')\n    if [[ \"$unsettled\" == \"0\" ]]; then\n        echo 'enable_htlc_settle did not work (bob settled)'\n        exit 1\n    fi\n    # bob pays alice\n    request2=$($alice add_request 0.04 --lightning --memo \"test2\")\n    invoice2=$(echo $request2 | jq -r \".lightning_invoice\")\n    rhash2=$(echo $request2 | jq -r \".rhash\")\n    screen -S bob_payment -dm -L -Logfile /tmp/bob/screen2.log $bob lnpay $invoice2 --timeout=600\n    sleep 1\n    unsettled=$($bob list_channels | jq '.[] | .local_unsettled_sent')\n    if [[ \"$unsettled\" == \"0\" ]]; then\n        echo 'enable_htlc_settle did not work (alice settled)'\n        exit 1\n    fi\n    # bob force closes\n    $bob close_channel $chan_id --force\n    new_blocks 1\n    wait_until_preimage alice $rhash1\n    wait_until_preimage bob $rhash2\n    # check both \"lnpay\" commands succeeded\n    success=$(cat /tmp/alice/screen1.log | jq -r \".success\")\n    if [[ \"$success\" != \"true\" ]]; then echo \"alice payment failed\"; exit 1; fi\n    success=$(cat /tmp/bob/screen2.log | jq -r \".success\")\n    if [[ \"$success\" != \"true\" ]]; then echo \"bob payment failed\"; exit 1; fi\n    cat /tmp/alice/screen1.log\n    cat /tmp/bob/screen2.log\nfi\n\n\nif [[ $1 == \"redeem_offered_htlcs\" ]]; then\n    # alice force closes and redeems using htlc timeout\n    $bob enable_htlc_settle false\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    $alice open_channel $bob_node 0.15 --password=''\n    new_blocks 3\n    wait_until_channel_open alice\n    # alice pays bob\n    invoice=$($bob add_request 0.04 --lightning --memo \"test\" | jq -r \".lightning_invoice\")\n    $alice lnpay $invoice --timeout=1 || true\n    unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')\n    if [[ \"$unsettled\" == \"0\" ]]; then\n        echo 'enable_htlc_settle did not work'\n        exit 1\n    fi\n    # bob goes away\n    $bob stop\n    echo \"alice balance before closing channel:\" $($alice getbalance)\n    balance_before=$($alice getbalance | jq '[.confirmed, .unconfirmed, .lightning] | to_entries | map(select(.value != null).value) | map(tonumber) | add ')\n    # alice force closes the channel\n    chan_id=$($alice list_channels | jq -r \".[0].channel_point\")\n    $alice close_channel $chan_id --force\n    new_blocks 1\n    sleep 3\n    echo \"alice balance after closing channel:\" $($alice getbalance)\n    new_blocks 150\n    sleep 10\n    new_blocks 1\n    sleep 3\n    echo \"alice balance after CLTV\" $($alice getbalance)\n    new_blocks 150\n    sleep 10\n    new_blocks 1\n    sleep 3\n    echo \"alice balance after CSV\" $($alice getbalance)\n    # fixme: add local to getbalance\n    wait_for_balance alice $(echo \"$balance_before - 0.02\" | bc -l)\n    $alice getbalance\nfi\n\n\nif [[ $1 == \"redeem_received_htlcs\" ]]; then\n    # bob force closes and redeems with the preimage\n    $bob enable_htlc_settle false\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    $alice open_channel $bob_node 0.15 --password=''\n    new_blocks 3\n    wait_until_channel_open alice\n    # alice pays bob\n    invoice=$($bob add_request 0.04 --lightning --memo \"test\" | jq -r \".lightning_invoice\")\n    $alice lnpay $invoice --timeout=1 || true\n    unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')\n    if [[ \"$unsettled\" == \"0\" ]]; then\n        echo 'enable_htlc_settle did not work'\n        exit 1\n    fi\n    $alice stop\n    chan_id=$($bob list_channels | jq -r \".[0].channel_point\")\n    $bob close_channel $chan_id --force\n    # if we exit here, bob GUI will show a warning\n    new_blocks 1\n    wait_for_balance bob 1.038\nfi\n\n\nif [[ $1 == \"breach_with_unspent_htlc\" ]]; then\n    $bob enable_htlc_settle false\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    channel=$($alice open_channel $bob_node 0.15 --password='')\n    new_blocks 3\n    wait_until_channel_open alice\n    echo \"alice pays bob\"\n    invoice=$($bob add_request 0.04 --lightning --memo \"test\" | jq -r \".lightning_invoice\")\n    $alice lnpay $invoice --timeout=1 || true\n    unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')\n    if [[ \"$unsettled\" == \"0\" ]]; then\n        echo \"enable_htlc_settle did not work, $unsettled\"\n        exit 1\n    fi\n    ctx=$($alice get_channel_ctx $channel --iknowwhatimdoing)\n    $bob enable_htlc_settle true\n    unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')\n    if [[ \"$unsettled\" != \"0\" ]]; then\n        echo \"enable_htlc_settle did not work, $unsettled\"\n        exit 1\n    fi\n    echo \"alice breaches with old ctx\"\n    $bitcoin_cli sendrawtransaction $ctx\n    new_blocks 1\n    wait_for_balance bob 1.14\nfi\n\n\nif [[ $1 == \"breach_with_spent_htlc\" ]]; then\n    $bob enable_htlc_settle false\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    channel=$($alice open_channel $bob_node 0.15 --password='')\n    new_blocks 3\n    wait_until_channel_open alice\n    echo \"alice pays bob\"\n    invoice=$($bob add_request 0.04 --lightning --memo \"test\" | jq -r \".lightning_invoice\")\n    $alice lnpay $invoice --timeout=1 || true\n    ctx=$($alice get_channel_ctx $channel --iknowwhatimdoing)\n    unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')\n    if [[ \"$unsettled\" == \"0\" ]]; then\n        echo \"enable_htlc_settle did not work, $unsettled\"\n        exit 1\n    fi\n    cp /tmp/alice/regtest/wallets/default_wallet /tmp/alice/regtest/wallets/toxic_wallet\n    $bob enable_htlc_settle true\n    unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')\n    if [[ \"$unsettled\" != \"0\" ]]; then\n        echo \"enable_htlc_settle did not work, $unsettled\"\n        exit 1\n    fi\n    echo $($bob getbalance)\n    echo \"bob goes offline\"\n    $bob stop\n    ctx_id=$($bitcoin_cli sendrawtransaction $ctx)\n    echo \"alice breaches with old ctx:\" $ctx_id\n    new_blocks 1\n    if [[ $($bitcoin_cli gettxout $ctx_id 0 | jq '.confirmations') != \"1\" ]]; then\n        echo \"breach tx not confirmed\"\n        exit 1\n    fi\n    echo \"wait for cltv_expiry blocks\"\n    # note: this will let alice redeem both to_local and the htlc.\n    # (to_local needs to_self_delay blocks; htlc needs whatever we put in invoice)\n    new_blocks 150\n    $alice stop\n    $alice daemon -d\n    sleep 1\n    $alice load_wallet -w /tmp/alice/regtest/wallets/toxic_wallet\n    # wait until alice has spent both ctx outputs\n    echo \"alice spends to_local and htlc outputs\"\n    if [ $TEST_ANCHOR_CHANNELS = True ] ; then\n        # to_local_anchor/to_remote_anchor: 0 and 1 (both are present due to untrimmed htlcs)\n        # htlc: 2, to_local: 3\n        wait_until_spent $ctx_id 2\n        wait_until_spent $ctx_id 3\n    else\n        # htlc: 0, to_local: 1\n        wait_until_spent $ctx_id 0\n        wait_until_spent $ctx_id 1\n    fi\n    new_blocks 1\n    echo \"bob comes back\"\n    $bob daemon -d\n    sleep 1\n    $bob load_wallet\n    wait_for_balance bob 1.039\n    $bob getbalance\nfi\n\nif [[ $1 == \"watchtower\" ]]; then\n    wait_for_balance alice 1\n    echo \"alice opens channel\"\n    bob_node=$($bob nodeid)\n    channel=$($alice open_channel $bob_node 0.15 --password='')\n    echo \"channel outpoint: $channel\"\n    new_blocks 3\n    wait_until_channel_open alice\n    echo \"alice pays bob\"\n    invoice1=$($bob add_request 0.01 --lightning --memo \"invoice1\" | jq -r \".lightning_invoice\")\n    $alice lnpay $invoice1\n    ctx=$($alice get_channel_ctx $channel --iknowwhatimdoing)\n    echo \"alice pays bob again\"\n    invoice2=$($bob add_request 0.01 --lightning --memo \"invoice2\" | jq -r \".lightning_invoice\")\n    $alice lnpay $invoice2\n    bob_ctn=$($bob list_channels | jq '.[0].local_ctn')\n    msg=\"waiting until watchtower is synchronized\"\n    # watchtower needs to be at latest revoked ctn\n    while watchtower_ctn=$($bob get_watchtower_ctn $channel) && [[ $watchtower_ctn != $((bob_ctn-1)) ]]; do\n        sleep 0.1\n        printf \"$msg $bob_ctn $watchtower_ctn\\r\"\n    done\n    printf \"\\n\"\n    echo \"stopping alice and bob\"\n    $bob stop\n    $alice stop\n    ctx_id=$($bitcoin_cli sendrawtransaction $ctx)\n    echo \"alice breaches with old ctx:\" $ctx_id\n    echo \"watchtower publishes justice transaction\"\n    if [ $TEST_ANCHOR_CHANNELS = True ] ; then\n        output_index=3\n    else\n        output_index=1\n    fi\n    wait_until_spent $ctx_id $output_index  # alice's to_local gets punished\nfi\n\nif [[ $1 == \"fw_fail_htlc\" ]]; then\n    $carol enable_htlc_settle false\n    bob_node=$($bob nodeid)\n    wait_for_balance carol 1\n    echo \"alice and carol open channels with bob\"\n    chan_id1=$($alice open_channel $bob_node 0.15 --password='' --push_amount=0.075)\n    chan_id2=$($carol open_channel $bob_node 0.15 --password='' --push_amount=0.075)\n    new_blocks 3\n    wait_until_channel_open alice\n    wait_until_channel_open carol\n    echo \"alice pays carol\"\n    invoice=$($carol add_request 0.01 --lightning --memo \"invoice\" | jq -r \".lightning_invoice\")\n    screen -S alice_payment -dm -L -Logfile /tmp/alice/screen1.log $alice lnpay $invoice --timeout=600\n    sleep 1\n    unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')\n    if [[ \"$unsettled\" == \"0\" ]]; then\n        echo 'enable_htlc_settle did not work (carol settled)'\n        exit 1\n    fi\n    $carol stop\n    ctx_id=$($bob close_channel $chan_id2 --force)\n    new_blocks 1\n    sleep 1\n    new_blocks 150 # cltv before bob can broadcast\n    # index of htlc\n    if [ $TEST_ANCHOR_CHANNELS = True ] ; then\n        output_index=2\n    else\n        output_index=0\n    fi\n    wait_until_spent $ctx_id $output_index\n    new_blocks 1   # confirm 2nd stage.\n    sleep 1\n    new_blocks 100 # deep\n    sleep 5        # give bob time to fail incoming htlc\n    unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')\n    if [[ \"$unsettled\" != \"0\" ]]; then\n        echo 'alice htlc was not failed'\n        exit 1\n    fi\nfi\n\nif [[ $1 == \"just_in_time\" ]]; then\n    bob_node=$($bob nodeid)\n    $alice setconfig zeroconf_trusted_node $bob_node\n    $alice setconfig use_recoverable_channels false\n    wait_for_balance carol 1\n    echo \"carol opens channel with bob\"\n    $carol open_channel $bob_node 0.15 --password=''\n    new_blocks 3\n    wait_until_channel_open carol\n    echo \"carol pays alice\"\n    # note: set amount to 0.001 to test failure: 'payment too low'\n    invoice=$($alice add_request 0.01 --lightning --memo \"invoice\" | jq -r \".lightning_invoice\")\n    success=$($carol lnpay $invoice| jq '.success')\n    if [[ $success != \"true\" ]]; then\n\techo \"JIT payment failed\"\n\texit 1\n    fi\nfi\n\nif [[ $1 == \"unixsockets\" ]]; then\n    # This looks different because it has to run the entire daemon\n    # Test domain socket behavior\n    ./run_electrum --regtest daemon -d --rpcsock=unix # Start daemon with unix domain socket\n    ./run_electrum --regtest stop # Errors if it can't connect\n    # Test custom socket path\n    f=$(mktemp --dry-run)\n    ./run_electrum --regtest daemon -d --rpcsock=unix --rpcsockpath=$f\n    [ -S $f ] # filename exists and is socket\n    ./run_electrum --regtest stop\n    rm $f # clean up\n    # Test for regressions in the ordinary TCP functionality.\n    ./run_electrum --regtest daemon -d --rpcsock=tcp\n    ./run_electrum --regtest stop\nfi\n"
  },
  {
    "path": "tests/regtest/run_bitcoind.sh",
    "content": "#!/usr/bin/env bash\nexport HOME=~\nset -eux pipefail\nmkdir -p ~/.bitcoin\ncat > ~/.bitcoin/bitcoin.conf <<EOF\nregtest=1\ntxindex=1\nprinttoconsole=1\nrpcuser=doggman\nrpcpassword=donkey\nrpcallowip=127.0.0.1\nzmqpubrawblock=tcp://127.0.0.1:28332\nzmqpubrawtx=tcp://127.0.0.1:28333\nfallbackfee=0.0002\n[regtest]\nrpcbind=0.0.0.0\nrpcport=18554\nEOF\nrm -rf ~/.bitcoin/regtest\nbitcoind -regtest &\nsleep 6\nbitcoin-cli createwallet test_wallet\naddr=$(bitcoin-cli getnewaddress)\nbitcoin-cli generatetoaddress 150 $addr\ntail -f ~/.bitcoin/regtest/debug.log\n"
  },
  {
    "path": "tests/regtest/run_electrumx.sh",
    "content": "#!/usr/bin/env bash\nexport HOME=~\nset -eux pipefail\ncd\nrm -rf $HOME/electrumx_db\nmkdir $HOME/electrumx_db\n\nexport COST_SOFT_LIMIT=0\nexport COST_HARD_LIMIT=0\nexport COIN=Bitcoin\nexport SERVICES=tcp://:51001,rpc://\nexport NET=regtest\nexport DAEMON_URL=http://doggman:donkey@127.0.0.1:18554\nexport DB_DIRECTORY=$HOME/electrumx_db\nexport DAEMON_POLL_INTERVAL_BLOCKS=100\nexport DAEMON_POLL_INTERVAL_MEMPOOL=100\n\nelectrumx_server\n"
  },
  {
    "path": "tests/regtest.py",
    "content": "import os\nimport sys\nimport unittest\nimport subprocess\nfrom typing import Mapping, Any\n\n\nclass TestLightning(unittest.TestCase):\n    agents: Mapping[str, Mapping[str, Any]]\n\n    @staticmethod\n    def run_shell(args, timeout=30):\n        process = subprocess.Popen(['tests/regtest/regtest.sh'] + args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, universal_newlines=True)\n        for line in iter(process.stdout.readline, ''):\n            sys.stdout.write(line)\n            sys.stdout.flush()\n        process.wait(timeout=timeout)\n        process.stdout.close()\n        assert process.returncode == 0\n\n    def setUp(self):\n        test_name = self.id().split('.')[-1]\n        sys.stdout.write(\"***** %s ******\\n\" % test_name)\n        # initialize and get funds\n        for agent, config_options in self.agents.items():\n            self.run_shell(['init', agent])\n            for k, v in config_options.items():\n                self.run_shell(['setconfig', agent, k, v])\n        # mine a block so that funds are confirmed\n        self.run_shell(['new_block'])\n        # start daemons\n        for agent in self.agents:\n            self.run_shell(['start', agent])\n\n    def tearDown(self):\n        for agent in self.agents:\n            self.run_shell(['stop', agent])\n\n\nclass TestUnixSockets(TestLightning):\n    agents = {}\n\n    def test_unixsockets(self):\n        self.run_shell(['unixsockets'])\n\n\nclass TestLightningAB(TestLightning):\n    agents = {\n        'alice': {\n            'test_force_disable_mpp': 'false',\n            'test_force_mpp': 'true',\n        },\n        'bob': {\n            'lightning_listen': 'localhost:9735',\n        }\n    }\n\n    def test_collaborative_close(self):\n        self.run_shell(['collaborative_close'])\n\n    def test_backup(self):\n        self.run_shell(['backup'])\n\n    def test_backup_local_forceclose(self):\n        self.run_shell(['backup_local_forceclose'])\n\n    def test_breach(self):\n        self.run_shell(['breach'])\n\n    def test_extract_preimage(self):\n        self.run_shell(['extract_preimage'])\n\n    def test_redeem_received_htlcs(self):\n        self.run_shell(['redeem_received_htlcs'])\n\n    def test_redeem_offered_htlcs(self):\n        self.run_shell(['redeem_offered_htlcs'])\n\n    def test_breach_with_unspent_htlc(self):\n        self.run_shell(['breach_with_unspent_htlc'])\n\n    def test_breach_with_spent_htlc(self):\n        self.run_shell(['breach_with_spent_htlc'])\n\n    def test_lnwatcher_waits_until_fees_go_down(self):\n        self.run_shell(['lnwatcher_waits_until_fees_go_down'])\n\n\nclass TestLightningSwapserver(TestLightning):\n    agents = {\n        'alice': {\n            'use_gossip': 'false',\n            'swapserver_url': 'http://localhost:5455',\n            'nostr_relays': \"''\",\n        },\n        'bob': {\n            'lightning_listen': 'localhost:9735',\n            'plugins.swapserver.enabled': 'true',\n            'plugins.swapserver.port': '5455',\n            'nostr_relays': \"''\",\n        }\n    }\n\n    def test_swapserver_success(self):\n        self.run_shell(['swapserver_success'])\n\n    def test_swapserver_forceclose(self):\n        self.run_shell(['swapserver_forceclose'])\n\n    def test_swapserver_refund(self):\n        self.run_shell(['swapserver_refund'])\n\n\n\nclass TestLightningWatchtower(TestLightning):\n    agents = {\n        'alice': {\n        },\n        'bob': {\n            'lightning_listen': 'localhost:9735',\n            'watchtower_url': 'http://wtuser:wtpassword@127.0.0.1:12345',\n        },\n        'carol': {\n            'plugins.watchtower.enabled': 'true',\n            'plugins.watchtower.server_user': 'wtuser',\n            'plugins.watchtower.server_password': 'wtpassword',\n            'plugins.watchtower.server_port': '12345',\n        }\n    }\n\n    def test_watchtower(self):\n        self.run_shell(['watchtower'])\n\n\nclass TestLightningABC(TestLightning):\n    agents = {\n        'alice': {\n        },\n        'bob': {\n            'lightning_listen': 'localhost:9735',\n            'lightning_forward_payments': 'true',\n        },\n        'carol': {\n        }\n    }\n\n    def test_fw_fail_htlc(self):\n        self.run_shell(['fw_fail_htlc'])\n\n\nclass TestLightningJIT(TestLightning):\n    agents = {\n        'alice': {\n            'accept_zeroconf_channels': 'true',\n        },\n        'bob': {\n            'lightning_listen': 'localhost:9735',\n            'lightning_forward_payments': 'true',\n            'accept_zeroconf_channels': 'true',\n        },\n        'carol': {\n        }\n    }\n\n    def test_just_in_time(self):\n        self.run_shell(['just_in_time'])\n\n\nclass TestLightningJITTrampoline(TestLightningJIT):\n    agents = {\n        'alice': {\n            'use_gossip': 'false',\n            'accept_zeroconf_channels': 'true',\n        },\n        'bob': {\n            'lightning_listen': 'localhost:9735',\n            'lightning_forward_payments': 'true',\n            'lightning_forward_trampoline_payments': 'true',\n            'accept_zeroconf_channels': 'true',\n        },\n        'carol': {\n            'use_gossip': 'false',\n        }\n    }\n"
  },
  {
    "path": "tests/slip39-vectors.json",
    "content": "[\n  [\n    \"1. Valid mnemonic without sharing (128 bits)\",\n    [\n      \"duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision keyboard\"\n    ],\n    \"bb54aac4b89dc868ba37d9cc21b2cece\",\n    \"xprv9s21ZrQH143K4QViKpwKCpS2zVbz8GrZgpEchMDg6KME9HZtjfL7iThE9w5muQA4YPHKN1u5VM1w8D4pvnjxa2BmpGMfXr7hnRrRHZ93awZ\"\n  ],\n  [\n    \"2. Mnemonic with invalid checksum (128 bits)\",\n    [\n      \"duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision kidney\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"3. Mnemonic with invalid padding (128 bits)\",\n    [\n      \"duckling enlarge academic academic email result length solution fridge kidney coal piece deal husband erode duke ajar music cargo fitness\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"4. Basic sharing 2-of-3 (128 bits)\",\n    [\n      \"shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed\",\n      \"shadow pistol academic acid actress prayer class unknown daughter sweater depict flip twice unkind craft early superior advocate guest smoking\"\n    ],\n    \"b43ceb7e57a0ea8766221624d01b0864\",\n    \"xprv9s21ZrQH143K2nNuAbfWPHBtfiSCS14XQgb3otW4pX655q58EEZeC8zmjEUwucBu9dPnxdpbZLCn57yx45RBkwJHnwHFjZK4XPJ8SyeYjYg\"\n  ],\n  [\n    \"5. Basic sharing 2-of-3 (128 bits)\",\n    [\n      \"shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"6. Mnemonics with different identifiers (128 bits)\",\n    [\n      \"adequate smoking academic acid debut wine petition glen cluster slow rhyme slow simple epidemic rumor junk tracks treat olympic tolerate\",\n      \"adequate stay academic agency agency formal party ting frequent learn upstairs remember smear leaf damage anatomy ladle market hush corner\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"7. Mnemonics with different iteration exponents (128 bits)\",\n    [\n      \"peasant leaves academic acid desert exact olympic math alive axle trial tackle drug deny decent smear dominant desert bucket remind\",\n      \"peasant leader academic agency cultural blessing percent network envelope medal junk primary human pumps jacket fragment payroll ticket evoke voice\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"8. Mnemonics with mismatching group thresholds (128 bits)\",\n    [\n      \"liberty category beard echo animal fawn temple briefing math username various wolf aviation fancy visual holy thunder yelp helpful payment\",\n      \"liberty category beard email beyond should fancy romp founder easel pink holy hairy romp loyalty material victim owner toxic custody\",\n      \"liberty category academic easy being hazard crush diminish oral lizard reaction cluster force dilemma deploy force club veteran expect photo\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"9. Mnemonics with mismatching group counts (128 bits)\",\n    [\n      \"average senior academic leaf broken teacher expect surface hour capture obesity desire negative dynamic dominant pistol mineral mailman iris aide\",\n      \"average senior academic agency curious pants blimp spew clothes slice script dress wrap firm shaft regular slavery negative theater roster\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"10. Mnemonics with greater group threshold than group counts (128 bits)\",\n    [\n      \"music husband acrobat acid artist finance center either graduate swimming object bike medical clothes station aspect spider maiden bulb welcome\",\n      \"music husband acrobat agency advance hunting bike corner density careful material civil evil tactics remind hawk discuss hobo voice rainbow\",\n      \"music husband beard academic black tricycle clock mayor estimate level photo episode exclude ecology papa source amazing salt verify divorce\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"11. Mnemonics with duplicate member indices (128 bits)\",\n    [\n      \"device stay academic always dive coal antenna adult black exceed stadium herald advance soldier busy dryer daughter evaluate minister laser\",\n      \"device stay academic always dwarf afraid robin gravity crunch adjust soul branch walnut coastal dream costume scholar mortgage mountain pumps\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"12. Mnemonics with mismatching member thresholds (128 bits)\",\n    [\n      \"hour painting academic academic device formal evoke guitar random modern justice filter withdraw trouble identify mailman insect general cover oven\",\n      \"hour painting academic agency artist again daisy capital beaver fiber much enjoy suitable symbolic identify photo editor romp float echo\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"13. Mnemonics giving an invalid digest (128 bits)\",\n    [\n      \"guilt walnut academic acid deliver remove equip listen vampire tactics nylon rhythm failure husband fatigue alive blind enemy teaspoon rebound\",\n      \"guilt walnut academic agency brave hamster hobo declare herd taste alpha slim criminal mild arcade formal romp branch pink ambition\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"14. Insufficient number of groups (128 bits, case 1)\",\n    [\n      \"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"15. Insufficient number of groups (128 bits, case 2)\",\n    [\n      \"eraser senior decision scared cargo theory device idea deliver modify curly include pancake both news skin realize vitamins away join\",\n      \"eraser senior decision roster beard treat identify grumpy salt index fake aviation theater cubic bike cause research dragon emphasis counter\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"16. Threshold number of groups, but insufficient number of members in one group (128 bits)\",\n    [\n      \"eraser senior decision shadow artist work morning estate greatest pipeline plan ting petition forget hormone flexible general goat admit surface\",\n      \"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"17. Threshold number of groups and members in each group (128 bits, case 1)\",\n    [\n      \"eraser senior decision roster beard treat identify grumpy salt index fake aviation theater cubic bike cause research dragon emphasis counter\",\n      \"eraser senior ceramic snake clay various huge numb argue hesitate auction category timber browser greatest hanger petition script leaf pickup\",\n      \"eraser senior ceramic shaft dynamic become junior wrist silver peasant force math alto coal amazing segment yelp velvet image paces\",\n      \"eraser senior ceramic round column hawk trust auction smug shame alive greatest sheriff living perfect corner chest sled fumes adequate\",\n      \"eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing\"\n    ],\n    \"7c3397a292a5941682d7a4ae2d898d11\",\n    \"xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV\"\n  ],\n  [\n    \"18. Threshold number of groups and members in each group (128 bits, case 2)\",\n    [\n      \"eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing\",\n      \"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice\",\n      \"eraser senior decision scared cargo theory device idea deliver modify curly include pancake both news skin realize vitamins away join\"\n    ],\n    \"7c3397a292a5941682d7a4ae2d898d11\",\n    \"xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV\"\n  ],\n  [\n    \"19. Threshold number of groups and members in each group (128 bits, case 3)\",\n    [\n      \"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice\",\n      \"eraser senior acrobat romp bishop medical gesture pumps secret alive ultimate quarter priest subject class dictate spew material endless market\"\n    ],\n    \"7c3397a292a5941682d7a4ae2d898d11\",\n    \"xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV\"\n  ],\n  [\n    \"20. Valid mnemonic without sharing (256 bits)\",\n    [\n      \"theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect luck\"\n    ],\n    \"989baf9dcaad5b10ca33dfd8cc75e42477025dce88ae83e75a230086a0e00e92\",\n    \"xprv9s21ZrQH143K41mrxxMT2FpiheQ9MFNmWVK4tvX2s28KLZAhuXWskJCKVRQprq9TnjzzzEYePpt764csiCxTt22xwGPiRmUjYUUdjaut8RM\"\n  ],\n  [\n    \"21. Mnemonic with invalid checksum (256 bits)\",\n    [\n      \"theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect lunar\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"22. Mnemonic with invalid padding (256 bits)\",\n    [\n      \"theory painting academic academic campus sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips facility obtain sister\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"23. Basic sharing 2-of-3 (256 bits)\",\n    [\n      \"humidity disease academic always aluminum jewelry energy woman receiver strategy amuse duckling lying evidence network walnut tactics forget hairy rebound impulse brother survive clothes stadium mailman rival ocean reward venture always armed unwrap\",\n      \"humidity disease academic agency actress jacket gross physics cylinder solution fake mortgage benefit public busy prepare sharp friar change work slow purchase ruler again tricycle involve viral wireless mixture anatomy desert cargo upgrade\"\n    ],\n    \"c938b319067687e990e05e0da0ecce1278f75ff58d9853f19dcaeed5de104aae\",\n    \"xprv9s21ZrQH143K3a4GRMgK8WnawupkwkP6gyHxRsXnMsYPTPH21fWwNcAytijtfyftqNfiaY8LgQVdBQvHZ9FBvtwdjC7LCYxjYruJFuLzyMQ\"\n  ],\n  [\n    \"24. Basic sharing 2-of-3 (256 bits)\",\n    [\n      \"humidity disease academic always aluminum jewelry energy woman receiver strategy amuse duckling lying evidence network walnut tactics forget hairy rebound impulse brother survive clothes stadium mailman rival ocean reward venture always armed unwrap\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"25. Mnemonics with different identifiers (256 bits)\",\n    [\n      \"smear husband academic acid deadline scene venture distance dive overall parking bracelet elevator justice echo burning oven chest duke nylon\",\n      \"smear isolate academic agency alpha mandate decorate burden recover guard exercise fatal force syndrome fumes thank guest drift dramatic mule\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"26. Mnemonics with different iteration exponents (256 bits)\",\n    [\n      \"finger trash academic acid average priority dish revenue academic hospital spirit western ocean fact calcium syndrome greatest plan losing dictate\",\n      \"finger traffic academic agency building lilac deny paces subject threaten diploma eclipse window unknown health slim piece dragon focus smirk\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"27. Mnemonics with mismatching group thresholds (256 bits)\",\n    [\n      \"flavor pink beard echo depart forbid retreat become frost helpful juice unwrap reunion credit math burning spine black capital lair\",\n      \"flavor pink beard email diet teaspoon freshman identify document rebound cricket prune headset loyalty smell emission skin often square rebound\",\n      \"flavor pink academic easy credit cage raisin crazy closet lobe mobile become drink human tactics valuable hand capture sympathy finger\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"28. Mnemonics with mismatching group counts (256 bits)\",\n    [\n      \"column flea academic leaf debut extra surface slow timber husky lawsuit game behavior husky swimming already paper episode tricycle scroll\",\n      \"column flea academic agency blessing garbage party software stadium verify silent umbrella therapy decorate chemical erode dramatic eclipse replace apart\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"29. Mnemonics with greater group threshold than group counts (256 bits)\",\n    [\n      \"smirk pink acrobat acid auction wireless impulse spine sprinkle fortune clogs elbow guest hush loyalty crush dictate tracks airport talent\",\n      \"smirk pink acrobat agency dwarf emperor ajar organize legs slice harvest plastic dynamic style mobile float bulb health coding credit\",\n      \"smirk pink beard academic alto strategy carve shame language rapids ruin smart location spray training acquire eraser endorse submit peaceful\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"30. Mnemonics with duplicate member indices (256 bits)\",\n    [\n      \"fishing recover academic always device craft trend snapshot gums skin downtown watch device sniff hour clock public maximum garlic born\",\n      \"fishing recover academic always aircraft view software cradle fangs amazing package plastic evaluate intend penalty epidemic anatomy quarter cage apart\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"31. Mnemonics with mismatching member thresholds (256 bits)\",\n    [\n      \"evoke garden academic academic answer wolf scandal modern warmth station devote emerald market physics surface formal amazing aquatic gesture medical\",\n      \"evoke garden academic agency deal revenue knit reunion decrease magazine flexible company goat repair alarm military facility clogs aide mandate\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"32. Mnemonics giving an invalid digest (256 bits)\",\n    [\n      \"river deal academic acid average forbid pistol peanut custody bike class aunt hairy merit valid flexible learn ajar very easel\",\n      \"river deal academic agency camera amuse lungs numb isolate display smear piece traffic worthy year patrol crush fact fancy emission\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"33. Insufficient number of groups (256 bits, case 1)\",\n    [\n      \"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"34. Insufficient number of groups (256 bits, case 2)\",\n    [\n      \"wildlife deal decision scared acne fatal snake paces obtain election dryer dominant romp tactics railroad marvel trust helpful flip peanut theory theater photo luck install entrance taxi step oven network dictate intimate listen\",\n      \"wildlife deal decision smug ancestor genuine move huge cubic strategy smell game costume extend swimming false desire fake traffic vegan senior twice timber submit leader payroll fraction apart exact forward pulse tidy install\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"35. Threshold number of groups, but insufficient number of members in one group (256 bits)\",\n    [\n      \"wildlife deal decision shadow analysis adjust bulb skunk muscle mandate obesity total guitar coal gravity carve slim jacket ruin rebuild ancestor numerous hour mortgage require herd maiden public ceiling pecan pickup shadow club\",\n      \"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"36. Threshold number of groups and members in each group (256 bits, case 1)\",\n    [\n      \"wildlife deal ceramic round aluminum pitch goat racism employer miracle percent math decision episode dramatic editor lily prospect program scene rebuild display sympathy have single mustang junction relate often chemical society wits estate\",\n      \"wildlife deal decision scared acne fatal snake paces obtain election dryer dominant romp tactics railroad marvel trust helpful flip peanut theory theater photo luck install entrance taxi step oven network dictate intimate listen\",\n      \"wildlife deal ceramic scatter argue equip vampire together ruin reject literary rival distance aquatic agency teammate rebound false argue miracle stay again blessing peaceful unknown cover beard acid island language debris industry idle\",\n      \"wildlife deal ceramic snake agree voter main lecture axis kitchen physics arcade velvet spine idea scroll promise platform firm sharp patrol divorce ancestor fantasy forbid goat ajar believe swimming cowboy symbolic plastic spelling\",\n      \"wildlife deal decision shadow analysis adjust bulb skunk muscle mandate obesity total guitar coal gravity carve slim jacket ruin rebuild ancestor numerous hour mortgage require herd maiden public ceiling pecan pickup shadow club\"\n    ],\n    \"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b\",\n    \"xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c\"\n  ],\n  [\n    \"37. Threshold number of groups and members in each group (256 bits, case 2)\",\n    [\n      \"wildlife deal decision scared acne fatal snake paces obtain election dryer dominant romp tactics railroad marvel trust helpful flip peanut theory theater photo luck install entrance taxi step oven network dictate intimate listen\",\n      \"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium\",\n      \"wildlife deal decision smug ancestor genuine move huge cubic strategy smell game costume extend swimming false desire fake traffic vegan senior twice timber submit leader payroll fraction apart exact forward pulse tidy install\"\n    ],\n    \"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b\",\n    \"xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c\"\n  ],\n  [\n    \"38. Threshold number of groups and members in each group (256 bits, case 3)\",\n    [\n      \"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium\",\n      \"wildlife deal acrobat romp anxiety axis starting require metric flexible geology game drove editor edge screw helpful have huge holy making pitch unknown carve holiday numb glasses survive already tenant adapt goat fangs\"\n    ],\n    \"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b\",\n    \"xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c\"\n  ],\n  [\n    \"39. Mnemonic with insufficient length\",\n    [\n      \"junk necklace academic academic acne isolate join hesitate lunar roster dough calcium chemical ladybug amount mobile glasses verify cylinder\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"40. Mnemonic with invalid master secret length\",\n    [\n      \"fraction necklace academic academic award teammate mouse regular testify coding building member verdict purchase blind camera duration email prepare spirit quarter\"\n    ],\n    \"\",\n    \"\"\n  ],\n  [\n    \"41. Valid mnemonics which can detect some errors in modular arithmetic\",\n    [\n      \"herald flea academic cage avoid space trend estate dryer hairy evoke eyebrow improve airline artwork garlic premium duration prevent oven\",\n      \"herald flea academic client blue skunk class goat luxury deny presence impulse graduate clay join blanket bulge survive dish necklace\",\n      \"herald flea academic acne advance fused brother frozen broken game ranked ajar already believe check install theory angry exercise adult\"\n    ],\n    \"ad6f2ad8b59bbbaa01369b9006208d9a\",\n    \"xprv9s21ZrQH143K2R4HJxcG1eUsudvHM753BZ9vaGkpYCoeEhCQx147C5qEcupPHxcXYfdYMwJmsKXrHDhtEwutxTTvFzdDCZVQwHneeQH8ioH\"\n  ],\n  [\n    \"42. Valid extendable mnemonic without sharing (128 bits)\",\n    [\n      \"testify swimming academic academic column loyalty smear include exotic bedroom exotic wrist lobe cover grief golden smart junior estimate learn\"\n    ],\n    \"1679b4516e0ee5954351d288a838f45e\",\n    \"xprv9s21ZrQH143K2w6eTpQnB73CU8Qrhg6gN3D66Jr16n5uorwoV7CwxQ5DofRPyok5DyRg4Q3BfHfCgJFk3boNRPPt1vEW1ENj2QckzVLQFXu\"\n  ],\n  [\n    \"43. Extendable basic sharing 2-of-3 (128 bits)\",\n    [\n      \"enemy favorite academic acid cowboy phrase havoc level response walnut budget painting inside trash adjust froth kitchen learn tidy punish\",\n      \"enemy favorite academic always academic sniff script carpet romp kind promise scatter center unfair training emphasis evening belong fake enforce\"\n    ],\n    \"48b1a4b80b8c209ad42c33672bdaa428\",\n    \"xprv9s21ZrQH143K4FS1qQdXYAFVAHiSAnjj21YAKGh2CqUPJ2yQhMmYGT4e5a2tyGLiVsRgTEvajXkxhg92zJ8zmWZas9LguQWz7WZShfJg6RS\"\n  ],\n  [\n    \"44. Valid extendable mnemonic without sharing (256 bits)\",\n    [\n      \"impulse calcium academic academic alcohol sugar lyrics pajamas column facility finance tension extend space birthday rainbow swimming purple syndrome facility trial warn duration snapshot shadow hormone rhyme public spine counter easy hawk album\"\n    ],\n    \"8340611602fe91af634a5f4608377b5235fa2d757c51d720c0c7656249a3035f\",\n    \"xprv9s21ZrQH143K2yJ7S8bXMiGqp1fySH8RLeFQKQmqfmmLTRwWmAYkpUcWz6M42oGoFMJRENmvsGQmunWTdizsi8v8fku8gpbVvYSiCYJTF1Y\"\n  ],\n  [\n    \"45. Extendable basic sharing 2-of-3 (256 bits)\",\n    [\n      \"western apart academic always artist resident briefing sugar woman oven coding club ajar merit pecan answer prisoner artist fraction amount desktop mild false necklace muscle photo wealthy alpha category unwrap spew losing making\",\n      \"western apart academic acid answer ancient auction flip image penalty oasis beaver multiple thunder problem switch alive heat inherit superior teaspoon explain blanket pencil numb lend punish endless aunt garlic humidity kidney observe\"\n    ],\n    \"8dc652d6d6cd370d8c963141f6d79ba440300f25c467302c1d966bff8f62300d\",\n    \"xprv9s21ZrQH143K2eFW2zmu3aayWWd6MJZBG7RebW35fiKcoCZ6jFi6U5gzffB9McDdiKTecUtRqJH9GzueCXiQK1LaQXdgthS8DgWfC8Uu3z7\"\n  ]\n]\n"
  },
  {
    "path": "tests/test_bitcoin.py",
    "content": "import asyncio\nimport base64\nimport json\nimport os\nimport sys\nimport inspect\n\nimport electrum_ecc as ecc\n\nfrom electrum import bitcoin\nfrom electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key,\n                              is_address, is_private_key,\n                              var_int, _op_push, address_to_script, OnchainOutputType, address_to_payload,\n                              deserialize_privkey, serialize_privkey, is_segwit_address,\n                              is_b58_address, address_to_scripthash, is_minikey,\n                              is_compressed_privkey, EncodeBase58Check, DecodeBase58Check,\n                              script_num_to_bytes, push_script, add_number_to_script,\n                              opcodes, base_encode, base_decode, BitcoinException,\n                              taproot_tweak_pubkey, taproot_tweak_seckey, taproot_output_script,\n                              control_block_for_taproot_script_spend)\nfrom electrum import bip32\nfrom electrum import segwit_addr\nfrom electrum.segwit_addr import DecodedBech32\nfrom electrum.bip32 import (BIP32Node, convert_bip32_intpath_to_strpath,\n                            xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation,\n                            is_xpub, convert_bip32_strpath_to_intpath,\n                            normalize_bip32_derivation, is_all_public_derivation)\nfrom electrum.crypto import sha256d, SUPPORTED_PW_HASH_VERSIONS\nfrom electrum import crypto, constants\nfrom electrum.util import bfh, InvalidPassword, randrange\nfrom electrum.storage import WalletStorage\nfrom electrum.keystore import xtype_from_derivation\n\nfrom . import ElectrumTestCase\nfrom . import FAST_TESTS\n\n\ndef needs_test_with_all_aes_implementations(func):\n    \"\"\"Function decorator to run a unit test multiple times:\n    once with each AES implementation.\n\n    NOTE: this is inherently sequential;\n    tests running in parallel would break things\n    \"\"\"\n    if FAST_TESTS:  # if set, only run tests once, using fastest implementation\n        return func\n    has_cryptodome = crypto.HAS_CRYPTODOME\n    has_cryptography = crypto.HAS_CRYPTOGRAPHY\n    has_pyaes = crypto.HAS_PYAES\n    if inspect.iscoroutinefunction(func):\n        async def run_test(*args, **kwargs):\n            try:\n                if has_pyaes:\n                    (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = False, False, True\n                    await func(*args, **kwargs)  # pyaes\n                if has_cryptodome:\n                    (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = True, False, False\n                    await func(*args, **kwargs)  # cryptodome\n                if has_cryptography:\n                    (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = False, True, False\n                    await func(*args, **kwargs)  # cryptography\n            finally:\n                crypto.HAS_CRYPTODOME = has_cryptodome\n                crypto.HAS_CRYPTOGRAPHY = has_cryptography\n                crypto.HAS_PYAES = has_pyaes\n    else:\n        def run_test(*args, **kwargs):\n            try:\n                if has_pyaes:\n                    (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = False, False, True\n                    func(*args, **kwargs)  # pyaes\n                if has_cryptodome:\n                    (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = True, False, False\n                    func(*args, **kwargs)  # cryptodome\n                if has_cryptography:\n                    (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = False, True, False\n                    func(*args, **kwargs)  # cryptography\n            finally:\n                crypto.HAS_CRYPTODOME = has_cryptodome\n                crypto.HAS_CRYPTOGRAPHY = has_cryptography\n                crypto.HAS_PYAES = has_pyaes\n    return run_test\n\n\ndef needs_test_with_all_chacha20_implementations(func):\n    \"\"\"Function decorator to run a unit test multiple times:\n    once with each ChaCha20/Poly1305 implementation.\n\n    NOTE: this is inherently sequential;\n    tests running in parallel would break things\n    \"\"\"\n    if FAST_TESTS:  # if set, only run tests once, using fastest implementation\n        return func\n    has_cryptodome = crypto.HAS_CRYPTODOME\n    has_cryptography = crypto.HAS_CRYPTOGRAPHY\n    if inspect.iscoroutinefunction(func):\n        async def run_test(*args, **kwargs):\n            try:\n                if has_cryptodome:\n                    (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = True, False\n                    await func(*args, **kwargs)  # cryptodome\n                if has_cryptography:\n                    (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = False, True\n                    await func(*args, **kwargs)  # cryptography\n            finally:\n                crypto.HAS_CRYPTODOME = has_cryptodome\n                crypto.HAS_CRYPTOGRAPHY = has_cryptography\n    else:\n        def run_test(*args, **kwargs):\n            try:\n                if has_cryptodome:\n                    (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = True, False\n                    func(*args, **kwargs)  # cryptodome\n                if has_cryptography:\n                    (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = False, True\n                    func(*args, **kwargs)  # cryptography\n            finally:\n                crypto.HAS_CRYPTODOME = has_cryptodome\n                crypto.HAS_CRYPTOGRAPHY = has_cryptography\n    return run_test\n\n\ndef disable_ecdsa_r_value_grinding(func):\n    \"\"\"Function decorator to run a unit test with ecdsa R-value grinding disabled.\n    This is used when we want to pass test vectors that were created without R-value grinding.\n    (see https://github.com/bitcoin/bitcoin/pull/13666 )\n\n    NOTE: this is inherently sequential;\n    tests running in parallel would break things\n    \"\"\"\n    is_grinding = ecc.ENABLE_ECDSA_R_VALUE_GRINDING\n    if inspect.iscoroutinefunction(func):\n        async def run_test(*args, **kwargs):\n            try:\n                ecc.ENABLE_ECDSA_R_VALUE_GRINDING = False\n                return await func(*args, **kwargs)\n            finally:\n                ecc.ENABLE_ECDSA_R_VALUE_GRINDING = is_grinding\n    else:\n        def run_test(*args, **kwargs):\n            try:\n                ecc.ENABLE_ECDSA_R_VALUE_GRINDING = False\n                return func(*args, **kwargs)\n            finally:\n                ecc.ENABLE_ECDSA_R_VALUE_GRINDING = is_grinding\n    return run_test\n\n\nclass Test_bitcoin(ElectrumTestCase):\n\n    def test_libsecp256k1_is_available(self):\n        # we want the unit testing framework to test with libsecp256k1 available.\n        self.assertTrue(bool(ecc._libsecp256k1))\n\n    def test_pycryptodomex_is_available(self):\n        # we want the unit testing framework to test with pycryptodomex available.\n        self.assertTrue(bool(crypto.HAS_CRYPTODOME))\n\n    def test_cryptography_is_available(self):\n        # we want the unit testing framework to test with cryptography available.\n        self.assertTrue(bool(crypto.HAS_CRYPTOGRAPHY))\n\n    def test_pyaes_is_available(self):\n        # we want the unit testing framework to test with pyaes available.\n        self.assertTrue(bool(crypto.HAS_PYAES))\n\n    @needs_test_with_all_aes_implementations\n    def test_crypto(self):\n        for message in [b\"Chancellor on brink of second bailout for banks\", b'\\xff'*512]:\n            self._do_test_crypto(message)\n\n    def _do_test_crypto(self, message: bytes):\n        G = ecc.GENERATOR\n        _r  = G.order()\n        pvk = randrange(_r)\n\n        Pub = pvk*G\n        pubkey_c = Pub.get_public_key_bytes(True)\n        #pubkey_u = point_to_ser(Pub,False)\n        addr_c = public_key_to_p2pkh(pubkey_c)\n\n        #print \"Private key            \", '%064x'%pvk\n        eck = ecc.ECPrivkey.from_secret_scalar(pvk)\n\n        #print \"Compressed public key  \", pubkey_c.encode('hex')\n        enc = crypto.ecies_encrypt_message(ecc.ECPubkey(pubkey_c), message)\n        dec = crypto.ecies_decrypt_message(eck, enc)\n        self.assertEqual(message, dec)\n\n        #print \"Uncompressed public key\", pubkey_u.encode('hex')\n        #enc2 = EC_KEY.encrypt_message(message, pubkey_u)\n        dec2 = crypto.ecies_decrypt_message(eck, enc)\n        self.assertEqual(message, dec2)\n\n        msg32 = sha256d(bitcoin.usermessage_magic(message))\n        sig65 = eck.ecdsa_sign_recoverable(msg32, is_compressed=True)\n        self.assertTrue(eck.ecdsa_verify_recoverable(sig65, msg32))\n\n    @staticmethod\n    def sign_message_with_wif_privkey(wif_privkey: str, msg: bytes) -> bytes:\n        txin_type, privkey, compressed = deserialize_privkey(wif_privkey)\n        key = ecc.ECPrivkey(privkey)\n        return bitcoin.ecdsa_sign_usermessage(key, msg, is_compressed=compressed)\n\n    def test_signmessage_legacy_address(self):\n        msg1 = b'Chancellor on brink of second bailout for banks'\n        msg2 = b'Electrum'\n\n        sig1 = self.sign_message_with_wif_privkey(\n            'L1TnU2zbNaAqMoVh65Cyvmcjzbrj41Gs9iTLcWbpJCMynXuap6UN', msg1)  # compressed pubkey\n        addr1 = '15hETetDmcXm1mM4sEf7U2KXC9hDHFMSzz'\n        sig2 = self.sign_message_with_wif_privkey(\n            '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD', msg2)  # uncompressed pubkey\n        addr2 = '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6'\n\n        sig1_b64 = base64.b64encode(sig1)\n        sig2_b64 = base64.b64encode(sig2)\n\n        self.assertEqual(sig1_b64, b'Hzsu0U/THAsPz/MSuXGBKSULz2dTfmrg1NsAhFp+wH5aKfmX4Db7ExLGa7FGn0m6Mf43KsbEOWpvUUUBTM3Uusw=')\n        self.assertEqual(sig2_b64, b'HBQdYfv7kOrxmRewLJnG7sV6KlU71O04hUnE4tai97p7Pg+D+yKaWXsdGgHTrKw90caQMo/D6b//qX50ge9P9iI=')\n\n        self.assertTrue(bitcoin.verify_usermessage_with_address(addr1, sig1, msg1))\n        self.assertTrue(bitcoin.verify_usermessage_with_address(addr2, sig2, msg2))\n\n        self.assertFalse(bitcoin.verify_usermessage_with_address(addr1, b'wrong', msg1))\n        self.assertFalse(bitcoin.verify_usermessage_with_address(addr1, sig2, msg1))\n\n    def test_signmessage_low_s(self):\n        \"\"\"`$ bitcoin-cli verifymessage` does NOT enforce the low-S rule for ecdsa sigs. This tests we do the same.\"\"\"\n        addr = \"15hETetDmcXm1mM4sEf7U2KXC9hDHFMSzz\"\n        sig_low_s = b'Hzsu0U/THAsPz/MSuXGBKSULz2dTfmrg1NsAhFp+wH5aKfmX4Db7ExLGa7FGn0m6Mf43KsbEOWpvUUUBTM3Uusw='\n        sig_high_s = b'IDsu0U/THAsPz/MSuXGBKSULz2dTfmrg1NsAhFp+wH5a1gZoH8kE7O05lE65YLZFzLx3sh/rDzXMbo1dQAJhhnU='\n        msg = b'Chancellor on brink of second bailout for banks'\n        self.assertTrue(bitcoin.verify_usermessage_with_address(address=addr, sig65=base64.b64decode(sig_low_s), message=msg))\n        self.assertTrue(bitcoin.verify_usermessage_with_address(address=addr, sig65=base64.b64decode(sig_high_s), message=msg))\n\n    def test_signmessage_segwit_witness_v0_address(self):\n        msg = b'Electrum'\n        # p2wpkh-p2sh\n        sig1 = self.sign_message_with_wif_privkey(\"p2wpkh-p2sh:L1cgMEnShp73r9iCukoPE3MogLeueNYRD9JVsfT1zVHyPBR3KqBY\", msg)\n        addr1 = \"3DYoBqQ5N6dADzyQjy9FT1Ls4amiYVaqTG\"\n        self.assertEqual(base64.b64encode(sig1), b'HyFaND+87TtVbRhkTfT3mPNBCQcJ32XXtNZGW8sFldJsNpOPCegEmdcCf5Thy18hdMH88GLxZLkOby/EwVUuSeA=')\n        self.assertTrue(bitcoin.verify_usermessage_with_address(addr1, sig1, msg))\n        self.assertFalse(bitcoin.verify_usermessage_with_address(addr1, sig1, b'heyheyhey'))\n        # p2wpkh\n        sig2 = self.sign_message_with_wif_privkey(\"p2wpkh:L1cgMEnShp73r9iCukoPE3MogLeueNYRD9JVsfT1zVHyPBR3KqBY\", msg)\n        addr2 = \"bc1qq2tmmcngng78nllq2pvrkchcdukemtj56uyue0\"\n        self.assertEqual(base64.b64encode(sig2), b'HyFaND+87TtVbRhkTfT3mPNBCQcJ32XXtNZGW8sFldJsNpOPCegEmdcCf5Thy18hdMH88GLxZLkOby/EwVUuSeA=')\n        self.assertTrue(bitcoin.verify_usermessage_with_address(addr2, sig2, msg))\n        self.assertFalse(bitcoin.verify_usermessage_with_address(addr2, sig2, b'heyheyhey'))\n\n    def test_signmessage_segwit_witness_v0_address_test_we_also_accept_sigs_from_trezor(self):\n        \"\"\"Trezor and some other projects use a slightly different scheme for message-signing\n        with p2wpkh and p2wpkh-p2sh addresses. Test that we also accept signatures from them.\n        see #3861\n        tests from https://github.com/trezor/trezor-firmware/blob/2ce1e6ba7dbe5bbaeeb336fff0a038e59cb40ef8/tests/device_tests/bitcoin/test_signmessage.py#L39\n        \"\"\"\n        msg = b\"This is an example of a signed message.\"\n        addr1 = \"3L6TyTisPBmrDAj6RoKmDzNnj4eQi54gD2\"\n        addr2 = \"bc1qannfxke2tfd4l7vhepehpvt05y83v3qsf6nfkk\"\n        sig1 = bytes.fromhex(\"23744de4516fac5c140808015664516a32fead94de89775cec7e24dbc24fe133075ac09301c4cc8e197bea4b6481661d5b8e9bf19d8b7b8a382ecdb53c2ee0750d\")\n        sig2 = bytes.fromhex(\"28b55d7600d9e9a7e2a49155ddf3cfdb8e796c207faab833010fa41fb7828889bc47cf62348a7aaa0923c0832a589fab541e8f12eb54fb711c90e2307f0f66b194\")\n        self.assertTrue(bitcoin.verify_usermessage_with_address(address=addr1, sig65=sig1, message=msg))\n        self.assertTrue(bitcoin.verify_usermessage_with_address(address=addr2, sig65=sig2, message=msg))\n        # if there is type information in the header of the sig (first byte), enforce that:\n        sig1_wrongtype = bytes.fromhex(\"27744de4516fac5c140808015664516a32fead94de89775cec7e24dbc24fe133075ac09301c4cc8e197bea4b6481661d5b8e9bf19d8b7b8a382ecdb53c2ee0750d\")\n        sig2_wrongtype = bytes.fromhex(\"24b55d7600d9e9a7e2a49155ddf3cfdb8e796c207faab833010fa41fb7828889bc47cf62348a7aaa0923c0832a589fab541e8f12eb54fb711c90e2307f0f66b194\")\n        self.assertFalse(bitcoin.verify_usermessage_with_address(address=addr1, sig65=sig1_wrongtype, message=msg))\n        self.assertFalse(bitcoin.verify_usermessage_with_address(address=addr2, sig65=sig2_wrongtype, message=msg))\n\n    @needs_test_with_all_aes_implementations\n    def test_decrypt_message(self):\n        key = WalletStorage.get_eckey_from_password('pw123')\n        self.assertEqual(b'me<(s_s)>age', crypto.ecies_decrypt_message(\n            key, b'QklFMQMDFtgT3zWSQsa+Uie8H/WvfUjlu9UN9OJtTt3KlgKeSTi6SQfuhcg1uIz9hp3WIUOFGTLr4RNQBdjPNqzXwhkcPi2Xsbiw6UCNJncVPJ6QBg=='))\n        self.assertEqual(b'me<(s_s)>age', crypto.ecies_decrypt_message(\n            key, b'QklFMQKXOXbylOQTSMGfo4MFRwivAxeEEkewWQrpdYTzjPhqjHcGBJwdIhB7DyRfRQihuXx1y0ZLLv7XxLzrILzkl/H4YUtZB4uWjuOAcmxQH4i/Og=='))\n        self.assertEqual(b'hey_there' * 100, crypto.ecies_decrypt_message(\n            key, b'QklFMQLOOsabsXtGQH8edAa6VOUa5wX8/DXmxX9NyHoAx1a5bWgllayGRVPeI2bf0ZdWK0tfal0ap0ZIVKbd2eOJybqQkILqT6E1/Syzq0Zicyb/AA1eZNkcX5y4gzloxinw00ubCA8M7gcUjJpOqbnksATcJ5y2YYXcHMGGfGurWu6uJ/UyrNobRidWppRMW5yR9/6utyNvT6OHIolCMEf7qLcmtneoXEiz51hkRdZS7weNf9mGqSbz9a2NL3sdh1A0feHIjAZgcCKcAvksNUSauf0/FnIjzTyPRpjRDMeDC8Ci3sGiuO3cvpWJwhZfbjcS26KmBv2CHWXfRRNFYOInHZNIXWNAoBB47Il5bGSMd+uXiGr+SQ9tNvcu+BiJNmFbxYqg+oQ8dGAl1DtvY2wJVY8k7vO9BIWSpyIxfGw7EDifhc5vnOmGe016p6a01C3eVGxgl23UYMrP7+fpjOcPmTSF4rk5U5ljEN3MSYqlf1QEv0OqlI9q1TwTK02VBCjMTYxDHsnt04OjNBkNO8v5uJ4NR+UUDBEp433z53I59uawZ+dbk4v4ZExcl8EGmKm3Gzbal/iJ/F7KQuX2b/ySEhLOFVYFWxK73X1nBvCSK2mC2/8fCw8oI5pmvzJwQhcCKTdEIrz3MMvAHqtPScDUOjzhXxInQOCb3+UBj1PPIdqkYLvZss1TEaBwYZjLkVnK2MBj7BaqT6Rp6+5A/fippUKHsnB6eYMEPR2YgDmCHL+4twxHJG6UWdP3ybaKiiAPy2OHNP6PTZ0HrqHOSJzBSDD+Z8YpaRg29QX3UEWlqnSKaan0VYAsV1VeaN0XFX46/TWO0L5tjhYVXJJYGqo6tIQJymxATLFRF6AZaD1Mwd27IAL04WkmoQoXfO6OFfwdp/shudY/1gBkDBvGPICBPtnqkvhGF+ZF3IRkuPwiFWeXmwBxKHsRx/3+aJu32Ml9+za41zVk2viaxcGqwTc5KMexQFLAUwqhv+aIik7U+5qk/gEVSuRoVkihoweFzKolNF+BknH2oB4rZdPixag5Zje3DvgjsSFlOl69W/67t/Gs8htfSAaHlsB8vWRQr9+v/lxTbrAw+O0E+sYGoObQ4qQMyQshNZEHbpPg63eWiHtJJnrVBvOeIbIHzoLDnMDsWVWZSMzAQ1vhX1H5QLgSEbRlKSliVY03kDkh/Nk/KOn+B2q37Ialq4JcRoIYFGJ8AoYEAD0tRuTqFddIclE75HzwaNG7NyKW1plsa72ciOPwsPJsdd5F0qdSQ3OSKtooTn7uf6dXOc4lDkfrVYRlZ0PX'))\n\n    @needs_test_with_all_aes_implementations\n    def test_encrypt_message(self):\n        key = WalletStorage.get_eckey_from_password('secret_password77')\n        msgs = [\n            bytes([0] * 555),\n            b'cannot think of anything funny'\n        ]\n        for plaintext in msgs:\n            ciphertext1 = crypto.ecies_encrypt_message(key, plaintext)\n            ciphertext2 = crypto.ecies_encrypt_message(key, plaintext)\n            self.assertEqual(plaintext, crypto.ecies_decrypt_message(key, ciphertext1))\n            self.assertEqual(plaintext, crypto.ecies_decrypt_message(key, ciphertext2))\n            self.assertNotEqual(ciphertext1, ciphertext2)\n\n    @needs_test_with_all_aes_implementations\n    def test_aes_homomorphic(self):\n        \"\"\"Make sure AES is homomorphic.\"\"\"\n        payload = u'\\u66f4\\u7a33\\u5b9a\\u7684\\u4ea4\\u6613\\u5e73\\u53f0'\n        password = u'secret'\n        for version in SUPPORTED_PW_HASH_VERSIONS:\n            enc = crypto.pw_encode(payload, password, version=version)\n            dec = crypto.pw_decode(enc, password, version=version)\n            self.assertEqual(dec, payload)\n\n    @needs_test_with_all_aes_implementations\n    def test_aes_encode_without_password(self):\n        \"\"\"When not passed a password, pw_encode is noop on the payload.\"\"\"\n        payload = u'\\u66f4\\u7a33\\u5b9a\\u7684\\u4ea4\\u6613\\u5e73\\u53f0'\n        for version in SUPPORTED_PW_HASH_VERSIONS:\n            enc = crypto.pw_encode(payload, None, version=version)\n            self.assertEqual(payload, enc)\n\n    @needs_test_with_all_aes_implementations\n    def test_aes_deencode_without_password(self):\n        \"\"\"When not passed a password, pw_decode is noop on the payload.\"\"\"\n        payload = u'\\u66f4\\u7a33\\u5b9a\\u7684\\u4ea4\\u6613\\u5e73\\u53f0'\n        for version in SUPPORTED_PW_HASH_VERSIONS:\n            enc = crypto.pw_decode(payload, None, version=version)\n            self.assertEqual(payload, enc)\n\n    @needs_test_with_all_aes_implementations\n    def test_aes_decode_with_invalid_password(self):\n        \"\"\"pw_decode raises an Exception when supplied an invalid password.\"\"\"\n        payload = u\"blah\"\n        password = u\"uber secret\"\n        wrong_password = u\"not the password\"\n        for version in SUPPORTED_PW_HASH_VERSIONS:\n            enc = crypto.pw_encode(payload, password, version=version)\n            with self.assertRaises(InvalidPassword):\n                crypto.pw_decode(enc, wrong_password, version=version)\n        # sometimes the PKCS7 padding gets removed cleanly,\n        # but then UnicodeDecodeError gets raised (internally):\n        enc = 'smJ7j6ccr8LnMOlx98s/ajgikv9s3R1PQuG3GyyIMmo='\n        with self.assertRaises(InvalidPassword):\n            crypto.pw_decode(enc, wrong_password, version=1)\n\n    @needs_test_with_all_chacha20_implementations\n    def test_chacha20_poly1305_encrypt__with_associated_data(self):\n        key = bytes.fromhex('37326d9d69a83b815ddfd947d21b0dd39111e5b6a5a44042c44d570ea03e3179')\n        nonce = bytes.fromhex('010203040506070809101112')\n        associated_data = bytes.fromhex('30c9572d4305d4f3ccb766b1db884da6f1e0086f55136a39740700c272095717')\n        data = bytes.fromhex('4a6cd75da76cedf0a8a47e3a5734a328')\n        self.assertEqual(bytes.fromhex('90fb51fcde1fbe4013500bd7a32280445d80ee21f0aa3acd30df72cf609de064'),\n                         crypto.chacha20_poly1305_encrypt(key=key, nonce=nonce, associated_data=associated_data, data=data))\n\n    @needs_test_with_all_chacha20_implementations\n    def test_chacha20_poly1305_decrypt__with_associated_data(self):\n        key = bytes.fromhex('37326d9d69a83b815ddfd947d21b0dd39111e5b6a5a44042c44d570ea03e3179')\n        nonce = bytes.fromhex('010203040506070809101112')\n        associated_data = bytes.fromhex('30c9572d4305d4f3ccb766b1db884da6f1e0086f55136a39740700c272095717')\n        data = bytes.fromhex('90fb51fcde1fbe4013500bd7a32280445d80ee21f0aa3acd30df72cf609de064')\n        self.assertEqual(bytes.fromhex('4a6cd75da76cedf0a8a47e3a5734a328'),\n                         crypto.chacha20_poly1305_decrypt(key=key, nonce=nonce, associated_data=associated_data, data=data))\n        with self.assertRaises(ValueError):\n            crypto.chacha20_poly1305_decrypt(key=key, nonce=nonce, associated_data=b'', data=data)\n\n    @needs_test_with_all_chacha20_implementations\n    def test_chacha20_poly1305_encrypt__without_associated_data(self):\n        key = bytes.fromhex('37326d9d69a83b815ddfd947d21b0dd39111e5b6a5a44042c44d570ea03e3179')\n        nonce = bytes.fromhex('010203040506070809101112')\n        data = bytes.fromhex('4a6cd75da76cedf0a8a47e3a5734a328')\n        self.assertEqual(bytes.fromhex('90fb51fcde1fbe4013500bd7a322804469c2be9b1385bc5ded5cd96be510280f'),\n                         crypto.chacha20_poly1305_encrypt(key=key, nonce=nonce, data=data))\n        self.assertEqual(bytes.fromhex('90fb51fcde1fbe4013500bd7a322804469c2be9b1385bc5ded5cd96be510280f'),\n                         crypto.chacha20_poly1305_encrypt(key=key, nonce=nonce, data=data, associated_data=b''))\n\n    @needs_test_with_all_chacha20_implementations\n    def test_chacha20_poly1305_decrypt__without_associated_data(self):\n        key = bytes.fromhex('37326d9d69a83b815ddfd947d21b0dd39111e5b6a5a44042c44d570ea03e3179')\n        nonce = bytes.fromhex('010203040506070809101112')\n        data = bytes.fromhex('90fb51fcde1fbe4013500bd7a322804469c2be9b1385bc5ded5cd96be510280f')\n        self.assertEqual(bytes.fromhex('4a6cd75da76cedf0a8a47e3a5734a328'),\n                         crypto.chacha20_poly1305_decrypt(key=key, nonce=nonce, data=data))\n        self.assertEqual(bytes.fromhex('4a6cd75da76cedf0a8a47e3a5734a328'),\n                         crypto.chacha20_poly1305_decrypt(key=key, nonce=nonce, data=data, associated_data=b''))\n\n    @needs_test_with_all_chacha20_implementations\n    def test_chacha20_encrypt__8_byte_nonce(self):\n        key = bytes.fromhex('37326d9d69a83b815ddfd947d21b0dd39111e5b6a5a44042c44d570ea03e3179')\n        nonce = bytes.fromhex('0102030405060708')\n        data = bytes.fromhex('38a0e0a7c865fe9ca31f0730cfcab610f18e6da88dc3790f1d243f711a257c78')\n        ciphertext = crypto.chacha20_encrypt(key=key, nonce=nonce, data=data)\n        self.assertEqual(bytes.fromhex('f62fbd74d197323c7c3d5658476a884d38ee6f4b5500add1e8dc80dcd9c15dff'), ciphertext)\n        self.assertEqual(data, crypto.chacha20_decrypt(key=key, nonce=nonce, data=ciphertext))\n\n    @needs_test_with_all_chacha20_implementations\n    def test_chacha20_encrypt__12_byte_nonce(self):\n        key = bytes.fromhex('37326d9d69a83b815ddfd947d21b0dd39111e5b6a5a44042c44d570ea03e3179')\n        nonce = bytes.fromhex('010203040506070809101112')\n        data = bytes.fromhex('38a0e0a7c865fe9ca31f0730cfcab610f18e6da88dc3790f1d243f711a257c78')\n        ciphertext = crypto.chacha20_encrypt(key=key, nonce=nonce, data=data)\n        self.assertEqual(bytes.fromhex('c0b1cb75c3c23c13f47dab393add738c92c62c4e2546cb3bf2b48269a4184028'), ciphertext)\n        self.assertEqual(data, crypto.chacha20_decrypt(key=key, nonce=nonce, data=ciphertext))\n\n    def test_sha256d(self):\n        self.assertEqual(b'\\x95MZI\\xfdp\\xd9\\xb8\\xbc\\xdb5\\xd2R&x)\\x95\\x7f~\\xf7\\xfalt\\xf8\\x84\\x19\\xbd\\xc5\\xe8\"\\t\\xf4',\n                         sha256d(u\"test\"))\n\n    def test_var_int(self):\n        for i in range(0xfd):\n            self.assertEqual(var_int(i), bfh(\"{:02x}\".format(i)))\n\n        self.assertEqual(var_int(0xfd), bfh(\"fdfd00\"))\n        self.assertEqual(var_int(0xfe), bfh(\"fdfe00\"))\n        self.assertEqual(var_int(0xff), bfh(\"fdff00\"))\n        self.assertEqual(var_int(0x1234), bfh(\"fd3412\"))\n        self.assertEqual(var_int(0xffff), bfh(\"fdffff\"))\n        self.assertEqual(var_int(0x10000), bfh(\"fe00000100\"))\n        self.assertEqual(var_int(0x12345678), bfh(\"fe78563412\"))\n        self.assertEqual(var_int(0xffffffff), bfh(\"feffffffff\"))\n        self.assertEqual(var_int(0x100000000), bfh(\"ff0000000001000000\"))\n        self.assertEqual(var_int(0x0123456789abcdef), bfh(\"ffefcdab8967452301\"))\n\n    def test_op_push(self):\n        self.assertEqual(_op_push(0x00), bfh('00'))\n        self.assertEqual(_op_push(0x12), bfh('12'))\n        self.assertEqual(_op_push(0x4b), bfh('4b'))\n        self.assertEqual(_op_push(0x4c), bfh('4c4c'))\n        self.assertEqual(_op_push(0xfe), bfh('4cfe'))\n        self.assertEqual(_op_push(0xff), bfh('4cff'))\n        self.assertEqual(_op_push(0x100), bfh('4d0001'))\n        self.assertEqual(_op_push(0x1234), bfh('4d3412'))\n        self.assertEqual(_op_push(0xfffe), bfh('4dfeff'))\n        self.assertEqual(_op_push(0xffff), bfh('4dffff'))\n        self.assertEqual(_op_push(0x10000), bfh('4e00000100'))\n        self.assertEqual(_op_push(0x12345678), bfh('4e78563412'))\n\n    def test_script_num_to_hex(self):\n        # test vectors from https://github.com/btcsuite/btcd/blob/fdc2bc867bda6b351191b5872d2da8270df00d13/txscript/scriptnum.go#L77\n        self.assertEqual(script_num_to_bytes(127), bfh('7f'))\n        self.assertEqual(script_num_to_bytes(-127), bfh('ff'))\n        self.assertEqual(script_num_to_bytes(128), bfh('8000'))\n        self.assertEqual(script_num_to_bytes(-128), bfh('8080'))\n        self.assertEqual(script_num_to_bytes(129), bfh('8100'))\n        self.assertEqual(script_num_to_bytes(-129), bfh('8180'))\n        self.assertEqual(script_num_to_bytes(256), bfh('0001'))\n        self.assertEqual(script_num_to_bytes(-256), bfh('0081'))\n        self.assertEqual(script_num_to_bytes(32767), bfh('ff7f'))\n        self.assertEqual(script_num_to_bytes(-32767), bfh('ffff'))\n        self.assertEqual(script_num_to_bytes(32768), bfh('008000'))\n        self.assertEqual(script_num_to_bytes(-32768), bfh('008080'))\n\n    def test_push_script(self):\n        # https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#push-operators\n        self.assertEqual(push_script(b\"\"), bytes([opcodes.OP_0]))\n        self.assertEqual(push_script(b'\\x07'), bytes([opcodes.OP_7]))\n        self.assertEqual(push_script(b'\\x10'), bytes([opcodes.OP_16]))\n        self.assertEqual(push_script(b'\\x81'), bytes([opcodes.OP_1NEGATE]))\n        self.assertEqual(push_script(b'\\x11'), bfh('0111'))\n        self.assertEqual(push_script(75 * b'\\x42'), bfh('4b' + 75 * '42'))\n        self.assertEqual(push_script(76 * b'\\x42'), bytes([opcodes.OP_PUSHDATA1]) + bfh('4c' + 76 * '42'))\n        self.assertEqual(push_script(100 * b'\\x42'), bytes([opcodes.OP_PUSHDATA1]) + bfh('64' + 100 * '42'))\n        self.assertEqual(push_script(255 * b'\\x42'), bytes([opcodes.OP_PUSHDATA1]) + bfh('ff' + 255 * '42'))\n        self.assertEqual(push_script(256 * b'\\x42'), bytes([opcodes.OP_PUSHDATA2]) + bfh('0001' + 256 * '42'))\n        self.assertEqual(push_script(520 * b'\\x42'), bytes([opcodes.OP_PUSHDATA2]) + bfh('0802' + 520 * '42'))\n\n    def test_add_number_to_script(self):\n        # https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#numbers\n        self.assertEqual(add_number_to_script(0), bytes([opcodes.OP_0]))\n        self.assertEqual(add_number_to_script(7), bytes([opcodes.OP_7]))\n        self.assertEqual(add_number_to_script(16), bytes([opcodes.OP_16]))\n        self.assertEqual(add_number_to_script(-1), bytes([opcodes.OP_1NEGATE]))\n        self.assertEqual(add_number_to_script(-127), bfh('01ff'))\n        self.assertEqual(add_number_to_script(-2), bfh('0182'))\n        self.assertEqual(add_number_to_script(17), bfh('0111'))\n        self.assertEqual(add_number_to_script(127), bfh('017f'))\n        self.assertEqual(add_number_to_script(-32767), bfh('02ffff'))\n        self.assertEqual(add_number_to_script(-128), bfh('028080'))\n        self.assertEqual(add_number_to_script(128), bfh('028000'))\n        self.assertEqual(add_number_to_script(32767), bfh('02ff7f'))\n        self.assertEqual(add_number_to_script(-8388607), bfh('03ffffff'))\n        self.assertEqual(add_number_to_script(-32768), bfh('03008080'))\n        self.assertEqual(add_number_to_script(32768), bfh('03008000'))\n        self.assertEqual(add_number_to_script(8388607), bfh('03ffff7f'))\n        self.assertEqual(add_number_to_script(-2147483647), bfh('04ffffffff'))\n        self.assertEqual(add_number_to_script(-8388608), bfh('0400008080'))\n        self.assertEqual(add_number_to_script(8388608), bfh('0400008000'))\n        self.assertEqual(add_number_to_script(2147483647), bfh('04ffffff7f'))\n\n    def test_address_to_script(self):\n        # bech32/bech32m native segwit\n        # test vectors from BIP-0173\n        # note: the ones that are commented out have been invalidated by BIP-0350\n        self.assertEqual(address_to_script('BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4').hex(), '0014751e76e8199196d454941c45d1b3a323f1433bd6')\n        self.assertEqual(address_to_script('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7', net=constants.BitcoinTestnet).hex(), '00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262')\n        self.assertEqual(address_to_script('tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy', net=constants.BitcoinTestnet).hex(), '0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433')\n        # self.assertEqual(address_to_script('bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'), '5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6')\n        # self.assertEqual(address_to_script('BC1SW50QA3JX3S'), '6002751e')\n        # self.assertEqual(address_to_script('bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'), '5210751e76e8199196d454941c45d1b3a323')\n\n        # bech32/bech32m native segwit\n        # test vectors from BIP-0350\n        self.assertEqual(address_to_script('bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y').hex(), '5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6')\n        self.assertEqual(address_to_script('BC1SW50QGDZ25J').hex(), '6002751e')\n        self.assertEqual(address_to_script('bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs').hex(), '5210751e76e8199196d454941c45d1b3a323')\n        self.assertEqual(address_to_script('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0').hex(), '512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')\n        self.assertEqual(address_to_script('tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', net=constants.BitcoinTestnet).hex(), '5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433')\n\n        # invalid addresses (from BIP-0173)\n        for net in [constants.BitcoinMainnet, constants.BitcoinTestnet]:\n            self.assertFalse(is_address('tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty', net=net))\n            self.assertFalse(is_address('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5', net=net))\n            self.assertFalse(is_address('BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2', net=net))\n            self.assertFalse(is_address('bc1rw5uspcuh', net=net))\n            self.assertFalse(is_address('bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90', net=net))\n            self.assertFalse(is_address('BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P', net=net))\n            self.assertFalse(is_address('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7', net=net))\n            self.assertFalse(is_address('bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du', net=net))\n            self.assertFalse(is_address('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv', net=net))\n            self.assertFalse(is_address('bc1gmk9yu', net=net))\n\n        # invalid addresses (from BIP-0350)\n        for net in [constants.BitcoinMainnet, constants.BitcoinTestnet]:\n            self.assertFalse(is_address('tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut', net=net))\n            self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd', net=net))\n            self.assertFalse(is_address('tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf', net=net))\n            self.assertFalse(is_address('BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL', net=net))\n            self.assertFalse(is_address('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh', net=net))\n            self.assertFalse(is_address('tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47', net=net))\n            self.assertFalse(is_address('bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4', net=net))\n            self.assertFalse(is_address('BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R', net=net))\n            self.assertFalse(is_address('bc1pw5dgrnzv', net=net))\n            self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav', net=net))\n            self.assertFalse(is_address('BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P', net=net))\n            self.assertFalse(is_address('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq', net=net))\n            self.assertFalse(is_address('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf', net=net))\n            self.assertFalse(is_address('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j', net=net))\n            self.assertFalse(is_address('bc1gmk9yu', net=net))\n\n        # bech32(m) mixed case:\n        bech32_mixed_case1 = 'BC1QW508D6QEJXTDG4Y5R3zarvary0c5xw7kv8f3t4'\n        self.assertFalse(is_address(bech32_mixed_case1))\n        self.assertTrue(is_address(bech32_mixed_case1.lower()))\n        self.assertTrue(is_address(bech32_mixed_case1.upper()))\n\n        # base58 P2PKH\n        self.assertEqual(address_to_script('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG').hex(), '76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac')\n        self.assertEqual(address_to_script('1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv').hex(), '76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac')\n        self.assertEqual(address_to_script('mutXcGt1CJdkRvXuN2xoz2quAAQYQ59bRX', net=constants.BitcoinTestnet).hex(), '76a9149da64e300c5e4eb4aaffc9c2fd465348d5618ad488ac')\n        self.assertEqual(address_to_script('miqtaRTkU3U8rzwKbEHx3g8FSz8GJtPS3K', net=constants.BitcoinTestnet).hex(), '76a914247d2d5b6334bdfa2038e85b20fc15264f8e5d2788ac')\n\n        # base58 P2SH\n        self.assertEqual(address_to_script('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT').hex(), 'a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487')\n        self.assertEqual(address_to_script('3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji').hex(), 'a914f47c8954e421031ad04ecd8e7752c9479206b9d387')\n        self.assertEqual(address_to_script('2N3LSvr3hv5EVdfcrxg2Yzecf3SRvqyBE4p', net=constants.BitcoinTestnet).hex(), 'a9146eae23d8c4a941316017946fc761a7a6c85561fb87')\n        self.assertEqual(address_to_script('2NE4ZdmxFmUgwu5wtfoN2gVniyMgRDYq1kk', net=constants.BitcoinTestnet).hex(), 'a914e4567743d378957cd2ee7072da74b1203c1a7a0b87')\n\n\n    def test_address_to_payload(self):\n        # bech32 P2WPKH\n        self.assertEqual(\n            address_to_payload('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'),\n            (OnchainOutputType.WITVER0_P2WPKH, bytes.fromhex('751e76e8199196d454941c45d1b3a323f1433bd6')))\n\n        # bech32 P2WSH\n        self.assertEqual(\n            address_to_payload('bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'),\n            (OnchainOutputType.WITVER0_P2WSH, bytes.fromhex('1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262')))\n\n        # bech32m P2TR\n        self.assertEqual(\n            address_to_payload('bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr'),\n            (OnchainOutputType.WITVER1_P2TR, bytes.fromhex('a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c')))\n\n        # base58 P2PKH\n        self.assertEqual(\n            address_to_payload('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'),\n            (OnchainOutputType.P2PKH, bytes.fromhex('28662c67561b95c79d2257d2a93d9d151c977e91')))\n\n        # base58 P2SH\n        self.assertEqual(\n            address_to_payload('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'),\n            (OnchainOutputType.P2SH, bytes.fromhex('2a84cf00d47f699ee7bbc1dea5ec1bdecb4ac154')))\n\n    def test_bech32_decode(self):\n        # bech32 native segwit\n        # test vectors from BIP-0173\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32, 'a', []),\n                         segwit_addr.bech32_decode('A12UEL5L'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32, 'a', []),\n                         segwit_addr.bech32_decode('a12uel5l'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32, 'an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio', []),\n                         segwit_addr.bech32_decode('an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32, 'abcdef', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]),\n                         segwit_addr.bech32_decode('abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32, '1', [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),\n                         segwit_addr.bech32_decode('11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32, 'split', [24, 23, 25, 24, 22, 28, 1, 16, 11, 29, 8, 25, 23, 29, 19, 13, 16, 23, 29, 22, 25, 28, 1, 16, 11, 3, 25, 29, 27, 25, 3, 3, 29, 19, 11, 25, 3, 3, 25, 13, 24, 29, 1, 25, 3, 3, 25, 13]),\n                         segwit_addr.bech32_decode('split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32, '?', []),\n                         segwit_addr.bech32_decode('?1ezyfcl'))\n\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('\\x201nwldj5'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('\\x7f1axkwrx'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('\\x801eym55h'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('pzry9x0s0muk'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('1pzry9x0s0muk'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('x1b4n0q5v'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('li1dgmt3'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('de1lg7wt\\xff'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('A1G7SGD8'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('10a06t8'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('1qzzfhee'))\n\n        # test vectors from BIP-0350\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32M, 'a', []),\n                         segwit_addr.bech32_decode('A1LQFN3A'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32M, 'a', []),\n                         segwit_addr.bech32_decode('a1lqfn3a'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32M, 'an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber1', []),\n                         segwit_addr.bech32_decode('an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32M, 'abcdef', [31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]),\n                         segwit_addr.bech32_decode('abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32M, '1', [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31]),\n                         segwit_addr.bech32_decode('11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32M, 'split', [24, 23, 25, 24, 22, 28, 1, 16, 11, 29, 8, 25, 23, 29, 19, 13, 16, 23, 29, 22, 25, 28, 1, 16, 11, 3, 25, 29, 27, 25, 3, 3, 29, 19, 11, 25, 3, 3, 25, 13, 24, 29, 1, 25, 3, 3, 25, 13]),\n                         segwit_addr.bech32_decode('split1checkupstagehandshakeupstreamerranterredcaperredlc445v'))\n        self.assertEqual(DecodedBech32(segwit_addr.Encoding.BECH32M, '?', []),\n                         segwit_addr.bech32_decode('?1v759aa'))\n\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('\\x201xj0phk'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('\\x7f1g6xzxy'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('\\x801vctc34'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('an84characterslonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11d6pts4'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('qyrz8wqd2c9m'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('1qyrz8wqd2c9m'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('y1b0jsk6g'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('lt1igcx5c0'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('in1muywd'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('mm1crxm3i'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('au1s5cgom'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('M1VUXWEZ'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('16plkw9'))\n        self.assertEqual(DecodedBech32(None, None, None),\n                         segwit_addr.bech32_decode('1p2gdwpf'))\n\n\nclass Test_xprv_xpub(ElectrumTestCase):\n\n    xprv_xpub = (\n        # Taken from test vectors in https://en.bitcoin.it/wiki/BIP_0032_TestVectors\n        {'xprv': 'xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76',\n         'xpub': 'xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy',\n         'xtype': 'standard'},\n        {'xprv': 'yprvAJEYHeNEPcyBoQYM7sGCxDiNCTX65u4ANgZuSGTrKN5YCC9MP84SBayrgaMyZV7zvkHrr3HVPTK853s2SPk4EttPazBZBmz6QfDkXeE8Zr7',\n         'xpub': 'ypub6XDth9u8DzXV1tcpDtoDKMf6kVMaVMn1juVWEesTshcX4zUVvfNgjPJLXrD9N7AdTLnbHFL64KmBn3SNaTe69iZYbYCqLCCNPZKbLz9niQ4',\n         'xtype': 'p2wpkh-p2sh'},\n        {'xprv': 'zprvAWgYBBk7JR8GkraNZJeEodAp2UR1VRWJTXyV1ywuUVs1awUgTiBS1ZTDtLA5F3MFDn1LZzu8dUpSKdT7ToDpvEG6PQu4bJs7zQY47Sd3sEZ',\n         'xpub': 'zpub6jftahH18ngZyLeqfLBFAm7YaWFVttE9pku5pNMX2qPzTjoq1FVgZMmhjecyB2nqFb31gHE9vNvbaggU6vvWpNZbXEWLLUjYjFqG95LNyT8',\n         'xtype': 'p2wpkh'},\n    )\n\n    def _do_test_bip32(self, seed: str, sequence: str):\n        node = BIP32Node.from_rootseed(bfh(seed), xtype='standard')\n        xprv, xpub = node.to_xprv(), node.to_xpub()\n        int_path = convert_bip32_strpath_to_intpath(sequence)\n        for n in int_path:\n            if n & bip32.BIP32_PRIME == 0:\n                xpub2 = BIP32Node.from_xkey(xpub).subkey_at_public_derivation([n]).to_xpub()\n            node = BIP32Node.from_xkey(xprv).subkey_at_private_derivation([n])\n            xprv, xpub = node.to_xprv(), node.to_xpub()\n            if n & bip32.BIP32_PRIME == 0:\n                self.assertEqual(xpub, xpub2)\n\n        return xpub, xprv\n\n    def test_bip32(self):\n        # see https://en.bitcoin.it/wiki/BIP_0032_TestVectors\n        # and https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#Test_Vectors\n        xpub, xprv = self._do_test_bip32(\"000102030405060708090a0b0c0d0e0f\", \"m/0'/1/2'/2/1000000000\")\n        self.assertEqual(\"xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy\", xpub)\n        self.assertEqual(\"xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76\", xprv)\n\n        xpub, xprv = self._do_test_bip32(\"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542\",\"m/0/2147483647'/1/2147483646'/2\")\n        self.assertEqual(\"xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt\", xpub)\n        self.assertEqual(\"xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j\", xprv)\n\n        xpub, xprv = self._do_test_bip32(\"4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be\", \"m/0h\")\n        self.assertEqual(\"xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y\", xpub)\n        self.assertEqual(\"xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L\", xprv)\n\n        xpub, xprv = self._do_test_bip32(\"3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678\", \"m/0h/1h\")\n        self.assertEqual(\"xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt\", xpub)\n        self.assertEqual(\"xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1\", xprv)\n\n    def test_xpub_from_xprv(self):\n        \"\"\"We can derive the xpub key from a xprv.\"\"\"\n        for xprv_details in self.xprv_xpub:\n            result = xpub_from_xprv(xprv_details['xprv'])\n            self.assertEqual(result, xprv_details['xpub'])\n\n    def test_is_xpub(self):\n        for xprv_details in self.xprv_xpub:\n            xpub = xprv_details['xpub']\n            self.assertTrue(is_xpub(xpub))\n        self.assertFalse(is_xpub('xpub1nval1d'))\n        self.assertFalse(is_xpub('xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52WRONGBADWRONG'))\n\n    def test_xpub_type(self):\n        for xprv_details in self.xprv_xpub:\n            xpub = xprv_details['xpub']\n            self.assertEqual(xprv_details['xtype'], xpub_type(xpub))\n\n    def test_is_xprv(self):\n        for xprv_details in self.xprv_xpub:\n            xprv = xprv_details['xprv']\n            self.assertTrue(is_xprv(xprv))\n        self.assertFalse(is_xprv('xprv1nval1d'))\n        self.assertFalse(is_xprv('xprv661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52WRONGBADWRONG'))\n\n    def test_bip32_from_xkey(self):\n        bip32node1 = BIP32Node.from_xkey(\"xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy\")\n        self.assertEqual(\n            BIP32Node(\n                xtype='standard',\n                eckey=ecc.ECPubkey(bytes.fromhex(\"022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011\")),\n                chaincode=bytes.fromhex(\"c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e\"),\n                depth=5,\n                fingerprint=bytes.fromhex(\"d880d7d8\"),\n                child_number=bytes.fromhex(\"3b9aca00\"),\n            ),\n            bip32node1)\n        with self.assertRaises(ValueError):\n            BIP32Node.from_xkey(\n                \"zpub6jftahH18ngZyLeqfLBFAm7YaWFVttE9pku5pNMX2qPzTjoq1FVgZMmhjecyB2nqFb31gHE9vNvbaggU6vvWpNZbXEWLLUjYjFqG95LNyT8\",\n                allow_custom_headers=False)\n        bip32node2 = BIP32Node.from_xkey(\n            \"zpub6jftahH18ngZyLeqfLBFAm7YaWFVttE9pku5pNMX2qPzTjoq1FVgZMmhjecyB2nqFb31gHE9vNvbaggU6vvWpNZbXEWLLUjYjFqG95LNyT8\",\n            allow_custom_headers=True)\n        self.assertEqual(bytes.fromhex(\"03f18e53f3386a5f9a9d2c369ad3b84b429eb397b4bc69ce600f2d833b54ba32f4\"),\n                         bip32node2.eckey.get_public_key_bytes(compressed=True))\n\n    def test_is_bip32_derivation(self):\n        self.assertTrue(is_bip32_derivation(\"m/0'/1\"))\n        self.assertTrue(is_bip32_derivation(\"m/0'/0'\"))\n        self.assertTrue(is_bip32_derivation(\"m/3'/-5/8h/\"))\n        self.assertTrue(is_bip32_derivation(\"m/44'/0'/0'/0/0\"))\n        self.assertTrue(is_bip32_derivation(\"m/49'/0'/0'/0/0\"))\n        self.assertTrue(is_bip32_derivation(\"m\"))\n        self.assertTrue(is_bip32_derivation(\"m/\"))\n        self.assertFalse(is_bip32_derivation(\"m5\"))\n        self.assertFalse(is_bip32_derivation(\"mmmmmm\"))\n        self.assertFalse(is_bip32_derivation(\"n/\"))\n        self.assertFalse(is_bip32_derivation(\"\"))\n        self.assertFalse(is_bip32_derivation(\"m/q8462\"))\n        self.assertFalse(is_bip32_derivation(\"m/-8h\"))\n\n    def test_convert_bip32_strpath_to_intpath(self):\n        self.assertEqual([0, 0x80000001, 0x80000001], convert_bip32_strpath_to_intpath(\"m/0/-1/1'\"))\n        self.assertEqual([], convert_bip32_strpath_to_intpath(\"m/\"))\n        self.assertEqual([2147483692, 2147488889, 221], convert_bip32_strpath_to_intpath(\"m/44'/5241h/221\"))\n\n    def test_convert_bip32_intpath_to_strpath(self):\n        self.assertEqual(\"m/0/1h/1h\", convert_bip32_intpath_to_strpath([0, 0x80000001, 0x80000001]))\n        self.assertEqual(\"m\", convert_bip32_intpath_to_strpath([]))\n        self.assertEqual(\"m/44h/5241h/221\", convert_bip32_intpath_to_strpath([2147483692, 2147488889, 221]))\n\n        self.assertEqual(\"m/0/1'/1'\", convert_bip32_intpath_to_strpath([0, 0x80000001, 0x80000001], hardened_char=\"'\"))\n        self.assertEqual(\"m\", convert_bip32_intpath_to_strpath([], hardened_char=\"'\"))\n        self.assertEqual(\"m/44'/5241'/221\", convert_bip32_intpath_to_strpath([2147483692, 2147488889, 221], hardened_char=\"'\"))\n\n    def test_normalize_bip32_derivation(self):\n        self.assertEqual(\"m/0/1h/1h\", normalize_bip32_derivation(\"m/0/1h/1'\"))\n        self.assertEqual(\"m\", normalize_bip32_derivation(\"m////\"))\n        self.assertEqual(\"m/0/2/1h\", normalize_bip32_derivation(\"m/0/2/-1/\"))\n        self.assertEqual(\"m/0/1h/1h/5h\", normalize_bip32_derivation(\"m/0//-1/1'///5h\"))\n\n    def test_is_xkey_consistent_with_key_origin_info(self):\n        ### actual data (high depth path)\n        self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info(\n            \"Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd\",\n            derivation_prefix=\"m/48'/1'/0'/2'\",\n            root_fingerprint=\"b2768d2f\"))\n        # ok to skip args\n        self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info(\n            \"Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd\",\n            derivation_prefix=\"m/48'/1'/0'/2'\"))\n        self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info(\n            \"Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd\",\n            root_fingerprint=\"b2768d2f\"))\n        # path changed: wrong depth\n        self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info(\n            \"Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd\",\n            derivation_prefix=\"m/48'/0'/2'\",\n            root_fingerprint=\"b2768d2f\"))\n        # path changed: wrong child index\n        self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info(\n            \"Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd\",\n            derivation_prefix=\"m/48'/1'/0'/3'\",\n            root_fingerprint=\"b2768d2f\"))\n        # path changed: but cannot tell\n        self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info(\n            \"Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd\",\n            derivation_prefix=\"m/48'/1'/1'/2'\",\n            root_fingerprint=\"b2768d2f\"))\n        # fp changed: but cannot tell\n        self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info(\n            \"Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd\",\n            derivation_prefix=\"m/48'/1'/0'/2'\",\n            root_fingerprint=\"aaaaaaaa\"))\n\n        ### actual data (depth=1 path)\n        self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info(\n            \"zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ\",\n            derivation_prefix=\"m/0'\",\n            root_fingerprint=\"b2e35a7d\"))\n        # path changed: wrong depth\n        self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info(\n            \"zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ\",\n            derivation_prefix=\"m/0'/0'\",\n            root_fingerprint=\"b2e35a7d\"))\n        # path changed: wrong child index\n        self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info(\n            \"zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ\",\n            derivation_prefix=\"m/1'\",\n            root_fingerprint=\"b2e35a7d\"))\n        # fp changed: can tell\n        self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info(\n            \"zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ\",\n            derivation_prefix=\"m/0'\",\n            root_fingerprint=\"aaaaaaaa\"))\n\n        ### actual data (depth=0 path)\n        self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info(\n            \"xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U\",\n            derivation_prefix=\"m\",\n            root_fingerprint=\"48adc7a0\"))\n        # path changed: wrong depth\n        self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info(\n            \"xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U\",\n            derivation_prefix=\"m/0\",\n            root_fingerprint=\"48adc7a0\"))\n        # fp changed: can tell\n        self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info(\n            \"xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U\",\n            derivation_prefix=\"m\",\n            root_fingerprint=\"aaaaaaaa\"))\n\n    def test_is_all_public_derivation(self):\n        self.assertFalse(is_all_public_derivation(\"m/0/1'/1'\"))\n        self.assertFalse(is_all_public_derivation(\"m/0/2/1'\"))\n        self.assertFalse(is_all_public_derivation(\"m/0/1'/1'/5\"))\n        self.assertTrue(is_all_public_derivation(\"m\"))\n        self.assertTrue(is_all_public_derivation(\"m/0\"))\n        self.assertTrue(is_all_public_derivation(\"m/75/22/3\"))\n\n    def test_xtype_from_derivation(self):\n        self.assertEqual('standard', xtype_from_derivation(\"m/44'\"))\n        self.assertEqual('standard', xtype_from_derivation(\"m/44'/\"))\n        self.assertEqual('standard', xtype_from_derivation(\"m/44'/0'/0'\"))\n        self.assertEqual('standard', xtype_from_derivation(\"m/44'/5241'/221\"))\n        self.assertEqual('standard', xtype_from_derivation(\"m/45'\"))\n        self.assertEqual('standard', xtype_from_derivation(\"m/45'/56165/271'\"))\n        self.assertEqual('p2wpkh-p2sh', xtype_from_derivation(\"m/49'\"))\n        self.assertEqual('p2wpkh-p2sh', xtype_from_derivation(\"m/49'/134\"))\n        self.assertEqual('p2wpkh', xtype_from_derivation(\"m/84'\"))\n        self.assertEqual('p2wpkh', xtype_from_derivation(\"m/84'/112'/992/112/33'/0/2\"))\n        self.assertEqual('p2wsh-p2sh', xtype_from_derivation(\"m/48'/0'/0'/1'\"))\n        self.assertEqual('p2wsh-p2sh', xtype_from_derivation(\"m/48'/0'/0'/1'/52112/52'\"))\n        self.assertEqual('p2wsh-p2sh', xtype_from_derivation(\"m/48'/9'/2'/1'\"))\n        self.assertEqual('p2wsh', xtype_from_derivation(\"m/48'/0'/0'/2'\"))\n        self.assertEqual('p2wsh', xtype_from_derivation(\"m/48'/1'/0'/2'/77'/0\"))\n\n    def test_version_bytes(self):\n        xprv_headers_b58 = {\n            'standard':    'xprv',\n            'p2wpkh-p2sh': 'yprv',\n            'p2wsh-p2sh':  'Yprv',\n            'p2wpkh':      'zprv',\n            'p2wsh':       'Zprv',\n        }\n        xpub_headers_b58 = {\n            'standard':    'xpub',\n            'p2wpkh-p2sh': 'ypub',\n            'p2wsh-p2sh':  'Ypub',\n            'p2wpkh':      'zpub',\n            'p2wsh':       'Zpub',\n        }\n        for xtype, xkey_header_bytes in constants.net.XPRV_HEADERS.items():\n            xkey_header_bytes = bfh(\"%08x\" % xkey_header_bytes)\n            xkey_bytes = xkey_header_bytes + bytes([0] * 74)\n            xkey_b58 = EncodeBase58Check(xkey_bytes)\n            self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype]))\n\n            xkey_bytes = xkey_header_bytes + bytes([255] * 74)\n            xkey_b58 = EncodeBase58Check(xkey_bytes)\n            self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype]))\n\n        for xtype, xkey_header_bytes in constants.net.XPUB_HEADERS.items():\n            xkey_header_bytes = bfh(\"%08x\" % xkey_header_bytes)\n            xkey_bytes = xkey_header_bytes + bytes([0] * 74)\n            xkey_b58 = EncodeBase58Check(xkey_bytes)\n            self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype]))\n\n            xkey_bytes = xkey_header_bytes + bytes([255] * 74)\n            xkey_b58 = EncodeBase58Check(xkey_bytes)\n            self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype]))\n\n\nclass Test_xprv_xpub_testnet(ElectrumTestCase):\n    TESTNET = True\n\n    def test_version_bytes(self):\n        xprv_headers_b58 = {\n            'standard':    'tprv',\n            'p2wpkh-p2sh': 'uprv',\n            'p2wsh-p2sh':  'Uprv',\n            'p2wpkh':      'vprv',\n            'p2wsh':       'Vprv',\n        }\n        xpub_headers_b58 = {\n            'standard':    'tpub',\n            'p2wpkh-p2sh': 'upub',\n            'p2wsh-p2sh':  'Upub',\n            'p2wpkh':      'vpub',\n            'p2wsh':       'Vpub',\n        }\n        for xtype, xkey_header_bytes in constants.net.XPRV_HEADERS.items():\n            xkey_header_bytes = bfh(\"%08x\" % xkey_header_bytes)\n            xkey_bytes = xkey_header_bytes + bytes([0] * 74)\n            xkey_b58 = EncodeBase58Check(xkey_bytes)\n            self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype]))\n\n            xkey_bytes = xkey_header_bytes + bytes([255] * 74)\n            xkey_b58 = EncodeBase58Check(xkey_bytes)\n            self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype]))\n\n        for xtype, xkey_header_bytes in constants.net.XPUB_HEADERS.items():\n            xkey_header_bytes = bfh(\"%08x\" % xkey_header_bytes)\n            xkey_bytes = xkey_header_bytes + bytes([0] * 74)\n            xkey_b58 = EncodeBase58Check(xkey_bytes)\n            self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype]))\n\n            xkey_bytes = xkey_header_bytes + bytes([255] * 74)\n            xkey_b58 = EncodeBase58Check(xkey_bytes)\n            self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype]))\n\n\nclass Test_keyImport(ElectrumTestCase):\n\n    priv_pub_addr = (\n           {'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',\n            'exported_privkey': 'p2pkh:KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',\n            'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997',\n            'address': '17azqT8T16coRmWKYFj3UjzJuxiYrYFRBR',\n            'minikey' : False,\n            'txin_type': 'p2pkh',\n            'compressed': True,\n            'addr_encoding': 'base58',\n            'scripthash': 'c9aecd1fef8d661a42c560bf75c8163e337099800b8face5ca3d1393a30508a7'},\n           {'priv': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD',\n            'exported_privkey': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD',\n            'pub': '0352d78b4b37e0f6d4e164423436f2925fa57817467178eca550a88f2821973c41',\n            'address': '1GXgZ5Qi6gmXTHVSpUPZLy4Ci2nbfb3ZNb',\n            'minikey': False,\n            'txin_type': 'p2pkh',\n            'compressed': True,\n            'addr_encoding': 'base58',\n            'scripthash': 'a9b2a76fc196c553b352186dfcca81fcf323a721cd8431328f8e9d54216818c1'},\n           {'priv': '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',\n            'exported_privkey': 'p2pkh:5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',\n            'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f',\n            'address': '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6',\n            'minikey': False,\n            'txin_type': 'p2pkh',\n            'compressed': False,\n            'addr_encoding': 'base58',\n            'scripthash': 'f5914651408417e1166f725a5829ff9576d0dbf05237055bf13abd2af7f79473'},\n           {'priv': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN',\n            'exported_privkey': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN',\n            'pub': '048f0431b0776e8210376c81280011c2b68be43194cb00bd47b7e9aa66284b713ce09556cde3fee606051a07613f3c159ef3953b8927c96ae3dae94a6ba4182e0e',\n            'address': '147kiRHHm9fqeMQSgqf4k35XzuWLP9fmmS',\n            'minikey': False,\n            'txin_type': 'p2pkh',\n            'compressed': False,\n            'addr_encoding': 'base58',\n            'scripthash': '6dd2e07ad2de9ba8eec4bbe8467eb53f8845acff0d9e6f5627391acc22ff62df'},\n           {'priv': 'LHJnnvRzsdrTX2j5QeWVsaBkabK7gfMNqNNqxnbBVRaJYfk24iJz',\n            'exported_privkey': 'p2wpkh-p2sh:Kz9XebiCXL2BZzhYJViiHDzn5iup1povWV8aqstzWU4sz1K5nVva',\n            'pub': '0279ad237ca0d812fb503ab86f25e15ebd5fa5dd95c193639a8a738dcd1acbad81',\n            'address': '3GeVJB3oKr7psgKR6BTXSxKtWUkfsHHhk7',\n            'minikey': False,\n            'txin_type': 'p2wpkh-p2sh',\n            'compressed': True,\n            'addr_encoding': 'base58',\n            'scripthash': 'd7b04e882fa6b13246829ac552a2b21461d9152eb00f0a6adb58457a3e63d7c5'},\n           {'priv': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW',\n            'exported_privkey': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW',\n            'pub': '0229da20a15b3363b2c28e3c5093c180b56c439df0b968a970366bb1f38435361e',\n            'address': '3C79goMwT7zSTjXnPoCg6VFGAnUpZAkyus',\n            'minikey': False,\n            'txin_type': 'p2wpkh-p2sh',\n            'compressed': True,\n            'addr_encoding': 'base58',\n            'scripthash': '714bf6bfe1083e69539f40d4c7a7dca85d187471b35642e55f20d7e866494cf7'},\n           {'priv': 'L8g5V8kFFeg2WbecahRSdobARbHz2w2STH9S8ePHVSY4fmia7Rsj',\n            'exported_privkey': 'p2wpkh:Kz6SuyPM5VktY5dr2d2YqdVgBA6LCWkiHqXJaC3BzxnMPSUuYzmF',\n            'pub': '03e9f948421aaa89415dc5f281a61b60dde12aae3181b3a76cd2d849b164fc6d0b',\n            'address': 'bc1qqmpt7u5e9hfznljta5gnvhyvfd2kdd0r90hwue',\n            'minikey': False,\n            'txin_type': 'p2wpkh',\n            'compressed': True,\n            'addr_encoding': 'bech32',\n            'scripthash': '1929acaaef3a208c715228e9f1ca0318e3a6b9394ab53c8d026137f847ecf97b'},\n           {'priv': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo',\n            'exported_privkey': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo',\n            'pub': '038c57657171c1f73e34d5b3971d05867d50221ad94980f7e87cbc2344425e6a1e',\n            'address': 'bc1qpakeeg4d9ydyjxd8paqrw4xy9htsg532xzxn50',\n            'minikey': False,\n            'txin_type': 'p2wpkh',\n            'compressed': True,\n            'addr_encoding': 'bech32',\n            'scripthash': '242f02adde84ebb2a7dd778b2f3a81b3826f111da4d8960d826d7a4b816cb261'},\n           # from http://bitscan.com/articles/security/spotlight-on-mini-private-keys\n           {'priv': 'SzavMBLoXU6kDrqtUVmffv',\n            'exported_privkey': 'p2pkh:5Kb8kLf9zgWQnogidDA76MzPL6TsZZY36hWXMssSzNydYXYB9KF',\n            'pub': '04588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9f88ff2a00d7e752d44cbe16e1ebcf0890b76ec7c78886109dee76ccfc8445424',\n            'address': '1CC3X2gu58d6wXUWMffpuzN9JAfTUWu4Kj',\n            'minikey': True,\n            'txin_type': 'p2pkh',\n            'compressed': False,  # this is actually ambiguous... issue #2748\n            'addr_encoding': 'base58',\n            'scripthash': '5b07ddfde826f5125ee823900749103cea37808038ecead5505a766a07c34445'},\n    )\n\n    def test_public_key_from_private_key(self):\n        for priv_details in self.priv_pub_addr:\n            txin_type, privkey, compressed = deserialize_privkey(priv_details['priv'])\n            result = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)\n            self.assertEqual(priv_details['pub'], result)\n            self.assertEqual(priv_details['txin_type'], txin_type)\n            self.assertEqual(priv_details['compressed'], compressed)\n\n    def test_address_from_private_key(self):\n        for priv_details in self.priv_pub_addr:\n            addr2 = address_from_private_key(priv_details['priv'])\n            self.assertEqual(priv_details['address'], addr2)\n\n    def test_is_valid_address(self):\n        for priv_details in self.priv_pub_addr:\n            addr = priv_details['address']\n            self.assertFalse(is_address(priv_details['priv']))\n            self.assertFalse(is_address(priv_details['pub']))\n            self.assertTrue(is_address(addr))\n\n            is_enc_b58 = priv_details['addr_encoding'] == 'base58'\n            self.assertEqual(is_enc_b58, is_b58_address(addr))\n\n            is_enc_bech32 = priv_details['addr_encoding'] == 'bech32'\n            self.assertEqual(is_enc_bech32, is_segwit_address(addr))\n\n        self.assertFalse(is_address(\"not an address\"))\n\n    def test_is_address_bad_checksums(self):\n        self.assertTrue(is_address('1819s5TxxbBtuRPr3qYskMVC8sb1pqapWx'))\n        self.assertFalse(is_address('1819s5TxxbBtuRPr3qYskMVC8sb1pqapWw'))\n\n        self.assertTrue(is_address('3LrjLVnngqnaJeo3BQwMBg34iqYsjZjQUe'))\n        self.assertFalse(is_address('3LrjLVnngqnaJeo3BQwMBg34iqYsjZjQUd'))\n\n        self.assertTrue(is_address('bc1qxq64lrwt02hm7tu25lr3hm9tgzh58snfe67yt6'))\n        self.assertFalse(is_address('bc1qxq64lrwt02hm7tu25lr3hm9tgzh58snfe67yt5'))\n\n    def test_is_private_key(self):\n        for priv_details in self.priv_pub_addr:\n            self.assertTrue(is_private_key(priv_details['priv']))\n            self.assertTrue(is_private_key(priv_details['exported_privkey']))\n            self.assertFalse(is_private_key(priv_details['pub']))\n            self.assertFalse(is_private_key(priv_details['address']))\n        self.assertFalse(is_private_key(\"not a privkey\"))\n\n    def test_serialize_privkey(self):\n        for priv_details in self.priv_pub_addr:\n            txin_type, privkey, compressed = deserialize_privkey(priv_details['priv'])\n            priv2 = serialize_privkey(privkey, compressed, txin_type)\n            self.assertEqual(priv_details['exported_privkey'], priv2)\n\n    def test_address_to_scripthash(self):\n        for priv_details in self.priv_pub_addr:\n            sh = address_to_scripthash(priv_details['address'])\n            self.assertEqual(priv_details['scripthash'], sh)\n\n    def test_is_minikey(self):\n        for priv_details in self.priv_pub_addr:\n            minikey = priv_details['minikey']\n            priv = priv_details['priv']\n            self.assertEqual(minikey, is_minikey(priv))\n\n    def test_is_compressed_privkey(self):\n        for priv_details in self.priv_pub_addr:\n            self.assertEqual(priv_details['compressed'],\n                             is_compressed_privkey(priv_details['priv']))\n\n    def test_segwit_uncompressed_pubkey(self):\n        with self.assertRaises(BitcoinException):\n            is_private_key(\"p2wpkh-p2sh:5JKXxT3wAZHcybJ9YNkuHur9vou6uuAnorBV9A8vVxGNFH5wvTW\",\n                           raise_on_error=True)\n\n    def test_wif_with_invalid_magic_byte_for_compressed_pubkey(self):\n        with self.assertRaises(BitcoinException):\n            is_private_key(\"KwFAa6AumokBD2dVqQLPou42jHiVsvThY1n25HJ8Ji8REf1wxAQb\",\n                           raise_on_error=True)\n\n\nclass TestBaseEncode(ElectrumTestCase):\n\n    def test_base43(self):\n        tx_hex = \"020000000001021cd0e96f9ca202e017ca3465e3c13373c0df3a4cdd91c1fd02ea42a1a65d2a410000000000fdffffff757da7cf8322e5063785e2d8ada74702d2648fa2add2d533ba83c52eb110df690200000000fdffffff02d07e010000000000160014b544c86eaf95e3bb3b6d2cabb12ab40fc59cad9ca086010000000000232102ce0d066fbfcf150a5a1bbc4f312cd2eb080e8d8a47e5f2ce1a63b23215e54fb5ac02483045022100a9856bf10a950810abceeabc9a86e6ba533e130686e3d7863971b9377e7c658a0220288a69ef2b958a7c2ecfa376841d4a13817ed24fa9a0e0a6b9cb48e6439794c701210324e291735f83ff8de47301b12034950b80fa4724926a34d67e413d8ff8817c53024830450221008f885978f7af746679200ed55fe2e86c1303620824721f95cc41eb7965a3dfcf02207872082ac4a3c433d41a203e6d685a459e70e551904904711626ac899238c20a0121023d4c9deae1aacf3f822dd97a28deaec7d4e4ff97be746d124a63d20e582f5b290a971600\"\n        tx_bytes = bfh(tx_hex)\n        tx_base43 = base_encode(tx_bytes, base=43)\n        self.assertEqual(\"3E2DH7.J3PKVZJ3RCOXQVS3Y./6-WE.75DDU0K58-0N1FRL565N8ZH-DG1Z.1IGWTE5HK8F7PWH5P8+V3XGZZ6GQBPHNDE+RD8CAQVV1/6PQEMJIZTGPMIJ93B8P$QX+Y2R:TGT9QW8S89U4N2.+FUT8VG+34USI/N/JJ3CE*KLSW:REE8T5Y*9:U6515JIUR$6TODLYHSDE3B5DAF:5TF7V*VAL3G40WBOM0DO2+CFKTTM$G-SO:8U0EW:M8V:4*R9ZDX$B1IRBP9PLMDK8H801PNTFB4$HL1+/U3F61P$4N:UAO88:N5D+J:HI4YR8IM:3A7K1YZ9VMRC/47$6GGW5JEL1N690TDQ4XW+TWHD:V.1.630QK*JN/.EITVU80YS3.8LWKO:2STLWZAVHUXFHQ..NZ0:.J/FTZM.KYDXIE1VBY7/:PHZMQ$.JZQ2.XT32440X/HM+UY/7QP4I+HTD9.DUSY-8R6HDR-B8/PF2NP7I2-MRW9VPW3U9.S0LQ.*221F8KVMD5ANJXZJ8WV4UFZ4R.$-NXVE+-FAL:WFERGU+WHJTHAP\",\n                         tx_base43)\n        self.assertEqual(tx_bytes,\n                         base_decode(tx_base43, base=43))\n\n    def test_base58(self):\n        data_hex = '0cd394bef396200774544c58a5be0189f3ceb6a41c8da023b099ce547dd4d8071ed6ed647259fba8c26382edbf5165dfd2404e7a8885d88437db16947a116e451a5d1325e3fd075f9d370120d2ab537af69f32e74fc0ba53aaaa637752964b3ac95cfea7'\n        data_bytes = bfh(data_hex)\n        data_base58 = base_encode(data_bytes, base=58)\n        self.assertEqual(\"VuvZ2K5UEcXCVcogny7NH4Evd9UfeYipsTdWuU4jLDhyaESijKtrGWZTFzVZJPjaoC9jFBs3SFtarhDhQhAxkXosUD8PmUb5UXW1tafcoPiCp8jHy7Fe2CUPXAbYuMvAyrkocbe6\",\n                         data_base58)\n        self.assertEqual(data_bytes,\n                         base_decode(data_base58, base=58))\n\n    def test_base58check(self):\n        data_hex = '0cd394bef396200774544c58a5be0189f3ceb6a41c8da023b099ce547dd4d8071ed6ed647259fba8c26382edbf5165dfd2404e7a8885d88437db16947a116e451a5d1325e3fd075f9d370120d2ab537af69f32e74fc0ba53aaaa637752964b3ac95cfea7'\n        data_bytes = bfh(data_hex)\n        data_base58check = EncodeBase58Check(data_bytes)\n        self.assertEqual(\"4GCCJsjHqFbHxWbFBvRg35cSeNLHKeNqkXqFHW87zRmz6iP1dJU9Tk2KHZkoKj45jzVsSV4ZbQ8GpPwko6V3Z7cRfux3zJhUw7TZB6Kpa8Vdya8cMuUtL5Ry3CLtMetaY42u52X7Ey6MAH\",\n                         data_base58check)\n        self.assertEqual(data_bytes,\n                         DecodeBase58Check(data_base58check))\n\n\nclass TestTaprootHelpers(ElectrumTestCase):\n\n    def test_taproot_tweak_homomorphism(self):\n        # For any byte string h it holds that\n        # taproot_tweak_pubkey(pubkey_gen(seckey), h)[1] == pubkey_gen(taproot_tweak_seckey(seckey, h)).\n        for secret_scalar in (8, 11, 99999):\n            privkey = ecc.ECPrivkey.from_secret_scalar(secret_scalar)\n            pubkey32 = privkey.get_public_key_bytes(compressed=True)[1:]\n            for tree_hash in (b\"\", b\"satoshi\", b\"1234\"*8, bytes(range(100)), ):\n                tweaked_pubkey = taproot_tweak_pubkey(pubkey32, tree_hash)[1]\n                tweaked_seckey = taproot_tweak_seckey(privkey.get_secret_bytes(), tree_hash)\n                self.assertEqual(tweaked_pubkey, ecc.ECPrivkey(tweaked_seckey).get_public_key_bytes(compressed=True)[1:])\n\n    def test_taproot_output_script(self):\n        # test vectors from https://github.com/bitcoin/bips/blob/70d9b07ab80ab3c267ece48f74e4e2250226d0cc/bip-0341/wallet-test-vectors.json\n        test_vector_file = os.path.join(os.path.dirname(__file__), \"bip-0341\", \"wallet-test-vectors.json\")\n        with open(test_vector_file, \"r\") as f:\n            vectors = json.load(f)\n        def transform_tree(tree_node):\n            if isinstance(tree_node, dict):\n                return (tree_node[\"leafVersion\"], bfh(tree_node[\"script\"]))\n            assert len(tree_node) == 2, len(tree_node)\n            return [transform_tree(tree_node[0]), transform_tree(tree_node[1])]\n        def flatten_tree(tree_node):\n            if isinstance(tree_node, tuple):\n                return [tree_node]\n            assert len(tree_node) == 2, len(tree_node)\n            return flatten_tree(tree_node[0]) + flatten_tree(tree_node[1])\n        assert len(vectors[\"scriptPubKey\"]) > 0, \"test vectors missing\"\n        for tcase in vectors[\"scriptPubKey\"]:\n            script_tree = transform_tree(tcase[\"given\"][\"scriptTree\"]) if tcase[\"given\"][\"scriptTree\"] else None\n            internal_pubkey = bfh(tcase[\"given\"][\"internalPubkey\"])\n            spk = taproot_output_script(internal_pubkey, script_tree=script_tree)\n            self.assertEqual(bfh(tcase[\"expected\"][\"scriptPubKey\"]), spk)\n            self.assertEqual(tcase[\"expected\"][\"bip350Address\"], bitcoin.script_to_address(spk))\n            if script_tree:\n                flat_tree = flatten_tree(script_tree)\n                for script_num, jcontrol_block in enumerate(tcase[\"expected\"][\"scriptPathControlBlocks\"]):\n                    leaf_script, control_block = control_block_for_taproot_script_spend(\n                        internal_pubkey=internal_pubkey, script_tree=script_tree, script_num=script_num)\n                    self.assertEqual(jcontrol_block, control_block.hex())\n                    self.assertEqual(flat_tree[script_num][1].hex(), leaf_script.hex())\n"
  },
  {
    "path": "tests/test_blockchain.py",
    "content": "import shutil\nimport tempfile\nimport os\n\nfrom electrum import constants, blockchain\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.blockchain import Blockchain, deserialize_header, hash_header, InvalidHeader\nfrom electrum.util import bfh, make_dir\n\nfrom . import ElectrumTestCase\n\n\nclass TestBlockchain(ElectrumTestCase):\n\n    HEADERS = {\n        'A': deserialize_header(bfh(\"0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff7f2002000000\"), 0),\n        'B': deserialize_header(bfh(\"0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f186c8dfd970a4545f79916bc1d75c9d00432f57c89209bf3bb115b7612848f509c25f45bffff7f2000000000\"), 1),\n        'C': deserialize_header(bfh(\"00000020686bdfc6a3db73d5d93e8c9663a720a26ecb1ef20eb05af11b36cdbc57c19f7ebf2cbf153013a1c54abaf70e95198fcef2f3059cc6b4d0f7e876808e7d24d11cc825f45bffff7f2000000000\"), 2),\n        'D': deserialize_header(bfh(\"00000020122baa14f3ef54985ae546d1611559e3f487bd2a0f46e8dbb52fbacc9e237972e71019d7feecd9b8596eca9a67032c5f4641b23b5d731dc393e37de7f9c2f299e725f45bffff7f2000000000\"), 3),\n        'E': deserialize_header(bfh(\"00000020f8016f7ef3a17d557afe05d4ea7ab6bde1b2247b7643896c1b63d43a1598b747a3586da94c71753f27c075f57f44faf913c31177a0957bbda42e7699e3a2141aed25f45bffff7f2001000000\"), 4),\n        'F': deserialize_header(bfh(\"000000201d589c6643c1d121d73b0573e5ee58ab575b8fdf16d507e7e915c5fbfbbfd05e7aee1d692d1615c3bdf52c291032144ce9e3b258a473c17c745047f3431ff8e2ee25f45bffff7f2000000000\"), 5),\n        'O': deserialize_header(bfh(\"00000020b833ed46eea01d4c980f59feee44a66aa1162748b6801029565d1466790c405c3a141ce635cbb1cd2b3a4fcdd0a3380517845ba41736c82a79cab535d31128066526f45bffff7f2001000000\"), 6),\n        'P': deserialize_header(bfh(\"00000020abe8e119d1877c9dc0dc502d1a253fb9a67967c57732d2f71ee0280e8381ff0a9690c2fe7c1a4450c74dc908fe94dd96c3b0637d51475e9e06a78e944a0c7fe28126f45bffff7f2000000000\"), 7),\n        'Q': deserialize_header(bfh(\"000000202ce41d94eb70e1518bc1f72523f84a903f9705d967481e324876e1f8cf4d3452148be228a4c3f2061bafe7efdfc4a8d5a94759464b9b5c619994d45dfcaf49e1a126f45bffff7f2000000000\"), 8),\n        'R': deserialize_header(bfh(\"00000020552755b6c59f3d51e361d16281842a4e166007799665b5daed86a063dd89857415681cb2d00ff889193f6a68a93f5096aeb2d84ca0af6185a462555822552221a626f45bffff7f2000000000\"), 9),\n        'S': deserialize_header(bfh(\"00000020a13a491cbefc93cd1bb1938f19957e22a134faf14c7dee951c45533e2c750f239dc087fc977b06c24a69c682d1afd1020e6dc1f087571ccec66310a786e1548fab26f45bffff7f2000000000\"), 10),\n        'T': deserialize_header(bfh(\"00000020dbf3a9b55dfefbaf8b6e43a89cf833fa2e208bbc0c1c5d76c0d71b9e4a65337803b243756c25053253aeda309604363460a3911015929e68705bd89dff6fe064b026f45bffff7f2002000000\"), 11),\n        'U': deserialize_header(bfh(\"000000203d0932b3b0c78eccb39a595a28ae4a7c966388648d7783fd1305ec8d40d4fe5fd67cb902a7d807cee7676cb543feec3e053aa824d5dfb528d5b94f9760313d9db726f45bffff7f2001000000\"), 12),\n        'G': deserialize_header(bfh(\"00000020b833ed46eea01d4c980f59feee44a66aa1162748b6801029565d1466790c405c3a141ce635cbb1cd2b3a4fcdd0a3380517845ba41736c82a79cab535d31128066928f45bffff7f2001000000\"), 6),\n        'H': deserialize_header(bfh(\"00000020e19e687f6e7f83ca394c114144dbbbc4f3f9c9450f66331a125413702a2e1a719690c2fe7c1a4450c74dc908fe94dd96c3b0637d51475e9e06a78e944a0c7fe26a28f45bffff7f2002000000\"), 7),\n        'I': deserialize_header(bfh(\"0000002009dcb3b158293c89d7cf7ceeb513add122ebc3880a850f47afbb2747f5e48c54148be228a4c3f2061bafe7efdfc4a8d5a94759464b9b5c619994d45dfcaf49e16a28f45bffff7f2000000000\"), 8),\n        'J': deserialize_header(bfh(\"000000206a65f3bdd3374a5a6c4538008ba0b0a560b8566291f9ef4280ab877627a1742815681cb2d00ff889193f6a68a93f5096aeb2d84ca0af6185a462555822552221c928f45bffff7f2000000000\"), 9),\n        'K': deserialize_header(bfh(\"00000020bb3b421653548991998f96f8ba486b652fdb07ca16e9cee30ece033547cd1a6e9dc087fc977b06c24a69c682d1afd1020e6dc1f087571ccec66310a786e1548fca28f45bffff7f2000000000\"), 10),\n        'L': deserialize_header(bfh(\"00000020c391d74d37c24a130f4bf4737932bdf9e206dd4fad22860ec5408978eb55d46303b243756c25053253aeda309604363460a3911015929e68705bd89dff6fe064ca28f45bffff7f2000000000\"), 11),\n        'M': deserialize_header(bfh(\"000000206a65f3bdd3374a5a6c4538008ba0b0a560b8566291f9ef4280ab877627a1742815681cb2d00ff889193f6a68a93f5096aeb2d84ca0af6185a4625558225522214229f45bffff7f2000000000\"), 9),\n        'N': deserialize_header(bfh(\"00000020383dab38b57f98aa9b4f0d5ff868bc674b4828d76766bf048296f4c45fff680a9dc087fc977b06c24a69c682d1afd1020e6dc1f087571ccec66310a786e1548f4329f45bffff7f2003000000\"), 10),\n        'X': deserialize_header(bfh(\"0000002067f1857f54b7fef732cb4940f7d1b339472b3514660711a820330fd09d8fba6b03b243756c25053253aeda309604363460a3911015929e68705bd89dff6fe0649b29f45bffff7f2002000000\"), 11),\n        'Y': deserialize_header(bfh(\"00000020db33c9768a9e5f7c37d0f09aad88d48165946c87d08f7d63793f07b5c08c527fd67cb902a7d807cee7676cb543feec3e053aa824d5dfb528d5b94f9760313d9d9b29f45bffff7f2000000000\"), 12),\n        'Z': deserialize_header(bfh(\"0000002047822b67940e337fda38be6f13390b3596e4dea2549250256879722073824e7f0f2596c29203f8a0f71ae94193092dc8f113be3dbee4579f1e649fa3d6dcc38c622ef45bffff7f2003000000\"), 13),\n    }\n    # tree of headers:\n    #                                            - M <- N <- X <- Y <- Z\n    #                                          /\n    #                             - G <- H <- I <- J <- K <- L\n    #                           /\n    # A <- B <- C <- D <- E <- F <- O <- P <- Q <- R <- S <- T <- U\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        constants.BitcoinRegtest.set_as_network()\n\n    @classmethod\n    def tearDownClass(cls):\n        super().tearDownClass()\n        constants.BitcoinMainnet.set_as_network()\n\n    def setUp(self):\n        super().setUp()\n        self.data_dir = self.electrum_path\n        make_dir(os.path.join(self.data_dir, 'forks'))\n        self.config = SimpleConfig({'electrum_path': self.data_dir})\n        blockchain.blockchains = {}\n\n    def _append_header(self, chain: Blockchain, header: dict):\n        self.assertTrue(chain.can_connect(header))\n        chain.save_header(header)\n\n    def test_get_height_of_last_common_block_with_chain(self):\n        blockchain.blockchains[constants.net.GENESIS] = chain_u = Blockchain(\n            config=self.config, forkpoint=0, parent=None,\n            forkpoint_hash=constants.net.GENESIS, prev_hash=None)\n        open(chain_u.path(), 'w+').close()\n        self._append_header(chain_u, self.HEADERS['A'])\n        self._append_header(chain_u, self.HEADERS['B'])\n        self._append_header(chain_u, self.HEADERS['C'])\n        self._append_header(chain_u, self.HEADERS['D'])\n        self._append_header(chain_u, self.HEADERS['E'])\n        self._append_header(chain_u, self.HEADERS['F'])\n        self._append_header(chain_u, self.HEADERS['O'])\n        self._append_header(chain_u, self.HEADERS['P'])\n        self._append_header(chain_u, self.HEADERS['Q'])\n\n        chain_l = chain_u.fork(self.HEADERS['G'])\n        self._append_header(chain_l, self.HEADERS['H'])\n        self._append_header(chain_l, self.HEADERS['I'])\n        self._append_header(chain_l, self.HEADERS['J'])\n        self._append_header(chain_l, self.HEADERS['K'])\n        self._append_header(chain_l, self.HEADERS['L'])\n\n        self.assertEqual({chain_u:  8, chain_l: 5}, chain_u.get_parent_heights())\n        self.assertEqual({chain_l: 11},             chain_l.get_parent_heights())\n\n        chain_z = chain_l.fork(self.HEADERS['M'])\n        self._append_header(chain_z, self.HEADERS['N'])\n        self._append_header(chain_z, self.HEADERS['X'])\n        self._append_header(chain_z, self.HEADERS['Y'])\n        self._append_header(chain_z, self.HEADERS['Z'])\n\n        self.assertEqual({chain_u:  8, chain_z: 5}, chain_u.get_parent_heights())\n        self.assertEqual({chain_l: 11, chain_z: 8}, chain_l.get_parent_heights())\n        self.assertEqual({chain_z: 13},             chain_z.get_parent_heights())\n        self.assertEqual(5, chain_u.get_height_of_last_common_block_with_chain(chain_l))\n        self.assertEqual(5, chain_l.get_height_of_last_common_block_with_chain(chain_u))\n        self.assertEqual(5, chain_u.get_height_of_last_common_block_with_chain(chain_z))\n        self.assertEqual(5, chain_z.get_height_of_last_common_block_with_chain(chain_u))\n        self.assertEqual(8, chain_l.get_height_of_last_common_block_with_chain(chain_z))\n        self.assertEqual(8, chain_z.get_height_of_last_common_block_with_chain(chain_l))\n\n        self._append_header(chain_u, self.HEADERS['R'])\n        self._append_header(chain_u, self.HEADERS['S'])\n        self._append_header(chain_u, self.HEADERS['T'])\n        self._append_header(chain_u, self.HEADERS['U'])\n\n        self.assertEqual({chain_u: 12, chain_z: 5}, chain_u.get_parent_heights())\n        self.assertEqual({chain_l: 11, chain_z: 8}, chain_l.get_parent_heights())\n        self.assertEqual({chain_z: 13},             chain_z.get_parent_heights())\n        self.assertEqual(5, chain_u.get_height_of_last_common_block_with_chain(chain_l))\n        self.assertEqual(5, chain_l.get_height_of_last_common_block_with_chain(chain_u))\n        self.assertEqual(5, chain_u.get_height_of_last_common_block_with_chain(chain_z))\n        self.assertEqual(5, chain_z.get_height_of_last_common_block_with_chain(chain_u))\n        self.assertEqual(8, chain_l.get_height_of_last_common_block_with_chain(chain_z))\n        self.assertEqual(8, chain_z.get_height_of_last_common_block_with_chain(chain_l))\n\n    def test_parents_after_forking(self):\n        blockchain.blockchains[constants.net.GENESIS] = chain_u = Blockchain(\n            config=self.config, forkpoint=0, parent=None,\n            forkpoint_hash=constants.net.GENESIS, prev_hash=None)\n        open(chain_u.path(), 'w+').close()\n        self._append_header(chain_u, self.HEADERS['A'])\n        self._append_header(chain_u, self.HEADERS['B'])\n        self._append_header(chain_u, self.HEADERS['C'])\n        self._append_header(chain_u, self.HEADERS['D'])\n        self._append_header(chain_u, self.HEADERS['E'])\n        self._append_header(chain_u, self.HEADERS['F'])\n        self._append_header(chain_u, self.HEADERS['O'])\n        self._append_header(chain_u, self.HEADERS['P'])\n        self._append_header(chain_u, self.HEADERS['Q'])\n\n        self.assertEqual(None, chain_u.parent)\n\n        chain_l = chain_u.fork(self.HEADERS['G'])\n        self._append_header(chain_l, self.HEADERS['H'])\n        self._append_header(chain_l, self.HEADERS['I'])\n        self._append_header(chain_l, self.HEADERS['J'])\n        self._append_header(chain_l, self.HEADERS['K'])\n        self._append_header(chain_l, self.HEADERS['L'])\n\n        self.assertEqual(None,    chain_l.parent)\n        self.assertEqual(chain_l, chain_u.parent)\n\n        chain_z = chain_l.fork(self.HEADERS['M'])\n        self._append_header(chain_z, self.HEADERS['N'])\n        self._append_header(chain_z, self.HEADERS['X'])\n        self._append_header(chain_z, self.HEADERS['Y'])\n        self._append_header(chain_z, self.HEADERS['Z'])\n\n        self.assertEqual(chain_z, chain_u.parent)\n        self.assertEqual(chain_z, chain_l.parent)\n        self.assertEqual(None,    chain_z.parent)\n\n        self._append_header(chain_u, self.HEADERS['R'])\n        self._append_header(chain_u, self.HEADERS['S'])\n        self._append_header(chain_u, self.HEADERS['T'])\n        self._append_header(chain_u, self.HEADERS['U'])\n\n        self.assertEqual(chain_z, chain_u.parent)\n        self.assertEqual(chain_z, chain_l.parent)\n        self.assertEqual(None,    chain_z.parent)\n\n    def test_forking_and_swapping(self):\n        blockchain.blockchains[constants.net.GENESIS] = chain_u = Blockchain(\n            config=self.config, forkpoint=0, parent=None,\n            forkpoint_hash=constants.net.GENESIS, prev_hash=None)\n        open(chain_u.path(), 'w+').close()\n\n        self._append_header(chain_u, self.HEADERS['A'])\n        self._append_header(chain_u, self.HEADERS['B'])\n        self._append_header(chain_u, self.HEADERS['C'])\n        self._append_header(chain_u, self.HEADERS['D'])\n        self._append_header(chain_u, self.HEADERS['E'])\n        self._append_header(chain_u, self.HEADERS['F'])\n        self._append_header(chain_u, self.HEADERS['O'])\n        self._append_header(chain_u, self.HEADERS['P'])\n        self._append_header(chain_u, self.HEADERS['Q'])\n        self._append_header(chain_u, self.HEADERS['R'])\n\n        chain_l = chain_u.fork(self.HEADERS['G'])\n        self._append_header(chain_l, self.HEADERS['H'])\n        self._append_header(chain_l, self.HEADERS['I'])\n        self._append_header(chain_l, self.HEADERS['J'])\n\n        # do checks\n        self.assertEqual(2, len(blockchain.blockchains))\n        self.assertEqual(1, len(os.listdir(os.path.join(self.data_dir, \"forks\"))))\n        self.assertEqual(0, chain_u.forkpoint)\n        self.assertEqual(None, chain_u.parent)\n        self.assertEqual(constants.net.GENESIS, chain_u._forkpoint_hash)\n        self.assertEqual(None, chain_u._prev_hash)\n        self.assertEqual(os.path.join(self.data_dir, \"blockchain_headers\"), chain_u.path())\n        self.assertEqual(10 * 80, os.stat(chain_u.path()).st_size)\n        self.assertEqual(6, chain_l.forkpoint)\n        self.assertEqual(chain_u, chain_l.parent)\n        self.assertEqual(hash_header(self.HEADERS['G']), chain_l._forkpoint_hash)\n        self.assertEqual(hash_header(self.HEADERS['F']), chain_l._prev_hash)\n        self.assertEqual(os.path.join(self.data_dir, \"forks\", \"fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_711a2e2a701354121a33660f45c9f9f3c4bbdb4441114c39ca837f6e7f689ee1\"), chain_l.path())\n        self.assertEqual(4 * 80, os.stat(chain_l.path()).st_size)\n\n        self._append_header(chain_l, self.HEADERS['K'])\n\n        # chains were swapped, do checks\n        self.assertEqual(2, len(blockchain.blockchains))\n        self.assertEqual(1, len(os.listdir(os.path.join(self.data_dir, \"forks\"))))\n        self.assertEqual(6, chain_u.forkpoint)\n        self.assertEqual(chain_l, chain_u.parent)\n        self.assertEqual(hash_header(self.HEADERS['O']), chain_u._forkpoint_hash)\n        self.assertEqual(hash_header(self.HEADERS['F']), chain_u._prev_hash)\n        self.assertEqual(os.path.join(self.data_dir, \"forks\", \"fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_aff81830e28e01ef7d23277c56779a6b93f251a2d50dcc09d7c87d119e1e8ab\"), chain_u.path())\n        self.assertEqual(4 * 80, os.stat(chain_u.path()).st_size)\n        self.assertEqual(0, chain_l.forkpoint)\n        self.assertEqual(None, chain_l.parent)\n        self.assertEqual(constants.net.GENESIS, chain_l._forkpoint_hash)\n        self.assertEqual(None, chain_l._prev_hash)\n        self.assertEqual(os.path.join(self.data_dir, \"blockchain_headers\"), chain_l.path())\n        self.assertEqual(11 * 80, os.stat(chain_l.path()).st_size)\n        for b in (chain_u, chain_l):\n            self.assertTrue(all([b.can_connect(b.read_header(i), check_height=False) for i in range(b.height())]))\n\n        self._append_header(chain_u, self.HEADERS['S'])\n        self._append_header(chain_u, self.HEADERS['T'])\n        self._append_header(chain_u, self.HEADERS['U'])\n        self._append_header(chain_l, self.HEADERS['L'])\n\n        chain_z = chain_l.fork(self.HEADERS['M'])\n        self._append_header(chain_z, self.HEADERS['N'])\n        self._append_header(chain_z, self.HEADERS['X'])\n        self._append_header(chain_z, self.HEADERS['Y'])\n        self._append_header(chain_z, self.HEADERS['Z'])\n\n        # chain_z became best chain, do checks\n        self.assertEqual(3, len(blockchain.blockchains))\n        self.assertEqual(2, len(os.listdir(os.path.join(self.data_dir, \"forks\"))))\n        self.assertEqual(0, chain_z.forkpoint)\n        self.assertEqual(None, chain_z.parent)\n        self.assertEqual(constants.net.GENESIS, chain_z._forkpoint_hash)\n        self.assertEqual(None, chain_z._prev_hash)\n        self.assertEqual(os.path.join(self.data_dir, \"blockchain_headers\"), chain_z.path())\n        self.assertEqual(14 * 80, os.stat(chain_z.path()).st_size)\n        self.assertEqual(9, chain_l.forkpoint)\n        self.assertEqual(chain_z, chain_l.parent)\n        self.assertEqual(hash_header(self.HEADERS['J']), chain_l._forkpoint_hash)\n        self.assertEqual(hash_header(self.HEADERS['I']), chain_l._prev_hash)\n        self.assertEqual(os.path.join(self.data_dir, \"forks\", \"fork2_9_2874a1277687ab8042eff9916256b860a5b0a08b0038456c5a4a37d3bdf3656a_6e1acd473503ce0ee3cee916ca07db2f656b48baf8968f999189545316423bbb\"), chain_l.path())\n        self.assertEqual(3 * 80, os.stat(chain_l.path()).st_size)\n        self.assertEqual(6, chain_u.forkpoint)\n        self.assertEqual(chain_z, chain_u.parent)\n        self.assertEqual(hash_header(self.HEADERS['O']), chain_u._forkpoint_hash)\n        self.assertEqual(hash_header(self.HEADERS['F']), chain_u._prev_hash)\n        self.assertEqual(os.path.join(self.data_dir, \"forks\", \"fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_aff81830e28e01ef7d23277c56779a6b93f251a2d50dcc09d7c87d119e1e8ab\"), chain_u.path())\n        self.assertEqual(7 * 80, os.stat(chain_u.path()).st_size)\n        for b in (chain_u, chain_l, chain_z):\n            self.assertTrue(all([b.can_connect(b.read_header(i), check_height=False) for i in range(b.height())]))\n\n        self.assertEqual(constants.net.GENESIS, chain_z.get_hash(0))\n        self.assertEqual(hash_header(self.HEADERS['F']), chain_z.get_hash(5))\n        self.assertEqual(hash_header(self.HEADERS['G']), chain_z.get_hash(6))\n        self.assertEqual(hash_header(self.HEADERS['I']), chain_z.get_hash(8))\n        self.assertEqual(hash_header(self.HEADERS['M']), chain_z.get_hash(9))\n        self.assertEqual(hash_header(self.HEADERS['Z']), chain_z.get_hash(13))\n\n    def test_doing_multiple_swaps_after_single_new_header(self):\n        blockchain.blockchains[constants.net.GENESIS] = chain_u = Blockchain(\n            config=self.config, forkpoint=0, parent=None,\n            forkpoint_hash=constants.net.GENESIS, prev_hash=None)\n        open(chain_u.path(), 'w+').close()\n\n        self._append_header(chain_u, self.HEADERS['A'])\n        self._append_header(chain_u, self.HEADERS['B'])\n        self._append_header(chain_u, self.HEADERS['C'])\n        self._append_header(chain_u, self.HEADERS['D'])\n        self._append_header(chain_u, self.HEADERS['E'])\n        self._append_header(chain_u, self.HEADERS['F'])\n        self._append_header(chain_u, self.HEADERS['O'])\n        self._append_header(chain_u, self.HEADERS['P'])\n        self._append_header(chain_u, self.HEADERS['Q'])\n        self._append_header(chain_u, self.HEADERS['R'])\n        self._append_header(chain_u, self.HEADERS['S'])\n\n        self.assertEqual(1, len(blockchain.blockchains))\n        self.assertEqual(0, len(os.listdir(os.path.join(self.data_dir, \"forks\"))))\n\n        chain_l = chain_u.fork(self.HEADERS['G'])\n        self._append_header(chain_l, self.HEADERS['H'])\n        self._append_header(chain_l, self.HEADERS['I'])\n        self._append_header(chain_l, self.HEADERS['J'])\n        self._append_header(chain_l, self.HEADERS['K'])\n        # now chain_u is best chain, but it's tied with chain_l\n\n        self.assertEqual(2, len(blockchain.blockchains))\n        self.assertEqual(1, len(os.listdir(os.path.join(self.data_dir, \"forks\"))))\n\n        chain_z = chain_l.fork(self.HEADERS['M'])\n        self._append_header(chain_z, self.HEADERS['N'])\n        self._append_header(chain_z, self.HEADERS['X'])\n\n        self.assertEqual(3, len(blockchain.blockchains))\n        self.assertEqual(2, len(os.listdir(os.path.join(self.data_dir, \"forks\"))))\n\n        # chain_z became best chain, do checks\n        self.assertEqual(0, chain_z.forkpoint)\n        self.assertEqual(None, chain_z.parent)\n        self.assertEqual(constants.net.GENESIS, chain_z._forkpoint_hash)\n        self.assertEqual(None, chain_z._prev_hash)\n        self.assertEqual(os.path.join(self.data_dir, \"blockchain_headers\"), chain_z.path())\n        self.assertEqual(12 * 80, os.stat(chain_z.path()).st_size)\n        self.assertEqual(9, chain_l.forkpoint)\n        self.assertEqual(chain_z, chain_l.parent)\n        self.assertEqual(hash_header(self.HEADERS['J']), chain_l._forkpoint_hash)\n        self.assertEqual(hash_header(self.HEADERS['I']), chain_l._prev_hash)\n        self.assertEqual(os.path.join(self.data_dir, \"forks\", \"fork2_9_2874a1277687ab8042eff9916256b860a5b0a08b0038456c5a4a37d3bdf3656a_6e1acd473503ce0ee3cee916ca07db2f656b48baf8968f999189545316423bbb\"), chain_l.path())\n        self.assertEqual(2 * 80, os.stat(chain_l.path()).st_size)\n        self.assertEqual(6, chain_u.forkpoint)\n        self.assertEqual(chain_z, chain_u.parent)\n        self.assertEqual(hash_header(self.HEADERS['O']), chain_u._forkpoint_hash)\n        self.assertEqual(hash_header(self.HEADERS['F']), chain_u._prev_hash)\n        self.assertEqual(os.path.join(self.data_dir, \"forks\", \"fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_aff81830e28e01ef7d23277c56779a6b93f251a2d50dcc09d7c87d119e1e8ab\"), chain_u.path())\n        self.assertEqual(5 * 80, os.stat(chain_u.path()).st_size)\n\n        self.assertEqual(constants.net.GENESIS, chain_z.get_hash(0))\n        self.assertEqual(hash_header(self.HEADERS['F']), chain_z.get_hash(5))\n        self.assertEqual(hash_header(self.HEADERS['G']), chain_z.get_hash(6))\n        self.assertEqual(hash_header(self.HEADERS['I']), chain_z.get_hash(8))\n        self.assertEqual(hash_header(self.HEADERS['M']), chain_z.get_hash(9))\n        self.assertEqual(hash_header(self.HEADERS['X']), chain_z.get_hash(11))\n\n        for b in (chain_u, chain_l, chain_z):\n            self.assertTrue(all([b.can_connect(b.read_header(i), check_height=False) for i in range(b.height())]))\n\n    def get_chains_that_contain_header_helper(self, header: dict):\n        height = header['block_height']\n        header_hash = hash_header(header)\n        return blockchain.get_chains_that_contain_header(height, header_hash)\n\n    def test_get_chains_that_contain_header(self):\n        blockchain.blockchains[constants.net.GENESIS] = chain_u = Blockchain(\n            config=self.config, forkpoint=0, parent=None,\n            forkpoint_hash=constants.net.GENESIS, prev_hash=None)\n        open(chain_u.path(), 'w+').close()\n        self._append_header(chain_u, self.HEADERS['A'])\n        self._append_header(chain_u, self.HEADERS['B'])\n        self._append_header(chain_u, self.HEADERS['C'])\n        self._append_header(chain_u, self.HEADERS['D'])\n        self._append_header(chain_u, self.HEADERS['E'])\n        self._append_header(chain_u, self.HEADERS['F'])\n        self._append_header(chain_u, self.HEADERS['O'])\n        self._append_header(chain_u, self.HEADERS['P'])\n        self._append_header(chain_u, self.HEADERS['Q'])\n\n        chain_l = chain_u.fork(self.HEADERS['G'])\n        self._append_header(chain_l, self.HEADERS['H'])\n        self._append_header(chain_l, self.HEADERS['I'])\n        self._append_header(chain_l, self.HEADERS['J'])\n        self._append_header(chain_l, self.HEADERS['K'])\n        self._append_header(chain_l, self.HEADERS['L'])\n\n        chain_z = chain_l.fork(self.HEADERS['M'])\n\n        self.assertEqual([chain_l, chain_z, chain_u], self.get_chains_that_contain_header_helper(self.HEADERS['A']))\n        self.assertEqual([chain_l, chain_z, chain_u], self.get_chains_that_contain_header_helper(self.HEADERS['C']))\n        self.assertEqual([chain_l, chain_z, chain_u], self.get_chains_that_contain_header_helper(self.HEADERS['F']))\n        self.assertEqual([chain_l, chain_z], self.get_chains_that_contain_header_helper(self.HEADERS['G']))\n        self.assertEqual([chain_l, chain_z], self.get_chains_that_contain_header_helper(self.HEADERS['I']))\n        self.assertEqual([chain_z], self.get_chains_that_contain_header_helper(self.HEADERS['M']))\n        self.assertEqual([chain_l], self.get_chains_that_contain_header_helper(self.HEADERS['K']))\n\n        self._append_header(chain_z, self.HEADERS['N'])\n        self._append_header(chain_z, self.HEADERS['X'])\n        self._append_header(chain_z, self.HEADERS['Y'])\n        self._append_header(chain_z, self.HEADERS['Z'])\n\n        self.assertEqual([chain_z, chain_l, chain_u], self.get_chains_that_contain_header_helper(self.HEADERS['A']))\n        self.assertEqual([chain_z, chain_l, chain_u], self.get_chains_that_contain_header_helper(self.HEADERS['C']))\n        self.assertEqual([chain_z, chain_l, chain_u], self.get_chains_that_contain_header_helper(self.HEADERS['F']))\n        self.assertEqual([chain_u], self.get_chains_that_contain_header_helper(self.HEADERS['O']))\n        self.assertEqual([chain_z, chain_l], self.get_chains_that_contain_header_helper(self.HEADERS['I']))\n\n    def test_target_to_bits(self):\n        # https://github.com/bitcoin/bitcoin/blob/7fcf53f7b4524572d1d0c9a5fdc388e87eb02416/src/arith_uint256.h#L269\n        self.assertEqual(0x05123456, Blockchain.target_to_bits(0x1234560000))\n        self.assertEqual(0x0600c0de, Blockchain.target_to_bits(0xc0de000000))\n\n        # tests from https://github.com/bitcoin/bitcoin/blob/a7d17daa5cd8bf6398d5f8d7e77290009407d6ea/src/test/arith_uint256_tests.cpp#L411\n        tuples = (\n            (0, 0x0000000000000000000000000000000000000000000000000000000000000000, 0),\n            (0x00123456, 0x0000000000000000000000000000000000000000000000000000000000000000, 0),\n            (0x01003456, 0x0000000000000000000000000000000000000000000000000000000000000000, 0),\n            (0x02000056, 0x0000000000000000000000000000000000000000000000000000000000000000, 0),\n            (0x03000000, 0x0000000000000000000000000000000000000000000000000000000000000000, 0),\n            (0x04000000, 0x0000000000000000000000000000000000000000000000000000000000000000, 0),\n            (0x00923456, 0x0000000000000000000000000000000000000000000000000000000000000000, 0),\n            (0x01803456, 0x0000000000000000000000000000000000000000000000000000000000000000, 0),\n            (0x02800056, 0x0000000000000000000000000000000000000000000000000000000000000000, 0),\n            (0x03800000, 0x0000000000000000000000000000000000000000000000000000000000000000, 0),\n            (0x04800000, 0x0000000000000000000000000000000000000000000000000000000000000000, 0),\n            (0x01123456, 0x0000000000000000000000000000000000000000000000000000000000000012, 0x01120000),\n            (0x02123456, 0x0000000000000000000000000000000000000000000000000000000000001234, 0x02123400),\n            (0x03123456, 0x0000000000000000000000000000000000000000000000000000000000123456, 0x03123456),\n            (0x04123456, 0x0000000000000000000000000000000000000000000000000000000012345600, 0x04123456),\n            (0x05009234, 0x0000000000000000000000000000000000000000000000000000000092340000, 0x05009234),\n            (0x20123456, 0x1234560000000000000000000000000000000000000000000000000000000000, 0x20123456),\n        )\n        for nbits1, target, nbits2 in tuples:\n            with self.subTest(original_compact_nbits=nbits1.to_bytes(length=4, byteorder=\"big\").hex()):\n                num = Blockchain.bits_to_target(nbits1)\n                self.assertEqual(target, num)\n                self.assertEqual(nbits2, Blockchain.target_to_bits(num))\n\n        # Make sure that we don't generate compacts with the 0x00800000 bit set\n        self.assertEqual(0x02008000, Blockchain.target_to_bits(0x80))\n\n        with self.assertRaises(InvalidHeader):  # target cannot be negative\n            Blockchain.bits_to_target(0x01fedcba)\n        with self.assertRaises(InvalidHeader):  # target cannot be negative\n            Blockchain.bits_to_target(0x04923456)\n        with self.assertRaises(InvalidHeader):  # overflow\n            Blockchain.bits_to_target(0xff123456)\n\n\nclass TestVerifyHeader(ElectrumTestCase):\n\n    # Data for Bitcoin block header #100.\n    valid_header = \"0100000095194b8567fe2e8bbda931afd01a7acd399b9325cb54683e64129bcd00000000660802c98f18fd34fd16d61c63cf447568370124ac5f3be626c2e1c3c9f0052d19a76949ffff001d33f3c25d\"\n    target = Blockchain.bits_to_target(0x1d00ffff)\n    prev_hash = \"00000000cd9b12643e6854cb25939b39cd7a1ad0af31a9bd8b2efe67854b1995\"\n\n    def setUp(self):\n        super().setUp()\n        self.header = deserialize_header(bfh(self.valid_header), 100)\n\n    def test_valid_header(self):\n        Blockchain.verify_header(self.header, self.prev_hash, self.target)\n\n    def test_expected_hash_mismatch(self):\n        with self.assertRaises(InvalidHeader):\n            Blockchain.verify_header(self.header, self.prev_hash, self.target,\n                                     expected_header_hash=\"foo\")\n\n    def test_prev_hash_mismatch(self):\n        with self.assertRaises(InvalidHeader):\n            Blockchain.verify_header(self.header, \"foo\", self.target)\n\n    def test_target_mismatch(self):\n        with self.assertRaises(InvalidHeader):\n            other_target = Blockchain.bits_to_target(0x1d00eeee)\n            Blockchain.verify_header(self.header, self.prev_hash, other_target)\n\n    def test_insufficient_pow(self):\n        with self.assertRaises(InvalidHeader):\n            self.header[\"nonce\"] = 42\n            Blockchain.verify_header(self.header, self.prev_hash, self.target)\n"
  },
  {
    "path": "tests/test_bolt11.py",
    "content": "from hashlib import sha256\nfrom decimal import Decimal\nfrom binascii import unhexlify, hexlify\nimport pprint\nimport unittest\n\nfrom electrum.lnaddr import shorten_amount, unshorten_amount, LnAddr, lnencode, lndecode\nfrom electrum.segwit_addr import bech32_encode, bech32_decode\nfrom electrum import segwit_addr\nfrom electrum.lnutil import UnknownEvenFeatureBits, LnFeatures, IncompatibleLightningFeatures\nfrom electrum import constants\nfrom electrum.util import bfh, ShortID\n\nfrom . import ElectrumTestCase\n\n\nRHASH=unhexlify('0001020304050607080900010203040506070809000102030405060708090102')\nPAYMENT_SECRET=unhexlify('1111111111111111111111111111111111111111111111111111111111111111')\nCONVERSION_RATE=1200\nPRIVKEY=unhexlify('e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734')\nPUBKEY=unhexlify('03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad')\n\n\nclass TestBolt11(ElectrumTestCase):\n    def test_shorten_amount(self):\n        tests = {\n            Decimal(10)/10**12: '10p',\n            Decimal(1000)/10**12: '1n',\n            Decimal(1200)/10**12: '1200p',\n            Decimal(123)/10**6: '123u',\n            Decimal(123)/1000: '123m',\n            Decimal(3): '3',\n            Decimal(1000): '1000',\n        }\n\n        for i, o in tests.items():\n            self.assertEqual(shorten_amount(i), o)\n            assert unshorten_amount(shorten_amount(i)) == i\n\n    @staticmethod\n    def compare(a, b):\n\n        if len([t[1] for t in a.tags if t[0] == 'h']) == 1:\n            h1 = sha256([t[1] for t in a.tags if t[0] == 'h'][0].encode('utf-8')).digest()\n            h2 = [t[1] for t in b.tags if t[0] == 'h'][0]\n            assert h1 == h2\n\n        # Need to filter out these, since they are being modified during\n        # encoding, i.e., hashed\n        a.tags = [t for t in a.tags if t[0] != 'h' and t[0] != 'n']\n        b.tags = [t for t in b.tags if t[0] != 'h' and t[0] != 'n']\n\n        assert b.pubkey.serialize() == PUBKEY, (hexlify(b.pubkey.serialize()), hexlify(PUBKEY))\n        assert b.signature is not None\n\n        # Unset these, they are generated during encoding/decoding\n        b.pubkey = None\n        b.signature = None\n\n        assert a.__dict__ == b.__dict__, (pprint.pformat([a.__dict__, b.__dict__]))\n\n    def test_roundtrip(self):\n        longdescription = ('One piece of chocolate cake, one icecream cone, one'\n                          ' pickle, one slice of swiss cheese, one slice of salami,'\n                          ' one lollypop, one piece of cherry pie, one sausage, one'\n                          ' cupcake, and one slice of watermelon')\n\n        timestamp = 1615922274\n        tests = [\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, tags=[('d', ''), ('9', 33282)]),\n             \"lnbc1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdqq9qypqszpyrpe4tym8d3q87d43cgdhhlsrt78epu7u99mkzttmt2wtsx0304rrw50addkryfrd3vn3zy467vxwlmf4uz7yvntuwjr2hqjl9lw5cqwtp2dy\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('9', 0x28200)]),\n             \"lnbc1m1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5xysxxatsyp3k7enxv4jsxqzpu9qy9qsqw8l2pulslacwjt86vle3sgfdmcct5v34gtcpfnujsf6ufqa7v7jzdpddnwgte82wkscdlwfwucrgn8z36rv9hzk5mukltteh0yqephqpk5vegu\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=Decimal('1'), tags=[('h', longdescription), ('9', 0x28200)]),\n             \"lnbc11ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsq0jnua6dc4p984aeafs6ss7tjjj7553ympvg82qrjq0zgdqgtdvt5wlwkvw4ds5sn96nazp6ct9ts37tcw708kzkk4p8znahpsgp9tnspnycsf7\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, net=constants.BitcoinTestnet, tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription), ('9', 0x28200)]),\n             \"lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfpp3x9et2e20v6pu37c5d9vax37wxq72un98hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqy5826t0z3sn29z396pmr4kv73lcx0v7y6vas6h3pysmqllmzwgm5ps2t468gm4psj52usjy6y4xcry4k84n2zggs6f9agwg95454v6gqrwmh4f\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[\n                ('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3),\n                       (unhexlify('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('030405060708090a'), 2, 30, 4)]),\n                ('f', '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'),\n                ('h', longdescription),\n                ('9', 0x28200)]),\n             \"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqfnk063vsrgjx7l6td6v42skuxql7epn5tmrl4qte2e78nqnsjlgjg3sgkxreqex5fw4c9chnvtc2hykqnyxr84zwfr8f3d9q3h0nfdgqenlzvj\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET,  amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription), ('9', 0x28200)]),\n             \"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfppj3a24vwu6r8ejrss3axul8rxldph2q7z9hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqqf6z4r7ruzr5txm5ln4netwa2f4x233tud7jy8gxrynyx07rxt7qm92yk2krlgwr7d8jknglur75sujeyapmda5nf3femrk2mep8a2cp4hlvup\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET,  amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription), ('9', 0x28200)]),\n             \"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfppqw508d6qejxtdg4y5r3zarvary0c5xw7khp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqy4wp73jma5uktd9y7yha56f98n2k0hxgnvp2qdcury00dapps3k3urgfy8tvv8jzwcafpy576msk5xx2hladf06m3s5mgx5msn4elfqqaaqjhk\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET,  amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription), ('9', 0x28200)]),\n             \"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqgt4gg9uktlpgnnuvczazusp5uwjv78na305ucsw06c8uk58e5stjqj9sz7fgavw0z688alt364js72mc9mg8yumhpes2dsmq5k9nr5qqddykxy\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET,  amount=24, tags=[('n', PUBKEY), ('h', longdescription), ('9', 0x28200)]),\n             \"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsq2y235rxw7v0gkn2t9ehc742tm3p22q2yjjykq4d85ze6g62yk60navxqz0ga96sqrszju8nlfajthem4gngxvyz4hwy39j4nqm8kv0qq9znxs7\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET,  amount=24, tags=[('h', longdescription), ('9', 2 + (1 << 9) + (1 << 15))]),\n             \"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqszrwfgrl5k3rt4q4mclc8t00p2tcjsf9pmpcq6lu5zhmampyvk43fk30eqpdm8t5qmdpzan25aqxqaqdzmy0smrtduazjcxx975vz78ccpx0qhev\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET,  amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 8) + (1 << 15))]),\n             \"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqg2wans8f6vkfd3l7zjv547hlc7wd7eqyxfwhtdudnkkgrpk6p9ffykwrvdtwm0aakaxujurdxgd7cllnfypmj22cvy7z333udg6zncgacqzmd2z9\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET,  amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 15))]),\n             \"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqs2dr525u5f4kjxdv0hq5c822qwxrtttjl4u586yl84x0kvvx66gz9ygy76005s5sjwgr7fp55ccsae47vpl4gqvwhc3exps964g743j5gqwtt68t\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET,  amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 14))]),\n             \"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrss2f8kr98446xls02yndup2ynwjh46u8kdeuuncexx2hnets0j0064nyq25gkd6jnttldzt5qqtszum5dufvuvryxt204w2p24557udxgcp0nlwtw\"),\n        ]\n        # Some old tests follow that do not have payment_secret. Note that if the parser raised due to the lack of features/payment_secret,\n        # old wallets that have these invoices saved (as paid/expired), could not be opened (though we could do a db upgrade and delete them).\n        tests.extend([\n            (LnAddr(date=timestamp, paymenthash=RHASH, tags=[('d', '')]),\n             \"lnbc1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqd9n3kwjjwglnfne5p4rvkze998m3xcxrc8kunl5khkchlaqhwhlyztuuwkrglv47mqg96mcqjjx70hh9luaj4te0u4ww6aclxwve3fqpkmdxlj\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60)]),\n             \"lnbc1m1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9rflz25dx0qw6kdg05u0c5hdc30yq6ga6ew4pz86n244va45nchns9zrs3wjxznsqnt37hz7pswvc56wvuhxcjyd6k3lqf4ujynyxuspmvr078\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, amount=Decimal('1'), tags=[('h', longdescription)]),\n             \"lnbc11ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs2qjafckq94q3js6lvqz2kmenn9ysjejyj8fm4hlx0xtqhaxfzlxjappkgp0hmm40dnuan4v3jy83lqjup2n0fdzgysg049y9l9uc98qq07kfd3\"),\n            (LnAddr(date=timestamp, paymenthash=RHASH, net=constants.BitcoinTestnet, tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]),\n             \"lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un98hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsr9zktgu78k8p9t8555ve37qwfvqn6ga37fnfwhgexmf20nzdpmuhwvuv7zra3xrh8y2ggxxuemqfsgka9x7uzsrcx8rfv85c8pmhq9gq4sampn\"),\n\n        ])\n\n        # Roundtrip\n        for lnaddr1, invoice_str1 in tests:\n            invoice_str2 = lnencode(lnaddr1, PRIVKEY)\n            self.assertEqual(invoice_str1, invoice_str2)\n            lnaddr2 = lndecode(invoice_str2, net=lnaddr1.net)\n            self.compare(lnaddr1, lnaddr2)\n\n    def test_n_decoding(self):\n        # We flip the signature recovery bit, which would normally give a different\n        # pubkey.\n        _, hrp, data = bech32_decode(\n            lnencode(LnAddr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('9', 33282)]), PRIVKEY),\n            ignore_long_length=True)\n        data[-1] ^= 1\n        lnaddr = lndecode(bech32_encode(segwit_addr.Encoding.BECH32, hrp, data), verbose=True)\n        self.assertNotEqual(lnaddr.pubkey.serialize(), PUBKEY)\n\n        # But not if we supply expliciy `n` specifier!\n        _, hrp, data = bech32_decode(\n            lnencode(LnAddr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('n', PUBKEY), ('9', 33282)]), PRIVKEY),\n            ignore_long_length=True)\n        data[-1] ^= 1\n        lnaddr = lndecode(bech32_encode(segwit_addr.Encoding.BECH32, hrp, data), verbose=True)\n        self.assertEqual(lnaddr.pubkey.serialize(), PUBKEY)\n\n    def test_min_final_cltv_expiry_decoding(self):\n        lnaddr = lndecode(\"lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qsp5qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsdqqcqzys9qypqsqp2h6a5xeytuc3fad2ed4gxvhd593lwjdna3dxsyeem0qkzjx6guk44jend0xq4zzvp6f3fy07wnmxezazzsxgmvqee8shxjuqu2eu0qpnvc95x\",\n                          net=constants.BitcoinSimnet)\n        self.assertEqual(144, lnaddr.get_min_final_cltv_delta())\n\n        lnaddr = lndecode(\"lntb15u1p0m6lzupp5zqjthgvaad9mewmdjuehwddyze9d8zyxcc43zhaddeegt37sndgsdq4xysyymr0vd4kzcmrd9hx7cqp7xqrrss9qy9qsqsp5vlhcs24hwm747w8f3uau2tlrdkvjaglffnsstwyamj84cxuhrn2s8tut3jqumepu42azyyjpgqa4w9w03204zp9h4clk499y2umstl6s29hqyj8vv4as6zt5567ux7l3f66m8pjhk65zjaq2esezk7ll2kcpljewkg\",\n                          net=constants.BitcoinTestnet)\n        self.assertEqual(30, lnaddr.get_min_final_cltv_delta())\n\n    def test_min_final_cltv_expiry_roundtrip(self):\n        for cltv in (1, 15, 16, 31, 32, 33, 150, 511, 512, 513, 1023, 1024, 1025):\n            lnaddr = LnAddr(\n                paymenthash=RHASH, payment_secret=b\"\\x01\"*32, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('c', cltv), ('9', 33282)])\n            self.assertEqual(cltv, lnaddr.get_min_final_cltv_delta())\n            invoice = lnencode(lnaddr, PRIVKEY)\n            self.assertEqual(cltv, lndecode(invoice).get_min_final_cltv_delta())\n\n    def test_features(self):\n        lnaddr = lndecode(\"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9qypqsztrz5v3jfnxskfv7g8chmyzyrfhf2vupcavuq5rce96kyt6g0zh337h206awccwp335zarqrud4wccgdn39vur44d8um4hmgv06aj0sgpdrv73z\")\n        self.assertEqual(33282, lnaddr.get_tag('9'))\n        self.assertEqual(LnFeatures(33282), lnaddr.get_features())\n\n    def test_payment_secret(self):\n        lnaddr = lndecode(\"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9q5sqqqqqqqqqqqqqqqpqsqvvh7ut50r00p3pg34ea68k7zfw64f8yx9jcdk35lh5ft8qdr8g4r0xzsdcrmcy9hex8un8d8yraewvhqc9l0sh8l0e0yvmtxde2z0hgpzsje5l\")\n        self.assertEqual((1 << 9) + (1 << 15) + (1 << 99), lnaddr.get_tag('9'))\n        self.assertEqual(b\"\\x11\" * 32, lnaddr.payment_secret)\n\n    def test_validate_and_compare_features(self):\n        lnaddr = lndecode(\"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9q5sqqqqqqqqqqqqqqqpqsqvvh7ut50r00p3pg34ea68k7zfw64f8yx9jcdk35lh5ft8qdr8g4r0xzsdcrmcy9hex8un8d8yraewvhqc9l0sh8l0e0yvmtxde2z0hgpzsje5l\")\n        lnaddr.validate_and_compare_features(LnFeatures((1 << 8) + (1 << 14) + (1 << 15)))\n        with self.assertRaises(IncompatibleLightningFeatures):\n            lnaddr.validate_and_compare_features(LnFeatures((1 << 8) + (1 << 14) + (1 << 16)))\n\n    def test_format_bolt11_routing_info_as_human_readable(self):\n        r_tags_expl = [\n            ['r', [(bfh('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), bfh('0102030405060708'), 1, 20, 3),\n                   (bfh('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), bfh('030405060708090a'), 2, 30, 4)]],\n            ['r', [(bfh('038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9'), bfh('f4240000000002cd'), 0, 1, 40)]],\n        ]\n        self.assertEqual(\n            [\n                ('r', [('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255',\n                        '66051x263430x1800', 1, 20, 3),\n                       ('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255',\n                        '197637x395016x2314', 2, 30, 4)]\n                 ),\n                ('r', [('038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9',\n                        '16000000x0x717', 0, 1, 40),]\n                 ),\n            ],\n            LnAddr.format_bolt11_routing_info_as_human_readable(r_tags_expl, has_explicit_r_tagtype=True))\n\n        r_tags_impl = [\n            [(bfh('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), bfh('0102030405060708'), 1, 20, 3),\n                   (bfh('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), bfh('030405060708090a'), 2, 30, 4)],\n            [(bfh('038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9'), bfh('f4240000000002cd'), 0, 1, 40)],\n        ]\n        self.assertEqual(\n            [\n                [('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255',\n                  '66051x263430x1800', 1, 20, 3),\n                 ('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255',\n                  '197637x395016x2314', 2, 30, 4)],\n                [('038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9',\n                  '16000000x0x717', 0, 1, 40),],\n            ],\n            LnAddr.format_bolt11_routing_info_as_human_readable(r_tags_impl, has_explicit_r_tagtype=False))\n\n        for has_explicit_r_tagtype in (False, True):\n            self.assertEqual(\n                [],\n                LnAddr.format_bolt11_routing_info_as_human_readable([], has_explicit_r_tagtype=has_explicit_r_tagtype))\n"
  },
  {
    "path": "tests/test_callbackmgr.py",
    "content": "import asyncio\nimport weakref\n\nfrom electrum import util\nfrom electrum.util import EventListener, event_listener, trigger_callback\nfrom electrum.utils.memory_leak import count_objects_in_memory, wait_until_obj_is_garbage_collected\nfrom electrum.simple_config import SimpleConfig\n\nfrom . import ElectrumTestCase, restore_wallet_from_text__for_unittest\n\n\nclass MyEventListener(EventListener):\n    def __init__(self, *, autostart: bool = False):\n        self._satoshi_cnt = 0\n        self._hal_cnt = 0\n        if autostart:\n            self.start()\n\n    def start(self):\n        self.register_callbacks()\n\n    def stop(self):\n        self.unregister_callbacks()\n\n    @event_listener\n    async def on_event_satoshi_moves_his_coins(self, arg1, arg2):\n        self._satoshi_cnt += 1\n\n    @event_listener\n    def on_event_hal_moves_his_coins(self, arg1, arg2):  # non-async\n        self._hal_cnt += 1\n\n\n_count_all_callbacks = util.callback_mgr.count_all_callbacks\n\n\nasync def fast_sleep():\n    # sleep a few event loop iterations\n    for i in range(5):\n        await asyncio.sleep(0)\n\n\nclass TestCallbackMgr(ElectrumTestCase):\n\n    def test_multiple_calls_to_register_callbacks(self):\n        self.assertEqual(0, _count_all_callbacks())\n        el1 = MyEventListener()\n        el2 = MyEventListener()\n        self.assertEqual(0, _count_all_callbacks())\n        el1.start()\n        self.assertEqual(2, _count_all_callbacks())\n        el2.start()\n        self.assertEqual(4, _count_all_callbacks())\n        el1.start()\n        self.assertEqual(4, _count_all_callbacks())\n        el1.stop()\n        self.assertEqual(2, _count_all_callbacks())\n        el1.stop()\n        self.assertEqual(2, _count_all_callbacks())\n        el1.stop()\n        self.assertEqual(2, _count_all_callbacks())\n        el2.stop()\n        self.assertEqual(0, _count_all_callbacks())\n\n    async def test_trigger_callback(self):\n        el1 = MyEventListener()\n        el1.start()\n        el2 = MyEventListener()\n        el2.start()\n        # trigger some cbs\n        self.assertEqual(el1._satoshi_cnt, 0)\n        self.assertEqual(el1._hal_cnt, 0)\n        trigger_callback('satoshi_moves_his_coins', 0, 0)\n        trigger_callback('satoshi_moves_his_coins', 0, 0)\n        trigger_callback('satoshi_moves_his_coins', 0, 0)\n        trigger_callback('hal_moves_his_coins', 0, 0)\n        await fast_sleep()\n        self.assertEqual(el1._satoshi_cnt, 3)\n        self.assertEqual(el2._satoshi_cnt, 3)\n        self.assertEqual(el1._hal_cnt, 1)\n        self.assertEqual(el2._hal_cnt, 1)\n        # stop one listener, see new triggers are only seen by other one still running\n        el1.stop()\n        trigger_callback('satoshi_moves_his_coins', 0, 0)\n        trigger_callback('hal_moves_his_coins', 0, 0)\n        await fast_sleep()\n        self.assertEqual(el1._satoshi_cnt, 3)\n        self.assertEqual(el2._satoshi_cnt, 4)\n        self.assertEqual(el1._hal_cnt, 1)\n        self.assertEqual(el2._hal_cnt, 2)\n\n    async def test_gc(self):\n        objmap = count_objects_in_memory([MyEventListener])\n        self.assertEqual(len(objmap[MyEventListener]), 0)\n        self.assertEqual(_count_all_callbacks(), 0)\n        el1 = MyEventListener()\n        el1.start()\n        el2 = MyEventListener()\n        el2.start()\n        objmap = count_objects_in_memory([MyEventListener])\n        self.assertEqual(len(objmap[MyEventListener]), 2)\n        self.assertEqual(_count_all_callbacks(), 4)\n        # test if we can get GC-ed if we explicitly unregister cbs:\n        el1.stop()  # calls unregister_callbacks\n        del el1\n        objmap = count_objects_in_memory([MyEventListener])\n        self.assertEqual(len(objmap[MyEventListener]), 1)\n        self.assertEqual(_count_all_callbacks(), 2)\n        # test if we can get GC-ed even without unregistering cbs:\n        del el2\n        objmap = count_objects_in_memory([MyEventListener])\n        self.assertEqual(len(objmap[MyEventListener]), 0)\n        self.assertEqual(_count_all_callbacks(), 0)\n\n    async def test_gc2(self):\n        def func():\n            el1 = MyEventListener(autostart=True)\n            el1.el2 = MyEventListener(autostart=True)\n            el1.el2.el3 = MyEventListener(autostart=True)\n            self.assertEqual(_count_all_callbacks(), 6)\n        func()\n        self.assertEqual(_count_all_callbacks(), 0)\n\n    async def test_gc_complex_using_wallet(self):\n        \"\"\"This test showcases why EventListener uses WeakMethodProper instead of weakref.WeakMethod.\n        We need the custom __eq__ for some reason.\n        \"\"\"\n        self.assertEqual(_count_all_callbacks(), 0)\n        config = SimpleConfig({'electrum_path': self.electrum_path})\n        wallet = restore_wallet_from_text__for_unittest(\n            \"9dk\", path=None, config=config,\n        )[\"wallet\"]\n        assert wallet.lnworker is not None\n        # now delete the wallet, and wait for it to get GC-ed\n        # note: need to wait for cyclic GC. example: wallet.lnworker.wallet\n        wr = weakref.ref(wallet)\n        del wallet\n        async with util.async_timeout(5):\n            await wait_until_obj_is_garbage_collected(wr)\n        # by now, all callbacks must have been cleaned up:\n        self.assertEqual(_count_all_callbacks(), 0)\n"
  },
  {
    "path": "tests/test_coinchooser.py",
    "content": "from electrum.coinchooser import CoinChooserPrivacy\nfrom electrum.util import NotEnoughFunds\nfrom electrum.transaction import PartialTxInput, TxOutpoint, Transaction, PartialTxOutput\nfrom electrum.fee_policy import FeePolicy, FixedFeePolicy\nfrom functools import partial\nfrom typing import Optional\n\nfrom . import ElectrumTestCase\n\n\nclass TestCoinChooser(ElectrumTestCase):\n\n    @staticmethod\n    def get_dummy_txin_1_284_474_sat() -> PartialTxInput:\n        # value of 1_284_474 sat\n        prevout_txid = bytes.fromhex(\n            \"b3d9174cb5d3234764a089bb91fdbd1117b7958be4870d1a544136ab017a67dd\"\n        )\n        coin = PartialTxInput(\n            prevout=TxOutpoint(txid=prevout_txid, out_idx=0),\n        )\n        coin.utxo = Transaction(\n            \"02000000000105a5a00ad10e754a17154446bbe1c557b44b86a7cd53308ad9ab813388a9d6d1520000000000fdffffff2c48659d10a752b0c0a3efa4092ebee5210943a2f41ff0607ba0e03a4cdf7bbd0000000000fdffffff93f4feb485581654caffa523c500dc417a98097fb731045040bb162acb3e14e90000000000fdffffffbf4b69292acaabf8a415db412eedd8a202d4dd2ca12e532628a756276fec00f50000000000fdffffffa96e18c45ecc56608d0be3c1b2cd93e52d569a9c3a68ed51aead570beeef29ff0000000000fdffffff017a991300000000001600142a55fbef3e419e1c862632a826ae89ade0b07e3a0247304402204de416135d26711df2cbd5209e3f79f95a1de5ddea5980215606ebfe639747bd0220286f9de38a96a078c818e97fe6fbbbe3637287461cd7407adf9e85ba6d899005012102c329033555adccaadb6c83fb486f540ba00aad3edba7a4ec3347b5cc0935c4050247304402200132c1c4c41b840f05efeacf96cccedab38d68c9021c79f940738572b049c1a302200d59ba4719d4d4e55ced651cde35cbf79838bf7122cbafd6584dd934a67db0ed01210380194ab3704b5524a0c97f78b4458b80efd365faaedaebe90fa7807eeab041700247304402202c966fbea5db4bb3794e843b59240d678a3c8e97be1d10c18475a467c101b97c02203edb0ca11e7605af2437ddffbbe416317bca8788c23c4816c13698042222d30f0121035edcdcad9affcff41302ad49a19ccbd47ae50b153ba7c2abaaf3bb2d22c859120247304402206890a622513bb9c8b8ca83e5e82532f9753ecd3188c27d8da9452e264f5500cc022012dad8fb872478b7f9d0d9d310859fca6534b8e9f44511eeef5450beaf13c690012103f683aa56e036b1307c1ae75e81553b6730aea312560e36afad487ba2bc6cf98f02473044022006807df115d6bce73e384651fbb9e932ee313133218be7769e52809b758529b6022078b2859f98a62211f130e69982f96d2968e1820c58bb817f9ab31645b6b4ceae0121026402c3c0a2b4dd5703686f8c5b5b3dcecbbf4d772ef83e440bfad22b742753e730a60300\"\n        )\n        coin.block_height = 100\n        return coin\n\n    @staticmethod\n    def get_dummy_txout_1(amount: Optional[int] = 1000000) -> PartialTxOutput:\n        output = PartialTxOutput.from_address_and_value(\n            address=\"bc1q2089yvkkyw7yq7m6a7lxt45n35c587hk4sgj7c\",\n            value=amount,\n        )\n        return output\n\n    def test_bucket_candidates_with_empty_buckets(self):\n        def sufficient_funds(buckets, *, bucket_value_sum):\n            return True\n        coin_chooser = CoinChooserPrivacy(enable_output_value_rounding=False)\n        self.assertEqual([[]], coin_chooser.bucket_candidates_any([], sufficient_funds))\n        self.assertEqual([[]], coin_chooser.bucket_candidates_prefer_confirmed([], sufficient_funds))\n        def sufficient_funds(buckets, *, bucket_value_sum):\n            return False\n        with self.assertRaises(NotEnoughFunds):\n            coin_chooser.bucket_candidates_any([], sufficient_funds)\n        with self.assertRaises(NotEnoughFunds):\n            coin_chooser.bucket_candidates_prefer_confirmed([], sufficient_funds)\n\n    def test_make_tx_no_outputs_adds_change(self):\n        coin_chooser = CoinChooserPrivacy(enable_output_value_rounding=False)\n        fee_estimator = partial(FeePolicy('eta:2').estimate_fee, allow_fallback_to_static_rates=True)\n\n        # dummy input with value of 330 sat\n        prevout_txid = bytes.fromhex(\"81d0b29f08c6256dcfbaf02ff1f1e756461cb1df550672e049af7429331c643f\")\n        single_txin = PartialTxInput(\n            prevout=TxOutpoint(txid=prevout_txid, out_idx=0),\n        )\n        single_txin.utxo = Transaction(\"02000000000101956449bdc8059b680a20483e64e139ce63fe64333b92cd7811a1b116d6b967ad0000000000fdffffff024a01000000000000160014a21d1fbcf571153f57b40855e059c134405a89ecd682010000000000160014fd7debf75d6c410bf6ba1c8ba05f90f23ce4646a0247304402207f07ec0c2415b31743527dea2f7bff3868f494dc0a5d45adec5e05031725a0af02202aa0ac7d06dbcad8ac0b9808a829b6bdaa98bc831aef31a5ab4e5d1890f7552101210278a5d9b2796f2743ccf1b36b2bf47695d766d0841c17b00ce83943c8b37dde0ceea60300\")\n\n        # dummy input to be used as potential additional input of higher value\n        coin = self.get_dummy_txin_1_284_474_sat()\n\n        tx = coin_chooser.make_tx(\n            coins=[coin],\n            inputs=[single_txin],\n            outputs=[],\n            change_addrs=[\"bc1q2089yvkkyw7yq7m6a7lxt45n35c587hk4sgj7c\"],\n            fee_estimator_vb=fee_estimator,\n            dust_threshold=500,\n        )\n        # make_tx should add one additional input and a change output\n        assert len(tx.outputs()) == 1, f\"expected 1 output got {len(tx.outputs())}\"\n        assert len(tx.inputs()) == 2, f\"expected 2 input got {len(tx.inputs())}\"\n\n        # dummy input with value of 99030 sat\n        prevout_txid = bytes.fromhex(\"81d0b29f08c6256dcfbaf02ff1f1e756461cb1df550672e049af7429331c643f\")\n        single_txin = PartialTxInput(\n            prevout=TxOutpoint(txid=prevout_txid, out_idx=1),\n        )\n        single_txin.utxo = Transaction(\"02000000000101956449bdc8059b680a20483e64e139ce63fe64333b92cd7811a1b116d6b967ad0000000000fdffffff024a01000000000000160014a21d1fbcf571153f57b40855e059c134405a89ecd682010000000000160014fd7debf75d6c410bf6ba1c8ba05f90f23ce4646a0247304402207f07ec0c2415b31743527dea2f7bff3868f494dc0a5d45adec5e05031725a0af02202aa0ac7d06dbcad8ac0b9808a829b6bdaa98bc831aef31a5ab4e5d1890f7552101210278a5d9b2796f2743ccf1b36b2bf47695d766d0841c17b00ce83943c8b37dde0ceea60300\")\n\n        tx = coin_chooser.make_tx(\n            coins=[coin],\n            inputs=[single_txin],\n            outputs=[],\n            change_addrs=[\"bc1q2089yvkkyw7yq7m6a7lxt45n35c587hk4sgj7c\"],\n            fee_estimator_vb=fee_estimator,\n            dust_threshold=500,\n        )\n        # make_tx should not add an additional input, as single_txin is large enough\n        assert len(tx.outputs()) == 1, f\"expected 1 output got {len(tx.outputs())}\"\n        assert len(tx.inputs()) == 1, f\"expected 1 input got {len(tx.inputs())}\"\n\n    def test_doesnt_round_output_value_with_zerofee_estimator(self):\n        # output value rounding is enabled (as by default)\n        coin_chooser = CoinChooserPrivacy(enable_output_value_rounding=True)\n\n        # fixed fee estimator always returns 0\n        fee_estimator = FixedFeePolicy(0).estimate_fee\n\n        tx = coin_chooser.make_tx(\n            coins=[],\n            inputs=[self.get_dummy_txin_1_284_474_sat()] ,\n            outputs=[self.get_dummy_txout_1(1_000_000)],\n            change_addrs=[],\n            fee_estimator_vb=fee_estimator,\n            dust_threshold=500,\n        )\n        assert tx.get_fee() == 0, f\"fee should be 0, is {tx.get_fee()}\"\n        assert len(tx.outputs()) == 2, f\"expected 2 output got {len(tx.outputs())}\"\n        assert len(tx.inputs()) == 1, f\"expected 1 input got {len(tx.inputs())}\"\n"
  },
  {
    "path": "tests/test_commands.py",
    "content": "import asyncio\nimport binascii\nimport datetime\nimport os.path\nimport unittest\nfrom unittest import mock\nfrom decimal import Decimal\nfrom os import urandom\nimport shutil\n\nimport electrum\nfrom electrum.commands import Commands, eval_bool\nfrom electrum import storage, wallet\nfrom electrum.lnutil import RECEIVED\nfrom electrum.lnworker import RecvMPPResolution\nfrom electrum.wallet import Abstract_Wallet\nfrom electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.submarine_swaps import SwapOffer, SwapFees, NostrTransport\nfrom electrum.transaction import Transaction, TxOutput, tx_from_any\nfrom electrum.util import UserFacingException, NotEnoughFunds\nfrom electrum.crypto import sha256\nfrom electrum.lnaddr import lndecode\nfrom electrum.daemon import Daemon\nfrom electrum import json_db\n\nfrom . import ElectrumTestCase\nfrom . import restore_wallet_from_text__for_unittest\nfrom .test_wallet_vertical import WalletIntegrityHelper\n\n\nclass TestCommands(ElectrumTestCase):\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n    def test_setconfig_non_auth_number(self):\n        self.assertEqual(7777, Commands._setconfig_normalize_value('rpcport', \"7777\"))\n        self.assertEqual(7777, Commands._setconfig_normalize_value('rpcport', '7777'))\n        self.assertAlmostEqual(Decimal(2.3), Commands._setconfig_normalize_value('somekey', '2.3'))\n\n    def test_setconfig_non_auth_number_as_string(self):\n        self.assertEqual(\"7777\", Commands._setconfig_normalize_value('somekey', \"'7777'\"))\n\n    def test_setconfig_non_auth_boolean(self):\n        self.assertEqual(True, Commands._setconfig_normalize_value('show_console_tab', \"true\"))\n        self.assertEqual(True, Commands._setconfig_normalize_value('show_console_tab', \"True\"))\n\n    def test_setconfig_non_auth_list(self):\n        self.assertEqual(['file:///var/www/', 'https://electrum.org'],\n            Commands._setconfig_normalize_value('url_rewrite', \"['file:///var/www/','https://electrum.org']\"))\n        self.assertEqual(['file:///var/www/', 'https://electrum.org'],\n            Commands._setconfig_normalize_value('url_rewrite', '[\"file:///var/www/\",\"https://electrum.org\"]'))\n\n    def test_setconfig_auth(self):\n        self.assertEqual(\"7777\", Commands._setconfig_normalize_value('rpcuser', \"7777\"))\n        self.assertEqual(\"7777\", Commands._setconfig_normalize_value('rpcuser', '7777'))\n        self.assertEqual(\"7777\", Commands._setconfig_normalize_value('rpcpassword', '7777'))\n        self.assertEqual(\"2asd\", Commands._setconfig_normalize_value('rpcpassword', '2asd'))\n        self.assertEqual(\"['file:///var/www/','https://electrum.org']\",\n            Commands._setconfig_normalize_value('rpcpassword', \"['file:///var/www/','https://electrum.org']\"))\n\n    def test_setconfig_none(self):\n        self.assertEqual(None, Commands._setconfig_normalize_value(\"somekey\", \"None\"))\n        self.assertEqual(None, Commands._setconfig_normalize_value(\"somekey\", \"null\"))\n        # but lowercase none does not work:  (maybe it should though...)\n        self.assertEqual(\"none\", Commands._setconfig_normalize_value(\"somekey\", \"none\"))\n        self.assertEqual(\"\", Commands._setconfig_normalize_value(\"somekey\", \"\"))\n        self.assertEqual(\"empty\", Commands._setconfig_normalize_value(\"somekey\", \"empty\"))\n\n    def test_eval_bool(self):\n        self.assertFalse(eval_bool(\"False\"))\n        self.assertFalse(eval_bool(\"false\"))\n        self.assertFalse(eval_bool(\"0\"))\n        self.assertTrue(eval_bool(\"True\"))\n        self.assertTrue(eval_bool(\"true\"))\n        self.assertTrue(eval_bool(\"1\"))\n        with self.assertRaises(ValueError):\n            eval_bool(\"Falsee\")\n\n    async def test_convert_xkey(self):\n        cmds = Commands(config=self.config)\n        xpubs = {\n            (\"xpub6CCWFbvCbqF92kGwm9nV7t7RvVoQUKaq5USMdyVP6jvv1NgN52KAX6NNYCeE8Ca7JQC4K5tZcnQrubQcjJ6iixfPs4pwAQJAQgTt6hBjg11\", \"standard\"),\n            (\"ypub6X2mZGb7kWnct3U4bWa7KyCw6TwrQwaKzaxaRNPGUkJo4UVbKgUj9A2WZQbp87E2i3Js4ZV85SmQnt2BSzWjXCLzjQXMkK7egQXXVHT4eKn\", \"p2wpkh-p2sh\"),\n            (\"zpub6qs2rwG2uCL6jLfBRsMjY4JSGS6JMZZpuhUoCmH9rkgg7aJpaLeHmDgeacZQ81sx7gRfp35gY77xgAdkAgvkKS2bbkDnLDw8x8bAsuKBrvP\", \"p2wpkh\"),\n        }\n        for xkey1, xtype1 in xpubs:\n            for xkey2, xtype2 in xpubs:\n                self.assertEqual(xkey2, await cmds.convert_xkey(xkey1, xtype2))\n\n        xprvs = {\n            (\"xprv9yD9r6PJmTgqpGCUf8FUkkAhNTxv4rryiFWkqb5mYQPw8aMDXUzuyJ3tgv5vUqYkdK1E6Q5jKxPss4HkMBYV4q8AfG8t7rxgyS4xQX4ndAm\", \"standard\"),\n            (\"yprvAJ3R9m4Dv9EKfZPbVV36xqGCYS7N1UrUdN2ycyyevQmpBgASn9AUbMi2i83WUkCg2x82qsgHnckRkLuK4sxVs4omXbqJhmnBFA8bo8ssinK\", \"p2wpkh-p2sh\"),\n            (\"zprvAcsgTRj94pmoWraiKqpjAvMhiQFox6qyYUZCQNsYJR9hEmyg2oL3DRNAjL16UerbSbEqbMGrFH6yddWsnaNWfJVNPwXjHgbfWtCFBgDxFkX\", \"p2wpkh\"),\n        }\n        for xkey1, xtype1 in xprvs:\n            for xkey2, xtype2 in xprvs:\n                self.assertEqual(xkey2, await cmds.convert_xkey(xkey1, xtype2))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_encrypt_decrypt(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n        cmds = Commands(config=self.config)\n        cleartext = \"asdasd this is the message\"\n        pubkey = \"021f110909ded653828a254515b58498a6bafc96799fb0851554463ed44ca7d9da\"\n        ciphertext = await cmds.encrypt(pubkey, cleartext)\n        self.assertEqual(cleartext, await cmds.decrypt(pubkey, ciphertext, wallet=wallet))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_export_private_key_imported(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n        cmds = Commands(config=self.config)\n        # single address tests\n        with self.assertRaises(UserFacingException):\n            await cmds.getprivatekeys(\"asdasd\", wallet=wallet)  # invalid addr, though might raise \"not in wallet\"\n        with self.assertRaises(UserFacingException):\n            await cmds.getprivatekeys(\"bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23\", wallet=wallet)  # not in wallet\n        self.assertEqual(\"p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL\",\n                         await cmds.getprivatekeys(\"bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw\", wallet=wallet))\n        # list of addresses tests\n        with self.assertRaises(UserFacingException):\n            await cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'asd'], wallet=wallet)\n        self.assertEqual(['p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'],\n                         await cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'], wallet=wallet))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_export_private_key_deterministic(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            'bitter grass shiver impose acquire brush forget axis eager alone wine silver',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n        cmds = Commands(config=self.config)\n        # single address tests\n        with self.assertRaises(UserFacingException):\n            await cmds.getprivatekeys(\"asdasd\", wallet=wallet)  # invalid addr, though might raise \"not in wallet\"\n        with self.assertRaises(UserFacingException):\n            await cmds.getprivatekeys(\"bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23\", wallet=wallet)  # not in wallet\n        self.assertEqual(\"p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2\",\n                         await cmds.getprivatekeys(\"bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af\", wallet=wallet))\n        # list of addresses tests\n        with self.assertRaises(UserFacingException):\n            await cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'asd'], wallet=wallet)\n        self.assertEqual(['p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'],\n                         await cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'], wallet=wallet))\n\n    async def test_verifymessage_enforces_strict_base64(self):\n        cmds = Commands(config=self.config)\n        msg = \"hello there\"\n        addr = \"bc1qq2tmmcngng78nllq2pvrkchcdukemtj56uyue0\"\n        sig = \"HznHvCsY//Zr5JvPIR3rN/RbCkttvrUs8Yt+vw+e1c29BLMSlcrN4+Y4Pq8e/UJuh2bDrUboTfsFhBJap+fPmNY=\"\n        self.assertTrue(await cmds.verifymessage(addr, sig, msg))\n        self.assertFalse(await cmds.verifymessage(addr, sig+\"trailinggarbage\", msg))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_decrypt_enforces_strict_base64(self, mock_save_db):\n        cmds = Commands(config=self.config)\n        wallet = restore_wallet_from_text__for_unittest(\n            '9dk',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']  # type: Abstract_Wallet\n        plaintext = \"hello there\"\n        ciphertext = \"QklFMQJEFgxfkXj+UNblbHR+4y6ZA2rGEeEhWo7h84lBFjlRY5JOPfV1zyC1fw5YmhIr7+3ceIV11lpf/Yv7gSqQCQ5Wuf1aGXceHZO0GjKVxBsuew==\"\n        pubkey = \"02a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc\"\n        self.assertEqual(plaintext, await cmds.decrypt(pubkey, ciphertext, wallet=wallet))\n        with self.assertRaises(binascii.Error):  # perhaps it should raise some nice UserFacingException instead\n            await cmds.decrypt(pubkey, ciphertext+\"trailinggarbage\", wallet=wallet)\n\n    def test_format_satoshis(self):\n        format_satoshis = electrum.commands.format_satoshis\n        # input type is highly polymorphic:\n        self.assertEqual(format_satoshis(None), None)\n        self.assertEqual(format_satoshis(1), \"0.00000001\")\n        self.assertEqual(format_satoshis(1.0), \"0.00000001\")\n        self.assertEqual(format_satoshis(Decimal(1)), \"0.00000001\")\n        # trailing zeroes are cut\n        self.assertEqual(format_satoshis(51000), \"0.00051\")\n        self.assertEqual(format_satoshis(123456_12345670), \"123456.1234567\")\n        # sub-satoshi precision is rounded\n        self.assertEqual(format_satoshis(Decimal(123.456)), \"0.00000123\")\n        self.assertEqual(format_satoshis(Decimal(123.5)), \"0.00000124\")\n        self.assertEqual(format_satoshis(Decimal(123.789)), \"0.00000124\")\n        self.assertEqual(format_satoshis(41754.681), \"0.00041755\")\n\n\nclass TestCommandsTestnet(ElectrumTestCase):\n    TESTNET = True\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n        self.config.NETWORK_OFFLINE = True\n        shutil.copytree(os.path.join(os.path.dirname(__file__), \"fiat_fx_data\"), os.path.join(self.electrum_path, \"cache\"))\n        self.config.FX_EXCHANGE = \"BitFinex\"\n        self.config.FX_CURRENCY = \"EUR\"\n        self._default_default_timezone = electrum.util.DEFAULT_TIMEZONE\n        electrum.util.DEFAULT_TIMEZONE = datetime.timezone.utc\n\n    def tearDown(self):\n        electrum.util.DEFAULT_TIMEZONE = self._default_default_timezone\n        super().tearDown()\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.daemon = Daemon(config=self.config, listen_jsonrpc=False)\n        assert self.daemon.network is None\n\n    async def asyncTearDown(self):\n        with mock.patch.object(wallet.Abstract_Wallet, 'save_db'):\n            await self.daemon.stop()\n        await super().asyncTearDown()\n\n    async def test_convert_xkey(self):\n        cmds = Commands(config=self.config)\n        xpubs = {\n            (\"tpubD8p5qNfjczgTGbh9qgNxsbFgyhv8GgfVkmp3L88qtRm5ibUYiDVCrn6WYfnGey5XVVw6Bc5QNQUZW5B4jFQsHjmaenvkFUgWtKtgj5AdPm9\", \"standard\"),\n            (\"upub59wfQ8qJTg6ZSuvwtR313Qdp8gP8TSBwTof5dPQ3QVsYp1N9t29Rr9TGF1pj8kAXUg3mKbmrTKasA2qmBJKb1bGUzB6ApDZpVC7LoHhyvBo\", \"p2wpkh-p2sh\"),\n            (\"vpub5UmvhoWDcMe3JD84impdFVjKJeXaQ4BSNvBJQnHvnWFRs7BP8gJzUD7QGDnK8epStKAa55NQuywR3KTKtzjbopx5rWnbQ8PJkvAzBtgaGBc\", \"p2wpkh\"),\n        }\n        for xkey1, xtype1 in xpubs:\n            for xkey2, xtype2 in xpubs:\n                self.assertEqual(xkey2, await cmds.convert_xkey(xkey1, xtype2))\n\n        xprvs = {\n            (\"tprv8c83gxdVUcznP8fMx2iNUBbaQgQC7MUbBUDG3c6YU9xgt7Dn5pfcgHUeNZTAvuYmNgVHjyTzYzGWwJr7GvKCm2FkPaaJipyipbfJeB3tdPW\", \"standard\"),\n            (\"uprv8vxJzdJQdJYGERrUnPVzgGh5aeYe3yU66ajUpzzRrALZwD31LUqBJM8nPmQkvpCgnKc6VT4Z1ed4pbTfzcjDZFwMFvGjJjoD6Kix2pCwVe7\", \"p2wpkh-p2sh\"),\n            (\"vprv9FnaJHyKmz5k5j3bckHctMnakch5zbTb1hFhcPtKEAiSzJrEb8zjvQnvQyNLvircBxiuEvf7UJycht5EiK9EMVcx8Fy9techN3nbRQRFhEv\", \"p2wpkh\"),\n        }\n        for xkey1, xtype1 in xprvs:\n            for xkey2, xtype2 in xprvs:\n                self.assertEqual(xkey2, await cmds.convert_xkey(xkey1, xtype2))\n\n    async def test_serialize(self):\n        cmds = Commands(config=self.config)\n        jsontx = {\n            \"inputs\": [\n                {\n                    \"prevout_hash\": \"9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539\",\n                    \"prevout_n\": 1,\n                    \"value_sats\": 1000000,\n                    \"privkey\": \"p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD\"\n                }\n            ],\n            \"outputs\": [\n                {\n                    \"address\": \"tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd\",\n                    \"value_sats\": 990000\n                }\n            ]\n        }\n        self.assertEqual(\"0200000000010139c5375fe9da7bd377c1783002b129f8c57d3e724d62f5eacb9739ca691a229d0100000000feffffff01301b0f0000000000160014ac0e2d229200bffb2167ed6fd196aef9d687d8bb0247304402206367fb2ddd723985f5f51e0f2435084c0a66f5c26f4403a75d3dd417b71a20450220545dc3637bcb49beedbbdf5063e05cad63be91af4f839886451c30ecd6edf1d20121021f110909ded653828a254515b58498a6bafc96799fb0851554463ed44ca7d9da00000000\",\n                         await cmds.serialize(jsontx))\n\n    async def test_serialize_custom_nsequence(self):\n        cmds = Commands(config=self.config)\n        jsontx = {\n            \"inputs\": [\n                {\n                    \"prevout_hash\": \"9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539\",\n                    \"prevout_n\": 1,\n                    \"value_sats\": 1000000,\n                    \"privkey\": \"p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD\",\n                    \"nsequence\": 0xfffffffd\n                }\n            ],\n            \"outputs\": [\n                {\n                    \"address\": \"tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd\",\n                    \"value_sats\": 990000\n                }\n            ]\n        }\n        self.assertEqual(\"0200000000010139c5375fe9da7bd377c1783002b129f8c57d3e724d62f5eacb9739ca691a229d0100000000fdffffff01301b0f0000000000160014ac0e2d229200bffb2167ed6fd196aef9d687d8bb0247304402201c551df0458528d19ba1dd79b134dcf0055f7b029dfc3d0d024e6253d069d13e02206d03cfc85a6fc648acb6fc6be630e4567d1dd00ddbcdee551ee0711414e2f33f0121021f110909ded653828a254515b58498a6bafc96799fb0851554463ed44ca7d9da00000000\",\n                         await cmds.serialize(jsontx))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_getprivatekeyforpath(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            'north rent dawn bunker hamster invest wagon market romance pig either squeeze',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n        cmds = Commands(config=self.config)\n        self.assertEqual(\"p2wpkh:cUzm7zPpWgLYeURgff4EsoMjhskCpsviBH4Y3aZcrBX8UJSRPjC2\",\n                         await cmds.getprivatekeyforpath([0, 10000], wallet=wallet))\n        self.assertEqual(\"p2wpkh:cUzm7zPpWgLYeURgff4EsoMjhskCpsviBH4Y3aZcrBX8UJSRPjC2\",\n                         await cmds.getprivatekeyforpath(\"m/0/10000\", wallet=wallet))\n        self.assertEqual(\"p2wpkh:cQAj4WGf1socCPCJNMjXYCJ8Bs5JUAk5pbDr4ris44QdgAXcV24S\",\n                         await cmds.getprivatekeyforpath(\"m/5h/100000/88h/7\", wallet=wallet))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_payto(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            'disagree rug lemon bean unaware square alone beach tennis exhibit fix mimic',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n        # bootstrap wallet\n        funding_tx = Transaction('0200000000010165806607dd458280cb57bf64a16cf4be85d053145227b98c28932e953076b8e20000000000fdffffff02ac150700000000001600147e3ddfe6232e448a8390f3073c7a3b2044fd17eb102908000000000016001427fbe3707bc57e5bb63d6f15733ec88626d8188a02473044022049ce9efbab88808720aa563e2d9bc40226389ab459c4390ea3e89465665d593502206c1c7c30a2f640af1e463e5107ee4cfc0ee22664cfae3f2606a95303b54cdef80121026269e54d06f7070c1f967eb2874ba60de550dfc327a945c98eb773672d9411fd77181e00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('ede61d39e501d65ccf34e6300da439419c43393f793bb9a8a4b06b2d0d80a8a0', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        cmds = Commands(config=self.config)\n        tx_str = await cmds.payto(\n            destination=\"tb1qsyzgpwa0vg2940u5t6l97etuvedr5dejpf9tdy\",\n            amount=\"0.00123456\",\n            feerate=50,\n            locktime=1972344,\n            wallet=wallet)\n\n        tx_str_2 = await cmds.payto(\n            destination=\"tb1qsyzgpwa0vg2940u5t6l97etuvedr5dejpf9tdy\",\n            amount=\"0.00123456\",\n            feerate=\"50.000\",  # test that passing a string feerate results in the same tx\n            locktime=1972344,\n            wallet=wallet)\n\n        self.assertEqual(tx_str, tx_str_2)\n        tx = tx_from_any(tx_str)\n        self.assertEqual(2, len(tx.outputs()))\n        txout = TxOutput.from_address_and_value(\"tb1qsyzgpwa0vg2940u5t6l97etuvedr5dejpf9tdy\", 123456)\n        self.assertTrue(txout in tx.outputs())\n        self.assertEqual(\"02000000000101a0a8800d2d6bb0a4a8b93b793f39439c4139a40d30e634cf5cd601e5391de6ed0100000000fdffffff0240e2010000000000160014810480bbaf62145abf945ebe5f657c665a3a3732462b060000000000160014a5103285eb519f826520a9f7d3227e1eaa7ec5f802473044022057a6f4b1ec63336c7d0ba233e785ec9f2e2d9c2d67617a50e069f4498ee6a3b7022032fb331e0bef06f46e9cb77bfe94413142653c4912516835e941fa7f170c1a53012103001b55f19541faaf7e6d57dd1bdb9fdc37725fc500e12f2418cc11e0aed4154978181e00\",\n                         tx_str)\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_payto__confirmed_only(self, mock_save_db):\n        \"\"\"test that payto respects 'confirmed_only' config var\"\"\"\n        wallet = restore_wallet_from_text__for_unittest(\n            'disagree rug lemon bean unaware square alone beach tennis exhibit fix mimic',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n        # bootstrap wallet\n        funding_tx = Transaction('0200000000010165806607dd458280cb57bf64a16cf4be85d053145227b98c28932e953076b8e20000000000fdffffff02ac150700000000001600147e3ddfe6232e448a8390f3073c7a3b2044fd17eb102908000000000016001427fbe3707bc57e5bb63d6f15733ec88626d8188a02473044022049ce9efbab88808720aa563e2d9bc40226389ab459c4390ea3e89465665d593502206c1c7c30a2f640af1e463e5107ee4cfc0ee22664cfae3f2606a95303b54cdef80121026269e54d06f7070c1f967eb2874ba60de550dfc327a945c98eb773672d9411fd77181e00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('ede61d39e501d65ccf34e6300da439419c43393f793bb9a8a4b06b2d0d80a8a0', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        cmds = Commands(config=self.config)\n\n        async def create_tx():\n            return await cmds.payto(\n                destination=\"tb1qsyzgpwa0vg2940u5t6l97etuvedr5dejpf9tdy\",\n                amount=\"0.00123456\",\n                feerate=50,\n                locktime=1972344,\n                wallet=wallet)\n\n        self.config.WALLET_SPEND_CONFIRMED_ONLY = True\n        with self.assertRaises(NotEnoughFunds):\n            tx_str = await create_tx()\n\n        self.config.WALLET_SPEND_CONFIRMED_ONLY = None  # default: false\n        tx_str = await create_tx()\n\n        tx = tx_from_any(tx_str)\n        self.assertEqual(2, len(tx.outputs()))\n        txout = TxOutput.from_address_and_value(\"tb1qsyzgpwa0vg2940u5t6l97etuvedr5dejpf9tdy\", 123456)\n        self.assertTrue(txout in tx.outputs())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_paytomany_multiple_max_spends(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            'kit virtual quantum festival fortune inform ladder saddle filter soldier start ghost',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n        # bootstrap wallet\n        funding_tx = Transaction('02000000000101f59876b1c65bbe3e182ccc7ea7224fe397bb9b70aadcbbf4f4074c75c8a074840000000000fdffffff021f351f00000000001600144eec851dd980cc36af1f629a32325f511604d6af56732d000000000016001439267bc7f3e3fabeae3bc3f73880de22d8b01ba50247304402207eac5f639806a00878488d58ca651d690292145bca5511531845ae21fab309d102207162708bd344840cc1bacff1092e426eb8484f83f5c068ba4ca579813de324540121020e0798c267ff06ee8b838cd465f3cfa6c843a122a04917364ce000c29ca205cae5f31f00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('e8e977bd9c857d84ec1b8f154ae2ee5dfa49fffb7688942a586196c1ad15de15', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        cmds = Commands(config=self.config)\n        tx_str = await cmds.paytomany(\n            outputs=[[\"tb1qk3g0t9pw5wctkzz7gh6k3ljfuukn729s67y54e\", 0.002],\n                     [\"tb1qr7evucrllljtryam6y2k3ntmlptq208pghql2h\", \"2!\"],\n                     [\"tb1qs3msqp0n0qade2haanjw2dkaa5lm77vwvce00h\", 0.003],\n                     [\"tb1qar4ye43tdfj6y5n3yndp9adhs2wuz2v0wgqn5l\", \"3!\"]],\n            fee=\"0.00005000\",\n            locktime=2094054,\n            wallet=wallet)\n\n        tx = tx_from_any(tx_str)\n        self.assertEqual(4, len(tx.outputs()))\n        self.assertEqual(\"0200000000010115de15adc19661582a948876fbff49fa5deee24a158f1bec847d859cbd77e9e80100000000fdffffff04400d030000000000160014b450f5942ea3b0bb085e45f568fe49e72d3f28b0e09304000000000016001484770005f3783adcaafdece4e536dded3fbf798e12190f00000000001600141fb2ce607fffe4b193bbd11568cd7bf856053ce19ca5160000000000160014e8ea4cd62b6a65a2527124da12f5b7829dc1298f02473044022079570c62352d7c462ee50851d27f829f7ea5757d258b6b38a6b377a4910ba597022056653f1b15a9693ba790e89ebac60e33b7a1d8357e05cd3d7ecc1ae00e9ab4a8012102eed460ead0cbaa71ad52b70899acf4ea12682ab237207b045c5cf9c6d11c2bcfe6f31f00\",\n                         tx_str)\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_signtransaction_without_wallet(self, mock_save_db):\n        cmds = Commands(config=self.config)\n        unsigned_tx = \"70736274ff0100a0020000000221d3645ba44f33fff6fe2666dc080279bc34b531c66888729712a80b204a32a10100000000fdffffffdd7f90d51acf98dc45ad7489316a983868c75e16bf14ffeb9eae01603a7b4da40100000000fdffffff02e8030000000000001976a9149a9ec2b35a7660c80dae38dd806fdf9b0fde68fd88ac74c11000000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88aca79b1d00000100e102000000018ba8cf9f0ff0b44c389e4a1cd25c0770636d95ccef161e313542647d435a5fd0000000006a4730440220373b3989905177f2e36d7e3d02b967d03092747fe7bbd3ba7b2c24623a88538c02207be79ee1d981060c2be6783f4946ce1bda1f64671b349ef14a4a6fecc047a71e0121030de43c5ed4c6272d20ce3becf3fb7afd5c3ccfb5d58ddfdf3047981e0b005e0dfdffffff02c0010700000000001976a9141cd3eb65bce2cae9f54544b65e46b3ad1f0b187288ac40420f00000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88ac979b1d00000100e102000000014e39236158716e91b0b2170ebe9d6b359d139e9ebfff163f2bafd0bec9890d04000000006a473044022070340deb95ca25ef86c4c7a9539b5c8f7b8351941635450311f914cd9c2f45ea02203fa7576e032ab5ae4763c78f5c2124573213c956286fd766582d9462515dc6540121033f6737e40a3a6087bc58bc5b82b427f9ed26d710b8fe2f70bfdd3d62abebcf74fdffffff02e8030000000000001976a91490350959750b3b38e451df16bd5957b7649bf5d288acac840100000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88ac979b1d00000000\"\n        privkey = \"cVtE728tULSA4gut4QWxo218q6PRsXHQAv84SXix83cuvScvGd1H\"\n        self.assertEqual(\"020000000221d3645ba44f33fff6fe2666dc080279bc34b531c66888729712a80b204a32a1010000006a47304402205b30e188e30c846f98dacc714c16b7cd3a58a3fa24973d289683c9d32813e24c0220153855a29e96fb083084417ba3e3873ccaeb08435dad93773ab60716f94a36160121033f6737e40a3a6087bc58bc5b82b427f9ed26d710b8fe2f70bfdd3d62abebcf74fdffffffdd7f90d51acf98dc45ad7489316a983868c75e16bf14ffeb9eae01603a7b4da4010000006a473044022010daa3dadf53bdcb071c6eff6b8787e3f675ed61feb4fef72d0bf9d99c0162f802200e73abd880b6f2ee5fe8c0abab731f1dddeb0f60df5e050a79c365bd718da1c80121033f6737e40a3a6087bc58bc5b82b427f9ed26d710b8fe2f70bfdd3d62abebcf74fdffffff02e8030000000000001976a9149a9ec2b35a7660c80dae38dd806fdf9b0fde68fd88ac74c11000000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88aca79b1d00\",\n                         await cmds.signtransaction_with_privkey(tx=unsigned_tx, privkey=privkey))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_signtransaction_with_wallet(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            'bitter grass shiver impose acquire brush forget axis eager alone wine silver',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n\n        # bootstrap wallet1\n        funding_tx = Transaction('01000000014576dacce264c24d81887642b726f5d64aa7825b21b350c7b75a57f337da6845010000006b483045022100a3f8b6155c71a98ad9986edd6161b20d24fad99b6463c23b463856c0ee54826d02200f606017fd987696ebbe5200daedde922eee264325a184d5bbda965ba5160821012102e5c473c051dae31043c335266d0ef89c1daab2f34d885cc7706b267f3269c609ffffffff0240420f00000000001600148a28bddb7f61864bdcf58b2ad13d5aeb3abc3c42a2ddb90e000000001976a914c384950342cb6f8df55175b48586838b03130fad88ac00000000')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 1000000\n        self.assertEqual('add2535aedcbb5ba79cc2260868bb9e57f328738ca192937f2c92e0e94c19203', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        cmds = Commands(config=self.config)\n\n        unsigned_tx = \"cHNidP8BAHECAAAAAQOSwZQOLsnyNykZyjiHMn/luYuGYCLMebq1y+1aU9KtAAAAAAD+////AigjAAAAAAAAFgAUaQtZqBQGAvsjzCkE7OnMTa82EFIwGw8AAAAAABYAFKwOLSKSAL/7IWftb9GWrvnWh9i7AAAAAAABAN8BAAAAAUV22sziZMJNgYh2Qrcm9dZKp4JbIbNQx7daV/M32mhFAQAAAGtIMEUCIQCj+LYVXHGpitmYbt1hYbINJPrZm2RjwjtGOFbA7lSCbQIgD2BgF/2YdpbrvlIA2u3eki7uJkMloYTVu9qWW6UWCCEBIQLlxHPAUdrjEEPDNSZtDvicHaqy802IXMdwayZ/MmnGCf////8CQEIPAAAAAAAWABSKKL3bf2GGS9z1iyrRPVrrOrw8QqLduQ4AAAAAGXapFMOElQNCy2+N9VF1tIWGg4sDEw+tiKwAAAAAIgYDD67ptKJbfbggI8qYkZJxLN1MtT09kzhZHHkJ5YGuHAwQsuNafQAAAIAAAAAAAAAAAAAiAgKFhOeJ459BORsvJ4UsoYq+wGpUEcIb41D+1h7scSDeUxCy41p9AAAAgAEAAAAAAAAAAAA=\"\n\n        self.assertEqual(\"020000000001010392c1940e2ec9f2372919ca3887327fe5b98b866022cc79bab5cbed5a53d2ad0000000000feffffff022823000000000000160014690b59a8140602fb23cc2904ece9cc4daf361052301b0f0000000000160014ac0e2d229200bffb2167ed6fd196aef9d687d8bb02473044022027e1e37172e52b2d84106663cff5bcf6e447dcb41f6483f99584cfb4de2785f4022005c72f6324ad130c78fca43fe5fc565526d1723f2c9dc3efea78f66d7ae9d4360121030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c00000000\",\n                         await cmds.signtransaction(tx=unsigned_tx, wallet=wallet))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_bumpfee(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            'right nominee cheese afford exotic pilot mask illness rug fringe degree pottery',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']  # type: Abstract_Wallet\n\n        funding_tx = Transaction(\"02000000000102789e8aa8caa79d87241ff9df0e3fd757a07c85a30195d76e8efced1d57c56b670000000000fdffffff7ee2b6abd52b332f797718ae582f8d3b979b83b1799e0a3bfb2c90c6e070c29e0100000000fdffffff020820000000000000160014c0eb720c93a61615d2d66542d381be8943ca553950c3000000000000160014d7dbd0196a2cbd76420f14a19377096cf6cddb75024730440220485b491ad8d3ce3b4da034a851882da84a06ec9800edff0d3fd6aa42eeba3b440220359ea85d32a05932ac417125e133fa54e54e7e9cd20ebc54b883576b8603fd65012103860f1fbf8a482b9d35d7d4d04be8fb33d856a514117cd8b73e372d36895feec60247304402206c2ca56cc030853fa59b4b3cb293f69a3378ead0f10cb76f640f8c2888773461022079b7055d0f6af6952a48e5b97218015b0723462d667765c142b41bd35e3d9c0a01210359e303f57647094a668d69e8ff0bd46c356d00aa7da6dc533c438e71c057f0793e721f00\")\n        funding_txid = funding_tx.txid()\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        cmds = Commands(config=self.config)\n        orig_rawtx = \"02000000000101b9723dfc69af058ef6613539a000d2cd098a2c8a74e802b6d8739db708ba8c9a0100000000fdffffff02a00f00000000000016001429e1fd187f0cac845946ae1b11dc136c536bfc0fe8b2000000000000160014100611bcb3aee7aad176936cf4ed56ade03027aa02473044022063c05e2347f16251922830ccc757231247b3c2970c225f988e9204844a1ab7b802204652d2c4816707e3d3bea2609b83b079001a435bad2a99cc2e730f276d07070c012102ee3f00141178006c78b0b458aab21588388335078c655459afe544211f15aee050721f00\"\n        orig_tx = tx_from_any(orig_rawtx)\n        orig_txid = orig_tx.txid()\n        self.assertEqual(\"02000000000101b9723dfc69af058ef6613539a000d2cd098a2c8a74e802b6d8739db708ba8c9a0100000000fdffffff02a00f00000000000016001429e1fd187f0cac845946ae1b11dc136c536bfc0f84b2000000000000160014100611bcb3aee7aad176936cf4ed56ade03027aa0247304402203aa63539b673a3bd70a76482b17f35f8843974fab28f84143a00450789010bc40220779c2ce2d0217f973f1f6c9f718e19fc7ebd14dd8821a962f002437cda3082ec012102ee3f00141178006c78b0b458aab21588388335078c655459afe544211f15aee000000000\",\n                         await cmds.bumpfee(tx=orig_rawtx, new_fee_rate='1.6', wallet=wallet))\n        # test txid as first arg\n        # -> first test while NOT having the tx in the wallet db:\n        with self.assertRaises(Exception) as ctx:\n            await cmds.bumpfee(tx=orig_txid, new_fee_rate='1.6', wallet=wallet)\n        self.assertTrue(\"Transaction not in wallet\" in ctx.exception.args[0])\n        # -> now test while having the tx:\n        assert wallet.adb.add_transaction(orig_tx)\n        self.assertEqual(\"02000000000101b9723dfc69af058ef6613539a000d2cd098a2c8a74e802b6d8739db708ba8c9a0100000000fdffffff02a00f00000000000016001429e1fd187f0cac845946ae1b11dc136c536bfc0f84b2000000000000160014100611bcb3aee7aad176936cf4ed56ade03027aa0247304402203aa63539b673a3bd70a76482b17f35f8843974fab28f84143a00450789010bc40220779c2ce2d0217f973f1f6c9f718e19fc7ebd14dd8821a962f002437cda3082ec012102ee3f00141178006c78b0b458aab21588388335078c655459afe544211f15aee000000000\",\n                         await cmds.bumpfee(tx=orig_txid, new_fee_rate='1.6', wallet=wallet))\n        wallet.adb.remove_transaction(orig_txid)  # undo side-effect on wallet\n        # test \"from_coins\" arg\n        self.assertEqual(\"02000000000101b9723dfc69af058ef6613539a000d2cd098a2c8a74e802b6d8739db708ba8c9a0100000000fdffffff02a00f00000000000016001429e1fd187f0cac845946ae1b11dc136c536bfc0f84b2000000000000160014100611bcb3aee7aad176936cf4ed56ade03027aa0247304402203aa63539b673a3bd70a76482b17f35f8843974fab28f84143a00450789010bc40220779c2ce2d0217f973f1f6c9f718e19fc7ebd14dd8821a962f002437cda3082ec012102ee3f00141178006c78b0b458aab21588388335078c655459afe544211f15aee000000000\",\n                         await cmds.bumpfee(tx=orig_rawtx, new_fee_rate='1.6', from_coins=\"9a8cba08b79d73d8b602e8748a2c8a09cdd200a0393561f68e05af69fc3d72b9:1\", wallet=wallet))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_importprivkey(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            'p2wpkh:cQUdWZehnGDwGn7CSc911cJBcWTAcnyzpLoJYTsFNYW1w6iaq7Nw p2wpkh:cNHsDLo137ngrr2wGf3mwqpwTUvpuDVAZrqzan9heHcMTK4rP5JB',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n        cmds = Commands(config=self.config)\n        self.assertEqual(2, len(wallet.get_addresses()))\n        # try importing a single bad privkey\n        out = await cmds.importprivkey(\"asdasd\", wallet=wallet)  # type: str\n        self.assertTrue(out.startswith(\"Error: \"))\n        self.assertTrue(\"cannot deserialize privkey\" in out)\n        # try importing empty string\n        self.assertEqual(\"Error: no keys given\",\n                         await cmds.importprivkey(\"\", wallet=wallet))\n        # try importing a single good privkey\n        self.assertEqual(\"Keypair imported: mfgn4NuNberN5D9gvXaYwkqA6Q6WmF7wtD\",\n                         await cmds.importprivkey(\"cVam1duhd5wSxPPFJFKHNoDA2ZjRq7okvnBWyajsnAEcfPjC6Wbm\", wallet=wallet))\n        # try importing a list of good privkeys\n        privkeys1_str = \" \".join([\n            \"p2pkh:cR1C6p34Gt9gxNJ57rUy96jgN3HQcZCgQzDWtCDNCnx4iLXM2S6g\",\n            \"p2pkh:cR1xqAf2hhhfxwAzquDss7ALrMeUN5gR82qp1nRWjqSQppnCNa27\",\n            \"cMnMgCvkELEmmnpK8MbcdE8aWRMSCxFMCJU61YReXVXiqjgjhee8\",\n            \"p2wpkh:cUfjuZDxEoATQwPmWCBH9kGArALfPij5JruQNfM6NTtYF12fds8Y\",\n            \"p2wpkh:cP2U7f2jgaQf1zBAWzNUrhs6mGRCg3uyTvNFUUQ9Q8eyXnpkXSqo\",\n            \"p2wpkh:cThVmpx3VgZRhbKQqK1FmLzaFTiUsN1Kp1CBwZVL6VfR33mNMxok\",\n        ])\n        self.assertEqual({\"good_keys\": 6, \"bad_keys\": 0},\n                         await cmds.importprivkey(privkeys1_str, wallet=wallet))\n        # try importing a list of mixed good/bad privkeys\n        privkeys2_str = \" \".join([\n            \"qweqwe\",\n            \"p2wpkh:cRFfD1EqocayY3xsw343inJ47LVsZHLbUgPzLmUbXhE6XNJ46Swn\",\n            \"p2pkh:cThVmpx3VgZRhbKQqK1FmLzaBAAADDDDkeeeeeeeeeeeeeeeeeeeey\",\n        ])\n        self.assertEqual({\"good_keys\": 1, \"bad_keys\": 2},\n                         await cmds.importprivkey(privkeys2_str, wallet=wallet))\n        self.assertEqual(10, len(wallet.get_addresses()))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_hold_invoice_commands(self, mock_save_db):\n        wallet: Abstract_Wallet = restore_wallet_from_text__for_unittest(\n            'disagree rug lemon bean unaware square alone beach tennis exhibit fix mimic',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n\n        cmds = Commands(config=self.config)\n        preimage: str = sha256(urandom(32)).hex()\n        payment_hash: str = sha256(bytes.fromhex(preimage)).hex()\n        with (mock.patch.object(wallet.lnworker, 'num_sats_can_receive', return_value=1000000)):\n            result = await cmds.add_hold_invoice(\n                payment_hash=payment_hash,\n                amount=Decimal(0.0001),\n                memo=\"test\",\n                expiry=3500,\n                wallet=wallet,\n            )\n        invoice = lndecode(invoice=result['invoice'])\n        assert invoice.paymenthash.hex() == payment_hash\n        assert wallet.lnworker.get_payment_info(bytes.fromhex(payment_hash), direction=RECEIVED)\n        assert payment_hash in wallet.lnworker.dont_expire_htlcs\n        assert invoice.get_amount_sat() == 10000\n        assert invoice.get_description() == \"test\"\n        assert wallet.get_label_for_rhash(rhash=invoice.paymenthash.hex()) == \"test\"\n        assert invoice.get_expiry() == 3500\n\n        cancel_result = await cmds.cancel_hold_invoice(\n            payment_hash=payment_hash,\n            wallet=wallet,\n        )\n        assert not wallet.lnworker.get_payment_info(bytes.fromhex(payment_hash), direction=RECEIVED)\n        assert payment_hash not in wallet.lnworker.dont_expire_htlcs\n        assert wallet.get_label_for_rhash(rhash=invoice.paymenthash.hex()) == \"\"\n        assert cancel_result['cancelled'] == payment_hash\n\n        with self.assertRaises(AssertionError):\n            # settling a cancelled invoice should raise\n            await cmds.settle_hold_invoice(\n                preimage=preimage,\n                wallet=wallet,\n            )\n        with self.assertRaises(AssertionError):\n            # cancelling an unknown invoice should raise\n            await cmds.cancel_hold_invoice(\n                payment_hash=sha256(urandom(32)).hex(),\n                wallet=wallet,\n            )\n\n        # add another hold invoice\n        preimage: bytes = sha256(urandom(32))\n        payment_hash: str = sha256(preimage).hex()\n        with mock.patch.object(wallet.lnworker, 'num_sats_can_receive', return_value=1000000):\n            await cmds.add_hold_invoice(\n                payment_hash=payment_hash,\n                amount=Decimal(0.0001),\n                wallet=wallet,\n            )\n\n        mock_htlc1 = mock.Mock()\n        mock_htlc1.htlc.cltv_abs = 800_000\n        mock_htlc1.htlc.amount_msat = 4_500_000\n        mock_htlc2 = mock.Mock()\n        mock_htlc2.htlc.cltv_abs = 800_144\n        mock_htlc2.htlc.amount_msat = 5_500_000\n        mock_htlc_status = mock.Mock()\n        mock_htlc_status.htlcs = [mock_htlc1, mock_htlc2]\n        mock_htlc_status.resolution = RecvMPPResolution.COMPLETE\n\n        payment_key = wallet.lnworker._get_payment_key(bytes.fromhex(payment_hash)).hex()\n        with mock.patch.dict(wallet.lnworker.received_mpp_htlcs, {payment_key: mock_htlc_status}):\n            status: dict = await cmds.check_hold_invoice(payment_hash=payment_hash, wallet=wallet)\n            assert status['status'] == 'paid'\n            assert status['received_amount_sat'] == 10000\n            assert status['closest_htlc_expiry_height'] == 800_000\n\n            settle_result = await cmds.settle_hold_invoice(\n                preimage=preimage.hex(),\n                wallet=wallet,\n            )\n        assert settle_result['settled'] == payment_hash\n        assert wallet.lnworker._preimages[payment_hash][0] == preimage.hex()\n        with (mock.patch.object(\n            wallet.lnworker,\n            'get_payment_value',\n            return_value=(None, 10000*1000, None, None),\n        )):\n            settled_status: dict = await cmds.check_hold_invoice(payment_hash=payment_hash, wallet=wallet)\n            assert settled_status['status'] == 'settled'\n            assert settled_status['received_amount_sat'] == 10000\n            assert settled_status['invoice_amount_sat'] == 10000\n            assert settled_status['preimage'] == preimage.hex()\n\n        with self.assertRaises(AssertionError):\n            # cancelling a settled invoice should raise\n            await cmds.cancel_hold_invoice(payment_hash=payment_hash, wallet=wallet)\n\n    @mock.patch.object(storage.WalletStorage, 'write')\n    @mock.patch.object(storage.WalletStorage, 'append')\n    async def test_onchain_history(self, *mock_args):\n        cmds = Commands(config=self.config, daemon=self.daemon)\n        wallet_path = self.get_wallet_file_path(\"client_3_3_8_xpub_with_realistic_history\")\n        await cmds.load_wallet(wallet_path=wallet_path)\n\n        expected_last_history_item = {\n            \"amount_sat\": -500200,\n            \"bc_balance\": \"0.75136687\",\n            \"bc_value\": \"-0.005002\",\n            \"confirmations\": 968,\n            \"date\": \"2020-07-02 11:57+00:00\",  # kind of a hack. normally, there is no timezone offset here\n            \"fee_sat\": 200,\n            \"group_id\": None,\n            \"height\": 1774910,\n            \"incoming\": False,\n            \"label\": \"\",\n            \"monotonic_timestamp\": 1593691025,\n            \"timestamp\": 1593691025,\n            \"txid\": \"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc\",\n            \"txpos_in_block\": 44,\n            \"wanted_height\": None,\n        }\n\n        hist = await cmds.onchain_history(wallet_path=wallet_path)\n        self.assertEqual(len(hist), 89)\n        self.assertEqual(hist[-1], expected_last_history_item)\n\n        with self.subTest(msg=\"'show_addresses' param\"):\n            hist = await cmds.onchain_history(wallet_path=wallet_path, show_addresses=True)\n            self.assertEqual(len(hist), 89)\n            self.assertEqual(\n                hist[-1],\n                expected_last_history_item | {\n                    'inputs': [\n                        {\n                            'coinbase': False,\n                            'nsequence': 4294967293,\n                            'prevout_hash': 'd42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67',\n                            'prevout_n': 0,\n                            'scriptSig': '',\n                            'witness': [\n                                '3044022056e0a02c45b5e4f93dc533c7f3fa95296684b0f41019ae91b5b7b083a5b651c202200a0e0c56bdfa299f4af8c604d359033863c9ce0a7fdd35acfbda5cff4a6ffa3301',\n                                '02eba8ba71542a884f2eec1f40594192be2628268f9fa141c9b12b026008dbb274'\n                            ]\n                        }\n                    ],\n                    'outputs': [\n                        {'address': 'tb1qr5mf6sumdlhjrq9t6wlyvdm960zu0n0t5d60ug', 'value_sat': 500000},\n                        {'address': 'tb1qp3p2d72gj2l7r6za056tgu4ezsurjphper4swh', 'value_sat': 762100}\n                    ],\n                }\n            )\n        with self.subTest(msg=\"'from_height' / 'to_height' params\"):\n            hist = await cmds.onchain_history(wallet_path=wallet_path, from_height=1638866, to_height=1665815)\n            self.assertEqual(len(hist), 8)\n        with self.subTest(msg=\"'year' param\"):\n            hist = await cmds.onchain_history(wallet_path=wallet_path, year=2019)\n            self.assertEqual(len(hist), 23)\n        with self.subTest(msg=\"timestamp and block height based filtering cannot be used together\"):\n            with self.assertRaises(UserFacingException):\n                hist = await cmds.onchain_history(wallet_path=wallet_path, year=2019, from_height=1638866, to_height=1665815)\n        with self.subTest(msg=\"'show_fiat' param\"):\n            self.config.FX_USE_EXCHANGE_RATE = True\n            hist = await cmds.onchain_history(wallet_path=wallet_path, show_fiat=True)\n            self.assertEqual(len(hist), 89)\n            self.assertEqual(\n                hist[-1],\n                expected_last_history_item | {\n                    \"acquisition_price\": \"41.67\",\n                    \"capital_gain\": \"-1.16\",\n                    \"fiat_currency\": \"EUR\",\n                    \"fiat_default\": True,\n                    \"fiat_fee\": \"0.02\",\n                    \"fiat_rate\": \"8097.91\",\n                    \"fiat_value\": \"-40.51\",\n                }\n            )\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_get_submarine_swap_providers(self, *mock_args):\n        wallet = restore_wallet_from_text__for_unittest(\n            'disagree rug lemon bean unaware square alone beach tennis exhibit fix mimic',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n\n        cmds = Commands(config=self.config)\n\n        offer1 = SwapOffer(\n            pairs=SwapFees(\n                percentage=Decimal('0.5'),\n                mining_fee=2000,\n                min_amount=10000,\n                max_forward=1000000,\n                max_reverse=500000\n            ),\n            relays=[\"wss://relay1.example.com\", \"wss://relay2.example.com\"],\n            timestamp=1640995200,\n            server_pubkey=\"a8cffad54f59e2c50a1d40ec0d57f1fc32df9cd2101fad8000215eb4a75b334d\",\n            pow_bits=10\n        )\n\n        offer2 = SwapOffer(\n            pairs=SwapFees(\n                percentage=Decimal('1.0'),\n                mining_fee=3000,\n                min_amount=20000,\n                max_forward=2000000,\n                max_reverse=1000000\n            ),\n            relays=[\"ws://relay3.example.onion\", \"wss://relay4.example.com\"],\n            timestamp=1640995300,\n            server_pubkey=\"7a483b6546be900481f6be2d2cc1b47c779ee89b4b66d1a066a8dc81c63ad1f0\",\n            pow_bits=12\n        )\n        mock_offers = [offer1, offer2]\n        mock_transport = mock.Mock(NostrTransport)\n        mock_transport.get_recent_offers.return_value = mock_offers\n\n        with mock.patch.object(\n            wallet.lnworker.swap_manager,\n            'create_transport'\n        ) as mock_create_transport:\n            mock_create_transport.return_value.__aenter__.return_value = mock_transport\n\n            result = await cmds.get_submarine_swap_providers(query_time=1, wallet=wallet)\n\n        expected_result = {\n            offer1.server_npub: {\n                \"percentage_fee\": offer1.pairs.percentage,\n                \"max_forward_sat\": offer1.pairs.max_forward,\n                \"max_reverse_sat\": offer1.pairs.max_reverse,\n                \"min_amount_sat\": offer1.pairs.min_amount,\n                \"prepayment\": 2 * offer1.pairs.mining_fee,\n            },\n            offer2.server_npub: {\n                \"percentage_fee\": offer2.pairs.percentage,\n                \"max_forward_sat\": offer2.pairs.max_forward,\n                \"max_reverse_sat\": offer2.pairs.max_reverse,\n                \"min_amount_sat\": offer2.pairs.min_amount,\n                \"prepayment\": 2 * offer2.pairs.mining_fee,\n            }\n        }\n        self.assertEqual(result, expected_result)\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_export_lightning_preimage(self, *mock_args):\n        w = restore_wallet_from_text__for_unittest(\n            'disagree rug lemon bean unaware square alone beach tennis exhibit fix mimic',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n        cmds = Commands(config=self.config)\n\n        preimage = os.urandom(32)\n        payment_hash = sha256(preimage)\n        w.lnworker.save_preimage(payment_hash, preimage)\n\n        assert await cmds.export_lightning_preimage(payment_hash=payment_hash.hex(), wallet=w) == preimage.hex()\n        assert await cmds.export_lightning_preimage(payment_hash=os.urandom(32).hex(), wallet=w) is None\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    @mock.patch('electrum.commands.LN_P2P_NETWORK_TIMEOUT', 0.001)\n    async def test_add_peer(self, *mock_args):\n        w = restore_wallet_from_text__for_unittest(\n            'disagree rug lemon bean unaware square alone beach tennis exhibit fix mimic',\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']\n        cmds = Commands(config=self.config)\n\n        # Mock the network and lnworker\n        mock_lnworker = mock.Mock()\n        mock_lnworker.lnpeermgr = mock.Mock()\n        w.lnworker = mock_lnworker\n        mock_peer = mock.Mock()\n        mock_peer.initialized = asyncio.Future()\n        connection_string = \"test_node_id@127.0.0.1:9735\"\n        called = False\n        async def lnpeermgr_add_peer(*args, **kwargs):\n            assert args[0] == connection_string\n            nonlocal called\n            called += 1\n            return mock_peer\n        mock_lnworker.lnpeermgr.add_peer = lnpeermgr_add_peer\n\n        # check if add_peer times out if peer doesn't initialize (LN_P2P_NETWORK_TIMEOUT is 0.001s)\n        with self.assertRaises(UserFacingException):\n            await cmds.add_peer(connection_string=connection_string, wallet=w)\n        # check if add_peer called lnpeermgr.add_peer\n        assert called == 1\n\n        mock_peer.initialized = asyncio.Future()\n        mock_peer.initialized.set_result(True)\n        # check if add_peer returns True if peer is initialized\n        result = await cmds.add_peer(connection_string=connection_string, wallet=w)\n        assert called == 2\n        self.assertTrue(result)\n"
  },
  {
    "path": "tests/test_contacts.py",
    "content": "import os\n\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.wallet import Abstract_Wallet\nfrom electrum.daemon import Daemon\n\nfrom . import ElectrumTestCase\nfrom . import restore_wallet_from_text__for_unittest\n\n\nclass TestContacts(ElectrumTestCase):\n    TESTNET = True\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n        self.wallet_path = os.path.join(self.electrum_path, \"somewallet1\")\n\n    async def test_saving_contacts(self):\n        text = 'cross end slow expose giraffe fuel track awake turtle capital ranch pulp'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet_path, config=self.config)\n        w = d['wallet']  # type: Abstract_Wallet\n        w.contacts[\"myNNuLYNgHE92nGQuJd5mXo6gy9gKXEDyQ\"] = (\"address\", \"alice\")\n        w.contacts[\"tb1q4syjltptqwhe62t3u5gwz9nsw87kmcwx003z05\"] = (\"address\", \"bob\")\n        self.assertEqual(2, len(w.contacts))\n        await w.stop()\n        del w\n        # re-open wallet from disk\n        w = Daemon._load_wallet(self.wallet_path, password=None, config=self.config)\n        self.assertEqual(2, len(w.contacts))\n        w.contacts[\"n4STqqWPrvkapAyvXY2wJzfoKMnuJbDWoH\"] = (\"address\", \"carol\")\n        self.assertEqual(3, len(w.contacts))\n"
  },
  {
    "path": "tests/test_daemon.py",
    "content": "import asyncio\nfrom collections import defaultdict\nimport os\nfrom typing import Optional, Iterable\n\nfrom electrum.commands import Commands\nfrom electrum.daemon import Daemon\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.wallet import Abstract_Wallet\nfrom electrum.lnworker import LNWallet, LNPeerManager\nfrom electrum.lnwatcher import LNWatcher\nfrom electrum import util\nfrom electrum.utils.memory_leak import count_objects_in_memory\n\nfrom . import ElectrumTestCase, as_testnet, restore_wallet_from_text__for_unittest\n\n\nclass DaemonTestCase(ElectrumTestCase):\n    config: 'SimpleConfig'\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n        self.config.NETWORK_OFFLINE = True\n\n        self.wallet_dir = os.path.dirname(self.config.get_wallet_path())\n        assert \"wallets\" == os.path.basename(self.wallet_dir)\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.daemon = Daemon(config=self.config, listen_jsonrpc=False)\n        assert self.daemon.network is None\n\n    async def asyncTearDown(self):\n        await self.daemon.stop()\n        await super().asyncTearDown()\n\n    def _restore_wallet_from_text(self, text, *, password: Optional[str], encrypt_file: bool = None, **kwargs) -> str:\n        \"\"\"Returns path for created wallet.\"\"\"\n        basename = util.get_new_wallet_name(self.wallet_dir)\n        path = os.path.join(self.wallet_dir, basename)\n        wallet_dict = restore_wallet_from_text__for_unittest(\n            text,\n            path=path,\n            password=password,\n            encrypt_file=encrypt_file,\n            config=self.config,\n            **kwargs,\n        )\n        # We return the path instead of the wallet object, as extreme\n        # care would be needed to use the wallet object directly:\n        # Unless the daemon knows about it, daemon._load_wallet might create a conflicting wallet object\n        # for the same fs path, and there would be two wallet objects contending for the same file.\n        return path\n\n\nclass TestUnifiedPassword(DaemonTestCase):\n\n    def setUp(self):\n        super().setUp()\n        self.config.WALLET_SHOULD_USE_SINGLE_PASSWORD = True\n\n    def _run_post_unif_sanity_checks(self, paths: Iterable[str], *, password: str):\n        for path in paths:\n            w = self.daemon.load_wallet(path, password)\n            self.assertIsNotNone(w)\n            w.check_password(password)\n            self.assertTrue(w.has_storage_encryption())\n            if w.can_have_keystore_encryption():\n                self.assertTrue(w.has_keystore_encryption())\n            if w.has_seed():\n                self.assertIsInstance(w.get_seed(password), str)\n        can_be_unified, is_unified, wallet_paths_can_unlock = self.daemon.check_password_for_directory(\n            old_password=password,\n            wallet_dir=self.wallet_dir,\n        )\n        self.assertEqual((True, True, len(paths)), (can_be_unified, is_unified, len(wallet_paths_can_unlock)))\n\n    # \"cannot unify pw\" tests --->\n\n    async def test_cannot_unify_two_std_wallets_both_have_ks_and_sto_enc(self):\n        path1 = self._restore_wallet_from_text(\"9dk\", password=\"123456\", encrypt_file=True)\n        path2 = self._restore_wallet_from_text(\"x8\",  password=\"asdasd\", encrypt_file=True)\n        with open(path1, \"rb\") as f:\n            raw1_before = f.read()\n        with open(path2, \"rb\") as f:\n            raw2_before = f.read()\n\n        can_be_unified, is_unified, _ = self.daemon.check_password_for_directory(old_password=\"123456\", wallet_dir=self.wallet_dir)\n        self.assertEqual((False, False), (can_be_unified, is_unified))\n        is_unified = self.daemon.update_password_for_directory(old_password=\"123456\", new_password=\"123456\")\n        self.assertFalse(is_unified)\n        # verify that files on disk haven't changed:\n        with open(path1, \"rb\") as f:\n            raw1_after = f.read()\n        with open(path2, \"rb\") as f:\n            raw2_after = f.read()\n        self.assertEqual(raw1_before, raw1_after)\n        self.assertEqual(raw2_before, raw2_after)\n\n    async def test_cannot_unify_mixed_wallets(self):\n        path1 = self._restore_wallet_from_text(\"9dk\", password=\"123456\", encrypt_file=True)\n        path2 = self._restore_wallet_from_text(\"9dk\",  password=\"asdasd\", encrypt_file=False)\n        path3 = self._restore_wallet_from_text(\"9dk\",  password=None)\n        with open(path1, \"rb\") as f:\n            raw1_before = f.read()\n        with open(path2, \"rb\") as f:\n            raw2_before = f.read()\n        with open(path3, \"rb\") as f:\n            raw3_before = f.read()\n\n        can_be_unified, is_unified, _ = self.daemon.check_password_for_directory(old_password=\"123456\", wallet_dir=self.wallet_dir)\n        self.assertEqual((False, False), (can_be_unified, is_unified))\n        is_unified = self.daemon.update_password_for_directory(old_password=\"123456\", new_password=\"123456\")\n        self.assertFalse(is_unified)\n        # verify that files on disk haven't changed:\n        with open(path1, \"rb\") as f:\n            raw1_after = f.read()\n        with open(path2, \"rb\") as f:\n            raw2_after = f.read()\n        with open(path3, \"rb\") as f:\n            raw3_after = f.read()\n        self.assertEqual(raw1_before, raw1_after)\n        self.assertEqual(raw2_before, raw2_after)\n        self.assertEqual(raw3_before, raw3_after)\n\n    # \"can unify pw\" tests --->\n\n    async def test_can_unify_two_std_wallets_both_have_ks_and_sto_enc(self):\n        path1 = self._restore_wallet_from_text(\"9dk\", password=\"123456\", encrypt_file=True)\n        path2 = self._restore_wallet_from_text(\"x8\",  password=\"123456\", encrypt_file=True)\n        can_be_unified, is_unified, _ = self.daemon.check_password_for_directory(old_password=\"123456\", wallet_dir=self.wallet_dir)\n        self.assertEqual((True, True), (can_be_unified, is_unified))\n        is_unified = self.daemon.update_password_for_directory(old_password=\"123456\", new_password=\"123456\")\n        self.assertTrue(is_unified)\n        self._run_post_unif_sanity_checks([path1, path2], password=\"123456\")\n\n    async def test_can_unify_two_std_wallets_one_has_ks_enc_other_has_both_enc(self):\n        path1 = self._restore_wallet_from_text(\"9dk\", password=\"123456\", encrypt_file=True)\n        path2 = self._restore_wallet_from_text(\"x8\",  password=\"123456\", encrypt_file=False)\n        with open(path2, \"rb\") as f:\n            raw2_before = f.read()\n\n        can_be_unified, is_unified, _ = self.daemon.check_password_for_directory(old_password=\"123456\", wallet_dir=self.wallet_dir)\n        self.assertEqual((True, False), (can_be_unified, is_unified))\n        is_unified = self.daemon.update_password_for_directory(old_password=\"123456\", new_password=\"123456\")\n        self.assertTrue(is_unified)\n        self._run_post_unif_sanity_checks([path1, path2], password=\"123456\")\n        # verify that file at path2 changed:\n        with open(path2, \"rb\") as f:\n            raw2_after = f.read()\n        self.assertNotEqual(raw2_before, raw2_after)\n\n    async def test_can_unify_two_std_wallets_one_without_password(self):\n        path1 = self._restore_wallet_from_text(\"9dk\", password=None)\n        path2 = self._restore_wallet_from_text(\"x8\",  password=\"123456\", encrypt_file=True)\n        can_be_unified, is_unified, _ = self.daemon.check_password_for_directory(old_password=\"123456\", wallet_dir=self.wallet_dir)\n        self.assertEqual((True, False), (can_be_unified, is_unified))\n        is_unified = self.daemon.update_password_for_directory(old_password=\"123456\", new_password=\"123456\")\n        self.assertTrue(is_unified)\n        self._run_post_unif_sanity_checks([path1, path2], password=\"123456\")\n\n    @as_testnet\n    async def test_can_unify_large_folder_yet_to_be_unified(self):\n        paths = []\n        # seed\n        paths.append(self._restore_wallet_from_text(\"9dk\", password=\"123456\", encrypt_file=True))\n        paths.append(self._restore_wallet_from_text(\"9dk\", password=\"123456\", encrypt_file=False))\n        paths.append(self._restore_wallet_from_text(\"9dk\", password=None))\n        # xpub\n        xpub = \"vpub5UqWay427dCjkpE3gPKLnkBUqDRoBed1328uNrLDoTyKo6HFSs9agfDMy1VXbVtcuBVRiAZQsPPsPdu1Ge8m8qvNZPyzJ4ecPsf6U1ieW4x\"\n        paths.append(self._restore_wallet_from_text(xpub, password=\"123456\", encrypt_file=True))\n        paths.append(self._restore_wallet_from_text(xpub, password=\"123456\", encrypt_file=False))\n        paths.append(self._restore_wallet_from_text(xpub, password=None))\n        # xprv\n        xprv = \"vprv9FrABTX8HFeSYL9aaMnLRcEkHBbJnBu9foDJaTvcF8SLvHx6uKqL8rtt7kTd66V4QPLfWPaCJMVZa3h9zuzLr7YFZd1uoEevqqyxp66oSbN\"\n        paths.append(self._restore_wallet_from_text(xprv, password=\"123456\", encrypt_file=True))\n        paths.append(self._restore_wallet_from_text(xprv, password=\"123456\", encrypt_file=False))\n        paths.append(self._restore_wallet_from_text(xprv, password=None))\n        # WIFs\n        wifs= \"p2wpkh:cRyfp9nJ8soK1bBUJAcWbMrsJZxKJpe7HBSxz5uXVbwydvUxz9zT p2wpkh:cV6J6T2AG4oXAXdYHAV6dbzR41QnGumDSVvWrmj2yYpos81RtyBK\"\n        paths.append(self._restore_wallet_from_text(wifs, password=\"123456\", encrypt_file=True))\n        paths.append(self._restore_wallet_from_text(wifs, password=\"123456\", encrypt_file=False))\n        paths.append(self._restore_wallet_from_text(wifs, password=None))\n        # addrs\n        addrs = \"tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd\"\n        paths.append(self._restore_wallet_from_text(addrs, password=\"123456\", encrypt_file=True))\n        paths.append(self._restore_wallet_from_text(addrs, password=\"123456\", encrypt_file=False))\n        paths.append(self._restore_wallet_from_text(addrs, password=None))\n        # do unification\n        can_be_unified, is_unified, _ = self.daemon.check_password_for_directory(old_password=\"123456\", wallet_dir=self.wallet_dir)\n        self.assertEqual((True, False), (can_be_unified, is_unified))\n        is_unified = self.daemon.update_password_for_directory(old_password=\"123456\", new_password=\"123456\")\n        self.assertTrue(is_unified)\n        self._run_post_unif_sanity_checks(paths, password=\"123456\")\n\n    # misc --->\n\n    async def test_wallet_objects_are_properly_garbage_collected_after_check_pw_for_dir(self):\n        orig_cb_count = util.callback_mgr.count_all_callbacks()\n        # GC sanity-check:\n        mclasses = [Abstract_Wallet, LNWallet, LNWatcher, LNPeerManager]\n        objmap = count_objects_in_memory(mclasses)\n        for mcls in mclasses:\n            self.assertEqual(len(objmap[mcls]), 0, msg=f\"too many lingering objs of type={mcls}\")\n        # restore some wallets\n        paths = []\n        paths.append(self._restore_wallet_from_text(\"9dk\", password=\"123456\", encrypt_file=True))\n        paths.append(self._restore_wallet_from_text(\"9dk\", password=\"123456\", encrypt_file=False))\n        paths.append(self._restore_wallet_from_text(\"9dk\", password=None))\n        paths.append(self._restore_wallet_from_text(\"9dk\", password=\"123456\", encrypt_file=True, passphrase=\"hunter2\"))\n        paths.append(self._restore_wallet_from_text(\"9dk\", password=\"999999\", encrypt_file=False, passphrase=\"hunter2\"))\n        paths.append(self._restore_wallet_from_text(\"9dk\", password=None, passphrase=\"hunter2\"))\n        # test unification\n        can_be_unified, is_unified, paths_succeeded = self.daemon.check_password_for_directory(old_password=\"123456\", wallet_dir=self.wallet_dir)\n        self.assertEqual((False, False, 5), (can_be_unified, is_unified, len(paths_succeeded)))\n        # gc\n        try:\n            async with util.async_timeout(5):\n                while True:\n                    objmap = count_objects_in_memory(mclasses)\n                    if sum(len(lst) for lst in objmap.values()) == 0:\n                        break  # all \"mclasses\"-type objects have been GC-ed\n                    await asyncio.sleep(0.01)\n        except asyncio.TimeoutError:\n            for mcls in mclasses:\n                self.assertEqual(len(objmap[mcls]), 0, msg=f\"too many lingering objs of type={mcls}\")\n        # also check callbacks have been cleaned up:\n        self.assertEqual(orig_cb_count, util.callback_mgr.count_all_callbacks())\n\n\nclass TestCommandsWithDaemon(DaemonTestCase):\n    TESTNET = True\n    SEED = \"bitter grass shiver impose acquire brush forget axis eager alone wine silver\"\n\n    async def test_wp_command_with_inmemory_wallet_has_password(self):\n        cmds = Commands(config=self.config, daemon=self.daemon)\n        wallet = restore_wallet_from_text__for_unittest(\n            self.SEED,\n            path=None,\n            password=\"123456\",\n            config=self.config)['wallet']\n        self.assertEqual(self.SEED, await cmds.getseed(wallet=wallet, password=\"123456\"))\n\n    async def test_wp_command_with_inmemory_wallet_no_password(self):\n        cmds = Commands(config=self.config, daemon=self.daemon)\n        wallet = restore_wallet_from_text__for_unittest(\n            self.SEED,\n            path=None,\n            config=self.config)['wallet']\n        self.assertEqual(self.SEED, await cmds.getseed(wallet=wallet))\n\n    async def test_wp_command_with_diskfile_wallet_has_password(self):\n        cmds = Commands(config=self.config, daemon=self.daemon)\n        wpath = self._restore_wallet_from_text(self.SEED, password=\"123456\", encrypt_file=True)\n        basename = os.path.basename(wpath)\n        await cmds.load_wallet(wallet_path=wpath, password=\"123456\")\n        wallet = self.daemon.get_wallet(wpath)\n        self.assertIsInstance(wallet, Abstract_Wallet)\n        self.assertEqual(self.SEED, await cmds.getseed(wallet_path=wpath, password=\"123456\"))\n        self.assertEqual(self.SEED, await cmds.getseed(wallet_path=basename, password='123456'))\n        self.assertEqual(self.SEED, await cmds.getseed(wallet=wallet, password=\"123456\"))\n\n    async def test_wp_command_with_diskfile_wallet_no_password(self):\n        cmds = Commands(config=self.config, daemon=self.daemon)\n        wpath = self._restore_wallet_from_text(self.SEED, password=None)\n        basename = os.path.basename(wpath)\n        await cmds.load_wallet(wallet_path=wpath, password=None)\n        wallet = self.daemon.get_wallet(wpath)\n        self.assertIsInstance(wallet, Abstract_Wallet)\n        self.assertEqual(self.SEED, await cmds.getseed(wallet_path=wpath))\n        self.assertEqual(self.SEED, await cmds.getseed(wallet_path=basename))\n        self.assertEqual(self.SEED, await cmds.getseed(wallet=wallet))\n\n\nclass TestLoadWallet(DaemonTestCase):\n\n    async def test_simple_load(self):\n        path1 = self._restore_wallet_from_text(\"9dk\", password=None)\n        wallet1 = self.daemon.load_wallet(path1, password=None)\n        await self.daemon._stop_wallet(path1)\n\n    async def test_password_checks_for_no_password(self):\n        real_password = None\n        path1 = self._restore_wallet_from_text(\"9dk\", password=real_password)\n        # load_wallet will not validate the password arg unless needed for storage.decrypt():\n        wallet1 = self.daemon.load_wallet(path1, password=\"garbage\")\n        await self.daemon._stop_wallet(path1)\n        # unless force_check_password is set:\n        with self.assertRaises(util.InvalidPassword):\n            wallet1 = self.daemon.load_wallet(path1, password=\"garbage\", force_check_password=True)\n\n        wallet1 = self.daemon.load_wallet(path1, password=real_password)\n        await self.daemon._stop_wallet(path1)\n\n        wallet1 = self.daemon.load_wallet(path1, password=real_password, force_check_password=True)\n        await self.daemon._stop_wallet(path1)\n\n        # load_wallet will not validate the password arg if wallet is already loaded, unless force_check_password\n        wallet1 = self.daemon.load_wallet(path1, password=real_password)\n        wallet1 = self.daemon.load_wallet(path1, password=\"garbage\")\n        with self.assertRaises(util.InvalidPassword):\n            wallet1 = self.daemon.load_wallet(path1, password=\"garbage\", force_check_password=True)\n\n\n    async def test_password_checks_for_ks_enc(self):\n        real_password = \"1234\"\n        path1 = self._restore_wallet_from_text(\"9dk\", password=real_password, encrypt_file=False)\n        # load_wallet will not validate the password arg unless needed for storage.decrypt():\n        wallet1 = self.daemon.load_wallet(path1, password=\"garbage\")\n        await self.daemon._stop_wallet(path1)\n        # unless force_check_password is set:\n        with self.assertRaises(util.InvalidPassword):\n            wallet1 = self.daemon.load_wallet(path1, password=\"garbage\", force_check_password=True)\n\n        wallet1 = self.daemon.load_wallet(path1, password=real_password)\n        await self.daemon._stop_wallet(path1)\n\n        wallet1 = self.daemon.load_wallet(path1, password=real_password, force_check_password=True)\n        await self.daemon._stop_wallet(path1)\n\n        # load_wallet will not validate the password arg if wallet is already loaded, unless force_check_password\n        wallet1 = self.daemon.load_wallet(path1, password=real_password)\n        wallet1 = self.daemon.load_wallet(path1, password=\"garbage\")\n        with self.assertRaises(util.InvalidPassword):\n            wallet1 = self.daemon.load_wallet(path1, password=\"garbage\", force_check_password=True)\n\n\n    async def test_password_checks_for_sto_enc(self):\n        real_password = \"1234\"\n        path1 = self._restore_wallet_from_text(\"9dk\", password=real_password, encrypt_file=True)\n\n        with self.assertRaises(util.InvalidPassword):\n            wallet1 = self.daemon.load_wallet(path1, password=\"garbage\")\n\n        with self.assertRaises(util.InvalidPassword):\n            wallet1 = self.daemon.load_wallet(path1, password=\"garbage\", force_check_password=True)\n\n        wallet1 = self.daemon.load_wallet(path1, password=real_password)\n        await self.daemon._stop_wallet(path1)\n\n        wallet1 = self.daemon.load_wallet(path1, password=real_password, force_check_password=True)\n        await self.daemon._stop_wallet(path1)\n\n        # load_wallet will not validate the password arg if wallet is already loaded, unless force_check_password\n        wallet1 = self.daemon.load_wallet(path1, password=real_password)\n        wallet1 = self.daemon.load_wallet(path1, password=\"garbage\")\n        with self.assertRaises(util.InvalidPassword):\n            wallet1 = self.daemon.load_wallet(path1, password=\"garbage\", force_check_password=True)\n"
  },
  {
    "path": "tests/test_descriptor.py",
    "content": "# Copyright (c) 2018-2023 The HWI developers\n# Copyright (c) 2023 The Electrum developers\n# Distributed under the MIT software license, see the accompanying\n# file COPYING or http://www.opensource.org/licenses/mit-license.php.\n#\n# originally from https://github.com/bitcoin-core/HWI/blob/f5a9b29c00e483cc99a1b8f4f5ef75413a092869/test/test_descriptor.py\n\nfrom binascii import unhexlify\nimport unittest\n\nimport electrum_ecc as ecc\n\nfrom electrum.descriptor import (\n    parse_descriptor,\n    MultisigDescriptor,\n    SHDescriptor,\n    TRDescriptor,\n    PKHDescriptor,\n    WPKHDescriptor,\n    WSHDescriptor,\n    PubkeyProvider,\n)\nfrom electrum.util import bfh\n\nfrom . import ElectrumTestCase, as_testnet\n\n\nclass TestDescriptor(ElectrumTestCase):\n\n    @as_testnet\n    def test_parse_descriptor_with_origin(self):\n        d = \"wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, WPKHDescriptor))\n        self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), \"00000001\")\n        self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), \"m/84h/1h/0h\")\n        self.assertEqual(desc.pubkeys[0].pubkey, \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\")\n        self.assertEqual(desc.pubkeys[0].deriv_path, \"/0/0\")\n        self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), \"m/84h/1h/0h/0/0\")\n        self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483732, 2147483649, 2147483648, 0, 0])\n        self.assertEqual(desc.to_string_no_checksum(), d)\n        e = desc.expand()\n        self.assertEqual(e.output_script, unhexlify(\"0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa\"))\n        self.assertEqual(e.redeem_script, None)\n        self.assertEqual(e.witness_script, None)\n        self.assertEqual(e.address(), \"tb1qm90ugl4d48jv8n6e5t9ln6t9zlpm5th690vysp\")\n\n    @as_testnet\n    def test_parse_multisig_descriptor_with_origin(self):\n        d = \"wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, WSHDescriptor))\n        self.assertTrue(isinstance(desc.subdescriptors[0], MultisigDescriptor))\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), \"00000001\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.get_derivation_path(), \"m/48h/0h/0h/2h\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].pubkey, \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, \"/0/0\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].get_full_derivation_path(), \"m/48h/0h/0h/2h/0/0\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483696, 2147483648, 2147483648, 2147483650, 0, 0])\n\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), \"00000002\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), \"m/48h/0h/0h/2h\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].pubkey, \"tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].deriv_path, \"/0/0\")\n        self.assertEqual(desc.to_string_no_checksum(), d)\n        e = desc.expand()\n        self.assertEqual(e.output_script, unhexlify(\"002084b64b2b8651df8fd3e9735f6269edbf9e03abf619ae0788be9f17bf18e83d59\"))\n        self.assertEqual(e.redeem_script, None)\n        self.assertEqual(e.witness_script, unhexlify(\"522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae\"))\n\n        d = \"sh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, SHDescriptor))\n        self.assertTrue(isinstance(desc.subdescriptors[0], MultisigDescriptor))\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), \"00000001\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.get_derivation_path(), \"m/48h/0h/0h/2h\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].pubkey, \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, \"/0/0\")\n\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), \"00000002\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), \"m/48h/0h/0h/2h\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].pubkey, \"tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].deriv_path, \"/0/0\")\n        self.assertEqual(desc.to_string_no_checksum(), d)\n        e = desc.expand()\n        self.assertEqual(e.output_script, unhexlify(\"a91495ee6326805b1586bb821fc3c0eeab2c68441b4187\"))\n        self.assertEqual(e.redeem_script, unhexlify(\"522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae\"))\n        self.assertEqual(e.witness_script, None)\n\n        d = \"sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, SHDescriptor))\n        self.assertTrue(isinstance(desc.subdescriptors[0], WSHDescriptor))\n        self.assertTrue(isinstance(desc.subdescriptors[0].subdescriptors[0], MultisigDescriptor))\n        self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), \"00000001\")\n        self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].origin.get_derivation_path(), \"m/48h/0h/0h/2h\")\n        self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].pubkey, \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\")\n        self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].deriv_path, \"/0/0\")\n\n        self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), \"00000002\")\n        self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].origin.get_derivation_path(), \"m/48h/0h/0h/2h\")\n        self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].pubkey, \"tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty\")\n        self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].deriv_path, \"/0/0\")\n        self.assertEqual(desc.to_string_no_checksum(), d)\n        e = desc.expand()\n        self.assertEqual(e.output_script, unhexlify(\"a914779ae0f6958e98b997cc177f9b554289905fbb5587\"))\n        self.assertEqual(e.redeem_script, unhexlify(\"002084b64b2b8651df8fd3e9735f6269edbf9e03abf619ae0788be9f17bf18e83d59\"))\n        self.assertEqual(e.witness_script, unhexlify(\"522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae\"))\n\n    @as_testnet\n    def test_parse_descriptor_without_origin(self):\n        d = \"wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, WPKHDescriptor))\n        self.assertEqual(desc.pubkeys[0].origin, None)\n        self.assertEqual(desc.pubkeys[0].pubkey, \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\")\n        self.assertEqual(desc.pubkeys[0].deriv_path, \"/0/0\")\n        self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), \"m/0/0\")\n        self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [0, 0])\n        self.assertEqual(desc.to_string_no_checksum(), d)\n        e = desc.expand()\n        self.assertEqual(e.output_script, unhexlify(\"0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa\"))\n        self.assertEqual(e.redeem_script, None)\n        self.assertEqual(e.witness_script, None)\n\n    @as_testnet\n    def test_parse_descriptor_with_origin_fingerprint_only(self):\n        d = \"wpkh([00000001]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, WPKHDescriptor))\n        self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), \"00000001\")\n        self.assertEqual(len(desc.pubkeys[0].origin.path), 0)\n        self.assertEqual(desc.pubkeys[0].pubkey, \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\")\n        self.assertEqual(desc.pubkeys[0].deriv_path, \"/0/0\")\n        self.assertEqual(desc.to_string_no_checksum(), d)\n        e = desc.expand()\n        self.assertEqual(e.output_script, unhexlify(\"0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa\"))\n        self.assertEqual(e.redeem_script, None)\n        self.assertEqual(e.witness_script, None)\n\n    def test_parse_descriptor_with_key_at_end_with_origin(self):\n        d = \"wpkh([00000001/84h/1h/0h/0/0]02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, WPKHDescriptor))\n        self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), \"00000001\")\n        self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), \"m/84h/1h/0h/0/0\")\n        self.assertEqual(desc.pubkeys[0].pubkey, \"02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7\")\n        self.assertEqual(desc.pubkeys[0].deriv_path, None)\n        self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), \"m/84h/1h/0h/0/0\")\n        self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483732, 2147483649, 2147483648, 0, 0])\n        self.assertEqual(desc.to_string_no_checksum(), d)\n        e = desc.expand()\n        self.assertEqual(e.output_script, unhexlify(\"0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa\"))\n        self.assertEqual(e.redeem_script, None)\n        self.assertEqual(e.witness_script, None)\n\n        d = \"pkh([00000001/84h/1h/0h/0/0]02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, PKHDescriptor))\n        self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), \"00000001\")\n        self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), \"m/84h/1h/0h/0/0\")\n        self.assertEqual(desc.pubkeys[0].pubkey, \"02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7\")\n        self.assertEqual(desc.pubkeys[0].deriv_path, None)\n        self.assertEqual(desc.to_string_no_checksum(), d)\n        e = desc.expand()\n        self.assertEqual(e.output_script, unhexlify(\"76a914d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa88ac\"))\n        self.assertEqual(e.redeem_script, None)\n        self.assertEqual(e.witness_script, None)\n\n    def test_parse_descriptor_with_key_at_end_without_origin(self):\n        d = \"wpkh(02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, WPKHDescriptor))\n        self.assertEqual(desc.pubkeys[0].origin, None)\n        self.assertEqual(desc.pubkeys[0].pubkey, \"02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7\")\n        self.assertEqual(desc.pubkeys[0].deriv_path, None)\n        self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), \"m\")\n        self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [])\n        self.assertEqual(desc.to_string_no_checksum(), d)\n\n    def test_parse_empty_descriptor(self):\n        self.assertRaises(ValueError, parse_descriptor, \"\")\n\n    @as_testnet\n    def test_parse_descriptor_replace_h(self):\n        d = \"wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)\"\n        desc = parse_descriptor(d)\n        self.assertIsNotNone(desc)\n        self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), \"m/84h/1h/0h\")\n\n    @as_testnet\n    def test_parse_descriptor_unknown_notation_for_hardened_derivation(self):\n        with self.assertRaises(ValueError):\n            desc = parse_descriptor(\"wpkh([00000001/84x/1x/0x]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)\")\n        with self.assertRaises(ValueError):\n            desc = parse_descriptor(\"wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0x)\")\n\n    def test_checksums(self):\n        with self.subTest(msg=\"Valid checksum\"):\n            self.assertIsNotNone(parse_descriptor(\"sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kwj\"))\n            self.assertIsNotNone(parse_descriptor(\"sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#hgmsckna\"))\n            self.assertIsNotNone(parse_descriptor(\"sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))\"))\n            self.assertIsNotNone(parse_descriptor(\"sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))\"))\n        with self.subTest(msg=\"Empty Checksum\"):\n            self.assertRaises(ValueError, parse_descriptor, \"sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#\")\n            self.assertRaises(ValueError, parse_descriptor, \"sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#\")\n        with self.subTest(msg=\"Too long Checksum\"):\n            self.assertRaises(ValueError, parse_descriptor, \"sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kwjq\")\n            self.assertRaises(ValueError, parse_descriptor, \"sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#hgmscknaq\")\n        with self.subTest(msg=\"Too Short Checksum\"):\n            self.assertRaises(ValueError, parse_descriptor, \"sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kw\")\n            self.assertRaises(ValueError, parse_descriptor, \"sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#hgmsckn\")\n        with self.subTest(msg=\"Error in Payload\"):\n            self.assertRaises(ValueError, parse_descriptor, \"sh(multi(3,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxf\")\n            self.assertRaises(ValueError, parse_descriptor, \"sh(multi(3,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5\")\n        with self.subTest(msg=\"Error in Checksum\"):\n            self.assertRaises(ValueError, parse_descriptor, \"sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kej\")\n            self.assertRaises(ValueError, parse_descriptor, \"sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09y5\")\n\n    @as_testnet\n    def test_tr_descriptor(self):\n        d = \"tr([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, TRDescriptor))\n        self.assertEqual(len(desc.pubkeys), 1)\n        self.assertEqual(len(desc.subdescriptors), 0)\n        self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), \"00000001\")\n        self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), \"m/84h/1h/0h\")\n        self.assertEqual(desc.pubkeys[0].pubkey, \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\")\n        self.assertEqual(desc.pubkeys[0].deriv_path, \"/0/0\")\n        self.assertEqual(desc.get_max_tree_depth(), None)\n        self.assertEqual(desc.to_string_no_checksum(), d)\n\n        d = \"tr([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,{pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B),{{pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B),pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)},pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)}})\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, TRDescriptor))\n        self.assertEqual(len(desc.subdescriptors), 4)\n        self.assertEqual(len(desc.desc_tree), 2)\n        self.assertEqual(len(desc.pubkeys), 1)\n        self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), \"00000001\")\n        self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), \"m/84h/1h/0h\")\n        self.assertEqual(desc.pubkeys[0].pubkey, \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\")\n        self.assertEqual(desc.pubkeys[0].deriv_path, \"/0/0\")\n        self.assertEqual(desc.desc_tree[0].to_string_no_checksum(), \"pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)\")\n        self.assertEqual(desc.desc_tree[1][0][0].to_string_no_checksum(), \"pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)\")\n        self.assertEqual(desc.desc_tree[1][0][1].to_string_no_checksum(), \"pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)\")\n        self.assertEqual(desc.desc_tree[1][1].to_string_no_checksum(), \"pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)\")\n        self.assertEqual(desc.get_max_tree_depth(), 3)\n        self.assertEqual(desc.to_string_no_checksum(), d)\n\n    def test_tr_descriptor_bip386(self):\n        # test vectors from https://github.com/bitcoin/bips/blob/e2f7481a132e1c5863f5ffcbff009964d7c2af20/bip-0386.mediawiki#test-vectors\n        # TODO add missing tests\n        self.assertEqual(\n            \"512077aab6e066f8a7419c5ab714c12c67d25007ed55a43cadcacb4d7a970a093f11\",\n            parse_descriptor(\"tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)\").expand().output_script.hex())\n        self.assertEqual(\n            \"512017cf18db381d836d8923b1bdb246cfcd818da1a9f0e6e7907f187f0b2f937754\",\n            parse_descriptor(\"tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,pk(669b8afcec803a0d323e9a17f3ea8e68e8abe5a278020a929adbec52421adbd0))\").expand().output_script.hex())\n\n    @as_testnet\n    def test_parse_descriptor_with_range(self):\n        d = \"wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, WPKHDescriptor))\n        self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), \"00000001\")\n        self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), \"m/84h/1h/0h\")\n        self.assertEqual(desc.pubkeys[0].pubkey, \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\")\n        self.assertEqual(desc.pubkeys[0].deriv_path, \"/0/*\")\n        self.assertEqual(desc.to_string_no_checksum(), d)\n        with self.assertRaises(ValueError):  # \"pos\" arg needed due to \"*\"\n            e = desc.expand()\n        e = desc.expand(pos=7)\n        self.assertEqual(e.output_script, unhexlify(\"0014c5f80de08f6ae8dd720bf4e4948ba498c96256a1\"))\n        self.assertEqual(e.redeem_script, None)\n        self.assertEqual(e.witness_script, None)\n\n        with self.assertRaises(ValueError):  # wildcard only allowed in last position\n            parse_descriptor(\"wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/*/0)\")\n        with self.assertRaises(ValueError):  # only one wildcard(*) is allowed\n            parse_descriptor(\"wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/*/*)\")\n\n    @as_testnet\n    def test_parse_multisig_descriptor_with_range(self):\n        d = \"wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/*))\"\n        desc = parse_descriptor(d)\n        self.assertTrue(isinstance(desc, WSHDescriptor))\n        self.assertTrue(isinstance(desc.subdescriptors[0], MultisigDescriptor))\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), \"00000001\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.get_derivation_path(), \"m/48h/0h/0h/2h\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].pubkey, \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, \"/0/*\")\n\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), \"00000002\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), \"m/48h/0h/0h/2h\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].pubkey, \"tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty\")\n        self.assertEqual(desc.subdescriptors[0].pubkeys[1].deriv_path, \"/0/*\")\n        self.assertEqual(desc.to_string_no_checksum(), d)\n        e = desc.expand(pos=7)\n        self.assertEqual(e.output_script, unhexlify(\"0020453cdf90aef0997947bc0605481f81dd2978ecd2d04ac36fb57397a82341682d\"))\n        self.assertEqual(e.redeem_script, None)\n        self.assertEqual(e.witness_script, unhexlify(\"5221034e703dfcd64f23ad5d6156ee3b9dd7566137626c663bb521bf710957599723342102c35627535d26de98ae749b7a7849df99cbe53af795005437ca647c8af9a006af52ae\"))\n\n    @as_testnet\n    def test_multisig_descriptor_with_mixed_range(self):\n        d = \"sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))\"\n        desc = parse_descriptor(d)\n        e = desc.expand(pos=7)\n        self.assertEqual(e.output_script, bfh(\"a914644ece12bab2f84ad6de96ec18de51e6168c028987\"))\n        self.assertEqual(e.redeem_script, bfh(\"0020824ce4ffab74a8d09c2f77ed447fb040ea5dfbed06f8e3b3327127a18634f6a7\"))\n        self.assertEqual(e.witness_script, bfh(\"5221034e703dfcd64f23ad5d6156ee3b9dd7566137626c663bb521bf7109575997233421033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae\"))\n        self.assertEqual(e.address(), \"2N2Pbxw3HNJ9jrUw8LCSfXyDWx9TKGRT2an\")\n\n    @as_testnet\n    def test_uncompressed_pubkey_in_segwit(self):\n        pubkey = ecc.ECPubkey(bfh(\"02a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc\"))\n        pubkey_comp_hex = pubkey.get_public_key_hex(compressed=True)\n        pubkey_uncomp_hex = pubkey.get_public_key_hex(compressed=False)\n        self.assertEqual(pubkey_comp_hex, \"02a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc\")\n        self.assertEqual(pubkey_uncomp_hex, \"04a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc3ccfc29410b8f23c15d88413a6b88c8cd44b016a7f1dd91a8d64c3107c6bce1a\")\n        # pkh\n        desc = parse_descriptor(f\"pkh({pubkey_comp_hex})\")\n        self.assertEqual(desc.expand().output_script, bfh(\"76a9140297bde2689a3c79ffe050583b62f86f2d9dae5488ac\"))\n        desc = parse_descriptor(f\"pkh({pubkey_uncomp_hex})\")\n        self.assertEqual(desc.expand().output_script, bfh(\"76a914e1f4a76b122f0288b013404cd52a9d1de0ced3c488ac\"))\n        # wpkh\n        desc = parse_descriptor(f\"wpkh({pubkey_comp_hex})\")\n        self.assertEqual(desc.expand().output_script, bfh(\"00140297bde2689a3c79ffe050583b62f86f2d9dae54\"))\n        with self.assertRaises(ValueError):  # only compressed public keys can be used in segwit scripts\n            desc = parse_descriptor(f\"wpkh({pubkey_uncomp_hex})\")\n        # sh(wsh(multi()))\n        desc = parse_descriptor(f\"sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,{pubkey_comp_hex})))\")\n        self.assertEqual(desc.expand(pos=2).output_script, bfh(\"a9148f162cce29ad81e63ed45cd09aff83418316eab687\"))\n        with self.assertRaises(ValueError):  # only compressed public keys can be used in segwit scripts\n            desc = parse_descriptor(f\"sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,{pubkey_uncomp_hex})))\")\n\n    @as_testnet\n    def test_parse_descriptor_context(self):\n        desc = parse_descriptor(\"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))\")\n        self.assertTrue(isinstance(desc, SHDescriptor))\n        with self.assertRaises(ValueError):  # Can only have sh() at top level\n            desc = parse_descriptor(\"wsh(sh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))\")\n        with self.assertRaises(ValueError):  # Can only have wsh() at top level or inside sh()\n            desc = parse_descriptor(\"wsh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))\")\n\n        desc = parse_descriptor(\"wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)\")\n        self.assertTrue(isinstance(desc, WPKHDescriptor))\n        with self.assertRaises(ValueError):  # Can only have wpkh() at top level or inside sh()\n            desc = parse_descriptor(\"wsh(wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0))\")\n\n    def test_parse_descriptor_ypub_zpub_forbidden(self):\n        desc = parse_descriptor(\"wpkh([535e473f/0h]xpub68W3CJPrQzHhTQcHM6tbCvNVB9ih4tbzsFBLwe7zZUj5uHuhxBUhvnXe1RQhbKCTiTj3D7kXni6yAD88i2xnjKHaJ5NqTtHawKnPFCDnmo4/0/*)\")\n        with self.assertRaises(ValueError):  # only standard xpub/xprv allowed\n            desc = parse_descriptor(\"wpkh([535e473f/0h]ypub6TLJVy4mZfqBJhoQBTgDR1TzM7s91WbVnMhZj31swV6xxPiwCqeGYrBn2dNHbDrP86qqxbM6FNTX3VjhRjNoXYyBAR5G3o75D3r2djmhZwM/0/*)\")\n        with self.assertRaises(ValueError):  # only standard xpub/xprv allowed\n            desc = parse_descriptor(\"wpkh([535e473f/0h]zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr/0/*)\")\n\n    @as_testnet\n    def test_sortedmulti_ranged_pubkey_order(self):\n        xpub1 = \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\"\n        xpub2 = \"tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty\"\n        # if ranged, we sort lexicographically\n        desc = parse_descriptor(f\"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/*,[00000002/48h/0h/0h/2h]{xpub2}/0/*)))\")\n        self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])\n        desc = parse_descriptor(f\"sh(wsh(sortedmulti(2,[00000002/48h/0h/0h/2h]{xpub2}/0/*,[00000001/48h/0h/0h/2h]{xpub1}/0/*)))\")\n        self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])\n        # if unsorted \"multi\", don't touch order\n        desc = parse_descriptor(f\"sh(wsh(multi(2,[00000002/48h/0h/0h/2h]{xpub2}/0/*,[00000001/48h/0h/0h/2h]{xpub1}/0/*)))\")\n        self.assertEqual([xpub2, xpub1], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])\n\n    @as_testnet\n    def test_sortedmulti_unranged_pubkey_order(self):\n        xpub1 = \"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B\"\n        xpub2 = \"tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty\"\n        # if not ranged, we sort according to final derived pubkey order\n        desc = parse_descriptor(f\"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/0,[00000002/48h/0h/0h/2h]{xpub2}/0/0)))\")\n        self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])\n        desc = parse_descriptor(f\"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/1,[00000002/48h/0h/0h/2h]{xpub2}/0/1)))\")\n        self.assertEqual([xpub2, xpub1], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])\n        desc = parse_descriptor(f\"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/4,[00000002/48h/0h/0h/2h]{xpub2}/0/4)))\")\n        self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])\n        # if unsorted \"multi\", don't touch order\n        desc = parse_descriptor(f\"sh(wsh(multi(2,[00000001/48h/0h/0h/2h]{xpub1}/0/1,[00000002/48h/0h/0h/2h]{xpub2}/0/1)))\")\n        self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])\n\n    def test_pubkey_provider_deriv_path(self):\n        xpub = \"xpub68W3CJPrQzHhTQcHM6tbCvNVB9ih4tbzsFBLwe7zZUj5uHuhxBUhvnXe1RQhbKCTiTj3D7kXni6yAD88i2xnjKHaJ5NqTtHawKnPFCDnmo4\"\n        # valid:\n        pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path=\"/1/7\")\n        pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path=\"/1/*\")\n        # invalid:\n        with self.assertRaises(ValueError):\n            pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path=\"1\")\n        with self.assertRaises(ValueError):\n            pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path=\"1/7\")\n        with self.assertRaises(ValueError):\n            pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path=\"m/1/7\")\n        with self.assertRaises(ValueError):\n            pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path=\"*/7\")\n        with self.assertRaises(ValueError):\n            pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path=\"*/*\")\n\n        pubkey_hex = \"02a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc\"\n        # valid:\n        pp = PubkeyProvider(origin=None, pubkey=pubkey_hex, deriv_path=None)\n        # invalid:\n        with self.assertRaises(ValueError):\n            pp = PubkeyProvider(origin=None, pubkey=pubkey_hex, deriv_path=\"/1/7\")\n"
  },
  {
    "path": "tests/test_fee_policy.py",
    "content": "from electrum.fee_policy import FeeHistogram\n\nfrom . import ElectrumTestCase\n\n\nclass Test_FeeHistogram(ElectrumTestCase):\n\n    def setUp(self):\n        super(Test_FeeHistogram, self).setUp()\n\n    def tearDown(self):\n        super(Test_FeeHistogram, self).tearDown()\n\n    def test_depth_target_to_fee(self):\n        mempool_fees = FeeHistogram()\n        mempool_fees.set_data([[49, 100110], [10, 121301], [6, 153731], [5, 125872], [1, 36488810]])\n        self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(1000000))\n        self.assertEqual( 6 * 1000, mempool_fees.depth_target_to_fee( 500000))\n        self.assertEqual( 7 * 1000, mempool_fees.depth_target_to_fee( 250000))\n        self.assertEqual(11 * 1000, mempool_fees.depth_target_to_fee( 200000))\n        self.assertEqual(50 * 1000, mempool_fees.depth_target_to_fee( 100000))\n        mempool_fees.set_data([])\n        self.assertEqual( 1 * 1000, mempool_fees.depth_target_to_fee(10 ** 5))\n        self.assertEqual( 1 * 1000, mempool_fees.depth_target_to_fee(10 ** 6))\n        self.assertEqual( 1 * 1000, mempool_fees.depth_target_to_fee(10 ** 7))\n        mempool_fees.set_data([[1, 36488810]])\n        self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(10 ** 5))\n        self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(10 ** 6))\n        self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(10 ** 7))\n        self.assertEqual( 1 * 1000, mempool_fees.depth_target_to_fee(10 ** 8))\n        mempool_fees.set_data([[5, 125872], [1, 36488810]])\n        self.assertEqual( 6 * 1000, mempool_fees.depth_target_to_fee(10 ** 5))\n        self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(10 ** 6))\n        self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(10 ** 7))\n        self.assertEqual( 1 * 1000, mempool_fees.depth_target_to_fee(10 ** 8))\n        mempool_fees.set_data([])\n        self.assertEqual(1 * 1000, mempool_fees.depth_target_to_fee(10 ** 5))\n        mempool_fees.set_data(None)\n        self.assertEqual(None, mempool_fees.depth_target_to_fee(10 ** 5))\n\n    def test_fee_to_depth(self):\n        mempool_fees = FeeHistogram()\n        mempool_fees.set_data([[49, 100000], [10, 120000], [6, 150000], [5, 125000], [1, 36000000]])\n        self.assertEqual(100000, mempool_fees.fee_to_depth(500))\n        self.assertEqual(100000, mempool_fees.fee_to_depth(50))\n        self.assertEqual(100000, mempool_fees.fee_to_depth(49))\n        self.assertEqual(220000, mempool_fees.fee_to_depth(48))\n        self.assertEqual(220000, mempool_fees.fee_to_depth(10))\n        self.assertEqual(370000, mempool_fees.fee_to_depth(9))\n        self.assertEqual(370000, mempool_fees.fee_to_depth(6.5))\n        self.assertEqual(370000, mempool_fees.fee_to_depth(6))\n        self.assertEqual(495000, mempool_fees.fee_to_depth(5.5))\n        self.assertEqual(36495000, mempool_fees.fee_to_depth(0.5))\n\n\n"
  },
  {
    "path": "tests/test_history_export/history_no_fx_client_4_5_2_9dk_with_ln.csv",
    "content": "oc_transaction_hash,ln_payment_hash,label,confirmations,amount_chain_bc,amount_lightning_bc,fiat_value,network_fee_bc,fiat_fee,timestamp\n024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c,,trans 3,1232719,0.1,0.,,0.,,2018-06-29 13:29:07\n0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39,,dsadsa,1232632,-0.00201,0.,,0.00001,,2018-06-29 15:52:26\n03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3,,Dsdhahuisad,1232611,-0.00201,0.,,0.00001,,2018-06-29 16:20:40\n7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba,,,1165279,-0.01000705,0.,,0.00000705,,2018-09-25 19:51:12\n972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535,,,1165278,-0.01000705,0.,,0.00000705,,2018-09-25 20:01:37\n737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6,,,1165278,-0.01000705,0.,,0.00000705,,2018-09-25 20:01:37\n41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c,,some description here. much creativity,1165027,-0.00201,0.,,0.00001,,2018-09-28 18:00:40\n4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e,,,1125041,-0.03190141,0.,,0.00000141,,2019-01-31 01:12:16\n30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0,,,1124381,-0.00000141,0.,,0.00000141,,2019-02-02 18:37:00\n4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c,,,1123808,0.14695224,0.,,0.,,2019-02-05 16:06:18\nb424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c,,,1122455,-0.00001202,0.,,0.00000202,,2019-02-14 18:27:57\n3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f,,,1011325,-0.17898625,0.,,0.00000246,,2019-07-09 18:53:36\nba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f,,old address 95645,996545,0.03099663,0.,,0.,,2019-10-15 16:02:48\n2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269,,,996545,0.00099811,0.,,0.,,2019-10-15 16:02:48\nb234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b,,,996545,-0.03199474,0.,,0.00000181,,2019-10-15 16:02:48\n25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea,,,803764,0.001,0.,,0.,,2020-07-07 17:45:49\n10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f,,,687142,-0.001,0.,,0.00000111,,2020-11-18 01:22:40\nefca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c,,,230215,0.0011,0.,,0.,,2022-10-04 21:28:03\n8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b,,,230212,0.0016,0.,,0.,,2022-10-04 22:15:06\n23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f,,,175482,0.00135,0.,,0.,,2022-11-04 16:27:49\n94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69,,Open channel,173292,-0.00405,0.,,0.00000289,,2022-11-14 22:34:14\nd57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1,,,164304,0.00404573,0.,,0.00000138,,2023-01-08 11:32:47\n6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce,,,164304,-0.00001097,0.,,0.00001097,,2023-01-08 11:32:47\n87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661,,asd,160828,-0.00100776,0.,,0.00000776,,2023-02-03 15:18:45\n97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe,,Open channel 2425096x21x1,154493,-0.003027,0.00302547,,0.00000153,,2023-03-19 20:01:43\n66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e,,Close channel 2425096x21x1,51197,0.00302364,-0.00302547,,0.00000183,,2023-10-03 17:15:38\n03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d,,fund me pls,7,0.005,0.,,0.,,2024-02-26 18:02:39\nc96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38,,Open channel 2579583x479x2,6,-0.0040019,0.004,,0.0000019,,2024-02-26 18:11:55\n778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4,,Payment request for BitPay invoice W6ic1rU1XThEmLAEvwkjdX for merchant SomberNight_testing,5,-0.0000531,0.,,0.0000021,,2024-02-26 18:26:49\n59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2,,moar money,5,0.00654165,0.,,0.,,2024-02-26 18:26:49\n,9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921,1 Blokaccino,,0.,-0.00001003,,0.00000003,,2024-02-26 18:59:55\n,a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f,1 Blokaccino,,0.,-0.00001003,,0.00000003,,2024-02-26 19:00:18\n,0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda,obiwan9847,,0.,-0.0005003,,0.0000003,,2024-02-26 19:34:02\n,720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b,hello there,,0.,-0.0001001,,0.0000001,,2024-02-26 19:34:33\n,cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3,general kenobi,,0.,0.000001,,0.,,2024-02-26 19:35:15\n,6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34,general kenobi 3,,0.,0.00000125,,0.,,2024-02-26 19:35:48\n"
  },
  {
    "path": "tests/test_history_export/history_no_fx_client_4_5_2_9dk_with_ln.json",
    "content": "{\n    \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": {\n        \"amount_sat\": 10000000,\n        \"bc_balance\": \"0.1\",\n        \"bc_value\": \"0.1\",\n        \"confirmations\": 1232719,\n        \"date\": \"2018-06-29 13:29\",\n        \"fee_sat\": null,\n        \"group_id\": null,\n        \"height\": 1346870,\n        \"incoming\": true,\n        \"label\": \"trans 3\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1530271747,\n        \"timestamp\": 1530271747,\n        \"txid\": \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\",\n        \"txpos_in_block\": 11,\n        \"value\": \"0.1\",\n        \"wanted_height\": null\n    },\n    \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": {\n        \"amount_sat\": 500000,\n        \"bc_balance\": \"0.00802364\",\n        \"bc_value\": \"0.005\",\n        \"confirmations\": 7,\n        \"date\": \"2024-02-26 18:02\",\n        \"fee_sat\": null,\n        \"group_id\": null,\n        \"height\": 2579582,\n        \"incoming\": true,\n        \"label\": \"fund me pls\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1708966959,\n        \"timestamp\": 1708966959,\n        \"txid\": \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\",\n        \"txpos_in_block\": 675,\n        \"value\": \"0.005\",\n        \"wanted_height\": null\n    },\n    \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": {\n        \"amount_sat\": -201000,\n        \"bc_balance\": \"0.09598\",\n        \"bc_value\": \"-0.00201\",\n        \"confirmations\": 1232611,\n        \"date\": \"2018-06-29 16:20\",\n        \"fee_sat\": 1000,\n        \"group_id\": null,\n        \"height\": 1346978,\n        \"incoming\": false,\n        \"label\": \"Dsdhahuisad\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1530282040,\n        \"timestamp\": 1530282040,\n        \"txid\": \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\",\n        \"txpos_in_block\": 3,\n        \"value\": \"-0.00201\",\n        \"wanted_height\": null\n    },\n    \"0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda\": {\n        \"amount_msat\": -50030000,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 19:34\",\n        \"direction\": 0,\n        \"fee_msat\": 30000,\n        \"group_id\": null,\n        \"label\": \"obiwan9847\",\n        \"lightning\": true,\n        \"ln_value\": \"-0.0005003\",\n        \"payment_hash\": \"0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda\",\n        \"preimage\": \"1b16da388f5d62648825a5022e72a27ec6e5b2242fb37adf8260306c5e544f3c\",\n        \"timestamp\": 1708972442,\n        \"type\": \"payment\",\n        \"value\": \"-0.0005003\"\n    },\n    \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": {\n        \"amount_sat\": -201000,\n        \"bc_balance\": \"0.09799\",\n        \"bc_value\": \"-0.00201\",\n        \"confirmations\": 1232632,\n        \"date\": \"2018-06-29 15:52\",\n        \"fee_sat\": 1000,\n        \"group_id\": null,\n        \"height\": 1346957,\n        \"incoming\": false,\n        \"label\": \"dsadsa\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1530280346,\n        \"timestamp\": 1530280346,\n        \"txid\": \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\",\n        \"txpos_in_block\": 10,\n        \"value\": \"-0.00201\",\n        \"wanted_height\": null\n    },\n    \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\": {\n        \"amount_sat\": -100000,\n        \"bc_balance\": \"0.\",\n        \"bc_value\": \"-0.001\",\n        \"confirmations\": 687142,\n        \"date\": \"2020-11-18 01:22\",\n        \"fee_sat\": 111,\n        \"group_id\": null,\n        \"height\": 1892447,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1605658960,\n        \"timestamp\": 1605658960,\n        \"txid\": \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\",\n        \"txpos_in_block\": 42,\n        \"value\": \"-0.001\",\n        \"wanted_height\": null\n    },\n    \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": {\n        \"amount_sat\": 99811,\n        \"bc_balance\": \"0.03199474\",\n        \"bc_value\": \"0.00099811\",\n        \"confirmations\": 996545,\n        \"date\": \"2019-10-15 16:02\",\n        \"fee_sat\": null,\n        \"group_id\": null,\n        \"height\": 1583044,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1571148168,\n        \"timestamp\": 1571148168,\n        \"txid\": \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\",\n        \"txpos_in_block\": 273,\n        \"value\": \"0.00099811\",\n        \"wanted_height\": null\n    },\n    \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": {\n        \"amount_sat\": 135000,\n        \"bc_balance\": \"0.00405\",\n        \"bc_value\": \"0.00135\",\n        \"confirmations\": 175482,\n        \"date\": \"2022-11-04 16:27\",\n        \"fee_sat\": null,\n        \"group_id\": null,\n        \"height\": 2404107,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1667575669,\n        \"timestamp\": 1667575669,\n        \"txid\": \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\",\n        \"txpos_in_block\": 18,\n        \"value\": \"0.00135\",\n        \"wanted_height\": null\n    },\n    \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": {\n        \"amount_sat\": 100000,\n        \"bc_balance\": \"0.001\",\n        \"bc_value\": \"0.001\",\n        \"confirmations\": 803764,\n        \"date\": \"2020-07-07 17:45\",\n        \"fee_sat\": null,\n        \"group_id\": null,\n        \"height\": 1775825,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1594136749,\n        \"timestamp\": 1594136749,\n        \"txid\": \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\",\n        \"txpos_in_block\": 81,\n        \"value\": \"0.001\",\n        \"wanted_height\": null\n    },\n    \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": {\n        \"amount_sat\": -141,\n        \"bc_balance\": \"0.03204603\",\n        \"bc_value\": \"-0.00000141\",\n        \"confirmations\": 1124381,\n        \"date\": \"2019-02-02 18:37\",\n        \"fee_sat\": 141,\n        \"group_id\": null,\n        \"height\": 1455208,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1549129020,\n        \"timestamp\": 1549129020,\n        \"txid\": \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\",\n        \"txpos_in_block\": 23,\n        \"value\": \"-0.00000141\",\n        \"wanted_height\": null\n    },\n    \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\": {\n        \"amount_sat\": -17898625,\n        \"bc_balance\": \"0.\",\n        \"bc_value\": \"-0.17898625\",\n        \"confirmations\": 1011325,\n        \"date\": \"2019-07-09 18:53\",\n        \"fee_sat\": 246,\n        \"group_id\": null,\n        \"height\": 1568264,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1562691216,\n        \"timestamp\": 1562691216,\n        \"txid\": \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\",\n        \"txpos_in_block\": 85,\n        \"value\": \"-0.17898625\",\n        \"wanted_height\": null\n    },\n    \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": {\n        \"amount_sat\": 14695224,\n        \"bc_balance\": \"0.17899827\",\n        \"bc_value\": \"0.14695224\",\n        \"confirmations\": 1123808,\n        \"date\": \"2019-02-05 16:06\",\n        \"fee_sat\": null,\n        \"group_id\": null,\n        \"height\": 1455781,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1549379178,\n        \"timestamp\": 1549379178,\n        \"txid\": \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\",\n        \"txpos_in_block\": 61,\n        \"value\": \"0.14695224\",\n        \"wanted_height\": null\n    },\n    \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": {\n        \"amount_sat\": -201000,\n        \"bc_balance\": \"0.06394885\",\n        \"bc_value\": \"-0.00201\",\n        \"confirmations\": 1165027,\n        \"date\": \"2018-09-28 18:00\",\n        \"fee_sat\": 1000,\n        \"group_id\": null,\n        \"height\": 1414562,\n        \"incoming\": false,\n        \"label\": \"some description here. much creativity\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1538150440,\n        \"timestamp\": 1538150440,\n        \"txid\": \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\",\n        \"txpos_in_block\": 81,\n        \"value\": \"-0.00201\",\n        \"wanted_height\": null\n    },\n    \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": {\n        \"amount_sat\": -3190141,\n        \"bc_balance\": \"0.03204744\",\n        \"bc_value\": \"-0.03190141\",\n        \"confirmations\": 1125041,\n        \"date\": \"2019-01-31 01:12\",\n        \"fee_sat\": 141,\n        \"group_id\": null,\n        \"height\": 1454548,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1548893536,\n        \"timestamp\": 1548893536,\n        \"txid\": \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\",\n        \"txpos_in_block\": 44,\n        \"value\": \"-0.03190141\",\n        \"wanted_height\": null\n    },\n    \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": {\n        \"amount_sat\": 654165,\n        \"bc_balance\": \"0.01051029\",\n        \"bc_value\": \"0.00654165\",\n        \"confirmations\": 5,\n        \"date\": \"2024-02-26 18:26\",\n        \"fee_sat\": null,\n        \"group_id\": null,\n        \"height\": 2579584,\n        \"incoming\": true,\n        \"label\": \"moar money\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1708968409,\n        \"timestamp\": 1708968409,\n        \"txid\": \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\",\n        \"txpos_in_block\": 1814,\n        \"value\": \"0.00654165\",\n        \"wanted_height\": null\n    },\n    \"6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34\": {\n        \"amount_msat\": 125000,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 19:35\",\n        \"direction\": 1,\n        \"fee_msat\": null,\n        \"group_id\": null,\n        \"label\": \"general kenobi 3\",\n        \"lightning\": true,\n        \"ln_value\": \"0.00000125\",\n        \"payment_hash\": \"6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34\",\n        \"preimage\": \"d6e4067da77dd810c6b0465a54e0557238b1b317c7b2a6ac679429780ad56f43\",\n        \"timestamp\": 1708972548,\n        \"type\": \"payment\",\n        \"value\": \"0.00000125\"\n    },\n    \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": {\n        \"amount_sat\": -1097,\n        \"bc_balance\": \"0.00403476\",\n        \"bc_value\": \"-0.00001097\",\n        \"confirmations\": 164304,\n        \"date\": \"2023-01-08 11:32\",\n        \"fee_sat\": 1097,\n        \"group_id\": null,\n        \"height\": 2415285,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1673173967,\n        \"timestamp\": 1673173967,\n        \"txid\": \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\",\n        \"txpos_in_block\": 17,\n        \"value\": \"-0.00001097\",\n        \"wanted_height\": null\n    },\n    \"720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\": {\n        \"amount_msat\": -10010000,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 19:34\",\n        \"direction\": 0,\n        \"fee_msat\": 10000,\n        \"group_id\": null,\n        \"label\": \"hello there\",\n        \"lightning\": true,\n        \"ln_value\": \"-0.0001001\",\n        \"payment_hash\": \"720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\",\n        \"preimage\": \"31a84f775c07316ad0cf3b894cf37bb2420b3f4d69a41794df2a4cf29c91e0a9\",\n        \"timestamp\": 1708972473,\n        \"type\": \"payment\",\n        \"value\": \"-0.0001001\"\n    },\n    \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": {\n        \"amount_sat\": -1000705,\n        \"bc_balance\": \"0.06595885\",\n        \"bc_value\": \"-0.01000705\",\n        \"confirmations\": 1165278,\n        \"date\": \"2018-09-25 20:01\",\n        \"fee_sat\": 705,\n        \"group_id\": null,\n        \"height\": 1414311,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1537898497,\n        \"timestamp\": 1537898497,\n        \"txid\": \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\",\n        \"txpos_in_block\": 45,\n        \"value\": \"-0.01000705\",\n        \"wanted_height\": null\n    },\n    \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": {\n        \"amount_sat\": -1000705,\n        \"bc_balance\": \"0.08597295\",\n        \"bc_value\": \"-0.01000705\",\n        \"confirmations\": 1165279,\n        \"date\": \"2018-09-25 19:51\",\n        \"fee_sat\": 705,\n        \"group_id\": null,\n        \"height\": 1414310,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1537897872,\n        \"timestamp\": 1537897872,\n        \"txid\": \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\",\n        \"txpos_in_block\": 48,\n        \"value\": \"-0.01000705\",\n        \"wanted_height\": null\n    },\n    \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": {\n        \"amount_sat\": -5310,\n        \"bc_balance\": \"0.00396864\",\n        \"bc_value\": \"-0.0000531\",\n        \"confirmations\": 5,\n        \"date\": \"2024-02-26 18:26\",\n        \"fee_sat\": 210,\n        \"group_id\": null,\n        \"height\": 2579584,\n        \"incoming\": false,\n        \"label\": \"Payment request for BitPay invoice W6ic1rU1XThEmLAEvwkjdX for merchant SomberNight_testing\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1708968409,\n        \"timestamp\": 1708968409,\n        \"txid\": \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\",\n        \"txpos_in_block\": 284,\n        \"value\": \"-0.0000531\",\n        \"wanted_height\": null\n    },\n    \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": {\n        \"amount_sat\": -100776,\n        \"bc_balance\": \"0.003027\",\n        \"bc_value\": \"-0.00100776\",\n        \"confirmations\": 160828,\n        \"date\": \"2023-02-03 15:18\",\n        \"fee_sat\": 776,\n        \"group_id\": null,\n        \"height\": 2418761,\n        \"incoming\": false,\n        \"label\": \"asd\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1675433925,\n        \"timestamp\": 1675433925,\n        \"txid\": \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\",\n        \"txpos_in_block\": 56,\n        \"value\": \"-0.00100776\",\n        \"wanted_height\": null\n    },\n    \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": {\n        \"amount_sat\": 160000,\n        \"bc_balance\": \"0.0027\",\n        \"bc_value\": \"0.0016\",\n        \"confirmations\": 230212,\n        \"date\": \"2022-10-04 22:15\",\n        \"fee_sat\": null,\n        \"group_id\": null,\n        \"height\": 2349377,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1664914506,\n        \"timestamp\": 1664914506,\n        \"txid\": \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\",\n        \"txpos_in_block\": 58,\n        \"value\": \"0.0016\",\n        \"wanted_height\": null\n    },\n    \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": {\n        \"amount_sat\": -405000,\n        \"bc_balance\": \"0.\",\n        \"bc_value\": \"-0.00405\",\n        \"confirmations\": 173292,\n        \"date\": \"2022-11-14 22:34\",\n        \"fee_sat\": 289,\n        \"group_id\": null,\n        \"height\": 2406297,\n        \"incoming\": false,\n        \"label\": \"Open channel\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1668461654,\n        \"timestamp\": 1668461654,\n        \"txid\": \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\",\n        \"txpos_in_block\": 24,\n        \"value\": \"-0.00405\",\n        \"wanted_height\": null\n    },\n    \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": {\n        \"amount_sat\": -1000705,\n        \"bc_balance\": \"0.0759659\",\n        \"bc_value\": \"-0.01000705\",\n        \"confirmations\": 1165278,\n        \"date\": \"2018-09-25 20:01\",\n        \"fee_sat\": 705,\n        \"group_id\": null,\n        \"height\": 1414311,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1537898497,\n        \"timestamp\": 1537898497,\n        \"txid\": \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\",\n        \"txpos_in_block\": 44,\n        \"value\": \"-0.01000705\",\n        \"wanted_height\": null\n    },\n    \"9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921\": {\n        \"amount_msat\": -1003100,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 18:59\",\n        \"direction\": 0,\n        \"fee_msat\": 3100,\n        \"group_id\": null,\n        \"label\": \"1 Blokaccino\",\n        \"lightning\": true,\n        \"ln_value\": \"-0.00001003\",\n        \"payment_hash\": \"9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921\",\n        \"preimage\": \"4dd0ddf9605e51998936d101b7741e89be08cb788c1e36b9b1d62cd5e9543af8\",\n        \"timestamp\": 1708970395,\n        \"type\": \"payment\",\n        \"value\": \"-0.00001003\"\n    },\n    \"a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f\": {\n        \"amount_msat\": -1003100,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 19:00\",\n        \"direction\": 0,\n        \"fee_msat\": 3100,\n        \"group_id\": null,\n        \"label\": \"1 Blokaccino\",\n        \"lightning\": true,\n        \"ln_value\": \"-0.00001003\",\n        \"payment_hash\": \"a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f\",\n        \"preimage\": \"48f81553d41291c78f5a09f21cc95eef11dd798e7abfd23fb1f1cb9eb8bf8359\",\n        \"timestamp\": 1708970418,\n        \"type\": \"payment\",\n        \"value\": \"-0.00001003\"\n    },\n    \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\": {\n        \"amount_sat\": -3199474,\n        \"bc_balance\": \"0.\",\n        \"bc_value\": \"-0.03199474\",\n        \"confirmations\": 996545,\n        \"date\": \"2019-10-15 16:02\",\n        \"fee_sat\": 181,\n        \"group_id\": null,\n        \"height\": 1583044,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1571148168,\n        \"timestamp\": 1571148168,\n        \"txid\": \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\",\n        \"txpos_in_block\": 286,\n        \"value\": \"-0.03199474\",\n        \"wanted_height\": null\n    },\n    \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": {\n        \"amount_sat\": -1202,\n        \"bc_balance\": \"0.17898625\",\n        \"bc_value\": \"-0.00001202\",\n        \"confirmations\": 1122455,\n        \"date\": \"2019-02-14 18:27\",\n        \"fee_sat\": 202,\n        \"group_id\": null,\n        \"height\": 1457134,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1550165277,\n        \"timestamp\": 1550165277,\n        \"txid\": \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\",\n        \"txpos_in_block\": 52,\n        \"value\": \"-0.00001202\",\n        \"wanted_height\": null\n    },\n    \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": {\n        \"amount_sat\": 3099663,\n        \"bc_balance\": \"0.03099663\",\n        \"bc_value\": \"0.03099663\",\n        \"confirmations\": 996545,\n        \"date\": \"2019-10-15 16:02\",\n        \"fee_sat\": null,\n        \"group_id\": null,\n        \"height\": 1583044,\n        \"incoming\": true,\n        \"label\": \"old address 95645\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1571148168,\n        \"timestamp\": 1571148168,\n        \"txid\": \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\",\n        \"txpos_in_block\": 191,\n        \"value\": \"0.03099663\",\n        \"wanted_height\": null\n    },\n    \"cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3\": {\n        \"amount_msat\": 100000,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 19:35\",\n        \"direction\": 1,\n        \"fee_msat\": null,\n        \"group_id\": null,\n        \"label\": \"general kenobi\",\n        \"lightning\": true,\n        \"ln_value\": \"0.000001\",\n        \"payment_hash\": \"cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3\",\n        \"preimage\": \"ee4f549b3844282ae596dfef14fc75e64067517fa92e9735fe792415bced3db9\",\n        \"timestamp\": 1708972515,\n        \"type\": \"payment\",\n        \"value\": \"0.000001\"\n    },\n    \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": {\n        \"amount_sat\": 404573,\n        \"bc_balance\": \"0.00404573\",\n        \"bc_value\": \"0.00404573\",\n        \"confirmations\": 164304,\n        \"date\": \"2023-01-08 11:32\",\n        \"fee_sat\": 138,\n        \"group_id\": null,\n        \"height\": 2415285,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1673173967,\n        \"timestamp\": 1673173967,\n        \"txid\": \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\",\n        \"txpos_in_block\": 16,\n        \"value\": \"0.00404573\",\n        \"wanted_height\": null\n    },\n    \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": {\n        \"amount_sat\": 110000,\n        \"bc_balance\": \"0.0011\",\n        \"bc_value\": \"0.0011\",\n        \"confirmations\": 230215,\n        \"date\": \"2022-10-04 21:28\",\n        \"fee_sat\": null,\n        \"group_id\": null,\n        \"height\": 2349374,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1664911683,\n        \"timestamp\": 1664911683,\n        \"txid\": \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\",\n        \"txpos_in_block\": 13,\n        \"value\": \"0.0011\",\n        \"wanted_height\": null\n    },\n    \"group:66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": {\n        \"bc_value\": \"0.00302364\",\n        \"children\": [\n            {\n                \"amount_sat\": 302364,\n                \"bc_balance\": \"0.00302364\",\n                \"bc_value\": \"0.00302364\",\n                \"confirmations\": 51197,\n                \"date\": \"2023-10-03 17:15\",\n                \"fee_sat\": 183,\n                \"group_id\": \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n                \"height\": 2528392,\n                \"incoming\": true,\n                \"label\": \"Close channel 2425096x21x1\",\n                \"lightning\": false,\n                \"ln_value\": \"0.\",\n                \"monotonic_timestamp\": 1696346138,\n                \"timestamp\": 1696346138,\n                \"txid\": \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n                \"txpos_in_block\": 155,\n                \"value\": \"0.00302364\",\n                \"wanted_height\": null\n            },\n            {\n                \"amount_msat\": -302547000,\n                \"bc_value\": \"0.\",\n                \"date\": \"2023-10-03 17:15\",\n                \"direction\": null,\n                \"fee_msat\": null,\n                \"group_id\": \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n                \"label\": \"Close channel 2425096x21x1\",\n                \"lightning\": true,\n                \"ln_value\": \"-0.00302547\",\n                \"payment_hash\": null,\n                \"preimage\": null,\n                \"timestamp\": 1696346138,\n                \"type\": \"channel_closing\",\n                \"value\": \"-0.00302547\"\n            }\n        ],\n        \"confirmations\": 51197,\n        \"date\": \"2023-10-03 17:15\",\n        \"fee_sat\": 0,\n        \"height\": 2528392,\n        \"label\": \"Close channel 2425096x21x1\",\n        \"lightning\": false,\n        \"ln_value\": \"-0.00302547\",\n        \"timestamp\": 1696346138,\n        \"txid\": \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n        \"value\": \"-0.00000183\",\n        \"wanted_height\": null\n    },\n    \"group:97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": {\n        \"bc_value\": \"-0.003027\",\n        \"children\": [\n            {\n                \"amount_sat\": -302700,\n                \"bc_balance\": \"0.\",\n                \"bc_value\": \"-0.003027\",\n                \"confirmations\": 154493,\n                \"date\": \"2023-03-19 20:01\",\n                \"fee_sat\": 153,\n                \"group_id\": \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n                \"height\": 2425096,\n                \"incoming\": false,\n                \"label\": \"Open channel\",\n                \"lightning\": false,\n                \"ln_value\": \"0.\",\n                \"monotonic_timestamp\": 1679252503,\n                \"timestamp\": 1679252503,\n                \"txid\": \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n                \"txpos_in_block\": 21,\n                \"value\": \"-0.003027\",\n                \"wanted_height\": null\n            },\n            {\n                \"amount_msat\": 302547000,\n                \"bc_value\": \"0.\",\n                \"date\": \"2023-03-19 20:01\",\n                \"direction\": null,\n                \"fee_msat\": null,\n                \"group_id\": \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n                \"label\": \"Open channel 2425096x21x1\",\n                \"lightning\": true,\n                \"ln_value\": \"0.00302547\",\n                \"payment_hash\": null,\n                \"preimage\": null,\n                \"timestamp\": 1679252503,\n                \"type\": \"channel_opening\",\n                \"value\": \"0.00302547\"\n            }\n        ],\n        \"confirmations\": 154493,\n        \"date\": \"2023-03-19 20:01\",\n        \"fee_sat\": 0,\n        \"height\": 2425096,\n        \"label\": \"Open channel 2425096x21x1\",\n        \"lightning\": false,\n        \"ln_value\": \"0.00302547\",\n        \"timestamp\": 1679252503,\n        \"txid\": \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n        \"value\": \"-0.00000153\",\n        \"wanted_height\": null\n    },\n    \"group:c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": {\n        \"bc_value\": \"-0.0040019\",\n        \"children\": [\n            {\n                \"amount_sat\": -400190,\n                \"bc_balance\": \"0.00402174\",\n                \"bc_value\": \"-0.0040019\",\n                \"confirmations\": 6,\n                \"date\": \"2024-02-26 18:11\",\n                \"fee_sat\": 190,\n                \"group_id\": \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                \"height\": 2579583,\n                \"incoming\": false,\n                \"label\": \"Open channel\",\n                \"lightning\": false,\n                \"ln_value\": \"0.\",\n                \"monotonic_timestamp\": 1708967515,\n                \"timestamp\": 1708967515,\n                \"txid\": \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                \"txpos_in_block\": 479,\n                \"value\": \"-0.0040019\",\n                \"wanted_height\": null\n            },\n            {\n                \"amount_msat\": 400000000,\n                \"bc_value\": \"0.\",\n                \"date\": \"2024-02-26 18:11\",\n                \"direction\": null,\n                \"fee_msat\": null,\n                \"group_id\": \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                \"label\": \"Open channel 2579583x479x2\",\n                \"lightning\": true,\n                \"ln_value\": \"0.004\",\n                \"payment_hash\": null,\n                \"preimage\": null,\n                \"timestamp\": 1708967515,\n                \"type\": \"channel_opening\",\n                \"value\": \"0.004\"\n            }\n        ],\n        \"confirmations\": 6,\n        \"date\": \"2024-02-26 18:11\",\n        \"fee_sat\": 0,\n        \"height\": 2579583,\n        \"label\": \"Open channel 2579583x479x2\",\n        \"lightning\": false,\n        \"ln_value\": \"0.004\",\n        \"timestamp\": 1708967515,\n        \"txid\": \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n        \"value\": \"-0.0000019\",\n        \"wanted_height\": null\n    }\n}"
  },
  {
    "path": "tests/test_history_export/history_with_fx_client_4_5_2_9dk_with_ln.csv",
    "content": "oc_transaction_hash,ln_payment_hash,label,confirmations,amount_chain_bc,amount_lightning_bc,fiat_value,network_fee_bc,fiat_fee,timestamp\n024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c,,trans 3,1232719,0.1,0.,531.46,0.,0.00,2018-06-29 13:29:07\n0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39,,dsadsa,1232632,-0.00201,0.,-10.68,0.00001,0.05,2018-06-29 15:52:26\n03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3,,Dsdhahuisad,1232611,-0.00201,0.,-10.68,0.00001,0.05,2018-06-29 16:20:40\n7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba,,,1165279,-0.01000705,0.,-54.80,0.00000705,0.04,2018-09-25 19:51:12\n972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535,,,1165278,-0.01000705,0.,-54.80,0.00000705,0.04,2018-09-25 20:01:37\n737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6,,,1165278,-0.01000705,0.,-54.80,0.00000705,0.04,2018-09-25 20:01:37\n41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c,,some description here. much creativity,1165027,-0.00201,0.,-11.48,0.00001,0.06,2018-09-28 18:00:40\n4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e,,,1125041,-0.03190141,0.,-97.59,0.00000141,0.00,2019-01-31 01:12:16\n30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0,,,1124381,-0.00000141,0.,-0.00,0.00000141,0.00,2019-02-02 18:37:00\n4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c,,,1123808,0.14695224,0.,452.27,0.,0.00,2019-02-05 16:06:18\nb424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c,,,1122455,-0.00001202,0.,-0.04,0.00000202,0.01,2019-02-14 18:27:57\n3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f,,,1011325,-0.17898625,0.,-2006.08,0.00000246,0.03,2019-07-09 18:53:36\nba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f,,old address 95645,996545,0.03099663,0.,230.19,0.,0.00,2019-10-15 16:02:48\n2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269,,,996545,0.00099811,0.,7.41,0.,0.00,2019-10-15 16:02:48\nb234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b,,,996545,-0.03199474,0.,-237.60,0.00000181,0.01,2019-10-15 16:02:48\n25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea,,,803764,0.001,0.,8.22,0.,0.00,2020-07-07 17:45:49\n10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f,,,687142,-0.001,0.,-15.00,0.00000111,0.02,2020-11-18 01:22:40\nefca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c,,,230215,0.0011,0.,22.42,0.,0.00,2022-10-04 21:28:03\n8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b,,,230212,0.0016,0.,32.61,0.,0.00,2022-10-04 22:15:06\n23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f,,,175482,0.00135,0.,28.65,0.,0.00,2022-11-04 16:27:49\n94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69,,Open channel,173292,-0.00405,0.,-65.11,0.00000289,0.05,2022-11-14 22:34:14\nd57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1,,,164304,0.00404573,0.,65.01,0.00000138,0.02,2023-01-08 11:32:47\n6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce,,,164304,-0.00001097,0.,-0.18,0.00001097,0.18,2023-01-08 11:32:47\n87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661,,asd,160828,-0.00100776,0.,-21.87,0.00000776,0.17,2023-02-03 15:18:45\n97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe,,Open channel 2425096x21x1,154493,-0.003027,0.00302547,-0.04,0.00000153,0.04,2023-03-19 20:01:43\n66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e,,Close channel 2425096x21x1,51197,0.00302364,-0.00302547,-0.05,0.00000183,0.05,2023-10-03 17:15:38\n03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d,,fund me pls,7,0.005,0.,251.36,0.,0.00,2024-02-26 18:02:39\nc96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38,,Open channel 2579583x479x2,6,-0.0040019,0.004,-0.10,0.0000019,0.10,2024-02-26 18:11:55\n778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4,,Payment request for BitPay invoice W6ic1rU1XThEmLAEvwkjdX for merchant SomberNight_testing,5,-0.0000531,0.,-2.67,0.0000021,0.11,2024-02-26 18:26:49\n59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2,,moar money,5,0.00654165,0.,328.87,0.,0.00,2024-02-26 18:26:49\n,9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921,1 Blokaccino,,0.,-0.00001003,-0.50,0.00000003,0.00,2024-02-26 18:59:55\n,a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f,1 Blokaccino,,0.,-0.00001003,-0.50,0.00000003,0.00,2024-02-26 19:00:18\n,0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda,obiwan9847,,0.,-0.0005003,-25.15,0.0000003,0.00,2024-02-26 19:34:02\n,720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b,hello there,,0.,-0.0001001,-5.03,0.0000001,0.00,2024-02-26 19:34:33\n,cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3,general kenobi,,0.,0.000001,0.05,0.,0.00,2024-02-26 19:35:15\n,6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34,general kenobi 3,,0.,0.00000125,0.06,0.,0.00,2024-02-26 19:35:48\n"
  },
  {
    "path": "tests/test_history_export/history_with_fx_client_4_5_2_9dk_with_ln.json",
    "content": "{\n    \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": {\n        \"amount_sat\": 10000000,\n        \"bc_balance\": \"0.1\",\n        \"bc_value\": \"0.1\",\n        \"confirmations\": 1232719,\n        \"date\": \"2018-06-29 13:29\",\n        \"fee_sat\": null,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": null,\n        \"fiat_rate\": \"5314.60\",\n        \"fiat_value\": \"531.46\",\n        \"group_id\": null,\n        \"height\": 1346870,\n        \"incoming\": true,\n        \"label\": \"trans 3\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1530271747,\n        \"timestamp\": 1530271747,\n        \"txid\": \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\",\n        \"txpos_in_block\": 11,\n        \"value\": \"0.1\",\n        \"wanted_height\": null\n    },\n    \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": {\n        \"amount_sat\": 500000,\n        \"bc_balance\": \"0.00802364\",\n        \"bc_value\": \"0.005\",\n        \"confirmations\": 7,\n        \"date\": \"2024-02-26 18:02\",\n        \"fee_sat\": null,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": null,\n        \"fiat_rate\": \"50273.00\",\n        \"fiat_value\": \"251.36\",\n        \"group_id\": null,\n        \"height\": 2579582,\n        \"incoming\": true,\n        \"label\": \"fund me pls\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1708966959,\n        \"timestamp\": 1708966959,\n        \"txid\": \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\",\n        \"txpos_in_block\": 675,\n        \"value\": \"0.005\",\n        \"wanted_height\": null\n    },\n    \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": {\n        \"acquisition_price\": \"10.68\",\n        \"amount_sat\": -201000,\n        \"bc_balance\": \"0.09598\",\n        \"bc_value\": \"-0.00201\",\n        \"capital_gain\": \"0.00\",\n        \"confirmations\": 1232611,\n        \"date\": \"2018-06-29 16:20\",\n        \"fee_sat\": 1000,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.05\",\n        \"fiat_rate\": \"5314.60\",\n        \"fiat_value\": \"-10.68\",\n        \"group_id\": null,\n        \"height\": 1346978,\n        \"incoming\": false,\n        \"label\": \"Dsdhahuisad\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1530282040,\n        \"timestamp\": 1530282040,\n        \"txid\": \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\",\n        \"txpos_in_block\": 3,\n        \"value\": \"-0.00201\",\n        \"wanted_height\": null\n    },\n    \"0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda\": {\n        \"amount_msat\": -50030000,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 19:34\",\n        \"direction\": 0,\n        \"fee_msat\": 30000,\n        \"fiat_default\": true,\n        \"fiat_value\": \"-25.15\",\n        \"group_id\": null,\n        \"label\": \"obiwan9847\",\n        \"lightning\": true,\n        \"ln_value\": \"-0.0005003\",\n        \"payment_hash\": \"0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda\",\n        \"preimage\": \"1b16da388f5d62648825a5022e72a27ec6e5b2242fb37adf8260306c5e544f3c\",\n        \"timestamp\": 1708972442,\n        \"type\": \"payment\",\n        \"value\": \"-0.0005003\"\n    },\n    \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": {\n        \"acquisition_price\": \"10.68\",\n        \"amount_sat\": -201000,\n        \"bc_balance\": \"0.09799\",\n        \"bc_value\": \"-0.00201\",\n        \"capital_gain\": \"0.00\",\n        \"confirmations\": 1232632,\n        \"date\": \"2018-06-29 15:52\",\n        \"fee_sat\": 1000,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.05\",\n        \"fiat_rate\": \"5314.60\",\n        \"fiat_value\": \"-10.68\",\n        \"group_id\": null,\n        \"height\": 1346957,\n        \"incoming\": false,\n        \"label\": \"dsadsa\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1530280346,\n        \"timestamp\": 1530280346,\n        \"txid\": \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\",\n        \"txpos_in_block\": 10,\n        \"value\": \"-0.00201\",\n        \"wanted_height\": null\n    },\n    \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\": {\n        \"acquisition_price\": \"8.22\",\n        \"amount_sat\": -100000,\n        \"bc_balance\": \"0.\",\n        \"bc_value\": \"-0.001\",\n        \"capital_gain\": \"6.78\",\n        \"confirmations\": 687142,\n        \"date\": \"2020-11-18 01:22\",\n        \"fee_sat\": 111,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.02\",\n        \"fiat_rate\": \"14997.00\",\n        \"fiat_value\": \"-15.00\",\n        \"group_id\": null,\n        \"height\": 1892447,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1605658960,\n        \"timestamp\": 1605658960,\n        \"txid\": \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\",\n        \"txpos_in_block\": 42,\n        \"value\": \"-0.001\",\n        \"wanted_height\": null\n    },\n    \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": {\n        \"amount_sat\": 99811,\n        \"bc_balance\": \"0.03199474\",\n        \"bc_value\": \"0.00099811\",\n        \"confirmations\": 996545,\n        \"date\": \"2019-10-15 16:02\",\n        \"fee_sat\": null,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": null,\n        \"fiat_rate\": \"7426.20\",\n        \"fiat_value\": \"7.41\",\n        \"group_id\": null,\n        \"height\": 1583044,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1571148168,\n        \"timestamp\": 1571148168,\n        \"txid\": \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\",\n        \"txpos_in_block\": 273,\n        \"value\": \"0.00099811\",\n        \"wanted_height\": null\n    },\n    \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": {\n        \"amount_sat\": 135000,\n        \"bc_balance\": \"0.00405\",\n        \"bc_value\": \"0.00135\",\n        \"confirmations\": 175482,\n        \"date\": \"2022-11-04 16:27\",\n        \"fee_sat\": null,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": null,\n        \"fiat_rate\": \"21224.00\",\n        \"fiat_value\": \"28.65\",\n        \"group_id\": null,\n        \"height\": 2404107,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1667575669,\n        \"timestamp\": 1667575669,\n        \"txid\": \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\",\n        \"txpos_in_block\": 18,\n        \"value\": \"0.00135\",\n        \"wanted_height\": null\n    },\n    \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": {\n        \"amount_sat\": 100000,\n        \"bc_balance\": \"0.001\",\n        \"bc_value\": \"0.001\",\n        \"confirmations\": 803764,\n        \"date\": \"2020-07-07 17:45\",\n        \"fee_sat\": null,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": null,\n        \"fiat_rate\": \"8219.70\",\n        \"fiat_value\": \"8.22\",\n        \"group_id\": null,\n        \"height\": 1775825,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1594136749,\n        \"timestamp\": 1594136749,\n        \"txid\": \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\",\n        \"txpos_in_block\": 81,\n        \"value\": \"0.001\",\n        \"wanted_height\": null\n    },\n    \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": {\n        \"acquisition_price\": \"0.01\",\n        \"amount_sat\": -141,\n        \"bc_balance\": \"0.03204603\",\n        \"bc_value\": \"-0.00000141\",\n        \"capital_gain\": \"-0.00\",\n        \"confirmations\": 1124381,\n        \"date\": \"2019-02-02 18:37\",\n        \"fee_sat\": 141,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.00\",\n        \"fiat_rate\": \"3110.90\",\n        \"fiat_value\": \"-0.00\",\n        \"group_id\": null,\n        \"height\": 1455208,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1549129020,\n        \"timestamp\": 1549129020,\n        \"txid\": \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\",\n        \"txpos_in_block\": 23,\n        \"value\": \"-0.00000141\",\n        \"wanted_height\": null\n    },\n    \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\": {\n        \"acquisition_price\": \"622.52\",\n        \"amount_sat\": -17898625,\n        \"bc_balance\": \"0.\",\n        \"bc_value\": \"-0.17898625\",\n        \"capital_gain\": \"1383.56\",\n        \"confirmations\": 1011325,\n        \"date\": \"2019-07-09 18:53\",\n        \"fee_sat\": 246,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.03\",\n        \"fiat_rate\": \"11208.00\",\n        \"fiat_value\": \"-2006.08\",\n        \"group_id\": null,\n        \"height\": 1568264,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1562691216,\n        \"timestamp\": 1562691216,\n        \"txid\": \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\",\n        \"txpos_in_block\": 85,\n        \"value\": \"-0.17898625\",\n        \"wanted_height\": null\n    },\n    \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": {\n        \"amount_sat\": 14695224,\n        \"bc_balance\": \"0.17899827\",\n        \"bc_value\": \"0.14695224\",\n        \"confirmations\": 1123808,\n        \"date\": \"2019-02-05 16:06\",\n        \"fee_sat\": null,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": null,\n        \"fiat_rate\": \"3077.70\",\n        \"fiat_value\": \"452.27\",\n        \"group_id\": null,\n        \"height\": 1455781,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1549379178,\n        \"timestamp\": 1549379178,\n        \"txid\": \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\",\n        \"txpos_in_block\": 61,\n        \"value\": \"0.14695224\",\n        \"wanted_height\": null\n    },\n    \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": {\n        \"acquisition_price\": \"10.68\",\n        \"amount_sat\": -201000,\n        \"bc_balance\": \"0.06394885\",\n        \"bc_value\": \"-0.00201\",\n        \"capital_gain\": \"0.80\",\n        \"confirmations\": 1165027,\n        \"date\": \"2018-09-28 18:00\",\n        \"fee_sat\": 1000,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.06\",\n        \"fiat_rate\": \"5710.90\",\n        \"fiat_value\": \"-11.48\",\n        \"group_id\": null,\n        \"height\": 1414562,\n        \"incoming\": false,\n        \"label\": \"some description here. much creativity\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1538150440,\n        \"timestamp\": 1538150440,\n        \"txid\": \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\",\n        \"txpos_in_block\": 81,\n        \"value\": \"-0.00201\",\n        \"wanted_height\": null\n    },\n    \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": {\n        \"acquisition_price\": \"169.54\",\n        \"amount_sat\": -3190141,\n        \"bc_balance\": \"0.03204744\",\n        \"bc_value\": \"-0.03190141\",\n        \"capital_gain\": \"-71.95\",\n        \"confirmations\": 1125041,\n        \"date\": \"2019-01-31 01:12\",\n        \"fee_sat\": 141,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.00\",\n        \"fiat_rate\": \"3059.10\",\n        \"fiat_value\": \"-97.59\",\n        \"group_id\": null,\n        \"height\": 1454548,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1548893536,\n        \"timestamp\": 1548893536,\n        \"txid\": \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\",\n        \"txpos_in_block\": 44,\n        \"value\": \"-0.03190141\",\n        \"wanted_height\": null\n    },\n    \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": {\n        \"amount_sat\": 654165,\n        \"bc_balance\": \"0.01051029\",\n        \"bc_value\": \"0.00654165\",\n        \"confirmations\": 5,\n        \"date\": \"2024-02-26 18:26\",\n        \"fee_sat\": null,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": null,\n        \"fiat_rate\": \"50273.00\",\n        \"fiat_value\": \"328.87\",\n        \"group_id\": null,\n        \"height\": 2579584,\n        \"incoming\": true,\n        \"label\": \"moar money\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1708968409,\n        \"timestamp\": 1708968409,\n        \"txid\": \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\",\n        \"txpos_in_block\": 1814,\n        \"value\": \"0.00654165\",\n        \"wanted_height\": null\n    },\n    \"6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34\": {\n        \"amount_msat\": 125000,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 19:35\",\n        \"direction\": 1,\n        \"fee_msat\": null,\n        \"fiat_default\": true,\n        \"fiat_value\": \"0.06\",\n        \"group_id\": null,\n        \"label\": \"general kenobi 3\",\n        \"lightning\": true,\n        \"ln_value\": \"0.00000125\",\n        \"payment_hash\": \"6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34\",\n        \"preimage\": \"d6e4067da77dd810c6b0465a54e0557238b1b317c7b2a6ac679429780ad56f43\",\n        \"timestamp\": 1708972548,\n        \"type\": \"payment\",\n        \"value\": \"0.00000125\"\n    },\n    \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": {\n        \"acquisition_price\": \"0.23\",\n        \"amount_sat\": -1097,\n        \"bc_balance\": \"0.00403476\",\n        \"bc_value\": \"-0.00001097\",\n        \"capital_gain\": \"-0.05\",\n        \"confirmations\": 164304,\n        \"date\": \"2023-01-08 11:32\",\n        \"fee_sat\": 1097,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.18\",\n        \"fiat_rate\": \"16069.00\",\n        \"fiat_value\": \"-0.18\",\n        \"group_id\": null,\n        \"height\": 2415285,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1673173967,\n        \"timestamp\": 1673173967,\n        \"txid\": \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\",\n        \"txpos_in_block\": 17,\n        \"value\": \"-0.00001097\",\n        \"wanted_height\": null\n    },\n    \"720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\": {\n        \"amount_msat\": -10010000,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 19:34\",\n        \"direction\": 0,\n        \"fee_msat\": 10000,\n        \"fiat_default\": true,\n        \"fiat_value\": \"-5.03\",\n        \"group_id\": null,\n        \"label\": \"hello there\",\n        \"lightning\": true,\n        \"ln_value\": \"-0.0001001\",\n        \"payment_hash\": \"720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\",\n        \"preimage\": \"31a84f775c07316ad0cf3b894cf37bb2420b3f4d69a41794df2a4cf29c91e0a9\",\n        \"timestamp\": 1708972473,\n        \"type\": \"payment\",\n        \"value\": \"-0.0001001\"\n    },\n    \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": {\n        \"acquisition_price\": \"53.18\",\n        \"amount_sat\": -1000705,\n        \"bc_balance\": \"0.06595885\",\n        \"bc_value\": \"-0.01000705\",\n        \"capital_gain\": \"1.62\",\n        \"confirmations\": 1165278,\n        \"date\": \"2018-09-25 20:01\",\n        \"fee_sat\": 705,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.04\",\n        \"fiat_rate\": \"5476.40\",\n        \"fiat_value\": \"-54.80\",\n        \"group_id\": null,\n        \"height\": 1414311,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1537898497,\n        \"timestamp\": 1537898497,\n        \"txid\": \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\",\n        \"txpos_in_block\": 45,\n        \"value\": \"-0.01000705\",\n        \"wanted_height\": null\n    },\n    \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": {\n        \"acquisition_price\": \"53.18\",\n        \"amount_sat\": -1000705,\n        \"bc_balance\": \"0.08597295\",\n        \"bc_value\": \"-0.01000705\",\n        \"capital_gain\": \"1.62\",\n        \"confirmations\": 1165279,\n        \"date\": \"2018-09-25 19:51\",\n        \"fee_sat\": 705,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.04\",\n        \"fiat_rate\": \"5476.40\",\n        \"fiat_value\": \"-54.80\",\n        \"group_id\": null,\n        \"height\": 1414310,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1537897872,\n        \"timestamp\": 1537897872,\n        \"txid\": \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\",\n        \"txpos_in_block\": 48,\n        \"value\": \"-0.01000705\",\n        \"wanted_height\": null\n    },\n    \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": {\n        \"acquisition_price\": \"2.67\",\n        \"amount_sat\": -5310,\n        \"bc_balance\": \"0.00396864\",\n        \"bc_value\": \"-0.0000531\",\n        \"capital_gain\": \"0.00\",\n        \"confirmations\": 5,\n        \"date\": \"2024-02-26 18:26\",\n        \"fee_sat\": 210,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.11\",\n        \"fiat_rate\": \"50273.00\",\n        \"fiat_value\": \"-2.67\",\n        \"group_id\": null,\n        \"height\": 2579584,\n        \"incoming\": false,\n        \"label\": \"Payment request for BitPay invoice W6ic1rU1XThEmLAEvwkjdX for merchant SomberNight_testing\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1708968409,\n        \"timestamp\": 1708968409,\n        \"txid\": \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\",\n        \"txpos_in_block\": 284,\n        \"value\": \"-0.0000531\",\n        \"wanted_height\": null\n    },\n    \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": {\n        \"acquisition_price\": \"20.82\",\n        \"amount_sat\": -100776,\n        \"bc_balance\": \"0.003027\",\n        \"bc_value\": \"-0.00100776\",\n        \"capital_gain\": \"1.05\",\n        \"confirmations\": 160828,\n        \"date\": \"2023-02-03 15:18\",\n        \"fee_sat\": 776,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.17\",\n        \"fiat_rate\": \"21698.00\",\n        \"fiat_value\": \"-21.87\",\n        \"group_id\": null,\n        \"height\": 2418761,\n        \"incoming\": false,\n        \"label\": \"asd\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1675433925,\n        \"timestamp\": 1675433925,\n        \"txid\": \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\",\n        \"txpos_in_block\": 56,\n        \"value\": \"-0.00100776\",\n        \"wanted_height\": null\n    },\n    \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": {\n        \"amount_sat\": 160000,\n        \"bc_balance\": \"0.0027\",\n        \"bc_value\": \"0.0016\",\n        \"confirmations\": 230212,\n        \"date\": \"2022-10-04 22:15\",\n        \"fee_sat\": null,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": null,\n        \"fiat_rate\": \"20379.27\",\n        \"fiat_value\": \"32.61\",\n        \"group_id\": null,\n        \"height\": 2349377,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1664914506,\n        \"timestamp\": 1664914506,\n        \"txid\": \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\",\n        \"txpos_in_block\": 58,\n        \"value\": \"0.0016\",\n        \"wanted_height\": null\n    },\n    \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": {\n        \"acquisition_price\": \"83.68\",\n        \"amount_sat\": -405000,\n        \"bc_balance\": \"0.\",\n        \"bc_value\": \"-0.00405\",\n        \"capital_gain\": \"-18.56\",\n        \"confirmations\": 173292,\n        \"date\": \"2022-11-14 22:34\",\n        \"fee_sat\": 289,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.05\",\n        \"fiat_rate\": \"16077.00\",\n        \"fiat_value\": \"-65.11\",\n        \"group_id\": null,\n        \"height\": 2406297,\n        \"incoming\": false,\n        \"label\": \"Open channel\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1668461654,\n        \"timestamp\": 1668461654,\n        \"txid\": \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\",\n        \"txpos_in_block\": 24,\n        \"value\": \"-0.00405\",\n        \"wanted_height\": null\n    },\n    \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": {\n        \"acquisition_price\": \"53.18\",\n        \"amount_sat\": -1000705,\n        \"bc_balance\": \"0.0759659\",\n        \"bc_value\": \"-0.01000705\",\n        \"capital_gain\": \"1.62\",\n        \"confirmations\": 1165278,\n        \"date\": \"2018-09-25 20:01\",\n        \"fee_sat\": 705,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.04\",\n        \"fiat_rate\": \"5476.40\",\n        \"fiat_value\": \"-54.80\",\n        \"group_id\": null,\n        \"height\": 1414311,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1537898497,\n        \"timestamp\": 1537898497,\n        \"txid\": \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\",\n        \"txpos_in_block\": 44,\n        \"value\": \"-0.01000705\",\n        \"wanted_height\": null\n    },\n    \"9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921\": {\n        \"amount_msat\": -1003100,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 18:59\",\n        \"direction\": 0,\n        \"fee_msat\": 3100,\n        \"fiat_default\": true,\n        \"fiat_value\": \"-0.50\",\n        \"group_id\": null,\n        \"label\": \"1 Blokaccino\",\n        \"lightning\": true,\n        \"ln_value\": \"-0.00001003\",\n        \"payment_hash\": \"9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921\",\n        \"preimage\": \"4dd0ddf9605e51998936d101b7741e89be08cb788c1e36b9b1d62cd5e9543af8\",\n        \"timestamp\": 1708970395,\n        \"type\": \"payment\",\n        \"value\": \"-0.00001003\"\n    },\n    \"a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f\": {\n        \"amount_msat\": -1003100,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 19:00\",\n        \"direction\": 0,\n        \"fee_msat\": 3100,\n        \"fiat_default\": true,\n        \"fiat_value\": \"-0.50\",\n        \"group_id\": null,\n        \"label\": \"1 Blokaccino\",\n        \"lightning\": true,\n        \"ln_value\": \"-0.00001003\",\n        \"payment_hash\": \"a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f\",\n        \"preimage\": \"48f81553d41291c78f5a09f21cc95eef11dd798e7abfd23fb1f1cb9eb8bf8359\",\n        \"timestamp\": 1708970418,\n        \"type\": \"payment\",\n        \"value\": \"-0.00001003\"\n    },\n    \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\": {\n        \"acquisition_price\": \"237.60\",\n        \"amount_sat\": -3199474,\n        \"bc_balance\": \"0.\",\n        \"bc_value\": \"-0.03199474\",\n        \"capital_gain\": \"0.00\",\n        \"confirmations\": 996545,\n        \"date\": \"2019-10-15 16:02\",\n        \"fee_sat\": 181,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.01\",\n        \"fiat_rate\": \"7426.20\",\n        \"fiat_value\": \"-237.60\",\n        \"group_id\": null,\n        \"height\": 1583044,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1571148168,\n        \"timestamp\": 1571148168,\n        \"txid\": \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\",\n        \"txpos_in_block\": 286,\n        \"value\": \"-0.03199474\",\n        \"wanted_height\": null\n    },\n    \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": {\n        \"acquisition_price\": \"0.06\",\n        \"amount_sat\": -1202,\n        \"bc_balance\": \"0.17898625\",\n        \"bc_value\": \"-0.00001202\",\n        \"capital_gain\": \"-0.02\",\n        \"confirmations\": 1122455,\n        \"date\": \"2019-02-14 18:27\",\n        \"fee_sat\": 202,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.01\",\n        \"fiat_rate\": \"3238.30\",\n        \"fiat_value\": \"-0.04\",\n        \"group_id\": null,\n        \"height\": 1457134,\n        \"incoming\": false,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1550165277,\n        \"timestamp\": 1550165277,\n        \"txid\": \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\",\n        \"txpos_in_block\": 52,\n        \"value\": \"-0.00001202\",\n        \"wanted_height\": null\n    },\n    \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": {\n        \"amount_sat\": 3099663,\n        \"bc_balance\": \"0.03099663\",\n        \"bc_value\": \"0.03099663\",\n        \"confirmations\": 996545,\n        \"date\": \"2019-10-15 16:02\",\n        \"fee_sat\": null,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": null,\n        \"fiat_rate\": \"7426.20\",\n        \"fiat_value\": \"230.19\",\n        \"group_id\": null,\n        \"height\": 1583044,\n        \"incoming\": true,\n        \"label\": \"old address 95645\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1571148168,\n        \"timestamp\": 1571148168,\n        \"txid\": \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\",\n        \"txpos_in_block\": 191,\n        \"value\": \"0.03099663\",\n        \"wanted_height\": null\n    },\n    \"cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3\": {\n        \"amount_msat\": 100000,\n        \"bc_value\": \"0.\",\n        \"date\": \"2024-02-26 19:35\",\n        \"direction\": 1,\n        \"fee_msat\": null,\n        \"fiat_default\": true,\n        \"fiat_value\": \"0.05\",\n        \"group_id\": null,\n        \"label\": \"general kenobi\",\n        \"lightning\": true,\n        \"ln_value\": \"0.000001\",\n        \"payment_hash\": \"cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3\",\n        \"preimage\": \"ee4f549b3844282ae596dfef14fc75e64067517fa92e9735fe792415bced3db9\",\n        \"timestamp\": 1708972515,\n        \"type\": \"payment\",\n        \"value\": \"0.000001\"\n    },\n    \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": {\n        \"amount_sat\": 404573,\n        \"bc_balance\": \"0.00404573\",\n        \"bc_value\": \"0.00404573\",\n        \"confirmations\": 164304,\n        \"date\": \"2023-01-08 11:32\",\n        \"fee_sat\": 138,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.02\",\n        \"fiat_rate\": \"16069.00\",\n        \"fiat_value\": \"65.01\",\n        \"group_id\": null,\n        \"height\": 2415285,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1673173967,\n        \"timestamp\": 1673173967,\n        \"txid\": \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\",\n        \"txpos_in_block\": 16,\n        \"value\": \"0.00404573\",\n        \"wanted_height\": null\n    },\n    \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": {\n        \"amount_sat\": 110000,\n        \"bc_balance\": \"0.0011\",\n        \"bc_value\": \"0.0011\",\n        \"confirmations\": 230215,\n        \"date\": \"2022-10-04 21:28\",\n        \"fee_sat\": null,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": null,\n        \"fiat_rate\": \"20379.27\",\n        \"fiat_value\": \"22.42\",\n        \"group_id\": null,\n        \"height\": 2349374,\n        \"incoming\": true,\n        \"label\": \"\",\n        \"lightning\": false,\n        \"ln_value\": \"0.\",\n        \"monotonic_timestamp\": 1664911683,\n        \"timestamp\": 1664911683,\n        \"txid\": \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\",\n        \"txpos_in_block\": 13,\n        \"value\": \"0.0011\",\n        \"wanted_height\": null\n    },\n    \"group:66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": {\n        \"acquisition_price\": \"0.04\",\n        \"bc_value\": \"0.00302364\",\n        \"capital_gain\": \"0.01\",\n        \"children\": [\n            {\n                \"amount_sat\": 302364,\n                \"bc_balance\": \"0.00302364\",\n                \"bc_value\": \"0.00302364\",\n                \"confirmations\": 51197,\n                \"date\": \"2023-10-03 17:15\",\n                \"fee_sat\": 183,\n                \"fiat_currency\": \"EUR\",\n                \"fiat_default\": true,\n                \"fiat_fee\": \"0.05\",\n                \"fiat_rate\": \"26224.00\",\n                \"fiat_value\": \"79.29\",\n                \"group_id\": \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n                \"height\": 2528392,\n                \"incoming\": true,\n                \"label\": \"Close channel 2425096x21x1\",\n                \"lightning\": false,\n                \"ln_value\": \"0.\",\n                \"monotonic_timestamp\": 1696346138,\n                \"timestamp\": 1696346138,\n                \"txid\": \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n                \"txpos_in_block\": 155,\n                \"value\": \"0.00302364\",\n                \"wanted_height\": null\n            },\n            {\n                \"amount_msat\": -302547000,\n                \"bc_value\": \"0.\",\n                \"date\": \"2023-10-03 17:15\",\n                \"direction\": null,\n                \"fee_msat\": null,\n                \"fiat_default\": true,\n                \"fiat_value\": \"-79.34\",\n                \"group_id\": \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n                \"label\": \"Close channel 2425096x21x1\",\n                \"lightning\": true,\n                \"ln_value\": \"-0.00302547\",\n                \"payment_hash\": null,\n                \"preimage\": null,\n                \"timestamp\": 1696346138,\n                \"type\": \"channel_closing\",\n                \"value\": \"-0.00302547\"\n            }\n        ],\n        \"confirmations\": 51197,\n        \"date\": \"2023-10-03 17:15\",\n        \"fee_sat\": 0,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.00\",\n        \"fiat_rate\": \"26224.00\",\n        \"fiat_value\": \"-0.05\",\n        \"height\": 2528392,\n        \"label\": \"Close channel 2425096x21x1\",\n        \"lightning\": false,\n        \"ln_value\": \"-0.00302547\",\n        \"timestamp\": 1696346138,\n        \"txid\": \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n        \"value\": \"-0.00000183\",\n        \"wanted_height\": null\n    },\n    \"group:97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": {\n        \"acquisition_price\": \"0.03\",\n        \"bc_value\": \"-0.003027\",\n        \"capital_gain\": \"0.01\",\n        \"children\": [\n            {\n                \"acquisition_price\": \"62.54\",\n                \"amount_sat\": -302700,\n                \"bc_balance\": \"0.\",\n                \"bc_value\": \"-0.003027\",\n                \"capital_gain\": \"16.89\",\n                \"confirmations\": 154493,\n                \"date\": \"2023-03-19 20:01\",\n                \"fee_sat\": 153,\n                \"fiat_currency\": \"EUR\",\n                \"fiat_default\": true,\n                \"fiat_fee\": \"0.04\",\n                \"fiat_rate\": \"26241.00\",\n                \"fiat_value\": \"-79.43\",\n                \"group_id\": \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n                \"height\": 2425096,\n                \"incoming\": false,\n                \"label\": \"Open channel\",\n                \"lightning\": false,\n                \"ln_value\": \"0.\",\n                \"monotonic_timestamp\": 1679252503,\n                \"timestamp\": 1679252503,\n                \"txid\": \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n                \"txpos_in_block\": 21,\n                \"value\": \"-0.003027\",\n                \"wanted_height\": null\n            },\n            {\n                \"amount_msat\": 302547000,\n                \"bc_value\": \"0.\",\n                \"date\": \"2023-03-19 20:01\",\n                \"direction\": null,\n                \"fee_msat\": null,\n                \"fiat_default\": true,\n                \"fiat_value\": \"79.39\",\n                \"group_id\": \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n                \"label\": \"Open channel 2425096x21x1\",\n                \"lightning\": true,\n                \"ln_value\": \"0.00302547\",\n                \"payment_hash\": null,\n                \"preimage\": null,\n                \"timestamp\": 1679252503,\n                \"type\": \"channel_opening\",\n                \"value\": \"0.00302547\"\n            }\n        ],\n        \"confirmations\": 154493,\n        \"date\": \"2023-03-19 20:01\",\n        \"fee_sat\": 0,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.00\",\n        \"fiat_rate\": \"26241.00\",\n        \"fiat_value\": \"-0.04\",\n        \"height\": 2425096,\n        \"label\": \"Open channel 2425096x21x1\",\n        \"lightning\": false,\n        \"ln_value\": \"0.00302547\",\n        \"timestamp\": 1679252503,\n        \"txid\": \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n        \"value\": \"-0.00000153\",\n        \"wanted_height\": null\n    },\n    \"group:c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": {\n        \"acquisition_price\": \"0.10\",\n        \"bc_value\": \"-0.0040019\",\n        \"capital_gain\": \"0.00\",\n        \"children\": [\n            {\n                \"acquisition_price\": \"201.19\",\n                \"amount_sat\": -400190,\n                \"bc_balance\": \"0.00402174\",\n                \"bc_value\": \"-0.0040019\",\n                \"capital_gain\": \"0.00\",\n                \"confirmations\": 6,\n                \"date\": \"2024-02-26 18:11\",\n                \"fee_sat\": 190,\n                \"fiat_currency\": \"EUR\",\n                \"fiat_default\": true,\n                \"fiat_fee\": \"0.10\",\n                \"fiat_rate\": \"50273.00\",\n                \"fiat_value\": \"-201.19\",\n                \"group_id\": \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                \"height\": 2579583,\n                \"incoming\": false,\n                \"label\": \"Open channel\",\n                \"lightning\": false,\n                \"ln_value\": \"0.\",\n                \"monotonic_timestamp\": 1708967515,\n                \"timestamp\": 1708967515,\n                \"txid\": \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                \"txpos_in_block\": 479,\n                \"value\": \"-0.0040019\",\n                \"wanted_height\": null\n            },\n            {\n                \"amount_msat\": 400000000,\n                \"bc_value\": \"0.\",\n                \"date\": \"2024-02-26 18:11\",\n                \"direction\": null,\n                \"fee_msat\": null,\n                \"fiat_default\": true,\n                \"fiat_value\": \"201.09\",\n                \"group_id\": \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                \"label\": \"Open channel 2579583x479x2\",\n                \"lightning\": true,\n                \"ln_value\": \"0.004\",\n                \"payment_hash\": null,\n                \"preimage\": null,\n                \"timestamp\": 1708967515,\n                \"type\": \"channel_opening\",\n                \"value\": \"0.004\"\n            }\n        ],\n        \"confirmations\": 6,\n        \"date\": \"2024-02-26 18:11\",\n        \"fee_sat\": 0,\n        \"fiat_currency\": \"EUR\",\n        \"fiat_default\": true,\n        \"fiat_fee\": \"0.00\",\n        \"fiat_rate\": \"50273.00\",\n        \"fiat_value\": \"-0.10\",\n        \"height\": 2579583,\n        \"label\": \"Open channel 2579583x479x2\",\n        \"lightning\": false,\n        \"ln_value\": \"0.004\",\n        \"timestamp\": 1708967515,\n        \"txid\": \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n        \"value\": \"-0.0000019\",\n        \"wanted_height\": null\n    }\n}"
  },
  {
    "path": "tests/test_i18n.py",
    "content": "from electrum import i18n\nfrom electrum.i18n import _ensure_translation_keeps_format_string_syntax_similar\n\nfrom . import ElectrumTestCase\n\n\nsyntax_check_decorator = _ensure_translation_keeps_format_string_syntax_similar\n\n\nclass TestSyntaxChecks(ElectrumTestCase):\n    # convention: source strings are lowercase, dest strings are uppercase\n\n    def test_no_format(self):\n        src, dst = (\"hello there\", \"HELLO THEEEEERE\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n\n    def test_malformed_src_string_raises(self):\n        src, dst = (\"hel{lo there\", \"HELLO THE{}RE\")\n        with self.assertRaises(ValueError):\n            syntax_check_decorator(lambda x: dst)(src)\n\n    def test_malformed_dst_string_gets_rejected(self):\n        src, dst = (\"hel{}lo there\", \"HELLO THE{RE\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hello there\", \"HELLO THE{RE\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hello there\", \"HELLO THE{{}RE\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n\n    def test_simple_substitution(self):\n        src, dst = (\"hel{}lo there\", \"HELLO THE{}RE\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hel{}lo {} there {}\", \"HELLO {} THE{}RE {}\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n\n    def test_positional_substitution(self):\n        src, dst = (\"hel{0}lo there\", \"HELLO THE{0}RE\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hel{0}lo there {1}\", \"HELLO THE{0}RE {1}\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hel{0}lo {2} there {1}\", \"HELLO THE{0}RE {2} {1}\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n\n    def test_keyword_substitution(self):\n        src, dst = (\"hello there {title}. {name}. welc\", \"HELLO THERE {title}. {name}. WELC\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n\n    def test_mixed_sub(self):\n        src, dst = (\"{1} aaa {qq} {0} bbb\", \"{1} AAA {qq} {0} BBB\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"{1} aaa {pp} {qq} {0} bbb\", \"{1} AAA {pp} {qq} {0} BBB\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n\n    def test_allow_reordering_replacement_fields(self):  # language-flexibility\n        src, dst = (\"time left: {0} minutes, {1} seconds\", \"TIME LEFT: {1} SECONDS, {0} MINUTES\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"{1} aaa {pp} {qq} {0} bbb\", \"{qq} AAA {0} {1} {pp} BBB\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n\n    def test_replacement_field_name_cannot_change(self):\n        # rejects:\n        src, dst = (\"hel{}lo there\", \"HELLO THE{RE}\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hel{}lo there\", \"HELLO THE{0}\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hel{0}lo there\", \"HELLO THE{}\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hel{0}lo there\", \"HELLO THE{RE}\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hel{RE}lo there\", \"HELLO THE{}\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hel{RE}lo there\", \"HELLO THE{0}\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        # we only check the set of field_names is invariant, so this is allowed:\n        src, dst = (\"hello there {} {} {} {p} {q}\", \"HELLO THERE {} {q} {q} {p} {q}\")\n        self.assertEqual(dst, syntax_check_decorator(lambda x: dst)(src))\n\n    def test_replacement_field_count_cannot_change(self):\n        # rejects:\n        src, dst = (\"hello there\", \"HELLO THERE {}\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hello there\", \"HELLO {} {} THERE\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hello {} there\", \"HELLO THERE {} {}\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hello there {}\", \"HELLO THERE\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hello there {p} {q} {r}\", \"HELLO THERE {p} {q}\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hello there {p} {q} {r}\", \"HELLO THERE {p} {q} {r} {s}\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n        src, dst = (\"hello there {p} {0}\", \"HELLO THERE {p}\")\n        self.assertEqual(src, syntax_check_decorator(lambda x: dst)(src))\n"
  },
  {
    "path": "tests/test_interface.py",
    "content": "import asyncio\nimport collections\nfrom typing import Optional, Sequence, Iterable, Mapping\n\nimport aiorpcx\nfrom aiorpcx import RPCError\n\nimport electrum\nfrom electrum.interface import ServerAddr, Interface, PaddedRSTransport\nfrom electrum import util, blockchain\nfrom electrum.util import OldTaskGroup, bfh\nfrom electrum.logging import Logger\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.transaction import Transaction\nfrom electrum import constants\nfrom electrum.wallet import Abstract_Wallet\nfrom electrum.blockchain import Blockchain\nfrom electrum.bitcoin import script_to_scripthash\nfrom electrum.synchronizer import history_status\n\nfrom . import ElectrumTestCase\nfrom . import restore_wallet_from_text__for_unittest\n\n\nclass TestServerAddr(ElectrumTestCase):\n\n    def test_from_str(self):\n        self.assertEqual(ServerAddr(host=\"104.198.149.61\", port=80, protocol=\"t\"),\n                         ServerAddr.from_str(\"104.198.149.61:80:t\"))\n        self.assertEqual(ServerAddr(host=\"ecdsa.net\", port=110, protocol=\"s\"),\n                         ServerAddr.from_str(\"ecdsa.net:110:s\"))\n        self.assertEqual(ServerAddr(host=\"2400:6180:0:d1::86b:e001\", port=50002, protocol=\"s\"),\n                         ServerAddr.from_str(\"[2400:6180:0:d1::86b:e001]:50002:s\"))\n        self.assertEqual(ServerAddr(host=\"localhost\", port=8080, protocol=\"s\"),\n                         ServerAddr.from_str(\"localhost:8080:s\"))\n\n    def test_from_str_with_inference(self):\n        self.assertEqual(None, ServerAddr.from_str_with_inference(\"104.198.149.61\"))\n        self.assertEqual(None, ServerAddr.from_str_with_inference(\"ecdsa.net\"))\n        self.assertEqual(None, ServerAddr.from_str_with_inference(\"2400:6180:0:d1::86b:e001\"))\n        self.assertEqual(None, ServerAddr.from_str_with_inference(\"[2400:6180:0:d1::86b:e001]\"))\n\n        self.assertEqual(ServerAddr(host=\"104.198.149.61\", port=80, protocol=\"s\"),\n                         ServerAddr.from_str_with_inference(\"104.198.149.61:80\"))\n        self.assertEqual(ServerAddr(host=\"ecdsa.net\", port=110, protocol=\"s\"),\n                         ServerAddr.from_str_with_inference(\"ecdsa.net:110\"))\n        self.assertEqual(ServerAddr(host=\"2400:6180:0:d1::86b:e001\", port=50002, protocol=\"s\"),\n                         ServerAddr.from_str_with_inference(\"[2400:6180:0:d1::86b:e001]:50002\"))\n\n        self.assertEqual(ServerAddr(host=\"104.198.149.61\", port=80, protocol=\"t\"),\n                         ServerAddr.from_str_with_inference(\"104.198.149.61:80:t\"))\n        self.assertEqual(ServerAddr(host=\"ecdsa.net\", port=110, protocol=\"s\"),\n                         ServerAddr.from_str_with_inference(\"ecdsa.net:110:s\"))\n        self.assertEqual(ServerAddr(host=\"2400:6180:0:d1::86b:e001\", port=50002, protocol=\"s\"),\n                         ServerAddr.from_str_with_inference(\"[2400:6180:0:d1::86b:e001]:50002:s\"))\n\n    def test_to_friendly_name(self):\n        self.assertEqual(\"104.198.149.61:80:t\",\n                         ServerAddr(host=\"104.198.149.61\", port=80, protocol=\"t\").to_friendly_name())\n        self.assertEqual(\"ecdsa.net:110\",\n                         ServerAddr(host=\"ecdsa.net\", port=110, protocol=\"s\").to_friendly_name())\n        self.assertEqual(\"ecdsa.net:50001:t\",\n                         ServerAddr(host=\"ecdsa.net\", port=50001, protocol=\"t\").to_friendly_name())\n        self.assertEqual(\"[2400:6180:0:d1::86b:e001]:50002\",\n                         ServerAddr(host=\"2400:6180:0:d1::86b:e001\", port=50002, protocol=\"s\").to_friendly_name())\n        self.assertEqual(\"[2400:6180:0:d1::86b:e001]:50001:t\",\n                         ServerAddr(host=\"2400:6180:0:d1::86b:e001\", port=50001, protocol=\"t\").to_friendly_name())\n\n\nclass MockNetwork:\n\n    def __init__(self, *, config: SimpleConfig):\n        self.config = config\n        self.asyncio_loop = util.get_asyncio_loop()\n        self.taskgroup = OldTaskGroup()\n        blockchain.read_blockchains(self.config)\n        blockchain.init_headers_file_for_best_chain()\n        self.proxy = None\n        self.debug = True\n        self.bhi_lock = asyncio.Lock()\n        self.interface = None  # type: Interface | None\n\n    async def connection_down(self, interface: Interface):\n        pass\n    def get_network_timeout_seconds(self, request_type) -> int:\n        return 10\n    def check_interface_against_healthy_spread_of_connected_servers(self, iface_to_check: Interface) -> bool:\n        return True\n    def update_fee_estimates(self, *, fee_est: dict[int, int] = None):\n        pass\n    async def switch_unwanted_fork_interface(self):\n        pass\n    async def switch_lagging_interface(self):\n        pass\n    def blockchain(self) -> Blockchain:\n        return self.interface.blockchain\n    def get_local_height(self) -> int:\n        return self.blockchain().height()\n\n\n# regtest chain:\nBLOCK_HEADERS: Mapping[int, bytes] = {\n    0: bfh(\"0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff7f2002000000\"),\n    1: bfh(\"0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f186c8dfd970a4545f79916bc1d75c9d00432f57c89209bf3bb115b7612848f509c25f45bffff7f2000000000\"),\n    2: bfh(\"00000020686bdfc6a3db73d5d93e8c9663a720a26ecb1ef20eb05af11b36cdbc57c19f7ebf2cbf153013a1c54abaf70e95198fcef2f3059cc6b4d0f7e876808e7d24d11cc825f45bffff7f2000000000\"),\n    3: bfh(\"00000020122baa14f3ef54985ae546d1611559e3f487bd2a0f46e8dbb52fbacc9e237972e71019d7feecd9b8596eca9a67032c5f4641b23b5d731dc393e37de7f9c2f299e725f45bffff7f2000000000\"),\n    4: bfh(\"00000020f8016f7ef3a17d557afe05d4ea7ab6bde1b2247b7643896c1b63d43a1598b747a3586da94c71753f27c075f57f44faf913c31177a0957bbda42e7699e3a2141aed25f45bffff7f2001000000\"),\n    5: bfh(\"000000201d589c6643c1d121d73b0573e5ee58ab575b8fdf16d507e7e915c5fbfbbfd05e7aee1d692d1615c3bdf52c291032144ce9e3b258a473c17c745047f3431ff8e2ee25f45bffff7f2000000000\"),\n    6: bfh(\"00000020b833ed46eea01d4c980f59feee44a66aa1162748b6801029565d1466790c405c3a141ce635cbb1cd2b3a4fcdd0a3380517845ba41736c82a79cab535d31128066526f45bffff7f2001000000\"),\n    7: bfh(\"00000020abe8e119d1877c9dc0dc502d1a253fb9a67967c57732d2f71ee0280e8381ff0a9690c2fe7c1a4450c74dc908fe94dd96c3b0637d51475e9e06a78e944a0c7fe28126f45bffff7f2000000000\"),\n    8: bfh(\"000000202ce41d94eb70e1518bc1f72523f84a903f9705d967481e324876e1f8cf4d3452148be228a4c3f2061bafe7efdfc4a8d5a94759464b9b5c619994d45dfcaf49e1a126f45bffff7f2000000000\"),\n    9: bfh(\"00000020552755b6c59f3d51e361d16281842a4e166007799665b5daed86a063dd89857415681cb2d00ff889193f6a68a93f5096aeb2d84ca0af6185a462555822552221a626f45bffff7f2000000000\"),\n    10: bfh(\"00000020a13a491cbefc93cd1bb1938f19957e22a134faf14c7dee951c45533e2c750f239dc087fc977b06c24a69c682d1afd1020e6dc1f087571ccec66310a786e1548fab26f45bffff7f2000000000\"),\n    11: bfh(\"00000020dbf3a9b55dfefbaf8b6e43a89cf833fa2e208bbc0c1c5d76c0d71b9e4a65337803b243756c25053253aeda309604363460a3911015929e68705bd89dff6fe064b026f45bffff7f2002000000\"),\n    12: bfh(\"000000203d0932b3b0c78eccb39a595a28ae4a7c966388648d7783fd1305ec8d40d4fe5fd67cb902a7d807cee7676cb543feec3e053aa824d5dfb528d5b94f9760313d9db726f45bffff7f2001000000\"),\n}\n\n_active_server_sessions = set()\ndef _get_active_server_session() -> 'ToyServerSession':\n    assert 1 == len(_active_server_sessions), len(_active_server_sessions)\n    return list(_active_server_sessions)[0]\n\nclass ToyServerSession(aiorpcx.RPCSession, Logger):\n\n    def __init__(self, *args, **kwargs):\n        aiorpcx.RPCSession.__init__(self, *args, **kwargs)\n        Logger.__init__(self)\n        self.logger.debug(f'connection from {self.remote_address()}')\n        self.cur_height = 6  # type: int  # chain tip\n        self.txs = {\n            \"bdae818ad3c1f261317738ae9284159bf54874356f186dbc7afd631dc1527fcb\": bfh(\"020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0200f2052a010000001600140297bde2689a3c79ffe050583b62f86f2d9dae540000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000\"),\n        }  # type: dict[str, bytes]\n        self.txid_to_block_height = collections.defaultdict(int)  # type: dict[str, int]\n        self.subbed_headers = False\n        self.notified_height = None  # type: Optional[int]\n        self.subbed_scripthashes = set()  # type: set[str]\n        self.sh_to_funding_txids = collections.defaultdict(set)  # type: dict[str, set[str]]\n        self.sh_to_spending_txids = collections.defaultdict(set)  # type: dict[str, set[str]]\n        self._method_counts = collections.defaultdict(int)  # type: dict[str, int]\n        _active_server_sessions.add(self)\n\n    async def connection_lost(self):\n        await super().connection_lost()\n        self.logger.debug(f'{self.remote_address()} disconnected')\n        _active_server_sessions.discard(self)\n\n    async def handle_request(self, request):\n        handlers = {\n            'server.version': self._handle_server_version,\n            'server.features': self._handle_server_features,\n            'blockchain.estimatefee': self._handle_estimatefee,\n            'blockchain.headers.subscribe': self._handle_headers_subscribe,\n            'blockchain.block.header': self._handle_block_header,\n            'blockchain.block.headers': self._handle_block_headers,\n            'blockchain.scripthash.subscribe': self._handle_scripthash_subscribe,\n            'blockchain.scripthash.get_history': self._handle_scripthash_get_history,\n            'blockchain.transaction.get': self._handle_transaction_get,\n            'blockchain.transaction.broadcast': self._handle_transaction_broadcast,\n            'blockchain.transaction.get_merkle': self._handle_transaction_get_merkle,\n            'mempool.get_info': self._handle_mempool_get_info,\n            'server.ping': self._handle_ping,\n        }\n        handler = handlers.get(request.method)\n        self._method_counts[request.method] += 1\n        coro = aiorpcx.handler_invocation(handler, request)()\n        return await coro\n\n    async def _handle_server_version(self, client_name='', protocol_version=None, *args, **kwargs):\n        return ['toy_server/0.1', '1.6']\n\n    async def _handle_server_features(self) -> dict:\n        return {\n            'genesis_hash': constants.net.GENESIS,\n            'hosts': {\"14.3.140.101\": {\"tcp_port\": 51001, \"ssl_port\": 51002}},\n            'protocol_max': '1.6',\n            'protocol_min': '1.6',\n            'pruning': None,\n            'server_version': 'ElectrumX 1.19.0',\n            'hash_function': 'sha256',\n        }\n\n    async def _handle_estimatefee(self, number, mode=None):\n        return 0.00001000\n\n    async def _handle_mempool_get_info(self):\n        return {\n            \"mempoolminfee\": 0.00001000,\n            \"minrelaytxfee\": 0.00001000,\n            \"incrementalrelayfee\": 0.00001000,\n        }\n\n    def _get_headersub_result(self):\n        return {'hex': BLOCK_HEADERS[self.cur_height].hex(), 'height': self.cur_height}\n\n    async def _handle_headers_subscribe(self):\n        self.subbed_headers = True\n        return self._get_headersub_result()\n\n    async def _handle_block_header(self, height):\n        return BLOCK_HEADERS[height].hex()\n\n    async def _handle_block_headers(self, start_height, count):\n        assert start_height <= self.cur_height, (start_height, self.cur_height)\n        last_height = min(start_height+count-1, self.cur_height)  # [start_height, last_height]\n        count = last_height - start_height + 1\n        headers = list(BLOCK_HEADERS[idx].hex() for idx in range(start_height, last_height+1))\n        return {'headers': headers, 'count': count, 'max': 2016}\n\n    async def _handle_ping(self):\n        return None\n\n    async def _handle_transaction_get(self, tx_hash: str, verbose=False):\n        assert not verbose\n        rawtx = self.txs.get(tx_hash)\n        if rawtx is None:\n            DAEMON_ERROR = 2\n            raise RPCError(DAEMON_ERROR, f'daemon error: unknown txid={tx_hash}')\n        return rawtx.hex()\n\n    async def _handle_transaction_get_merkle(self, tx_hash: str, height: int) -> dict:\n        # Fake stuff. Client will ignore it due to config.NETWORK_SKIPMERKLECHECK\n        return {\n            \"merkle\":\n            [\n                \"713d6c7e6ce7bbea708d61162231eaa8ecb31c4c5dd84f81c20409a90069cb24\",\n                \"03dbaec78d4a52fbaf3c7aa5d3fccd9d8654f323940716ddf5ee2e4bda458fde\",\n                \"e670224b23f156c27993ac3071940c0ff865b812e21e0a162fe7a005d6e57851\",\n                \"369a1619a67c3108a8850118602e3669455c70cdcdb89248b64cc6325575b885\",\n                \"4756688678644dcb27d62931f04013254a62aeee5dec139d1aac9f7b1f318112\",\n                \"7b97e73abc043836fd890555bfce54757d387943a6860e5450525e8e9ab46be5\",\n                \"61505055e8b639b7c64fd58bce6fc5c2378b92e025a02583303f69930091b1c3\",\n                \"27a654ff1895385ac14a574a0415d3bbba9ec23a8774f22ec20d53dd0b5386ff\",\n                \"5312ed87933075e60a9511857d23d460a085f3b6e9e5e565ad2443d223cfccdc\",\n                \"94f60b14a9f106440a197054936e6fb92abbd69d6059b38fdf79b33fc864fca0\",\n                \"2d64851151550e8c4d337f335ee28874401d55b358a66f1bafab2c3e9f48773d\"\n            ],\n            \"block_height\": height,\n            \"pos\": 710,\n        }\n\n    async def _handle_transaction_broadcast(self, raw_tx: str) -> str:\n        tx = Transaction(raw_tx)\n        txid = tx.txid()\n        self.txs[txid] = bfh(raw_tx)\n        touched_sh = await self._process_added_tx(txid=txid)\n        if touched_sh:\n            await self._send_notifications(touched_sh=touched_sh)\n        return txid\n\n    async def _process_added_tx(self, *, txid: str) -> set[str]:\n        \"\"\"Returns touched scripthashes.\"\"\"\n        tx = Transaction(self.txs[txid])\n        touched_sh = set()\n        # update sh_to_funding_txids\n        for txout in tx.outputs():\n            sh = script_to_scripthash(txout.scriptpubkey)\n            self.sh_to_funding_txids[sh].add(txid)\n            touched_sh.add(sh)\n        # update sh_to_spending_txids\n        for txin in tx.inputs():\n            if parent_tx_raw := self.txs.get(txin.prevout.txid.hex()):\n                parent_tx = Transaction(parent_tx_raw)\n                ptxout = parent_tx.outputs()[txin.prevout.out_idx]\n                sh = script_to_scripthash(ptxout.scriptpubkey)\n                self.sh_to_spending_txids[sh].add(txid)\n                touched_sh.add(sh)\n        return touched_sh\n\n    async def _handle_scripthash_subscribe(self, sh: str) -> Optional[str]:\n        self.subbed_scripthashes.add(sh)\n        hist = self._calc_sh_history(sh)\n        return history_status(hist)\n\n    async def _handle_scripthash_get_history(self, sh: str) -> Sequence[dict]:\n        hist_tuples = self._calc_sh_history(sh)\n        hist_dicts = [{\"height\": height, \"tx_hash\": txid} for (txid, height) in hist_tuples]\n        for hist_dict in hist_dicts:  # add \"fee\" key for mempool txs\n            if hist_dict[\"height\"] in (0, -1,):\n                hist_dict[\"fee\"] = 0\n        return hist_dicts\n\n    def _calc_sh_history(self, sh: str) -> Sequence[tuple[str, int]]:\n        txids = self.sh_to_funding_txids[sh] | self.sh_to_spending_txids[sh]\n        hist = []\n        for txid in txids:\n            bh = self.txid_to_block_height[txid]\n            hist.append((txid, bh))\n        hist.sort(key=lambda x: x[1])  # FIXME put mempool txs last\n        return hist\n\n    async def _send_notifications(self, *, touched_sh: Iterable[str], height_changed: bool = False) -> None:\n        if height_changed and self.subbed_headers and self.notified_height != self.cur_height:\n            self.notified_height = self.cur_height\n            args = (self._get_headersub_result(),)\n            await self.send_notification('blockchain.headers.subscribe', args)\n        touched_sh = set(sh for sh in touched_sh if sh in self.subbed_scripthashes)\n        for sh in touched_sh:\n            hist = self._calc_sh_history(sh)\n            args = (sh, history_status(hist))\n            await self.send_notification(\"blockchain.scripthash.subscribe\", args)\n\n    async def mine_block(self, *, txids_mined: Iterable[str] = None):\n        if txids_mined is None:\n            txids_mined = []\n        self.cur_height += 1\n        touched_sh = set()\n        for txid in txids_mined:\n            self.txid_to_block_height[txid] = self.cur_height\n            touched_sh |= await self._process_added_tx(txid=txid)\n        await self._send_notifications(touched_sh=touched_sh, height_changed=True)\n\n\nclass TestInterface(ElectrumTestCase):\n    REGTEST = True\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n        self.config.NETWORK_SKIPMERKLECHECK = True\n        self._orig_WAIT_FOR_BUFFER_GROWTH_SECONDS = PaddedRSTransport.WAIT_FOR_BUFFER_GROWTH_SECONDS\n        PaddedRSTransport.WAIT_FOR_BUFFER_GROWTH_SECONDS = 0\n\n    def tearDown(self):\n        PaddedRSTransport.WAIT_FOR_BUFFER_GROWTH_SECONDS = self._orig_WAIT_FOR_BUFFER_GROWTH_SECONDS\n        super().tearDown()\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self._server: asyncio.base_events.Server = await aiorpcx.serve_rs(ToyServerSession, \"127.0.0.1\")\n        server_socket_addr = self._server.sockets[0].getsockname()\n        self._server_port = server_socket_addr[1]\n        self.network = MockNetwork(config=self.config)\n\n    async def asyncTearDown(self):\n        if self.network.interface:\n            await self.network.interface.close()\n        self._server.close()\n        await self._server.wait_closed()\n        await super().asyncTearDown()\n\n    async def _start_iface_and_wait_for_sync(self):\n        interface = Interface(network=self.network, server=ServerAddr(host=\"127.0.0.1\", port=self._server_port, protocol=\"t\"))\n        self.network.interface = interface\n        async with util.async_timeout(5):\n            await interface.ready\n            await interface._blockchain_updated.wait()\n        return interface\n\n    async def test_client_syncs_headers_to_tip(self):\n        interface = await self._start_iface_and_wait_for_sync()\n        self.assertEqual(_get_active_server_session().cur_height, interface.tip)\n        self.assertFalse(interface.got_disconnected.is_set())\n\n    async def test_transaction_get(self):\n        interface = await self._start_iface_and_wait_for_sync()\n        # try requesting tx unknown to server:\n        with self.assertRaises(RPCError) as ctx:\n            await interface.get_transaction(\"deadbeef\"*8)\n        self.assertTrue(\"unknown txid\" in ctx.exception.message)\n        # try requesting known tx:\n        rawtx = await interface.get_transaction(\"bdae818ad3c1f261317738ae9284159bf54874356f186dbc7afd631dc1527fcb\")\n        self.assertEqual(rawtx, _get_active_server_session().txs[\"bdae818ad3c1f261317738ae9284159bf54874356f186dbc7afd631dc1527fcb\"].hex())\n        self.assertEqual(_get_active_server_session()._method_counts[\"blockchain.transaction.get\"], 2)\n\n    async def test_transaction_broadcast(self):\n        interface = await self._start_iface_and_wait_for_sync()\n        rawtx1 = \"020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025200ffffffff0200f2052a010000001600140297bde2689a3c79ffe050583b62f86f2d9dae540000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000\"\n        tx = Transaction(rawtx1)\n        # broadcast\n        await interface.broadcast_transaction(tx)\n        self.assertEqual(bfh(rawtx1), _get_active_server_session().txs.get(tx.txid()))\n        # now request tx.\n        # as we just broadcast this same tx, this will hit the client iface cache, and won't call the server.\n        self.assertEqual(_get_active_server_session()._method_counts[\"blockchain.transaction.get\"], 0)\n        rawtx2 = await interface.get_transaction(tx.txid())\n        self.assertEqual(rawtx1, rawtx2)\n        self.assertEqual(_get_active_server_session()._method_counts[\"blockchain.transaction.get\"], 0)\n\n    async def test_dont_request_gethistory_if_status_change_results_from_mempool_txs_simply_getting_mined(self):\n        \"\"\"After a new block is mined, we recv \"blockchain.scripthash.subscribe\" notifs.\n        We opportunistically guess the scripthash status changed purely because touching mempool txs just got mined.\n        If the guess is correct, we won't call the \"blockchain.scripthash.get_history\" RPC.\n        \"\"\"\n        interface = await self._start_iface_and_wait_for_sync()\n        w1 = restore_wallet_from_text__for_unittest(\"9dk\", path=None, config=self.config)['wallet']  # type: Abstract_Wallet\n        w1.start_network(self.network)\n        await w1.up_to_date_changed_event.wait()\n        self.assertEqual(_get_active_server_session()._method_counts[\"blockchain.scripthash.get_history\"], 0)\n        # fund w1 (in mempool)\n        funding_tx = \"01000000000101e855888b77b1688d08985b863bfe85b354049b4eba923db9b5cf37089975d5d10000000000fdffffff0280969800000000001600140297bde2689a3c79ffe050583b62f86f2d9dae5460abe9000000000016001472df47551b6e7e0c8428814d2e572bc5ac773dda024730440220383efa2f0f5b87f8ce5d6b6eaf48cba03bf522b23fbb23b2ac54ff9d9a8f6a8802206f67d1f909f3c7a22ac0308ac4c19853ffca3a9317e1d7e0c88cc3a86853aaac0121035061949222555a0df490978fe6e7ebbaa96332ecb5c266918fd800c0eef736e7358d1400\"\n        funding_txid = await _get_active_server_session()._handle_transaction_broadcast(funding_tx)\n        await w1.up_to_date_changed_event.wait()\n        while not w1.is_up_to_date():\n            await w1.up_to_date_changed_event.wait()\n        self.assertEqual(_get_active_server_session()._method_counts[\"blockchain.scripthash.get_history\"], 1)\n        self.assertEqual(\n            w1.adb.get_address_history(\"bcrt1qq2tmmcngng78nllq2pvrkchcdukemtj5jnxz44\"),\n            {funding_txid: 0})\n        # mine funding tx\n        await _get_active_server_session().mine_block(txids_mined=[funding_txid])\n        await w1.up_to_date_changed_event.wait()\n        while not w1.is_up_to_date():\n            await w1.up_to_date_changed_event.wait()\n        # see if we managed to guess new history, and hence did not need to call get_history RPC\n        self.assertEqual(_get_active_server_session()._method_counts[\"blockchain.scripthash.get_history\"], 1)\n        self.assertEqual(\n            w1.adb.get_address_history(\"bcrt1qq2tmmcngng78nllq2pvrkchcdukemtj5jnxz44\"),\n            {funding_txid: 7})\n\n"
  },
  {
    "path": "tests/test_invoices.py",
    "content": "import os\nimport time\n\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.wallet import Standard_Wallet, Abstract_Wallet\nfrom electrum.invoices import PR_UNPAID, PR_PAID, PR_UNCONFIRMED, BaseInvoice, Invoice, LN_EXPIRY_NEVER\nfrom electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED\nfrom electrum.transaction import Transaction, PartialTxOutput\nfrom electrum.util import TxMinedInfo, InvoiceError\nfrom electrum.fee_policy import FixedFeePolicy\n\nfrom . import ElectrumTestCase\nfrom . import restore_wallet_from_text__for_unittest\n\n\nclass TestWalletPaymentRequests(ElectrumTestCase):\n    \"\"\"test 'incoming' invoices\"\"\"\n    TESTNET = True\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n        self.wallet1_path = os.path.join(self.electrum_path, \"somewallet1\")\n        self.wallet2_path = os.path.join(self.electrum_path, \"somewallet2\")\n        self._orig_get_cur_time = BaseInvoice._get_cur_time\n\n    def tearDown(self):\n        super().tearDown()\n        BaseInvoice._get_cur_time = staticmethod(self._orig_get_cur_time)\n\n    def create_wallet2(self) -> Standard_Wallet:\n        text = 'cross end slow expose giraffe fuel track awake turtle capital ranch pulp'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet2_path, config=self.config)\n        wallet2 = d['wallet']  # type: Standard_Wallet\n        # bootstrap wallet\n        funding_tx = Transaction('0200000000010132515e6aade1b79ec7dd3bac0896d8b32c56195d23d07d48e21659cef24301560100000000fdffffff0112841e000000000016001477fe6d2a27e8860c278d4d2cd90bad716bb9521a02473044022041ed68ef7ef122813ac6a5e996b8284f645c53fbe6823b8e430604a8915a867802203233f5f4d347a687eb19b2aa570829ab12aeeb29a24cc6d6d20b8b3d79e971ae012102bee0ee043817e50ac1bb31132770f7c41e35946ccdcb771750fb9696bdd1b307ad951d00')\n        funding_txid = funding_tx.txid()\n        assert 'db949963c3787c90a40fb689ffdc3146c27a9874a970d1fd20921afbe79a7aa9' == funding_txid\n        wallet2.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        return wallet2\n\n    async def test_wallet_with_ln_creates_payreq_and_gets_paid_on_ln(self):\n        text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet1_path, config=self.config)\n        wallet1 = d['wallet']  # type: Standard_Wallet\n        self.assertIsNotNone(wallet1.lnworker)\n        self.assertTrue(wallet1.has_lightning())\n        # create payreq\n        addr = wallet1.get_unused_address()\n        pr_key = wallet1.create_request(amount_sat=10000, message=\"msg\", address=None, exp_delay=86400)\n        pr = wallet1.get_request(pr_key)\n        self.assertIsNotNone(pr)\n        self.assertTrue(pr.is_lightning())\n        self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr))\n        # get paid on LN\n        wallet1.lnworker.set_request_status(bytes.fromhex(pr.rhash), PR_PAID)\n        self.assertEqual(PR_PAID, wallet1.get_invoice_status(pr))\n\n    async def test_wallet_with_ln_creates_payreq_and_gets_paid_onchain(self):\n        text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet1_path, config=self.config)\n        wallet1 = d['wallet']  # type: Standard_Wallet\n        wallet1.db.put('stored_height', 1000)\n        self.assertIsNotNone(wallet1.lnworker)\n        self.assertTrue(wallet1.has_lightning())\n        # create payreq\n        addr = wallet1.get_unused_address()\n        pr_key = wallet1.create_request(amount_sat=10000, message=\"msg\", address=addr, exp_delay=86400)\n        pr = wallet1.get_request(pr_key)\n        self.assertIsNotNone(pr)\n        self.assertTrue(not pr.is_lightning())\n        self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr))\n        self.assertEqual(1000, pr.height)\n        # get paid onchain\n        wallet2 = self.create_wallet2()  # type: Standard_Wallet\n        outputs = [PartialTxOutput.from_address_and_value(pr.get_address(), pr.get_amount_sat())]\n        tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        wallet2.sign_transaction(tx, password=None)\n        wallet1.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))\n        # tx gets mined\n        wallet1.db.put('stored_height', 1010)\n        tx_info = TxMinedInfo(_height=1001,\n                              timestamp=pr.get_time() + 100,\n                              txpos=1,\n                              header_hash=\"01\"*32)\n        wallet1.adb.add_verified_tx(tx.txid(), tx_info)\n        self.assertEqual(PR_PAID, wallet1.get_invoice_status(pr))\n\n    async def test_wallet_without_ln_creates_payreq_and_gets_paid_onchain(self):\n        text = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet1_path, config=self.config)\n        wallet1 = d['wallet']  # type: Standard_Wallet\n        wallet1.db.put('stored_height', 1000)\n        self.assertIsNone(wallet1.lnworker)\n        self.assertFalse(wallet1.has_lightning())\n        # create payreq\n        addr = wallet1.get_unused_address()\n        pr_key = wallet1.create_request(amount_sat=10000, message=\"msg\", address=addr, exp_delay=86400)\n        pr = wallet1.get_request(pr_key)\n        self.assertIsNotNone(pr)\n        self.assertFalse(pr.is_lightning())\n        self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr))\n        self.assertEqual(1000, pr.height)\n        # get paid onchain\n        wallet2 = self.create_wallet2()  # type: Standard_Wallet\n        outputs = [PartialTxOutput.from_address_and_value(pr.get_address(), pr.get_amount_sat())]\n        tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        wallet2.sign_transaction(tx, password=None)\n        wallet1.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))\n        # tx gets mined\n        wallet1.db.put('stored_height', 1010)\n        tx_info = TxMinedInfo(_height=1001,\n                              timestamp=pr.get_time() + 100,\n                              txpos=1,\n                              header_hash=\"01\"*32)\n        wallet1.adb.add_verified_tx(tx.txid(), tx_info)\n        self.assertEqual(PR_PAID, wallet1.get_invoice_status(pr))\n\n    async def test_wallet_gets_paid_onchain_in_the_past(self):\n        text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet1_path, config=self.config)\n        wallet1 = d['wallet']  # type: Standard_Wallet\n        wallet1.db.put('stored_height', 1000)\n        self.assertIsNotNone(wallet1.lnworker)\n        self.assertTrue(wallet1.has_lightning())\n        # create payreq\n        addr = wallet1.get_unused_address()\n        pr_key = wallet1.create_request(amount_sat=10000, message=\"msg\", address=addr, exp_delay=86400)\n        pr = wallet1.get_request(pr_key)\n        self.assertIsNotNone(pr)\n        self.assertTrue(not pr.is_lightning())\n        self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr))\n        self.assertEqual(1000, pr.height)\n        # get paid onchain\n        wallet2 = self.create_wallet2()  # type: Standard_Wallet\n        outputs = [PartialTxOutput.from_address_and_value(pr.get_address(), pr.get_amount_sat())]\n        tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        wallet2.sign_transaction(tx, password=None)\n        wallet1.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))\n        # tx mined in the past (before invoice creation)\n        tx_info = TxMinedInfo(_height=990,\n                              timestamp=pr.get_time() + 100,\n                              txpos=1,\n                              header_hash=\"01\" * 32)\n        wallet1.adb.add_verified_tx(tx.txid(), tx_info)\n        self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr))\n\n    async def test_wallet_reuse_addr_of_expired_request(self):\n        text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet1_path, gap_limit=3, config=self.config)\n        wallet1 = d['wallet']  # type: Standard_Wallet\n        self.assertIsNotNone(wallet1.lnworker)\n        self.assertTrue(wallet1.has_lightning())\n        # create payreq1\n        addr1 = wallet1.get_unused_address()\n        pr1_key = wallet1.create_request(amount_sat=10000, message=\"msg\", address=addr1, exp_delay=86400)\n        pr1 = wallet1.get_request(pr1_key)\n        self.assertTrue(not pr1.is_lightning())\n        self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr1))\n        self.assertEqual(addr1, pr1.get_address())\n        self.assertFalse(pr1.has_expired())\n\n        BaseInvoice._get_cur_time = lambda *args: time.time() + 100_000\n        self.assertTrue(pr1.has_expired())\n\n        # create payreq2\n        addr2 = wallet1.get_unused_address()\n        self.assertEqual(addr1, addr2)\n        pr2_key = wallet1.create_request(amount_sat=10000, message=\"msg\", address=addr2, exp_delay=86400)\n        pr2 = wallet1.get_request(pr2_key)\n        self.assertTrue(not pr2.is_lightning())\n        self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr2))\n        self.assertEqual(addr2, pr2.get_address())\n        self.assertFalse(pr2.has_expired())\n\n    async def test_wallet_get_request_by_addr(self):\n        text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet1_path, gap_limit=3, config=self.config)\n        wallet1 = d['wallet']  # type: Standard_Wallet\n        self.assertIsNotNone(wallet1.lnworker)\n        self.assertTrue(wallet1.has_lightning())\n        # create payreq1\n        addr1 = wallet1.get_unused_address()\n        pr1_key = wallet1.create_request(amount_sat=10000, message=\"msg\", address=addr1, exp_delay=86400)\n        pr1 = wallet1.get_request(pr1_key)\n        self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr1))\n        self.assertFalse(pr1.has_expired())\n        self.assertEqual(pr1, wallet1.get_request_by_addr(addr1))\n\n        BaseInvoice._get_cur_time = lambda *args: time.time() + 100_000\n        self.assertTrue(pr1.has_expired())\n        self.assertEqual(None, wallet1.get_request_by_addr(addr1))\n\n        # create payreq2\n        addr2 = wallet1.get_unused_address()\n        self.assertEqual(addr1, addr2)\n        pr2_key = wallet1.create_request(amount_sat=10000, message=\"msg\", address=addr2, exp_delay=86400)\n        pr2 = wallet1.get_request(pr2_key)\n        self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr2))\n        self.assertFalse(pr2.has_expired())\n        self.assertEqual(pr2, wallet1.get_request_by_addr(addr1))\n\n        # pr2 gets paid onchain\n        wallet2 = self.create_wallet2()  # type: Standard_Wallet\n        outputs = [PartialTxOutput.from_address_and_value(pr2.get_address(), pr2.get_amount_sat())]\n        tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        wallet2.sign_transaction(tx, password=None)\n        wallet1.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr2))\n        self.assertEqual(pr2, wallet1.get_request_by_addr(addr1))\n\n        # FIXME the expired pr should stay \"expired\" - this might require storing state for it (see #8061):\n        self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr1))\n\n        # now make both invoices be past their expiration date. pr2 should be unaffected.\n        BaseInvoice._get_cur_time = lambda *args: time.time() + 200_000\n        self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr2))\n        self.assertEqual(pr2, wallet1.get_request_by_addr(addr1))\n\n\nclass TestBaseInvoice(ElectrumTestCase):\n    TESTNET = True\n\n    async def test_arg_validation(self):\n        amount_sat = 10_000\n        outputs = [PartialTxOutput.from_address_and_value(\"tb1qmjzmg8nd4z56ar4fpngzsr6euktrhnjg9td385\", amount_sat)]\n        invoice = Invoice(\n            amount_msat=amount_sat * 1000,\n            message=\"mymsg\",\n            time=1692716965,\n            exp=LN_EXPIRY_NEVER,\n            outputs=outputs,\n            bip70=None,\n            height=0,\n            lightning_invoice=None,\n        )\n        with self.assertRaises(InvoiceError):\n            invoice.amount_msat = 10**20\n        with self.assertRaises(InvoiceError):\n            invoice.set_amount_msat(10**20)\n        with self.assertRaises(InvoiceError):\n            invoice2 = Invoice(\n                amount_msat=10**20,\n                message=\"mymsg\",\n                time=1692716965,\n                exp=LN_EXPIRY_NEVER,\n                outputs=outputs,\n                bip70=None,\n                height=0,\n                lightning_invoice=None,\n            )\n        with self.assertRaises(TypeError):\n            invoice.time = \"asd\"\n        with self.assertRaises(TypeError):\n            invoice.exp = \"asd\"\n\n"
  },
  {
    "path": "tests/test_jsondb.py",
    "content": "import contextlib\nimport copy\nimport traceback\nimport json\nfrom typing import Any\n\nimport jsonpatch\nfrom jsonpatch import JsonPatchException\nfrom jsonpointer import JsonPointerException\n\nfrom . import ElectrumTestCase\n\nfrom electrum.json_db import JsonDB\n\nclass TestJsonpatch(ElectrumTestCase):\n\n    async def test_op_replace(self):\n        data1 = {'foo': 'bar', 'numbers': [1, 3, 4, 8], 'dictlevelA1': {'secret1': 2, 'secret2': 4, 'secret3': 6}}\n        patches = [{\"op\": \"replace\", \"path\": \"/dictlevelA1/secret2\", \"value\": 2222}]\n        jpatch = jsonpatch.JsonPatch(patches)\n        data2 = jpatch.apply(data1)\n        self.assertEqual(\n            {'foo': 'bar', 'numbers': [1, 3, 4, 8], 'dictlevelA1': {'secret1': 2, 'secret2': 2222, 'secret3': 6}},\n            data2\n        )\n\n    @contextlib.contextmanager\n    def _customAssertRaises(self, *args, **kwargs):\n        with self.assertRaises(*args, **kwargs) as ctx:\n            try:\n                yield ctx\n            except Exception as e:\n                # save original traceback now, as assertRaises will destroy most of it imminently:\n                ctx._customctx_original_tb = \"\".join(traceback.format_exception(e))\n                raise\n\n    async def test_patch_does_not_leak_privatekeys(self):\n        data1 = {\n            'dictlevelB1': 'secret77',\n            'dictlevelC1': [1, \"secret99\", 4, 8],\n            'dictlevelA1': {\"dictlevelA2_aa\": \"secret11\", \"dictlevelA2_bb\": \"secret12\", \"dictlevelA2_cc\": \"secret13\"}}\n        def fail_if_leaking_secret(ctx) -> None:\n            self.assertNotIn(\"secret\", str(ctx.exception))\n            self.assertNotIn(\"secret\", repr(ctx.exception))\n            self.assertNotIn(\"secret\", ctx._customctx_original_tb)\n            self.assertNotIn(\"dictlevel\", str(ctx.exception))\n            self.assertNotIn(\"dictlevel\", repr(ctx.exception))\n            self.assertNotIn(\"dictlevel\", ctx._customctx_original_tb)\n            self.assertIn(\"redacted\", str(ctx.exception))  # injected by our monkeypatching\n            self.assertIn(\"redacted\", repr(ctx.exception))  # injected by our monkeypatching\n        # op \"replace\"\n        with self.subTest(msg=\"replace_dict_inner_key_missing\"):\n            patches = [{\"op\": \"replace\", \"path\": \"/dictlevelA1/dictlevelX2\", \"value\": \"nakamoto_secret\"}]\n            jpatch = jsonpatch.JsonPatch(patches)\n            with self._customAssertRaises(JsonPatchException) as ctx:\n                data2 = jpatch.apply(data1)\n            fail_if_leaking_secret(ctx)\n        with self.subTest(msg=\"replace_dict_outer_key_missing\"):\n            patches = [{\"op\": \"replace\", \"path\": \"/dictlevelX1/dictlevelX2\", \"value\": \"nakamoto_secret\"}]\n            jpatch = jsonpatch.JsonPatch(patches)\n            with self._customAssertRaises(JsonPointerException) as ctx:\n                data2 = jpatch.apply(data1)\n            fail_if_leaking_secret(ctx)\n        # op \"remove\"\n        with self.subTest(msg=\"remove_dict_inner_key_missing\"):\n            patches = [{\"op\": \"remove\", \"path\": \"/dictlevelA1/dictlevelX2\"}]\n            jpatch = jsonpatch.JsonPatch(patches)\n            with self._customAssertRaises(JsonPatchException) as ctx:\n                data2 = jpatch.apply(data1)\n            fail_if_leaking_secret(ctx)\n        with self.subTest(msg=\"remove_dict_outer_key_missing\"):\n            patches = [{\"op\": \"remove\", \"path\": \"/dictlevelX1/dictlevelX2\"}]\n            jpatch = jsonpatch.JsonPatch(patches)\n            with self._customAssertRaises(JsonPointerException) as ctx:\n                data2 = jpatch.apply(data1)\n            fail_if_leaking_secret(ctx)\n        # op \"add\"\n        with self.subTest(msg=\"add_dict_inner_key_missing\"):\n            patches = [{\"op\": \"add\", \"path\": \"/dictlevelA1/dictlevelX2/dictlevelX3/dictlevelX4\", \"value\": \"nakamoto_secret\"}]\n            jpatch = jsonpatch.JsonPatch(patches)\n            with self._customAssertRaises(JsonPointerException) as ctx:\n                data2 = jpatch.apply(data1)\n            fail_if_leaking_secret(ctx)\n        with self.subTest(msg=\"add_dict_outer_key_missing\"):\n            patches = [{\"op\": \"add\", \"path\": \"/dictlevelX1/dictlevelX2/dictlevelX3/dictlevelX4\", \"value\": \"nakamoto_secret\"}]\n            jpatch = jsonpatch.JsonPatch(patches)\n            with self._customAssertRaises(JsonPointerException) as ctx:\n                data2 = jpatch.apply(data1)\n            fail_if_leaking_secret(ctx)\n\n\ndef pop1_from_dict(d: dict, key: str) -> Any:\n    return d.pop(key)\n\n\ndef pop2_from_dict(d: dict, key: str) -> Any:\n    val = d[key]\n    del d[key]\n    return val\n\n\nclass TestJsonDB(ElectrumTestCase):\n\n    async def test_jsonpatch_replace_after_remove(self):\n        data = { 'a':{} }\n        # op \"add\"\n        patches = [{\"op\": \"add\", \"path\": \"/a/b\", \"value\": \"42\"}]\n        jpatch = jsonpatch.JsonPatch(patches)\n        data = jpatch.apply(data)\n        self.assertEqual(data, {'a': {\"b\": \"42\"}})\n        # remove\n        patches = [{\"op\": \"remove\", \"path\": \"/a/b\"}]\n        jpatch = jsonpatch.JsonPatch(patches)\n        data = jpatch.apply(data)\n        self.assertEqual(data, {'a': {}})\n        # replace\n        patches = [{\"op\": \"replace\", \"path\": \"/a/b\", \"value\": \"43\"}]\n        jpatch = jsonpatch.JsonPatch(patches)\n        with self.assertRaises(JsonPatchException):\n            data = jpatch.apply(data)\n\n    async def test_jsondb_replace_after_remove(self):\n        for pop_from_dict in [pop1_from_dict, pop2_from_dict]:\n            with self.subTest(pop_from_dict):\n                data = { 'a': {'b': {'c': 0}}, 'd': 3}\n                db = JsonDB(repr(data))\n                a = db.get_dict('a')\n                # remove\n                b = pop_from_dict(a, 'b')\n                self.assertEqual(len(db.pending_changes), 1)\n                # replace item. this must not been written to db\n                b['c'] = 42\n                self.assertEqual(len(db.pending_changes), 1)\n                patches = json.loads('[' + ','.join(db.pending_changes) + ']')\n                jpatch = jsonpatch.JsonPatch(patches)\n                data = jpatch.apply(data)\n                self.assertEqual(data, {'a': {}, 'd': 3})\n\n    async def test_jsondb_replace_after_remove_nested(self):\n        for pop_from_dict in [pop1_from_dict, pop2_from_dict]:\n            with self.subTest(pop_from_dict):\n                data = { 'a': {'b': {'c': 0}}, 'd': 3}\n                db = JsonDB(repr(data))\n                # remove\n                a = pop_from_dict(db.data, \"a\")\n                self.assertEqual(len(db.pending_changes), 1)\n                b = a['b']\n                # replace item. this must not be written to db\n                b['c'] = 42\n                self.assertEqual(len(db.pending_changes), 1)\n                patches = json.loads('[' + ','.join(db.pending_changes) + ']')\n                jpatch = jsonpatch.JsonPatch(patches)\n                data = jpatch.apply(data)\n                self.assertEqual(data, {'d': 3})\n"
  },
  {
    "path": "tests/test_lnchannel.py",
    "content": "# Copyright (C) 2018 The Electrum developers\n# Copyright (C) 2015-2018 The Lightning Network Developers\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# Many of these unit tests are heavily based on unit tests in lnd\n# (around commit 42de4400bff5105352d0552155f73589166d162b).\n\nimport unittest\nfrom functools import lru_cache\nfrom unittest import mock\nimport os\nimport binascii\nfrom pprint import pformat\nimport logging\nimport dataclasses\nimport time\nfrom typing import TYPE_CHECKING\n\nfrom electrum import bitcoin\nfrom electrum import lnpeer\nfrom electrum import lnchannel\nfrom electrum import lnutil\nfrom electrum.crypto import privkey_to_pubkey\nfrom electrum.lnutil import (\n    SENT, LOCAL, REMOTE, RECEIVED, UpdateAddHtlc, LnFeatures, secret_to_pubkey, ChannelType,\n    effective_htlc_tx_weight, LocalConfig, RemoteConfig, OnlyPubkeyKeypair,\n)\nfrom electrum.logging import console_stderr_handler\nfrom electrum.lnchannel import ChannelState, Channel\nfrom electrum.json_db import StoredDict\nfrom electrum.coinchooser import PRNG\n\nfrom . import ElectrumTestCase\n\nif TYPE_CHECKING:\n    from .test_lnpeer import MockLNWallet\n\n\none_bitcoin_in_msat = bitcoin.COIN * 1000\n\n\ndef _convert_to_rconfig_from_lconfig(lconfig: LocalConfig) -> RemoteConfig:\n    \"\"\"converts Alice's local config to Bob's remote config (neutering private keys, etc)\"\"\"\n    ctn = 0\n    pcp_secret = lnutil.get_per_commitment_secret_from_seed(\n        lconfig.per_commitment_secret_seed,\n        lnutil.RevocationStore.START_INDEX - ctn)\n    pcp_point = secret_to_pubkey(int.from_bytes(pcp_secret, 'big'))\n    rconfig = RemoteConfig(\n        payment_basepoint=OnlyPubkeyKeypair(pubkey=lconfig.payment_basepoint.pubkey),\n        multisig_key=OnlyPubkeyKeypair(pubkey=lconfig.multisig_key.pubkey),\n        htlc_basepoint=OnlyPubkeyKeypair(pubkey=lconfig.htlc_basepoint.pubkey),\n        delayed_basepoint=OnlyPubkeyKeypair(pubkey=lconfig.delayed_basepoint.pubkey),\n        revocation_basepoint=OnlyPubkeyKeypair(pubkey=lconfig.revocation_basepoint.pubkey),\n        to_self_delay=lconfig.to_self_delay,\n        dust_limit_sat=lconfig.dust_limit_sat,\n        max_htlc_value_in_flight_msat=lconfig.max_htlc_value_in_flight_msat,\n        max_accepted_htlcs=lconfig.max_accepted_htlcs,\n        initial_msat=lconfig.initial_msat,\n        reserve_sat=lconfig.reserve_sat,\n        htlc_minimum_msat=lconfig.htlc_minimum_msat,\n        upfront_shutdown_script=lconfig.upfront_shutdown_script,\n        announcement_node_sig=lconfig.announcement_node_sig,\n        announcement_bitcoin_sig=lconfig.announcement_bitcoin_sig,\n        next_per_commitment_point=pcp_point,\n        current_per_commitment_point=None,\n    )\n    return rconfig\n\n\ndef create_channel_state(\n    *,\n    funding_txid: str,\n    funding_index: int,\n    funding_sat: int,\n    is_initiator: bool,\n    other_node_id: bytes,\n    channel_type: ChannelType,\n    local_config: LocalConfig,\n    remote_config: RemoteConfig,\n):\n    channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index)\n    state = {\n            \"channel_id\":channel_id.hex(),\n            \"short_channel_id\":channel_id[:8],\n            \"funding_outpoint\":lnpeer.Outpoint(funding_txid, funding_index),\n            \"remote_config\": remote_config,\n            \"local_config\": local_config,\n            \"constraints\":lnpeer.ChannelConstraints(\n                flags=lnchannel.CF_ANNOUNCE_CHANNEL,\n                capacity=funding_sat,\n                is_initiator=is_initiator,\n                funding_txn_minimum_depth=3,\n            ),\n            \"node_id\":other_node_id.hex(),\n            'onion_keys': {},\n            'data_loss_protect_remote_pcp': {},\n            'state': 'PREOPENING',\n            'log': {},\n            'unfulfilled_htlcs': {},\n            'revocation_store': {},\n            'channel_type': channel_type,\n    }\n    return StoredDict(state, None)\n\n\ndef create_test_channels(\n    *,\n    alice_lnwallet: 'MockLNWallet',\n    bob_lnwallet: 'MockLNWallet',\n    feerate=6000,\n    local_msat=None,\n    remote_msat=None,\n    random_seed=None,\n    anchor_outputs: bool = False,\n    local_max_inflight=None,\n    remote_max_inflight=None,\n    max_accepted_htlcs=5,\n) -> tuple[Channel, Channel]:\n    if random_seed is None:  # needed for deterministic randomness\n        random_seed = os.urandom(32)\n    random_gen = PRNG(random_seed)\n    alice_name = alice_lnwallet.name\n    bob_name = bob_lnwallet.name\n    alice_pubkey = alice_lnwallet.node_keypair.pubkey\n    bob_pubkey = bob_lnwallet.node_keypair.pubkey\n    funding_txid = random_gen.get_bytes(32).hex()\n    funding_index = 0\n    funding_sat = ((local_msat + remote_msat) // 1000) if local_msat is not None and remote_msat is not None else (bitcoin.COIN * 10)\n    local_msat = local_msat if local_msat is not None else (funding_sat * 1000 // 2)\n    remote_msat = remote_msat if remote_msat is not None else (funding_sat * 1000 // 2)\n    local_max_inflight = funding_sat * 1000 if local_max_inflight is None else local_max_inflight\n    remote_max_inflight = funding_sat * 1000 if remote_max_inflight is None else remote_max_inflight\n\n    for config in [alice_lnwallet.config, bob_lnwallet.config]:\n        config.LIGHTNING_MAX_FUNDING_SAT = max(config.LIGHTNING_MAX_FUNDING_SAT, funding_sat)\n\n    peer_features = alice_lnwallet.features | LnFeatures.OPTION_SUPPORT_LARGE_CHANNEL_OPT\n    channel_type = ChannelType.OPTION_STATIC_REMOTEKEY\n    if anchor_outputs:\n        channel_type |= ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX\n    # create alice's local config\n    alice_lconfig = alice_lnwallet.make_local_config_for_new_channel(\n        funding_sat=funding_sat,\n        push_msat=remote_msat,\n        initiator=LOCAL,\n        channel_type=channel_type,\n        multisig_funding_keypair=None,\n        peer_features=peer_features,\n        channel_seed=random_gen.get_bytes(32),\n    )\n    alice_lconfig.funding_locked_received = True\n    alice_lconfig.dust_limit_sat = 200\n    alice_lconfig.to_self_delay = 5\n    alice_lconfig.reserve_sat = 0\n    alice_lconfig.max_accepted_htlcs = max_accepted_htlcs\n    alice_lconfig.max_htlc_value_in_flight_msat = local_max_inflight\n    # create bob's local config\n    bob_lconfig = bob_lnwallet.make_local_config_for_new_channel(\n        funding_sat=funding_sat,\n        push_msat=remote_msat,\n        initiator=REMOTE,\n        channel_type=channel_type,\n        multisig_funding_keypair=None,\n        peer_features=peer_features,\n        channel_seed=random_gen.get_bytes(32),\n    )\n    bob_lconfig.funding_locked_received = True\n    bob_lconfig.dust_limit_sat = 1300\n    bob_lconfig.to_self_delay = 4\n    bob_lconfig.reserve_sat = 0\n    bob_lconfig.max_accepted_htlcs = max_accepted_htlcs\n    bob_lconfig.max_htlc_value_in_flight_msat = remote_max_inflight\n\n    alice, bob = (\n        lnchannel.Channel(\n            create_channel_state(\n                funding_txid=funding_txid,\n                funding_index=funding_index,\n                funding_sat=funding_sat,\n                is_initiator=True,\n                other_node_id=bob_pubkey,\n                channel_type=channel_type,\n                local_config=alice_lconfig,\n                remote_config=_convert_to_rconfig_from_lconfig(bob_lconfig),\n            ),\n            name=f\"{alice_name}->{bob_name}\",\n            initial_feerate=feerate,\n            lnworker=alice_lnwallet,\n        ),\n        lnchannel.Channel(\n            create_channel_state(\n                funding_txid=funding_txid,\n                funding_index=funding_index,\n                funding_sat=funding_sat,\n                is_initiator=False,\n                other_node_id=alice_pubkey,\n                channel_type=channel_type,\n                local_config=bob_lconfig,\n                remote_config=_convert_to_rconfig_from_lconfig(alice_lconfig),\n            ),\n            name=f\"{bob_name}->{alice_name}\",\n            initial_feerate=feerate,\n            lnworker=bob_lnwallet,\n        )\n    )\n\n    alice.hm.log[LOCAL]['ctn'] = 0\n    bob.hm.log[LOCAL]['ctn'] = 0\n\n    alice._state = ChannelState.OPEN\n    bob._state = ChannelState.OPEN\n\n    a_out = alice.get_latest_commitment(LOCAL).outputs()\n    b_out = bob.get_next_commitment(REMOTE).outputs()\n    assert a_out == b_out, \"\\n\" + pformat((a_out, b_out))\n\n    sig_from_bob, a_htlc_sigs = bob.sign_next_commitment()\n    sig_from_alice, b_htlc_sigs = alice.sign_next_commitment()\n\n    assert len(a_htlc_sigs) == 0\n    assert len(b_htlc_sigs) == 0\n\n    alice.open_with_first_pcp(alice.config[REMOTE].next_per_commitment_point, sig_from_bob)\n    bob.open_with_first_pcp(bob.config[REMOTE].next_per_commitment_point, sig_from_alice)\n\n    alice_second = lnutil.secret_to_pubkey(int.from_bytes(\n        lnutil.get_per_commitment_secret_from_seed(alice.config[LOCAL].per_commitment_secret_seed, lnutil.RevocationStore.START_INDEX - 1), \"big\"))\n    bob_second = lnutil.secret_to_pubkey(int.from_bytes(\n        lnutil.get_per_commitment_secret_from_seed(bob.config[LOCAL].per_commitment_secret_seed, lnutil.RevocationStore.START_INDEX - 1), \"big\"))\n\n    # from funding_locked:\n    alice.config[REMOTE].next_per_commitment_point = bob_second\n    bob.config[REMOTE].next_per_commitment_point = alice_second\n\n    alice._fallback_sweep_address = bitcoin.pubkey_to_address('p2wpkh', alice.config[LOCAL].payment_basepoint.pubkey.hex())\n    bob._fallback_sweep_address = bitcoin.pubkey_to_address('p2wpkh', bob.config[LOCAL].payment_basepoint.pubkey.hex())\n\n    return alice, bob\n\n\nclass TestFee(ElectrumTestCase):\n    \"\"\"\n    test\n    https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2\n    \"\"\"\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.alice_lnwallet = self.create_mock_lnwallet(name=\"alice\", has_anchors=self.TEST_ANCHOR_CHANNELS)\n        self.bob_lnwallet = self.create_mock_lnwallet(name=\"bob\", has_anchors=self.TEST_ANCHOR_CHANNELS)\n\n    async def test_fee(self):\n        alice_channel, bob_channel = create_test_channels(\n            feerate=253,\n            local_msat=10_000_000_000,\n            remote_msat=5_000_000_000,\n            anchor_outputs=self.TEST_ANCHOR_CHANNELS,\n            alice_lnwallet=self.alice_lnwallet,\n            bob_lnwallet=self.bob_lnwallet,\n        )\n        expected_value = 9_999_056 if self.TEST_ANCHOR_CHANNELS else 9_999_817\n        self.assertIn(expected_value, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()])\n\n\nclass TestChannel(ElectrumTestCase):\n    maxDiff = 999\n\n    def assertOutputExistsByValue(self, tx, amt_sat):\n        for o in tx.outputs():\n            if o.value == amt_sat:\n                break\n        else:\n            self.assertFalse()\n\n    def assertNumberNonAnchorOutputs(self, number, tx):\n        self.assertEqual(number, len(tx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0))\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        console_stderr_handler.setLevel(logging.DEBUG)\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.alice_lnwallet = self.create_mock_lnwallet(name=\"alice\", has_anchors=self.TEST_ANCHOR_CHANNELS)\n        self.bob_lnwallet = self.create_mock_lnwallet(name=\"bob\", has_anchors=self.TEST_ANCHOR_CHANNELS)\n\n        # Create a test channel which will be used for the duration of this\n        # unittest. The channel will be funded evenly with Alice having 5 BTC,\n        # and Bob having 5 BTC.\n        self.alice_channel, self.bob_channel = create_test_channels(\n            anchor_outputs=self.TEST_ANCHOR_CHANNELS, alice_lnwallet=self.alice_lnwallet, bob_lnwallet=self.bob_lnwallet)\n\n        self.paymentPreimage = b\"\\x01\" * 32\n        paymentHash = bitcoin.sha256(self.paymentPreimage)\n        self.htlc = UpdateAddHtlc(\n            payment_hash=paymentHash,\n            amount_msat=one_bitcoin_in_msat,\n            cltv_abs=5,\n            timestamp=0,\n        )\n\n        # First Alice adds the outgoing HTLC to her local channel's state\n        # update log. Then Alice sends this wire message over to Bob who adds\n        # this htlc to his remote state update log.\n        self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc).htlc_id\n        self.assertNotEqual(list(self.alice_channel.hm.htlcs_by_direction(REMOTE, RECEIVED, 1).values()), [])\n\n        before = self.bob_channel.balance_minus_outgoing_htlcs(REMOTE)\n        beforeLocal = self.bob_channel.balance_minus_outgoing_htlcs(LOCAL)\n\n        self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc).htlc_id\n\n        self.htlc = self.bob_channel.hm.log[REMOTE]['adds'][0]\n\n    def test_concurrent_reversed_payment(self):\n        self.htlc = dataclasses.replace(\n            self.htlc,\n            payment_hash=bitcoin.sha256(32 * b'\\x02'),\n            amount_msat=self.htlc.amount_msat + 1000,\n        )\n        self.bob_channel.add_htlc(self.htlc)\n        self.alice_channel.receive_htlc(self.htlc)\n\n        self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(LOCAL))\n        self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(LOCAL))\n        self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(REMOTE))\n        self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(REMOTE))\n\n        self.alice_channel.receive_new_commitment(*self.bob_channel.sign_next_commitment())\n\n        self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_latest_commitment(LOCAL))\n        self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(LOCAL))\n        self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(REMOTE))\n        self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(REMOTE))\n\n        self.alice_channel.revoke_current_commitment()\n\n        self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_latest_commitment(LOCAL))\n        self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(LOCAL))\n        self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(REMOTE))\n        self.assertNumberNonAnchorOutputs(4, self.alice_channel.get_next_commitment(REMOTE))\n\n    async def test_SimpleAddSettleWorkflow(self):\n        alice_channel, bob_channel = self.alice_channel, self.bob_channel\n        htlc = self.htlc\n\n        # Starting point: alice has sent an update_add_htlc message to bob\n        # but the htlc is not yet committed to\n        alice_out = alice_channel.get_latest_commitment(LOCAL).outputs()\n        if not alice_channel.has_anchors():\n            # ctx outputs are ordered by increasing amounts\n            low_amt_idx = 0\n            assert len(alice_out[low_amt_idx].address) == 62  # p2wsh\n            high_amt_idx = 1\n            assert len(alice_out[high_amt_idx].address) == 42  # p2wpkh\n        else:\n            # using anchor outputs, all outputs are p2wsh\n            low_amt_idx = 2\n            assert len(alice_out[low_amt_idx].address) == 62\n            high_amt_idx = 3\n            assert len(alice_out[high_amt_idx].address) == 62\n        self.assertLess(alice_out[low_amt_idx].value, 5 * 10**8, alice_out)\n        self.assertEqual(alice_out[high_amt_idx].value, 5 * 10**8, alice_out)\n\n        alice_out = alice_channel.get_latest_commitment(REMOTE).outputs()\n        if not alice_channel.has_anchors():\n            low_amt_idx = 0\n            assert len(alice_out[low_amt_idx].address) == 42\n            high_amt_idx = 1\n            assert len(alice_out[high_amt_idx].address) == 62\n        else:\n            low_amt_idx = 2\n            assert len(alice_out[low_amt_idx].address) == 62\n            high_amt_idx = 3\n            assert len(alice_out[high_amt_idx].address) == 62\n        self.assertLess(alice_out[low_amt_idx].value, 5 * 10**8)\n        self.assertEqual(alice_out[high_amt_idx].value, 5 * 10**8)\n\n        self.assertTrue(alice_channel.signature_fits(alice_channel.get_latest_commitment(LOCAL)))\n\n        self.assertNotEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 1), [])\n\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 0), [])\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 1), [htlc])\n\n        self.assertEqual(bob_channel.included_htlcs(REMOTE, SENT, 0), [])\n        self.assertEqual(bob_channel.included_htlcs(REMOTE, SENT, 1), [])\n\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, SENT, 0), [])\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, SENT, 1), [])\n\n        self.assertEqual(bob_channel.included_htlcs(REMOTE, RECEIVED, 0), [])\n        self.assertEqual(bob_channel.included_htlcs(REMOTE, RECEIVED, 1), [])\n\n        from electrum.lnutil import extract_ctn_from_tx_and_chan\n        tx0 = str(alice_channel.force_close_tx())\n        self.assertEqual(alice_channel.get_oldest_unrevoked_ctn(LOCAL), 0)\n        self.assertEqual(extract_ctn_from_tx_and_chan(alice_channel.force_close_tx(), alice_channel), 0)\n        self.assertTrue(alice_channel.signature_fits(alice_channel.get_latest_commitment(LOCAL)))\n\n        # Next alice commits this change by sending a signature message. Since\n        # we expect the messages to be ordered, Bob will receive the HTLC we\n        # just sent before he receives this signature, so the signature will\n        # cover the HTLC.\n        aliceSig, aliceHtlcSigs = alice_channel.sign_next_commitment()\n        self.assertEqual(len(aliceHtlcSigs), 1, \"alice should generate one htlc signature\")\n\n        self.assertTrue(alice_channel.signature_fits(alice_channel.get_latest_commitment(LOCAL)))\n\n        self.assertEqual(next(iter(alice_channel.hm.get_htlcs_in_next_ctx(REMOTE)))[0], RECEIVED)\n        self.assertEqual(alice_channel.hm.get_htlcs_in_next_ctx(REMOTE), bob_channel.hm.get_htlcs_in_next_ctx(LOCAL))\n        self.assertEqual(alice_channel.get_latest_commitment(REMOTE).outputs(), bob_channel.get_next_commitment(LOCAL).outputs())\n\n        # Bob receives this signature message, and checks that this covers the\n        # state he has in his remote log. This includes the HTLC just sent\n        # from Alice.\n        self.assertTrue(bob_channel.signature_fits(bob_channel.get_latest_commitment(LOCAL)))\n        bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs)\n        self.assertTrue(bob_channel.signature_fits(bob_channel.get_latest_commitment(LOCAL)))\n\n        self.assertEqual(bob_channel.get_oldest_unrevoked_ctn(REMOTE), 0)\n        self.assertEqual(bob_channel.included_htlcs(LOCAL, RECEIVED, 1), [htlc])\n\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 0), [])\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 1), [htlc])\n\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, SENT, 0), [])\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, SENT, 1), [])\n\n        self.assertEqual(bob_channel.included_htlcs(REMOTE, RECEIVED, 0), [])\n        self.assertEqual(bob_channel.included_htlcs(REMOTE, RECEIVED, 1), [])\n\n        # Bob revokes his prior commitment given to him by Alice, since he now\n        # has a valid signature for a newer commitment.\n        bobRevocation = bob_channel.revoke_current_commitment()\n        self.assertTrue(bob_channel.signature_fits(bob_channel.get_latest_commitment(LOCAL)))\n\n        # Bob finally sends a signature for Alice's commitment transaction.\n        # This signature will cover the HTLC, since Bob will first send the\n        # revocation just created. The revocation also acks every received\n        # HTLC up to the point where Alice sent her signature.\n        bobSig, bobHtlcSigs = bob_channel.sign_next_commitment()\n        self.assertTrue(bob_channel.signature_fits(bob_channel.get_latest_commitment(LOCAL)))\n\n        self.assertEqual(len(bobHtlcSigs), 1)\n\n        self.assertTrue(alice_channel.signature_fits(alice_channel.get_latest_commitment(LOCAL)))\n\n        # so far: Alice added htlc, Alice signed.\n        self.assertNumberNonAnchorOutputs(2, alice_channel.get_latest_commitment(LOCAL))\n        self.assertNumberNonAnchorOutputs(2, alice_channel.get_next_commitment(LOCAL))\n        self.assertNumberNonAnchorOutputs(2, alice_channel.get_oldest_unrevoked_commitment(REMOTE))\n        self.assertNumberNonAnchorOutputs(3, alice_channel.get_latest_commitment(REMOTE))\n\n        # Alice then processes this revocation, sending her own revocation for\n        # her prior commitment transaction. Alice shouldn't have any HTLCs to\n        # forward since she's sending an outgoing HTLC.\n        alice_channel.receive_revocation(bobRevocation)\n\n        self.assertTrue(alice_channel.signature_fits(alice_channel.get_latest_commitment(LOCAL)))\n\n        self.assertNumberNonAnchorOutputs(2, alice_channel.get_latest_commitment(LOCAL))\n        self.assertNumberNonAnchorOutputs(3, alice_channel.get_latest_commitment(REMOTE))\n        self.assertNumberNonAnchorOutputs(2, alice_channel.force_close_tx())\n\n        self.assertEqual(len(alice_channel.hm.log[LOCAL]['adds']), 1)\n        self.assertEqual(alice_channel.get_next_commitment(LOCAL).outputs(),\n                         bob_channel.get_latest_commitment(REMOTE).outputs())\n\n        # Alice then processes bob's signature, and since she just received\n        # the revocation, she expects this signature to cover everything up to\n        # the point where she sent her signature, including the HTLC.\n        alice_channel.receive_new_commitment(bobSig, bobHtlcSigs)\n\n        self.assertNumberNonAnchorOutputs(3, alice_channel.get_latest_commitment(REMOTE))\n        self.assertNumberNonAnchorOutputs(3, alice_channel.force_close_tx())\n\n        self.assertEqual(len(alice_channel.hm.log[LOCAL]['adds']), 1)\n\n        tx1 = str(alice_channel.force_close_tx())\n        self.assertNotEqual(tx0, tx1)\n\n        # Alice then generates a revocation for bob.\n        aliceRevocation = alice_channel.revoke_current_commitment()\n\n        tx2 = str(alice_channel.force_close_tx())\n        # since alice already has the signature for the next one, it doesn't change her force close tx (it was already the newer one)\n        self.assertEqual(tx1, tx2)\n\n        # Finally Bob processes Alice's revocation, at this point the new HTLC\n        # is fully locked in within both commitment transactions. Bob should\n        # also be able to forward an HTLC now that the HTLC has been locked\n        # into both commitment transactions.\n        self.assertTrue(bob_channel.signature_fits(bob_channel.get_latest_commitment(LOCAL)))\n        bob_channel.receive_revocation(aliceRevocation)\n\n        # At this point, both sides should have the proper number of satoshis\n        # sent, and commitment height updated within their local channel\n        # state.\n        aliceSent = 0\n        bobSent = 0\n\n        self.assertEqual(alice_channel.total_msat(SENT), aliceSent, \"alice has incorrect milli-satoshis sent\")\n        self.assertEqual(alice_channel.total_msat(RECEIVED), bobSent, \"alice has incorrect milli-satoshis received\")\n        self.assertEqual(bob_channel.total_msat(SENT), bobSent, \"bob has incorrect milli-satoshis sent\")\n        self.assertEqual(bob_channel.total_msat(RECEIVED), aliceSent, \"bob has incorrect milli-satoshis received\")\n        self.assertEqual(bob_channel.get_oldest_unrevoked_ctn(LOCAL), 1, \"bob has incorrect commitment height\")\n        self.assertEqual(alice_channel.get_oldest_unrevoked_ctn(LOCAL), 1, \"alice has incorrect commitment height\")\n\n        # Both commitment transactions should have three outputs, and one of\n        # them should be exactly the amount of the HTLC.\n        alice_ctx = alice_channel.get_next_commitment(LOCAL)\n        bob_ctx = bob_channel.get_next_commitment(LOCAL)\n        self.assertNumberNonAnchorOutputs(3, alice_ctx)\n        self.assertNumberNonAnchorOutputs(3, bob_ctx)\n        self.assertOutputExistsByValue(alice_ctx, htlc.amount_msat // 1000)\n        self.assertOutputExistsByValue(bob_ctx, htlc.amount_msat // 1000)\n\n        # Now we'll repeat a similar exchange, this time with Bob settling the\n        # HTLC once he learns of the preimage.\n        preimage = self.paymentPreimage\n        bob_channel.settle_htlc(preimage, self.bobHtlcIndex)\n\n        alice_channel.receive_htlc_settle(preimage, self.aliceHtlcIndex)\n\n        tx3 = str(alice_channel.force_close_tx())\n        # just settling a htlc does not change her force close tx\n        self.assertEqual(tx2, tx3)\n\n        bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment()\n        self.assertEqual(len(bobHtlcSigs2), 0)\n\n        self.assertEqual(list(alice_channel.hm.htlcs_by_direction(REMOTE, RECEIVED).values()), [htlc])\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, alice_channel.get_oldest_unrevoked_ctn(REMOTE)), [htlc])\n\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 1), [htlc])\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 2), [htlc])\n\n        self.assertEqual(bob_channel.included_htlcs(REMOTE, SENT, 1), [htlc])\n        self.assertEqual(bob_channel.included_htlcs(REMOTE, SENT, 2), [])\n\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, SENT, 1), [])\n        self.assertEqual(alice_channel.included_htlcs(REMOTE, SENT, 2), [])\n\n        self.assertEqual(bob_channel.included_htlcs(REMOTE, RECEIVED, 1), [])\n        self.assertEqual(bob_channel.included_htlcs(REMOTE, RECEIVED, 2), [])\n\n        alice_ctx_bob_version = bob_channel.get_latest_commitment(REMOTE).outputs()\n        alice_ctx_alice_version = alice_channel.get_next_commitment(LOCAL).outputs()\n        self.assertEqual(alice_ctx_alice_version, alice_ctx_bob_version)\n\n        alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2)\n\n        tx4 = str(alice_channel.force_close_tx())\n        self.assertNotEqual(tx3, tx4)\n\n        self.assertEqual(alice_channel.balance(LOCAL), 500000000000)\n        self.assertEqual(1, alice_channel.get_oldest_unrevoked_ctn(LOCAL))\n        self.assertEqual(len(alice_channel.included_htlcs(LOCAL, RECEIVED, ctn=2)), 0)\n        aliceRevocation2 = alice_channel.revoke_current_commitment()\n        aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment()\n        self.assertEqual(aliceHtlcSigs2, [], \"alice should generate no htlc signatures\")\n        self.assertNumberNonAnchorOutputs(3, bob_channel.get_latest_commitment(LOCAL))\n        bob_channel.receive_revocation(aliceRevocation2)\n\n        bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2)\n\n        bobRevocation2 = bob_channel.revoke_current_commitment()\n        received = lnchannel.htlcsum(bob_channel.hm.received_in_ctn(bob_channel.get_latest_ctn(LOCAL)))\n        self.assertEqual(one_bitcoin_in_msat, received)\n        alice_channel.receive_revocation(bobRevocation2)\n\n        # At this point, Bob should have 6 BTC settled, with Alice still having\n        # 4 BTC. Alice's channel should show 1 BTC sent and Bob's channel\n        # should show 1 BTC received. They should also be at commitment height\n        # two, with the revocation window extended by 1 (5).\n        mSatTransferred = one_bitcoin_in_msat\n        self.assertEqual(alice_channel.total_msat(SENT), mSatTransferred, \"alice satoshis sent incorrect\")\n        self.assertEqual(alice_channel.total_msat(RECEIVED), 0, \"alice satoshis received incorrect\")\n        self.assertEqual(bob_channel.total_msat(RECEIVED), mSatTransferred, \"bob satoshis received incorrect\")\n        self.assertEqual(bob_channel.total_msat(SENT), 0, \"bob satoshis sent incorrect\")\n        self.assertEqual(bob_channel.get_latest_ctn(LOCAL), 2, \"bob has incorrect commitment height\")\n        self.assertEqual(alice_channel.get_latest_ctn(LOCAL), 2, \"alice has incorrect commitment height\")\n\n        alice_channel.update_fee(100000, True)\n        alice_outputs = alice_channel.get_next_commitment(REMOTE).outputs()\n        old_outputs = bob_channel.get_next_commitment(LOCAL).outputs()\n        bob_channel.update_fee(100000, False)\n        new_outputs = bob_channel.get_next_commitment(LOCAL).outputs()\n        self.assertNotEqual(old_outputs, new_outputs)\n        self.assertEqual(alice_outputs, new_outputs)\n\n        tx5 = str(alice_channel.force_close_tx())\n        # sending a fee update does not change her force close tx\n        self.assertEqual(tx4, tx5)\n\n        force_state_transition(alice_channel, bob_channel)\n\n        tx6 = str(alice_channel.force_close_tx())\n        self.assertNotEqual(tx5, tx6)\n\n        self.htlc = dataclasses.replace(\n            self.htlc,\n            amount_msat=self.htlc.amount_msat * 5,\n        )\n        bob_index = bob_channel.add_htlc(self.htlc).htlc_id\n        alice_index = alice_channel.receive_htlc(self.htlc).htlc_id\n\n        force_state_transition(bob_channel, alice_channel)\n\n        alice_channel.settle_htlc(self.paymentPreimage, alice_index)\n        bob_channel.receive_htlc_settle(self.paymentPreimage, bob_index)\n\n        force_state_transition(alice_channel, bob_channel)\n        self.assertEqual(alice_channel.total_msat(SENT), one_bitcoin_in_msat, \"alice satoshis sent incorrect\")\n        self.assertEqual(alice_channel.total_msat(RECEIVED), 5 * one_bitcoin_in_msat, \"alice satoshis received incorrect\")\n        self.assertEqual(bob_channel.total_msat(RECEIVED), one_bitcoin_in_msat, \"bob satoshis received incorrect\")\n        self.assertEqual(bob_channel.total_msat(SENT), 5 * one_bitcoin_in_msat, \"bob satoshis sent incorrect\")\n\n    def alice_to_bob_fee_update(self, fee=1111):\n        aoldctx = self.alice_channel.get_next_commitment(REMOTE).outputs()\n        self.alice_channel.update_fee(fee, True)\n        anewctx = self.alice_channel.get_next_commitment(REMOTE).outputs()\n        self.assertNotEqual(aoldctx, anewctx)\n        boldctx = self.bob_channel.get_next_commitment(LOCAL).outputs()\n        self.bob_channel.update_fee(fee, False)\n        bnewctx = self.bob_channel.get_next_commitment(LOCAL).outputs()\n        self.assertNotEqual(boldctx, bnewctx)\n        self.assertEqual(anewctx, bnewctx)\n        return fee\n\n    def test_UpdateFeeSenderCommits(self):\n        alice_channel, bob_channel = self.alice_channel, self.bob_channel\n\n        old_feerate = alice_channel.get_next_feerate(LOCAL)\n\n        fee = self.alice_to_bob_fee_update()\n        self.assertEqual(alice_channel.get_next_feerate(LOCAL), old_feerate)\n\n        alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment()\n        #self.assertEqual(alice_channel.get_next_feerate(LOCAL), old_feerate)\n\n        bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs)\n\n        self.assertNotEqual(fee, bob_channel.get_oldest_unrevoked_feerate(LOCAL))\n        self.assertEqual(fee, bob_channel.get_latest_feerate(LOCAL))\n        rev = bob_channel.revoke_current_commitment()\n        self.assertEqual(fee, bob_channel.get_oldest_unrevoked_feerate(LOCAL))\n\n        alice_channel.receive_revocation(rev)\n\n\n        bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()\n        alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)\n\n        self.assertNotEqual(fee, alice_channel.get_oldest_unrevoked_feerate(LOCAL))\n        self.assertEqual(fee, alice_channel.get_latest_feerate(LOCAL))\n        rev = alice_channel.revoke_current_commitment()\n        self.assertEqual(fee, alice_channel.get_oldest_unrevoked_feerate(LOCAL))\n\n        bob_channel.receive_revocation(rev)\n        self.assertEqual(fee, bob_channel.get_oldest_unrevoked_feerate(LOCAL))\n        self.assertEqual(fee, bob_channel.get_latest_feerate(LOCAL))\n\n\n    def test_UpdateFeeReceiverCommits(self):\n        fee = self.alice_to_bob_fee_update()\n\n        alice_channel, bob_channel = self.alice_channel, self.bob_channel\n\n        bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()\n        alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)\n\n        alice_revocation = alice_channel.revoke_current_commitment()\n        bob_channel.receive_revocation(alice_revocation)\n        alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment()\n        bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs)\n\n        self.assertNotEqual(fee, bob_channel.get_oldest_unrevoked_feerate(LOCAL))\n        self.assertEqual(fee, bob_channel.get_latest_feerate(LOCAL))\n        bob_revocation = bob_channel.revoke_current_commitment()\n        self.assertEqual(fee, bob_channel.get_oldest_unrevoked_feerate(LOCAL))\n\n        bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()\n        alice_channel.receive_revocation(bob_revocation)\n        alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)\n\n        self.assertNotEqual(fee, alice_channel.get_oldest_unrevoked_feerate(LOCAL))\n        self.assertEqual(fee, alice_channel.get_latest_feerate(LOCAL))\n        alice_revocation = alice_channel.revoke_current_commitment()\n        self.assertEqual(fee, alice_channel.get_oldest_unrevoked_feerate(LOCAL))\n\n        bob_channel.receive_revocation(alice_revocation)\n        self.assertEqual(fee, bob_channel.get_oldest_unrevoked_feerate(LOCAL))\n        self.assertEqual(fee, bob_channel.get_latest_feerate(LOCAL))\n\n    @unittest.skip(\"broken probably because we haven't implemented detecting when we come out of a situation where we violate reserve\")\n    def test_AddHTLCNegativeBalance(self):\n        # the test in lnd doesn't set the fee to zero.\n        # probably lnd subtracts commitment fee after deciding weather\n        # an htlc can be added. so we set the fee to zero so that\n        # the test can work.\n        self.alice_to_bob_fee_update(0)\n        force_state_transition(self.alice_channel, self.bob_channel)\n\n        self.htlc = dataclasses.replace(\n            self.htlc,\n            payment_hash=bitcoin.sha256(32 * b'\\x02'),\n        )\n        self.alice_channel.add_htlc(self.htlc)\n        self.htlc = dataclasses.replace(\n            self.htlc,\n            payment_hash=bitcoin.sha256(32 * b'\\x03'),\n        )\n        self.alice_channel.add_htlc(self.htlc)\n        # now there are three htlcs (one was in setUp)\n\n        # Alice now has an available balance of 2 BTC. We'll add a new HTLC of\n        # value 2 BTC, which should make Alice's balance negative (since she\n        # has to pay a commitment fee).\n        new = dataclasses.replace(\n            self.htlc,\n            amount_msat=int(self.htlc.amount_msat * 2.5),\n            payment_hash=bitcoin.sha256(32 * b'\\x04'),\n        )\n        with self.assertRaises(lnutil.PaymentFailure) as cm:\n            self.alice_channel.add_htlc(new)\n        self.assertIn('Not enough local balance', cm.exception.args[0])\n\n    def test_unfunded_channel_can_be_removed(self):\n        \"\"\"\n        Test that an incoming channel which stays unfunded longer than\n        lnutil.CHANNEL_OPENING_TIMEOUT_BLOCKS and lnutil.CHANNEL_OPENING_TIMEOUT_SEC\n        can be removed\n        \"\"\"\n        # set the init_height and init_timestamp\n        self.current_height = 800_000\n        self.bob_channel.storage['init_height'] = self.current_height\n        self.alice_channel.storage['init_height'] = self.current_height\n        self.bob_channel.storage['init_timestamp'] = int(time.time())\n        self.alice_channel.storage['init_timestamp'] = int(time.time())\n\n        mock_lnworker = mock.Mock()\n        mock_blockchain = mock.Mock()\n        mock_lnworker.wallet = mock.Mock()\n        mock_lnworker.wallet.is_up_to_date = lambda: True\n        mock_blockchain.is_tip_stale = lambda: False\n        mock_lnworker.network.blockchain = lambda: mock_blockchain\n        mock_lnworker.network.get_local_height = lambda: self.current_height\n        self.bob_channel.lnworker = mock_lnworker\n        self.alice_channel.lnworker = mock_lnworker\n\n        # test that the non-initiator can remove the channel after timeout\n        self.assertFalse(self.bob_channel.is_initiator())\n        self.bob_channel._state = ChannelState.OPENING\n        self.assertFalse(self.bob_channel.can_be_deleted())\n        self.current_height += lnutil.CHANNEL_OPENING_TIMEOUT_BLOCKS + 1\n        self.assertFalse(self.bob_channel.can_be_deleted())  # needs both block and time based timeout\n        self.bob_channel.storage['init_timestamp'] -= lnutil.CHANNEL_OPENING_TIMEOUT_SEC + 1\n        self.alice_channel.storage['init_timestamp'] -= lnutil.CHANNEL_OPENING_TIMEOUT_SEC + 1\n        self.assertTrue(self.bob_channel.can_be_deleted())  # now both timeouts are reached\n        self.current_height = 800_000  # reset to check if we can delete with just the time based timeout\n        self.assertFalse(self.bob_channel.can_be_deleted())\n\n        # test that the initiator can't remove the channel, even after timeout\n        self.current_height += lnutil.CHANNEL_OPENING_TIMEOUT_BLOCKS + 1\n        self.assertTrue(self.alice_channel.is_initiator())\n        self.alice_channel._state = ChannelState.OPENING\n        self.assertFalse(self.alice_channel.can_be_deleted())\n\nclass TestChannelAnchors(TestChannel):\n    TEST_ANCHOR_CHANNELS = True\n\n\nclass TestAvailableToSpend(ElectrumTestCase):\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.alice_lnwallet = self.create_mock_lnwallet(name=\"alice\", has_anchors=self.TEST_ANCHOR_CHANNELS)\n        self.bob_lnwallet = self.create_mock_lnwallet(name=\"bob\", has_anchors=self.TEST_ANCHOR_CHANNELS)\n\n    async def test_DesyncHTLCs(self):\n        alice_channel, bob_channel = create_test_channels(\n            anchor_outputs=self.TEST_ANCHOR_CHANNELS, alice_lnwallet=self.alice_lnwallet, bob_lnwallet=self.bob_lnwallet)\n        self.assertEqual(499986152000 if not alice_channel.has_anchors() else 499980692000, alice_channel.available_to_spend(LOCAL))\n        self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL))\n\n        paymentPreimage = b\"\\x01\" * 32\n        paymentHash = bitcoin.sha256(paymentPreimage)\n        htlc = UpdateAddHtlc(\n            payment_hash=paymentHash,\n            amount_msat=one_bitcoin_in_msat * 41 // 10,\n            cltv_abs=5,\n            timestamp=0,\n        )\n\n        alice_idx = alice_channel.add_htlc(htlc).htlc_id\n        bob_idx = bob_channel.receive_htlc(htlc).htlc_id\n        self.assertEqual(89984088000 if not alice_channel.has_anchors() else 89978628000, alice_channel.available_to_spend(LOCAL))\n        self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL))\n\n        force_state_transition(alice_channel, bob_channel)\n        bob_channel.fail_htlc(bob_idx)\n        alice_channel.receive_fail_htlc(alice_idx, error_bytes=None)\n        self.assertEqual(89984088000 if not alice_channel.has_anchors() else 89978628000, alice_channel.available_to_spend(LOCAL))\n        self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL))\n        # Alice now has gotten all her original balance (5 BTC) back, however,\n        # adding a new HTLC at this point SHOULD fail, since if she adds the\n        # HTLC and signs the next state, Bob cannot assume she received the\n        # FailHTLC, and must assume she doesn't have the necessary balance\n        # available.\n        # We try adding an HTLC of value 1 BTC, which should fail because the\n        # balance is unavailable.\n        htlc = UpdateAddHtlc(\n            payment_hash=paymentHash,\n            amount_msat=one_bitcoin_in_msat,\n            cltv_abs=5,\n            timestamp=0,\n        )\n        with self.assertRaises(lnutil.PaymentFailure):\n            alice_channel.add_htlc(htlc)\n        # Now do a state transition, which will ACK the FailHTLC, making Alice\n        # able to add the new HTLC.\n        force_state_transition(alice_channel, bob_channel)\n        self.assertEqual(499986152000 if not alice_channel.has_anchors() else 499980692000, alice_channel.available_to_spend(LOCAL))\n        self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL))\n        alice_channel.add_htlc(htlc)\n\n    async def test_single_payment(self):\n        alice_channel, bob_channel = create_test_channels(\n            anchor_outputs=self.TEST_ANCHOR_CHANNELS,\n            local_msat=4000000000,\n            remote_msat=4000000000,\n            local_max_inflight=1000000000,\n            remote_max_inflight=2000000000,\n            alice_lnwallet=self.alice_lnwallet,\n            bob_lnwallet=self.bob_lnwallet,\n        )\n\n        # alice can send 20 but bob can only receive 10, because of stricter receiving rules\n        self.assertEqual(2000000000, alice_channel.available_to_spend(LOCAL))\n        self.assertEqual(1000000000, bob_channel.available_to_spend(REMOTE))\n\n        # bob can send 10, alice can receive 10\n        self.assertEqual(1000000000, bob_channel.available_to_spend(LOCAL))\n        self.assertEqual(1000000000, alice_channel.available_to_spend(REMOTE))\n\n        paymentPreimage1 = b\"\\x01\" * 32\n        htlc = UpdateAddHtlc(\n            payment_hash=bitcoin.sha256(paymentPreimage1),\n            amount_msat=1000000000,\n            cltv_abs=5,\n            timestamp=0,\n        )\n        # put 10mBTC inflight a->b\n        alice_idx1 = alice_channel.add_htlc(htlc).htlc_id\n        bob_idx1 = bob_channel.receive_htlc(htlc).htlc_id\n        force_state_transition(alice_channel, bob_channel)\n\n        self.assertEqual(1000000000, alice_channel.available_to_spend(LOCAL))\n        self.assertEqual(0, bob_channel.available_to_spend(REMOTE))\n\n        self.assertEqual(1000000000, bob_channel.available_to_spend(LOCAL))\n        self.assertEqual(1000000000, alice_channel.available_to_spend(REMOTE))\n\n        paymentPreimage2 = b\"\\x02\" * 32\n        htlc2 = UpdateAddHtlc(\n            payment_hash=bitcoin.sha256(paymentPreimage2),\n            amount_msat=1500000000,\n            cltv_abs=5,\n            timestamp=0,\n        )\n        # try to add another 15mBTC HTLC while 15mBTC already inflight\n        with self.assertRaises(lnutil.PaymentFailure):\n            alice_idx2 = alice_channel.add_htlc(htlc2).htlc_id\n\n        # settle htlc 1 to clear inflight\n        bob_channel.settle_htlc(paymentPreimage1, bob_idx1)\n        alice_channel.receive_htlc_settle(paymentPreimage1, alice_idx1)\n        force_state_transition(alice_channel, bob_channel)\n\n        self.assertEqual(2000000000, alice_channel.available_to_spend(LOCAL))\n        self.assertEqual(1000000000, alice_channel.available_to_spend(REMOTE))\n\n        self.assertEqual(1000000000, bob_channel.available_to_spend(LOCAL))\n        self.assertEqual(1000000000, alice_channel.available_to_spend(REMOTE))\n\n\nclass TestAvailableToSpendAnchors(TestAvailableToSpend):\n    TEST_ANCHOR_CHANNELS = True\n\n\nclass TestChanReserve(ElectrumTestCase):\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        alice_lnwallet = self.create_mock_lnwallet(name=\"alice\", has_anchors=self.TEST_ANCHOR_CHANNELS)\n        bob_lnwallet = self.create_mock_lnwallet(name=\"bob\", has_anchors=self.TEST_ANCHOR_CHANNELS)\n        alice_channel, bob_channel = create_test_channels(anchor_outputs=False, alice_lnwallet=alice_lnwallet, bob_lnwallet=bob_lnwallet)\n        alice_min_reserve = int(.5 * one_bitcoin_in_msat // 1000)\n        # We set Bob's channel reserve to a value that is larger than\n        # his current balance in the channel. This will ensure that\n        # after a channel is first opened, Bob can still receive HTLCs\n        # even though his balance is less than his channel reserve.\n        bob_min_reserve = 6 * one_bitcoin_in_msat // 1000\n        # bob min reserve was decided by alice, but applies to bob\n\n        alice_channel.config[LOCAL].reserve_sat = bob_min_reserve\n        alice_channel.config[REMOTE].reserve_sat = alice_min_reserve\n\n        bob_channel.config[LOCAL].reserve_sat = alice_min_reserve\n        bob_channel.config[REMOTE].reserve_sat = bob_min_reserve\n\n        self.alice_channel = alice_channel\n        self.bob_channel = bob_channel\n\n    @unittest.skip(\"broken probably because we haven't implemented detecting when we come out of a situation where we violate reserve\")\n    def test_part1(self):\n        # Add an HTLC that will increase Bob's balance. This should succeed,\n        # since Alice stays above her channel reserve, and Bob increases his\n        # balance (while still being below his channel reserve).\n        #\n        # Resulting balances:\n        #\tAlice:\t4.5\n        #\tBob:\t5.0\n        paymentPreimage = b\"\\x01\" * 32\n        paymentHash = bitcoin.sha256(paymentPreimage)\n        htlc = UpdateAddHtlc(\n            payment_hash=paymentHash,\n            amount_msat=int(.5 * one_bitcoin_in_msat),\n            cltv_abs=5,\n            timestamp=0,\n        )\n        self.alice_channel.add_htlc(htlc)\n        self.bob_channel.receive_htlc(htlc)\n        # Force a state transition, making sure this HTLC is considered valid\n        # even though the channel reserves are not met.\n        force_state_transition(self.alice_channel, self.bob_channel)\n\n        aliceSelfBalance = self.alice_channel.balance(LOCAL)\\\n                - lnchannel.htlcsum(self.alice_channel.hm.htlcs_by_direction(LOCAL, SENT).values())\n        bobBalance = self.bob_channel.balance(REMOTE)\\\n                - lnchannel.htlcsum(self.alice_channel.hm.htlcs_by_direction(REMOTE, SENT).values())\n        self.assertEqual(aliceSelfBalance, one_bitcoin_in_msat*4.5)\n        self.assertEqual(bobBalance, one_bitcoin_in_msat*5)\n        # Now let Bob try to add an HTLC. This should fail, since it will\n        # decrease his balance, which is already below the channel reserve.\n        #\n        # Resulting balances:\n        #\tAlice:\t4.5\n        #\tBob:\t5.0\n        with self.assertRaises(lnutil.PaymentFailure):\n            htlc = dataclasses.replace(htlc, payment_hash=bitcoin.sha256(32 * b'\\x02'))\n            self.bob_channel.add_htlc(htlc)\n        with self.assertRaises(lnutil.RemoteMisbehaving):\n            self.alice_channel.receive_htlc(htlc)\n\n    def part2(self):\n        paymentPreimage = b\"\\x01\" * 32\n        paymentHash = bitcoin.sha256(paymentPreimage)\n        # Now we'll add HTLC of 3.5 BTC to Alice's commitment, this should put\n        # Alice's balance at 1.5 BTC.\n        #\n        # Resulting balances:\n        #\tAlice:\t1.5\n        #\tBob:\t9.5\n        htlc = UpdateAddHtlc(\n            payment_hash=paymentHash,\n            amount_msat=int(3.5 * one_bitcoin_in_msat),\n            cltv_abs=5,\n        )\n        self.alice_channel.add_htlc(htlc)\n        self.bob_channel.receive_htlc(htlc)\n        # Add a second HTLC of 1 BTC. This should fail because it will take\n        # Alice's balance all the way down to her channel reserve, but since\n        # she is the initiator the additional transaction fee makes her\n        # balance dip below.\n        htlc = dataclasses.replace(htlc, amount_msat=one_bitcoin_in_msat)\n        with self.assertRaises(lnutil.PaymentFailure):\n            self.alice_channel.add_htlc(htlc)\n        with self.assertRaises(lnutil.RemoteMisbehaving):\n            self.bob_channel.receive_htlc(htlc)\n\n    def part3(self):\n        # Add a HTLC of 2 BTC to Alice, and the settle it.\n        # Resulting balances:\n        #\tAlice:\t3.0\n        #\tBob:\t7.0\n        paymentPreimage = b\"\\x01\" * 32\n        paymentHash = bitcoin.sha256(paymentPreimage)\n        htlc = UpdateAddHtlc(\n            payment_hash=paymentHash,\n            amount_msat=int(2 * one_bitcoin_in_msat),\n            cltv_abs=5,\n            timestamp=0,\n        )\n        alice_idx = self.alice_channel.add_htlc(htlc).htlc_id\n        bob_idx = self.bob_channel.receive_htlc(htlc).htlc_id\n        force_state_transition(self.alice_channel, self.bob_channel)\n        self.check_bals(one_bitcoin_in_msat * 3\n                        - self.alice_channel.get_next_fee(LOCAL),\n                        one_bitcoin_in_msat * 5)\n        self.bob_channel.settle_htlc(paymentPreimage, bob_idx)\n        self.alice_channel.receive_htlc_settle(paymentPreimage, alice_idx)\n        force_state_transition(self.alice_channel, self.bob_channel)\n        self.check_bals(one_bitcoin_in_msat * 3\n                        - self.alice_channel.get_next_fee(LOCAL),\n                        one_bitcoin_in_msat * 7)\n        # And now let Bob add an HTLC of 1 BTC. This will take Bob's balance\n        # all the way down to his channel reserve, but since he is not paying\n        # the fee this is okay.\n        htlc = dataclasses.replace(htlc, amount_msat=one_bitcoin_in_msat)\n        self.bob_channel.add_htlc(htlc)\n        self.alice_channel.receive_htlc(htlc)\n        force_state_transition(self.alice_channel, self.bob_channel)\n        self.check_bals(one_bitcoin_in_msat * 3 \\\n                        - self.alice_channel.get_next_fee(LOCAL),\n                        one_bitcoin_in_msat * 6)\n\n    def check_bals(self, amt1, amt2):\n        self.assertEqual(self.alice_channel.available_to_spend(LOCAL), amt1)\n        self.assertEqual(self.bob_channel.available_to_spend(REMOTE), amt1)\n        self.assertEqual(self.alice_channel.available_to_spend(REMOTE), amt2)\n        self.assertEqual(self.bob_channel.available_to_spend(LOCAL), amt2)\n\n\nclass TestChanReserveAnchors(TestChanReserve):\n    TEST_ANCHOR_CHANNELS = True\n\n\nclass TestDust(ElectrumTestCase):\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.alice_lnwallet = self.create_mock_lnwallet(name=\"alice\", has_anchors=self.TEST_ANCHOR_CHANNELS)\n        self.bob_lnwallet = self.create_mock_lnwallet(name=\"bob\", has_anchors=self.TEST_ANCHOR_CHANNELS)\n\n    async def test_DustLimit(self):\n        \"\"\"Test that addition of an HTLC below the dust limit changes the balances.\"\"\"\n        alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS, alice_lnwallet=self.alice_lnwallet, bob_lnwallet=self.bob_lnwallet)\n        dust_limit_alice = alice_channel.config[LOCAL].dust_limit_sat\n        dust_limit_bob = bob_channel.config[LOCAL].dust_limit_sat\n        self.assertLess(dust_limit_alice, dust_limit_bob)\n\n        bob_ctx = bob_channel.get_latest_commitment(LOCAL)\n        bobs_original_outputs = [x.value for x in bob_ctx.outputs()]\n        paymentPreimage = b\"\\x01\" * 32\n        paymentHash = bitcoin.sha256(paymentPreimage)\n        fee_per_kw = alice_channel.get_next_feerate(LOCAL)\n        success_weight = effective_htlc_tx_weight(success=True, has_anchors=self.TEST_ANCHOR_CHANNELS)\n        # we put a single sat less into the htlc than bob can afford\n        # to pay for his htlc success transaction\n        below_dust_for_bob = dust_limit_bob - 1\n        htlc_amt = below_dust_for_bob + success_weight * (fee_per_kw // 1000)\n        htlc = UpdateAddHtlc(\n            payment_hash=paymentHash,\n            amount_msat=1000 * htlc_amt,\n            cltv_abs=5,  # consistent with channel policy\n            timestamp=0,\n        )\n\n        # add the htlc\n        alice_htlc_id = alice_channel.add_htlc(htlc).htlc_id\n        bob_htlc_id = bob_channel.receive_htlc(htlc).htlc_id\n        force_state_transition(alice_channel, bob_channel)\n        alice_ctx = alice_channel.get_latest_commitment(LOCAL)\n        bob_ctx = bob_channel.get_latest_commitment(LOCAL)\n        bobs_second_outputs = [x.value for x in bob_ctx.outputs()]\n        self.assertNotEqual(bobs_original_outputs, bobs_second_outputs)\n        # the htlc appears as an output in alice's ctx, as she has a lower\n        # dust limit (also because her timeout tx costs less)\n        self.assertEqual(3, len(alice_ctx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0))\n        # htlc in bob's case goes to miner fees\n        self.assertEqual(2, len(bob_ctx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0))\n        self.assertEqual(htlc_amt, sum(bobs_original_outputs) - sum(bobs_second_outputs))\n        empty_ctx_fee = lnutil.calc_fees_for_commitment_tx(\n            num_htlcs=0, feerate=fee_per_kw, is_local_initiator=True,\n            round_to_sat=True, has_anchors=self.TEST_ANCHOR_CHANNELS)[LOCAL] // 1000\n        self.assertEqual(empty_ctx_fee + htlc_amt, bob_channel.get_next_fee(LOCAL))\n\n        bob_channel.settle_htlc(paymentPreimage, bob_htlc_id)\n        alice_channel.receive_htlc_settle(paymentPreimage, alice_htlc_id)\n        force_state_transition(bob_channel, alice_channel)\n        bob_ctx = bob_channel.get_latest_commitment(LOCAL)\n        bobs_third_outputs = [x.value for x in bob_ctx.outputs()]\n        # htlc is added back into the balance\n        self.assertEqual(sum(bobs_original_outputs), sum(bobs_third_outputs))\n        # balance shifts in bob's direction after settlement\n        self.assertEqual(htlc_amt, bobs_third_outputs[1 + (2 if self.TEST_ANCHOR_CHANNELS else 0)] - bobs_original_outputs[1 + (2 if self.TEST_ANCHOR_CHANNELS else 0)])\n        self.assertEqual(2, len(alice_channel.get_next_commitment(LOCAL).outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0))\n        self.assertEqual(2, len(bob_channel.get_next_commitment(LOCAL).outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0))\n        self.assertEqual(htlc_amt, alice_channel.total_msat(SENT) // 1000)\n\n\nclass TestDustAnchors(TestDust):\n    TEST_ANCHOR_CHANNELS = True\n\n\ndef force_state_transition(chanA, chanB):\n    chanB.receive_new_commitment(*chanA.sign_next_commitment())\n    rev = chanB.revoke_current_commitment()\n    bob_sig, bob_htlc_sigs = chanB.sign_next_commitment()\n    chanA.receive_revocation(rev)\n    chanA.receive_new_commitment(bob_sig, bob_htlc_sigs)\n    chanB.receive_revocation(chanA.revoke_current_commitment())\n"
  },
  {
    "path": "tests/test_lnhtlc.py",
    "content": "from pprint import pprint\nimport unittest\nfrom typing import NamedTuple\n\nfrom electrum.lnutil import RECEIVED, LOCAL, REMOTE, SENT, HTLCOwner, Direction\nfrom electrum.lnhtlc import HTLCManager\nfrom electrum.json_db import StoredDict\n\nfrom . import ElectrumTestCase\n\nclass H(NamedTuple):\n    owner : str\n    htlc_id : int\n\nclass TestHTLCManager(ElectrumTestCase):\n    def test_adding_htlcs_race(self):\n        A = HTLCManager(StoredDict({}, None))\n        B = HTLCManager(StoredDict({}, None))\n        A.channel_open_finished()\n        B.channel_open_finished()\n        ah0, bh0 = H('A', 0), H('B', 0)\n        B.recv_htlc(A.send_htlc(ah0))\n        self.assertEqual(B.log[REMOTE]['locked_in'][0][LOCAL], 1)\n        A.recv_htlc(B.send_htlc(bh0))\n        self.assertEqual(B.get_htlcs_in_latest_ctx(LOCAL), [])\n        self.assertEqual(A.get_htlcs_in_latest_ctx(LOCAL), [])\n        self.assertEqual(B.get_htlcs_in_next_ctx(LOCAL), [(RECEIVED, ah0)])\n        self.assertEqual(A.get_htlcs_in_next_ctx(LOCAL), [(RECEIVED, bh0)])\n        A.send_ctx()\n        B.recv_ctx()\n        B.send_ctx()\n        A.recv_ctx()\n        self.assertEqual(B.get_htlcs_in_oldest_unrevoked_ctx(LOCAL), [])\n        self.assertEqual(A.get_htlcs_in_oldest_unrevoked_ctx(LOCAL), [])\n        self.assertEqual(B.get_htlcs_in_latest_ctx(LOCAL), [(RECEIVED, ah0)])\n        self.assertEqual(A.get_htlcs_in_latest_ctx(LOCAL), [(RECEIVED, bh0)])\n        B.send_rev()\n        A.recv_rev()\n        A.send_rev()\n        B.recv_rev()\n        self.assertEqual(B.get_htlcs_in_oldest_unrevoked_ctx(LOCAL), [(RECEIVED, ah0)])\n        self.assertEqual(A.get_htlcs_in_oldest_unrevoked_ctx(LOCAL), [(RECEIVED, bh0)])\n        self.assertEqual(B.get_htlcs_in_latest_ctx(LOCAL), [(RECEIVED, ah0)])\n        self.assertEqual(A.get_htlcs_in_latest_ctx(LOCAL), [(RECEIVED, bh0)])\n        A.send_ctx()\n        B.recv_ctx()\n        B.send_ctx()\n        A.recv_ctx()\n        self.assertEqual(B.get_htlcs_in_oldest_unrevoked_ctx(LOCAL), [(RECEIVED, ah0)])\n        self.assertEqual(A.get_htlcs_in_oldest_unrevoked_ctx(LOCAL), [(RECEIVED, bh0)])\n        self.assertEqual(B.get_htlcs_in_latest_ctx(LOCAL), [(RECEIVED, ah0), (SENT, bh0)][::-1])\n        self.assertEqual(A.get_htlcs_in_latest_ctx(LOCAL), [(RECEIVED, bh0), (SENT, ah0)][::-1])\n        B.send_rev()\n        A.recv_rev()\n        A.send_rev()\n        B.recv_rev()\n        self.assertEqual(B.get_htlcs_in_oldest_unrevoked_ctx(LOCAL), [(RECEIVED, ah0), (SENT, bh0)][::-1])\n        self.assertEqual(A.get_htlcs_in_oldest_unrevoked_ctx(LOCAL), [(RECEIVED, bh0), (SENT, ah0)][::-1])\n        self.assertEqual(B.get_htlcs_in_latest_ctx(LOCAL), [(RECEIVED, ah0), (SENT, bh0)][::-1])\n        self.assertEqual(A.get_htlcs_in_latest_ctx(LOCAL), [(RECEIVED, bh0), (SENT, ah0)][::-1])\n\n    def test_single_htlc_full_lifecycle(self):\n        def htlc_lifecycle(htlc_success: bool):\n            A = HTLCManager(StoredDict({}, None))\n            B = HTLCManager(StoredDict({}, None))\n            A.channel_open_finished()\n            B.channel_open_finished()\n            B.recv_htlc(A.send_htlc(H('A', 0)))\n            self.assertEqual(len(B.get_htlcs_in_next_ctx(REMOTE)), 0)\n            self.assertEqual(len(A.get_htlcs_in_next_ctx(REMOTE)), 1)\n            self.assertEqual(len(B.get_htlcs_in_next_ctx(LOCAL)), 1)\n            self.assertEqual(len(A.get_htlcs_in_next_ctx(LOCAL)), 0)\n            A.send_ctx()\n            B.recv_ctx()\n            B.send_rev()\n            A.recv_rev()\n            B.send_ctx()\n            A.recv_ctx()\n            A.send_rev()\n            B.recv_rev()\n            self.assertEqual(len(A.get_htlcs_in_latest_ctx(LOCAL)), 1)\n            self.assertEqual(len(B.get_htlcs_in_latest_ctx(LOCAL)), 1)\n            if htlc_success:\n                B.send_settle(0)\n                A.recv_settle(0)\n            else:\n                B.send_fail(0)\n                A.recv_fail(0)\n            self.assertEqual(list(A.htlcs_by_direction(REMOTE, RECEIVED).values()), [H('A', 0)])\n            self.assertNotEqual(A.get_htlcs_in_latest_ctx(LOCAL), [])\n            self.assertNotEqual(B.get_htlcs_in_latest_ctx(REMOTE), [])\n\n            self.assertEqual(A.get_htlcs_in_next_ctx(LOCAL), [])\n            self.assertNotEqual(A.get_htlcs_in_next_ctx(REMOTE), [])\n            self.assertEqual(A.get_htlcs_in_next_ctx(REMOTE), A.get_htlcs_in_latest_ctx(REMOTE))\n\n            self.assertEqual(B.get_htlcs_in_next_ctx(REMOTE), [])\n            B.send_ctx()\n            A.recv_ctx()\n            A.send_rev() # here pending_htlcs(REMOTE) should become empty\n            self.assertEqual(A.get_htlcs_in_next_ctx(REMOTE), [])\n\n            B.recv_rev()\n            A.send_ctx()\n            B.recv_ctx()\n            B.send_rev()\n            A.recv_rev()\n            self.assertEqual(B.get_htlcs_in_latest_ctx(LOCAL), [])\n            self.assertEqual(A.get_htlcs_in_latest_ctx(LOCAL), [])\n            self.assertEqual(A.get_htlcs_in_latest_ctx(REMOTE), [])\n            self.assertEqual(B.get_htlcs_in_latest_ctx(REMOTE), [])\n            self.assertEqual(len(A.all_settled_htlcs_ever(LOCAL)), int(htlc_success))\n            self.assertEqual(len(A.sent_in_ctn(2)), int(htlc_success))\n            self.assertEqual(len(B.received_in_ctn(2)), int(htlc_success))\n\n            A.recv_htlc(B.send_htlc(H('B', 0)))\n            self.assertEqual(A.get_htlcs_in_next_ctx(REMOTE), [])\n            self.assertNotEqual(A.get_htlcs_in_next_ctx(LOCAL), [])\n            self.assertNotEqual(B.get_htlcs_in_next_ctx(REMOTE), [])\n            self.assertEqual(B.get_htlcs_in_next_ctx(LOCAL), [])\n\n            B.send_ctx()\n            A.recv_ctx()\n            A.send_rev()\n            B.recv_rev()\n\n            self.assertNotEqual(A.get_htlcs_in_next_ctx(REMOTE), A.get_htlcs_in_latest_ctx(REMOTE))\n            self.assertEqual(A.get_htlcs_in_next_ctx(LOCAL), A.get_htlcs_in_latest_ctx(LOCAL))\n            self.assertEqual(B.get_htlcs_in_next_ctx(REMOTE), B.get_htlcs_in_latest_ctx(REMOTE))\n            self.assertNotEqual(B.get_htlcs_in_next_ctx(LOCAL), B.get_htlcs_in_next_ctx(REMOTE))\n\n        htlc_lifecycle(htlc_success=True)\n        htlc_lifecycle(htlc_success=False)\n\n    def test_remove_htlc_while_owing_commitment(self):\n        def htlc_lifecycle(htlc_success: bool):\n            A = HTLCManager(StoredDict({}, None))\n            B = HTLCManager(StoredDict({}, None))\n            A.channel_open_finished()\n            B.channel_open_finished()\n            ah0 = H('A', 0)\n            B.recv_htlc(A.send_htlc(ah0))\n            A.send_ctx()\n            B.recv_ctx()\n            B.send_rev()\n            A.recv_rev()\n            if htlc_success:\n                B.send_settle(0)\n                A.recv_settle(0)\n            else:\n                B.send_fail(0)\n                A.recv_fail(0)\n            self.assertEqual([], A.get_htlcs_in_oldest_unrevoked_ctx(LOCAL))\n            self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_oldest_unrevoked_ctx(REMOTE))\n            self.assertEqual([], A.get_htlcs_in_latest_ctx(LOCAL))\n            self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_latest_ctx(REMOTE))\n            self.assertEqual([], A.get_htlcs_in_next_ctx(LOCAL))\n            self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_next_ctx(REMOTE))\n            B.send_ctx()\n            A.recv_ctx()\n            A.send_rev()\n            B.recv_rev()\n            self.assertEqual([], A.get_htlcs_in_oldest_unrevoked_ctx(LOCAL))\n            self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_oldest_unrevoked_ctx(REMOTE))\n            self.assertEqual([], A.get_htlcs_in_latest_ctx(LOCAL))\n            self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_latest_ctx(REMOTE))\n            self.assertEqual([], A.get_htlcs_in_next_ctx(LOCAL))\n            self.assertEqual([], A.get_htlcs_in_next_ctx(REMOTE))\n\n        htlc_lifecycle(htlc_success=True)\n        htlc_lifecycle(htlc_success=False)\n\n    def test_adding_htlc_between_send_ctx_and_recv_rev(self):\n        A = HTLCManager(StoredDict({}, None))\n        B = HTLCManager(StoredDict({}, None))\n        A.channel_open_finished()\n        B.channel_open_finished()\n        A.send_ctx()\n        B.recv_ctx()\n        B.send_rev()\n        ah0 = H('A', 0)\n        B.recv_htlc(A.send_htlc(ah0))\n        self.assertEqual([], A.get_htlcs_in_latest_ctx(LOCAL))\n        self.assertEqual([], A.get_htlcs_in_latest_ctx(REMOTE))\n        self.assertEqual([], A.get_htlcs_in_next_ctx(LOCAL))\n        self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_next_ctx(REMOTE))\n        A.recv_rev()\n        self.assertEqual([], A.get_htlcs_in_latest_ctx(LOCAL))\n        self.assertEqual([], A.get_htlcs_in_latest_ctx(REMOTE))\n        self.assertEqual([], A.get_htlcs_in_next_ctx(LOCAL))\n        self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_next_ctx(REMOTE))\n        A.send_ctx()\n        B.recv_ctx()\n        self.assertEqual([], A.get_htlcs_in_latest_ctx(LOCAL))\n        self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_latest_ctx(REMOTE))\n        self.assertEqual([], A.get_htlcs_in_next_ctx(LOCAL))\n        self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_next_ctx(REMOTE))\n        B.send_rev()\n        A.recv_rev()\n        self.assertEqual([], A.get_htlcs_in_latest_ctx(LOCAL))\n        self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_latest_ctx(REMOTE))\n        self.assertEqual([(Direction.SENT, ah0)], A.get_htlcs_in_next_ctx(LOCAL))\n        self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_next_ctx(REMOTE))\n        B.send_ctx()\n        A.recv_ctx()\n        self.assertEqual([], A.get_htlcs_in_oldest_unrevoked_ctx(LOCAL))\n        self.assertEqual([(Direction.SENT, ah0)], A.get_htlcs_in_latest_ctx(LOCAL))\n        self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_latest_ctx(REMOTE))\n        self.assertEqual([(Direction.SENT, ah0)], A.get_htlcs_in_next_ctx(LOCAL))\n        self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_next_ctx(REMOTE))\n        A.send_rev()\n        B.recv_rev()\n        self.assertEqual([(Direction.SENT, ah0)], A.get_htlcs_in_oldest_unrevoked_ctx(LOCAL))\n        self.assertEqual([(Direction.SENT, ah0)], A.get_htlcs_in_latest_ctx(LOCAL))\n        self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_latest_ctx(REMOTE))\n        self.assertEqual([(Direction.SENT, ah0)], A.get_htlcs_in_next_ctx(LOCAL))\n        self.assertEqual([(Direction.RECEIVED, ah0)], A.get_htlcs_in_next_ctx(REMOTE))\n\n    def test_unacked_local_updates(self):\n        A = HTLCManager(StoredDict({}, None))\n        B = HTLCManager(StoredDict({}, None))\n        A.channel_open_finished()\n        B.channel_open_finished()\n        self.assertEqual({}, A.get_unacked_local_updates())\n\n        ah0 = H('A', 0)\n        B.recv_htlc(A.send_htlc(ah0))\n        A.store_local_update_raw_msg(b\"upd_msg0\", is_commitment_signed=False)\n        self.assertEqual({1: [b\"upd_msg0\"]}, A.get_unacked_local_updates())\n\n        ah1 = H('A', 1)\n        B.recv_htlc(A.send_htlc(ah1))\n        A.store_local_update_raw_msg(b\"upd_msg1\", is_commitment_signed=False)\n        self.assertEqual({1: [b\"upd_msg0\", b\"upd_msg1\"]}, A.get_unacked_local_updates())\n\n        A.send_ctx()\n        B.recv_ctx()\n        A.store_local_update_raw_msg(b\"ctx1\", is_commitment_signed=True)\n        self.assertEqual({1: [b\"upd_msg0\", b\"upd_msg1\", b\"ctx1\"]}, A.get_unacked_local_updates())\n\n        ah2 = H('A', 2)\n        B.recv_htlc(A.send_htlc(ah2))\n        A.store_local_update_raw_msg(b\"upd_msg2\", is_commitment_signed=False)\n        self.assertEqual({1: [b\"upd_msg0\", b\"upd_msg1\", b\"ctx1\"], 2: [b\"upd_msg2\"]}, A.get_unacked_local_updates())\n\n        B.send_rev()\n        A.recv_rev()\n        self.assertEqual({2: [b\"upd_msg2\"]}, A.get_unacked_local_updates())\n"
  },
  {
    "path": "tests/test_lnmsg.py",
    "content": "import io\n\nfrom electrum.lnmsg import (read_bigsize_int, write_bigsize_int, FieldEncodingNotMinimal,\n                            UnexpectedEndOfStream, LNSerializer, UnknownMandatoryTLVRecordType,\n                            MalformedMsg, MsgTrailingGarbage, MsgInvalidFieldOrder, encode_msg,\n                            decode_msg, UnexpectedFieldSizeForEncoder, OnionWireSerializer,\n                            UnknownMsgType)\nfrom electrum.lnonion import OnionRoutingFailure\nfrom electrum.util import bfh\nfrom electrum.lnutil import ShortChannelID, LnFeatures\nfrom electrum.channel_db import NodeInfo\nfrom electrum import constants\n\nfrom . import ElectrumTestCase\n\n\nclass TestLNMsg(ElectrumTestCase):\n    TESTNET = True\n\n    def test_write_bigsize_int(self):\n        self.assertEqual(bfh(\"00\"), write_bigsize_int(0))\n        self.assertEqual(bfh(\"fc\"), write_bigsize_int(252))\n        self.assertEqual(bfh(\"fd00fd\"), write_bigsize_int(253))\n        self.assertEqual(bfh(\"fdffff\"), write_bigsize_int(65535))\n        self.assertEqual(bfh(\"fe00010000\"), write_bigsize_int(65536))\n        self.assertEqual(bfh(\"feffffffff\"), write_bigsize_int(4294967295))\n        self.assertEqual(bfh(\"ff0000000100000000\"), write_bigsize_int(4294967296))\n        self.assertEqual(bfh(\"ffffffffffffffffff\"), write_bigsize_int(18446744073709551615))\n\n    def test_read_bigsize_int(self):\n        self.assertEqual(0, read_bigsize_int(io.BytesIO(bfh(\"00\"))))\n        self.assertEqual(252, read_bigsize_int(io.BytesIO(bfh(\"fc\"))))\n        self.assertEqual(253, read_bigsize_int(io.BytesIO(bfh(\"fd00fd\"))))\n        self.assertEqual(65535, read_bigsize_int(io.BytesIO(bfh(\"fdffff\"))))\n        self.assertEqual(65536, read_bigsize_int(io.BytesIO(bfh(\"fe00010000\"))))\n        self.assertEqual(4294967295, read_bigsize_int(io.BytesIO(bfh(\"feffffffff\"))))\n        self.assertEqual(4294967296, read_bigsize_int(io.BytesIO(bfh(\"ff0000000100000000\"))))\n        self.assertEqual(18446744073709551615, read_bigsize_int(io.BytesIO(bfh(\"ffffffffffffffffff\"))))\n\n        with self.assertRaises(FieldEncodingNotMinimal):\n            read_bigsize_int(io.BytesIO(bfh(\"fd00fc\")))\n        with self.assertRaises(FieldEncodingNotMinimal):\n            read_bigsize_int(io.BytesIO(bfh(\"fe0000ffff\")))\n        with self.assertRaises(FieldEncodingNotMinimal):\n            read_bigsize_int(io.BytesIO(bfh(\"ff00000000ffffffff\")))\n        with self.assertRaises(UnexpectedEndOfStream):\n            read_bigsize_int(io.BytesIO(bfh(\"fd00\")))\n        with self.assertRaises(UnexpectedEndOfStream):\n            read_bigsize_int(io.BytesIO(bfh(\"feffff\")))\n        with self.assertRaises(UnexpectedEndOfStream):\n            read_bigsize_int(io.BytesIO(bfh(\"ffffffffff\")))\n        self.assertEqual(None, read_bigsize_int(io.BytesIO(bfh(\"\"))))\n        with self.assertRaises(UnexpectedEndOfStream):\n            read_bigsize_int(io.BytesIO(bfh(\"fd\")))\n        with self.assertRaises(UnexpectedEndOfStream):\n            read_bigsize_int(io.BytesIO(bfh(\"fe\")))\n        with self.assertRaises(UnexpectedEndOfStream):\n            read_bigsize_int(io.BytesIO(bfh(\"ff\")))\n\n    def test_read_tlv_stream_tests1(self):\n        # from https://github.com/lightningnetwork/lightning-rfc/blob/452a0eb916fedf4c954137b4fd0b61b5002b34ad/01-messaging.md#tlv-decoding-failures\n        lnser = LNSerializer()\n        for tlv_stream_name in (\"n1\", \"n2\"):\n            with self.subTest(tlv_stream_name=tlv_stream_name):\n                with self.assertRaises(UnexpectedEndOfStream):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd\")), tlv_stream_name=tlv_stream_name)\n                with self.assertRaises(UnexpectedEndOfStream):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd01\")), tlv_stream_name=tlv_stream_name)\n                with self.assertRaises(FieldEncodingNotMinimal):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd000100\")), tlv_stream_name=tlv_stream_name)\n                with self.assertRaises(UnexpectedEndOfStream):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd0101\")), tlv_stream_name=tlv_stream_name)\n                with self.assertRaises(UnexpectedEndOfStream):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0ffd\")), tlv_stream_name=tlv_stream_name)\n                with self.assertRaises(UnexpectedEndOfStream):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0ffd26\")), tlv_stream_name=tlv_stream_name)\n                with self.assertRaises(UnexpectedEndOfStream):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0ffd2602\")), tlv_stream_name=tlv_stream_name)\n                with self.assertRaises(FieldEncodingNotMinimal):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0ffd000100\")), tlv_stream_name=tlv_stream_name)\n                with self.assertRaises(UnexpectedEndOfStream):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0ffd0201000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\")), tlv_stream_name=\"n1\")\n                with self.assertRaises(UnknownMandatoryTLVRecordType):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"1200\")), tlv_stream_name=tlv_stream_name)\n                with self.assertRaises(UnknownMandatoryTLVRecordType):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd010200\")), tlv_stream_name=tlv_stream_name)\n                with self.assertRaises(UnknownMandatoryTLVRecordType):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fe0100000200\")), tlv_stream_name=tlv_stream_name)\n                with self.assertRaises(UnknownMandatoryTLVRecordType):\n                    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"ff010000000000000200\")), tlv_stream_name=tlv_stream_name)\n        with self.assertRaises(MsgTrailingGarbage):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0109ffffffffffffffffff\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(FieldEncodingNotMinimal):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"010100\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(FieldEncodingNotMinimal):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"01020001\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(FieldEncodingNotMinimal):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0103000100\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(FieldEncodingNotMinimal):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"010400010000\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(FieldEncodingNotMinimal):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"01050001000000\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(FieldEncodingNotMinimal):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0106000100000000\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(FieldEncodingNotMinimal):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"010700010000000000\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(FieldEncodingNotMinimal):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"01080001000000000000\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(UnexpectedEndOfStream):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"020701010101010101\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(MsgTrailingGarbage):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0209010101010101010101\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(UnexpectedEndOfStream):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0321023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(UnexpectedEndOfStream):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0329023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb0000000000000001\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(UnexpectedEndOfStream):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0330023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb000000000000000100000000000001\")), tlv_stream_name=\"n1\")\n        # check if ECC point is valid?... skip for now.\n        #with self.assertRaises(Exception):\n        #    lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0331043da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb00000000000000010000000000000002\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(MsgTrailingGarbage):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0332023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb0000000000000001000000000000000001\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(UnexpectedEndOfStream):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd00fe00\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(UnexpectedEndOfStream):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd00fe0101\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(MsgTrailingGarbage):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd00fe03010101\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(UnknownMandatoryTLVRecordType):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0000\")), tlv_stream_name=\"n1\")\n\n    def test_read_tlv_stream_tests2(self):\n        # from https://github.com/lightningnetwork/lightning-rfc/blob/452a0eb916fedf4c954137b4fd0b61b5002b34ad/01-messaging.md#tlv-decoding-successes\n        lnser = LNSerializer()\n        for tlv_stream_name in (\"n1\", \"n2\"):\n            with self.subTest(tlv_stream_name=tlv_stream_name):\n                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"\")), tlv_stream_name=tlv_stream_name))\n                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"2100\")), tlv_stream_name=tlv_stream_name))\n                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd020100\")), tlv_stream_name=tlv_stream_name))\n                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd00fd00\")), tlv_stream_name=tlv_stream_name))\n                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd00ff00\")), tlv_stream_name=tlv_stream_name))\n                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fe0200000100\")), tlv_stream_name=tlv_stream_name))\n                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"ff020000000000000100\")), tlv_stream_name=tlv_stream_name))\n\n        self.assertEqual({\"tlv1\": {\"amount_msat\": 0}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0100\")), tlv_stream_name=\"n1\"))\n        self.assertEqual({\"tlv1\": {\"amount_msat\": 1}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"010101\")), tlv_stream_name=\"n1\"))\n        self.assertEqual({\"tlv1\": {\"amount_msat\": 256}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"01020100\")), tlv_stream_name=\"n1\"))\n        self.assertEqual({\"tlv1\": {\"amount_msat\": 65536}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0103010000\")), tlv_stream_name=\"n1\"))\n        self.assertEqual({\"tlv1\": {\"amount_msat\": 16777216}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"010401000000\")), tlv_stream_name=\"n1\"))\n        self.assertEqual({\"tlv1\": {\"amount_msat\": 4294967296}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"01050100000000\")), tlv_stream_name=\"n1\"))\n        self.assertEqual({\"tlv1\": {\"amount_msat\": 1099511627776}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0106010000000000\")), tlv_stream_name=\"n1\"))\n        self.assertEqual({\"tlv1\": {\"amount_msat\": 281474976710656}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"010701000000000000\")), tlv_stream_name=\"n1\"))\n        self.assertEqual({\"tlv1\": {\"amount_msat\": 72057594037927936}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"01080100000000000000\")), tlv_stream_name=\"n1\"))\n        self.assertEqual({\"tlv2\": {\"scid\": ShortChannelID.from_components(0, 0, 550)}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"02080000000000000226\")), tlv_stream_name=\"n1\"))\n        self.assertEqual({\"tlv3\": {\"node_id\": bfh(\"023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb\"),\n                                   \"amount_msat_1\": 1,\n                                   \"amount_msat_2\": 2}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0331023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb00000000000000010000000000000002\")), tlv_stream_name=\"n1\"))\n        self.assertEqual({\"tlv4\": {\"cltv_delta\": 550}},\n                         lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"fd00fe020226\")), tlv_stream_name=\"n1\"))\n\n    def test_read_tlv_stream_tests3(self):\n        # from https://github.com/lightningnetwork/lightning-rfc/blob/452a0eb916fedf4c954137b4fd0b61b5002b34ad/01-messaging.md#tlv-stream-decoding-failure\n        lnser = LNSerializer()\n        with self.assertRaises(MsgInvalidFieldOrder):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0208000000000000022601012a\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(MsgInvalidFieldOrder):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"0208000000000000023102080000000000000451\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(MsgInvalidFieldOrder):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"1f000f012a\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(MsgInvalidFieldOrder):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"1f001f012a\")), tlv_stream_name=\"n1\")\n        with self.assertRaises(MsgInvalidFieldOrder):\n            lnser.read_tlv_stream(fd=io.BytesIO(bfh(\"ffffffffffffffffff000000\")), tlv_stream_name=\"n2\")\n\n    def test_encode_decode_msg__missing_mandatory_field_gets_set_to_zeroes(self):\n        # \"channel_update\": \"signature\" missing -> gets set to zeroes\n        self.assertEqual(bfh(\"01020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea33090000000000d43100006f00025e6ed0830100009000000000000000c8000001f400000023000000003b9aca00\"),\n                         encode_msg(\n                             \"channel_update\",\n                             short_channel_id=ShortChannelID.from_components(54321, 111, 2),\n                             channel_flags=b'\\x00',\n                             message_flags=b'\\x01',\n                             cltv_expiry_delta=144,\n                             htlc_minimum_msat=200,\n                             htlc_maximum_msat=1_000_000_000,\n                             fee_base_msat=500,\n                             fee_proportional_millionths=35,\n                             chain_hash=constants.net.rev_genesis_bytes(),\n                             timestamp=1584320643,\n                         ))\n        self.assertEqual(('channel_update',\n                         {'chain_hash': b'CI\\x7f\\xd7\\xf8&\\x95q\\x08\\xf4\\xa3\\x0f\\xd9\\xce\\xc3\\xae\\xbay\\x97 \\x84\\xe9\\x0e\\xad\\x01\\xea3\\t\\x00\\x00\\x00\\x00',\n                          'channel_flags': b'\\x00',\n                          'cltv_expiry_delta': 144,\n                          'fee_base_msat': 500,\n                          'fee_proportional_millionths': 35,\n                          'htlc_maximum_msat': 1000000000,\n                          'htlc_minimum_msat': 200,\n                          'message_flags': b'\\x01',\n                          'short_channel_id': b'\\x00\\xd41\\x00\\x00o\\x00\\x02',\n                          'signature': bytes(64),\n                          'timestamp': 1584320643}\n                          ),\n                         decode_msg(bfh(\"01020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea33090000000000d43100006f00025e6ed0830100009000000000000000c8000001f400000023000000003b9aca00\")))\n\n    def test_encode_decode_msg__ints_can_be_passed_as_bytes(self):\n        self.assertEqual(bfh(\"01020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea33090000000000d43100006f00025e6ed0830100009000000000000000c8000001f400000023000000003b9aca00\"),\n                         encode_msg(\n                             \"channel_update\",\n                             short_channel_id=ShortChannelID.from_components(54321, 111, 2),\n                             channel_flags=b'\\x00',\n                             message_flags=b'\\x01',\n                             cltv_expiry_delta=int.to_bytes(144, length=2, byteorder=\"big\", signed=False),\n                             htlc_minimum_msat=int.to_bytes(200, length=8, byteorder=\"big\", signed=False),\n                             htlc_maximum_msat=int.to_bytes(1_000_000_000, length=8, byteorder=\"big\", signed=False),\n                             fee_base_msat=int.to_bytes(500, length=4, byteorder=\"big\", signed=False),\n                             fee_proportional_millionths=int.to_bytes(35, length=4, byteorder=\"big\", signed=False),\n                             chain_hash=constants.net.rev_genesis_bytes(),\n                             timestamp=int.to_bytes(1584320643, length=4, byteorder=\"big\", signed=False),\n                         ))\n        self.assertEqual(('channel_update',\n                         {'chain_hash': b'CI\\x7f\\xd7\\xf8&\\x95q\\x08\\xf4\\xa3\\x0f\\xd9\\xce\\xc3\\xae\\xbay\\x97 \\x84\\xe9\\x0e\\xad\\x01\\xea3\\t\\x00\\x00\\x00\\x00',\n                          'channel_flags': b'\\x00',\n                          'cltv_expiry_delta': 144,\n                          'fee_base_msat': 500,\n                          'fee_proportional_millionths': 35,\n                          'htlc_maximum_msat': 1000000000,\n                          'htlc_minimum_msat': 200,\n                          'message_flags': b'\\x01',\n                          'short_channel_id': b'\\x00\\xd41\\x00\\x00o\\x00\\x02',\n                          'signature': bytes(64),\n                          'timestamp': 1584320643}\n                          ),\n                         decode_msg(bfh(\"01020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea33090000000000d43100006f00025e6ed0830100009000000000000000c8000001f400000023000000003b9aca00\")))\n        # \"htlc_minimum_msat\" is passed as bytes but with incorrect length\n        with self.assertRaises(UnexpectedFieldSizeForEncoder):\n            encode_msg(\n                \"channel_update\",\n                short_channel_id=ShortChannelID.from_components(54321, 111, 2),\n                channel_flags=b'\\x00',\n                message_flags=b'\\x01',\n                cltv_expiry_delta=int.to_bytes(144, length=2, byteorder=\"big\", signed=False),\n                htlc_minimum_msat=int.to_bytes(200, length=4, byteorder=\"big\", signed=False),\n                htlc_maximum_msat=int.to_bytes(1_000_000_000, length=8, byteorder=\"big\", signed=False),\n                fee_base_msat=int.to_bytes(500, length=4, byteorder=\"big\", signed=False),\n                fee_proportional_millionths=int.to_bytes(35, length=4, byteorder=\"big\", signed=False),\n                chain_hash=constants.net.rev_genesis_bytes(),\n                timestamp=int.to_bytes(1584320643, length=4, byteorder=\"big\", signed=False),\n            )\n\n    def test_encode_decode_msg__commitment_signed(self):\n        # \"commitment_signed\" is interesting because of the \"htlc_signature\" field,\n        #  which is a concatenation of multiple (\"num_htlcs\") signatures.\n        # 5 htlcs\n        self.assertEqual(bfh(\"0084010101010101010101010101010101010101010101010101010101010101010106112951d0a6d7fc1dbca3bd1cdbda9acfee7f668b3c0a36bd944f7e2f305b274ba46a61279e15163b2d376c664bb3481d7c5e107a5b268301e39aebbda27d2d00056548bd093a2bd2f4f053f0c6eb2c5f541d55eb8a2ede4d35fe974e5d3cd0eec3138bfd4115f4483c3b14e7988b48811d2da75f29f5e6eee691251fb4fba5a2610ba8fe7007117fe1c9fa1a6b01805c84cfffbb0eba674b64342c7cac567dea50728c1bb1aadc6d23fc2f4145027eafca82d6072cc9ce6529542099f728a0521e4b2044df5d02f7f2cdf84404762b1979528aa689a3e060a2a90ba8ef9a83d24d31ffb0d95c71d9fb9049b24ecf2c949c1486e7eb3ae160d70d54e441dc785dc57f7f3c9901b9537398c66f546cfc1d65e0748895d14699342c407fe119ac17db079b103720124a5ba22d4ba14c12832324dea9cb60c61ee74376ee7dcffdd1836e354aa8838ce3b37854fa91465cc40c73b702915e3580bfebaace805d52373b57ac755ebe4a8fe97e5fc21669bea124b809c79968479148f7174f39b8014542\"),\n                         encode_msg(\n                             \"commitment_signed\",\n                             channel_id=b'\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01',\n                             signature=b\"\\x06\\x11)Q\\xd0\\xa6\\xd7\\xfc\\x1d\\xbc\\xa3\\xbd\\x1c\\xdb\\xda\\x9a\\xcf\\xee\\x7ff\\x8b<\\n6\\xbd\\x94O~/0['K\\xa4ja'\\x9e\\x15\\x16;-7lfK\\xb3H\\x1d|^\\x10z[&\\x83\\x01\\xe3\\x9a\\xeb\\xbd\\xa2}-\",\n                             num_htlcs=5,\n                             htlc_signature=bfh(\"6548bd093a2bd2f4f053f0c6eb2c5f541d55eb8a2ede4d35fe974e5d3cd0eec3138bfd4115f4483c3b14e7988b48811d2da75f29f5e6eee691251fb4fba5a2610ba8fe7007117fe1c9fa1a6b01805c84cfffbb0eba674b64342c7cac567dea50728c1bb1aadc6d23fc2f4145027eafca82d6072cc9ce6529542099f728a0521e4b2044df5d02f7f2cdf84404762b1979528aa689a3e060a2a90ba8ef9a83d24d31ffb0d95c71d9fb9049b24ecf2c949c1486e7eb3ae160d70d54e441dc785dc57f7f3c9901b9537398c66f546cfc1d65e0748895d14699342c407fe119ac17db079b103720124a5ba22d4ba14c12832324dea9cb60c61ee74376ee7dcffdd1836e354aa8838ce3b37854fa91465cc40c73b702915e3580bfebaace805d52373b57ac755ebe4a8fe97e5fc21669bea124b809c79968479148f7174f39b8014542\"),\n                         ))\n        self.assertEqual(('commitment_signed',\n                         {'channel_id': b'\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01',\n                          'signature': b\"\\x06\\x11)Q\\xd0\\xa6\\xd7\\xfc\\x1d\\xbc\\xa3\\xbd\\x1c\\xdb\\xda\\x9a\\xcf\\xee\\x7ff\\x8b<\\n6\\xbd\\x94O~/0['K\\xa4ja'\\x9e\\x15\\x16;-7lfK\\xb3H\\x1d|^\\x10z[&\\x83\\x01\\xe3\\x9a\\xeb\\xbd\\xa2}-\",\n                          'num_htlcs': 5,\n                          'htlc_signature': bfh(\"6548bd093a2bd2f4f053f0c6eb2c5f541d55eb8a2ede4d35fe974e5d3cd0eec3138bfd4115f4483c3b14e7988b48811d2da75f29f5e6eee691251fb4fba5a2610ba8fe7007117fe1c9fa1a6b01805c84cfffbb0eba674b64342c7cac567dea50728c1bb1aadc6d23fc2f4145027eafca82d6072cc9ce6529542099f728a0521e4b2044df5d02f7f2cdf84404762b1979528aa689a3e060a2a90ba8ef9a83d24d31ffb0d95c71d9fb9049b24ecf2c949c1486e7eb3ae160d70d54e441dc785dc57f7f3c9901b9537398c66f546cfc1d65e0748895d14699342c407fe119ac17db079b103720124a5ba22d4ba14c12832324dea9cb60c61ee74376ee7dcffdd1836e354aa8838ce3b37854fa91465cc40c73b702915e3580bfebaace805d52373b57ac755ebe4a8fe97e5fc21669bea124b809c79968479148f7174f39b8014542\")}\n                          ),\n                         decode_msg(bfh(\"0084010101010101010101010101010101010101010101010101010101010101010106112951d0a6d7fc1dbca3bd1cdbda9acfee7f668b3c0a36bd944f7e2f305b274ba46a61279e15163b2d376c664bb3481d7c5e107a5b268301e39aebbda27d2d00056548bd093a2bd2f4f053f0c6eb2c5f541d55eb8a2ede4d35fe974e5d3cd0eec3138bfd4115f4483c3b14e7988b48811d2da75f29f5e6eee691251fb4fba5a2610ba8fe7007117fe1c9fa1a6b01805c84cfffbb0eba674b64342c7cac567dea50728c1bb1aadc6d23fc2f4145027eafca82d6072cc9ce6529542099f728a0521e4b2044df5d02f7f2cdf84404762b1979528aa689a3e060a2a90ba8ef9a83d24d31ffb0d95c71d9fb9049b24ecf2c949c1486e7eb3ae160d70d54e441dc785dc57f7f3c9901b9537398c66f546cfc1d65e0748895d14699342c407fe119ac17db079b103720124a5ba22d4ba14c12832324dea9cb60c61ee74376ee7dcffdd1836e354aa8838ce3b37854fa91465cc40c73b702915e3580bfebaace805d52373b57ac755ebe4a8fe97e5fc21669bea124b809c79968479148f7174f39b8014542\")))\n        # single htlc\n        self.assertEqual(bfh(\"008401010101010101010101010101010101010101010101010101010101010101013b14af0c549dfb1fb287ff57c012371b3932996db5929eda5f251704751fb49d0dc2dcb88e5021575cb572fb71693758543f97d89e9165f913bfb7488d7cc26500012d31103b9f6e71131e4fee86fdfbdeba90e52b43fcfd11e8e53811cd4d59b2575ae6c3c82f85bea144c88cc35e568f1e6bdd0c57337e86de0b5da7cd9994067a\"),\n                         encode_msg(\n                             \"commitment_signed\",\n                             channel_id=b'\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01',\n                             signature=b';\\x14\\xaf\\x0cT\\x9d\\xfb\\x1f\\xb2\\x87\\xffW\\xc0\\x127\\x1b92\\x99m\\xb5\\x92\\x9e\\xda_%\\x17\\x04u\\x1f\\xb4\\x9d\\r\\xc2\\xdc\\xb8\\x8eP!W\\\\\\xb5r\\xfbqi7XT?\\x97\\xd8\\x9e\\x91e\\xf9\\x13\\xbf\\xb7H\\x8d|\\xc2e',\n                             num_htlcs=1,\n                             htlc_signature=bfh(\"2d31103b9f6e71131e4fee86fdfbdeba90e52b43fcfd11e8e53811cd4d59b2575ae6c3c82f85bea144c88cc35e568f1e6bdd0c57337e86de0b5da7cd9994067a\"),\n                         ))\n        self.assertEqual(('commitment_signed',\n                         {'channel_id': b'\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01',\n                          'signature': b';\\x14\\xaf\\x0cT\\x9d\\xfb\\x1f\\xb2\\x87\\xffW\\xc0\\x127\\x1b92\\x99m\\xb5\\x92\\x9e\\xda_%\\x17\\x04u\\x1f\\xb4\\x9d\\r\\xc2\\xdc\\xb8\\x8eP!W\\\\\\xb5r\\xfbqi7XT?\\x97\\xd8\\x9e\\x91e\\xf9\\x13\\xbf\\xb7H\\x8d|\\xc2e',\n                          'num_htlcs': 1,\n                          'htlc_signature': bfh(\"2d31103b9f6e71131e4fee86fdfbdeba90e52b43fcfd11e8e53811cd4d59b2575ae6c3c82f85bea144c88cc35e568f1e6bdd0c57337e86de0b5da7cd9994067a\")}\n                          ),\n                         decode_msg(bfh(\"008401010101010101010101010101010101010101010101010101010101010101013b14af0c549dfb1fb287ff57c012371b3932996db5929eda5f251704751fb49d0dc2dcb88e5021575cb572fb71693758543f97d89e9165f913bfb7488d7cc26500012d31103b9f6e71131e4fee86fdfbdeba90e52b43fcfd11e8e53811cd4d59b2575ae6c3c82f85bea144c88cc35e568f1e6bdd0c57337e86de0b5da7cd9994067a\")))\n        # zero htlcs\n        self.assertEqual(bfh(\"008401010101010101010101010101010101010101010101010101010101010101014e206ecf904d9237b1c5b4e08513555e9a5932c45b5f68be8764ce998df635ae04f6ce7bbcd3b4fd08e2daab7f9059b287ecab4155367b834682633497173f450000\"),\n                         encode_msg(\n                             \"commitment_signed\",\n                             channel_id=b'\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01',\n                             signature=b'N n\\xcf\\x90M\\x927\\xb1\\xc5\\xb4\\xe0\\x85\\x13U^\\x9aY2\\xc4[_h\\xbe\\x87d\\xce\\x99\\x8d\\xf65\\xae\\x04\\xf6\\xce{\\xbc\\xd3\\xb4\\xfd\\x08\\xe2\\xda\\xab\\x7f\\x90Y\\xb2\\x87\\xec\\xabAU6{\\x83F\\x82c4\\x97\\x17?E',\n                             num_htlcs=0,\n                             htlc_signature=bfh(\"\"),\n                         ))\n        self.assertEqual(('commitment_signed',\n                         {'channel_id': b'\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01',\n                          'signature': b'N n\\xcf\\x90M\\x927\\xb1\\xc5\\xb4\\xe0\\x85\\x13U^\\x9aY2\\xc4[_h\\xbe\\x87d\\xce\\x99\\x8d\\xf65\\xae\\x04\\xf6\\xce{\\xbc\\xd3\\xb4\\xfd\\x08\\xe2\\xda\\xab\\x7f\\x90Y\\xb2\\x87\\xec\\xabAU6{\\x83F\\x82c4\\x97\\x17?E',\n                          'num_htlcs': 0,\n                          'htlc_signature': bfh(\"\")}\n                          ),\n                         decode_msg(bfh(\"008401010101010101010101010101010101010101010101010101010101010101014e206ecf904d9237b1c5b4e08513555e9a5932c45b5f68be8764ce998df635ae04f6ce7bbcd3b4fd08e2daab7f9059b287ecab4155367b834682633497173f450000\")))\n\n    def test_encode_decode_msg__init(self):\n        # \"init\" is interesting because it has TLVs optionally\n        self.assertEqual(bfh(\"00100000000220c2\"),\n                         encode_msg(\n                             \"init\",\n                             gflen=0,\n                             flen=2,\n                             features=(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT |\n                                       LnFeatures.GOSSIP_QUERIES_OPT |\n                                       LnFeatures.GOSSIP_QUERIES_REQ |\n                                       LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT),\n                         ))\n        self.assertEqual(bfh(\"00100000000220c2\"),\n                         encode_msg(\"init\", gflen=0, flen=2, features=bfh(\"20c2\")))\n        self.assertEqual(bfh(\"00100000000220c2012043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000\"),\n                         encode_msg(\n                             \"init\",\n                             gflen=0,\n                             flen=2,\n                             features=(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT |\n                                       LnFeatures.GOSSIP_QUERIES_OPT |\n                                       LnFeatures.GOSSIP_QUERIES_REQ |\n                                       LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT),\n                             init_tlvs={\n                                 'networks':\n                                     {'chains': b'CI\\x7f\\xd7\\xf8&\\x95q\\x08\\xf4\\xa3\\x0f\\xd9\\xce\\xc3\\xae\\xbay\\x97 \\x84\\xe9\\x0e\\xad\\x01\\xea3\\t\\x00\\x00\\x00\\x00'}\n                             }\n                         ))\n        self.assertEqual(('init',\n                         {'gflen': 2,\n                          'globalfeatures': b'\"\\x00',\n                          'flen': 3,\n                          'features': b'\\x02\\xa2\\xa1',\n                          'init_tlvs': {}}\n                          ),\n                         decode_msg(bfh(\"001000022200000302a2a1\")))\n        self.assertEqual(('init',\n                         {'gflen': 2,\n                          'globalfeatures': b'\"\\x00',\n                          'flen': 3,\n                          'features': b'\\x02\\xaa\\xa2',\n                          'init_tlvs': {\n                              'networks':\n                                  {'chains': b'CI\\x7f\\xd7\\xf8&\\x95q\\x08\\xf4\\xa3\\x0f\\xd9\\xce\\xc3\\xae\\xbay\\x97 \\x84\\xe9\\x0e\\xad\\x01\\xea3\\t\\x00\\x00\\x00\\x00'}\n                          }}),\n                         decode_msg(bfh(\"001000022200000302aaa2012043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000\")))\n\n    def test_decode_onion_error(self):\n        orf = OnionRoutingFailure.from_bytes(bfh(\"400f0000000017d2d8b0001d9458\"))\n        self.assertEqual(('incorrect_or_unknown_payment_details', {'htlc_msat': 399694000, 'height': 1938520}),\n                         OnionWireSerializer.decode_msg(orf.to_bytes()))\n        self.assertEqual({'htlc_msat': 399694000, 'height': 1938520},\n                         orf.decode_data())\n\n        orf2 = OnionRoutingFailure(26399, bytes.fromhex(\"0000000017d2d8b0001d9458\"))\n        with self.assertRaises(UnknownMsgType):\n            OnionWireSerializer.decode_msg(orf2.to_bytes())\n        self.assertEqual(None, orf2.decode_data())\n\n    def test_address_parsing_and_serialization(self):\n        \"\"\"Tests the NodeInfo bolt7 node_announcement addresses field serialization and parsing\"\"\"\n        taf = NodeInfo.to_addresses_field\n        paf = NodeInfo.parse_addresses_field\n\n        # -- INVALID INPUTS --\n        invalid_inputs_parsing = (\n            b'', # empty input\n            b'\\x06\\x00', # address type 6 (\\x06) is not specified\n        )\n        invalid_inputs_serialization = (\n            (\"::1\", 9735),  # local ipv6\n            (\"::\", 9735),  # local ipv6\n            (\"::1\", 0),  # local host, invalid port\n            (\"::1\", 65536),  # local host, invalid port\n            (\"127.0.0.1\", 9735),  # local ipv4\n            (\"localhost\", 9735),  # local host\n            (\"domain.com\", 0),  # domain, invalid port\n            (\"domain.com\", 65536),  # domain, invalid port\n            (\"domain.com\", -1),  # domain, invalid port\n            (\"expyuzz4wqqyqhjn.onion\", 9735),  # onion v2, not supported\n            (\"\", 9735),  # empty address\n        )\n        for invalid_input in invalid_inputs_parsing:\n            self.assertEqual(paf(invalid_input), [])\n        for host, port in invalid_inputs_serialization:\n            self.assertEqual(taf(host, port), b'')\n\n        # -- VALID INPUTS --\n        valid_inputs = (\n            (\"34.138.100.228\", 9735),  # ipv4\n            (\"2001:41d0:0001:b40d:0000:0000:0000:0001\", 9735),  # ipv6\n            (\"2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion\", 9735), # onion v3\n            (\"ecb.europa.eu\", 8624),  # domain\n        )\n        valid_inputs_with_defined_output = [\n            [[\"2001:41d0:1:b40d::1\", 9735], [(\"2001:41d0:0001:b40d:0000:0000:0000:0001\", 9735)]]  # ipv6\n        ]\n        for host, port in valid_inputs:\n            self.assertEqual(paf(taf(host, port)), [(host, port)])\n        for input_, output in valid_inputs_with_defined_output:\n            self.assertEqual(paf(taf(*input_)), output)\n"
  },
  {
    "path": "tests/test_lnpeer.py",
    "content": "import asyncio\nimport dataclasses\nimport shutil\nimport copy\nimport tempfile\nfrom decimal import Decimal\nimport os\nfrom contextlib import contextmanager\nfrom collections import defaultdict\nimport logging\nimport concurrent\nfrom concurrent import futures\nfrom functools import lru_cache\nfrom unittest import mock\nfrom typing import Iterable, NamedTuple, Tuple, List, Dict, Sequence, Mapping\nfrom types import MappingProxyType\nimport time\nimport statistics\n\nfrom aiorpcx import timeout_after, TaskTimeout\nfrom electrum_ecc import ECPrivkey\n\nimport electrum\nimport electrum.trampoline\nfrom electrum import bitcoin\nfrom electrum import util\nfrom electrum import constants\nfrom electrum import bip32\nfrom electrum.network import Network, ProxySettings\nfrom electrum import simple_config, lnutil\nfrom electrum.lnaddr import lnencode, LnAddr, lndecode\nfrom electrum.bitcoin import COIN, sha256\nfrom electrum.transaction import Transaction\nfrom electrum.util import NetworkRetryManager, bfh, OldTaskGroup, EventListener, InvoiceError\nfrom electrum.lnpeer import Peer\nfrom electrum.lntransport import LNPeerAddr\nfrom electrum.crypto import privkey_to_pubkey\nfrom electrum.lnutil import Keypair, PaymentFailure, LnFeatures, HTLCOwner, PaymentFeeBudget, RECEIVED\nfrom electrum.lnchannel import ChannelState, PeerState, Channel\nfrom electrum.lnrouter import LNPathFinder, PathEdge, LNPathInconsistent\nfrom electrum.channel_db import ChannelDB, InvalidGossipMsg\nfrom electrum.lnworker import LNWallet, NoPathFound, SentHtlcInfo, PaySession, LNPeerManager\nfrom electrum.lnmsg import encode_msg, decode_msg\nfrom electrum import lnmsg\nfrom electrum.logging import console_stderr_handler, Logger\nfrom electrum.lnworker import PaymentInfo\nfrom electrum.lnonion import OnionFailureCode, OnionRoutingFailure, OnionHopsDataSingle, OnionPacket\nfrom electrum.lnutil import LOCAL, REMOTE, UpdateAddHtlc, RecvMPPResolution\nfrom electrum.invoices import PR_PAID, PR_UNPAID, Invoice, LN_EXPIRY_NEVER\nfrom electrum.interface import GracefulDisconnect\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.fee_policy import FeeTimeEstimates, FEE_ETA_TARGETS\nfrom electrum.mpp_split import split_amount_normal\nfrom electrum.wallet import Abstract_Wallet, Standard_Wallet\n\nfrom .test_lnchannel import create_test_channels\nfrom .test_bitcoin import needs_test_with_all_chacha20_implementations\nfrom . import ElectrumTestCase, restore_wallet_from_text__for_unittest\n\n\nclass MockNetwork:\n    def __init__(self, *, config: SimpleConfig):\n        self.lnwatcher = None\n        self.interface = None\n        self.fee_estimates = FeeTimeEstimates()\n        self.populate_fee_estimates()\n        self.config = config\n        self.asyncio_loop = util.get_asyncio_loop()\n        self.channel_db = ChannelDB(self)\n        self.channel_db.data_loaded.set()\n        self.path_finder = LNPathFinder(self.channel_db)\n        self.lngossip = MockLNGossip()\n        self.tx_queue = asyncio.Queue()\n        self.proxy = ProxySettings()\n        self._blockchain = MockBlockchain()\n\n    def get_local_height(self):\n        return self.blockchain().height()\n\n    def blockchain(self):\n        return self._blockchain\n\n    async def broadcast_transaction(self, tx):\n        await self.tx_queue.put(tx)\n\n    async def try_broadcasting(self, tx, name):\n        await self.broadcast_transaction(tx)\n\n    def populate_fee_estimates(self):\n        for target in FEE_ETA_TARGETS[:-1]:\n            self.fee_estimates.set_data(target, 50000 // target)\n\n\nclass MockBlockchain:\n    def __init__(self):\n        # Let's return a non-zero, realistic height.\n        # 0 might hide relative vs abs locktime confusion bugs.\n        self._height = 600_000\n\n    def height(self):\n       return self._height\n\n    def is_tip_stale(self):\n        return False\n\n\nclass MockLNGossip:\n    def get_sync_progress_estimate(self):\n        return None, None, None\n\n\nclass MockWalletFactory(electrum.wallet.Wallet):\n\n    @staticmethod\n    def wallet_class(wallet_type):\n        real_wallet_class = electrum.wallet.Wallet.wallet_class(wallet_type)\n        if real_wallet_class is Standard_Wallet:\n            return MockStandardWallet\n        return real_wallet_class\n\n\nclass MockStandardWallet(Standard_Wallet):\n    def _init_lnworker(self):\n        ln_xprv = self.db.get('lightning_xprv') or self.db.get('lightning_privkey2')\n        assert ln_xprv\n        self.lnworker = MockLNWallet(self, ln_xprv)\n\n    def basename(self):\n        passphrase = self.db.get(\"keystore\").get(\"passphrase\")\n        assert passphrase\n        return passphrase  # lol, super secure name\n\ndef _create_mock_lnwallet(*, name, has_anchors, data_dir: str) -> 'MockLNWallet':\n    config = SimpleConfig({}, read_user_dir_function=lambda: data_dir)\n    config.ENABLE_ANCHOR_CHANNELS = has_anchors\n    config.INITIAL_TRAMPOLINE_FEE_LEVEL = 0\n\n    network = MockNetwork(config=config)\n\n    wallet = restore_wallet_from_text__for_unittest(\n        \"9dk\", path=None, passphrase=name, config=config,\n        wallet_factory=MockWalletFactory,\n    )['wallet']  # type: MockStandardWallet\n    wallet.is_up_to_date = lambda: True\n    wallet.adb.network = wallet.network = network\n\n    lnworker = wallet.lnworker\n    assert isinstance(lnworker, MockLNWallet), f\"{lnworker=!r}\"\n    lnworker.lnpeermgr.network = network\n    lnworker.logger.info(f\"created LNWallet[{name}] with nodeID={lnworker.node_keypair.pubkey.hex()}\")\n    return lnworker\n\nclass MockLNWallet(LNWallet):\n    MPP_EXPIRY = 2  # HTLC timestamps are cast to int, so this cannot be 1\n    TIMEOUT_SHUTDOWN_FAIL_PENDING_HTLCS = 0\n    MPP_SPLIT_PART_FRACTION = 1  # this disables the forced splitting\n\n    def __init__(self, *args, **kwargs):\n        LNWallet.__init__(self, *args, **kwargs)\n        self.features &= ~LnFeatures.BASIC_MPP_OPT  # by default, disable MPP\n\n    def _add_channel(self, chan: Channel):\n        self._channels[chan.channel_id] = chan\n        # assert chan.lnworker == self  # this fails as some tests are reusing chans in a weird way\n        chan.lnworker = self\n\n    @LNWallet.features.setter\n    def features(self, value):\n        self.lnpeermgr.features = value\n\n    @property\n    def name(self):\n        return self.wallet.basename()\n\n    async def stop(self):\n        await LNWallet.stop(self)\n        if self.channel_db:\n            self.channel_db.stop()\n            await self.channel_db.stopped_event.wait()\n\n    async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: LnAddr, *, full_path=None):\n        paysession = PaySession(\n            payment_hash=decoded_invoice.paymenthash,\n            payment_secret=decoded_invoice.payment_secret,\n            initial_trampoline_fee_level=0,\n            invoice_features=decoded_invoice.get_features(),\n            r_tags=decoded_invoice.get_routing_info('r'),\n            min_final_cltv_delta=decoded_invoice.get_min_final_cltv_delta(),\n            amount_to_pay=amount_msat,\n            invoice_pubkey=decoded_invoice.pubkey.serialize(),\n            uses_trampoline=False,\n            use_two_trampolines=False,\n        )\n        payment_key = decoded_invoice.paymenthash + decoded_invoice.payment_secret\n        self._paysessions[payment_key] = paysession\n        return [r async for r in self.create_routes_for_payment(\n            amount_msat=amount_msat,\n            paysession=paysession,\n            full_path=full_path,\n            budget=PaymentFeeBudget.from_invoice_amount(invoice_amount_msat=amount_msat, config=self.config),\n        )]\n\n\nclass MockTransport:\n    def __init__(self, name):\n        self.queue = asyncio.Queue()  # incoming messages\n        self._name = name\n        self.peer_addr = None\n\n    def name(self):\n        return self._name\n\n    async def read_messages(self):\n        while True:\n            data = await self.queue.get()\n            if isinstance(data, asyncio.Event):  # to artificially delay messages\n                await data.wait()\n                continue\n            yield data\n\nclass NoFeaturesTransport(MockTransport):\n    \"\"\"\n    This answers the init message with a init that doesn't signal any features.\n    Used for testing that we require DATA_LOSS_PROTECT.\n    \"\"\"\n    def send_bytes(self, data):\n        decoded = decode_msg(data)\n        print(decoded)\n        if decoded[0] == 'init':\n            self.queue.put_nowait(encode_msg('init', lflen=1, gflen=1, localfeatures=b\"\\x00\", globalfeatures=b\"\\x00\"))\n\nclass PutIntoOthersQueueTransport(MockTransport):\n    def __init__(self, keypair, name):\n        super().__init__(name)\n        self.other_mock_transport = None\n        self.privkey = keypair.privkey\n\n    def send_bytes(self, data):\n        self.other_mock_transport.queue.put_nowait(data)\n\ndef transport_pair(k1, k2, name1, name2):\n    t1 = PutIntoOthersQueueTransport(k1, name1)\n    t2 = PutIntoOthersQueueTransport(k2, name2)\n    t1.other_mock_transport = t2\n    t2.other_mock_transport = t1\n    return t1, t2\n\n\nclass PeerInTests(Peer):\n    DELAY_INC_MSG_PROCESSING_SLEEP = 0  # disable rate-limiting\n\n\nhigh_fee_channel = {\n   'local_balance_msat': 10 * bitcoin.COIN * 1000 // 2,\n   'remote_balance_msat': 10 * bitcoin.COIN * 1000 // 2,\n   'local_base_fee_msat': 500_000,\n   'local_fee_rate_millionths': 500,\n   'remote_base_fee_msat': 500_000,\n   'remote_fee_rate_millionths': 500,\n}\n\nlow_fee_channel = {\n    'local_balance_msat': 10 * bitcoin.COIN * 1000 // 2,\n    'remote_balance_msat': 10 * bitcoin.COIN * 1000 // 2,\n    'local_base_fee_msat': 1_000,\n    'local_fee_rate_millionths': 1,\n    'remote_base_fee_msat': 1_000,\n    'remote_fee_rate_millionths': 1,\n}\n\ndepleted_channel = {\n    'local_balance_msat': 330 * 1000, # local pays anchors\n    'remote_balance_msat': 10 * bitcoin.COIN * 1000,\n    'local_base_fee_msat': 1_000,\n    'local_fee_rate_millionths': 1,\n    'remote_base_fee_msat': 1_000,\n    'remote_fee_rate_millionths': 1,\n}\n\n_GRAPH_DEFINITIONS = {\n    # A -- B\n    'single_chan' : {\n        'alice': {\n            'channels': {\n                'bob': {\n                   'local_balance_msat': 10 * bitcoin.COIN * 1000 // 2,\n                   'remote_balance_msat': 10 * bitcoin.COIN * 1000 // 2,\n                },\n            },\n        },\n        'bob': {\n        },\n    },\n    #                A\n    #     high fee /   \\ low fee\n    #             B     C\n    #     high fee \\   / low fee\n    #                D\n    'square_graph': {\n        'alice': {\n            'channels': {\n                # we should use copies of channel definitions if\n                # we want to independently alter them in a test\n                'bob': high_fee_channel.copy(),\n                'carol': low_fee_channel.copy(),\n            },\n        },\n        'bob': {\n            'channels': {\n                'dave': high_fee_channel.copy(),\n            },\n            'config': {\n                SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS: True,\n                SimpleConfig.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS: True,\n            },\n        },\n        'carol': {\n            'channels': {\n                'dave': low_fee_channel.copy(),\n            },\n            'config': {\n                SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS: True,\n                SimpleConfig.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS: True,\n            },\n        },\n        'dave': {\n        },\n    },\n    # A -- B -- C -- D -- E\n    'line_graph': {\n        'alice': {\n            'channels': {\n                'bob': low_fee_channel.copy(),\n            },\n        },\n        'bob': {  # Trampoline Forwarder\n            'channels': {\n                'carol': low_fee_channel.copy(),\n            },\n            'config': {\n                SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS: True,\n                SimpleConfig.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS: True,\n            },\n        },\n        'carol': {\n            'channels': {\n                'dave': low_fee_channel.copy(),\n            },\n            'config': {\n                SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS: True,\n            },\n        },\n        'dave': {  # Trampoline Forwarder\n            'channels': {\n                'edward': low_fee_channel.copy(),\n            },\n            'config': {\n                SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS: True,\n                SimpleConfig.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS: True,\n            },\n        },\n        'edward': {\n        },\n    },\n}\n\n\nclass Graph(NamedTuple):\n    workers: Dict[str, MockLNWallet]\n    peers: Dict[Tuple[str, str], Peer]\n    channels: Dict[Tuple[str, str], Channel]\n\n\nclass PaymentDone(Exception): pass\nclass PaymentTimeout(Exception): pass\nclass SuccessfulTest(Exception): pass\n\n\ndef inject_chan_into_gossipdb(*, channel_db: ChannelDB, graph: Graph, node1name: str, node2name: str) -> None:\n    chan_ann_raw = graph.channels[(node1name, node2name)].construct_channel_announcement_without_sigs()[0]\n    chan_ann_dict = decode_msg(chan_ann_raw)[1]\n    channel_db.add_channel_announcements(chan_ann_dict, trusted=True)\n\n    chan_upd1_raw = graph.channels[(node1name, node2name)].get_outgoing_gossip_channel_update()\n    chan_upd1_dict = decode_msg(chan_upd1_raw)[1]\n    channel_db.add_channel_update(chan_upd1_dict, verify=False)\n\n    chan_upd2_raw = graph.channels[(node2name, node1name)].get_outgoing_gossip_channel_update()\n    chan_upd2_dict = decode_msg(chan_upd2_raw)[1]\n    channel_db.add_channel_update(chan_upd2_dict, verify=False)\n\n\nclass TestPeer(ElectrumTestCase):\n    TESTNET = True\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        console_stderr_handler.setLevel(logging.DEBUG)\n\n    def setUp(self):\n        super().setUp()\n        self.GRAPH_DEFINITIONS = copy.deepcopy(_GRAPH_DEFINITIONS)\n\n    async def asyncTearDown(self):\n        electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {}\n        await super().asyncTearDown()\n\n    @staticmethod\n    def prepare_invoice(\n            w2: MockLNWallet,  # receiver\n            *,\n            amount_msat=100_000_000,\n            include_routing_hints=False,\n            payment_preimage: bytes = None,\n            payment_hash: bytes = None,\n            invoice_features: LnFeatures = None,\n            min_final_cltv_delta: int = None,\n            expiry: int = None,\n    ) -> Tuple[LnAddr, Invoice]:\n        amount_btc = amount_msat/Decimal(COIN*1000)\n        if payment_preimage is None and not payment_hash:\n            payment_preimage = os.urandom(32)\n        if payment_hash is None:\n            payment_hash = sha256(payment_preimage)\n        if payment_preimage:\n            w2.save_preimage(payment_hash, payment_preimage)\n        if include_routing_hints:\n            routing_hints = w2.calc_routing_hints_for_invoice(amount_msat)\n        else:\n            routing_hints = []\n            trampoline_hints = []\n        if invoice_features is None:\n            invoice_features = w2.features.for_invoice()\n        if invoice_features.supports(LnFeatures.PAYMENT_SECRET_OPT):\n            payment_secret = w2.get_payment_secret(payment_hash)\n        else:\n            payment_secret = None\n        if min_final_cltv_delta is None:\n            min_final_cltv_delta = lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED\n        info = PaymentInfo(\n            payment_hash=payment_hash,\n            amount_msat=amount_msat,\n            direction=RECEIVED,\n            status=PR_UNPAID,\n            min_final_cltv_delta=min_final_cltv_delta,\n            expiry_delay=expiry or LN_EXPIRY_NEVER,\n            invoice_features=invoice_features,\n        )\n        w2.save_payment_info(info)\n        lnaddr1 = LnAddr(\n            paymenthash=payment_hash,\n            amount=amount_btc,\n            tags=[\n                ('c', min_final_cltv_delta),\n                ('d', 'coffee'),\n                ('9', invoice_features),\n                ('x', expiry or 3600),\n            ] + routing_hints,\n            payment_secret=payment_secret,\n        )\n        invoice = lnencode(lnaddr1, w2.node_keypair.privkey)\n        lnaddr2 = lndecode(invoice)  # unlike lnaddr1, this now has a pubkey set\n        return lnaddr2, Invoice.from_bech32(invoice)\n\n    async def _activate_trampoline(self, w: MockLNWallet):\n        if w.network.channel_db:\n            w.network.channel_db.stop()\n            await w.network.channel_db.stopped_event.wait()\n            w.network.channel_db = None\n\n    def prepare_recipient(self, w2, payment_hash, test_hold_invoice, test_failure):\n        if not test_hold_invoice and not test_failure:\n            return\n        preimage_hex, is_public = w2._preimages.pop(payment_hash.hex())\n        preimage = bytes.fromhex(preimage_hex)\n        if test_hold_invoice:\n            async def cb(payment_hash):\n                if not test_failure:\n                    w2.save_preimage(payment_hash, preimage, mark_as_public=is_public)\n                else:\n                    raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')\n            w2.register_hold_invoice(payment_hash, cb)\n\n    def prepare_lnwallets(self, graph_definition) -> Mapping[str, MockLNWallet]:\n        workers = {}  # type: Dict[str, MockLNWallet]\n        for a, definition in graph_definition.items():\n            workers[a] = self.create_mock_lnwallet(name=a, has_anchors=self.TEST_ANCHOR_CHANNELS)\n        return workers\n\n    def prepare_chans_and_peers_in_graph(\n        self,\n        graph_definition,\n        *,\n        workers: Dict[str, MockLNWallet] = None,\n        channels: Mapping[Tuple[str, str], Channel] = None,\n    ) -> Graph:\n        # create workers\n        if workers is None:\n            workers = self.prepare_lnwallets(graph_definition=graph_definition)\n        keys = {name: w.node_keypair for name, w in workers.items()}\n\n        if channels is None:\n            channels = {}  # type: Dict[Tuple[str, str], Channel]\n        transports = {}\n        peers = {}\n\n        # create channels\n        for a, definition in graph_definition.items():\n            for b, channel_def in definition.get('channels', {}).items():\n                if ((a, b) in channels) or ((b, a) in channels):\n                    # if either chan direction is present, both must be present\n                    channel_ab = channels[(a, b)]\n                    channel_ba = channels[(b, a)]\n                else:  # create new chans now\n                    channel_ab, channel_ba = create_test_channels(\n                        alice_lnwallet=workers[a],\n                        bob_lnwallet=workers[b],\n                        local_msat=channel_def['local_balance_msat'],\n                        remote_msat=channel_def['remote_balance_msat'],\n                        anchor_outputs=self.TEST_ANCHOR_CHANNELS\n                    )\n                    channels[(a, b)], channels[(b, a)] = channel_ab, channel_ba\n                workers[a]._add_channel(channel_ab)\n                workers[b]._add_channel(channel_ba)\n                transport_ab, transport_ba = transport_pair(keys[a], keys[b], channel_ab.name, channel_ba.name)\n                transports[(a, b)], transports[(b, a)] = transport_ab, transport_ba\n                # set fees\n                if 'local_fee_rate_millionths' in channel_def:\n                    channel_ab.forwarding_fee_proportional_millionths = channel_def['local_fee_rate_millionths']\n                if 'local_base_fee_msat' in channel_def:\n                    channel_ab.forwarding_fee_base_msat = channel_def['local_base_fee_msat']\n                if 'remote_fee_rate_millionths' in channel_def:\n                    channel_ba.forwarding_fee_proportional_millionths = channel_def['remote_fee_rate_millionths']\n                if 'remote_base_fee_msat' in channel_def:\n                    channel_ba.forwarding_fee_base_msat = channel_def['remote_base_fee_msat']\n\n        # create peers\n        for ab in channels.keys():\n            peers[ab] = PeerInTests(workers[ab[0]], keys[ab[1]].pubkey, transports[ab])\n\n        # add peers to workers\n        for a, w in workers.items():\n            for ab, peer_ab in peers.items():\n                if ab[0] == a:\n                    w.lnpeermgr._peers[peer_ab.pubkey] = peer_ab\n\n        # set forwarding properties\n        for a, definition in graph_definition.items():\n            for property in definition.get('config', {}).items():\n                workers[a].network.config.set_key(*property)\n\n        # mark_open won't work if state is already OPEN.\n        # so set it to FUNDED\n        for channel_ab in channels.values():\n           channel_ab._state = ChannelState.FUNDED\n\n        # this populates the channel graph:\n        for ab, peer_ab in peers.items():\n            peer_ab.mark_open(channels[ab])\n\n        graph = Graph(\n            workers=workers,\n            peers=peers,\n            channels=channels,\n        )\n        for a in workers:\n            print(f\"{a:5s}: {keys[a].pubkey}\")\n            print(f\"       {keys[a].pubkey.hex()}\")\n        return graph\n\n\nclass TestPeerUtils(TestPeer):\n\n    def test_decode_short_ids(self):\n        \"\"\"\n        Test Peer.decode_short_ids() against some data from\n        https://github.com/lightning/bolts/commit/313c0f290eb87e96dc8195cad0c891418a826c2c\n        \"\"\"\n        # Test uncompressed encoding with three scids\n        encoded_uncompressed = bytes.fromhex(\"00\" + \"0000000000003043\" + \"00000000000778d6\" + \"000000000046e1c1\")\n        result = Peer.decode_short_ids(encoded_uncompressed)\n        self.assertEqual(len(result), 3)\n        self.assertEqual(result[0], bytes.fromhex(\"0000000000003043\"))  # 0x0x12355\n        self.assertEqual(result[1], bytes.fromhex(\"00000000000778d6\"))  # 0x7x30934\n        self.assertEqual(result[2], bytes.fromhex(\"000000000046e1c1\"))  # 0x70x57793\n\n        # Test empty list\n        encoded_empty = bytes.fromhex(\"00\")\n        result = Peer.decode_short_ids(encoded_empty)\n        self.assertEqual(result, [])\n\n        # Test single scid\n        encoded_single = bytes.fromhex(\"00\" + \"000000000000008e\")  # 0x0x142\n        result = Peer.decode_short_ids(encoded_single)\n        self.assertEqual(len(result), 1)\n        self.assertEqual(result[0], bytes.fromhex(\"000000000000008e\"))\n\n        # test invalid size raises exception\n        encoded_invalid = bytes.fromhex(\"00\" + \"00\" * 9)\n        with self.assertRaises(Exception) as ctx:\n            Peer.decode_short_ids(encoded_invalid)\n        self.assertIn(\"invalid size\", str(ctx.exception))\n\n        # Test unsupported encoding raises exception (considering it even passes the length check)\n        encoded_unsupported = bytes.fromhex(\"01\" + \"00\" * 8)  # 01 was zlib before removed\n        with self.assertRaises(Exception) as ctx:\n            Peer.decode_short_ids(encoded_unsupported)\n        self.assertIn(\"unexpected first byte\", str(ctx.exception))\n\n    async def test_maybe_save_remote_update(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        alice_bob_peer, bob_alice_peer = graph.peers[('alice', 'bob')], graph.peers[('bob', 'alice')]\n        alice_bob_chan, bob_alice_chan = graph.channels[('alice', 'bob')], graph.channels[('bob', 'alice')]\n\n        # prepare channel update from alice\n        alice_to_bob_chan_update = alice_bob_chan.get_outgoing_gossip_channel_update()\n        raw = alice_to_bob_chan_update\n        payload = decode_msg(alice_to_bob_chan_update)[1]\n        payload['raw'] = raw\n\n        # bob should accept the update and save it\n        self.assertIsNone(bob_alice_chan.storage.get('remote_update'))\n        bob_alice_peer.maybe_save_remote_update(payload)\n        self.assertEqual(bob_alice_chan.storage.get('remote_update'), raw.hex())\n\n        # alice shouldn't save her own channel update as remote update\n        self.assertIsNone(alice_bob_chan.storage.get('remote_update'))\n        alice_bob_peer.maybe_save_remote_update(payload)\n        self.assertIsNone(alice_bob_chan.storage.get('remote_update'))\n\n        ChannelDB.verify_channel_update(payload, start_node=bob_alice_peer.pubkey)\n        # trying to verify the sig against the wrong pubkey should fail obviously\n        with self.assertRaises(InvalidGossipMsg):\n            ChannelDB.verify_channel_update(payload, start_node=alice_bob_peer.pubkey)\n\n\nclass TestPeerDirect(TestPeer):\n\n    def prepare_peers(\n            self, alice_channel: Channel, bob_channel: Channel,\n    ):\n        graph = self.prepare_chans_and_peers_in_graph(\n            self.GRAPH_DEFINITIONS['single_chan'],\n            channels={('alice', 'bob'): alice_channel, ('bob', 'alice'): bob_channel},\n        )\n        p1, p2 = graph.peers.values()\n        w1, w2 = graph.workers.values()\n        return p1, p2, w1, w2\n\n    async def test_reestablish(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        alice_channel, bob_channel = graph.channels.values()\n\n        for chan in (alice_channel, bob_channel):\n            chan.peer_state = PeerState.DISCONNECTED\n        async def reestablish():\n            await asyncio.gather(\n                p1.reestablish_channel(alice_channel),\n                p2.reestablish_channel(bob_channel))\n            self.assertEqual(alice_channel.peer_state, PeerState.GOOD)\n            self.assertEqual(bob_channel.peer_state, PeerState.GOOD)\n            gath.cancel()\n        gath = asyncio.gather(reestablish(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch())\n        with self.assertRaises(asyncio.CancelledError):\n            await gath\n\n    async def test_reestablish_with_old_state(self):\n        async def f(alice_slow: bool, bob_slow: bool):\n            random_seed = os.urandom(32)\n            alice_lnwallet, bob_lnwallet = self.prepare_lnwallets(self.GRAPH_DEFINITIONS['single_chan']).values()\n            alice_channel, bob_channel = create_test_channels(random_seed=random_seed, alice_lnwallet=alice_lnwallet, bob_lnwallet=bob_lnwallet)\n            alice_channel_0, bob_channel_0 = create_test_channels(random_seed=random_seed, alice_lnwallet=alice_lnwallet, bob_lnwallet=bob_lnwallet)  # these are identical\n            p1, p2, w1, w2 = self.prepare_peers(alice_channel, bob_channel)\n            lnaddr, pay_req = self.prepare_invoice(w2)\n            async def pay():\n                result, log = await w1.pay_invoice(pay_req)\n                self.assertEqual(result, True)\n                gath.cancel()\n            gath = asyncio.gather(pay(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch())\n            with self.assertRaises(asyncio.CancelledError):\n                await gath\n            p1, p2, w1, w2 = self.prepare_peers(alice_channel_0, bob_channel)\n            for chan in (alice_channel_0, bob_channel):\n                chan.peer_state = PeerState.DISCONNECTED\n\n            async def alice_sends_reest():\n                if alice_slow: await asyncio.sleep(0.05)\n                await p1.reestablish_channel(alice_channel_0)\n            async def bob_sends_reest():\n                if bob_slow: await asyncio.sleep(0.05)\n                await p2.reestablish_channel(bob_channel)\n\n            with self.assertRaises(GracefulDisconnect):\n                async with OldTaskGroup() as group:\n                    await group.spawn(p1._message_loop())\n                    await group.spawn(p1.htlc_switch())\n                    await group.spawn(p2._message_loop())\n                    await group.spawn(p2.htlc_switch())\n                    await group.spawn(alice_sends_reest)\n                    await group.spawn(bob_sends_reest)\n            self.assertEqual(alice_channel_0.peer_state, PeerState.BAD)\n            self.assertEqual(alice_channel_0._state, ChannelState.WE_ARE_TOXIC)\n            self.assertEqual(bob_channel._state, ChannelState.FORCE_CLOSING)\n\n        with self.subTest(msg=\"both fast\"):\n            # FIXME: we want to test the case where both Alice and Bob sends channel-reestablish before\n            #        receiving what the other sent. This is not a reliable way to do that...\n            await f(alice_slow=False, bob_slow=False)\n        with self.subTest(msg=\"alice is slow\"):\n            await f(alice_slow=True, bob_slow=False)\n        with self.subTest(msg=\"bob is slow\"):\n            await f(alice_slow=False, bob_slow=True)\n\n    @staticmethod\n    def _send_fake_htlc(peer: Peer, chan: Channel) -> UpdateAddHtlc:\n        htlc = UpdateAddHtlc(amount_msat=10000, payment_hash=os.urandom(32), cltv_abs=999, timestamp=1)\n        htlc = chan.add_htlc(htlc)\n        peer.send_message(\n            \"update_add_htlc\",\n            channel_id=chan.channel_id,\n            id=htlc.htlc_id,\n            cltv_expiry=htlc.cltv_abs,\n            amount_msat=htlc.amount_msat,\n            payment_hash=htlc.payment_hash,\n            onion_routing_packet=1366 * b\"0\",\n        )\n        return htlc\n\n    async def _test_reestablish_replay_messages(self, rev_then_sig: bool):\n        alice_lnwallet, bob_lnwallet = self.prepare_lnwallets(self.GRAPH_DEFINITIONS['single_chan']).values()\n        chan_AB, chan_BA = create_test_channels(alice_lnwallet=alice_lnwallet, bob_lnwallet=bob_lnwallet)\n        # note: we don't start peer.htlc_switch() so that the fake htlcs are left alone.\n        async def f():\n            p1, p2, w1, w2 = self.prepare_peers(chan_AB, chan_BA)\n            p1.MIN_TIME_BETWEEN_SENDING_COMMITSIGS = 0\n            p2.MIN_TIME_BETWEEN_SENDING_COMMITSIGS = 0\n            async with OldTaskGroup() as group:\n                await group.spawn(p1._message_loop())\n                await group.spawn(p2._message_loop())\n                await p1.initialized\n                await p2.initialized\n                self._send_fake_htlc(p2, chan_BA)\n                self._send_fake_htlc(p1, chan_AB)\n                p2.transport.queue.put_nowait(asyncio.Event())  # break Bob's incoming pipe\n                if rev_then_sig:\n                    self.assertTrue(p2.maybe_send_commitment(chan_BA))\n                    await p1.received_commitsig_event.wait()\n                else:\n                    self.assertTrue(p1.maybe_send_commitment(chan_AB))\n                    self.assertTrue(p2.maybe_send_commitment(chan_BA))\n                    await p1.received_commitsig_event.wait()\n                await group.cancel_remaining()\n            # simulating disconnection. recreate transports.\n            self.logger.info(\"simulating disconnection. recreating transports.\")\n            p1, p2, w1, w2 = self.prepare_peers(chan_AB, chan_BA)\n            p1.MIN_TIME_BETWEEN_SENDING_COMMITSIGS = 0\n            p2.MIN_TIME_BETWEEN_SENDING_COMMITSIGS = 0\n            for chan in (chan_AB, chan_BA):\n                chan.peer_state = PeerState.DISCONNECTED\n            async with OldTaskGroup() as group:\n                await group.spawn(p1._message_loop())\n                await group.spawn(p2._message_loop())\n                with self.assertLogs('electrum', level='INFO') as logs:\n                    async with OldTaskGroup() as group2:\n                        await group2.spawn(p1.reestablish_channel(chan_AB))\n                        await group2.spawn(p2.reestablish_channel(chan_BA))\n                s = \"replaying a revoke_and_ack \" + (\"first\" if rev_then_sig else \"last\")\n                self.assertTrue(any((\"alice->bob\" in msg and s in msg) for msg in logs.output))\n                self.assertTrue(any((\"alice->bob\" in msg and\n                                     \"replayed 2 unacked messages. ['update_add_htlc', 'commitment_signed']\" in msg) for msg in logs.output))\n                self.assertEqual(chan_AB.peer_state, PeerState.GOOD)\n                self.assertEqual(chan_BA.peer_state, PeerState.GOOD)\n                await group.cancel_remaining()\n            raise SuccessfulTest()\n        with self.assertRaises(SuccessfulTest):\n            await f()\n\n    async def test_reestablish_replay_messages_rev_then_sig(self):\n        \"\"\"\n        See https://github.com/lightning/bolts/pull/810#issue-728299277\n\n        Rev then Sig\n        A            B\n         <---add-----\n         ----add---->\n         <---sig-----\n         ----rev----x\n         ----sig----x\n\n        A needs to retransmit:\n        ----rev-->      (note that 'add' can be first too)\n        ----add-->\n        ----sig-->\n        \"\"\"\n        await self._test_reestablish_replay_messages(True)\n\n    async def test_reestablish_replay_messages_sig_then_rev(self):\n        \"\"\"\n        See https://github.com/lightning/bolts/pull/810#issue-728299277\n\n        Sig then Rev\n        A            B\n         <---add-----\n         ----add---->\n         ----sig----x\n         <---sig-----\n         ----rev----x\n\n        A needs to retransmit:\n        ----add-->\n        ----sig-->\n        ----rev-->\n        \"\"\"\n        await self._test_reestablish_replay_messages(False)\n\n    async def _test_simple_payment(\n            self,\n            test_trampoline: bool,\n            test_hold_invoice=False,\n            test_failure=False,\n            test_bundle=False,\n            test_bundle_timeout=False\n    ):\n        \"\"\"Alice pays Bob a single HTLC via direct channel.\"\"\"\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        w1, w2 = graph.workers.values()\n        results = {}\n        async def pay(lnaddr, pay_req):\n            self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            result, log = await w1.pay_invoice(pay_req)\n            if result is True:\n                self.assertEqual(PR_PAID, w2.get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n                results[lnaddr] = PaymentDone()\n            else:\n                results[lnaddr] = PaymentFailure()\n        lnaddr, pay_req = self.prepare_invoice(w2)\n        to_pay = [(lnaddr, pay_req)]\n        self.prepare_recipient(w2, lnaddr.paymenthash, test_hold_invoice, test_failure)\n\n        if test_bundle:\n            lnaddr2, pay_req2 = self.prepare_invoice(w2)\n            w2.bundle_payments([lnaddr.paymenthash, lnaddr2.paymenthash])\n            if not test_bundle_timeout:\n                to_pay.append((lnaddr2, pay_req2))\n\n        if test_trampoline:\n            await self._activate_trampoline(w1)\n            # declare bob as trampoline node\n            electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n                'bob': LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=w2.node_keypair.pubkey),\n            }\n\n        async def f():\n            async with OldTaskGroup() as group:\n                await group.spawn(p1._message_loop())\n                await group.spawn(p1.htlc_switch())\n                await group.spawn(p2._message_loop())\n                await group.spawn(p2.htlc_switch())\n                await asyncio.sleep(0.01)\n                invoice_features = lnaddr.get_features()\n                self.assertFalse(invoice_features.supports(LnFeatures.BASIC_MPP_OPT))\n                for lnaddr_to_pay, pay_req_to_pay in to_pay:\n                    await group.spawn(pay(lnaddr_to_pay, pay_req_to_pay))\n                elapsed = 0\n                while len(results) < len(to_pay) and elapsed < 4:\n                    await asyncio.sleep(0.05)  # wait for all payments to finish/fail (or timeout)\n                    elapsed += 0.05\n                self.assertEqual(len(results), len(to_pay), msg=\"timeout\")\n                # all payment results should be similar\n                self.assertEqual(len(set(type(res) for res in results.values())), 1, msg=results)\n                raise list(results.values())[0]\n\n        await f()\n\n    async def test_simple_payment_success(self):\n        for test_trampoline in [False, True]:\n            with self.assertRaises(PaymentDone):\n                await self._test_simple_payment(test_trampoline=test_trampoline)\n\n    async def test_simple_payment_failure(self):\n        for test_trampoline in [False, True]:\n            with self.assertRaises(PaymentFailure):\n                await self._test_simple_payment(test_trampoline=test_trampoline, test_failure=True)\n\n    async def test_payment_bundle(self):\n        for test_trampoline in [False, True]:\n            with self.assertRaises(PaymentDone):\n                await self._test_simple_payment(test_trampoline=test_trampoline, test_bundle=True)\n\n    async def test_payment_bundle_timeout(self):\n        for test_trampoline in [False, True]:\n            with self.assertRaises(PaymentFailure):\n                await self._test_simple_payment(test_trampoline=test_trampoline, test_bundle=True, test_bundle_timeout=True)\n\n    async def test_payment_bundle_with_hold_invoice(self):\n        for test_trampoline in [False, True]:\n            with self.assertRaises(PaymentDone):\n                await self._test_simple_payment(test_trampoline=test_trampoline, test_bundle=True, test_hold_invoice=True)\n\n    async def test_simple_payment_success_with_hold_invoice(self):\n        for test_trampoline in [False, True]:\n            with self.assertRaises(PaymentDone):\n                await self._test_simple_payment(test_trampoline=test_trampoline, test_hold_invoice=True)\n\n    async def test_simple_payment_failure_with_hold_invoice(self):\n        for test_trampoline in [False, True]:\n            with self.assertRaises(PaymentFailure):\n                await self._test_simple_payment(test_trampoline=test_trampoline, test_hold_invoice=True, test_failure=True)\n\n    async def test_check_invoice_before_payment(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        w1, w2 = graph.workers.values()\n        async def try_paying_some_invoices():\n            # feature bits: unknown even fbit\n            invoice_features = w2.features.for_invoice() | (1 << 990)  # add undefined even fbit\n            lnaddr, pay_req = self.prepare_invoice(w2, invoice_features=invoice_features)\n            with self.assertRaises(lnutil.UnknownEvenFeatureBits):\n                result, log = await w1.pay_invoice(pay_req)\n            # feature bits: not all transitive dependencies are set\n            invoice_features = LnFeatures((1 << 8) + (1 << 17))\n            lnaddr, pay_req = self.prepare_invoice(w2, invoice_features=invoice_features)\n            with self.assertRaises(lnutil.IncompatibleOrInsaneFeatures):\n                result, log = await w1.pay_invoice(pay_req)\n            # too large CLTV\n            lnaddr, pay_req = self.prepare_invoice(w2, min_final_cltv_delta=10**6)\n            with self.assertRaises(InvoiceError):\n                result, log = await w1.pay_invoice(pay_req)\n            raise SuccessfulTest()\n\n        async def f():\n            async with OldTaskGroup() as group:\n                await group.spawn(p1._message_loop())\n                await group.spawn(p1.htlc_switch())\n                await group.spawn(p2._message_loop())\n                await group.spawn(p2.htlc_switch())\n                await asyncio.sleep(0.01)\n                await group.spawn(try_paying_some_invoices())\n\n        with self.assertRaises(SuccessfulTest):\n            await f()\n\n    async def test_reject_invalid_min_final_cltv_delta(self):\n        \"\"\"\n        Tests that htlcs with a final cltv delta < the minimum requested in the invoice get\n        rejected immediately upon receiving them.\n        \"\"\"\n        async def run_test(test_trampoline):\n            graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n            p1, p2 = graph.peers.values()\n            w1, w2 = graph.workers.values()\n\n            async def try_pay_with_too_low_final_cltv_delta(lnaddr, w1=w1, w2=w2):\n                self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n                assert lnaddr.get_min_final_cltv_delta() == 400  # what the receiver expects\n                lnaddr.tags = [tag for tag in lnaddr.tags if tag[0] != 'c'] + [['c', 144]]\n                b11 = lnencode(lnaddr, w2.node_keypair.privkey)\n                pay_req = Invoice.from_bech32(b11)\n                assert pay_req._lnaddr.get_min_final_cltv_delta() == 144  # what w1 will use to pay\n                result, log = await w1.pay_invoice(pay_req)\n                if not result:\n                    raise PaymentFailure()\n                raise PaymentDone()\n\n            # create invoice with high min final cltv delta\n            lnaddr, _pay_req = self.prepare_invoice(w2, min_final_cltv_delta=400)\n\n            if test_trampoline:\n                await self._activate_trampoline(w1)\n                # declare bob as trampoline node\n                electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n                    'bob': LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=w2.node_keypair.pubkey),\n                }\n\n            async def f():\n                async with OldTaskGroup() as group:\n                    await group.spawn(p1._message_loop())\n                    await group.spawn(p1.htlc_switch())\n                    await group.spawn(p2._message_loop())\n                    await group.spawn(p2.htlc_switch())\n                    await asyncio.sleep(0.01)\n                    await group.spawn(try_pay_with_too_low_final_cltv_delta(lnaddr))\n\n            with self.assertRaises(PaymentFailure):\n                await f()\n\n        for _test_trampoline in [False, True]:\n            await run_test(_test_trampoline)\n\n    async def test_reject_payment_for_expired_invoice(self):\n        \"\"\"Tests that new htlcs paying an invoice that has already been expired will get rejected.\"\"\"\n        async def run_test(test_trampoline):\n            graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n            p1, p2 = graph.peers.values()\n            w1, w2 = graph.workers.values()\n\n            # create lightning invoice in the past, so it is expired\n            with mock.patch('time.time', return_value=int(time.time()) - 10000):\n                lnaddr, _pay_req = self.prepare_invoice(w2, expiry=3600)\n                b11 = lnencode(lnaddr, w2.node_keypair.privkey)\n                pay_req = Invoice.from_bech32(b11)\n\n            async def try_pay_expired_invoice(pay_req: Invoice, w1=w1):\n                assert pay_req.has_expired()\n                assert lnaddr.is_expired()\n                with mock.patch.object(w1, \"_check_bolt11_invoice\", return_value=lnaddr):\n                    result, log = await w1.pay_invoice(pay_req)\n                if not result:\n                    raise PaymentFailure()\n                raise PaymentDone()\n\n            if test_trampoline:\n                await self._activate_trampoline(w1)\n                # declare bob as trampoline node\n                electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n                    'bob': LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=w2.node_keypair.pubkey),\n                }\n\n            async def f():\n                async with OldTaskGroup() as group:\n                    await group.spawn(p1._message_loop())\n                    await group.spawn(p1.htlc_switch())\n                    await group.spawn(p2._message_loop())\n                    await group.spawn(p2.htlc_switch())\n                    await asyncio.sleep(0.01)\n                    await group.spawn(try_pay_expired_invoice(pay_req))\n\n            with self.assertRaises(PaymentFailure):\n                await f()\n\n        for _test_trampoline in [False, True]:\n            await run_test(_test_trampoline)\n\n    async def test_reject_mpp_for_non_mpp_invoice(self):\n        \"\"\"Test that we reject a payment if it is mpp and we didn't signal support for mpp in the invoice\"\"\"\n        async def run_test(test_trampoline):\n            graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n            p1, p2 = graph.peers.values()\n            w1, w2 = graph.workers.values()\n            w1.config.TEST_FORCE_MPP = True  # force alice to send mpp\n\n            if test_trampoline:\n                await self._activate_trampoline(w1)\n                await self._activate_trampoline(w2)\n                # declare bob as trampoline node\n                electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n                    'bob': LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=w2.node_keypair.pubkey),\n                }\n\n            lnaddr, pay_req = self.prepare_invoice(w2)\n            self.assertFalse(lnaddr.get_features().supports(LnFeatures.BASIC_MPP_OPT))\n            self.assertFalse(lnaddr.get_features().supports(LnFeatures.BASIC_MPP_REQ))\n\n            async def try_pay_invoice_with_mpp(pay_req: Invoice, w1=w1):\n                result, log = await w1.pay_invoice(pay_req)\n                if not result:\n                    raise PaymentFailure()\n                raise PaymentDone()\n\n            async def f():\n                async with OldTaskGroup() as group:\n                    await group.spawn(p1._message_loop())\n                    await group.spawn(p1.htlc_switch())\n                    await group.spawn(p2._message_loop())\n                    await group.spawn(p2.htlc_switch())\n                    await asyncio.sleep(0.01)\n                    await group.spawn(try_pay_invoice_with_mpp(pay_req))\n\n            with self.assertRaises(PaymentFailure):\n                await f()\n\n        for _test_trampoline in [False, True]:\n            await run_test(_test_trampoline)\n\n    async def test_reject_multiple_payments_of_same_invoice(self):\n        \"\"\"Tests that new htlcs paying an invoice that has already been paid will get rejected.\"\"\"\n        async def run_test(test_trampoline):\n            graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n            p1, p2 = graph.peers.values()\n            w1, w2 = graph.workers.values()\n\n            lnaddr, _pay_req = self.prepare_invoice(w2)\n\n            async def try_pay_invoice_twice(pay_req: Invoice, w1=w1):\n                result, log = await w1.pay_invoice(pay_req)\n                assert result is True\n                # now pay the same invoice again, the payment should be rejected by w2\n                w1.set_payment_status(pay_req._lnaddr.paymenthash, PR_UNPAID, direction=lnutil.SENT)\n                result, log = await w1.pay_invoice(pay_req)\n                if not result:\n                    # w1.pay_invoice returned a payment failure as the payment got rejected by w2\n                    raise SuccessfulTest()\n                raise PaymentDone()\n\n            if test_trampoline:\n                await self._activate_trampoline(w1)\n                # declare bob as trampoline node\n                electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n                    'bob': LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=w2.node_keypair.pubkey),\n                }\n\n            async def f():\n                async with OldTaskGroup() as group:\n                    await group.spawn(p1._message_loop())\n                    await group.spawn(p1.htlc_switch())\n                    await group.spawn(p2._message_loop())\n                    await group.spawn(p2.htlc_switch())\n                    await asyncio.sleep(0.01)\n                    await group.spawn(try_pay_invoice_twice(_pay_req))\n\n            with self.assertRaises(SuccessfulTest):\n                await f()\n\n        for _test_trampoline in [False, True]:\n            await run_test(_test_trampoline)\n\n    async def test_payment_race(self):\n        \"\"\"Alice and Bob pay each other simultaneously.\n        They both send 'update_add_htlc' and receive each other's update\n        before sending 'commitment_signed'. Neither party should fulfill\n        the respective HTLCs until those are irrevocably committed to.\n        \"\"\"\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        w1, w2 = graph.workers.values()\n        alice_channel, bob_channel = graph.channels.values()\n        async def pay():\n            await util.wait_for2(p1.initialized, 1)\n            await util.wait_for2(p2.initialized, 1)\n            # prep\n            _maybe_send_commitment1 = p1.maybe_send_commitment\n            _maybe_send_commitment2 = p2.maybe_send_commitment\n            lnaddr2, pay_req2 = self.prepare_invoice(w2)\n            lnaddr1, pay_req1 = self.prepare_invoice(w1)\n            # alice sends htlc BUT NOT COMMITMENT_SIGNED\n            p1.maybe_send_commitment = lambda x: None\n            route1 = (await w1.create_routes_from_invoice(lnaddr2.get_amount_msat(), decoded_invoice=lnaddr2))[0][0].route\n            paysession1 = w1._paysessions[lnaddr2.paymenthash + lnaddr2.payment_secret]\n            shi1 = SentHtlcInfo(\n                route=route1,\n                payment_secret_orig=lnaddr2.payment_secret,\n                payment_secret_bucket=lnaddr2.payment_secret,\n                amount_msat=lnaddr2.get_amount_msat(),\n                bucket_msat=lnaddr2.get_amount_msat(),\n                amount_receiver_msat=lnaddr2.get_amount_msat(),\n                trampoline_fee_level=None,\n                trampoline_route=None,\n            )\n            await w1.pay_to_route(\n                sent_htlc_info=shi1,\n                paysession=paysession1,\n                min_final_cltv_delta=lnaddr2.get_min_final_cltv_delta(),\n            )\n            p1.maybe_send_commitment = _maybe_send_commitment1\n            # bob sends htlc BUT NOT COMMITMENT_SIGNED\n            p2.maybe_send_commitment = lambda x: None\n            route2 = (await w2.create_routes_from_invoice(lnaddr1.get_amount_msat(), decoded_invoice=lnaddr1))[0][0].route\n            paysession2 = w2._paysessions[lnaddr1.paymenthash + lnaddr1.payment_secret]\n            shi2 = SentHtlcInfo(\n                route=route2,\n                payment_secret_orig=lnaddr1.payment_secret,\n                payment_secret_bucket=lnaddr1.payment_secret,\n                amount_msat=lnaddr1.get_amount_msat(),\n                bucket_msat=lnaddr1.get_amount_msat(),\n                amount_receiver_msat=lnaddr1.get_amount_msat(),\n                trampoline_fee_level=None,\n                trampoline_route=None,\n            )\n            await w2.pay_to_route(\n                sent_htlc_info=shi2,\n                paysession=paysession2,\n                min_final_cltv_delta=lnaddr1.get_min_final_cltv_delta(),\n            )\n            p2.maybe_send_commitment = _maybe_send_commitment2\n            # sleep a bit so that they both receive msgs sent so far\n            await asyncio.sleep(0.2)\n            # now they both send COMMITMENT_SIGNED\n            p1.maybe_send_commitment(alice_channel)\n            p2.maybe_send_commitment(bob_channel)\n\n            htlc_log1 = await paysession1.sent_htlcs_q.get()\n            self.assertTrue(htlc_log1.success)\n            htlc_log2 = await paysession2.sent_htlcs_q.get()\n            self.assertTrue(htlc_log2.success)\n            raise PaymentDone()\n\n        async def f():\n            async with OldTaskGroup() as group:\n                await group.spawn(p1._message_loop())\n                await group.spawn(p1.htlc_switch())\n                await group.spawn(p2._message_loop())\n                await group.spawn(p2.htlc_switch())\n                await asyncio.sleep(0.01)\n                await group.spawn(pay())\n        with self.assertRaises(PaymentDone):\n            await f()\n\n    #@unittest.skip(\"too expensive\")\n    async def test_payments_stresstest(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        w1, w2 = graph.workers.values()\n        alice_channel, bob_channel = graph.channels.values()\n        alice_init_balance_msat = alice_channel.balance(HTLCOwner.LOCAL)\n        bob_init_balance_msat = bob_channel.balance(HTLCOwner.LOCAL)\n        num_payments = 50\n        payment_value_msat = 10_000_000  # make it large enough so that there are actually HTLCs on the ctx\n        max_htlcs_in_flight = asyncio.Semaphore(5)\n        async def single_payment(pay_req):\n            async with max_htlcs_in_flight:\n                await w1.pay_invoice(pay_req)\n        async def many_payments():\n            async with OldTaskGroup() as group:\n                for i in range(num_payments):\n                    lnaddr, pay_req = self.prepare_invoice(w2, amount_msat=payment_value_msat)\n                    await group.spawn(single_payment(pay_req))\n            gath.cancel()\n        gath = asyncio.gather(many_payments(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch())\n        with self.assertRaises(asyncio.CancelledError):\n            await gath\n        self.assertEqual(alice_init_balance_msat - num_payments * payment_value_msat, alice_channel.balance(HTLCOwner.LOCAL))\n        self.assertEqual(alice_init_balance_msat - num_payments * payment_value_msat, bob_channel.balance(HTLCOwner.REMOTE))\n        self.assertEqual(bob_init_balance_msat + num_payments * payment_value_msat, bob_channel.balance(HTLCOwner.LOCAL))\n        self.assertEqual(bob_init_balance_msat + num_payments * payment_value_msat, alice_channel.balance(HTLCOwner.REMOTE))\n\n    async def test_payment_recv_mpp_confusion1(self):\n        \"\"\"Regression test for https://github.com/spesmilo/electrum/security/advisories/GHSA-8r85-vp7r-hjxf\"\"\"\n        # This test checks that the following attack does not work:\n        #   - Bob creates invoice1: 1 BTC, H1, S1\n        #   - Bob creates invoice2: 1 BTC, H2, S2;  both given to attacker to pay\n        #   - Alice sends htlc1: 0.1 BTC, H1, S1  (total_msat=1 BTC)\n        #   - Alice sends htlc2: 0.9 BTC, H2, S1  (total_msat=1 BTC)\n        #   - Bob(victim) reveals preimage for H1 and fulfills htlc1 (fails other)\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        w1, w2 = graph.workers.values()\n        alice_channel, bob_channel = graph.channels.values()\n        async def pay():\n            self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr1.paymenthash, direction=RECEIVED))\n            self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr2.paymenthash, direction=RECEIVED))\n\n            route = (await w1.create_routes_from_invoice(amount_msat=1000, decoded_invoice=lnaddr1))[0][0].route\n            p1.pay(\n                route=route,\n                chan=alice_channel,\n                amount_msat=1000,\n                total_msat=lnaddr1.get_amount_msat(),\n                payment_hash=lnaddr1.paymenthash,\n                min_final_cltv_delta=lnaddr1.get_min_final_cltv_delta(),\n                payment_secret=lnaddr1.payment_secret,\n            )\n            p1.pay(\n                route=route,\n                chan=alice_channel,\n                amount_msat=lnaddr1.get_amount_msat() - 1000,\n                total_msat=lnaddr1.get_amount_msat(),\n                payment_hash=lnaddr2.paymenthash,\n                min_final_cltv_delta=lnaddr1.get_min_final_cltv_delta(),\n                payment_secret=lnaddr1.payment_secret,\n            )\n\n            while nhtlc_success + nhtlc_failed < 2:\n                await htlc_resolved.wait()\n            self.assertEqual(0, nhtlc_success)\n            self.assertEqual(2, nhtlc_failed)\n            raise SuccessfulTest()\n\n        w2.features |= LnFeatures.BASIC_MPP_OPT\n        lnaddr1, _pay_req = self.prepare_invoice(w2, amount_msat=100_000_000)\n        lnaddr2, _pay_req = self.prepare_invoice(w2, amount_msat=100_000_000)\n        self.assertTrue(lnaddr1.get_features().supports(LnFeatures.BASIC_MPP_OPT))\n        self.assertTrue(lnaddr2.get_features().supports(LnFeatures.BASIC_MPP_OPT))\n\n        async def f():\n            async with OldTaskGroup() as group:\n                await group.spawn(p1._message_loop())\n                await group.spawn(p1.htlc_switch())\n                await group.spawn(p2._message_loop())\n                await group.spawn(p2.htlc_switch())\n                await asyncio.sleep(0.01)\n                await group.spawn(pay())\n\n        htlc_resolved = asyncio.Event()\n        nhtlc_success = 0\n        nhtlc_failed = 0\n        async def on_htlc_fulfilled(*args):\n            htlc_resolved.set()\n            htlc_resolved.clear()\n            nonlocal nhtlc_success\n            nhtlc_success += 1\n        async def on_htlc_failed(*args):\n            htlc_resolved.set()\n            htlc_resolved.clear()\n            nonlocal nhtlc_failed\n            nhtlc_failed += 1\n        util.register_callback(on_htlc_fulfilled, [\"htlc_fulfilled\"])\n        util.register_callback(on_htlc_failed, [\"htlc_failed\"])\n\n        with self.assertRaises(SuccessfulTest):\n            await f()\n\n    async def test_payment_recv_mpp_confusion2(self):\n        \"\"\"Regression test for https://github.com/spesmilo/electrum/security/advisories/GHSA-8r85-vp7r-hjxf\"\"\"\n        # This test checks that the following attack does not work:\n        #   - Bob creates invoice: 1 BTC\n        #   - Alice sends htlc1: 0.1 BTC  (total_msat=0.2 BTC)\n        #   - Alice sends htlc2: 0.1 BTC  (total_msat=1 BTC)\n        #   - Bob(victim) reveals preimage and fulfills htlc2 (fails other)\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        w1, w2 = graph.workers.values()\n        alice_channel, bob_channel = graph.channels.values()\n        async def pay():\n            self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr1.paymenthash, direction=RECEIVED))\n\n            route = (await w1.create_routes_from_invoice(amount_msat=1000, decoded_invoice=lnaddr1))[0][0].route\n            p1.pay(\n                route=route,\n                chan=alice_channel,\n                amount_msat=1000,\n                total_msat=2000,\n                payment_hash=lnaddr1.paymenthash,\n                min_final_cltv_delta=lnaddr1.get_min_final_cltv_delta(),\n                payment_secret=lnaddr1.payment_secret,\n            )\n            p1.pay(\n                route=route,\n                chan=alice_channel,\n                amount_msat=1000,\n                total_msat=lnaddr1.get_amount_msat(),\n                payment_hash=lnaddr1.paymenthash,\n                min_final_cltv_delta=lnaddr1.get_min_final_cltv_delta(),\n                payment_secret=lnaddr1.payment_secret,\n            )\n\n            while nhtlc_success + nhtlc_failed < 2:\n                await htlc_resolved.wait()\n            self.assertEqual(0, nhtlc_success)\n            self.assertEqual(2, nhtlc_failed)\n            raise SuccessfulTest()\n\n        w2.features |= LnFeatures.BASIC_MPP_OPT\n        lnaddr1, _pay_req = self.prepare_invoice(w2, amount_msat=100_000_000)\n        self.assertTrue(lnaddr1.get_features().supports(LnFeatures.BASIC_MPP_OPT))\n\n        async def f():\n            async with OldTaskGroup() as group:\n                await group.spawn(p1._message_loop())\n                await group.spawn(p1.htlc_switch())\n                await group.spawn(p2._message_loop())\n                await group.spawn(p2.htlc_switch())\n                await asyncio.sleep(0.01)\n                await group.spawn(pay())\n\n        htlc_resolved = asyncio.Event()\n        nhtlc_success = 0\n        nhtlc_failed = 0\n        async def on_htlc_fulfilled(*args):\n            htlc_resolved.set()\n            htlc_resolved.clear()\n            nonlocal nhtlc_success\n            nhtlc_success += 1\n        async def on_htlc_failed(*args):\n            htlc_resolved.set()\n            htlc_resolved.clear()\n            nonlocal nhtlc_failed\n            nhtlc_failed += 1\n        util.register_callback(on_htlc_fulfilled, [\"htlc_fulfilled\"])\n        util.register_callback(on_htlc_failed, [\"htlc_failed\"])\n\n        with self.assertRaises(SuccessfulTest):\n            await f()\n\n    async def test_dont_settle_partial_mpp_trigger_with_invalid_cltv_htlc(self):\n        \"\"\"Alice gets two htlcs as part of a mpp, one has a cltv too close to expiry and will get failed.\n        Test that the other htlc won't get settled if the mpp isn't complete anymore after failing the other htlc.\n        \"\"\"\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        w1, w2 = graph.workers.values()\n        alice_channel, bob_channel = graph.channels.values()\n        async def pay():\n            await util.wait_for2(p1.initialized, 1)\n            await util.wait_for2(p2.initialized, 1)\n            w2.features |= LnFeatures.BASIC_MPP_OPT\n            lnaddr1, _pay_req = self.prepare_invoice(w2, amount_msat=10_000, min_final_cltv_delta=144)\n            self.assertTrue(lnaddr1.get_features().supports(LnFeatures.BASIC_MPP_OPT))\n            route = (await w1.create_routes_from_invoice(amount_msat=10_000, decoded_invoice=lnaddr1))[0][0].route\n\n            # now p1 sends two htlcs, one is valid (1 msat), one is invalid (9_999 msat)\n            p1.pay(\n                route=route,\n                chan=alice_channel,\n                amount_msat=1,\n                total_msat=lnaddr1.get_amount_msat(),\n                payment_hash=lnaddr1.paymenthash,\n                # this htlc is valid and will get accepted, but it shouldn't get settled\n                min_final_cltv_delta=400,\n                payment_secret=lnaddr1.payment_secret,\n            )\n            await asyncio.sleep(0.1)\n            assert w1.get_preimage(lnaddr1.paymenthash) is None\n            p1.pay(\n                route=route,\n                chan=alice_channel,\n                amount_msat=9_999,\n                total_msat=lnaddr1.get_amount_msat(),\n                payment_hash=lnaddr1.paymenthash,\n                # this htlc will get failed directly as the cltv is too close to expiry (< 144)\n                min_final_cltv_delta=1,\n                payment_secret=lnaddr1.payment_secret,\n            )\n\n            while nhtlc_success + nhtlc_failed < 2:\n                await htlc_resolved.wait()\n            # both htlcs of the mpp set should get failed and w2 shouldn't release the preimage\n            self.assertEqual(0, nhtlc_success, f\"{nhtlc_success=} | {nhtlc_failed=}\")\n            self.assertEqual(2, nhtlc_failed,  f\"{nhtlc_success=} | {nhtlc_failed=}\")\n            assert w1.get_preimage(lnaddr1.paymenthash) is None, \"w1 shouldn't get the preimage\"\n            raise SuccessfulTest()\n\n        async def f():\n            async with OldTaskGroup() as group:\n                await group.spawn(p1._message_loop())\n                await group.spawn(p1.htlc_switch())\n                await group.spawn(p2._message_loop())\n                await group.spawn(p2.htlc_switch())\n                await asyncio.sleep(0.01)\n                await group.spawn(pay())\n\n        htlc_resolved = asyncio.Event()\n        nhtlc_success = 0\n        nhtlc_failed = 0\n        async def on_htlc_fulfilled(*args):\n            htlc_resolved.set()\n            htlc_resolved.clear()\n            nonlocal nhtlc_success\n            nhtlc_success += 1\n        async def on_htlc_failed(*args):\n            htlc_resolved.set()\n            htlc_resolved.clear()\n            nonlocal nhtlc_failed\n            nhtlc_failed += 1\n        util.register_callback(on_htlc_fulfilled, [\"htlc_fulfilled\"])\n        util.register_callback(on_htlc_failed, [\"htlc_failed\"])\n\n        try:\n            with self.assertRaises(SuccessfulTest):\n                await f()\n        finally:\n            util.unregister_callback(on_htlc_fulfilled)\n            util.unregister_callback(on_htlc_failed)\n\n    async def test_mpp_cleanup_after_expiry(self):\n        \"\"\"\n        1. Alice sends two HTLCs to Bob, not reaching total_msat, and eventually they MPP_TIMEOUT\n        2. Bob fails both HTLCs\n        3. Alice then retries and sends HTLCs again to Bob, for the same RHASH,\n           this time reaching total_msat, and the payment succeeds\n\n        Test that the sets are properly cleaned up after MPP_TIMEOUT\n        and the sender gets a second chance to pay the same invoice.\n        \"\"\"\n        async def run_test(test_trampoline: bool):\n            graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n            alice_peer, bob_peer = graph.peers.values()\n            alice_wallet, bob_wallet = graph.workers.values()\n            alice_channel, bob_channel = graph.channels.values()\n            bob_wallet.features |= LnFeatures.BASIC_MPP_OPT\n            lnaddr1, pay_req1 = self.prepare_invoice(bob_wallet, amount_msat=10_000)\n\n            if test_trampoline:\n                await self._activate_trampoline(alice_wallet)\n                # declare bob as trampoline node\n                electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n                    'bob': LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=bob_wallet.node_keypair.pubkey),\n                }\n\n            async def _test():\n                route = (await alice_wallet.create_routes_from_invoice(amount_msat=10_000, decoded_invoice=lnaddr1))[0][0].route\n                assert len(bob_wallet.received_mpp_htlcs) == 0\n                # now alice sends two small htlcs, so the set stays incomplete\n                alice_peer.pay(  # htlc 1\n                    route=route,\n                    chan=alice_channel,\n                    amount_msat=lnaddr1.get_amount_msat() // 4,\n                    total_msat=lnaddr1.get_amount_msat(),\n                    payment_hash=lnaddr1.paymenthash,\n                    min_final_cltv_delta=400,\n                    payment_secret=lnaddr1.payment_secret,\n                )\n                alice_peer.pay(  # htlc 2\n                    route=route,\n                    chan=alice_channel,\n                    amount_msat=lnaddr1.get_amount_msat() // 4,\n                    total_msat=lnaddr1.get_amount_msat(),\n                    payment_hash=lnaddr1.paymenthash,\n                    min_final_cltv_delta=400,\n                    payment_secret=lnaddr1.payment_secret,\n                )\n                await asyncio.sleep(bob_wallet.MPP_EXPIRY // 2)  # give bob time to receive the htlc\n                bob_payment_key = bob_wallet._get_payment_key(lnaddr1.paymenthash).hex()\n                assert bob_wallet.received_mpp_htlcs[bob_payment_key].resolution == RecvMPPResolution.WAITING\n                assert len(bob_wallet.received_mpp_htlcs[bob_payment_key].htlcs) == 2\n                # now wait until bob expires the mpp (set)\n                async with util.async_timeout(bob_wallet.MPP_EXPIRY * 3):  # this can take some time, esp. on CI\n                    while nhtlc_success + nhtlc_failed < 2:\n                        await alice_htlc_resolved.wait()\n                # check that bob failed the htlc\n                assert nhtlc_success == 0 and nhtlc_failed == 2\n                # check that bob deleted the mpp set as it should be expired and resolved now\n                assert bob_payment_key not in bob_wallet.received_mpp_htlcs\n                alice_wallet._paysessions.clear()\n                assert alice_wallet.get_preimage(lnaddr1.paymenthash) is None  # bob didn't preimage\n                # now try to pay again, this time the full amount\n                result, log = await alice_wallet.pay_invoice(pay_req1)\n                assert result is True\n                assert alice_wallet.get_preimage(lnaddr1.paymenthash) is not None  # bob revealed preimage\n                assert len(bob_wallet.received_mpp_htlcs) == 0  # bob should also clean up a successful set\n                raise SuccessfulTest()\n\n            async def f():\n                async with OldTaskGroup() as group:\n                    await group.spawn(alice_peer._message_loop())\n                    await group.spawn(alice_peer.htlc_switch())\n                    await group.spawn(bob_peer._message_loop())\n                    await group.spawn(bob_peer.htlc_switch())\n                    await asyncio.sleep(0.01)\n                    await group.spawn(_test())\n\n            alice_htlc_resolved = asyncio.Event()\n            nhtlc_success = 0\n            nhtlc_failed = 0\n            async def on_sender_htlc_fulfilled(*args):\n                alice_htlc_resolved.set()\n                alice_htlc_resolved.clear()\n                nonlocal nhtlc_success\n                nhtlc_success += 1\n            async def on_sender_htlc_failed(*args):\n                alice_htlc_resolved.set()\n                alice_htlc_resolved.clear()\n                nonlocal nhtlc_failed\n                nhtlc_failed += 1\n            util.register_callback(on_sender_htlc_fulfilled, [\"htlc_fulfilled\"])\n            util.register_callback(on_sender_htlc_failed, [\"htlc_failed\"])\n\n            try:\n                with self.assertRaises(SuccessfulTest):\n                    await f()\n            finally:\n                util.unregister_callback(on_sender_htlc_fulfilled)\n                util.unregister_callback(on_sender_htlc_failed)\n\n        for use_trampoline in [True, False]:\n            self.logger.debug(f\"test_mpp_cleanup_after_expiry: {use_trampoline=}\")\n            await run_test(use_trampoline)\n\n    async def test_legacy_shutdown_low(self):\n        await self._test_shutdown(alice_fee=100, bob_fee=150)\n\n    async def test_legacy_shutdown_high(self):\n        await self._test_shutdown(alice_fee=2000, bob_fee=100)\n\n    async def test_modern_shutdown_with_overlap(self):\n        await self._test_shutdown(\n            alice_fee=1,\n            bob_fee=200,\n            alice_fee_range={'min_fee_satoshis': 1, 'max_fee_satoshis': 10},\n            bob_fee_range={'min_fee_satoshis': 10, 'max_fee_satoshis': 300})\n\n    @mock.patch('electrum.lnpeer.LN_P2P_NETWORK_TIMEOUT', 0.05)\n    async def test_modern_shutdown_no_overlap(self):\n        with self.assertLogs('electrum', level='ERROR') as logs:\n            with self.assertRaisesRegex(Exception, \"closing_signed not received\"):\n                await self._test_shutdown(\n                    alice_fee=1,\n                    bob_fee=200,\n                    alice_fee_range={'min_fee_satoshis': 1, 'max_fee_satoshis': 10},\n                    bob_fee_range={'min_fee_satoshis': 50, 'max_fee_satoshis': 300})\n        self.assertTrue(any((\"bob->alice\" in msg and \"There is no overlap between between their and our fee range.\" in msg) for msg in logs.output))\n        self.assertTrue(any((\"alice->bob\" in msg and \"closing_signed not received, force closing.\" in msg) for msg in logs.output))\n        # note: \"Task exception was never retrieved\" for \"Exception: There is no overlap [...]\"\n        #       is because we don't start peer.main_loop and hence peer.taskgroup is never joined.\n\n    async def _test_shutdown(self, alice_fee, bob_fee, alice_fee_range=None, bob_fee_range=None):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        w1, w2 = graph.workers.values()\n        alice_channel, bob_channel = graph.channels.values()\n        w1.network.config.TEST_SHUTDOWN_FEE = alice_fee\n        w2.network.config.TEST_SHUTDOWN_FEE = bob_fee\n        if alice_fee_range is not None:\n            w1.network.config.TEST_SHUTDOWN_FEE_RANGE = alice_fee_range\n        else:\n            w1.network.config.TEST_SHUTDOWN_LEGACY = True\n        if bob_fee_range is not None:\n            w2.network.config.TEST_SHUTDOWN_FEE_RANGE = bob_fee_range\n        else:\n            w2.network.config.TEST_SHUTDOWN_LEGACY = True\n        w2.enable_htlc_settle = False\n        lnaddr, pay_req = self.prepare_invoice(w2)\n        async def pay():\n            await util.wait_for2(p1.initialized, 1)\n            await util.wait_for2(p2.initialized, 1)\n            # alice sends htlc\n            route = (await w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr))[0][0].route\n            p1.pay(route=route,\n                   chan=alice_channel,\n                   amount_msat=lnaddr.get_amount_msat(),\n                   total_msat=lnaddr.get_amount_msat(),\n                   payment_hash=lnaddr.paymenthash,\n                   min_final_cltv_delta=lnaddr.get_min_final_cltv_delta(),\n                   payment_secret=lnaddr.payment_secret)\n            await p2.received_commitsig_event.wait()\n            # alice closes\n            await p1.close_channel(alice_channel.channel_id)\n            gath.cancel()\n        async def set_settle():\n            await asyncio.sleep(0.1)\n            w2.enable_htlc_settle = True\n        gath = asyncio.gather(pay(), set_settle(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch())\n        with self.assertRaises(asyncio.CancelledError):\n            await gath\n\n    async def test_warning(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        alice_channel, bob_channel = graph.channels.values()\n\n        async def action():\n            await util.wait_for2(p1.initialized, 1)\n            await util.wait_for2(p2.initialized, 1)\n            p1.send_warning(alice_channel.channel_id, 'be warned!', close_connection=True)\n        gath = asyncio.gather(action(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch())\n        with self.assertRaises(GracefulDisconnect):\n            await gath\n\n    async def test_error(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        alice_channel, bob_channel = graph.channels.values()\n\n        async def action():\n            await util.wait_for2(p1.initialized, 1)\n            await util.wait_for2(p2.initialized, 1)\n            p1.send_error(alice_channel.channel_id, 'some error happened!', force_close_channel=True)\n            assert alice_channel.is_closed()\n            gath.cancel()\n        gath = asyncio.gather(action(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch())\n        with self.assertRaises(GracefulDisconnect):\n            await gath\n\n    async def test_close_upfront_shutdown_script(self):\n        alice_lnwallet, bob_lnwallet = self.prepare_lnwallets(self.GRAPH_DEFINITIONS['single_chan']).values()\n        alice_channel, bob_channel = create_test_channels(alice_lnwallet=alice_lnwallet, bob_lnwallet=bob_lnwallet)\n\n        # create upfront shutdown script for bob, alice doesn't use upfront\n        # shutdown script\n        bob_uss_pub = privkey_to_pubkey(os.urandom(32))\n        bob_uss_addr = bitcoin.pubkey_to_address('p2wpkh', bob_uss_pub.hex())\n        bob_uss = bitcoin.address_to_script(bob_uss_addr)\n\n        # bob commits to close to bob_uss\n        alice_channel.config[HTLCOwner.REMOTE].upfront_shutdown_script = bob_uss\n        # but bob closes to some receiving address, which we achieve by not\n        # setting the upfront shutdown script in the channel config\n        bob_channel.config[HTLCOwner.LOCAL].upfront_shutdown_script = b''\n\n        p1, p2, w1, w2 = self.prepare_peers(alice_channel, bob_channel)\n        w1.network.config.FEE_POLICY = 'feerate:5000'\n        w2.network.config.FEE_POLICY = 'feerate:1000'\n\n        async def test():\n            async def close():\n                await util.wait_for2(p1.initialized, 1)\n                await util.wait_for2(p2.initialized, 1)\n                # bob closes channel with different shutdown script\n                await p1.close_channel(alice_channel.channel_id)\n                self.fail(\"p1.close_channel should have raised above!\")\n\n            async def main_loop(peer):\n                    async with peer.taskgroup as group:\n                        await group.spawn(peer._message_loop())\n                        await group.spawn(peer.htlc_switch())\n\n            coros = [close(), main_loop(p1), main_loop(p2)]\n            gath = asyncio.gather(*coros)\n            await gath\n\n        with self.assertRaises(GracefulDisconnect):\n            await test()\n        # check that neither party broadcast a closing tx (as it was not even signed)\n        self.assertEqual(0, w1.network.tx_queue.qsize())\n        self.assertEqual(0, w2.network.tx_queue.qsize())\n\n        # -- new scenario:\n        # bob sends the same upfront_shutdown_script has he announced\n        alice_channel.config[HTLCOwner.REMOTE].upfront_shutdown_script = bob_uss\n        bob_channel.config[HTLCOwner.LOCAL].upfront_shutdown_script = bob_uss\n\n        p1, p2, w1, w2 = self.prepare_peers(alice_channel, bob_channel)\n        w1.network.config.FEE_POLICY = 'feerate:5000'\n        w2.network.config.FEE_POLICY = 'feerate:1000'\n\n        async def test():\n            async def close():\n                await util.wait_for2(p1.initialized, 1)\n                await util.wait_for2(p2.initialized, 1)\n                await p1.close_channel(alice_channel.channel_id)\n                gath.cancel()\n\n            async def main_loop(peer):\n                async with peer.taskgroup as group:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n\n            coros = [close(), main_loop(p1), main_loop(p2)]\n            gath = asyncio.gather(*coros)\n            await gath\n\n        with self.assertRaises(asyncio.CancelledError):\n            await test()\n\n        # check if p1 has broadcast the closing tx, and if it pays to Bob's uss\n        self.assertEqual(1, w1.network.tx_queue.qsize())\n        closing_tx = w1.network.tx_queue.get_nowait()  # type: Transaction\n        self.assertEqual(2, len(closing_tx.outputs()))\n        self.assertEqual(1, len(closing_tx.get_output_idxs_from_address(bob_uss_addr)))\n\n    async def test_channel_usage_after_closing(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n        w1, w2 = graph.workers.values()\n        alice_channel, bob_channel = graph.channels.values()\n        lnaddr, pay_req = self.prepare_invoice(w2)\n\n        lnaddr = w1._check_bolt11_invoice(pay_req.lightning_invoice)\n        shi = (await w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr))[0][0]\n        route, amount_msat = shi.route, shi.amount_msat\n        assert amount_msat == lnaddr.get_amount_msat()\n\n        await w1.force_close_channel(alice_channel.channel_id)\n        # check if a tx (commitment transaction) was broadcasted:\n        assert w1.network.tx_queue.qsize() == 1\n\n        with self.assertRaises(NoPathFound) as e:\n            await w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr)\n\n        peer = w1.lnpeermgr._peers[route[0].node_id]\n        # AssertionError is ok since we shouldn't use old routes, and the\n        # route finding should fail when channel is closed\n        async def f():\n            shi = SentHtlcInfo(\n                route=route,\n                payment_secret_orig=lnaddr.payment_secret,\n                payment_secret_bucket=lnaddr.payment_secret,\n                amount_msat=amount_msat,\n                bucket_msat=amount_msat,\n                amount_receiver_msat=amount_msat,\n                trampoline_fee_level=None,\n                trampoline_route=None,\n            )\n            paysession = w1._paysessions[lnaddr.paymenthash + lnaddr.payment_secret]\n            pay = w1.pay_to_route(\n                sent_htlc_info=shi,\n                paysession=paysession,\n                min_final_cltv_delta=lnaddr.get_min_final_cltv_delta(),\n            )\n            await asyncio.gather(pay, p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch())\n        with self.assertRaises(PaymentFailure):\n            await f()\n\n    async def test_sending_weird_messages_that_should_be_ignored(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n\n        async def send_weird_messages():\n            await util.wait_for2(p1.initialized, 1)\n            await util.wait_for2(p2.initialized, 1)\n            # peer1 sends known message with trailing garbage\n            # BOLT-01 says peer2 should ignore trailing garbage\n            raw_msg1 = encode_msg('ping', num_pong_bytes=4, byteslen=4) + bytes(range(55))\n            p1.transport.send_bytes(raw_msg1)\n            await asyncio.sleep(0.05)\n            # peer1 sends unknown 'odd-type' message\n            # BOLT-01 says peer2 should ignore whole message\n            raw_msg2 = (43333).to_bytes(length=2, byteorder=\"big\") + bytes(range(55))\n            p1.transport.send_bytes(raw_msg2)\n            await asyncio.sleep(0.05)\n            raise SuccessfulTest()\n\n        async def f():\n            async with OldTaskGroup() as group:\n                for peer in [p1, p2]:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n                for peer in [p1, p2]:\n                    await peer.initialized\n                await group.spawn(send_weird_messages())\n\n        with self.assertRaises(SuccessfulTest):\n            await f()\n\n    async def test_sending_weird_messages__unknown_even_type(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n\n        async def send_weird_messages():\n            await util.wait_for2(p1.initialized, 1)\n            await util.wait_for2(p2.initialized, 1)\n            # peer1 sends unknown 'even-type' message\n            # BOLT-01 says peer2 should close the connection\n            raw_msg2 = (43334).to_bytes(length=2, byteorder=\"big\") + bytes(range(55))\n            p1.transport.send_bytes(raw_msg2)\n            await asyncio.sleep(0.05)\n\n        failing_task = None\n        async def f():\n            nonlocal failing_task\n            async with OldTaskGroup() as group:\n                await group.spawn(p1._message_loop())\n                await group.spawn(p1.htlc_switch())\n                failing_task = await group.spawn(p2._message_loop())\n                await group.spawn(p2.htlc_switch())\n                for peer in [p1, p2]:\n                    await peer.initialized\n                await group.spawn(send_weird_messages())\n\n        with self.assertRaises(GracefulDisconnect):\n            await f()\n        self.assertTrue(isinstance(failing_task.exception().__cause__, lnmsg.UnknownMandatoryMsgType))\n\n    async def test_sending_weird_messages__known_msg_with_insufficient_length(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n        p1, p2 = graph.peers.values()\n\n        async def send_weird_messages():\n            await util.wait_for2(p1.initialized, 1)\n            await util.wait_for2(p2.initialized, 1)\n            # peer1 sends known message with insufficient length for the contents\n            # BOLT-01 says peer2 should fail the connection\n            raw_msg1 = encode_msg('ping', num_pong_bytes=4, byteslen=4)[:-1]\n            p1.transport.send_bytes(raw_msg1)\n            await asyncio.sleep(0.05)\n\n        failing_task = None\n        async def f():\n            nonlocal failing_task\n            async with OldTaskGroup() as group:\n                await group.spawn(p1._message_loop())\n                await group.spawn(p1.htlc_switch())\n                failing_task = await group.spawn(p2._message_loop())\n                await group.spawn(p2.htlc_switch())\n                for peer in [p1, p2]:\n                    await peer.initialized\n                await group.spawn(send_weird_messages())\n\n        with self.assertRaises(GracefulDisconnect):\n            await f()\n        self.assertTrue(isinstance(failing_task.exception().__cause__, lnmsg.UnexpectedEndOfStream))\n\n    async def test_hold_invoice_set_doesnt_get_expired(self):\n        \"\"\"\n        Alice pays a hold invoice from Bob, Bob doesn't release preimage. Verify that Bob doesn't\n        expire the htlc set MIN_FINAL_CLTV_DELTA_ACCEPTED blocks before htlc.cltv_abs (as we would do with normal htlc sets).\n        The htlc set should only get failed if the user of the hold invoice callback explicitly removes the\n        callback (e.g. after refunding and failing a swap), otherwise it should get timed out onchain (force-close).\n\n        This only tests hold invoice logic for hold invoices registered with `LNWallet.register_hold_invoice()`,\n        as used e.g. by submarine swaps. It doesn't cover the hold invoices created by the hold invoice CLI\n        which behave differently and use the persisted `LNWallet.dont_expire_htlcs` dict.\n        \"\"\"\n        async def run_test(test_trampoline):\n            graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n            alice_p, bob_p = graph.peers.values()\n            alice_w, bob_w = graph.workers.values()\n\n            lnaddr, pay_req = self.prepare_invoice(bob_w, min_final_cltv_delta=150)\n            del bob_w._preimages[pay_req.rhash]  # del preimage so bob doesn't settle\n            payment_key = bob_w._get_payment_key(lnaddr.paymenthash).hex()\n\n            cb_got_called = False\n            async def cb(_payment_hash):\n                self.logger.debug(f\"hold invoice callback called. {bob_w.network.get_local_height()=}\")\n                nonlocal cb_got_called\n                cb_got_called = True\n\n            bob_w.register_hold_invoice(lnaddr.paymenthash, cb)\n\n            async def check_mpp_state():\n                async def wait_for_resolution():\n                    while True:\n                        await asyncio.sleep(0.1)\n                        if payment_key not in bob_w.received_mpp_htlcs:\n                            continue\n                        if not bob_w.received_mpp_htlcs[payment_key].resolution == RecvMPPResolution.SETTLING:\n                            continue\n                        return\n                await util.wait_for2(wait_for_resolution(), timeout=2)\n                assert cb_got_called\n                mpp_set = bob_w.received_mpp_htlcs[payment_key]\n                self.assertEqual(mpp_set.resolution, RecvMPPResolution.SETTLING, msg=mpp_set.resolution)\n                self.assertEqual(len(mpp_set.htlcs), 1, f\"should get only one htlc: {mpp_set.htlcs=}\")\n                left_to_expiry = next(iter(mpp_set.htlcs)).htlc.cltv_abs - bob_w.network.get_local_height()\n                # now mine up to one block after the expiry\n                bob_w.network._blockchain._height += left_to_expiry + 1\n                await asyncio.sleep(0.2)\n                # bob still has the mpp set and it is not failed\n                # it should only get removed once the channel is redeemed\n                self.assertIn(bob_w.received_mpp_htlcs[payment_key].resolution, (RecvMPPResolution.COMPLETE, RecvMPPResolution.SETTLING))\n                # now also check that the mpp set will get set failed if the hold invoice\n                # is being explicitly unregistered, and we don't have a preimage to settle it\n                bob_w.unregister_hold_invoice(lnaddr.paymenthash)\n                self.assertEqual(bob_w.received_mpp_htlcs[payment_key].resolution, RecvMPPResolution.FAILED)\n                raise SuccessfulTest()\n\n            if test_trampoline:\n                await self._activate_trampoline(alice_w)\n                # declare bob as trampoline node\n                electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n                    'bob': LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=bob_w.node_keypair.pubkey),\n                }\n\n            async def f():\n                async with OldTaskGroup() as group:\n                    await group.spawn(alice_p._message_loop())\n                    await group.spawn(alice_p.htlc_switch())\n                    await group.spawn(bob_p._message_loop())\n                    await group.spawn(bob_p.htlc_switch())\n                    await asyncio.sleep(0.01)\n                    await group.spawn(alice_w.pay_invoice(pay_req))\n                    await group.spawn(check_mpp_state())\n\n            with self.assertRaises(SuccessfulTest):\n                await f()\n\n        for _test_trampoline in [False, True]:\n            await run_test(_test_trampoline)\n\n    async def test_htlc_switch_iteration_benchmark(self):\n        \"\"\"Test how long a call to _run_htlc_switch_iteration takes with 10 trampoline\n        mpp sets of 1 htlc each. Raise if it takes longer than 20ms (median).\n        To create flamegraph with py-spy raise NUM_ITERATIONS to 1000 (for more samples) then run:\n        $ py-spy record -o flamegraph.svg --subprocesses -- python -m pytest tests/test_lnpeer.py::TestPeerDirect::test_htlc_switch_iteration_benchmark\n        \"\"\"\n        NUM_ITERATIONS = 25\n        alice_lnwallet, bob_lnwallet = self.prepare_lnwallets(self.GRAPH_DEFINITIONS['single_chan']).values()\n        alice_channel, bob_channel = create_test_channels(\n            alice_lnwallet=alice_lnwallet, bob_lnwallet=bob_lnwallet, max_accepted_htlcs=20,\n        )\n        alice_p, bob_p, alice_w, bob_w = self.prepare_peers(alice_channel, bob_channel)\n\n        await self._activate_trampoline(alice_w)\n        electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n            'bob': LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=bob_w.node_keypair.pubkey),\n        }\n\n        # create 10 invoices (10 pending htlc sets with 1 htlc each)\n        invoices = []  # type: list[tuple[LnAddr, Invoice]]\n        for i in range(10):\n            lnaddr, pay_req = self.prepare_invoice(bob_w)\n            # prevent bob from settling so that htlc switch will have to iterate through all pending htlcs\n            bob_w.dont_settle_htlcs[pay_req.rhash] = None\n            invoices.append((lnaddr, pay_req))\n        self.assertEqual(len(invoices), 10, msg=len(invoices))\n\n        iterations = []\n        do_benchmark = False\n        _run_bob_htlc_switch_iteration = bob_p._run_htlc_switch_iteration\n        def timed_htlc_switch_iteration():\n            start = time.perf_counter()\n            _run_bob_htlc_switch_iteration()\n            duration = time.perf_counter() - start\n            if do_benchmark:\n                iterations.append(duration)\n        bob_p._run_htlc_switch_iteration = timed_htlc_switch_iteration\n\n        async def benchmark_htlc_switch_iterations():\n            waited = 0\n            while not len(bob_w.received_mpp_htlcs) == 10 :\n                waited += 0.1\n                await asyncio.sleep(0.1)\n                if waited > 2:\n                    raise TimeoutError()\n            nonlocal do_benchmark\n            do_benchmark = True\n            while len(iterations) < NUM_ITERATIONS:\n                await asyncio.sleep(0.05)\n            # average = sum(iterations) / len(iterations)\n            median_duration = statistics.median(iterations)\n            res = f\"median duration per htlc switch iteration: {median_duration:.6f}s over {len(iterations)=}\"\n            self.logger.info(res)\n            self.assertLess(median_duration, 0.02, msg=res)\n            raise SuccessfulTest()\n\n        async def f():\n            async with OldTaskGroup() as group:\n                await group.spawn(alice_p._message_loop())\n                await group.spawn(alice_p.htlc_switch())\n                await group.spawn(bob_p._message_loop())\n                await group.spawn(bob_p.htlc_switch())\n                await asyncio.sleep(0.01)\n                for _lnaddr, req in invoices:\n                    await group.spawn(alice_w.pay_invoice(req))\n                await benchmark_htlc_switch_iterations()\n\n        with self.assertRaises(SuccessfulTest):\n            await f()\n\n    async def test_dont_expire_htlcs(self):\n        \"\"\"\n        Test that htlcs registered in LNWallet.dont_expire_htlcs don't get expired before the\n        specified expiry delta if their preimage isn't available.\n        Also test that htlcs registered in LNWallet.dont_expire_htlcs get settled right away if their\n        preimage is available.\n        \"\"\"\n        async def run_test(test_trampoline, test_expiry):\n            graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])\n            p1, p2 = graph.peers.values()\n            w1, w2 = graph.workers.values()\n            if test_trampoline:\n                await self._activate_trampoline(w1)\n                # declare bob as trampoline node\n                electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n                    'bob': LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=w2.node_keypair.pubkey),\n                }\n\n            preimage = os.urandom(32)\n            lnaddr, pay_req = self.prepare_invoice(w2, payment_preimage=preimage, min_final_cltv_delta=144)\n\n            # delete preimage, this would fail the htlcs if payment_hash wasn't in dont_expire_htlcs\n            del w2._preimages[pay_req.rhash]\n            # add payment_hash to dont_expire_htlcs so the htlcs are not getting failed\n            w2.dont_expire_htlcs[pay_req.rhash] = None if not test_expiry else 20\n\n            async def pay(lnaddr, pay_req):\n                self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n                result, log = await util.wait_for2(w1.pay_invoice(pay_req), timeout=3)\n                if result is True:\n                    self.assertEqual(PR_PAID, w2.get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n                    return PaymentDone()\n                else:\n                    self.assertIsNone(w2.get_preimage(lnaddr.paymenthash))\n                    return PaymentFailure()\n\n            async def wait_for_htlcs():\n                payment_key = w2._get_payment_key(lnaddr.paymenthash)\n                while payment_key.hex() not in w2.received_mpp_htlcs:\n                    await asyncio.sleep(0.05)\n                if not test_expiry:\n                    # the htlcs should never get expired if the dont_expire_htlcs value is None\n                    w2.network.blockchain()._height += 1000\n                await asyncio.sleep(0.25)  # give w2 some time to do mistakes\n                self.assertEqual(w2.received_mpp_htlcs[payment_key.hex()].resolution, RecvMPPResolution.COMPLETE)\n                if test_expiry:\n                    # we set an expiry delta of 20 blocks before expiry, htlc expiry should be +144 current height\n                    # so adding some blocks should get the htlcs failed\n                    w2.network.blockchain()._height += 50\n                    await asyncio.sleep(0.1)\n                    # the htlcs should not get failed yet as 144-50 > 20\n                    self.assertEqual(w2.received_mpp_htlcs[payment_key.hex()].resolution, RecvMPPResolution.COMPLETE)\n                    w2.network.blockchain()._height += 75\n                    return  # the htlcs should get failed and pay should return PaymentFailure\n\n                # saving the preimage should let the htlcs get fulfilled\n                w2.save_preimage(lnaddr.paymenthash, preimage)\n\n            async def f():\n                async with OldTaskGroup() as group:\n                    await group.spawn(p1._message_loop())\n                    await group.spawn(p1.htlc_switch())\n                    await group.spawn(p2._message_loop())\n                    await group.spawn(p2.htlc_switch())\n                    await asyncio.sleep(0.01)\n                    invoice_features = lnaddr.get_features()\n                    self.assertFalse(invoice_features.supports(LnFeatures.BASIC_MPP_OPT))\n                    pay_task = await group.spawn(pay(lnaddr, pay_req))\n                    await util.wait_for2(wait_for_htlcs(), timeout=3)\n                    raise await pay_task\n\n            await f()\n\n        for test_trampoline in [False, True]:\n            for test_expiry in [False, True]:\n                with self.assertRaises(PaymentFailure if test_expiry else PaymentDone):\n                    await run_test(test_trampoline, test_expiry )\n\n\nclass TestPeerForwarding(TestPeer):\n\n    async def test_payment_in_graph_with_direct_channel(self):\n        \"\"\"Test payment over a direct channel where sender has multiple available channels.\"\"\"\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['line_graph'])\n        peers = graph.peers.values()\n        # use same MPP_SPLIT_PART_FRACTION as in regular LNWallet\n        graph.workers['bob'].MPP_SPLIT_PART_FRACTION = LNWallet.MPP_SPLIT_PART_FRACTION\n\n        # mock split_amount_normal so it's possible to test both cases, the amount getting sorted\n        # out because one part is below the min size and the other case of both parts being just\n        # above the min size, so no part is getting sorted out\n        def mocked_split_amount_normal(total_amount: int, num_parts: int) -> List[int]:\n            if num_parts == 2 and total_amount == 21_000_000:  # test amount 21k sat\n                # this will not get sorted out by suggest_splits\n                return [10_500_000, 10_500_000]\n            elif num_parts == 2 and total_amount == 21_000_001:  # 2nd test case\n                # this will get sorted out by suggest_splits\n                return [11_000_002, 9_999_999]\n            else:\n                return split_amount_normal(total_amount, num_parts)\n\n        async def pay(lnaddr, pay_req):\n            self.assertEqual(PR_UNPAID, graph.workers['alice'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            with mock.patch('electrum.mpp_split.split_amount_normal',\n                                side_effect=mocked_split_amount_normal):\n                result, log = await graph.workers['bob'].pay_invoice(pay_req)\n            self.assertTrue(result)\n            self.assertEqual(PR_PAID, graph.workers['alice'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n\n        async def f():\n            async with OldTaskGroup() as group:\n                for peer in peers:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n                for peer in peers:\n                    await peer.initialized\n                for test in [21_000_000, 21_000_001]:\n                    lnaddr, pay_req = self.prepare_invoice(\n                        graph.workers['alice'],\n                        amount_msat=test,\n                        include_routing_hints=True,\n                        invoice_features=LnFeatures.BASIC_MPP_OPT\n                                         | LnFeatures.PAYMENT_SECRET_REQ\n                                         | LnFeatures.VAR_ONION_REQ\n                    )\n                    await pay(lnaddr, pay_req)\n                raise PaymentDone()\n        with self.assertRaises(PaymentDone):\n            await f()\n\n    async def test_payment_multihop(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])\n        peers = graph.peers.values()\n        async def pay(lnaddr, pay_req):\n            self.assertEqual(PR_UNPAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            result, log = await graph.workers['alice'].pay_invoice(pay_req)\n            self.assertTrue(result)\n            self.assertEqual(PR_PAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            raise PaymentDone()\n        async def f():\n            async with OldTaskGroup() as group:\n                for peer in peers:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n                for peer in peers:\n                    await peer.initialized\n                lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], include_routing_hints=True)\n                await group.spawn(pay(lnaddr, pay_req))\n        with self.assertRaises(PaymentDone):\n            await f()\n\n    async def test_payment_multihop_with_preselected_path(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])\n        peers = graph.peers.values()\n        async def pay(pay_req):\n            with self.subTest(msg=\"bad path: edges do not chain together\"):\n                path = [PathEdge(start_node=graph.workers['alice'].node_keypair.pubkey,\n                                 end_node=graph.workers['carol'].node_keypair.pubkey,\n                                 short_channel_id=graph.channels[('alice', 'bob')].short_channel_id),\n                        PathEdge(start_node=graph.workers['bob'].node_keypair.pubkey,\n                                 end_node=graph.workers['dave'].node_keypair.pubkey,\n                                 short_channel_id=graph.channels['bob', 'dave'].short_channel_id)]\n                with self.assertRaises(LNPathInconsistent):\n                    await graph.workers['alice'].pay_invoice(pay_req, full_path=path)\n            with self.subTest(msg=\"bad path: last node id differs from invoice pubkey\"):\n                path = [PathEdge(start_node=graph.workers['alice'].node_keypair.pubkey,\n                                 end_node=graph.workers['bob'].node_keypair.pubkey,\n                                 short_channel_id=graph.channels[('alice', 'bob')].short_channel_id)]\n                with self.assertRaises(LNPathInconsistent):\n                    await graph.workers['alice'].pay_invoice(pay_req, full_path=path)\n            with self.subTest(msg=\"good path\"):\n                path = [PathEdge(start_node=graph.workers['alice'].node_keypair.pubkey,\n                                 end_node=graph.workers['bob'].node_keypair.pubkey,\n                                 short_channel_id=graph.channels[('alice', 'bob')].short_channel_id),\n                        PathEdge(start_node=graph.workers['bob'].node_keypair.pubkey,\n                                 end_node=graph.workers['dave'].node_keypair.pubkey,\n                                 short_channel_id=graph.channels['bob', 'dave'].short_channel_id)]\n                result, log = await graph.workers['alice'].pay_invoice(pay_req, full_path=path)\n                self.assertTrue(result)\n                self.assertEqual(\n                    [edge.short_channel_id for edge in path],\n                    [edge.short_channel_id for edge in log[0].route])\n            raise PaymentDone()\n        async def f():\n            async with OldTaskGroup() as group:\n                for peer in peers:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n                for peer in peers:\n                    await peer.initialized\n                lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], include_routing_hints=True)\n                await group.spawn(pay(pay_req))\n        with self.assertRaises(PaymentDone):\n            await f()\n\n    async def test_payment_multihop_temp_node_failure(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])\n        graph.workers['bob'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True\n        graph.workers['carol'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True\n        peers = graph.peers.values()\n        async def pay(lnaddr, pay_req):\n            self.assertEqual(PR_UNPAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            result, log = await graph.workers['alice'].pay_invoice(pay_req)\n            self.assertFalse(result)\n            self.assertEqual(PR_UNPAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            self.assertEqual(OnionFailureCode.TEMPORARY_NODE_FAILURE, log[0].failure_msg.code)\n            raise PaymentDone()\n        async def f():\n            async with OldTaskGroup() as group:\n                for peer in peers:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n                for peer in peers:\n                    await peer.initialized\n                lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], include_routing_hints=True)\n                await group.spawn(pay(lnaddr, pay_req))\n        with self.assertRaises(PaymentDone):\n            await f()\n\n    async def test_payment_multihop_route_around_failure(self):\n        # Alice will pay Dave. Alice first tries A->C->D route, due to lower fees, but Carol\n        # will fail the htlc and get blacklisted. Alice will then try A->B->D and succeed.\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])\n        graph.workers['carol'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True\n        peers = graph.peers.values()\n        async def pay(lnaddr, pay_req):\n            self.assertEqual(500000000000, graph.channels[('alice', 'bob')].balance(LOCAL))\n            self.assertEqual(500000000000, graph.channels[('dave', 'bob')].balance(LOCAL))\n            self.assertEqual(PR_UNPAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            result, log = await graph.workers['alice'].pay_invoice(pay_req, attempts=2)\n            self.assertEqual(2, len(log))\n            self.assertTrue(result)\n            self.assertEqual(PR_PAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            self.assertEqual([graph.channels[('alice', 'carol')].short_channel_id, graph.channels[('carol', 'dave')].short_channel_id],\n                             [edge.short_channel_id for edge in log[0].route])\n            self.assertEqual([graph.channels[('alice', 'bob')].short_channel_id, graph.channels[('bob', 'dave')].short_channel_id],\n                             [edge.short_channel_id for edge in log[1].route])\n            self.assertEqual(OnionFailureCode.TEMPORARY_NODE_FAILURE, log[0].failure_msg.code)\n            self.assertEqual(499899450000, graph.channels[('alice', 'bob')].balance(LOCAL))\n            await asyncio.sleep(0.2)  # wait for COMMITMENT_SIGNED / REVACK msgs to update balance\n            self.assertEqual(500100000000, graph.channels[('dave', 'bob')].balance(LOCAL))\n            raise PaymentDone()\n        async def f():\n            async with OldTaskGroup() as group:\n                for peer in peers:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n                for peer in peers:\n                    await peer.initialized\n                lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], include_routing_hints=True)\n                invoice_features = lnaddr.get_features()\n                self.assertFalse(invoice_features.supports(LnFeatures.BASIC_MPP_OPT))\n                await group.spawn(pay(lnaddr, pay_req))\n        with self.assertRaises(PaymentDone):\n            await f()\n\n    async def test_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(self):\n        # This test checks that the following attack does not work:\n        #   - Bob creates payment request with HASH1, for 1 BTC; and gives the payreq to Alice\n        #   - Alice sends htlc A->B->D, for 100k sat, with HASH1\n        #   - Bob must not release the preimage of HASH1\n        graph_def = self.GRAPH_DEFINITIONS['square_graph']\n        graph_def.pop('carol')\n        graph_def['alice']['channels'].pop('carol')\n        # now graph is linear: A <-> B <-> D\n        graph = self.prepare_chans_and_peers_in_graph(graph_def)\n        peers = graph.peers.values()\n        async def pay():\n            lnaddr1, pay_req1 = self.prepare_invoice(\n                graph.workers['bob'],\n                amount_msat=100_000_000_000,\n            )\n            lnaddr2, pay_req2 = self.prepare_invoice(\n                graph.workers['dave'],\n                amount_msat=100_000_000,\n                payment_hash=lnaddr1.paymenthash,  # Dave is cooperating with Alice, and he reuses Bob's hash\n                include_routing_hints=True,\n            )\n            with self.subTest(msg=\"try to make Bob forward in legacy (non-trampoline) mode\"):\n                result, log = await graph.workers['alice'].pay_invoice(pay_req2, attempts=1)\n                self.assertFalse(result)\n                self.assertEqual(OnionFailureCode.TEMPORARY_NODE_FAILURE, log[0].failure_msg.code)\n                self.assertEqual(None, graph.workers['alice'].get_preimage(lnaddr1.paymenthash))\n            with self.subTest(msg=\"try to make Bob forward in trampoline mode\"):\n                # declare Bob as trampoline forwarding node\n                electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n                    graph.workers['bob'].name: LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey),\n                }\n                await self._activate_trampoline(graph.workers['alice'])\n                result, log = await graph.workers['alice'].pay_invoice(pay_req2, attempts=5)\n                self.assertFalse(result)\n                self.assertEqual(OnionFailureCode.TEMPORARY_NODE_FAILURE, log[0].failure_msg.code)\n                self.assertEqual(None, graph.workers['alice'].get_preimage(lnaddr1.paymenthash))\n            raise SuccessfulTest()\n\n        async def f():\n            async with OldTaskGroup() as group:\n                for peer in peers:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n                for peer in peers:\n                    await peer.initialized\n                await group.spawn(pay())\n\n        with self.assertRaises(SuccessfulTest):\n            await f()\n\n    async def test_payment_with_temp_channel_failure_and_liquidity_hints(self):\n        # prepare channels such that a temporary channel failure happens at c->d\n        graph_definition = self.GRAPH_DEFINITIONS['square_graph']\n        graph_definition['alice']['channels']['carol']['local_balance_msat'] = 200_000_000\n        graph_definition['alice']['channels']['carol']['remote_balance_msat'] = 200_000_000\n        graph_definition['carol']['channels']['dave']['local_balance_msat'] = 50_000_000\n        graph_definition['carol']['channels']['dave']['remote_balance_msat'] = 200_000_000\n        graph_definition['alice']['channels']['bob']['local_balance_msat'] = 200_000_000\n        graph_definition['alice']['channels']['bob']['remote_balance_msat'] = 200_000_000\n        graph_definition['bob']['channels']['dave']['local_balance_msat'] = 200_000_000\n        graph_definition['bob']['channels']['dave']['remote_balance_msat'] = 200_000_000\n        graph = self.prepare_chans_and_peers_in_graph(graph_definition)\n\n        # the payment happens in two attempts:\n        # 1. along a->c->d due to low fees with temp channel failure:\n        #   with chanupd: ORPHANED, private channel update\n        #   c->d gets a liquidity hint and gets blocked\n        # 2. along a->b->d with success\n        amount_to_pay = 100_000_000\n        peers = graph.peers.values()\n        async def pay(lnaddr, pay_req):\n            self.assertEqual(PR_UNPAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            result, log = await graph.workers['alice'].pay_invoice(pay_req, attempts=3)\n            self.assertTrue(result)\n            self.assertEqual(2, len(log))\n            self.assertEqual(PR_PAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            self.assertEqual(OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, log[0].failure_msg.code)\n\n            liquidity_hints = graph.workers['alice'].network.path_finder.liquidity_hints\n            pubkey_a = graph.workers['alice'].node_keypair.pubkey\n            pubkey_b = graph.workers['bob'].node_keypair.pubkey\n            pubkey_c = graph.workers['carol'].node_keypair.pubkey\n            pubkey_d = graph.workers['dave'].node_keypair.pubkey\n            # check liquidity hints for failing route:\n            hint_ac = liquidity_hints.get_hint(graph.channels[('alice', 'carol')].short_channel_id)\n            hint_cd = liquidity_hints.get_hint(graph.channels[('carol', 'dave')].short_channel_id)\n            self.assertEqual(amount_to_pay, hint_ac.can_send(pubkey_a < pubkey_c))\n            self.assertEqual(None, hint_ac.cannot_send(pubkey_a < pubkey_c))\n            self.assertEqual(None, hint_cd.can_send(pubkey_c < pubkey_d))\n            self.assertEqual(amount_to_pay, hint_cd.cannot_send(pubkey_c < pubkey_d))\n            # check liquidity hints for successful route:\n            hint_ab = liquidity_hints.get_hint(graph.channels[('alice', 'bob')].short_channel_id)\n            hint_bd = liquidity_hints.get_hint(graph.channels[('bob', 'dave')].short_channel_id)\n            self.assertEqual(amount_to_pay, hint_ab.can_send(pubkey_a < pubkey_b))\n            self.assertEqual(None, hint_ab.cannot_send(pubkey_a < pubkey_b))\n            self.assertEqual(amount_to_pay, hint_bd.can_send(pubkey_b < pubkey_d))\n            self.assertEqual(None, hint_bd.cannot_send(pubkey_b < pubkey_d))\n\n            raise PaymentDone()\n        async def f():\n            async with OldTaskGroup() as group:\n                for peer in peers:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n                for peer in peers:\n                    await peer.initialized\n                lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], amount_msat=amount_to_pay, include_routing_hints=True)\n                await group.spawn(pay(lnaddr, pay_req))\n        with self.assertRaises(PaymentDone):\n            await f()\n\n    async def _run_mpp(self, graph, kwargs):\n        \"\"\"Tests a multipart payment scenario for failing and successful cases.\"\"\"\n        self.assertEqual(500_000_000_000, graph.channels[('alice', 'bob')].balance(LOCAL))\n        self.assertEqual(500_000_000_000, graph.channels[('alice', 'carol')].balance(LOCAL))\n        amount_to_pay = 600_000_000_000\n        peers = graph.peers.values()\n        async def pay(\n                attempts=1,\n                alice_uses_trampoline=False,\n                bob_forwarding=True,\n                mpp_invoice=True,\n                disable_trampoline_receiving=False,\n                test_hold_invoice=False,\n                test_failure=False,\n        ):\n            alice_w = graph.workers['alice']\n            bob_w = graph.workers['bob']\n            carol_w = graph.workers['carol']\n            dave_w = graph.workers['dave']\n            if mpp_invoice:\n                dave_w.features |= LnFeatures.BASIC_MPP_OPT\n            if disable_trampoline_receiving:\n                dave_w.features &= ~LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM\n            if not bob_forwarding:\n                bob_w.enable_htlc_forwarding = False\n            if alice_uses_trampoline:\n                await self._activate_trampoline(alice_w)\n            else:\n                assert alice_w.network.channel_db is not None\n            lnaddr, pay_req = self.prepare_invoice(dave_w, include_routing_hints=True, amount_msat=amount_to_pay)\n            self.prepare_recipient(dave_w, lnaddr.paymenthash, test_hold_invoice, test_failure)\n            self.assertEqual(PR_UNPAID, dave_w.get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            result, log = await alice_w.pay_invoice(pay_req, attempts=attempts)\n            if not bob_forwarding:\n                # reset to previous state, sleep 2s so that the second htlc can time out\n                graph.workers['bob'].enable_htlc_forwarding = True\n                await asyncio.sleep(2)\n            if result:\n                self.assertEqual(PR_PAID, dave_w.get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n                # check mpp is cleaned up\n                async with OldTaskGroup() as g:\n                    for peer in peers:\n                        await g.spawn(peer.wait_one_htlc_switch_iteration())\n                # wait another iteration\n                async with OldTaskGroup() as g:\n                    for peer in peers:\n                        await g.spawn(peer.wait_one_htlc_switch_iteration())\n                for peer in peers:\n                    self.assertEqual(len(peer.lnworker.received_mpp_htlcs), 0)\n                raise PaymentDone()\n            elif len(log) == 1 and log[0].failure_msg.code == OnionFailureCode.MPP_TIMEOUT:\n                raise PaymentTimeout()\n            else:\n                raise NoPathFound()\n\n        async with OldTaskGroup() as group:\n            for peer in peers:\n                await group.spawn(peer._message_loop())\n                await group.spawn(peer.htlc_switch())\n            for peer in peers:\n                await peer.initialized\n            await group.spawn(pay(**kwargs))\n\n    async def test_payment_multipart(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])\n        with self.assertRaises(PaymentDone):\n            await self._run_mpp(graph, {})\n\n    async def test_payment_multipart_with_hold_invoice(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])\n        with self.assertRaises(PaymentDone):\n            await self._run_mpp(graph, {'test_hold_invoice': True})\n\n    async def test_payment_multipart_with_timeout(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])\n        with self.assertRaises(PaymentTimeout):\n            await self._run_mpp(graph, {'bob_forwarding': False})\n\n    async def test_payment_multipart_wrong_invoice(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])\n        with self.assertRaises(NoPathFound):\n            await self._run_mpp(graph, {'mpp_invoice': False})\n\n    async def test_payment_multipart_trampoline_e2e(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])\n        electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n            graph.workers['bob'].name: LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey),\n            graph.workers['carol'].name: LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=graph.workers['carol'].node_keypair.pubkey),\n        }\n        # end-to-end trampoline: we attempt\n        # * a payment with one trial: fails, because\n        #   we need at least one trial because the initial fees are too low\n        # * a payment with several trials: should succeed\n        with self.assertRaises(NoPathFound):\n            await self._run_mpp(graph, {'alice_uses_trampoline': True, 'attempts': 1})\n        with self.assertRaises(PaymentDone):\n            await self._run_mpp(graph,{'alice_uses_trampoline': True, 'attempts': 30})\n\n    async def test_payment_multipart_trampoline_legacy(self):\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])\n        electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n            graph.workers['bob'].name: LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey),\n            graph.workers['carol'].name: LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=graph.workers['carol'].node_keypair.pubkey),\n        }\n        # trampoline-to-legacy: this is restricted, as there are no forwarders capable of doing this\n        with self.assertRaises(NoPathFound):\n            await self._run_mpp(graph, {'alice_uses_trampoline': True, 'attempts': 30, 'disable_trampoline_receiving': True})\n\n    async def test_fail_pending_htlcs_on_shutdown(self):\n        \"\"\"Alice tries to pay Dave via MPP. Dave receives some HTLCs but not all.\n        Dave shuts down (stops wallet).\n        We test if Dave fails the pending HTLCs during shutdown.\n        \"\"\"\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])\n        self.assertEqual(500_000_000_000, graph.channels[('alice', 'bob')].balance(LOCAL))\n        self.assertEqual(500_000_000_000, graph.channels[('alice', 'carol')].balance(LOCAL))\n        amount_to_pay = 600_000_000_000\n        peers = graph.peers.values()\n        graph.workers['dave'].MPP_EXPIRY = 120\n        graph.workers['dave'].TIMEOUT_SHUTDOWN_FAIL_PENDING_HTLCS = 3\n        async def pay():\n            graph.workers['dave'].features |= LnFeatures.BASIC_MPP_OPT\n            graph.workers['bob'].enable_htlc_forwarding = False  # Bob will hold forwarded HTLCs\n            assert graph.workers['alice'].network.channel_db is not None\n            lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], include_routing_hints=True, amount_msat=amount_to_pay)\n            result, log = await graph.workers['alice'].pay_invoice(pay_req, attempts=1)\n        async def stop():\n            hm = graph.channels[('dave', 'carol')].hm\n            while len(hm.htlcs(LOCAL)) == 0 or len(hm.htlcs(REMOTE)) == 0:\n                await asyncio.sleep(0.1)\n            self.assertTrue(len(hm.htlcs(LOCAL)) > 0)\n            self.assertTrue(len(hm.htlcs(REMOTE)) > 0)\n            await graph.workers['dave'].stop()\n            # Dave is supposed to have failed the pending incomplete MPP HTLCs\n            self.assertEqual(0, len(hm.htlcs(LOCAL)))\n            self.assertEqual(0, len(hm.htlcs(REMOTE)))\n            raise SuccessfulTest()\n\n        async def f():\n            async with OldTaskGroup() as group:\n                for peer in peers:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n                for peer in peers:\n                    await peer.initialized\n                await group.spawn(pay())\n                await group.spawn(stop())\n\n        with self.assertRaises(SuccessfulTest):\n            await f()\n\n    async def _run_trampoline_payment(\n            self, graph: Graph, *,\n            include_routing_hints=True,\n            test_hold_invoice=False,\n            test_failure=False,\n            attempts=2,\n            sender_name=\"alice\",\n            destination_name=\"dave\",\n            tf_names=(\"bob\", \"carol\"),\n    ):\n\n        sender_w = graph.workers[sender_name]\n        dest_w = graph.workers[destination_name]\n\n        async def pay(lnaddr, pay_req):\n            self.assertEqual(PR_UNPAID, dest_w.get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            result, log = await sender_w.pay_invoice(pay_req, attempts=attempts)\n            async with OldTaskGroup() as g:\n                for peer in peers:\n                    await g.spawn(peer.wait_one_htlc_switch_iteration())\n            async with OldTaskGroup() as g:\n                for peer in peers:\n                    await g.spawn(peer.wait_one_htlc_switch_iteration())\n            for peer in peers:\n                self.assertEqual(len(peer.lnworker.active_forwardings), 0)\n            if result:\n                self.assertEqual(PR_PAID, dest_w.get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n                raise PaymentDone()\n            else:\n                raise NoPathFound()\n\n        async def f():\n            await self._activate_trampoline(sender_w)\n            async with OldTaskGroup() as group:\n                for peer in peers:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n                for peer in peers:\n                    await peer.initialized\n                lnaddr, pay_req = self.prepare_invoice(dest_w, include_routing_hints=include_routing_hints)\n                self.prepare_recipient(dest_w, lnaddr.paymenthash, test_hold_invoice, test_failure)\n                await group.spawn(pay(lnaddr, pay_req))\n\n        peers = graph.peers.values()\n\n        # declare routing nodes as trampoline nodes\n        electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {}\n        for tf_name in tf_names:\n            peer_addr = LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=graph.workers[tf_name].node_keypair.pubkey)\n            electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS[graph.workers[tf_name].name] = peer_addr\n\n        await f()\n\n    def create_square_graph(self, *, direct=False, test_mpp_consolidation=False, is_legacy=False):\n        graph_definition = self.GRAPH_DEFINITIONS['square_graph']\n        if not direct:\n            # deplete channel from alice to carol and from bob to dave\n            graph_definition['alice']['channels']['carol'] = depleted_channel\n            graph_definition['bob']['channels']['dave'] = depleted_channel\n            # insert a channel from bob to carol\n            graph_definition['bob']['channels']['carol'] = low_fee_channel\n            # now the only route possible is alice -> bob -> carol -> dave\n        if test_mpp_consolidation:\n            # deplete alice to carol so that all htlcs go through bob\n            graph_definition['alice']['channels']['carol'] = depleted_channel\n        graph = self.prepare_chans_and_peers_in_graph(graph_definition)\n        if test_mpp_consolidation:\n            graph.workers['dave'].features |= LnFeatures.BASIC_MPP_OPT\n            graph.workers['alice'].network.config.TEST_FORCE_MPP = True # trampoline must wait until all incoming htlcs are received before sending outgoing htlcs\n            graph.workers['bob'].network.config.TEST_FORCE_MPP = True   # trampoline must wait until all outgoing htlcs have failed before failing incoming htlcs\n        if is_legacy:\n            # turn off trampoline features in invoice\n            graph.workers['dave'].features = graph.workers['dave'].features ^ LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM\n        return graph\n\n    async def test_trampoline_mpp_consolidation(self):\n        with self.assertRaises(PaymentDone):\n            graph = self.create_square_graph(direct=False, test_mpp_consolidation=True, is_legacy=True)\n            await self._run_trampoline_payment(graph)\n\n    async def test_trampoline_mpp_consolidation_forwarding_amount(self):\n        \"\"\"sanity check that bob is forwarding less than he is receiving\"\"\"\n        # alice->bob->carol->dave\n        graph = self.create_square_graph(direct=False, test_mpp_consolidation=True, is_legacy=True)\n        # bump alices trampoline fee level so the first payment succeeds and the htlc sums can be compared usefully below.\n        alice = graph.workers['alice']\n        alice.config.INITIAL_TRAMPOLINE_FEE_LEVEL = 6\n        with self.assertRaises(PaymentDone):\n            await self._run_trampoline_payment(graph, attempts=1)\n\n        # assert bob hasn't forwarded more than he received\n        bob_alice_channel = graph.channels[('bob', 'alice')]\n        htlcs_bob_received_from_alice = bob_alice_channel.hm.all_htlcs_ever()\n        bob_carol_channel = graph.channels[('bob', 'carol')]\n        htlcs_bob_sent_to_carol = bob_carol_channel.hm.all_htlcs_ever()\n        sum_bob_received = sum(htlc.amount_msat for (direction, htlc) in htlcs_bob_received_from_alice)\n        sum_bob_sent = sum(htlc.amount_msat for (direction, htlc) in htlcs_bob_sent_to_carol)\n        assert sum_bob_sent < sum_bob_received, f\"{sum_bob_sent=} > {sum_bob_received=}\"\n\n    async def test_trampoline_mpp_consolidation_with_hold_invoice(self):\n        with self.assertRaises(PaymentDone):\n            graph = self.create_square_graph(direct=False, test_mpp_consolidation=True, is_legacy=True)\n            await self._run_trampoline_payment(graph, test_hold_invoice=True)\n\n    async def test_trampoline_mpp_consolidation_with_hold_invoice_failure(self):\n        with self.assertRaises(NoPathFound):\n            graph = self.create_square_graph(direct=False, test_mpp_consolidation=True, is_legacy=True)\n            await self._run_trampoline_payment(graph, test_hold_invoice=True, test_failure=True)\n\n    async def test_payment_trampoline_legacy(self):\n        # alice -> T1_bob -> carol -> dave\n        with self.assertRaises(PaymentDone):\n            graph = self.create_square_graph(direct=False, is_legacy=True)\n            await self._run_trampoline_payment(graph, include_routing_hints=True)\n        with self.assertRaises(NoPathFound):\n            graph = self.create_square_graph(direct=False, is_legacy=True)\n            await self._run_trampoline_payment(graph, include_routing_hints=False)\n\n    async def test_payment_trampoline_e2e_alice_t1_dave(self):\n        with self.assertRaises(PaymentDone):\n            graph = self.create_square_graph(direct=True, is_legacy=False)\n            await self._run_trampoline_payment(graph)\n\n    async def test_payment_trampoline_e2e_alice_t1_t2_dave(self):\n        with self.assertRaises(PaymentDone):\n            graph = self.create_square_graph(direct=False, is_legacy=False)\n            await self._run_trampoline_payment(graph)\n\n    async def test_payment_trampoline_e2e_alice_t1_carol_t2_edward(self):\n        # alice -> T1_bob -> carol -> T2_dave -> edward\n        graph_definition = self.GRAPH_DEFINITIONS['line_graph']\n        graph = self.prepare_chans_and_peers_in_graph(graph_definition)\n        inject_chan_into_gossipdb(\n            channel_db=graph.workers['bob'].channel_db, graph=graph,\n            node1name='carol', node2name='dave')\n        with self.assertRaises(PaymentDone):\n            await self._run_trampoline_payment(\n                graph, sender_name='alice', destination_name='edward',tf_names=('bob', 'dave'))\n\n    async def test_multi_trampoline_payment(self):\n        \"\"\"\n        Alice splits her payment to Dave between two trampoline forwarding nodes Carol and Bob.\n        This should test Multi-Trampoline MPP:\n        https://github.com/lightning/bolts/blob/bc7a1a0bc97b2293e7f43dd8a06529e5fdcf7cd2/proposals/trampoline.md#multi-trampoline-mpp\n        \"\"\"\n        graph_definition = self.GRAPH_DEFINITIONS['square_graph']\n        # payment amount is 100_000_000 msat, size the channels so that alice must use both to succeed\n        graph_definition['alice']['channels']['bob']['local_balance_msat'] = int(100_000_000 * 0.75)\n        graph_definition['alice']['channels']['carol']['local_balance_msat'] = int(100_000_000 * 0.75)\n        g = self.prepare_chans_and_peers_in_graph(graph_definition)\n        w = g.workers['alice'], g.workers['carol'], g.workers['bob'], g.workers['dave']\n        alice_w, carol_w, bob_w, dave_w = w\n\n        alice_w.config.TEST_FORCE_MPP = True\n        bob_w.config.TEST_FORCE_MPP = True\n        carol_w.config.TEST_FORCE_MPP = True\n        dave_w.features |= LnFeatures.BASIC_MPP_OPT\n\n        with self.assertRaises(PaymentDone):\n            await self._run_trampoline_payment(\n                g,\n                sender_name='alice',\n                destination_name='dave',\n                tf_names=('bob', 'carol'),\n                attempts=30,  # the default used in LNWallet.pay_invoice()\n            )\n\n    async def test_forwarder_fails_for_inconsistent_trampoline_onions(self):\n        \"\"\"\n        verify that the receiver of a trampoline forwarding fails the mpp set\n        if the trampoline onions are not similar\n        In this test alice tries to forward through bob, however in one trampoline onion she sends\n        amt_to_forward is off by one msat. Bob should compare the trampoline onions and fail the set.\n        \"\"\"\n\n        # store a modified trampoline onion to be injected into lnworker.new_onion_packet later when sending the htlcs\n        modified_trampoline_onion = None\n        def modified_new_onion_packet_trampoline(payment_path_pubkeys, session_key, hops_data: List[OnionHopsDataSingle], **kwargs):\n            nonlocal modified_trampoline_onion\n            assert modified_trampoline_onion is None, \"this mock should get called only once\"\n            modified_hops_data = copy.copy(hops_data)\n            # first payload (i[0]) is for bob who is supposed to forward the trampoline payment, in this\n            # test he should fail the incoming htlcs as their trampolines are not similar\n            new_payload = dict(modified_hops_data[0].payload)\n            amt_to_forward = dict(new_payload['amt_to_forward'])\n            amt_to_forward['amt_to_forward'] -= 1\n            new_payload['amt_to_forward'] = amt_to_forward\n            modified_hops_data[0] = dataclasses.replace(modified_hops_data[0], payload=new_payload)\n            self.logger.debug(f\"{modified_hops_data=}\\nsent_{hops_data=}\")\n            modified_trampoline_onion = electrum.lnonion.new_onion_packet(\n                payment_path_pubkeys,\n                session_key,\n                modified_hops_data,\n                **kwargs\n            )\n            # return the unmodified onion\n            return electrum.lnonion.new_onion_packet(\n                payment_path_pubkeys,\n                session_key,\n                hops_data,\n                **kwargs\n            )\n\n        # this gets called in lnworker per sent htlc, for one sent htlc we inject the modified trampoline\n        # onion created before in the mock above\n        def modified_new_onion_packet_lnworker(payment_path_pubkeys, session_key, hops_data: List[OnionHopsDataSingle], **kwargs):\n            nonlocal modified_trampoline_onion\n            hops_data = copy.copy(hops_data)\n            if modified_trampoline_onion:\n                assert isinstance(modified_trampoline_onion, OnionPacket)\n                assert len(hops_data) == 1\n                new_payload = dict(hops_data[0].payload)\n                new_payload['trampoline_onion_packet'] = {\n                    \"version\": modified_trampoline_onion.version,\n                    \"public_key\": modified_trampoline_onion.public_key,\n                    \"hops_data\": modified_trampoline_onion.hops_data,\n                    \"hmac\": modified_trampoline_onion.hmac,\n                }\n                hops_data[0] = dataclasses.replace(hops_data[0], payload=MappingProxyType(new_payload))\n                modified_trampoline_onion = None\n            return electrum.lnonion.new_onion_packet(\n                payment_path_pubkeys,\n                session_key,\n                hops_data,\n                **kwargs\n            )\n\n        graph = self.create_square_graph(direct=False, test_mpp_consolidation=True, is_legacy=True)\n        alice = graph.workers['alice']\n        alice.config.INITIAL_TRAMPOLINE_FEE_LEVEL = 6  # set high so the first attempt would succeed\n        with self.assertRaises(PaymentFailure):\n            with mock.patch('electrum.trampoline.new_onion_packet', side_effect=modified_new_onion_packet_trampoline), \\\n                    mock.patch('electrum.lnworker.new_onion_packet', side_effect=modified_new_onion_packet_lnworker):\n                        await self._run_trampoline_payment(graph, attempts=1)\n        bob_alice_channel = graph.channels[('bob', 'alice')]\n        bob_hm = bob_alice_channel.hm\n        assert len(bob_hm.all_htlcs_ever()) == 2\n        assert all(bob_hm.was_htlc_failed(htlc_id=htlc.htlc_id, htlc_proposer=HTLCOwner.REMOTE) for (_, htlc) in bob_hm.all_htlcs_ever())\n\n    async def test_payment_with_malformed_onion(self):\n        \"\"\"\n        Alice -> Bob -> Carol. Carol fails htlc with update_fail_malformed_htlc because she is unable\n        to parse the onion Alice sent to her.\n        \"\"\"\n        graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['line_graph'])\n        peers = graph.peers.values()\n\n        async def pay(lnaddr, pay_req):\n            self.assertEqual(PR_UNPAID, graph.workers['carol'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n            result, log = await graph.workers['alice'].pay_invoice(pay_req)\n            self.assertEqual(OnionFailureCode.INVALID_ONION_VERSION, log[0].failure_msg.code)\n            self.assertFalse(result, msg=log)\n            raise PaymentFailure()\n\n        # this will make carol send update_fail_malformed_htlc\n        graph.workers['carol'].config.TEST_FAIL_HTLCS_AS_MALFORMED = True\n\n        async def f():\n            async with OldTaskGroup() as group:\n                for peer in peers:\n                    await group.spawn(peer._message_loop())\n                    await group.spawn(peer.htlc_switch())\n                for peer in peers:\n                    await peer.initialized\n                lnaddr, pay_req = self.prepare_invoice(graph.workers['carol'], include_routing_hints=True)\n                await group.spawn(pay(lnaddr, pay_req))\n\n        with self.assertLogs('electrum', level='INFO') as logs:\n            with self.assertRaises(PaymentFailure):\n                await f()\n            self.assertTrue(\n                any('carol->bob' in msg and 'fail_malformed_htlc' in msg for msg in logs.output)\n            )\n            self.assertTrue(\n                any('bob->carol' in msg and 'on_update_fail_malformed_htlc' in msg for msg in logs.output)\n            )\n\n    async def test_dont_settle_htlcs_receiver_and_forwarder(self):\n        \"\"\"\n        Test that the receiver and forwarder doesn't settle htlcs once they get the preimage if the payment\n        hash is in LNWallet.dont_settle_htlcs. E.g. the forwarder could be a just-in-time channel provider.\n        Alice -> Bob -> Carol. Carol and Bob shouldn't release the preimage.\n        \"\"\"\n        async def run_test(test_trampoline):\n            graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['line_graph'])\n            peers = graph.peers.values()\n\n            if test_trampoline:\n                electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {\n                    graph.workers['bob'].name: LNPeerAddr(host=\"127.0.0.1\", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey),\n                }\n                await self._activate_trampoline(graph.workers['carol'])\n                await self._activate_trampoline(graph.workers['alice'])\n\n            lnaddr, pay_req = self.prepare_invoice(graph.workers['carol'], include_routing_hints=True)\n            # test both receiver (carol) and forwarder (bob)\n            graph.workers['bob'].dont_settle_htlcs[lnaddr.paymenthash.hex()] = None\n            graph.workers['carol'].dont_settle_htlcs[lnaddr.paymenthash.hex()] = None\n\n            payment_successful = asyncio.Event()\n            async def pay():\n                self.assertEqual(PR_UNPAID, graph.workers['carol'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n                result, log = await graph.workers['alice'].pay_invoice(pay_req)\n                self.assertEqual(PR_PAID, graph.workers['carol'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n                self.assertTrue(result)\n                payment_successful.set()\n\n            async def check_doesnt_settle():\n                while not graph.workers['carol'].received_mpp_htlcs:\n                    await asyncio.sleep(0.1)  # wait until carol received the htlcs\n\n                await asyncio.sleep(0.2)  # give carol time to accidentally release the preimage\n                self.assertEqual(PR_UNPAID, graph.workers['carol'].get_payment_status(lnaddr.paymenthash, direction=RECEIVED))\n                self.assertIsNone(graph.workers['bob'].get_preimage(lnaddr.paymenthash), \"bob got preimage from carol\")\n                # now allow carol to release the preimage to bob\n                del graph.workers['carol'].dont_settle_htlcs[lnaddr.paymenthash.hex()]\n\n                # wait for carol to release the preimage to bob\n                while not graph.workers['bob'].get_preimage(lnaddr.paymenthash):\n                    await asyncio.sleep(0.1)\n\n                # give bob some time to settle the htlcs to alice (this would complete the payment)\n                await asyncio.sleep(0.2)\n                self.assertIsNone(graph.workers['alice'].get_preimage(lnaddr.paymenthash), \"alice got preimage from bob\")\n                self.assertFalse(payment_successful.is_set(), \"bob released preimage\")\n\n                # now allow bob to settle the htlcs\n                del graph.workers['bob'].dont_settle_htlcs[lnaddr.paymenthash.hex()]\n                await payment_successful.wait()\n                raise PaymentDone()\n\n            async def f():\n                async with OldTaskGroup() as group:\n                    for peer in peers:\n                        await group.spawn(peer._message_loop())\n                        await group.spawn(peer.htlc_switch())\n                    for peer in peers:\n                        await peer.initialized\n\n                    await group.spawn(pay())\n                    await group.spawn(check_doesnt_settle())\n                    # stop the taskgroup if anything takes too long\n                    await group.spawn(asyncio.wait_for(asyncio.sleep(4), timeout=3))\n\n            await f()\n\n        for trampoline in (False, True):\n            with self.assertRaises(PaymentDone):\n                await run_test(trampoline)\n\n\nclass TestPeerDirectAnchors(TestPeerDirect):\n    TEST_ANCHOR_CHANNELS = True\n\nclass TestPeerForwardingAnchors(TestPeerForwarding):\n    TEST_ANCHOR_CHANNELS = True\n\n\ndef run(coro):\n    return asyncio.run_coroutine_threadsafe(coro, loop=util.get_asyncio_loop()).result()\n"
  },
  {
    "path": "tests/test_lnpeermgr.py",
    "content": "import logging\nimport os\nimport socket\nimport asyncio\nfrom unittest import mock\n\nfrom . import ElectrumTestCase\n\nfrom electrum.lntransport import ConnStringFormatError\nfrom electrum.logging import console_stderr_handler\n\n\nclass TestLNPeerManager(ElectrumTestCase):\n    TESTNET = True\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        console_stderr_handler.setLevel(logging.DEBUG)\n\n    async def asyncSetUp(self):\n        lnwallet = self.create_mock_lnwallet(name='mock_lnwallet_anchors', has_anchors=True)\n        self.lnpeermgr = lnwallet.lnpeermgr\n        await super().asyncSetUp()\n\n    async def test_add_peer_conn_string_errors(self):\n        unknown_node_id = os.urandom(33)\n        peermgr = self.lnpeermgr\n        peermgr._add_peer = mock.Mock(side_effect=NotImplementedError)\n\n        # Trampoline enabled, unknown node (no address in trampolines)\n        channel_db = peermgr.network.channel_db\n        peermgr.network.channel_db = None\n        try:\n            with self.assertRaises(ConnStringFormatError) as cm:\n                await peermgr.add_peer(unknown_node_id.hex())\n            self.assertIn(\"Address unknown for node\", str(cm.exception))\n        finally:\n            peermgr.network.channel_db = channel_db  # re-set channel db\n\n        # Trampoline disabled, unknown node (no address in channel_db)\n        with mock.patch.object(peermgr.network.channel_db, 'get_node_addresses', return_value=[]):\n            with self.assertRaises(ConnStringFormatError) as cm:\n                await peermgr.add_peer(unknown_node_id.hex())\n            self.assertIn(\"Don't know any addresses for node\", str(cm.exception))\n\n        # .onion address, but no proxy configured\n        onion_conn_str = unknown_node_id.hex() + \"@somewhere.onion:9735\"\n        self.assertFalse(peermgr.network.proxy.enabled)\n        with self.assertRaises(ConnStringFormatError) as cm:\n            await peermgr.add_peer(onion_conn_str)\n        self.assertIn(\".onion address, but no proxy configured\", str(cm.exception))\n\n        # Hostname does not resolve (getaddrinfo failed)\n        bad_host_conn_str = unknown_node_id.hex() + \"@badhost:9735\"\n        loop = asyncio.get_running_loop()\n        with mock.patch.object(loop, 'getaddrinfo', side_effect=socket.gaierror):\n            with self.assertRaises(ConnStringFormatError) as cm:\n                await peermgr.add_peer(bad_host_conn_str)\n            self.assertIn(\"Hostname does not resolve\", str(cm.exception))\n\n    def test_choose_preferred_address(self):\n        peermgr = self.lnpeermgr\n\n        # prefer most recent IP address\n        addr_list = [\n            (\"192.168.1.1\", 9735, 100),\n            (\"host.onion\", 9735, 200),\n            (\"10.0.0.1\", 9735, 150),\n            (\"host.com\", 9735, 250)\n        ]\n        result = peermgr.choose_preferred_address(addr_list)\n        self.assertEqual(result, (\"10.0.0.1\", 9735, 150))  # Most recent IP\n\n        # no IP, proxy disabled, filter .onion and choose random\n        self.assertFalse(peermgr.network.proxy.enabled)\n        addr_list = [(\"host.com\", 9735, 100), (\"host.onion\", 9735, 200)]\n        result = peermgr.choose_preferred_address(addr_list)\n        self.assertEqual(result, (\"host.com\", 9735, 100))\n\n        # empty list after filtering\n        addr_list = [(\"host.onion\", 9735, 100)]\n        result = peermgr.choose_preferred_address(addr_list)\n        self.assertIsNone(result)\n\n        # return onion if proxy enabled\n        peermgr.network.proxy.enabled = True\n        addr_list = [(\"host.onion\", 9735, 100)]\n        result = peermgr.choose_preferred_address(addr_list)\n        self.assertEqual(result, (\"host.onion\", 9735, 100))\n"
  },
  {
    "path": "tests/test_lnrouter.py",
    "content": "from math import inf\nimport unittest\nimport tempfile\nimport shutil\nimport asyncio\nfrom typing import Optional\nfrom os import urandom\nfrom types import MappingProxyType\n\nfrom electrum import util\nfrom electrum.channel_db import NodeInfo\nfrom electrum.onion_message import is_onion_message_node\nfrom electrum.trampoline import create_trampoline_onion\nfrom electrum.util import bfh\nfrom electrum.lnutil import ShortChannelID, LnFeatures\nfrom electrum.lnonion import (OnionHopsDataSingle, new_onion_packet,\n                              process_onion_packet, _decode_onion_error, decode_onion_error,\n                              OnionFailureCode)\nfrom electrum import bitcoin, lnrouter\nfrom electrum.constants import BitcoinTestnet\nfrom electrum.simple_config import SimpleConfig\nfrom electrum.lnrouter import (PathEdge, LiquidityHintMgr, DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH,\n                               DEFAULT_PENALTY_BASE_MSAT, fee_for_edge_msat, LNPaymentTRoute, TrampolineEdge)\n\nfrom . import ElectrumTestCase\nfrom .test_bitcoin import needs_test_with_all_chacha20_implementations\n\n\ndef channel(number: int) -> ShortChannelID:\n    return ShortChannelID(bfh(format(number, '016x')))\n\n\ndef node(character: str) -> bytes:\n    return b'\\x02' + f'{character}'.encode() * 32\n\n\ndef alias(character: str) -> bytes:\n    return (character * 8).encode('utf-8')\n\n\ndef node_features(extra: LnFeatures = None) -> bytes:\n    lnf = LnFeatures(0) | LnFeatures.VAR_ONION_OPT\n    if extra:\n        lnf |= extra\n    return lnf.to_bytes(8, 'big')\n\n\nclass Test_LNRouter(ElectrumTestCase):\n    TESTNET = True\n\n    cdb = None  # type: Optional[lnrouter.ChannelDB]\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n        self.assertIsNone(self.cdb)  # sanity-check side effects from previous tests\n\n    async def asyncTearDown(self):\n        # if the test called prepare_graph(), channeldb needs to be cleaned up\n        if self.cdb:\n            self.cdb.stop()\n            await self.cdb.stopped_event.wait()\n        await super().asyncTearDown()\n\n    def prepare_graph(self):\n        \"\"\"\n        Network topology with channel ids:\n                3\n            A  ---  B\n            |    2/ |\n          6 |   E   | 1\n            | /5 \\7 |\n            D  ---  C\n                4\n        valid routes from A -> E:\n        A -3-> B -2-> E\n        A -6-> D -5-> E\n        A -6-> D -4-> C -7-> E\n        A -3-> B -1-> C -7-> E\n        A -6-> D -4-> C -1-> B -2-> E\n        A -3-> B -1-> C -4-> D -5-> E\n        \"\"\"\n\n        class fake_network:\n            config = self.config\n            asyncio_loop = util.get_asyncio_loop()\n            trigger_callback = lambda *args: None\n            register_callback = lambda *args: None\n            interface = None\n\n        fake_network.channel_db = lnrouter.ChannelDB(fake_network())\n        fake_network.channel_db.data_loaded.set()\n        self.cdb = fake_network.channel_db\n        self.path_finder = lnrouter.LNPathFinder(self.cdb)\n        self.assertEqual(self.cdb.num_channels, 0)\n        self.cdb.add_channel_announcements({\n            'node_id_1': node('b'), 'node_id_2': node('c'),\n            'bitcoin_key_1': node('b'), 'bitcoin_key_2': node('c'),\n            'short_channel_id': channel(1),\n            'chain_hash': BitcoinTestnet.rev_genesis_bytes(),\n            'len': 0, 'features': b''\n        }, trusted=True)\n        self.assertEqual(self.cdb.num_channels, 1)\n        self.cdb.add_channel_announcements({\n            'node_id_1': node('b'), 'node_id_2': node('e'),\n            'bitcoin_key_1': node('b'), 'bitcoin_key_2': node('e'),\n            'short_channel_id': channel(2),\n            'chain_hash': BitcoinTestnet.rev_genesis_bytes(),\n            'len': 0, 'features': b''\n        }, trusted=True)\n        self.cdb.add_channel_announcements({\n            'node_id_1': node('a'), 'node_id_2': node('b'),\n            'bitcoin_key_1': node('a'), 'bitcoin_key_2': node('b'),\n            'short_channel_id': channel(3),\n            'chain_hash': BitcoinTestnet.rev_genesis_bytes(),\n            'len': 0, 'features': b''\n        }, trusted=True)\n        self.cdb.add_channel_announcements({\n            'node_id_1': node('c'), 'node_id_2': node('d'),\n            'bitcoin_key_1': node('c'), 'bitcoin_key_2': node('d'),\n            'short_channel_id': channel(4),\n            'chain_hash': BitcoinTestnet.rev_genesis_bytes(),\n            'len': 0, 'features': b''\n        }, trusted=True)\n        self.cdb.add_channel_announcements({\n            'node_id_1': node('d'), 'node_id_2': node('e'),\n            'bitcoin_key_1': node('d'), 'bitcoin_key_2': node('e'),\n            'short_channel_id': channel(5),\n            'chain_hash': BitcoinTestnet.rev_genesis_bytes(),\n            'len': 0, 'features': b''\n        }, trusted=True)\n        self.cdb.add_channel_announcements({\n            'node_id_1': node('a'), 'node_id_2': node('d'),\n            'bitcoin_key_1': node('a'), 'bitcoin_key_2': node('d'),\n            'short_channel_id': channel(6),\n            'chain_hash': BitcoinTestnet.rev_genesis_bytes(),\n            'len': 0, 'features': b''\n        }, trusted=True)\n        self.cdb.add_channel_announcements({\n            'node_id_1': node('c'), 'node_id_2': node('e'),\n            'bitcoin_key_1': node('c'), 'bitcoin_key_2': node('e'),\n            'short_channel_id': channel(7),\n            'chain_hash': BitcoinTestnet.rev_genesis_bytes(),\n            'len': 0, 'features': b''\n        }, trusted=True)\n\n        self.cdb.add_node_announcements({\n            'node_id': node('a'),\n            'alias': alias('a'),\n            'addresses': [],\n            'features': node_features(LnFeatures.OPTION_ONION_MESSAGE_OPT),\n            'timestamp': 0\n        })\n        self.cdb.add_node_announcements({\n            'node_id': node('b'),\n            'alias': alias('b'),\n            'addresses': [],\n            'features': node_features(),\n            'timestamp': 0\n        })\n        self.cdb.add_node_announcements({\n            'node_id': node('c'),\n            'alias': alias('c'),\n            'addresses': [],\n            'features': node_features(LnFeatures.OPTION_ONION_MESSAGE_OPT),\n            'timestamp': 0\n        })\n        self.cdb.add_node_announcements({\n            'node_id': node('d'),\n            'alias': alias('d'),\n            'addresses': [],\n            'features': node_features(LnFeatures.OPTION_ONION_MESSAGE_OPT),\n            'timestamp': 0\n        })\n        self.cdb.add_node_announcements({\n            'node_id': node('e'),\n            'alias': alias('e'),\n            'addresses': [],\n            'features': node_features(),\n            'timestamp': 0\n        })\n\n        def add_chan_upd(payload):\n            self.cdb.add_channel_update(payload, verify=False)\n\n        add_chan_upd({'short_channel_id': channel(1), 'message_flags': b'\\x00', 'channel_flags': b'\\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(1), 'message_flags': b'\\x00', 'channel_flags': b'\\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(2), 'message_flags': b'\\x00', 'channel_flags': b'\\x00', 'cltv_expiry_delta': 99, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(2), 'message_flags': b'\\x00', 'channel_flags': b'\\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(3), 'message_flags': b'\\x00', 'channel_flags': b'\\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(3), 'message_flags': b'\\x00', 'channel_flags': b'\\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(4), 'message_flags': b'\\x00', 'channel_flags': b'\\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(4), 'message_flags': b'\\x00', 'channel_flags': b'\\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(5), 'message_flags': b'\\x00', 'channel_flags': b'\\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(5), 'message_flags': b'\\x00', 'channel_flags': b'\\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 999, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(6), 'message_flags': b'\\x00', 'channel_flags': b'\\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 200, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(6), 'message_flags': b'\\x00', 'channel_flags': b'\\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(7), 'message_flags': b'\\x00', 'channel_flags': b'\\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n        add_chan_upd({'short_channel_id': channel(7), 'message_flags': b'\\x00', 'channel_flags': b'\\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})\n\n    async def test_find_path_for_payment(self):\n        self.prepare_graph()\n        amount_to_send = 100000\n\n        path = self.path_finder.find_path_for_payment(\n            nodeA=node('a'),\n            nodeB=node('e'),\n            invoice_amount_msat=amount_to_send)\n        self.assertEqual([\n            PathEdge(start_node=node('a'), end_node=node('b'), short_channel_id=channel(3)),\n            PathEdge(start_node=node('b'), end_node=node('e'), short_channel_id=channel(2)),\n        ], path)\n\n        route = self.path_finder.create_route_from_path(path)\n        self.assertEqual(node('b'), route[0].node_id)\n        self.assertEqual(channel(3), route[0].short_channel_id)\n\n    async def test_find_path_for_payment_with_node_filter(self):\n        self.prepare_graph()\n        amount_to_send = 100000\n\n        def node_filter(node_id: bytes, node_info: 'NodeInfo'):\n            return node_info.node_id != node('b')\n\n        path = self.path_finder.find_path_for_payment(\n            nodeA=node('a'),\n            nodeB=node('e'),\n            invoice_amount_msat=amount_to_send,\n            node_filter=node_filter)\n        self.assertEqual([\n            PathEdge(start_node=node('a'), end_node=node('d'), short_channel_id=channel(6)),\n            PathEdge(start_node=node('d'), end_node=node('e'), short_channel_id=channel(5)),\n        ], path)\n\n        route = self.path_finder.create_route_from_path(path)\n        self.assertEqual(node('d'), route[0].node_id)\n        self.assertEqual(channel(6), route[0].short_channel_id)\n\n    async def test_find_path_liquidity_hints(self):\n        self.prepare_graph()\n        amount_to_send = 100000\n\n        \"\"\"\n        assume failure over channel 2, B -> E\n        A -3-> B |-2-> E\n        A -6-> D -5-> E  <= chosen path\n        A -6-> D -4-> C -7-> E\n        A -3-> B -1-> C -7-> E\n        A -6-> D -4-> C -1-> B -2-> E\n        A -3-> B -1-> C -4-> D -5-> E\n        \"\"\"\n        self.path_finder.liquidity_hints.update_cannot_send(node('b'), node('e'), channel(2), amount_to_send - 1)\n        path = self.path_finder.find_path_for_payment(\n            nodeA=node('a'),\n            nodeB=node('e'),\n            invoice_amount_msat=amount_to_send)\n        self.assertEqual(channel(6), path[0].short_channel_id)\n        self.assertEqual(channel(5), path[1].short_channel_id)\n\n        \"\"\"\n        assume failure over channel 5, D -> E\n        A -3-> B |-2-> E\n        A -6-> D |-5-> E\n        A -6-> D -4-> C -7-> E\n        A -3-> B -1-> C -7-> E  <= chosen path\n        A -6-> D -4-> C -1-> B |-2-> E\n        A -3-> B -1-> C -4-> D |-5-> E\n        \"\"\"\n        self.path_finder.liquidity_hints.update_cannot_send(node('d'), node('e'), channel(5), amount_to_send - 1)\n        path = self.path_finder.find_path_for_payment(\n            nodeA=node('a'),\n            nodeB=node('e'),\n            invoice_amount_msat=amount_to_send)\n        self.assertEqual(channel(3), path[0].short_channel_id)\n        self.assertEqual(channel(1), path[1].short_channel_id)\n        self.assertEqual(channel(7), path[2].short_channel_id)\n\n        \"\"\"\n        assume success over channel 4, D -> C\n        A -3-> B |-2-> E\n        A -6-> D |-5-> E\n        A -6-> D -4-> C -7-> E  <= smaller penalty: chosen path\n        A -3-> B -1-> C -7-> E\n        A -6-> D -4-> C -1-> B |-2-> E\n        A -3-> B -1-> C -4-> D |-5-> E\n        \"\"\"\n        self.path_finder.liquidity_hints.update_can_send(node('d'), node('c'), channel(4), amount_to_send + 1000)\n        path = self.path_finder.find_path_for_payment(\n            nodeA=node('a'),\n            nodeB=node('e'),\n            invoice_amount_msat=amount_to_send)\n        self.assertEqual(channel(6), path[0].short_channel_id)\n        self.assertEqual(channel(4), path[1].short_channel_id)\n        self.assertEqual(channel(7), path[2].short_channel_id)\n\n    async def test_find_path_liquidity_hints_inflight_htlcs(self):\n        self.prepare_graph()\n        amount_to_send = 100000\n\n        \"\"\"\n        add inflight htlc to channel 2, B -> E\n        A -3-> B -2(1)-> E\n        A -6-> D -5-> E <= chosen path\n        A -6-> D -4-> C -7-> E\n        A -3-> B -1-> C -7-> E\n        A -6-> D -4-> C -1-> B -2-> E\n        A -3-> B -1-> C -4-> D -5-> E\n        \"\"\"\n        self.path_finder.liquidity_hints.add_htlc(node('b'), node('e'), channel(2))\n        path = self.path_finder.find_path_for_payment(\n            nodeA=node('a'),\n            nodeB=node('e'),\n            invoice_amount_msat=amount_to_send)\n        self.assertEqual(channel(6), path[0].short_channel_id)\n        self.assertEqual(channel(5), path[1].short_channel_id)\n\n        \"\"\"\n        remove inflight htlc from channel 2, B -> E\n        A -3-> B -2(0)-> E <= chosen path\n        A -6-> D -5-> E\n        A -6-> D -4-> C -7-> E\n        A -3-> B -1-> C -7-> E\n        A -6-> D -4-> C -1-> B -2-> E\n        A -3-> B -1-> C -4-> D -5-> E\n        \"\"\"\n        self.path_finder.liquidity_hints.remove_htlc(node('b'), node('e'), channel(2))\n        path = self.path_finder.find_path_for_payment(\n            nodeA=node('a'),\n            nodeB=node('e'),\n            invoice_amount_msat=amount_to_send)\n        self.assertEqual(channel(3), path[0].short_channel_id)\n        self.assertEqual(channel(2), path[1].short_channel_id)\n\n    def test_liquidity_hints(self):\n        liquidity_hints = LiquidityHintMgr()\n        node_from = bytes(0)\n        node_to = bytes(1)\n        channel_id = ShortChannelID.from_components(0, 0, 0)\n        amount_to_send = 1_000_000\n\n        # check default penalty\n        self.assertEqual(\n            fee_for_edge_msat(amount_to_send, DEFAULT_PENALTY_BASE_MSAT, DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH),\n            liquidity_hints.penalty(node_from, node_to, channel_id, amount_to_send)\n        )\n        liquidity_hints.update_can_send(node_from, node_to, channel_id, 1_000_000)\n        liquidity_hints.update_cannot_send(node_from, node_to, channel_id, 2_000_000)\n        hint = liquidity_hints.get_hint(channel_id)\n        self.assertEqual(1_000_000, hint.can_send(node_from < node_to))\n        self.assertEqual(None, hint.cannot_send(node_to < node_from))\n        self.assertEqual(2_000_000, hint.cannot_send(node_from < node_to))\n        # the can_send backward hint is set automatically\n        self.assertEqual(2_000_000, hint.can_send(node_to < node_from))\n\n        # check penalties\n        self.assertEqual(0., liquidity_hints.penalty(node_from, node_to, channel_id, 1_000_000))\n        self.assertEqual(650, liquidity_hints.penalty(node_from, node_to, channel_id, 1_500_000))\n        self.assertEqual(inf, liquidity_hints.penalty(node_from, node_to, channel_id, 2_000_000))\n\n        # test that we don't overwrite significant info with less significant info\n        liquidity_hints.update_can_send(node_from, node_to, channel_id, 500_000)\n        hint = liquidity_hints.get_hint(channel_id)\n        self.assertEqual(1_000_000, hint.can_send(node_from < node_to))\n\n        # test case when can_send > cannot_send\n        liquidity_hints.update_can_send(node_from, node_to, channel_id, 3_000_000)\n        hint = liquidity_hints.get_hint(channel_id)\n        self.assertEqual(3_000_000, hint.can_send(node_from < node_to))\n        self.assertEqual(None, hint.cannot_send(node_from < node_to))\n\n        # test inflight htlc\n        liquidity_hints.reset_liquidity_hints()\n        liquidity_hints.add_htlc(node_from, node_to, channel_id)\n        liquidity_hints.get_hint(channel_id)\n        # we have got 600 (attempt) + 600 (inflight) penalty\n        self.assertEqual(1200, liquidity_hints.penalty(node_from, node_to, channel_id, 1_000_000))\n\n    @needs_test_with_all_chacha20_implementations\n    def test_new_onion_packet(self):\n        # test vector from bolt-04\n        payment_path_pubkeys = [\n            bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),\n            bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'),\n            bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'),\n            bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'),\n            bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'),\n        ]\n        session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')\n        associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')\n        hops_data = [\n            OnionHopsDataSingle(\n                _raw_bytes_payload=bfh(\"1202023a98040205dc06080000000000000001\"),\n            ),\n            OnionHopsDataSingle(\n                _raw_bytes_payload=bfh(\"52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f\"),\n            ),\n            OnionHopsDataSingle(\n                _raw_bytes_payload=bfh(\"12020230d4040204e206080000000000000003\"),\n            ),\n            OnionHopsDataSingle(\n                _raw_bytes_payload=bfh(\"1202022710040203e806080000000000000004\"),\n            ),\n            OnionHopsDataSingle(\n                _raw_bytes_payload=bfh(\"fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\"),\n            ),\n        ]\n        packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=associated_data)\n        self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f7f3416a5aa36dc7eeb3ec6d421e9615471ab870a33ac07fa5d5a51df0a8823aabe3fea3f90d387529d4f72837f9e687230371ccd8d263072206dbed0234f6505e21e282abd8c0e4f5b9ff8042800bbab065036eadd0149b37f27dde664725a49866e052e809d2b0198ab9610faa656bbf4ec516763a59f8f42c171b179166ba38958d4f51b39b3e98706e2d14a2dafd6a5df808093abfca5aeaaca16eded5db7d21fb0294dd1a163edf0fb445d5c8d7d688d6dd9c541762bf5a5123bf9939d957fe648416e88f1b0928bfa034982b22548e1a4d922690eecf546275afb233acf4323974680779f1a964cfe687456035cc0fba8a5428430b390f0057b6d1fe9a8875bfa89693eeb838ce59f09d207a503ee6f6299c92d6361bc335fcbf9b5cd44747aadce2ce6069cfdc3d671daef9f8ae590cf93d957c9e873e9a1bc62d9640dc8fc39c14902d49a1c80239b6c5b7fd91d05878cbf5ffc7db2569f47c43d6c0d27c438abff276e87364deb8858a37e5a62c446af95d8b786eaf0b5fcf78d98b41496794f8dcaac4eef34b2acfb94c7e8c32a9e9866a8fa0b6f2a06f00a1ccde569f97eec05c803ba7500acc96691d8898d73d8e6a47b8f43c3d5de74458d20eda61474c426359677001fbd75a74d7d5db6cb4feb83122f133206203e4e2d293f838bf8c8b3a29acb321315100b87e80e0edb272ee80fda944e3fb6084ed4d7f7c7d21c69d9da43d31a90b70693f9b0cc3eac74c11ab8ff655905688916cfa4ef0bd04135f2e50b7c689a21d04e8e981e74c6058188b9b1f9dfc3eec6838e9ffbcf22ce738d8a177c19318dffef090cee67e12de1a3e2a39f61247547ba5257489cbc11d7d91ed34617fcc42f7a9da2e3cf31a94a210a1018143173913c38f60e62b24bf0d7518f38b5bab3e6a1f8aeb35e31d6442c8abb5178efc892d2e787d79c6ad9e2fc271792983fa9955ac4d1d84a36c024071bc6e431b625519d556af38185601f70e29035ea6a09c8b676c9d88cf7e05e0f17098b584c4168735940263f940033a220f40be4c85344128b14beb9e75696db37014107801a59b13e89cd9d2258c169d523be6d31552c44c82ff4bb18ec9f099f3bf0e5b1bb2ba9a87d7e26f98d294927b600b5529c47e04d98956677cbcee8fa2b60f49776d8b8c367465b7c626da53700684fb6c918ead0eab8360e4f60edd25b4f43816a75ecf70f909301825b512469f8389d79402311d8aecb7b3ef8599e79485a4388d87744d899f7c47ee644361e17040a7958c8911be6f463ab6a9b2afacd688ec55ef517b38f1339efc54487232798bb25522ff4572ff68567fe830f92f7b8113efce3e98c3fffbaedce4fd8b50e41da97c0c08e423a72689cc68e68f752a5e3a9003e64e35c957ca2e1c48bb6f64b05f56b70b575ad2f278d57850a7ad568c24a4d32a3d74b29f03dc125488bc7c637da582357f40b0a52d16b3b40bb2c2315d03360bc24209e20972c200566bcf3bbe5c5b0aedd83132a8a4d5b4242ba370b6d67d9b67eb01052d132c7866b9cb502e44796d9d356e4e3cb47cc527322cd24976fe7c9257a2864151a38e568ef7a79f10d6ef27cc04ce382347a2488b1f404fdbf407fe1ca1c9d0d5649e34800e25e18951c98cae9f43555eef65fee1ea8f15828807366c3b612cd5753bf9fb8fced08855f742cddd6f765f74254f03186683d646e6f09ac2805586c7cf11998357cafc5df3f285329366f475130c928b2dceba4aa383758e7a9d20705c4bb9db619e2992f608a1ba65db254bb389468741d0502e2588aeb54390ac600c19af5c8e61383fc1bebe0029e4474051e4ef908828db9cca13277ef65db3fd47ccc2179126aaefb627719f421e20'),\n                         packet.to_bytes())\n\n    @needs_test_with_all_chacha20_implementations\n    def test_process_onion_packet(self):\n        # this test is not from bolt-04, but is based on the one there;\n        # here the TLV payloads are all known types. This allows testing\n        # decoding the onion and parsing hops_data into known TLV dicts.\n        payment_path_pubkeys = [\n            bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),\n            bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'),\n            bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'),\n            bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'),\n            bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'),\n        ]\n        payment_path_privkeys = [\n            bfh('4141414141414141414141414141414141414141414141414141414141414141'),\n            bfh('4242424242424242424242424242424242424242424242424242424242424242'),\n            bfh('4343434343434343434343434343434343434343434343434343434343434343'),\n            bfh('4444444444444444444444444444444444444444444444444444444444444444'),\n            bfh('4545454545454545454545454545454545454545454545454545454545454545'),\n        ]\n        session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')\n        associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')\n        hops_data = [\n            OnionHopsDataSingle(payload={\n                'amt_to_forward': {'amt_to_forward': 15000},\n                'outgoing_cltv_value': {'outgoing_cltv_value': 1500},\n                'short_channel_id': {'short_channel_id': bfh('0000000000000001')}}),\n            OnionHopsDataSingle(payload={\n                'amt_to_forward': {'amt_to_forward': 14000},\n                'outgoing_cltv_value': {'outgoing_cltv_value': 1400},\n                'short_channel_id': {'short_channel_id': bfh('0000000000000002')}}),\n            OnionHopsDataSingle(payload={\n                'amt_to_forward': {'amt_to_forward': 12500},\n                'outgoing_cltv_value': {'outgoing_cltv_value': 1250},\n                'short_channel_id': {'short_channel_id': bfh('0000000000000003')}}),\n            OnionHopsDataSingle(payload={\n                'amt_to_forward': {'amt_to_forward': 10000},\n                'outgoing_cltv_value': {'outgoing_cltv_value': 1000},\n                'short_channel_id': {'short_channel_id': bfh('0000000000000004')}}),\n            OnionHopsDataSingle(payload={\n                'amt_to_forward': {'amt_to_forward': 10000},\n                'outgoing_cltv_value': {'outgoing_cltv_value': 1000},\n                'payment_data': {'payment_secret': bfh('24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704'), 'total_msat': 10000}}),\n        ]\n        packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=associated_data)\n        self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f7f3416a5aa36dc7eeb3ec6d421e9615471ab858ba970cd3cceb768b44e692be2f390c0b7fe70122abae84d7801db070dfb1638cd8d263072206dbed0234f6505e21e282abd8587124c572aad8de04610a136d6c71a7648c0ef66f1b3655d8a9eea1f92349132c93befbd6c37dbfc55615814ae09e4cbef721c01b487007811bbbfdc1fc7bd869aeb70eb08b4140ff5f501394b3653ada2a3b36a263535ea421d26818afb278df46abcec093305b715cac22b0b03645f8f4797cf2987b1bf4bfdd9ed8648ed42ed1a831fc36ccd45416a132580281ddac4e7470e4d2afd675baad9282ec6335403a73e1391427e330996c834db93848b4ae29dd975f678b2f5155ad6865ca23190725d4b7238fb44f0e3762dd59091b45c97d45df8164a15d9ca0329ec76f957b0a0e49ae372154620708df5c0fa991f0dd12b6bff1ebaf9e2376bb64bc24713f7c57da569bcd9c43a50c088416564b786a87d1f40936a051a3dbfe023bd867a5e66148b61cdd24a79f8c18682150e55aa6969ce9becf51f7c69e72deafcd0659f6be4f78463eaef8716e56615c77b3fbea8190806359909dcbec13c1592523b3d2985ec3e83d42cb7286a66a22f58704ddf6979ceb6883ab4ad8ac99d30251035189ffd514e03ce1576844513d66965d4adfc2523f4eee0dede229ab96303e31348c72bc0c8c816c666a904e5ccbabadf5a919720438f4a14dbd4a802f8d4b942f0ca8572f59644c9ac1912c8c8efefc4afa7f19e27411d46b7541c55985e28ce5cd7620b335fea51de55fa00ef977e8522181ad19e5e04f93bcfc83a36edd7e96fe48e846f2e54fe7a7090fe8e46ba72123e1cdee0667777c38c4930e50401074d8ab31a9717457fcefaa46323003af553bee2b49ea7f907eb2ff3301463e64a8c53975c853bbdd2956b9001b5ce1562264963fce84201daaf752de6df7ca31291226969c9851d1fc4ea88ca67d38c38587c2cdd8bc4d3f7bdf705497a1e054246f684554b3b8dfac43194f1eadec7f83b711e663b5645bde6d7f8cefb59758303599fed25c3b4d2e4499d439c915910dd283b3e7118320f1c6e7385009fbcb9ae79bab72a85e644182b4dafc0a173241f2ae68ae6a504f17f102da1e91de4548c7f5bc1c107354519077a4e83407f0d6a8f0975b4ac0c2c7b30637a998dda27b56b56245371296b816876b859677bcf3473a07e0f300e788fdd60c51b1626b46050b182457c6d716994847aaef667ca45b2cede550c92d336ff29ce6effd933b875f81381cda6e59e9727e728a58c0b3e74035beeeb639ab7463744322bf40138b81895e9a8e8850c9513782dc7a79f04380c216cb177951d8940d576486b887a232fcd382adcbd639e70af0c1a08bcf1405496606fce4645aef10d769dc0c010a8a433d8cd24d5943843a89cdbc8d16531db027b312ab2c03a7f1fdb7f2bcb128639c49e86705c948137fd42d0080fda4be4e9ee812057c7974acbf0162730d3b647b355ac1a5adbb2993832eba443b7c9b5a0ae1fc00a6c0c2b0b65b9019690565739d6439bf602066a3a9bd9c67b83606de51792d25ae517cbbdf6e1827fa0e8b2b5c6023cbb1e9f0e10b786dc6fa154e282fd9c90b8d46ca685d0f4434760035073c92d131564b6845ef57457488add4f709073bbb41f5f31f8226904875a9fd9e1b7a2901e71426104d7a298a05af0d4ab549fbd69c539ebe64949a9b6088f16e2e4bc827c305cb8d64536b8364dc3d5f7519c3b431faa38b47a958cf0c6dcabf205280693abf747c262f44cd6ffa11b32fc38d4f9c3631d554d8b57389f1390ac65c06357843ee6d9f289bb054ef25de45c5149c090fe6ddcd4095696dcc9a5cfc09c8bdfd5b83a153'),\n                         packet.to_bytes())\n        for i, privkey in enumerate(payment_path_privkeys):\n            processed_packet = process_onion_packet(packet, privkey, associated_data=associated_data)\n            self.assertEqual(hops_data[i].to_bytes(), processed_packet.hop_data.to_bytes())\n            packet = processed_packet.next_packet\n\n    def test_create_legacy_trampoline_onion_multiple_rtags(self):\n        \"\"\"Test to verify we don't overfill the trampoline onion with r_tags if there are more tags than available space\"\"\"\n        dummy_route: LNPaymentTRoute = [\n            TrampolineEdge(\n                invoice_routing_info=[\n                    bfh(\"010305061295fa30847df41ae6ee809b560e78d65c2a7337a41c725ea3920b65e08a03b62b00003a0002000003e8000000010050\"),\n                    bfh(\"01037414fe3dcfedc4a0a0e153205d9a973af5096d1cd1c8c53d07ed12d7dd966f19f424000000000020000003e8000008ca0050\"),\n                    bfh(\"01038550162fa86287884a6a052471934abb5cb261c5a2b15386df8104d3c7bcb85dddd92ee1898ee15c000003e8000000010090\"),\n                    bfh(\"010244bb7ba2392ab2d493ad04ad4afcd482ca44a2bfe5b42bcc830bfe00e5b08082f424000000000029000003e8000008ca0050\")\n                ],\n                invoice_features=LnFeatures.VAR_ONION_REQ | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.BASIC_MPP_OPT,\n                short_channel_id=ShortChannelID.from_str(\"0x0x0\"),\n                start_node=node('a'),\n                end_node=node('b'),\n                fee_base_msat=0,\n                fee_proportional_millionths=0,\n                cltv_delta=0,\n                node_features=0\n            ),\n            TrampolineEdge(\n                invoice_routing_info=[],\n                invoice_features=None,\n                short_channel_id=ShortChannelID.from_str(\"0x0x0\"),\n                start_node=node('b'),\n                end_node=node('c'),\n                fee_base_msat=0,\n                fee_proportional_millionths=0,\n                cltv_delta=0,\n                node_features=0\n            ),\n        ]\n        # create a trampoline onion, this shouldn't raise InvalidPayloadSize\n        create_trampoline_onion(\n            route=dummy_route,\n            amount_msat=0,\n            final_cltv_abs=0,\n            total_msat=0,\n            payment_hash=urandom(32),\n            payment_secret=urandom(32),\n        )\n\n    @needs_test_with_all_chacha20_implementations\n    def test_decode_onion_error(self):\n        # test vector from bolt-04\n        payment_path_pubkeys = [\n            bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),\n            bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'),\n            bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'),\n            bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'),\n            bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'),\n        ]\n        session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')\n        error_packet_for_node_0 = bfh('9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d')\n\n        decoded_error, index_of_sender = _decode_onion_error(error_packet_for_node_0, payment_path_pubkeys, session_key)\n        self.assertEqual(bfh('4c2fc8bc08510334b6833ad9c3e79cd1b52ae59dfe5c2a4b23ead50f09f7ee0b0002200200fe0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),\n                             decoded_error)\n        self.assertEqual(4, index_of_sender)\n\n        failure_msg, index_of_sender = decode_onion_error(error_packet_for_node_0, payment_path_pubkeys, session_key)\n        self.assertEqual(4, index_of_sender)\n        self.assertEqual(OnionFailureCode.TEMPORARY_NODE_FAILURE, failure_msg.code)\n        self.assertEqual(b'', failure_msg.data)\n\n    async def test_find_path_for_onion_message(self):\n        self.prepare_graph()\n        amount_to_send = 1000  # we route along channels, and we use find_path_for_payment, so dummy this.\n\n        path = self.path_finder.find_path_for_payment(\n            nodeA=node('a'),\n            nodeB=node('c'),\n            invoice_amount_msat=amount_to_send,\n            node_filter=is_onion_message_node)\n        self.assertEqual([\n            PathEdge(start_node=node('a'), end_node=node('d'), short_channel_id=channel(6)),\n            PathEdge(start_node=node('d'), end_node=node('c'), short_channel_id=channel(4)),\n        ], path)\n\n        # impossible routes\n        path = self.path_finder.find_path_for_payment(\n            nodeA=node('e'),\n            nodeB=node('a'),\n            invoice_amount_msat=amount_to_send,\n            node_filter=is_onion_message_node)\n        self.assertIsNone(path)\n\n        path = self.path_finder.find_path_for_payment(\n            nodeA=node('a'),\n            nodeB=node('e'),\n            invoice_amount_msat=amount_to_send,\n            node_filter=is_onion_message_node)\n        self.assertIsNone(path)\n"
  },
  {
    "path": "tests/test_lntransport.py",
    "content": "import asyncio\nfrom typing import List\n\nimport electrum_ecc as ecc\n\nfrom electrum import util\nfrom electrum import lntransport\nfrom electrum.lntransport import LNPeerAddr, LNResponderTransport, LNTransport, extract_nodeid, split_host_port, ConnStringFormatError\nfrom electrum.util import OldTaskGroup\n\nfrom . import ElectrumTestCase\nfrom .test_bitcoin import needs_test_with_all_chacha20_implementations\n\n\nclass TestLNTransport(ElectrumTestCase):\n\n    @needs_test_with_all_chacha20_implementations\n    async def test_responder(self):\n        # local static\n        ls_priv=bytes.fromhex('2121212121212121212121212121212121212121212121212121212121212121')\n        # ephemeral\n        e_priv=bytes.fromhex('2222222222222222222222222222222222222222222222222222222222222222')\n\n        class Writer:\n            def __init__(self):\n                self.state = 0\n            def write(self, data):\n                assert self.state == 0\n                self.state += 1\n                assert len(data) == 50\n        class Reader:\n            def __init__(self):\n                self.state = 0\n            async def read(self, num_bytes):\n                assert self.state in (0, 1)\n                self.state += 1\n                if self.state-1 == 0:\n                    assert num_bytes == 50\n                    return bytes.fromhex('00036360e856310ce5d294e8be33fc807077dc56ac80d95d9cd4ddbd21325eff73f70df6086551151f58b8afe6c195782c6a')\n                elif self.state-1 == 1:\n                    assert num_bytes == 66\n                    return bytes.fromhex('00b9e3a702e93e3a9948c2ed6e5fd7590a6e1c3a0344cfc9d5b57357049aa22355361aa02e55a8fc28fef5bd6d71ad0c38228dc68b1c466263b47fdf31e560e139ba')\n        transport = LNResponderTransport(ls_priv, Reader(), Writer())\n        await transport.handshake(epriv=e_priv)\n\n    @needs_test_with_all_chacha20_implementations\n    async def test_loop(self):\n        responder_shaked = asyncio.Event()\n        server_shaked = asyncio.Event()\n        responder_key = ecc.ECPrivkey.generate_random_key()\n        initiator_key = ecc.ECPrivkey.generate_random_key()\n        messages_sent_by_client = [\n            b'hello from client',\n            b'long data from client ' + bytes(range(256)) * 100 + b'... client done',\n            b'client is running out of things to say',\n        ]\n        messages_sent_by_server = [\n            b'hello from server',\n            b'hello2 from server',\n            b'long data from server ' + bytes(range(256)) * 100 + b'... server done',\n        ]\n        async def read_messages(transport, expected_messages):\n            ctr = 0\n            async for msg in transport.read_messages():\n                self.assertEqual(expected_messages[ctr], msg)\n                ctr += 1\n                if ctr == len(expected_messages):\n                    return\n        async def write_messages(transport, expected_messages):\n            for msg in expected_messages:\n                transport.send_bytes(msg)\n                await asyncio.sleep(0.01)\n\n        async def cb(reader, writer):\n            t = LNResponderTransport(responder_key.get_secret_bytes(), reader, writer)\n            transports.append(t)\n            self.assertEqual(await t.handshake(), initiator_key.get_public_key_bytes())\n            async with OldTaskGroup() as group:\n                await group.spawn(read_messages(t, messages_sent_by_client))\n                await group.spawn(write_messages(t, messages_sent_by_server))\n            responder_shaked.set()\n        async def connect(port: int):\n            peer_addr = LNPeerAddr('127.0.0.1', port, responder_key.get_public_key_bytes())\n            t = LNTransport(initiator_key.get_secret_bytes(), peer_addr, e_proxy=None)\n            transports.append(t)\n            await t.handshake()\n            async with OldTaskGroup() as group:\n                await group.spawn(read_messages(t, messages_sent_by_server))\n                await group.spawn(write_messages(t, messages_sent_by_client))\n            server_shaked.set()\n\n        transports = []  # type: List[lntransport.LNTransportBase]\n        async def f():\n            server = await asyncio.start_server(cb, '127.0.0.1', port=None)\n            server_port = server.sockets[0].getsockname()[1]\n            try:\n                async with OldTaskGroup() as group:\n                    await group.spawn(connect(port=server_port))\n                    await group.spawn(responder_shaked.wait())\n                    await group.spawn(server_shaked.wait())\n            finally:\n                for t in transports:\n                    t.close()\n                server.close()\n                await server.wait_closed()\n\n        await f()\n\n    def test_split_host_port(self):\n        self.assertEqual(split_host_port(\"[::1]:8000\"), (\"::1\", \"8000\"))\n        self.assertEqual(split_host_port(\"[::1]\"), (\"::1\", \"9735\"))\n        self.assertEqual(split_host_port(\"[2601:602:8800:9a:dc59:a4ff:fede:24a9]:9735\"), (\"2601:602:8800:9a:dc59:a4ff:fede:24a9\", \"9735\"))\n        self.assertEqual(split_host_port(\"[2601:602:8800::a4ff:fede:24a9]:9735\"), (\"2601:602:8800::a4ff:fede:24a9\", \"9735\"))\n        self.assertEqual(split_host_port(\"kæn.guru:8000\"), (\"kæn.guru\", \"8000\"))\n        self.assertEqual(split_host_port(\"kæn.guru\"), (\"kæn.guru\", \"9735\"))\n        self.assertEqual(split_host_port(\"127.0.0.1:8000\"), (\"127.0.0.1\", \"8000\"))\n        self.assertEqual(split_host_port(\"127.0.0.1\"), (\"127.0.0.1\", \"9735\"))\n        # accepted by getaddrinfo but not ipaddress.ip_address\n        self.assertEqual(split_host_port(\"127.0.0:8000\"), (\"127.0.0\", \"8000\"))\n        self.assertEqual(split_host_port(\"127.0.0\"), (\"127.0.0\", \"9735\"))\n        self.assertEqual(split_host_port(\"electrum.org:8000\"), (\"electrum.org\", \"8000\"))\n        self.assertEqual(split_host_port(\"electrum.org\"), (\"electrum.org\", \"9735\"))\n\n        with self.assertRaises(ConnStringFormatError):\n            split_host_port(\"electrum.org:8000:\")\n        with self.assertRaises(ConnStringFormatError):\n            split_host_port(\"electrum.org:\")\n\n    def test_extract_nodeid(self):\n        pubkey1 = ecc.GENERATOR.get_public_key_bytes(compressed=True)\n        with self.assertRaises(ConnStringFormatError):\n            extract_nodeid(\"00\" * 32 + \"@localhost\")\n        with self.assertRaises(ConnStringFormatError):\n            extract_nodeid(\"00\" * 33 + \"@\")\n        # pubkey + host\n        self.assertEqual(extract_nodeid(\"00\" * 33 + \"@localhost\"), (b\"\\x00\" * 33, \"localhost\"))\n        self.assertEqual(extract_nodeid(f\"{pubkey1.hex()}@11.22.33.44\"), (pubkey1, \"11.22.33.44\"))\n        self.assertEqual(extract_nodeid(f\"{pubkey1.hex()}@[2001:41d0:e:734::1]\"), (pubkey1, \"[2001:41d0:e:734::1]\"))\n        # pubkey + host + port\n        self.assertEqual(extract_nodeid(f\"{pubkey1.hex()}@11.22.33.44:5555\"), (pubkey1, \"11.22.33.44:5555\"))\n        self.assertEqual(extract_nodeid(f\"{pubkey1.hex()}@[2001:41d0:e:734::1]:8888\"), (pubkey1, \"[2001:41d0:e:734::1]:8888\"))\n        # just pubkey\n        self.assertEqual(extract_nodeid(f\"{pubkey1.hex()}\"), (pubkey1, None))\n\n\nclass TestLNPeerAddr(ElectrumTestCase):\n\n    def test_validate_net_address(self):\n        # Test invalid host\n        with self.assertRaises(ValueError):\n            LNPeerAddr(\"\", 9735, b'\\x00'*33)\n        with self.assertRaises(ValueError):\n            LNPeerAddr(\"999.999.999.999\", 9735, b'\\x00'*33)\n        # Test invalid port\n        with self.assertRaises(ValueError):\n            LNPeerAddr(\"127.0.0.1\", -1, b'\\x00'*33)\n        with self.assertRaises(ValueError):\n            LNPeerAddr(\"127.0.0.1\", 70000, b'\\x00'*33)\n\n    def test_is_onion(self):\n        # Test onion addresses\n        addr1 = LNPeerAddr(\"example.onion\", 9735, b'\\x00'*33)\n        self.assertTrue(addr1.is_onion())\n        addr2 = LNPeerAddr(\"subdomain.example.onion\", 9735, b'\\x00'*33)\n        self.assertTrue(addr2.is_onion())\n\n        # Test non-onion\n        addr3 = LNPeerAddr(\"example.com\", 9735, b'\\x00'*33)\n        self.assertFalse(addr3.is_onion())\n        addr4 = LNPeerAddr(\"127.0.0.1\", 9735, b'\\x00'*33)\n        self.assertFalse(addr4.is_onion())\n        addr5 = LNPeerAddr(\"::1\", 9735, b'\\x00'*33)\n        self.assertFalse(addr5.is_onion())\n        addr6 = LNPeerAddr(\"onion\", 9735, b'\\x00'*33)\n        self.assertFalse(addr6.is_onion())\n"
  },
  {
    "path": "tests/test_lnurl.py",
    "content": "from unittest import TestCase\n\nfrom electrum import lnurl\n\n\nclass TestLnurl(TestCase):\n    def test_decode(self):\n        LNURL = (\n            \"LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWXQ96S9\"\n        )\n        url = lnurl.decode_lnurl(LNURL)\n        self.assertEqual(\"https://service.io/?q=3fc3645b439ce8e7\", url)\n\n    def test_encode(self):\n        lnurl_ = lnurl.encode_lnurl(\"https://jhoenicke.de/.well-known/lnurlp/mempool\")\n        self.assertEqual(\n            \"LNURL1DP68GURN8GHJ76NGDAJKU6TRDDJJUER99UH8WETVDSKKKMN0WAHZ7MRWW4EXCUP0D4JK6UR0DAKQHMHNX2\",\n            lnurl_)\n\n    def test_lightning_address_to_url(self):\n        url = lnurl.lightning_address_to_url(\"mempool@jhoenicke.de\")\n        self.assertEqual(\"https://jhoenicke.de/.well-known/lnurlp/mempool\", url)\n\n    def test_parse_lnurl3_response(self):\n        # Test successful parsing with all fields\n        sample_response = {\n            'callback': 'https://service.io/withdraw?sessionid=123',\n            'k1': 'abcdef1234567890',\n            'defaultDescription': 'Withdraw from service',\n            'minWithdrawable': 10_000_000,\n            'maxWithdrawable': 100_000_000,\n        }\n\n        result = lnurl._parse_lnurl3_response(sample_response)\n\n        self.assertEqual('https://service.io/withdraw?sessionid=123', result.callback_url)\n        self.assertEqual('abcdef1234567890', result.k1)\n        self.assertEqual('Withdraw from service', result.default_description)\n        self.assertEqual(10_000, result.min_withdrawable_sat)\n        self.assertEqual(100_000, result.max_withdrawable_sat)\n\n        # Test with .onion URL\n        onion_response = {\n            'callback': 'http://robosatsy56bwqn56qyadmcxkx767hnabg4mihxlmgyt6if5gnuxvzad.onion/withdraw?sessionid=123',\n            'k1': 'abcdef1234567890',\n            'minWithdrawable': 10_000_000,\n            'maxWithdrawable': 100_000_000\n        }\n\n        result = lnurl._parse_lnurl3_response(onion_response)\n        self.assertEqual('http://robosatsy56bwqn56qyadmcxkx767hnabg4mihxlmgyt6if5gnuxvzad.onion/withdraw?sessionid=123',\n                         result.callback_url)\n        self.assertEqual('', result.default_description)  # Missing defaultDescription uses empty string\n\n        # Test missing callback (should raise error)\n        no_callback_response = {\n            'k1': 'abcdef1234567890',\n            'minWithdrawable': 10_000_000,\n            'maxWithdrawable': 100_000_000\n        }\n\n        with self.assertRaises(lnurl.LNURLError):\n            lnurl._parse_lnurl3_response(no_callback_response)\n\n        # Test unsafe callback URL\n        unsafe_response = {\n            'callback': 'http://service.io/withdraw?sessionid=123',  # HTTP URL\n            'k1': 'abcdef1234567890',\n            'minWithdrawable': 10_000_000,\n            'maxWithdrawable': 100_000_000\n        }\n\n        with self.assertRaises(lnurl.LNURLError):\n            lnurl._parse_lnurl3_response(unsafe_response)\n\n        # Test missing k1 (should raise error)\n        no_k1_response = {\n            'callback': 'https://service.io/withdraw?sessionid=123',\n            'minWithdrawable': 10_000_000,\n            'maxWithdrawable': 100_000_000\n        }\n\n        with self.assertRaises(lnurl.LNURLError):\n            lnurl._parse_lnurl3_response(no_k1_response)\n\n        # Test missing withdrawable amounts (should raise error)\n        no_amounts_response = {\n            'callback': 'https://service.io/withdraw?sessionid=123',\n            'k1': 'abcdef1234567890',\n        }\n\n        with self.assertRaises(lnurl.LNURLError):\n            lnurl._parse_lnurl3_response(no_amounts_response)\n\n        # Test malformed withdrawable amounts (should raise error)\n        bad_amounts_response = {\n            'callback': 'https://service.io/withdraw?sessionid=123',\n            'k1': 'abcdef1234567890',\n            'minWithdrawable': 'this is not a number',\n            'maxWithdrawable': 100_000_000\n        }\n\n        with self.assertRaises(lnurl.LNURLError):\n            lnurl._parse_lnurl3_response(bad_amounts_response)\n"
  },
  {
    "path": "tests/test_lnutil.py",
    "content": "import os\nimport json\nfrom typing import Dict, List\n\nfrom electrum import bitcoin\nfrom electrum.json_db import StoredDict\nfrom electrum.lnutil import (\n    RevocationStore, get_per_commitment_secret_from_seed, make_offered_htlc, make_received_htlc, make_commitment,\n    make_htlc_tx_witness, make_htlc_tx_output, make_htlc_tx_inputs, secret_to_pubkey, derive_blinded_pubkey,\n    derive_privkey, derive_pubkey, make_htlc_tx, extract_ctn_from_tx, get_compressed_pubkey_from_bech32,\n    ScriptHtlc, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures, ln_compare_features,\n    IncompatibleLightningFeatures, ChannelType, offered_htlc_trim_threshold_sat, received_htlc_trim_threshold_sat,\n    ImportedChannelBackupStorage, list_enabled_ln_feature_bits, PaymentFeeBudget,\n)\nfrom electrum.util import bfh, MyEncoder\nfrom electrum.transaction import Transaction, PartialTransaction, Sighash\nfrom electrum.lnworker import LNWallet\nfrom electrum.wallet import Standard_Wallet\nfrom electrum.simple_config import SimpleConfig\n\nfrom . import ElectrumTestCase, as_testnet\nfrom . import restore_wallet_from_text__for_unittest\nfrom .test_bitcoin import disable_ecdsa_r_value_grinding\n\n\n# test vectors for a single channel\n# https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#appendix-c-commitment-and-htlc-transaction-test-vectors\nfunding_tx_id = '8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be'\nfunding_output_index = 0\nfunding_amount_satoshi = 10000000\ncommitment_number = 42\nlocal_delay = 144\nlocal_dust_limit_satoshi = 546\n\nlocal_payment_basepoint = bytes.fromhex('034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa')\nremote_payment_basepoint = bytes.fromhex('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991')\n# obs = get_obscured_ctn(42, local_payment_basepoint, remote_payment_basepoint)\nlocal_funding_privkey = bytes.fromhex('30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f374901')\nlocal_funding_pubkey = bytes.fromhex('023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb')\nremote_funding_pubkey = bytes.fromhex('030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1')\nlocal_privkey = bytes.fromhex('bb13b121cdc357cd2e608b0aea294afca36e2b34cf958e2e6451a2f27469449101')\nlocalpubkey = bytes.fromhex('030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e7')\nremotepubkey = bytes.fromhex('0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b')\nlocal_delayedpubkey = bytes.fromhex('03fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c')\nlocal_revocation_pubkey = bytes.fromhex('0212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b19')\n# funding wscript = 5221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae\n\n\n# anchor test vectors are from https://github.com/lightningnetwork/lightning-rfc/commit/1739746afa3863ca783df9be4b7b0338afb63b49\nanchor_test_vector_path = os.path.join(os.path.dirname(__file__), \"anchor-vectors.json\")\nwith open(anchor_test_vector_path) as f:\n    ANCHOR_TEST_VECTORS = json.load(f)\n\n# in a commitment transaction with all the below htlcs, the order is different,\n# indices 1 and 2 are swapped\nTEST_HTLCS = [\n    {\n        'incoming': True,\n        'amount':   1000000,\n        'expiry':   500,\n        'preimage': \"0000000000000000000000000000000000000000000000000000000000000000\",\n    },\n    {\n        'incoming': True,\n        'amount':   2000000,\n        'expiry':   501,\n        'preimage': \"0101010101010101010101010101010101010101010101010101010101010101\",\n    },\n    {\n        'incoming': False,\n        'amount':   2000000,\n        'expiry':   502,\n        'preimage': \"0202020202020202020202020202020202020202020202020202020202020202\",\n    },\n    {\n        'incoming': False,\n        'amount':   3000000,\n        'expiry':   503,\n        'preimage': \"0303030303030303030303030303030303030303030303030303030303030303\",\n    },\n    {\n        'incoming': True,\n        'amount':   4000000,\n        'expiry':   504,\n        'preimage': \"0404040404040404040404040404040404040404040404040404040404040404\",\n    }\n]\n\n\nclass TestLNUtil(ElectrumTestCase):\n    def test_shachain_store(self):\n        tests = [\n            {\n                \"name\": \"insert_secret correct sequence\",\n                \"inserts\": [\n                    {\n                        \"index\": 281474976710655,\n                        \"secret\": \"7cc854b54e3e0dcdb010d7a3fee464a9687b\" +\\\n                            \"e6e8db3be6854c475621e007a5dc\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710654,\n                        \"secret\": \"c7518c8ae4660ed02894df8976fa1a3659c1\" +\\\n                            \"a8b4b5bec0c4b872abeba4cb8964\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710653,\n                        \"secret\": \"2273e227a5b7449b6e70f1fb4652864038b1\" +\\\n                            \"cbf9cd7c043a7d6456b7fc275ad8\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710652,\n                        \"secret\": \"27cddaa5624534cb6cb9d7da077cf2b22ab2\" +\\\n                            \"1e9b506fd4998a51d54502e99116\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710651,\n                        \"secret\": \"c65716add7aa98ba7acb236352d665cab173\" +\\\n                            \"45fe45b55fb879ff80e6bd0c41dd\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710650,\n                        \"secret\": \"969660042a28f32d9be17344e09374b37996\" +\\\n                            \"2d03db1574df5a8a5a47e19ce3f2\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710649,\n                        \"secret\": \"a5a64476122ca0925fb344bdc1854c1c0a59\" +\\\n                            \"fc614298e50a33e331980a220f32\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710648,\n                        \"secret\": \"05cde6323d949933f7f7b78776bcc1ea6d9b\" +\\\n                            \"31447732e3802e1f7ac44b650e17\",\n                        \"successful\": True\n                    }\n                ]\n            },\n            {\n                \"name\": \"insert_secret #1 incorrect\",\n                \"inserts\": [\n                    {\n                        \"index\": 281474976710655,\n                        \"secret\": \"02a40c85b6f28da08dfdbe0926c53fab2d\" +\\\n                            \"e6d28c10301f8f7c4073d5e42e3148\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710654,\n                        \"secret\": \"c7518c8ae4660ed02894df8976fa1a3659\" +\\\n                            \"c1a8b4b5bec0c4b872abeba4cb8964\",\n                        \"successful\": False\n                    }\n                ]\n            },\n            {\n                \"name\": \"insert_secret #2 incorrect (#1 derived from incorrect)\",\n                \"inserts\": [\n                    {\n                        \"index\": 281474976710655,\n                        \"secret\": \"02a40c85b6f28da08dfdbe0926c53fab2de6\" +\\\n                            \"d28c10301f8f7c4073d5e42e3148\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710654,\n                        \"secret\": \"dddc3a8d14fddf2b68fa8c7fbad274827493\" +\\\n                            \"7479dd0f8930d5ebb4ab6bd866a3\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710653,\n                        \"secret\": \"2273e227a5b7449b6e70f1fb4652864038b1\" +\\\n                            \"cbf9cd7c043a7d6456b7fc275ad8\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710652,\n                        \"secret\": \"27cddaa5624534cb6cb9d7da077cf2b22a\" +\\\n                            \"b21e9b506fd4998a51d54502e99116\",\n                        \"successful\": False\n                    }\n                ]\n            },\n            {\n                \"name\": \"insert_secret #3 incorrect\",\n                \"inserts\": [\n                    {\n                        \"index\": 281474976710655,\n                        \"secret\": \"7cc854b54e3e0dcdb010d7a3fee464a9687b\" +\\\n                            \"e6e8db3be6854c475621e007a5dc\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710654,\n                        \"secret\": \"c7518c8ae4660ed02894df8976fa1a3659c1\" +\\\n                            \"a8b4b5bec0c4b872abeba4cb8964\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710653,\n                        \"secret\": \"c51a18b13e8527e579ec56365482c62f180b\" +\\\n                            \"7d5760b46e9477dae59e87ed423a\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710652,\n                        \"secret\": \"27cddaa5624534cb6cb9d7da077cf2b22ab2\" +\\\n                            \"1e9b506fd4998a51d54502e99116\",\n                        \"successful\": False\n                    }\n                ]\n            },\n            {\n                \"name\": \"insert_secret #4 incorrect (1,2,3 derived from incorrect)\",\n                \"inserts\": [\n                    {\n                        \"index\": 281474976710655,\n                        \"secret\": \"02a40c85b6f28da08dfdbe0926c53fab2de6\" +\\\n                            \"d28c10301f8f7c4073d5e42e3148\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710654,\n                        \"secret\": \"dddc3a8d14fddf2b68fa8c7fbad274827493\" +\\\n                            \"7479dd0f8930d5ebb4ab6bd866a3\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710653,\n                        \"secret\": \"c51a18b13e8527e579ec56365482c62f18\" +\\\n                            \"0b7d5760b46e9477dae59e87ed423a\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710652,\n                        \"secret\": \"ba65d7b0ef55a3ba300d4e87af29868f39\" +\\\n                            \"4f8f138d78a7011669c79b37b936f4\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710651,\n                        \"secret\": \"c65716add7aa98ba7acb236352d665cab1\" +\\\n                            \"7345fe45b55fb879ff80e6bd0c41dd\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710650,\n                        \"secret\": \"969660042a28f32d9be17344e09374b379\" +\\\n                            \"962d03db1574df5a8a5a47e19ce3f2\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710649,\n                        \"secret\": \"a5a64476122ca0925fb344bdc1854c1c0a\" +\\\n                            \"59fc614298e50a33e331980a220f32\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710649,\n                        \"secret\": \"05cde6323d949933f7f7b78776bcc1ea6d9b\" +\\\n                            \"31447732e3802e1f7ac44b650e17\",\n                        \"successful\": False\n                    }\n                ]\n            },\n            {\n                \"name\": \"insert_secret #5 incorrect\",\n                \"inserts\": [\n                    {\n                        \"index\": 281474976710655,\n                        \"secret\": \"7cc854b54e3e0dcdb010d7a3fee464a9687b\" +\\\n                            \"e6e8db3be6854c475621e007a5dc\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710654,\n                        \"secret\": \"c7518c8ae4660ed02894df8976fa1a3659c1a\" +\\\n                            \"8b4b5bec0c4b872abeba4cb8964\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710653,\n                        \"secret\": \"2273e227a5b7449b6e70f1fb4652864038b1\" +\\\n                            \"cbf9cd7c043a7d6456b7fc275ad8\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710652,\n                        \"secret\": \"27cddaa5624534cb6cb9d7da077cf2b22ab21\" +\\\n                            \"e9b506fd4998a51d54502e99116\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710651,\n                        \"secret\": \"631373ad5f9ef654bb3dade742d09504c567\" +\\\n                            \"edd24320d2fcd68e3cc47e2ff6a6\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710650,\n                        \"secret\": \"969660042a28f32d9be17344e09374b37996\" +\\\n                            \"2d03db1574df5a8a5a47e19ce3f2\",\n                        \"successful\": False\n                    }\n                ]\n            },\n            {\n                \"name\": \"insert_secret #6 incorrect (5 derived from incorrect)\",\n                \"inserts\": [\n                    {\n                        \"index\": 281474976710655,\n                        \"secret\": \"7cc854b54e3e0dcdb010d7a3fee464a9687b\" +\\\n                            \"e6e8db3be6854c475621e007a5dc\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710654,\n                        \"secret\": \"c7518c8ae4660ed02894df8976fa1a3659c1a\" +\\\n                            \"8b4b5bec0c4b872abeba4cb8964\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710653,\n                        \"secret\": \"2273e227a5b7449b6e70f1fb4652864038b1\" +\\\n                            \"cbf9cd7c043a7d6456b7fc275ad8\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710652,\n                        \"secret\": \"27cddaa5624534cb6cb9d7da077cf2b22ab21\" +\\\n                            \"e9b506fd4998a51d54502e99116\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710651,\n                        \"secret\": \"631373ad5f9ef654bb3dade742d09504c567\" +\\\n                            \"edd24320d2fcd68e3cc47e2ff6a6\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710650,\n                        \"secret\": \"b7e76a83668bde38b373970155c868a65330\" +\\\n                            \"4308f9896692f904a23731224bb1\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710649,\n                        \"secret\": \"a5a64476122ca0925fb344bdc1854c1c0a59f\" +\\\n                            \"c614298e50a33e331980a220f32\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710648,\n                        \"secret\": \"05cde6323d949933f7f7b78776bcc1ea6d9b\" +\\\n                            \"31447732e3802e1f7ac44b650e17\",\n                        \"successful\": False\n                    }\n                ]\n            },\n            {\n                \"name\": \"insert_secret #7 incorrect\",\n                \"inserts\": [\n                    {\n                        \"index\": 281474976710655,\n                        \"secret\": \"7cc854b54e3e0dcdb010d7a3fee464a9687b\" +\\\n                            \"e6e8db3be6854c475621e007a5dc\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710654,\n                        \"secret\": \"c7518c8ae4660ed02894df8976fa1a3659c1a\" +\\\n                            \"8b4b5bec0c4b872abeba4cb8964\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710653,\n                        \"secret\": \"2273e227a5b7449b6e70f1fb4652864038b1\" +\\\n                            \"cbf9cd7c043a7d6456b7fc275ad8\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710652,\n                        \"secret\": \"27cddaa5624534cb6cb9d7da077cf2b22ab21\" +\\\n                            \"e9b506fd4998a51d54502e99116\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710651,\n                        \"secret\": \"c65716add7aa98ba7acb236352d665cab173\" +\\\n                            \"45fe45b55fb879ff80e6bd0c41dd\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710650,\n                        \"secret\": \"969660042a28f32d9be17344e09374b37996\" +\\\n                            \"2d03db1574df5a8a5a47e19ce3f2\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710649,\n                        \"secret\": \"e7971de736e01da8ed58b94c2fc216cb1d\" +\\\n                            \"ca9e326f3a96e7194fe8ea8af6c0a3\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710648,\n                        \"secret\": \"05cde6323d949933f7f7b78776bcc1ea6d\" +\\\n                            \"9b31447732e3802e1f7ac44b650e17\",\n                        \"successful\": False\n                    }\n                ]\n            },\n            {\n                \"name\": \"insert_secret #8 incorrect\",\n                \"inserts\": [\n                    {\n                        \"index\": 281474976710655,\n                        \"secret\": \"7cc854b54e3e0dcdb010d7a3fee464a9687b\" +\\\n                            \"e6e8db3be6854c475621e007a5dc\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710654,\n                        \"secret\": \"c7518c8ae4660ed02894df8976fa1a3659c1a\" +\\\n                            \"8b4b5bec0c4b872abeba4cb8964\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710653,\n                        \"secret\": \"2273e227a5b7449b6e70f1fb4652864038b1\" +\\\n                            \"cbf9cd7c043a7d6456b7fc275ad8\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710652,\n                        \"secret\": \"27cddaa5624534cb6cb9d7da077cf2b22ab21\" +\\\n                            \"e9b506fd4998a51d54502e99116\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710651,\n                        \"secret\": \"c65716add7aa98ba7acb236352d665cab173\" +\\\n                            \"45fe45b55fb879ff80e6bd0c41dd\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710650,\n                        \"secret\": \"969660042a28f32d9be17344e09374b37996\" +\\\n                            \"2d03db1574df5a8a5a47e19ce3f2\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710649,\n                        \"secret\": \"a5a64476122ca0925fb344bdc1854c1c0a\" +\\\n                            \"59fc614298e50a33e331980a220f32\",\n                        \"successful\": True\n                    },\n                    {\n                        \"index\": 281474976710648,\n                        \"secret\": \"a7efbc61aac46d34f77778bac22c8a20c6\" +\\\n                            \"a46ca460addc49009bda875ec88fa4\",\n                        \"successful\": False\n                    }\n                ]\n            }\n        ]\n\n        for test in tests:\n            receiver = RevocationStore(StoredDict({}, None))\n            for insert in test[\"inserts\"]:\n                secret = bytes.fromhex(insert[\"secret\"])\n\n                try:\n                    receiver.add_next_entry(secret)\n                except Exception as e:\n                    if insert[\"successful\"]:\n                        raise Exception(\"Failed ({}): error was received but it shouldn't: {}\".format(test[\"name\"], e))\n                else:\n                    if not insert[\"successful\"]:\n                        raise Exception(\"Failed ({}): error wasn't received\".format(test[\"name\"]))\n\n            for insert in test[\"inserts\"]:\n                secret = bytes.fromhex(insert[\"secret\"])\n                index = insert[\"index\"]\n                if insert[\"successful\"]:\n                    self.assertEqual(secret, receiver.retrieve_secret(index))\n\n            print(\"Passed ({})\".format(test[\"name\"]))\n\n    def test_shachain_produce_consume(self):\n        seed = bitcoin.sha256(b\"shachaintest\")\n        consumer = RevocationStore(StoredDict({}, None))\n        for i in range(10000):\n            secret = get_per_commitment_secret_from_seed(seed, RevocationStore.START_INDEX - i)\n            try:\n                consumer.add_next_entry(secret)\n            except Exception as e:\n                raise Exception(\"iteration \" + str(i) + \": \" + str(e))\n            if i % 1000 == 0:\n                c1 = consumer\n                s1 = json.dumps(c1.storage, cls=MyEncoder)\n                c2 = RevocationStore(StoredDict(json.loads(s1), None))\n                s2 = json.dumps(c2.storage, cls=MyEncoder)\n                self.assertEqual(s1, s2)\n\n    def test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate(self):\n        to_local_msat = 6988000000\n        to_remote_msat = 3000000000\n        local_feerate_per_kw = 0\n        # base commitment transaction fee = 0\n        # actual commitment transaction fee = 0\n\n        per_commitment_secret = 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100\n        per_commitment_point = secret_to_pubkey(per_commitment_secret)\n\n        remote_htlcpubkey = remotepubkey\n        local_htlcpubkey = localpubkey\n\n        htlc_cltv_timeout = {}\n        htlc_payment_preimage = {}\n        htlc = {}\n        htlc_pubkeys = {\n            \"revocation_pubkey\": local_revocation_pubkey,\n            \"remote_htlcpubkey\": remote_htlcpubkey,\n            \"local_htlcpubkey\": local_htlcpubkey,\n        }\n\n        htlc_cltv_timeout[2] = 502\n        htlc_payment_preimage[2] = b\"\\x02\" * 32\n        htlc[2] = make_offered_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[2]), has_anchors=False)\n\n        htlc_cltv_timeout[3] = 503\n        htlc_payment_preimage[3] = b\"\\x03\" * 32\n        htlc[3] = make_offered_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[3]), has_anchors=False)\n\n        htlc_cltv_timeout[0] = 500\n        htlc_payment_preimage[0] = b\"\\x00\" * 32\n        htlc[0] = make_received_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[0]), cltv_abs=htlc_cltv_timeout[0], has_anchors=False)\n\n        htlc_cltv_timeout[1] = 501\n        htlc_payment_preimage[1] = b\"\\x01\" * 32\n        htlc[1] = make_received_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[1]), cltv_abs=htlc_cltv_timeout[1], has_anchors=False)\n\n        htlc_cltv_timeout[4] = 504\n        htlc_payment_preimage[4] = b\"\\x04\" * 32\n        htlc[4] = make_received_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[4]), cltv_abs=htlc_cltv_timeout[4], has_anchors=False)\n\n        remote_signature = bfh(\"304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b70606\")\n        output_commit_tx = \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\"\n\n        htlc_obj = {}\n        for num, msat in [(0, 1000 * 1000),\n                          (2, 2000 * 1000),\n                          (1, 2000 * 1000),\n                          (3, 3000 * 1000),\n                          (4, 4000 * 1000)]:\n            htlc_obj[num] = UpdateAddHtlc(amount_msat=msat, payment_hash=bitcoin.sha256(htlc_payment_preimage[num]), cltv_abs=0, htlc_id=None, timestamp=0)\n        htlcs = [ScriptHtlc(htlc[x], htlc_obj[x]) for x in range(5)]\n\n        our_commit_tx = make_commitment(\n            ctn=commitment_number,\n            local_funding_pubkey=local_funding_pubkey,\n            remote_funding_pubkey=remote_funding_pubkey,\n            remote_payment_pubkey=remotepubkey,\n            funder_payment_basepoint=local_payment_basepoint,\n            fundee_payment_basepoint=remote_payment_basepoint,\n            revocation_pubkey=local_revocation_pubkey,\n            delayed_pubkey=local_delayedpubkey,\n            to_self_delay=local_delay,\n            funding_txid=funding_tx_id,\n            funding_pos=funding_output_index,\n            funding_sat=funding_amount_satoshi,\n            local_amount=to_local_msat,\n            remote_amount=to_remote_msat,\n            dust_limit_sat=local_dust_limit_satoshi,\n            fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=len(htlcs), feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False),\n            htlcs=htlcs,\n            has_anchors=False\n        )\n        self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)\n        self.assertEqual(str(our_commit_tx), output_commit_tx)\n\n        signature_for_output_remote_htlc = {}\n        signature_for_output_remote_htlc[0] = \"304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a6\"\n        signature_for_output_remote_htlc[2] = \"3045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b\"\n        signature_for_output_remote_htlc[1] = \"304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f202\"\n        signature_for_output_remote_htlc[3] = \"3045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554\"\n        signature_for_output_remote_htlc[4] = \"304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d\"\n\n        output_htlc_tx = {}\n        SUCCESS = True\n        TIMEOUT = False\n        output_htlc_tx[0] = (SUCCESS, \"020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219700000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a60147304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000\")\n\n        output_htlc_tx[2] = (TIMEOUT, \"020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219701000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b0147304402205735e9f335dfd123f730ac5bf184fd7d5b672e4d84c51a3f0478cc229bb44936022018b1cec3e3b29e5cc335d7e326bc29d75a7e063216427d081cb83ebdbd828b4d01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000\")\n\n        output_htlc_tx[1] = (SUCCESS, \"020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219702000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f202014730440220481a48f83c358ae0f220e37f88e56b3d434cefaded82065b8e7a9fd78fee7a26022022674ab37a4c39e6efba302f760ca05931d8add8d65231c5bf34a6c2a76b15bf012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000\")\n\n        output_htlc_tx[3] = (TIMEOUT, \"020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219703000000000000000001b80b0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554014730440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac0872701008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000\")\n\n        output_htlc_tx[4] = (SUCCESS, \"020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219704000000000000000001a00f0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d014730440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000\")\n\n        htlc_output_index = {0: 0, 1: 2, 2: 1, 3: 3, 4: 4}\n\n        for i in range(5):\n            self.assertEqual(output_htlc_tx[i][1], self.htlc_tx(\n                htlc[i],\n                htlc_output_index[i],\n                htlcs[i].htlc.amount_msat,\n                htlc_payment_preimage[i],\n                signature_for_output_remote_htlc[i],\n                output_htlc_tx[i][0],\n                htlc_cltv_timeout[i] if not output_htlc_tx[i][0] else 0,\n                local_feerate_per_kw,\n                our_commit_tx,\n                False,\n            ))\n\n    def htlc_tx(self, htlc: bytes, htlc_output_index: int, amount_msat: int,\n                htlc_payment_preimage: bytes, remote_htlc_sig: str,\n                success: bool, cltv_abs: int,\n                local_feerate_per_kw: int, our_commit_tx: PartialTransaction,\n                has_anchors: bool) -> str:\n        _script, our_htlc_tx_output = make_htlc_tx_output(\n            amount_msat=amount_msat,\n            local_feerate=local_feerate_per_kw,\n            revocationpubkey=local_revocation_pubkey,\n            local_delayedpubkey=local_delayedpubkey,\n            success=success,\n            to_self_delay=local_delay,\n            has_anchors=has_anchors\n        )\n        our_htlc_tx_inputs = make_htlc_tx_inputs(\n            htlc_output_txid=our_commit_tx.txid(),\n            htlc_output_index=htlc_output_index,\n            amount_msat=amount_msat,\n            witness_script=htlc)\n        our_htlc_tx = make_htlc_tx(\n            cltv_abs=cltv_abs,\n            inputs=our_htlc_tx_inputs,\n            output=our_htlc_tx_output)\n\n        remote_sighash = Sighash.ALL\n        if has_anchors:\n            remote_sighash = Sighash.ANYONECANPAY | Sighash.SINGLE\n            our_htlc_tx.inputs()[0].nsequence = 1\n\n        our_htlc_tx.inputs()[0].sighash = Sighash.ALL\n        local_sig = our_htlc_tx.sign_txin(0, local_privkey[:-1])\n\n        our_htlc_tx_witness = make_htlc_tx_witness(\n            remotehtlcsig=bfh(remote_htlc_sig) + remote_sighash.to_bytes(1, 'big'),\n            localhtlcsig=local_sig,\n            payment_preimage=htlc_payment_preimage if success else b'',  # will put 00 on witness if timeout\n            witness_script=htlc)\n        our_htlc_tx._inputs[0].witness = our_htlc_tx_witness\n        return str(our_htlc_tx)\n\n    def test_commitment_tx_with_one_output(self):\n        to_local_msat= 6988000000\n        to_remote_msat= 3000000000\n        local_feerate_per_kw= 9651181\n        remote_signature = bfh(\"3044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e\")\n        output_commit_tx= \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431100400473044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b101473044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\"\n\n        our_commit_tx = make_commitment(\n            ctn=commitment_number,\n            local_funding_pubkey=local_funding_pubkey,\n            remote_funding_pubkey=remote_funding_pubkey,\n            remote_payment_pubkey=remotepubkey,\n            funder_payment_basepoint=local_payment_basepoint,\n            fundee_payment_basepoint=remote_payment_basepoint,\n            revocation_pubkey=local_revocation_pubkey,\n            delayed_pubkey=local_delayedpubkey,\n            to_self_delay=local_delay,\n            funding_txid=funding_tx_id,\n            funding_pos=funding_output_index,\n            funding_sat=funding_amount_satoshi,\n            local_amount=to_local_msat,\n            remote_amount=to_remote_msat,\n            dust_limit_sat=local_dust_limit_satoshi,\n            fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False),\n            htlcs=[],\n            has_anchors=False\n        )\n        self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)\n\n        self.assertEqual(str(our_commit_tx), output_commit_tx)\n\n    def test_commitment_tx_with_fee_greater_than_funder_amount(self):\n        to_local_msat= 6988000000\n        to_remote_msat= 3000000000\n        local_feerate_per_kw= 9651936\n        remote_signature = bfh(\"3044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e\")\n        output_commit_tx= \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431100400473044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b101473044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\"\n\n        our_commit_tx = make_commitment(\n            ctn=commitment_number,\n            local_funding_pubkey=local_funding_pubkey,\n            remote_funding_pubkey=remote_funding_pubkey,\n            remote_payment_pubkey=remotepubkey,\n            funder_payment_basepoint=local_payment_basepoint,\n            fundee_payment_basepoint=remote_payment_basepoint,\n            revocation_pubkey=local_revocation_pubkey,\n            delayed_pubkey=local_delayedpubkey,\n            to_self_delay=local_delay,\n            funding_txid=funding_tx_id,\n            funding_pos=funding_output_index,\n            funding_sat=funding_amount_satoshi,\n            local_amount=to_local_msat,\n            remote_amount=to_remote_msat,\n            dust_limit_sat=local_dust_limit_satoshi,\n            fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False),\n            htlcs=[],\n            has_anchors=False\n        )\n        self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)\n\n        self.assertEqual(str(our_commit_tx), output_commit_tx)\n\n    def test_extract_commitment_number_from_tx(self):\n        raw_tx = \"02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220\"\n        tx = Transaction(raw_tx)\n        self.assertEqual(commitment_number, extract_ctn_from_tx(tx, 0, local_payment_basepoint, remote_payment_basepoint))\n\n    def test_per_commitment_secret_from_seed(self):\n        self.assertEqual(0x02a40c85b6f28da08dfdbe0926c53fab2de6d28c10301f8f7c4073d5e42e3148.to_bytes(byteorder=\"big\", length=32),\n                         get_per_commitment_secret_from_seed(0x0000000000000000000000000000000000000000000000000000000000000000.to_bytes(byteorder=\"big\", length=32), 281474976710655))\n        self.assertEqual(0x7cc854b54e3e0dcdb010d7a3fee464a9687be6e8db3be6854c475621e007a5dc.to_bytes(byteorder=\"big\", length=32),\n                         get_per_commitment_secret_from_seed(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.to_bytes(byteorder=\"big\", length=32), 281474976710655))\n        self.assertEqual(0x56f4008fb007ca9acf0e15b054d5c9fd12ee06cea347914ddbaed70d1c13a528.to_bytes(byteorder=\"big\", length=32),\n                         get_per_commitment_secret_from_seed(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.to_bytes(byteorder=\"big\", length=32), 0xaaaaaaaaaaa))\n        self.assertEqual(0x9015daaeb06dba4ccc05b91b2f73bd54405f2be9f217fbacd3c5ac2e62327d31.to_bytes(byteorder=\"big\", length=32),\n                         get_per_commitment_secret_from_seed(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.to_bytes(byteorder=\"big\", length=32), 0x555555555555))\n        self.assertEqual(0x915c75942a26bb3a433a8ce2cb0427c29ec6c1775cfc78328b57f6ba7bfeaa9c.to_bytes(byteorder=\"big\", length=32),\n                         get_per_commitment_secret_from_seed(0x0101010101010101010101010101010101010101010101010101010101010101.to_bytes(byteorder=\"big\", length=32), 1))\n\n    def test_key_derivation(self):\n        # BOLT3, Appendix E\n        base_secret = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\n        per_commitment_secret = 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100\n        revocation_basepoint_secret = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\n        base_point = secret_to_pubkey(base_secret)\n        self.assertEqual(base_point, bfh('036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2'))\n        per_commitment_point = secret_to_pubkey(per_commitment_secret)\n        self.assertEqual(per_commitment_point, bfh('025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486'))\n        localpubkey = derive_pubkey(base_point, per_commitment_point)\n        self.assertEqual(localpubkey, bfh('0235f2dbfaa89b57ec7b055afe29849ef7ddfeb1cefdb9ebdc43f5494984db29e5'))\n        localprivkey = derive_privkey(base_secret, per_commitment_point)\n        self.assertEqual(localprivkey, 0xcbced912d3b21bf196a766651e436aff192362621ce317704ea2f75d87e7be0f)\n        revocation_basepoint = secret_to_pubkey(revocation_basepoint_secret)\n        self.assertEqual(revocation_basepoint, bfh('036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2'))\n        revocationpubkey = derive_blinded_pubkey(revocation_basepoint, per_commitment_point)\n        self.assertEqual(revocationpubkey, bfh('02916e326636d19c33f13e8c0c3a03dd157f332f3e99c317c141dd865eb01f8ff0'))\n\n    def test_simple_commitment_tx_with_no_HTLCs(self):\n        to_local_msat = 7000000000\n        to_remote_msat = 3000000000\n        local_feerate_per_kw = 15000\n        # base commitment transaction fee = 10860\n        # actual commitment transaction fee = 10860\n        # to_local amount 6989140 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac\n        # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b)\n        remote_signature = bfh(\"3045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c0\")\n        # local_signature = 3044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c3836939\n        our_commit_tx = make_commitment(\n            ctn=commitment_number,\n            local_funding_pubkey=local_funding_pubkey,\n            remote_funding_pubkey=remote_funding_pubkey,\n            remote_payment_pubkey=remotepubkey,\n            funder_payment_basepoint=local_payment_basepoint,\n            fundee_payment_basepoint=remote_payment_basepoint,\n            revocation_pubkey=local_revocation_pubkey,\n            delayed_pubkey=local_delayedpubkey,\n            to_self_delay=local_delay,\n            funding_txid=funding_tx_id,\n            funding_pos=funding_output_index,\n            funding_sat=funding_amount_satoshi,\n            local_amount=to_local_msat,\n            remote_amount=to_remote_msat,\n            dust_limit_sat=local_dust_limit_satoshi,\n            fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False),\n            htlcs=[],\n            has_anchors=False\n        )\n        self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)\n        ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220'\n        self.assertEqual(str(our_commit_tx), ref_commit_tx_str)\n\n    @disable_ecdsa_r_value_grinding\n    def test_commitment_tx_anchors_test_vectors(self):\n        # this test is only valid for the original anchor output test vectors (not anchors-zero-fee-htlcs),\n        # therefore we patch the effective htlc tx weight to result in a finite weight\n        from electrum import lnutil\n        effective_htlc_tx_weight_original = lnutil.effective_htlc_tx_weight\n\n        def effective_htlc_tx_weight_patched(success: bool, has_anchors: bool):\n            return lnutil.HTLC_SUCCESS_WEIGHT_ANCHORS if success else lnutil.HTLC_TIMEOUT_WEIGHT_ANCHORS\n        lnutil.effective_htlc_tx_weight = effective_htlc_tx_weight_patched\n        try:\n            self._test_commitment_tx_anchors_test_vectors()\n        finally:\n            lnutil.effective_htlc_tx_weight = effective_htlc_tx_weight_original\n\n    def _test_commitment_tx_anchors_test_vectors(self):\n        for test_vector in ANCHOR_TEST_VECTORS:\n            with self.subTest(test_vector['Name']):\n                to_local_msat = test_vector['LocalBalance']\n                to_remote_msat = test_vector['RemoteBalance']\n                local_feerate_per_kw = test_vector['FeePerKw']\n                ref_commit_tx_str = test_vector['ExpectedCommitmentTxHex']\n                remote_signature = bfh(test_vector['RemoteSigHex'])\n                use_test_htlcs = test_vector['UseTestHtlcs']\n                htlc_descs = test_vector['HtlcDescs']  # type: List[Dict[str, str]]\n\n                remote_htlcpubkey = remotepubkey\n                local_htlcpubkey = localpubkey\n\n                # test of the commitment transaction, build htlc outputs first\n                test_htlcs = {}\n                if use_test_htlcs:\n                    # only consider htlcs whose sweep transaction creates outputs above dust limit\n                    threshold_sat_received = received_htlc_trim_threshold_sat(dust_limit_sat=local_dust_limit_satoshi, feerate=local_feerate_per_kw, has_anchors=True)\n                    threshold_sat_offered = offered_htlc_trim_threshold_sat(dust_limit_sat=local_dust_limit_satoshi, feerate=local_feerate_per_kw, has_anchors=True)\n                    for test_index, test_htlc in enumerate(TEST_HTLCS):\n                        if test_htlc['incoming']:\n                            htlc_script = make_received_htlc(\n                                revocation_pubkey=local_revocation_pubkey,\n                                remote_htlcpubkey=remote_htlcpubkey,\n                                local_htlcpubkey=local_htlcpubkey,\n                                payment_hash=bitcoin.sha256(bfh(test_htlc['preimage'])),\n                                cltv_abs=test_htlc['expiry'],\n                                has_anchors=True)\n                        else:\n                            htlc_script = make_offered_htlc(\n                                revocation_pubkey=local_revocation_pubkey,\n                                remote_htlcpubkey=remote_htlcpubkey,\n                                local_htlcpubkey=local_htlcpubkey,\n                                payment_hash=bitcoin.sha256(bfh(test_htlc['preimage'])),\n                                has_anchors=True)\n                        update_add_htlc = UpdateAddHtlc(\n                            amount_msat=test_htlc['amount'],\n                            payment_hash=bitcoin.sha256(bfh(test_htlc['preimage'])),\n                            cltv_abs=test_htlc['expiry'],\n                            htlc_id=None,\n                            timestamp=0)\n                        # only add htlcs whose spending transaction creates above-dust outputs\n                        # TODO: should we include this check in make_commitment?\n                        if test_htlc['amount'] // 1000 >= (threshold_sat_received if test_htlc['incoming'] else threshold_sat_offered):\n                            test_htlcs[test_index] = ScriptHtlc(htlc_script, update_add_htlc)\n\n                our_commit_tx = make_commitment(\n                    ctn=commitment_number,\n                    local_funding_pubkey=local_funding_pubkey,\n                    remote_funding_pubkey=remote_funding_pubkey,\n                    remote_payment_pubkey=remote_payment_basepoint,  # no key rotation for anchors\n                    funder_payment_basepoint=local_payment_basepoint,\n                    fundee_payment_basepoint=remote_payment_basepoint,\n                    revocation_pubkey=local_revocation_pubkey,\n                    delayed_pubkey=local_delayedpubkey,\n                    to_self_delay=local_delay,\n                    funding_txid=funding_tx_id,\n                    funding_pos=funding_output_index,\n                    funding_sat=funding_amount_satoshi,\n                    local_amount=to_local_msat,\n                    remote_amount=to_remote_msat,\n                    dust_limit_sat=local_dust_limit_satoshi,\n                    fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=len(test_htlcs), feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=True),\n                    htlcs=list(test_htlcs.values()),\n                    has_anchors=True\n                )\n                self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)\n                self.assertEqual(str(our_commit_tx), ref_commit_tx_str)  # only works without r value grinding\n\n                # test the transactions spending the htlc outputs\n                # we need to keep track of the htlc order in order to compare to test vectors\n                sorted_htlcs = {h[0]: h[1] for h in sorted(test_htlcs.items(), key=lambda x: (x[1].htlc.amount_msat, -x[1].htlc.cltv_abs))}\n                if use_test_htlcs:\n                    for output_index, (test_index, htlc) in enumerate(sorted_htlcs.items()):\n                        test_htlc = TEST_HTLCS[test_index]\n                        our_htlc = self.htlc_tx(\n                            htlc=htlc.redeem_script,\n                            htlc_output_index=output_index + 2,  # first two are anchors\n                            amount_msat=htlc.htlc.amount_msat,\n                            htlc_payment_preimage=bfh(test_htlc['preimage']),\n                            remote_htlc_sig=htlc_descs[output_index]['RemoteSigHex'],\n                            success=test_htlc['incoming'],\n                            cltv_abs=test_htlc['expiry'] if not test_htlc['incoming'] else 0,  # expiry is for timeout transaction\n                            local_feerate_per_kw=local_feerate_per_kw,\n                            our_commit_tx=our_commit_tx,\n                            has_anchors=True\n                        )\n                        ref_htlc = htlc_descs[output_index]['ResolutionTxHex']\n                        self.assertEqual(our_htlc, ref_htlc)  # only works without r value grinding\n\n    def sign_and_insert_remote_sig(self, tx: PartialTransaction, remote_pubkey: bytes, remote_signature: bytes, pubkey: bytes, privkey: bytes):\n        assert type(remote_pubkey) is bytes\n        assert len(remote_pubkey) == 33\n        assert type(remote_signature) is bytes\n        assert type(pubkey) is bytes\n        assert type(privkey) is bytes\n        assert len(pubkey) == 33\n        assert len(privkey) == 33\n        tx.sign({pubkey: privkey[:-1]})\n        sighash = Sighash.to_sigbytes(Sighash.ALL)\n        tx.add_signature_to_txin(txin_idx=0, signing_pubkey=remote_pubkey, sig=remote_signature + sighash)\n\n    def test_get_compressed_pubkey_from_bech32(self):\n        self.assertEqual(b'\\x03\\x84\\xef\\x87\\xd9d\\xa2\\xaaa7=\\xff\\xb8\\xfe=t8[}>;\\n\\x13\\xa8e\\x8eo:\\xf5Mi\\xb5H',\n                         get_compressed_pubkey_from_bech32('ln1qwzwlp7evj325cfh8hlm3l3awsu9klf78v9p82r93ehn4a2ddx65s66awg5'))\n\n    def test_ln_features_validate_transitive_dependencies(self):\n        features = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\n        self.assertTrue(features.validate_transitive_dependencies())\n        features = LnFeatures.PAYMENT_SECRET_OPT\n        self.assertFalse(features.validate_transitive_dependencies())\n        features = LnFeatures.PAYMENT_SECRET_REQ\n        self.assertFalse(features.validate_transitive_dependencies())\n        features = LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ\n        self.assertTrue(features.validate_transitive_dependencies())\n        features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ\n        self.assertFalse(features.validate_transitive_dependencies())\n        features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT\n        self.assertTrue(features.validate_transitive_dependencies())\n        features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ\n        self.assertTrue(features.validate_transitive_dependencies())\n\n    def test_ln_features_for_init_message(self):\n        features = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\n        self.assertEqual(features, features.for_init_message())\n        features = LnFeatures.PAYMENT_SECRET_OPT\n        self.assertEqual(features, features.for_init_message())\n        features = LnFeatures.PAYMENT_SECRET_REQ\n        self.assertEqual(features, features.for_init_message())\n        features = LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ\n        self.assertEqual(features, features.for_init_message())\n        features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ\n        self.assertEqual(features, features.for_init_message())\n        features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT\n        self.assertEqual(features, features.for_init_message())\n        features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ\n        self.assertEqual(features, features.for_init_message())\n\n    def test_ln_features_for_invoice(self):\n        features = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\n        self.assertEqual(LnFeatures(0), features.for_invoice())\n        features = LnFeatures.PAYMENT_SECRET_OPT\n        self.assertEqual(features, features.for_invoice())\n        features = LnFeatures.PAYMENT_SECRET_REQ\n        self.assertEqual(features, features.for_invoice())\n        features = LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ\n        self.assertEqual(features, features.for_invoice())\n        features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\n        self.assertEqual(LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ,\n                         features.for_invoice())\n        features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\n        self.assertEqual(LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT,\n                         features.for_invoice())\n        features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ\n        self.assertEqual(features, features.for_invoice())\n\n    def test_ln_compare_features(self):\n        f1 = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT\n        f2 = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\n        self.assertEqual(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT,\n                         ln_compare_features(f1, f2))\n        self.assertEqual(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT,\n                         ln_compare_features(f2, f1))\n        # note that the args are not commutative; if we (first arg) REQ a feature, OPT will get auto-set\n        f1 = LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT\n        f2 = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\n        self.assertEqual(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT,\n                         ln_compare_features(f1, f2))\n        self.assertEqual(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT,\n                         ln_compare_features(f2, f1))\n\n        f1 = LnFeatures(0)\n        f2 = LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT\n        self.assertEqual(LnFeatures(0), ln_compare_features(f1, f2))\n        self.assertEqual(LnFeatures(0), ln_compare_features(f2, f1))\n\n        f1 = LnFeatures(0)\n        f2 = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\n        with self.assertRaises(IncompatibleLightningFeatures):\n            ln_compare_features(f1, f2)\n        with self.assertRaises(IncompatibleLightningFeatures):\n            ln_compare_features(f2, f1)\n\n        f1 = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT | LnFeatures.VAR_ONION_OPT\n        f2 = LnFeatures.PAYMENT_SECRET_OPT | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.VAR_ONION_OPT\n        self.assertEqual(LnFeatures.PAYMENT_SECRET_OPT |\n                         LnFeatures.PAYMENT_SECRET_REQ |\n                         LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT |\n                         LnFeatures.VAR_ONION_OPT,\n                         ln_compare_features(f1, f2))\n        self.assertEqual(LnFeatures.PAYMENT_SECRET_OPT |\n                         LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT |\n                         LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ |\n                         LnFeatures.VAR_ONION_OPT,\n                         ln_compare_features(f2, f1))\n\n    def test_list_enabled_ln_feature_bits(self):\n        self.assertEqual((0, 2, 6), list_enabled_ln_feature_bits(77))\n        self.assertEqual((), list_enabled_ln_feature_bits(0))\n\n    def test_ln_features_supports(self):\n        f_null = LnFeatures(0)\n        f_opt = LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT\n        f_req = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\n        f_optreq = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT\n        self.assertFalse(f_null.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT))\n        self.assertFalse(f_null.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ))\n        self.assertTrue(f_opt.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT))\n        self.assertTrue(f_opt.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ))\n        self.assertTrue(f_req.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT))\n        self.assertTrue(f_req.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ))\n        self.assertTrue(f_optreq.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT))\n        self.assertTrue(f_optreq.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ))\n        with self.assertRaises(ValueError):\n            f_opt.supports(f_optreq)\n        with self.assertRaises(ValueError):\n            f_optreq.supports(f_optreq)\n        f1 = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT | LnFeatures.VAR_ONION_OPT\n        self.assertTrue(f1.supports(LnFeatures.PAYMENT_SECRET_OPT))\n        self.assertTrue(f1.supports(LnFeatures.BASIC_MPP_REQ))\n        self.assertFalse(f1.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT))\n        self.assertFalse(f1.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_REQ_ELECTRUM))\n\n    def test_lnworker_decode_channel_update_msg(self):\n        msg_without_prefix = bytes.fromhex(\"439b71c8ddeff63004e4ff1f9764a57dcf20232b79d9d669aef0e31c42be8e44208f7d868d0133acb334047f30e9399dece226ccd98e5df5330adf7f356290516fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008762700054a00005ef2cf9c0101009000000000000003e80000000000000001000000002367b880\")\n        # good messages\n        self.assertNotEqual(\n            None,\n            LNWallet._decode_channel_update_msg(msg_without_prefix))\n        self.assertNotEqual(\n            None,\n            LNWallet._decode_channel_update_msg(bytes.fromhex(\"0102\") + msg_without_prefix))\n        # bad messages\n        self.assertEqual(\n            None,\n            LNWallet._decode_channel_update_msg(bytes.fromhex(\"0102030405\")))\n        self.assertEqual(\n            None,\n            LNWallet._decode_channel_update_msg(bytes.fromhex(\"ffff\") + msg_without_prefix))\n        self.assertEqual(\n            None,\n            LNWallet._decode_channel_update_msg(bytes.fromhex(\"0101\") + msg_without_prefix))\n\n    def test_channel_type(self):\n        # test compliance and non compliance with LN features\n        features = LnFeatures(LnFeatures.BASIC_MPP_OPT | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)\n        self.assertTrue(ChannelType.OPTION_STATIC_REMOTEKEY.complies_with_features(features))\n\n        features = LnFeatures(LnFeatures.BASIC_MPP_OPT | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM)\n        self.assertFalse(ChannelType.OPTION_STATIC_REMOTEKEY.complies_with_features(features))\n\n        # ignore unknown channel types\n        channel_type = ChannelType(0b10000000001000000000010).discard_unknown_and_check()\n        self.assertEqual(ChannelType(0b10000000001000000000000), channel_type)\n\n    @as_testnet\n    async def test_decode_imported_channel_backup_v0(self):\n        encrypted_cb = \"channel_backup:Adn87xcGIs9H2kfp4VpsOaNKWCHX08wBoqq37l1cLYKGlJamTeoaLEwpJA81l1BXF3GP/mRxqkY+whZG9l51G8izIY/kmMSvnh0DOiZEdwaaT/1/MwEHfsEomruFqs+iW24SFJPHbMM7f80bDtIxcLfZkKmgcKBAOlcqtq+dL3U3yH74S8BDDe2L4snaxxpCjF0JjDMBx1UR/28D+QlIi+lbvv1JMaCGXf+AF1+3jLQf8+lVI+rvFdyArws6Ocsvjf+ANQeSGUwW6Nb2xICQcMRgr1DO7bO4pgGu408eYRr2v3ayJBVtnKwSwd49gF5SDSjTDAO4CCM0uj9H5RxyzH7fqotkd9J80MBr84RiBXAeXKz+Ap8608/FVqgQ9BOcn6LhuAQdE5zXpmbQyw5jUGkPvHuseR+rzthzncy01odUceqTNg==\"\n        config = SimpleConfig({'electrum_path': self.electrum_path})\n        d = restore_wallet_from_text__for_unittest(\"9dk\", path=None, config=config)\n        wallet1 = d['wallet']  # type: Standard_Wallet\n        decoded_cb = ImportedChannelBackupStorage.from_encrypted_str(encrypted_cb, password=wallet1.get_fingerprint())\n        self.assertEqual(\n            ImportedChannelBackupStorage(\n                funding_txid='97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe',\n                funding_index=1,\n                funding_address='tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp',\n                is_initiator=True,\n                node_id=bfh('02bf82e22f99dcd7ac1de4aad5152ce48f0694c46ec582567f379e0adbf81e2d0f'),\n                privkey=bfh('7e634853dc47f0bc2f2e0d1054b302fcb414371ddbd889f29ba8aa4e8b62c772'),\n                host='lightning.electrum.org',\n                port=9739,\n                channel_seed=bfh('ce9bad44ff8521d9f57fd202ad7cdedceb934f0056f42d0f3aa7a576b505332a'),\n                local_delay=1008,\n                remote_delay=720,\n                remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'),\n                remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'),\n                local_payment_pubkey=None,\n                multisig_funding_privkey=None,\n            ),\n            decoded_cb,\n        )\n\n    @as_testnet\n    async def test_decode_imported_channel_backup_v1(self):\n        encrypted_cb = \"channel_backup:AVYIedu0qSLfY2M2bBxF6dA4RAxcmobp+3h9mxALWWsv5X7hhNg0XYOKNd11FE6BJOZgZnIZ4CCAlHtLNj0/9S5GbNhbNZiQXxeHMwC1lHvtjawkwSejIJyOI52DkDFHBAGZRd4fJjaPJRHnUizWfySVR4zjd08lTinpoIeL7C7tXBW1N6YqceqV7RpeoywlBXJtFfCCuw0hnUKgq3SMlBKapkNAIgGrg15aIHNcYeENxCxr5FD1s7DIwFSECqsBVnu/Ogx2oii8BfuxqJq8vuGq4Ib/BVaSVtdb2E1wklAor/CG0p9Fg9mFWND98JD+64nz9n/knPFFyHxTXErn+ct3ZcStsLYynWKUIocgu38PtzCJ7r5ivqOw4O49fbbzdjcgMUGklPYxjuinETneCo+dCPa1uepOGTqeOYmnjVYtYZYXOlWV1F5OtNoM7MwwJjAbz84=\"\n        config = SimpleConfig({'electrum_path': self.electrum_path})\n        d = restore_wallet_from_text__for_unittest(\"9dk\", path=None, config=config)\n        wallet1 = d['wallet']  # type: Standard_Wallet\n        decoded_cb = ImportedChannelBackupStorage.from_encrypted_str(encrypted_cb, password=wallet1.get_fingerprint())\n        self.assertEqual(\n            ImportedChannelBackupStorage(\n                funding_txid='97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe',\n                funding_index=1,\n                funding_address='tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp',\n                is_initiator=True,\n                node_id=bfh('02bf82e22f99dcd7ac1de4aad5152ce48f0694c46ec582567f379e0adbf81e2d0f'),\n                privkey=bfh('7e634853dc47f0bc2f2e0d1054b302fcb414371ddbd889f29ba8aa4e8b62c772'),\n                host='195.201.207.61',\n                port=9739,\n                channel_seed=bfh('ce9bad44ff8521d9f57fd202ad7cdedceb934f0056f42d0f3aa7a576b505332a'),\n                local_delay=1008,\n                remote_delay=720,\n                remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'),\n                remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'),\n                local_payment_pubkey=bfh('0308d686712782a44b0cef220485ad83dae77853a5bf8501a92bb79056c9dcb25a'),\n                multisig_funding_privkey=None,\n            ),\n            decoded_cb,\n        )\n\n    async def test_payment_fee_budget(self):\n        config = SimpleConfig()\n        # test value above cutoff\n        invoice_amount_msat = 1_000_000 * 1000\n        budget = PaymentFeeBudget.from_invoice_amount(\n            invoice_amount_msat=invoice_amount_msat,\n            config=config,\n        )\n        reversed_fee_msat = PaymentFeeBudget.reverse_from_total_amount(\n            total_amount_msat=invoice_amount_msat + budget.fee_msat,\n            config=config,\n        )\n        self.assertGreater(budget.fee_msat, config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT)\n        self.assertEqual(reversed_fee_msat, budget.fee_msat)\n\n        # test value below cutoff\n        invoice_amount_msat = 1000\n        budget = PaymentFeeBudget.from_invoice_amount(\n            invoice_amount_msat=invoice_amount_msat,\n            config=config,\n        )\n        reversed_fee_msat = PaymentFeeBudget.reverse_from_total_amount(\n            total_amount_msat=invoice_amount_msat + budget.fee_msat,\n            config=config,\n        )\n        self.assertEqual(budget.fee_msat, config.LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT)\n        self.assertEqual(reversed_fee_msat, budget.fee_msat)\n\n\n"
  },
  {
    "path": "tests/test_lnwallet.py",
    "content": "import logging\nimport os\n\nfrom . import ElectrumTestCase\n\nfrom electrum.lnutil import RECEIVED, MIN_FINAL_CLTV_DELTA_ACCEPTED\nfrom electrum.logging import console_stderr_handler\nfrom electrum.invoices import LN_EXPIRY_NEVER, PR_UNPAID\n\n\nclass TestLNWallet(ElectrumTestCase):\n    TESTNET = True\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        console_stderr_handler.setLevel(logging.DEBUG)\n\n    async def asyncSetUp(self):\n        self.lnwallet_anchors = self.create_mock_lnwallet(name='mock_lnwallet_anchors', has_anchors=True)\n        await super().asyncSetUp()\n\n    def test_create_payment_info(self):\n        wallet = self.lnwallet_anchors\n        tests = (\n            (100_000, 200, 100),\n            (None, 200, 100),\n            (None, None, LN_EXPIRY_NEVER),\n            (100_000, None, 0),\n        )\n        for amount_msat, min_final_cltv_delta, exp_delay in tests:\n            payment_hash = wallet.create_payment_info(\n                amount_msat=amount_msat,\n                min_final_cltv_delta=min_final_cltv_delta,\n                exp_delay=exp_delay,\n            )\n            self.assertIsNotNone(wallet.get_preimage(payment_hash))\n            pi = wallet.get_payment_info(payment_hash, direction=RECEIVED)\n            self.assertEqual(pi.amount_msat, amount_msat)\n            self.assertEqual(pi.min_final_cltv_delta, min_final_cltv_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED)\n            self.assertEqual(pi.expiry_delay, exp_delay or LN_EXPIRY_NEVER)\n            self.assertEqual(pi.db_key, f\"{payment_hash.hex()}:{int(pi.direction)}\")\n            self.assertEqual(pi.status, PR_UNPAID)\n        self.assertIsNone(wallet.get_payment_info(os.urandom(32), direction=RECEIVED))\n\n    def test_create_payment_info__amount_must_not_be_zero(self):\n        wallet = self.lnwallet_anchors\n        amount_msat, min_final_cltv_delta, exp_delay = (0, 200, 100)\n        with self.assertRaises(ValueError):\n            wallet.create_payment_info(\n                amount_msat=amount_msat,\n                min_final_cltv_delta=min_final_cltv_delta,\n                exp_delay=exp_delay,\n            )\n"
  },
  {
    "path": "tests/test_mnemonic.py",
    "content": "from typing import NamedTuple, Optional\nimport json\nimport os\n\nfrom electrum import keystore\nfrom electrum import mnemonic\nfrom electrum import slip39\nfrom electrum import old_mnemonic\nfrom electrum.util import bfh\nfrom electrum.mnemonic import is_new_seed, is_old_seed, calc_seed_type, is_matching_seed, can_seed_have_passphrase\nfrom electrum.version import SEED_PREFIX_SW, SEED_PREFIX\n\nfrom . import ElectrumTestCase\nfrom .test_wallet_vertical import UNICODE_HORROR, UNICODE_HORROR_HEX\n\n\nclass SeedTestCase(NamedTuple):\n    words: str\n    bip32_seed: str\n    lang: Optional[str] = 'en'\n    words_hex: Optional[str] = None\n    entropy: Optional[int] = None\n    passphrase: Optional[str] = None\n    passphrase_hex: Optional[str] = None\n    seed_version: str = SEED_PREFIX\n\n\nSEED_TEST_CASES = {\n    'english': SeedTestCase(\n        words='wild father tree among universe such mobile favorite target dynamic credit identify',\n        seed_version=SEED_PREFIX_SW,\n        bip32_seed='aac2a6302e48577ab4b46f23dbae0774e2e62c796f797d0a1b5faeb528301e3064342dafb79069e7c4c6b8c38ae11d7a973bec0d4f70626f8cc5184a8d0b0756'),\n    'english_with_passphrase': SeedTestCase(\n        words='wild father tree among universe such mobile favorite target dynamic credit identify',\n        seed_version=SEED_PREFIX_SW,\n        passphrase='Did you ever hear the tragedy of Darth Plagueis the Wise?',\n        bip32_seed='4aa29f2aeb0127efb55138ab9e7be83b36750358751906f86c662b21a1ea1370f949e6d1a12fa56d3d93cadda93038c76ac8118597364e46f5156fde6183c82f'),\n    'japanese': SeedTestCase(\n        lang='ja',\n        words='なのか ひろい しなん まなぶ つぶす さがす おしゃれ かわく おいかける けさき かいとう さたん',\n        words_hex='e381aae381aee3818b20e381b2e3828de3818420e38197e381aae3829320e381bee381aae381b5e3829920e381a4e381b5e38299e3819920e38195e3818be38299e3819920e3818ae38197e38283e3828c20e3818be3828fe3818f20e3818ae38184e3818be38191e3828b20e38191e38195e3818d20e3818be38184e381a8e3818620e38195e3819fe38293',\n        entropy=1938439226660562861250521787963972783469,\n        bip32_seed='d3eaf0e44ddae3a5769cb08a26918e8b308258bcb057bb704c6f69713245c0b35cb92c03df9c9ece5eff826091b4e74041e010b701d44d610976ce8bfb66a8ad'),\n    'japanese_with_passphrase': SeedTestCase(\n        lang='ja',\n        words='なのか ひろい しなん まなぶ つぶす さがす おしゃれ かわく おいかける けさき かいとう さたん',\n        words_hex='e381aae381aee3818b20e381b2e3828de3818420e38197e381aae3829320e381bee381aae381b5e3829920e381a4e381b5e38299e3819920e38195e3818be38299e3819920e3818ae38197e38283e3828c20e3818be3828fe3818f20e3818ae38184e3818be38191e3828b20e38191e38195e3818d20e3818be38184e381a8e3818620e38195e3819fe38293',\n        entropy=1938439226660562861250521787963972783469,\n        passphrase=UNICODE_HORROR,\n        passphrase_hex=UNICODE_HORROR_HEX,\n        bip32_seed='251ee6b45b38ba0849e8f40794540f7e2c6d9d604c31d68d3ac50c034f8b64e4bc037c5e1e985a2fed8aad23560e690b03b120daf2e84dceb1d7857dda042457'),\n    'chinese': SeedTestCase(\n        lang='zh',\n        words='眼 悲 叛 改 节 跃 衡 响 疆 股 遂 冬',\n        words_hex='e79cbc20e682b220e58f9b20e694b920e88a8220e8b78320e8a1a120e5938d20e7968620e882a120e9818220e586ac',\n        seed_version=SEED_PREFIX_SW,\n        entropy=3083737086352778425940060465574397809099,\n        bip32_seed='0b9077db7b5a50dbb6f61821e2d35e255068a5847e221138048a20e12d80b673ce306b6fe7ac174ebc6751e11b7037be6ee9f17db8040bb44f8466d519ce2abf'),\n    'chinese_with_passphrase': SeedTestCase(\n        lang='zh',\n        words='眼 悲 叛 改 节 跃 衡 响 疆 股 遂 冬',\n        words_hex='e79cbc20e682b220e58f9b20e694b920e88a8220e8b78320e8a1a120e5938d20e7968620e882a120e9818220e586ac',\n        seed_version=SEED_PREFIX_SW,\n        entropy=3083737086352778425940060465574397809099,\n        passphrase='给我一些测试向量谷歌',\n        passphrase_hex='e7bb99e68891e4b880e4ba9be6b58be8af95e59091e9878fe8b0b7e6ad8c',\n        bip32_seed='6c03dd0615cf59963620c0af6840b52e867468cc64f20a1f4c8155705738e87b8edb0fc8a6cee4085776cb3a629ff88bb1a38f37085efdbf11ce9ec5a7fa5f71'),\n    'spanish': SeedTestCase(\n        lang='es',\n        words='almíbar tibio superar vencer hacha peatón príncipe matar consejo polen vehículo odisea',\n        words_hex='616c6d69cc8162617220746962696f20737570657261722076656e63657220686163686120706561746fcc816e20707269cc816e63697065206d6174617220636f6e73656a6f20706f6c656e2076656869cc8163756c6f206f6469736561',\n        entropy=3423992296655289706780599506247192518735,\n        bip32_seed='18bffd573a960cc775bbd80ed60b7dc00bc8796a186edebe7fc7cf1f316da0fe937852a969c5c79ded8255cdf54409537a16339fbe33fb9161af793ea47faa7a'),\n    'spanish_with_passphrase': SeedTestCase(\n        lang='es',\n        words='almíbar tibio superar vencer hacha peatón príncipe matar consejo polen vehículo odisea',\n        words_hex='616c6d69cc8162617220746962696f20737570657261722076656e63657220686163686120706561746fcc816e20707269cc816e63697065206d6174617220636f6e73656a6f20706f6c656e2076656869cc8163756c6f206f6469736561',\n        entropy=3423992296655289706780599506247192518735,\n        passphrase='araña difícil solución término cárcel',\n        passphrase_hex='6172616ecc83612064696669cc8163696c20736f6c7563696fcc816e207465cc81726d696e6f206361cc817263656c',\n        bip32_seed='363dec0e575b887cfccebee4c84fca5a3a6bed9d0e099c061fa6b85020b031f8fe3636d9af187bf432d451273c625e20f24f651ada41aae2c4ea62d87e9fa44c'),\n    'spanish2': SeedTestCase(\n        lang='es',\n        words='equipo fiar auge langosta hacha calor trance cubrir carro pulmón oro áspero',\n        words_hex='65717569706f20666961722061756765206c616e676f7374612068616368612063616c6f72207472616e63652063756272697220636172726f2070756c6d6fcc816e206f726f2061cc81737065726f',\n        seed_version=SEED_PREFIX_SW,\n        entropy=448346710104003081119421156750490206837,\n        bip32_seed='001ebce6bfde5851f28a0d44aae5ae0c762b600daf3b33fc8fc630aee0d207646b6f98b18e17dfe3be0a5efe2753c7cdad95860adbbb62cecad4dedb88e02a64'),\n    'spanish3': SeedTestCase(\n        lang='es',\n        words='vidrio jabón muestra pájaro capucha eludir feliz rotar fogata pez rezar oír',\n        words_hex='76696472696f206a61626fcc816e206d756573747261207061cc816a61726f206361707563686120656c756469722066656c697a20726f74617220666f676174612070657a2072657a6172206f69cc8172',\n        seed_version=SEED_PREFIX_SW,\n        entropy=3444792611339130545499611089352232093648,\n        passphrase='¡Viva España! repiten veinte pueblos y al hablar dan fe del ánimo español... ¡Marquen arado martillo y clarín',\n        passphrase_hex='c2a1566976612045737061c3b16121207265706974656e207665696e746520707565626c6f73207920616c206861626c61722064616e2066652064656c20c3a16e696d6f2065737061c3b16f6c2e2e2e20c2a14d61727175656e20617261646f206d617274696c6c6f207920636c6172c3ad6e',\n        bip32_seed='c274665e5453c72f82b8444e293e048d700c59bf000cacfba597629d202dcf3aab1cf9c00ba8d3456b7943428541fed714d01d8a0a4028fc3a9bb33d981cb49f'),\n}\n\n\nclass Test_NewMnemonic(ElectrumTestCase):\n\n    def test_mnemonic_to_seed_basic(self):\n        # note: not a valid electrum seed\n        seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic='foobar', passphrase='none')\n        self.assertEqual('741b72fd15effece6bfe5a26a52184f66811bd2be363190e07a42cca442b1a5bb22b3ad0eb338197287e6d314866c7fba863ac65d3f156087a5052ebc7157fce',\n                         seed.hex())\n\n    def test_mnemonic_to_seed(self):\n        for test_name, test in SEED_TEST_CASES.items():\n            if test.words_hex is not None:\n                self.assertEqual(test.words_hex, test.words.encode('utf8').hex(), msg=test_name)\n            self.assertTrue(is_new_seed(test.words, prefix=test.seed_version), msg=test_name)\n            m = mnemonic.Mnemonic(lang=test.lang)\n            if test.entropy is not None:\n                self.assertEqual(test.entropy, m.mnemonic_decode(test.words), msg=test_name)\n            if test.passphrase_hex is not None:\n                self.assertEqual(test.passphrase_hex, test.passphrase.encode('utf8').hex(), msg=test_name)\n            seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic=test.words, passphrase=test.passphrase)\n            self.assertEqual(test.bip32_seed, seed.hex(), msg=test_name)\n\n    def test_random_seeds(self):\n        iters = 10\n        m = mnemonic.Mnemonic(lang='en')\n        pool = set()\n        for _ in range(iters):\n            seed = m.make_seed(seed_type=\"standard\")\n            pool.add(seed)\n            with self.subTest(seed=seed, msg=\"decode-encode\"):\n                i = m.mnemonic_decode(seed)\n                self.assertEqual(m.mnemonic_encode(i), seed)\n            with self.subTest(seed=seed, msg=\"num-words\"):\n                self.assertTrue(12 <= len(seed.split()) <= 13)\n        self.assertEqual(iters, len(pool))\n\n\nclass Test_OldMnemonic(ElectrumTestCase):\n\n    def test(self):\n        seed = '8edad31a95e7d59f8837667510d75a4d'\n        result = old_mnemonic.mn_encode(seed)\n        words = 'hardly point goal hallway patience key stone difference ready caught listen fact'\n        self.assertEqual(result, words.split())\n        self.assertEqual(old_mnemonic.mn_decode(result), seed)\n\n\nclass Test_BIP39(ElectrumTestCase):\n\n    def test_checksum(self):\n        mnemonic = u'gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog'\n        is_checksum_valid, is_wordlist_valid = keystore.bip39_is_checksum_valid(mnemonic)\n        self.assertTrue(is_wordlist_valid)\n        self.assertTrue(is_checksum_valid)\n\n    def test_cjk_normalization(self):\n        # test case from https://github.com/Electron-Cash/Electron-Cash/issues/2740\n        cjk_toku_kanchi_hex = \"e381a8e3818ae3818fe38080e3818be38293e381a1e38080e3819de38186e38193e38299e38080e381bee38293e3818be38299e38080e3818de3818be3818fe38080e381bbe38184e3818fe38080e3818fe38186e381b5e3818fe38080e381a6e381b5e3819fe38299e38080e38191e3828fe38197e38184e38080e3819fe381a1e381afe38299e381aae38080e381b2e381a4e38288e38186e38080e381bbe38182e38293\"\n        cjk_toku_kanchi = bfh(cjk_toku_kanchi_hex).decode(\"utf-8\")\n        assert cjk_toku_kanchi == \"とおく　かんち　そうご　まんが　きかく　ほいく　くうふく　てふだ　けわしい　たちばな　ひつよう　ほあん\"\n\n        bip32_seed = keystore.bip39_to_seed(cjk_toku_kanchi, passphrase=\"\")\n        self.assertEqual(\n            bytes.fromhex(\"2c53f34b1bcd338681783e671386ccaa12f5f1b08a2eee679a43f8acf956130d9246ddc98aaf50e9bdceb9b2cb4096a94fe0beec816b2a233eda29a376d17912\"),\n            bip32_seed)\n\n        bip32_seed = keystore.bip39_to_seed(cjk_toku_kanchi, passphrase=cjk_toku_kanchi)\n        self.assertEqual(\n            bytes.fromhex(\"885df1b4e9906fb0ef187f4414cced1092c85cbf0b30676865f80f294d745a3a7ddfd37d8a512e644684bdfa8ced2da970f0d742423c72d3300f622c3a42f609\"),\n            bip32_seed)\n\n\nclass Test_seeds(ElectrumTestCase):\n    \"\"\" Test old and new seeds. \"\"\"\n\n    mnemonics = {\n        ('cell dumb heartbeat north boom tease ship baby bright kingdom rare squeeze', 'old'),\n        ('cell dumb heartbeat north boom tease ' * 4, 'old'),\n        ('cell dumb heartbeat north boom tease ship baby bright kingdom rare badword', ''),\n        ('cElL DuMb hEaRtBeAt nOrTh bOoM TeAsE ShIp bAbY BrIgHt kInGdOm rArE SqUeEzE', 'old'),\n        ('   cElL  DuMb hEaRtBeAt nOrTh bOoM  TeAsE ShIp    bAbY BrIgHt kInGdOm rArE SqUeEzE   ', 'old'),\n        # below seed is actually 'invalid old' as it maps to 33 hex chars\n        ('hurry idiot prefer sunset mention mist jaw inhale impossible kingdom rare squeeze', 'old'),\n        ('cram swing cover prefer miss modify ritual silly deliver chunk behind inform able', 'standard'),\n        ('cram swing cover prefer miss modify ritual silly deliver chunk behind inform', ''),\n        ('ostrich security deer aunt climb inner alpha arm mutual marble solid task', 'standard'),\n        ('OSTRICH SECURITY DEER AUNT CLIMB INNER ALPHA ARM MUTUAL MARBLE SOLID TASK', 'standard'),\n        ('   oStRiCh sEcUrItY DeEr aUnT ClImB       InNeR AlPhA ArM MuTuAl mArBlE   SoLiD TaSk  ', 'standard'),\n        ('science dawn member doll dutch real can brick knife deny drive list', '2fa'),\n        ('science dawn member doll dutch real ca brick knife deny drive list', ''),\n        (' sCience dawn   member doll Dutch rEAl can brick knife deny drive  lisT', '2fa'),\n        # pre-version-2.7 2fa seed with 25 words:\n        ('bind clever room kidney crucial sausage spy edit canvas soul liquid ribbon slam open alpha suffer gate relax voice carpet law hill woman tonight abstract', '2fa'),\n        ('  bInd cLEveR    room kidney crucial sausage spy edit canvas soul liquid ribbon SLAM open alpha suffer gate relax voice carpet law hill woman tonight abstract ', '2fa'),\n        # pre-version-2.7 2fa seed with 24 words:\n        ('sibling leg cable timber patient foot occur plate travel finger chef scale radio citizen promote immune must chef fluid sea sphere common acid lab', '2fa'),\n        ('frost pig brisk excite novel report camera enlist axis nation novel desert', 'segwit'),\n        ('  fRoSt pig brisk excIte novel rePort CamEra enlist axis nation nOVeL dEsert ', 'segwit'),\n        # short seed cheat sheet:\n        ('x8', 'standard'),\n        ('9dk', 'segwit'),\n        ('abandon bike', 'segwit'),  # <- has valid English words\n        ('6vs', '2fa_segwit'),\n        ('agree install', '2fa_segwit'),  # <- has valid English words\n    }\n\n    def test_new_seed(self):\n        seed = \"cram swing cover prefer miss modify ritual silly deliver chunk behind inform able\"\n        self.assertTrue(is_new_seed(seed))\n\n        seed = \"cram swing cover prefer miss modify ritual silly deliver chunk behind inform\"\n        self.assertFalse(is_new_seed(seed))\n\n    def test_old_seed(self):\n        self.assertTrue(is_old_seed(\" \".join([\"like\"] * 12)))\n        self.assertFalse(is_old_seed(\" \".join([\"like\"] * 18)))\n        self.assertTrue(is_old_seed(\" \".join([\"like\"] * 24)))\n        self.assertFalse(is_old_seed(\"not a seed\"))\n\n        self.assertTrue(is_old_seed(\"0123456789ABCDEF\" * 2))\n        self.assertTrue(is_old_seed(\"0123456789ABCDEF\" * 4))\n\n    def test_calc_seed_type(self):\n        for idx, (seed_words, _type) in enumerate(self.mnemonics):\n            with self.subTest(msg=f\"seed_type_subcase_{idx}\", seed_words=seed_words):\n                self.assertEqual(_type, calc_seed_type(seed_words), msg=seed_words)\n\n    def test_is_matching_seed(self):\n        self.assertTrue(is_matching_seed(seed=\"9dk\", seed_again=\"9dk \"))\n        self.assertTrue(is_matching_seed(seed=\"9dk\", seed_again=\" 9dk\"))\n        self.assertTrue(is_matching_seed(seed=\"9dk\", seed_again=\"  9dk \"))\n        self.assertTrue(is_matching_seed(seed=\"when blade focus\", seed_again=\"when blade focus \"))\n        self.assertTrue(is_matching_seed(seed=\"when blade focus\", seed_again=\" when  blade       focus  \"))\n        self.assertTrue(is_matching_seed(seed=\" when  blade  focus  \", seed_again=\" when  blade       focus  \"))\n        self.assertTrue(is_matching_seed(\n            seed=\" when  blade  focus  \",\n            seed_again=\n            \"\"\" when  blade\n\n               focus  \"\"\"))\n\n        self.assertFalse(is_matching_seed(seed=\"when blade focus\", seed_again=\"wen blade focus\"))\n        self.assertFalse(is_matching_seed(seed=\"when blade focus\", seed_again=\"when bladefocus\"))\n        self.assertFalse(is_matching_seed(seed=\"when blade focus\", seed_again=\"when blAde focus\"))\n        self.assertFalse(is_matching_seed(seed=\"when blade focus\", seed_again=\"when bl4de focus\"))\n        self.assertFalse(is_matching_seed(seed=\"when blade focus\", seed_again=\"when bla4de focus\"))\n\n    def test_can_seed_have_passphrase(self):\n        seed_invalid = 'xxx'\n        with self.assertRaises(Exception) as ctx:\n            self.assertFalse(can_seed_have_passphrase(seed_invalid))\n        self.assertTrue(\"unexpected seed type\" in ctx.exception.args[0])\n        seed_old = 'cell dumb heartbeat north boom tease ship baby bright kingdom rare squeeze'\n        self.assertFalse(can_seed_have_passphrase(seed_old))\n        seed_standard = 'cram swing cover prefer miss modify ritual silly deliver chunk behind inform able'\n        self.assertTrue(can_seed_have_passphrase(seed_standard))\n        seed_segwit = 'frost pig brisk excite novel report camera enlist axis nation novel desert'\n        self.assertTrue(can_seed_have_passphrase(seed_segwit))\n        seed_2fa_12 = 'science dawn member doll dutch real can brick knife deny drive list'\n        self.assertTrue(can_seed_have_passphrase(seed_2fa_12))\n        seed_2fa_24 = 'sibling leg cable timber patient foot occur plate travel finger chef scale radio citizen promote immune must chef fluid sea sphere common acid lab'\n        self.assertFalse(can_seed_have_passphrase(seed_2fa_24))\n        seed_2fa_25 = 'bind clever room kidney crucial sausage spy edit canvas soul liquid ribbon slam open alpha suffer gate relax voice carpet law hill woman tonight abstract'\n        self.assertFalse(can_seed_have_passphrase(seed_2fa_25))\n        seed_2fa_segwit = 'agree install'\n        self.assertTrue(can_seed_have_passphrase(seed_2fa_segwit))\n\n\nclass Test_slip39(ElectrumTestCase):\n    \"\"\" Test SLIP39 test vectors. \"\"\"\n\n    def test_slip39_vectors(self):\n        test_vector_file = os.path.join(os.path.dirname(__file__), \"slip39-vectors.json\")\n        with open(test_vector_file, \"r\") as f:\n            vectors = json.load(f)\n        for description, mnemonics, expected_secret, extended_private_key in vectors:\n            if expected_secret:\n                encrypted_seed = slip39.recover_ems(mnemonics)\n                assert bytes.fromhex(expected_secret) == encrypted_seed.decrypt(\"TREZOR\"), 'Incorrect secret for test vector \"{}\".'.format(description)\n            else:\n                with self.assertRaises(slip39.Slip39Error):\n                    slip39.recover_ems(mnemonics)\n                    self.fail(\n                        'Failed to raise exception for test vector \"{}\".'.format(description)\n                    )\n\n    def test_make_group_prefix(self):\n        self.assertEqual(slip39._make_group_prefix(5, 0, 4, 3, 2, 1), \"academic cover decision\")\n"
  },
  {
    "path": "tests/test_mpp_split.py",
    "content": "import random\n\nimport electrum.mpp_split as mpp_split  # side effect for PART_PENALTY\nfrom electrum.lnutil import NoPathFound\n\nfrom . import ElectrumTestCase\n\nPART_PENALTY = mpp_split.PART_PENALTY\n\n\nclass TestMppSplit(ElectrumTestCase):\n    def setUp(self):\n        super().setUp()\n        # to make tests reproducible:\n        random.seed(0)\n        # key tuple denotes (channel_id, node_id)\n        self.channels_with_funds = {\n            (b\"0\", b\"0\"): (1_000_000_000, 3),\n            (b\"1\", b\"1\"): (500_000_000, 2),\n            (b\"2\", b\"0\"): (302_000_000, 2),\n            (b\"3\", b\"2\"): (101_000_000, 1),\n        }\n\n    def tearDown(self):\n        super().tearDown()\n        # undo side effect\n        mpp_split.PART_PENALTY = PART_PENALTY\n\n    def test_suggest_splits(self):\n        with self.subTest(msg=\"do a payment with the maximal amount spendable over a single channel\"):\n            splits = mpp_split.suggest_splits(1_000_000_000, self.channels_with_funds, exclude_single_part_payments=True)\n            self.assertEqual({\n                (b\"0\", b\"0\"): [671_020_676],\n                (b\"1\", b\"1\"): [328_979_324],\n                (b\"2\", b\"0\"): [],\n                (b\"3\", b\"2\"): []},\n                splits[0].config\n            )\n\n        with self.subTest(msg=\"payment amount that does not require to be split\"):\n            splits = mpp_split.suggest_splits(50_000_000, self.channels_with_funds, exclude_single_part_payments=False)\n            self.assertEqual({(b\"0\", b\"0\"): [50_000_000]}, splits[0].config)\n            self.assertEqual({(b\"1\", b\"1\"): [50_000_000]}, splits[1].config)\n            self.assertEqual({(b\"2\", b\"0\"): [50_000_000]}, splits[2].config)\n            self.assertEqual({(b\"3\", b\"2\"): [50_000_000]}, splits[3].config)\n            self.assertEqual(2, splits[4].config.number_parts())\n\n        with self.subTest(msg=\"do a payment with a larger amount than what is supported by a single channel\"):\n            splits = mpp_split.suggest_splits(1_100_000_000, self.channels_with_funds, exclude_single_part_payments=False)\n            self.assertEqual(2, splits[0].config.number_parts())\n\n        with self.subTest(msg=\"do a payment with the maximal amount spendable over all channels\"):\n            splits = mpp_split.suggest_splits(\n                sum([x[0] for x in self.channels_with_funds.values()]), self.channels_with_funds, exclude_single_part_payments=True)\n            self.assertEqual({\n                (b\"0\", b\"0\"): [1_000_000_000],\n                (b\"1\", b\"1\"): [500_000_000],\n                (b\"2\", b\"0\"): [302_000_000],\n                (b\"3\", b\"2\"): [101_000_000]},\n                splits[0].config\n            )\n\n        with self.subTest(msg=\"do a payment with the amount supported by all channels\"):\n            splits = mpp_split.suggest_splits(101_000_000, self.channels_with_funds, exclude_single_part_payments=False)\n            for split in splits[:3]:\n                self.assertEqual(1, split.config.number_nonzero_channels())\n            # due to exhaustion of the smallest channel, the algorithm favors\n            # a splitting of the parts into two\n            self.assertEqual(2, splits[4].config.number_parts())\n\n        with self.subTest(msg=\"no htlc slots available\"):\n            channels = self.channels_with_funds.copy()\n            # set all available slots to 0\n            for chan, (amount, _slots) in channels.items():\n                channels[chan] = (amount, 0)\n            with self.assertRaises(NoPathFound):\n                mpp_split.suggest_splits(20_000_000, channels, exclude_single_part_payments=False)\n\n        with self.subTest(msg=\"only one channel can add htlcs\"):\n            channels = self.channels_with_funds.copy()\n            # set all available slots to 0 except for the first channel\n            for chan, (amount, _slots) in channels.items():\n                if chan != (b\"0\", b\"0\"):\n                    channels[chan] = (amount, 0)\n            splits = mpp_split.suggest_splits(1_000_000_000, channels, exclude_single_part_payments=True)\n            for split in splits:\n                # check that the whole amount has been split on this channel\n                self.assertEqual(sum(split.config[(b\"0\", b\"0\")]), 1_000_000_000)\n\n        with self.subTest(msg=\"test exclude single channel splits\"):\n            splits = mpp_split.suggest_splits(1_000_000_000, self.channels_with_funds, exclude_single_channel_splits=True)\n            for split in splits:\n                for channel_split in split.config.values():\n                    assert len(channel_split) <= 1, split\n\n    def test_send_to_single_node(self):\n        splits = mpp_split.suggest_splits(1_000_000_000, self.channels_with_funds, exclude_single_part_payments=False, exclude_multinode_payments=True)\n        for split in splits:\n            assert split.config.number_nonzero_nodes() == 1\n\n    def test_saturation(self):\n        \"\"\"Split configurations which spend the full amount in a channel should be avoided.\"\"\"\n        channels_with_funds = {\n            (b\"0\", b\"0\"): (159_799_733_076, 1),\n            (b\"1\", b\"1\"): (499_986_152_000, 1)\n        }\n        splits = mpp_split.suggest_splits(600_000_000_000, channels_with_funds, exclude_single_part_payments=True)\n\n        uses_full_amount = False\n        for c, a in splits[0].config.items():\n            if a == channels_with_funds[c]:\n                uses_full_amount |= True\n\n        self.assertFalse(uses_full_amount)\n\n    def test_payment_below_min_part_size(self):\n        amount = mpp_split.MIN_PART_SIZE_MSAT // 2\n        splits = mpp_split.suggest_splits(amount, self.channels_with_funds, exclude_single_part_payments=False)\n        # we only get four configurations that end up spending the full amount\n        # in a single channel\n        self.assertEqual(4, len(splits))\n\n    def test_suggest_part_penalty(self):\n        \"\"\"Test is mainly for documentation purposes.\n        Decreasing the part penalty from 1.0 towards 0.0 leads to an increase\n        in the number of parts a payment is split. A configuration which has\n        about equally distributed amounts will result.\"\"\"\n        with self.subTest(msg=\"split payments with intermediate part penalty\"):\n            mpp_split.PART_PENALTY = 1.0\n            splits = mpp_split.suggest_splits(1_100_000_000, self.channels_with_funds)\n            self.assertEqual(2, splits[0].config.number_parts())\n\n        with self.subTest(msg=\"split payments with intermediate part penalty\"):\n            mpp_split.PART_PENALTY = 0.3\n            splits = mpp_split.suggest_splits(1_100_000_000, self.channels_with_funds)\n            self.assertEqual(4, splits[0].config.number_parts())\n\n        with self.subTest(msg=\"split payments with no part penalty\"):\n            mpp_split.PART_PENALTY = 0.0\n            splits = mpp_split.suggest_splits(1_100_000_000, self.channels_with_funds)\n            self.assertEqual(5, splits[0].config.number_parts())\n\n    def test_suggest_splits_single_channel(self):\n        channels_with_funds = {\n            (b\"0\", b\"0\"): (1_000_000_000, 3),\n        }\n        with self.subTest(msg=\"do a payment with the maximal amount spendable on a single channel\"):\n            splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_part_payments=False)\n            self.assertEqual(1, len(splits[0].config[(b\"0\", b\"0\")]))\n            self.assertEqual({(b\"0\", b\"0\"): [1_000_000_000]}, splits[0].config)\n\n        with self.subTest(msg=\"test sending an amount greater than what we have available\"):\n            self.assertRaises(NoPathFound, mpp_split.suggest_splits, *(1_100_000_000, channels_with_funds))\n\n        with self.subTest(msg=\"test sending a large amount over a single channel in chunks\"):\n            mpp_split.PART_PENALTY = 0.5\n            splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_part_payments=False)\n            self.assertEqual(2, len(splits[0].config[(b\"0\", b\"0\")]))\n\n        with self.subTest(msg=\"test sending a large amount over a single channel in chunks\"):\n            mpp_split.PART_PENALTY = 0.3\n            splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_part_payments=False)\n            self.assertEqual(3, len(splits[0].config[(b\"0\", b\"0\")]))\n        with self.subTest(msg=\"exclude all single channel splits\"):\n            mpp_split.PART_PENALTY = 0.3\n            splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_channel_splits=True)\n            self.assertEqual(1, len(splits[0].config[(b\"0\", b\"0\")]))\n"
  },
  {
    "path": "tests/test_network.py",
    "content": "import asyncio\nimport tempfile\nimport unittest\nfrom typing import List\n\nfrom electrum import constants\nfrom electrum.simple_config import SimpleConfig\nfrom electrum import blockchain\nfrom electrum.interface import Interface, ServerAddr, ChainResolutionMode\nfrom electrum.crypto import sha256\nfrom electrum.util import OldTaskGroup\nfrom electrum import util\n\nfrom . import ElectrumTestCase\n\n\nCRM = ChainResolutionMode\n\n\nclass MockBlockchain:\n\n    def __init__(self, headers: List[str]):\n        self._headers = headers\n        self.forkpoint = len(headers)\n\n    def height(self) -> int:\n        return len(self._headers) - 1\n\n    def save_header(self, header: dict) -> None:\n        assert header['block_height'] == self.height()+1, f\"new {header['block_height']=}, cur {self.height()=}\"\n        self._headers.append(header['mock']['id'])\n\n    def check_header(self, header: dict) -> bool:\n        return header['mock']['id'] in self._headers\n\n    def can_connect(self, header: dict, *, check_height: bool = True) -> bool:\n        height = header['block_height']\n        if check_height and self.height() != height - 1:\n            return False\n        if self.check_header(header):\n            return True\n        return header['mock']['prev_id'] in self._headers\n\n    def fork(parent, header: dict) -> 'MockBlockchain':\n        if not parent.can_connect(header, check_height=False):\n            raise Exception(\"forking header does not connect to parent chain\")\n        forkpoint = header.get('block_height')\n        self = MockBlockchain(parent._headers[:forkpoint])\n        self.save_header(header)\n        chain_id = header['mock']['id']\n        with blockchain.blockchains_lock:\n            blockchain.blockchains[chain_id] = self\n        return self\n\n\nclass MockNetwork:\n\n    def __init__(self, config: SimpleConfig):\n        self.config = config\n        self.asyncio_loop = util.get_asyncio_loop()\n        self.taskgroup = OldTaskGroup()\n        self.proxy = None\n\nclass MockInterface(Interface):\n    def __init__(self, config: SimpleConfig):\n        self.config = config\n        network = MockNetwork(config)\n        super().__init__(network=network, server=ServerAddr.from_str('mock-server:50000:t'))\n        self.q = asyncio.Queue()\n\n    async def get_block_header(self, height: int, *, mode: ChainResolutionMode) -> dict:\n        assert self.q.qsize() > 0, (height, mode)\n        item = await self.q.get()\n        self.logger.debug(f\"step with {height=}. {mode=}. will get {item=}\")\n        assert item['block_height'] == height, (item['block_height'], height)\n        assert mode in item['mock'], (mode, item)\n        return item\n\n    async def run(self):\n        return\n\n    async def _maybe_warm_headers_cache(self, *args, **kwargs):\n        return\n\n\nclass TestHeaderChainResolution(ElectrumTestCase):\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        constants.BitcoinRegtest.set_as_network()\n\n    @classmethod\n    def tearDownClass(cls):\n        super().tearDownClass()\n        constants.BitcoinMainnet.set_as_network()\n\n    def tearDown(self):\n        blockchain.blockchains = {}\n        super().tearDown()\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n        self.interface = MockInterface(self.config)\n\n    async def test_catchup_one_block_behind(self):\n        \"\"\"Single chain, but client is behind. The client's height is 5, server is on block 6.\n        - first missing block found during *catchup* phase\n        \"\"\"\n        ifa = self.interface\n        ifa.tip = 6\n        ifa.blockchain = MockBlockchain([\"00a\", \"01a\", \"02a\", \"03a\", \"04a\", \"05a\"])\n        blockchain.blockchains = {\n            \"00a\": ifa.blockchain,\n        }\n        ifa.q.put_nowait({'block_height': 6, 'mock': {CRM.CATCHUP:1, 'id': '06a', 'prev_id': '05a'}})\n        res = await ifa.sync_until(ifa.tip)\n        self.assertEqual((CRM.CATCHUP, 7), res)\n        self.assertEqual(ifa.q.qsize(), 0)\n        self.assertEqual(len(blockchain.blockchains), 1)\n\n    async def test_catchup_already_up_to_date(self):\n        \"\"\"Single chain, local chain tip already matches server tip.\"\"\"\n        ifa = self.interface\n        ifa.tip = 5\n        ifa.blockchain = MockBlockchain([\"00a\", \"01a\", \"02a\", \"03a\", \"04a\", \"05a\"])\n        blockchain.blockchains = {\n            \"00a\": ifa.blockchain,\n        }\n        ifa.q.put_nowait({'block_height': 5, 'mock': {CRM.CATCHUP:1, 'id': '05a', 'prev_id': '04a'}})\n        res = await ifa.sync_until(ifa.tip)\n        self.assertEqual((CRM.CATCHUP, 6), res)\n        self.assertEqual(ifa.q.qsize(), 0)\n        self.assertEqual(len(blockchain.blockchains), 1)\n\n    async def test_catchup_client_ahead_of_lagging_server(self):\n        \"\"\"Single chain, server is lagging.\"\"\"\n        ifa = self.interface\n        ifa.tip = 3\n        ifa.blockchain = MockBlockchain([\"00a\", \"01a\", \"02a\", \"03a\", \"04a\", \"05a\"])\n        blockchain.blockchains = {\n            \"00a\": ifa.blockchain,\n        }\n        ifa.q.put_nowait({'block_height': 3, 'mock': {CRM.CATCHUP:1, 'id': '03a', 'prev_id': '02a'}})\n        res = await ifa.sync_until(ifa.tip)\n        self.assertEqual((CRM.CATCHUP, 4), res)\n        self.assertEqual(ifa.q.qsize(), 0)\n        self.assertEqual(len(blockchain.blockchains), 1)\n\n    async def test_catchup_fast_forward(self):\n        \"\"\"Single chain, but client is behind. The client's height is 5, server is already on block 12.\n        - first missing block found during *backward* phase\n        \"\"\"\n        ifa = self.interface\n        ifa.tip = 12\n        ifa.blockchain = MockBlockchain([\"00a\", \"01a\", \"02a\", \"03a\", \"04a\", \"05a\"])\n        blockchain.blockchains = {\n            \"00a\": ifa.blockchain,\n        }\n        ifa.q.put_nowait({'block_height': 12, 'mock': {CRM.CATCHUP:1, 'id': '12a', 'prev_id': '11a'}})\n        ifa.q.put_nowait({'block_height': 6, 'mock': {CRM.BACKWARD:1, 'id': '06a', 'prev_id': '05a'}})\n        ifa.q.put_nowait({'block_height': 7, 'mock': {CRM.CATCHUP: 1, 'id': '07a', 'prev_id': '06a'}})\n        ifa.q.put_nowait({'block_height': 8, 'mock': {CRM.CATCHUP: 1, 'id': '08a', 'prev_id': '07a'}})\n        ifa.q.put_nowait({'block_height': 9, 'mock': {CRM.CATCHUP: 1, 'id': '09a', 'prev_id': '08a'}})\n        res = await ifa.sync_until(ifa.tip, next_height=9)\n        self.assertEqual((CRM.CATCHUP, 10), res)\n        self.assertEqual(ifa.q.qsize(), 0)\n        self.assertEqual(len(blockchain.blockchains), 1)\n\n    async def test_fork(self):\n        \"\"\"client starts on main chain, has no knowledge of any fork.\n        server is on other side of chain split, the last common block is height 6.\n        - first missing block found during *binary* phase\n        - is *new* fork\n        \"\"\"\n        ifa = self.interface\n        ifa.tip = 8\n        ifa.blockchain = MockBlockchain([\"00a\", \"01a\", \"02a\", \"03a\", \"04a\", \"05a\", \"06a\", \"07a\", \"08a\", \"09a\", \"10a\", \"11a\", \"12a\"])\n        blockchain.blockchains = {\n            \"00a\": ifa.blockchain,\n        }\n        ifa.q.put_nowait({'block_height': 8, 'mock': {CRM.CATCHUP:1, 'id': '08b', 'prev_id': '07b'}})\n        ifa.q.put_nowait({'block_height': 7, 'mock': {CRM.BACKWARD:1, 'id': '07b', 'prev_id': '06a'}})\n        ifa.q.put_nowait({'block_height': 5, 'mock': {CRM.BACKWARD:1, 'id': '05a', 'prev_id': '04a'}})\n        ifa.q.put_nowait({'block_height': 6, 'mock': {CRM.BINARY:1, 'id': '06a', 'prev_id': '05a'}})\n        res = await ifa.sync_until(ifa.tip, next_height=7)\n        self.assertEqual((CRM.FORK, 8), res)\n        self.assertEqual(ifa.q.qsize(), 0)\n        self.assertEqual(len(blockchain.blockchains), 2)\n\n    async def test_can_connect_during_backward(self):\n        \"\"\"client starts on main chain. client already knows about another fork, which has local height 4.\n        server is on that fork but has more blocks.\n        - first missing block found during *backward* phase\n        - is *existing* fork\n        \"\"\"\n        ifa = self.interface\n        ifa.tip = 8\n        ifa.blockchain = MockBlockchain([\"00a\", \"01a\", \"02a\", \"03a\", \"04a\", \"05a\", \"06a\", \"07a\", \"08a\", \"09a\", \"10a\", \"11a\", \"12a\"])\n        blockchain.blockchains = {\n            \"00a\": ifa.blockchain,\n            \"03b\": MockBlockchain([\"00a\", \"01a\", \"02a\", \"03b\", \"04b\"]),\n        }\n        ifa.q.put_nowait({'block_height': 8, 'mock': {CRM.CATCHUP:1, 'id': '08b', 'prev_id': '07b'}})\n        ifa.q.put_nowait({'block_height': 7, 'mock': {CRM.BACKWARD:1, 'id': '07b', 'prev_id': '06b'}})\n        ifa.q.put_nowait({'block_height': 5, 'mock': {CRM.BACKWARD:1, 'id': '05b', 'prev_id': '04b'}})\n        ifa.q.put_nowait({'block_height': 6, 'mock': {CRM.CATCHUP:1, 'id': '06b', 'prev_id': '05b'}})\n        res = await ifa.sync_until(ifa.tip, next_height=6)\n        self.assertEqual((CRM.CATCHUP, 7), res)\n        self.assertEqual(ifa.q.qsize(), 0)\n        self.assertEqual(len(blockchain.blockchains), 2)\n\n    async def test_chain_false_during_binary(self):\n        \"\"\"client starts on main chain, has no knowledge of any fork.\n        server is on other side of chain split, the last common block is height 3.\n        - first missing block found during *binary* phase\n        - is *new* fork\n        \"\"\"\n        ifa = self.interface\n        ifa.tip = 8\n        ifa.blockchain = MockBlockchain([\"00a\", \"01a\", \"02a\", \"03a\", \"04a\", \"05a\", \"06a\", \"07a\", \"08a\", \"09a\", \"10a\", \"11a\", \"12a\"])\n        blockchain.blockchains = {\n            \"00a\": ifa.blockchain,\n        }\n        ifa.q.put_nowait({'block_height': 8, 'mock': {CRM.CATCHUP:1, 'id': '08b', 'prev_id': '07b'}})\n        ifa.q.put_nowait({'block_height': 7, 'mock': {CRM.BACKWARD:1, 'id': '07b', 'prev_id': '06b'}})\n        ifa.q.put_nowait({'block_height': 5, 'mock': {CRM.BACKWARD:1, 'id': '05b', 'prev_id': '04b'}})\n        ifa.q.put_nowait({'block_height': 1, 'mock': {CRM.BACKWARD:1, 'id': '01a', 'prev_id': '00a'}})\n        ifa.q.put_nowait({'block_height': 3, 'mock': {CRM.BINARY:1, 'id': '03a', 'prev_id': '02a'}})\n        ifa.q.put_nowait({'block_height': 4, 'mock': {CRM.BINARY:1, 'id': '04b', 'prev_id': '03a'}})\n        ifa.q.put_nowait({'block_height': 5, 'mock': {CRM.CATCHUP:1, 'id': '05b', 'prev_id': '04b'}})\n        ifa.q.put_nowait({'block_height': 6, 'mock': {CRM.CATCHUP:1, 'id': '06b', 'prev_id': '05b'}})\n        res = await ifa.sync_until(ifa.tip, next_height=6)\n        self.assertEqual((CRM.CATCHUP, 7), res)\n        self.assertEqual(ifa.q.qsize(), 0)\n        self.assertEqual(len(blockchain.blockchains), 2)\n\n    async def test_chain_true_during_binary(self):\n        \"\"\"client starts on main chain. client already knows about another fork, which has local height 10.\n        server is on that fork but has more blocks.\n        - first missing block found during *binary* phase\n        - is *existing* fork\n        \"\"\"\n        ifa = self.interface\n        ifa.tip = 20\n        ifa.blockchain = MockBlockchain([\"00a\", \"01a\", \"02a\", \"03a\", \"04a\", \"05a\", \"06a\", \"07a\", \"08a\", \"09a\", \"10a\", \"11a\", \"12a\", \"13a\", \"14a\"])\n        blockchain.blockchains = {\n            \"00a\": ifa.blockchain,\n            \"07b\": MockBlockchain([\"00a\", \"01a\", \"02a\", \"03a\", \"04a\", \"05a\", \"06a\", \"07b\", \"08b\", \"09b\", \"10b\"]),\n        }\n        ifa.q.put_nowait({'block_height': 20, 'mock': {CRM.CATCHUP:1, 'id': '20b', 'prev_id': '19b'}})\n        ifa.q.put_nowait({'block_height': 15, 'mock': {CRM.BACKWARD:1, 'id': '15b', 'prev_id': '14b'}})\n        ifa.q.put_nowait({'block_height': 13, 'mock': {CRM.BACKWARD:1, 'id': '13b', 'prev_id': '12b'}})\n        ifa.q.put_nowait({'block_height': 9, 'mock': {CRM.BACKWARD:1, 'id': '09b', 'prev_id': '08b'}})\n        ifa.q.put_nowait({'block_height': 11, 'mock': {CRM.BINARY:1, 'id': '11b', 'prev_id': '10b'}})\n        ifa.q.put_nowait({'block_height': 10, 'mock': {CRM.BINARY:1, 'id': '10b', 'prev_id': '09b'}})\n        ifa.q.put_nowait({'block_height': 11, 'mock': {CRM.CATCHUP:1, 'id': '11b', 'prev_id': '10b'}})\n        ifa.q.put_nowait({'block_height': 12, 'mock': {CRM.CATCHUP:1, 'id': '12b', 'prev_id': '11b'}})\n        ifa.q.put_nowait({'block_height': 13, 'mock': {CRM.CATCHUP:1, 'id': '13b', 'prev_id': '12b'}})\n        res = await ifa.sync_until(ifa.tip, next_height=13)\n        self.assertEqual((CRM.CATCHUP, 14), res)\n        self.assertEqual(ifa.q.qsize(), 0)\n        self.assertEqual(len(blockchain.blockchains), 2)\n\n\nif __name__ == \"__main__\":\n    constants.BitcoinRegtest.set_as_network()\n    unittest.main()\n"
  },
  {
    "path": "tests/test_onion_message.py",
    "content": "import asyncio\nimport io\nimport os\nimport time\nimport dataclasses\nimport logging\nfrom functools import partial\nfrom types import MappingProxyType\n\nimport electrum_ecc as ecc\nfrom electrum_ecc import ECPrivkey\n\nfrom electrum import SimpleConfig\nfrom electrum.lnmsg import decode_msg, OnionWireSerializer\nfrom electrum.lnonion import (\n    OnionHopsDataSingle, OnionPacket, process_onion_packet, get_bolt04_onion_key, encrypt_onionmsg_data_tlv,\n    get_shared_secrets_along_route, new_onion_packet, ONION_MESSAGE_LARGE_SIZE, HOPS_DATA_SIZE, InvalidPayloadSize,\n    encrypt_hops_recipient_data)\nfrom electrum.crypto import get_ecdh, privkey_to_pubkey\nfrom electrum.lnutil import LnFeatures, Keypair\nfrom electrum.onion_message import (\n    blinding_privkey, create_blinded_path,OnionMessageManager, NoRouteFound, Timeout\n)\nfrom electrum.util import bfh, read_json_file, OldTaskGroup, get_asyncio_loop\nfrom electrum.logging import console_stderr_handler\n\nfrom . import ElectrumTestCase\n\n\nTIME_STEP = 0.01  # run tests 100 x faster\nOnionMessageManager.SLEEP_DELAY *= TIME_STEP\nOnionMessageManager.REQUEST_REPLY_TIMEOUT *= TIME_STEP\nOnionMessageManager.REQUEST_REPLY_RETRY_DELAY *= TIME_STEP\nOnionMessageManager.FORWARD_RETRY_TIMEOUT *= TIME_STEP\nOnionMessageManager.FORWARD_RETRY_DELAY *= TIME_STEP\n\n# test vectors https://github.com/lightning/bolts/pull/759/files\npath = os.path.join(os.path.dirname(__file__), 'blinded-onion-message-onion-test.json')\ntest_vectors = read_json_file(path)\nONION_MESSAGE_PACKET = bfh(test_vectors['onionmessage']['onion_message_packet'])\nHOPS = test_vectors['generate']['hops']\nALICE_TLVS = HOPS[0]['tlvs']\nBOB_TLVS =   HOPS[1]['tlvs']\nCAROL_TLVS = HOPS[2]['tlvs']\nDAVE_TLVS =  HOPS[3]['tlvs']\n\nALICE_PUBKEY = bfh(test_vectors['route']['first_node_id'])\nBOB_PUBKEY =   bfh(ALICE_TLVS['next_node_id'])\nCAROL_PUBKEY = bfh(BOB_TLVS['next_node_id'])\nDAVE_PUBKEY =  bfh(CAROL_TLVS['next_node_id'])\n\nBLINDING_SECRET = bfh(HOPS[0]['path_key_secret'])\nBLINDING_OVERRIDE_SECRET = bfh(ALICE_TLVS['path_key_override_secret'])\n\nSESSION_KEY = bfh(test_vectors['generate']['session_key'])\n\n\nclass TestOnionMessage(ElectrumTestCase):\n\n    def test_path_pubkeys_blinded_path_appended(self):\n\n        hop_shared_secrets1, blinded_node_ids1 = get_shared_secrets_along_route([ALICE_PUBKEY], BLINDING_SECRET)\n        hop_shared_secrets2, blinded_node_ids2 = get_shared_secrets_along_route([BOB_PUBKEY, CAROL_PUBKEY, DAVE_PUBKEY], BLINDING_OVERRIDE_SECRET)\n        hop_shared_secrets = hop_shared_secrets1 + hop_shared_secrets2\n        blinded_node_ids = blinded_node_ids1 + blinded_node_ids2\n\n        for i, ss in enumerate(hop_shared_secrets):\n            self.assertEqual(ss, bfh(HOPS[i]['ss']))\n        for i, ss in enumerate(blinded_node_ids):\n            self.assertEqual(ss, bfh(HOPS[i]['blinded_node_id']))\n\n        hops_data = [\n            OnionHopsDataSingle(\n                tlv_stream_name='onionmsg_tlv',\n                blind_fields={\n                    'next_node_id': {'node_id': bfh(ALICE_TLVS['next_node_id'])},\n                    'next_path_key_override': {'path_key': bfh(ALICE_TLVS['next_path_key_override'])},\n                },\n            ),\n            OnionHopsDataSingle(\n                tlv_stream_name='onionmsg_tlv',\n                blind_fields={\n                    'next_node_id': {'node_id': bfh(BOB_TLVS['next_node_id'])},\n                    'unknown_tag_561': {'data': bfh(BOB_TLVS['unknown_tag_561'])},\n                },\n            ),\n            OnionHopsDataSingle(\n                tlv_stream_name='onionmsg_tlv',\n                blind_fields={\n                    'padding': {'padding': bfh(CAROL_TLVS['padding'])},\n                    'next_node_id': {'node_id': bfh(CAROL_TLVS['next_node_id'])},\n                },\n            ),\n            OnionHopsDataSingle(\n                tlv_stream_name='onionmsg_tlv',\n                payload={'message': {'text': bfh(test_vectors['onionmessage']['unknown_tag_1'])}},\n                blind_fields={\n                    'padding': {'padding': bfh(DAVE_TLVS['padding'])},\n                    'path_id': {'data': bfh(DAVE_TLVS['path_id'])},\n                    'unknown_tag_65535': {'data': bfh(DAVE_TLVS['unknown_tag_65535'])},\n                },\n            )\n        ]\n\n        encrypt_hops_recipient_data('onionmsg_tlv', hops_data, hop_shared_secrets)\n        packet = new_onion_packet(blinded_node_ids, SESSION_KEY, hops_data, onion_message=True)\n        self.assertEqual(packet.to_bytes(), ONION_MESSAGE_PACKET)\n\n    def test_onion_message_payload_size(self):\n        # Note: payload size is not _strictly_ limited to (1300+66, 32768+66), but Electrum only generates these sizes\n        # However, the spec allows for other payload sizes.\n        # https://github.com/lightning/bolts/blob/master/04-onion-routing.md\n        # \"SHOULD set onion_message_packet len to 1366 or 32834.\"\n        hop_shared_secrets, blinded_node_ids = get_shared_secrets_along_route([DAVE_PUBKEY], SESSION_KEY)\n\n        def hops_data_for_message(message):\n            return [\n                OnionHopsDataSingle(\n                    tlv_stream_name='onionmsg_tlv',\n                    payload={'message': {'text': message.encode('utf-8')}},\n                    blind_fields={\n                        'path_id': {'data': bfh('deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0')},\n                    },\n                ),\n            ]\n        hops_data = hops_data_for_message('short_message')  # fit in HOPS_DATA_SIZE\n        encrypt_hops_recipient_data('onionmsg_tlv', hops_data, hop_shared_secrets)\n        packet = new_onion_packet(blinded_node_ids, SESSION_KEY, hops_data, onion_message=True)\n        self.assertEqual(len(packet.to_bytes()), HOPS_DATA_SIZE + 66)\n\n        hops_data = hops_data_for_message('A' * HOPS_DATA_SIZE)  # fit in ONION_MESSAGE_LARGE_SIZE\n        encrypt_hops_recipient_data('onionmsg_tlv', hops_data, hop_shared_secrets)\n        packet = new_onion_packet(blinded_node_ids, SESSION_KEY, hops_data, onion_message=True)\n\n        self.assertEqual(len(packet.to_bytes()), ONION_MESSAGE_LARGE_SIZE + 66)\n\n        hops_data = hops_data_for_message('A' * ONION_MESSAGE_LARGE_SIZE)  # does not fit in ONION_MESSAGE_LARGE_SIZE\n        encrypt_hops_recipient_data('onionmsg_tlv', hops_data, hop_shared_secrets)\n\n        with self.assertRaises(InvalidPayloadSize):\n            new_onion_packet(blinded_node_ids, SESSION_KEY, hops_data, onion_message=True)\n\n    def test_decode_onion_message_packet(self):\n        op = OnionPacket.from_bytes(ONION_MESSAGE_PACKET)\n        self.assertEqual(op.hmac, bfh('8e7fc7590ff05a9e991de03f023d0aaf8688ed6170def5091c66576a424ac1cb'))\n        self.assertEqual(op.public_key, bfh('02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337'))\n        self.assertEqual(op.hops_data, bfh('93b828776d70aabbd8cef1a5b52d5a397ae1a20f20435ff6057cd8be339d5aee226660ef73b64afa45dbf2e6e8e26eb96a259b2db5aeecda1ce2e768bbc35d389d7f320ca3d2bd14e2689bef2f5ac0307eaaabc1924eb972c1563d4646ae131accd39da766257ed35ea36e4222527d1db4fa7b2000aab9eafcceed45e28b5560312d4e2299bd8d1e7fe27d10925966c28d497aec400b4630485e82efbabc00550996bdad5d6a9a8c75952f126d14ad2cff91e16198691a7ef2937de83209285f1fb90944b4e46bca7c856a9ce3da10cdf2a7d00dc2bf4f114bc4d3ed67b91cbde558ce9af86dc81fbdc37f8e301b29e23c1466659c62bdbf8cff5d4c20f0fb0851ec72f5e9385dd40fdd2e3ed67ca4517117825665e50a3e26f73c66998daf18e418e8aef9ce2d20da33c3629db2933640e03e7b44c2edf49e9b482db7b475cfd4c617ae1d46d5c24d697846f9f08561eac2b065f9b382501f6eabf07343ed6c602f61eab99cdb52adf63fd44a8db2d3016387ea708fc1c08591e19b4d9984ebe31edbd684c2ea86526dd8c7732b1d8d9117511dc1b643976d356258fce8313b1cb92682f41ab72dedd766f06de375f9edacbcd0ca8c99b865ea2b7952318ea1fd20775a28028b5cf59dece5de14f615b8df254eee63493a5111ea987224bea006d8f1b60d565eef06ac0da194dba2a6d02e79b2f2f34e9ca6e1984a507319d86e9d4fcaeea41b4b9144e0b1826304d4cc1da61cfc5f8b9850697df8adc5e9d6f3acb3219b02764b4909f2b2b22e799fd66c383414a84a7d791b899d4aa663770009eb122f90282c8cb9cda16aba6897edcf9b32951d0080c0f52be3ca011fbec3fb16423deb47744645c3b05fdbd932edf54ba6efd26e65340a8e9b1d1216582e1b30d64524f8ca2d6c5ba63a38f7120a3ed71bed8960bcac2feee2dd41c90be48e3c11ec518eb3d872779e4765a6cc28c6b0fa71ab57ced73ae963cc630edae4258cba2bf25821a6ae049fec2fca28b5dd1bb004d92924b65701b06dcf37f0ccd147a13a03f9bc0f98b7d78fe9058089756931e2cd0e0ed92ec6759d07b248069526c67e9e6ce095118fd3501ba0f858ef030b76c6f6beb11a09317b5ad25343f4b31aef02bc555951bc7791c2c289ecf94d5544dcd6ad3021ed8e8e3db34b2a73e1eedb57b578b068a5401836d6e382110b73690a94328c404af25e85a8d6b808893d1b71af6a31fadd8a8cc6e31ecc0d9ff7e6b91fd03c274a5c1f1ccd25b61150220a3fddb04c91012f5f7a83a5c90deb2470089d6e38cd5914b9c946eca6e9d31bbf8667d36cf87effc3f3ff283c21dd4137bd569fe7cf758feac94053e4baf7338bb592c8b7c291667fadf4a9bf9a2a154a18f612cbc7f851b3f8f2070e0a9d180622ee4f8e81b0ab250d504cef24116a3ff188cc829fcd8610b56343569e8dc997629410d1967ca9dd1d27eec5e01e4375aad16c46faba268524b154850d0d6fe3a76af2c6aa3e97647c51036049ac565370028d6a439a2672b6face56e1b171496c0722cfa22d9da631be359661617c5d5a2d286c5e19db9452c1e21a0107b6400debda2decb0c838f342dd017cdb2dccdf1fe97e3df3f881856b546997a3fed9e279c720145101567dd56be21688fed66bf9759e432a9aa89cbbd225d13cdea4ca05f7a45cfb6a682a3d5b1e18f7e6cf934fae5098108bae9058d05c3387a01d8d02a656d2bfff67e9f46b2d8a6aac28129e52efddf6e552214c3f8a45bc7a912cca9a7fec1d7d06412c6972cb9e3dc518983f56530b8bffe7f92c4b6eb47d4aef59fb513c4653a42de61bc17ad772'))\n\n    def test_decode_onion_message(self):\n        msg = test_vectors['decrypt']['hops'][0]['onion_message']\n        msgtype, data = decode_msg(bfh(msg))\n        self.assertEqual(msgtype, 'onion_message')\n        self.assertEqual(data, {\n            'path_key': bfh(test_vectors['route']['first_path_key']),\n            'len': 1366,\n            'onion_message_packet': ONION_MESSAGE_PACKET,\n        })\n\n    def test_decrypt_onion_message(self):\n        o = OnionPacket.from_bytes(ONION_MESSAGE_PACKET)\n        our_privkey = bfh(test_vectors['decrypt']['hops'][0]['privkey'])\n        blinding = bfh(test_vectors['route']['first_path_key'])\n\n        shared_secret = get_ecdh(our_privkey, blinding)\n        b_hmac = get_bolt04_onion_key(b'blinded_node_id', shared_secret)\n        b_hmac_int = int.from_bytes(b_hmac, byteorder=\"big\")\n\n        our_privkey_int = int.from_bytes(our_privkey, byteorder=\"big\")\n        our_privkey_int = our_privkey_int * b_hmac_int % ecc.CURVE_ORDER\n        our_privkey = our_privkey_int.to_bytes(32, byteorder=\"big\")\n\n        p = process_onion_packet(o, our_privkey, is_onion_message=True, tlv_stream_name='onionmsg_tlv')\n\n        self.assertEqual(p.hop_data.blind_fields, {})\n        self.assertEqual(p.hop_data.hmac, bfh('a5296325ba478ba1e1a9d1f30a2d5052b2e2889bbd64f72c72bc71d8817288a2'))\n        self.assertEqual(p.hop_data.payload, {'encrypted_recipient_data': {'encrypted_recipient_data': bfh('49531cf38d3280b7f4af6d6461a2b32e3df50acfd35176fc61422a1096eed4dfc3806f29bf74320f712a61c766e7f7caac0c42f86040125fbaeec0c7613202b206dbdd31fda56394367b66a711bfd7d5bedbe20bed1b')}})\n        self.assertEqual(p.hop_data.tlv_stream_name, 'onionmsg_tlv')\n\n        onion_message_bob = test_vectors['decrypt']['hops'][1]['onion_message']\n        msgtype, data = decode_msg(bfh(onion_message_bob))\n        self.assertEqual(msgtype, 'onion_message')\n        self.assertEqual(data, {\n            'path_key': bfh(ALICE_TLVS['next_path_key_override']),\n            'len': 1366,\n            'onion_message_packet': p.next_packet.to_bytes(),\n        })\n\n    def test_blinding_privkey(self):\n        a = blinding_privkey(bfh('4141414141414141414141414141414141414141414141414141414141414141'),\n                             bfh('031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f'))\n        self.assertEqual(a, bfh('7e959bf6bdd3a98caf26cbbee7b69678381d5fa2882c6c12eb2042c2367264b0'))\n\n    def test_create_blinded_path(self):\n        pubkey = ALICE_PUBKEY\n        session_key = bfh('3030303030303030303030303030303030303030303030303030303030303030') # typo?\n        final_recipient_data = {'path_id': {'data': bfh('0102')}}\n        rp = create_blinded_path(session_key, [pubkey], final_recipient_data)\n\n        self.assertEqual(pubkey, rp['first_node_id'])\n        self.assertEqual(bfh('022ed557f5ad336b31a49857e4e9664954ac33385aa20a93e2d64bfe7f08f51277'), rp['first_path_key'])\n        self.assertEqual(b\"\\x01\", rp['num_hops'])\n        self.assertEqual([{\n            'blinded_node_id': bfh('031e5d91e6c417f6e8c16d1086db1887edef7be9334f5e744d04edb8da7507481e'),\n            'enclen': 20,\n            'encrypted_recipient_data': bfh('2dbaa54a819775aa0548ab85db68c5099e7b1180')\n        }], rp['path'])\n\n        # TODO: serialization test to test_lnmsg.py\n        with io.BytesIO() as blinded_path_fd:\n            OnionWireSerializer.write_field(\n                fd=blinded_path_fd,\n                field_type='blinded_path',\n                count=1,\n                value=rp)\n            blinded_path = blinded_path_fd.getvalue()\n        self.assertEqual(blinded_path, bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619022ed557f5ad336b31a49857e4e9664954ac33385aa20a93e2d64bfe7f08f5127701031e5d91e6c417f6e8c16d1086db1887edef7be9334f5e744d04edb8da7507481e00142dbaa54a819775aa0548ab85db68c5099e7b1180'))\n\n    def prepare_blinded_path_bob_to_dave(self):\n        final_recipient_data = {\n            'padding': {'padding': bfh(DAVE_TLVS['padding'])},\n            'path_id': {'data': bfh(DAVE_TLVS['path_id'])},\n            'unknown_tag_65535': {'data': bfh(DAVE_TLVS['unknown_tag_65535'])}\n        }\n        hop_extras = [\n            {'unknown_tag_561': {'data': bfh(BOB_TLVS['unknown_tag_561'])}},\n            {'padding': {'padding': bfh(CAROL_TLVS['padding'])}}\n        ]\n        return create_blinded_path(BLINDING_OVERRIDE_SECRET, [BOB_PUBKEY, CAROL_PUBKEY, DAVE_PUBKEY], final_recipient_data, hop_extras=hop_extras)\n\n    def test_create_onionmessage_to_blinded_path_via_alice(self):\n        blinded_path_to_dave = self.prepare_blinded_path_bob_to_dave()\n        hop_shared_secrets, blinded_node_ids = get_shared_secrets_along_route([ALICE_PUBKEY], BLINDING_SECRET)\n        hops_data = [\n            OnionHopsDataSingle(\n                tlv_stream_name='onionmsg_tlv',\n                blind_fields={\n                    'next_node_id': {'node_id': BOB_PUBKEY},\n                    'next_path_key_override': {'path_key': bfh(ALICE_TLVS['next_path_key_override'])},\n                },\n            ),\n        ]\n        # encrypt encrypted_data_tlv here\n        for i in range(len(hops_data)):\n            encrypted_recipient_data = encrypt_onionmsg_data_tlv(shared_secret=hop_shared_secrets[i], **hops_data[i].blind_fields)\n            new_payload = dict(hops_data[i].payload)\n            new_payload['encrypted_recipient_data'] = {'encrypted_recipient_data': encrypted_recipient_data}\n            hops_data[i] = dataclasses.replace(hops_data[i], payload=new_payload)\n\n        blinded_path_blinded_ids = []\n        for i, x in enumerate(blinded_path_to_dave.get('path')):\n            blinded_path_blinded_ids.append(x.get('blinded_node_id'))\n            payload = {'encrypted_recipient_data': {'encrypted_recipient_data': x.get('encrypted_recipient_data')}}\n            if i == len(blinded_path_to_dave.get('path')) - 1:\n                # add final recipient payload\n                payload['message'] = {'text': bfh(test_vectors['onionmessage']['unknown_tag_1'])}\n            hops_data.append(\n                OnionHopsDataSingle(\n                    tlv_stream_name='onionmsg_tlv',\n                    payload=payload),\n            )\n        payment_path_pubkeys = blinded_node_ids + blinded_path_blinded_ids\n        hop_shared_secrets, _ = get_shared_secrets_along_route(payment_path_pubkeys, SESSION_KEY)\n        encrypt_hops_recipient_data('onionmsg_tlv', hops_data, hop_shared_secrets)\n        packet = new_onion_packet(payment_path_pubkeys, SESSION_KEY, hops_data, onion_message=True)\n        self.assertEqual(packet.to_bytes(), ONION_MESSAGE_PACKET)\n\n\nclass MockNetwork:\n    def __init__(self):\n        self.asyncio_loop = get_asyncio_loop()\n        self.taskgroup = OldTaskGroup()\n        self.config = SimpleConfig()\n        self.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS = True\n\n\nclass MockPeer:\n    their_features = LnFeatures(LnFeatures.OPTION_ONION_MESSAGE_OPT)\n\n    def __init__(self, pubkey, on_send_message=None):\n        self.pubkey = pubkey\n        self.on_send_message = on_send_message\n\n    async def wait_one_htlc_switch_iteration(self, *args):\n        pass\n\n    def is_initialized(self):\n        return True\n\n    def send_message(self, *args, **kwargs):\n        if self.on_send_message:\n            self.on_send_message(*args, **kwargs)\n\n\nclass TestOnionMessageManager(ElectrumTestCase):\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        console_stderr_handler.setLevel(logging.DEBUG)\n\n    def setUp(self):\n        super().setUp()\n\n        def keypair(privkey: ECPrivkey):\n            priv = privkey.get_secret_bytes()\n            return Keypair(pubkey=privkey_to_pubkey(priv), privkey=priv)\n\n        self.alice = keypair(ECPrivkey(privkey_bytes=b'\\x41'*32))\n        self.bob = keypair(ECPrivkey(privkey_bytes=b'\\x42'*32))\n        self.carol = keypair(ECPrivkey(privkey_bytes=b'\\x43'*32))\n        self.dave = keypair(ECPrivkey(privkey_bytes=b'\\x44'*32))\n        self.eve = keypair(ECPrivkey(privkey_bytes=b'\\x45'*32))\n\n    async def run_test1(self, t):\n        t1 = t.submit_send(\n            payload={'message': {'text': 'alice_timeout'.encode('utf-8')}},\n            node_id_or_blinded_path=self.alice.pubkey)\n\n        with self.assertRaises(Timeout):\n            await t1\n\n    async def run_test2(self, t):\n        t2 = t.submit_send(\n            payload={'message': {'text': 'bob_slow_timeout'.encode('utf-8')}},\n            node_id_or_blinded_path=self.bob.pubkey)\n\n        with self.assertRaises(Timeout):\n            await t2\n\n    async def run_test3(self, t, rkey):\n        t3 = t.submit_send(\n            payload={'message': {'text': 'carol_with_immediate_reply'.encode('utf-8')}},\n            node_id_or_blinded_path=self.carol.pubkey,\n            key=rkey)\n\n        t3_result = await t3\n        self.assertEqual(t3_result, ({'path_id': {'data': b'electrum' + rkey}}, {}))\n\n    async def run_test4(self, t, rkey):\n        t4 = t.submit_send(\n            payload={'message': {'text': 'dave_with_slow_reply'.encode('utf-8')}},\n            node_id_or_blinded_path=self.dave.pubkey,\n            key=rkey)\n\n        t4_result = await t4\n        self.assertEqual(t4_result, ({'path_id': {'data': b'electrum' + rkey}}, {}))\n\n    async def run_test5(self, t):\n        t5 = t.submit_send(\n            payload={'message': {'text': 'no_peer'.encode('utf-8')}},\n            node_id_or_blinded_path=self.eve.pubkey)\n\n        with self.assertRaises(NoRouteFound):\n            await t5\n\n    async def test_request_and_reply(self):\n        n = MockNetwork()\n        lnw = self.create_mock_lnwallet(name='test_request_and_reply', has_anchors=False)\n\n        def slow(*args, **kwargs):\n            time.sleep(2*TIME_STEP)\n\n        def withreply(key, *args, **kwargs):\n            t.on_onion_message_received({'path_id': {'data': b'electrum' + key}}, {})\n\n        def slowwithreply(key, *args, **kwargs):\n            time.sleep(2*TIME_STEP)\n            t.on_onion_message_received({'path_id': {'data': b'electrum' + key}}, {})\n\n        rkey1 = bfh('0102030405060708')\n        rkey2 = bfh('0102030405060709')\n\n        lnw.lnpeermgr._peers[self.alice.pubkey] = MockPeer(self.alice.pubkey)\n        lnw.lnpeermgr._peers[self.bob.pubkey] = MockPeer(self.bob.pubkey, on_send_message=slow)\n        lnw.lnpeermgr._peers[self.carol.pubkey] = MockPeer(self.carol.pubkey, on_send_message=partial(withreply, rkey1))\n        lnw.lnpeermgr._peers[self.dave.pubkey] = MockPeer(self.dave.pubkey, on_send_message=partial(slowwithreply, rkey2))\n        t = OnionMessageManager(lnw)\n        t.start_network(network=n)\n\n        try:\n            await asyncio.sleep(TIME_STEP)\n            self.logger.debug('tests in sequence')\n            await self.run_test1(t)\n            await self.run_test2(t)\n            await self.run_test3(t, rkey1)\n            await self.run_test4(t, rkey2)\n            await self.run_test5(t)\n            self.logger.debug('tests in parallel')\n            async with OldTaskGroup() as group:\n                await group.spawn(self.run_test1(t))\n                await group.spawn(self.run_test2(t))\n                await group.spawn(self.run_test3(t, rkey1))\n                await group.spawn(self.run_test4(t, rkey2))\n                await group.spawn(self.run_test5(t))\n        finally:\n            await asyncio.sleep(TIME_STEP)\n\n            self.logger.debug('stopping manager')\n            await t.stop()\n            await lnw.stop()\n\n    async def test_forward(self):\n        n = MockNetwork()\n        lnw = self.create_mock_lnwallet(name='alice', has_anchors=False)\n        lnw.node_keypair = self.alice\n\n        self.was_sent = False\n\n        def on_send(to: str, *args, **kwargs):\n            self.assertEqual(to, 'bob')\n            self.was_sent = True\n            # validate what's sent to bob\n            self.assertEqual(bfh(HOPS[1]['E']), kwargs['blinding'])\n            message_type, payload = decode_msg(bfh(test_vectors['decrypt']['hops'][1]['onion_message']))\n            self.assertEqual(message_type, 'onion_message')\n            self.assertEqual(payload['onion_message_packet'], kwargs['onion_message_packet'])\n\n        lnw.lnpeermgr._peers[self.bob.pubkey] = MockPeer(self.bob.pubkey, on_send_message=partial(on_send, 'bob'))\n        lnw.lnpeermgr._peers[self.carol.pubkey] = MockPeer(self.carol.pubkey, on_send_message=partial(on_send, 'carol'))\n        t = OnionMessageManager(lnw)\n        t.start_network(network=n)\n\n        onionmsg = bfh(test_vectors['onionmessage']['onion_message_packet'])\n        try:\n            t.on_onion_message({\n                'path_key': bfh(test_vectors['route']['first_path_key']),\n                'len': len(onionmsg),\n                'onion_message_packet': onionmsg\n            })\n        finally:\n            await asyncio.sleep(2*TIME_STEP)\n\n            self.logger.debug('stopping manager')\n            await t.stop()\n            await lnw.stop()\n\n        self.assertTrue(self.was_sent)\n\n    async def test_receive_unsolicited(self):\n        n = MockNetwork()\n        lnw = self.create_mock_lnwallet(name='dave', has_anchors=False)\n        lnw.node_keypair = self.dave\n\n        t = OnionMessageManager(lnw)\n        t.start_network(network=n)\n\n        self.received_unsolicited = False\n\n        def my_on_onion_message_received_unsolicited(*args, **kwargs):\n            self.received_unsolicited = True\n\n        t.on_onion_message_received_unsolicited = my_on_onion_message_received_unsolicited\n        packet = bfh(test_vectors['decrypt']['hops'][3]['onion_message'])\n        message_type, payload = decode_msg(packet)\n        try:\n            t.on_onion_message(payload)\n            self.assertTrue(self.received_unsolicited)\n        finally:\n            await asyncio.sleep(TIME_STEP)\n\n            self.logger.debug('stopping manager')\n            await t.stop()\n            await lnw.stop()\n"
  },
  {
    "path": "tests/test_payment_identifier.py",
    "content": "import os\nimport asyncio\nfrom unittest.mock import patch\n\nfrom electrum import SimpleConfig\nfrom electrum.invoices import Invoice\nfrom electrum.payment_identifier import (\n    maybe_extract_bech32_lightning_payment_identifier, PaymentIdentifier, PaymentIdentifierType,\n    PaymentIdentifierState, invoice_from_payment_identifier, remove_uri_prefix,\n)\nfrom electrum.lnurl import LNURL6Data, LNURL3Data, LNURLError\nfrom electrum.transaction import PartialTxOutput\n\nfrom . import ElectrumTestCase\nfrom . import restore_wallet_from_text__for_unittest\n\n\nclass WalletMock:\n    def __init__(self, electrum_path):\n        self.config = SimpleConfig({\n            'electrum_path': electrum_path,\n            'decimal_point': 5\n        })\n        self.contacts = None\n\n\nclass TestPaymentIdentifier(ElectrumTestCase):\n    def setUp(self):\n        super().setUp()\n        self.wallet = WalletMock(self.electrum_path)\n\n        self.config = SimpleConfig({\n            'electrum_path': self.electrum_path,\n            'decimal_point': 5\n        })\n        self.wallet2_path = os.path.join(self.electrum_path, \"somewallet2\")\n\n    def test_maybe_extract_bech32_lightning_payment_identifier(self):\n        bolt11 = \"lnbc1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdqq9qypqszpyrpe4tym8d3q87d43cgdhhlsrt78epu7u99mkzttmt2wtsx0304rrw50addkryfrd3vn3zy467vxwlmf4uz7yvntuwjr2hqjl9lw5cqwtp2dy\"\n        lnurl = \"lnurl1dp68gurn8ghj7um9wfmxjcm99e5k7telwy7nxenrxvmrgdtzxsenjcm98pjnwxq96s9\"\n        self.assertEqual(bolt11, maybe_extract_bech32_lightning_payment_identifier(f\"{bolt11}\".upper()))\n        self.assertEqual(bolt11, maybe_extract_bech32_lightning_payment_identifier(f\"lightning:{bolt11}\"))\n        self.assertEqual(bolt11, maybe_extract_bech32_lightning_payment_identifier(f\"  lightning:{bolt11}   \".upper()))\n        self.assertEqual(lnurl, maybe_extract_bech32_lightning_payment_identifier(lnurl))\n        self.assertEqual(lnurl, maybe_extract_bech32_lightning_payment_identifier(f\"  lightning:{lnurl}   \".upper()))\n\n        self.assertEqual(None, maybe_extract_bech32_lightning_payment_identifier(f\"bitcoin:{bolt11}\"))\n        self.assertEqual(None, maybe_extract_bech32_lightning_payment_identifier(f\":{bolt11}\"))\n        self.assertEqual(None, maybe_extract_bech32_lightning_payment_identifier(f\"garbage text\"))\n\n    def test_remove_uri_prefix(self):\n        lightning, bitcoin = 'lightning', 'bitcoin'\n        tests = (\n            (lightning, '', ''),\n            (lightning, 'lightning:test', 'test'),\n            (lightning, 'bitcoin:test', 'bitcoin:test'),\n            (lightning, 'lightningtest', 'lightningtest'),\n            (lightning, 'lightning test', 'lightning test'),\n            (bitcoin, 'lightning:test', 'lightning:test'),\n            (bitcoin, 'bitcoin:test', 'test'),\n            (bitcoin, 'bitcoin', 'bitcoin'),\n            (bitcoin, 'bitcoin:', ''),\n        )\n        for prefix, input_str, expected_output_str in tests:\n            output_str = remove_uri_prefix(input_str, prefix=prefix)\n            self.assertEqual(expected_output_str, output_str, msg=output_str)\n        with self.assertRaises(AssertionError):\n            remove_uri_prefix(data=1234, prefix=\"test\")\n\n    def test_bolt11(self):\n        # no amount, no fallback address\n        bolt11 = 'lnbc1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdqq9qypqszpyrpe4tym8d3q87d43cgdhhlsrt78epu7u99mkzttmt2wtsx0304rrw50addkryfrd3vn3zy467vxwlmf4uz7yvntuwjr2hqjl9lw5cqwtp2dy'\n        for pi_str in [\n            f'{bolt11}',\n            f'  {bolt11}',\n            f'{bolt11}  ',\n            f'lightning:{bolt11}',\n            f'  lightning:{bolt11}',\n            f'lightning:{bolt11}  ',\n            f'lightning:{bolt11.upper()}',\n            f'lightning:{bolt11}'.upper(),\n        ]:\n            pi = PaymentIdentifier(None, pi_str)\n            self.assertTrue(pi.is_valid())\n            self.assertEqual(PaymentIdentifierType.BOLT11, pi.type)\n            self.assertFalse(pi.is_amount_locked())\n            self.assertFalse(pi.is_error())\n            self.assertIsNotNone(pi.bolt11)\n\n        for pi_str in [\n            f'lightning:  {bolt11}',\n            f'bitcoin:{bolt11}'\n        ]:\n            pi = PaymentIdentifier(None, pi_str)\n            self.assertFalse(pi.is_valid())\n\n        # amount, fallback address\n        bolt_11_w_fallback = 'lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj'\n        pi = PaymentIdentifier(None, bolt_11_w_fallback)\n        self.assertTrue(pi.is_valid())\n        self.assertEqual(PaymentIdentifierType.BOLT11, pi.type)\n        self.assertIsNotNone(pi.bolt11)\n        self.assertTrue(pi.is_lightning())\n        self.assertTrue(pi.is_onchain())\n        self.assertTrue(pi.is_amount_locked())\n\n        self.assertFalse(pi.is_error())\n        self.assertFalse(pi.need_resolve())\n        self.assertFalse(pi.need_finalize())\n        self.assertFalse(pi.is_multiline())\n\n    def test_bip21(self):\n        bip21 = 'bitcoin:bc1qj3zx2zc4rpv3npzmznxhdxzn0wm7pzqp8p2293?message=unit_test'\n        for pi_str in [\n            f'{bip21}',\n            f'  {bip21}',\n            f'{bip21}  ',\n            f'{bip21}'.upper(),\n        ]:\n            pi = PaymentIdentifier(None, pi_str)\n            self.assertTrue(pi.is_available())\n            self.assertFalse(pi.is_lightning())\n            self.assertTrue(pi.is_onchain())\n            self.assertIsNotNone(pi.bip21)\n\n        # amount, expired, message\n        bip21 = 'bitcoin:bc1qy7ps80x5csdqpfcekn97qfljxtg2lrya8826ds?amount=0.001&message=unit_test&time=1707382023&exp=3600'\n\n        pi = PaymentIdentifier(None, bip21)\n        self.assertTrue(pi.is_available())\n        self.assertFalse(pi.is_lightning())\n        self.assertTrue(pi.is_onchain())\n        self.assertIsNotNone(pi.bip21)\n\n        self.assertTrue(pi.has_expired())\n        self.assertEqual('unit_test', pi.bip21.get('message'))\n\n        # amount, expired, message, lightning w matching amount\n        bip21 = 'bitcoin:1RustyRX2oai4EYYDpQGWvEL62BBGqN9T?amount=0.02&message=unit_test&time=1707382023&exp=3600&lightning=lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj'\n\n        pi = PaymentIdentifier(None, bip21)\n        self.assertTrue(pi.is_available())\n        self.assertTrue(pi.is_lightning())\n        self.assertTrue(pi.is_onchain())\n        self.assertIsNotNone(pi.bip21)\n        self.assertIsNotNone(pi.bolt11)\n\n        self.assertTrue(pi.has_expired())\n        self.assertEqual('unit_test', pi.bip21.get('message'))\n\n        # amount, expired, message, lightning w non-matching amount\n        bip21 = 'bitcoin:1RustyRX2oai4EYYDpQGWvEL62BBGqN9T?amount=0.01&message=unit_test&time=1707382023&exp=3600&lightning=lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj'\n\n        pi = PaymentIdentifier(None, bip21)\n        self.assertFalse(pi.is_valid())\n\n        # amount bounds\n        bip21 = 'bitcoin:1RustyRX2oai4EYYDpQGWvEL62BBGqN9T?amount=-1'\n        pi = PaymentIdentifier(None, bip21)\n        self.assertFalse(pi.is_valid())\n\n        bip21 = 'bitcoin:1RustyRX2oai4EYYDpQGWvEL62BBGqN9T?amount=21000001'\n        pi = PaymentIdentifier(None, bip21)\n        self.assertFalse(pi.is_valid())\n\n        bip21 = 'bitcoin:1RustyRX2oai4EYYDpQGWvEL62BBGqN9T?amount=0'\n        pi = PaymentIdentifier(None, bip21)\n        self.assertFalse(pi.is_valid())\n\n    def test_lnurl_basic(self):\n        \"\"\"Test basic LNURL parsing without resolve\"\"\"\n        valid_lnurl = 'lnurl1dp68gurn8ghj7um9wfmxjcm99e5k7telwy7nxenrxvmrgdtzxsenjcm98pjnwxq96s9'\n        pi = PaymentIdentifier(None, valid_lnurl)\n        self.assertTrue(pi.is_valid())\n        self.assertEqual(PaymentIdentifierType.LNURL, pi.type)\n        self.assertFalse(pi.is_available())\n        self.assertTrue(pi.need_resolve())\n        self.assertEqual(PaymentIdentifierState.NEED_RESOLVE, pi.state)\n\n        # Test with lightning: prefix\n        lightning_lnurl = f'lightning:{valid_lnurl}'\n        pi = PaymentIdentifier(None, lightning_lnurl)\n        self.assertTrue(pi.is_valid())\n        self.assertEqual(PaymentIdentifierType.LNURL, pi.type)\n        self.assertTrue(pi.need_resolve())\n\n    @patch('electrum.payment_identifier.request_lnurl')\n    def test_lnurl_pay_resolve(self, mock_request_lnurl):\n        \"\"\"Test LNURL-pay (LNURL6) with mocked resolve\"\"\"\n        valid_lnurl = 'LNURL1DP68GURN8GHJ7MRWVF5HGUEWD3HXZERYWFJHXUEWVDHK6TMVDE6HYMRS9ANRV46DXETQPJQCS4'\n\n        # Mock lnurl-p response\n        mock_lnurl6_data = LNURL6Data(\n            callback_url='https://example.com/lnurl-pay',\n            max_sendable_sat=1_000_000,\n            min_sendable_sat=1_000,\n            metadata_plaintext='Test payment',\n            comment_allowed=100,\n        )\n        mock_request_lnurl.return_value = mock_lnurl6_data\n\n        pi = PaymentIdentifier(None, valid_lnurl)\n        self.assertTrue(pi.need_resolve())\n        self.assertEqual(PaymentIdentifierType.LNURL, pi.type)\n\n        async def run_resolve():\n            await pi._do_resolve()\n\n        asyncio.run(run_resolve())\n\n        self.assertEqual(PaymentIdentifierType.LNURLP, pi.type)\n        self.assertEqual(PaymentIdentifierState.LNURLP_FINALIZE, pi.state)\n        self.assertTrue(pi.need_finalize())\n        self.assertIsNotNone(pi.lnurl_data)\n        self.assertTrue(isinstance(pi.lnurl_data, LNURL6Data))\n        self.assertEqual(1_000, pi.lnurl_data.min_sendable_sat)\n        self.assertEqual(1_000_000, pi.lnurl_data.max_sendable_sat)\n        self.assertEqual('Test payment', pi.lnurl_data.metadata_plaintext)\n        self.assertEqual(100, pi.lnurl_data.comment_allowed)\n\n    @patch('electrum.payment_identifier.request_lnurl')\n    def test_lnurl_withdraw_resolve(self, mock_request_lnurl):\n        \"\"\"Test LNURL-withdraw (LNURL3) with mocked resolve\"\"\"\n        valid_lnurl = 'LNURL1DP68GURN8GHJ7MRWVF5HGUEWD3HXZERYWFJHXUEWVDHK6TM4WPNHYCTYV4EJ7DFCVGENSDPH8QCRZETXVGCXGCMPVFJR' \\\n                        'WENP8P3NJEP3XE3NQWRPXFJR2VRRVSCX2V33V5UNVC3SXP3RXCFSVFSKVWPCV3SKZWTP8YUZ7AMFW35XGUNPWUHKZURF9AMRZT' \\\n                        'MVDE6HYMP0FETHVUNZDAMHQ7JSF4RX73TZ2VU9Z3J3GVMSLCJ57F'\n\n        # Mock lnurl-w response\n        mock_lnurl3_data = LNURL3Data(\n            callback_url='https://example.com/lnurl-withdraw',\n            k1='test-k1-value',\n            default_description='Test withdrawal',\n            min_withdrawable_sat=1_000,\n            max_withdrawable_sat=500_000,\n        )\n        mock_request_lnurl.return_value = mock_lnurl3_data\n\n        pi = PaymentIdentifier(None, valid_lnurl)\n        self.assertTrue(pi.need_resolve())\n        self.assertEqual(PaymentIdentifierType.LNURL, pi.type)\n\n        async def run_resolve():\n            await pi._do_resolve()\n\n        asyncio.run(run_resolve())\n\n        self.assertEqual(PaymentIdentifierType.LNURLW, pi.type)\n        self.assertEqual(PaymentIdentifierState.LNURLW_FINALIZE, pi.state)\n        self.assertIsNotNone(pi.lnurl_data)\n        self.assertEqual('test-k1-value', pi.lnurl_data.k1)\n        self.assertEqual('Test withdrawal', pi.lnurl_data.default_description)\n        self.assertEqual(1000, pi.lnurl_data.min_withdrawable_sat)\n        self.assertEqual(500000, pi.lnurl_data.max_withdrawable_sat)\n\n    @patch('electrum.payment_identifier.request_lnurl')\n    def test_lnurl_resolve_error(self, mock_request_lnurl):\n        \"\"\"Test LNURL resolve error handling\"\"\"\n        lnurl = 'LNURL1DP68GURN8GHJ7MRWVF5HGUEWD3HXZERYWFJHXUEWVDHK6TM4WPNHYCTYV4EJ7DFCVGENSDPH8QCRZETXVGCXGCMPVFJR' \\\n                  'WENP8P3NJEP3XE3NQWRPXFJR2VRRVSCX2V33V5UNVC3SXP3RXCFSVFSKVWPCV3SKZWTP8YUZ7AMFW35XGUNPWUHKZURF9AMRZT' \\\n                  'MVDE6HYMP0FETHVUNZDAMHQ7JSF4RX73TZ2VU9Z3J3GVMSLCJ57F'\n\n        # Mock LNURL error\n        mock_request_lnurl.side_effect = LNURLError(\"Server error\")\n\n        pi = PaymentIdentifier(None, lnurl)\n        self.assertTrue(pi.need_resolve())\n\n        async def run_resolve():\n            await pi._do_resolve()\n\n        asyncio.run(run_resolve())\n\n        self.assertEqual(PaymentIdentifierState.ERROR, pi.state)\n        self.assertTrue(pi.is_error())\n        self.assertIn(\"Server error\", pi.get_error())\n\n    def test_multiline(self):\n        pi_str = '\\n'.join([\n            'bc1qj3zx2zc4rpv3npzmznxhdxzn0wm7pzqp8p2293,0.01',\n            'bc1q66ex4c3vek4cdmrfjxtssmtguvs3r30pf42jpj,0.01',\n        ])\n        pi = PaymentIdentifier(self.wallet, pi_str)\n        self.assertTrue(pi.is_valid())\n        self.assertTrue(pi.is_multiline())\n        self.assertFalse(pi.is_multiline_max())\n        self.assertIsNotNone(pi.multiline_outputs)\n        self.assertEqual(2, len(pi.multiline_outputs))\n        self.assertTrue(all(lambda x: isinstance(x, PartialTxOutput) for x in pi.multiline_outputs))\n        self.assertEqual(1000, pi.multiline_outputs[0].value)\n        self.assertEqual(1000, pi.multiline_outputs[1].value)\n\n        pi_str = '\\n'.join([\n            'bc1qj3zx2zc4rpv3npzmznxhdxzn0wm7pzqp8p2293,0.01',\n            'bc1q66ex4c3vek4cdmrfjxtssmtguvs3r30pf42jpj,0.01',\n            'bc1qy7ps80x5csdqpfcekn97qfljxtg2lrya8826ds,!',\n        ])\n        pi = PaymentIdentifier(self.wallet, pi_str)\n        self.assertTrue(pi.is_valid())\n        self.assertTrue(pi.is_multiline())\n        self.assertTrue(pi.is_multiline_max())\n        self.assertIsNotNone(pi.multiline_outputs)\n        self.assertEqual(3, len(pi.multiline_outputs))\n        self.assertTrue(all(lambda x: isinstance(x, PartialTxOutput) for x in pi.multiline_outputs))\n        self.assertEqual(1000, pi.multiline_outputs[0].value)\n        self.assertEqual(1000, pi.multiline_outputs[1].value)\n        self.assertEqual('!', pi.multiline_outputs[2].value)\n\n        pi_str = '\\n'.join([\n            'bc1qj3zx2zc4rpv3npzmznxhdxzn0wm7pzqp8p2293,0.01',\n            'bc1q66ex4c3vek4cdmrfjxtssmtguvs3r30pf42jpj,2!',\n            'bc1qy7ps80x5csdqpfcekn97qfljxtg2lrya8826ds,3!',\n        ])\n        pi = PaymentIdentifier(self.wallet, pi_str)\n        self.assertTrue(pi.is_valid())\n        self.assertTrue(pi.is_multiline())\n        self.assertTrue(pi.is_multiline_max())\n        self.assertIsNotNone(pi.multiline_outputs)\n        self.assertEqual(3, len(pi.multiline_outputs))\n        self.assertTrue(all(lambda x: isinstance(x, PartialTxOutput) for x in pi.multiline_outputs))\n        self.assertEqual(1000, pi.multiline_outputs[0].value)\n        self.assertEqual('2!', pi.multiline_outputs[1].value)\n        self.assertEqual('3!', pi.multiline_outputs[2].value)\n\n        pi_str = '\\n'.join([\n            'bc1qj3zx2zc4rpv3npzmznxhdxzn0wm7pzqp8p2293,0.01',\n            'script(OP_RETURN baddc0ffee),0'\n        ])\n        pi = PaymentIdentifier(self.wallet, pi_str)\n        self.assertTrue(pi.is_valid())\n        self.assertTrue(pi.is_multiline())\n        self.assertIsNotNone(pi.multiline_outputs)\n        self.assertEqual(2, len(pi.multiline_outputs))\n        self.assertTrue(all(lambda x: isinstance(x, PartialTxOutput) for x in pi.multiline_outputs))\n        self.assertEqual(1000, pi.multiline_outputs[0].value)\n        self.assertEqual(0, pi.multiline_outputs[1].value)\n\n    def test_spk(self):\n        address = 'bc1qj3zx2zc4rpv3npzmznxhdxzn0wm7pzqp8p2293'\n        for pi_str in [\n            f'{address}',\n            f'  {address}',\n            f'{address}  ',\n            f'{address}'.upper(),\n        ]:\n            pi = PaymentIdentifier(None, pi_str)\n            self.assertTrue(pi.is_valid())\n            self.assertTrue(pi.is_available())\n\n        spk = 'script(OP_RETURN baddc0ffee)'\n        for pi_str in [\n            f'{spk}',\n            f'  {spk}',\n            f'{spk}  ',\n        ]:\n            pi = PaymentIdentifier(None, pi_str)\n            self.assertTrue(pi.is_valid())\n            self.assertTrue(pi.is_available())\n\n    def test_email_and_domain(self):\n        # TODO resolve mock\n        domain_pi_strings = (\n            'some.domain',\n            'some.weird.but.valid.domain',\n            'lnbcsome.weird.but.valid.domain',\n            'bc1qsome.weird.but.valid.domain',\n            'lnurlsome.weird.but.valid.domain',\n        )\n        for pi_str in domain_pi_strings:\n            pi = PaymentIdentifier(None, pi_str)\n            self.assertTrue(pi.is_valid())\n            self.assertEqual(PaymentIdentifierType.DOMAINLIKE, pi.type)\n            self.assertFalse(pi.is_available())\n            self.assertTrue(pi.need_resolve())\n\n        email_pi_strings = (\n            'user@some.domain',\n            'user@some.weird.but.valid.domain',\n            'lnbcuser@some.domain',\n            'lnurluser@some.domain',\n            'bc1quser@some.domain',\n            'lightning:user@some.domain',\n            'lightning:user@some.weird.but.valid.domain',\n            'lightning:lnbcuser@some.domain',\n            'lightning:lnurluser@some.domain',\n            'lightning:bc1quser@some.domain',\n        )\n        for pi_str in email_pi_strings:\n            pi = PaymentIdentifier(None, pi_str)\n            self.assertTrue(pi.is_valid())\n            self.assertEqual(PaymentIdentifierType.EMAILLIKE, pi.type)\n            self.assertFalse(pi.is_available())\n            self.assertTrue(pi.need_resolve())\n\n    def test_bip70(self):\n        pi_str = 'bitcoin:?r=https://test.bitpay.com/i/87iLJoaYVyJwFXtdassQJv'\n        pi = PaymentIdentifier(None, pi_str)\n        self.assertTrue(pi.is_valid())\n        self.assertEqual(PaymentIdentifierType.BIP70, pi.type)\n        self.assertFalse(pi.is_available())\n        self.assertTrue(pi.need_resolve())\n\n        # TODO resolve mock\n\n    async def test_invoice_from_payment_identifier(self):\n        # amount, expired, message, lightning w matching amount\n        bip21 = 'bitcoin:1RustyRX2oai4EYYDpQGWvEL62BBGqN9T?amount=0.02&message=unit_test&time=1707382023&exp=3600&lightning=lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj'\n\n        pi = PaymentIdentifier(None, bip21)\n        invoice = invoice_from_payment_identifier(pi, None, None)\n        self.assertTrue(isinstance(invoice, Invoice))\n        self.assertTrue(invoice.is_lightning())\n        self.assertEqual(2_000_000_000, invoice.amount_msat)\n\n        text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet2_path, config=self.config)\n        wallet2 = d['wallet']  # type: Standard_Wallet\n\n        # no amount bip21+lightning, MAX amount passed\n        bip21 = 'bitcoin:1RustyRX2oai4EYYDpQGWvEL62BBGqN9T?message=unit_test&time=1707382023&exp=3600&lightning=lnbc1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdqq9qypqszpyrpe4tym8d3q87d43cgdhhlsrt78epu7u99mkzttmt2wtsx0304rrw50addkryfrd3vn3zy467vxwlmf4uz7yvntuwjr2hqjl9lw5cqwtp2dy'\n        pi = PaymentIdentifier(None, bip21)\n        invoice = invoice_from_payment_identifier(pi, wallet2, '!')\n        self.assertTrue(isinstance(invoice, Invoice))\n        self.assertFalse(invoice.is_lightning())\n\n        # no amount lightning, MAX amount passed -> expect raise\n        bolt11 = 'lightning:lnbc1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdqq9qypqszpyrpe4tym8d3q87d43cgdhhlsrt78epu7u99mkzttmt2wtsx0304rrw50addkryfrd3vn3zy467vxwlmf4uz7yvntuwjr2hqjl9lw5cqwtp2dy'\n        pi = PaymentIdentifier(None, bolt11)\n        with self.assertRaises(AssertionError):\n            invoice_from_payment_identifier(pi, wallet2, '!')\n        invoice = invoice_from_payment_identifier(pi, wallet2, 1)\n        self.assertEqual(1000, invoice.amount_msat)\n"
  },
  {
    "path": "tests/test_psbt.py",
    "content": "from pprint import pprint\nimport unittest\n\nfrom electrum import constants\nfrom electrum.transaction import (tx_from_any, PartialTransaction, BadHeaderMagic, UnexpectedEndOfStream,\n                                  SerializationError, PSBTInputConsistencyFailure)\n\nfrom . import ElectrumTestCase\n\n\nclass TestValidPSBT(ElectrumTestCase):\n    # test cases from BIP-0174\n    TESTNET = True\n\n    def test_valid_psbt_001(self):\n        # Case: PSBT with one P2PKH input. Outputs are empty\n        tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab300000000000000'))\n        tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA')\n        for tx in (tx1, tx2):\n            self.assertEqual(1, len(tx.inputs()))\n            self.assertFalse(tx.inputs()[0].is_complete())\n\n    def test_valid_psbt_002(self):\n        # Case: PSBT with one P2PKH input and one P2SH-P2WPKH input. First input is signed and finalized. Outputs are empty\n        tx1 = tx_from_any(bytes.fromhex('70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac000000000001076a47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa882920001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb82308000000'))\n        tx2 = tx_from_any('cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA')\n        for tx in (tx1, tx2):\n            self.assertEqual(2, len(tx.inputs()))\n            self.assertTrue(tx.inputs()[0].is_complete())\n            self.assertFalse(tx.inputs()[1].is_complete())\n\n    def test_valid_psbt_003(self):\n        # Case: PSBT with one P2PKH input which has a non-final scriptSig and has a sighash type specified. Outputs are empty\n        tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000001030401000000000000'))\n        tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==')\n        for tx in (tx1, tx2):\n            self.assertEqual(1, len(tx.inputs()))\n            self.assertEqual(1, tx.inputs()[0].sighash)\n            self.assertFalse(tx.inputs()[0].is_complete())\n\n    def test_valid_psbt_004(self):\n        # Case: PSBT with one P2PKH input and one P2SH-P2WPKH input both with non-final scriptSigs. P2SH-P2WPKH input's redeemScript is available. Outputs filled.\n        tx1 = tx_from_any(bytes.fromhex('70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac00000000000100df0200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf6000000006a473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e13000001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb8230800220202ead596687ca806043edc3de116cdf29d5e9257c196cd055cf698c8d02bf24e9910b4a6ba670000008000000080020000800022020394f62be9df19952c5587768aeb7698061ad2c4a25c894f47d8c162b4d7213d0510b4a6ba6700000080010000800200008000'))\n        tx2 = tx_from_any('cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEA3wIAAAABJoFxNx7f8oXpN63upLN7eAAMBWbLs61kZBcTykIXG/YAAAAAakcwRAIgcLIkUSPmv0dNYMW1DAQ9TGkaXSQ18Jo0p2YqncJReQoCIAEynKnazygL3zB0DsA5BCJCLIHLRYOUV663b8Eu3ZWzASECZX0RjTNXuOD0ws1G23s59tnDjZpwq8ubLeXcjb/kzjH+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=')\n        for tx in (tx1, tx2):\n            self.assertEqual(2, len(tx.inputs()))\n            self.assertFalse(tx.inputs()[0].is_complete())\n            self.assertFalse(tx.inputs()[1].is_complete())\n            self.assertTrue(tx.inputs()[1].redeem_script is not None)\n\n    def test_valid_psbt_005(self):\n        # Case: PSBT with one P2SH-P2WSH input of a 2-of-2 multisig, redeemScript, witnessScript, and keypaths are available. Contains one signature.\n        tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))\n        tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=')\n        for tx in (tx1, tx2):\n            self.assertEqual(1, len(tx.inputs()))\n            self.assertFalse(tx.inputs()[0].is_complete())\n            self.assertTrue(tx.inputs()[0].redeem_script is not None)\n            self.assertTrue(tx.inputs()[0].witness_script is not None)\n            self.assertEqual(2, len(tx.inputs()[0].bip32_paths))\n            self.assertEqual(1, len(tx.inputs()[0].sigs_ecdsa))\n\n    def test_valid_psbt_006(self):\n        # Case: PSBT with one P2WSH input of a 2-of-2 multisig. witnessScript, keypaths, and global xpubs are available. Contains no signatures. Outputs filled.\n        tx1 = tx_from_any(bytes.fromhex('70736274ff01005202000000019dfc6628c26c5899fe1bd3dc338665bfd55d7ada10f6220973df2d386dec12760100000000ffffffff01f03dcd1d000000001600147b3a00bfdc14d27795c2b74901d09da6ef133579000000004f01043587cf02da3fd0088000000097048b1ad0445b1ec8275517727c87b4e4ebc18a203ffa0f94c01566bd38e9000351b743887ee1d40dc32a6043724f2d6459b3b5a4d73daec8fbae0472f3bc43e20cd90c6a4fae000080000000804f01043587cf02da3fd00880000001b90452427139cd78c2cff2444be353cd58605e3e513285e528b407fae3f6173503d30a5e97c8adbc557dac2ad9a7e39c1722ebac69e668b6f2667cc1d671c83cab0cd90c6a4fae000080010000800001012b0065cd1d000000002200202c5486126c4978079a814e13715d65f36459e4d6ccaded266d0508645bafa6320105475221029da12cdb5b235692b91536afefe5c91c3ab9473d8e43b533836ab456299c88712103372b34234ed7cf9c1fea5d05d441557927be9542b162eb02e1ab2ce80224c00b52ae2206029da12cdb5b235692b91536afefe5c91c3ab9473d8e43b533836ab456299c887110d90c6a4fae0000800000008000000000220603372b34234ed7cf9c1fea5d05d441557927be9542b162eb02e1ab2ce80224c00b10d90c6a4fae0000800100008000000000002202039eff1f547a1d5f92dfa2ba7af6ac971a4bd03ba4a734b03156a256b8ad3a1ef910ede45cc500000080000000800100008000'))\n        tx2 = tx_from_any('cHNidP8BAFICAAAAAZ38ZijCbFiZ/hvT3DOGZb/VXXraEPYiCXPfLTht7BJ2AQAAAAD/////AfA9zR0AAAAAFgAUezoAv9wU0neVwrdJAdCdpu8TNXkAAAAATwEENYfPAto/0AiAAAAAlwSLGtBEWx7IJ1UXcnyHtOTrwYogP/oPlMAVZr046QADUbdDiH7h1A3DKmBDck8tZFmztaTXPa7I+64EcvO8Q+IM2QxqT64AAIAAAACATwEENYfPAto/0AiAAAABuQRSQnE5zXjCz/JES+NTzVhgXj5RMoXlKLQH+uP2FzUD0wpel8itvFV9rCrZp+OcFyLrrGnmaLbyZnzB1nHIPKsM2QxqT64AAIABAACAAAEBKwBlzR0AAAAAIgAgLFSGEmxJeAeagU4TcV1l82RZ5NbMre0mbQUIZFuvpjIBBUdSIQKdoSzbWyNWkrkVNq/v5ckcOrlHPY5DtTODarRWKZyIcSEDNys0I07Xz5wf6l0F1EFVeSe+lUKxYusC4ass6AIkwAtSriIGAp2hLNtbI1aSuRU2r+/lyRw6uUc9jkO1M4NqtFYpnIhxENkMak+uAACAAAAAgAAAAAAiBgM3KzQjTtfPnB/qXQXUQVV5J76VQrFi6wLhqyzoAiTACxDZDGpPrgAAgAEAAIAAAAAAACICA57/H1R6HV+S36K6evaslxpL0DukpzSwMVaiVritOh75EO3kXMUAAACAAAAAgAEAAIAA')\n        for tx in (tx1, tx2):\n            self.assertEqual(1, len(tx.inputs()))\n            self.assertFalse(tx.inputs()[0].is_complete())\n            self.assertTrue(tx.inputs()[0].witness_script is not None)\n            self.assertEqual(2, len(tx.inputs()[0].bip32_paths))\n            self.assertEqual(2, len(tx.xpubs))\n            self.assertEqual(0, len(tx.inputs()[0].sigs_ecdsa))\n\n    def test_valid_psbt_007(self):\n        # Case: PSBT with unknown types in the inputs.\n        tx1 = tx_from_any(bytes.fromhex('70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0000'))\n        tx2 = tx_from_any('cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=')\n        for tx in (tx1, tx2):\n            self.assertEqual(1, len(tx.inputs()))\n            self.assertEqual(1, len(tx.inputs()[0]._unknown))\n\n    def test_valid_psbt_008(self):\n        # Case: PSBT with `PSBT_GLOBAL_XPUB`.\n        constants.BitcoinMainnet.set_as_network()\n        try:\n            tx1 = tx_from_any(bytes.fromhex('70736274ff01009d0100000002710ea76ab45c5cb6438e607e59cc037626981805ae9e0dfd9089012abb0be5350100000000ffffffff190994d6a8b3c8c82ccbcfb2fba4106aa06639b872a8d447465c0d42588d6d670000000000ffffffff0200e1f505000000001976a914b6bc2c0ee5655a843d79afedd0ccc3f7dd64340988ac605af405000000001600141188ef8e4ce0449eaac8fb141cbf5a1176e6a088000000004f010488b21e039e530cac800000003dbc8a5c9769f031b17e77fea1518603221a18fd18f2b9a54c6c8c1ac75cbc3502f230584b155d1c7f1cd45120a653c48d650b431b67c5b2c13f27d7142037c1691027569c503100008000000080000000800001011f00e1f5050000000016001433b982f91b28f160c920b4ab95e58ce50dda3a4a220203309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c47304402202d704ced830c56a909344bd742b6852dccd103e963bae92d38e75254d2bb424502202d86c437195df46c0ceda084f2a291c3da2d64070f76bf9b90b195e7ef28f77201220603309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c1827569c5031000080000000800000008000000000010000000001011f00e1f50500000000160014388fb944307eb77ef45197d0b0b245e079f011de220202c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b11047304402204cb1fb5f869c942e0e26100576125439179ae88dca8a9dc3ba08f7953988faa60220521f49ca791c27d70e273c9b14616985909361e25be274ea200d7e08827e514d01220602c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b1101827569c5031000080000000800000008000000000000000000000220202d20ca502ee289686d21815bd43a80637b0698e1fbcdbe4caed445f6c1a0a90ef1827569c50310000800000008000000080000000000400000000'))\n            tx2 = tx_from_any('cHNidP8BAJ0BAAAAAnEOp2q0XFy2Q45gflnMA3YmmBgFrp4N/ZCJASq7C+U1AQAAAAD/////GQmU1qizyMgsy8+y+6QQaqBmObhyqNRHRlwNQliNbWcAAAAAAP////8CAOH1BQAAAAAZdqkUtrwsDuVlWoQ9ea/t0MzD991kNAmIrGBa9AUAAAAAFgAUEYjvjkzgRJ6qyPsUHL9aEXbmoIgAAAAATwEEiLIeA55TDKyAAAAAPbyKXJdp8DGxfnf+oVGGAyIaGP0Y8rmlTGyMGsdcvDUC8jBYSxVdHH8c1FEgplPEjWULQxtnxbLBPyfXFCA3wWkQJ1acUDEAAIAAAACAAAAAgAABAR8A4fUFAAAAABYAFDO5gvkbKPFgySC0q5XljOUN2jpKIgIDMJaA8zx9446mpHzU7NZvH1pJdHxv+4gI7QkDkkPjrVxHMEQCIC1wTO2DDFapCTRL10K2hS3M0QPpY7rpLTjnUlTSu0JFAiAthsQ3GV30bAztoITyopHD2i1kBw92v5uQsZXn7yj3cgEiBgMwloDzPH3jjqakfNTs1m8fWkl0fG/7iAjtCQOSQ+OtXBgnVpxQMQAAgAAAAIAAAACAAAAAAAEAAAAAAQEfAOH1BQAAAAAWABQ4j7lEMH63fvRRl9CwskXgefAR3iICAsd3Fh9z0LfHK57nveZQKT0T8JW8dlatH1Jdpf0uELEQRzBEAiBMsftfhpyULg4mEAV2ElQ5F5rojcqKncO6CPeVOYj6pgIgUh9JynkcJ9cOJzybFGFphZCTYeJb4nTqIA1+CIJ+UU0BIgYCx3cWH3PQt8crnue95lApPRPwlbx2Vq0fUl2l/S4QsRAYJ1acUDEAAIAAAACAAAAAgAAAAAAAAAAAAAAiAgLSDKUC7iiWhtIYFb1DqAY3sGmOH7zb5MrtRF9sGgqQ7xgnVpxQMQAAgAAAAIAAAACAAAAAAAQAAAAA')\n            for tx in (tx1, tx2):\n                self.assertEqual(1, len(tx.xpubs))\n        finally:\n            constants.BitcoinTestnet.set_as_network()\n\n    def test_valid_psbt__input_with_both_witness_utxo_and_nonwitness_utxo(self):\n        # Case: PSBT where an input has both WITNESS_UTXO and UTXO.\n        # test it does not raise\n        tx = tx_from_any(bytes.fromhex('70736274ff0100710100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0000000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d720000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a1c3914000001011f8096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a0100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000'))\n        self.assertEqual(1, len(tx.inputs()))\n\n\nclass TestInvalidPSBT(ElectrumTestCase):\n    # test cases from BIP-0174\n    TESTNET = True\n\n    def test_invalid_psbt_001(self):\n        # Case: Network transaction, not PSBT format\n        with self.assertRaises(BadHeaderMagic):\n            tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('0200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf6000000006a473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300'))\n        with self.assertRaises(BadHeaderMagic):\n            tx2 = PartialTransaction.from_raw_psbt('AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAABqRzBEAiBwsiRRI+a/R01gxbUMBD1MaRpdJDXwmjSnZiqdwlF5CgIgATKcqdrPKAvfMHQOwDkEIkIsgctFg5RXrrdvwS7dlbMBIQJlfRGNM1e44PTCzUbbezn22cONmnCry5st5dyNv+TOMf7///8C09/1BQAAAAAZdqkU0MWZA8W6woaHYOkP1SGkZlqnZSCIrADh9QUAAAAAF6kUNUXm4zuDLEcFDyTT7rk8nAOUi8eHsy4TAA==')\n\n    def test_invalid_psbt_002(self):\n        # Case: PSBT missing outputs\n        with self.assertRaises(UnexpectedEndOfStream):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000000'))\n        with self.assertRaises(UnexpectedEndOfStream):\n            tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==')\n\n    def test_invalid_psbt_003(self):\n        # Case: PSBT where one input has a filled scriptSig in the unsigned tx\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff0100fd0a010200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be4000000006a47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa88292feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac00000000000001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb82308000000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAP0KAQIAAAACqwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QAAAAAakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpL+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAABASAA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHhwEEFgAUhdE1N/LiZUBaNNuvqePdoB+4IwgAAAA=')\n\n    def test_invalid_psbt_004(self):\n        # Case: PSBT where inputs and outputs are provided but without an unsigned tx\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8AAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==')\n\n    def test_invalid_psbt_005(self):\n        # Case: PSBT with duplicate keys in an input\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000001003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQA/AgAAAAH//////////////////////////////////////////wAAAAAA/////wEAAAAAAAAAAANqAQAAAAAAAAAA')\n\n    def test_invalid_psbt_006(self):\n        # Case: PSBT With invalid global transaction typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff020001550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8CAAFVAgAAAAEnmiMjpd+1H8RfIg+liw/BPh4zQnkqhdfjbNYzO1y8OQAAAAAA/////wGgWuoLAAAAABl2qRT/6cAGEJfMO2NvLLBGD6T8Qn0rRYisAAAAAAABASCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA')\n\n    def test_invalid_psbt_007(self):\n        # Case: PSBT With invalid input witness utxo typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac000000000002010020955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAIBACCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA')\n\n    def test_invalid_psbt_008(self):\n        # Case: PSBT With invalid pubkey length for input partial signature typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87210203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIQIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYwQwIgBCS1jv+qppThVZ6lyTu/1KiQZCJAVc3wcLZ3FGlELQcCH1yOsP6mUW1guKyzOtZO3mDoeFv7OqlLmb34YVHbmpoBAQQiACB3H9GK1FlmbdSfPVZOPbxC9MhHdONgraFoFqjtSI1WgQEFR1IhA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GIQPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvVKuIgYDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYQtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==')\n\n    def test_invalid_psbt_009(self):\n        # Case: PSBT With invalid redeemscript typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a01020400220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQIEACIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA')\n\n    def test_invalid_psbt_010(self):\n        # Case: PSBT With invalid witnessscript typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d568102050047522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoECBQBHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA')\n\n    def test_invalid_psbt_011(self):\n        # Case: PSBT With invalid bip32 typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae210603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd10b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriEGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb0QtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==')\n\n    def test_invalid_psbt_012(self):\n        # Case: PSBT With invalid non-witness utxo typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f0000000000020000bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAIAALsCAAAAAarXOTEBi9JfhK5AC2iEi+CdtwbqwqwYKYur7nGrZW+LAAAAAEhHMEQCIFj2/HxqM+GzFUjUgcgmwBW9MBNarULNZ3kNq2bSrSQ7AiBKHO0mBMZzW2OT5bQWkd14sA8MWUL7n3UYVvqpOBV9ugH+////AoDw+gIAAAAAF6kUD7lGNCFpa4LIM68kHHjBfdveSTSH0PIKJwEAAAAXqRQpynT4oI+BmZQoGFyXtdhS5AY/YYdlAAAAAQfaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')\n\n    def test_invalid_psbt_013(self):\n        # Case: PSBT With invalid final scriptsig typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000020700da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAACBwDaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')\n\n    def test_invalid_psbt_014(self):\n        # Case: PSBT With invalid final script witness typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903020800da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAggA2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')\n\n    def test_invalid_psbt_015(self):\n        # Case: PSBT With invalid pubkey in output BIP 32 derivation paths typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00210203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca58710d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIQIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1PtnuylhxDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA')\n\n    def test_invalid_psbt_016(self):\n        # Case: PSBT With invalid input sighash type typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff0100730200000001301ae986e516a1ec8ac5b4bc6573d32f83b465e23ad76167d68b38e730b4dbdb0000000000ffffffff02747b01000000000017a91403aa17ae882b5d0d54b25d63104e4ffece7b9ea2876043993b0000000017a914b921b1ba6f722e4bfa83b6557a3139986a42ec8387000000000001011f00ca9a3b00000000160014d2d94b64ae08587eefc8eeb187c601e939f9037c0203000100000000010016001462e9e982fff34dd8239610316b090cd2a3b747cb000100220020876bad832f1d168015ed41232a9ea65a1815d9ef13c0ef8759f64b5b2b278a65010125512103b7ce23a01c5b4bf00a642537cdfabb315b668332867478ef51309d2bd57f8a8751ae00'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wCAwABAAAAAAEAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A')\n\n    def test_invalid_psbt_017(self):\n        # Case: PSBT With invalid output redeemScript typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff0100730200000001301ae986e516a1ec8ac5b4bc6573d32f83b465e23ad76167d68b38e730b4dbdb0000000000ffffffff02747b01000000000017a91403aa17ae882b5d0d54b25d63104e4ffece7b9ea2876043993b0000000017a914b921b1ba6f722e4bfa83b6557a3139986a42ec8387000000000001011f00ca9a3b00000000160014d2d94b64ae08587eefc8eeb187c601e939f9037c0002000016001462e9e982fff34dd8239610316b090cd2a3b747cb000100220020876bad832f1d168015ed41232a9ea65a1815d9ef13c0ef8759f64b5b2b278a65010125512103b7ce23a01c5b4bf00a642537cdfabb315b668332867478ef51309d2bd57f8a8751ae00'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAgAAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A')\n\n    def test_invalid_psbt_018(self):\n        # Case: PSBT With invalid output witnessScript typed key\n        with self.assertRaises(SerializationError):\n            tx1 = tx_from_any(bytes.fromhex('70736274ff0100730200000001301ae986e516a1ec8ac5b4bc6573d32f83b465e23ad76167d68b38e730b4dbdb0000000000ffffffff02747b01000000000017a91403aa17ae882b5d0d54b25d63104e4ffece7b9ea2876043993b0000000017a914b921b1ba6f722e4bfa83b6557a3139986a42ec8387000000000001011f00ca9a3b00000000160014d2d94b64ae08587eefc8eeb187c601e939f9037c00010016001462e9e982fff34dd8239610316b090cd2a3b747cb000100220020876bad832f1d168015ed41232a9ea65a1815d9ef13c0ef8759f64b5b2b278a6521010025512103b7ce23a01c5b4bf00a642537cdfabb315b668332867478ef51309d2bd57f8a8751ae00'))\n        with self.assertRaises(SerializationError):\n            tx2 = tx_from_any('cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAQAWABRi6emC//NN2COWEDFrCQzSo7dHywABACIAIIdrrYMvHRaAFe1BIyqeploYFdnvE8Dvh1n2S1srJ4plIQEAJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A')\n\n    def test_invalid_psbt__input_with_both_witness_utxo_and_nonwitness_utxo_that_are_inconsistent(self):\n        # Case: PSBT where an input has both WITNESS_UTXO and UTXO but which are inconsistent.\n        with self.assertRaises(PSBTInputConsistencyFailure):\n            tx = tx_from_any(bytes.fromhex('70736274ff0100710100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0000000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d720000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a1c3914000001011f8096990000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a0100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000'))\n\n\nclass TestPSBTSignerChecks(ElectrumTestCase):\n    # test cases from BIP-0174\n    TESTNET = True\n\n    @unittest.skip(\"the check this test is testing is intentionally disabled in transaction.py\")\n    def test_psbt_fails_signer_checks_001(self):\n        # Case: A Witness UTXO is provided for a non-witness input\n        with self.assertRaises(PSBTInputConsistencyFailure):\n            tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac0000000000010122d3dff505000000001976a914d48ed3110b94014cb114bd32d6f4d066dc74256b88ac0001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb8230800220202ead596687ca806043edc3de116cdf29d5e9257c196cd055cf698c8d02bf24e9910b4a6ba670000008000000080020000800022020394f62be9df19952c5587768aeb7698061ad2c4a25c894f47d8c162b4d7213d0510b4a6ba6700000080010000800200008000'))\n            for txin in tx1.inputs():\n                txin.validate_data(for_signing=True)\n        with self.assertRaises(PSBTInputConsistencyFailure):\n            tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEBItPf9QUAAAAAGXapFNSO0xELlAFMsRS9Mtb00GbcdCVriKwAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=')\n            for txin in tx2.inputs():\n                txin.validate_data(for_signing=True)\n\n    def test_psbt_fails_signer_checks_002(self):\n        # Case: redeemScript with non-witness UTXO does not match the scriptPubKey\n        with self.assertRaises(PSBTInputConsistencyFailure):\n            tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752af2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))\n        with self.assertRaises(PSBTInputConsistencyFailure):\n            tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq8iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')\n\n    def test_psbt_fails_signer_checks_003(self):\n        # Case: redeemScript with witness UTXO does not match the scriptPubKey\n        with self.assertRaises(PSBTInputConsistencyFailure):\n            tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028900010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))\n        with self.assertRaises(PSBTInputConsistencyFailure):\n            tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQABBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')\n\n    def test_psbt_fails_signer_checks_004(self):\n        # Case: witnessScript with witness UTXO does not match the redeemScript\n        with self.assertRaises(PSBTInputConsistencyFailure):\n            tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ad2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'))\n        with self.assertRaises(PSBTInputConsistencyFailure):\n            tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSrSIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=')\n\n\nclass TestPSBTComplexChecks(ElectrumTestCase):\n    # test cases from BIP-0174\n    TESTNET = True\n\n    def test_psbt_combiner_unknown_fields(self):\n        tx1 = tx_from_any(\"70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f00\")\n        tx2 = tx_from_any(\"70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00\")\n        tx1.combine_with_other_psbt(tx2)\n        self.assertEqual(\"70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00\",\n                         tx1.serialize_as_bytes().hex())\n"
  },
  {
    "path": "tests/test_simple_config.py",
    "content": "import ast\nimport sys\nimport os\nimport tempfile\nimport shutil\nfrom io import StringIO\n\nfrom electrum.simple_config import SimpleConfig, read_user_config\n\nfrom . import ElectrumTestCase\n\n\nMAX_MSG_SIZE_DEFAULT = SimpleConfig.NETWORK_MAX_INCOMING_MSG_SIZE.get_default_value()\nassert isinstance(MAX_MSG_SIZE_DEFAULT, int), MAX_MSG_SIZE_DEFAULT\n\n\nclass Test_SimpleConfig(ElectrumTestCase):\n\n    def setUp(self):\n        super(Test_SimpleConfig, self).setUp()\n        # make sure \"read_user_config\" and \"user_dir\" return a temporary directory.\n        self.electrum_dir = tempfile.mkdtemp()\n        # Do the same for the user dir to avoid overwriting the real configuration\n        # for development machines with electrum installed :)\n        self.user_dir = tempfile.mkdtemp()\n\n        self.options = {\"electrum_path\": self.electrum_dir}\n        self._saved_stdout = sys.stdout\n        self._stdout_buffer = StringIO()\n        sys.stdout = self._stdout_buffer\n\n    def tearDown(self):\n        super(Test_SimpleConfig, self).tearDown()\n        # Remove the temporary directory after each test (to make sure we don't\n        # pollute /tmp for nothing.\n        shutil.rmtree(self.electrum_dir)\n        shutil.rmtree(self.user_dir)\n\n        # Restore the \"real\" stdout\n        sys.stdout = self._saved_stdout\n\n    def test_simple_config_key_rename(self):\n        \"\"\"auto_cycle was renamed auto_connect\"\"\"\n        fake_read_user = lambda _: {\"auto_cycle\": True}\n        read_user_dir = lambda : self.user_dir\n        config = SimpleConfig(options=self.options,\n                              read_user_config_function=fake_read_user,\n                              read_user_dir_function=read_user_dir)\n        self.assertEqual(config.get(\"auto_connect\"), True)\n        self.assertEqual(config.get(\"auto_cycle\"), None)\n        fake_read_user = lambda _: {\"auto_connect\": False, \"auto_cycle\": True}\n        config = SimpleConfig(options=self.options,\n                              read_user_config_function=fake_read_user,\n                              read_user_dir_function=read_user_dir)\n        self.assertEqual(config.get(\"auto_connect\"), False)\n        self.assertEqual(config.get(\"auto_cycle\"), None)\n\n    def test_simple_config_command_line_overrides_everything(self):\n        \"\"\"Options passed by command line override all other configuration\n        sources\"\"\"\n        fake_read_user = lambda _: {\"electrum_path\": \"b\"}\n        read_user_dir = lambda : self.user_dir\n        config = SimpleConfig(options=self.options,\n                              read_user_config_function=fake_read_user,\n                              read_user_dir_function=read_user_dir)\n        self.assertEqual(self.options.get(\"electrum_path\"),\n                         config.get(\"electrum_path\"))\n\n    def test_simple_config_user_config_is_used_if_others_arent_specified(self):\n        \"\"\"If no system-wide configuration and no command-line options are\n        specified, the user configuration is used instead.\"\"\"\n        fake_read_user = lambda _: {\"electrum_path\": self.electrum_dir}\n        read_user_dir = lambda : self.user_dir\n        config = SimpleConfig(options={},\n                              read_user_config_function=fake_read_user,\n                              read_user_dir_function=read_user_dir)\n        self.assertEqual(self.options.get(\"electrum_path\"),\n                         config.get(\"electrum_path\"))\n\n    def test_cannot_set_options_passed_by_command_line(self):\n        fake_read_user = lambda _: {\"electrum_path\": \"b\"}\n        read_user_dir = lambda : self.user_dir\n        config = SimpleConfig(options=self.options,\n                              read_user_config_function=fake_read_user,\n                              read_user_dir_function=read_user_dir)\n        config.set_key(\"electrum_path\", \"c\")\n        self.assertEqual(self.options.get(\"electrum_path\"),\n                         config.get(\"electrum_path\"))\n\n    def test_can_set_options_set_in_user_config(self):\n        another_path = tempfile.mkdtemp()\n        fake_read_user = lambda _: {\"electrum_path\": self.electrum_dir}\n        read_user_dir = lambda : self.user_dir\n        config = SimpleConfig(options={},\n                              read_user_config_function=fake_read_user,\n                              read_user_dir_function=read_user_dir)\n        config.set_key(\"electrum_path\", another_path)\n        self.assertEqual(another_path, config.get(\"electrum_path\"))\n\n    def test_user_config_is_not_written_with_read_only_config(self):\n        \"\"\"The user config does not contain command-line options when saved.\"\"\"\n        fake_read_user = lambda _: {\"something\": \"a\"}\n        read_user_dir = lambda : self.user_dir\n        self.options.update({\"something\": \"c\"})\n        config = SimpleConfig(options=self.options,\n                              read_user_config_function=fake_read_user,\n                              read_user_dir_function=read_user_dir)\n        config.save_user_config()\n        contents = None\n        with open(os.path.join(self.electrum_dir, \"config\"), \"r\") as f:\n            contents = f.read()\n        result = ast.literal_eval(contents)\n        result.pop('config_version', None)\n        self.assertEqual({\"something\": \"a\"}, result)\n\n    def test_configvars_set_and_get(self):\n        config = SimpleConfig(self.options)\n        self.assertEqual(\"server\", config.cv.NETWORK_SERVER.key())\n\n        def _set_via_assignment():\n            config.NETWORK_SERVER = \"example.com:443:s\"\n\n        for f in (\n            lambda: config.set_key(\"server\", \"example.com:443:s\"),\n            _set_via_assignment,\n            lambda: config.cv.NETWORK_SERVER.set(\"example.com:443:s\"),\n        ):\n            self.assertTrue(config.get(\"server\") is None)\n            self.assertTrue(config.NETWORK_SERVER is None)\n            self.assertTrue(config.cv.NETWORK_SERVER.get() is None)\n            f()\n            self.assertEqual(\"example.com:443:s\", config.get(\"server\"))\n            self.assertEqual(\"example.com:443:s\", config.NETWORK_SERVER)\n            self.assertEqual(\"example.com:443:s\", config.cv.NETWORK_SERVER.get())\n            # revert:\n            config.NETWORK_SERVER = None\n\n    def test_configvars_setter_catches_typo(self):\n        config = SimpleConfig(self.options)\n        assert not hasattr(config, \"NETORK_AUTO_CONNECTT\")\n        with self.assertRaises(AttributeError):\n            config.NETORK_AUTO_CONNECTT = False\n\n    def test_configvars_get_default_value(self):\n        config = SimpleConfig(self.options)\n        self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.get_default_value())\n        self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE)\n\n        config.NETWORK_MAX_INCOMING_MSG_SIZE = 5_555_555\n        self.assertEqual(5_555_555, config.NETWORK_MAX_INCOMING_MSG_SIZE)\n        self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.get_default_value())\n\n        config.NETWORK_MAX_INCOMING_MSG_SIZE = None\n        self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE)\n\n    def test_configvars_convert_getter(self):\n        config = SimpleConfig(self.options)\n        self.assertEqual(None, config.NETWORK_PROXY)\n        config.user_config[config.cv.NETWORK_PROXY.key()] = None\n        self.assertEqual(\"none\", config.NETWORK_PROXY)\n        config.NETWORK_PROXY = None\n        self.assertEqual(None, config.NETWORK_PROXY)\n\n    def test_configvars_convert_setter(self):\n        config = SimpleConfig(self.options)\n        self.assertEqual(60, config.CLI_TIMEOUT)\n        assert isinstance(config.CLI_TIMEOUT, float)\n\n        config.CLI_TIMEOUT = 10\n        self.assertEqual(10, config.CLI_TIMEOUT)\n        assert isinstance(config.CLI_TIMEOUT, float)\n\n        config.CLI_TIMEOUT = None\n        self.assertEqual(60, config.CLI_TIMEOUT)\n        assert isinstance(config.CLI_TIMEOUT, float)\n\n    def test_configvars_is_set(self):\n        config = SimpleConfig(self.options)\n        self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE)\n        self.assertFalse(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set())\n\n        config.NETWORK_MAX_INCOMING_MSG_SIZE = 5_555_555\n        self.assertTrue(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set())\n\n        config.NETWORK_MAX_INCOMING_MSG_SIZE = None\n        self.assertFalse(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set())\n        self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE)\n\n        config.NETWORK_MAX_INCOMING_MSG_SIZE = MAX_MSG_SIZE_DEFAULT\n        self.assertTrue(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set())\n        self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE)\n\n    def test_configvars_is_modifiable(self):\n        config = SimpleConfig({**self.options, \"server\": \"example.com:443:s\"})\n\n        self.assertFalse(config.is_modifiable(\"server\"))\n        self.assertFalse(config.cv.NETWORK_SERVER.is_modifiable())\n\n        config.NETWORK_SERVER = \"other-example.com:80:t\"\n        self.assertEqual(\"example.com:443:s\", config.NETWORK_SERVER)\n\n        self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE)\n        self.assertTrue(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_modifiable())\n        config.NETWORK_MAX_INCOMING_MSG_SIZE = 5_555_555\n        self.assertEqual(5_555_555, config.NETWORK_MAX_INCOMING_MSG_SIZE)\n\n        config.make_key_not_modifiable(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE)\n        self.assertFalse(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_modifiable())\n        config.NETWORK_MAX_INCOMING_MSG_SIZE = 2_222_222\n        self.assertEqual(5_555_555, config.NETWORK_MAX_INCOMING_MSG_SIZE)\n\n    def test_configvars_from_key(self):\n        config = SimpleConfig(self.options)\n        self.assertEqual(config.cv.NETWORK_SERVER, config.cv.from_key(\"server\"))\n        with self.assertRaises(KeyError):\n            config.cv.from_key(\"server333\")\n\n    def test_recursive_config(self):\n        config = SimpleConfig(self.options)\n        n = len(config.user_config)\n        config.set_key('x.y.z', 1)\n        self.assertEqual(len(config.user_config), n + 1)\n        config.set_key('x.y.w', 1)\n        self.assertEqual(len(config.user_config), n + 1)\n        config.set_key('x.y.z', None)\n        self.assertEqual(len(config.user_config), n + 1)\n        config.set_key('x.y.w', None)\n        self.assertEqual(len(config.user_config), n)\n\n\nclass TestUserConfig(ElectrumTestCase):\n\n    def setUp(self):\n        super(TestUserConfig, self).setUp()\n        self._saved_stdout = sys.stdout\n        self._stdout_buffer = StringIO()\n        sys.stdout = self._stdout_buffer\n\n        self.user_dir = tempfile.mkdtemp()\n\n    def tearDown(self):\n        super(TestUserConfig, self).tearDown()\n        shutil.rmtree(self.user_dir)\n        sys.stdout = self._saved_stdout\n\n    def test_no_path_means_no_result(self):\n       result = read_user_config(None)\n       self.assertEqual({}, result)\n\n    def test_path_without_config_file(self):\n        \"\"\"We pass a path but if does not contain a \"config\" file.\"\"\"\n        result = read_user_config(self.user_dir)\n        self.assertEqual({}, result)\n\n    def test_path_with_reprd_object(self):\n\n        class something(object):\n            pass\n\n        thefile = os.path.join(self.user_dir, \"config\")\n        payload = something()\n        with open(thefile, \"w\") as f:\n            f.write(repr(payload))\n\n        with self.assertRaises(ValueError):\n            read_user_config(self.user_dir)\n"
  },
  {
    "path": "tests/test_storage_upgrade/client_1_9_8_seeded",
    "content": "{'addr_history':{'177hEYTccmuYH8u68pYfaLteTxwJrVgvJj':[],'15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc':[],'1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf':[],'1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs':[],'1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC':[],'1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm':[],'1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj':[],'1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa':[]},'accounts_expanded':{},'master_public_key':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb','use_encryption':False,'seed':'2605aafe50a45bdf2eb155302437e678','accounts':{0:{0:['1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC','1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj','177hEYTccmuYH8u68pYfaLteTxwJrVgvJj','1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm','15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc'],1:['1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs','1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa','1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf']}},'seed_version':4}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_0_4_importedkeys",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\": [\n                    \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\",\n                    \"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\"\n                ],\n                \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\": [\n                    \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\",\n                    \"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"\n                ],\n                \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\": [\n                    \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\",\n                    \"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\"\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"imported\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_0_4_multisig",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                [\n                    \"03c3a8549f35d7842192e7e00afa25ef1c779d05f1c891ba7c30de968fb29e3e78\",\n                    \"02e191e105bccf1b4562d216684632b9ec22c87e1457b537eb27516afa75c56831\"\n                ],\n                [\n                    \"03793397f02b3bd3d0f6f0dafc7d42b9701234a269805d89efbbc2181683368e4b\",\n                    \"02153705b8e4df41dc9d58bc0360c79a9209b3fc289ec54118f0b149d5a3b3546d\"\n                ],\n                [\n                    \"02511e8cfb39c8ce1c790f26bcab68ba5d5f79845ec1c6a92b0ac9f331648d866a\",\n                    \"02c29c1ea70e23d866204a11ec8d8ecd70d6f51f58dd8722824cacb1985d4d1870\"\n                ]\n            ],\n            \"receiving\": [\n                [\n                    \"0283ce4f0f12811e1b27438a3edb784aeb600ca7f4769c9c49c3e704e216421d3e\",\n                    \"03a1bbada7401cade3b25a23e354186c772e2ba4ac0d9c0447627f7d540eb9891d\"\n                ],\n                [\n                    \"0286b45a0bcaa215716cbc59a22b6b1910f9ebad5884f26f55c2bb38943ee8fdb6\",\n                    \"02799680336c6bd19005588fad12256223cb8a416649d60ea5d164860c0872b931\"\n                ],\n                [\n                    \"039e2bf377709e41bba49fb1f3f873b9b87d50ae3b574604cd9b96402211ea1f36\",\n                    \"02ef9ceaaf754ba46f015e1d704f1a06157cc4441da0cfaf096563b22ec225ca5f\"\n                ],\n                [\n                    \"025220baaca5bff1a5ffbf4d36e9fcc6f5d05f4af750ef29f6d88d9b5f95fef79a\",\n                    \"02350c81bebfa3a894df69302a6601175731d443948a12d8ec7860981988e3803e\"\n                ],\n                [\n                    \"028fd6411534d722b625482659de54dd609f5b5c935ae8885ca24bfd3266210527\",\n                    \"03b9c7780575f17e64f9dfd5947945b1dbdb65aecef562ac076335fd7aa09844e4\"\n                ],\n                [\n                    \"0353066065985ec06dbef33e7a081d9240023891a51c4e9eda7b3eb1b4af165e04\",\n                    \"028c3fa7622e4c8bac07a2c549885a045532e67a934ca10e20729d0fdfe3a75339\"\n                ],\n                [\n                    \"02253b4eabf2834af86b409d5ca8e671de9a75c3937bff2dac9521c377ca195668\",\n                    \"02d5e83c445684eb502049f48e621e1ca16e07e5dc4013c84d661379635f58877b\"\n                ],\n                [\n                    \"030d38e4c7a5c7c9551adcace3b70dcaa02bf841febd6dc308f3abd7b7bf2bdc49\",\n                    \"0375a0b50cd7f3af51550207a766c5db326b2294f5a4b456a90190e4fbeb720d97\"\n                ],\n                [\n                    \"0327280215ba4a0d8c404085c4f6091906a9e1ada7ce4202a640ac701446095954\",\n                    \"037cd9b5e6664d28a61e01626056cdb7e008815b365c8b65fa50ac44d6c1ad126e\"\n                ],\n                [\n                    \"02f80a80146674da828fc67a062d1ab47fb0714cf40ec5c517ee23ea71d3033474\",\n                    \"03fd8ab9bc9458b87e0b7b2a46ea6b46de0a5f6ecaf1a204579698bfa881ff93ce\"\n                ],\n                [\n                    \"034965bd56c6ca97e0e5ffa79cdc1f15772fa625b76da84cc8adb1707e2e101775\",\n                    \"033e13cb19d930025bfc801b829e64d12934f9f19df718f4ea6160a4fb61320a9c\"\n                ],\n                [\n                    \"034de271009a06d733de22601c3d3c6fe8b3ec5a44f49094ac002dc1c90a3b096d\",\n                    \"023f0b2f653c0fdbdc292040fee363ceaa5828cfd8e012abcf6cd9bad2eaa3dc72\"\n                ],\n                [\n                    \"022aec8931c5b17bdcdd6637db34718db6f267cb0a55a611eb6602e15deb6ed4df\",\n                    \"021de5d4bbb73b6dfab2c0df6970862b08130902ff3160f31681f34aecf39721f6\"\n                ],\n                [\n                    \"02a0e3b52293ec73f89174ff6e5082fcfebc45f2fdd9cfe12a6981aa120a7c1fa7\",\n                    \"0371d41b5f18e8e1990043c1e52f998937bc7e81b8ace4ddfc5cd0d029e4c81894\"\n                ],\n                [\n                    \"030bc1cbe4d750067254510148e3af9bc84925cdd17db3b54d9bbf4a409b83719a\",\n                    \"0371c4800364a8a32bfbda7ea7724c1f5bdbd794df8a6080a3bd3b52c52cf32402\"\n                ],\n                [\n                    \"0318c5cd5f19ff037e3dec3ce5ac1a48026f5a58c4129271b12ae22f8542bcd718\",\n                    \"03b5c70db71d520d04f810742e7a5f42d810e94ec6cbf4b48fa6dd7b4d425e76c1\"\n                ],\n                [\n                    \"0213f68b86a8c4a0840fa88d9a06904c59292ec50172813b8cca62768f3b708811\",\n                    \"0353037209eb400ba7fcfa9f296a8b2745e1bbcbfb28c4adebf74de2e0e6a58c00\"\n                ],\n                [\n                    \"028decff8a7f5a7982402d95b050fbc9958e449f154990bbfe0f553a1d4882fd03\",\n                    \"025ecd14812876e885d8f54cab30d1c2a8ae6c6ed0847e96abd65a3700148d94e2\"\n                ],\n                [\n                    \"0267f8dab8fdc1df4231414f31cfeb58ce96f3471ba78328cd429263d151c81fed\",\n                    \"03e0d01df1fd9e958a7324d29afefbc76793a40447a2625c494355c577727d69ba\"\n                ],\n                [\n                    \"03de3c4d173b27cdfdd8e56fbf3cd6ee8729b94209c20e5558ddd7a76281a37e2e\",\n                    \"0218ccb595d7fa559f0bae1ea76d19526980b027fb9be009b6b486d8f8eb0e00d5\"\n                ]\n            ],\n            \"xpub\": \"xpub661MyMwAqRbcFUEYv1psxyPnjiHhTYe85AwFRs5jShbpgrfQ9UXBmxantqgGT3oAVLiHDYoR3ruT3xRGcxsmBMJxyg94FGcxF86QnzYDc6e\",\n            \"xpub2\": \"xpub661MyMwAqRbcGFd5DccFn4YW2HEdPhVZ2NEBAn416bvDFBi8HN5udmB6DkWpuXFtXaXZdq9UvMoiHxaauk6R1CZgKUR8vpng4LoudP4YVXA\"\n        }\n    },\n    \"master_private_keys\": {\n        \"x1/\": \"xprv9s21ZrQH143K2zA5ozHsbqT4BgTD45vGhx1edUg7tN4qp4LFbwCwEAGK3ZVaBaCRQnuy7AJ7qbPGxKiynNtGd7CzjBXEV4mEwStnPo98Xve\"\n    },\n    \"master_public_keys\": {\n        \"x1/\": \"xpub661MyMwAqRbcFUEYv1psxyPnjiHhTYe85AwFRs5jShbpgrfQ9UXBmxantqgGT3oAVLiHDYoR3ruT3xRGcxsmBMJxyg94FGcxF86QnzYDc6e\",\n        \"x2/\": \"xpub661MyMwAqRbcGFd5DccFn4YW2HEdPhVZ2NEBAn416bvDFBi8HN5udmB6DkWpuXFtXaXZdq9UvMoiHxaauk6R1CZgKUR8vpng4LoudP4YVXA\"\n    },\n    \"seed\": \"start accuse bounce inhale crucial infant october radar enforce stage dumb spot account\",\n    \"seed_version\": 11,\n    \"use_encryption\": false,\n    \"wallet_type\": \"2of2\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_0_4_seeded",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"03d8e267e8de7769b52a8727585b3c44b4e148b86b2c90e3393f78a75bd6aab83f\",\n                \"03f09b3562bec870b4eb8626c20d449ee85ef17ea896a6a82b454e092eef91b296\",\n                \"02df953880df9284715e8199254edcf3708c635adc92a90dbf97fbd64d1eb88a36\"\n            ],\n            \"receiving\": [\n                \"02cd4d73d5e335dafbf5c9338f88ceea3d7511ab0f9b8910745ac940ff40913a30\",\n                \"0243ed44278a178101e0fb14d36b68e6e13d00fe3434edb56e4504ea6f5db2e467\",\n                \"0367c0aa3681ec3635078f79f8c78aa339f19e38d9e1c9e2853e30e66ade02cac3\",\n                \"0237d0fe142cff9d254a3bdd3254f0d5f72676b0099ba799764a993a0d0ba80111\",\n                \"020a899fd417527b3929c8f625c93b45392244bab69ff91b582ed131977d5cd91e\",\n                \"039e84264920c716909b88700ef380336612f48237b70179d0b523784de28101f7\",\n                \"03125452df109a51be51fe21e71c3a4b0bba900c9c0b8d29b4ee2927b51f570848\",\n                \"0291fa554217090bab96eeff63e1c6fdec37358ed597d18fa32c60c02a48878c8c\",\n                \"030b6354a4365bab55e86269fb76241fd69716f02090ead389e1fce13d474aa569\",\n                \"023dcba431d8887ab63595f0df1e978e4a5f1c3aac6670e43d03956448a229f740\",\n                \"0332a61cbe04fe027033369ce7569b860c24462878bdd8c0332c22a3f5fdcc1790\",\n                \"021249480422d93dba2aafcd4575e6f630c4e3a2a832dd8a15f884e1052b6836e4\",\n                \"02516e91dede15d3a15dd648591bb92e107b3a53d5bc34b286ab389ce1af3130aa\",\n                \"02e1da3dddd81fa6e4895816da9d4b8ab076d6ea8034b1175169c0f247f002f4cf\",\n                \"0390ef1e3fdbe137767f8b5abad0088b105eee8c39e075305545d405be3154757a\",\n                \"03fca30eb33c6e1ffa071d204ccae3060680856ae9b93f31f13dd11455e67ee85d\",\n                \"034f6efdbbe1bfa06b32db97f16ff3a0dd6cf92769e8d9795c465ff76d2fbcb794\",\n                \"021e2901009954f23d2bf3429d4a531c8ca3f68e9598687ef816f20da08ff53848\",\n                \"02d3ccf598939ff7919ee23d828d229f85e3e58842582bf054491c59c8b974aa6e\",\n                \"03a1daffa39f42c1aaae24b859773a170905c6ee8a6dab8c1bfbfc93f09b88f4db\"\n            ],\n            \"xpub\": \"xpub661MyMwAqRbcFsrzES8RWNiD7RxDqT4p8NjvTY9mLi8xdphQ9x1TiY8GnqCpQx4LqJBdcGeXrsAa2b2G7ZcjJcest9wHcqYfTqXmQja6vfV\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"master_private_keys\": {\n        \"x/\": \"xprv9s21ZrQH143K3PnX8QbR9EmUZQ7jRzLxm9pKf9k9nNbym2NFcQhDAjonwZ39jtWLYp6qk5UHotj13p2y7w1ZhhvvyV5eCcaPUrKofs9CXQ9\"\n    },\n    \"master_public_keys\": {\n        \"x/\": \"xpub661MyMwAqRbcFsrzES8RWNiD7RxDqT4p8NjvTY9mLi8xdphQ9x1TiY8GnqCpQx4LqJBdcGeXrsAa2b2G7ZcjJcest9wHcqYfTqXmQja6vfV\"\n    },\n    \"seed\": \"seven direct thunder glare prevent please fatal blush buzz artefact gate vendor above\",\n    \"seed_version\": 11,\n    \"use_encryption\": false,\n    \"wallet_type\": \"standard\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_0_4_trezor_multiacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902\",\n                \"03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740\",\n                \"028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee\"\n            ],\n            \"receiving\": [\n                \"03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0\",\n                \"024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97\",\n                \"03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b\",\n                \"028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136\",\n                \"02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5\",\n                \"02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4\",\n                \"023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53\",\n                \"02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4\",\n                \"029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92\",\n                \"02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066\",\n                \"0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447\",\n                \"0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea\",\n                \"02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027\",\n                \"0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50\",\n                \"03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459\",\n                \"0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c\",\n                \"028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804\",\n                \"03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736\",\n                \"029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f\",\n                \"02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\"\n        },\n        \"1\": {\n            \"change\": [\n                \"03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34\",\n                \"0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e\",\n                \"036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8\"\n            ],\n            \"receiving\": [\n                \"02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58\",\n                \"039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9\",\n                \"0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec\",\n                \"02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604\",\n                \"0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f\",\n                \"0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6\",\n                \"03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd\",\n                \"03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00\",\n                \"028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3\",\n                \"02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85\",\n                \"03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898\",\n                \"021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4\",\n                \"03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f\",\n                \"0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff\",\n                \"03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7\",\n                \"02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec\",\n                \"0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a\",\n                \"024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6\",\n                \"026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089\",\n                \"02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986\",\n                \"03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {\n        \"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi\": [\n            [\n                \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\",\n                490002\n            ]\n        ]\n    },\n    \"labels\": {\n        \"0\": \"Main account\",\n        \"1\": \"acc1\"\n    },\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\",\n        \"x/1'\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\",\n        \"x/2'\": \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\"\n    },\n    \"next_account2\": [\n        \"2\",\n        \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\",\n        \"031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e\",\n        \"1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ\"\n    ],\n    \"transactions\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": \"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000\"\n    },\n    \"verified_tx3\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": [\n            490002,\n            1508090436,\n            607\n        ]\n    },\n    \"wallet_type\": \"trezor\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_0_4_trezor_singleacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80\",\n                \"0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890\",\n                \"038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1\"\n            ],\n            \"receiving\": [\n                \"020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae\",\n                \"03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74\",\n                \"03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046\",\n                \"02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f\",\n                \"031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d\",\n                \"03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717\",\n                \"0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631\",\n                \"035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f\",\n                \"02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be\",\n                \"026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5\",\n                \"0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457\",\n                \"03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429\",\n                \"028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a\",\n                \"03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4\",\n                \"0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482\",\n                \"02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f\",\n                \"02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31\",\n                \"02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1\",\n                \"034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f\",\n                \"032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c\"\n            ],\n            \"xpub\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"labels\": {\n        \"0\": \"Main account\"\n    },\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\",\n        \"x/1'\": \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\"\n    },\n    \"next_account2\": [\n        \"1\",\n        \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\",\n        \"03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b\",\n        \"18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG\"\n    ],\n    \"wallet_type\": \"trezor\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_0_4_watchaddresses",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\": [\n                    null,\n                    null\n                ],\n                \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\": [\n                    null,\n                    null\n                ],\n                \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\": [\n                    null,\n                    null\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"wallet_type\": \"imported\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_1_1_importedkeys",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\": [\n                    \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\",\n                    \"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\"\n                ],\n                \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\": [\n                    \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\",\n                    \"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"\n                ],\n                \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\": [\n                    \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\",\n                    \"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\"\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"pruned_txo\": {},\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"imported\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_1_1_multisig",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                [\n                    \"03b5ca15f87baa1bb9d2508a9cf7cb596915a2749a6932bd71a5f353d72e2ff51e\",\n                    \"03069d12bb7dc9fe7b8dab9ab2c7828173a4a4a5bacb10b9004854aef2ada2e440\"\n                ],\n                [\n                    \"036d7aeef82d50520f7d30d20a6b58a5e61c40949af4c147a105a8724478ba6339\",\n                    \"021208a4a6c76934fbc2eed72a4a71713a5a093fb203ec3197edd1e4be8d9fb342\"\n                ],\n                [\n                    \"03ee5bd2bc7f9800b85f6f0a3fe8c23c797fa90d832f0332dfc72532e298dce54e\",\n                    \"03474b76f33036673e1df73800b06d2df4b3617768c2b6a4f8a7f7d17c2b08cec3\"\n                ]\n            ],\n            \"receiving\": [\n                [\n                    \"0288d4cc7e83b7028b8d2197c4efb490cb3dd248ee8683c715d9c59eb1884b2696\",\n                    \"02c8ffee4ef168237f4a303dfe4957e328a8163c827cbe8ad07dcc24304b343869\"\n                ],\n                [\n                    \"022770e608e45981a31bad39a747a827ff4ce1eb28348fbe29ab776bdbf39346b4\",\n                    \"03ebd247971aced7e2f49c495658ac5c32f764ebc4df5d033505e665f8d3f87b56\"\n                ],\n                [\n                    \"0256ede358326a99878d9de6c2c6a156548c266195fecea7906ddbb170da740f8d\",\n                    \"02a500e7438d672c374713a9179fef03cbf075dd4c854566d6d9f4d899c01a4cf4\"\n                ],\n                [\n                    \"03fe2f59f10f6703bd3a43d0ae665ab72fb8b73b14f3a389b92e735e825fffdbe9\",\n                    \"0255dd91624ba62481e432b9575729757b046501b8310b1dee915df6c4472f7979\"\n                ],\n                [\n                    \"0262c7c02f83196f6e3b9dd29e1bcad4834891b69ece12f628eea4379af6e701f8\",\n                    \"0319ce2894fdf42bc87d45167a64b24ee2acdb5d45b6e4aadce4154a1479c8c58a\"\n                ],\n                [\n                    \"03bfb9ca9edab6650a908ffdcc0514f784aaccac466ba26c15340bc89a158d0b4c\",\n                    \"03bcce80eed7b494f793b38b55cc25ae62e462ec7bf4d8ff6e4d583e8d04a4ac6d\"\n                ],\n                [\n                    \"0301dc9a41a44189e40c786048a0b6c13cc8865f3674fdf8e6cb2ab041eb71c0c7\",\n                    \"020ded564880e7298068cf1498efcfb0f2306c6003e3de09f89030477ff7d02e18\"\n                ],\n                [\n                    \"03baffd970ecba170c31f48a95694a1063d14c834ccf2fdce0df46c3b81ab8edfb\",\n                    \"0243ec650fc7c6642f7fb3b98e1df62f8b28b2e8722e79ccb271badba3545e8fc2\"\n                ],\n                [\n                    \"024be204a4bd321a727fb4a427189ae2f761f2a2c9898e9c37072e8a01026736d4\",\n                    \"0239dc233c3e9e7c32287fdd7932c248650a36d8ab033875d272281297fadf292a\"\n                ],\n                [\n                    \"02197190b214c0215511d17e54e3e82cbe09f08e5ba2fb47aeafe01d8a88a8cb25\",\n                    \"034a13cf01e26e9aa574f9ba37e75f6df260958154b0f6425e0242eacd5a3979c5\"\n                ],\n                [\n                    \"0226660fce4351019be974959b6b7dcf18d5aa280c6315af362ab60374b5283746\",\n                    \"0304e49d2337a529ed8a647eceb555cd82e7e2546073568e30254530a61c174100\"\n                ],\n                [\n                    \"0324bb7d892dbe30930eb8de4b021f6d5d7e7da0c4ac9e3b95e1a2c684258d5d6c\",\n                    \"02487aa272f0d3a86358064e080daf209ee501654e083f0917ad2aff3bbeb43424\"\n                ],\n                [\n                    \"03678b52056416da4baa8d51dca8eea534e38bd1d9328c8d01d5774c7107a0f9c1\",\n                    \"0331deff043d709fc8171e08625a9adffba1bb614417b589a206c3a80eff86eddd\"\n                ],\n                [\n                    \"023a94d91c08c8c574199bc16e12789630c97cb990aeb5a54d938ff3c86786aabf\",\n                    \"02d139837e34858f733e7e1b7d61b51d2730c57c274ed644ab80aff6e9e2fdef73\"\n                ],\n                [\n                    \"032f92dc11020035cd16995cfdc4bc6bef92bc4a06eb70c43474e6f7a782c9c0e1\",\n                    \"0307d2c32713f010a0d0186e47670c6e46d7a7e623026f9ed99eb27cdae2ae4b49\"\n                ],\n                [\n                    \"02f66a91a024628d6f6969af2ed9ded087a88e9be86e4b3e5830868643244ec1ae\",\n                    \"02f2a83ebb1fbbd04e59a93284e35320c74347176c0592512411a15efa7bf5fa44\"\n                ],\n                [\n                    \"03585bae6f04f2d3f927d79321b819cccf2bcd1d28d616aac9407c6c13d590dfbd\",\n                    \"021f48f02b485b9b3223fca4fbc4dd823a8151053b8640b3766c37dfa99ba78006\"\n                ],\n                [\n                    \"02b28e2d6f1ac3fde4b34c938e83c0ef0d85fd540d8c33b33a109f4ebbc4a36a4d\",\n                    \"030a25a960e28e751a95d3c0167fad496f9ec4bc307637c69b3bd6682930532736\"\n                ],\n                [\n                    \"03782c0dee8d279c547d26853e31d90bc7d098e16015c2cc334f2cc2a2964f2118\",\n                    \"021fe4d6392dba40f1aa35fa9ec3ebfde710423f036482f6a5b3c47d0e149dfe47\"\n                ],\n                [\n                    \"0379b464b4f9cced0c71ee66c4fca1e61190bac9a6294242aabd4108f6a986a029\",\n                    \"030a5802c5997ebae590147cb5eeba1690455c5d2a87306345586e808167072b50\"\n                ]\n            ],\n            \"xpub\": \"xpub661MyMwAqRbcErzzVC45mcZaZM7gpxh4iwfsQVuyTma3qpWuRi9ZRdL8ACqu25LP2jssmKmpEbnGohH9XnoZ1etW3TKaiy5dWnUuiN6LvD9\",\n            \"xpub2\": \"xpub661MyMwAqRbcH4DqLo2tRYzSnnqjXk21aqNz3oAuYkr66YxucWgc2X8oLdS2KLPSKqrfZwStQYEpUp5jVxQfTBmEwtw3JaPRf6mq6JLD3Qr\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"master_private_keys\": {\n        \"x1/\": \"xprv9s21ZrQH143K2NvXPAX5QUcr1KHCRVyDMikGc7WMuS34y2BktAqJsq1eJvk7JWroKM8PdGa2FHWiTpAvH9nj6BkQos5XhJU5mfS12tdtBYy\"\n    },\n    \"master_public_keys\": {\n        \"x1/\": \"xpub661MyMwAqRbcErzzVC45mcZaZM7gpxh4iwfsQVuyTma3qpWuRi9ZRdL8ACqu25LP2jssmKmpEbnGohH9XnoZ1etW3TKaiy5dWnUuiN6LvD9\",\n        \"x2/\": \"xpub661MyMwAqRbcH4DqLo2tRYzSnnqjXk21aqNz3oAuYkr66YxucWgc2X8oLdS2KLPSKqrfZwStQYEpUp5jVxQfTBmEwtw3JaPRf6mq6JLD3Qr\"\n    },\n    \"pruned_txo\": {},\n    \"seed\": \"snack oxygen clock very envelope staff table bus sense fiscal cereal pilot abuse\",\n    \"seed_version\": 11,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"2of2\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_1_1_seeded",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"03cbd39265f007d39045ccab5833e1ae16c357f9d35e67099d8e41940bf63ec330\",\n                \"03c94e9590d9bcd579caae15d062053e2820fe2a405c153dd4dca4618b7172ea6f\",\n                \"028a875b6f7e56f8cba66a1cec5dc1dfca9df79b7c92702d0a551c6c1b49d0f59b\"\n            ],\n            \"receiving\": [\n                \"02fa100994f912df3e9538c244856828531f84e707f4d9eccfdd312c2e3ef7cf10\",\n                \"02fe230740aa27ace4f4b2e8b330cd57792051acf03652ae1622704d7eb7d4e5e4\",\n                \"03e3f65a991f417d69a732e040090c8c2f18baf09c3a9dc8aa465949aeb0b3271f\",\n                \"0382aa34a9cb568b14ebae35e69b3be6462d9ed8f30d48e0a6983e5af74fa441d3\",\n                \"03dfd8638e751e48fd42bf020874f49fbb5f54e96eff67d72eeeda3aa2f84f01c6\",\n                \"033904139de555bdf978e45931702c27837312ed726736eeff340ca6e0a439d232\",\n                \"03c6ca845d5bd9055f8889edcd53506cf714ac1042d9e059db630ec7e1af34133d\",\n                \"030b3bafc8a4ff8822951d4983f65b9bc43552c8181937188ba8c26e4c1d1be3ab\",\n                \"03828c371d3984ca5a248997a3e096ce21f9aeeb2f2a16457784b92a55e2aef288\",\n                \"033f42b4fbc434a587f6c6a0d10ac401f831a77c9e68453502a50fe278b6d9265c\",\n                \"0384e2c23268e2eb88c674c860519217af42fd6816273b299f0a6c39ddcc05bfa2\",\n                \"0257c60adde9edca8c14b6dd804004abc66bac17cc2acbb0490fcab8793289b921\",\n                \"02e2a67b1618a3a449f45296ea72a8fa9d8be6c58759d11d038c2fe034981efa73\",\n                \"02a9ef53a502b3a38c2849b130e2b20de9e89b023274463ea1a706ed92719724eb\",\n                \"037fc8802a11ba7ef06682908c24bcaedca1e2240111a1dd229bf713e2aa1d65a1\",\n                \"03ea0685fbd134545869234d1f219fff951bc3ec9e3e7e41d8b90283cd3f445470\",\n                \"0296bbe06cdee522b6ee654cc3592fce1795e9ff4dc0e2e2dea8acaf6d2d6b953b\",\n                \"036beac563bc85f9bc479a15d1937ea8e2c20637825a134c01d257d43addab217a\",\n                \"03389a4a6139de61a2e0e966b07d7b25b0c5f3721bf6fdcad20e7ae11974425bd9\",\n                \"026cffa2321319433518d75520c3a852542e0fa8b95e2cf4af92932a7c48ee9dbd\"\n            ],\n            \"xpub\": \"xpub661MyMwAqRbcGDxKhL5YS1kaB5B7q8H6xPZwCrgZ1iE2XXaiUeqD9MFEYRAuX7UNfdAED9yhAZdCB4ZS8dFrGDVU3x9ZK8uej8u8Pa2DLMq\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"master_private_keys\": {\n        \"x/\": \"xprv9s21ZrQH143K3jsrbJYY4soqd3LdRfZFbAeLQUGwTNh3ejFZw7WxbYvkhAmPM88Swt1JwFX6DVGjPXeUcGcqa1XFuJPeiQaC9wiZ16PTKgQ\"\n    },\n    \"master_public_keys\": {\n        \"x/\": \"xpub661MyMwAqRbcGDxKhL5YS1kaB5B7q8H6xPZwCrgZ1iE2XXaiUeqD9MFEYRAuX7UNfdAED9yhAZdCB4ZS8dFrGDVU3x9ZK8uej8u8Pa2DLMq\"\n    },\n    \"pruned_txo\": {},\n    \"seed\": \"flat toe story egg tide casino leave liquid strike cat busy knife absorb\",\n    \"seed_version\": 11,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"standard\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_1_1_trezor_multiacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902\",\n                \"03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740\",\n                \"028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee\"\n            ],\n            \"receiving\": [\n                \"03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0\",\n                \"024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97\",\n                \"03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b\",\n                \"028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136\",\n                \"02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5\",\n                \"02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4\",\n                \"023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53\",\n                \"02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4\",\n                \"029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92\",\n                \"02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066\",\n                \"0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447\",\n                \"0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea\",\n                \"02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027\",\n                \"0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50\",\n                \"03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459\",\n                \"0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c\",\n                \"028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804\",\n                \"03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736\",\n                \"029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f\",\n                \"02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\"\n        },\n        \"1\": {\n            \"change\": [\n                \"03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34\",\n                \"0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e\",\n                \"036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8\"\n            ],\n            \"receiving\": [\n                \"02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58\",\n                \"039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9\",\n                \"0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec\",\n                \"02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604\",\n                \"0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f\",\n                \"0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6\",\n                \"03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd\",\n                \"03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00\",\n                \"028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3\",\n                \"02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85\",\n                \"03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898\",\n                \"021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4\",\n                \"03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f\",\n                \"0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff\",\n                \"03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7\",\n                \"02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec\",\n                \"0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a\",\n                \"024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6\",\n                \"026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089\",\n                \"02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986\",\n                \"03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {\n        \"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC\": [],\n        \"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi\": [\n            [\n                \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\",\n                490002\n            ]\n        ],\n        \"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz\": [],\n        \"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM\": [],\n        \"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ\": [],\n        \"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S\": [],\n        \"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH\": [],\n        \"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw\": [],\n        \"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb\": [],\n        \"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX\": [],\n        \"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ\": [],\n        \"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp\": [],\n        \"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk\": [],\n        \"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD\": [],\n        \"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp\": [],\n        \"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz\": [],\n        \"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid\": [],\n        \"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu\": [],\n        \"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo\": [],\n        \"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj\": [],\n        \"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv\": [],\n        \"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC\": [],\n        \"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe\": [],\n        \"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG\": []\n    },\n    \"labels\": {},\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\",\n        \"x/1'\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\",\n        \"x/2'\": \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\"\n    },\n    \"next_account2\": [\n        \"2\",\n        \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\",\n        \"031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e\",\n        \"1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ\"\n    ],\n    \"pruned_txo\": {},\n    \"transactions\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": \"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000\"\n    },\n    \"txi\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": {}\n    },\n    \"txo\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": {\n            \"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi\": [\n                [\n                    0,\n                    5000,\n                    false\n                ]\n            ]\n        }\n    },\n    \"verified_tx3\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": [\n            490002,\n            1508090436,\n            607\n        ]\n    },\n    \"wallet_type\": \"trezor\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_1_1_trezor_singleacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80\",\n                \"0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890\",\n                \"038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1\"\n            ],\n            \"receiving\": [\n                \"020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae\",\n                \"03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74\",\n                \"03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046\",\n                \"02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f\",\n                \"031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d\",\n                \"03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717\",\n                \"0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631\",\n                \"035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f\",\n                \"02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be\",\n                \"026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5\",\n                \"0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457\",\n                \"03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429\",\n                \"028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a\",\n                \"03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4\",\n                \"0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482\",\n                \"02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f\",\n                \"02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31\",\n                \"02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1\",\n                \"034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f\",\n                \"032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c\"\n            ],\n            \"xpub\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"labels\": {\n        \"0\": \"Main account\"\n    },\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\",\n        \"x/1'\": \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\"\n    },\n    \"next_account2\": [\n        \"1\",\n        \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\",\n        \"03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b\",\n        \"18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG\"\n    ],\n    \"pruned_txo\": {},\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"wallet_type\": \"trezor\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_1_1_watchaddresses",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\": [\n                    null,\n                    null\n                ],\n                \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\": [\n                    null,\n                    null\n                ],\n                \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\": [\n                    null,\n                    null\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"pruned_txo\": {},\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"wallet_type\": \"imported\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_2_0_importedkeys",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\": [\n                    \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\",\n                    \"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\"\n                ],\n                \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\": [\n                    \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\",\n                    \"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"\n                ],\n                \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\": [\n                    \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\",\n                    \"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\"\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"pruned_txo\": {},\n    \"stored_height\": 489714,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"imported\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_2_0_multisig",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                [\n                    \"037ba2d9d7446d54f1b46c902427e58a4b63915745de40f31db52e95e2eb8c559c\",\n                    \"03aab9d4cb98fec92e1a9fc93b93f439b30cdb47cb3fae113779d0d26e85ceca7b\"\n                ],\n                [\n                    \"036c6cb5ed99f4d3c8d2dd594c0a791e266a443d57a51c3c7320e0e90cf040dad0\",\n                    \"03f777561f36c795911e1e42b3b4babe473bcce32463eb9340b48d86fded8a226a\"\n                ],\n                [\n                    \"03de4acea515b1b3b6a2b574d08539ced475f86fdf00b43bff16ec43f6f8efc8b7\",\n                    \"036ebfdd8ba75c94e0cb1819ecba464d04a77bab11c8fc2b7e90dd952092c01f0e\"\n                ]\n            ],\n            \"receiving\": [\n                [\n                    \"03e768d9de027e4edaf0685abb240dde9af1188f5b5d2aa08773b0083972bdec74\",\n                    \"0280eccb8edec0e6de521abba3831f51900e9d0655c59cddf054b72a70b520ddae\"\n                ],\n                [\n                    \"02f9c0b7e8fe426a45540027abca63c27109db47b5c86886b99db63450444bb460\",\n                    \"03cb5cdcc26b0aa326bc895fcc38b63416880cdc404efbeab3ff14f849e4f4bd63\"\n                ],\n                [\n                    \"024d6267b9348a64f057b8e094649de36e45da586ef8ca5ecb7137f6294f6fd9e3\",\n                    \"034c14b014eb28abfeaa0676b195bde158ab9b4c3806428e587a8a3c3c0f2d38bb\"\n                ],\n                [\n                    \"02bc3d5456aa836e9a155296be6a464dfa45eb2164dd0691c53c8a7a05b2cb7c42\",\n                    \"03a374129009d7e407a5f185f74100554937c118faf3bbe4fe1cac31547f46effa\"\n                ],\n                [\n                    \"024808c2d17387cd6d466d13b278f76d4d04a7d31734f0708a8baf20ae8c363f9a\",\n                    \"02e18dfc7f5ea9e8b6afe0853a9aba55861208b32f22c81aa4be0e6aee7951963d\"\n                ],\n                [\n                    \"0331bef7adca60ae484a12cc3c4b788d4296e0b52500731bf5dff1b935973d4768\",\n                    \"025774c45aeac2ae87b7a67e79517ffb8264bdf1b56905a76e7e7579f875cbed55\"\n                ],\n                [\n                    \"020566e7351b4bfe6c0d7bda3af24267245a856af653dd00c482555f305b71a8e3\",\n                    \"036545f66ad2fe95eeb0ec1feb501d552773e0910ec6056d6b827bc0bb970a1ecc\"\n                ],\n                [\n                    \"038dc34e68a49d2205f4934b739e510dca95961d0f8ab6f6cd9279d68048cfd93b\",\n                    \"03810c50d1e2ff0e39179788e8506784bc214768884f6f71dc4323f6c29e25c888\"\n                ],\n                [\n                    \"035059ff052ab044fd807905067ec79b19177edcf1b1b969051dc0e6957b1e1eab\",\n                    \"03d790376a0144860017bea5b5f2f0a9f184a55623e9a1e8f3670bf6aba273f4fb\"\n                ],\n                [\n                    \"02bb730d880b90e421d9ac97313b3c0eec6b12a8c778388d52a188af7dc026db43\",\n                    \"030ae3ae865b805c3c11668b46ec4f324d50f6b5fbc2bb3a9ae0ddc4aea0d1487a\"\n                ],\n                [\n                    \"0306eeb93a37b7dcbb5c20146cfd3036e9a16e5b35ecfe77261a6e257ee0a7b178\",\n                    \"03fb49f5f1d843ca6d62cee86fd4f79b6cc861f692e54576a9c937fdff13714be9\"\n                ],\n                [\n                    \"03f4c358e03bd234055c1873e77f451bea6b54167d36c005abeb704550fbe7bee1\",\n                    \"03fc36f11d726fd4321f99177a0fff9b924ec6905d581a16436417d2ea884d3c80\"\n                ],\n                [\n                    \"024d68322a93f2924d6a0290ebe7481e29215f1c182bd8fdeb514ade8563321c87\",\n                    \"02aa5502de7b402e064dfebc28cb09316a0f90eec333104c981f571b8bc69279e2\"\n                ],\n                [\n                    \"03cbda5b33a72be05b0e50ef7a9872e28d82d5a883e78a73703f53e40a5184f7a5\",\n                    \"02ebf10a631436aa0fdef9c61e1f7d645aa149c67d3cb8d94d673eb3a994c36f86\"\n                ],\n                [\n                    \"0285891a0f1212efff208baf289fd6316f08615bee06c0b9385cc0baad60ebc08a\",\n                    \"0356a6c4291f26a5b0c798f3d0b9837d065a50c9af7708f928c540017f150c40b6\"\n                ],\n                [\n                    \"02403988346d00e9b949a230647edbe5c03ce36b06c4c64da774a13aca0f49ce92\",\n                    \"02717944f0bb32067fb0f858f7a7b422984c33d42fd5de9a055d00c33b72731426\"\n                ],\n                [\n                    \"02161a510f42bcc7cdd24e7541a0bdbcac08b1c63b491df1974c6d5cd977d57750\",\n                    \"03006d73c0ab9fdd8867690d9282031995cfd094b5bdc3ff66f3832c5b8a9ca7f9\"\n                ],\n                [\n                    \"03d80ea710e1af299f1079dd528d6cdc5797faa310bafa90ca7c45ea44d5ba64f3\",\n                    \"02b29e1170d6bec16ace70536565f1dff1480cba2a7545cfec7b522568a6ab5c38\"\n                ],\n                [\n                    \"02c3f6e8dea3cace7aab89d8258751827cb5791424c71fa82ae30192251ca11a28\",\n                    \"02a43d2d952e1f3fb58c56dadabb39cf5ed437c566f504a79f2ade243abd2c9139\"\n                ],\n                [\n                    \"0308e96e38eb89ca5abaa6776a1968a1cbb33197ec91d40bb44bede61cb11a517f\",\n                    \"034d0545444e5a5410872a3384cedd3fb198a8211bb391107e8e2c0b0b67932b20\"\n                ]\n            ],\n            \"xpub\": \"xpub661MyMwAqRbcFCKg479EAwb6KLrQNcFSQKNjQRJpRFSiFRnp87cpntXkDUEvRtFTEARirm9584ML8sLBkF3gDBcyYgknnxCCrBMwPDDMQwC\",\n            \"xpub2\": \"xpub661MyMwAqRbcFaEDoCANCiY9dhXvA8GgXFSLXYADmxmatLidGTxnVL6vuoFAMg9ugX8MTKjZPiP9uUPXusUji11LnWWLCw8Lzgx7pM5sg1s\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"master_private_keys\": {\n        \"x1/\": \"xprv9s21ZrQH143K2iFCx5cDooeMmK1uy9Xb36T8c2uCruujNdTfaaJaF6DGNDcDKkX1U4V1XiEcvCqoNsQhMQUnp8ZvMgxDBDErtMACo2HtGgQ\"\n    },\n    \"master_public_keys\": {\n        \"x1/\": \"xpub661MyMwAqRbcFCKg479EAwb6KLrQNcFSQKNjQRJpRFSiFRnp87cpntXkDUEvRtFTEARirm9584ML8sLBkF3gDBcyYgknnxCCrBMwPDDMQwC\",\n        \"x2/\": \"xpub661MyMwAqRbcFaEDoCANCiY9dhXvA8GgXFSLXYADmxmatLidGTxnVL6vuoFAMg9ugX8MTKjZPiP9uUPXusUji11LnWWLCw8Lzgx7pM5sg1s\"\n    },\n    \"pruned_txo\": {},\n    \"seed\": \"such duck column calm verb sock used message army suffer humble olive abstract\",\n    \"seed_version\": 11,\n    \"stored_height\": 490033,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"2of2\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_2_0_seeded",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"038f4bae4a901fe5f2a30a06a09681fff6678e8efda4e881f71dcdc0fdb36dd1b8\",\n                \"032c628bec66fe98c3921b4fea6f18d241e6b23f4baf9e56c78b7a5262cd4cc412\",\n                \"0232b68a11cde50a49fb3155fe2c9e9cf7aa9f4bcb0f51c3963b13c997e40de40d\"\n            ],\n            \"receiving\": [\n                \"0237246e68c6916c43c7c5aca1031df0c442439b80ceda07eaf72645a0597ed6aa\",\n                \"03f35bee973012909d839c9999137b7f2f3296c02791764da3f55561425bb1d53c\",\n                \"02fdbe9f95e2279045e6ef5f04172c6fe9476ba09d70aa0a8483347bfc10dee65e\",\n                \"026bc52dc91445594bb639c7a996d682ac74a4564381874b9d36cc5feea103d7a4\",\n                \"0319182796c6377447234eeee9fe62ce6b25b83a9c46965d9a02c579a23f9fa57a\",\n                \"02e23d202a45515ce509c8b9548a251de3ad8e64c92b24bb74b354c8d4d0dc85af\",\n                \"0307d7ccb51aa6860606bcbe008acc1aae5b53d19d0752a20a327b6ec164399b52\",\n                \"038a2362fde711e1a4b9c5f8fe1090a0a38aec3643c0c3d69b00660b213dc4bfb8\",\n                \"0396255ef7b75e5d8ffc18d01b9012a98141ee5458a68cde8b25c492c569a22ab8\",\n                \"02c7edf03d215b7d3478fb26e9375d541440f4a8b5c562c0eb98fab6215dbea731\",\n                \"024286902b95da3daf6ffb571d5465537dae5b4e00139e6465e440d6a26892158e\",\n                \"03aa0d3fa1fe190a24e14d6aabd9c163c7fe70707b00f7e0f9fa6b4d3a4e441149\",\n                \"03995d433093a2ae9dc305fe8664f6ab9143b2f7eaf6f31bc5fefdacb183699808\",\n                \"033c5da7c4c7a3479ddb569fecbcbb8725867370746c04ff5d2a84d1706607bbab\",\n                \"036a097331c285c83c4dab7d454170b60a94d8d9daa152b0af6af81dbd7f0cc440\",\n                \"033ed002ddf99c1e21cb8468d0f5512d71466ac5ba4003b33d71a181e3a696e3c5\",\n                \"02a6a0f30d1a341063a57a0549a3d16d9487b1d4e0d4bffadabdc62d1ad1a43f8f\",\n                \"02dcae71fc2e31013cf12ad78f9e16672eeb7c75e536f4f7d36adb54f9682884eb\",\n                \"028ef32bc57b95697dacdb29b724e3d0fa860ffdc33c295962b680d31b23232090\",\n                \"0314afd1ac2a4bf324d6e73f466a60f511d59088843f93c895507e7af1ccdb5a3b\"\n            ],\n            \"xpub\": \"xpub661MyMwAqRbcEuc5dCRqgPpGX2bKStk4g2cbZ96SSmKsQmLUrhaQEtrfnBMsXSUSyKWuCtjLiZ8zXrxcNeC2LR8gnZPrQJdmUEeofS2yuux\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"master_private_keys\": {\n        \"x/\": \"xprv9s21ZrQH143K2RXcXAtqKFsXxzkq3S2DJogzkkgptRntXy1LKAG9h6YBvw8JjSUogF1UNneyYgS5uYshMBemqr41XsC7bTr8Fjx1uAyLbPC\"\n    },\n    \"master_public_keys\": {\n        \"x/\": \"xpub661MyMwAqRbcEuc5dCRqgPpGX2bKStk4g2cbZ96SSmKsQmLUrhaQEtrfnBMsXSUSyKWuCtjLiZ8zXrxcNeC2LR8gnZPrQJdmUEeofS2yuux\"\n    },\n    \"pruned_txo\": {},\n    \"seed\": \"agree tongue gas total hollow clip wasp slender dolphin rebel ozone omit achieve\",\n    \"seed_version\": 11,\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"standard\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_2_0_trezor_multiacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902\",\n                \"03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740\",\n                \"028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee\"\n            ],\n            \"receiving\": [\n                \"03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0\",\n                \"024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97\",\n                \"03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b\",\n                \"028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136\",\n                \"02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5\",\n                \"02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4\",\n                \"023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53\",\n                \"02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4\",\n                \"029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92\",\n                \"02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066\",\n                \"0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447\",\n                \"0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea\",\n                \"02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027\",\n                \"0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50\",\n                \"03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459\",\n                \"0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c\",\n                \"028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804\",\n                \"03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736\",\n                \"029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f\",\n                \"02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\"\n        },\n        \"1\": {\n            \"change\": [\n                \"03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34\",\n                \"0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e\",\n                \"036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8\"\n            ],\n            \"receiving\": [\n                \"02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58\",\n                \"039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9\",\n                \"0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec\",\n                \"02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604\",\n                \"0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f\",\n                \"0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6\",\n                \"03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd\",\n                \"03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00\",\n                \"028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3\",\n                \"02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85\",\n                \"03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898\",\n                \"021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4\",\n                \"03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f\",\n                \"0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff\",\n                \"03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7\",\n                \"02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec\",\n                \"0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a\",\n                \"024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6\",\n                \"026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089\",\n                \"02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986\",\n                \"03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {\n        \"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC\": [],\n        \"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi\": [\n            [\n                \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\",\n                490002\n            ]\n        ],\n        \"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz\": [],\n        \"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM\": [],\n        \"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ\": [],\n        \"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S\": [],\n        \"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH\": [],\n        \"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw\": [],\n        \"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb\": [],\n        \"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX\": [],\n        \"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ\": [],\n        \"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp\": [],\n        \"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk\": [],\n        \"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD\": [],\n        \"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp\": [],\n        \"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz\": [],\n        \"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid\": [],\n        \"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu\": [],\n        \"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo\": [],\n        \"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj\": [],\n        \"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv\": [],\n        \"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC\": [],\n        \"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe\": [],\n        \"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG\": []\n    },\n    \"labels\": {},\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\",\n        \"x/1'\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\",\n        \"x/2'\": \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\"\n    },\n    \"next_account2\": [\n        \"2\",\n        \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\",\n        \"031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e\",\n        \"1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ\"\n    ],\n    \"pruned_txo\": {},\n    \"stored_height\": 490006,\n    \"transactions\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": \"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000\"\n    },\n    \"txi\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": {}\n    },\n    \"txo\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": {\n            \"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi\": [\n                [\n                    0,\n                    5000,\n                    false\n                ]\n            ]\n        }\n    },\n    \"verified_tx3\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": [\n            490002,\n            1508090436,\n            607\n        ]\n    },\n    \"wallet_type\": \"trezor\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_2_0_trezor_singleacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80\",\n                \"0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890\",\n                \"038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1\"\n            ],\n            \"receiving\": [\n                \"020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae\",\n                \"03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74\",\n                \"03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046\",\n                \"02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f\",\n                \"031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d\",\n                \"03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717\",\n                \"0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631\",\n                \"035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f\",\n                \"02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be\",\n                \"026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5\",\n                \"0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457\",\n                \"03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429\",\n                \"028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a\",\n                \"03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4\",\n                \"0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482\",\n                \"02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f\",\n                \"02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31\",\n                \"02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1\",\n                \"034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f\",\n                \"032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c\"\n            ],\n            \"xpub\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"labels\": {\n        \"0\": \"Main account\"\n    },\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\",\n        \"x/1'\": \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\"\n    },\n    \"next_account2\": [\n        \"1\",\n        \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\",\n        \"03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b\",\n        \"18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG\"\n    ],\n    \"pruned_txo\": {},\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"wallet_type\": \"trezor\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_2_0_watchaddresses",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\": [\n                    null,\n                    null\n                ],\n                \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\": [\n                    null,\n                    null\n                ],\n                \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\": [\n                    null,\n                    null\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"pruned_txo\": {},\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"wallet_type\": \"imported\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_3_2_importedkeys",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\": [\n                    \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\",\n                    \"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\"\n                ],\n                \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\": [\n                    \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\",\n                    \"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"\n                ],\n                \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\": [\n                    \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\",\n                    \"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\"\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"pruned_txo\": {},\n    \"stored_height\": 489715,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"imported\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_3_2_multisig",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                [\n                    \"03083942fe75c1345833faa4d31a635e088ca173047ddd6ef5b7f1395892ef339d\",\n                    \"03c02f486ed1f0e6d1aefbdea293c8cb44b34a3c719849c45e52ef397e6540bbda\"\n                ],\n                [\n                    \"0326d9adb5488c6aba8238e26c6185f4d2f1b072673e33fb6b495d62dc800ff988\",\n                    \"023634ebe9d7448af227be5c85e030656b353df81c7cf9d23bc2c7403b9af7509b\"\n                ],\n                [\n                    \"0223728d8dd019e2bd2156754c2136049a3d2a39bf2cb65965945f4c598fdb6db6\",\n                    \"037b6d4df2dde500789f79aa2549e8a6cb421035cda485581f7851175e0c95d00e\"\n                ],\n                [\n                    \"03c47ade02def712ebbf142028d304971bec99ca53be8e668e9cf15ff0ef186e19\",\n                    \"02e212ad25880f2c9be7dfd1966e4b6ae8b3ea40e09d482378b942ca2e716397b0\"\n                ],\n                [\n                    \"03dab42b0eaee6b0e0d982fbf03364b378f39a1b3a80e980460ae96930a10bff6c\",\n                    \"02baf8778e83fbad7148f3860ce059b3d27002c323eab5957693fb8e529f2d757f\"\n                ],\n                [\n                    \"02fc3019e886b0ce171242ddedb5f8dcde87d80ad9f707edb8e6db66a4389bea49\",\n                    \"0241b4e9394698af006814acf09bf301f79d6feb2e1831a7bc3e8097311b1a96dd\"\n                ]\n            ],\n            \"receiving\": [\n                [\n                    \"023e2bf49bc40aeed95cb1697d8542354df8572a8f93f5abe1bcec917778cc9fc6\",\n                    \"03cf4e80c4bf3779e402b85f268ada2384932651cc41e324e51fc69d6af55ae593\"\n                ],\n                [\n                    \"02d9ba257aa3aba2517bb889d1d5a2e435d10c9352b2330600decab8c8082db242\",\n                    \"03de9e91769733f6943483167602dd3d439e34b7078186066af8e90ec58076c2a7\"\n                ],\n                [\n                    \"02ccdd5b486cefa658af0c49d85aefa3ab62f808335ffcd4b8d4197a3c50ab073c\",\n                    \"03e80dbbd0fb93d01d6446d0af1c18c16d26bdbb2538d8bf7f2f68ce95ba857667\"\n                ],\n                [\n                    \"031605867287fe3b1fee55e07b2f513792374bb5baf30f316970c5bc095651a789\",\n                    \"02c0802b96cee67d6acec5266eb3b491c303cea009d57a6bb7aee83cc602206ad5\"\n                ],\n                [\n                    \"037d07d30dec97da4ea09d568f96f0eb6cd86d02781a7adff16c1647e1bcd23260\",\n                    \"03d856a53bc90be84810ce94c8aac0791c9a63379fd61790c11dae926647aa4eec\"\n                ],\n                [\n                    \"028887f2d54ffefc98e5a605c83bedba79367c2a4fe11b98ec6582896ffad79216\",\n                    \"0259dab6dafe52306fe6e3686f27a36e0650c99789bb19cbcd0907db00957030a9\"\n                ],\n                [\n                    \"039d83064dd37681eaf7babe333b210685ba9fe63627e2f2d525c1fb9c4d84d772\",\n                    \"03381011299678d6b72ff82d6a47ed414b9e35fcf97fc391b3ff1607fb0bf18617\"\n                ],\n                [\n                    \"03ace6ceb95c93a446ae9ff5211385433c9bbf5785d52b4899e80623586f354004\",\n                    \"0369de6b20b87219b3a56ea8007c33091f090698301b89dd6132cf6ef24b7889a0\"\n                ],\n                [\n                    \"031ec2b1d53da6a162138fb8f4a1ec27d62c45c13dddecebbd55ad8a5d05397382\",\n                    \"02417a3320e15c2a5f0345ac927a10d7218883170a9e64837e629d14f8f3de7c78\"\n                ],\n                [\n                    \"02b85c8b2f33b6a8a882c383368be8e0a91491ea57595b6a690f01041be5bef4fb\",\n                    \"0383ad57c7899284e9497e9dccb1de5bf8559b87157f13fee5677dcf2fbeb7b782\"\n                ],\n                [\n                    \"03eaa9e3ea81b2fa6e636373d860c0014e67ac6363c9284e465384986c2ec77ee2\",\n                    \"03b1bd0d6355d99e8cab6d177f10f05eb8ddd3e762871f176d78a79f14ae037826\"\n                ],\n                [\n                    \"03ecd1b458e7c2b71a6542f8e64c750358c1421542ffe7630cc3ecc6866d379dfe\",\n                    \"02d5c5432ca5e4243430f73a69c180c23bda8c7c269d7b824a4463e3ac58850984\"\n                ],\n                [\n                    \"028098ae6e772460047cdd6694230dcfc44da8ceabcae0624225f2452be7ae26c4\",\n                    \"02add86858446c8a59ed3132264a8141292cd4ece6653bf3605895cceb00ba30b9\"\n                ],\n                [\n                    \"02f580882255cda6fae954294164b26f2c4b6b2744c0930daaa7a9953275f2f410\",\n                    \"02c09c5e369910d84057637157bdf1fb721387bb2867c3c2adb2d91711498bbe5e\"\n                ],\n                [\n                    \"025e628f78c95135669ab8b9178f4396b0b513cbeae9ca631ba5e5e8321a4a05bc\",\n                    \"03476f35b4defcc67334a0ff2ce700fb55df39b0f7f4ff993907e21091f6a29a31\"\n                ],\n                [\n                    \"026fa6f3214dce2ad2325dae3cd8d6728ce62af1903e308797ff071129fe111eca\",\n                    \"03d07eb26749caceca56ffe77d9837aaf2f657c028bd3575724b7e2f1a8b3261a5\"\n                ],\n                [\n                    \"03894311c920ef03295c3f1c8851f5dc9c77e903943940820b084953a0a92efcc3\",\n                    \"0368b0b3774f9de81b9f10e884d819ccf22b3c0ed507d12ce2a13efc36d06cdc17\"\n                ],\n                [\n                    \"024f8a61c23aa4a13a3a9eb9519ed3ec734f54c5e71d55f1805e873c31a125c467\",\n                    \"039e9c6708767bd563fcdca049c4d8a1acab4a051d4f804ae31b5e9de07942570f\"\n                ],\n                [\n                    \"038f9b8f4b9fe6af5ced879a16bb6d56d81831f11987d23b32716ca4331f6cbabf\",\n                    \"035453374f020646f6eda9528543ec0363923a3b7bbb40bc9db34740245d0132e7\"\n                ],\n                [\n                    \"02e30cd68ae23b3b3239d4e98745660b08d7ce30f2f6296647af977268a23b6c86\",\n                    \"02ee5e33d164f0ad6b63f0c412734c1960507286ad675a343df9f0479d21a86ecc\"\n                ]\n            ],\n            \"xpub\": \"xpub661MyMwAqRbcGAPwDuNBFdPguAcMFDrUFznD8RsCFkjQqtMPE66H5CDpecMJ9giZ1GVuZUpxhX4nFh1R3fzzr4hjDoxDSHymXVXQa7H1TjG\",\n            \"xpub2\": \"xpub661MyMwAqRbcFMKuZtmYryCNiNvHAki74TizX3b6dxaREtjLMoqnLJbd1zQKjWwKEThmB4VRtwePAWHNk9G5nHvAEvMHDYemERPQ7bMjQE3\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"master_private_keys\": {\n        \"x1/\": \"xprv9s21ZrQH143K3gKU7sqAtVSxM8mrqm8ctmrcL3TahRCRy62EgYn2XPuLoJAGbBGvL4ArbPoAay5jo7L1UbBv15SsmrSKdTQSgDE351WSkm6\"\n    },\n    \"master_public_keys\": {\n        \"x1/\": \"xpub661MyMwAqRbcGAPwDuNBFdPguAcMFDrUFznD8RsCFkjQqtMPE66H5CDpecMJ9giZ1GVuZUpxhX4nFh1R3fzzr4hjDoxDSHymXVXQa7H1TjG\",\n        \"x2/\": \"xpub661MyMwAqRbcFMKuZtmYryCNiNvHAki74TizX3b6dxaREtjLMoqnLJbd1zQKjWwKEThmB4VRtwePAWHNk9G5nHvAEvMHDYemERPQ7bMjQE3\"\n    },\n    \"pruned_txo\": {},\n    \"seed\": \"brick huge enforce behave cabin cram okay friend sketch actor casual barrel abuse\",\n    \"seed_version\": 11,\n    \"stored_height\": 490033,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"2of2\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_3_2_seeded",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"03b37d18c0c52da686e8fd3cc5d242e62036ac2b38f101439227f9e15b46f88c42\",\n                \"026f946e309e64dcb4e62b00a12aee9ee14d26989880e690d8c307f45385958875\",\n                \"03c75552e48d1d44f966fb9cfe483b9479cc882edcf81e2faf92fba27c7bbecbc1\",\n                \"020965e9f1468ebda183fea500856c7e2afcc0ccdc3da9ccafc7548658d35d1fb3\",\n                \"03da778470ee52e0e22b34505a7cc4a154e67de67175e609a6466db4833a4623ed\",\n                \"0243f6bbb6fea8e0da750645b18973bc4bd107c224d136f26c7219aab6359c2705\"\n            ],\n            \"receiving\": [\n                \"0376bf85c1bf8960947fe575adc0a3f3ba08f6172336a1099793efd0483b19e089\",\n                \"03f0fe0412a3710a5a8a1c2e01fe6065b7a902f1ccbf38cd7669806423860ad111\",\n                \"03eacb81482ba01a741b5ee8d52bb6e48647107ef9a638ca9a7b09f6d98964a456\",\n                \"03c8b598f6153a87fc37f693a148a7c1d32df30597404e6a162b3b5198d0f2ba33\",\n                \"03fefef3ee4f918e9cd3e56501018bcededc48090b33c15bf1a4c3155c8059610a\",\n                \"0390562881078a8b0d54d773d6134091e2da43c8a97f4f3088a92ca64d21fcf549\",\n                \"0366a0977bb35903390e6b86bbb6faa818e603954042e98fe954a4b8d81d815311\",\n                \"025d176af6047d959cfdd9842f35d31837034dd4269324dc771c698d28ad9ae3d6\",\n                \"02667adce009891ee872612f31cd23c5e94604567140b81d0eae847f5539c906d6\",\n                \"03de40832017ba85e8131c2af31079ab25a72646d28c8d2b6a39c98c4d1253ae2f\",\n                \"02854c17fdef156b1681f494dfc7a10c6a8033d0c577b287947b72ecada6e6386b\",\n                \"0283ff8f775ba77038f787b9bf667f538f186f861b003833600065b4ad8fd84362\",\n                \"03b0a4e9a6ffecd955bd0e2b169113b544a7cba1688dca6fce204552403dc28391\",\n                \"02445465cf40603506dbe7fa853bc1aae0d79ca90e57b6a7af6ffc1341c4ca8e2d\",\n                \"0220ea678e2541f809da75552c07f9e64863a254029446d6270e433a4434be2bd7\",\n                \"02640e87aab83bd84fe964eac72657b34d5ad924026f8d2222557c56580607808e\",\n                \"020fa9a0c3b335c6cdc6588b14c596dfae242547dd68e5c6bce6a9347152ff4021\",\n                \"03f7f052076dc35483c91033edef2cc93b54fb054fe3b36546800fa1a76b1d321a\",\n                \"030fd12243e1ffe1fc6ec3cdb7e020a467d3146d55d52af915552f2481a91657cd\",\n                \"02dd1a2becbc344a297b104e4bb41f7de4f5fcff1f3244e4bb124fbb6a70b5eb18\"\n            ],\n            \"xpub\": \"xpub661MyMwAqRbcEnd8FGgkz7V8iJZ2FvDcg669i7NSS7h7nmq5k5WeHohNqosRSjx9CKiRxMgTidPWA5SJYsjrXhr1azR3boubNp24gZHUeY4\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"master_private_keys\": {\n        \"x/\": \"xprv9s21ZrQH143K2JYf9F9kcyYQAGiXrTVmJsAYuixpsnA8uyVwCYCPk1NtzYuNmeLRLKcMYb3UoPgTocYsHsAje3mSjX4jp3Ci17VhuESjsBU\"\n    },\n    \"master_public_keys\": {\n        \"x/\": \"xpub661MyMwAqRbcEnd8FGgkz7V8iJZ2FvDcg669i7NSS7h7nmq5k5WeHohNqosRSjx9CKiRxMgTidPWA5SJYsjrXhr1azR3boubNp24gZHUeY4\"\n    },\n    \"pruned_txo\": {},\n    \"seed\": \"scheme grape nephew hen song purity pizza syrup must dentist bright grit accuse\",\n    \"seed_version\": 11,\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"standard\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_3_2_trezor_multiacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902\",\n                \"03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740\",\n                \"028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee\",\n                \"021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8\",\n                \"031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4\",\n                \"033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d\"\n            ],\n            \"receiving\": [\n                \"03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0\",\n                \"024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97\",\n                \"03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b\",\n                \"028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136\",\n                \"02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5\",\n                \"02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4\",\n                \"023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53\",\n                \"02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4\",\n                \"029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92\",\n                \"02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066\",\n                \"0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447\",\n                \"0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea\",\n                \"02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027\",\n                \"0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50\",\n                \"03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459\",\n                \"0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c\",\n                \"028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804\",\n                \"03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736\",\n                \"029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f\",\n                \"02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\"\n        },\n        \"1\": {\n            \"change\": [\n                \"03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34\",\n                \"0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e\",\n                \"036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8\",\n                \"03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff\",\n                \"03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7\",\n                \"022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9\"\n            ],\n            \"receiving\": [\n                \"02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58\",\n                \"039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9\",\n                \"0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec\",\n                \"02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604\",\n                \"0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f\",\n                \"0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6\",\n                \"03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd\",\n                \"03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00\",\n                \"028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3\",\n                \"02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85\",\n                \"03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898\",\n                \"021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4\",\n                \"03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f\",\n                \"0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff\",\n                \"03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7\",\n                \"02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec\",\n                \"0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a\",\n                \"024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6\",\n                \"026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089\",\n                \"02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986\",\n                \"03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {\n        \"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC\": [],\n        \"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi\": [\n            [\n                \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\",\n                490002\n            ]\n        ],\n        \"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz\": [],\n        \"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM\": [],\n        \"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ\": [],\n        \"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6\": [],\n        \"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S\": [],\n        \"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH\": [],\n        \"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw\": [],\n        \"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb\": [],\n        \"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX\": [],\n        \"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ\": [],\n        \"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp\": [],\n        \"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk\": [],\n        \"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD\": [],\n        \"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp\": [],\n        \"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz\": [],\n        \"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs\": [],\n        \"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid\": [],\n        \"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu\": [],\n        \"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo\": [],\n        \"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj\": [],\n        \"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv\": [],\n        \"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC\": [],\n        \"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe\": [],\n        \"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL\": [],\n        \"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG\": []\n    },\n    \"labels\": {},\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\",\n        \"x/1'\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\",\n        \"x/2'\": \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\"\n    },\n    \"next_account2\": [\n        \"2\",\n        \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\",\n        \"031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e\",\n        \"1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ\"\n    ],\n    \"pruned_txo\": {},\n    \"stored_height\": 490008,\n    \"transactions\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": \"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000\"\n    },\n    \"txi\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": {}\n    },\n    \"txo\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": {\n            \"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi\": [\n                [\n                    0,\n                    5000,\n                    false\n                ]\n            ]\n        }\n    },\n    \"verified_tx3\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": [\n            490002,\n            1508090436,\n            607\n        ]\n    },\n    \"wallet_type\": \"trezor\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_3_2_trezor_singleacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80\",\n                \"0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890\",\n                \"038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1\",\n                \"029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156\",\n                \"034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e\",\n                \"036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e\"\n            ],\n            \"receiving\": [\n                \"020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae\",\n                \"03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74\",\n                \"03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046\",\n                \"02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f\",\n                \"031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d\",\n                \"03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717\",\n                \"0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631\",\n                \"035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f\",\n                \"02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be\",\n                \"026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5\",\n                \"0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457\",\n                \"03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429\",\n                \"028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a\",\n                \"03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4\",\n                \"0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482\",\n                \"02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f\",\n                \"02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31\",\n                \"02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1\",\n                \"034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f\",\n                \"032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c\"\n            ],\n            \"xpub\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"labels\": {\n        \"0\": \"Main account\"\n    },\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\",\n        \"x/1'\": \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\"\n    },\n    \"next_account2\": [\n        \"1\",\n        \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\",\n        \"03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b\",\n        \"18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG\"\n    ],\n    \"pruned_txo\": {},\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"wallet_type\": \"trezor\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_3_2_watchaddresses",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\": [\n                    null,\n                    null\n                ],\n                \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\": [\n                    null,\n                    null\n                ],\n                \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\": [\n                    null,\n                    null\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"pruned_txo\": {},\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"wallet_type\": \"imported\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_4_3_importedkeys",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\": [\n                    \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\",\n                    \"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\"\n                ],\n                \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\": [\n                    \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\",\n                    \"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"\n                ],\n                \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\": [\n                    \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\",\n                    \"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\"\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"stored_height\": 477636,\n    \"use_encryption\": false,\n    \"wallet_type\": \"imported\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_4_3_multisig",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                [\n                    \"03467a8bae231aff83aa01999ee4d3834894969df7a3b0753e23ae7a3aae089f6b\",\n                    \"02180c539980494b4e59edbda5e5340be2f5fbf07e7c3898b0488950dda04f3476\"\n                ],\n                [\n                    \"03d8e18a428837e707f35d8e2da106da2e291b8acbf40ca0e7bf1ac102cda1de11\",\n                    \"03fad368e3eb468a7fe721805c89f4405581854a58dcef7205a0ab9b903fd39c23\"\n                ],\n                [\n                    \"0331c9414d3eee5bee3c2dcab911537376148752af83471bf3b623c184562815d9\",\n                    \"02dcd25d2752a6303f3a8366fae2d62a9ff46519d70da96380232fc9818ee7029e\"\n                ],\n                [\n                    \"03bb18a304533086e85782870413688eabef6a444a620bf679f77095b9d06f5a16\",\n                    \"02f089ed84b0f7b6cb0547741a18517f2e67d7b5d4d4dd050490345831ce2aef9e\"\n                ],\n                [\n                    \"02dc6ebde88fdfeb2bcd69fce5c5c76db6409652c347d766b91671e37d0747e423\",\n                    \"038086a75e36ac0d6e321b581464ea863ab0be9c77098b01d9bc8561391ed0c695\"\n                ],\n                [\n                    \"02a0b30b12f0c4417a4bef03cb64aa55e4de52326cf9ebe0714613b7375d48a22e\",\n                    \"02c149adda912e8dc060e3bbe4020c96cff1a32e0c95098b2573e67b330e714df0\"\n                ]\n            ],\n            \"m\": 2,\n            \"receiving\": [\n                [\n                    \"0254281a737060e919b071cb58cc16a3865e36ea65d08a7a50ba2e10b80ff326d5\",\n                    \"0257421fa90b0f0bc75b67dd54ffa61dc421d583f307c58c48b719dd59078023e4\"\n                ],\n                [\n                    \"03854ce9bbc7813d535099658bcc6c671a2c25a269fdb044ee0ed5deb95da0d7e0\",\n                    \"025379ca82313dde797e5aa3f222dddf0f7223cb271f79ecce2c8178bea3e33c62\"\n                ],\n                [\n                    \"03ae6ad5ffc75d71adc2ab87e3adc63fa8696a8656e1135adb5ae88ddb6d39089f\",\n                    \"025ed8821f8b37aef69b1aabf89e4e405f09206c330c78e94206b21139ddafcc4f\"\n                ],\n                [\n                    \"033ea4d8b88d36d14a52983ae30d486254af2dfa1c7f8e04bc9d8e34b3ffe4b32a\",\n                    \"02b441a3e47a338d89027755b81724219362b8d9b66142d32fcb91c9c7829d8c9f\"\n                ],\n                [\n                    \"029195704b9bbc3014452bbf07baa7bf6277dfefd9721aea8438f2671ba57b898b\",\n                    \"022264503140f99b41c0269666ab6d16b2dad72865dbd2bf6153d45f5d11978e4d\"\n                ],\n                [\n                    \"037e3caa2d151123821dff34fd8a76ac0d56fa97c41127e9b330a115bf12d76674\",\n                    \"02a4ae28e2011537de4cce0c47af4ac0484b38d408befcb731c3d752922fcd3c5b\"\n                ],\n                [\n                    \"02226853ca32e72b4771ccc47c0aae27c65ed0d25c525c1f673b913b97dca46cc5\",\n                    \"027a9c855fc4e6b3f8495e77347a1e03c0298c6a86bd5a89800195bd445ae3e3bd\"\n                ],\n                [\n                    \"02890f7eee0766d2dde92f3146cd461ae0fa9caf07e1f3559d023a20349bae5e44\",\n                    \"0380249f30829b3656c32064ddf657311159cecb36f9dbbf8e50e3d7279b70c57e\"\n                ],\n                [\n                    \"02ab9613fd5a67a3fdf6b6241d757ce92b2640d9d436e968742cb7c4ec4bb3e6e9\",\n                    \"0204b29cc980b18dfb3a4f9ca6796c6be3e0aee2462719b4a787e31c8c5d79c8cf\"\n                ],\n                [\n                    \"029103b50ecc0cc818c1c97e8acb8ce3e1d86f67e49f60c8496683f15e753c3eed\",\n                    \"0247abb2c5e4cde22eb59a203557c0bbe87e9c449e6c2973e693ac14d0d9cf3f28\"\n                ],\n                [\n                    \"02817c935c971e6e318ba9e25402df26ca016a4e532459be5841c2d83a5aa8a967\",\n                    \"03331fe3a2e4aa3e2dc1d8d4afc5a88c57350806b905e593b5876c6b9cef71fd4d\"\n                ],\n                [\n                    \"03023c6797af5c9c3d7db2fbeb9d7236601fe5438036200f2f59d9b997d29ec123\",\n                    \"023b1084f008cf2e9632967095958bb0bbd59e60a0537e6003d780c7ebccb2d4f5\"\n                ],\n                [\n                    \"0245e0bdebe483fef984e4e023eb34641e65909cd566eb6bd6c0bce592296265a1\",\n                    \"0363bad4b477d551f46b19afcc10decf6a4c1200becb5b22c032c62e6d90b373b8\"\n                ],\n                [\n                    \"0379ba2f8c5e8e5e3f358615d230348fe8d7855ef9c0e1cf97aac4ec09dfe690aa\",\n                    \"02ecda86ff40b286a3faadf9a5b361ab7a5beb50426296a8c0e3d222f404ae4380\"\n                ],\n                [\n                    \"02e090227c22efa7f60f290408ce9f779e27b39d4acec216111cc3a8b9594ab451\",\n                    \"02144954ddabb55abcfe49ea703a4e909ab86db2f971a2e85fc006dffbdf85af52\"\n                ],\n                [\n                    \"025dc4bd1c4809470b5a14cf741519ad7f5f2ccd331b42e0afd2ce182cdf25f82d\",\n                    \"03d292524190af850665c2255a785d66c59fea2b502d4037bb31fdde10ad9b043f\"\n                ],\n                [\n                    \"027e7c549f613ae9ba1d806c8c8256f870e1c7912e3e91cbb326d61fb20ac3a096\",\n                    \"03fbbf15ee2b49878c022d0b30478b6a3acb61f24af6754b3f8bcb4d2e71968099\"\n                ],\n                [\n                    \"02c188eaf5391e52fdcd66f8522df5ae996e20c524577ac9ffa7a9a9af54508f7c\",\n                    \"03fe28f1ea4a0f708fa2539988758efd5144a128cc12aed28285e4483382a6636a\"\n                ],\n                [\n                    \"03bea51abacd82d971f1ef2af58dcbd1b46cdfa5a3a107af526edf40ca3097b78d\",\n                    \"02267d2c8d43034d03219bb5bc0af842fb08f028111fc363ec43ab3b631134228a\"\n                ],\n                [\n                    \"03c3a0ecdbf8f0a162434b0db53b3b51ce02886cbc20c52e19a42b5f681dac6ffb\",\n                    \"02d1ede70e7b1520a6ccabd91488af24049f1f1cf2661c07d8d87aee31d5aec7c9\"\n                ]\n            ],\n            \"xpubs\": [\n                \"xpub661MyMwAqRbcFafkG2opdo3ou3zUEpFK3eKpWWYkdA5kfvootPkZzqvUV1rtLYRLdUxvXBZApzZpwyR2mXBd1hRtnc4LoaLTQWDFcPKnKiQ\",\n                \"xpub661MyMwAqRbcFrxPbuWkHdMeaZMjb4jKpm51RHxQ3czEDmyK3Qa3Z43niVzVjFyhJs6SrdXgQg56DHMDcC94a7MCtn9Pwh2bafhHGJbLWeH\"\n            ]\n        }\n    },\n    \"accounts_expanded\": {},\n    \"master_private_keys\": {\n        \"x1/\": \"xprv9s21ZrQH143K3NsvVsyjvVQv2XXFBc1UTY9QcuYnVHTFLyeAVsFo1FjJsBk48XK16jZLqRs1B5Sa6SCqYdA2XFvB9riBca2GyGccYGKKP6t\"\n    },\n    \"master_public_keys\": {\n        \"x1/\": \"xpub661MyMwAqRbcFrxPbuWkHdMeaZMjb4jKpm51RHxQ3czEDmyK3Qa3Z43niVzVjFyhJs6SrdXgQg56DHMDcC94a7MCtn9Pwh2bafhHGJbLWeH\",\n        \"x2/\": \"xpub661MyMwAqRbcFafkG2opdo3ou3zUEpFK3eKpWWYkdA5kfvootPkZzqvUV1rtLYRLdUxvXBZApzZpwyR2mXBd1hRtnc4LoaLTQWDFcPKnKiQ\"\n    },\n    \"pruned_txo\": {},\n    \"seed\": \"angry work entry banana taste climb script fold level rate organ edge account\",\n    \"seed_version\": 11,\n    \"stored_height\": 490033,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"2of2\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_4_3_seeded",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"02707eb483e51d859b52605756aee6773ea74c148d415709467f0b2a965cd78648\",\n                \"0321cddfb60d7ac41fdf866b75e4ad0b85cc478a3a84dc2e8db17d9a2b9f61c3b5\",\n                \"0368b237dea621f6e1d580a264580380da95126e46c7324b601c403339e25a6de9\",\n                \"02334d75548225b421f556e39f50425da8b8a36960cce564db8001f7508fef49f6\",\n                \"02990b264de812802743a378e7846338411c3afab895cff35fb24a430fa6b43733\",\n                \"02bc3b39ca00a777e95d89f773428bad5051272b0df582f52eb8d6ebb5bb849383\"\n            ],\n            \"receiving\": [\n                \"0286c9d9b59daa3845b2d96ce13ac0312baebaf318251bac6d634bcac5ff815d9d\",\n                \"0220b65829b3a030972be34559c4bb1fc91f8dfd7e1703ddb43da9aa28aa224864\",\n                \"02fe34b26938c29faee00d8d704eae92b7c97d487825892290309073dc85ae5374\",\n                \"03ea255ae2ba7169802543cf7af135783f4fca91924fd0285bdbe386d78a0ab87e\",\n                \"027115aeea786e2745812f2ec2ae8fee3d038d96c9556b1324ac50c913b83a9e6a\",\n                \"03627439bb701352e35d0cf8e00617d8e9bf329697e430b0a5d999370097e025b4\",\n                \"034120249c6b15d051525156845aefaa83988adf9ed1dd18b796217dcf9824b617\",\n                \"02dfeb0c89eee66026d7650ee618c2172551f97fdd9ed249e696c54734d26e39a3\",\n                \"037e031bb4e51beb5c739ba6ab64aa696e85457ea63cc56698b7d9b731fd1e8e61\",\n                \"0302ea6818525492adc5ed8cfd2966efd704915199559fe1c06d6651fd36533012\",\n                \"0349394140560d685d455595f697d17b44e832ec453b5a2f02a3f5ed66205f3d30\",\n                \"036815bf2437df00440b15cfa7123544648cf266247989e82540d6b1cae1589892\",\n                \"02f98568e8f0f4b780f005e538a7452a60b2c06a5d2e3a23fa26d88459d118ef56\",\n                \"02e36ccb8b05a2762a08f60541d1a5a136afd6a73119eea8c7c377cc8b07eb2e2f\",\n                \"031566539feb6f0a212cca2604906b1c1f5cfc5bf5d5206e0c695e37ef3a141fd2\",\n                \"025754e770bedeef6f4e932fa231b858b49d28183e1be6da23e597c67dd7785f19\",\n                \"03a29961f5fb9c197cffe743081a761442a3cf9ded0be2fa07ab67023a74c08d28\",\n                \"023184c1995a9f51af566c9c0b4da92d7fd4a5c59ff93c34a323e94671ddbe414a\",\n                \"029efdb15d3aec708b3af2aee34a9157ff731bec94e4f19f634ab43d3101e47bd8\",\n                \"03e16b13fe6bb9aa6dc4e331e19ab4d3d291a2670b97e6040e87a7c7309b243af9\"\n            ],\n            \"xpub\": \"xpub661MyMwAqRbcF1KGEGXxFTupKQHTTUan1qZMTp4yUxiwF2uRRum7u1TCnaJRjaSBW4d42Fwfi6xfLvfRfgtDixekGDWK9CPWguR7YzXKKeV\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"master_private_keys\": {\n        \"x/\": \"xprv9s21ZrQH143K2XEo8EzwtKy5mNSy41rvecdkfRfMvdBxNEaGtNSsMD8iwHsc91UxKtSrDHXex53NkMRRDwnm4PmqS7N35K8BR1KCD2qm5iE\"\n    },\n    \"master_public_keys\": {\n        \"x/\": \"xpub661MyMwAqRbcF1KGEGXxFTupKQHTTUan1qZMTp4yUxiwF2uRRum7u1TCnaJRjaSBW4d42Fwfi6xfLvfRfgtDixekGDWK9CPWguR7YzXKKeV\"\n    },\n    \"seed\": \"smart fish version ocean category disagree hospital mystery survey chef kid latin about\",\n    \"seed_version\": 11,\n    \"use_encryption\": false,\n    \"wallet_type\": \"standard\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_4_3_trezor_multiacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902\",\n                \"03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740\",\n                \"028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee\",\n                \"021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8\",\n                \"031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4\",\n                \"033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d\"\n            ],\n            \"receiving\": [\n                \"03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0\",\n                \"024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97\",\n                \"03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b\",\n                \"028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136\",\n                \"02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5\",\n                \"02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4\",\n                \"023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53\",\n                \"02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4\",\n                \"029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92\",\n                \"02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066\",\n                \"0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447\",\n                \"0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea\",\n                \"02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027\",\n                \"0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50\",\n                \"03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459\",\n                \"0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c\",\n                \"028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804\",\n                \"03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736\",\n                \"029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f\",\n                \"02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\"\n        },\n        \"1\": {\n            \"change\": [\n                \"03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34\",\n                \"0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e\",\n                \"036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8\",\n                \"03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff\",\n                \"03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7\",\n                \"022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9\"\n            ],\n            \"receiving\": [\n                \"02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58\",\n                \"039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9\",\n                \"0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec\",\n                \"02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604\",\n                \"0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f\",\n                \"0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6\",\n                \"03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd\",\n                \"03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00\",\n                \"028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3\",\n                \"02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85\",\n                \"03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898\",\n                \"021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4\",\n                \"03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f\",\n                \"0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff\",\n                \"03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7\",\n                \"02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec\",\n                \"0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a\",\n                \"024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6\",\n                \"026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089\",\n                \"02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986\",\n                \"03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {\n        \"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi\": [\n            [\n                \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\",\n                490002\n            ]\n        ]\n    },\n    \"labels\": {\n        \"0\": \"Main account\"\n    },\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\",\n        \"x/1'\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\",\n        \"x/2'\": \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\"\n    },\n    \"next_account2\": [\n        \"2\",\n        \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\",\n        \"031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e\",\n        \"1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ\"\n    ],\n    \"pruned_txo\": {},\n    \"stored_height\": 490009,\n    \"transactions\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": \"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000\"\n    },\n    \"txi\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": {}\n    },\n    \"txo\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": {\n            \"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi\": [\n                [\n                    0,\n                    5000,\n                    false\n                ]\n            ]\n        }\n    },\n    \"verified_tx3\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": [\n            490002,\n            1508090436,\n            607\n        ]\n    },\n    \"wallet_type\": \"trezor\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_4_3_trezor_singleacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80\",\n                \"0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890\",\n                \"038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1\",\n                \"029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156\",\n                \"034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e\",\n                \"036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e\"\n            ],\n            \"receiving\": [\n                \"020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae\",\n                \"03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74\",\n                \"03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046\",\n                \"02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f\",\n                \"031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d\",\n                \"03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717\",\n                \"0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631\",\n                \"035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f\",\n                \"02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be\",\n                \"026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5\",\n                \"0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457\",\n                \"03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429\",\n                \"028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a\",\n                \"03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4\",\n                \"0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482\",\n                \"02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f\",\n                \"02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31\",\n                \"02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1\",\n                \"034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f\",\n                \"032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c\"\n            ],\n            \"xpub\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"labels\": {\n        \"0\": \"Main account\"\n    },\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\",\n        \"x/1'\": \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\"\n    },\n    \"next_account2\": [\n        \"1\",\n        \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\",\n        \"03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b\",\n        \"18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG\"\n    ],\n    \"pruned_txo\": {},\n    \"stored_height\": 485855,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"wallet_type\": \"trezor\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_4_3_watchaddresses",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\": [\n                    null,\n                    null\n                ],\n                \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\": [\n                    null,\n                    null\n                ],\n                \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\": [\n                    null,\n                    null\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"pruned_txo\": {},\n    \"stored_height\": 490038,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"wallet_type\": \"imported\"\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_5_4_importedkeys",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\": [\n                    \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\",\n                    \"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\"\n                ],\n                \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\": [\n                    \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\",\n                    \"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"\n                ],\n                \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\": [\n                    \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\",\n                    \"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\"\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {},\n    \"pruned_txo\": {},\n    \"stored_height\": 489716,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"imported\",\n    \"winpos-qt\": [\n        595,\n        261,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_5_4_multisig",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                [\n                    \"02a63209b49df0bb98d8a262e9891fe266ffdce4be09d5e1ffaf269a10d7e7a17c\",\n                    \"02a074035006ed8ee8f200859c004c073b687140f7d40bd333cdbbe43bad1e50bc\"\n                ],\n                [\n                    \"0280e2367142669e08e27fb9fd476076a7f34f596e130af761aef54ec54954a64d\",\n                    \"02719a66c59f76c36921cf7b330fca7aaa4d863ee367828e7d89cd2f1aad98c3ac\"\n                ],\n                [\n                    \"0332083e80df509d3bd8a06538ca20030086c9ed3313300f7313ed98421482020f\",\n                    \"032f336744f53843d8a007990fa909e35e42e1e32460fae2e0fc1aef7c2cff2180\"\n                ],\n                [\n                    \"03fe014e5816497f9e27d26ce3ae8d374edadec410227b2351e9e65eb4c5d32ab7\",\n                    \"0226edd8c3af9e339631145fd8a9f6d321fdc52fe0dc8e30503541c348399dd52a\"\n                ],\n                [\n                    \"03e6717b18d7cbe264c6f5d0ad80f915163f6f6c08c121ac144a7664b95aedfdf3\",\n                    \"03d69a074eba3bc2c1c7b1f6f85822be39aee20341923e406c2b445c255545394a\"\n                ],\n                [\n                    \"023112f87a5b9b2eadc73b8d5657c137b50609cd83f128d130172a0ed9e3fea9bc\",\n                    \"029a81fd5ba57a2c2c6cfbcb34f369d87af8759b66364d5411eddd28e8a65f67fa\"\n                ]\n            ],\n            \"m\": 2,\n            \"receiving\": [\n                [\n                    \"03c35c3da2c864ee3192a847ffd3f67fa59c095d8c2c0f182ed9556308ec37231e\",\n                    \"03cfcb6d1774bfd916bd261232645f6c765da3401bf794ab74e84a6931d8318786\"\n                ],\n                [\n                    \"03973c83f84a4cf5d7b21d1e8b29d6cbd4cb40d7460166835cd1e1fd2418cfcf2e\",\n                    \"03596801e66976959ac1bdb4025d65a412d95d320ed9d1280ac3e89b041e663cf4\"\n                ],\n                [\n                    \"02b78ac89bfdf90559f24313d7393af272092827efc33ba3a0d716ee8b75fd08ff\",\n                    \"038e21fae8a033459e15a700551c1980131eb555bbb8b23774f8851aa10dcac6b8\"\n                ],\n                [\n                    \"0288e9695bb24f336421d5dcf16efb799e7d1f8284413fe08e9569588bc116567e\",\n                    \"027123ba3314f77a8eb8bb57ba1015dd6d61b709420f6a3320ba4571b728ef2d91\"\n                ],\n                [\n                    \"0312e1483f7f558aef1a14728cc125bb4ee5cff0e7fa916ba8edd25e3ebceb05e9\",\n                    \"02dad92a9893ad95d3be5ebc40828cef080e4317e3a47af732127c3fee41451356\"\n                ],\n                [\n                    \"03a694e428a74d37194edc9e231e68399767fdb38a20eca7b72caf81b7414916a8\",\n                    \"03129a0cef4ed428031972050f00682974b3d9f30a571dc3917377595923ac41d8\"\n                ],\n                [\n                    \"026ed41491a6d0fb3507f3ca7de7fb2fbfdfb28463ae2b91f2ab782830d8d5b32c\",\n                    \"03211b3c30c41d54734b3f13b8c9354dac238d82d012839ee0199b2493d7e7b6fc\"\n                ],\n                [\n                    \"03480e87ffa55a96596be0af1d97bca86987741eb5809675952a854d59f5e8adc2\",\n                    \"0215f04df467d411e2a9ed8883a21860071ab721314503019a10ed30e225e522e7\"\n                ],\n                [\n                    \"0389fce63841e9231d5890b1a0c19479f8f40f4f463ef8e54ef306641abe545ac8\",\n                    \"02396961d498c2dcb3c7081b50c5a4df15fda31300285a4c779a59c9abc98ea20d\"\n                ],\n                [\n                    \"03d4a3053e9e08dc21a334106b5f7d9ac93e42c9251ceb136b83f1a614925eb1fb\",\n                    \"025533963c22b4f5fbfe75e6ee5ad7ee1c7bff113155a7695a408049e0b16f1c52\"\n                ],\n                [\n                    \"038a07c8d2024b9118651474bd881527e8b9eb85fc90fdcb04c1e38688d498de4b\",\n                    \"03164b188eb06a3ea96039047d0db1c8f9be34bfd454e35471b1c2f429acd40afb\"\n                ],\n                [\n                    \"0214070cd393f39c062ce1e982a8225e5548dbbbd654aeba6d36bfcc7a685c7b12\",\n                    \"029c6a9fb61705cc39bef34b09c684a362d4862b16a3b0b39ca4f94d75cd72290c\"\n                ],\n                [\n                    \"027b3497f72f581fea0a678bc20482b6fc7b4b507f7263d588001d73fdf5fe314e\",\n                    \"021b80b159d19b6978a41c2a6bf7d3448bc73001885f933f7854f450b5873091f3\"\n                ],\n                [\n                    \"0303e9d76e4fe7336397c760f6fdfd5fb7500f83e491efb604fa2442db6e1da417\",\n                    \"03a8d1b22a73d4c181aecd8cfe8bb2ee30c5dd386249d2a5a3b071b7a25b9da73a\"\n                ],\n                [\n                    \"0298e472b74832af856fb68eed02ff00a235fd0424d833bc305613e9f44087d0ee\",\n                    \"03bb9bc2e4aaa9b022b35c8d122dfccb6c28ae8f0996a8fb4a021af8ec96a7beaf\"\n                ],\n                [\n                    \"02e933a4afb354500da03373514247e1be12e67cc4683e0cb82f508878cc3cc048\",\n                    \"02c07a57b071bc449a95dd80308e53b26e4ebf4d523f620eecb17f96ae3aa814e9\"\n                ],\n                [\n                    \"03f73476951078b3ccc549bc7e6362797aaaacb1ea0edc81404b4d16cb321255a3\",\n                    \"03b3a825fb9fc497e568fba69f70e2c3dcdc793637e242fce578546fcbd33cb312\"\n                ],\n                [\n                    \"03bbdf99fddeea64a96bbb9d1e6d7ced571c9c7757045dcbd8c40137125b017dc5\",\n                    \"03aedf4452afefb1c3da25e698f621cb3a3a0130aa299488e018b93a45b5e6c21d\"\n                ],\n                [\n                    \"03b85891edb147d43c0a5935a20d6bbf8d32c542bfecccf3ae0158b65bd639b34e\",\n                    \"03b34713c636a1c103b82d6cec917d442c59522ddc5a60bf7412266dd9790e7760\"\n                ],\n                [\n                    \"028ddf53b85f6c01122a96bd6c181ee17ca222ee9eca85bdeeb25c4b5315005e3b\",\n                    \"02f4821995bfd5d0adb7a78d6e3a967ac72ace9d9a4f9392aff2711533893e017b\"\n                ]\n            ],\n            \"xpubs\": [\n                \"xpub661MyMwAqRbcGHtCYBSGGVgMSihroMkuyE25GPyzfQvS2vSFG7SgJYf7rtXJjMh7srBJj8WddLtjapHnUQLwJ7kxsy5HiNZnGvF9pm2du7b\",\n                \"xpub661MyMwAqRbcEdd7bzA86LbhMoTv8NeyqcNP5z1Tiz9ajCRQDzdeXHw3h5ucDNGWr6mPFCZBcBE31VNKyR3vWM7WEeisu5m4VsCyuA6H8fp\"\n            ]\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {\n        \"32JvbwfEGJwZHGm3nwYiXyfsnGCb3L8hMX\": [],\n        \"32pWy5sKkQsjyDz45tog47cA8vQyzC3UUZ\": [],\n        \"334yqX1WtS6mY2vize7znTaL64HspwVkGF\": [],\n        \"33GY9w6a4XmLAWxNgNFFRXTTRxbu3Nz8ip\": [],\n        \"33geBcyW8Bw53EgAv3qwMVkVnvxZWj5J1X\": [],\n        \"35BneogkCNxSiSN1YLmhKLP8giDbGkZiTX\": [],\n        \"37U4J5b9B7rQnQXYstMoQnb6i9aWpptnLi\": [],\n        \"37gqbHdbrCcGyrNF21AiDkofVCie5LpFmQ\": [],\n        \"37t1Q5R92co4by2aagtLcqdWTDEzFuAuwZ\": [],\n        \"37z3ruAHCxnzeJeLz96ZpkbwS3CLbtXtPc\": [],\n        \"39qePsKaeviFEMC6CWX37DqaQda4jA2E6A\": [],\n        \"3A5eratrDWu4SqsoHpuqswNsQmp9k8TXR2\": [],\n        \"3B1N3PG5dNPYsTAuHFbVfkwXeZqqNS1CuP\": [],\n        \"3BABbvd3eAuwiqJwppm54dJauKnRUieQU8\": [],\n        \"3CAsH7BJnNT4kmwrbG8XZMMwW6ue8w4auJ\": [],\n        \"3CX2GLCTfpFHSgAmbGRmuDKGHMbWY8tCp7\": [],\n        \"3CrLUTVHuG1Y3swny9YDmkfJ89iHHU93NB\": [],\n        \"3CxRa6yAQ2N2rpDHyUTaViGG4XVASAqwAN\": [],\n        \"3DLTrsdYabso7QpxoLSW5ZFjLxBwrLEqqW\": [],\n        \"3GG3APgrdDCTmC9tTwWu3sNV9aAnpFcddA\": [],\n        \"3JDWpTxnsKoKut9WdG4k933qmPE5iJ8hRR\": [],\n        \"3LdHoahj7rHRrQVe38D4iN43ySBpW5HQRZ\": [],\n        \"3Lt56BqiJwZ1um1FtXJXzbY5uk32GVBa8K\": [],\n        \"3MM9417myjN7ubMDkaK1wQ9RbjEc1zHCRH\": [],\n        \"3NTivFVXva4DCjPmsf5p5Gt1dmuV39qD2v\": [],\n        \"3QCwtjMywMtT3Vg6BwS146LcQjJnZPAPHZ\": []\n    },\n    \"master_private_keys\": {\n        \"x1/\": \"xprv9s21ZrQH143K29YeVxd7jCexomdRiuw8UPSnHbbrAecbrQ6FgTKPyVcZqp2256L5DSTdb8UepPVaDwJecswTrEhdyZiaNGERJpfzWV5FcN5\"\n    },\n    \"master_public_keys\": {\n        \"x1/\": \"xpub661MyMwAqRbcEdd7bzA86LbhMoTv8NeyqcNP5z1Tiz9ajCRQDzdeXHw3h5ucDNGWr6mPFCZBcBE31VNKyR3vWM7WEeisu5m4VsCyuA6H8fp\",\n        \"x2/\": \"xpub661MyMwAqRbcGHtCYBSGGVgMSihroMkuyE25GPyzfQvS2vSFG7SgJYf7rtXJjMh7srBJj8WddLtjapHnUQLwJ7kxsy5HiNZnGvF9pm2du7b\"\n    },\n    \"pruned_txo\": {},\n    \"seed\": \"park dash merit trend life field acid wrap dinosaur kit bar hotel abuse\",\n    \"seed_version\": 11,\n    \"stored_height\": 490034,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"2of2\",\n    \"winpos-qt\": [\n        564,\n        329,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_5_4_seeded",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"0253e61683b66ebf5a4916334adf1409ffe031016717868c9600d313e87538e745\",\n                \"021762e47578385ecedc03c7055da1713971c82df242920e7079afaf153cc37570\",\n                \"0303a8d6a35956c228aa95a17aab3dee0bca255e8b4f7e8155b23acef15cf4a974\",\n                \"02e881bc60018f9a6c566e2eb081a670f48d89b4a6615466788a4e2ce20246d4c6\",\n                \"02f0090e29817ef64c17f27bf6cdebc1222f7e11d7112073f45708e8d218340777\",\n                \"035b9c53b85fd0c2b434682675ac862bfcc7c5bb6993aee8e542f01d96ff485d67\"\n            ],\n            \"receiving\": [\n                \"024fbc610bd51391794c40a7e04b0e4d4adeb6b0c0cc84ac0b3dad90544e428c47\",\n                \"024a2832afb0a366b149b6a64b648f0df0d28c15caa77f7bbf62881111d6915fe9\",\n                \"028cd24716179906bee99851a9062c6055ec298a3956b74631e30f5239a50cb328\",\n                \"039761647d7584ba83386a27875fe3d7715043c2817f4baca91e7a0c81d164d73d\",\n                \"02606fc2f0ce90edc495a617329b3c5c5cc46e36d36e6c66015b1615137278eabd\",\n                \"02191cc2986e33554e7b155f9eddcc3904fdba43a5a3638499d3b7b5452692b740\",\n                \"024b5bf755b2f65cab1f7e5505febc1db8b91781e5aac352902e79bc96ad7d9ad0\",\n                \"0309816cb047402b84133f4f3c5e56c215e860204513278beef54a87254e44c14a\",\n                \"03f53d34337c12ddb94950b1fee9e4a9cf06ad591db66194871d31a17ec7b59ac7\",\n                \"0325ede4b08073d7f288741c2c577878919fd5d832a9e6e04c9eac5563ae13aa83\",\n                \"02eca43081b04f68d6c8b81781acd59e5b8d2ba44dba195369afc40790fd9edef7\",\n                \"029a8ca96c64d3a98345be1594208908f2be5e6af6bcc6ff3681f271e75fcf232e\",\n                \"02fbe0804980750163a216cc91cfe86e907addf0e80797a8ea5067977eb4897c1b\",\n                \"0344f32fc1ee8b2eb08f419325529f495d77a3b5ea683bbce7a44178705ab59302\",\n                \"021dd62bdf18256bd5316ce3cbcca58785378058a41ba2d1c58f4cc76449b3c424\",\n                \"035e61cdbdb4306e58a816a19ad92c7ca3a392b67ac6d7257646868ffe512068c5\",\n                \"0326a4db82f21787d0246f8144abe6cda124383b7d93a6536f36c05af530ea262a\",\n                \"02b352a27a8f9c57b8e5c89b357ba9d0b5cb18bf623509b34cd881fcf8b89a819a\",\n                \"02a59188edef1ed29c158a0adb970588d2031cfe53e72e83d35b7e8dd0c0c77525\",\n                \"02e8b9e42a54d072c8887542c405f6c99cfabf41bdde639944b44ba7408837afd1\"\n            ],\n            \"xpub\": \"xpub661MyMwAqRbcGh7ywNf1BYoFCs8mht2YnvkMYUJTazrAWbnbvkrisvSvrKGjRTDtw324xzprbDgphsmPv2pB6K5Sux3YNHC8pnJANCBY6vG\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {\n        \"12LXoVHUnAXn6BVBpshjwd7sSTwp5nsd7W\": [],\n        \"12iXPYBErR6ZMESB9Nv74S4pVxdGMNLiW2\": [],\n        \"13jmb5Vc2qh29tPhg637BwCJN7hStGWYXE\": [],\n        \"14dHBBbwFVC7niSCqrb5HCHRK5K8rrgaW6\": [],\n        \"14xsHuYGs4gKpRK3deuYwhMBTAwUeu2dpB\": [],\n        \"15MpWMUasNVPTpzC5hK2AuVFwQ3AHd8fkv\": [],\n        \"17nmvao3F84ebPrcv1LUxPUSS94U9EvCUt\": [],\n        \"17yotEc8oUgJVQUnkjZSQjcqqZEbFFnXx8\": [],\n        \"1A3c1rCCS2MYYobffyUHwPqkqE5ZpvG8Um\": [],\n        \"1AtCzmcth79q6HgeyDnM3NLfr29hBHcfcg\": [],\n        \"1AufJhUsMbqwbLK9JzUGQ9tTwphCQiVCwD\": [],\n        \"1B77DkhJ8qHcwPQC2c1HyuNcYu5TzxxaJ7\": [],\n        \"1D4bgjc4MDtEPWNTVfqG5bAodVu3D1Gjft\": [],\n        \"1DefMPXdeCSQC5ieu8kR7hNGAXykNzWXpm\": [],\n        \"1E673RESY1SvTWwUr5hQ1E7dGiRiSgkYFP\": [],\n        \"1Ex6hnmpgp3FQrpR5aYvp9zpXemFiH7vky\": [],\n        \"1FH2iAc5YgJKj1KcpJ1djuW3wJ2GbQezAv\": [],\n        \"1GpjShJMGrLQGP6nZFDEswU7qUUgJbNRKi\": [],\n        \"1H4BtV4Grfq2azQgHSNziN7MViQMDR9wxd\": [],\n        \"1HnWq29dPuDRA7gx9HQLySGdwGWiNx4UP1\": [],\n        \"1LMuebyhm8vnuw5qX3tqU2BhbacegeaFuE\": [],\n        \"1LTJK8ffwJzRaNR5dDEKqJt6T8b4oVbaZx\": [],\n        \"1LtXYvRr4j1WpLLA398nbmKhzhqq4abKi8\": [],\n        \"1NfsUmibBxnuA3ir8GJvPUtY5czuiCfuYK\": [],\n        \"1Q3cZjzADnnx5pcc1NN2ekJjLijNjXMXfr\": [],\n        \"1okpBWorqo5WsBf5KmocsfhBCEDhNstW2\": []\n    },\n    \"master_private_keys\": {\n        \"x/\": \"xprv9s21ZrQH143K4D3WqM7zpQrWeqJHJRJhRhpkk5tr2fKBdoTTPDYUL88T12Ad9RHwViugcMbngkMDY626vD5syaFDoUB2cpLeraBaHvZHWFn\"\n    },\n    \"master_public_keys\": {\n        \"x/\": \"xpub661MyMwAqRbcGh7ywNf1BYoFCs8mht2YnvkMYUJTazrAWbnbvkrisvSvrKGjRTDtw324xzprbDgphsmPv2pB6K5Sux3YNHC8pnJANCBY6vG\"\n    },\n    \"pruned_txo\": {},\n    \"seed\": \"tent alien genius panic stage below spoon swap merge hammer gorilla squeeze ability\",\n    \"seed_version\": 11,\n    \"stored_height\": 489715,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        100,\n        100,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_5_4_trezor_multiacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902\",\n                \"03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740\",\n                \"028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee\",\n                \"021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8\",\n                \"031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4\",\n                \"033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d\"\n            ],\n            \"receiving\": [\n                \"03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0\",\n                \"024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97\",\n                \"03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b\",\n                \"028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136\",\n                \"02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5\",\n                \"02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4\",\n                \"023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53\",\n                \"02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4\",\n                \"029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92\",\n                \"02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066\",\n                \"0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447\",\n                \"0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea\",\n                \"02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027\",\n                \"0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50\",\n                \"03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459\",\n                \"0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c\",\n                \"028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804\",\n                \"03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736\",\n                \"029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f\",\n                \"02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\"\n        },\n        \"1\": {\n            \"change\": [\n                \"03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34\",\n                \"0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e\",\n                \"036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8\",\n                \"03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff\",\n                \"03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7\",\n                \"022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9\"\n            ],\n            \"receiving\": [\n                \"02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58\",\n                \"039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9\",\n                \"0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec\",\n                \"02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604\",\n                \"0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f\",\n                \"0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6\",\n                \"03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd\",\n                \"03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00\",\n                \"028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3\",\n                \"02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85\",\n                \"03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898\",\n                \"021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4\",\n                \"03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f\",\n                \"0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff\",\n                \"03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7\",\n                \"02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec\",\n                \"0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a\",\n                \"024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6\",\n                \"026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089\",\n                \"02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986\",\n                \"03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129\"\n            ],\n            \"xpub\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {\n        \"12bBPWWDwvtXrR9ntSgaQ7AnGyVJr16m5q\": [],\n        \"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi\": [\n            [\n                \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\",\n                490002\n            ]\n        ],\n        \"13853om3ye5c8x6K1LfT3uCWEnG14Z82ML\": [],\n        \"13BGVmizH8fk3qNm1biNZxAaQY3vPwurjZ\": [],\n        \"13Tvp2DLQFpUxvc7JxAD3TXfAUWvjhwUiL\": [],\n        \"15EQcTGzduGXSaRihKy1FY99EQQco8k2UW\": [],\n        \"15paDwtQ33jJmJhjoBJhpWYGJDFCZppEF9\": [],\n        \"17X8K766zBYLTjSNvHB9hA6SWRPMTcT556\": [],\n        \"17zSo4aveNaE5DiTmwNZtxrJmS5ymzvwqj\": [],\n        \"19BRVkUFfrAcxW9poaBSEUA2yv7SwN3SXh\": [],\n        \"19gPT2mb9FQCiiPdAmMAaberShzNRiAtTB\": [],\n        \"1A3vopoUcrWn7JbiAzGZactQz8HbnC1MoD\": [],\n        \"1D1bn2Jzcx4D2GXbxzrJ1GwP4eNq98Q948\": [],\n        \"1DvytpRGLJujPtSLYTRABzpy2r6hKJBYQd\": [],\n        \"1EGg2acXNhJfv1bU3ixrbrmgxFtAUWpdY\": [],\n        \"1Ev3S9YWxS7KWT8kyLmEuKV5sexNKcMUKV\": [],\n        \"1FfpRnukxbfBnoudWvw9sdmc86YbVs7eGb\": [],\n        \"1GBxNE82WLgd38CzoFTEkz6QS9EwLj1ym7\": [],\n        \"1JFDe97zENNUiKeizcFUHss13vS2AcrVdE\": [],\n        \"1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ\": [],\n        \"1JQqX3yg6VYxL6unuRArDQaBZYo3ktSCCP\": [],\n        \"1JUbrr4grE71ZgWNqm9z9ZHHJDcCzFYM4V\": [],\n        \"1JuHUVbYfBLDUhTHx5tkDDyDbCnMsF8C9w\": [],\n        \"1KZu7p244ETkdB5turRP4vhG2QJskARYWS\": [],\n        \"1LE7jioE7y24m3MMZayRKpvdCy2Dz2LQae\": [],\n        \"1LVr2pTU7LPQu8o8DqsxcGrvwu5rZADxfi\": [],\n        \"1LmugnVryiuMbgdUAv3LucnRMLvqg8AstU\": [],\n        \"1MPN5vptDZCXc11fZjpW1pvAgUZ5Ksh3ky\": []\n    },\n    \"labels\": {\n        \"0\": \"Main account\"\n    },\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\",\n        \"x/1'\": \"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH\",\n        \"x/2'\": \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\"\n    },\n    \"next_account2\": [\n        \"2\",\n        \"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa\",\n        \"031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e\",\n        \"1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ\"\n    ],\n    \"pruned_txo\": {},\n    \"stored_height\": 490009,\n    \"transactions\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": \"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000\"\n    },\n    \"txi\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": {}\n    },\n    \"txo\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": {\n            \"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi\": [\n                [\n                    0,\n                    5000,\n                    false\n                ]\n            ]\n        }\n    },\n    \"verified_tx3\": {\n        \"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837\": [\n            490002,\n            1508090436,\n            607\n        ]\n    },\n    \"wallet_type\": \"trezor\",\n    \"winpos-qt\": [\n        757,\n        469,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_5_4_trezor_singleacc",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80\",\n                \"0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890\",\n                \"038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1\",\n                \"029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156\",\n                \"034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e\",\n                \"036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e\"\n            ],\n            \"receiving\": [\n                \"020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae\",\n                \"03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74\",\n                \"03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046\",\n                \"02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f\",\n                \"031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d\",\n                \"03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717\",\n                \"0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631\",\n                \"035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f\",\n                \"02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be\",\n                \"026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5\",\n                \"0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457\",\n                \"03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429\",\n                \"028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a\",\n                \"03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4\",\n                \"0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482\",\n                \"02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f\",\n                \"02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31\",\n                \"02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1\",\n                \"034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f\",\n                \"032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c\"\n            ],\n            \"xpub\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {},\n    \"labels\": {\n        \"0\": \"Main account\"\n    },\n    \"master_public_keys\": {\n        \"x/0'\": \"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9\",\n        \"x/1'\": \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\"\n    },\n    \"next_account2\": [\n        \"1\",\n        \"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG\",\n        \"03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b\",\n        \"18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG\"\n    ],\n    \"pruned_txo\": {},\n    \"stored_height\": 490046,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"wallet_type\": \"trezor\",\n    \"winpos-qt\": [\n        522,\n        328,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_5_4_watchaddresses",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\": [\n                    null,\n                    null\n                ],\n                \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\": [\n                    null,\n                    null\n                ],\n                \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\": [\n                    null,\n                    null\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {},\n    \"pruned_txo\": {},\n    \"stored_height\": 490038,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"wallet_type\": \"imported\",\n    \"winpos-qt\": [\n        406,\n        393,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_6_4_importedkeys",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\": [\n                    \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\",\n                    \"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\"\n                ],\n                \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\": [\n                    \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\",\n                    \"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"\n                ],\n                \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\": [\n                    \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\",\n                    \"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\"\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {},\n    \"pruned_txo\": {},\n    \"stored_height\": 489716,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"imported\",\n    \"winpos-qt\": [\n        510,\n        338,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_6_4_multisig",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                [\n                    \"03d0bcdc86a64cc2024c84853e88985f6f30d3dc3f219b432680c338a3996a89ed\",\n                    \"024f326d48aa0a62310590b10522b69d250a2439544aa4dc496f7ba6351e6ebbfe\"\n                ],\n                [\n                    \"03c0416928528a9aaaee558590447ee63fd33fa497deebefcf363b1af90d867762\",\n                    \"03db7de16cd6f3dcd0329a088382652bc3e6b21ee1a732dd9655e192c887ed88a7\"\n                ],\n                [\n                    \"0291790656844c9d9c24daa344c0b426089eadd3952935c58ce6efe00ef1369828\",\n                    \"02c2a5493893643102f77f91cba709f11aaab3e247863311d6fc3d3fc82624c3cc\"\n                ],\n                [\n                    \"023dc976bd1410a7e9f34c230051db58a3f487763f00df1f529b10f55ee85b931c\",\n                    \"036c318a7530eedf3584fd8b24c4024656508e35057a0e7654f21e89e121d0bd30\"\n                ],\n                [\n                    \"02c8820711b39272e9730a1c5c5c78fe39a642b8097f8724b2592cc987017680ce\",\n                    \"0380e3ebe0ea075e33acb3f796ad6548fde86d37c62fe8e4f6ab5d2073c1bb1d43\"\n                ],\n                [\n                    \"0369a32ddd213677a0509c85af514537d5ee04c68114da3bc720faeb3adb45e6f8\",\n                    \"0370e85ac01af5e3fd5a5c3969c8bca3e4fc24efb9f82d34d5790e718a507cecb6\"\n                ]\n            ],\n            \"m\": 2,\n            \"receiving\": [\n                [\n                    \"0207739a9ff4a643e1d4adb03736ec43d13ec897bdff76b40a25d3a16e19e464aa\",\n                    \"02372ea4a291aeb1fadb26f36976348fc169fc70514797e53b789a87c9b27cc568\"\n                ],\n                [\n                    \"0248ae7671882ec87dd6bacf7eb2ff078558456cf5753952cddb5dde08f471f3d6\",\n                    \"035bac54828b383545d7b70824a8be2f2d9584f656bfdc680298a38e9383ed9e51\"\n                ],\n                [\n                    \"02cb99ba41dfbd510cd25491c12bd0875fe8155b5a6694ab781b42bd949252ff26\",\n                    \"03b520feba42149947f8b2bbc7e8c03f9376521f20ac7b7f122dd44ab27309d7c6\"\n                ],\n                [\n                    \"0395902d5ebb4905edd7c4aedecf17be0675a2ffeb27d85af25451659c05cc5198\",\n                    \"02b4a01d4bd25cadcbf49900005e8d5060ed9cdc35eb33f2cd65cc45cc7ebc00c5\"\n                ],\n                [\n                    \"02f9d06c136f05acc94e4572399f17238bb56fa15271e3cb816ae7bb9be24b00b6\",\n                    \"035516437612574b2b563929c49308911651205e7cebb621940742e570518f1c50\"\n                ],\n                [\n                    \"0376a7de3abaee6631bd4441658987c27e0c7eee2190a86d44841ae718a014ee43\",\n                    \"03cb702364ffd59cb92b2e2128c18d8a5a255be2b95eb950641c5f17a5a900eecb\"\n                ],\n                [\n                    \"03240c5e868ecb02c4879ae5f5bad809439fdbd2825769d75be188e34f6e533a67\",\n                    \"026b0d05784e4b4c8193443ce60bea162eee4d99f9dfa94a53ae3bc046a8574eeb\"\n                ],\n                [\n                    \"02d087cccb7dc457074aa9decc04de5a080757493c6aa12fa5d7d3d389cfdb5b8e\",\n                    \"0293ab7d0d8bbb2d433e7521a1100a08d75a32a02be941f731d5809b22d86edb33\"\n                ],\n                [\n                    \"03d1b83ab13c5b35701129bed42c1f1fbe86dd503181ad66af3f4fb729f46a277e\",\n                    \"0382ec5e920bc5c60afa6775952760668af42b67d36d369cd0e9acc17e6d0a930d\"\n                ],\n                [\n                    \"03f1737db45f3a42aebd813776f179d5724fce9985e715feb54d836020b8517bfe\",\n                    \"0287a9dfb8ee2adab81ef98d52acd27c25f558d2a888539f7d583ef8c00c34d6dc\"\n                ],\n                [\n                    \"038eb8804e433023324c1d439cd5fbbd641ca85eadcfc5a8b038cb833a755dac21\",\n                    \"0361a7c80f0d9483c416bc63d62506c3c8d34f6233b6d100bb43b6fe8ec39388b9\"\n                ],\n                [\n                    \"0336437ada4cd35bec65469afce298fe49e846085949d93ef59bf77e1a1d804e4a\",\n                    \"0321898ed89df11fcfb1be44bb326e4bb3272464f000a9e51fb21d25548619d377\"\n                ],\n                [\n                    \"0260f0e59d6a80c49314d5b5b857d1df64d474aba48a37c95322292786397f3dc6\",\n                    \"03acd6c9aeac54c9510304c2c97b7e206bbf5320c1e268a2757d400356a30c627b\"\n                ],\n                [\n                    \"0373dc423d6ee57fac3b9de5e2b87cf36c21f2469f17f32f5496e9e7454598ba8e\",\n                    \"031ddc1f40c8b8bf68117e790e2d18675b57166e9521dff1da44ba368be76555b3\"\n                ],\n                [\n                    \"031878b39bc6e35b33ceac396b429babd02d15632e4a926be0220ccbd710c7d7b9\",\n                    \"025a71cc5009ae07e3e991f78212e99dd5be7adf941766d011197f331ce8c1bed0\"\n                ],\n                [\n                    \"032d3b42ed4913a134145f004cf105b66ae97a9914c35fb73d37170d37271acfcd\",\n                    \"0322adeb83151937ddcd32d5bf2d3ed07c245811d0f7152716f82120f21fb25426\"\n                ],\n                [\n                    \"0312759ff0441c59cb477b5ec1b22e76a794cd821c13b8900d72e34e9848f088c2\",\n                    \"02d868626604046887d128388e86c595483085f86a395d68920e244013b544ef3b\"\n                ],\n                [\n                    \"038c4d5f49ab08be619d4fed7161c339ea37317f92d36d4b3487f7934794b79df4\",\n                    \"03f4afb40ae7f4a886f9b469a81168ad549ad341390ff91ebf043c4e4bfa05ecc1\"\n                ],\n                [\n                    \"02378b36e9f84ba387f0605a738288c159a5c277bbea2ea70191ade359bc597dbb\",\n                    \"029fd6f0ee075a08308c0ccda7ace4ad9107573d2def988c2e207ac1d69df13355\"\n                ],\n                [\n                    \"02cfecde7f415b0931fc1ec06055ff127e9c3bec82af5e3affb15191bf995ffc1a\",\n                    \"02abb7481504173a7aa1b9860915ef62d09a323425f680d71746be6516f0bb4acf\"\n                ]\n            ],\n            \"xpubs\": [\n                \"xpub661MyMwAqRbcF4mZnFnBRYGBaiD9aQRp9w2jaPUrDg3Eery5gywV7eFMzQKmNyY1W4m4fUwsinMw1tFhMNEZ9KjNtkUSBHPXdcXBwCg5ctV\",\n                \"xpub661MyMwAqRbcGHU5H41miJ2wXBLYYk4psK7pB5pWyxK6m5EARwLrKtmpnMzP52qGsKZEtjJCyohVEaZTFXbohjVdfpDFifgMBT82EvkFpsW\"\n            ]\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {\n        \"329Ju5tiAr4vHZExAT4KydYEkfKiHraY2N\": [],\n        \"32HJ13iTVh3sCWyXzipcGb1e78ZxcHrQ7v\": [],\n        \"32cAdiAapUzNVRYXmDud5J5vEDcGsPHjD8\": [],\n        \"33fKLmoCo8oFfeV987P6KrNTghSHjJM251\": [],\n        \"34cE6ZcgXvHEyKbEP2Jpz5C3aEWhvPoPG2\": [],\n        \"36xsnTKKBojYRHEApVR6bCFbDLp9oqNAxU\": [],\n        \"372PG6D3chr8tWF3J811dKSpPS84MPU6SE\": [],\n        \"378nVF8daT4r3jfX1ebKRheUVZX5zaa9wd\": [],\n        \"392ZtXKp2THrk5VtbandXxFLB8yr2g14aA\": [],\n        \"39cCrU3Zz3SsHiQUDiyPS1Qd5ZL3Rh1GhQ\": [],\n        \"3A2cRoBdem5tdRjq514Pp7ZvaxydgZiaNG\": [],\n        \"3Ceoi3MKdh2xiziHDAzmriwjDx4dvxxLzm\": [],\n        \"3FcXdG8mh1YeQCYVib8Aw7zwnKpComimLH\": [],\n        \"3J4b31yAbQkKhejSW7Qz54qNJDEy3t9uSe\": [],\n        \"3JpJrSxE1GP1X5h82zvLA2TbMZ8nUsGW6z\": [],\n        \"3K1dzpbcop1MotuqyFQyEuXbvQehaKnGVM\": [],\n        \"3L8Us8SN22Hj6GnZPRCLaowA1ZtbptXxxL\": [],\n        \"3LANyoJyShQ8w55tvopoGiZ2BTVjLfChiP\": [],\n        \"3LoJGQdXTzVaDYudUguP4jNJYy4gNDaRpN\": [],\n        \"3MD8jVH7Crp5ucFomDnWqB6kQrEQ9VF5xv\": [],\n        \"3ME8DemkFJSn2tHS23yuk2WfaMP86rd3s7\": [],\n        \"3MFNr17oSZpFtH16hGPgXz2em2hJkd3SZn\": [],\n        \"3QHRTYnW2HWCWoeisVcy3xsAFC5xb6UYAK\": [],\n        \"3QKwygVezHFBthudRUh8V7wwtWjZk3whpB\": [],\n        \"3QNPY3dznFwRv6VMcKgmn8FGJdsuSRRjco\": [],\n        \"3QNwwD8dp6kvS8Fys4ZxVJYZAwCXdXQBKo\": []\n    },\n    \"master_private_keys\": {\n        \"x1/\": \"xprv9s21ZrQH143K3oPcB2UmMA6Cy9W49HLyW6CDNhQuRcn7tGu1tQ2bn6TLw8HFWbu5oP38Z2fFCo5Q4n3fog4DTqywYqfSDWhYbDgVD1TGZoP\"\n    },\n    \"master_public_keys\": {\n        \"x1/\": \"xpub661MyMwAqRbcGHU5H41miJ2wXBLYYk4psK7pB5pWyxK6m5EARwLrKtmpnMzP52qGsKZEtjJCyohVEaZTFXbohjVdfpDFifgMBT82EvkFpsW\",\n        \"x2/\": \"xpub661MyMwAqRbcF4mZnFnBRYGBaiD9aQRp9w2jaPUrDg3Eery5gywV7eFMzQKmNyY1W4m4fUwsinMw1tFhMNEZ9KjNtkUSBHPXdcXBwCg5ctV\"\n    },\n    \"pruned_txo\": {},\n    \"seed\": \"turkey weapon legend tower style multiply tomorrow wet like frame leave cash achieve\",\n    \"seed_version\": 11,\n    \"stored_height\": 490035,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"2of2\",\n    \"winpos-qt\": [\n        610,\n        418,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_6_4_seeded",
    "content": "{\n    \"accounts\": {\n        \"0\": {\n            \"change\": [\n                \"03236a8ce6fd3d343358f92d3686b33fd6e7301bf9f635e94c21825780ab79c93d\",\n                \"0393e39f6b4a3651013fca3352b89f1ae31751d4268603f1423c71ff79cbb453a1\",\n                \"033d9722ecf50846527037295736708b20857b4dd7032fc02317f9780d6715e8ff\",\n                \"03f1d56d2ade1daae5706ea945cab2af719060a955c8ad78153693d8d08ed6b456\",\n                \"029260d935322dd3188c3c6b03a7b82e174f11ca7b4d332521740c842c34649137\",\n                \"0266e8431b49f129b892273ab4c8834a19c6432d5ed0a72f6e88be8c629c731ede\"\n            ],\n            \"receiving\": [\n                \"0350f41cfac3fa92310bb4f36e4c9d45ec39f227a0c6e7555748dff17e7a127f67\",\n                \"02f997d3ed0e460961cdfa91dec4fa09f6a7217b2b14c91ed71d208375914782ba\",\n                \"029a498e2457744c02f4786ac5f0887619505c1dae99de24cf500407089d523414\",\n                \"03b15b06044de7935a0c1486566f0459f5e66c627b57d2cda14b418e8b9017aca1\",\n                \"026e9c73bdf2160630720baa3da2611b6e34044ad52519614d264fbf4adc5c229a\",\n                \"0205184703b5a8df9ae622ea0e8326134cbeb92e1f252698bc617c9598aff395a1\",\n                \"02af55f9af0e46631cb7fde6d1df6715dc6018df51c2370932507e3d6d41c19eec\",\n                \"0374e0c89aa4ecf1816f374f6de8750b9c6648d67fe0316a887a132c608af5e7c0\",\n                \"0321bb62f5b5c393aa82750c5512703e39f4824f4c487d1dc130f690360c0e5847\",\n                \"0338ea6ebb2ed80445f64b2094b290c81d0e085e6000367eb64b1dc5049f11c2e9\",\n                \"020c3371a9fd283977699c44a205621dea8abfc8ebc52692a590c60e22202fa49b\",\n                \"0395555e4646f94b10af7d9bc57e1816895ad2deddef9d93242d6d342cea3d753b\",\n                \"02ffa4495d020d17b54da83eaf8fbe489d81995577021ade3a340a39f5a0e2d45c\",\n                \"030f0e16b2d55c3b40b64835f87ab923d58bcdbb1195fadc2f05b6714d9331e837\",\n                \"02f70041fc4b1155785784a7c23f35d5d6490e300a7dd5b7053f88135fc1f14dfd\",\n                \"03b39508c6f9c7b8c3fb8a1b91e61a0850c3ac76ccd1a53fbc5b853a94979cffa8\",\n                \"03b02aa869aa14b0ec03c4935cc12f221c3f204f44d64146d468e07370c040bfe7\",\n                \"02b7d246a721e150aaf0e0e60a30ad562a32ef76a450101f3f772fef4d92b212d9\",\n                \"037cd5271b31466a75321d7c9e16f995fd0a2b320989c14bee82e161c83c714321\",\n                \"03d4ad77e15be312b29987630734d27ca6e9ee418faa6a8d6a50581eca40662829\"\n            ],\n            \"xpub\": \"xpub661MyMwAqRbcGwHDovebbFy19vHfW2Cqtyf2TaJkAwhFWsLYfHHYcCnM7smpvntxJP1YMVT5triFbWiCGXPRPhqdCxFumA77MuQB1CeWHpE\"\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {\n        \"12qKnKuhCZ1Q9XBi1N6SnxYEUtb5XZXuY5\": [],\n        \"1321ddunxShHmF4cjh3v5yqR7uatvSNndK\": [],\n        \"13Ji3kGWn9qxLcWGhd46xjV6hg8SRw8x2P\": [],\n        \"145q5ZDXuFi6v9dA2t8HyD8ysorfb81NRt\": [],\n        \"14gB2wLy2DMkBVtuU6HHP3kQYNFYPzAguU\": [],\n        \"16VGRwtZwp4yapQN5fS8CprK6mmnEicCEj\": [],\n        \"16ahKVzCviRi24rwkoKgiSVSkvRNiQudE1\": [],\n        \"16wjKZ1CWAMEzSR4UxQTWqXRm9jcJ9Dbuf\": [],\n        \"18ReWGJBq1XkJaPAirVdT6RqDskcFeD5Ho\": [],\n        \"1A1ECMMJU4NicWNwfMBn3XJriB4WHAcPUC\": [],\n        \"1Bvxbfc2wXB8z8kyz2uyKw2Ps8JeGQM9FP\": [],\n        \"1EDWUz4kPq8ZbCdQq8rLhFc3qSZ6Fpt1TD\": [],\n        \"1EsvTarawMm5BfF44hpRtE4GfZFfZZ1JG3\": [],\n        \"1JgaekD2ETMJm6oRNnwTWRK9ZxXeUcbi18\": [],\n        \"1KHdLodsSWj1LrrD9d1RbApfqzpxRs5sxu\": [],\n        \"1KgGwpKhruHWpMNtrpRExDWLLk5qHCHBdg\": [],\n        \"1LFf8d3XD9atZvMVMAiq9ygaeZbphbKzSo\": [],\n        \"1N3XncDQsWE2qff1EVyQEmR6JLLzD3mEL7\": [],\n        \"1NUtLcVQNmY5TJCieM1cUmBmv18AafY1vq\": [],\n        \"1NYFsm7PpneT65byRtm8niyvtzKsbEeuXA\": [],\n        \"1NvEcSvfCe8LPvPkK4ZxhjzaUncTPqe9jX\": [],\n        \"1PV8xdkYKxeMpnzeeA4eYEpL24j1G9ApV2\": [],\n        \"1PdiGtznaW1mok6ETffeRvPP5f4ekBRAfq\": [],\n        \"1QApNe4DtK7HAbJrn5kYkYxZMt86U5ChSb\": [],\n        \"1QnH7F6RBXFe7LtszQ6KTRUPkQKRtXTnm\": [],\n        \"1ekukhMNSWCfnRsmpkuTRuLMbz6cstkrq\": []\n    },\n    \"master_private_keys\": {\n        \"x/\": \"xprv9s21ZrQH143K4TCkhu7bE82GbtTB6ZUzXkjRfBu8ccAGe51Q7jyJ4QTsGbWxpHxnatKeYV7Ad83m7KC81THBm2xmyxA1q8BuuRXSGnmhhR8\"\n    },\n    \"master_public_keys\": {\n        \"x/\": \"xpub661MyMwAqRbcGwHDovebbFy19vHfW2Cqtyf2TaJkAwhFWsLYfHHYcCnM7smpvntxJP1YMVT5triFbWiCGXPRPhqdCxFumA77MuQB1CeWHpE\"\n    },\n    \"pruned_txo\": {},\n    \"seed\": \"heart cabbage scout rely square census satoshi home purpose legal replace move able\",\n    \"seed_version\": 11,\n    \"stored_height\": 489716,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        582,\n        394,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_6_4_watchaddresses",
    "content": "{\n    \"accounts\": {\n        \"/x\": {\n            \"imported\": {\n                \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\": [\n                    null,\n                    null\n                ],\n                \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\": [\n                    null,\n                    null\n                ],\n                \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\": [\n                    null,\n                    null\n                ]\n            }\n        }\n    },\n    \"accounts_expanded\": {},\n    \"addr_history\": {},\n    \"pruned_txo\": {},\n    \"stored_height\": 490038,\n    \"transactions\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"wallet_type\": \"imported\",\n    \"winpos-qt\": [\n        582,\n        425,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_7_18_importedkeys",
    "content": "{\n    \"addr_history\": {\n        \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\": [],\n        \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\": [],\n        \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\": []\n    },\n    \"keystore\": {\n        \"keypairs\": {\n            \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\": \"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\",\n            \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\": \"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\",\n            \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\": \"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"\n        },\n        \"type\": \"imported\"\n    },\n    \"pruned_txo\": {},\n    \"pubkeys\": {\n        \"change\": [],\n        \"receiving\": [\n            \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\",\n            \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\",\n            \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\"\n        ]\n    },\n    \"seed_version\": 13,\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        420,\n        312,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_7_18_multisig",
    "content": "{\n    \"addr_history\": {\n        \"32WKXQ6BWtGJDVTpdcUMhtRZWzgk5eKnhD\": [],\n        \"33rvo2pxaccCV7jLwvth36sdLkdEqhM8B8\": [],\n        \"347kG9dzt2M1ZPTa2zzcmVrAE75LuZs9A2\": [],\n        \"34BBeAVEe5AM6xkRebddFG8JH6Vx1M5hHH\": [],\n        \"34MAGbxxCHPX8ASfKsyNkzpqPEUTZ5i1Kx\": [],\n        \"36uNpoPSgUhN5Cc1wRQyL77aD1RL3a9X6f\": [],\n        \"384xygkfYsSuXN478zhN4jmNcky1bPo7Cq\": [],\n        \"39GBGaGpp1ePBsjjaw8NmbZNZkMzhfmZ3W\": [],\n        \"3BRhw13g9ShGcuHbHExxtFfvhjrxiSiA7J\": [],\n        \"3BboKZc2VgjKVxoC5gndLGpwEkPJuQrZah\": [],\n        \"3C3gKJ2UQNNHY2SG4h43zRS1faSLhnqQEr\": [],\n        \"3CEY1V5WvCTxjHEPG5BY4eXpcYhakTvULJ\": [],\n        \"3DJyQ94H9g18PR6hfzZNxwwdU6773JaYHd\": [],\n        \"3Djb7sWog5ANggPWHm4xT5JiTrTSCmVQ8N\": [],\n        \"3EfgjpUeJBhp3DcgP9wz3EhHNdkCbiJe2L\": [],\n        \"3FWgjvaL8xN6ne19WCEeD5xxryyKAQ5tn1\": [],\n        \"3H4ZtDFovXxwWXCpRo8mrCczjTrtbT6eYL\": [],\n        \"3HvnjPzpaE3VGWwGTALZBguT8p9fyAcfHS\": [],\n        \"3JGuY9EpzuZkDLR7vVGhqK7zmX9jhYEfmD\": [],\n        \"3JvrP4gpCUeQzqgPyDt2XePXn3kpqFTo9i\": [],\n        \"3K3TVvsfo52gdwz7gk84hfP77gRmpc3hkf\": [],\n        \"3K5uh5viV4Dac267Q3eNurQQBnpEbYck5G\": [],\n        \"3KaoWE1m3QrtvxTQLFfvNs8gwQH8kQDpFM\": [],\n        \"3Koo71MC4wBfiDKTsck7qCrRjtGx2SwZqT\": [],\n        \"3L8XBt8KxwqNX1vJprp6C9YfNW4hkYrC6d\": [],\n        \"3QmZjxPwcsHZgVUR2gQ6wdbGJBbFro8KLJ\": []\n    },\n    \"pruned_txo\": {},\n    \"pubkeys\": {\n        \"change\": [\n            [\n                \"031bfbbfb36b5e526bf4d94bfc59f170177b2c821f7d4d4c0e1ee945467fe031a0\",\n                \"03c4664d68e3948e2017c5c55f7c1aec72c1c15686b07875b0f20d5f856ebeb703\"\n            ],\n            [\n                \"03c515314e4b695a809d3ba08c20bef00397a0e2df729eaf17b8e082825395e06b\",\n                \"032391d8ab8cad902e503492f1051129cee42dc389231d3cdba60541d70e163244\"\n            ],\n            [\n                \"035934f55c09ecec3e8f2aa72407ee7ba3c2f077be08b92a27bc4e81b5e27643fe\",\n                \"0332b121ed13753a1f573feaf4d0a94bf5dd1839b94018844a30490dd501f5f5fb\"\n            ],\n            [\n                \"02b1367f7f07cbe1ef2c75ac83845c173770e42518da20efde3239bf988dbff5ac\",\n                \"03f3a8b9033b3545fbe47cab10a6f42c51393ed6e525371e864109f0865a0af43c\"\n            ],\n            [\n                \"02e7c25f25ecc17969a664d5225c37ec76184a8843f7a94655f5ed34b97c52445d\",\n                \"030ae4304923e6d8d6cd67324fa4c8bc44827918da24a05f9240df7c91c8e8db8f\"\n            ],\n            [\n                \"02deb653a1d54372dbc8656fe0a461d91bcaec18add290ccaa742bdaefdb9ec69b\",\n                \"023c1384f90273e3fc8bc551e71ace8f34831d4a364e56a6e778cd802b7f7965a6\"\n            ]\n        ],\n        \"receiving\": [\n            [\n                \"02d978f23dc1493db4daf066201f25092d91d60c4b749ca438186764e6d80e6aa1\",\n                \"02912a8c05d16800589579f08263734957797d8e4bc32ad7411472d3625fd51f10\"\n            ],\n            [\n                \"024a4b4f2553d7f4cc2229922387aad70e5944a5266b2feb15f453cedbb5859b13\",\n                \"03f8c6751ee93a0f4afb7b2263982b849b3d4d13c2e30b3f8318908ad148274b4b\"\n            ],\n            [\n                \"03cd88a88aabc4b833b4631f4ffb4b9dc4a0845bb7bc3309fab0764d6aa08c4f25\",\n                \"03568901b1f3fb8db05dd5c2092afc90671c3eb8a34b03f08bcfb6b20adf98f1cd\"\n            ],\n            [\n                \"030530ffe2e4a41312a41f708febab4408ca8e431ce382c1eedb837901839b550d\",\n                \"024d53412197fc609a6ca6997c6634771862f2808c155723fac03ea89a5379fdcc\"\n            ],\n            [\n                \"02de503d2081b523087ca195dbae55bafb27031a918a1cfedbd2c4c0da7d519902\",\n                \"03f4a27a98e41bddb7543bf81a9c53313bf9cfb2c2ebdb6bf96551221d8aecb01a\"\n            ],\n            [\n                \"03504bc595ac0d947299759871bfdcf46bcdd8a0590c44a78b8b69f1b152019418\",\n                \"0291f188301773dbc7c1d12e88e3aa86e6d4a88185a896f02852141e10e7e986ab\"\n            ],\n            [\n                \"0389c3ab262b7994d2202e163632a264f49dd5f78517e01c9210b6d0a29f524cd4\",\n                \"034bdfa9cc0c6896cb9488329d14903cfe60a2879771c5568adfc452f8dba1b2cb\"\n            ],\n            [\n                \"02c55a517c162aae2cb5b36eef78b51aa15040e7293033a5b55ba299e375da297d\",\n                \"027273faf29e922d95987a09c2554229becb857a68112bd139409eb111e7cdb45e\"\n            ],\n            [\n                \"02401e62d645dc64d43f77ba1f360b529a4c644ed3fc15b35932edafbaf741e844\",\n                \"02c44cbffc13cb53134354acd18c54c59fa78ec61307e147fa0f6f536fb030a675\"\n            ],\n            [\n                \"02194a538f37b388b2b138f73a37d7fbb9a3e62f6b5a00bad2420650adc4fb44d9\",\n                \"03e5cc15d47fcdcf815baa0e15227bc5e6bd8af6cae6add71f724e95bc29714ce5\"\n            ],\n            [\n                \"037ebf7b2029c8ea0c1861f98e0952c544a38b9e7caebbf514ff58683063cd0e78\",\n                \"022850577856c810dead8d3d44f28a3b71aaf21cdc682db1beb8056408b1d57d52\"\n            ],\n            [\n                \"02aea7537611754fdafd98f341c5a6827f8301eaf98f5710c02f17a07a8938a30e\",\n                \"032fa37659a8365fdae3b293a855c5a692faca687b0875e9720219f9adf4bdb6c2\"\n            ],\n            [\n                \"0224b0b8d200238495c58e1bc83afd2b57f9dbb79f9a1fdb40747bebb51542c8d3\",\n                \"03b88cd2502e62b69185b989abb786a57de27431ece4eabb26c934848d8426cbd6\"\n            ],\n            [\n                \"032802b0be2a00a1e28e1e29cfd2ad79d36ef936a0ef1c834b0bbe55c1b2673bff\",\n                \"032669b2d80f9110e49d49480acf696b74ecca28c21e7d9c1dd2743104c54a0b13\"\n            ],\n            [\n                \"03fcfa90eac92950dd66058bbef0feb153e05a114af94b6843d15200ef7cf9ea4a\",\n                \"023246268fbe8b9a023d9a3fa413f666853bbf92c4c0af47731fdded51751e0c3a\"\n            ],\n            [\n                \"020cf5fffe70b174e242f6193930d352c54109578024677c1a13ffce5e1f9e6a29\",\n                \"03cb996663b9c895c3e04689f0cf1473974023fa0d59416be2a0b01ccdaa3cc484\"\n            ],\n            [\n                \"03467e4fff9b33c73b0140393bde3b35a3f804bce79eccf9c53a1f76c59b7452bd\",\n                \"03251c2a041e953c8007d9ee838569d6be9eacfbf65857e875d87c32a8123036d8\"\n            ],\n            [\n                \"02192e19803bfa6f55748aada33f778f0ebb22a1c573e5e49cba14b6a431ef1c37\",\n                \"02224ce74f1ee47ba6eaaf75618ce2d4768a041a553ee5eb60b38895f3f6de11dc\"\n            ],\n            [\n                \"032679be8a73fa5f72d438d6963857bd9e49aef6134041ca950c70b017c0c7d44f\",\n                \"025a8463f1c68e85753bd2d37a640ab586d8259f21024f6173aeed15a23ad4287b\"\n            ],\n            [\n                \"03ab0355c95480f0157ae48126f893a6d434aa1341ad04c71517b104f3eda08d3d\",\n                \"02ba4aadba99ae8dc60515b15a087e8763496fcf4026f5a637d684d0d0f8a5f76c\"\n            ]\n        ]\n    },\n    \"seed_version\": 13,\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"2of2\",\n    \"winpos-qt\": [\n        523,\n        230,\n        840,\n        405\n    ],\n    \"x1/\": {\n        \"seed\": \"pudding sell evoke crystal try order supply chase fine drive nurse double\",\n        \"type\": \"bip32\",\n        \"xprv\": \"xprv9s21ZrQH143K2MK5erSSgeaPA1H7gENYS6grakohkaK2M4tzqo6XAjLoRPcBRW9NbGNpaZN3pdoSKLeiQJwmqdSi3GJWZLnK1Txpbn3zinV\",\n        \"xpub\": \"xpub661MyMwAqRbcEqPYksyT3nX7i37c5h6PoKcTP9DKJur1DsE9PLQmiXfHGe8RmN538Pj8t3qUQcZXCMrkS5z1uWJ6jf9EptAFbC4Z2nKaEQE\"\n    },\n    \"x2/\": {\n        \"type\": \"bip32\",\n        \"xprv\": null,\n        \"xpub\": \"xpub661MyMwAqRbcGYXvLgWjW91feK49GajmPdEarB3Ny8JDduUhzTcEThc8Xs1GyqMR4S7xPHvSq4sbDEFzQh3hjJJFEksUzvnjYnap5RX9o4j\"\n    }\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_7_18_seeded",
    "content": "{\n    \"addr_history\": {\n        \"12nzqpb4vxiFmcvypswSWK1f4cvGwhYAE8\": [],\n        \"13sapXcP5Wq25PiXh5Zr9mLhyjdfrppWyi\": [],\n        \"14EzC5y5eFCXg4T7cH4hXoivzysEpGXBTM\": [],\n        \"15PUQBi2eEzprCZrS8dkfXuoNv8TuqwoBm\": [],\n        \"16NvXzjxHbiNAULoRRTBjSmecMgF87FAtb\": [],\n        \"16oyPjLM4R96aZCnSHqBBkDMgbE2ehDWFe\": [],\n        \"1BfhL8ZPcaZkXTZKASQYcFJsPfXNwCwVMV\": [],\n        \"1Bn3vun14mDWBDkx4PvK2SyWK1nqB9MSmM\": [],\n        \"1BrCEnhf763JhVNcZsjGcNmmisBfRkrdcn\": [],\n        \"1BvXCwXAdaSTES4ENALv3Tw6TJcZbMzu5o\": [],\n        \"1C2vzgDyPqtvzFRYUgavoLvk3KGujkUUjg\": [],\n        \"1CN22zUHuX5SxGTmGvPTa2X6qiCJZjDUAW\": [],\n        \"1CUT9Su42c4MFxrfbrouoniuhVuvRjsKYS\": [],\n        \"1DLaXDPng4wWXW7AdDG3cLkuKXgEUpjFHq\": [],\n        \"1DTLcXN6xPUVXP1ZQmt2heXe2KHDSdvRNv\": [],\n        \"1F1zYJag8yXVnDgGGy7waQT3Sdyp7wLZm3\": [],\n        \"1Fim67c46NHTcSUu329uF8brTmkoiz6Ej8\": [],\n        \"1Go6JcgkfZuA7fyQFKuLddee9hzpo31uvL\": [],\n        \"1J6mhetXo9Eokq7NGjwbKnHryxUCpgbCDn\": [],\n        \"1K9sFmS7qM2P5JpVGQhHMqQgAnNiujS5jZ\": [],\n        \"1KBdFn9tGPYEqXnHyJAHxBfCQFF9v3mq95\": [],\n        \"1LRWRLWHE2pdMviVeTeJBa8nFbUTWSCvrg\": [],\n        \"1LpXAktoSKbRx7QFkyb2KkSNJXSGLtTg9T\": [],\n        \"1LtxCQLTqD1q5Q5BReP932t5D7pKx5wiap\": [],\n        \"1MX5AS3pA5jBhmg4DDuDQEuNhPGS4cGU4F\": [],\n        \"1Pz9bYFMeqZkXahx9yPjXtJwL69zB3xCp2\": []\n    },\n    \"keystore\": {\n        \"seed\": \"giraffe tuition frog desk airport rural since dizzy regular victory mind coconut\",\n        \"type\": \"bip32\",\n        \"xprv\": \"xprv9s21ZrQH143K28Jvnpm7hU3xPt18neaDpcpoMKTyi9ewNRg6puJ2RAE5gZNPQ73bbmU9WsagxLQ3a6i2t1M9W289HY9Q5sEzFsLaYq3ZQf3\",\n        \"xpub\": \"xpub661MyMwAqRbcEcPPtrJ84bzgwuqdC7J5BqkQ9hsbGVBvFE1FNScGxxYZXpC9ncowEe7EZVbAerSypw3wCjrmLmsHeG3RzySw5iEJhAfZaZT\"\n    },\n    \"pruned_txo\": {},\n    \"pubkeys\": {\n        \"change\": [\n            \"033e860b0823ed2bf143594b07031d9d95d35f6e4ad6093ddc3071b8d2760f133f\",\n            \"03f51e8798a1a46266dee899bada3e1517a7a57a8402deeef30300a8918c81889a\",\n            \"0308168b05810f62e3d08c61e3c545ccbdce9af603adbdf23dcc366c47f1c5634c\",\n            \"03d7eddff48be72310347efa93f6022ac261cc33ee0704cdad7b6e376e9f90f574\",\n            \"0287e34a1d3fd51efdc83f946f2060f13065e39e587c347b65a579b95ef2307d45\",\n            \"02df34e258a320a11590eca5f0cb0246110399de28186011e8398ce99dd806854a\"\n        ],\n        \"receiving\": [\n            \"031082ff400cbe517cc2ae37492a6811d129b8fb0a8c6bd083313f234e221527ae\",\n            \"03fac4d7402c0d8b290423a05e09a323b51afebd4b5917964ba115f48ab280ef07\",\n            \"03c0a8c4ab604634256d3cfa350c4b6ca294a4374193055195a46626a6adea920f\",\n            \"03b0bc3112231a9bea6f5382f4324f23b4e2deb5f01a90b0fe006b816367e43958\",\n            \"03a59c08c8e2d66523c888416e89fa1aaec679f7043aa5a9145925c7a80568e752\",\n            \"0346fefc07ab2f38b16c8d979a8ffe05bc9f31dd33291b4130797fa7d78f6e4a35\",\n            \"025eb34724546b3c6db2ee8b59fbc4731bafadac5df51bd9bbb20b456d550ef56e\",\n            \"02b79c26e2eac48401d8a278c63eec84dc5bef7a71fa7ce01a6e333902495272e2\",\n            \"03a3a212462a2b12dc33a89a3e85684f3a02a647db3d7eaae18c029a6277c4f8ac\",\n            \"02d13fc5b57c4d057accf42cc918912221c528907a1474b2c6e1b9ca24c9655c1a\",\n            \"023c87c3ca86f25c282d9e6b8583b0856a4888f46666b413622d72baad90a25221\",\n            \"030710e320e9911ebfc89a6b377a5c2e5ae0ab16b9a3df54baa9dbd3eb710bf03c\",\n            \"03406b5199d34be50725db2fcd440e487d13d1f7611e604db81bb06cdd9077ffa5\",\n            \"0378139461735db84ff4d838eb408b9c124e556cfb6bac571ed6b2d0ec671abd0c\",\n            \"030538379532c476f664d8795c0d8e5d29aea924d964c685ea5c2343087f055a82\",\n            \"02d1b93fa37b824b4842c46ef36e5c50aadbac024a6f066b482be382bec6b41e5a\",\n            \"02d64e92d12666cde831eb21e00079ecfc3c4f64728415cc38f899aca32f1a5558\",\n            \"0347480bf4d321f5dce2fcd496598fbdce19825de6ed5b06f602d66de7155ac1c0\",\n            \"03242e3dfd8c4b6947b0fbb0b314620c0c3758600bb842f0848f991e9a2520a81c\",\n            \"021acadf6300cb7f2cca11c6e1c7e59e3cf923a786f6371c3b85dd6f8b65c68470\"\n        ]\n    },\n    \"seed_version\": 13,\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        709,\n        314,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_7_18_trezor_singleacc",
    "content": "{\n    \"addr_history\": {\n        \"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC\": [],\n        \"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz\": [],\n        \"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM\": [],\n        \"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ\": [],\n        \"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6\": [],\n        \"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S\": [],\n        \"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH\": [],\n        \"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw\": [],\n        \"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb\": [],\n        \"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX\": [],\n        \"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ\": [],\n        \"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp\": [],\n        \"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk\": [],\n        \"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD\": [],\n        \"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp\": [],\n        \"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz\": [],\n        \"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs\": [],\n        \"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid\": [],\n        \"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu\": [],\n        \"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo\": [],\n        \"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj\": [],\n        \"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv\": [],\n        \"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC\": [],\n        \"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe\": [],\n        \"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL\": [],\n        \"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG\": []\n    },\n    \"keystore\": {\n        \"derivation\": \"m/44'/0'/0'\",\n        \"hw_type\": \"trezor\",\n        \"label\": \"trezor1\",\n        \"type\": \"hardware\",\n        \"xpub\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\"\n    },\n    \"pruned_txo\": {},\n    \"pubkeys\": {\n        \"change\": [\n            \"03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902\",\n            \"03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740\",\n            \"028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee\",\n            \"021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8\",\n            \"031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4\",\n            \"033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d\"\n        ],\n        \"receiving\": [\n            \"03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0\",\n            \"024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97\",\n            \"03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b\",\n            \"028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136\",\n            \"02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5\",\n            \"02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4\",\n            \"023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53\",\n            \"02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4\",\n            \"029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92\",\n            \"02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066\",\n            \"0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447\",\n            \"0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea\",\n            \"02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027\",\n            \"0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50\",\n            \"03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459\",\n            \"0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c\",\n            \"028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804\",\n            \"03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736\",\n            \"029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f\",\n            \"02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105\"\n        ]\n    },\n    \"seed_version\": 13,\n    \"stored_height\": 490013,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        631,\n        410,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_7_18_watchaddresses",
    "content": "{\n    \"addr_history\": {\n        \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\": [],\n        \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\": [],\n        \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\": []\n    },\n    \"addresses\": [\n        \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\",\n        \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\",\n        \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\"\n    ],\n    \"pruned_txo\": {},\n    \"seed_version\": 13,\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"verified_tx3\": {},\n    \"wallet_type\": \"imported\",\n    \"winpos-qt\": [\n        553,\n        402,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_8_3_importedkeys",
    "content": "{\n    \"addr_history\": {\n        \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\": [],\n        \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\": [],\n        \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\": []\n    },\n    \"addresses\": {\n        \"change\": [],\n        \"receiving\": [\n            \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\",\n            \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\",\n            \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\"\n        ]\n    },\n    \"keystore\": {\n        \"keypairs\": {\n            \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\": \"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\",\n            \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\": \"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\",\n            \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\": \"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"\n        },\n        \"type\": \"imported\"\n    },\n    \"pruned_txo\": {},\n    \"seed_version\": 13,\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        100,\n        100,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_8_3_importedkeys_flawed_previous_upgrade_from_2_7_18",
    "content": "{\n    \"addr_history\": {\n        \"15VBrfYwoXvDWyXHq1myxDv4h36qUmCHcE\": [],\n        \"179vRrzjT9k7k5oCNCx6eodYCaLKPy9UQn\": [],\n        \"18o6WCBWdAaM5kjKnyEL4HysoT324rvJu7\": [],\n        \"1A9F6ZEqmfKeuLeEq5eWFxajgiJfGCc7ar\": [],\n        \"1BTjGNUmeMSPBTuXTdwD3DLyCugAZaFb7w\": [],\n        \"1CjW4KM38acCRw3spiFKiZsj7xmmQqqwd8\": [],\n        \"1EaDNLPwHRraX1N3ecPWJ2mm7NRgdtvpCj\": [],\n        \"1PYtQBkjXHQX6YtMzEgehN638o784pK3ce\": [],\n        \"1yT2T4ha3i1GZoK2iP8EpcgSNG34R2ufM\": []\n    },\n    \"addresses\": {\n        \"change\": [],\n        \"receiving\": [\n            \"1PYtQBkjXHQX6YtMzEgehN638o784pK3ce\",\n            \"1yT2T4ha3i1GZoK2iP8EpcgSNG34R2ufM\",\n            \"1CjW4KM38acCRw3spiFKiZsj7xmmQqqwd8\",\n            \"1A9F6ZEqmfKeuLeEq5eWFxajgiJfGCc7ar\",\n            \"18o6WCBWdAaM5kjKnyEL4HysoT324rvJu7\",\n            \"1EaDNLPwHRraX1N3ecPWJ2mm7NRgdtvpCj\",\n            \"179vRrzjT9k7k5oCNCx6eodYCaLKPy9UQn\",\n            \"1BTjGNUmeMSPBTuXTdwD3DLyCugAZaFb7w\",\n            \"15VBrfYwoXvDWyXHq1myxDv4h36qUmCHcE\"\n        ]\n    },\n    \"keystore\": {\n        \"keypairs\": {\n            \"0206b77fd06f212ad7d85f4a054c231ba4e7894b1773dcbb449671ee54618ff5e9\": \"L52LWS2hB5ev9JYiisFewJH9Q16U7yYcSNt3M8UKLmL5p1q3v2H2\",\n            \"028cda4a0f03cbcbc695d9cac0858081fd5458acfd29564127d329553245afca42\": \"KzRhkN9Psm9BobcPx3X3VykVA8yhCBrVvE4tTyq6NE283sL6uvYG\",\n            \"02ba4117a24d7e38ae14c429fce0d521aa1fb6bb97558a13f1ef2bc0a476a1741f\": \"KySXfvidmMBf8iw6m3R9WtdfKcQPWXenwMZtpno5XpfLMNHH8PMn\",\n            \"031bb44462038b97010624a8f8cb15a10fd0d277f12aba3ccf5ce0d36fc6df3112\": \"KxmcmCvNrZFgy2jyz9W353XbMwCYWHzYTQVzbaDfZM4FLxemgmKh\",\n            \"0339081c4a0ce22c01aa78a5d025e7a109100d1a35ef0f8f06a0d4c5f9ffefc042\": \"L53Ks569m3H1dRzua3nGzBE3AaEV8dMvBoHDeSJGnWEDeL775mJ5\",\n            \"0339ea71aba2805238e636c2f1b3e5a8308b1dbdbb335787c51f2f6bf3f6218643\": \"KwHDUpfvnSC58bs3nGy7YpducXkbmo6UUHrydBHy6sT1mRJcVvBo\",\n            \"04e7dc460c87267cf0958d6904d9cd99a4af0d64d61858636aec7a02e5f9a578d27c1329d5ddc45a937130ed4a59e4147cb4907724321baa6a976f9972a17f79ba\": \"5JECca5E7r1eNgME7NsPdE29XiVCVwXSzEihnhAQXuMdsJ4VL8S\",\n            \"04e9ad0bf70c51c06c2459961175c47cfec59d58ebef4ffcd9836904ef11230afce03ab5eaac5958b538382195b5aea9bf057c0486079869bb72ef9c958f33f1ed\": \"5Jt9rGLWgxoJUo4eoYEECskLmRA4BkZqHPHg7DdghKBaWarKuxW\",\n            \"04f8cbd67830ab37138c92898a64a4edf836a60aa5b36956547788bd205c635d6a3056fa6a079961384ae336e737d4c45835821c8915dbc5e18a7def88df83946b\": \"5KRjCNThRDP8aQTJ3Hq9HUSVNRNUB2e69xwLfMUsrXYLXT7U8b9\"\n        },\n        \"type\": \"imported\"\n    },\n    \"pruned_txo\": {},\n    \"pubkeys\": {\n        \"change\": [],\n        \"receiving\": [\n            \"04e9ad0bf70c51c06c2459961175c47cfec59d58ebef4ffcd9836904ef11230afce03ab5eaac5958b538382195b5aea9bf057c0486079869bb72ef9c958f33f1ed\",\n            \"0339081c4a0ce22c01aa78a5d025e7a109100d1a35ef0f8f06a0d4c5f9ffefc042\",\n            \"0339ea71aba2805238e636c2f1b3e5a8308b1dbdbb335787c51f2f6bf3f6218643\",\n            \"02ba4117a24d7e38ae14c429fce0d521aa1fb6bb97558a13f1ef2bc0a476a1741f\",\n            \"028cda4a0f03cbcbc695d9cac0858081fd5458acfd29564127d329553245afca42\",\n            \"04e7dc460c87267cf0958d6904d9cd99a4af0d64d61858636aec7a02e5f9a578d27c1329d5ddc45a937130ed4a59e4147cb4907724321baa6a976f9972a17f79ba\",\n            \"04f8cbd67830ab37138c92898a64a4edf836a60aa5b36956547788bd205c635d6a3056fa6a079961384ae336e737d4c45835821c8915dbc5e18a7def88df83946b\"\n        ]\n    },\n    \"seed_version\": 13,\n    \"stored_height\": 492756,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        100,\n        100,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_8_3_multisig",
    "content": "{\n    \"addr_history\": {\n        \"32Qk6Q7XYD2v3et9g5fA97ky8XRAJNDZCS\": [],\n        \"339axnadPaQg3ngChNBKap2dndUWrSwjk6\": [],\n        \"34FG8qzA6UYLxrnkpVkM9mrGYix3ZyePJZ\": [],\n        \"35CR3h2dFF3EkRX5yK47NGuF2FcLtJvpUM\": [],\n        \"35zrocLBQbHfEqysgv2v5z3RH7BRGQzSMJ\": [],\n        \"36uBJPkgiQwav23ybewbgkQ2zEzJDY2EX1\": [],\n        \"37nSiBvGXm1PNYseymaJn5ERcU4mSMueYc\": [],\n        \"39r4XCmfU4J3N98YQ8Fwvm8VN1Fukfj7QW\": [],\n        \"3BDqFoYMxyy7nWCpRChYV6YCGh9qnWDmav\": [],\n        \"3CGCLSHU8ZjeXv6oukJ3eAQN4fqEQ7wuyX\": [],\n        \"3DCNnfh7oWLsnS3p5QdWfW3hvcFF8qAPFq\": [],\n        \"3DPheE9uany9ET2qBnWF1wh3zDtptGP6Ts\": [],\n        \"3EeNJHgSYVJPxYR2NaYv2M2ZnXkPRWSHQh\": [],\n        \"3FWZ7pJPxZhGr8p6HNr9LLsHA8sABcP7cF\": [],\n        \"3FZbzEF9HdRqzif2cKUFnwW9AFTJcibjVK\": [],\n        \"3GEhQHTrWykC6Jfu923qtpxJECsEGVdhUc\": [],\n        \"3HJ95uxwW6rMoEhYgUfcgpd3ExU3fjkfNb\": [],\n        \"3HbdMVgKRqadNiHRNGizUCyTQYpJ1aXFav\": [],\n        \"3J6xRF9d16QNsvoXkYkeTwTU8L5N3Y8f7c\": [],\n        \"3JBbS3GvhvoLgtLcuMvHCtqjE7dnbpTMkz\": [],\n        \"3KNWZasWDBuVzzp5Y5cbEgjeYn3NKHZKso\": [],\n        \"3KQ5tTEbkQSkKiccKFDPrhLnBjSMey6CQM\": [],\n        \"3KrFHcAzNJYjukGDDZm2HeV5Mok4NGQaD6\": [],\n        \"3LNZbX9wenL3bLxJTQnPidSvVt3EtDrnUg\": [],\n        \"3LzjAqqfiN8w4TSiW8Up7bKLD5CicBUC3a\": [],\n        \"3Nro51wauHugv72NMtY9pmLnwX3FXWU1eE\": []\n    },\n    \"addresses\": {\n        \"change\": [\n            \"34FG8qzA6UYLxrnkpVkM9mrGYix3ZyePJZ\",\n            \"3LzjAqqfiN8w4TSiW8Up7bKLD5CicBUC3a\",\n            \"3GEhQHTrWykC6Jfu923qtpxJECsEGVdhUc\",\n            \"3Nro51wauHugv72NMtY9pmLnwX3FXWU1eE\",\n            \"3JBbS3GvhvoLgtLcuMvHCtqjE7dnbpTMkz\",\n            \"3CGCLSHU8ZjeXv6oukJ3eAQN4fqEQ7wuyX\"\n        ],\n        \"receiving\": [\n            \"35zrocLBQbHfEqysgv2v5z3RH7BRGQzSMJ\",\n            \"3FWZ7pJPxZhGr8p6HNr9LLsHA8sABcP7cF\",\n            \"3DPheE9uany9ET2qBnWF1wh3zDtptGP6Ts\",\n            \"3HbdMVgKRqadNiHRNGizUCyTQYpJ1aXFav\",\n            \"3KQ5tTEbkQSkKiccKFDPrhLnBjSMey6CQM\",\n            \"35CR3h2dFF3EkRX5yK47NGuF2FcLtJvpUM\",\n            \"3HJ95uxwW6rMoEhYgUfcgpd3ExU3fjkfNb\",\n            \"3FZbzEF9HdRqzif2cKUFnwW9AFTJcibjVK\",\n            \"39r4XCmfU4J3N98YQ8Fwvm8VN1Fukfj7QW\",\n            \"3LNZbX9wenL3bLxJTQnPidSvVt3EtDrnUg\",\n            \"32Qk6Q7XYD2v3et9g5fA97ky8XRAJNDZCS\",\n            \"339axnadPaQg3ngChNBKap2dndUWrSwjk6\",\n            \"3EeNJHgSYVJPxYR2NaYv2M2ZnXkPRWSHQh\",\n            \"3BDqFoYMxyy7nWCpRChYV6YCGh9qnWDmav\",\n            \"3DCNnfh7oWLsnS3p5QdWfW3hvcFF8qAPFq\",\n            \"3KNWZasWDBuVzzp5Y5cbEgjeYn3NKHZKso\",\n            \"37nSiBvGXm1PNYseymaJn5ERcU4mSMueYc\",\n            \"3KrFHcAzNJYjukGDDZm2HeV5Mok4NGQaD6\",\n            \"36uBJPkgiQwav23ybewbgkQ2zEzJDY2EX1\",\n            \"3J6xRF9d16QNsvoXkYkeTwTU8L5N3Y8f7c\"\n        ]\n    },\n    \"pruned_txo\": {},\n    \"seed_version\": 13,\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"2of2\",\n    \"winpos-qt\": [\n        671,\n        238,\n        840,\n        405\n    ],\n    \"x1/\": {\n        \"seed\": \"property play install hill hunt follow trash comic pulse consider canyon limit\",\n        \"type\": \"bip32\",\n        \"xprv\": \"xprv9s21ZrQH143K46tCjDh5i4H9eSJpnMrYyLUbVZheTbNjiamdxPiffMEYLgxuYsMFokFrNEZ6S6z5wSXXszXaCVQWf6jzZvn14uYZhsnM9Sb\",\n        \"xpub\": \"xpub661MyMwAqRbcGaxfqFE65CDtCU9KBpaQLZQCHx7G1vuibP6nVw2vD9Z2Bz2DsH43bDZGXjmcvx2TD9wq3CmmFcoT96RCiDd1wMSUB2UH7Gu\"\n    },\n    \"x2/\": {\n        \"type\": \"bip32\",\n        \"xprv\": null,\n        \"xpub\": \"xpub661MyMwAqRbcEncvVc1zrPFZSKe7iAP1LTRhzxuXpmztu1kTtnfj8XNFzzmGH1X1gcGxczBZ3MmYKkxXgZKJCsNXXdasNaQJKJE4KcUjn1L\"\n    }\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_8_3_seeded",
    "content": "{\n    \"addr_history\": {\n        \"13sNgoAhqDUTB3YSzWYcKKvP2EczG5JGmt\": [],\n        \"14C6nXs2GRaK3o5U5e8dJSpVRCoqTsyAkJ\": [],\n        \"14fH7oRM4bqJtJkgJEynTShcUXQwdxH6mw\": [],\n        \"16FECc7nP2wor1ijXKihGofUoCkoJnq6XR\": [],\n        \"16cMJC5ZAtPnvLQBzfHm9YR9GoDxUseMEk\": [],\n        \"17CbQhK3gutqgWt2iLX69ZeSCvw8yFxPLz\": [],\n        \"17jEaAyekE8BHPvPmkJqFUh1v1GSi6ywoV\": [],\n        \"19F5SjaWYVCKMPWR8q1Freo4RGChSmFztL\": [],\n        \"19snysSPZEbgjmeMtuT7qDMTLH2fa7zrWW\": [],\n        \"1AFgvLGNHP3nZDNrZ4R2BZKnbwDVAEUP4q\": [],\n        \"1AwWgUbjQfRhKVLKm1o7qfpXnqeN3cu7Ms\": [],\n        \"1B4FU2WEd2NQzd2MkWBLHw87uJhBxoVghh\": [],\n        \"1BEBouVJFihDmEQMTAv4bNV2Q7dZh5iJzv\": [],\n        \"1BdB7ahc8TSR9RJDmWgGSgsWji2BgzcVvC\": [],\n        \"1DGhQ1up6dMieEwFdsQQFHRriyyR59rYVq\": [],\n        \"1HBAAqFVndXBcWdWQNYVYSDK9kdUu8ZRU3\": [],\n        \"1HMrRJkTayNRBZdXZKVb7oLZKj24Pq65T6\": [],\n        \"1HiB2QCfNem8b4cJaZ2Rt9T4BbUCPXvTpT\": [],\n        \"1HkbtbyocwHWjKBmzKmq8szv3cFgSGy7dL\": [],\n        \"1K5CWjgZEYcKTsJWeQrH6NcMPzFUAikD8z\": [],\n        \"1KMDUXdqpthH1XZU4q5kdSoMZmCW9yDMcN\": [],\n        \"1KmHNiNmeS7tWRLYTFDMrTbKR6TERYicst\": [],\n        \"1NQwmHYdxU1pFTTWyptn8vPW1hsSWJBRTn\": [],\n        \"1NuPofeK8yNEjtVAu9Rc2pKS9kw8YWUatL\": [],\n        \"1Q3eTNJWTnfxPkUJXQkeCqPh1cBQjjEXFn\": [],\n        \"1QEuVTdenchPn9naMhakYx8QwGUXE6JYp\": []\n    },\n    \"addresses\": {\n        \"change\": [\n            \"1K5CWjgZEYcKTsJWeQrH6NcMPzFUAikD8z\",\n            \"19snysSPZEbgjmeMtuT7qDMTLH2fa7zrWW\",\n            \"1DGhQ1up6dMieEwFdsQQFHRriyyR59rYVq\",\n            \"17CbQhK3gutqgWt2iLX69ZeSCvw8yFxPLz\",\n            \"1Q3eTNJWTnfxPkUJXQkeCqPh1cBQjjEXFn\",\n            \"17jEaAyekE8BHPvPmkJqFUh1v1GSi6ywoV\"\n        ],\n        \"receiving\": [\n            \"1KMDUXdqpthH1XZU4q5kdSoMZmCW9yDMcN\",\n            \"1HkbtbyocwHWjKBmzKmq8szv3cFgSGy7dL\",\n            \"1HiB2QCfNem8b4cJaZ2Rt9T4BbUCPXvTpT\",\n            \"14fH7oRM4bqJtJkgJEynTShcUXQwdxH6mw\",\n            \"1NuPofeK8yNEjtVAu9Rc2pKS9kw8YWUatL\",\n            \"16FECc7nP2wor1ijXKihGofUoCkoJnq6XR\",\n            \"19F5SjaWYVCKMPWR8q1Freo4RGChSmFztL\",\n            \"1NQwmHYdxU1pFTTWyptn8vPW1hsSWJBRTn\",\n            \"1HBAAqFVndXBcWdWQNYVYSDK9kdUu8ZRU3\",\n            \"1B4FU2WEd2NQzd2MkWBLHw87uJhBxoVghh\",\n            \"1HMrRJkTayNRBZdXZKVb7oLZKj24Pq65T6\",\n            \"1KmHNiNmeS7tWRLYTFDMrTbKR6TERYicst\",\n            \"1BdB7ahc8TSR9RJDmWgGSgsWji2BgzcVvC\",\n            \"14C6nXs2GRaK3o5U5e8dJSpVRCoqTsyAkJ\",\n            \"1AFgvLGNHP3nZDNrZ4R2BZKnbwDVAEUP4q\",\n            \"13sNgoAhqDUTB3YSzWYcKKvP2EczG5JGmt\",\n            \"1AwWgUbjQfRhKVLKm1o7qfpXnqeN3cu7Ms\",\n            \"1QEuVTdenchPn9naMhakYx8QwGUXE6JYp\",\n            \"1BEBouVJFihDmEQMTAv4bNV2Q7dZh5iJzv\",\n            \"16cMJC5ZAtPnvLQBzfHm9YR9GoDxUseMEk\"\n        ]\n    },\n    \"keystore\": {\n        \"seed\": \"novel clay width echo swing blanket absorb salute asset under ginger final\",\n        \"type\": \"bip32\",\n        \"xprv\": \"xprv9s21ZrQH143K2jfFF6ektPj6zCCsDGGjQxhD2FQ21j6yrA1piWWEjch2kf1smzB2rzm8rPkdJuHf3vsKqMX9ogtE2A7JF49qVUHrgtjRymM\",\n        \"xpub\": \"xpub661MyMwAqRbcFDjiM8BmFXfqYE3McizanBcopdoda4dxixLyG3pVHR1WbwgjLo9RL882KRfpfpxh7a7zXPogDdR4xj9TpJWJGsbwaodLSKe\"\n    },\n    \"pruned_txo\": {},\n    \"seed_type\": \"standard\",\n    \"seed_version\": 13,\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        100,\n        100,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_8_3_trezor_singleacc",
    "content": "{\n    \"addr_history\": {\n        \"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC\": [],\n        \"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz\": [],\n        \"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM\": [],\n        \"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ\": [],\n        \"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6\": [],\n        \"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S\": [],\n        \"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH\": [],\n        \"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw\": [],\n        \"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb\": [],\n        \"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX\": [],\n        \"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ\": [],\n        \"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp\": [],\n        \"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk\": [],\n        \"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD\": [],\n        \"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp\": [],\n        \"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz\": [],\n        \"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs\": [],\n        \"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid\": [],\n        \"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu\": [],\n        \"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo\": [],\n        \"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj\": [],\n        \"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv\": [],\n        \"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC\": [],\n        \"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe\": [],\n        \"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL\": [],\n        \"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG\": []\n    },\n    \"addresses\": {\n        \"change\": [\n            \"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ\",\n            \"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM\",\n            \"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG\",\n            \"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6\",\n            \"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL\",\n            \"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs\"\n        ],\n        \"receiving\": [\n            \"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu\",\n            \"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw\",\n            \"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH\",\n            \"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC\",\n            \"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ\",\n            \"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid\",\n            \"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz\",\n            \"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj\",\n            \"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz\",\n            \"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC\",\n            \"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo\",\n            \"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb\",\n            \"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe\",\n            \"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv\",\n            \"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp\",\n            \"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S\",\n            \"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX\",\n            \"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp\",\n            \"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk\",\n            \"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD\"\n        ]\n    },\n    \"keystore\": {\n        \"derivation\": \"m/44'/0'/0'\",\n        \"hw_type\": \"trezor\",\n        \"label\": \"trezor1\",\n        \"type\": \"hardware\",\n        \"xpub\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\"\n    },\n    \"pruned_txo\": {},\n    \"seed_version\": 13,\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        744,\n        390,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_8_3_watchaddresses",
    "content": "{\n    \"addr_history\": {\n        \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\": [],\n        \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\": [],\n        \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\": []\n    },\n    \"addresses\": [\n        \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\",\n        \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\",\n        \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\"\n    ],\n    \"pruned_txo\": {},\n    \"seed_version\": 13,\n    \"stored_height\": 0,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"verified_tx3\": {},\n    \"wallet_type\": \"imported\",\n    \"winpos-qt\": [\n        535,\n        380,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_9_3_importedkeys",
    "content": "{\n    \"addr_history\": {\n        \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\": [],\n        \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\": [],\n        \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\": []\n    },\n    \"addresses\": {\n        \"change\": [],\n        \"receiving\": [\n            \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\",\n            \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\",\n            \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\"\n        ]\n    },\n    \"keystore\": {\n        \"keypairs\": {\n            \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\": \"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\",\n            \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\": \"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\",\n            \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\": \"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"\n        },\n        \"type\": \"imported\"\n    },\n    \"pruned_txo\": {},\n    \"seed_version\": 13,\n    \"stored_height\": -1,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        100,\n        100,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_9_3_importedkeys_keystore_changes",
    "content": "{\n    \"addr_history\": {\n        \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\": [],\n        \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\": [],\n        \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\": []\n    },\n    \"addresses\": {\n        \"change\": [],\n        \"receiving\": [\n            \"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\",\n            \"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\",\n            \"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\"\n        ]\n    },\n    \"keystore\": {\n        \"keypairs\": {\n            \"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\": \"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\",\n            \"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\": \"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\",\n            \"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\": \"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"\n        },\n        \"type\": \"imported\"\n    },\n    \"pruned_txo\": {},\n    \"seed_version\": 13,\n    \"stored_height\": -1,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        100,\n        100,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_9_3_multisig",
    "content": "{\n    \"addr_history\": {\n        \"31uiqKhw4PQSmZWnCkqpeh6moB8B1jXEt3\": [],\n        \"32PBjkXmwRoEQt8HBZcAEUbNwaHw5dR5fe\": [],\n        \"33FQMD675LMRLZDLYLK7QV6TMYA1uYW1sw\": [],\n        \"33MQEs6TCgxmAJhZvUEXYr6gCkEoEYzUfm\": [],\n        \"33vuhs2Wor9Xkax66ucDkscPcU6nQHw8LA\": [],\n        \"35tbMt1qBGmy5RNcsdGZJgs7XVbf5gEgPs\": [],\n        \"36zhHEtGA33NjHJdxCMjY6DLeU2qxhiLUE\": [],\n        \"37rZuTsieKVpRXshwrY8qvFBn6me42mYr5\": [],\n        \"38A2KDXYRmRKZRRCGgazrj19i22kDr8d4V\": [],\n        \"38GZH5GhxLKi5so9Aka6orY2EDZkvaXdxm\": [],\n        \"3AEtxrCwiYv5Y5CRmHn1c5nZnV3Hpfh5BM\": [],\n        \"3AaHWprY1MytygvQVDLp6i63e9o5CwMSN5\": [],\n        \"3DAD19hHXNxAfZjCtUbWjZVxw1fxQqCbY7\": [],\n        \"3GK4CBbgwumoeR9wxJjr1QnfnYhGUEzHhN\": [],\n        \"3H18xmkyX3XAb5MwucqKpEhTnh3qz8V4Mn\": [],\n        \"3JhkakvHAyFvukJ3cyaVgiyaqjYNo2gmsS\": [],\n        \"3JtA4x1AKW4BR5YAEeLR5D157Nd92NHArC\": [],\n        \"3KQosfGFGsUniyqsidE2Y4Bz1y4iZUkGW6\": [],\n        \"3KXe1z2Lfk22zL6ggQJLpHZfc9dKxYV95p\": [],\n        \"3KZiENj4VHdUycv9UDts4ojVRsaMk8LC5c\": [],\n        \"3KeTKHJbkZN1QVkvKnHRqYDYP7UXsUu6va\": [],\n        \"3L5aZKtDKSd65wPLMRooNtWHkKd5Mz6E3i\": [],\n        \"3LAPqjqW4C2Se9HNziUhNaJQS46X1r9p3M\": [],\n        \"3P3JJPoyNFussuyxkDbnYevYim5XnPGmwZ\": [],\n        \"3PgNdMYSaPRymskby885DgKoTeA1uZr6Gi\": [],\n        \"3Pm7DaUzaDMxy2mW5WzHp1sE9hVWEpdf7J\": []\n    },\n    \"addresses\": {\n        \"change\": [\n            \"31uiqKhw4PQSmZWnCkqpeh6moB8B1jXEt3\",\n            \"3JhkakvHAyFvukJ3cyaVgiyaqjYNo2gmsS\",\n            \"3GK4CBbgwumoeR9wxJjr1QnfnYhGUEzHhN\",\n            \"3LAPqjqW4C2Se9HNziUhNaJQS46X1r9p3M\",\n            \"33MQEs6TCgxmAJhZvUEXYr6gCkEoEYzUfm\",\n            \"3AEtxrCwiYv5Y5CRmHn1c5nZnV3Hpfh5BM\"\n        ],\n        \"receiving\": [\n            \"3P3JJPoyNFussuyxkDbnYevYim5XnPGmwZ\",\n            \"33FQMD675LMRLZDLYLK7QV6TMYA1uYW1sw\",\n            \"3DAD19hHXNxAfZjCtUbWjZVxw1fxQqCbY7\",\n            \"3AaHWprY1MytygvQVDLp6i63e9o5CwMSN5\",\n            \"3H18xmkyX3XAb5MwucqKpEhTnh3qz8V4Mn\",\n            \"36zhHEtGA33NjHJdxCMjY6DLeU2qxhiLUE\",\n            \"37rZuTsieKVpRXshwrY8qvFBn6me42mYr5\",\n            \"38A2KDXYRmRKZRRCGgazrj19i22kDr8d4V\",\n            \"38GZH5GhxLKi5so9Aka6orY2EDZkvaXdxm\",\n            \"33vuhs2Wor9Xkax66ucDkscPcU6nQHw8LA\",\n            \"3L5aZKtDKSd65wPLMRooNtWHkKd5Mz6E3i\",\n            \"3KXe1z2Lfk22zL6ggQJLpHZfc9dKxYV95p\",\n            \"3KQosfGFGsUniyqsidE2Y4Bz1y4iZUkGW6\",\n            \"3KZiENj4VHdUycv9UDts4ojVRsaMk8LC5c\",\n            \"32PBjkXmwRoEQt8HBZcAEUbNwaHw5dR5fe\",\n            \"3KeTKHJbkZN1QVkvKnHRqYDYP7UXsUu6va\",\n            \"3JtA4x1AKW4BR5YAEeLR5D157Nd92NHArC\",\n            \"3PgNdMYSaPRymskby885DgKoTeA1uZr6Gi\",\n            \"3Pm7DaUzaDMxy2mW5WzHp1sE9hVWEpdf7J\",\n            \"35tbMt1qBGmy5RNcsdGZJgs7XVbf5gEgPs\"\n        ]\n    },\n    \"pruned_txo\": {},\n    \"seed_version\": 13,\n    \"stored_height\": 485855,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"2of2\",\n    \"winpos-qt\": [\n        617,\n        227,\n        840,\n        405\n    ],\n    \"x1/\": {\n        \"seed\": \"speed cruise market wasp ability alarm hold essay grass coconut tissue recipe\",\n        \"type\": \"bip32\",\n        \"xprv\": \"xprv9s21ZrQH143K48ig2wcAuZoEKaYdNRaShKFR3hLrgwsNW13QYRhXH6gAG1khxim6dw2RtAzF8RWbQxr1vvWUJFfEu2SJZhYbv6pfreMpuLB\",\n        \"xpub\": \"xpub661MyMwAqRbcGco98y9BGhjxscP7mtJJ4YB1r5kUFHQMNoNZ5y1mptze7J37JypkbrmBdnqTvSNzxL7cE1FrHg16qoj9S12MUpiYxVbTKQV\"\n    },\n    \"x2/\": {\n        \"type\": \"bip32\",\n        \"xprv\": null,\n        \"xpub\": \"xpub661MyMwAqRbcGrCDZaVs9VC7Z6579tsGvpqyDYZEHKg2MXoDkxhrWoukqvwDPXKdxVkYA6Hv9XHLETptfZfNpcJZmsUThdXXkTNGoBjQv1o\"\n    }\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_9_3_old_seeded_with_realistic_history",
    "content": "{\n    \"addr_history\": {\n        \"mfxZoA5dHaVn6QiHMbRwQTQUB7VFpZQFW4\": [\n            [\n                \"8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471\",\n                1446399\n            ]\n        ],\n        \"mgSZfzhK7VDj7MQRxjRLSC7FSESskDKsrN\": [],\n        \"mgaArVGf5heGUko1i24wYrvkfFcN442U4v\": [\n            [\n                \"76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4\",\n                1251084\n            ]\n        ],\n        \"mhBod4XkgRs3orfynhDGR7gLkDuD23tHKP\": [],\n        \"mhgErkPdC6BP5h6JAQ7nw2EfNerepQB8QL\": [],\n        \"mhwQ9cxhAxaED747XuzgUo3F39MDc6txHr\": [\n            [\n                \"8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471\",\n                1446399\n            ]\n        ],\n        \"mjUYaF2jPsTWYEUWidQiKuqnHDRh49Q95W\": [],\n        \"mjWMtS2NZvAcZLdHGanWrsVwnyYEtBr9si\": [],\n        \"mk8xgq5ubvpXQKFNNeMzYpy25G5zSQ6Vtc\": [],\n        \"mmL4aJtiAPVUQca9AaZZdPUht9FvS2eb4a\": [\n            [\n                \"e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25\",\n                1297404\n            ],\n            [\n                \"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306\",\n                1297610\n            ]\n        ],\n        \"mnFrzrrgj65VAynUzzhnuSkrrWwivtR7a2\": [],\n        \"mnQ53GF9oa4njpWswsnmUQ9A4Hif8ct86q\": [],\n        \"mo2ougmAzBmvQW5iJCojfi7n2Rt7RaRtGc\": [],\n        \"mp2CafXnWnN8rR6BnFToVQ8bXNY4jnAecr\": [\n            [\n                \"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306\",\n                1297610\n            ]\n        ],\n        \"mp9iZVBSUokAUX1p57Kjc4mHrtqqEhxjrh\": [\n            [\n                \"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67\",\n                1230168\n            ],\n            [\n                \"72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112\",\n                1230191\n            ]\n        ],\n        \"mpEGnkPKtMyfHo8EaUFks7xFZJdSgLjnC7\": [\n            [\n                \"3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308\",\n                1231594\n            ]\n        ],\n        \"mpHhM9knUvMLHjhy7kDjXuEKV72hM6FzuK\": [],\n        \"mpg6Y3SCPa8CSDYdzA7GEx855Zxg3ZhasV\": [],\n        \"mpmDNEsBXxRC55RpqKpqSEy37edNzSx9Cw\": [],\n        \"mq9piav8nf5yw9pJt4bsob7mpngw6EK8Bx\": [],\n        \"mqA4SNeHJtqUmxXWsmddweawM9DRRGVMuN\": [],\n        \"mqEq8T5ktH1E9RbVramCwAXnrUa8EdzZrk\": [],\n        \"mqSpdctdWV7gdiXWEJVgkt7yMoL8FSVtLB\": [],\n        \"mqYXSDuvMpdibxUy6ftKW1564L9UE5eeFX\": [\n            [\n                \"336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa\",\n                1242088\n            ],\n            [\n                \"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462\",\n                1242088\n            ]\n        ],\n        \"mqvJJCT4WHQHQvj7bvCNQDdHdr9WjCPMaH\": [],\n        \"mrHCUTD63vsP9K3oon2AWZ9bKjqDhd5PMm\": [\n            [\n                \"475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d\",\n                1281357\n            ],\n            [\n                \"e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25\",\n                1297404\n            ]\n        ],\n        \"mrdUepR2frXufJH5zeHEQZRVbYiAjVNufo\": [],\n        \"msgSKRJY9y8GPFsDpsnso23RQbEWFY2DJL\": [],\n        \"mt2ijtY7BUkQRpErd99N9131YgsEL7oBSt\": [\n            [\n                \"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968\",\n                1231594\n            ],\n            [\n                \"3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308\",\n                1231594\n            ]\n        ],\n        \"mtPuWkAj3cYHj4xoDYYL4yJ5tTckeXb6go\": [],\n        \"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd\": [\n            [\n                \"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d\",\n                1230163\n            ],\n            [\n                \"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67\",\n                1230168\n            ],\n            [\n                \"6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254\",\n                1230626\n            ],\n            [\n                \"31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f\",\n                1230627\n            ],\n            [\n                \"9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb\",\n                1255526\n            ]\n        ],\n        \"muwtzEGtaACt6KrLxwdu8itbfsKo8WerW7\": [],\n        \"mvJ2vrvNySAXWxVYuCn2tX6amyfchFGHvK\": [\n            [\n                \"c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a\",\n                1251074\n            ],\n            [\n                \"475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d\",\n                1281357\n            ]\n        ],\n        \"mvXwR94pXVgP7kdrW3vTiDQtVrkP3NY3zn\": [\n            [\n                \"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462\",\n                1242088\n            ]\n        ],\n        \"mvbi7ywmFLL8FLrcn7XEYATbn1kBeeNbzx\": [],\n        \"mwNWMKSGou8ZJzXgfDaAUy1C8Jip3TEmdf\": [\n            [\n                \"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306\",\n                1297610\n            ],\n            [\n                \"8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471\",\n                1446399\n            ]\n        ],\n        \"mweezhmgY1kEvmSNpfrqdLcd6NHekupXKp\": [\n            [\n                \"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462\",\n                1242088\n            ],\n            [\n                \"2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd\",\n                1251055\n            ]\n        ],\n        \"mwzAXyVfyG5mcGKAQANs67M3HhENmu2Uh2\": [],\n        \"mx1KFACmoAA2EedMAcoQ4ed5dRtsh3ow4L\": [],\n        \"mxTwRCnJgafjVaUVLsniWZPpXhz5dFzRyb\": [\n            [\n                \"7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a\",\n                1230624\n            ],\n            [\n                \"6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254\",\n                1230626\n            ]\n        ],\n        \"myJLELfyhG1Fu7iPyfpHfviVCQuLwsNCBm\": [\n            [\n                \"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d\",\n                1230163\n            ],\n            [\n                \"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67\",\n                1230168\n            ]\n        ],\n        \"mycxtgambuph71Hg6MdwRoF7c7GkFvHiZ6\": [\n            [\n                \"c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9\",\n                1323966\n            ],\n            [\n                \"8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471\",\n                1446399\n            ]\n        ],\n        \"myunReLavjcSN8mWUn3jqhirHWYiok51jU\": [],\n        \"myvhXvymTD4Ncgfg8r2WXiTZeDY3TtZGUY\": [\n            [\n                \"56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216\",\n                1231595\n            ],\n            [\n                \"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462\",\n                1242088\n            ]\n        ],\n        \"mywTRsN56GipUEbmCnoUV9YFdrUU5CmUxd\": [\n            [\n                \"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67\",\n                1230168\n            ],\n            [\n                \"72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112\",\n                1230191\n            ]\n        ],\n        \"mzeE9KrQrsfqYAuTN5EJXcs91rUJU8Y8Bb\": [],\n        \"mzrXKAWzbctb6Ee1LkbXLmdsNhPtLucUkw\": [],\n        \"n1Hjparfsp2c4yCZ72KbotNcY84XLS73jj\": [],\n        \"n1gdKukb5TUu37x5GahHsp4Gp2fdowdZPH\": [\n            [\n                \"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968\",\n                1231594\n            ],\n            [\n                \"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462\",\n                1242088\n            ]\n        ],\n        \"n1yXnvBc7giip11h2D2NX3azXqhasAeFeM\": [],\n        \"n23NSQfgAmVaW1qE1kgnxkW8JvWfveAktH\": [\n            [\n                \"3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308\",\n                1231594\n            ]\n        ],\n        \"n3JYuxqhKja3QcJG3sm4yKTmZUBpTbVQyP\": [],\n        \"n4FfEQrf1PS3no7FCPhhDugxqgR4fUSvdX\": [\n            [\n                \"2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d\",\n                1231593\n            ],\n            [\n                \"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968\",\n                1231594\n            ]\n        ],\n        \"n4g4z5GRAWXXcx5f3m7Cyyek9LRRPHcuJy\": []\n    },\n    \"addresses\": {\n        \"change\": [\n            \"mywTRsN56GipUEbmCnoUV9YFdrUU5CmUxd\",\n            \"mt2ijtY7BUkQRpErd99N9131YgsEL7oBSt\",\n            \"n23NSQfgAmVaW1qE1kgnxkW8JvWfveAktH\",\n            \"mvXwR94pXVgP7kdrW3vTiDQtVrkP3NY3zn\",\n            \"mrHCUTD63vsP9K3oon2AWZ9bKjqDhd5PMm\",\n            \"mmL4aJtiAPVUQca9AaZZdPUht9FvS2eb4a\",\n            \"mp2CafXnWnN8rR6BnFToVQ8bXNY4jnAecr\",\n            \"mhwQ9cxhAxaED747XuzgUo3F39MDc6txHr\",\n            \"n1yXnvBc7giip11h2D2NX3azXqhasAeFeM\",\n            \"mpmDNEsBXxRC55RpqKpqSEy37edNzSx9Cw\",\n            \"msgSKRJY9y8GPFsDpsnso23RQbEWFY2DJL\",\n            \"mhBod4XkgRs3orfynhDGR7gLkDuD23tHKP\",\n            \"mvbi7ywmFLL8FLrcn7XEYATbn1kBeeNbzx\",\n            \"mzeE9KrQrsfqYAuTN5EJXcs91rUJU8Y8Bb\"\n        ],\n        \"receiving\": [\n            \"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd\",\n            \"myJLELfyhG1Fu7iPyfpHfviVCQuLwsNCBm\",\n            \"mp9iZVBSUokAUX1p57Kjc4mHrtqqEhxjrh\",\n            \"n4FfEQrf1PS3no7FCPhhDugxqgR4fUSvdX\",\n            \"n1gdKukb5TUu37x5GahHsp4Gp2fdowdZPH\",\n            \"mpEGnkPKtMyfHo8EaUFks7xFZJdSgLjnC7\",\n            \"myvhXvymTD4Ncgfg8r2WXiTZeDY3TtZGUY\",\n            \"mqYXSDuvMpdibxUy6ftKW1564L9UE5eeFX\",\n            \"mweezhmgY1kEvmSNpfrqdLcd6NHekupXKp\",\n            \"mvJ2vrvNySAXWxVYuCn2tX6amyfchFGHvK\",\n            \"mxTwRCnJgafjVaUVLsniWZPpXhz5dFzRyb\",\n            \"mgaArVGf5heGUko1i24wYrvkfFcN442U4v\",\n            \"mycxtgambuph71Hg6MdwRoF7c7GkFvHiZ6\",\n            \"mgSZfzhK7VDj7MQRxjRLSC7FSESskDKsrN\",\n            \"mwNWMKSGou8ZJzXgfDaAUy1C8Jip3TEmdf\",\n            \"mqSpdctdWV7gdiXWEJVgkt7yMoL8FSVtLB\",\n            \"n4g4z5GRAWXXcx5f3m7Cyyek9LRRPHcuJy\",\n            \"mqvJJCT4WHQHQvj7bvCNQDdHdr9WjCPMaH\",\n            \"n1Hjparfsp2c4yCZ72KbotNcY84XLS73jj\",\n            \"mfxZoA5dHaVn6QiHMbRwQTQUB7VFpZQFW4\",\n            \"mjWMtS2NZvAcZLdHGanWrsVwnyYEtBr9si\",\n            \"mhgErkPdC6BP5h6JAQ7nw2EfNerepQB8QL\",\n            \"mx1KFACmoAA2EedMAcoQ4ed5dRtsh3ow4L\",\n            \"myunReLavjcSN8mWUn3jqhirHWYiok51jU\",\n            \"mrdUepR2frXufJH5zeHEQZRVbYiAjVNufo\",\n            \"mk8xgq5ubvpXQKFNNeMzYpy25G5zSQ6Vtc\",\n            \"mqEq8T5ktH1E9RbVramCwAXnrUa8EdzZrk\",\n            \"mq9piav8nf5yw9pJt4bsob7mpngw6EK8Bx\",\n            \"mpHhM9knUvMLHjhy7kDjXuEKV72hM6FzuK\",\n            \"mwzAXyVfyG5mcGKAQANs67M3HhENmu2Uh2\",\n            \"mnQ53GF9oa4njpWswsnmUQ9A4Hif8ct86q\",\n            \"mpg6Y3SCPa8CSDYdzA7GEx855Zxg3ZhasV\",\n            \"mo2ougmAzBmvQW5iJCojfi7n2Rt7RaRtGc\",\n            \"mzrXKAWzbctb6Ee1LkbXLmdsNhPtLucUkw\",\n            \"mnFrzrrgj65VAynUzzhnuSkrrWwivtR7a2\",\n            \"muwtzEGtaACt6KrLxwdu8itbfsKo8WerW7\",\n            \"n3JYuxqhKja3QcJG3sm4yKTmZUBpTbVQyP\",\n            \"mqA4SNeHJtqUmxXWsmddweawM9DRRGVMuN\",\n            \"mjUYaF2jPsTWYEUWidQiKuqnHDRh49Q95W\",\n            \"mtPuWkAj3cYHj4xoDYYL4yJ5tTckeXb6go\"\n        ]\n    },\n    \"keystore\": {\n        \"mpk\": \"e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3\",\n        \"seed\": \"acb740e454c3134901d7c8f16497cc1c\",\n        \"type\": \"old\"\n    },\n    \"pruned_txo\": {},\n    \"seed_type\": \"old\",\n    \"seed_version\": 13,\n    \"stored_height\": 1482542,\n    \"transactions\": {\n        \"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67\": \"01000000029d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2000000008b483045022100a146a2078a318c1266e42265a369a8eef8993750cb3faa8dd80754d8d541d5d202207a6ab8864986919fd1a7fd5854f1e18a8a0431df924d7a878ec3dc283e3d75340141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfeffffff9d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2010000008a47304402201c7fa37b74a915668b0244c01f14a9756bbbec1031fb69390bcba236148ab37e02206151581f9aa0e6758b503064c1e661a726d75c6be3364a5a121a8c12cf618f64014104dc28da82e141416aaf771eb78128d00a55fdcbd13622afcbb7a3b911e58baa6a99841bfb7b99bcb7e1d47904fda5d13fdf9675cdbbe73e44efcc08165f49bac6feffffff02b0183101000000001976a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac005a6202000000001976a9145eb4eeaefcf9a709f8671444933243fbd05366a388ac54c51200\",\n        \"2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d\": \"010000000132201ff125888a326635a2fc6e971cd774c4d0c1a757d742d0f6b5b020f7203a050000006a47304402201d20bb5629a35b84ff9dd54788b98e265623022894f12152ac0e6158042550fe02204e98969e1f7043261912dd0660d3da64e15acf5435577fc02a00eccfe76b323f012103a336ad86546ab66b6184238fe63bb2955314be118b32fa45dd6bd9c4c5875167fdffffff0254959800000000001976a9148d2db0eb25b691829a47503006370070bc67400588ac80969800000000001976a914f96669095e6df76cfdf5c7e49a1909f002e123d088ace8ca1200\",\n        \"2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd\": \"01000000036cdf8d2226c57d7cc8485636d8e823c14790d5f24e6cf38ba9323babc7f6db2901000000171600143fc0dbdc2f939c322aed5a9c3544468ec17f5c3efdffffff507dce91b2a8731636e058ccf252f02b5599489b624e003435a29b9862ccc38c0200000017160014c50ff91aa2a790b99aa98af039ae1b156e053375fdffffff6254162cf8ace3ddfb3ec242b8eade155fa91412c5bde7f55decfac5793743c1010000008b483045022100de9599dcd7764ca8d4fcbe39230602e130db296c310d4abb7f7ae4d139c4d46402200fbfd8e6dc94d90afa05b0c0eab3b84feb465754db3f984fbf059447282771c30141045eecefd39fabba7b0098c3d9e85794e652bdbf094f3f85a3de97a249b98b9948857ea1e8209ee4f196a6bbcfbad103a38698ee58766321ba1cdee0cbfb60e7b2fdffffff01e85af70100000000160014e8d29f07cd5f813317bec4defbef337942d85d74ed161300\",\n        \"31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f\": \"010000000454022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a000000008b483045022100ea8fe74db2aba23ad36ac66aaa481bad2b4d1b3c331869c1d60a28ce8cfad43c02206fa817281b33fbf74a6dd7352bdc5aa1d6d7966118a4ad5b7e153f37205f1ae80141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a01000000171600146dfe07e12af3db7c715bf1c455f8517e19c361e7fdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a020000006a47304402200b1fb89e9a772a8519294acd61a53a29473ce76077165447f49a686f1718db5902207466e2e8290f84114dc9d6c56419cb79a138f03d7af8756de02c810f19e4e03301210222bfebe09c2638cfa5aa8223fb422fe636ba9675c5e2f53c27a5d10514f49051fdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a0300000000fdffffff018793140d000000001600144b3e27ddf4fc5f367421ee193da5332ef351b70022c71200\",\n        \"336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa\": \"010000000232201ff125888a326635a2fc6e971cd774c4d0c1a757d742d0f6b5b020f7203a020000006a4730440220198c0ba2b2aefa78d8cca01401d408ecdebea5ac05affce36f079f6e5c8405ca02200eabb1b9a01ff62180cf061dfacedba6b2e07355841b9308de2d37d83489c7b80121031c663e5534fe2a6de816aded6bb9afca09b9e540695c23301f772acb29c64a05fdfffffffb28ff16811d3027a2405be68154be8fdaff77284dbce7a2314c4107c2c941600000000000fdffffff015e104f01000000001976a9146dfd56a0b5d0c9450d590ad21598ecfeaa438bd788ac79f31200\",\n        \"3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308\": \"010000000168091e76227e99b098ef8d6d5f7c1bb2a154dd49103b93d7b8d7408d49f07be0000000008a47304402202f683a63af571f405825066bd971945a35e7142a75c9a5255d364b25b7115d5602206c59a7214ae729a519757e45fdc87061d357813217848cf94df74125221267ac014104aecb9d427e10f0c370c32210fe75b6e72ccc4f415076cf1a6318fbed5537388862c914b29269751ab3a04962df06d96f5f4f54e393a0afcbfa44b590385ae61afdffffff0240420f00000000001976a9145f917fd451ca6448978ebb2734d2798274daf00b88aca8063d00000000001976a914e1232622a96a04f5e5a24ca0792bb9c28b089d6e88ace9ca1200\",\n        \"475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d\": \"01000000013a7e6f19a963adc7437d2f3eb0936f1fc9ef4ba7e083e19802eb1111525a59c2000000008b483045022100958d3931051306489d48fe69b32561e0a16e82a2447c07be9d1069317084b5e502202f70c2d9be8248276d334d07f08f934ffeea83977ad241f9c2de954a2d577f94014104d950039cec15ad10ad4fb658873bc746148bc861323959e0c84bf10f8633104aa90b64ce9f80916ab0a4238e025dcddf885b9a2dd6e901fe043a433731db8ab4fdffffff02a086010000000000160014bbfab2cc3267cea2df1b68c392cb3f0294978ca922940d00000000001976a914760f657c67273a06cad5b1d757a95f4ed79f5a4b88ac4c8d1300\",\n        \"56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216\": \"0100000001614b142aeeb827d35d2b77a5b11f16655b6776110ddd9f34424ff49d85706cf90200000000fdffffff02784a4c00000000001600148464f47f35cbcda2e4e5968c5a3a862c43df65a1404b4c00000000001976a914c9efecf0ecba8b42dce0ae2b28e3ea0573d351c988ace9ca1200\",\n        \"6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254\": \"010000000496941b9f18710b39bacde890e39a7fa401e6bf49985857cb7adfb8a45147ef1e000000001716001441aec99157d762708339d7faf7a63a8c479ed84cfdffffff96941b9f18710b39bacde890e39a7fa401e6bf49985857cb7adfb8a45147ef1e0100000000fdffffff1a5d1e4ca513983635b0df49fd4f515c66dd26d7bff045cfbd4773aa5d93197f000000006a4730440220652145460092ef42452437b942cb3f563bf15ad90d572d0b31d9f28449b7a8dd022052aae24f58b8f76bd2c9cf165cc98623f22870ccdbef1661b6dbe01c0ef9010f01210375b63dd8e93634bbf162d88b25d6110b5f5a9638f6fe080c85f8b21c2199a1fdfdffffff1a5d1e4ca513983635b0df49fd4f515c66dd26d7bff045cfbd4773aa5d93197f010000008a47304402207517c52b241e6638a84b05385e0b3df806478c2e444f671ca34921f6232ee2e70220624af63d357b83e3abe7cdf03d680705df0049ec02f02918ee371170e3b4a73d014104de408e142c00615294813233cdfe9e7774615ae25d18ba4a1e3b70420bb6666d711464518457f8b947034076038c6f0cfc8940d85d3de0386e0ad88614885c7cfdffffff0480969800000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac809698000000000017a914f2a76207d7b54bd34282281205923841341d9e1f87002d3101000000001976a914b8d4651937cd7db5bcf5fc98e6d2d8cfa131e85088ac743db20a00000000160014c7d0df09e03173170aed0247243874c6872748ed20c71200\",\n        \"72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112\": \"0100000002677b2113f26697718c8991823ec0e637f08cb61426da8da508b97449c872490f000000008b4830450221009c50c0f56f34781dfa7b3d540ac724436c67ffdc2e5b2d5a395c9ebf72116ef802205a94a490ea14e4824f36f1658a384aeaecadd54839600141eb20375a49d476d1014104c291245c2ee3babb2a35c39389df56540867f93794215f743b9aa97f5ba114c4cdee8d49d877966728b76bc649bb349efd73adef1d77452a9aac26f8c51ae1ddfdffffff677b2113f26697718c8991823ec0e637f08cb61426da8da508b97449c872490f010000008b483045022100ae0b286493491732e7d3f91ab4ac4cebf8fe8a3397e979cb689e62d350fdcf2802206cf7adf8b29159dd797905351da23a5f6dab9b9dbf5028611e86ccef9ff9012e014104c62c4c4201d5c6597e5999f297427139003fdb82e97c2112e84452d1cfdef31f92dd95e00e4d31a6f5f9af0dadede7f6f4284b84144e912ff15531f36358bda7fdffffff019f7093030000000022002027ce908c4ee5f5b76b4722775f23e20c5474f459619b94040258290395b88afb6ec51200\",\n        \"76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4\": \"0100000001f4ba9948cdc4face8315c7f0819c76643e813093ffe9fbcf83d798523c7965db000000006a473044022061df431a168483d144d4cffe1c5e860c0a431c19fc56f313a899feb5296a677c02200208474cc1d11ad89b9bebec5ec00b1e0af0adaba0e8b7f28eed4aaf8d409afb0121039742bf6ab70f12f6353e9455da6ed88f028257950450139209b6030e89927997fdffffff01d4f84b00000000001976a9140b93db89b6bf67b5c2db3370b73d806f458b3d0488ac0a171300\",\n        \"7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a\": \"0100000002681b6a8dd3a406ee10e4e4aece3c2e69f6680c02f53157be6374c5c98322823a00000000232200209adfa712053a06cc944237148bcefbc48b16eb1dbdc43d1377809bcef1bea9affdffffff681b6a8dd3a406ee10e4e4aece3c2e69f6680c02f53157be6374c5c98322823a0100000023220020f40ed2e3fbffd150e5b74f162c3ce5dae0dfeba008a7f0f8271cf1cf58bfb442fdffffff02801d2c04000000001976a9140cc01e19090785d629cdcc98316f328df554de4f88ac6d455d05000000001976a914b9e828990a8731af4527bcb6d0cddf8d5ffe90ce88ac1fc71200\",\n        \"8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471\": \"0100000002b9480e55d5afe8f63565c9bd9ff9eb304dd881ba9c5634a87d114c5c5983fac1000000008b483045022100acc3f465902feed13f7358626003c517b2b659007b8876e401ee6933c034b7f80220309eae30631d444a3fca218edbae04e6e2f3492958f31bb8b8f762c02974e671014104d2416bae1a485b6e1ef78d30b41ce1acff13da94f53897c3847f004bc5f87237f53e2fc8d56073cd7a3d3932b2a10eff9cc5e4a4da52f1ad445806c1f8b986e0fdffffff06a38bc8fab255df2662059889fc0ba6ebf873eab591165607b5936634e753e4000000008b48304502210099e11a4e861963e50b2536bfd3d0d70e5faf17e758717a1d36e78973638ba8f802204435f2b0664e0bc95c54d21520696391911d6aaa11dfcbc613c51f594e884680014104861473a447374a30387cca4548dd6462d9526b44beb1029b5f074a5fa8f09e01d21dbae1599aae3f9221e5ed830df7bae69caf4565e50471e34d51801d9a588afdffffff02f0230000000000001976a9141a8fd125d7d6728c0d84bd7b9f6f16442eef776988aca0860100000000001976a91404d808d10acfd7f6dcf81f88912ccf7285ed447688acfe111600\",\n        \"9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb\": \"01000000013409c10fd732d9e4b3a9a1c4beb511fa5eb32bc51fd169102a21aa8519618f800000000000fdffffff0640420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac40420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac40420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac80841e00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac64064a000000000016001469825d422ca80f2a5438add92d741c7df45211f280969800000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac65281300\",\n        \"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d\": \"010000000400899af3606e93106a5d0f470e4e2e480dfc2fd56a7257a1f0f4d16fd5961a0f000000006a47304402205b32a834956da303f6d124e1626c7c48a30b8624e33f87a2ae04503c87946691022068aa7f936591fb4b3272046634cf526e4f8a018771c38aff2432a021eea243b70121034bb61618c932b948b9593d1b506092286d9eb70ea7814becef06c3dfcc277d67fdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753000000006b483045022100de775a580c6cb47061d5a00c6739033f468420c5719f9851f32c6992610abd3902204e6b296e812bb84a60c18c966f6166718922780e6344f243917d7840398eb3db0121025d7317c6910ad2ad3d29a748c7796ddf01e4a8bc5e3bf2a98032f0a20223e4aafdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753010000006a4730440220615a26f38bf6eb7043794c08fb81f273896b25783346332bec4de8dfaf7ed4d202201c2bc4515fc9b07ded5479d5be452c61ce785099f5e33715e9abd4dbec410e11012103caa46fcb1a6f2505bf66c17901320cc2378057c99e35f0630c41693e97ebb7cffdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753030000006b483045022100c8fba762dc50041ee3d5c7259c01763ed913063019eefec66678fb8603624faa02200727783ccbdbda8537a6201c63e30c0b2eb9afd0e26cb568d885e6151ef2a8540121027254a862a288cfd98853161f575c49ec0b38f79c3ef0bf1fb89986a3c36a8906fdffffff0240787d01000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac3bfc1502000000001976a914c30f2af6a79296b6531bf34dba14c8419be8fb7d88ac52c51200\",\n        \"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462\": \"0100000003aabec9cb99096073ae47cfb84bfd5b0063ae7f157956fd37c5d1a79d74ee6e33000000008b4830450221008136fc880d5e24fdd9d2a43f5085f374fef013b814f625d44a8075104981d92a0220744526ec8fc7887c586968f22403f0180d54c9b7ff8db9b553a3c4497982e8250141047b8b4c91c5a93a1f2f171c619ca41770427aa07d6de5130c3ba23204b05510b3bd58b7a1b35b9c4409104cfe05e1677fc8b51c03eac98b206e5d6851b31d2368fdffffff16d23bdc750c7023c085a6fc76e3e468944919783535ea2c13826f181058a656010000008a47304402204148410f2d796b1bb976b83904167d28b65dcd7c21b3876022b4fa70abc86280022039ea474245c3dc8cd7e5a572a155df7a6a54496e50c73d9fed28e76a1cf998c00141044702781daed201e35aa07e74d7bda7069e487757a71e3334dc238144ad78819de4120d262e8488068e16c13eea6092e3ab2f729c13ef9a8c42136d6365820f7dfdffffff68091e76227e99b098ef8d6d5f7c1bb2a154dd49103b93d7b8d7408d49f07be0010000008b4830450221008228af51b61a4ee09f58b4a97f204a639c9c9d9787f79b2fc64ea54402c8547902201ed81fca828391d83df5fbd01a3fa5dd87168c455ed7451ba8ccb5bf06942c3b0141046fcdfab26ac08c827e68328dbbf417bbe7577a2baaa5acc29d3e33b3cc0c6366df34455a9f1754cb0952c48461f71ca296b379a574e33bcdbb5ed26bad31220bfdffffff0210791c00000000001976a914a4b991e7c72996c424fe0215f70be6aa7fcae22c88ac80c3c901000000001976a914b0f6e64ea993466f84050becc101062bb502b4e488ac7af31200\",\n        \"c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9\": \"01000000014f86fabb180a955cd9f304aca1917d5dc08f1fbf0be501d1fc2c76ab60d5f56e0000000000fdffffff01a6250000000000001976a914c695421e5fe3cf96c75410ed160418dbda96dbc588acbd331400\",\n        \"c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a\": \"01000000018557003cb450f53922f63740f0f77db892ef27e15b2614b56309bfcee96a0ad3010000006a473044022041923c905ae4b5ed9a21aa94c60b7dbcb8176d58d1eb1506d9fb1e293b65ce01022015d6e9d2e696925c6ad46ce97cc23dec455defa6309b839abf979effc83b8b160121029332bf6bed07dcca4be8a5a9d60648526e205d60c75a21291bffcdefccafdac3fdffffff01c01c0f00000000001976a914a2185918aa1006f96ed47897b8fb620f28a1b09988ac01171300\",\n        \"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968\": \"01000000016d445091b7b4fa19cbbee30141071b2202d0c27d195b9d6d2bcc7085c9cd9127010000008b483045022100daf671b52393af79487667eddc92ebcc657e8ae743c387b25d1c1a2e19c7a4e7022015ef2a52ea7e94695de8898821f9da539815775516f18329896e5fc52a3563b30141041704a3daafaace77c8e6e54cf35ed27d0bf9bb8bcd54d1b955735ff63ec54fe82a80862d455c12e739108b345d585014bf6aa0cbd403817c89efa18b3c06d6b5fdffffff02144a4c00000000001976a9148942ac692ace81019176c4fb0ac408b18b49237f88ac404b4c00000000001976a914dd36d773acb68ac1041bc31b8a40ee504b164b2e88ace9ca1200\",\n        \"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306\": \"010000000125af87b0c2ebb9539d644e97e6159ccb8e1aa80fe986d01f60d2f3f37f207ae8010000008b483045022100baed0747099f7b28a5624005d50adf1069120356ac68c471a56c511a5bf6972b022046fbf8ec6950a307c3c18ca32ad2955c559b0d9bbd9ec25b64f4806f78cadf770141041ea9afa5231dc4d65a2667789ebf6806829b6cf88bfe443228f95263730b7b70fb8b00b2b33777e168bcc7ad8e0afa5c7828842794ce3814c901e24193700f6cfdffffff02a0860100000000001976a914ade907333744c953140355ff60d341cedf7609fd88ac68830a00000000001976a9145d48feae4c97677e4ca7dcd73b0d9fd1399c962b88acc9cc1300\",\n        \"e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25\": \"01000000010db780fff7dfcef6dba9268ecf4f6df45a1a86b86cad6f59738a0ce29b145c47010000008a47304402202887ec6ec200e4e2b4178112633011cbdbc999e66d398b1ff3998e23f7c5541802204964bd07c0f18c48b7b9c00fbe34c7bc035efc479e21a4fa196027743f06095f0141044f1714ed25332bb2f74be169784577d0838aa66f2374f5d8cbbf216063626822d536411d13cbfcef1ff3cc1d58499578bc4a3c4a0be2e5184b2dd7963ef67713fdffffff02a0860100000000001600145bbdf3ba178f517d4812d286a40c436a9088076e6a0b0c00000000001976a9143fc16bef782f6856ff6638b1b99e4d3f863581d388acfbcb1300\"\n    },\n    \"tx_fees\": {},\n    \"txi\": {\n        \"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67\": {\n            \"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd\": [\n                [\n                    \"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d:0\",\n                    25000000\n                ]\n            ],\n            \"myJLELfyhG1Fu7iPyfpHfviVCQuLwsNCBm\": [\n                [\n                    \"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d:1\",\n                    34995259\n                ]\n            ]\n        },\n        \"2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d\": {},\n        \"2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd\": {\n            \"mweezhmgY1kEvmSNpfrqdLcd6NHekupXKp\": [\n                [\n                    \"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462:1\",\n                    30000000\n                ]\n            ]\n        },\n        \"31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f\": {\n            \"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd\": [\n                [\n                    \"6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254:0\",\n                    10000000\n                ]\n            ]\n        },\n        \"336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa\": {},\n        \"3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308\": {\n            \"mt2ijtY7BUkQRpErd99N9131YgsEL7oBSt\": [\n                [\n                    \"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968:0\",\n                    4999700\n                ]\n            ]\n        },\n        \"475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d\": {\n            \"mvJ2vrvNySAXWxVYuCn2tX6amyfchFGHvK\": [\n                [\n                    \"c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a:0\",\n                    990400\n                ]\n            ]\n        },\n        \"56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216\": {},\n        \"6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254\": {\n            \"mxTwRCnJgafjVaUVLsniWZPpXhz5dFzRyb\": [\n                [\n                    \"7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a:1\",\n                    89998701\n                ]\n            ]\n        },\n        \"72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112\": {\n            \"mp9iZVBSUokAUX1p57Kjc4mHrtqqEhxjrh\": [\n                [\n                    \"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67:1\",\n                    40000000\n                ]\n            ],\n            \"mywTRsN56GipUEbmCnoUV9YFdrUU5CmUxd\": [\n                [\n                    \"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67:0\",\n                    19994800\n                ]\n            ]\n        },\n        \"76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4\": {},\n        \"7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a\": {},\n        \"8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471\": {\n            \"mwNWMKSGou8ZJzXgfDaAUy1C8Jip3TEmdf\": [\n                [\n                    \"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306:0\",\n                    100000\n                ]\n            ],\n            \"mycxtgambuph71Hg6MdwRoF7c7GkFvHiZ6\": [\n                [\n                    \"c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9:0\",\n                    9638\n                ]\n            ]\n        },\n        \"9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb\": {},\n        \"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d\": {},\n        \"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462\": {\n            \"mqYXSDuvMpdibxUy6ftKW1564L9UE5eeFX\": [\n                [\n                    \"336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa:0\",\n                    21958750\n                ]\n            ],\n            \"myvhXvymTD4Ncgfg8r2WXiTZeDY3TtZGUY\": [\n                [\n                    \"56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216:1\",\n                    5000000\n                ]\n            ],\n            \"n1gdKukb5TUu37x5GahHsp4Gp2fdowdZPH\": [\n                [\n                    \"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968:1\",\n                    5000000\n                ]\n            ]\n        },\n        \"c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9\": {},\n        \"c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a\": {},\n        \"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968\": {\n            \"n4FfEQrf1PS3no7FCPhhDugxqgR4fUSvdX\": [\n                [\n                    \"2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d:1\",\n                    10000000\n                ]\n            ]\n        },\n        \"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306\": {\n            \"mmL4aJtiAPVUQca9AaZZdPUht9FvS2eb4a\": [\n                [\n                    \"e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25:1\",\n                    789354\n                ]\n            ]\n        },\n        \"e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25\": {\n            \"mrHCUTD63vsP9K3oon2AWZ9bKjqDhd5PMm\": [\n                [\n                    \"475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d:1\",\n                    889890\n                ]\n            ]\n        }\n    },\n    \"txo\": {\n        \"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67\": {\n            \"mp9iZVBSUokAUX1p57Kjc4mHrtqqEhxjrh\": [\n                [\n                    1,\n                    40000000,\n                    false\n                ]\n            ],\n            \"mywTRsN56GipUEbmCnoUV9YFdrUU5CmUxd\": [\n                [\n                    0,\n                    19994800,\n                    false\n                ]\n            ]\n        },\n        \"2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d\": {\n            \"n4FfEQrf1PS3no7FCPhhDugxqgR4fUSvdX\": [\n                [\n                    1,\n                    10000000,\n                    false\n                ]\n            ]\n        },\n        \"2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd\": {},\n        \"31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f\": {},\n        \"336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa\": {\n            \"mqYXSDuvMpdibxUy6ftKW1564L9UE5eeFX\": [\n                [\n                    0,\n                    21958750,\n                    false\n                ]\n            ]\n        },\n        \"3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308\": {\n            \"mpEGnkPKtMyfHo8EaUFks7xFZJdSgLjnC7\": [\n                [\n                    0,\n                    1000000,\n                    false\n                ]\n            ],\n            \"n23NSQfgAmVaW1qE1kgnxkW8JvWfveAktH\": [\n                [\n                    1,\n                    3999400,\n                    false\n                ]\n            ]\n        },\n        \"475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d\": {\n            \"mrHCUTD63vsP9K3oon2AWZ9bKjqDhd5PMm\": [\n                [\n                    1,\n                    889890,\n                    false\n                ]\n            ]\n        },\n        \"56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216\": {\n            \"myvhXvymTD4Ncgfg8r2WXiTZeDY3TtZGUY\": [\n                [\n                    1,\n                    5000000,\n                    false\n                ]\n            ]\n        },\n        \"6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254\": {\n            \"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd\": [\n                [\n                    0,\n                    10000000,\n                    false\n                ]\n            ]\n        },\n        \"72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112\": {},\n        \"76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4\": {\n            \"mgaArVGf5heGUko1i24wYrvkfFcN442U4v\": [\n                [\n                    0,\n                    4978900,\n                    false\n                ]\n            ]\n        },\n        \"7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a\": {\n            \"mxTwRCnJgafjVaUVLsniWZPpXhz5dFzRyb\": [\n                [\n                    1,\n                    89998701,\n                    false\n                ]\n            ]\n        },\n        \"8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471\": {\n            \"mfxZoA5dHaVn6QiHMbRwQTQUB7VFpZQFW4\": [\n                [\n                    1,\n                    100000,\n                    false\n                ]\n            ],\n            \"mhwQ9cxhAxaED747XuzgUo3F39MDc6txHr\": [\n                [\n                    0,\n                    9200,\n                    false\n                ]\n            ]\n        },\n        \"9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb\": {\n            \"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd\": [\n                [\n                    0,\n                    1000000,\n                    false\n                ],\n                [\n                    1,\n                    1000000,\n                    false\n                ],\n                [\n                    2,\n                    1000000,\n                    false\n                ],\n                [\n                    3,\n                    2000000,\n                    false\n                ],\n                [\n                    5,\n                    10000000,\n                    false\n                ]\n            ]\n        },\n        \"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d\": {\n            \"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd\": [\n                [\n                    0,\n                    25000000,\n                    false\n                ]\n            ],\n            \"myJLELfyhG1Fu7iPyfpHfviVCQuLwsNCBm\": [\n                [\n                    1,\n                    34995259,\n                    false\n                ]\n            ]\n        },\n        \"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462\": {\n            \"mvXwR94pXVgP7kdrW3vTiDQtVrkP3NY3zn\": [\n                [\n                    0,\n                    1866000,\n                    false\n                ]\n            ],\n            \"mweezhmgY1kEvmSNpfrqdLcd6NHekupXKp\": [\n                [\n                    1,\n                    30000000,\n                    false\n                ]\n            ]\n        },\n        \"c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9\": {\n            \"mycxtgambuph71Hg6MdwRoF7c7GkFvHiZ6\": [\n                [\n                    0,\n                    9638,\n                    false\n                ]\n            ]\n        },\n        \"c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a\": {\n            \"mvJ2vrvNySAXWxVYuCn2tX6amyfchFGHvK\": [\n                [\n                    0,\n                    990400,\n                    false\n                ]\n            ]\n        },\n        \"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968\": {\n            \"mt2ijtY7BUkQRpErd99N9131YgsEL7oBSt\": [\n                [\n                    0,\n                    4999700,\n                    false\n                ]\n            ],\n            \"n1gdKukb5TUu37x5GahHsp4Gp2fdowdZPH\": [\n                [\n                    1,\n                    5000000,\n                    false\n                ]\n            ]\n        },\n        \"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306\": {\n            \"mp2CafXnWnN8rR6BnFToVQ8bXNY4jnAecr\": [\n                [\n                    1,\n                    689000,\n                    false\n                ]\n            ],\n            \"mwNWMKSGou8ZJzXgfDaAUy1C8Jip3TEmdf\": [\n                [\n                    0,\n                    100000,\n                    false\n                ]\n            ]\n        },\n        \"e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25\": {\n            \"mmL4aJtiAPVUQca9AaZZdPUht9FvS2eb4a\": [\n                [\n                    1,\n                    789354,\n                    false\n                ]\n            ]\n        }\n    },\n    \"use_encryption\": false,\n    \"verified_tx3\": {\n        \"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67\": [\n            1230168,\n            1510528889,\n            25\n        ],\n        \"2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d\": [\n            1231593,\n            1511484570,\n            30\n        ],\n        \"2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd\": [\n            1251055,\n            1512046701,\n            245\n        ],\n        \"31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f\": [\n            1230627,\n            1510871704,\n            71\n        ],\n        \"336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa\": [\n            1242088,\n            1511680407,\n            146\n        ],\n        \"3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308\": [\n            1231594,\n            1511485793,\n            27\n        ],\n        \"475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d\": [\n            1281357,\n            1518388420,\n            13\n        ],\n        \"56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216\": [\n            1231595,\n            1511487012,\n            215\n        ],\n        \"6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254\": [\n            1230626,\n            1510870499,\n            1\n        ],\n        \"72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112\": [\n            1230191,\n            1510536040,\n            201\n        ],\n        \"76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4\": [\n            1251084,\n            1512048610,\n            1\n        ],\n        \"7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a\": [\n            1230624,\n            1510868089,\n            289\n        ],\n        \"8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471\": [\n            1446399,\n            1543859403,\n            92\n        ],\n        \"9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb\": [\n            1255526,\n            1513816274,\n            12\n        ],\n        \"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d\": [\n            1230163,\n            1510527129,\n            10\n        ],\n        \"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462\": [\n            1242088,\n            1511680407,\n            147\n        ],\n        \"c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9\": [\n            1323966,\n            1528290006,\n            54\n        ],\n        \"c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a\": [\n            1251074,\n            1512047838,\n            4\n        ],\n        \"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968\": [\n            1231594,\n            1511485793,\n            26\n        ],\n        \"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306\": [\n            1297610,\n            1526308364,\n            61\n        ],\n        \"e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25\": [\n            1297404,\n            1526137238,\n            56\n        ]\n    },\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        671,\n        324,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_9_3_seeded",
    "content": "{\n    \"addr_history\": {\n        \"12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes\": [],\n        \"12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1\": [],\n        \"13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB\": [],\n        \"13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c\": [],\n        \"14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz\": [],\n        \"14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA\": [],\n        \"15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV\": [],\n        \"17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z\": [],\n        \"18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv\": [],\n        \"18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B\": [],\n        \"19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz\": [],\n        \"19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G\": [],\n        \"1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq\": [],\n        \"1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d\": [],\n        \"1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs\": [],\n        \"1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado\": [],\n        \"1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z\": [],\n        \"1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52\": [],\n        \"1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP\": [],\n        \"1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv\": [],\n        \"1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb\": [],\n        \"1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ\": [],\n        \"1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G\": [],\n        \"1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN\": [],\n        \"1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J\": [],\n        \"1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt\": []\n    },\n    \"addresses\": {\n        \"change\": [\n            \"1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP\",\n            \"1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z\",\n            \"15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV\",\n            \"1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq\",\n            \"19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G\",\n            \"1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb\"\n        ],\n        \"receiving\": [\n            \"14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA\",\n            \"13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB\",\n            \"19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz\",\n            \"1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv\",\n            \"1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt\",\n            \"13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c\",\n            \"1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ\",\n            \"12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes\",\n            \"12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1\",\n            \"14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz\",\n            \"1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN\",\n            \"17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z\",\n            \"1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado\",\n            \"18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv\",\n            \"1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G\",\n            \"18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B\",\n            \"1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d\",\n            \"1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs\",\n            \"1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52\",\n            \"1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J\"\n        ]\n    },\n    \"keystore\": {\n        \"seed\": \"cereal wise two govern top pet frog nut rule sketch bundle logic\",\n        \"type\": \"bip32\",\n        \"xprv\": \"xprv9s21ZrQH143K29XjRjUs6MnDB9wXjXbJP2kG1fnRk8zjdDYWqVkQYUqaDtgZp5zPSrH5PZQJs8sU25HrUgT1WdgsPU8GbifKurtMYg37d4v\",\n        \"xpub\": \"xpub661MyMwAqRbcEdcCXm1sTViwjBn28zK9kFfrp4C3JUXiW1sfP34f6HA45B9yr7EH5XGzWuTfMTdqpt9XPrVQVUdgiYb5NW9m8ij1FSZgGBF\"\n    },\n    \"pruned_txo\": {},\n    \"seed_type\": \"standard\",\n    \"seed_version\": 13,\n    \"stored_height\": -1,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        619,\n        310,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_9_3_trezor_singleacc",
    "content": "{\n    \"addr_history\": {\n        \"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC\": [],\n        \"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz\": [],\n        \"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM\": [],\n        \"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ\": [],\n        \"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6\": [],\n        \"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S\": [],\n        \"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH\": [],\n        \"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw\": [],\n        \"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb\": [],\n        \"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX\": [],\n        \"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ\": [],\n        \"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp\": [],\n        \"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk\": [],\n        \"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD\": [],\n        \"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp\": [],\n        \"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz\": [],\n        \"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs\": [],\n        \"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid\": [],\n        \"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu\": [],\n        \"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo\": [],\n        \"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj\": [],\n        \"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv\": [],\n        \"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC\": [],\n        \"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe\": [],\n        \"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL\": [],\n        \"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG\": []\n    },\n    \"addresses\": {\n        \"change\": [\n            \"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ\",\n            \"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM\",\n            \"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG\",\n            \"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6\",\n            \"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL\",\n            \"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs\"\n        ],\n        \"receiving\": [\n            \"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu\",\n            \"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw\",\n            \"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH\",\n            \"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC\",\n            \"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ\",\n            \"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid\",\n            \"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz\",\n            \"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj\",\n            \"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz\",\n            \"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC\",\n            \"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo\",\n            \"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb\",\n            \"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe\",\n            \"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv\",\n            \"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp\",\n            \"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S\",\n            \"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX\",\n            \"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp\",\n            \"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk\",\n            \"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD\"\n        ]\n    },\n    \"keystore\": {\n        \"derivation\": \"m/44'/0'/0'\",\n        \"hw_type\": \"trezor\",\n        \"label\": \"trezor1\",\n        \"type\": \"hardware\",\n        \"xpub\": \"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y\"\n    },\n    \"pruned_txo\": {},\n    \"seed_version\": 13,\n    \"stored_height\": 490014,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        753,\n        486,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_2_9_3_watchaddresses",
    "content": "{\n    \"addr_history\": {\n        \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\": [],\n        \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\": [],\n        \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\": []\n    },\n    \"addresses\": [\n        \"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs\",\n        \"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa\",\n        \"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf\"\n    ],\n    \"pruned_txo\": {},\n    \"seed_version\": 13,\n    \"stored_height\": 490039,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"verified_tx3\": {},\n    \"wallet_type\": \"imported\",\n    \"winpos-qt\": [\n        499,\n        386,\n        840,\n        405\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_3_2_3_ledger_standard_keystore_changes",
    "content": "{\n    \"addr_history\": {\n        \"bc1q0k4hemnmw5czyq7yyka5mpc3hvz37lk0urhd34\": [],\n        \"bc1q2tgeuhkr85pjkrys44zn2a7lfap0g8u7ny68p3\": [],\n        \"bc1q2xm8slpsqlt47u0j7segcsfmaq6s4s2pvx2526\": [],\n        \"bc1q4dhwcvnnm8a0umt4gvn2tatq66qf9d37rx5t8u\": [],\n        \"bc1q4pqe6tcfyl8m35myj9trz7fn4w0kdpljnt3sxd\": [],\n        \"bc1q5l345sqf8fhlurn4hgu8dxu0w76j5tf3kc2f7h\": [],\n        \"bc1q5nd447vdf9gx0l8xmj500wr859pny29xurgcpn\": [],\n        \"bc1qaa2xnanrpmttw35gc4xqvz9ldz5sggqvc2ed72\": [],\n        \"bc1qav4zrnx5g4s5h2z5hzr9hncg8qwt96ezltepmp\": [],\n        \"bc1qcxryu22d3k66k4l55dzupvx45e88lmvp3rcww3\": [],\n        \"bc1qd4us67486cn5qy44z6v6ervv5cuzrykq0vlcw2\": [],\n        \"bc1qdd773rd9p8t3eylv2gvs2tmn2n79pwcfz65uyp\": [],\n        \"bc1qdwafv8hy0cx9utkkj4hs6ntafm3x95m9zgpgvn\": [],\n        \"bc1qehqt2um35x0c49snyugf94hvh7jz3vcjt0ya6m\": [],\n        \"bc1qex23ueucc9hxyxgk3jg8ahw7cgw954legfnrxg\": [],\n        \"bc1qf4tx5eesmrcy478gk384s2jv4lfh9dwt9jws0e\": [],\n        \"bc1qh9l2au0f6m2fl3l3qa6perw5xnpvjul8lyylkt\": [],\n        \"bc1qkmprcg50zcsdd0p3w70w2rxs5hwmwwn2xd0ls9\": [],\n        \"bc1qkztpz05djsatmxxafgjqqldp0yfs8knr6um3e4\": [],\n        \"bc1qrgj0zygryl6edylgm6gzx5j9rghdufrn5fp6hw\": [],\n        \"bc1qscxh3na5uqapjm006xmg4s0geurq7nw427ywca\": [],\n        \"bc1qunqye3f6cw88wqsjkks7amskder0rvufu49l6e\": [],\n        \"bc1qv077qy5udlr3q8ammxq9ecq57vh9lxjnwh0vy9\": [],\n        \"bc1qw9nqstryl3e0e49jg6670u6mu8507takz66qgv\": [],\n        \"bc1qx4neqay68lmvgrav3yslzuempv9xn7aqdks5r6\": [],\n        \"bc1qzhwpu84e5ajet4mxxr9ylc0fwass3q5k32uj5u\": []\n    },\n    \"addresses\": {\n        \"change\": [\n            \"bc1qdd773rd9p8t3eylv2gvs2tmn2n79pwcfz65uyp\",\n            \"bc1qv077qy5udlr3q8ammxq9ecq57vh9lxjnwh0vy9\",\n            \"bc1qx4neqay68lmvgrav3yslzuempv9xn7aqdks5r6\",\n            \"bc1qh9l2au0f6m2fl3l3qa6perw5xnpvjul8lyylkt\",\n            \"bc1qw9nqstryl3e0e49jg6670u6mu8507takz66qgv\",\n            \"bc1qaa2xnanrpmttw35gc4xqvz9ldz5sggqvc2ed72\"\n        ],\n        \"receiving\": [\n            \"bc1qav4zrnx5g4s5h2z5hzr9hncg8qwt96ezltepmp\",\n            \"bc1qzhwpu84e5ajet4mxxr9ylc0fwass3q5k32uj5u\",\n            \"bc1qehqt2um35x0c49snyugf94hvh7jz3vcjt0ya6m\",\n            \"bc1q0k4hemnmw5czyq7yyka5mpc3hvz37lk0urhd34\",\n            \"bc1qf4tx5eesmrcy478gk384s2jv4lfh9dwt9jws0e\",\n            \"bc1q2xm8slpsqlt47u0j7segcsfmaq6s4s2pvx2526\",\n            \"bc1q5nd447vdf9gx0l8xmj500wr859pny29xurgcpn\",\n            \"bc1qex23ueucc9hxyxgk3jg8ahw7cgw954legfnrxg\",\n            \"bc1qscxh3na5uqapjm006xmg4s0geurq7nw427ywca\",\n            \"bc1qdwafv8hy0cx9utkkj4hs6ntafm3x95m9zgpgvn\",\n            \"bc1qkmprcg50zcsdd0p3w70w2rxs5hwmwwn2xd0ls9\",\n            \"bc1qunqye3f6cw88wqsjkks7amskder0rvufu49l6e\",\n            \"bc1q5l345sqf8fhlurn4hgu8dxu0w76j5tf3kc2f7h\",\n            \"bc1q4pqe6tcfyl8m35myj9trz7fn4w0kdpljnt3sxd\",\n            \"bc1qkztpz05djsatmxxafgjqqldp0yfs8knr6um3e4\",\n            \"bc1q4dhwcvnnm8a0umt4gvn2tatq66qf9d37rx5t8u\",\n            \"bc1q2tgeuhkr85pjkrys44zn2a7lfap0g8u7ny68p3\",\n            \"bc1qrgj0zygryl6edylgm6gzx5j9rghdufrn5fp6hw\",\n            \"bc1qd4us67486cn5qy44z6v6ervv5cuzrykq0vlcw2\",\n            \"bc1qcxryu22d3k66k4l55dzupvx45e88lmvp3rcww3\"\n        ]\n    },\n    \"keystore\": {\n        \"cfg\": {\n            \"mode\": 0,\n            \"pair\": \"\"\n        },\n        \"derivation\": \"m/84'/0'/0'\",\n        \"hw_type\": \"ledger\",\n        \"label\": \"\",\n        \"type\": \"hardware\",\n        \"xpub\": \"zpub6qmVsnBYWipPzoeuZwtVeVnC42achPEZpGopT7jsop5WgDuFqKT3aS3EuAAQ6G76wbwtvDMdzffwxyEtwa6iafXSgjW2RjraiXfsgxQHnz8\"\n    },\n    \"seed_version\": 18,\n    \"spent_outpoints\": {},\n    \"stored_height\": 646576,\n    \"transactions\": {},\n    \"tx_fees\": {},\n    \"txi\": {},\n    \"txo\": {},\n    \"use_encryption\": false,\n    \"verified_tx3\": {},\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        168,\n        276,\n        840,\n        400\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_3_3_8_xpub_with_realistic_history",
    "content": "{\n    \"addr_history\": {\n        \"tb1q04m5vxgzsctgn8kgyfxcen3pqxdr2yx53vzwzl\": [\n            [\n                \"866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff\",\n                1772350\n            ]\n        ],\n        \"tb1q07efauuddxdf6hpfceqvpcwef5wpg8ja29evz3\": [\n            [\n                \"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b\",\n                1746825\n            ],\n            [\n                \"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b\",\n                1772251\n            ]\n        ],\n        \"tb1q0k62kjqt053p37a9v8lnqstc7jhuhjtjphw3h7\": [],\n        \"tb1q0l3cxy8xs8ujxm6cv9h2xgra430rwaw2xux6qe\": [],\n        \"tb1q0phzvy7039yyw93fa5te3p4ns9ftmv7xvv0fgj\": [\n            [\n                \"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e\",\n                1665679\n            ],\n            [\n                \"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0\",\n                1665687\n            ]\n        ],\n        \"tb1q0raz8xxcpznvaqpc0ecy5kpztck7z4ddkzr0qq\": [\n            [\n                \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52\",\n                1612648\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1q0tkgjg5f3wnquswmtpah2fsmxp0vl9rarvgluv\": [\n            [\n                \"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5\",\n                1612648\n            ],\n            [\n                \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\",\n                1612704\n            ]\n        ],\n        \"tb1q22tlp3vzkawdvudlcyfrhd87ql8765q600hftd\": [\n            [\n                \"fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa\",\n                1772346\n            ],\n            [\n                \"901856f632b06573e2384972c38a2b87d7f058595f77cd2fcca47893bb6dc366\",\n                1772347\n            ]\n        ],\n        \"tb1q27juqmgmq2749wylmyqk00lvx9mgaz4k5nfnud\": [\n            [\n                \"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94\",\n                1744777\n            ]\n        ],\n        \"tb1q2hr4vf8jkga66m82gg9zmxwszdjuw5450zclv0\": [\n            [\n                \"133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62\",\n                1607022\n            ],\n            [\n                \"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5\",\n                1607022\n            ]\n        ],\n        \"tb1q2qsa3ygu6l2z2kyvgwpnnmurym99m9duelc2hf\": [],\n        \"tb1q309xc56t5r928v093pu3h4x99ffa5xmwcgav8r\": [\n            [\n                \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3\",\n                1747567\n            ],\n            [\n                \"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295\",\n                1747569\n            ]\n        ],\n        \"tb1q3dpgc58vpdh3n4gaa5265ghllfwzy7l8v786fl\": [\n            [\n                \"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2\",\n                1667168\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1q3dvf0y9tmf24k4y5d37ay0vacyaq5qva7lg50t\": [\n            [\n                \"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61\",\n                1413374\n            ],\n            [\n                \"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6\",\n                1612788\n            ]\n        ],\n        \"tb1q3p7gwqhj2n27gny6zuxpf3ajqrqaqnfkl57vz0\": [\n            [\n                \"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54\",\n                1747720\n            ],\n            [\n                \"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab\",\n                1772251\n            ]\n        ],\n        \"tb1q49afhhhsg8fqkpjfdgelnvyq3fnaglzw74kda4\": [\n            [\n                \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3\",\n                1747567\n            ],\n            [\n                \"781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05\",\n                1774902\n            ]\n        ],\n        \"tb1q49g7md82fy3yrhpf6r4mdnyht3hut2zhahen7h\": [\n            [\n                \"56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b\",\n                1612072\n            ],\n            [\n                \"dc16e73a88aff40d703f8eba148eae7265d1128a013f6f570a8bd76d86a5e137\",\n                1666106\n            ]\n        ],\n        \"tb1q4arrqquh2ptjvak5eg5ld7mrt9ncq7lae7fw7t\": [\n            [\n                \"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d\",\n                1665679\n            ],\n            [\n                \"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38\",\n                1665815\n            ]\n        ],\n        \"tb1q4eeeqvrylpkshxwa3whfza39vzyv3yc0flv9rj\": [\n            [\n                \"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04\",\n                1638861\n            ],\n            [\n                \"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e\",\n                1665679\n            ]\n        ],\n        \"tb1q4qhgk2r4ngpnk5j0rq28f2cyhlguje8x92g99s\": [\n            [\n                \"38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3\",\n                1666768\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1q4tu9pesq3yl38xc677lunm5ywaaykgnswxc0ev\": [\n            [\n                \"d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156\",\n                1413150\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1q6lzmxd6hr5y2utp5y5knmh8kefanet5pvgkphw\": [\n            [\n                \"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867\",\n                1612009\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1q6yjsyw749hjg4wqa2navhdaj2wxpqtkztzrh8c\": [\n            [\n                \"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7\",\n                1612648\n            ],\n            [\n                \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\",\n                1612704\n            ]\n        ],\n        \"tb1q6z3t5mrmhwqu0gw3kpwrpcfzpezcha3j8xpdma\": [],\n        \"tb1q70z22hvlhhjn69xpv2jwkkxprf0pvzh2z5p24r\": [\n            [\n                \"48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a\",\n                1666551\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1q730gzvu52y6t07465flt6ae8eny2mnsh7drhw4\": [\n            [\n                \"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5\",\n                1612648\n            ],\n            [\n                \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\",\n                1612704\n            ]\n        ],\n        \"tb1q767gch8ucagh23h40frfm8x6jmc37qvxpn8x2f\": [\n            [\n                \"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3\",\n                1774146\n            ],\n            [\n                \"d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3\",\n                1774752\n            ]\n        ],\n        \"tb1q7lpc88aa3qw2lsmm3dnah3876clxq4j7apzgf3\": [\n            [\n                \"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877\",\n                1772375\n            ],\n            [\n                \"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3\",\n                1774146\n            ]\n        ],\n        \"tb1q8564fhyum66n239wt0gp8m0khlqgwgac8ft2r0\": [\n            [\n                \"48a4cd850234b2316395959c343a3cbfd57e706e256399ac39927330f03268d6\",\n                1747720\n            ]\n        ],\n        \"tb1q8afxv7tzczj99lwf4et6le4k2u0tytqgt6g44w\": [\n            [\n                \"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d\",\n                1665679\n            ],\n            [\n                \"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2\",\n                1667168\n            ]\n        ],\n        \"tb1q8k9sp22vjun7hf0sfvs2n8mfwt8xl43d68xml2\": [\n            [\n                \"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a\",\n                1772346\n            ],\n            [\n                \"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514\",\n                1772348\n            ]\n        ],\n        \"tb1q8zwvzwh8tnthf3d2qsxpyemu5mwwddysy5pxc2\": [],\n        \"tb1q92pxe3a3zyddfz5k74csaqt2vzc5sl37fgm5wn\": [\n            [\n                \"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab\",\n                1772251\n            ],\n            [\n                \"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877\",\n                1772375\n            ]\n        ],\n        \"tb1q955va7ngp2zzzrfwmn29575v6ksqfzrvvfd658\": [\n            [\n                \"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd\",\n                1612004\n            ],\n            [\n                \"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687\",\n                1612005\n            ]\n        ],\n        \"tb1q97f8vmmcvcjgme0kstta62atpzp5z3t7z7vsa7\": [\n            [\n                \"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7\",\n                1612648\n            ],\n            [\n                \"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d\",\n                1665679\n            ]\n        ],\n        \"tb1q97kf6e7qfudh2zyy0vmp3cevtw3jteqa0qupts\": [\n            [\n                \"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15\",\n                1746271\n            ],\n            [\n                \"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d\",\n                1746274\n            ]\n        ],\n        \"tb1q98enaj75ssnrjjkx3svkm5jg8af65u44rx5pnp\": [],\n        \"tb1q9d7jlkj9tvvhc6n7zmc02ndyh3n6vex0d8fts4\": [\n            [\n                \"d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee\",\n                1746834\n            ],\n            [\n                \"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506\",\n                1747567\n            ]\n        ],\n        \"tb1q9jtcype5swm4reyz4sktvq609shw88fwzjz9jg\": [\n            [\n                \"26b1fb057113f6ce39a20f5baa493015b152cc1c0af312b3ee8950e9a4bbf47a\",\n                1774145\n            ]\n        ],\n        \"tb1q9mgamdnm0jch3e73ykvlgymwg5nhs76t8jv4yg\": [\n            [\n                \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c\",\n                1584541\n            ],\n            [\n                \"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5\",\n                1612648\n            ]\n        ],\n        \"tb1q9mqvf4w35aljtgl4p6kca9xnhesc4l7vpamdp0\": [],\n        \"tb1qa4gcpuzu0vwunqrnycpv2rx6gpfsuq9d4sg25y\": [],\n        \"tb1qa4gwte9kr0tsndl5q69k6q3yte5uh7senrm7fc\": [\n            [\n                \"43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35\",\n                1607959\n            ],\n            [\n                \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52\",\n                1612648\n            ]\n        ],\n        \"tb1qa6dgfxczcjshyhv6d4ck0qvs3mgdjd2gpdqqzj\": [\n            [\n                \"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506\",\n                1747567\n            ]\n        ],\n        \"tb1qadwk4rmwxcayxg27f6cpv46dusyksk273pq09u\": [],\n        \"tb1qaj6eud755xul5y70vy073rhx29qn26xw65nanw\": [\n            [\n                \"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94\",\n                1744777\n            ],\n            [\n                \"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9\",\n                1744791\n            ]\n        ],\n        \"tb1qaj8j0ldjswlmtamfxl3kgrq7gcc7nqnun4dwn6\": [],\n        \"tb1qalw5ax88hnr6gse240prn370020uxq505tw3n8\": [],\n        \"tb1qaqkakr58cs8jq7zyhx4dwt8maemadrfnwevsc9\": [],\n        \"tb1qav362fjlwyvuraeqz5gmf0hrrvv9hp9jgv3ap9\": [\n            [\n                \"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9\",\n                1744791\n            ],\n            [\n                \"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15\",\n                1746271\n            ]\n        ],\n        \"tb1qayg9tz462wythfdxw6gxpapwdp5y8ugth7fx43\": [\n            [\n                \"85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834\",\n                1747541\n            ],\n            [\n                \"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057\",\n                1747567\n            ]\n        ],\n        \"tb1qc50swmqxgw3e9j890t8rp90397rg3j0djy9rz6\": [\n            [\n                \"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04\",\n                1638861\n            ],\n            [\n                \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4\",\n                1663206\n            ]\n        ],\n        \"tb1qc5ztxm2kvtrtxun50v0rn6asm9tv0t3mfzh68v\": [],\n        \"tb1qckp4ztmstwtyxzml3dmfvegeq5mfxwu2h3q94l\": [],\n        \"tb1qcmmu23wur97duygz524t07s40gdxzgc4kfpkp5\": [\n            [\n                \"94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9\",\n                1747721\n            ]\n        ],\n        \"tb1qcs8sn834hc65nv0lypxf4zzh8yrp0vqw293vdl\": [\n            [\n                \"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc\",\n                1721735\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1qczlwxujnm7hg4559hhsywfc993my4807p50qdm\": [],\n        \"tb1qczu7px50v092ztuhe7vxwcjs9p8mukg0gn9y28\": [\n            [\n                \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c\",\n                1584541\n            ],\n            [\n                \"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7\",\n                1612648\n            ]\n        ],\n        \"tb1qd0q3cnqu0xsx7pmc4xqeqvphe2k5a4lhjs05h0\": [\n            [\n                \"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c\",\n                1746274\n            ],\n            [\n                \"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b\",\n                1746825\n            ]\n        ],\n        \"tb1qdekelvx2uh3dfwy9rxgmsu4dra0flkkf80t5z7\": [\n            [\n                \"366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f\",\n                1746833\n            ],\n            [\n                \"6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a\",\n                1746834\n            ],\n            [\n                \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3\",\n                1747567\n            ]\n        ],\n        \"tb1qdlv5pjqdk27x04m6xte3kcsz9h2euuylhv4tgl\": [],\n        \"tb1qdn5xyyvkr5y20p6sfynturxhpd9gx24maw4n98\": [],\n        \"tb1qe7wv04mlsg7hkarsdx07jgr7mgs80pe6nl87sq\": [],\n        \"tb1qeh090ruc3cs5hry90tev4fsvrnegulw8xssdzx\": [\n            [\n                \"c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0\",\n                1584540\n            ],\n            [\n                \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c\",\n                1584541\n            ]\n        ],\n        \"tb1qevkywexfy5gnydx0mrsrrthzncymydc0zz4rqx\": [\n            [\n                \"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295\",\n                1747569\n            ]\n        ],\n        \"tb1qez8zr7fjzdln2fmgyepmmd2jkcnkrvg3czqqn9\": [],\n        \"tb1qflmaysgnsh9acv6ewxj08eur45lgsrrdgmvxz2\": [],\n        \"tb1qfrlx5pza9vmez6vpx7swt8yp0nmgz3qa7jjkuf\": [\n            [\n                \"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38\",\n                1665815\n            ],\n            [\n                \"22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f\",\n                1692449\n            ]\n        ],\n        \"tb1qftpp8e7t3mk7c48sw4mgwqn24yhuzl5t9u4fzd\": [\n            [\n                \"3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549\",\n                1606858\n            ],\n            [\n                \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52\",\n                1612648\n            ]\n        ],\n        \"tb1qg26z824j42qrl9tssjpjkyp4n042y35sre6yya\": [\n            [\n                \"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0\",\n                1665687\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1qg8nggy5f8avhucghpc8h5garasdqfwv37nh7tc\": [],\n        \"tb1qgas5dck220ygdafe3ehhpgqpclve8j5a0c2cs2\": [],\n        \"tb1qgg2avhyk30s8a0n72t8sm3cggdmqgdutdvwfa8\": [\n            [\n                \"781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05\",\n                1774902\n            ]\n        ],\n        \"tb1qgga8dl6z86cajdgtrmmdwvq9f2695e6epp064p\": [\n            [\n                \"40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1\",\n                1665686\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1qh32r5shqhp2k5cl467m9rj8jw2rkqmjl9g0tn7\": [\n            [\n                \"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2\",\n                1667168\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1qh6qyzl3pds56azgjqkhkk7kfkavzxghjwsv0rl\": [],\n        \"tb1qhezs0203uw8wyagjpjs5yv57xdmsta077qkazu\": [\n            [\n                \"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b\",\n                1772251\n            ],\n            [\n                \"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab\",\n                1772251\n            ]\n        ],\n        \"tb1qhksthm48t4eqrzup3gzzlqnf433z8aq5uj03jr\": [\n            [\n                \"4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e\",\n                1747720\n            ]\n        ],\n        \"tb1qhmerp6zrxw852kthwu7hq8tplmk26r6aklvcgw\": [\n            [\n                \"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9\",\n                1607028\n            ],\n            [\n                \"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be\",\n                1609187\n            ]\n        ],\n        \"tb1qhnpmvc5uvgw28tvtyv9nr5ckx3fz48lrd94mym\": [],\n        \"tb1qhvpcyyj29tt2rtpe693whfse5hpzh4r7ums7zv\": [],\n        \"tb1qhzay07kvxkuerlel4e6dps33dtr3yxmnf34v9s\": [\n            [\n                \"b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2\",\n                1665693\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1qjduurjclneffxv6tgv7rnspaxu85v7saf9mfj0\": [\n            [\n                \"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82\",\n                1772251\n            ],\n            [\n                \"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a\",\n                1772346\n            ]\n        ],\n        \"tb1qjy0wuqaejah9l4h3hn505jlph9pn6p7mzjasnw\": [\n            [\n                \"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67\",\n                1774477\n            ],\n            [\n                \"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc\",\n                1774910\n            ]\n        ],\n        \"tb1qk7u2mcu02v7fgvls9ttuwq49a6e5kae5kxkts9\": [\n            [\n                \"0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0\",\n                1772375\n            ],\n            [\n                \"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67\",\n                1774477\n            ]\n        ],\n        \"tb1qkahwe0pkcnnm9fzwy3f5spwd9vv3cvdzk5dkkc\": [\n            [\n                \"0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d\",\n                1666105\n            ],\n            [\n                \"4133b7570dd1424ac0f05fba67d7af95eae19de82ff367007ac33742f13d1d8e\",\n                1666106\n            ]\n        ],\n        \"tb1ql2yks7mu0u95hpjgagly0uxlh98fs9qg00hkr5\": [\n            [\n                \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4\",\n                1663206\n            ],\n            [\n                \"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d\",\n                1665679\n            ]\n        ],\n        \"tb1qldxhwr6y2mfckhjf832sfepn2sd28jvqykgyfe\": [\n            [\n                \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\",\n                1612704\n            ],\n            [\n                \"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d\",\n                1636331\n            ]\n        ],\n        \"tb1qltq9ex98gwm2aj5wnn4me7qnzrgdnp2hwq7pwn\": [\n            [\n                \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4\",\n                1663206\n            ],\n            [\n                \"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc\",\n                1721735\n            ]\n        ],\n        \"tb1qm0z2hh76fngnp3zl3yglvlm6nm98qz4exupta9\": [\n            [\n                \"18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf\",\n                1772648\n            ],\n            [\n                \"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67\",\n                1774477\n            ]\n        ],\n        \"tb1qm3qwl94e7xcu2nxe8z0d3w2x0s0xwrpahm6ceq\": [\n            [\n                \"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38\",\n                1665815\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1qmy2luym2mtyydcwlmf7gxpe30k0cp8gt7gj63k\": [],\n        \"tb1qmy8uqjkh2d2dcgnz6yyrtjk05n5y4ey8qzayyu\": [\n            [\n                \"7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55\",\n                1772374\n            ],\n            [\n                \"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877\",\n                1772375\n            ]\n        ],\n        \"tb1qn7e3uh2lfrmxheg0t5gvm2cqrn3cqgurz4as76\": [],\n        \"tb1qnlesczfxk2z7xgeyep3tr3xkh3z8rcmh4j95gt\": [\n            [\n                \"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295\",\n                1747569\n            ],\n            [\n                \"94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9\",\n                1747721\n            ]\n        ],\n        \"tb1qp3p2d72gj2l7r6za056tgu4ezsurjphper4swh\": [\n            [\n                \"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc\",\n                1774910\n            ]\n        ],\n        \"tb1qpea0mzjyztv4ctskscsu94sj248t85vmggsl6c\": [\n            [\n                \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52\",\n                1612648\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1qpntvn7xwp2nn6alu7lc4d360tjlvdyrtzf02xh\": [],\n        \"tb1qptq7mkutq0m6an0npf8t89dxvtecqp08uqphcn\": [\n            [\n                \"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be\",\n                1609187\n            ],\n            [\n                \"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e\",\n                1609312\n            ]\n        ],\n        \"tb1qq6zuqfwc97d3mqy46dva4vn8jvlkck63c3y0mp\": [\n            [\n                \"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755\",\n                1607028\n            ],\n            [\n                \"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be\",\n                1609187\n            ]\n        ],\n        \"tb1qql6g008ymlcfmrkwg8lfl7tsgays6s427pjlt6\": [\n            [\n                \"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271\",\n                1609187\n            ],\n            [\n                \"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd\",\n                1612004\n            ]\n        ],\n        \"tb1qqmvztjh4yg6x5fgw3wq685zkna5jv0e06v4ee4\": [],\n        \"tb1qr6g9qrkssn822tklp83accz4j9s4sat9g068g3\": [],\n        \"tb1qrkgr9yme0zedgemjpvrt852rq2qfz27s832yhr\": [\n            [\n                \"69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1\",\n                1413150\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1qrln929gz055hse2ylytl3cxnse6wxshek97t7j\": [],\n        \"tb1qrmex0u0vkefcmxr6fc2sxuvdxh67p99nsqnklw\": [\n            [\n                \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4\",\n                1663206\n            ],\n            [\n                \"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d\",\n                1665679\n            ]\n        ],\n        \"tb1qsmk2jc6fzr0e9xkf7w9l3ha8s0txha3vruffrp\": [\n            [\n                \"0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761\",\n                1772346\n            ],\n            [\n                \"e20bf91006e084c63c424240e857af8c3e27a5d8356af4dbe5ddc8ad4e71c336\",\n                1772347\n            ]\n        ],\n        \"tb1qsrgn2zg9lgyeva68tgjqv0urs830vcnsmajg0x\": [\n            [\n                \"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687\",\n                1612005\n            ],\n            [\n                \"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867\",\n                1612009\n            ]\n        ],\n        \"tb1qswg8tcmndprjqc56s5zxskd4jq7ay267phaefp\": [\n            [\n                \"ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21\",\n                1772346\n            ],\n            [\n                \"9e0789481c2f5b2fda96f0af534af3caa5cb5ddc2bbf27aea72c80154d9272d2\",\n                1772347\n            ]\n        ],\n        \"tb1qsyhawdg9zj2cepa0zg096rna2nxg4zj0c0fnvq\": [\n            [\n                \"19a666acc1bb5656e49a1d99087c5a0c2a135c40f1a58e5306f2d85f46786801\",\n                1772373\n            ]\n        ],\n        \"tb1qt44lpapl38spldm0dtmsm6z300mw8qayy659zr\": [\n            [\n                \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\",\n                1612704\n            ],\n            [\n                \"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d\",\n                1665679\n            ]\n        ],\n        \"tb1qtgsfkgptcxdn6dz6wh8c4dguk3cezwne5j5c47\": [],\n        \"tb1qtk62c2ypvuz7e42y039tq7tczhsndxs84eqj8y\": [\n            [\n                \"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135\",\n                1746825\n            ],\n            [\n                \"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54\",\n                1747720\n            ]\n        ],\n        \"tb1quc085vmkgkpdr5wpqvgt6dyw35s5hqrncml8sh\": [\n            [\n                \"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d\",\n                1746274\n            ],\n            [\n                \"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c\",\n                1746274\n            ]\n        ],\n        \"tb1quneg2cv7v8ne9z64whgcvg6hzwhxuselhpak3e\": [],\n        \"tb1quw4g923ww4zs042cts9kmvrvcr95jfahqasfrg\": [\n            [\n                \"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5\",\n                1607022\n            ],\n            [\n                \"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e\",\n                1609312\n            ]\n        ],\n        \"tb1qw5dyx8xn3mp8g6syyqyd6sxxlaatrv2qvszwta\": [\n            [\n                \"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d\",\n                1636331\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1qw9jdeld07zf53jw85vh7pnv4xdep523v96p9gv\": [\n            [\n                \"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514\",\n                1772348\n            ],\n            [\n                \"866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff\",\n                1772350\n            ]\n        ],\n        \"tb1qwg8fgt97d7wm3jkzxmkznwe7ngxy08l89v0hxp\": [\n            [\n                \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n                1609182\n            ],\n            [\n                \"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271\",\n                1609187\n            ]\n        ],\n        \"tb1qwqxjpfytaq08qteus5dhwf92u5kzfzyv45kyd4\": [\n            [\n                \"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e\",\n                1609312\n            ],\n            [\n                \"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04\",\n                1638861\n            ]\n        ],\n        \"tb1qwu3708q32l7wdcvfhf9vfhgazp8yzggf5x4y72\": [\n            [\n                \"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506\",\n                1747567\n            ]\n        ],\n        \"tb1qwzhmm9ajms63h5t87u2w999jl5akptkl4e5d7z\": [\n            [\n                \"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5\",\n                1607022\n            ],\n            [\n                \"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5\",\n                1607022\n            ]\n        ],\n        \"tb1qwzxfucd24m4j4y6nzasnucrx2dty4ht2h0lud0\": [\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ],\n            [\n                \"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94\",\n                1744777\n            ]\n        ],\n        \"tb1qxx7t6g3dpts4ytlzetdqv8e04qdal36xg9d7zc\": [\n            [\n                \"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057\",\n                1747567\n            ]\n        ],\n        \"tb1qy5xx4uyqv6yhq9eptha8n5shqj94vqw7euftmk\": [\n            [\n                \"d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3\",\n                1774752\n            ]\n        ],\n        \"tb1qy6uuespwqm9m9wdjvmwr07l9fvn0ge93mzskzw\": [\n            [\n                \"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9\",\n                1744791\n            ],\n            [\n                \"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135\",\n                1746825\n            ]\n        ],\n        \"tb1qy9986nnmd8dhznxfs9sh9c5nknnj0s4vwgky6c\": [],\n        \"tb1qyeg0h0fy8vw3mq0alvdffe0ax8dltalmjzse33\": [\n            [\n                \"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6\",\n                1612788\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1qyf62fc39qsmnxxv873meuu9au6p3cag9slgh9p\": [\n            [\n                \"65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc\",\n                1746833\n            ],\n            [\n                \"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057\",\n                1747567\n            ]\n        ],\n        \"tb1qyprqlu8dn88d4kgk5yldruvg96tjamups3ww69\": [],\n        \"tb1qz2xgj9eahs855rudhd4xreatp99xp3jx5mjmh7\": [\n            [\n                \"72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390\",\n                1692476\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ],\n        \"tb1qz9z4uw5tnh0yjpz4a4pfhv0wrpegfyv9yl2n7g\": [\n            [\n                \"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4\",\n                1638866\n            ],\n            [\n                \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n                1744777\n            ]\n        ]\n    },\n    \"addresses\": {\n        \"change\": [\n            \"tb1qczu7px50v092ztuhe7vxwcjs9p8mukg0gn9y28\",\n            \"tb1qwzhmm9ajms63h5t87u2w999jl5akptkl4e5d7z\",\n            \"tb1quw4g923ww4zs042cts9kmvrvcr95jfahqasfrg\",\n            \"tb1qptq7mkutq0m6an0npf8t89dxvtecqp08uqphcn\",\n            \"tb1qql6g008ymlcfmrkwg8lfl7tsgays6s427pjlt6\",\n            \"tb1q955va7ngp2zzzrfwmn29575v6ksqfzrvvfd658\",\n            \"tb1qwqxjpfytaq08qteus5dhwf92u5kzfzyv45kyd4\",\n            \"tb1qsrgn2zg9lgyeva68tgjqv0urs830vcnsmajg0x\",\n            \"tb1q6lzmxd6hr5y2utp5y5knmh8kefanet5pvgkphw\",\n            \"tb1q97f8vmmcvcjgme0kstta62atpzp5z3t7z7vsa7\",\n            \"tb1qpea0mzjyztv4ctskscsu94sj248t85vmggsl6c\",\n            \"tb1q730gzvu52y6t07465flt6ae8eny2mnsh7drhw4\",\n            \"tb1qldxhwr6y2mfckhjf832sfepn2sd28jvqykgyfe\",\n            \"tb1qyeg0h0fy8vw3mq0alvdffe0ax8dltalmjzse33\",\n            \"tb1qw5dyx8xn3mp8g6syyqyd6sxxlaatrv2qvszwta\",\n            \"tb1q4eeeqvrylpkshxwa3whfza39vzyv3yc0flv9rj\",\n            \"tb1ql2yks7mu0u95hpjgagly0uxlh98fs9qg00hkr5\",\n            \"tb1q8afxv7tzczj99lwf4et6le4k2u0tytqgt6g44w\",\n            \"tb1q4arrqquh2ptjvak5eg5ld7mrt9ncq7lae7fw7t\",\n            \"tb1q0phzvy7039yyw93fa5te3p4ns9ftmv7xvv0fgj\",\n            \"tb1qg26z824j42qrl9tssjpjkyp4n042y35sre6yya\",\n            \"tb1qm3qwl94e7xcu2nxe8z0d3w2x0s0xwrpahm6ceq\",\n            \"tb1qaj6eud755xul5y70vy073rhx29qn26xw65nanw\",\n            \"tb1q3dpgc58vpdh3n4gaa5265ghllfwzy7l8v786fl\",\n            \"tb1qav362fjlwyvuraeqz5gmf0hrrvv9hp9jgv3ap9\",\n            \"tb1q97kf6e7qfudh2zyy0vmp3cevtw3jteqa0qupts\",\n            \"tb1quc085vmkgkpdr5wpqvgt6dyw35s5hqrncml8sh\",\n            \"tb1qczlwxujnm7hg4559hhsywfc993my4807p50qdm\",\n            \"tb1qd0q3cnqu0xsx7pmc4xqeqvphe2k5a4lhjs05h0\",\n            \"tb1qswg8tcmndprjqc56s5zxskd4jq7ay267phaefp\",\n            \"tb1qtk62c2ypvuz7e42y039tq7tczhsndxs84eqj8y\",\n            \"tb1q07efauuddxdf6hpfceqvpcwef5wpg8ja29evz3\",\n            \"tb1qxx7t6g3dpts4ytlzetdqv8e04qdal36xg9d7zc\",\n            \"tb1q309xc56t5r928v093pu3h4x99ffa5xmwcgav8r\",\n            \"tb1qwu3708q32l7wdcvfhf9vfhgazp8yzggf5x4y72\",\n            \"tb1qevkywexfy5gnydx0mrsrrthzncymydc0zz4rqx\",\n            \"tb1q3p7gwqhj2n27gny6zuxpf3ajqrqaqnfkl57vz0\",\n            \"tb1qcmmu23wur97duygz524t07s40gdxzgc4kfpkp5\",\n            \"tb1qhezs0203uw8wyagjpjs5yv57xdmsta077qkazu\",\n            \"tb1qsmk2jc6fzr0e9xkf7w9l3ha8s0txha3vruffrp\",\n            \"tb1q92pxe3a3zyddfz5k74csaqt2vzc5sl37fgm5wn\",\n            \"tb1q22tlp3vzkawdvudlcyfrhd87ql8765q600hftd\",\n            \"tb1qn7e3uh2lfrmxheg0t5gvm2cqrn3cqgurz4as76\",\n            \"tb1q98enaj75ssnrjjkx3svkm5jg8af65u44rx5pnp\",\n            \"tb1qgas5dck220ygdafe3ehhpgqpclve8j5a0c2cs2\",\n            \"tb1qyprqlu8dn88d4kgk5yldruvg96tjamups3ww69\",\n            \"tb1qw9jdeld07zf53jw85vh7pnv4xdep523v96p9gv\",\n            \"tb1qflmaysgnsh9acv6ewxj08eur45lgsrrdgmvxz2\",\n            \"tb1q04m5vxgzsctgn8kgyfxcen3pqxdr2yx53vzwzl\",\n            \"tb1qez8zr7fjzdln2fmgyepmmd2jkcnkrvg3czqqn9\",\n            \"tb1q7lpc88aa3qw2lsmm3dnah3876clxq4j7apzgf3\",\n            \"tb1q767gch8ucagh23h40frfm8x6jmc37qvxpn8x2f\",\n            \"tb1qjy0wuqaejah9l4h3hn505jlph9pn6p7mzjasnw\",\n            \"tb1qhnpmvc5uvgw28tvtyv9nr5ckx3fz48lrd94mym\",\n            \"tb1qaqkakr58cs8jq7zyhx4dwt8maemadrfnwevsc9\",\n            \"tb1qy5xx4uyqv6yhq9eptha8n5shqj94vqw7euftmk\",\n            \"tb1qgg2avhyk30s8a0n72t8sm3cggdmqgdutdvwfa8\",\n            \"tb1qp3p2d72gj2l7r6za056tgu4ezsurjphper4swh\",\n            \"tb1q0l3cxy8xs8ujxm6cv9h2xgra430rwaw2xux6qe\",\n            \"tb1q6z3t5mrmhwqu0gw3kpwrpcfzpezcha3j8xpdma\",\n            \"tb1qdn5xyyvkr5y20p6sfynturxhpd9gx24maw4n98\",\n            \"tb1q9mqvf4w35aljtgl4p6kca9xnhesc4l7vpamdp0\",\n            \"tb1qhvpcyyj29tt2rtpe693whfse5hpzh4r7ums7zv\",\n            \"tb1qg8nggy5f8avhucghpc8h5garasdqfwv37nh7tc\"\n        ],\n        \"receiving\": [\n            \"tb1qeh090ruc3cs5hry90tev4fsvrnegulw8xssdzx\",\n            \"tb1q3dvf0y9tmf24k4y5d37ay0vacyaq5qva7lg50t\",\n            \"tb1q9mgamdnm0jch3e73ykvlgymwg5nhs76t8jv4yg\",\n            \"tb1q4tu9pesq3yl38xc677lunm5ywaaykgnswxc0ev\",\n            \"tb1qrkgr9yme0zedgemjpvrt852rq2qfz27s832yhr\",\n            \"tb1q2hr4vf8jkga66m82gg9zmxwszdjuw5450zclv0\",\n            \"tb1qq6zuqfwc97d3mqy46dva4vn8jvlkck63c3y0mp\",\n            \"tb1qftpp8e7t3mk7c48sw4mgwqn24yhuzl5t9u4fzd\",\n            \"tb1qhmerp6zrxw852kthwu7hq8tplmk26r6aklvcgw\",\n            \"tb1qwg8fgt97d7wm3jkzxmkznwe7ngxy08l89v0hxp\",\n            \"tb1q0raz8xxcpznvaqpc0ecy5kpztck7z4ddkzr0qq\",\n            \"tb1qa4gwte9kr0tsndl5q69k6q3yte5uh7senrm7fc\",\n            \"tb1qt44lpapl38spldm0dtmsm6z300mw8qayy659zr\",\n            \"tb1q0tkgjg5f3wnquswmtpah2fsmxp0vl9rarvgluv\",\n            \"tb1q6yjsyw749hjg4wqa2navhdaj2wxpqtkztzrh8c\",\n            \"tb1qz9z4uw5tnh0yjpz4a4pfhv0wrpegfyv9yl2n7g\",\n            \"tb1qgga8dl6z86cajdgtrmmdwvq9f2695e6epp064p\",\n            \"tb1q49g7md82fy3yrhpf6r4mdnyht3hut2zhahen7h\",\n            \"tb1qc50swmqxgw3e9j890t8rp90397rg3j0djy9rz6\",\n            \"tb1qrmex0u0vkefcmxr6fc2sxuvdxh67p99nsqnklw\",\n            \"tb1qltq9ex98gwm2aj5wnn4me7qnzrgdnp2hwq7pwn\",\n            \"tb1qhzay07kvxkuerlel4e6dps33dtr3yxmnf34v9s\",\n            \"tb1qfrlx5pza9vmez6vpx7swt8yp0nmgz3qa7jjkuf\",\n            \"tb1qcs8sn834hc65nv0lypxf4zzh8yrp0vqw293vdl\",\n            \"tb1qkahwe0pkcnnm9fzwy3f5spwd9vv3cvdzk5dkkc\",\n            \"tb1q70z22hvlhhjn69xpv2jwkkxprf0pvzh2z5p24r\",\n            \"tb1q4qhgk2r4ngpnk5j0rq28f2cyhlguje8x92g99s\",\n            \"tb1qz2xgj9eahs855rudhd4xreatp99xp3jx5mjmh7\",\n            \"tb1qa6dgfxczcjshyhv6d4ck0qvs3mgdjd2gpdqqzj\",\n            \"tb1qdekelvx2uh3dfwy9rxgmsu4dra0flkkf80t5z7\",\n            \"tb1q27juqmgmq2749wylmyqk00lvx9mgaz4k5nfnud\",\n            \"tb1qwzxfucd24m4j4y6nzasnucrx2dty4ht2h0lud0\",\n            \"tb1q9d7jlkj9tvvhc6n7zmc02ndyh3n6vex0d8fts4\",\n            \"tb1qh32r5shqhp2k5cl467m9rj8jw2rkqmjl9g0tn7\",\n            \"tb1qyf62fc39qsmnxxv873meuu9au6p3cag9slgh9p\",\n            \"tb1qayg9tz462wythfdxw6gxpapwdp5y8ugth7fx43\",\n            \"tb1q49afhhhsg8fqkpjfdgelnvyq3fnaglzw74kda4\",\n            \"tb1qy6uuespwqm9m9wdjvmwr07l9fvn0ge93mzskzw\",\n            \"tb1qnlesczfxk2z7xgeyep3tr3xkh3z8rcmh4j95gt\",\n            \"tb1qhksthm48t4eqrzup3gzzlqnf433z8aq5uj03jr\",\n            \"tb1q8564fhyum66n239wt0gp8m0khlqgwgac8ft2r0\",\n            \"tb1qjduurjclneffxv6tgv7rnspaxu85v7saf9mfj0\",\n            \"tb1q8k9sp22vjun7hf0sfvs2n8mfwt8xl43d68xml2\",\n            \"tb1qsyhawdg9zj2cepa0zg096rna2nxg4zj0c0fnvq\",\n            \"tb1qmy8uqjkh2d2dcgnz6yyrtjk05n5y4ey8qzayyu\",\n            \"tb1qk7u2mcu02v7fgvls9ttuwq49a6e5kae5kxkts9\",\n            \"tb1qm0z2hh76fngnp3zl3yglvlm6nm98qz4exupta9\",\n            \"tb1q9jtcype5swm4reyz4sktvq609shw88fwzjz9jg\",\n            \"tb1qckp4ztmstwtyxzml3dmfvegeq5mfxwu2h3q94l\",\n            \"tb1qaj8j0ldjswlmtamfxl3kgrq7gcc7nqnun4dwn6\",\n            \"tb1qy9986nnmd8dhznxfs9sh9c5nknnj0s4vwgky6c\",\n            \"tb1qh6qyzl3pds56azgjqkhkk7kfkavzxghjwsv0rl\",\n            \"tb1qmy2luym2mtyydcwlmf7gxpe30k0cp8gt7gj63k\",\n            \"tb1qtgsfkgptcxdn6dz6wh8c4dguk3cezwne5j5c47\",\n            \"tb1qe7wv04mlsg7hkarsdx07jgr7mgs80pe6nl87sq\",\n            \"tb1qrln929gz055hse2ylytl3cxnse6wxshek97t7j\",\n            \"tb1qadwk4rmwxcayxg27f6cpv46dusyksk273pq09u\",\n            \"tb1qdlv5pjqdk27x04m6xte3kcsz9h2euuylhv4tgl\",\n            \"tb1quneg2cv7v8ne9z64whgcvg6hzwhxuselhpak3e\",\n            \"tb1qa4gcpuzu0vwunqrnycpv2rx6gpfsuq9d4sg25y\",\n            \"tb1qqmvztjh4yg6x5fgw3wq685zkna5jv0e06v4ee4\",\n            \"tb1qpntvn7xwp2nn6alu7lc4d360tjlvdyrtzf02xh\",\n            \"tb1qr6g9qrkssn822tklp83accz4j9s4sat9g068g3\",\n            \"tb1q0k62kjqt053p37a9v8lnqstc7jhuhjtjphw3h7\",\n            \"tb1qalw5ax88hnr6gse240prn370020uxq505tw3n8\",\n            \"tb1q8zwvzwh8tnthf3d2qsxpyemu5mwwddysy5pxc2\",\n            \"tb1q2qsa3ygu6l2z2kyvgwpnnmurym99m9duelc2hf\",\n            \"tb1qc5ztxm2kvtrtxun50v0rn6asm9tv0t3mfzh68v\"\n        ]\n    },\n    \"invoices\": {\n        \"c58dbd42b883d60433d9fb626b772406\": {\n            \"hex\": \"0801120b783530392b7368613235361a9f160ac50c3082064130820529a003020102020900e1222c5cacc7e4c2300d06092a864886f70d01010b05003081b4310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e312d302b060355040b1324687474703a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f313330310603550403132a476f2044616464792053656375726520436572746966696361746520417574686f72697479202d204732301e170d3138303830323138343432305a170d3139313030313231313333385a303d3121301f060355040b1318446f6d61696e20436f6e74726f6c2056616c696461746564311830160603550403130f746573742e6269747061792e636f6d30820122300d06092a864886f70d01010105000382010f003082010a0282010100d14377b6f2b50aa73427c248915563d2c8804dd824b1d8f015dd1ae2a390c8022fd963355849b217309208c3450a016ff4001ee336a3189edadce12bc41e4999e45f6fcf0b00daa7e9a4b12e72dce19c51ae8e55b8bfec02b80a58ccb2c1b3e5bde81f679b9993bf52c871a1fadef6bb5f7d8d9208889400ba2be1d2baf82ec303470852570c3bbb6b89334d8974e61b8867bf299fb802c57e9b00d9b9f70572ac6d81fecd304c83aaf21f4f3b529e9898ea9b868f8f07b4189668e71854ae776bacd0d9706a8be03f528c68ad023e3b45bfa55e9b42e535aafc7eb8672645dcdeaf7204a468d2b84f27ed12072a411627647108e421abe7308e3bac305896f10203010001a38202ca308202c6300c0603551d130101ff04023000301d0603551d250416301406082b0601050507030106082b06010505070302300e0603551d0f0101ff0404030205a030370603551d1f0430302e302ca02aa0288626687474703a2f2f63726c2e676f64616464792e636f6d2f676469673273312d3835342e63726c305d0603551d20045630543048060b6086480186fd6d010717013039303706082b06010505070201162b687474703a2f2f6365727469666963617465732e676f64616464792e636f6d2f7265706f7369746f72792f3008060667810c010201307606082b06010505070101046a3068302406082b060105050730018618687474703a2f2f6f6373702e676f64616464792e636f6d2f304006082b060105050730028634687474703a2f2f6365727469666963617465732e676f64616464792e636f6d2f7265706f7369746f72792f67646967322e637274301f0603551d2304183016801440c2bd278ecc348330a233d7fb6cb3f0b42c80ce302f0603551d1104283026820f746573742e6269747061792e636f6d82137777772e746573742e6269747061792e636f6d301d0603551d0e041604142cd8def7d64c620cce78bdec365fd961cfd035e130820104060a2b06010401d6790204020481f50481f200f0007700a4b90990b418581487bb13a2cc67700a3c359804f91bdfb8e377cd0ec80ddc1000000164fbf55c500000040300483046022100d6a82312b4a16c7ed7911a2e559168ea77d01e3dcf9cfe63a6662e2661546405022100a43fe7cbe2fd9ae72a349ba1b19de71e7804cae2c3755774c4b8de4d432cbcd5007500747eda8331ad331091219cce254f4270c2bffd5e422008c6373579e6107bcc5600000164fbf55ed7000004030046304402201727366c115d0fb8f940cce989730727cc59d15c94120146f1c239989deeecaa0220602b946de08661ed7ff3ed64988a3d872151c89f20a9c0dbbc6edcc04870380a300d06092a864886f70d01010b05000382010100060cf534f7adea711fabcd9338fa89f4bc244da0a6af232f7d337f2d8ea79394343b5f5dfb07c3f2e71195755e0a8f3f51b0444e7e17b4b926cb0f9dc49253dc66f1ed8fd52297be8b515a4240ac59fc3f7f1fa810ab24b196c4ae57827abf25e76838edc0a86bfdd0c386f9c4b6a7fa94672b79177646323875cfff2da42106be46ff0e1b41e3b5706f1be19b582005fbedacf88d69370a2ccda66bffe6e7ef5f33a408707ebd3ddc20b1ff24d6bd94cc6db5e069277d78d56218cfddbfcd83f2be5e477734f548a6da0ae58bed212bf817914f3fe6f9afa4ad59fe5d97b0514e277dc6ba48672e074b23fe89a5f72c58ec139b3f1c09d557b6e7d2df5f52880ad409308204d0308203b8a003020102020107300d06092a864886f70d01010b0500308183310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e3131302f06035504031328476f20446164647920526f6f7420436572746966696361746520417574686f72697479202d204732301e170d3131303530333037303030305a170d3331303530333037303030305a3081b4310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e312d302b060355040b1324687474703a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f313330310603550403132a476f2044616464792053656375726520436572746966696361746520417574686f72697479202d20473230820122300d06092a864886f70d01010105000382010f003082010a0282010100b9e0cb10d4af76bdd49362eb3064b881086cc304d962178e2fff3e65cf8fce62e63c521cda16454b55ab786b63836290ce0f696c99c81a148b4ccc4533ea88dc9ea3af2bfe80619d7957c4cf2ef43f303c5d47fc9a16bcc3379641518e114b54f828bed08cbef030381ef3b026f86647636dde7126478f384753d1461db4e3dc00ea45acbdbc71d9aa6f00dbdbcd303a794f5f4c47f81def5bc2c49d603bb1b24391d8a4334eeab3d6274fad258aa5c6f4d5d0a6ae7405645788b54455d42d2a3a3ef8b8bde9320a029464c4163a50f14aaee77933af0c20077fe8df0439c269026c6352fa77c11bc87487c8b993185054354b694ebc3bd3492e1fdcc1d252fb0203010001a382011a30820116300f0603551d130101ff040530030101ff300e0603551d0f0101ff040403020106301d0603551d0e0416041440c2bd278ecc348330a233d7fb6cb3f0b42c80ce301f0603551d230418301680143a9a8507106728b6eff6bd05416e20c194da0fde303406082b0601050507010104283026302406082b060105050730018618687474703a2f2f6f6373702e676f64616464792e636f6d2f30350603551d1f042e302c302aa028a0268624687474703a2f2f63726c2e676f64616464792e636f6d2f6764726f6f742d67322e63726c30460603551d20043f303d303b0604551d20003033303106082b06010505070201162568747470733a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f300d06092a864886f70d01010b05000382010100087e6c9310c838b896a9904bffa15f4f04ef6c3e9c8806c9508fa673f757311bbebce42fdbf8bad35be0b4e7e679620e0ca2d76a637331b5f5a848a43b082da25d90d7b47c254f115630c4b6449d7b2c9de55ee6ef0c61aabfe42a1bee849eb8837dc143ce44a713700d911ff4c813ad8360d9d872a873241eb5ac220eca17896258441bab892501000fcdc41b62db51b4d30f512a9bf4bc73fc76ce36a4cdd9d82ceaae9bf52ab290d14d75188a3f8a4190237d5b4bfea403589b46b2c3606083f87d5041cec2a190c3bbef022fd21554ee4415d90aaea78a33edb12d763626dc04eb9ff7611f15dc876fee469628ada1267d0a09a72e04a38dbcf8bc0430012294020a0474657374121f08848e06121976a914314f713cd59781894277dcfdd31b5f179860b61e88ac18dbe793f80520dfee93f8052a5a5061796d656e74207265717565737420666f722042697450617920696e766f69636520526452684a4e3334426f45417a443151614c7067524b20666f72206d65726368616e7420536f6d6265724e696768745f74657374696e67323068747470733a2f2f746573742e6269747061792e636f6d2f692f526452684a4e3334426f45417a443151614c7067524b3a4c7b22696e766f6963654964223a22526452684a4e3334426f45417a443151614c7067524b222c226d65726368616e744964223a225372384b5774647158666b6a58563751704171773336227d450000803f2a80022896c21e381d423cd31672981c90f65919db4e30dd5a2f62b9be1e73a9a875ba176086e66507aeb67ad4b1c5cfd6bab57b49a6b8895b12bf14dbe868dfacc3ce04e5617c96bd1a4eb8c5fcb14122e569dfb9d860d67ed8d494f0a348a3e6ea00d27aab41c18ea625b872411543a52db374f97f7f01ed3df3be5f33551661ecb83dbab311d17d8942925934a0eab7965bb3e8a908bedd82001c6f1afe2ebca2616da288716452da8f4f32ca625a34f3f52b04b7ff32816b4875cfe3789e132ae1959d2a68d34aaed937c60b268cd7737889981071431e406318fd3619615d84647a0b900060f997ed8aa7ce50f65212d9d654409ae77001d3952b784a687a4082\",\n            \"requestor\": \"test.bitpay.com\",\n            \"txid\": null\n        },\n        \"tb1qh6qyzl3pds56azgjqkhkk7kfkavzxghjwsv0rl\": {\n            \"hex\": \"2229121c08f8c70d12160014be80417e216c29ae891205af6b7ac9b7582322f2180020002a0574657374742a00\",\n            \"requestor\": null,\n            \"txid\": null\n        }\n    },\n    \"keystore\": {\n        \"pw_hash_version\": 1,\n        \"type\": \"bip32\",\n        \"xprv\": null,\n        \"xpub\": \"vpub5VmsevU91fpRaJkfa8b6c9MK53gKY8rSzZjrZdp6dkHZjnFhM1HN74ezHY96JCgFnbQJhRbeUyr5S1vzdcTB6qUKrrG7GBuwPYDTzBjLQmv\"\n    },\n    \"labels\": {\n        \"tb1qaj8j0ldjswlmtamfxl3kgrq7gcc7nqnun4dwn6\": \"123\",\n        \"tb1qckp4ztmstwtyxzml3dmfvegeq5mfxwu2h3q94l\": \"asdasd\",\n        \"tb1qy9986nnmd8dhznxfs9sh9c5nknnj0s4vwgky6c\": \"sfsafas\"\n    },\n    \"payment_requests\": {\n        \"tb1qaj8j0ldjswlmtamfxl3kgrq7gcc7nqnun4dwn6\": {\n            \"address\": \"tb1qaj8j0ldjswlmtamfxl3kgrq7gcc7nqnun4dwn6\",\n            \"amount\": 20000,\n            \"exp\": null,\n            \"id\": \"dbc868ee2e\",\n            \"memo\": \"123\",\n            \"time\": 1594159917\n        },\n        \"tb1qckp4ztmstwtyxzml3dmfvegeq5mfxwu2h3q94l\": {\n            \"address\": \"tb1qckp4ztmstwtyxzml3dmfvegeq5mfxwu2h3q94l\",\n            \"amount\": 410000,\n            \"exp\": null,\n            \"id\": \"aa668f9d93\",\n            \"memo\": \"asdasd\",\n            \"time\": 1594159909\n        },\n        \"tb1qy9986nnmd8dhznxfs9sh9c5nknnj0s4vwgky6c\": {\n            \"address\": \"tb1qy9986nnmd8dhznxfs9sh9c5nknnj0s4vwgky6c\",\n            \"amount\": 0,\n            \"exp\": null,\n            \"id\": \"4329cf1c01\",\n            \"memo\": \"sfsafas\",\n            \"time\": 1594159923\n        }\n    },\n    \"seed_type\": \"segwit\",\n    \"seed_version\": 18,\n    \"spent_outpoints\": {\n        \"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61\": {\n            \"1\": \"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6\"\n        },\n        \"0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761\": {\n            \"0\": \"e20bf91006e084c63c424240e857af8c3e27a5d8356af4dbe5ddc8ad4e71c336\"\n        },\n        \"0448b48ce3cf3265619f4f915b2bbb8cab661666decdf5df805fbf884679c51e\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"1\": \"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82\"\n        },\n        \"07412f8a52ec8d3a58f9911daeccfb4164a368d5d8e36f354a72edf722119415\": {\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"09a4c03e3bdf83bbe3955f907ee52da4fc12f4813d459bc75228b64ad08617c7\": {\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be\": {\n            \"0\": \"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e\"\n        },\n        \"0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0\": {\n            \"0\": \"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67\"\n        },\n        \"0b39f916a3889c69981d9285bade5f078cbb07e74502311d9c2417a7a638de52\": {\n            \"0\": \"19a666acc1bb5656e49a1d99087c5a0c2a135c40f1a58e5306f2d85f46786801\"\n        },\n        \"0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d\": {\n            \"0\": \"4133b7570dd1424ac0f05fba67d7af95eae19de82ff367007ac33742f13d1d8e\"\n        },\n        \"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0\": {\n            \"0\": \"b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2\",\n            \"1\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687\": {\n            \"1\": \"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867\"\n        },\n        \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\": {\n            \"1\": \"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271\"\n        },\n        \"11769927f369180fac9e3d728d084c46aa0b8bddef99d4ea85e580d3dc1c30e9\": {\n            \"1\": \"4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e\"\n        },\n        \"133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"2\": \"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5\"\n        },\n        \"18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf\": {\n            \"0\": \"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67\"\n        },\n        \"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04\": {\n            \"0\": \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4\",\n            \"1\": \"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e\"\n        },\n        \"22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f\": {\n            \"0\": \"72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390\"\n        },\n        \"270d167a047a9414fa301029bca0faa909af033fae54400df83dcdbd260ebd52\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"2860ae00b39a0411768c897bdb806cdacf2aeefd62d6d95fdd20f648bb82b211\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d\": {\n            \"0\": \"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38\"\n        },\n        \"2fb9610fd2307d342b735e907eebb804571807e78549c9df322f428d1b863ed7\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"3116900ba48ad3dcd112dd876764c44dca9e68b27ac36d5417600b1e00e6ce6e\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"315223ccb031589de2774fc51a0b277952c04c9d576fb8fc651830286325b350\": {\n            \"1\": \"18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf\"\n        },\n        \"3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549\": {\n            \"0\": \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52\",\n            \"1\": \"43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35\",\n            \"3\": \"133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62\"\n        },\n        \"366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f\": {\n            \"0\": \"65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc\",\n            \"1\": \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3\"\n        },\n        \"38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3\": {\n            \"0\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"3b9e0581602f4656cb04633dac13662bc62d9f5191caa15cc901dcc76e430856\": {\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"3db1964fe70b0d8511bef6be222fcec67159b6412050e56fb2dd36a4bcdce039\": {\n            \"1\": \"26b1fb057113f6ce39a20f5baa493015b152cc1c0af312b3ee8950e9a4bbf47a\"\n        },\n        \"3ee0eb4cfbc1fb73d5facbebf310c9c97f7e14b94090b409f274ea1d2d4c6ad1\": {\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1\": {\n            \"0\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab\": {\n            \"0\": \"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877\",\n            \"1\": \"fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa\"\n        },\n        \"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15\": {\n            \"1\": \"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d\"\n        },\n        \"43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35\": {\n            \"1\": \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52\"\n        },\n        \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3\": {\n            \"0\": \"781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05\",\n            \"1\": \"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295\"\n        },\n        \"456469af3de265b2f19c11f27ea88ccd17beb0831fb7c4864498125117ded136\": {\n            \"0\": \"d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156\"\n        },\n        \"48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a\": {\n            \"0\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e\": {\n            \"0\": \"40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1\",\n            \"1\": \"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0\"\n        },\n        \"4a33546eeaed0e25f9e6a58968be92a804a7e70a5332360dabc79f93cd059752\": {\n            \"0\": \"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755\",\n            \"1\": \"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9\",\n            \"2\": \"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9\",\n            \"3\": \"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755\"\n        },\n        \"4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e\": {\n            \"0\": \"48a4cd850234b2316395959c343a3cbfd57e706e256399ac39927330f03268d6\"\n        },\n        \"4cb75138f2da3440b29c5f52a58d0bcfc5244344a9dbf300005ad52e2e099782\": {\n            \"1\": \"48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a\"\n        },\n        \"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755\": {\n            \"0\": \"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be\"\n        },\n        \"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc\": {\n            \"0\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"530d1c4c48a351e76440f3aabb51c16d2028ad83e66f48338b6354842d44127d\": {\n            \"0\": \"0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0\"\n        },\n        \"54ede80a5e9fa5c457050f8781cf7c5de0d729f24841585a78ade023edd4e83f\": {\n            \"17\": \"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4\"\n        },\n        \"56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b\": {\n            \"0\": \"dc16e73a88aff40d703f8eba148eae7265d1128a013f6f570a8bd76d86a5e137\"\n        },\n        \"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b\": {\n            \"0\": \"6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a\",\n            \"1\": \"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b\"\n        },\n        \"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54\": {\n            \"1\": \"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab\"\n        },\n        \"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38\": {\n            \"0\": \"22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f\",\n            \"1\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc\": {\n            \"0\": \"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057\",\n            \"1\": \"d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee\"\n        },\n        \"666e42b90a3f74de168f98eeb1603cceec83bffa4585722f18fb62e5859a5c28\": {\n            \"0\": \"0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d\"\n        },\n        \"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4\": {\n            \"1\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1\": {\n            \"0\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"6ae39e1fd138cb8a52b349ea6d1b13e41eaeb9586704fc2fa5c6381bef899094\": {\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b\": {\n            \"0\": \"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab\",\n            \"1\": \"0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761\"\n        },\n        \"6de9a4b2a954ae980b96c949f21b9ac6eed38df72736de1936ff18b3c3a5f378\": {\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"10\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"102\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"106\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"107\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"109\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"110\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"112\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"113\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"114\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"117\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"118\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"12\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"120\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"124\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"125\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"127\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"134\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"136\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"138\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"141\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"146\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"147\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"148\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"149\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"15\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"151\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"153\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"156\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"16\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"160\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"162\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"165\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"170\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"173\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"174\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"175\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"176\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"179\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"189\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"192\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"195\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"198\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"199\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"200\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"25\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"26\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"30\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"36\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"43\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"51\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"52\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"55\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"57\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"59\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"72\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"80\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"88\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"89\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"93\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"97\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"98\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a\": {\n            \"1\": \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3\"\n        },\n        \"7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55\": {\n            \"0\": \"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877\"\n        },\n        \"72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390\": {\n            \"0\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4\": {\n            \"0\": \"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc\",\n            \"1\": \"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d\",\n            \"2\": \"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d\"\n        },\n        \"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3\": {\n            \"1\": \"d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3\"\n        },\n        \"77030e79f58370693c857efd71c77d0b1b584059361df378fd6362a23db1056d\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d\": {\n            \"1\": \"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c\"\n        },\n        \"7a0de50c0a753910a7eb1d0c6f1c4738b45bdceafd994c0504e5283210cba5df\": {\n            \"0\": \"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61\"\n        },\n        \"7a6ec63d6cd61edd3058f3f1c8da65a7b208d9b5119d68929ce36054fac44fa5\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\": {\n            \"0\": \"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94\"\n        },\n        \"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a\": {\n            \"0\": \"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514\"\n        },\n        \"839135d3a4e74e7d64b62b0f3c3176528ff7039c26ebaa19973a26478752cb46\": {\n            \"0\": \"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82\"\n        },\n        \"85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834\": {\n            \"0\": \"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057\"\n        },\n        \"8ab025435b1353d78b1d20992c23c180534c9202846360662e5f1f5007b67f21\": {\n            \"0\": \"7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55\"\n        },\n        \"8b344d1b83f0c8ea3b3152a10bfa51c5253e31531d5b456195ec43e07169f289\": {\n            \"0\": \"69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1\"\n        },\n        \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"1\": \"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7\",\n            \"2\": \"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5\"\n        },\n        \"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5\": {\n            \"0\": \"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e\"\n        },\n        \"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d\": {\n            \"0\": \"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2\"\n        },\n        \"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514\": {\n            \"1\": \"866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff\"\n        },\n        \"979b000c96f9842165ec1449c3bb4629217e4e95b3fcb75ea3541e4b67b64af6\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd\": {\n            \"1\": \"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687\"\n        },\n        \"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e\": {\n            \"0\": \"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04\"\n        },\n        \"9fb64bbd59cb2bbd1d16b43f74f795d39375354789420b2ebfc8124fae3958f3\": {\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9\": {\n            \"0\": \"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135\",\n            \"1\": \"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15\"\n        },\n        \"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867\": {\n            \"1\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"a912ebcd60302c657301c49b0e20709d8ae29aec2b7b1459fa7357425d6769a1\": {\n            \"0\": \"85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834\"\n        },\n        \"a9d9481cd3d1501b7a8d4360cf8af9c81f907ea80def3ffcc656c0373b22ba5e\": {\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6\": {\n            \"1\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271\": {\n            \"1\": \"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd\"\n        },\n        \"af2c12ebaec902f8d3605c18473a274d6fb90adb9a2caac6196ea020292bee99\": {\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"af907dc5a4c177c35126456f4c9069bb90a133d1b2b15111cf8d72f4a56f11f7\": {\n            \"0\": \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c\"\n        },\n        \"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5\": {\n            \"0\": \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\",\n            \"1\": \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\"\n        },\n        \"b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2\": {\n            \"0\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"b6ddbf30659904350d05b4829423f4acea0820d52d1bed4ffcc33ccfb82d56e6\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"101\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"102\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"107\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"109\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"12\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"15\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"17\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"18\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"2\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"20\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"27\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"3\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"32\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"39\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"41\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"42\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"50\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"52\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"53\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"54\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"55\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"58\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"62\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"66\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"70\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"74\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"75\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"77\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"78\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"8\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"80\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"81\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"82\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"87\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"93\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"94\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"97\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7\": {\n            \"0\": \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\",\n            \"1\": \"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d\"\n        },\n        \"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94\": {\n            \"1\": \"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9\"\n        },\n        \"be7ba3c3de4f62592e23e546fed8e1ba6b02592ceb8b297c04d536d57d6a9218\": {\n            \"2\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9\": {\n            \"0\": \"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be\"\n        },\n        \"c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\",\n            \"1\": \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c\"\n        },\n        \"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295\": {\n            \"1\": \"94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9\"\n        },\n        \"c26329086092268a5f6a18bc20d81271dfac5b65ebbd6ceb6d103be5a5adf4e2\": {\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"c27901da01e6e05ec21395e7ce0a9b69c6b7a0d30d9f043c60a25843d3243686\": {\n            \"1\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"c758ba0bafdb3b25d8cf866809db64ece5d31cb5bfe418369cf945db02def241\": {\n            \"299\": \"c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0\"\n        },\n        \"c83b20ef4c53362cd1dd517b89005f638c681f2bf7f66384114c6e4105f73066\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"ca18da7ac5f817579f971324034577a564600384e6cdbc45acd9f2fc63835b7a\": {\n            \"1\": \"56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b\"\n        },\n        \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52\": {\n            \"0\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n            \"1\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"cd8a9b7bb5780b911f1d627bea5aa07e92fe0da8cd799e7640f8dbb0432ac9f0\": {\n            \"1\": \"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4\"\n        },\n        \"d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee\": {\n            \"0\": \"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506\"\n        },\n        \"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82\": {\n            \"1\": \"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a\"\n        },\n        \"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67\": {\n            \"0\": \"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc\"\n        },\n        \"d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156\": {\n            \"0\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5\": {\n            \"1\": \"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5\"\n        },\n        \"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c\": {\n            \"0\": \"ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21\",\n            \"1\": \"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b\"\n        },\n        \"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135\": {\n            \"0\": \"366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f\",\n            \"1\": \"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54\"\n        },\n        \"e64d9d8313736de85e55d1bcf43d24ccd3f841dd6654b53d525bfc69d7f70eea\": {\n            \"1\": \"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82\"\n        },\n        \"e8498c07730f78fcd30a2f5ad4f5df7191db947a7e5f70bd3e0f07f205a473d5\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\": {\n            \"0\": \"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d\",\n            \"1\": \"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d\"\n        },\n        \"ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21\": {\n            \"0\": \"9e0789481c2f5b2fda96f0af534af3caa5cb5ddc2bbf27aea72c80154d9272d2\"\n        },\n        \"ee817525ed76318f476385e96173a676efb627d265eb86e4a5c483f3e4b9adaa\": {\n            \"3\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2\": {\n            \"0\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\",\n            \"1\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"ef91cd3d463aae564cbe08af7aa89f38cddb536c6701cc615938242a6ddede20\": {\n            \"1\": \"3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549\"\n        },\n        \"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d\": {\n            \"0\": \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\"\n        },\n        \"fc579c56611dcb0681e5b81ceea2b004af1018c4903ac4397e229860ebd3d9cf\": {\n            \"0\": \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\"\n        },\n        \"fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa\": {\n            \"0\": \"901856f632b06573e2384972c38a2b87d7f058595f77cd2fcca47893bb6dc366\"\n        },\n        \"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877\": {\n            \"0\": \"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3\"\n        },\n        \"ff6ea64ce1a2a38ccbdd51998290250d8b9c7937caaa5c89bb15ac276cb9acaf\": {\n            \"1\": \"38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3\"\n        }\n    },\n    \"stored_height\": 1775877,\n    \"transactions\": {\n        \"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61\": \"01000000000101dfa5cb103228e504054c99fdeadc5bb438471c6f0c1deba71039750a0ce50d7a0000000000fdffffff02d3030000000000001600142a9d0b586e93440c6f6dde7baef3cd5c04d5bb1ce8030000000000001600148b589790abda555b54946c7dd23d9dc13a0a019d024730440220378df1dc830292515f18fc1d1d31dd93ca6187705dee6983fe0e6c104081e91402207e457ebc320cff113eeb78ea875c5be07ff4f48626ab6e21ff957f5d1c91ec5a0121036fb36e520403ef925e1ed41fcb6c858fd425c7cf4e4bdeda7d85cdb469d9d407608f1500\",\n        \"0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761\": \"020000000001011bfe68f11fce4d584911c02191fd17784c9b284b76ca26aad6fa9f497cbcde6b01000000002852208001c99598000000000016001486eca9634910df929ac9f38bf8dfa783d66bf62c0400473044022009f6111e0b18921ccb5bd5b0f603caa6093571e454b8db2ff67755e8e924a4ae0220157d0779b5ba3f79f71b94ac9ec78f1bc0cf299da6b0ee14e0bbae2716c79bf2014730440220605b3de279569bd70e6419251de934e31739fc49092862d48348010dcbd7e6220220295b65fae25fa2bc84fe9f642e56206e2d1c25feac7d4084b5a3547af4d95ee601475221025f3c508d0adbddd6ce87b0bcd173bb0a0e3dc84de8e755468a01f298667f104f21032157fde1aa538570586f9e3978ef7ba1c9ea61fe04678e6f65bc196a7d57919452aee02af620\",\n        \"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be\": \"020000000001025597474bbcb6ca948d182d3f97df2934e08a34a4abfa441ebab8ba0bde612a4d0000000000feffffffc924805fe715df8e925743a62a63152b8901463789027ee4ae967767660f5fbf0000000000feffffff02a1e64700000000001600140ac1eddb8b03f7aecdf30a4eb395a662f38005e7ffffff0000000000220020675c976d0e46f9ac11025826d4d968437588bf540043ea62e6527336a30f10c30247304402206ea454483037f344d2f009488a7e044be422e172e641811d84634bfbe8899b6402200219c939dda27e1a99a6364e3f86ca536d7e942c85e77aef6744d2e3af55b4810121033025b5ffd9e8abb848e1757525ba93e1ab131497d5c811f6a1987460f274744d024830450221008ec109e40f11b7def5e5423ed87b166958dec8b8c21290f25d4e175d5fbe01a702207af2be6161b73e3eefca6336f4fdc4830d49b554bc30721f169a31321ec039c7012103db186cb34a832d565fd6400aff799acbd932e6ed8bed195aa0dc855207ffa9a5e28d1800\",\n        \"0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0\": \"020000000001017d12442d8454638b33486fe683ad28206dc151bbaaf34064e751a3484c1c0d530000000000feffffff019a33410000000000160014b7b8ade38f533c9433f02ad7c702a5eeb34b7734034730440220511e317b4655fecef9cfeb68356a4382ec5b8f6fc7ed4a6eae55ee940dc15d88022043d1006b4540777c5b3e3119df9e60bd0685a83c1dc0f597d68cd8d8e249791701201df376228cfff0bd7fb256b355d0e51a432c3e486622acdd6612e3d2b132b4326a8201208763a914c015c8f3b13d6fc9da8db184c2ebcbf4e4ad5a6c8821037a80615a0c38fc944f003225f7670dcca9ac4b9ccbbee21c9bc95e02d3377efc677503e20b1bb17521028710822dfbbfcde0b2dd793d1dc4195387fe8c0267b04daf2b5d0fd2382d6bb068ac00000000\",\n        \"0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d\": \"02000000000101285c9a85e562fb182f728545fabf83ecce3c60b1ee988f16de743f0ab9426e660000000000fdffffff01acf0ff0000000000160014b76eecbc36c4e7b2a44e24534805cd2b191c31a2024730440220571cf8bceba931853a49561eb32e5998995b5683308f7729215f2a29db30a101022074f1172e6ccba194c5b242eb176ca4d721758b620f16a5e7d6a5288a8a83c0e701210244a0615ab8a53d42252d371b8dbf4f6802e10c3a3b708075fa681746e3a6b5ce00000000\",\n        \"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0\": \"020000000001018ed0132bb5f35d097572081524cd5e847c895e765b93d5af46b8a8bef621244a0100000000fdffffff0220a1070000000000220020302981db44eb5dad0dab3987134a985b360ae2227a7e7a10cfe8cffd23bacdc9b07912000000000016001442b423aab2aa803f957084832b10359beaa2469002473044022065c5e28900b4706487223357e8539e176552e3560e2081ac18de7c26e8e420ba02202755c7fc8177ff502634104c090e3fd4c4252bfa8566d4eb6605bb9e236e7839012103b63bbf85ec9e5e312e4d7a2b45e690f48b916a442e787a47a6092d6c052394c5966a1900\",\n        \"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687\": \"02000000000101dd12647382dc8e8620141b0ff1f5bb8bcdc6eecec47d6971a4d1994d09aef49d0100000000feffffff02ffffff0000000000220020c3b2464da6785a0e63d9b70624eefad269775016846e0ea4bc9819269c6abe1f38dff5020000000016001480d1350905fa099677475a24063f8381e2f6627002473044022019459342deef15f596c388ce68749080d2a54a20dd014a2d3fdcf2206eb78834022063693a44dde77f30d52aa1a9aa940a9bfd2c5013e05ccd50d3173472e8bf415c0121028833486dfa1a24d0aaa9d72deb0d69a835dac9fa6dbc5b86d6d5245f126b057be4981800\",\n        \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\": \"020000000001801ec5794688bf5f80dff5cdde661666ab8cbb2b5b914f9f616532cfe38cb448040000000000fdffffff15941122f7ed724a356fe3d8d568a36441fbccae1d91f9583a8dec528a2f41070100000000fdffffffc71786d04ab62852c79b453d81f412fca42de57e905f95e3bb83df3b3ec0a4090100000000fdffffff62ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130000000000fdffffff62ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130100000000fdffffff52bd0e26bdcd3df80d4054ae3f03af09a9faa0bc291030fa14947a047a160d270000000000fdffffff11b282bb48f620dd5fd9d662fdee2acfda6c80db7b898c7611049ab300ae60280000000000fdffffffd73e861b8d422f32dfc94985e707185704b8eb7e905e732b347d30d20f61b92f0000000000fdffffffd73e861b8d422f32dfc94985e707185704b8eb7e905e732b347d30d20f61b92f0100000000fdffffff6ecee6001e0b6017546dc37ab2689eca4dc4646787dd12d1dcd38aa40b9016310000000000fdffffff5608436ec7dc01c95ca1ca91519f2dc62b6613ac3d6304cb56462f6081059e3b0100000000fdffffffd16a4c2d1dea74f209b49040b9147e7fc9c910f3ebcbfad573fbc1fb4cebe03e0100000000fdffffff949089ef1b38c6a52ffc046758b9ae1ee4131b6dea49b3528acb38d11f9ee36a0100000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d0100000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d0a00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d0c00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d0f00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d1000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d1900000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d1a00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d1e00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d2400000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d2b00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d3300000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d3400000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d3700000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d3900000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d3b00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d4800000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d5000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d5800000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d5900000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d5d00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6100000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6200000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6600000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6a00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6b00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6d00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6e00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7100000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7200000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7500000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7600000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7800000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7c00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7d00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7f00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d8600000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d8800000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d8a00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d8d00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9200000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9300000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9400000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9500000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9700000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9900000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9c00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96da000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96da200000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96da500000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96daa00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dad00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dae00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96daf00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96db000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96db300000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dbd00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dc000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dc300000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dc600000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dc700000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dc800000000fdffffff6d05b13da26263fd78f31d365940581b0b7dc771fd7e853c697083f5790e03770000000000fdffffffa54fc4fa5460e39c92689d11b5d908b2a765dac8f1f35830dd1ed66c3dc66e7a0000000000fdffffff8c6d5a1b0a0b34113c0f73cbdb61ad490743dca7b9e3f335999f209c1eb4278f0000000000fdfffffff64ab6674b1e54a35eb7fcb3954e7e212946bbc34914ec652184f9960c009b970000000000fdfffffff35839ae4f12c8bf2e0b428947357593d395f7743fb4161dbd2bcb59bd4bb69f0100000000fdffffff5eba223b37c056c6fc3fef0da87e901fc8f98acf60438d7a1b50d1d31c48d9a90100000000fdffffff99ee2b2920a06e19c6aa2c9adb0ab96f4d273a47185c60d3f802c9aeeb122caf0100000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60000000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60200000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60300000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60800000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60c00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60f00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb61100000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb61200000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb61400000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb61b00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb62000000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb62700000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb62900000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb62a00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63200000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63400000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63500000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63600000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63700000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63a00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63e00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64200000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64600000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64a00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64b00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64d00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64e00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65000000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65100000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65200000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65700000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65d00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65e00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb66100000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb66500000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb66600000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb66b00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb66d00000000fdffffff18926a7dd536d5047c298beb2c59026bbae1d8fe46e5232e59624fdec3a37bbe0200000000fdffffffa0ddbc2ef967817407d33c3650e04f81c57623ca8a4b3bf21a58cc1b979447c20000000000fdffffffe2f4ada5e53b106deb6cbdeb655bacdf7112d820bc186a5f8a269260082963c20100000000fdffffff863624d34358a2603c049f0dd3a0b7c6699b0acee79513c25ee0e601da0179c20100000000fdffffff6630f705416e4c118463f6f72b1f688c635f00897b51ddd12c36534cef203bc80000000000fdffffffd573a405f2070f3ebd705f7e7a94db9171dff5d45a2f0ad3fc780f73078c49e80000000000fdffffffaaadb9e4f383c4a5e486eb65d227b6ef76a67361e98563478f3176ed257581ee0300000000fdffffffcfd9d3eb6098227e39c43a90c41810af04b0a2ee1cb8e58106cb1d61569c57fc0000000000fdffffff02c0570100000000001600145e943a84bef855b4eb05ea509a0a543e2f2f36b600e1f50500000000160014720e942cbe6f9db8cac236ec29bb3e9a0c479fe70247304402205ca4578de2fa8a5938f3fdefa3d72983366762111d794af8c7bb414b4a96da360220667e1d360c3354c156c4a9dd37a41fe21c9dc68fc5fc2ba7af656707168a6c20012102f241daab862c0c11a11923f1a07dd27a7f20c76ea4b5291b161b2fb83f0d6e200247304402204d8c6a23be6872dc7c10948a24a35fa06e8bef2b00b943c23c8ba57834fe41ec02200a8e52c8d4d00694844027c8bdb31addec086b87467b797287b267104a83f387012102a95d186deb94de2064e94d0b5fc2f5b5d44bf26ce0d56a395110bc7d3f57a29102483045022100d88f21c74735a6fda572831d3997b783ae0c9d6566054c8df33e8a971fc2356602201bfe3870c53101581e20a9f6ae4daeb22642b9d43f05dac63b126e7db2f05cb7012103200803a9de725739d0e57b31be2c335a145df533e2f17cfb5f12245ad5fd6ce202483045022100a39882bd88c2ea7a6b6c6f0c740d98254f36bb67f1e8430992ec3da661889657022072ae60f95d60710f97b5165f926ad02e13350521fe4d1656913f9a402b931e11012102701d428192a54dffc6f0ae98d22553e994fb75c16af0d69ed6fb6f895c5d5c8a024730440220034e90bed6ec9b473415432f3817d46dffdf02563a00a3c96310d102db23f9eb022060e0803af307f57c902db623b7e91c8bd1300878fb640afcd97ff503400b5d2a01210214c000fb566eac4c72bf453f7559657678fb248996f73dd223abd389cb931a3f02473044022075e9da9f05c7cdc3f6aea1201a9e31ba19120c669e152a783b363eb8a9b1ee9f022013345feadc0dd8b1a684034a7145344701e13f598e42f8454fa49e033b473ac101210305b8c5c2d662b639c97f718dbd224e5ab4c5dc247e76ec093dc9bce2eaf79e7702483045022100b20783343dec7c5216b4fd7495df3a5227e316faea08635c971e6ec32c8e35ea022049f2098d1b1196d920833b9bf856ab8d2ad851ab6ac03dc95b1da786483751c8012103441a9dc8b461423981c7fd279748a9d44c75d3b7828e74b6336c8d1f52ad0e8f02473044022041a939dd09dbc1d4bd62316bf968df1a1e5595f6d8d942da0ccfa8394e79e27d022031d341915760c889c4eb9d5c9775b582fb88636677d72b3ceb8f0dc0acc6a3f1012103a9643d6b9f3ecd6508ef55910eb9221777c324898133d33ef75bbee074cb2c18024730440220685f90c7ff1019dace8750b34f6fb084434dc6b7c05aabd2347c9c0c53a1f0b10220425fc844d68283896394cc1581fa3b0f5d896d5d229778c7624d2d00dbe1a6e7012102936bafa88cb5a3230811280cd49c35c089b9e0fbf580f02f9d66536030008ebc0247304402200df330de41c70930868da17341263605a3e4141a49b1be5b0d53186f15bb84a102201fb48d676de243711dc743b559ff9f0620b562caccb2683d70a1b6a5da61b35a0121028062a5a9f3af3e3738b4b64a89d101be4ae853e2d887b504b9b830531d18c17b02483045022100e5bbfc79c8f73445b5bf50c08ea229f1e0f559355b8f44503462c40626570e140220441ab082d66947e755dfb32585dc00aab9bf4f5eba4af9eae1687bec6ba2f69f0121033d42c7bb48223e6fe4823c8aad640e42fd0a010e074b9e1edfa0fd010ad7468602483045022100d7d9f34075e68ec2399f74e57f46828788eebb4619ba2fce607458b6ed5efd7a0220344c1a88f06992e80e3f4a547d0edc62e879402321fe3c80271a28900199225b01210244d85f07a4f91d2bb69eab264b43504df429f1a130230aa27a289c3ef0feeea102483045022100bdd81c1405cab31b2e1b2c0f79c2689c19a043b8c85b92b97be215c4c6b30f5302206aed161fb8b262141d5598bafbdb262345ecbdad0320b881f03339fa7d66252d012103d3051fda851abb65863a294b9f41cc6f0f7b9e335301ac0fe417cec694344e2b02483045022100d62d62a47e08975f6e3651ca9d5570c25c0fcacc2e523336d06993244072a38202201bf60faa4139a391729df3336125ddd9571b67e7b2801b2bee848f8a17157faa01210361c902c442ec98654793f958606361d682d15cfc00dfc599615cc893414c8c5d02483045022100831cced452304d326364ff46e9d7bc69fa529eab51280bea6a00edfec561e7f20220161bbe02551d6edac0b576285837732dbc9ea64cb7eaadbabd7ab9a8bf69fc79012102fee9e35cb0d7ee40239c05715ff13adf46d7304b5a65986f53eea0dee1d1ea4e02473044022013c34bafcc250d9c474dad07be5d231b1e2295e9f2882b7967d20fd1e9f77a070220608cae845e5f9ff0f7f82f97bb04cb6859b32961213d22e2eb8e54c1eb45a16c012103ac3998602ce0f36d5f39ad56e9c38dae5c1e309ff421da2b1cff0a0129f0bb2c02473044022011f840a0b98d0cbc4749f33374c580cacedc7a6d3868eed4225a77165912c947022056802dbe210f387798d8c4d99a8934f1558301a6abf95312dbf347f64631f7de0121022269d914457a7d544e647850066f74baddd7bc84cfc294ba3118d78e98ef98e002473044022018e807aa65eb0fa25d2d5d209cfe195884fd7db7ed80e0f0519cdce5e2da96d602200649378859459da4dbb340005ae763230d61befee1a89377d66d052f02ad051a012103e795f563cb1329e76311a11623b7baaee423938e07f02b37d25d1130bde3bab902483045022100b654c8012ddb4d55de3c9682e55e65c8bba86ad82a5cf189acc9cd40cb2e6fdc0220608bf0ab5ddc206ce8a31bb156e6ed656eb8f6276f129d64cdc406bfdd7a7a25012103d8f6cbdd107a872da15104db9b16233162da3386204fffbef45b759e31f0e9810247304402207f49cec4cc4b79b6ae5a98f5c33577796ff3259643012c2ba2f340a772b41cec0220085d08c537ac6426b403cc8c64e2d5e01a22f72cffd44085f1057e3a87d210f10121027258650586c18aec62a509b9d5435fa2c2d574c6a05712a3aadd67b119966c4902483045022100caa3e740e74a0227425ea41d63050976612534fb739c3af61f05cbd90090e8a102206e467b07ea5b228e4697c3ec58c596b191895c9b7ad426f7da299f402598b1be01210360cbe3d42881780f9b95023be84ef1b54705c294c67527029f06346c302c76310247304402202fdf8f747827755307ba16cbe8495b204c576d3989972fc87a41c89bb886c53102207a791f5db12764642d083280867f042a3fe03014a73c81915f724f2e207b072a012102df6a0ed5df4d5c4fbe456a073b6b348f77c2f0291c824ede694fa564ecef9c0202483045022100e5066e2c0dc41afa627b71ad22098a47895916dee4a61fe2af1fb3cb09e9959f02205b55fad3e64889b77ca880c461812c0c0f2b0651e9708f559ba6ab12bcc15ffa0121032bc03395fbf0f9ccc484b155fc756ff42e2fcddf300e78eb038296750560212802483045022100d57e90e5b8a2c02902a8684d1a5f18114b3551b059936921cf9ea81c66bc72150220062f83973ee29a2a2c158baaee0cacd29703794faf7604266e1bab534f6bafe90121039810dc13eb43259bae6f0324df29c5de8055414a2424d2b3f0a4df088685db4b02483045022100a30511b70e6580dc9908e507ef09bcfa4b11aaa631fffc4ba3a977565e98a747022038bbad47e77d5ab47ae08d6baf630358daed5fdd994a8606f9d68ef9c8dc7a5501210276503b497f680b5516635131b411fc2196e3e06023b5906cdaa46e4a2ed2d2f0024830450221008dc03e6b9a360a3be9e2b1675684c26aa33ebc1d698d9f8716c408f254aaaf6e022036fe6715680a3472f1b2ebf05e285a72df3284ca9a2bb15fe45714704bcfed0e012102b6af3fd0c64ee3e9f49c67492419dcc41b303fa7417d72de493c56899a8726f902483045022100b321a8c56fa7d973906188e83c7140d1fc7241bd335b56283cc6966768380e2e022036f85d065bfc585905e9d02c72142a44155f0d531423e8ab5bff89bc8afe743901210203580b308ba623468054d7e2b6586aa580ea823878a61b2f3ca7dffad6a66c0b024730440220660970a008a34ee4c0f07fef354f92b5d51d689d938e52f0737bc8bc1cb3d2b402207e51dd30a9b13193b46a8eccb2b01e0da4814d1ecf7a78f82d844feea56c3579012103cb1332962aff65740d3a1f27071e1592e357313e4f5a273d9ae451bd566b7dc70247304402201a4af8bb735fccf12c537f21823102a76776755def68c609d8d2c42cc27d6d1f022003c2a9d217e171fd3f4bc81c7266e41f62087723565f35d11f7bdc727141e1c90121026bbd0c860bff7c78a5cd1072341b9eec2a97ff93f6da65b3a9f89963f075177702483045022100dbf86780582a3572fd26e4068bb4a858cd9c96b96ffea8d158ef7e75faf29a900220667c357c3bc9875bbbce30c384568f04d018e1ca38aea1d88425de7a2c5a43df012103e3fbd0562e3eee6f5e146380e4c207cf86d71d468dbfba04b666ba97c572fea80247304402206ca203f3b0be03b74ed24f29f9e6fa6308e754543feca71bad94a925fc90c7a602204ba06478fba93d1224a29fc368ab6a42f0b814a06b8264d47661e332b4047c7201210371958a463ec5b8a8b1e677c925c844827732dc57663df524fc282bf4a8e63a3202483045022100b3a8e3e37f3604618795e09c4c902d83c2e8be56ff13c1dc4a01317c240b27aa0220665785e980c0efbd6fed0d5030ab8fe821d9c1ac7bb70ca7b385def3fbe3eaae01210265a54945095ab51bf92b21d67e854a48761992eb200923128e590536f3c6bd3b024830450221009d7973b55322a502db88d944070bc629cd2611925244a6821c2ffcc7d526789702207761c6b1c2c49526a118fae1ee16f89203cdcfa285c6e3c09409cc013c51fd1301210317c90ded4e94522e0ba9095323e7c54a48a01854b48eca8e8438c809115365310247304402200c94a592bb0647a3c99bf1e7236a585fa23cd0f6c0511048dd386eba6a731d1a0220588b05f9f4f6fec5d4c1408dcb9789d63553adcabf62be98f4171da51fcbd3e7012103bb7347770870dad01bb64249130dcbd01c6c5d0161128f07c950e1767e52eccd024730440220048e5341582f04b14e8417719bb0c4e5988ddc00a82d78736adbdd75edfc8f0f02206cda1f7ee5c1bb143a3632d650262986cd0b01084b6df62eb2fdd43ca4b936e6012103beded311004b9b0a74f372404ad26931bc602f02ce5e73359a9ffd53932bd5010247304402205a7464c7ae24f5b798c9948e3cbf0bcab51779bf0204c14cc728f62978928d32022018519b54e2d9df4ce216271f4bcc9b464e70e9332c4f0d9d7d4208a420ddef1f01210297422f440617cffe84b7f262e7d9d7d3a096ae35ca6671b82a7f68e1aab06e54024830450221009d6d2829a6d83292daec5464e3b898140af57342256e05cda5e30d7f6aa62ddb022075dc9856a634e8c9f63d915d6bf843040fe501a17443d7ddb608beea54ba3304012103ce1fae187ace35be51966b8c7f297071574a3b38f3c5bbfbf86c8040d42355b9024730440220513cf06c6282679806451245be1925e76a4ce12acdeb3d281bf13bebb31e778c02200abfe90103eb0b5fa4618509e20726df44e624925d6483a3d38be85948e4844e012102b1d57fd3af19ce5db8e5ae1cf49e35d950dd0af9064b0341540219c9eb7f568402483045022100e884e5becddf0ce3f8869c15d9afdcd5935fe2e4e894ac63c0f446f15f75e939022013e53c863dbea2eafc69989c3f051218037af4cd1997b84eafafcd512627520a0121031f1a78704b847e8457a305250b714193be3f91be4af76014a48e458c65e04ffa02483045022100cbef8325309828f85ec85db4585682c17acdb73bedf7ad5af4953333440b2c0402203177d3fd532d1b3255b432398c83c4db7caa2df8bf6b9b747741b6679e6bad750121024c1e587d8fea9bf063989908c6fb809fb934aaf837e728792b66bd6f9c5ec8c102473044022016b8de72ad65106bf52949d09fad3c61c8d02657bb5cbc33510478861ed179b50220541efa16fb89e35dff3cb4fe1531524f24af71926c47c05ff630f3cdc33dfc70012102c6b802ae2655b29b595f732870010b5ef41e9d17b7d55266663ea0ee6b7e73490247304402204ab6eeee3accc1cdf971f0b97842a082490d57df9626d85de1bfcb99233b798e022001da59296c31d5bc86fc92fad105d73827c19d707d8b4d25779521ab7a4ca57b0121028c81744700b83080588a27afa0299fc579ffb976ba18d22272286e1fc6d3461302483045022100b3ba9f23d2a6e7eebf7359383fef06bb6c5705bfaa24cdfe7c4d4cee6087c1cf022029e28d2c51aec457ceda44fe3fef2202679a9457b6f0b99931616063e76faa8b0121029fbfbeffbbe78216e4706243ca22e51b0fec0f27276315eb2ae787c01553fc8e02483045022100ee9195a10712c2b324861f691b53f51caee993173db7899d84438688553c6044022061b231a4fa2302588f871a16caeb01d8c823e03388b4bd1f4bd7cd3d1a4dbd6601210337c70409f2c81395ffc6166e77bc6f55b94384d2698d51b331959c61212a8b0a0247304402200c205017bac4c7a697e4f2c26e1b2e509d741ff678fee863d22d55a381c7d5a0022049eb959696482e15ef4ea87930ba65827c9e82a5faf5c943b50bee6fbe9b216a01210372516284152ceb93928a317b490992a2852b946ff6ed88188fa02f07eac9da270247304402200d67009ba1086d1fa2c4ca84695f2f147e1bc959311680c8858c06cec34f7ed302202e6d9b77ecc8c4b219a1aee5bb77d254e8224818e0539bb7c025bdda8a0c4522012102a103ed174acf2efd20a439c05758be0aa68342aa84ac137e34755909990efe19024730440220486c257316f9c2ac355a47c5583877614b55c19bdb524202ce5e36f3e9c6df420220054645195ee2826360b827fc149df2ce690a02f6280ed74b6ac057d2b6af4c49012103841fdc55923d84bbc127106d9d1227fa43d8182232b1b052af9748f1f74151800247304402200457aa9d4a2f93947538badba604698bb524f2b9b7d50ccd272148d5f520aba302206821df8877d1c80b399fb7e2d303996808c961eb2201ce483f5f0e80bbf2435e012103c725e032fee4278bfcda8ed4f7c016b53d172105fbb6fddb15006930334612d00247304402200efd62819dc59b033d1a93d57ec891c6e73f74ab16a80a98bbd9fe2c4e3c51cc02203e612db06c70b6768d5b9ef7139ee43a67051b690a4902be6e0bfe47dcfd61e1012102cd61fe8579146ab5886fd65cdc6f0d7af82c60a69c7bdda0dd069840c32e422202473044022011222f0b85a838d2109ad59be0d170be7410e4b0b9f1fc55b4d6c57d90412fd102202fe3550872be7b1eda2acaafada0618b1a57658a1bce850b935c0c6c3784340901210261e5751a99aaa19f55d433b933795498ebd024216e9deadcde5a3b0e21567ea802483045022100e4cf8b8c0cacade19754dfe9b71fe16309e967c3054b3f6aeb2a3d11015fdfb8022048bddc40fb827c5f6eff4495228dcba01bda33481512d25c8e8a441a8cc5f8df0121039973ed8c16473b8365db648ed9483125d7b362b1a0aebdc73c2f50b1ba4f722802473044022002db1fcfa3d3ba7385b454d00b3acabe26c5131ccdcbb7c7fa502f8ba0bdfbb902206ebb9b31530162c73786faeeb1ce62f63f80d6051fd128ef2fc137f8ccf2fda2012102ba74d3bdf4ba42dfd71e58748141329161b5960db1031695c501546f594d7a4502473044022073bd690be32b35a65cfb4e1f75325ba9ccea9a2b51e1dea08f08d1cea9715db702203d9c66c0345bad60615a30bbbe6f4a6df99088bf1abfaa2ded775ce6ff0d4080012102084ac811562f8d1ef3ebe69942d4b181c0564909325283a4625a2e7ecca3079f024730440220199bb1ced776dd8f5f6fb20270ba41bf4f00c281ffc96221a138efa10b9426ca02200fb2cf9d1b1226294a9bac3a66ce451ad017adfbf1cc466f23c47b3d505ae6ad012103563d229d8316ec138126bae20d5e47de8811476d61667b54a6729dcc99b2673a02483045022100cae241de5241b92cacdcf8e41a08df64ec8ca0e2506cb62ad67e50472eebe80f02203b9fc91f71cf1a70126f2f871d6a38d214e71dc67fbca9213c7f45b110641d4a0121029aa4ce980696a57e1682d013870bd0d7c93a5f5c12784205dc1a92ff5ec4e14e024730440220070c1667041ec5191270294572102f9b3d55a00de5c420820701845f8824431302203ce7d52e405209945629e1261f024462ea307d2c43a3c803b100d2eeb25b83140121035f1beedee72f9dc99e5b090bb48e61a80e562100b16d31ef79a7cc9ac49796b302473044022064ade1e9322e9cb30c9438ce47835487f0a2d177875f587a1d040ef33ca39599022046ac705129edba5d69170b23198cb653e3cbd5f633925ef37e9d0330ff4d9b2c01210275badf52793e5dadc7760a267ea8506102f3b2fe4eec683385e9917f950056f70247304402202129f6ca5bf62781bfd91ee0009aff3bb5dd44f645de9351bc487efbd7820f460220698e503d84508e5e6224690b1fa6a830098b6dc395544c83c22ffd2d9f04e069012103087e36546486c87ac8688dea4329e3be45260e7598c71e95b70807515361174802483045022100889b3a5510d759f916ed7b0b8de2a699330485a4bf0f4732340e04d22e248303022048a754cf5299e38a130eea0034bfb305d5fd43b1c3f3665c8b95bffc5bb1f8a6012102bdf183bcde63c8dd5da7d5f59251575e16ed0100cdabdef6ba909050a810ede102473044022057e61f8169de97844a6b614ff72b3cfde0829eeedf327398d4e35b099491929002202556a74d7c9e9ba52885d9208dab96dd4501761daaf2a02107c58aaa35383b87012103d2f80d82d7386c964c79f41afdf574c1a354e821531a31eeba5b7d24dbff6e0702473044022024f5bdc1cb8a0f28cb31a7118399b269e1ae3d3a0ada297a8abc7b49dbeba621022044a92b5e27e6dd05c515965248d1c3e9748febb03816c2c30c873f5bb90a8d5b01210292be5af5ff81b9ef324e985fc1b462fdd379455c16f2266ce980881f4563f6c602483045022100ca9921fdf8d1f8c7774f33f09b265585945f6e677bad28d6a4269838178cdc4002203c8c846d18ca3468dca960462d9211f27a30114806ea0e095affaf6c36158a03012102502c465e4daf7cf57b6f1c422fda20e2fb0e0130c4ea6d280b44ed15c5baf49c0247304402205638a8892e4267366b60635b185bdce9aecbb976c094a0e2a0c85eb5eff8545a02204c862d5b5bcb45f5dd76785e8211556bb7d99e647926f1e2e1f0bc387451aa64012102569f6dc169eb0be825809405a862685a714f8f176c6a345e2a861a5afbc944d202483045022100ecd82847fa98f9806ab3f96c425aef73fbeb836e48dba8638b041546003135ca022045981af499a473623db929da3fe626497ff5a0d1d9ddbce81c2b357dabd45cfd012102c99f9837b73ffc3c836f731a859eaaa02710c8f17bde5ae7eba7278183215b5602473044022032e661e8bab6ad7c370d34d5aab2c8aa13e0615fbf7fc3eb94b1814fcdde8fc6022015a2e2d33f03a920e8b0309aa006bdb472411fd5cff9ee3dd2413fb5c59711ad0121031837bd9e027764f3895d3072e63a0f5b9ee1ab1b4468016336c3a6553821ba680247304402205b46ffd3cc47d135c645dfcf9690d96a7e0eab97c11db912e0194d15e25c659402206a7b5100c3a34fcdeb192d43fed900fb6b57ec7adfd6bb2034a131fcd48ef8a8012102576a907953cb4273afbeb646b796817f4576095cd2afdb2c98c958e1d0a8d9dc024830450221008f3442265107ac941b6973f658e944a9f5c8953416d0621c5de6be3d4f94733b022025168f4154a9e9fff01d269ce887539338a7d644c3fe21de9044f554fa3230e8012103612e2d1fb0d47f37e20c2cf94758e56c2e2906ebe1aa6cfdfc4ad0b8e1e414b002473044022100a554a6cc8daa97fb5f9d1847ab1f89519fd8e93de42ff04f111e1433303d4ee7021f24277dca5ae1636299c3f72620b667e94a7ffbefd5803c5fed60706f1c02f801210327d818f8b41f72378c1b0d7a66e0ac2c4b71926414f97cbadefb279f68d40bdd02473044022054710c5705c4c3e1f6377e2afa0862c12ab4d849bbf230c30f785ba0625e3fbf02205e6f51dc752821b0e0b574834df9f227018a4fa84f0f6051fb86f1f30bcfde6701210298c65155d0a11462961d4772c77f46413bf1b8abdcfa38364d6df03074d7061002483045022100929734557ce6040e216b84e160a5fd9082841e87e5cb04ef98388511be7160610220183210244bfe59ace43386370a6eaec75e9c175f3aa9730426ca620897ccedeb0121023a82af5f9f2086511549b68f3a086468c59c4b9523ceb911d13d647dcf0a711a02483045022100901bebbe9a762a861ffb98182916ad03a09dc7ece3d7a22f78a39363e554e8d002205ea956e24ef0f50a6ed97bee744433186d4db7431ece807eb4f25538605bc94c012103d1f3c8ceb8faaeeaa72b4f80bc00cec024f84b6142acc5f26a391e279e47054b02483045022100b1a1ba42f29c1f7cd5003030cad4abb5a1b4230cd36255160354f197a6cbccd3022051a2a5e0d0242185e10b1ad8832ef606ca8ba82142ca3db852ad3423c941ef82012102ef0c7ca2f809cbd0789742af4a5ff64b7c224d6d1cf98304a796243477f0ad730247304402206cfd94652530d201742d0afc9a209922a707ef1657824e5e37e5bc6d06d2069102204858ed1c8e951ceb8a6bd05c2649e71406b4cfa062270011a703180ef48c24bb012103ea2c67188488ffb5539c7739ad741e1e06993434ca6ca2c0fedb338b616d893302473044022051788c730b6a3813d9e9aa7ec6d9eb00e242e152f73fe751ed08fad55b4fa0630220054924322f027325b0a0343cf76cd3eb368c9c9e0f002d48e2271cacf5e3985c0121030bb12820b92043d6dba28201e1716832209be90e7a3f734f97597671fcd9abf602483045022100ea66aad186ea5aa9d544f1ec035ecb3dffc730a7e37a9914a36b2a15a1f528d202207df78cc86478e2bff81b4108de186c4d6e909d6c4f90c08f06a462fa8951ca57012103c6bcbd9beadd3097818b5cda3b0de22a977e52ef4311da75473212c3e8f285bd0247304402201d327b2b41956a99555eb403546f56b2a93f8dba82d8743397a0f3611727a30502204a0667875f78e0c8fc6f634cf08dcf4c6ede87cc5c145039f2d58c91f3a8f789012103350c3f29355038a27bbf3b4c341c95f897f5101fc92b5de809316dc385d5a6d902483045022100d79aff561fc547426cb3f2f53c05fe5c777933a5bc09f1f59f26f07bb1aee0e402200677285c537d5cae799d42ae327980b1685687c7c9131f3b42a864361b07eba4012103ae1eede14077bee7d15681d2067957b6788b545d3150b794f64163167f9a0aab02483045022100e9021252e013e1bc35702141ed8d5801c86ec478e44c63a138c1f28d9e3c449702202b5a54fb9bd951572e6b13f596a7360a163bee54a50ed7845c5115d084a2d5fb012102f2a5569b2dcaefbe315aecc369ef91fc015d9de39087110c1842a452270cce2502473044022058ddf44b46986f00285557480be978ebd7f19167b6d4a933693a801c866e75860220074cf2c71a6f7a9b81885a8de87a954410639eaeab73f0c7a516f87188c9bf3e012102bae5f4360c9b54ca39637bf3647fd2d27f4eed62434b54a35b4b187b976481bd02483045022100c76364f1d4e031982b54c5f58f21a48701a1ba8a7f73739b96b4fd8a4e5e8e3e0220374d241f426d230b6aad969c8ec498f9615667bc08f9bdce6f583b30738f0e45012102d487b38e6c89d2c676fe4d58cd7aa87af96bdcd56f70f725870edc0735421b6502483045022100f380ea9506607973c22c5a7661a3dbaf7241b23ae8a2ab8952d7b4c9da27be4d02201600ed41da446239a5589b7f408af0c4d6e84450482e430f5bc9e64ee35c4063012102c084123cd7edadcbe9b46383324d9ac7b859ba5dd83a83a987fa953c30f3c54802473044022038b0acbc41bd607dab810703156abfb68be6f7ab1ac9a8e9e6fb31f9258630ac022001a147063a006e15ef18ab4c489c18165d666bf0f53e473b07656533cbe613df01210205f18b211c6ae487a77149aa5fc9c8cdc7e46277ebe35d42fc0075f1df02b6180248304502210090fc509374b90c92d6e3f69d0b0c8e9f1b69e31308687fed99ef900ffbac2a3102207afaa4a83f25678ec22821cb23364f8aa0448b1d9ad58e6ff74278aef32adfce012103f49090cbf658376fc23aef943a1673fcc20b2f43debe87e077341d438f02a21f0247304402200bf353072de9d62ecbeb26cbf6719d481b92da763e18ddf38587ba2eb5e7b73102204d35e3887e990824d06f0800c596a000aeeeb0b8d10fc589ad1ce109184002ac012102f6e517ec416289de83f19de790666971566d0a85ceda305d2d7d49317c91298f0247304402205272489a79fa0886fbecfd0502165cd8baad5840f02e654c792af4517bc465f40220486d8e75fe7f709d642fca86957b2be02cac41215015dbc4d370d5c39a123b5c012102746b9fe695b0112cecd6fd8c0aedd4e7bc958a4410a6fca3b41a189f4ce389840247304402204053a17f2626a48bb1c04090ae807878ccae30e5fa457ecd199bfdbee0ef9b4d02204069cb619cefd99a59ac3b7473c8a8b31e12a7299f3565c8fa01082e9c969a290121029e88d4e04d9a1196804c732cf53e1e2f8c97968a4ce1fbdbbd67a724365fa92b0247304402207d03e263351cc42d75823cb6db0d8241d31740cf45043a4563dba02393f2650402200934b5675a7e9857b17792456b654c54928cc01a42212557475c9abf4e2a07f801210236f32a5c6fe05bf86f009ef5f7e52411bdfd678c6b7459a37b3e107fb096286e02473044022064c69bb855d9aa62744ab0f83570dff3544d0fee0f2c1a8c79c0e5829326ec0a0220706d778ba06f5a999296f594f81923c5c7c1cd4bee778e49757790d258fee179012103af2887aec7d097f54f0f25b522a39a4650186888ce7af752302ad38aab2f5cdc0247304402200546ba5cdfe018da09abd386b106ae45e9c6da258ea25025efd35a832c353a47022032d62308c00745ff634c6a26d8c7bb4fd0bf4c77b265ae7c636684e381c4ddb8012103ad78d1527d1ac50052b38cf87cc0c8b21f18157f7bf3df26f818822915d81d6e0247304402200a2e97b35496bedc0c33ede16aa79cf69b0d957e637712ee0e6b71c24b99582102205449224ff532280a28516491e259d219dd4dddbe2fedf2ffbfcc74462c280b3d0121036242c693811d0322af9ae9a702a464df00636c9c247b12aa6429edb2a6add40c024730440220036427ceb063da7af99f99ba096bec89076587e0748b917833432f05eefc491802203b1a9c2316c390a0240c513f421a27df0e0698b10fc2fb20e72080a71939faa9012102607ce01956997ad04e78bc1dcb0dede2a3e16d3c9d1d5a65525a27c6e56b8c6702483045022100e02f52db5f6f7e19371f8d573773750af3a0f6583d07ae9878df0fc78cd23bc90220074abf0ec40e8156ec7ee22ec6871b70b2674c6be1ab4a36091ef2fcb23e1df60121021ef81716b7c521bc725e30c082024f5eee98d530a42cc8b856ef360ff77cceb602473044022008f2ca5df5947cac8f8c71e2d8ee988fb4ff70909009a8e010d938353caedb4702204011337adc03ef84b1d40fe74b227d2811ee8218543d295a43e214500b0d0991012103e4c62f5dbec739dc8f4f1ba2f8fba3e4c379fd2edf981068ed738ddad2a7b63c024730440220726d1059ab00960b22625ba225ec84e2c72122c0c75d145a15208fb2627367480220536a971f786a6c9d135ce8221b237c93593d5c9e25c5a5eb83f301c68e8a5368012102345fd6313df3ff61abb4a4dfa5b29a3aced1c59218099322d5ca1a4e480b97d602473044022054663988d1e4268b75e95678b490167e0b56f090d5da166bffc0184fb0b0a7c402200ad26ac931ff4a48e32eedcfa6fc01b552d1f4e65f0ebcb40e49d519c1a802550121033078832b98466d296842675a092dd5a844c35b1042f6f6e8641f691caef6386702483045022100cb00c8fab71bdb7948a36c911ff22de21946bf9072b370441d482662b6c6fc0802206d9c92f3f3ed0dde2d75121c74d57126e4a876e4eb6b51fd39f71feecda9f5b80121036ecf41e461d399b61f950d7ce6c0c4472d876fef3e338524ab3158fe155f1bea02483045022100f200319f4fd1dab6fadf52eb9214bfc33681451df3ed4d73f7c85f74992ac7a0022012238b4b0251c81ad68ab264b246869e461c8b25bb2c87caebb3aa154ef577d9012102ea01decc330841ce783c52aa689526866e26a550b45e0adef16fb91af00ba55b02483045022100b7862322a6929ab542a21de55b6258cd988dcae6bc2a81950c92212e95e2da1702200194bd2f73c02c50069890dcc893dcbae124208d887e9a681ff64be072a9d85a012102e0bc445d85752b875bda77ff689d9cf1a0bff22b12fc9b9ec51a579f6a47b46c024730440220699412c3aefd9c72a0d714a4ce7588220a588d101d2030e7f43482ef00842fc502207693962a884739211668e9ac33219af886ca6ece96d4219d647291dca15899c00121039b9a47ce2500d0a9d80e813f8f9d1d5db0bee1409ff2e30fba1eee9309c0b7cc024730440220277787de7653f16356c8d9a518e612d14af43e40479d8cac4be8e9500a0c8a4a02200f14a8923a3e676cd73c230a1421aa959ed08f93ae3c2efb569fd7b69b0fd39301210253233efbd49d8c3cc0eeb7d9324d15de3b27d1c72b7242dee00a2af28f0402fa0247304402207cd572f4b0ebe4b72ab2e1327db56b911c5267abecc0951ccc1c9a4354b010c30220650a0233dc87901ffdb3ccff40adc3da12522f9eddef33d64230902a1ec205f1012102ab099b991b3932b2026642775f1dcbc1a8540528f2c67e10d1c8d0bcaaab013002483045022100a7e34c84d9266b9f323ff6ed81c05ac7d80e7f47f488f4bdfbd36120399adc1502206076bec9fd0a47d7d22b21f3a41ead2cf0175d35460b447cf16f8c0be23d6f4d0121021d54e1ab39ea2963b95ddf53ea16a9be892692287d0b9515f3f5b1fe4117257c0247304402204d3e9facdbf9107bad3e382d33dd5b3fe888c7e7945c24a6f68ba62922911de302206aeb8c693ff8e70a507a107073850ddacafeac2ede997f03a25c4c68cfa4eb1c012102cddd7587c323a979a8516fc245f94f3f00ec63888c3a893e48aaa6958aaab6da02483045022100809a65b908f767621f104a7e4cc81bbd2da432c816e2afc4440bd62216cc97ad02207f4fac54c5bfd0a4bc0bf168d39c56ee824a086b9cbb837868e06f66f972346e0121032468940d6100ef2a9f685e180634c476937cdec1c9c36eb9da366095fe2336d30247304402204ebd4331734c2533d7adae2b69ee76ff4bc1ddc6c9ceb51bc9d3412b9738bcdc02200e7e0f569fba6fe35843eb5acfa0ed91b0b697099a8803e4d78663393a5c2177012103a76df1fc9c88bb1f187d515a3a069efe8443f15677479fe0cd2b40fb8fe16fc602473044022020b9f07f8f3da078fc3e7dc30101fe5534034c7c68cb20ca762806dd9aac009402201c78fd2d2cda64effc1ad28d5f0ac73f5934e3e50d91133ed535263c954a7fd50121025dbc5252304aa6c6fb39d5ad1abd3a1053ff9164c96a1bbbc45416b1db7f86aa02473044022034d55b776082d01f8712537e331ce17bdef66a1819ff28679a6e7236089994610220785b07347d1806bbd5aa42a6f7040e392cbf968109e58ccb8c11d899ec68960f012103509416c83437dd681ec998913231dba828439de8addb4209638293837f29230b0247304402205c8344c0428464a8b08c8d5b880c38c7ce9144682b68ab2880a3501f0458e85a02200d58f002fe2dce5b7a5f3cd38e0a503b6fc1a4fb25669734edfcf90adf4489ad0121036946ab831d45359388fbe156cb1580bce4c857ebc1174094abf76d93017d866b02483045022100e372c19e30b8892fffd9046564752c4077565c42c213b168e2ff54553dde9c500220130b63f097aafe9af62e5df52107874e008147db6ef0d1a85f0599f3ff8ce9fa0121020d17d4a9621273d2e7019d070a23878ea6bba62ea5ffaec56519cfdcb33d0cbe02483045022100a2d28693078225669496e1fcccac2a74717ddf15b8c78cebe2df0c6e33753f9802200e7ac783086200020e850f318991a873b9ea8dd4774e75204d85d9434b0627a30121021da147d3836c2a0a8d043383c97e39ada42eb9eb68b89a92a76040e6cf5e074e02483045022100fe5a906ad5dc5e606d24ce13e772a6ee67d93802fd8873a571e09025317e7f4d0220555eda7146b96cad862312539afdf69ebdf2fd8cc6687adb4db2270eef47a3950121023f4acff0b31f68be9ac7958026a9488d9763d8d3b51761367014846fbf8d44bc0247304402205568b46b254485e0a4191edac9c571b65da6e5a6e267870372dc299bfd0bfeea022037cb7dcf9345761e704b7a71222e722c286695e5e7cc6698e665222dfa30761e0121026b7e8db02de8bc41e26cf7db502bf488f68ca36955c94937c68ce84d3421f77a02473044022033d365e61bed6b22eedd3cf0788a7fde4be69b7dd4e82cf15c3a725ec475006a02206e34b67113769991893d18dd5e309aa80fc950066b6cd1a6ef814f16ac29acc6012103cd0320e6a0c40a0225a7407aadfa3cc97cdc92a74140a3e8e79d657fe4834a5a0247304402204e122ff0505a7730ac20ac75750b92f6795d8a9f5381c7b49cf3a5fa5caffe80022010755719a1bc9650c0e21034806c640aa3e752cf51ee257ddcf9c3d61ca0aa7e01210287604532a855517f4e5d847d73c0d56b6855de9646866934993e7bf29c2a4b2702483045022100920c0a4fbe3c35c8a0918b38b99a4c026b1fe2caed60d4dfb9ef05c6207716840220770de6ce1f950fed729ad927d49ca3def7db777fd9e1f76e7af4f38e2dcf638b012102e7be3162cdb5a06d80f053e04c1911857bbaa3434b5d779655d9c1f59c83cbda0247304402203c3ec97c10776f6b446a92a29a06db5920e4fbd00c5a9bed1158954dbdb0462202205c81bac02be56def57bb961d29d3a3675b265b7bd4e916df9b65788d8cd5c799012103d7f3937d473912d871e114c2b1edea130b266e400b60abfbedc452f3907aa4af02483045022100cc2ac9bffd947a80c29852db3ff96189b7e27e9e3f468be28062bbe051a56f480220340ad59b5448103221fd828db99cfbb95f4ab01ab2ed0757dcdc9042f218bd1001210335b15649a9724f688780d96d07d423d3db82af9930257c9148a4346e53ddcdd702473044022079633c368de61803c61ea8b5cebd5e7fb733a9ff723885ac8598769ff11ed2e102202ce95d374db197b2f0faac5caaf9913d6c736416404d10f17f8b8cabcb8b16e1012103bbd0f99eb54b0c31fcc3049ae3e285a93bb7dd9114699bdd432deb3cfd4d4e35024830450221009389640b406320e4934b4a315d9d7d592e1d71bbc84de86c256969ccf4d23de70220777ac1e873bbe7c80a251385d4a1196bf5742fbd42be40bded802e379a08c9a20121037275c5f8581d6b13c2d6aae85d04cbb852ae37319d6182811384203b5da326a7024830450221008ca270b36dbc5831c5880222478ca969a526e567dbba5d46dbfeef61631a537f0220666d8f17f3370b2f7668e34523d21327fdf87e1da88a2b237a01127543727290012103dddcb85320a559b844f3ee118b2c6afc27bea2f9a3efe841e22af69de788164d024730440220567cad052a1279f50ad9d716bff6a86ed898345193be50e3c8240914fbb89b43022020fafdf44134eb83f760e3664b638515c094b499bade8777364fb4c7f8b96310012102bc5c3ccc349a6fff2d93f23bf4fc1c7dc5a4204af25db19e1e84627d1b7d306502483045022100b7cdbd1b5f7697fd603374bb76e45a7910fe535ae7de597a4d1ef0f5fccc539b02207b112ef060619a72210d357f82e2577470449fd9a14289692d9abdb86fa649ee0121025479d2d2dd0494cd208cf428705d01ebbbd2a1b7ab466f61a871dad0ecf44a9102473044022068d5a7f099802fd8836b8f6eb8caa5f67274bc77a419fcf1c93df39b8c521200022041d5cc5e570f507560237b2b2bafd39b39258a9c6beb053f00c5e46212c952b0012102c46fef722f6a91b855ec492ac5ea236e6ae19c7fd063bfc3a7b564b34fc0569502473044022041dbc2166dfcbfb776950e2e5dd62d3d79ce88e0d950dd57b9c1c2e396cdda5d02200140367dad0b5b16b832b797380a03ab6cd8e1e50c76abef421296f96217a2a401210322b89531a89dabb8fd7a58ddc756ae280d6f5f34cc39aff544c832f38fd0230f024630430220719a8e183599dd9d67cbea5e233a334b8e204d6ebfb8ef527c5395ecd5de406b021f797f52cc167f4b1c0bb68da79bc6356f4bf0b23a65dbdb223e1fc44b3da8fd0121034ceb45d36c46d20f3cb094ebb152e74bd34376744b9579be71970d93ec73a84402483045022100ab12edcba4d7e3f9d1a05a5968b78d7bcc977c1b8e5b971c0834dc43c5b52fe402203f7a2cbd2f2c6f607f7bfb8ee069edce974824ba63f85be5626b972aa88caf5a012102be16c5f02b2ad9666524afe75008496567c7b32f5e7fda6d935bb5cf1bf6969e02483045022100901f21573f2b43861bd5c052f05eea416eefbe06b513d2482e7abf767e2c96c402203092fd94396127aff55d0d36ea0d52abd5acfca922ccd5c18ea0d669bca057070121021f5c2e322c5cd812bf53aa768b2d10c2f805ebe8c5e8ec844bde5229ce267fc80247304402202ed9261934d488439b8a237b8e9a6fea5273338fd347036a63fafd59b0ad5e7d0220771364eb54e52197c9b7c98ee33b4e76f9ee95c3ff719f5f28d45b9785ecd2b20121030085c1f67ec8c77fbc59e14dcbe96e1f326e437cf594cee45e2802fca8db13e5dd8d1800\",\n        \"133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62\": \"0200000000010149559bf09c3852fb1d592d23093a8880be3c9ea9f2156dd72caa46065f0893330300000000fdffffff037891d20000000000160014cbdcec59a36d7704220d9ea57e8f73a34a320b78807c4301000000001600142a4bdab5f3f1948a9db9d18446f4035e92fb32a640787d010000000016001455c75624f2b23bad6cea420a2d99d01365c752b402483045022100e8a0c1a5294abd5b6ce8d0708f1130f65978182ca0e812fc8b86f7fb9d2c816c0220496c63ddd1ffa0d706f2a862c89c76a815eafe69e1c20fe9c115001167f09b8b01210232600ebee335ee2d9daa5fc9d29cb87f08d373b60c2afe4a746bce1f4dc984c16d851800\",\n        \"18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf\": \"0200000000010150b3256328301865fcb86f579d4cc05279270b1ac54f77e29d5831b0cc2352310100000000feffffff01b65b1e0000000000160014dbc4abdfda4cd130c45f8911f67f7a9eca700ab90347304402205a1a37dc879e813c57231a090a3357388f74d76d8a9f56e397cfb2f484a0568702201b47a2f627bc869aeb97e0ea5cbaf099fd3b7448a6ac04a9bed0795aee9b529501201239a020c26f312b2f98ef962f57f2174d59d51877626c226ba6ce16d034cc046a8201208763a914f8e749b7c5ad3d1c21e6cee72e5e8fa746408f77882103a19107c132cd0dd808e6a475fa7198097c0d4276d69769e2839596ee72e8cee6677503f30c1bb1752102f007cc8ee8b72377a7b12665cd365034ec214b484e90346ed677907729d020ef68ac670c1b00\",\n        \"19a666acc1bb5656e49a1d99087c5a0c2a135c40f1a58e5306f2d85f46786801\": \"0200000000010152de38a6a717249c1d310245e707bb8c075fdeba85921d98699c88a316f9390b0000000000feffffff01f282010000000000160014812fd7350514958c87af121e5d0e7d54cc8a8a4f0347304402203f975ff11898eed3d6084b66e9b7f139988186f9cb62b8852e3c2728d65fd5c502203de7c73c40250805d45be0294e0092ec18cc12e09da22c845c6bbab4d0ac4fe8012047174e7713a89f09472a106b5af1e06ccd4f5d8b0cc2b66358fc3035153c78a76a8201208763a9145f4851882715e86b9501742bc93852a309777b238821020b7152458b69109bde6e9b72b5e1aa653a10dcdb1bf03ba20c043851af76577e677503e00b1bb1752103cbed54ce4d89084052407f4f50004ab6073b4ffd6a0e72a185eaa034541ed1db68ac00000000\",\n        \"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04\": \"020000000001013e54401c5fd14de41059da0ad630abd985e7be8fdc27c0d448b1cb4860e7689e0000000000fdffffff0240420f0000000000160014c51f076c0643a392c8e57ace3095f12f8688c9ed80bd210000000000160014ae73903064f86d0b99dd8bae9176256088c8930f02483045022100ac4f389e545f6c77157cb5adb8e97d2f60c7c369367c7c6391800d9d668a75bb022026c2aea0e28c99f276a0b92e11be02e6979484973ba0e3d89a9b150c3d6e45bb012102aa77796e54c0f5ca83028dab8d24960b489bbcc8fd85f75ba79c0fe1ec1d74e7cc011900\",\n        \"22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f\": \"02000000000101383d0a7fc6353806e9800ec0b713413ecf3c347068c93459c263c5631c7a27620000000000fdffffff013286010000000000160014e063bee32b0c198c3130ce3a89ddb0325e553fd702473044022030fcf2a1efad64cf01dbecb9903798c2d3379d613be4ae96f656a46147b87bb602204df7be8916fa840d9525acfc60d08c69303ed534fbdf95b456bd446d45db2d63012103b13579b2ea3924a211616fcc7ce452ef873a471c52fb55b405b9e7ee6b2fd01520d31900\",\n        \"26b1fb057113f6ce39a20f5baa493015b152cc1c0af312b3ee8950e9a4bbf47a\": \"0200000000010139e0dcbca436ddb26fe5502041b65971c6ce2f22bef6be11850d0be74f96b13d0100000000fdffffff02a0860100000000001600142c9782073483b751e482ac2cb6034f2c2ee39d2e08897800000000001600149e8d4f2a05da2dae2160d663b1a2b302c8831b8e0247304402205f456814f66be3f60f32bd91ab08bfd8fbf0487d68a40b558a438ffcfd89de8602202bbc9456605497fb7e0ca1eb64a716e06381864994eedf1a5fe8566af905d27201210208f544d0d78ee385be2b724a8ed4d9f74b830ceedf501569c5a926ac094d882214121b00\",\n        \"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d\": \"02000000000102f733b90182901888d7d39b03b4f9031b57005a8b1b53a6c07d52945c110cbfb70100000000fdffffff02782afe5f2893a26f3f63b6b175990a5b53801d37f90e13834511e3456696e80100000000fdffffff022491040000000000160014af4630039750572676d4ca29f6fb635967807bfd20a1070000000000160014a340a1cdd08a0cf976a8dbc6138d1ef765621b740247304402200d854abdb8c02b2674e107a6b7e2423a67bf95c28eded829606445637ba126c202207ac0df686d158ab6592f5dc052bd45e913a632da64470762f691854fe81bb3bd012103167f921ed4937224b895a2ac440dee060a06b42e39d1b3e7666035f7a358d9f40247304402207449cc026cd0358ddf30a58fecd5f4c7b69dd90e08dc2a0cde73770c15261a8102207984312685f3a6373d2f3930162a71376cc9c4f8197a3e47eb76034b7e9e8fe8012102fc2767de8ba05c5eb6dc6939d931896eb8247822f08172f76d5e934462756c808e6a1900\",\n        \"3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549\": \"020000000120dede6d2a24385961cc01676c53dbcd389fa87aaf08be4c56ae3a463dcd91ef010000006b4830450221008d31c621268af558f512fc2f66f08970cba3c566b0bb3aadacb808215bfa9f1402207d259a8967224542c1dc1ac5d02d203f07466475e7e8c8028bc5751985b15a39012102440d6e2887ecf49cb991d9f4a271ebb71bca3f21ad4a4c3c599dbf0945962a96fdffffff04a0860100000000001600144ac213e7cb8eedec54f0757687026aa92fc17e8b400d0300000000001976a914426cfb6ee69e5f2f42fa9095ea296bf28494d1f088ac4c436600000000001976a914bcb2f82a41a751bd4230ab975bb93102321c0d2f88ac0087930300000000160014d02cbb783c1b8602561df3525e762580dedda016c6841800\",\n        \"366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f\": \"02000000000101359121955b09561ae4e67782e6daf8c464bee327671faa38f942d6faf54d16e60000000000fdffffff02c8cf0300000000002200208ec9e8e9178d72d3e50b4a94b89f4f3a1e31cc779bb824243c3cc0a2237e518e90d00300000000001600146e6d9fb0cae5e2d4b8851991b872ad1f5e9fdac9040047304402205018996c947617eb1de1d24bca9440614f088c268da15f1b2f69a86200ed3e8c022023d7eae5b65fb37b3014f9aaecc666006bb436bfbeb9d1d6c8df4b131737d0b001473044022019a5e7cadb28f61ede4d026b4b5cf74ff0605550a45a505e3c9742a5f995d2ce02202519d966e88aa273fe7f9a84f231905cefd0d50e659d3bd3990b4922f06b1de70169522102981da3a3bb6082918cebababf2fbb628131381f42e92068dc1683ffa3d34c3c02103542fd6968a94190934a6774d254f9746915b94d96ecc79233b1d858e458748ce210379f32d6f67f41ff1bbf60964fb2d36053e6f2c4bc2237f6824e935778594417d53ae90a71a00\",\n        \"38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3\": \"02000000000101afacb96c27ac15bb895caaca37799c8b0d2590829951ddcb8ca3a2e14ca66eff0100000000fdffffff01a971ff0000000000160014a82e8b28759a033b524f181474ab04bfd1c964e60247304402206aa506d16285f4c4c6a26fc2dcffd5cfc0c180b6a9de79e47a05e5b1804b54ed022039c699e64c6b5da70c99895e79d4767f2bea343f68c1573a0c281078c9eaccba0121021a38690801dd07e240835beb9a158632f0b8910500f6eb4a47811454573ca1fe00000000\",\n        \"40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1\": \"020000000001018ed0132bb5f35d097572081524cd5e847c895e765b93d5af46b8a8bef621244a0000000000fdffffff0196a0070000000000160014423a76ff423eb1d9350b1ef6d730054ab45a6759040047304402200f568dfd6ca12c4a6e9bf04da1758a914356abfda487c6cdd952ad618feffe9d02205a8844aab29ed07bdbfc6fac91033aa48621d2ee82777cec158f357ca77010c90147304402202184d71c11ecdd543b4b8f981684704aaae9482658dd3fd92a1ea13959612df902201eb66dafeb59ee0a45c00fd4aa5e56d12542e11ee4e898bbd78f2747e148ef4601475221020402f62988efce421d9e971a398ce3894dd22308c760473ec0d0e8f681b9986b2102111bc325b384aef5a8bef0e8ca62cda01557523e69af3c4af781b3b5ae07a2cd52ae956a1900\",\n        \"4133b7570dd1424ac0f05fba67d7af95eae19de82ff367007ac33742f13d1d8e\": \"020000000001013d43805662199ffb80908707c514a454b42e64f493293778863fee8c1459720b0000000000feffffff0132f0ff0000000000220020a321733bf4b16e7945a15d1d943aaad6f7be574ec677e39bdc2d74a76318ea2a0247304402203deb1aafbcf32d2f88e0a9badfea82ae45366e105acb8d15b2dff333935f2fc802200275d262de1a1a3480df91d1150a3638944908ea15cef36af431f1c90ab31d9e012103cedc8cbaaa215317369d477073a56395645a75eee4ec9c05ac65402d78fb7e1d396c1900\",\n        \"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab\": \"02000000000102540b948e0398359964d663e1141b9d4d0ff11d0acf41c10ebe5921d1b7fc0d600100000000feffffff1bfe68f11fce4d584911c02191fd17784c9b284b76ca26aad6fa9f497cbcde6b0000000000feffffff02a83e0a00000000001600142a826cc7b1111ad48a96f5710e816a60b1487e3e8096980000000000220020b4939bd05d78f36b9e9f6bf03c30c4e67581cf7e0ec4268bea9f744bc3ebc7c502473044022029c18f6d794db6ad46233415ed487887fb44f85006b8bdcd87dcec737dca8e760220350410e5673f49ed54cd73b47df334ef97af263be61c45c3bfbd0f3a13e853f10121027b395c8a3a2a6af10e0a9801ee565504bcb1ebe2fe4018a2aa197bec04ba7b8d024730440220599d00baa756438f24cb27619e470732ac470cbf2dfe6e5642dd6dbdb561122d022046d3a8e9a0162919afdfb226525563ddc7dadaee47f3af77053f8c5461a5df2f012103e1d3f1ed07e6fa904398e66ccfb9412ccba22b48310f1a73ffe725488b37fbdfda0a1b00\",\n        \"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15\": \"02000000000101e98933d883b3a54aab90f73b3ccd1674508da33129f20b509339d152d2fd9ea00100000000feffffff028096980000000000220020c4da95e05b7a693f6aa2d0d292d55e423d50a540062a4761aa7c92f4f664d7e764fd6402000000001600142fac9d67c04f1b7508847b3618e32c5ba325e41d0247304402201bf43b3220ab0c87399ea634dcbad9e490ec42c51925872ee4aed891e0fd6a6e0220541e5848e2a3eb155013bd08a9b80e2fed893850b1f841fdfd6b432272ba1c14012103f10b7f777cecf6c778206f1d111b7197a37d573646abaeeed27ea77629cf43ea5ea51a00\",\n        \"43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35\": \"020000000149559bf09c3852fb1d592d23093a8880be3c9ea9f2156dd72caa46065f089333010000006a473044022075917a07b65b7c6e28e6a15bb3891b98e1fad1ffdc6f2079af4bc16a6d8bd306022058f14e7059b1d9769d142ac0291ba7aaa926bf0cc55af253976c55bf6032361c0121027987fe586ee2f7f8896ef6564d6db49d5536bcaea939d5a0b755bc943859edc3fdffffff0274850100000000001976a91469a44dc6dedf4cb2c89a5522ede64b693b0d19e588aca086010000000000160014ed50e5e4b61bd709b7f4068b6d02245e69cbfa1915891800\",\n        \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3\": \"020000000001023f6e08e1ae725b5074b78e797cc5a0b5e3e5b2076299accd633ecbea2f706f360100000000fdffffff4a1484d03c738eafa35ab4aa92ebaa000b55471f380cdbf40297fcfdf35add6f0100000000fdffffff02400d030000000000160014a97a9bdef041d20b06496a33f9b0808a67d47c4eb4920400000000001600148bca6c534ba0caa3b1e588791bd4c52a53da1b6e0247304402202c158809e554ba3c91a4cb6ae322d5d7bddc512dfce0a35756ea66f33352939f02204ec4cce554bd9c8a6ba041f0a97ba95c09ad4cb6c6abc51a1f210cb572f865600121033817f61417c86451ff44ca34187f7ea0d07f4e04b090649cc11f6caf8bdc80ae0247304402205ca1d185512100ca1710d936c6f76bf34e88f42d4026092a15f0e9c22218c86a0220520e5488687d77412b455e2eb5fe04ad252a5c4b2273cb3cffde817504d794280121033817f61417c86451ff44ca34187f7ea0d07f4e04b090649cc11f6caf8bdc80ae6eaa1a00\",\n        \"48a4cd850234b2316395959c343a3cbfd57e706e256399ac39927330f03268d6\": \"020000000001012e6c3b9be30d0974b63bdb87b39b94ebca7568e0c3ce17b75552eed484a95d4c0000000000fdffffff0288130000000000001600143d3554dc9cdeb53544ae5bd013edf6bfc08723b8f86f01000000000016001422724d9409827b7b272a393e38f1686e953d0d620247304402206012d0f629daa8cb0fcd499d0c868d9be6910472b4ce39f580bb459706152dd802200db21446afb153e4895402171db935aa215c9d924b6b729428ce1232079cdc62012102bc73056f47c2519e6a613c901af81d402a63edd4fc21c3515b3ee50271d4d2f807ab1a00\",\n        \"48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a\": \"020000000001018297092e2ed55a0000f3dba9444324c5cf0b8da5525f9cb24034daf23851b74c0100000000fdffffff013286010000000000160014f3c4a55d9fbde53d14c162a4eb58c11a5e160aea0247304402202d07ce05cbbefca036db44de0960bf59f10b6c9ed8662aceb063d90ef919503a02205a209883954fc55dcd02c7dca46d86c94d64ebaa22506096b7b7f6699234bec501210266ab2af4f31a58980235db11a40b1a8067979d7a96a0a2ce1aa4bd5e04d0ef7cf66d1900\",\n        \"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e\": \"02000000000101040b9b5f22e27b170c53b346669666275a447ad63aeb6af0630e81fa184d8d1e0100000000fdffffff0220a1070000000000220020ae6dcbda47596fa706490ea05121211e89fb4b9a03a5f699ed22cdde3ea8632b981b1a0000000000160014786e2613cf8948471629ed179886b38152bdb3c60247304402202f6c4f50493168e1534211eda87a0b3b262894c1261c4af1ef09c0699d3deee502204b0a0e770945d16f7581546b8ec635d95e1e1f32f1bd477179122f27f0ac2abc01210327a2c042e02ea2460408e02ceca407b07f92536e9743017129629fac8ab441518e6a1900\",\n        \"4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e\": \"02000000000101e9301cdcd380e585ead499efdd8b0baa464c088d723d9eac0f1869f3279976110100000000fdffffff0248840100000000001600149e2926cb12c16b71c46de2683ad9da137f6399d3a086010000000000160014bda0bbeea75d72018b818a042f8269ac6223f41402483045022100aab3034f55cd37ce2c2d4e88aeba123ac1235ed78bdc0536e289022022e26c7e0220126f4f43f21138324dc3466579624ac81da55dceb20150c286f2f975d7954cb701210371975765db5b2a45bb4bae75da6f3132001373c34bf3dff4ea34394ec856560baaaa1a00\",\n        \"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755\": \"02000000000102529705cd939fc7ab0d3632530ae7a704a892be6889a5e6f9250eedea6e54334a0000000000fdffffff529705cd939fc7ab0d3632530ae7a704a892be6889a5e6f9250eedea6e54334a0300000000fdffffff016623a000000000001600140685c025d82f9b1d8095d359dab267933f6c5b5102483045022100d59c596c8936db15109f881c61c4fd1b1a2f87e40ca8eec5b0222b970a90703602202a32c287cb8dbe8febc7a48a824a50a8859866ac8951f601301fa10ca33d8fc60121036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f02473044022008a4a1e4ce6be8edd5c42a2bb10704e2ea516256543d4a578457ca68d1f2ce21022043b3de363e10ac2cac4e956d58e4eeb7de8442aa9a590a7869308d0c603f637001210200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc72851800\",\n        \"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc\": \"02000000000101f428f94ec07b2db855bf3c4fa1f2cf52cafda8549c7276a30eafd34620bb99750000000000fdffffff01e884010000000000160014c40f099e35be3549b1ff204c9a8857390617b00e02473044022054b9f765cfc34f3466a8140e58fcd0728592dea248f8780fae8894be4d3c92410220022c0934c2ef669561abea5d7725717b79985e0cb7dfcb4285d8f2350e981f790121020882352af1f0e881af998db72440362227b8a704c313a346a9f31ed69183f61f86451a00\",\n        \"56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b\": \"020000000001017a5b8363fcf2d9ac45bccde684036064a57745032413979f5717f8c57ada18ca0100000000fdffffff01faf8ff0000000000160014a951edb4ea492241dc29d0ebb6cc975c6fc5a8570247304402200517f5a2d82af25ececaddc99aa60e2add01c9e75eafccf7edcea2c2dac84e3502203f96021cb8863c9eb54822819d7c91f2b951e3ee7d81c1519c32f661371c81cc0121034a0d29b76f5e49294f06383756e43fc7ab878c8c44be405a8ef4a35b7e99eaba00000000\",\n        \"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b\": \"020000000001012c5783edec9a110c012a72ed737acde1a97ca9b4cc618384416f61adde5732dc0100000000fdffffff0220a107000000000017a9148d2301dae8ecde65a8e40b5c032f08733323f05187ec2c2c01000000001600147fb29ef38d699a9d5c29c640c0e1d94d1c141e5d0247304402205c97a7dbb86412bbe591f2e251bc7a5e3bfff3c0b0962d3c0f04394413c1866b0220145fb4f7f5ce695a73492897032dbc97dffd4b4fd84eddf4e9376c95a7c0b8fd012102fd3e5f7f16949777fcb27232fc4264afab433c7f4dbfb475ce3921aa7b10c06a45a71a00\",\n        \"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54\": \"02000000000101359121955b09561ae4e67782e6daf8c464bee327671faa38f942d6faf54d16e60100000000fdffffff0220a1070000000000160014fe58a514623f94907f1cc0767030a301fa70e3a0b0400f0000000000160014887c8702f254d5e44c9a170c14c7b200c1d04d360247304402205b8f5cc055c3403828cadd6d9e689473efc39ba6c71f009d659bb87481b6d33502206f441eb6a069d9a8ce30264bae05d78c9306f0050594f0e773b8594f378a4f53012103a18179864ca030e8f840cf38828a8c2d9999692f3d3280b35f6bad95948ff21a07ab1a00\",\n        \"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38\": \"020000000001019dc51e473824610ab60c03941b7b0652c778a301398ce1ac2eea74fd3a59c02b0000000000fdffffff02a08601000000000016001448fe6a045d2b3791698137a0e59c817cf681441df709030000000000160014dc40ef96b9f1b1c54cd9389ed8b9467c1e670c3d0247304402202dd9810e77538682880396afda3dd371bdd283c9a9938b457ec779675b22ce45022061bf32a97f00bd6c1a26d1816457c41fa8aa9b4116eeb1d9cddb6877c8a2e1880121023243fea612362b6815e77bcd8ca683c25ca645d8f46a182d5cca9d869f10561c156b1900\",\n        \"65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc\": \"020000000001013f6e08e1ae725b5074b78e797cc5a0b5e3e5b2076299accd633ecbea2f706f360000000000fdffffff02a0860100000000001600142274a4e2250437331987f4779e70bde6831c75056048020000000000220020b0f76bb39db4b448e02b627ce44940f16c3259f05b8c6a7741508110c8104df3040047304402201c52ab2c60da000cebd61afbd2243c04a8d4a8546f45b71dd1b8022c4eaa87720220555afe691270a86680ad5bcebbca2fb061c631e9b15c13209cf29403f73520a10147304402203e9e699d4d7fa4abdeffc29de54ec9ee919e9d64ec6232ee9abcf9fb160467b8022043d064580f44cb013a7a4eb54d6f0899ffbd9b0412ac4ea288e2df8fd121322f016952210225dd8bab4e55853f7f0f29738e7a65d2d80b83a36f25a72383fd29ff756b876121038ccfb1267cb871b7d2e9786fd023423384089e7155496ba0f5adea95ebde160d2103c71bc2b94e5e425d1a4065e4caea72c52bae848ff87e8dd5cf55d3fc171881ee53ae90a71a00\",\n        \"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4\": \"02000000023fe8d4ed23e0ad785a584148f229d7e05d7ccf81870f0557c4a59f5e0ae8ed5411000000fc004730440220597b8e7b29af25d8b923c8890125cabd34c3743a69256b72a6873cbf4e9b20d8022063327f53c2187dacc1458130921674bd712a46b2cb2974e899e14a34a2ffc7dc014730440220579412961b76a459283f9ceea056e0b53a437a674fc9b83338c0b87cb7b5f0a202200649fedebef31927ea3a72bbb97fdba74b6d96bb7e91189dd027e1f753e1f665014c69522102ba13fa0ae4c4ff16286cb456ecc99d8b62d2ca3ed641879a707e937960ed940821033c6e501ff0f7c5e10ce2d275a32ae249171ae1cc42a471e11cdfde866b8108cd210349126e756301ce867810a498561f4c1090acf8f7ea8ac59d8d165598411d208e53aefdfffffff0c92a43b0dbf840769e79cda80dfe927ea05aea7b621d1f910b78b57b9b8acd01000000fdfd0000473044022047b474c0f1324b0a555eb2ac31dba97879953bd51f2659e056b16106523cf473022028db2ea106ddd6c848b950e5e2842df3c6f999574b93d7b47ba7d427fa6c20cf01483045022100a3b2efdfcab6909c81759e63ae77b7779a34e5e507455cee3dc576dc6cd545e70220404b292295b468fa7549490a1c3bb39827104ed1411c58b0c69c6dc6dd375d5e014c69522102713f42b3eec7346af75178a1c8932c071e9065fc29e30c95cbf016424c294ddc2103256b6de1c6a0fa29eef2dca9f9eb352a2cb1bd846bb0a7816f80f377254249b721036adc4dcfcaa42d0ba4445dcd66c0bc35a9255174b9d15fd299132a653892b82b53aefdffffff02885801000000000017a914e02e44af71e72872f27aea597b44510c8c1de43587a08601000000000016001411455e3a8b9dde490455ed429bb1ee1872849185d1011900\",\n        \"69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1\": \"0100000000010189f26971e043ec9561455b1d53313e25c551fa0ba152313beac8f0831b4d348b0000000000fdffffff0210270000000000001600141d9032937978b2d467720b06b3d1430280912bd0b8480000000000001600147aa10569c55277967928b922ee954bd9f85a38fe0247304402203659c09a488db68421e5ce205ad2dc0ca9dd2faa53f54999a3fcafa5d54a5e39022057a82693a173c8ef26aa87b3c24f8e0e36c29442210405addead5ff3fd7e8836012103d1b4228f511421436cb607f4be5ad2d42e3f149704a5a50ca51c2f04831c2b5d608f1500\",\n        \"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b\": \"020000000001014b51252a3ee4a4d1a1a23653d55ac6345cfd78eb9239ffd47b82549bee0fee5f0100000000feffffff02a495930000000000160014be4507a9f1e38ee275120ca142329e337705f5fe809698000000000022002000a71ad29758efb8d6b0574d5b0fba16ebc3152516d238ffc5bcc7287aa688db02473044022033671e1b249517b9d5473aa3b12bead09f6aa2974dea6d96699c164d99677ef2022013740ae88da9e92dc1517d4b8b91c0078be0f1ccc133e5237355f2753148c1c90121031befa5da2f79eca0f75b6000c5163e84a15a9486771420d84745986f69178514da0a1b00\",\n        \"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc\": \"02000000000101670c80d8306cd206e010033b76b3dec4876fefe58aec73d56c3ed915e06d2fd40000000000fdffffff0220a10700000000001600141d369d439b6fef2180abd3be463765d3c5c7cdebf4a00b00000000001600140c42a6f94892bfe1e85d7d34b472b914383906e102473044022056e0a02c45b5e4f93dc533c7f3fa95296684b0f41019ae91b5b7b083a5b651c202200a0e0c56bdfa299f4af8c604d359033863c9ce0a7fdd35acfbda5cff4a6ffa33012102eba8ba71542a884f2eec1f40594192be2628268f9fa141c9b12b026008dbb2743d151b00\",\n        \"6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a\": \"02000000014b51252a3ee4a4d1a1a23653d55ac6345cfd78eb9239ffd47b82549bee0fee5f00000000fdfd0000483045022100827147968d97eb2c839d6ebd18af1c247aeef1d9914d3d69f7bc423364cd6d1302207bd7f3744ca312b4fffa54ed44ae4628fdaa77f04578c07f2bb6f79f54a721d10147304402200bef8afbc94f839296961922e7f3bb04659c7b33439112e8aecd1287d6d6ba7202205df39f4212e51e39662480d221a56f3bab9187714ad73909fb4ec806fd670ff1014c69522102b6aae77f674ef3eb5ad3a3bba9af5ea7a9374822950ce21a87e44a9245b72c182103b35498d1b18fd0ffeefe9ce4188a526a590439a42833d660ee39a04a9c0fb2502103c0696060ec15e5e62748353eb0a001ef6fc1b59f09a1def067990d78c0a9ba4c53aefdffffff0200cf03000000000017a914aafe0188ea6c7cf51b39fbdb1fe6b508dbc646e68790d00300000000001600146e6d9fb0cae5e2d4b8851991b872ad1f5e9fdac97aa71a00\",\n        \"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506\": \"02000000000101eea31e951f2da9ed75369f1ebca77d13cf582b413464df36eed75f968358d8d10000000000fdffffff0294c00000000000001600147723e79c1157fce6e189ba4ac4dd1d104e412109a086010000000000160014ee9a849b02c4a1725d9a6d716781908ed0d935480247304402204fc7aa5709889fd53ad5b30023b307ac95de8d4dc4088f3a0c07db6cafb1c4b702207e07a06b3810cf89506a0ca6c0ff708e12b99b552d214231a960fc6f4b63cd7a012102a8688635248bc44a7c70f5f0dce799ed3f37e52418817919315d6b239700f7706eaa1a00\",\n        \"7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55\": \"02000000000101217fb607501f5f2e6660638402924c5380c1232c99201d8bd753135b4325b08a0000000000feffffff01fe2c0f0000000000160014d90fc04ad75354dc2262d10835cacfa4e84ae48703473044022056c6d8ebf46d9f68fcb83e0e6960a1785d42ee2384d64f8604b4074b9c0de0a902206fe977d57700504e85e6c761e788e9816643999f8bc885aa79ef20db7b08dc810120cf62e39adfa255e3fd410b31b3f402c1f9c1cdc6a935ff719f091226b954e6aa6a8201208763a9148cea0d0439af5eba9f7bd617673c1e69d54ce9aa882102c2e4b61054447297b7a2b9b15aacb0a21713a5676a1a6fcdc069387811935984677503e00b1bb1752102674adead3e1548cb4298eac9d6d62db66c63864b752f48f66fba9be12f0bf1b168ac00000000\",\n        \"72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390\": \"020000000001015fdf6216a1edf986878f35e92ba5747885eb06e12fa2aaf277ddfc557cd7c3220000000000fdffffff01c485010000000000160014128c89173dbc0f4a0f8dbb6a61e7ab094a60c6460247304402202d2e7c9a9bbc234dc4db6e306084701ad7457c1ff2f123a1687a9b4b40651f5802200f505b075926efd011c6eb3080f3d524f47ec3ac6a4fe8cc1da0e6ef1666b131012103ee66ad83bf88fb3f79929075aa9cad3df5f9f9ab9e679624daf19a90ef1a9f443bd31900\",\n        \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4\": \"02000000000101040b9b5f22e27b170c53b346669666275a447ad63aeb6af0630e81fa184d8d1e0000000000fdffffff03a086010000000000160014fac05c98a743b6aeca8e9cebbcf81310d0d98557b819060000000000160014fa89687b7c7f0b4b8648ea3e47f0dfb94e98140820a10700000000001600141ef267f1ecb6538d987a4e1503718d35f5e094b3024730440220134cc1a95deb967ce52d4024649d77f5da8a0a1ef67e8ed5caf983895d26bcad022062820d0ea14925eba8b018b1c9133a5ae67e77bc43b52ee59277d4cba4b90bae0121039015d8c169a8a12f689763b2774e19f7c00e20ab3f009689254a79e65f880efde5601900\",\n        \"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3\": \"02000000000101771806666e0ada1dab1ae15b601fe9e475fae4090adb0b5f2da2bc31ec25aafe0000000000fdffffff02400d0300000000001600147da9196571d573ae7956ebf8b7720f0472723de95c1a070000000000160014f6bc8c5cfcc7517546f57a469d9cda96f11f01860247304402204e0440c23b2040eb929d9fb613c2fe6786591f533209753485d6cfe5dc21dc7902200d38da5c8cbdea107cafe3fc807bc070884903d2aef5daf147f3cf859736f03701210370e25ec03a8ce13b40c17fa6de50c2571309f0c4c64735414caf3139e4a4cd1c41121b00\",\n        \"781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05\": \"02000000000101a34b263554ad4996c506866afa47e378782cb99d544c1d299e05688c844627440000000000fdffffff02d8850100000000001600144215d65c968be07ebe7e52cf0dc708437604378ba0860100000000002200200ffe3c46c354b37cf108c0817fd97a8f406d983acff859adf0fc90f4693d70c202473044022045efef7d761014f60621e1874d5278572e0227881b676f37becee445876ad699022026115c79df4edd5652ac384b835ca81b484c54cf2966efff6b665def71ed3f8f0121029a6fa868e54dac3ecd33b63ecde197e936f2afeb1cfbe4ed610e342b154ca30135151b00\",\n        \"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d\": \"0200000000010115fb06d9938141ffe4fb9072559498c12620b4eb4ada6a8b29a9105c9f97d4430100000000feffffff028096980000000000220020116efd5fc9445470bb55ef4c922e14a13a97e1833861d5b7680b1a976ee221991c66cc0100000000160014e61e7a33764582d1d1c10310bd348e8d214b807302473044022026585abbca5c31ccf751cc9948c06de75d2ab8a2ba0f2a0d2a4678769860ca01022064e1b14ea596e7a9bdd351351433a52ff07bef6d1275ed3f88a4171843e6dde00121029a412b2f222badbf5f20b13dd9d1df711c84e2d4cb15c5d798f349433c757b9861a51a00\",\n        \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\": \"02000000000112d004b67c3f20e564af8a1ec46db16279be55ebec9f727b9db66c1a9881592f0c0100000000fdffffffb395dd32cf04fa8785c4374f4399d155eb641e43e326bfb41f22549f08f1b8380000000000fdffffffe1ff9efb1c7b3175d6bb7fd91cfbaf37169b735f0160a1f9e61a4ac2c35eb0400000000000fdffffff9a52d653d1ef6567d22ca994da08557e78fc07f93d68e2dfb3507c0ea315f5480000000000fdffffffcc7dd19f508527a7199d7afc9cb79411b8e2e0319c8a7a30d06dc03e860d03500000000000fdffffff383d0a7fc6353806e9800ec0b713413ecf3c347068c93459c263c5631c7a27620100000000fdffffffb44190a315a60c1f6e67ad5772db3987a06486da223d3fabecbece3c078d4c670100000000fdfffffff157f70aa01e34954220e38ad8111d9aae7eea4d4d3205abf4f76bbdcb3a07690000000000fdffffff908331680dea290ae117d18b4a5f9878b9f75ae6b3c164fab9d533608f9c52720000000000fdffffff674835df83f869ddd411828709559267e6201d934153c0ba7a905401be3087a10100000000fdffffffe620ec4adaa37e408b6bc8abd32af295462f1060abf94c3244bd6fa5e01f1eac0100000000fdffffffe2bffd7aafc4fe2e64fd46768ffe8c328d41c9fdc95030aaabcc76abb4541fb40000000000fdffffff528f3ad6bed1c06de1464e4e6c5a623df2938c030d4676b1a767c109b929cbcb0000000000fdffffff528f3ad6bed1c06de1464e4e6c5a623df2938c030d4676b1a767c109b929cbcb0100000000fdffffff56b1cdeb5224a6fc0a22593cdc936626670b41f5e43932749697e35618bc87d50000000000fdffffffb2a0f40c1b6e1b8d42554d4d58d31893491ec88dedb60ab6c32074f7ee8755ef0000000000fdffffffb2a0f40c1b6e1b8d42554d4d58d31893491ec88dedb60ab6c32074f7ee8755ef0100000000fdffffff3d1ddf60caebe1fa31bfe5e80e0c6117e916fb4f0ede32391989fc210164a8f80000000000fdffffff01c09f1d0300000000160014708c9e61aaaeeb2a935317613e606653564add6a024730440220394005e6d811a7e27a1d450885e0274c3d3a7d62a7d8c1ff1d4b73d01b11051302202734f752c8ced1494c2ff24cf99ca1f5cf5d76268561331aef9457268470f1b1012102f6d8119faffc8650cb12f120de5283d4798d8a6d20191630b78c7768951122ae0247304402206e6a5ce1d9b3d4fca6d9bc3485596ad593891593e30c4bc770d17abfaee395a5022018dc4747470dd43c34634059766e73a4fe99a0b1e7b7af8c06474fc17a0518e60121028b210ec4ccd3217670690f81d1fd75390f6792bc558a6a93ad9b15620a0281f302473044022001baeaff5cd7ef8adc937467bfb4b6498bd66415d39fffe05343affbd895155c02202184031dc89aea513e3e3c1985d36ac1f32e8b102744b69b77e558f2f4340457012102850bf25b060b0a79d72ad0110247e7856d5c6452d684785daa3137d5d8c90a510247304402201ceccce75d2af1f8941c02ee421f4e6f404e87e56c246f79ce19c09ee0e119f402201eb003170de726a60ce7a60f9d53b3c4e748cfb9b9994c681acda0e72a92b26a012103ad9cf6d8b2d59e6e5de9b74a720763f55d95db451dddc7216df5625d82028a6302473044022071e07a61b137def04dd364b6d2e807b18d48bada99ca4d71cf8439b1efc3af0302203acb936c7f49fa2c025feecd5cab330c6a232c1401e89fbf8775b3d375b2c9d30121030178b7d23e9ff9b451ad76616e3f031be6e7a113fbbe6ac91f4278555b4cf8250247304402202650920eadc36bb635f45812795201c9765bb3bb4457b598860d8da7ea6f073e022012d99927e13b19a257c0b42fb41a390942e943548ee8d091c0010bef6513e6da01210221b33a4f3c68a1da5546bf165fcd07d2d12d030c32c26060945ee7173ba3b84602473044022031288b3af2d909a0b7b5408811c33f1fe99a3c078a66f6a106facc0fc5cd2ed902202f35bc8da7d3142b8ee51da13b7f1dbe1581508675b4ce98f1e782d2495e7d04012102c379796daf31645f2126029d2affb43ecf6c4734efb0b546ad18890b38a1cd0d0247304402207a8ef2d0cf753f0459b74b391de09222035f75ea12c428f1d78ff116bb4640f302204d018f083a01d37ce62f91faba1b4982e7ac8b103fee83ddb0e74cbdd5bf817d0121039fe845d96ba9abbdc152323e25f20ff58dc9623bd6cc3fbc82c4830eb011d3db0247304402200d9cd669cce905a5c0baf4a982431420daacfb5890b1a090f0f3fe6862ae8c7902201021b5cfd837d58a684a583c9086dee35d7961681314bd907e07ef3759fae8e9012103b682489cb0bcc9f5bc430e987981f4688e89910cbfed23abc8e12aff5bbbbabe0247304402204280a064196292677439e6d818385de519a79e570600e42a07e94e0c24ad89bb02206430a3625e8940ebb51ab59f0251aaae61a9c9999214007cb4a45dfc9ce7a44a01210210b0619e1ece91cbf8e797e410d2fdfc60b855133eddd5f870b22b29423f6d710247304402206995c91f7da5ccd15bb2b4d4d9dc1efbffd26611fb54023cbde92d279ec8983c02202b3b7e47f488ca5d018f5d1da157bab728e12b2e271cbab80d87d53767775e36012103b3280e632280f8cde9e19120147358b77ec3f7b6f4ba9c84f164cf3b7ba7a79c02473044022006028b175a23c4da30f1609c30a80ce6b5d5c08d9857f5b2339f274acc168e7d02202574a3648f2833f7b5ef697f0b85d5bed382ecea840aa844168b471b2023250c01210320bc7ac7e9b9e01db74bdd7d5b4905647b657b0e57d0a91cb0c317fa9d4672bb024730440220014ca3e53a8d0a950d3a18172e3df1fbf13cbd2c587a9b5aa1c44340697b2461022024ede3373892f1ae502a4ffbb761a7d484fa567d4f975c20e06c430b1af268b4012102a8c26f0ca20941e5675c847ee6ca78e387c63845f4e49b8b9680d3717114530802473044022011d98a594c48af630a7f0632a9d5d7a311ec85c48a0f37a681df5f97105d7666022035871815b14eb8f46532184f7cdcac0d38683b984355f484053c5f8664bbfec4012102dd3eaa75f9b5bd1f26451b2b048e1b58316d708743c2a3c482eb0e1ff12d789b0247304402206e9b717de5d1e698dd7f084278c26e1213fae0ea2b47b70f4f4918ed0fee4b940220428170ca0826fe3e83dc0fdec06c202d5a96fb82679790952ef103e15e1f8c810121031fa8cc56975d8aa5dba41a2ff5c3de5a0ad760c4b2885b960404c1f933c801e80247304402205459cb2c56fa278652f40cfdffa98eea37b34d20974f754d66deaea3ea93a1c70220264bed176d268e37c5f3392121d885f3543b93cd97ad027a926c7ec316b5896e0121025d87558f20c8b9f2d0df278303833cd35ea5b85111d9259a18f93a505f026ee4024730440220799ab8195364fd824fb1cf3f42b01ba2da3189527eee5036befe52f0edd4433202204355621e136912fc3c6f157a4e650b67815617662cc23079936269b2a9004adb0121020a99e99729a8d03f3f4d6ce6ecc811b4beb24cf4cea715f59327b46215da08c1024730440220741c8364e4972b6e32fbd27e43a3863196dca67cfdb012a8534dc21328535a8c02206811e3866778ae8606d087ca3ff2fc91ee6671b8d22a88d14701859176d0bd4d0121037f5d9cdf431065cef3a5e6d5b2d9084f1db6b28f91caa840bf6249ac23e91858879f1a00\",\n        \"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a\": \"02000000000101824a4760117708acf040b536e8826465df9c961ec7150070c5b98ecff0aa8bd30100000000fdffffff01b4dcf505000000001600143d8b00a94c9727eba5f04b20a99f6972ce6fd62d02473044022003fdcfa7c316345121bd632d58f752bb2494d2855b79ea0be3c177945df439bd0220606b17fbbf4a691c4dcc6b9c3cf850acd3bf0196548e673dd3bd6a8f212a27a00121031b10087aa732c0d30d30374036078e32b3d27ba6cc87e806234524017f30c5ae390b1b00\",\n        \"85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834\": \"02000000000101a169675d425773fa59147b2bec9ae28a9d70200e9bc40173652c3060cdeb12a9000000001716001422309b95e817d27c649e57573eaafa0bd362170efdffffff02a086010000000000160014e910558aba5388bba5a6769060f42e686843f10b88360f000000000017a914098f87a72795ed628c0e9789ede7984a7c035ba687024830450221008e3b7ea45faaff213ad719a16f5052cb265a2af5fc19f0bec38874551114da6e02207911a3022a37a78eb034de7d1b537e7854fe4abd4b1045b2a757c50c46df9e7201210240dced583553aeddbd0e6a75d08944fe638ed89d3c3dc80f330af09a575400e752aa1a00\",\n        \"866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff\": \"0200000000010114456eae9cac04d9192527ce27b77a2ea5ab88def7669b416fed14b5b87a5a950100000000feffffff02ffffff000000000022002068eed609d74d05b2ece890a372e78d27a94c82d80f02a2a177567e353f3790f4ba535f04000000001600147d774619028616899ec8224d8cce21019a3510d40247304402202a35e3caae822a5071e3cbe16895208571c0aad44ace141ae47deba35149501902204b40d3b54f712d8cd4d1845587840d1b60eb810d7705e66469bca2f269a62fb60121031e449109ca6f091abd125eb2734530e0ed7ffbf7b6b69e6c92176732a1db5c0c3d0b1b00\",\n        \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c\": \"02000000000102f7116fa5f4728dcf1151b1b2d133a190bb69904c6f452651c377c1a4c57d90af0000000000fdffffffa0ddbc2ef967817407d33c3650e04f81c57623ca8a4b3bf21a58cc1b979447c20100000000fdffffff04fc160600000000001600144091d4a839df35629d96c18b9545111b2065aba258a0070000000000160014c0b9e09a8f63caa12f97cf98676250284fbe590f20a10700000000001600142ed1ddb67b7cb178e7d12599f4136e4527787b4b20a1070000000000160014a65bfd8e799d4e5b9a4dd337dcf8397a1d32cdb602483045022100f8fe2ce0c9e49d67684a5f4b8c227e50771f066da68e301ede49d838520572f602201e53c69200e21c151b6159beb9ac7f04a719b3992fca19b3b3ac9ba7fdf2e398012102bd47e76302733aef1d811e6e093900692b70fa6dddf4eb86fdd39d8a196c079602483045022100a7139ea2141e99a3c4f0c881ddf0c248b97b26cb9a28de3b45b59e7d64d01b2602206f517b08a1d679778a52c36134cebee55e56bef0eaf8048ed63100dd31844e580121020402f62988efce421d9e971a398ce3894dd22308c760473ec0d0e8f681b9986b9c2d1800\",\n        \"901856f632b06573e2384972c38a2b87d7f058595f77cd2fcca47893bb6dc366\": \"02000000000101aa9b9f6533ee33a07c4097d5ff2e13937019aac75e8fecea1be720b1145b4afe0000000000feffffff014f95980000000000220020ac4b15ebf67e62e9f9f1671e8502a21bd615bfa95868b9a2525bc63de8b8a4fb024730440220106edf9eaa446e1a097371a43d14d54461b05070022f8c01d28f46c6662153a202203d3c74ccfb737ab0ddea3e4f632b2061d5878e223eb2a72bf3bc318f39b5c67b012102677bcfd76eafe311d0972baaf6166b62e5e545b684a81d905b85250e5917bc013a0b1b00\",\n        \"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5\": \"02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f784169851800\",\n        \"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d\": \"02000000000102f428f94ec07b2db855bf3c4fa1f2cf52cafda8549c7276a30eafd34620bb99750100000000fdfffffff428f94ec07b2db855bf3c4fa1f2cf52cafda8549c7276a30eafd34620bb99750200000000fdffffff028c180600000000001600143f52667962c0a452fdc9ae57afe6b6571eb22c0820a10700000000002200200e08faec6735beb913f0a7179e0dd14f0ae292b0233654413495a1006eec84a70247304402201d0a22029b241f4cd29d263ffc03764c8bccee849967ad1ec390a32284add4dc02205ca46bbe1e9252bd6c0fc9ff6bf3b5d81e69913c29dbfcc8693889f7a4266c27012102f622e0fe7a26c9c9c212d99bf7e03963f7aa1d39b2e8a557552122820ef44db40247304402204623b53d41297def5b685437850f2a0a5ba8a9531496de641b70b0d5587a0b920220346eb070e7cb7267f5f9178321011eac5c700d98f66ad54157edc7801ccf22ff0121036efd65489509807fb2602db4fd51479b285135f0be25d59bdf3b78b9e7ca88218e6a1900\",\n        \"94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9\": \"0200000000010195729410827cc4a43abb89ef0a7faa4e41f7fc66c64dd81c16fcbf6e8d2f51c20100000000fdffffff0204870100000000001976a914ff9a7edffdfb4c3742a6f362ffaf11ddc63d344788ac94d3010000000000160014c6f7c545dc197cde1102a2aab7fa157a1a612315024730440220137098a9c5342a5f5e235798c554603343be6da4334b116b7f846cb07b664ce602201433b8ff720b0d8629e605352996d53710859355d9028cd58f560a0deffc9eda012103eb10d3530984c437f7061cf7f4b1ed5677471cab80b18cd76c3a89567d15f56b08ab1a00\",\n        \"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514\": \"020000000001010a23c987f7dc02b8b02ce98f1c8a7c331cb5fc508963767783adb6c75478c9810000000000feffffff02c9879600000000002200208a2f43a27d9dc7872cc662081c533e258990f681a90babd8fa61efc7466eb83c52545f05000000001600147164dcfdaff09348c9c7a32fe0cd9533721a2a2c0247304402202563f9705946111c6f13c3a44ee5e8915582281eb64a2c28dd39519bc2b13aa102202a7adfcffc7cb90fc0240493f842615ceb512aee44169d7c06926cf09500f0e301210308df6757696a7870f75cd4b1de04a10fcb3163c3e014e8c090581882b59c35f03b0b1b00\",\n        \"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd\": \"020000000001017102c148a879068bc7eb23559016a9a6e69606420548f291dd3c2be8529a68ae0100000000feffffff02ffffff00000000002200206cb96749dae46fbd7d3c6e14d44e2ba3d2ac43b71769b4ff75e02c65d1fe8d97d0dff503000000001600142d28cefa680a84210d2edcd45a7a8cd5a004886c02473044022002aaead668b30f40bc44842f43febac77ea0852e81db006652f3379919a48466022045fbb17b7be757c637ec8e45abd92e52d1e51ed8498005e740c1c5e8b1571392012102a57eb2dae8cc038ae16e778125b854d06ed9cee36d55314c7c46e57c7290fb66e2981800\",\n        \"9e0789481c2f5b2fda96f0af534af3caa5cb5ddc2bbf27aea72c80154d9272d2\": \"02000000000101217de7a4c5cc7c86a28780a3503fbd675553b8fe33179ec1a75f7abee4f426ea0000000000feffffff010b9598000000000022002019d8c5f534f0421556786462fd239cfd48b6c074ee4fa00e5ce042d79e8ef49602473044022025d64859e81e55318279e5f83bdbc4b39cc7b7325da7dd78d8dbfac1fe3f044402206ee91f4bb9e7c804f588cf340fdd5e4b2f844ccf87e4817a95ca2a5d38fb6aa40121033188e837a2145b5c1b98f0e6b05232c4af07462c4d5fbdc325291808aff56db33a0b1b00\",\n        \"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e\": \"02000000000102bec4a6f2975596519dfe162c636967c2716cb12e5e93189735a0e341d59cc6090000000000fdffffffe546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930000000000fdffffff028800310000000000160014700d20a48be81e702f3c851b7724aae52c24888c404b4c000000000017a914e2ccc6c05b1781dced3a1bffaf6c68d6e18bb5da8702483045022100a2d5c8766f25c5805beb3401e6c31577f1b33dc2ee991f5e17dad677058723a302205e2f745b03f57c463e6e90c634c2f26dbb3d787bed6927e3f04f7bd31a2645620121023a87f5598a10286ef18f50fb042436b5c6ce51aa43f10fa1bd60e2d2adcd7cd6024730440220510db041d21264d41cfc8ff691aa78ee9e3a25bdb763c4d69d17ae9c309cfb9802201b519ab7d491820b8bbbea848274cd0e3329f19c7e6e0f8732333dc995bf60f80121023d393cfe08065459ceb17325fee071dd983a12d376c2bb34e90461e7488811785f8e1800\",\n        \"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9\": \"02000000000101942d53c23fc9fb59f37b73180418427759c4ac2ac027d76734b473403d35ddb90100000000fdffffff0280841e000000000016001426b9ccc02e06cbb2b9b266dc37fbe54b26f464b1ac94fd0200000000160014eb23a5265f7119c1f7201511b4bee31b185b84b202473044022000edf5442332503900710059f60e8312a61cb1a5a0037cde603b3870f1cc654002201f14e9e0b81bec823d3a6df1e581f9eb3a51ef0e041006177b9cc28483d2e01a0121039b1b0b2c9994e4a3cc2070b99eafa35412afde11c06756f6cedf06d4a64709506f9f1a00\",\n        \"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867\": \"0200000000010187a621e402bb6dc6afb8b699720967bda2e2a6d6b51d3352c3d6341416a9860c0100000000feffffff02ffffff00000000002200206fb3cae6030e3a497646453ecd3c05a0c253a79195798f402c452a8d5fdba315a0def50100000000160014d7c5b337571d08ae2c34252d3ddcf6ca7b3cae81024730440220795ad9ba37fc9e7b34222faf54bf9723131200c2264b6dbf45fe5913ecbaeca3022063b6d3a65af1ca38993deac651c35094b9e3f76731f6914b3c1825499e0fa5420121031359f4b3250369fd496f35c8f37a3db10c84eabb3ad9de13907618ec5c5976a9e8981800\",\n        \"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6\": \"0200000000010161bdbd30a49e41742ec8b41ab69801687becb44540706bdfc8aa034f6515da010100000000fdffffff020000000000000000056a0333221166030000000000001600142650fbbd243b1d1d81fdfb1a94e5fd31dbf5f7fb02473044022008da90e08be152bc377405e8f8021a36d84c313a2f466b83a8e0628eec7d11a6022006c80ab66477f05c5e86479af9fb1f8141f073d7f2de5c91c1c5668bf63677eb012102fea1fd504bd69df76fc8ebadd921192c5d83fa45a300a4177e28fc0920c704f6f39b1800\",\n        \"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271\": \"02000000000101cab7d8923a6c14dfa0c81132d9390df75d2802eca35870c3ac6a4489a700ab0f0100000000feffffff02ffffff0000000000220020688742f8047a47d131b9767e0f88c92f1e7b511b04b597bc822d2d2bd26dfacf68e0f5040000000016001407f487bce4dff09d8ece41fe9ff97047490d42aa0247304402202064ac3ba7625d658bd51919e2ef6faeb1b0bd78643bb031064066b9dcd71807022050037c48ab9ca6bc862ab8c5d034d9e2d7c4ff16167667f2ad1dc5fc8a4e91b1012103706703ea1266b396a24f58dfbdf8e31e55071003b5bb69c82d5880ac4814d585e28d1800\",\n        \"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5\": \"020000000001018c6d5a1b0a0b34113c0f73cbdb61ad490743dca7b9e3f335999f209c1eb4278f0200000000fdffffff02780c030000000000160014f45e8133945134b7fabaa27ebd7727ccc8adce17e0930400000000001600147aec8922898ba60e41db587b75261b305ecf947d0247304402201f24f822ddb9c1792a912089389ff7161f98f7fe001fdae299bcfc6bd13463b702207099f7fceb0e94d2c63215fa0265a60ac074f0b09a66fe2c18a2fb8d181f085f01210228fe856f58094e4df1531cd385d0864e8cfaec9631f31aa64ffa593a96e8b5cc679b1800\",\n        \"b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2\": \"02000000000101d004b67c3f20e564af8a1ec46db16279be55ebec9f727b9db66c1a9881592f0c0000000000fdffffff0196a0070000000000160014b8ba47facc35b991ff3fae74d0c2316ac7121b730400473044022068dde34b246cb6905476240e97d2c7de9477602e6ab9a22dfe66d01a27166106022022903cb12a0588602048a4129540e15efed8a41d2fba7607dbb1e75dc50fdebb0147304402206e8409516f1a99a8b346b7cb7360dfcedc0da6ec05b39e010d7644edd3f14c0e02205552e5456a204edb84eee4713e4c6f93e8c74b753c0f361c0cc157860c3dd84201475221028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c521030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c52ae9c6a1900\",\n        \"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7\": \"020000000001018c6d5a1b0a0b34113c0f73cbdb61ad490743dca7b9e3f335999f209c1eb4278f0100000000fdffffff02400d030000000000160014d125023bd52de48ab81d54facbb7b2538c102ec250920400000000001600142f92766f7866248de5f682d7dd2bab088341457e0247304402200b42c87b46bf34815d16998c13b80ba473d5755992fd277a1a4cdedba1cba2af0220028b8f3dd379bcb0521a450bdb45d1f5be28b94e1f2cf20bb65c2b8bbd6581330121024c296513697547826cd2f98280dbcd44965390ce5f40f3e3321a2de83ded6620229b1800\",\n        \"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94\": \"020000000001016710a176ee982b948f2fba92d16f3533d3503b806a771769dcb4cede3f4fd47d0000000000feffffff02570400000000000016001457a5c06d1b02bd52b89fd90167bfec31768e8ab6dd191c0300000000160014ecb59e37d4a1b9fa13cf611fe88ee651413568ce0247304402204d460fde321c0a82648af3d21b19acb228bceba8040ff677a5c6985d7fc050b5022066c7f1a692f415d7a159a5a2a3e2cf457e8f824184a87b44618cebb6b28b16de012103a45a5e3b05ab7c340976ad921dfaf018b3720cd86fab014603237ea10de4a250889f1a00\",\n        \"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9\": \"02000000000102529705cd939fc7ab0d3632530ae7a704a892be6889a5e6f9250eedea6e54334a0100000000fdffffff529705cd939fc7ab0d3632530ae7a704a892be6889a5e6f9250eedea6e54334a0200000000fdffffff0186c4a70000000000160014bef230e843338f455977773d701d61feecad0f5d02483045022100eb2be404c770b0f8dbff2506c2924334d025efdec9a055a26d729ea0238b864102201ac7a7e5417028dc3554e4f5c90fcc2366e238225fb7b4d512bfa9bfae2f9f3f01210240ef5d2efee3b04b313a254df1b13a0b155451581e73943b21f3346bf6e1ba350248304502210089c43ea1b3274a7b1afa9aa5531c44344116dcfa921186a4736ba52f0e86853602201b330b2daa20e0f393548d20968ad55b32ea646d04ade550e4be7842c3d493310121024a410b1212e88573561887b2bc38c90c074e4be425b9f3d971a9207825d9d3c872851800\",\n        \"c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0\": \"0200000000010141f2de02db45f99c3618e4bfb51cd3e5ec64db096886cfd8253bdbaf0bba58c72b01000000fdffffff029002040000000000160014482cc4416e2652675d1924f952aa3385be6c247140420f0000000000160014cdde578f988e214b8c857af2caa60c1cf28e7dc702483045022100f51771e279f9f95bf46289d0a60a13ac2165c5724ed5296ec7d5c53e5d237b2c02206b56552ef0b56a23778d876a5af2218f099491e5563d6e762e63758299ef73fb0121024fcb297ca268e5b7132fd523435cd7f2c1ec712e8368f5f3b3f227c68a9b427a902d1800\",\n        \"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295\": \"02000000000101a34b263554ad4996c506866afa47e378782cb99d544c1d299e05688c844627440100000000fdffffff028c36010000000000160014cb2c4764c925113234cfd8e031aee29e09b2370f605b0300000000001600149ff30c0926b285e32324c862b1c4d6bc4471e37702473044022036c27642095bf69a46324dd69a9eb3c5f48c6120e64fe39cce09d15527d3136c02207a58e175550c26d44bbc30dc51561bb3cd0400c2efe80db59fdec9a3aae26bcc01210383b014bca7874eedf690c8acfc93e3e3e1b99313dc3633454fd8e515c868de826faa1a00\",\n        \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52\": \"0200000000010249559bf09c3852fb1d592d23093a8880be3c9ea9f2156dd72caa46065f0893330000000000fdffffff358c51e22998661e230319657ef7fdbe7c745ea95957bd8f5227887b1edafc430100000000fdffffff0274850100000000001600140e7afd8a4412d95c2e168621c2d612554eb3d19ba08601000000000016001478fa2398d808a6ce80387e704a58225e2de155ad02473044022009368af6729bb888ba1cbce9a7c80787c15c6d7f1d443b70b800e86021a14d44022030f4d53818b7aed34b390b29accc03f8925bf6ae39502c1a48c1b4fa0bc08425012102c645db0de1449a56f1a38adb5367076b7cb9f36fe3876c0acf5e806c382a53140247304402201a91a39082623db2482525f8cb643c44bb68a33a1f685159b2d1537a780b56eb022008bd74e10597d3568f5bcb3a98b20e223ff9e2ac65dbf7072f7db96f255c308f012103af7486487df942e4c20dad086d96b0ad33f72ad0fa8d0416822e47758746b39f679b1800\",\n        \"d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee\": \"02000000000101cc3ac5653962a63e971ff7c6ebf70779f146c5525a77baadd36797742d6d43650100000000fdffffff01ce470200000000001600142b7d2fda455b197c6a7e16f0f54da4bc67a664cf040047304402205dc27eb2447a07cffdd1b9d41c1dea2b4e45b7d013e00acb8b165f79025cb121022006c9cdca43be5ee62f77543dd84d8ba05c0fa40d9bf54948fbe18286dec2430a0147304402204ac25cd19c6c3e604d11aeb4beb0ed34632c9974e751dfb5306d3e626fe6bb8e022069a00a721e67077fa8b42cbc5c8e3d7ddec9e0e1dfa88b591bb032d2a76541ff01695221025c78967ffec432318ae59e44c3a4ac7ae1d74db1d8587b862e0779055b35f7ae210386a7718a016752d5178fb48712b8f1a99502ce491991e8c7d453c43c67aa77ea2103e26f7ffad9d6d3c7edaa106b229eeb8b59af12d865ffe3ea763dae95dc5857a353ae91a71a00\",\n        \"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057\": \"02000000000102cc3ac5653962a63e971ff7c6ebf70779f146c5525a77baadd36797742d6d43650000000000fdffffff3478939200ea32cb65763d56f945b49f591a8a0cf554e688c4e9090aeceea1850000000000fdffffff02108501000000000016001431bcbd222d0ae1522fe2cada061f2fa81bdfc74604870100000000001976a9147b359a3a5876d3c2d499dcd78584091181229e6288ac0247304402205d1e1f0b69a4fb4ef306d7fe135f1558fe24f1f9801a5d49a6219fe3b83e6020022043b5cd2fde26f62fe446ef8251c110bdff3002e198fb1ecd1f812541458be1c7012103fee46fdcfdabf0a6b242ef391d0a0b2186855f21a26063cccde92d8fc6b979fe0247304402203d2a6146a4c6ca24868931df2f324b41ed9b92a2147b8dccc3692a4aa0c5acee02206ded2bd809b983978cd19ceebb7b7ec4ec95624fe8f538457fe4490832d1d24d012103d8db57b0a0888bf7f7da4961d70e9ea65794ada7c35376a5c93cea0ebfefeb5d6eaa1a00\",\n        \"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82\": \"020000000001031ec5794688bf5f80dff5cdde661666ab8cbb2b5b914f9f616532cfe38cb4480401000000171600146d5b6b42476e7b56ff74858ad5773354be8763e6fdffffff46cb528747263a9719aaeb269c03f78f5276313c0f2bb6647d4ee7a4d335918300000000171600140bdc8086150c54076e3f150968206549b56a8276fdffffffea0ef7d769fc5b523db55466dd41f8d3cc243df4bcd1555ee86d7313839d4de60100000017160014c2749b374708095072eaa445903ab1814a2b9e4efdffffff02002067000000000017a914d5c8280a6559c9bab4ebc50ac6fba885ef5b054a8700e1f505000000001600149379c1cb1f9e5293334b433c39c03d370f467a1d02473044022014c21c9e700067ed54b99e0548e6d2b639634fc04ccc7fdd4461311910ca23ec02200c8a0ebb81943009dc271f4ba39fbf3851d63e5d213251962a2bfccecc73d6180121023e68b3754a77dc99c93a2ab24c4bee22d9b783ca38a694f49608b0475d332fa60247304402205778a997e500520be0de51a7c707e1f77ad08e17cb738e8e56174cbe99f74495022026c087ded7e008a3a63deb93898537400bd0788fbbf16d0028a9fd0075a54836012102dbceafa500b943f93007d40dc22ae90a375d94ba29bd7591dc998450b1a858360247304402205b12f6c8ed0462be812e649534bdba7b6a5f12197c9f6dde1c540936e6c01f560220614bd71f02d4d4c8cc88dbf508ab8cb3394f27292db0b2bc181ff7cee503ebcc0121025a5823b569b8da23bc9d90537ac493731bc3172f908c6237c2b761cc827fb247da0a1b00\",\n        \"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67\": \"02000000000102b01ae0d3a3a34b48f73c0d0b14debed8059352cef7962c988692986bd6dfe50a0000000000feffffffbf2434265b777bcee606a05bdef28acdd8ca0cd2d03632a49ba7fbb610b9b5180000000000feffffff02dc42130000000000160014911eee03b9976e5fd6f1bce8fa4be1b9433d07db404b4c0000000000220020f3a22dbf7450c4125db98f5ec2ea36bd1c9dd8fe687994a98f370459d502745b02473044022003f036d8ab8c5c992fa4f01cac538d9ade369da5178ca1367d4eeab78b844b9c02207d082c7339133b0f8054377da99d292a03b400424c9db2dac3a93a53b275f48f01210368c3e8ffd98ab4b4c8ec61b3f875845fa5d0c4fb7f6f3b75a082ab1eda1215790247304402205cd416a603fcb62a7fcc02c78814ddb60ce9f8049db5ff8b11cc9e127e453a9a02200ea3b6265db9cb35aeae35fa1191657e2e901cc63550d6305eebacaa8dec37fc0121030f2ce9b4596792eb06752c5b3ef42cc72ad61acb9812b4f6b551dca86aca74848c131b00\",\n        \"d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156\": \"0100000000010136d1de175112984486c4b71f83b0be17cd8ca87ef2119cf1b265e23daf6964450000000000fdffffff021027000000000000160014aaf850e600893f139b1af7bfc9ee84777a4b2270ec4b00000000000016001493ebec8c80210a1d82c7412226b1665cf848623102483045022100a4ac1a65b8320f23dce6f9f6f671e92d606a571b5dc0dfc54a5cf1d18d3f3ae802200479a5b4352fe953ee07df21faac56be6a49a8ae583bb8a54004cb21c19cba97012102a239fa04fc1dbc55e017b3a9c72f5349f96aaf39a7fbb63148e5a0714697a6e0608f1500\",\n        \"d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3\": \"02000000000101a3a2d1a5f27a5e8a3a2a48bbe28071f15742f89e8ad1c8e86580c740d858e5750100000000fdffffff02b485020000000000160014250c6af08066897017215dfa79d217048b5601dee09304000000000022002060e574a9c598622248101ccc426053b8ca8424d56bd4f4ff594b8c4a55e1f6730247304402201bb64d16d67b2f4a871aab1356ef2d748608a93882b4dd70e38a3fab6ef552f102205ecf13c839d7a90de296ee75d8b43029195a730a49cb6f62f952087e4dfa9b4701210271cefdadd18ed89a0b6348027ebe5ea6c2406ab8db4bd420e80b86acaa8654189f141b00\",\n        \"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5\": \"0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca010851800\",\n        \"dc16e73a88aff40d703f8eba148eae7265d1128a013f6f570a8bd76d86a5e137\": \"020000000001015b71ffdecfbd7bf6c6cdc12c44a3beb578a5bad3aee0535aa03f04c34e4a33560000000000feffffff0180f8ff00000000002200203128e44db8a47057e5802a7c33f6114127ce1919864a01eac587957ffd7847910247304402207e7b02eef6952ed3398ce77776df2be2b4ef8084fd431a9d234cdaa45b4b710e0220367a4e683d8ebe8e5c2523be9b35e371c4e0a95fe2f91fdcfc8183a1f62c81bb0121028ef2e4200940b329f6e9fafeea0f0b91fc5baa14be629f90c4460d1b61f06577396c1900\",\n        \"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c\": \"020000000001017d23dff709affe0292e1c54968fe59999ab38aca7a56105f5673b0a019ed58790100000000feffffff028096980000000000220020392a484af4656d87203187746cfa12eba4986fc13819042b30b88587ced64b7fd4ce3301000000001600146bc11c4c1c79a06f0778a981903037caad4ed7f70247304402205da8ee1405f03ad943a4d1a95323b1c8eaad9d8364dd685f97d0de3b38869f810220649507c82fb80de316ea42720e309276dee373d3f73626ba6f7e27e27deff6330121022cf26a8a8f1cf65705a9f3889edea9f9577e39798e06d78df7ba0f23810245c061a51a00\",\n        \"e20bf91006e084c63c424240e857af8c3e27a5d8356af4dbe5ddc8ad4e71c336\": \"0200000000010161b73d74dff04648ebbeed7fd5ea4f700221dbf819b2d53503438292cdb571030000000000feffffff014f95980000000000220020cf2ba59021e807102f5ff2a6bdbdff172e30620ff9054a3b3c576847aebe24cf0247304402206053be0c12eecac6b899974f708f64a2aa4a163d53437535a7102b1ef20fcbe0022013d92de946a017714cdef698d808dead778ee62f7183e95e6c3a61b842bd4a6301210285ba6775416f686355229226b22e707990fff303e2001c0cacbaba482bf9ab613a0b1b00\",\n        \"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135\": \"02000000000101e98933d883b3a54aab90f73b3ccd1674508da33129f20b509339d152d2fd9ea00000000000fdffffff0220a1070000000000220020106442099ed51039e7ad438e8b0022ae7cc94ce527fa13099031471700272abb98e21600000000001600145db4ac28816705ecd5447c4ab0797815e1369a07024730440220279aaf8ff3bed63a72c8de6ccad8a33d4bd9eac0e0ff8402aa503b420dd4bde4022066d3d755901952b26823de847a3af1e3d7fe40df37af1871bdaba109280cf53f01210240aa29029982d026322d2bbe6db0350df770591acf1f9aa480ac023a4546c8a288a71a00\",\n        \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\": \"02000000000103f54c9d06153e05829192def700a5281f47b67d1e894caf1e5a95a744102c09b30000000000fdfffffff54c9d06153e05829192def700a5281f47b67d1e894caf1e5a95a744102c09b30100000000fdfffffff733b90182901888d7d39b03b4f9031b57005a8b1b53a6c07d52945c110cbfb70000000000fdffffff024c0b030000000000160014fb4d770f4456d38b5e493c5504e433541aa3c98020a10700000000001600145d6bf0f43f89e01fb76f6af70de8517bf6e383a40247304402204aa2a2b5143cd3dcb538e8e3c7d4dcbe05443944c14e0b2fce73309b1064e393022018c007b74ea00e8852ff1039f3187cf247a297436b0040bdb6df3a403d9cc57301210263fa9bbbfb7fb212bedba0bbe6af9ed90aff45aea0511fc552f3cf146da21f5f0247304402201b2db5e3f150b468a703901b35c6bf19ca6d97d498bf149e2231cf6f4b22cf86022075a8947db53db5d40b55c6e14dbd5a9ccb46200c2a739846e5476e4f66df5f080121027483e3dd39649553ef51b8e09fc498f866d67faa164d45e266c97f94dda8dc9e0247304402207f01ec9439be25ca4c4ee7e4c7a9cc2cb21eddfcaa56e166eb0c3e00243575e8022059760010fd9be1132b9eeccc5c1c410809367ec26e6f0424282433ce51bd99640121030682ca2df861e021334ef8a40f98ac4f3eb05e3c2d94a698cbd9c5c6f52817d29f9b1800\",\n        \"ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21\": \"020000000001012c5783edec9a110c012a72ed737acde1a97ca9b4cc618384416f61adde5732dc0000000000695c4280018595980000000000160014839075e373684720629a85046859b5903dd22b5e040047304402201bde7c53efca4b7c9fbb30a31f0dd45c0581d542dd079af899217453c0feed4702205a9796300301fab6c5e7ee2f3127dd610cfcd4c8b0c97a27e27b001c97ae58b201483045022100df6e6660127f2cff1091c660a635bb3596097b24436a80e282d6ffbf89370e46022039ca0bce8b7b890d24fd70e246f4d87d75de49b360ff4874b36f2a1c22afba7501475221023f60881fb2837e6b3ef8baaee95cdebe12f775fdbf106e35ae54092afba47f5c21026d33825007f57b2a60af2127fa120ee2a13d9f5391eb43b918b618b8e3b80c5252ae8ab07920\",\n        \"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2\": \"020000000001019dbd55d238dd32954f636199e4558fc9483d7d0c1db18ec29d412a50da3b36940000000000fdffffff02840a0300000000001600148b428c50ec0b6f19d51ded15aa22fffa5c227be7400d030000000000160014bc543a42e0b8556a63f5d7b651c8f27287606e5f02473044022064773aecc89daa7279f5f5631c224fd75df5446ba8c32344ae7e24ee98a7117d02201f36dbbac9a5b76d55be27a8b3236391a103a3e61fc2b0efd546c570ff1f6781012103d0671a7e8bdef7d3cde1adf2320fbaa65f4dfd0456dc9ba3159df04f40948c895f701900\",\n        \"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d\": \"0200000000010102782afe5f2893a26f3f63b6b175990a5b53801d37f90e13834511e3456696e80000000000fdffffff028083010000000000160014751a431cd38ec2746a042008dd40c6ff7ab1b14004870100000000001976a91437b99663ca47bdd5e3b0be4fd1cc7c0faa79f58088ac02473044022000808b6ad3e3a3560478304038f5edac7a82066ab0597508c9852a7dbd576f66022035ecec51ad06f4760446b4a7ccdc574adaa7aa44ccfb820c95490ddd88d005320121026cee9d63a491119f300a505a640b07010cec8682086b447c5330148e5f3e7a7feaf71800\",\n        \"fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa\": \"02000000000101abbfac32862eaba435a0a5ad260a61e5fda1cb594c33a5ea523786f33ef6d7420100000000d279618001c9959800000000001600145297f0c582b75cd671bfc1123bb4fe07cfed501a040047304402202b533376d653e243ce38893454957413692dc0ad326aba704dba7524ff8cffdc022011d82213cfbb2677d445a54ad10bd9dabbc7c67baa7f736232864119695e6e7e01463043021f6a6e269cb49db20fe9cf787c7b038d4868f228e527b16b8a6efabd20664194022027cb81608e5b8c3af8ead51d2404c29482c639321e7f8705113273ff6983779801475221024b56076bcfb784b0f58edf280290ab20daaf9c079e40642b158d5d3fad77faa321025f074d4778e25d5f10e8d4b20715edb1f5fdbbcf314f47111026719cfec7ca6c52ae736df320\",\n        \"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877\": \"02000000000102abbfac32862eaba435a0a5ad260a61e5fda1cb594c33a5ea523786f33ef6d7420000000000feffffff551df8e2dfb3dbf8bb7326d6d1c647adcc51281133fd88978b25ee7111a751720000000000feffffff0264280a0000000000160014f7c3839fbd881cafc37b8b67dbc4fed63e60565e40420f0000000000220020c0accfb484c7ae9940d7efc0c7ba921a1e69cba406303b384c37de34866788ad0247304402201ff5533f8f6ed979b93e8e349c934c46af6a70d35bea800782c999a9337ea1aa02201ad823951ecd1b6a203a26d396bf1bce13151e86800a3a4026236cc1ebd63c2c012103d488a84c6c19a5acf3b36a45852ee4132cfedb1278fd55368344732d6747dedf0247304402201185cb9ffab376d55ed9ea64d1715da6eeb5e20348b67b8b35ed02340aa8e71602202f8c48e228236e27ce5faeb09aba2bb65195f7997ba603f137a068f76225dd62012103c115d4fa6d752e4629a05d0073fe2ed2cf88b46e1b680a6813e96d67f340cb32f40a1b00\"\n    },\n    \"tx_fees\": {},\n    \"txi\": {\n        \"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be\": {\n            \"tb1qhmerp6zrxw852kthwu7hq8tplmk26r6aklvcgw\": [\n                [\n                    \"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9:0\",\n                    10994822\n                ]\n            ],\n            \"tb1qq6zuqfwc97d3mqy46dva4vn8jvlkck63c3y0mp\": [\n                [\n                    \"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755:0\",\n                    10494822\n                ]\n            ]\n        },\n        \"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0\": {\n            \"tb1q0phzvy7039yyw93fa5te3p4ns9ftmv7xvv0fgj\": [\n                [\n                    \"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e:1\",\n                    1711000\n                ]\n            ]\n        },\n        \"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687\": {\n            \"tb1q955va7ngp2zzzrfwmn29575v6ksqfzrvvfd658\": [\n                [\n                    \"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd:1\",\n                    66445264\n                ]\n            ]\n        },\n        \"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04\": {\n            \"tb1qwqxjpfytaq08qteus5dhwf92u5kzfzyv45kyd4\": [\n                [\n                    \"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e:0\",\n                    3211400\n                ]\n            ]\n        },\n        \"22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f\": {\n            \"tb1qfrlx5pza9vmez6vpx7swt8yp0nmgz3qa7jjkuf\": [\n                [\n                    \"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38:0\",\n                    100000\n                ]\n            ]\n        },\n        \"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d\": {\n            \"tb1q97f8vmmcvcjgme0kstta62atpzp5z3t7z7vsa7\": [\n                [\n                    \"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7:1\",\n                    299600\n                ]\n            ],\n            \"tb1qt44lpapl38spldm0dtmsm6z300mw8qayy659zr\": [\n                [\n                    \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802:1\",\n                    500000\n                ]\n            ]\n        },\n        \"4133b7570dd1424ac0f05fba67d7af95eae19de82ff367007ac33742f13d1d8e\": {\n            \"tb1qkahwe0pkcnnm9fzwy3f5spwd9vv3cvdzk5dkkc\": [\n                [\n                    \"0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d:0\",\n                    16773292\n                ]\n            ]\n        },\n        \"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab\": {\n            \"tb1q3p7gwqhj2n27gny6zuxpf3ajqrqaqnfkl57vz0\": [\n                [\n                    \"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54:1\",\n                    999600\n                ]\n            ],\n            \"tb1qhezs0203uw8wyagjpjs5yv57xdmsta077qkazu\": [\n                [\n                    \"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b:0\",\n                    9672100\n                ]\n            ]\n        },\n        \"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15\": {\n            \"tb1qav362fjlwyvuraeqz5gmf0hrrvv9hp9jgv3ap9\": [\n                [\n                    \"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9:1\",\n                    50173100\n                ]\n            ]\n        },\n        \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3\": {\n            \"tb1qdekelvx2uh3dfwy9rxgmsu4dra0flkkf80t5z7\": [\n                [\n                    \"6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a:1\",\n                    250000\n                ],\n                [\n                    \"366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f:1\",\n                    250000\n                ]\n            ]\n        },\n        \"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e\": {\n            \"tb1q4eeeqvrylpkshxwa3whfza39vzyv3yc0flv9rj\": [\n                [\n                    \"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04:1\",\n                    2211200\n                ]\n            ]\n        },\n        \"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc\": {\n            \"tb1qltq9ex98gwm2aj5wnn4me7qnzrgdnp2hwq7pwn\": [\n                [\n                    \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4:0\",\n                    100000\n                ]\n            ]\n        },\n        \"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b\": {\n            \"tb1qd0q3cnqu0xsx7pmc4xqeqvphe2k5a4lhjs05h0\": [\n                [\n                    \"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c:1\",\n                    20172500\n                ]\n            ]\n        },\n        \"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54\": {\n            \"tb1qtk62c2ypvuz7e42y039tq7tczhsndxs84eqj8y\": [\n                [\n                    \"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135:1\",\n                    1499800\n                ]\n            ]\n        },\n        \"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38\": {\n            \"tb1q4arrqquh2ptjvak5eg5ld7mrt9ncq7lae7fw7t\": [\n                [\n                    \"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d:0\",\n                    299300\n                ]\n            ]\n        },\n        \"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b\": {\n            \"tb1q07efauuddxdf6hpfceqvpcwef5wpg8ja29evz3\": [\n                [\n                    \"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b:1\",\n                    19672300\n                ]\n            ]\n        },\n        \"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc\": {\n            \"tb1qjy0wuqaejah9l4h3hn505jlph9pn6p7mzjasnw\": [\n                [\n                    \"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67:0\",\n                    1262300\n                ]\n            ]\n        },\n        \"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506\": {\n            \"tb1q9d7jlkj9tvvhc6n7zmc02ndyh3n6vex0d8fts4\": [\n                [\n                    \"d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee:0\",\n                    149454\n                ]\n            ]\n        },\n        \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4\": {\n            \"tb1qc50swmqxgw3e9j890t8rp90397rg3j0djy9rz6\": [\n                [\n                    \"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04:0\",\n                    1000000\n                ]\n            ]\n        },\n        \"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3\": {\n            \"tb1q7lpc88aa3qw2lsmm3dnah3876clxq4j7apzgf3\": [\n                [\n                    \"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877:0\",\n                    665700\n                ]\n            ]\n        },\n        \"781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05\": {\n            \"tb1q49afhhhsg8fqkpjfdgelnvyq3fnaglzw74kda4\": [\n                [\n                    \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3:0\",\n                    200000\n                ]\n            ]\n        },\n        \"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d\": {\n            \"tb1q97kf6e7qfudh2zyy0vmp3cevtw3jteqa0qupts\": [\n                [\n                    \"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15:1\",\n                    40172900\n                ]\n            ]\n        },\n        \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\": {\n            \"tb1q0raz8xxcpznvaqpc0ecy5kpztck7z4ddkzr0qq\": [\n                [\n                    \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52:1\",\n                    100000\n                ]\n            ],\n            \"tb1q3dpgc58vpdh3n4gaa5265ghllfwzy7l8v786fl\": [\n                [\n                    \"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2:0\",\n                    199300\n                ]\n            ],\n            \"tb1q4qhgk2r4ngpnk5j0rq28f2cyhlguje8x92g99s\": [\n                [\n                    \"38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3:0\",\n                    16740777\n                ]\n            ],\n            \"tb1q4tu9pesq3yl38xc677lunm5ywaaykgnswxc0ev\": [\n                [\n                    \"d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156:0\",\n                    10000\n                ]\n            ],\n            \"tb1q6lzmxd6hr5y2utp5y5knmh8kefanet5pvgkphw\": [\n                [\n                    \"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867:1\",\n                    32890528\n                ]\n            ],\n            \"tb1q70z22hvlhhjn69xpv2jwkkxprf0pvzh2z5p24r\": [\n                [\n                    \"48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a:0\",\n                    99890\n                ]\n            ],\n            \"tb1qcs8sn834hc65nv0lypxf4zzh8yrp0vqw293vdl\": [\n                [\n                    \"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc:0\",\n                    99560\n                ]\n            ],\n            \"tb1qg26z824j42qrl9tssjpjkyp4n042y35sre6yya\": [\n                [\n                    \"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0:1\",\n                    1210800\n                ]\n            ],\n            \"tb1qgga8dl6z86cajdgtrmmdwvq9f2695e6epp064p\": [\n                [\n                    \"40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1:0\",\n                    499862\n                ]\n            ],\n            \"tb1qh32r5shqhp2k5cl467m9rj8jw2rkqmjl9g0tn7\": [\n                [\n                    \"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2:1\",\n                    200000\n                ]\n            ],\n            \"tb1qhzay07kvxkuerlel4e6dps33dtr3yxmnf34v9s\": [\n                [\n                    \"b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2:0\",\n                    499862\n                ]\n            ],\n            \"tb1qm3qwl94e7xcu2nxe8z0d3w2x0s0xwrpahm6ceq\": [\n                [\n                    \"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38:1\",\n                    199159\n                ]\n            ],\n            \"tb1qpea0mzjyztv4ctskscsu94sj248t85vmggsl6c\": [\n                [\n                    \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52:0\",\n                    99700\n                ]\n            ],\n            \"tb1qrkgr9yme0zedgemjpvrt852rq2qfz27s832yhr\": [\n                [\n                    \"69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1:0\",\n                    10000\n                ]\n            ],\n            \"tb1qw5dyx8xn3mp8g6syyqyd6sxxlaatrv2qvszwta\": [\n                [\n                    \"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d:0\",\n                    99200\n                ]\n            ],\n            \"tb1qyeg0h0fy8vw3mq0alvdffe0ax8dltalmjzse33\": [\n                [\n                    \"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6:1\",\n                    870\n                ]\n            ],\n            \"tb1qz2xgj9eahs855rudhd4xreatp99xp3jx5mjmh7\": [\n                [\n                    \"72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390:0\",\n                    99780\n                ]\n            ],\n            \"tb1qz9z4uw5tnh0yjpz4a4pfhv0wrpegfyv9yl2n7g\": [\n                [\n                    \"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4:1\",\n                    100000\n                ]\n            ]\n        },\n        \"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a\": {\n            \"tb1qjduurjclneffxv6tgv7rnspaxu85v7saf9mfj0\": [\n                [\n                    \"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82:1\",\n                    100000000\n                ]\n            ]\n        },\n        \"866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff\": {\n            \"tb1qw9jdeld07zf53jw85vh7pnv4xdep523v96p9gv\": [\n                [\n                    \"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514:1\",\n                    90133586\n                ]\n            ]\n        },\n        \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c\": {\n            \"tb1qeh090ruc3cs5hry90tev4fsvrnegulw8xssdzx\": [\n                [\n                    \"c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0:1\",\n                    1000000\n                ]\n            ]\n        },\n        \"901856f632b06573e2384972c38a2b87d7f058595f77cd2fcca47893bb6dc366\": {\n            \"tb1q22tlp3vzkawdvudlcyfrhd87ql8765q600hftd\": [\n                [\n                    \"fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa:0\",\n                    9999817\n                ]\n            ]\n        },\n        \"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5\": {\n            \"tb1qwzhmm9ajms63h5t87u2w999jl5akptkl4e5d7z\": [\n                [\n                    \"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5:1\",\n                    13999800\n                ]\n            ]\n        },\n        \"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d\": {\n            \"tb1ql2yks7mu0u95hpjgagly0uxlh98fs9qg00hkr5\": [\n                [\n                    \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4:1\",\n                    399800\n                ]\n            ],\n            \"tb1qrmex0u0vkefcmxr6fc2sxuvdxh67p99nsqnklw\": [\n                [\n                    \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4:2\",\n                    500000\n                ]\n            ]\n        },\n        \"94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9\": {\n            \"tb1qnlesczfxk2z7xgeyep3tr3xkh3z8rcmh4j95gt\": [\n                [\n                    \"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295:1\",\n                    220000\n                ]\n            ]\n        },\n        \"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514\": {\n            \"tb1q8k9sp22vjun7hf0sfvs2n8mfwt8xl43d68xml2\": [\n                [\n                    \"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a:0\",\n                    99998900\n                ]\n            ]\n        },\n        \"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd\": {\n            \"tb1qql6g008ymlcfmrkwg8lfl7tsgays6s427pjlt6\": [\n                [\n                    \"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271:1\",\n                    83222632\n                ]\n            ]\n        },\n        \"9e0789481c2f5b2fda96f0af534af3caa5cb5ddc2bbf27aea72c80154d9272d2\": {\n            \"tb1qswg8tcmndprjqc56s5zxskd4jq7ay267phaefp\": [\n                [\n                    \"ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21:0\",\n                    9999749\n                ]\n            ]\n        },\n        \"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e\": {\n            \"tb1qptq7mkutq0m6an0npf8t89dxvtecqp08uqphcn\": [\n                [\n                    \"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be:0\",\n                    4712097\n                ]\n            ],\n            \"tb1quw4g923ww4zs042cts9kmvrvcr95jfahqasfrg\": [\n                [\n                    \"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5:0\",\n                    3499600\n                ]\n            ]\n        },\n        \"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9\": {\n            \"tb1qaj6eud755xul5y70vy073rhx29qn26xw65nanw\": [\n                [\n                    \"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94:1\",\n                    52173277\n                ]\n            ]\n        },\n        \"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867\": {\n            \"tb1qsrgn2zg9lgyeva68tgjqv0urs830vcnsmajg0x\": [\n                [\n                    \"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687:1\",\n                    49667896\n                ]\n            ]\n        },\n        \"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6\": {\n            \"tb1q3dvf0y9tmf24k4y5d37ay0vacyaq5qva7lg50t\": [\n                [\n                    \"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61:1\",\n                    1000\n                ]\n            ]\n        },\n        \"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271\": {\n            \"tb1qwg8fgt97d7wm3jkzxmkznwe7ngxy08l89v0hxp\": [\n                [\n                    \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca:1\",\n                    100000000\n                ]\n            ]\n        },\n        \"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5\": {\n            \"tb1q9mgamdnm0jch3e73ykvlgymwg5nhs76t8jv4yg\": [\n                [\n                    \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c:2\",\n                    500000\n                ]\n            ]\n        },\n        \"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7\": {\n            \"tb1qczu7px50v092ztuhe7vxwcjs9p8mukg0gn9y28\": [\n                [\n                    \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c:1\",\n                    499800\n                ]\n            ]\n        },\n        \"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94\": {\n            \"tb1qwzxfucd24m4j4y6nzasnucrx2dty4ht2h0lud0\": [\n                [\n                    \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067:0\",\n                    52273088\n                ]\n            ]\n        },\n        \"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295\": {\n            \"tb1q309xc56t5r928v093pu3h4x99ffa5xmwcgav8r\": [\n                [\n                    \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3:1\",\n                    299700\n                ]\n            ]\n        },\n        \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52\": {\n            \"tb1qa4gwte9kr0tsndl5q69k6q3yte5uh7senrm7fc\": [\n                [\n                    \"43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35:1\",\n                    100000\n                ]\n            ],\n            \"tb1qftpp8e7t3mk7c48sw4mgwqn24yhuzl5t9u4fzd\": [\n                [\n                    \"3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549:0\",\n                    100000\n                ]\n            ]\n        },\n        \"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057\": {\n            \"tb1qayg9tz462wythfdxw6gxpapwdp5y8ugth7fx43\": [\n                [\n                    \"85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834:0\",\n                    100000\n                ]\n            ],\n            \"tb1qyf62fc39qsmnxxv873meuu9au6p3cag9slgh9p\": [\n                [\n                    \"65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc:0\",\n                    100000\n                ]\n            ]\n        },\n        \"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67\": {\n            \"tb1qk7u2mcu02v7fgvls9ttuwq49a6e5kae5kxkts9\": [\n                [\n                    \"0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0:0\",\n                    4273050\n                ]\n            ],\n            \"tb1qm0z2hh76fngnp3zl3yglvlm6nm98qz4exupta9\": [\n                [\n                    \"18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf:0\",\n                    1989558\n                ]\n            ]\n        },\n        \"d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3\": {\n            \"tb1q767gch8ucagh23h40frfm8x6jmc37qvxpn8x2f\": [\n                [\n                    \"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3:1\",\n                    465500\n                ]\n            ]\n        },\n        \"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5\": {\n            \"tb1q2hr4vf8jkga66m82gg9zmxwszdjuw5450zclv0\": [\n                [\n                    \"133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62:2\",\n                    25000000\n                ]\n            ]\n        },\n        \"dc16e73a88aff40d703f8eba148eae7265d1128a013f6f570a8bd76d86a5e137\": {\n            \"tb1q49g7md82fy3yrhpf6r4mdnyht3hut2zhahen7h\": [\n                [\n                    \"56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b:0\",\n                    16775418\n                ]\n            ]\n        },\n        \"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c\": {\n            \"tb1quc085vmkgkpdr5wpqvgt6dyw35s5hqrncml8sh\": [\n                [\n                    \"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d:1\",\n                    30172700\n                ]\n            ]\n        },\n        \"e20bf91006e084c63c424240e857af8c3e27a5d8356af4dbe5ddc8ad4e71c336\": {\n            \"tb1qsmk2jc6fzr0e9xkf7w9l3ha8s0txha3vruffrp\": [\n                [\n                    \"0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761:0\",\n                    9999817\n                ]\n            ]\n        },\n        \"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135\": {\n            \"tb1qy6uuespwqm9m9wdjvmwr07l9fvn0ge93mzskzw\": [\n                [\n                    \"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9:0\",\n                    2000000\n                ]\n            ]\n        },\n        \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\": {\n            \"tb1q0tkgjg5f3wnquswmtpah2fsmxp0vl9rarvgluv\": [\n                [\n                    \"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5:1\",\n                    300000\n                ]\n            ],\n            \"tb1q6yjsyw749hjg4wqa2navhdaj2wxpqtkztzrh8c\": [\n                [\n                    \"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7:0\",\n                    200000\n                ]\n            ],\n            \"tb1q730gzvu52y6t07465flt6ae8eny2mnsh7drhw4\": [\n                [\n                    \"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5:0\",\n                    199800\n                ]\n            ]\n        },\n        \"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2\": {\n            \"tb1q8afxv7tzczj99lwf4et6le4k2u0tytqgt6g44w\": [\n                [\n                    \"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d:0\",\n                    399500\n                ]\n            ]\n        },\n        \"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d\": {\n            \"tb1qldxhwr6y2mfckhjf832sfepn2sd28jvqykgyfe\": [\n                [\n                    \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802:0\",\n                    199500\n                ]\n            ]\n        },\n        \"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877\": {\n            \"tb1q92pxe3a3zyddfz5k74csaqt2vzc5sl37fgm5wn\": [\n                [\n                    \"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab:0\",\n                    671400\n                ]\n            ],\n            \"tb1qmy8uqjkh2d2dcgnz6yyrtjk05n5y4ey8qzayyu\": [\n                [\n                    \"7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55:0\",\n                    994558\n                ]\n            ]\n        }\n    },\n    \"txo\": {\n        \"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61\": {\n            \"tb1q3dvf0y9tmf24k4y5d37ay0vacyaq5qva7lg50t\": [\n                [\n                    1,\n                    1000,\n                    false\n                ]\n            ]\n        },\n        \"0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761\": {\n            \"tb1qsmk2jc6fzr0e9xkf7w9l3ha8s0txha3vruffrp\": [\n                [\n                    0,\n                    9999817,\n                    false\n                ]\n            ]\n        },\n        \"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be\": {\n            \"tb1qptq7mkutq0m6an0npf8t89dxvtecqp08uqphcn\": [\n                [\n                    0,\n                    4712097,\n                    false\n                ]\n            ]\n        },\n        \"0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0\": {\n            \"tb1qk7u2mcu02v7fgvls9ttuwq49a6e5kae5kxkts9\": [\n                [\n                    0,\n                    4273050,\n                    false\n                ]\n            ]\n        },\n        \"0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d\": {\n            \"tb1qkahwe0pkcnnm9fzwy3f5spwd9vv3cvdzk5dkkc\": [\n                [\n                    0,\n                    16773292,\n                    false\n                ]\n            ]\n        },\n        \"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0\": {\n            \"tb1qg26z824j42qrl9tssjpjkyp4n042y35sre6yya\": [\n                [\n                    1,\n                    1210800,\n                    false\n                ]\n            ]\n        },\n        \"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687\": {\n            \"tb1qsrgn2zg9lgyeva68tgjqv0urs830vcnsmajg0x\": [\n                [\n                    1,\n                    49667896,\n                    false\n                ]\n            ]\n        },\n        \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\": {\n            \"tb1qwg8fgt97d7wm3jkzxmkznwe7ngxy08l89v0hxp\": [\n                [\n                    1,\n                    100000000,\n                    false\n                ]\n            ]\n        },\n        \"133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62\": {\n            \"tb1q2hr4vf8jkga66m82gg9zmxwszdjuw5450zclv0\": [\n                [\n                    2,\n                    25000000,\n                    false\n                ]\n            ]\n        },\n        \"18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf\": {\n            \"tb1qm0z2hh76fngnp3zl3yglvlm6nm98qz4exupta9\": [\n                [\n                    0,\n                    1989558,\n                    false\n                ]\n            ]\n        },\n        \"19a666acc1bb5656e49a1d99087c5a0c2a135c40f1a58e5306f2d85f46786801\": {\n            \"tb1qsyhawdg9zj2cepa0zg096rna2nxg4zj0c0fnvq\": [\n                [\n                    0,\n                    99058,\n                    false\n                ]\n            ]\n        },\n        \"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04\": {\n            \"tb1q4eeeqvrylpkshxwa3whfza39vzyv3yc0flv9rj\": [\n                [\n                    1,\n                    2211200,\n                    false\n                ]\n            ],\n            \"tb1qc50swmqxgw3e9j890t8rp90397rg3j0djy9rz6\": [\n                [\n                    0,\n                    1000000,\n                    false\n                ]\n            ]\n        },\n        \"26b1fb057113f6ce39a20f5baa493015b152cc1c0af312b3ee8950e9a4bbf47a\": {\n            \"tb1q9jtcype5swm4reyz4sktvq609shw88fwzjz9jg\": [\n                [\n                    0,\n                    100000,\n                    false\n                ]\n            ]\n        },\n        \"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d\": {\n            \"tb1q4arrqquh2ptjvak5eg5ld7mrt9ncq7lae7fw7t\": [\n                [\n                    0,\n                    299300,\n                    false\n                ]\n            ]\n        },\n        \"3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549\": {\n            \"tb1qftpp8e7t3mk7c48sw4mgwqn24yhuzl5t9u4fzd\": [\n                [\n                    0,\n                    100000,\n                    false\n                ]\n            ]\n        },\n        \"366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f\": {\n            \"tb1qdekelvx2uh3dfwy9rxgmsu4dra0flkkf80t5z7\": [\n                [\n                    1,\n                    250000,\n                    false\n                ]\n            ]\n        },\n        \"38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3\": {\n            \"tb1q4qhgk2r4ngpnk5j0rq28f2cyhlguje8x92g99s\": [\n                [\n                    0,\n                    16740777,\n                    false\n                ]\n            ]\n        },\n        \"40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1\": {\n            \"tb1qgga8dl6z86cajdgtrmmdwvq9f2695e6epp064p\": [\n                [\n                    0,\n                    499862,\n                    false\n                ]\n            ]\n        },\n        \"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab\": {\n            \"tb1q92pxe3a3zyddfz5k74csaqt2vzc5sl37fgm5wn\": [\n                [\n                    0,\n                    671400,\n                    false\n                ]\n            ]\n        },\n        \"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15\": {\n            \"tb1q97kf6e7qfudh2zyy0vmp3cevtw3jteqa0qupts\": [\n                [\n                    1,\n                    40172900,\n                    false\n                ]\n            ]\n        },\n        \"43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35\": {\n            \"tb1qa4gwte9kr0tsndl5q69k6q3yte5uh7senrm7fc\": [\n                [\n                    1,\n                    100000,\n                    false\n                ]\n            ]\n        },\n        \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3\": {\n            \"tb1q309xc56t5r928v093pu3h4x99ffa5xmwcgav8r\": [\n                [\n                    1,\n                    299700,\n                    false\n                ]\n            ],\n            \"tb1q49afhhhsg8fqkpjfdgelnvyq3fnaglzw74kda4\": [\n                [\n                    0,\n                    200000,\n                    false\n                ]\n            ]\n        },\n        \"48a4cd850234b2316395959c343a3cbfd57e706e256399ac39927330f03268d6\": {\n            \"tb1q8564fhyum66n239wt0gp8m0khlqgwgac8ft2r0\": [\n                [\n                    0,\n                    5000,\n                    false\n                ]\n            ]\n        },\n        \"48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a\": {\n            \"tb1q70z22hvlhhjn69xpv2jwkkxprf0pvzh2z5p24r\": [\n                [\n                    0,\n                    99890,\n                    false\n                ]\n            ]\n        },\n        \"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e\": {\n            \"tb1q0phzvy7039yyw93fa5te3p4ns9ftmv7xvv0fgj\": [\n                [\n                    1,\n                    1711000,\n                    false\n                ]\n            ]\n        },\n        \"4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e\": {\n            \"tb1qhksthm48t4eqrzup3gzzlqnf433z8aq5uj03jr\": [\n                [\n                    1,\n                    100000,\n                    false\n                ]\n            ]\n        },\n        \"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755\": {\n            \"tb1qq6zuqfwc97d3mqy46dva4vn8jvlkck63c3y0mp\": [\n                [\n                    0,\n                    10494822,\n                    false\n                ]\n            ]\n        },\n        \"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc\": {\n            \"tb1qcs8sn834hc65nv0lypxf4zzh8yrp0vqw293vdl\": [\n                [\n                    0,\n                    99560,\n                    false\n                ]\n            ]\n        },\n        \"56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b\": {\n            \"tb1q49g7md82fy3yrhpf6r4mdnyht3hut2zhahen7h\": [\n                [\n                    0,\n                    16775418,\n                    false\n                ]\n            ]\n        },\n        \"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b\": {\n            \"tb1q07efauuddxdf6hpfceqvpcwef5wpg8ja29evz3\": [\n                [\n                    1,\n                    19672300,\n                    false\n                ]\n            ]\n        },\n        \"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54\": {\n            \"tb1q3p7gwqhj2n27gny6zuxpf3ajqrqaqnfkl57vz0\": [\n                [\n                    1,\n                    999600,\n                    false\n                ]\n            ]\n        },\n        \"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38\": {\n            \"tb1qfrlx5pza9vmez6vpx7swt8yp0nmgz3qa7jjkuf\": [\n                [\n                    0,\n                    100000,\n                    false\n                ]\n            ],\n            \"tb1qm3qwl94e7xcu2nxe8z0d3w2x0s0xwrpahm6ceq\": [\n                [\n                    1,\n                    199159,\n                    false\n                ]\n            ]\n        },\n        \"65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc\": {\n            \"tb1qyf62fc39qsmnxxv873meuu9au6p3cag9slgh9p\": [\n                [\n                    0,\n                    100000,\n                    false\n                ]\n            ]\n        },\n        \"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4\": {\n            \"tb1qz9z4uw5tnh0yjpz4a4pfhv0wrpegfyv9yl2n7g\": [\n                [\n                    1,\n                    100000,\n                    false\n                ]\n            ]\n        },\n        \"69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1\": {\n            \"tb1qrkgr9yme0zedgemjpvrt852rq2qfz27s832yhr\": [\n                [\n                    0,\n                    10000,\n                    false\n                ]\n            ]\n        },\n        \"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b\": {\n            \"tb1qhezs0203uw8wyagjpjs5yv57xdmsta077qkazu\": [\n                [\n                    0,\n                    9672100,\n                    false\n                ]\n            ]\n        },\n        \"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc\": {\n            \"tb1qp3p2d72gj2l7r6za056tgu4ezsurjphper4swh\": [\n                [\n                    1,\n                    762100,\n                    false\n                ]\n            ]\n        },\n        \"6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a\": {\n            \"tb1qdekelvx2uh3dfwy9rxgmsu4dra0flkkf80t5z7\": [\n                [\n                    1,\n                    250000,\n                    false\n                ]\n            ]\n        },\n        \"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506\": {\n            \"tb1qa6dgfxczcjshyhv6d4ck0qvs3mgdjd2gpdqqzj\": [\n                [\n                    1,\n                    100000,\n                    false\n                ]\n            ],\n            \"tb1qwu3708q32l7wdcvfhf9vfhgazp8yzggf5x4y72\": [\n                [\n                    0,\n                    49300,\n                    false\n                ]\n            ]\n        },\n        \"7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55\": {\n            \"tb1qmy8uqjkh2d2dcgnz6yyrtjk05n5y4ey8qzayyu\": [\n                [\n                    0,\n                    994558,\n                    false\n                ]\n            ]\n        },\n        \"72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390\": {\n            \"tb1qz2xgj9eahs855rudhd4xreatp99xp3jx5mjmh7\": [\n                [\n                    0,\n                    99780,\n                    false\n                ]\n            ]\n        },\n        \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4\": {\n            \"tb1ql2yks7mu0u95hpjgagly0uxlh98fs9qg00hkr5\": [\n                [\n                    1,\n                    399800,\n                    false\n                ]\n            ],\n            \"tb1qltq9ex98gwm2aj5wnn4me7qnzrgdnp2hwq7pwn\": [\n                [\n                    0,\n                    100000,\n                    false\n                ]\n            ],\n            \"tb1qrmex0u0vkefcmxr6fc2sxuvdxh67p99nsqnklw\": [\n                [\n                    2,\n                    500000,\n                    false\n                ]\n            ]\n        },\n        \"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3\": {\n            \"tb1q767gch8ucagh23h40frfm8x6jmc37qvxpn8x2f\": [\n                [\n                    1,\n                    465500,\n                    false\n                ]\n            ]\n        },\n        \"781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05\": {\n            \"tb1qgg2avhyk30s8a0n72t8sm3cggdmqgdutdvwfa8\": [\n                [\n                    0,\n                    99800,\n                    false\n                ]\n            ]\n        },\n        \"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d\": {\n            \"tb1quc085vmkgkpdr5wpqvgt6dyw35s5hqrncml8sh\": [\n                [\n                    1,\n                    30172700,\n                    false\n                ]\n            ]\n        },\n        \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\": {\n            \"tb1qwzxfucd24m4j4y6nzasnucrx2dty4ht2h0lud0\": [\n                [\n                    0,\n                    52273088,\n                    false\n                ]\n            ]\n        },\n        \"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a\": {\n            \"tb1q8k9sp22vjun7hf0sfvs2n8mfwt8xl43d68xml2\": [\n                [\n                    0,\n                    99998900,\n                    false\n                ]\n            ]\n        },\n        \"85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834\": {\n            \"tb1qayg9tz462wythfdxw6gxpapwdp5y8ugth7fx43\": [\n                [\n                    0,\n                    100000,\n                    false\n                ]\n            ]\n        },\n        \"866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff\": {\n            \"tb1q04m5vxgzsctgn8kgyfxcen3pqxdr2yx53vzwzl\": [\n                [\n                    1,\n                    73356218,\n                    false\n                ]\n            ]\n        },\n        \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c\": {\n            \"tb1q9mgamdnm0jch3e73ykvlgymwg5nhs76t8jv4yg\": [\n                [\n                    2,\n                    500000,\n                    false\n                ]\n            ],\n            \"tb1qczu7px50v092ztuhe7vxwcjs9p8mukg0gn9y28\": [\n                [\n                    1,\n                    499800,\n                    false\n                ]\n            ]\n        },\n        \"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5\": {\n            \"tb1quw4g923ww4zs042cts9kmvrvcr95jfahqasfrg\": [\n                [\n                    0,\n                    3499600,\n                    false\n                ]\n            ]\n        },\n        \"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d\": {\n            \"tb1q8afxv7tzczj99lwf4et6le4k2u0tytqgt6g44w\": [\n                [\n                    0,\n                    399500,\n                    false\n                ]\n            ]\n        },\n        \"94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9\": {\n            \"tb1qcmmu23wur97duygz524t07s40gdxzgc4kfpkp5\": [\n                [\n                    1,\n                    119700,\n                    false\n                ]\n            ]\n        },\n        \"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514\": {\n            \"tb1qw9jdeld07zf53jw85vh7pnv4xdep523v96p9gv\": [\n                [\n                    1,\n                    90133586,\n                    false\n                ]\n            ]\n        },\n        \"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd\": {\n            \"tb1q955va7ngp2zzzrfwmn29575v6ksqfzrvvfd658\": [\n                [\n                    1,\n                    66445264,\n                    false\n                ]\n            ]\n        },\n        \"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e\": {\n            \"tb1qwqxjpfytaq08qteus5dhwf92u5kzfzyv45kyd4\": [\n                [\n                    0,\n                    3211400,\n                    false\n                ]\n            ]\n        },\n        \"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9\": {\n            \"tb1qav362fjlwyvuraeqz5gmf0hrrvv9hp9jgv3ap9\": [\n                [\n                    1,\n                    50173100,\n                    false\n                ]\n            ],\n            \"tb1qy6uuespwqm9m9wdjvmwr07l9fvn0ge93mzskzw\": [\n                [\n                    0,\n                    2000000,\n                    false\n                ]\n            ]\n        },\n        \"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867\": {\n            \"tb1q6lzmxd6hr5y2utp5y5knmh8kefanet5pvgkphw\": [\n                [\n                    1,\n                    32890528,\n                    false\n                ]\n            ]\n        },\n        \"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6\": {\n            \"tb1qyeg0h0fy8vw3mq0alvdffe0ax8dltalmjzse33\": [\n                [\n                    1,\n                    870,\n                    false\n                ]\n            ]\n        },\n        \"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271\": {\n            \"tb1qql6g008ymlcfmrkwg8lfl7tsgays6s427pjlt6\": [\n                [\n                    1,\n                    83222632,\n                    false\n                ]\n            ]\n        },\n        \"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5\": {\n            \"tb1q0tkgjg5f3wnquswmtpah2fsmxp0vl9rarvgluv\": [\n                [\n                    1,\n                    300000,\n                    false\n                ]\n            ],\n            \"tb1q730gzvu52y6t07465flt6ae8eny2mnsh7drhw4\": [\n                [\n                    0,\n                    199800,\n                    false\n                ]\n            ]\n        },\n        \"b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2\": {\n            \"tb1qhzay07kvxkuerlel4e6dps33dtr3yxmnf34v9s\": [\n                [\n                    0,\n                    499862,\n                    false\n                ]\n            ]\n        },\n        \"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7\": {\n            \"tb1q6yjsyw749hjg4wqa2navhdaj2wxpqtkztzrh8c\": [\n                [\n                    0,\n                    200000,\n                    false\n                ]\n            ],\n            \"tb1q97f8vmmcvcjgme0kstta62atpzp5z3t7z7vsa7\": [\n                [\n                    1,\n                    299600,\n                    false\n                ]\n            ]\n        },\n        \"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94\": {\n            \"tb1q27juqmgmq2749wylmyqk00lvx9mgaz4k5nfnud\": [\n                [\n                    0,\n                    1111,\n                    false\n                ]\n            ],\n            \"tb1qaj6eud755xul5y70vy073rhx29qn26xw65nanw\": [\n                [\n                    1,\n                    52173277,\n                    false\n                ]\n            ]\n        },\n        \"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9\": {\n            \"tb1qhmerp6zrxw852kthwu7hq8tplmk26r6aklvcgw\": [\n                [\n                    0,\n                    10994822,\n                    false\n                ]\n            ]\n        },\n        \"c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0\": {\n            \"tb1qeh090ruc3cs5hry90tev4fsvrnegulw8xssdzx\": [\n                [\n                    1,\n                    1000000,\n                    false\n                ]\n            ]\n        },\n        \"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295\": {\n            \"tb1qevkywexfy5gnydx0mrsrrthzncymydc0zz4rqx\": [\n                [\n                    0,\n                    79500,\n                    false\n                ]\n            ],\n            \"tb1qnlesczfxk2z7xgeyep3tr3xkh3z8rcmh4j95gt\": [\n                [\n                    1,\n                    220000,\n                    false\n                ]\n            ]\n        },\n        \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52\": {\n            \"tb1q0raz8xxcpznvaqpc0ecy5kpztck7z4ddkzr0qq\": [\n                [\n                    1,\n                    100000,\n                    false\n                ]\n            ],\n            \"tb1qpea0mzjyztv4ctskscsu94sj248t85vmggsl6c\": [\n                [\n                    0,\n                    99700,\n                    false\n                ]\n            ]\n        },\n        \"d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee\": {\n            \"tb1q9d7jlkj9tvvhc6n7zmc02ndyh3n6vex0d8fts4\": [\n                [\n                    0,\n                    149454,\n                    false\n                ]\n            ]\n        },\n        \"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057\": {\n            \"tb1qxx7t6g3dpts4ytlzetdqv8e04qdal36xg9d7zc\": [\n                [\n                    0,\n                    99600,\n                    false\n                ]\n            ]\n        },\n        \"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82\": {\n            \"tb1qjduurjclneffxv6tgv7rnspaxu85v7saf9mfj0\": [\n                [\n                    1,\n                    100000000,\n                    false\n                ]\n            ]\n        },\n        \"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67\": {\n            \"tb1qjy0wuqaejah9l4h3hn505jlph9pn6p7mzjasnw\": [\n                [\n                    0,\n                    1262300,\n                    false\n                ]\n            ]\n        },\n        \"d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156\": {\n            \"tb1q4tu9pesq3yl38xc677lunm5ywaaykgnswxc0ev\": [\n                [\n                    0,\n                    10000,\n                    false\n                ]\n            ]\n        },\n        \"d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3\": {\n            \"tb1qy5xx4uyqv6yhq9eptha8n5shqj94vqw7euftmk\": [\n                [\n                    0,\n                    165300,\n                    false\n                ]\n            ]\n        },\n        \"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5\": {\n            \"tb1qwzhmm9ajms63h5t87u2w999jl5akptkl4e5d7z\": [\n                [\n                    1,\n                    13999800,\n                    false\n                ]\n            ]\n        },\n        \"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c\": {\n            \"tb1qd0q3cnqu0xsx7pmc4xqeqvphe2k5a4lhjs05h0\": [\n                [\n                    1,\n                    20172500,\n                    false\n                ]\n            ]\n        },\n        \"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135\": {\n            \"tb1qtk62c2ypvuz7e42y039tq7tczhsndxs84eqj8y\": [\n                [\n                    1,\n                    1499800,\n                    false\n                ]\n            ]\n        },\n        \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\": {\n            \"tb1qldxhwr6y2mfckhjf832sfepn2sd28jvqykgyfe\": [\n                [\n                    0,\n                    199500,\n                    false\n                ]\n            ],\n            \"tb1qt44lpapl38spldm0dtmsm6z300mw8qayy659zr\": [\n                [\n                    1,\n                    500000,\n                    false\n                ]\n            ]\n        },\n        \"ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21\": {\n            \"tb1qswg8tcmndprjqc56s5zxskd4jq7ay267phaefp\": [\n                [\n                    0,\n                    9999749,\n                    false\n                ]\n            ]\n        },\n        \"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2\": {\n            \"tb1q3dpgc58vpdh3n4gaa5265ghllfwzy7l8v786fl\": [\n                [\n                    0,\n                    199300,\n                    false\n                ]\n            ],\n            \"tb1qh32r5shqhp2k5cl467m9rj8jw2rkqmjl9g0tn7\": [\n                [\n                    1,\n                    200000,\n                    false\n                ]\n            ]\n        },\n        \"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d\": {\n            \"tb1qw5dyx8xn3mp8g6syyqyd6sxxlaatrv2qvszwta\": [\n                [\n                    0,\n                    99200,\n                    false\n                ]\n            ]\n        },\n        \"fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa\": {\n            \"tb1q22tlp3vzkawdvudlcyfrhd87ql8765q600hftd\": [\n                [\n                    0,\n                    9999817,\n                    false\n                ]\n            ]\n        },\n        \"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877\": {\n            \"tb1q7lpc88aa3qw2lsmm3dnah3876clxq4j7apzgf3\": [\n                [\n                    0,\n                    665700,\n                    false\n                ]\n            ]\n        }\n    },\n    \"use_encryption\": true,\n    \"verified_tx3\": {\n        \"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61\": [\n            1413374,\n            1536860419,\n            1141,\n            \"000000007d7349e92f81e9e8ffe1a46eecdd3a88a4a1228de227e14faeef69a1\"\n        ],\n        \"0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761\": [\n            1772346,\n            1592576908,\n            59,\n            \"0000000000007b7b8dac9a0c3863164bc968a6ed59e5b30b81f4c4bf3c23b49e\"\n        ],\n        \"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be\": [\n            1609187,\n            1574268754,\n            55,\n            \"0000000000009e7b077d1b0e51098bf744bbe94ad1c6593e45c34efd97bc425a\"\n        ],\n        \"0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0\": [\n            1772375,\n            1592591253,\n            21,\n            \"0000000000000167e02b208939cf45855de05b8c3ac1d355b240798be1386ba1\"\n        ],\n        \"0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d\": [\n            1666105,\n            1581963801,\n            275,\n            \"0000000000136445a3ea6ebb32ab280d744eaaff37da4ddc6ceb80496f422438\"\n        ],\n        \"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0\": [\n            1665687,\n            1581525379,\n            234,\n            \"00000000001b6c273648dabe0b50b9d5df20a9377a4e685df7c3fef5889fe89e\"\n        ],\n        \"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687\": [\n            1612005,\n            1575945038,\n            152,\n            \"00000000d6f0ea275bd4e4c678c76e3b3089a2426f85fc433c63bd41b7f56cd8\"\n        ],\n        \"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca\": [\n            1609182,\n            1574265630,\n            31,\n            \"00000000000001eda6b8ae2f48779ebef52e72fe13431a404b9f7bcb60f6b670\"\n        ],\n        \"133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62\": [\n            1607022,\n            1573058015,\n            15,\n            \"00000000000001dcecfa9ea4a8637479884a5ba7b3aef561d1634376877b1d49\"\n        ],\n        \"18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf\": [\n            1772648,\n            1592710653,\n            68,\n            \"00000000000000fd9f5f67fb99e41d09f272b03798ef18c4db8b6db49430a313\"\n        ],\n        \"19a666acc1bb5656e49a1d99087c5a0c2a135c40f1a58e5306f2d85f46786801\": [\n            1772373,\n            1592590852,\n            11,\n            \"00000000000001265ba5ac89f4321a5abd421f80dcf57b7f665a4593d3bd7f99\"\n        ],\n        \"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04\": [\n            1638861,\n            1578439053,\n            39,\n            \"0000000000000172c2b2a077ccda3b5c7e63e0574a557d3216630e6488b6fd2f\"\n        ],\n        \"22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f\": [\n            1692449,\n            1585622204,\n            20,\n            \"00000000000005a7bab64ba095994085cf29576d433277b27b07ae8e91dd81f7\"\n        ],\n        \"26b1fb057113f6ce39a20f5baa493015b152cc1c0af312b3ee8950e9a4bbf47a\": [\n            1774145,\n            1593267834,\n            30,\n            \"00000000000000427fe2218fdcd0bb7e6b21be6c0a729a42ca5aaa61ca59c163\"\n        ],\n        \"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d\": [\n            1665679,\n            1581516561,\n            96,\n            \"000000001b9a933341a6414ba96c9a2bde2778e20b2ef3a8139953a6709d869a\"\n        ],\n        \"3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549\": [\n            1606858,\n            1572981006,\n            74,\n            \"00000000a58936b6caab26c619fe393ff581e4751942c9e32060ff99f3e95846\"\n        ],\n        \"366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f\": [\n            1746833,\n            1590770265,\n            47,\n            \"000000009124abefd02c781857eaa680da3232c076e546e787cf568daec28751\"\n        ],\n        \"38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3\": [\n            1666768,\n            1582726987,\n            20,\n            \"0000000000172e1a5d7dbb64ae5d887aa9f9a517e385b91964da899c1ac3df10\"\n        ],\n        \"40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1\": [\n            1665686,\n            1581524171,\n            191,\n            \"000000000000090571d7b168808d0999972f4e839709e18a7a60273242f15185\"\n        ],\n        \"4133b7570dd1424ac0f05fba67d7af95eae19de82ff367007ac33742f13d1d8e\": [\n            1666106,\n            1581965019,\n            272,\n            \"0000000000009176617a2d62fcab4ae45ed25bc745f36a73cf554637e50d73b3\"\n        ],\n        \"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab\": [\n            1772251,\n            1592534504,\n            34,\n            \"000000000000012e0e87b7c6185f0b1123c9ef0225c1602efe9b7615d4b76cb6\"\n        ],\n        \"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15\": [\n            1746271,\n            1590159393,\n            57,\n            \"000000000000048c1958cb2946020e4c90998be2b29e3f045a2e421e78732b3a\"\n        ],\n        \"43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35\": [\n            1607959,\n            1573496148,\n            4,\n            \"000000000000017945c56111ba49057d5373e9a2e4c893b302a60134763dbd2c\"\n        ],\n        \"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3\": [\n            1747567,\n            1591210595,\n            5,\n            \"0000000000000324e02615bd6c65e356d64d4d0d1155175eed5b89d86d6de064\"\n        ],\n        \"48a4cd850234b2316395959c343a3cbfd57e706e256399ac39927330f03268d6\": [\n            1747720,\n            1591293898,\n            64,\n            \"00000000fb526ae2018b173272874512c78add000cf067affbbccbc66dfae71b\"\n        ],\n        \"48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a\": [\n            1666551,\n            1582490047,\n            393,\n            \"000000000000250d0f7d2e82e138fe0e365a7611ecccabea6cb23e66154e9622\"\n        ],\n        \"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e\": [\n            1665679,\n            1581516561,\n            216,\n            \"000000001b9a933341a6414ba96c9a2bde2778e20b2ef3a8139953a6709d869a\"\n        ],\n        \"4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e\": [\n            1747720,\n            1591293898,\n            62,\n            \"00000000fb526ae2018b173272874512c78add000cf067affbbccbc66dfae71b\"\n        ],\n        \"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755\": [\n            1607028,\n            1573062661,\n            84,\n            \"000000000b69680b3a72b998dfbe15b9f2e4d1d9ec55514c793df17ea52757ea\"\n        ],\n        \"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc\": [\n            1721735,\n            1587735242,\n            78,\n            \"000000007226bf7af17b4d4ec148796fd3f958498a1af61170450fa5dda5ada8\"\n        ],\n        \"56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b\": [\n            1612072,\n            1575992282,\n            151,\n            \"00000000001638164f3c334289bad17de9a9cb79dcc004b4897b64a69a5372d6\"\n        ],\n        \"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b\": [\n            1746825,\n            1590760652,\n            58,\n            \"0000000039adfa26118ca702d16bd9ad54ebc22a80856778b5909673647ce9db\"\n        ],\n        \"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54\": [\n            1747720,\n            1591293898,\n            63,\n            \"00000000fb526ae2018b173272874512c78add000cf067affbbccbc66dfae71b\"\n        ],\n        \"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38\": [\n            1665815,\n            1581628321,\n            458,\n            \"000000000004437da930fc00f9576e5762d92036c1b682d3d9b6f1695e9038b3\"\n        ],\n        \"65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc\": [\n            1746833,\n            1590770265,\n            48,\n            \"000000009124abefd02c781857eaa680da3232c076e546e787cf568daec28751\"\n        ],\n        \"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4\": [\n            1638866,\n            1578441818,\n            95,\n            \"00000000ae9551c6b0e4ba47dd13695c9335e5d02b4f2e382220bfb59a0322d3\"\n        ],\n        \"69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1\": [\n            1413150,\n            1536757748,\n            470,\n            \"00000000000000214096fdc98df2896f0305325d07aa2bb21f3a86bddfd49681\"\n        ],\n        \"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b\": [\n            1772251,\n            1592534504,\n            33,\n            \"000000000000012e0e87b7c6185f0b1123c9ef0225c1602efe9b7615d4b76cb6\"\n        ],\n        \"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc\": [\n            1774910,\n            1593691025,\n            44,\n            \"0000000000000070f822bcfbcc2e01a3972e82d8c040b0df210dc29fe29de264\"\n        ],\n        \"6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a\": [\n            1746834,\n            1590771466,\n            46,\n            \"00000000d4cee21f4cc91d30467bf8b00415e2f6965b26305546cee9e575f9a4\"\n        ],\n        \"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506\": [\n            1747567,\n            1591210595,\n            8,\n            \"0000000000000324e02615bd6c65e356d64d4d0d1155175eed5b89d86d6de064\"\n        ],\n        \"7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55\": [\n            1772374,\n            1592590926,\n            6,\n            \"000000000000016ae199e46406b0de734fa90af98c0006399ee1f42cd907cec7\"\n        ],\n        \"72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390\": [\n            1692476,\n            1585625983,\n            27,\n            \"000000000000048eb935349ddde604b91cb4b6d441900ebed6c2e85594a57b79\"\n        ],\n        \"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4\": [\n            1663206,\n            1579613987,\n            27,\n            \"00000000000001402754827057f3ca9c064191971d1e7529cce4c525b603f9d6\"\n        ],\n        \"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3\": [\n            1774146,\n            1593269035,\n            11,\n            \"00000000cd1509e282e3894d8318236bbd1079c789ef7f36d4bb0540a43e5ed3\"\n        ],\n        \"781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05\": [\n            1774902,\n            1593687024,\n            59,\n            \"00000000000000eb1417d08ad9fea42729f18b05161630ee35179a5acebb2b05\"\n        ],\n        \"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d\": [\n            1746274,\n            1590160929,\n            76,\n            \"0000000068f5999a101d92927dbe76ce2e56f7c9b62db2c6043ce25090f069df\"\n        ],\n        \"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067\": [\n            1744777,\n            1589559214,\n            2,\n            \"00000000000009ed8402e1a75a94863b07bd68febf8628226c1e237dd67d028c\"\n        ],\n        \"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a\": [\n            1772346,\n            1592576908,\n            25,\n            \"0000000000007b7b8dac9a0c3863164bc968a6ed59e5b30b81f4c4bf3c23b49e\"\n        ],\n        \"85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834\": [\n            1747541,\n            1591206287,\n            12,\n            \"000000000000058bc2dece8e8009648f324eb2ba8819b30df1500c1c4cb718fe\"\n        ],\n        \"866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff\": [\n            1772350,\n            1592580676,\n            38,\n            \"0000000000000152f21c3d41160026c6cd07b5236f2de66fd54a5df857345edc\"\n        ],\n        \"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c\": [\n            1584541,\n            1572497343,\n            45,\n            \"0000000000000123f0bbe9f0c6dc639a68afa054c3cec5bdc440fbf1dc60deea\"\n        ],\n        \"901856f632b06573e2384972c38a2b87d7f058595f77cd2fcca47893bb6dc366\": [\n            1772347,\n            1592578128,\n            55,\n            \"000000000000573b617c6814f0b063a24d87fcafd9c081fbe845b17a5a14fb7b\"\n        ],\n        \"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5\": [\n            1607022,\n            1573058015,\n            17,\n            \"00000000000001dcecfa9ea4a8637479884a5ba7b3aef561d1634376877b1d49\"\n        ],\n        \"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d\": [\n            1665679,\n            1581516561,\n            212,\n            \"000000001b9a933341a6414ba96c9a2bde2778e20b2ef3a8139953a6709d869a\"\n        ],\n        \"94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9\": [\n            1747721,\n            1591295101,\n            21,\n            \"00000000acaeb070125b32d6ee0b247da309103d8c6895ca3b9db110f9570393\"\n        ],\n        \"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514\": [\n            1772348,\n            1592579365,\n            68,\n            \"00000000000043e46d2c439cc1381091d2a899a7e73cef342fcd7a8ca30510e5\"\n        ],\n        \"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd\": [\n            1612004,\n            1575943833,\n            107,\n            \"00000000000002071db8bd4eee3dd79446fdc25fe4494bd149d2dbab8f8fc351\"\n        ],\n        \"9e0789481c2f5b2fda96f0af534af3caa5cb5ddc2bbf27aea72c80154d9272d2\": [\n            1772347,\n            1592578128,\n            96,\n            \"000000000000573b617c6814f0b063a24d87fcafd9c081fbe845b17a5a14fb7b\"\n        ],\n        \"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e\": [\n            1609312,\n            1574367249,\n            46,\n            \"000000000029a561c0e454997df161efaf13a84f337049ff4795b66c22c9e887\"\n        ],\n        \"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9\": [\n            1744791,\n            1589564965,\n            32,\n            \"00000000000004b1edb3a19b30988ee533e48ede0d14d58e842eb797aca9edf7\"\n        ],\n        \"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867\": [\n            1612009,\n            1575947371,\n            78,\n            \"000000000000009804f234e6cb02c39ada4c01bdc9c2072cab789146c518e9b3\"\n        ],\n        \"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6\": [\n            1612788,\n            1576617716,\n            233,\n            \"000000009e157af1f4140b003477c9e8a9cd43b93b0798973125dc35c526a2a6\"\n        ],\n        \"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271\": [\n            1609187,\n            1574268754,\n            157,\n            \"0000000000009e7b077d1b0e51098bf744bbe94ad1c6593e45c34efd97bc425a\"\n        ],\n        \"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5\": [\n            1612648,\n            1576479029,\n            104,\n            \"0000000007d38aeacc48c15a3ec552ecbf5c77e967fcab05a5f4f86600df6df7\"\n        ],\n        \"b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2\": [\n            1665693,\n            1581531211,\n            147,\n            \"00000000451769f59ad32d65ca059982480f3071e9b30557ad380022c7efb0eb\"\n        ],\n        \"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7\": [\n            1612648,\n            1576479029,\n            105,\n            \"0000000007d38aeacc48c15a3ec552ecbf5c77e967fcab05a5f4f86600df6df7\"\n        ],\n        \"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94\": [\n            1744777,\n            1589559214,\n            3,\n            \"00000000000009ed8402e1a75a94863b07bd68febf8628226c1e237dd67d028c\"\n        ],\n        \"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9\": [\n            1607028,\n            1573062661,\n            119,\n            \"000000000b69680b3a72b998dfbe15b9f2e4d1d9ec55514c793df17ea52757ea\"\n        ],\n        \"c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0\": [\n            1584540,\n            1572496183,\n            11,\n            \"00000000000000d1cd960abe8520e3fd13e54f77022dc602dbd7b9797774f0ad\"\n        ],\n        \"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295\": [\n            1747569,\n            1591210894,\n            8,\n            \"000000000000031396814946ea32d808d10a94fb17034f1b5f66cb9d8aaee2b6\"\n        ],\n        \"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52\": [\n            1612648,\n            1576479029,\n            103,\n            \"0000000007d38aeacc48c15a3ec552ecbf5c77e967fcab05a5f4f86600df6df7\"\n        ],\n        \"d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee\": [\n            1746834,\n            1590771466,\n            64,\n            \"00000000d4cee21f4cc91d30467bf8b00415e2f6965b26305546cee9e575f9a4\"\n        ],\n        \"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057\": [\n            1747567,\n            1591210595,\n            6,\n            \"0000000000000324e02615bd6c65e356d64d4d0d1155175eed5b89d86d6de064\"\n        ],\n        \"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82\": [\n            1772251,\n            1592534504,\n            38,\n            \"000000000000012e0e87b7c6185f0b1123c9ef0225c1602efe9b7615d4b76cb6\"\n        ],\n        \"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67\": [\n            1774477,\n            1593456977,\n            34,\n            \"0000000000016871a9502945205abed1fd579c8b5aaf0f0523bf658653ae64ec\"\n        ],\n        \"d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156\": [\n            1413150,\n            1536757748,\n            460,\n            \"00000000000000214096fdc98df2896f0305325d07aa2bb21f3a86bddfd49681\"\n        ],\n        \"d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3\": [\n            1774752,\n            1593613036,\n            61,\n            \"000000000003bb43eb7b8b15e70eb6233e149b612514d3d486bf2c7ddef6c249\"\n        ],\n        \"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5\": [\n            1607022,\n            1573058015,\n            16,\n            \"00000000000001dcecfa9ea4a8637479884a5ba7b3aef561d1634376877b1d49\"\n        ],\n        \"dc16e73a88aff40d703f8eba148eae7265d1128a013f6f570a8bd76d86a5e137\": [\n            1666106,\n            1581965019,\n            243,\n            \"0000000000009176617a2d62fcab4ae45ed25bc745f36a73cf554637e50d73b3\"\n        ],\n        \"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c\": [\n            1746274,\n            1590160929,\n            77,\n            \"0000000068f5999a101d92927dbe76ce2e56f7c9b62db2c6043ce25090f069df\"\n        ],\n        \"e20bf91006e084c63c424240e857af8c3e27a5d8356af4dbe5ddc8ad4e71c336\": [\n            1772347,\n            1592578128,\n            45,\n            \"000000000000573b617c6814f0b063a24d87fcafd9c081fbe845b17a5a14fb7b\"\n        ],\n        \"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135\": [\n            1746825,\n            1590760652,\n            69,\n            \"0000000039adfa26118ca702d16bd9ad54ebc22a80856778b5909673647ce9db\"\n        ],\n        \"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802\": [\n            1612704,\n            1576527199,\n            55,\n            \"000000000000008be0f9e93ab4a22da3f67d282c03468bfb9c3c9b479f3a03c2\"\n        ],\n        \"ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21\": [\n            1772346,\n            1592576908,\n            49,\n            \"0000000000007b7b8dac9a0c3863164bc968a6ed59e5b30b81f4c4bf3c23b49e\"\n        ],\n        \"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2\": [\n            1667168,\n            1583123549,\n            18,\n            \"00000000000000e73c100f00385fc395b2a74123a95870c34c892e3846d8608b\"\n        ],\n        \"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d\": [\n            1636331,\n            1577757044,\n            230,\n            \"0000000000007d672215de5c6d66419af6dbb0acb3adb31727433d4d46f9e3d0\"\n        ],\n        \"fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa\": [\n            1772346,\n            1592576908,\n            60,\n            \"0000000000007b7b8dac9a0c3863164bc968a6ed59e5b30b81f4c4bf3c23b49e\"\n        ],\n        \"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877\": [\n            1772375,\n            1592591253,\n            9,\n            \"0000000000000167e02b208939cf45855de05b8c3ac1d355b240798be1386ba1\"\n        ]\n    },\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        750,\n        391,\n        840,\n        407\n    ]\n}"
  },
  {
    "path": "tests/test_storage_upgrade/client_4_5_2_9dk_with_ln",
    "content": "{\n    \"active_forwardings\": {},\n    \"addr_history\": {\n        \"tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq\": [\n            [\n                \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\",\n                1455208\n            ],\n            [\n                \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\",\n                1568264\n            ]\n        ],\n        \"tb1q02g5nde0heaed0y24rztkedh9nvswknw50h7fx\": [],\n        \"tb1q069xqa4lej2tljmd8fcvvfedav54nmspvjnfs2\": [],\n        \"tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5\": [\n            [\n                \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\",\n                1346957\n            ],\n            [\n                \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\",\n                1346978\n            ]\n        ],\n        \"tb1q0j2gt4ap2s08cz5vzm5jg87fdeps7x8v4djgrm\": [],\n        \"tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4\": [\n            [\n                \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\",\n                1414311\n            ],\n            [\n                \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\",\n                1414562\n            ]\n        ],\n        \"tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl\": [\n            [\n                \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\",\n                1454548\n            ],\n            [\n                \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\",\n                1455208\n            ]\n        ],\n        \"tb1q3s4hkssd34tyxdlhafthv4muckjtgwuhltu37t\": [],\n        \"tb1q3t0xcpmzreece8xdxq8k5aaxrt3r623tqldp8n\": [],\n        \"tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg\": [],\n        \"tb1q7mhmnxal53vtc5flh69nph44vah5j56eyesjx9\": [],\n        \"tb1q8evsj0vkzfak2y5qnqx4yf9lty462l7yfhegyd\": [],\n        \"tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5\": [\n            [\n                \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                2579583\n            ],\n            [\n                \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\",\n                2579584\n            ]\n        ],\n        \"tb1q99hkhlswfnj8r5wy2xu9a9m0vy8mvffwzhrx6n\": [],\n        \"tb1q9u3ufsm9ksql8utguap40ch8zpnw83fgzf4z97\": [],\n        \"tb1qad28sgvvrxjnxdnfjxcuepgzzhzlapgxcwuj0k\": [],\n        \"tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6\": [\n            [\n                \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\",\n                2579584\n            ]\n        ],\n        \"tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9\": [\n            [\n                \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\",\n                2415285\n            ],\n            [\n                \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\",\n                2415285\n            ]\n        ],\n        \"tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07\": [],\n        \"tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r\": [\n            [\n                \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n                2528392\n            ]\n        ],\n        \"tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3\": [\n            [\n                \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\",\n                1457134\n            ],\n            [\n                \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\",\n                1568264\n            ]\n        ],\n        \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\": [\n            [\n                \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\",\n                2349374\n            ],\n            [\n                \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\",\n                2349377\n            ],\n            [\n                \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\",\n                2406297\n            ]\n        ],\n        \"tb1qdgscjnjm6w59chq0xgghwtq42vfhhn0murqx6hrfz2yaf2yx9v9skh03as\": [\n            [\n                \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\",\n                2406297\n            ],\n            [\n                \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\",\n                2415285\n            ]\n        ],\n        \"tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l\": [\n            [\n                \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\",\n                1414311\n            ],\n            [\n                \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\",\n                1414311\n            ]\n        ],\n        \"tb1qf03zdjdnzxwztxs9d3g9ynsvvs5rmjhvtmln35\": [],\n        \"tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp\": [\n            [\n                \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n                2425096\n            ],\n            [\n                \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n                2528392\n            ]\n        ],\n        \"tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8\": [\n            [\n                \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\",\n                2404107\n            ],\n            [\n                \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\",\n                2406297\n            ]\n        ],\n        \"tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z\": [\n            [\n                \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\",\n                1583044\n            ],\n            [\n                \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\",\n                1583044\n            ]\n        ],\n        \"tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e\": [\n            [\n                \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\",\n                2415285\n            ],\n            [\n                \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\",\n                2418761\n            ]\n        ],\n        \"tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5\": [],\n        \"tb1qh0csljush2tad6t0s4qgx4r5t0rzcw9729l7kx\": [],\n        \"tb1qh878je0rfut79fkudf4mkl8m4cn8uzfsluersy\": [],\n        \"tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu\": [\n            [\n                \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\",\n                1346978\n            ],\n            [\n                \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\",\n                1414310\n            ]\n        ],\n        \"tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg\": [\n            [\n                \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\",\n                2418761\n            ],\n            [\n                \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n                2425096\n            ]\n        ],\n        \"tb1qjv7a78ea0jp9d793d5ra7mtzkjzezwldz5zvr6\": [],\n        \"tb1qkneqe450eqxtpr3r5z8aw4234sjmpknm0gxsae\": [],\n        \"tb1qkp86pkt75ds257snenp3q7vs29pf4g6cmhy2hw\": [],\n        \"tb1qlclgzsp2tktdl66xuk3je7ztztstxvjatly8wy\": [],\n        \"tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd\": [\n            [\n                \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\",\n                1455781\n            ],\n            [\n                \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\",\n                1568264\n            ]\n        ],\n        \"tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c\": [\n            [\n                \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\",\n                1455208\n            ],\n            [\n                \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\",\n                1457134\n            ]\n        ],\n        \"tb1qndaru6pfal030ev296uxwuulxrezaj0j70ceje\": [],\n        \"tb1qplsf242vay6vavy4eguef855fx3klmp9505g88\": [\n            [\n                \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\",\n                2579582\n            ],\n            [\n                \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                2579583\n            ]\n        ],\n        \"tb1qq0gdz0vz02ypa3cawlljstrx8cxydhvalcv8wc\": [],\n        \"tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu\": [\n            [\n                \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\",\n                1346870\n            ],\n            [\n                \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\",\n                1346957\n            ]\n        ],\n        \"tb1qr7mjlxgc6at67tx0s8ypa5efx8clc47xh6yjqg\": [],\n        \"tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3\": [\n            [\n                \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\",\n                2579584\n            ]\n        ],\n        \"tb1qs087qkcawefutkkv8pg037t6txldk5szfntamj\": [],\n        \"tb1qs3j3j05rjefnjqf0mlpztszg8acz868wnxgz6j\": [],\n        \"tb1qsd2c4xwg47hnngn2uqg66y5rxz2hp074u9rq6r\": [],\n        \"tb1qssypacgyt40r8t95myqgrhrdhq93f5y4jmgmqe53w4tlydphcnaqmpm8kr\": [\n            [\n                \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                2579583\n            ]\n        ],\n        \"tb1qt339ksrha0n5a6lwpql778erkm272hxgamdc0u\": [],\n        \"tb1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sak7ncg\": [],\n        \"tb1qtqqddqrlg4xj3dzvjnea8wh5zy2fdf3jxl7qhs\": [],\n        \"tb1qucj6lx6eatgm5396fe539x53cp8zr0yzgclk6q\": [],\n        \"tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj\": [\n            [\n                \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\",\n                1414310\n            ],\n            [\n                \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\",\n                1414311\n            ]\n        ],\n        \"tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk\": [\n            [\n                \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\",\n                1775825\n            ],\n            [\n                \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\",\n                1892447\n            ]\n        ],\n        \"tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct\": [\n            [\n                \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\",\n                1414562\n            ],\n            [\n                \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\",\n                1454548\n            ]\n        ],\n        \"tb1qvwwxv48k9vch5ddmf83g4fhd0tnx3mt8jp6rka\": [],\n        \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\": [\n            [\n                \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\",\n                1583044\n            ],\n            [\n                \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\",\n                1583044\n            ]\n        ],\n        \"tb1qym6srwn87eu2sa5prkgd2lqva0nh0tr2xkeftp\": [],\n        \"tb1qzyaz308030saay93zqma0at032vfqa9y0gfge3\": []\n    },\n    \"addresses\": {\n        \"change\": [\n            \"tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5\",\n            \"tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu\",\n            \"tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj\",\n            \"tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l\",\n            \"tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4\",\n            \"tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct\",\n            \"tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl\",\n            \"tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq\",\n            \"tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3\",\n            \"tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9\",\n            \"tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e\",\n            \"tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg\",\n            \"tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r\",\n            \"tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5\",\n            \"tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5\",\n            \"tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6\",\n            \"tb1q02g5nde0heaed0y24rztkedh9nvswknw50h7fx\",\n            \"tb1qzyaz308030saay93zqma0at032vfqa9y0gfge3\",\n            \"tb1q3s4hkssd34tyxdlhafthv4muckjtgwuhltu37t\",\n            \"tb1qjv7a78ea0jp9d793d5ra7mtzkjzezwldz5zvr6\",\n            \"tb1qndaru6pfal030ev296uxwuulxrezaj0j70ceje\",\n            \"tb1qsd2c4xwg47hnngn2uqg66y5rxz2hp074u9rq6r\",\n            \"tb1qtqqddqrlg4xj3dzvjnea8wh5zy2fdf3jxl7qhs\",\n            \"tb1q9u3ufsm9ksql8utguap40ch8zpnw83fgzf4z97\",\n            \"tb1qlclgzsp2tktdl66xuk3je7ztztstxvjatly8wy\",\n            \"tb1q7mhmnxal53vtc5flh69nph44vah5j56eyesjx9\"\n        ],\n        \"receiving\": [\n            \"tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu\",\n            \"tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd\",\n            \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\",\n            \"tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c\",\n            \"tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk\",\n            \"tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z\",\n            \"tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8\",\n            \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\",\n            \"tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07\",\n            \"tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg\",\n            \"tb1qplsf242vay6vavy4eguef855fx3klmp9505g88\",\n            \"tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3\",\n            \"tb1qt339ksrha0n5a6lwpql778erkm272hxgamdc0u\",\n            \"tb1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sak7ncg\",\n            \"tb1qf03zdjdnzxwztxs9d3g9ynsvvs5rmjhvtmln35\",\n            \"tb1qq0gdz0vz02ypa3cawlljstrx8cxydhvalcv8wc\",\n            \"tb1qkp86pkt75ds257snenp3q7vs29pf4g6cmhy2hw\",\n            \"tb1q3t0xcpmzreece8xdxq8k5aaxrt3r623tqldp8n\",\n            \"tb1qr7mjlxgc6at67tx0s8ypa5efx8clc47xh6yjqg\",\n            \"tb1qkneqe450eqxtpr3r5z8aw4234sjmpknm0gxsae\",\n            \"tb1qs087qkcawefutkkv8pg037t6txldk5szfntamj\",\n            \"tb1q069xqa4lej2tljmd8fcvvfedav54nmspvjnfs2\",\n            \"tb1q0j2gt4ap2s08cz5vzm5jg87fdeps7x8v4djgrm\",\n            \"tb1qs3j3j05rjefnjqf0mlpztszg8acz868wnxgz6j\",\n            \"tb1qh878je0rfut79fkudf4mkl8m4cn8uzfsluersy\",\n            \"tb1qvwwxv48k9vch5ddmf83g4fhd0tnx3mt8jp6rka\",\n            \"tb1q8evsj0vkzfak2y5qnqx4yf9lty462l7yfhegyd\",\n            \"tb1q99hkhlswfnj8r5wy2xu9a9m0vy8mvffwzhrx6n\",\n            \"tb1qh0csljush2tad6t0s4qgx4r5t0rzcw9729l7kx\",\n            \"tb1qad28sgvvrxjnxdnfjxcuepgzzhzlapgxcwuj0k\",\n            \"tb1qym6srwn87eu2sa5prkgd2lqva0nh0tr2xkeftp\",\n            \"tb1qucj6lx6eatgm5396fe539x53cp8zr0yzgclk6q\"\n        ]\n    },\n    \"channels\": {\n        \"384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb\": {\n            \"alias\": \"03be2508ab4ce20a\",\n            \"channel_id\": \"384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb\",\n            \"channel_type\": 4096,\n            \"constraints\": {\n                \"capacity\": 400000,\n                \"flags\": 0,\n                \"funding_txn_minimum_depth\": 3,\n                \"is_initiator\": true\n            },\n            \"data_loss_protect_remote_pcp\": {},\n            \"fail_htlc_reasons\": {},\n            \"funding_height\": [\n                \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\",\n                2579583,\n                1708967515\n            ],\n            \"funding_inputs\": [\n                [\n                    \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\",\n                    1\n                ]\n            ],\n            \"funding_outpoint\": {\n                \"output_index\": 2,\n                \"txid\": \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\"\n            },\n            \"has_onchain_backup\": true,\n            \"local_config\": {\n                \"announcement_bitcoin_sig\": \"\",\n                \"announcement_node_sig\": \"\",\n                \"channel_seed\": \"ebd40c233e830b550e053e4c71e633af352905f0caf39bfbdd4eb64d93a3b701\",\n                \"current_commitment_signature\": \"fe7bac0a3a310e8945fdbf4e4b0bed2c4717eef95237a3790901e8972efa3336326600e76328558d66ab1699752f744f301224a7b9d0672289a2abba74a7f477\",\n                \"current_htlc_signatures\": \"\",\n                \"delayed_basepoint\": {\n                    \"privkey\": \"a3fa15f6adb5579f2ff5d0382832ee3a3bbd07148226a6d25bbc4722b5ff7d7e\",\n                    \"pubkey\": \"0313bd49ae13538a507c86781ca0de2ca40151715a5557eee6a89bb3f8f97f47a6\"\n                },\n                \"dust_limit_sat\": 546,\n                \"funding_locked_received\": true,\n                \"htlc_basepoint\": {\n                    \"privkey\": \"60fef00cc28fa8f9353130ff8e7e2e5185618c759f69f1f23edee29c1fee1a24\",\n                    \"pubkey\": \"02c910690935cfadc58f42ddb7fe07b72e47a6da168f2d27f3e7414257e1282e35\"\n                },\n                \"htlc_minimum_msat\": 1,\n                \"initial_msat\": 400000000,\n                \"max_accepted_htlcs\": 30,\n                \"max_htlc_value_in_flight_msat\": 400000000,\n                \"multisig_key\": {\n                    \"privkey\": \"75d8af77523548f38db197a5a258c91a0b22f9e529cd787128f90393ab86e494\",\n                    \"pubkey\": \"02ebef86631b36d05117734751f7e9b96c5cb78211647854abc9001559b4b3611b\"\n                },\n                \"payment_basepoint\": {\n                    \"pubkey\": \"02411162b8c1c103969fcdce17b0a744fbd102f5e9fc6e389b3c64afe273622d14\"\n                },\n                \"per_commitment_secret_seed\": \"c72e72846a3f887925b409ba22f0f600e58bb1659d2df714900c78c4b0896eaf\",\n                \"reserve_sat\": 4000,\n                \"revocation_basepoint\": {\n                    \"privkey\": \"fe643cf7618909542c1ba7d3477474debc421df90ea58c4ed6bafd68fca458cb\",\n                    \"pubkey\": \"02673d448f0d69cdfae1260a897613d7aff5ff16a4e8964fe77c59850e80ee1cff\"\n                },\n                \"to_self_delay\": 1008,\n                \"upfront_shutdown_script\": \"\"\n            },\n            \"local_scid_alias\": \"5ac8709dc7e3339a\",\n            \"log\": {\n                \"-1\": {\n                    \"adds\": {},\n                    \"ctn\": 18,\n                    \"fails\": {},\n                    \"fee_updates\": {\n                        \"0\": {\n                            \"ctn_local\": 0,\n                            \"ctn_remote\": 0,\n                            \"rate\": 253\n                        }\n                    },\n                    \"locked_in\": {},\n                    \"next_htlc_id\": 0,\n                    \"revack_pending\": false,\n                    \"settles\": {}\n                },\n                \"1\": {\n                    \"adds\": {\n                        \"0\": [\n                            1001100,\n                            \"9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921\",\n                            2580192,\n                            0,\n                            1708970395\n                        ],\n                        \"1\": [\n                            1003100,\n                            \"9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921\",\n                            2580192,\n                            1,\n                            1708970395\n                        ],\n                        \"2\": [\n                            1001100,\n                            \"a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f\",\n                            2580192,\n                            2,\n                            1708970417\n                        ],\n                        \"3\": [\n                            1003100,\n                            \"a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f\",\n                            2580192,\n                            3,\n                            1708970418\n                        ],\n                        \"4\": [\n                            50006000,\n                            \"0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda\",\n                            2580311,\n                            4,\n                            1708972441\n                        ],\n                        \"5\": [\n                            50008000,\n                            \"0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda\",\n                            2580311,\n                            5,\n                            1708972441\n                        ],\n                        \"6\": [\n                            50030000,\n                            \"0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda\",\n                            2580311,\n                            6,\n                            1708972442\n                        ],\n                        \"7\": [\n                            10002000,\n                            \"720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\",\n                            2580311,\n                            7,\n                            1708972473\n                        ],\n                        \"8\": [\n                            10004000,\n                            \"720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\",\n                            2580311,\n                            8,\n                            1708972473\n                        ]\n                    },\n                    \"ctn\": 17,\n                    \"fails\": {\n                        \"0\": {\n                            \"-1\": 3,\n                            \"1\": 3\n                        },\n                        \"2\": {\n                            \"-1\": 7,\n                            \"1\": 7\n                        },\n                        \"4\": {\n                            \"-1\": 11,\n                            \"1\": 11\n                        },\n                        \"5\": {\n                            \"-1\": 13,\n                            \"1\": 13\n                        },\n                        \"7\": {\n                            \"-1\": 17,\n                            \"1\": 17\n                        }\n                    },\n                    \"fee_updates\": {\n                        \"0\": {\n                            \"ctn_local\": 0,\n                            \"ctn_remote\": 0,\n                            \"rate\": 253\n                        },\n                        \"1\": {\n                            \"ctn_local\": 1,\n                            \"ctn_remote\": 1,\n                            \"rate\": 254\n                        }\n                    },\n                    \"locked_in\": {\n                        \"0\": {\n                            \"-1\": 2,\n                            \"1\": 2\n                        },\n                        \"1\": {\n                            \"-1\": 4,\n                            \"1\": 4\n                        },\n                        \"2\": {\n                            \"-1\": 6,\n                            \"1\": 6\n                        },\n                        \"3\": {\n                            \"-1\": 8,\n                            \"1\": 8\n                        },\n                        \"4\": {\n                            \"-1\": 10,\n                            \"1\": 10\n                        },\n                        \"5\": {\n                            \"-1\": 12,\n                            \"1\": 12\n                        },\n                        \"6\": {\n                            \"-1\": 14,\n                            \"1\": 14\n                        },\n                        \"7\": {\n                            \"-1\": 16,\n                            \"1\": 16\n                        },\n                        \"8\": {\n                            \"-1\": 18,\n                            \"1\": 18\n                        }\n                    },\n                    \"next_htlc_id\": 9,\n                    \"revack_pending\": false,\n                    \"settles\": {\n                        \"1\": {\n                            \"-1\": 5,\n                            \"1\": 5\n                        },\n                        \"3\": {\n                            \"-1\": 9,\n                            \"1\": 9\n                        },\n                        \"6\": {\n                            \"-1\": 15,\n                            \"1\": 15\n                        }\n                    },\n                    \"unacked_updates\": {},\n                    \"was_revoke_last\": false\n                }\n            },\n            \"node_id\": \"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134\",\n            \"onion_keys\": {\n                \"8\": \"a1457ee8bf00183142e702f789aaf7c94f401872ed960e3627841e22005a12e2\"\n            },\n            \"peer_network_addresses\": {\n                \"34.250.234.192:9735\": 1708968288\n            },\n            \"remote_config\": {\n                \"announcement_bitcoin_sig\": \"\",\n                \"announcement_node_sig\": \"\",\n                \"current_per_commitment_point\": \"0242ad9b0d3b24e58e657c3b3b882d1ebadccc07eeb29ce0c588d9266365785153\",\n                \"delayed_basepoint\": {\n                    \"pubkey\": \"036f22e4114f8b11d93ec6070eb49bc60614b73bcea91912ca82e6f3bf3d7c055a\"\n                },\n                \"dust_limit_sat\": 546,\n                \"htlc_basepoint\": {\n                    \"pubkey\": \"03440de7b169401a4b26b93945d7e5c7512dd404856b2493a85352d365318d652b\"\n                },\n                \"htlc_minimum_msat\": 1,\n                \"initial_msat\": 0,\n                \"max_accepted_htlcs\": 30,\n                \"max_htlc_value_in_flight_msat\": 180000000,\n                \"multisig_key\": {\n                    \"pubkey\": \"0396ecb0125961b5bbb506b6e0c9565178b70e35ab0ec13690715be04a4655e0f5\"\n                },\n                \"next_per_commitment_point\": \"03338204605ae6b11537000011434fbef308a8940ab743b11ab8939b221726689b\",\n                \"payment_basepoint\": {\n                    \"pubkey\": \"02ba797a471c9e5983e15cf4dc8a9f27f83e1e2593c8ae271895ed3d07dceb8378\"\n                },\n                \"reserve_sat\": 4000,\n                \"revocation_basepoint\": {\n                    \"pubkey\": \"02ccd1fe1183a4d6eea2bbc7bc4554e029149e77f2a7ebdb7ccced40b0d488c436\"\n                },\n                \"to_self_delay\": 720,\n                \"upfront_shutdown_script\": \"\"\n            },\n            \"remote_update\": \"0102e969e6790715a64857d031a62a36fc79ff1fbaa156990621faf499b9786df51224b0a20df7d31ba246c1bf1a676ce31e06c34c1171b2489a0950f1e2226a8ed043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000005ac8709dc7e3339a65dcce98030000900000000000000001000003e800000064000000000aba9500\",\n            \"revocation_store\": {\n                \"buckets\": {\n                    \"0\": [\n                        \"fee8a7e8bc41b6323b5f1a5cbee3a288282d1531aa72f1af0f677011675d9057\",\n                        281474976710639\n                    ],\n                    \"1\": [\n                        \"f99155a8cdb9398244bba78d46ff5fc4b174677962581caaaeb5da1c1d457ad4\",\n                        281474976710638\n                    ],\n                    \"2\": [\n                        \"b82468458b06aeaae492f8843f21e576cd6fa36e00fc75d4d45b55cf3179c7c7\",\n                        281474976710644\n                    ],\n                    \"3\": [\n                        \"2419d17b43780b398c4a8422c52612fecd00580c6030947c31f8e37de2833d5f\",\n                        281474976710648\n                    ],\n                    \"4\": [\n                        \"8477b931bd315d88131ec71a947ab9c974840cb8f41b851b9275e6ad133a0722\",\n                        281474976710640\n                    ]\n                },\n                \"index\": 281474976710637\n            },\n            \"short_channel_id\": \"275c7f0001df0002\",\n            \"state\": \"OPEN\",\n            \"unfulfilled_htlcs\": {}\n        },\n        \"fe13acd33700e2dc135c833b790eb75e1ed71429773b36192315f3fede7f7696\": {\n            \"alias\": \"02b529cba39c16c9\",\n            \"channel_id\": \"fe13acd33700e2dc135c833b790eb75e1ed71429773b36192315f3fede7f7696\",\n            \"channel_type\": 4096,\n            \"closing_height\": [\n                \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\",\n                2528392,\n                1696346138\n            ],\n            \"constraints\": {\n                \"capacity\": 302547,\n                \"flags\": 0,\n                \"funding_txn_minimum_depth\": 3,\n                \"is_initiator\": true\n            },\n            \"data_loss_protect_remote_pcp\": {},\n            \"fail_htlc_reasons\": {},\n            \"funding_height\": [\n                \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\",\n                2425096,\n                1679252503\n            ],\n            \"funding_inputs\": [\n                [\n                    \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\",\n                    1\n                ]\n            ],\n            \"funding_outpoint\": {\n                \"output_index\": 1,\n                \"txid\": \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\"\n            },\n            \"has_onchain_backup\": true,\n            \"local_config\": {\n                \"announcement_bitcoin_sig\": \"\",\n                \"announcement_node_sig\": \"\",\n                \"channel_seed\": \"ce9bad44ff8521d9f57fd202ad7cdedceb934f0056f42d0f3aa7a576b505332a\",\n                \"current_commitment_signature\": \"efebaf3c74a8f1d8c917d4618374844e4c27cfc55012810b46ef07651495a2da621906c2c6d2e52db0f26b9bdea439fcecc59b7d8473f246baed68fa33f69218\",\n                \"current_htlc_signatures\": \"\",\n                \"delayed_basepoint\": {\n                    \"privkey\": \"fe9b095e8c63991625a6e6f64cbb0b83c1fc168b0a6b6860f40ef3a824ba67db\",\n                    \"pubkey\": \"03c9009d971f8d23cb3cc8df627cc97d3a16494b4402d2e2e15349516e733f9c94\"\n                },\n                \"dust_limit_sat\": 546,\n                \"funding_locked_received\": true,\n                \"htlc_basepoint\": {\n                    \"privkey\": \"a914f221742a6f0f036686f8a3307e63efd197d08317bc5fb579bd6c6395ce4a\",\n                    \"pubkey\": \"02cd0d979db8fcbc80953ed47225cc29da216aca2523137823ed2c7dff7f996997\"\n                },\n                \"htlc_minimum_msat\": 1,\n                \"initial_msat\": 302547000,\n                \"max_accepted_htlcs\": 30,\n                \"max_htlc_value_in_flight_msat\": 302547000,\n                \"multisig_key\": {\n                    \"privkey\": \"bfafbc5a1924d01c8570a260debd2f54623bdf19d4780e1349866d000de11477\",\n                    \"pubkey\": \"022aab353e6b25226a3cb38e81dafe8fca3416642847dab7d38aaafe00eb8294ee\"\n                },\n                \"payment_basepoint\": {\n                    \"pubkey\": \"0308d686712782a44b0cef220485ad83dae77853a5bf8501a92bb79056c9dcb25a\"\n                },\n                \"per_commitment_secret_seed\": \"2fed6f7f013377d1f79f5f0857d5f522c83c086372b2b73a7113d05d8b931c4d\",\n                \"reserve_sat\": 3025,\n                \"revocation_basepoint\": {\n                    \"privkey\": \"7fa3fe7f4079369e33cee5854aeb4df4b4e392831a7a8174a2a93aec88f0ab12\",\n                    \"pubkey\": \"03d1bfed013abf573d96dd0e55ebb500a00e81decf32890d862aa119852792a576\"\n                },\n                \"to_self_delay\": 1008,\n                \"upfront_shutdown_script\": \"\"\n            },\n            \"local_scid_alias\": \"2fe34de423b66e0a\",\n            \"log\": {\n                \"-1\": {\n                    \"adds\": {},\n                    \"ctn\": 21,\n                    \"fails\": {},\n                    \"fee_updates\": {\n                        \"0\": {\n                            \"ctn_local\": 0,\n                            \"ctn_remote\": 0,\n                            \"rate\": 253\n                        }\n                    },\n                    \"locked_in\": {},\n                    \"next_htlc_id\": 0,\n                    \"revack_pending\": false,\n                    \"settles\": {}\n                },\n                \"1\": {\n                    \"adds\": {},\n                    \"ctn\": 21,\n                    \"fails\": {},\n                    \"fee_updates\": {\n                        \"0\": {\n                            \"ctn_local\": 0,\n                            \"ctn_remote\": 0,\n                            \"rate\": 253\n                        },\n                        \"1\": {\n                            \"ctn_local\": 1,\n                            \"ctn_remote\": 1,\n                            \"rate\": 254\n                        },\n                        \"2\": {\n                            \"ctn_local\": 2,\n                            \"ctn_remote\": 2,\n                            \"rate\": 3263\n                        },\n                        \"3\": {\n                            \"ctn_local\": 3,\n                            \"ctn_remote\": 3,\n                            \"rate\": 253\n                        },\n                        \"4\": {\n                            \"ctn_local\": 4,\n                            \"ctn_remote\": 4,\n                            \"rate\": 9596\n                        },\n                        \"5\": {\n                            \"ctn_local\": 5,\n                            \"ctn_remote\": 5,\n                            \"rate\": 19903\n                        },\n                        \"6\": {\n                            \"ctn_local\": 6,\n                            \"ctn_remote\": 6,\n                            \"rate\": 5009\n                        },\n                        \"7\": {\n                            \"ctn_local\": 7,\n                            \"ctn_remote\": 7,\n                            \"rate\": 24999\n                        },\n                        \"8\": {\n                            \"ctn_local\": 8,\n                            \"ctn_remote\": 8,\n                            \"rate\": 5017\n                        },\n                        \"9\": {\n                            \"ctn_local\": 9,\n                            \"ctn_remote\": 9,\n                            \"rate\": 16250\n                        },\n                        \"10\": {\n                            \"ctn_local\": 10,\n                            \"ctn_remote\": 10,\n                            \"rate\": 5019\n                        },\n                        \"11\": {\n                            \"ctn_local\": 11,\n                            \"ctn_remote\": 11,\n                            \"rate\": 1664\n                        },\n                        \"12\": {\n                            \"ctn_local\": 12,\n                            \"ctn_remote\": 12,\n                            \"rate\": 253\n                        },\n                        \"13\": {\n                            \"ctn_local\": 13,\n                            \"ctn_remote\": 13,\n                            \"rate\": 797\n                        },\n                        \"14\": {\n                            \"ctn_local\": 14,\n                            \"ctn_remote\": 14,\n                            \"rate\": 254\n                        },\n                        \"15\": {\n                            \"ctn_local\": 15,\n                            \"ctn_remote\": 15,\n                            \"rate\": 1277\n                        },\n                        \"16\": {\n                            \"ctn_local\": 16,\n                            \"ctn_remote\": 16,\n                            \"rate\": 254\n                        },\n                        \"17\": {\n                            \"ctn_local\": 17,\n                            \"ctn_remote\": 17,\n                            \"rate\": 1277\n                        },\n                        \"18\": {\n                            \"ctn_local\": 18,\n                            \"ctn_remote\": 18,\n                            \"rate\": 254\n                        },\n                        \"19\": {\n                            \"ctn_local\": 19,\n                            \"ctn_remote\": 19,\n                            \"rate\": 1528\n                        },\n                        \"20\": {\n                            \"ctn_local\": 20,\n                            \"ctn_remote\": 20,\n                            \"rate\": 753\n                        },\n                        \"21\": {\n                            \"ctn_local\": 21,\n                            \"ctn_remote\": 21,\n                            \"rate\": 253\n                        }\n                    },\n                    \"locked_in\": {},\n                    \"next_htlc_id\": 0,\n                    \"revack_pending\": false,\n                    \"settles\": {},\n                    \"unacked_updates\": {},\n                    \"was_revoke_last\": true\n                }\n            },\n            \"node_id\": \"02bf82e22f99dcd7ac1de4aad5152ce48f0694c46ec582567f379e0adbf81e2d0f\",\n            \"onion_keys\": {},\n            \"peer_network_addresses\": {\n                \"195.201.207.61:9739\": 1691082818,\n                \"lightning.electrum.org:9739\": 1695607695\n            },\n            \"remote_config\": {\n                \"announcement_bitcoin_sig\": \"\",\n                \"announcement_node_sig\": \"\",\n                \"current_per_commitment_point\": \"02f44e36980d56667d751800d67cd6fb3a70f16bb170dbf9ba69b2a3772e6f8728\",\n                \"delayed_basepoint\": {\n                    \"pubkey\": \"02cd6f31e5bdba5abfc555e5e9b8d9160c7a4b9dbe2991cf10d81ba7160f4d694c\"\n                },\n                \"dust_limit_sat\": 546,\n                \"htlc_basepoint\": {\n                    \"pubkey\": \"02a3da13a041abed2d51275c4561d495a31e58eb37e8787e360fe7abe2b2eb2b21\"\n                },\n                \"htlc_minimum_msat\": 1,\n                \"initial_msat\": 0,\n                \"max_accepted_htlcs\": 30,\n                \"max_htlc_value_in_flight_msat\": 5000000000,\n                \"multisig_key\": {\n                    \"pubkey\": \"03a5bf1debe9c97bb56c7a4316b45bf2f8d35612db2964253ae8efd0f7ca8e1f70\"\n                },\n                \"next_per_commitment_point\": \"03d00d13b8823f9fe5b4676e768d4656c661c14d7bdfe7bd72ff4363c6a437dcc8\",\n                \"payment_basepoint\": {\n                    \"pubkey\": \"02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd\"\n                },\n                \"reserve_sat\": 3025,\n                \"revocation_basepoint\": {\n                    \"pubkey\": \"022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae\"\n                },\n                \"to_self_delay\": 720,\n                \"upfront_shutdown_script\": \"\"\n            },\n            \"remote_update\": \"0102beb6d231566566e014c6f417f247a5e8e882fd6b44ff4526ee230ace401d6ae57205b5c5dd2de21b9ceecbd8676d99a4588266b38b8af59305103c956127122843497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000002fe34de423b66e0a6510eb91030200900000000000000001000003e8000000640000000012088038\",\n            \"revocation_store\": {\n                \"buckets\": {\n                    \"0\": [\n                        \"9e6980c55b42595e57b73b3454a4848af482ecbb891eecd004faa2a31c9c9bcd\",\n                        281474976710635\n                    ],\n                    \"1\": [\n                        \"b1ce9bdf3f57b398888115fd5decc7012bd4d5d14d556f072136482d4220c64b\",\n                        281474976710638\n                    ],\n                    \"2\": [\n                        \"4f83e00cf80dcb984cecf2aea3caff592a20611a1c0cc6c48e7fd73898ec0331\",\n                        281474976710636\n                    ],\n                    \"3\": [\n                        \"5c63a6477a353af231afcde34a0d0de8246a32cb3a2982bc2f99ce011e1c81f7\",\n                        281474976710648\n                    ],\n                    \"4\": [\n                        \"ca39a056abd0d958de12768a79b45d99596fbcc26dd8ef35abe84ea052ac945b\",\n                        281474976710640\n                    ]\n                },\n                \"index\": 281474976710634\n            },\n            \"short_channel_id\": \"2501080000150001\",\n            \"state\": \"REDEEMED\",\n            \"unfulfilled_htlcs\": {}\n        }\n    },\n    \"fiat_value\": {},\n    \"forwarding_failures\": {},\n    \"frozen_addresses\": [\n        \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\",\n        \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\",\n        \"tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07\"\n    ],\n    \"frozen_coins\": {\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e:0\": true\n    },\n    \"imported_channel_backups\": {},\n    \"invoices\": {\n        \"0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda\": {\n            \"amount_msat\": 50000000,\n            \"bip70\": null,\n            \"exp\": 3153600000,\n            \"height\": 0,\n            \"lightning_invoice\": \"lntb500u1pjaekv5pp5pq6r2jans03la7aqsv2tvtc7cjctatvs5t4fwajjuyhc46fsaldqsp5djuklpf9us6xtz0xvkngsr0n84zce8zydlzvu6m4peccw480037qdqsda3xjampdcunsdphcqzynxq8zals8sq9qlzqqqqqqqqqqqqqqqqqqqqqqqqqq9scqfppqs7nz7zggvsljj76uw5unet2x25flvw5przjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cngq3etvuwr5lpagqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng8vsq5qqq2qqqqqqqqlgqqqqqeqqjq0kkkehxtv7uv9y53sa8ag3qad342xa7u8eejp62wq4sah493k9h4pexe36x867u93zacnrkdcapfarr4q089jpmpzaym0n5m8cm46wgqf5updq\",\n            \"message\": \"obiwan9847\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1qs7nz7zggvsljj76uw5unet2x25flvw5p9r9rks\",\n                    50000\n                ]\n            ],\n            \"time\": 1708972436\n        },\n        \"1caa46e49c\": {\n            \"amount_msat\": 5100000,\n            \"bip70\": \"0801120b783530392b7368613235361a9e160ac40c3082064030820528a0030201020208138720f4708a0b91300d06092a864886f70d01010b05003081b4310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e312d302b060355040b1324687474703a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f313330310603550403132a476f2044616464792053656375726520436572746966696361746520417574686f72697479202d204732301e170d3230303830323136323433305a170d3231313030313231313333385a303d3121301f060355040b1318446f6d61696e20436f6e74726f6c2056616c696461746564311830160603550403130f746573742e6269747061792e636f6d30820122300d06092a864886f70d01010105000382010f003082010a0282010100d14377b6f2b50aa73427c248915563d2c8804dd824b1d8f015dd1ae2a390c8022fd963355849b217309208c3450a016ff4001ee336a3189edadce12bc41e4999e45f6fcf0b00daa7e9a4b12e72dce19c51ae8e55b8bfec02b80a58ccb2c1b3e5bde81f679b9993bf52c871a1fadef6bb5f7d8d9208889400ba2be1d2baf82ec303470852570c3bbb6b89334d8974e61b8867bf299fb802c57e9b00d9b9f70572ac6d81fecd304c83aaf21f4f3b529e9898ea9b868f8f07b4189668e71854ae776bacd0d9706a8be03f528c68ad023e3b45bfa55e9b42e535aafc7eb8672645dcdeaf7204a468d2b84f27ed12072a411627647108e421abe7308e3bac305896f10203010001a38202ca308202c6300c0603551d130101ff04023000301d0603551d250416301406082b0601050507030106082b06010505070302300e0603551d0f0101ff0404030205a030380603551d1f0431302f302da02ba0298627687474703a2f2f63726c2e676f64616464792e636f6d2f676469673273312d323137372e63726c305d0603551d20045630543048060b6086480186fd6d010717013039303706082b06010505070201162b687474703a2f2f6365727469666963617465732e676f64616464792e636f6d2f7265706f7369746f72792f3008060667810c010201307606082b06010505070101046a3068302406082b060105050730018618687474703a2f2f6f6373702e676f64616464792e636f6d2f304006082b060105050730028634687474703a2f2f6365727469666963617465732e676f64616464792e636f6d2f7265706f7369746f72792f67646967322e637274301f0603551d2304183016801440c2bd278ecc348330a233d7fb6cb3f0b42c80ce302f0603551d1104283026820f746573742e6269747061792e636f6d82137777772e746573742e6269747061792e636f6d301d0603551d0e041604142cd8def7d64c620cce78bdec365fd961cfd035e130820103060a2b06010401d6790204020481f40481f100ef007500f65c942fd1773022145418083094568ee34d131933bfdf0c2f200bcc4ef164e300000173affd2c3f000004030046304402202ab9dfff3fc828a44a12dfeb3ce37f70a1dd0c8bd86c0083431aa3f50abaa469022049aa04fa3ae4325ff3a1539015e454960606e569fa3de4e650e7b6329118c4f50076005cdc4392fee6ab4544b15e9ad456e61037fbd5fa47dca17394b25ee6f6c70eca00000173affd2d7a0000040300473045022100ed2170727ac7ac9ef1eb82a598d5959721e236ebd5a9c1d38cc46848bd61582902205fc1840c8d4ca52a9162a61b4630ec81e9e2f43dc4188262c5cf309af8aaebf9300d06092a864886f70d01010b05000382010100492841f57eba01263ab062521d250aa679d53cf01c8107f86d4b00b6fd1bd778262046807f364d048cb99d19c1372e6aec3f9cddd023d4189f16a38301d9ca72d59a97a1f2b226a1501c8833397aa8498e824be4aa61c1f7f0b6a603cd7fba19077f2c623f1d09185ec634e83de34e8fa8cc0eb2f296a319098779cb2fd93149ad868940eb37054246d522ee00b28155b95a0d753e26c6d96978bfe0809d1946252e033f806d1872ba4acb83284d66608bbb57d489203cb2fc3d49d235d934111d5f9420719279d545d1eb0ab9ad1a57bc9b6d1a7d256db29a3e49ed206e21e3eb352effbbfb531ba0ceb4c70b1339e89f39428fcd4651dfb20748ab810be9af0ad409308204d0308203b8a003020102020107300d06092a864886f70d01010b0500308183310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e3131302f06035504031328476f20446164647920526f6f7420436572746966696361746520417574686f72697479202d204732301e170d3131303530333037303030305a170d3331303530333037303030305a3081b4310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e312d302b060355040b1324687474703a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f313330310603550403132a476f2044616464792053656375726520436572746966696361746520417574686f72697479202d20473230820122300d06092a864886f70d01010105000382010f003082010a0282010100b9e0cb10d4af76bdd49362eb3064b881086cc304d962178e2fff3e65cf8fce62e63c521cda16454b55ab786b63836290ce0f696c99c81a148b4ccc4533ea88dc9ea3af2bfe80619d7957c4cf2ef43f303c5d47fc9a16bcc3379641518e114b54f828bed08cbef030381ef3b026f86647636dde7126478f384753d1461db4e3dc00ea45acbdbc71d9aa6f00dbdbcd303a794f5f4c47f81def5bc2c49d603bb1b24391d8a4334eeab3d6274fad258aa5c6f4d5d0a6ae7405645788b54455d42d2a3a3ef8b8bde9320a029464c4163a50f14aaee77933af0c20077fe8df0439c269026c6352fa77c11bc87487c8b993185054354b694ebc3bd3492e1fdcc1d252fb0203010001a382011a30820116300f0603551d130101ff040530030101ff300e0603551d0f0101ff040403020106301d0603551d0e0416041440c2bd278ecc348330a233d7fb6cb3f0b42c80ce301f0603551d230418301680143a9a8507106728b6eff6bd05416e20c194da0fde303406082b0601050507010104283026302406082b060105050730018618687474703a2f2f6f6373702e676f64616464792e636f6d2f30350603551d1f042e302c302aa028a0268624687474703a2f2f63726c2e676f64616464792e636f6d2f6764726f6f742d67322e63726c30460603551d20043f303d303b0604551d20003033303106082b06010505070201162568747470733a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f300d06092a864886f70d01010b05000382010100087e6c9310c838b896a9904bffa15f4f04ef6c3e9c8806c9508fa673f757311bbebce42fdbf8bad35be0b4e7e679620e0ca2d76a637331b5f5a848a43b082da25d90d7b47c254f115630c4b6449d7b2c9de55ee6ef0c61aabfe42a1bee849eb8837dc143ce44a713700d911ff4c813ad8360d9d872a873241eb5ac220eca17896258441bab892501000fcdc41b62db51b4d30f512a9bf4bc73fc76ce36a4cdd9d82ceaae9bf52ab290d14d75188a3f8a4190237d5b4bfea403589b46b2c3606083f87d5041cec2a190c3bbef022fd21554ee4415d90aaea78a33edb12d763626dc04eb9ff7611f15dc876fee469628ada1267d0a09a72e04a38dbcf8bc0430012293020a0474657374121e08ec27121976a9147f20306730683c828f9a3a990193007106aaca9788ac18a393f3ae0620b69af3ae062a5a5061796d656e74207265717565737420666f722042697450617920696e766f696365205736696331725531585468456d4c414576776b6a645820666f72206d65726368616e7420536f6d6265724e696768745f74657374696e67323068747470733a2f2f746573742e6269747061792e636f6d2f692f5736696331725531585468456d4c414576776b6a64583a4c7b22696e766f6963654964223a225736696331725531585468456d4c414576776b6a6458222c226d65726368616e744964223a225372384b5774647158666b6a58563751704171773336227d450000803f2a80021f8847944b466e37c5253355156d732e2a31668ad3f1df42d3306169c16769b949f14ee035d6473fda2b74cad4e02959fb6152394ef7988bff99e846a37626819f200f8e6d38e5c9f5cda53a6628c8ddb0f665d515c5d986c314bad7b040c65f7e5acab6be87622ce038f2269fafbd20593f9ab1ea9886c5e517fb1773023821346d0e71d53e89a51a6fa999288afa5d6e29a5d20f3aa3ccea96323fd3a93786be109536a78c23d44923ea0803f2f12edb1fe61a991ec8681bae682027e8bbee97d8c1e0a1401b83c69d1f13dc03c70ee0edc25b34c127f208a419c96a4c6cf471b2d6bbcbe9f1eab9780e87dbfda5bc30d67c08708e1ca6c9e962b541e0b044\",\n            \"exp\": 915,\n            \"height\": 2579583,\n            \"lightning_invoice\": null,\n            \"message\": \"Payment request for BitPay invoice W6ic1rU1XThEmLAEvwkjdX for merchant SomberNight_testing\",\n            \"outputs\": [\n                [\n                    0,\n                    \"ms78fxCmtdXSeAkEYrWzb42KpUjesW9pAk\",\n                    5100\n                ]\n            ],\n            \"time\": 1708968355\n        },\n        \"38db6963e70c49bf3885a2147214febf63f097e8f175f72759c57bbd88b9b417\": {\n            \"amount_msat\": 3000000000,\n            \"bip70\": null,\n            \"exp\": 3600,\n            \"height\": 0,\n            \"lightning_invoice\": \"lntb30m1pjaejj3pp58rdkjcl8p3ym7wy95g28y987ha3lp9lg796lwf6ec4ammz9ekstsdpyxysysctvvcsxzgz5dahzqmmxyppk7enxv4jsxqrrsscqp79qy9qsqsp56ttzywd32dynd404fhnak56nugjtqlw4vr3djuxmx6tjsl246qaqyfl5ffxmu6fcgddf3wtu0deyhz9n9agy86t2g0h7q5nhnxuz5q5zalk2p03c4whyfj4533tf7g4e3ac2ckzyrpt9pn2fycrj7yw2p9gqalt34k\",\n            \"message\": \"1 Half a Ton of Coffee\",\n            \"outputs\": null,\n            \"time\": 1708968529\n        },\n        \"4b56023361\": {\n            \"amount_msat\": \"!\",\n            \"bip70\": null,\n            \"exp\": 0,\n            \"height\": 2579583,\n            \"lightning_invoice\": null,\n            \"message\": \"complex invoice that I am saving now, heh\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1q327fh2mle04q0ecrmd23j40tfdt7d4wy3vzcgn\",\n                    100000\n                ],\n                [\n                    2,\n                    \"6a04deadbeef\",\n                    0\n                ],\n                [\n                    0,\n                    \"tb1qjv7a78ea0jp9d793d5ra7mtzkjzezwldz5zvr6\",\n                    \"2!\"\n                ],\n                [\n                    0,\n                    \"tb1qsd2c4xwg47hnngn2uqg66y5rxz2hp074u9rq6r\",\n                    \"!\"\n                ]\n            ],\n            \"time\": 1708968026\n        },\n        \"6058a43f59d04f9470a031a5d90e92050032d51ffcc162eeadd0fab678a5282e\": {\n            \"amount_msat\": 1000000,\n            \"bip70\": null,\n            \"exp\": 3600,\n            \"height\": 0,\n            \"lightning_invoice\": \"lntb10u1pjaejrhpp5vpv2g06e6p8egu9qxxjajr5jq5qr94gllnqk9m4d6ratv7999qhqdq5xysyymr0ddskxcmfdehsxqrrsscqp79qy9qsqsp54lwu0yns8g637z2m4jy08dcda45tmuy73gljscw893eqg5ek8ezqvmt89kxcmxsgq0mf7xlgl8pz7axkj9y5tv7nyvg7zakldxw9a79rycup0gv78nxcgs6qkcrhgcjv7esl53uz4gpawplzxjn5985xx9sq4lr4t5\",\n            \"message\": \"1 Blokaccino\",\n            \"outputs\": null,\n            \"time\": 1708968055\n        },\n        \"60d6706211\": {\n            \"amount_msat\": 100000000,\n            \"bip70\": null,\n            \"exp\": 0,\n            \"height\": 2577034,\n            \"lightning_invoice\": null,\n            \"message\": \"self8\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1qplsf242vay6vavy4eguef855fx3klmp9505g88\",\n                    100000\n                ]\n            ],\n            \"time\": 1707140611\n        },\n        \"720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\": {\n            \"amount_msat\": 10000000,\n            \"bip70\": null,\n            \"exp\": 3153600000,\n            \"height\": 0,\n            \"lightning_invoice\": \"lntb100u1pjaekd5pp5wgyy3w2lg9eku67usw85tplxypwyzaczx5lnl47ek6cuad3juedssp54urtt78vsz6skchpkd9xdtyfjrt9qlat6qdr233fq77z9kr5m72sdqjdpjkcmr0yp6xsetjv5cqzynxq8zals8sq9qlzqqqqqqqqqqqqqqqqqqqqqqqqqq9scqfppqs7nz7zggvsljj76uw5unet2x25flvw5przjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng8vsq5qqq2qqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cngq3etvuwr5lpagqqqqlgqqqqqeqqjqqey9vw6ysj7ttgsz0wm6csuum92fx2znycruz4f9m8lmu2xr9czyqzrr4d7an54asqpahgchcgglvam5n5g5lgcezah5gf74mmk8dhcp2jjhdv\",\n            \"message\": \"hello there\",\n            \"outputs\": null,\n            \"time\": 1708972468\n        },\n        \"7cf4953836\": {\n            \"amount_msat\": 5100000,\n            \"bip70\": \"0801120b783530392b7368613235361a9e160ac40c3082064030820528a0030201020208138720f4708a0b91300d06092a864886f70d01010b05003081b4310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e312d302b060355040b1324687474703a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f313330310603550403132a476f2044616464792053656375726520436572746966696361746520417574686f72697479202d204732301e170d3230303830323136323433305a170d3231313030313231313333385a303d3121301f060355040b1318446f6d61696e20436f6e74726f6c2056616c696461746564311830160603550403130f746573742e6269747061792e636f6d30820122300d06092a864886f70d01010105000382010f003082010a0282010100d14377b6f2b50aa73427c248915563d2c8804dd824b1d8f015dd1ae2a390c8022fd963355849b217309208c3450a016ff4001ee336a3189edadce12bc41e4999e45f6fcf0b00daa7e9a4b12e72dce19c51ae8e55b8bfec02b80a58ccb2c1b3e5bde81f679b9993bf52c871a1fadef6bb5f7d8d9208889400ba2be1d2baf82ec303470852570c3bbb6b89334d8974e61b8867bf299fb802c57e9b00d9b9f70572ac6d81fecd304c83aaf21f4f3b529e9898ea9b868f8f07b4189668e71854ae776bacd0d9706a8be03f528c68ad023e3b45bfa55e9b42e535aafc7eb8672645dcdeaf7204a468d2b84f27ed12072a411627647108e421abe7308e3bac305896f10203010001a38202ca308202c6300c0603551d130101ff04023000301d0603551d250416301406082b0601050507030106082b06010505070302300e0603551d0f0101ff0404030205a030380603551d1f0431302f302da02ba0298627687474703a2f2f63726c2e676f64616464792e636f6d2f676469673273312d323137372e63726c305d0603551d20045630543048060b6086480186fd6d010717013039303706082b06010505070201162b687474703a2f2f6365727469666963617465732e676f64616464792e636f6d2f7265706f7369746f72792f3008060667810c010201307606082b06010505070101046a3068302406082b060105050730018618687474703a2f2f6f6373702e676f64616464792e636f6d2f304006082b060105050730028634687474703a2f2f6365727469666963617465732e676f64616464792e636f6d2f7265706f7369746f72792f67646967322e637274301f0603551d2304183016801440c2bd278ecc348330a233d7fb6cb3f0b42c80ce302f0603551d1104283026820f746573742e6269747061792e636f6d82137777772e746573742e6269747061792e636f6d301d0603551d0e041604142cd8def7d64c620cce78bdec365fd961cfd035e130820103060a2b06010401d6790204020481f40481f100ef007500f65c942fd1773022145418083094568ee34d131933bfdf0c2f200bcc4ef164e300000173affd2c3f000004030046304402202ab9dfff3fc828a44a12dfeb3ce37f70a1dd0c8bd86c0083431aa3f50abaa469022049aa04fa3ae4325ff3a1539015e454960606e569fa3de4e650e7b6329118c4f50076005cdc4392fee6ab4544b15e9ad456e61037fbd5fa47dca17394b25ee6f6c70eca00000173affd2d7a0000040300473045022100ed2170727ac7ac9ef1eb82a598d5959721e236ebd5a9c1d38cc46848bd61582902205fc1840c8d4ca52a9162a61b4630ec81e9e2f43dc4188262c5cf309af8aaebf9300d06092a864886f70d01010b05000382010100492841f57eba01263ab062521d250aa679d53cf01c8107f86d4b00b6fd1bd778262046807f364d048cb99d19c1372e6aec3f9cddd023d4189f16a38301d9ca72d59a97a1f2b226a1501c8833397aa8498e824be4aa61c1f7f0b6a603cd7fba19077f2c623f1d09185ec634e83de34e8fa8cc0eb2f296a319098779cb2fd93149ad868940eb37054246d522ee00b28155b95a0d753e26c6d96978bfe0809d1946252e033f806d1872ba4acb83284d66608bbb57d489203cb2fc3d49d235d934111d5f9420719279d545d1eb0ab9ad1a57bc9b6d1a7d256db29a3e49ed206e21e3eb352effbbfb531ba0ceb4c70b1339e89f39428fcd4651dfb20748ab810be9af0ad409308204d0308203b8a003020102020107300d06092a864886f70d01010b0500308183310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e3131302f06035504031328476f20446164647920526f6f7420436572746966696361746520417574686f72697479202d204732301e170d3131303530333037303030305a170d3331303530333037303030305a3081b4310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e312d302b060355040b1324687474703a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f313330310603550403132a476f2044616464792053656375726520436572746966696361746520417574686f72697479202d20473230820122300d06092a864886f70d01010105000382010f003082010a0282010100b9e0cb10d4af76bdd49362eb3064b881086cc304d962178e2fff3e65cf8fce62e63c521cda16454b55ab786b63836290ce0f696c99c81a148b4ccc4533ea88dc9ea3af2bfe80619d7957c4cf2ef43f303c5d47fc9a16bcc3379641518e114b54f828bed08cbef030381ef3b026f86647636dde7126478f384753d1461db4e3dc00ea45acbdbc71d9aa6f00dbdbcd303a794f5f4c47f81def5bc2c49d603bb1b24391d8a4334eeab3d6274fad258aa5c6f4d5d0a6ae7405645788b54455d42d2a3a3ef8b8bde9320a029464c4163a50f14aaee77933af0c20077fe8df0439c269026c6352fa77c11bc87487c8b993185054354b694ebc3bd3492e1fdcc1d252fb0203010001a382011a30820116300f0603551d130101ff040530030101ff300e0603551d0f0101ff040403020106301d0603551d0e0416041440c2bd278ecc348330a233d7fb6cb3f0b42c80ce301f0603551d230418301680143a9a8507106728b6eff6bd05416e20c194da0fde303406082b0601050507010104283026302406082b060105050730018618687474703a2f2f6f6373702e676f64616464792e636f6d2f30350603551d1f042e302c302aa028a0268624687474703a2f2f63726c2e676f64616464792e636f6d2f6764726f6f742d67322e63726c30460603551d20043f303d303b0604551d20003033303106082b06010505070201162568747470733a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f300d06092a864886f70d01010b05000382010100087e6c9310c838b896a9904bffa15f4f04ef6c3e9c8806c9508fa673f757311bbebce42fdbf8bad35be0b4e7e679620e0ca2d76a637331b5f5a848a43b082da25d90d7b47c254f115630c4b6449d7b2c9de55ee6ef0c61aabfe42a1bee849eb8837dc143ce44a713700d911ff4c813ad8360d9d872a873241eb5ac220eca17896258441bab892501000fcdc41b62db51b4d30f512a9bf4bc73fc76ce36a4cdd9d82ceaae9bf52ab290d14d75188a3f8a4190237d5b4bfea403589b46b2c3606083f87d5041cec2a190c3bbef022fd21554ee4415d90aaea78a33edb12d763626dc04eb9ff7611f15dc876fee469628ada1267d0a09a72e04a38dbcf8bc0430012293020a0474657374121e08ec27121976a91411d7bafbe8214c3538012545ef3017b6b65fbdc588ac18de91f3ae0620c199f3ae062a5a5061796d656e74207265717565737420666f722042697450617920696e766f69636520396f63615a3333387538756e6f704e58384d656b615620666f72206d65726368616e7420536f6d6265724e696768745f74657374696e67323068747470733a2f2f746573742e6269747061792e636f6d2f692f396f63615a3333387538756e6f704e58384d656b61563a4c7b22696e766f6963654964223a22396f63615a3333387538756e6f704e58384d656b6156222c226d65726368616e744964223a225372384b5774647158666b6a58563751704171773336227d450000803f2a800251046f14d47a5d588cc74930e4fd002e92710419c7ab67384aaa8130bb24cabd79bb1e76f4f64a185845992690a5f39d6fffbd34fae96c9fde42e6f7e263ce0c3b5d59ac68037a0c1e072dcb96e14939c20db38be84e73c34b728dd2e43ff36bafa46e5710b7386228005e737655f089924d1633405c39c9dd11467ddeb5b0a0de936564130d96413e5504111f3a2e9e8998a8d82f5afa3de22ae9f62a45cf338054768ae2ceaaa3217f8c09b9556422a47a17a5c3c4a65a4d3ad64febed0db4ead40767305640e4752cce5e0ffb9a2ab7e76cc34f9b2d2b2446e097775740aca1e4b6cf237e5d96c9b0b58cd46a890e75178318f13432dcc93c8ba5f7b9ff0a\",\n            \"exp\": 995,\n            \"height\": 2579583,\n            \"lightning_invoice\": null,\n            \"message\": \"Payment request for BitPay invoice 9ocaZ338u8unopNX8MekaV for merchant SomberNight_testing\",\n            \"outputs\": [\n                [\n                    0,\n                    \"mh9JDXVZ5SPhXEt3fmKvMKqxyXohJs2M6h\",\n                    5100\n                ]\n            ],\n            \"time\": 1708968158\n        },\n        \"8da0e9b62fb695600d85dd65dbf8d6dc31239ef3bef429cdc3b3e555054c0f4e\": {\n            \"amount_msat\": 1000000,\n            \"bip70\": null,\n            \"exp\": 3600,\n            \"height\": 0,\n            \"lightning_invoice\": \"lntb10u1pjaejn9pp53kswnd30k62kqrv9m4jah7xkmscj88hnhm6znnwrk0j42p2vpa8qdq5xysyymr0ddskxcmfdehsxqrrsscqp79qy9qsqsp52h7895hwlulrq2736ksk7rucep9mjnltt98a64lq0p7y98w0qzrq0l64sqvrwsur5nxtdvp0503xuy3tl82fzg93w0mmnyp8v0qj6mhqd2ctcff3k7e9zdzm3aa447styh322j6zg7n30eamq2e873rtcgcq9jzygc\",\n            \"message\": \"1 Blokaccino\",\n            \"outputs\": null,\n            \"time\": 1708968549\n        },\n        \"9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921\": {\n            \"amount_msat\": 1000000,\n            \"bip70\": null,\n            \"exp\": 3600,\n            \"height\": 0,\n            \"lightning_invoice\": \"lntb10u1pjae5v4pp5n5txl639wgxjquggen3yy3lkc4fl7hkea7phg6u5qp3pg4kcfyssdq5xysyymr0ddskxcmfdehsxqrrsscqp79qy9qsqsp5zevepzucm46xw7g8mcxjhrd2w90pvkmnshcevgm2hajnpr5mqnhq0putmsqtps2ktwug33ulpkv3v0xrudyddsgay5rfsfljzhs0h373yd4eafrzfutzmqtxgkdhfrye6ta89vvmryr0k5v3sp8uyvpkwnqpu7gnlq\",\n            \"message\": \"1 Blokaccino\",\n            \"outputs\": null,\n            \"time\": 1708970389\n        },\n        \"a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f\": {\n            \"amount_msat\": 1000000,\n            \"bip70\": null,\n            \"exp\": 3600,\n            \"height\": 0,\n            \"lightning_invoice\": \"lntb10u1pjae5ddpp553dj8uz728c2cgnk8ns68upg7v992x0kw7vrnlw3m78aqdllua0sdq5xysyymr0ddskxcmfdehsxqrrsscqp79qy9qsqsp5y30eg9jka2ysvxjmwe75q45dyng8w235r62datfl2f4r8kyz8qqqc3lytjlmap9f5dhyzqq57kcn3sqj7h7exz6kn0q7t0jfwh44330hvzmneyu8035dzvjv0k4uy8my8hd7xk8v4chz9u3rr2cdnkky74cqapgfdw\",\n            \"message\": \"1 Blokaccino\",\n            \"outputs\": null,\n            \"time\": 1708970413\n        },\n        \"ac79af81115c8a502bc4993ec2398b42885efdce3d9584f0096f8d5495e05c4e\": {\n            \"amount_msat\": 50000000,\n            \"bip70\": null,\n            \"exp\": 3153600000,\n            \"height\": 0,\n            \"lightning_invoice\": \"lntb500u1pjaektupp543u6lqg3tj99q27ynylvywvtg2y9alww8k2cfuqfd7x4f90qt38qsp5p6ynhghrc6a97krp28q0e4ratq7farjshm8wep4n0sq5pdzsm73qdqsda3xjampdcunsdpkcqzynxq8zals8sq9qlzqqqqqqqqqqqqqqqqqqqqqqqqqq9scqfppqqjnrm8m6kvte0g4yxh5230dd28kw8ll5rzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng8vsq5qqq2qqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cngq3etvuwr5lpagqqqqlgqqqqqeqqjqzeye3m5mz64l8yv3tgnmmrlar589v0cd90qwf4tzc4vdyh9szwyyjp8ppjzcl3jgshkn36zyehqz6974pqy3kr2s4pkyfst9v9mphdgpa3jag0\",\n            \"message\": \"obiwan9846\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1qqjnrm8m6kvte0g4yxh5230dd28kw8ll5es4hqk\",\n                    50000\n                ]\n            ],\n            \"time\": 1708972412\n        },\n        \"b57bcb58ad\": {\n            \"amount_msat\": 200000000,\n            \"bip70\": null,\n            \"exp\": 0,\n            \"height\": 2579583,\n            \"lightning_invoice\": null,\n            \"message\": \"simple invoice\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1qldfm352fwhu9jq36e7l7dnvvd4phnuf3f5tt67\",\n                    200000\n                ]\n            ],\n            \"time\": 1708968042\n        }\n    },\n    \"keystore\": {\n        \"derivation\": \"m/0h\",\n        \"pw_hash_version\": 1,\n        \"root_fingerprint\": \"535e473f\",\n        \"seed\": \"9dk\",\n        \"seed_type\": \"segwit\",\n        \"type\": \"bip32\",\n        \"xprv\": \"vprv9FrABTX8HFeSYL9aaMnLRcEkHBbJnBu9foDJaTvcF8SLvHx6uKqL8rtt7kTd66V4QPLfWPaCJMVZa3h9zuzLr7YFZd1uoEevqqyxp66oSbN\",\n        \"xpub\": \"vpub5UqWay427dCjkpE3gPKLnkBUqDRoBed1328uNrLDoTyKo6HFSs9agfDMy1VXbVtcuBVRiAZQsPPsPdu1Ge8m8qvNZPyzJ4ecPsf6U1ieW4x\"\n    },\n    \"labels\": {\n        \"005c586950a2651ab6074be2999cce05dfbbc8fd55494364b1a76e6495fe7d8f\": \"\",\n        \"010319d78093aafa6e96466ec432609dbf038163dcf149c4bbb276b0c433ea5c\": \"\",\n        \"022d7c0bad2c680bb7737334e9e2598b7857a1820a85f7e490577497697548c7\": \"dsadsa6\",\n        \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": \"trans 3\",\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": \"Dsdhahuisad\",\n        \"047bc78c0e5df336b69ff557d349a7390fcaecd72530c399ffca5f28c6332cca\": \"dsadsa5\",\n        \"05d15cb10ed73b0156508eebc7e9d2be8acc66977b6729fefcca246cf1013b10\": \"fdsfds\",\n        \"0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda\": \"obiwan9847\",\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": \"dsadsa\",\n        \"10dfc80e50af1a918feee2445e9febee9652332b41465b62d24c133c67ee9c78\": \"Send to BTC address\",\n        \"1ba407b5dd658e117cd6db010d4d6dc44be850c1458e7e03104dab90859c293d:0\": \"donkey\",\n        \"2e8d5d519a8b6d931039be01177189b745546ad032988fad26143e03c5219a51\": \"1\",\n        \"301d337a55646f0008f65704d957832e3f7d0d75000ff7a5003f36caeae6ea38\": \"Ooo\",\n        \"3c5854f3b423f71c068aa6f81ebe2cba5f340ae7886754415c1c181f3180bbb2\": \"dsadsa3\",\n        \"3ca09d669bee0053b8b2631098b24e9930c7bc85faa4307246a6ce07f65d3aa6\": \"Bbb\",\n        \"3ceedd6e2d9530e407de3c5daaef082da40db1e7a506a7fbed3bc276229db93f\": \"Miner fee for sending to BTC address\",\n        \"4036d7a9e9a0cd0aae5d2f82369ce724e749cff02da299294261e148ed45096e\": \"dsadsa111\",\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": \"some description here. much creativity\",\n        \"42898c5f074d25e859e25bb6919372a7cb23303c8b9e237bbaf4d46a9388e1e2\": \"abc\",\n        \"49cd7d5ec851ecfe5a0f09767233bde32af2b29c4724048d1ac058d8def59ee3:1\": \"JOHN\",\n        \"534983338aedbad8966d48237af41f0c91682d7e15bd981a85cd6891f0d57047\": \"2\",\n        \"5455c9168b4bd2ab59990c907c85abda8cf64117000d35e71cd25da50109711f\": \"3\",\n        \"6504921d5e24366cba7c09b75279c26bbf2c10fb1ade28cadcb855bbf1f14ec4\": \"dsadsa424242\",\n        \"68d57cd697984f71719b5b6f147bbac45d00b3c6832a88c05d618dfceacf0dd7\": \"bhubuh\",\n        \"695f710e1bf7ed848e4f243eb2d3438098e01f9feb512c4d1923449b96a7c661\": \"\",\n        \"6a4cd838730a107be350d503e299f375c99c653c119aa948fd76956ae65bd5c1\": \"dsadsaddd\",\n        \"720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\": \"hello there\",\n        \"7c76a71b5ae8a57699dba33d751f42b58c6e87830c7cf8872e6a8efa4c1638d3\": \"\",\n        \"7f4f0539518dd92142b5bd40f3392982e229230b597621d1a731ab6dbc6c0987\": \"5\",\n        \"858d65d9767ffcdf61f7efe0e3437da7bd9a63589339571e75f0a5c3176346ac\": \"1\",\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": \"asd\",\n        \"8a5d23b9c90bbdf17dc765124313fde6185a6b4733a548d050656739ae8ccfbb\": \"2\",\n        \"91519a762f3fcdee50a7732cc47a95ce88edf68a73fa1dd808e5cf45d1539d1d\": \"fdsfdsf\",\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": \"Open channel\",\n        \"96d0a96238295380b7623b1167331ba10b9ab1c9689c832d3726a58739d3a48e\": \"dsadsa1\",\n        \"971887db6088cb5fb63fdd98f37eb508a14626e23d1714b09917d894defe4589\": \"buy\",\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": \"Open channel\",\n        \"9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921\": \"1 Blokaccino\",\n        \"a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f\": \"1 Blokaccino\",\n        \"aa7d5fdf59635630b6c4442f6f7e920331e0357017bfb60f0185d6cebe7e40bd\": \"dsadsa111\",\n        \"aaa87d23dc4e2ce0761387da8054bdd7d8a957e73c4f9d1c464733e0416d9038\": \"\",\n        \"ab513232e8838123f5eb598c7ccb0769c41e8a7a20069a34e13799af12045cd3\": \"dsa\",\n        \"b4c18d5fc3a9787fb6698f73d00c227cba62dc49448b8738e61d45ad65ff8bb0\": \"dsadsa4\",\n        \"b90a9be31cbd6e1ae14db818740df9704e9118940402456f1dc4ef7e3b887a73\": \"Donkey\",\n        \"bb3b9454a2a725184897ce93ef824bd3723be62bf78cd8a69f199de99350ca30\": \"Send to BTC address\",\n        \"bcrt1q069xqa4lej2tljmd8fcvvfedav54nmspwm2y8r\": \"\",\n        \"bcrt1q07ulrxeuu45uqen0clqe85v5en6rf77cypla9a\": \"\",\n        \"bcrt1q0quewquwhlfgahhsdg0q3r5lmyzufrtpnqmkwu\": \"\",\n        \"bcrt1q3t0xcpmzreece8xdxq8k5aaxrt3r623tzk5vs6\": \"\",\n        \"bcrt1q6k5h4cz6ra8nzhg90xm9wldvadgh0fptfqj60p\": \"dshsdu\",\n        \"bcrt1qchyc02y9mv4xths4je9puc4yzuxt8rfmgnqych\": \"\",\n        \"bcrt1qd7tjvgttaxzkszzh5ty4yq97r8wscgtemm9au3\": \"\",\n        \"bcrt1qdy4xwmgklqmyrfj336g4f54582zxtm2y4k35sk\": \"\",\n        \"bcrt1qf03zdjdnzxwztxs9d3g9ynsvvs5rmjhvfjx7xa\": \"\",\n        \"bcrt1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxp7gsyw\": \"\",\n        \"bcrt1qgcgk7j9kpt2mygmhmnu4zep79cd289t6lsxfft\": \"dsadsa\",\n        \"bcrt1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s477hsl4\": \"\",\n        \"bcrt1qkneqe450eqxtpr3r5z8aw4234sjmpknmdpla2s\": \"\",\n        \"bcrt1qkp86pkt75ds257snenp3q7vs29pf4g6ce7a8q8\": \"\",\n        \"bcrt1qm7ckcjsed98zhvhv3dr56a22w3fehlkxx7vrly\": \"\",\n        \"bcrt1qn7d2x7272lznt5hhk9s07q3cqnrqljnwladrd3\": \"dsadsaddsdsa\",\n        \"bcrt1qplsf242vay6vavy4eguef855fx3klmp9kxd9sw\": \"a\",\n        \"bcrt1qq0gdz0vz02ypa3cawlljstrx8cxydhvaa342e3\": \"\",\n        \"bcrt1qq2tmmcngng78nllq2pvrkchcdukemtj5jnxz44\": \"\",\n        \"bcrt1qr7mjlxgc6at67tx0s8ypa5efx8clc47x4nalhp\": \"donke\",\n        \"bcrt1qrdzfu6mlgrxpupd4syxrv77ncku89a0yd95n7c\": \"\",\n        \"bcrt1qs087qkcawefutkkv8pg037t6txldk5szt6jsvm\": \"\",\n        \"bcrt1qt339ksrha0n5a6lwpql778erkm272hxglj54c4\": \"\",\n        \"bcrt1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sll870p\": \"\",\n        \"bcrt1quhk94rhlsflc4wgxl9qzd6p6wszt30uxfuyznm\": \"\",\n        \"bcrt1qusm48zmlzwr32csxdw4ar7atw260h22c9ten9l\": \"hihi hi4655\",\n        \"bcrt1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpwp9f0z\": \"\",\n        \"bcrt1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsf57wq4\": \"ddddd\",\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": \"Open channel\",\n        \"ce0bea6a71ef5d8649e67a3235a1d1f5f8611e47725d54cb16d5f9ebabd92c8a\": \"Miner fee for sending to BTC address\",\n        \"d0ae4c76f769dccfa0187633839d704ae803e810fd592e4f2d14178aaacc94d6:1\": \"JOHN\",\n        \"d8a07501eb18302f5b4fa0535c1a7a71eee058de1013f0c2e2ceb53ca50bb99c\": \"dsadsa\",\n        \"eaa745cef67b98bcabd604598406cd782cfafc99a419c2c7a8a1dde163cf7dab\": \"44\",\n        \"fdf0d67d412785e337b2883cf56784345e215513331d966c5ee875ff6d48b38d\": \"dsjaiodsa\",\n        \"tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r\": \"at some point there was some change here\",\n        \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\": \"old address 95645\"\n    },\n    \"lightning_payments\": {\n        \"0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda\": [\n            50000000,\n            -1,\n            3\n        ],\n        \"1f7797ab2b374a2948152bcdef2f6f109ae426c4c2a98217d70a47f377914684\": [\n            500000000,\n            1,\n            0\n        ],\n        \"20eaf11cd4b70bc776762e9a6d8bfd71c11b8d84d98e05971462b8fa7d7213bd\": [\n            600000000,\n            1,\n            0\n        ],\n        \"2b870b931dc3524a339f60a7e25990deddcf84a6fe29774499a0bedfa6bc3c79\": [\n            null,\n            1,\n            0\n        ],\n        \"37cfa1f06e9f0898366f27b72428f4dd80cd062ea33857840f4b08a6a6620f3a\": [\n            1000,\n            1,\n            0\n        ],\n        \"720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\": [\n            10000000,\n            -1,\n            0\n        ],\n        \"83789af4bfe11b4df0c872a41ce616b3696f435178b038248186ec2ae3b74557\": [\n            null,\n            1,\n            0\n        ],\n        \"9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921\": [\n            1000000,\n            -1,\n            3\n        ],\n        \"a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f\": [\n            1000000,\n            -1,\n            3\n        ],\n        \"cbfe7efe210475de7cc9eb40e6abac806e6bd10050a85046ace3d12682fb59b0\": [\n            null,\n            1,\n            0\n        ],\n        \"e99bc7e3afa145ff0139913ea867a15ce6053f9638ea54bcd06e9f44996905e2\": [\n            654165000,\n            1,\n            0\n        ],\n        \"fa4f9b20ab4399333e1eded38626908d21295bd27660b360c9102a2eadb250ab\": [\n            100000000,\n            1,\n            0\n        ]\n    },\n    \"lightning_preimages\": {\n        \"0834354bb383e3fefba08314b62f1ec4b0bead90a2ea977652e12f8ae930efda\": \"1b16da388f5d62648825a5022e72a27ec6e5b2242fb37adf8260306c5e544f3c\",\n        \"1f7797ab2b374a2948152bcdef2f6f109ae426c4c2a98217d70a47f377914684\": \"d42fb7744149ae37bfb1ac9643cc08ff2f20039f0b1a34f55009d9c87d7b6f69\",\n        \"20eaf11cd4b70bc776762e9a6d8bfd71c11b8d84d98e05971462b8fa7d7213bd\": \"fd5471493a955b377070d8f26257235a6b4514efabcc0c6a1eace07658dd151a\",\n        \"283096e1fa83bead0affe0d6080307216374dbf6282d507cb08e327ad1384a0a\": \"b6d74c32e3c1ed5a107357016c4cb630a845513c106c47f149c7e0b91f3b356b\",\n        \"2b870b931dc3524a339f60a7e25990deddcf84a6fe29774499a0bedfa6bc3c79\": \"5e78390e8fb1a22bce877fe13978931eaad656205664c4542171de944cabcf6b\",\n        \"37cfa1f06e9f0898366f27b72428f4dd80cd062ea33857840f4b08a6a6620f3a\": \"039d8bb06ab26974290f261aa56865e982abf03c6ac84b3b47d3e086b66ecf3e\",\n        \"83789af4bfe11b4df0c872a41ce616b3696f435178b038248186ec2ae3b74557\": \"6e70a163fc7c80680452276fbfe4bb92fed4c24ab94a6db3aba088dc0d37b556\",\n        \"9d166fea25720d207108cce24247f6c553ff5ed9ef83746b9400621456d84921\": \"4dd0ddf9605e51998936d101b7741e89be08cb788c1e36b9b1d62cd5e9543af8\",\n        \"a45b23f05e51f0ac22763ce1a3f028f30a5519f6779839fdd1df8fd037ffe75f\": \"48f81553d41291c78f5a09f21cc95eef11dd798e7abfd23fb1f1cb9eb8bf8359\",\n        \"cbfe7efe210475de7cc9eb40e6abac806e6bd10050a85046ace3d12682fb59b0\": \"b679e5385248982b930ce54d69e0dbdd77e6982fa5066b9dc955bbe64d749e40\",\n        \"e99bc7e3afa145ff0139913ea867a15ce6053f9638ea54bcd06e9f44996905e2\": \"ea65d5cfcec58946c93486361b6b04332acebc341637d85e442d988bd4e072b8\",\n        \"fa4f9b20ab4399333e1eded38626908d21295bd27660b360c9102a2eadb250ab\": \"f68f5296b4d9e9ca860dd234d025ab152edc3e857783ae8194581934889eaad6\"\n    },\n    \"lightning_xprv\": \"vprv9HAix419EKycrbTEJxT1zKCLURqA9LWRHeohrddxQ35QuvfH8fqMurdF4mseJ5oytUCJH5VvhEXSmCTs5SaoT7jEr2hcy7e8uCFLJtuDSme\",\n    \"notes_text\": \"some notes\\n\\nthat I wrote\\nto trigger some regressions in the future maybe\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nlots of newlines ^\\nand some button mashing for good measure\\n\\\"':\\n~AS@}{D:L}{ASLD}{QW#'\\\\,;lM ?><CZXNV){QWERU_(QW)}\\n{A~SDF:L>~SA@X:\\nC>LVZX@~<MFGVSDPMF@PSD\\n\\n\\nUNICODE_HORROR == '\\u20bf \\ud83d\\ude00 \\ud83d\\ude08     \\u3046 \\u3051\\u305f\\u307e \\u308f\\u308b w\\u0362\\u0362\\u035dh\\u0361o\\u0362\\u0361 \\u0338\\u0362k\\u0335\\u035fn\\u0334\\u0358\\u01ebw\\u0338\\u031bs\\u0358 \\u0300\\u0301w\\u0358\\u0362\\u1e29\\u0335a\\u0489\\u0321\\u0362t \\u0327\\u0315h\\u0301o\\u0335r\\u034f\\u0335rors\\u0321 \\u0336\\u0361\\u0360l\\u012f\\u0336e\\u035f\\u035f \\u0336\\u035din\\u0362 \\u034ft\\u0315h\\u0337\\u0321\\u035fe \\u035f\\u035fd\\u031ba\\u035cr\\u0315\\u0361k\\u0322\\u0328 \\u0361h\\u0334e\\u034fa\\u0337\\u0322\\u0321rt\\u0301\\u034f \\u0334\\u0337\\u0360\\u00f2\\u0335\\u0336f\\u0338 u\\u0327\\u0358n\\u00ed\\u031b\\u035cc\\u0362\\u034fo\\u0337\\u034fd\\u0338\\u0362e\\u0321\\u035d?\\u035e'\\n\\n\\n\\n\\nthe end?\",\n    \"num_parents\": {\n        \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": 1,\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": 9,\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": 3\n    },\n    \"onchain_channel_backups\": {},\n    \"payment_requests\": {\n        \"1f7797ab2b374a2948152bcdef2f6f109ae426c4c2a98217d70a47f377914684\": {\n            \"amount_msat\": 500000000,\n            \"bip70\": null,\n            \"exp\": 604800,\n            \"height\": 2579581,\n            \"message\": \"fund me pls\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1qplsf242vay6vavy4eguef855fx3klmp9505g88\",\n                    500000\n                ]\n            ],\n            \"payment_hash\": \"1f7797ab2b374a2948152bcdef2f6f109ae426c4c2a98217d70a47f377914684\",\n            \"time\": 1708966429\n        },\n        \"20eaf11cd4b70bc776762e9a6d8bfd71c11b8d84d98e05971462b8fa7d7213bd\": {\n            \"amount_msat\": 600000000,\n            \"bip70\": null,\n            \"exp\": 0,\n            \"height\": 2425298,\n            \"message\": \"test11\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07\",\n                    600000\n                ]\n            ],\n            \"payment_hash\": \"20eaf11cd4b70bc776762e9a6d8bfd71c11b8d84d98e05971462b8fa7d7213bd\",\n            \"time\": 1679412397\n        },\n        \"2b870b931dc3524a339f60a7e25990deddcf84a6fe29774499a0bedfa6bc3c79\": {\n            \"amount_msat\": 0,\n            \"bip70\": null,\n            \"exp\": 0,\n            \"height\": 2282893,\n            \"message\": \"\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8\",\n                    0\n                ]\n            ],\n            \"payment_hash\": \"2b870b931dc3524a339f60a7e25990deddcf84a6fe29774499a0bedfa6bc3c79\",\n            \"time\": 1656342706\n        },\n        \"37cfa1f06e9f0898366f27b72428f4dd80cd062ea33857840f4b08a6a6620f3a\": {\n            \"amount_msat\": 1000,\n            \"bip70\": null,\n            \"exp\": 0,\n            \"height\": 2315899,\n            \"message\": \"dust\",\n            \"outputs\": [],\n            \"payment_hash\": \"37cfa1f06e9f0898366f27b72428f4dd80cd062ea33857840f4b08a6a6620f3a\",\n            \"time\": 1660226295\n        },\n        \"83789af4bfe11b4df0c872a41ce616b3696f435178b038248186ec2ae3b74557\": {\n            \"amount_msat\": null,\n            \"bip70\": null,\n            \"exp\": 604800,\n            \"height\": 2577034,\n            \"message\": \"\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1qplsf242vay6vavy4eguef855fx3klmp9505g88\",\n                    0\n                ]\n            ],\n            \"payment_hash\": \"83789af4bfe11b4df0c872a41ce616b3696f435178b038248186ec2ae3b74557\",\n            \"time\": 1707140595\n        },\n        \"cbfe7efe210475de7cc9eb40e6abac806e6bd10050a85046ace3d12682fb59b0\": {\n            \"amount_msat\": 0,\n            \"bip70\": null,\n            \"exp\": 0,\n            \"height\": 2425401,\n            \"message\": \"\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg\",\n                    0\n                ]\n            ],\n            \"payment_hash\": \"cbfe7efe210475de7cc9eb40e6abac806e6bd10050a85046ace3d12682fb59b0\",\n            \"time\": 1679486061\n        },\n        \"e99bc7e3afa145ff0139913ea867a15ce6053f9638ea54bcd06e9f44996905e2\": {\n            \"amount_msat\": 654165000,\n            \"bip70\": null,\n            \"exp\": 0,\n            \"height\": 2579583,\n            \"message\": \"moar money\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3\",\n                    654165\n                ]\n            ],\n            \"payment_hash\": \"e99bc7e3afa145ff0139913ea867a15ce6053f9638ea54bcd06e9f44996905e2\",\n            \"time\": 1708967887\n        },\n        \"fa4f9b20ab4399333e1eded38626908d21295bd27660b360c9102a2eadb250ab\": {\n            \"amount_msat\": 100000000,\n            \"bip70\": null,\n            \"exp\": 3600,\n            \"height\": 2419260,\n            \"message\": \"\",\n            \"outputs\": [\n                [\n                    0,\n                    \"tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07\",\n                    100000\n                ]\n            ],\n            \"payment_hash\": \"fa4f9b20ab4399333e1eded38626908d21295bd27660b360c9102a2eadb250ab\",\n            \"time\": 1675819053\n        }\n    },\n    \"prevouts_by_scripthash\": {\n        \"08cf1ff744d66d4699c5654e162654bacc04558035de45a2f613abe5329d4286\": {\n            \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d:1\": 500000\n        },\n        \"0dbcae29aaa4fde8a0542b606d89c6ae46350f0fc7a3a957337f4d7ef03b5dd3\": {\n            \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:1\": 99810\n        },\n        \"0eaed6a38625ac78bde7585d061872cb55609f8d05c2a4b1f4230f1ea0f78a90\": {\n            \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535:1\": 7596590\n        },\n        \"102dcf626801074a661c851fcc3536c2101eaa2ec03ed03a11b9bb9c19741237\": {\n            \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269:0\": 99811\n        },\n        \"143f42f87521ff360780106377c98d2c27599b6028b7f2858fc71636f003eb9e\": {\n            \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c:1\": 8798\n        },\n        \"241de66b34b781feb863a601c2d49a05ebea8ad0c0f6760ea73e502fafba5bf7\": {\n            \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3:1\": 9598000\n        },\n        \"244501478da81fb6a3ffd88d222708dcb6194931136947873cc0fccc55f24661\": {\n            \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f:0\": 113600\n        },\n        \"2a06d09e67dc4c49523796cf899d2e4002429d9e1280456264b79d1c7caac727\": {\n            \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d:0\": 381400\n        },\n        \"3248021fc00b3f196a362dc9f940a25d488a040498a2d08b7a60aee9ac18626c\": {\n            \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e:0\": 3190000\n        },\n        \"327923b903a21b12aa85f694c8c0de952805a78e4a0164631850c5dc607360df\": {\n            \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3:0\": 200000\n        },\n        \"3641e700a31ec0763da7ecdab2809abd7e6c7668414902ea13958f12c5ca9ed1\": {\n            \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea:0\": 1387695\n        },\n        \"3658a8d8b2614ec7814d2973305e174a12f9d4e9cacf84f81b09cf2fa90d6fe2\": {\n            \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f:0\": 99889\n        },\n        \"374127d2991c7bf24c117cc4d73058d2f56987ce4591930271ae5b87599a40a2\": {\n            \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2:1\": 654165\n        },\n        \"385645f17d282282564fc60e25d026d1258b7c23074ce18127577fc6e1e5ede1\": {\n            \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69:1\": 404711\n        },\n        \"39c770e551fa2d861b8bf9be640933abbde97b6fd3d5b86e21e815b493745448\": {\n            \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba:1\": 8597295\n        },\n        \"3b64546ad58f7d7d5632bd6ecc6bd8d69bfab794674713dac2a4a260406aa792\": {\n            \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661:0\": 100000\n        },\n        \"3fea64e654f5b0ebf560cd7def07419823d048251e5cba13b9bc1d19fc5570f9\": {\n            \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce:0\": 403476\n        },\n        \"4940877cf9e1587732a4379592b0490273e92379e93e7a024656f02690a7b955\": {\n            \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4:0\": 5100\n        },\n        \"513018b2d303921ccf5194decd9e9c1f2a7f93f687ef301254fcd3a20dea0c71\": {\n            \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe:1\": 302547\n        },\n        \"54f31d22030e3b6ff2ffc9d6fef4a9184ff9bd37285c99bd405b662a4fb4b99c\": {\n            \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c:1\": 1148200\n        },\n        \"5dea1555deacfbc9698f32073bb26ac2b3a6462752816a727af8e2e288d80857\": {\n            \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f:0\": 17898379\n        },\n        \"6abaf5808033282b0d182f1393cd89d8e73245b317ee42896449206f6d67f009\": {\n            \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c:0\": 1000\n        },\n        \"6c9528c720baace52d4daf3af546fc33be00b981618807a99e088d148e3569e9\": {\n            \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1:0\": 404573\n        },\n        \"6d6a947e3b89b4901842539aa1059a8f44dafe5029c39d1bbad63bfdbf98d4f4\": {\n            \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c:0\": 10000000\n        },\n        \"76bbdc72fdc74fbb0b049d21b0f80045f2a227cde1cd57a00925a437638984b5\": {\n            \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f:0\": 3099663\n        },\n        \"80c018b3fbc6a3d64e21c8a53a4f5b7eb39cb62998272bd6e0f5a5a5b5947bf2\": {\n            \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6:1\": 6595885\n        },\n        \"846dc81dba7a3df5a44c955bc73afc9b51ab7ef56e3170b835ae9d37967e1323\": {\n            \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c:1\": 6394885\n        },\n        \"969c34a4c63b26dada2c5042db47236fe4bf0d1b16736bc644596b9e1a0863f0\": {\n            \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c:0\": 200000\n        },\n        \"9d905269b1d499af60d605b3a3c16afe21eea08dec1b2013f4707bde14b40c97\": {\n            \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c:0\": 14695224\n        },\n        \"a8445ee64c4775b070c64df9b8006788787da9734502ede664c0d17db65f9efc\": {\n            \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2:0\": 627994\n        },\n        \"af4144146cb7abcd1dd2c812bdcc9661b0611a6a70f682167bb683ea0e91b1de\": {\n            \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f:1\": 135000\n        },\n        \"b77873e2123e43f37054e972ae4685ff203b9d35c14989da321d242f8a0d0799\": {\n            \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:2\": 400000\n        },\n        \"b81fe544d6d281c5aa695c99dba16ba1fd2bd66f5d342ace52ead7c168b36e2f\": {\n            \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69:0\": 0\n        },\n        \"ba80fd5d5ce4a19c5b2750ec73e621dcc46637090dbae62d3f5bc5bb68565360\": {\n            \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4:1\": 94500\n        },\n        \"bc42e29c85e9d6096a1fd59582e91eccd045acaa7683326ea4dc10d579c0cbc4\": {\n            \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe:0\": 0\n        },\n        \"ceb18da9abab996544f4120b0b7fc221581fdf81a7ac4fc1d311ff9c3dd8571a\": {\n            \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535:0\": 1000000\n        },\n        \"cf1356579698ef6bad07be8da12e82f69687505dddffd1f31f70a626347e69bb\": {\n            \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39:1\": 9799000\n        },\n        \"d20f43d6c9c52327eb9ac64c2b9f4b2b54ef197a485936a2bb46b2a68b37168e\": {\n            \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b:1\": 988000\n        },\n        \"d47dd057642f30739fe81d4c56edcbd658ba87445ecf763edfb508b4c615581c\": {\n            \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6:0\": 1000000\n        },\n        \"d5f4d828cfea4006e972fab9e4c23176761621eb98f8b35a4b4a12671ee04453\": {\n            \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b:0\": 3199293\n        },\n        \"dadfddf343dd5eefc2dedbea6f4ad1f48e85852e60d739593ea7d821e59c0893\": {\n            \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:0\": 0\n        },\n        \"e4dbc5351a813f40d2a14dad58b8337eb7cca5071d54978f0aaae195bcd3dd20\": {\n            \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39:0\": 200000\n        },\n        \"e690891dd2a7bfa84bb93d56a9aef8fb6b4e8cdca9014b1bcfb16ab69d05f5f0\": {\n            \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e:0\": 302364\n        },\n        \"e7f95bc81c2de2279d0193d4bc6f5950fa0df01be599f5aeb4ffb63b29d7d1f7\": {\n            \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661:1\": 302700\n        },\n        \"eac5af8eeb1ff53507f99263ccfc8b8b2a85bf947634293a33c5eb70178d5875\": {\n            \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:1\": 3194603\n        },\n        \"ebc0e58e3591ec18b2bb8cce5e58dabfa7d121e026ce0683a02ca4990505aafb\": {\n            \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e:1\": 3204744\n        },\n        \"f04bf26fc16a27abaa2f10540da7a24e369e6ec408f2d901202a7c0cd4a6112d\": {\n            \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:0\": 10000\n        },\n        \"f38506e05a767c8f162b2271551c63a5ddae94e6eb395e441993de38e921fea6\": {\n            \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b:0\": 160000,\n            \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c:0\": 110000\n        },\n        \"f58156a910255e946f7cf2c7daf08ce3faeaf0deda0c9f300679ad9517ef7a68\": {\n            \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba:0\": 1000000\n        },\n        \"faf2c38001ff18046f939975176e9ea069090ed1ef4959625f24d31b3368d68f\": {\n            \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea:1\": 100000\n        },\n        \"fe0e7e43ca025de6ec189c121b245ac207f40177385e829fa725dff68e24e2ec\": {\n            \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c:1\": 15313760\n        }\n    },\n    \"qt-console-history\": [\n        \"wallet.basename.__qualname__\",\n        \"wallet.basename.__module__\",\n        \"wallet.basename.__module__ + \\\".\\\" + wallet.basename.__qualname__\",\n        \"util._event_listeners\",\n        \"wallet.basename.__module__\",\n        \"wallet.basename.__qualname__\",\n        \"wallet.get_history.__qualname__\",\n        \"wallet.get_full_history.__qualname__\",\n        \"wallet.is_up_to_date.__qualname__\",\n        \"wallet.is_up_to_date.__module__\",\n        \"util._event_listeners\",\n        \"window\",\n        \"window._list_callbacks\",\n        \"window._list_callbacks()\",\n        \"list(window._list_callbacks())\",\n        \"util._event_listeners\",\n        \"network\",\n        \"network.get_parameters()\",\n        \"getinfo()\",\n        \"add_request(\\\"0.001\\\")\",\n        \"[bitcoin.address_to_scripthash(addr) for addr in wallet.get_addresses()]\",\n        \"len(wallet.get_addresses())\",\n        \"[bitcoin.address_to_scripthash(addr) for addr in wallet.get_addresses()]\",\n        \"import gc\",\n        \"gc.collect()\",\n        \"from electrum.lnutil import ImportedChannelBackupStorage\",\n        \"encrypted_cb = \\\"channel_backup:Adn87xcGIs9H2kfp4VpsOaNKWCHX08wBoqq37l1cLYKGlJamTeoaLEwpJA81l1BXF3GP/mRxqkY+whZG9l51G8izIY/kmMSvnh0DOiZEdwaaT/1/MwEHfsEomruFqs+iW24SFJPHbMM7f80bDtIxcLfZkKmgcKBAOlcqtq+dL3U3yH74S8BDDe2L4snaxxpCjF0JjDMBx1UR/28D+QlIi+lbvv1JMaCGXf+AF1+3jLQf8+lVI+rvFdyArws6Ocsvjf+ANQeSGUwW6Nb2xICQcMRgr1DO7bO4pgGu408eYRr2v3ayJBVtnKwSwd49gF5SDSjTDAO4CCM0uj9H5RxyzH7fqotkd9J80MBr84RiBXAeXKz+Ap8608/FVqgQ9BOcn6LhuAQdE5zXpmbQyw5jUGkPvHuseR+rzthzncy01odUceqTNg==\\\"\",\n        \"ImportedChannelBackupStorage.from_encrypted_str(encrypted_cb, password=wallet.get_fingerprint())\",\n        \"wallet.get_fingerprint()\",\n        \"getmasterprivate()\",\n        \"wallet.lnworker.get_channel_by_id(bytes.fromhex(\\\"fe13acd33700e2dc135c833b790eb75e1ed71429773b36192315f3fede7f7696\\\"))\",\n        \"wallet.lnworker.get_channel_by_id(bytes.fromhex(\\\"fe13acd33700e2dc135c833b790eb75e1ed71429773b36192315f3fede7f7696\\\")).config[1]\",\n        \"wallet.lnworker.get_channel_by_id(bytes.fromhex(\\\"fe13acd33700e2dc135c833b790eb75e1ed71429773b36192315f3fede7f7696\\\")).config[1].payment_basepoint\",\n        \"wallet.lnworker.get_channel_by_id(bytes.fromhex(\\\"fe13acd33700e2dc135c833b790eb75e1ed71429773b36192315f3fede7f7696\\\")).config[1].payment_basepoint.pubkey.hex()\",\n        \"wallet.lnworker.get_channel_by_id(bytes.fromhex(\\\"fe13acd33700e2dc135c833b790eb75e1ed71429773b36192315f3fede7f7696\\\")).get_peer_addresses()\",\n        \"pa = wallet.lnworker.get_channel_by_id(bytes.fromhex(\\\"fe13acd33700e2dc135c833b790eb75e1ed71429773b36192315f3fede7f7696\\\")).get_peer_addresses()\",\n        \"list(pa)\",\n        \"util\",\n        \"util.callback_mgr\",\n        \"util.callback_mgr._running_cb_futs\",\n        \"len(util.callback_mgr._running_cb_futs)\",\n        \"config.cv.LIGHTNING_USE_GOSSIP\",\n        \"config.cv.LIGHTNING_USE_GOSSIP.get_short_desc()\",\n        \"config.cv.LIGHTNING_USE_GOSSIP.get_long_desc()\",\n        \"wallet.lnworker\",\n        \"wallet.lnworker.MPP_SPLIT_PART_FRACTION\",\n        \"wallet.lnworker.MPP_SPLIT_PART_FRACTION = 1\",\n        \"wallet.lnworker.MPP_SPLIT_PART_FRACTION\",\n        \"wallet.invoices\",\n        \"wallet._invoices\"\n    ],\n    \"reserved_addresses\": [\n        \"tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r\",\n        \"tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5\"\n    ],\n    \"seed_version\": 57,\n    \"spent_outpoints\": {\n        \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": {\n            \"0\": \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\"\n        },\n        \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": {\n            \"1\": \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\"\n        },\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": {\n            \"1\": \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\"\n        },\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": {\n            \"1\": \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\"\n        },\n        \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": {\n            \"0\": \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\"\n        },\n        \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": {\n            \"1\": \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\"\n        },\n        \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": {\n            \"1\": \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\"\n        },\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": {\n            \"0\": \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\",\n            \"1\": \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\"\n        },\n        \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": {\n            \"0\": \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\"\n        },\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": {\n            \"1\": \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\"\n        },\n        \"4a87be118635dc782e9dd8be26a5be0fa6a1c5267b9db8303f0af85fbf6173ba\": {\n            \"1\": \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\"\n        },\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": {\n            \"0\": \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\",\n            \"1\": \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\"\n        },\n        \"60850b8952f6bf52f1fca76e051888637cb38c1c8a85301c7fa5359f76d1703d\": {\n            \"1\": \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\"\n        },\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": {\n            \"0\": \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\"\n        },\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": {\n            \"1\": \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\"\n        },\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": {\n            \"1\": \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\"\n        },\n        \"820fb2b07ba74b4ac3974d2ccaaf8096290614fe7ec83266ae028d6d857b1e41\": {\n            \"1\": \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\"\n        },\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": {\n            \"1\": \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\"\n        },\n        \"8aed48c4e4ab646c1c5692487c2f1a4414221fef1805463fb21f7549b41a0ff0\": {\n            \"1\": \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\"\n        },\n        \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": {\n            \"0\": \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\"\n        },\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": {\n            \"1\": \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\"\n        },\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": {\n            \"1\": \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\"\n        },\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": {\n            \"1\": \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\"\n        },\n        \"983bbe3d0593b37996821733e53417fd06bcf559adcacbf339dd7729cb19673d\": {\n            \"0\": \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\"\n        },\n        \"a3ee0b514e9af4735097072dd06beba9b9ba02a072e242d1cfe0fce6bfad4bad\": {\n            \"1\": \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\"\n        },\n        \"aff92372faa2717819781f1b9a8771abd3fa20f5b504b682d264cc9575acca19\": {\n            \"0\": \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\"\n        },\n        \"b30af3bfa76e031e5aea6c2c5cb7cbcd57a145697b74e820ba2a3a5858bdc98c\": {\n            \"1\": \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\"\n        },\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": {\n            \"1\": \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\"\n        },\n        \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": {\n            \"0\": \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\"\n        },\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": {\n            \"1\": \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\"\n        },\n        \"ca675c39c9f64bce9ede8f551d2b5871da2aa4747322a2f75878ad76b0b645d9\": {\n            \"1\": \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\"\n        },\n        \"d1d575990837cfb5b93d92ba4e9b0454b385fe3b865b98088d68b1778b8855e8\": {\n            \"0\": \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\"\n        },\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": {\n            \"0\": \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\"\n        },\n        \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": {\n            \"0\": \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\",\n            \"1\": \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\"\n        },\n        \"f2dbe14e9f10e068c6bd4a534d8c8a2945c6bcd69cc85b4d5eb37aca4d3b5384\": {\n            \"0\": \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\"\n        }\n    },\n    \"stored_height\": 2579588,\n    \"submarine_swaps\": {},\n    \"transactions\": {\n        \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": \"01000000000101e855888b77b1688d08985b863bfe85b354049b4eba923db9b5cf37089975d5d10000000000fdffffff0280969800000000001600140297bde2689a3c79ffe050583b62f86f2d9dae5460abe9000000000016001472df47551b6e7e0c8428814d2e572bc5ac773dda024730440220383efa2f0f5b87f8ce5d6b6eaf48cba03bf522b23fbb23b2ac54ff9d9a8f6a8802206f67d1f909f3c7a22ac0308ac4c19853ffca3a9317e1d7e0c88cc3a86853aaac0121035061949222555a0df490978fe6e7ebbaa96332ecb5c266918fd800c0eef736e7358d1400\",\n        \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": \"0200000000010119caac7595cc64d282b604b5f520fad3ab71879a1b1f78197871a2fa7223f9af0000000000fdffffff02d8d105000000000016001440bb0029ce2482a73fff367f0f7ea2390d7a1c5f20a10700000000001600140fe095554ce934ceb095ca39949e9449a36fec25024730440220582ac0428398fb55846cb570e26fd78566b4bb67febfc3c835057e74f1e03ee0022022aa084ba38768a4ce451face547fd9217684e5fefebc29853ffc38ee9eed0ee0121031284572b5ebec99319a87ec710d162a9d21e0afa2a89f8a5cbe290f95369a22e7d5c2700\",\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": \"01000000000101399d64669c44e99825ddef70e299d85c9235030f9d75c2ac3103d23616c9f10e0100000000feffffff02400d0300000000002200205208fb0ad888e82e4452ab6cbb06a51c09ca36c6f2a86e5e81884de1860cf400307492000000000016001496ea3197df375c6a62188cf9119aee984c0cc6150247304402203e535dd045e4e7d062af6d11888643ddce48649e123abecf6a42f6048f892f18022076b9e21191096e9c1800904e24fce6758a7d93e8795d35c161a334b83e1286860121030644479c1b8c106e2f0a495fe18dc45572b3413bda2db919c1fcf0f77f43241ca18d1400\",\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": \"010000000001012ca35f85d1189b6cdc8d0c95fe637c23928fb23dbc492f9d078f85e9ea6444020000000000feffffff02400d0300000000002200203bf21a5a90ce63776ecf8216ad92b0afc831c1d5fd1d213d29f916181aac74a958859500000000001600147fb9f19b3ce569c0666fc7c193d194ccf434fbd80247304402200571cebd2f7845524862e773661d08d3e14b0a1fd109db2f325ae405e25dd83d02200a92d4ecfe49919389eb9754cc60b5af058a167690b876dad6f09f15f1b69b19012102a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc8c8d1400\",\n        \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\": \"02000000000101eafcb255247f0572767b990a52274f705dbb4cdc2d7675b1f7cae7b7e4ebdd250100000000fdffffff01318601000000000017a914bff6c01dc206f8fb7062e314e378e9aeae017a5c870247304402203cd1869eb6ce787f4f27e471b58acfd288d57ca55c8c996aba89d1865647408902200f57bf52775d397b4f02ac584bf6bab96335e89a2b2bfa0bb8f4aa98ee62dd66012102fb265cce2019e555fe23fb45e4cbc3d966bb399a52b2e93b808ad7961b426de15ee01c00\",\n        \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": \"0200000001f00f1ab449751fb23f460518ef1f2214441a2f7c4892561c6c64abe4c448ed8a010000006b48304502210092443dd340a1caa7791351324aee87b35b6057a7d9237fc95e1f1d029dd2fe2402200cd3cd13551313d186726bd6beb6d386f79d0a978972e5f923303df1700c957d0121030638cdea69f69ff45ed0d97d8a5c41ce7bbe0d774be898143f7a73cb73ad72b0fdffffff01e38501000000000016001446116f48b60ad5b22377dcf951643e2e1aa3957ac3271800\",\n        \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": \"02000000000101411e7b856d8d02ae6632c87efe1406299680afca2c4d97c34a4ba77bb0b20f820100000000feffffff02c0bb01000000000016001455d5618cfc2d731d7c3d80b92bd2c87835c1234c580f0200000000001600144f28f480d74dbfa2cf5c1c85cbe29368cba5f3a60247304402201f857d44cbf474275010d4fc996bf78e15b5631ea6557f842c3a3aebdd7fc8be02205e21cabbf5247ad08c62bea14adcde8f0bad722de3473435f500a8fba7ea6c900121031abaf20c4146bdee1ad87cbc56c892cbe93a92fd11614d379f80be5f60b1f3d50aaf2400\",\n        \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": \"02000000000101ad4badbfe6fce0cfd142e272a002bab9a9eb6bd02d07975073f49a4e510beea301000000171600142d886eecf1d36b2a317f74ae3ae0088944017d27feffffff02af2c150000000000160014e866546b96bddfa46ae298c8363527531e03573ba086010000000000160014e437538b7f13871562066babd1fbab72b4fba9580247304402202ce3a50013652650b1b93b89aed5d36bab4adc13ab7054aedb908735b0d2923d02202a4a5bd091f88a186b2d9e92ca99f3129c6cd15861eba06d4b6677601fa1fc9a012102c821655842477341331fb3faa44a04547c5c039b8f44f48b1b7c45266f545ea7d0181b00\",\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": \"010000000001016ef7ba954c29f9d1aa21d5fb1ff44e2e36d04536ce17ebd15306130f81f2bd4d0100000000fdffffff0210270000000000001600149f9aa3795e57c535d2f7b160ff023804c60fca6eebbe3000000000001600147bcf38d8a69071c21abbfe0532cb6aeacaf5c2940248304502210086c464555baeb19f60d9a8284bbca0c77009e6864e751bd4ca87105d0f9d1acf02207b6d4f3d5142af6fe7636e60967aac294155bdea469bd5c0ac18612e8ed8456d012103cd68ad8bddacdc82a39a82efd7a045f2d9513a9de31049286b696c87a8efbe8767341600\",\n        \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\": \"02000000000103e00607297fdfc9808759f2042adfe27912e79031bbff6deb627583a65538bc300100000000fdffffff6c4bec256f089c29f5b6bc5c4cd40fdeca1729cc2106a2cc6e0630c071e429400000000000fdffffff5c22c57d029a8b261ebd9290e2ddd58283fd0fe0a8bef084eb39ebc42e3624b40100000000fdffffff018b1b110100000000160014ef8dfaec6a39b7efc77828fc3840a26da11bfe7502473044022073382b79c486c708f16a1e53c8327060c193ba8db376df3c40abd7b2cf0c39c7022028f210de787f1bdc9cf0060e612b1d71b7f36b1f86956b43a53d516f1aeb2657012103ff475c0005da39a068516cbbfe5f0b5fcc126aba81e405728adab8bdd572d9a3024730440220243989034479fd8de480207d59aec4106ff2277f11f7a982509d309ae90e108f02203282db9a7f38d43c2672abe64f0bfd5d28f60b053fcb1b87671d5dbaa987e12301210242c23a20435cd52192581537b2017288b8cdf3fef349100c11a125d9529017e10247304402207712fbcfac6e0011f404945aa1480332da34eeb5ac47623e341a4f9692caa49802202c6ec5e739aa52f675ffefbf4c057ab6b4a357f66902d96b6af5327206c9367f012103834e1e34194508e3fc7db19d3c6964149efd58f8c67548cc765c3284cad24d6707ee1700\",\n        \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": \"010000000001036ef7ba954c29f9d1aa21d5fb1ff44e2e36d04536ce17ebd15306130f81f2bd4d0000000000fdffffff3d70d1769f35a57f1c30858a1c8cb37c638818056ea7fcf152bff652890b85600100000000fdffffff84533b4dca7ab35e4d5bc89cd6bcc645298a8c4d534abdc668e0109f4ee1dbf20000000000fdffffff01383be00000000000160014dfb16c4a19694e2bb2ec8b474d754a74539bfec602483045022100dfbf8dde6a9070a13a808d317e4fb6d35906a3b0ba4f53543ddababfa0df631e02204bc0b692c63c500d517ee96df74ac13e0d680898d761895ea80ed7458031dbc5012102462df8e9cd6196186dd4d5444b6a4c7acd006a7d4c59b8b16e7517828fb8cfc302483045022100b59c985153d9a2a27bdac393ec238dfca3dc2dd63bb181c778ad41568ae2017802204b3c71b2e90dbfb054f432ea95c6fefe29d8c2c36ee725a7916b0241ba6409130121030ea1026664c47391f82ff1bb693b1bfc959ec3b3cbc80a8dfd31fc637cad765702483045022100d056da28e2cd316f5e86aaa56298f0f5d1a313cbbad95f65712b9a4a0be1448f022059d8719fece4677da694a7ca1b61a9ee386eb108e58bdf648dbde0db704ad40f01210208b66c5067eb109735768b92a2800aa6844ae7575911298e0fd6ced709987d04a4361600\",\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": \"01000000000101a6c78418b9383dfcf1cf93f1b084f4e5106f862c82ac0b602efa2568195778730100000000feffffff02400d03000000000022002005b05ff139f7a25173a8ada54eaf24c7d9764409c829ca792726fb7aff1eb8360594610000000000160014641837eaea0de6ed898e30ff6e093c566d790521024730440220736f7035d58d08d3e7be11edcdbf3d91cbd86cda16b19ec48b12e5257c21c6bb02202987a4976bbc5e759ba8d5dc7dedc56eadb449966c0e2520f9a1392c2342cb6e0121039c25ce87cddec7e7b0a7a4541047cba7554d6d4093965869787ad2eca4be8a55a1951500\",\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": \"010000000001013cbef1e09f39646ab1e2ba273edcafa11f9da5ea25ac0fd194ccec51fa4a28410100000000fdffffff02f0ac3000000000001600144616df751811e5114e045a6b7cad889463555c0488e6300000000000160014553a3b97c2cc7d3d4edeace3281ff038bd06762902473044022070000f29d154f419ebe2dc740c80eb89dc0d571d96d0469f629761d7fb13cad9022008c94670cb3e682a5f076c4cd6634b8ec32168cd648c3f6c89597c105cf5368a0121030bd1ee5d34698af47ee3ceb954b3009ecc0614bb64b54d669adfb233a36d2ae8d3311600\",\n        \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": \"020000000001013d6719cb2977dd39f3cbcaad59f5bc06fd1734e53317829679b393053dbe3b980000000000fdffffff021a950900000000001600141f81d08aedb71cd788380d642b755ecb961a1edc55fb0900000000001600141b449e6b7f40cc1e05b5810c367bd3c5b872f5e402473044022064462613a5a80a83ed4badc54ff4e6e85e06781a55ebccc1eb253dc865c478cb02204c4bd331205a9cf90094ca60ba8f1367f5246b92595a361b40fafce522ba4dbf01210251700930db16fbda3de7141685dfae05fda67b234ff7c4861562a3ce07f592497f5c2700\",\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": \"02000000000101fe13acd33700e2dc135c833b790eb75e1ed71429773b36192315f3fede7f76970100000000aa771980011c9d040000000000160014c6c1e628487ca44a22beb15e9035f7f76bbc103e04004730440220196ae31431605048d0af586af779d903c813abada54fcf35877b2b4569ee27e502204fa7c6a675369fe1ccc3d5d2e658fc4be7fb480d9f3bd6d1049a7c67264189c401483045022100bd1fdd55c6db44fff54bebf3acd649a5e135254f187561761c150521555e3713022042035709c099da8956af5e6806726170f0104cf5f145ec4632a46d806f3c447601475221022aab353e6b25226a3cb38e81dafe8fca3416642847dab7d38aaafe00eb8294ee2103a5bf1debe9c97bb56c7a4316b45bf2f8d35612db2964253ae8efd0f7ca8e1f7052ae1388d520\",\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": \"02000000000101e1c8758e0a1e9728001f7c49b49493384fca2257594de3868cebc779a3b77bd50000000000fdffffff0114280600000000001600144343eef6273442ab066a852a7d6ecc61950b2e7b0247304402205f8a63b3c273abd010a9c095d9591767b6a628452c93acb30809a7989fc3bfa602204b5f812b1c7151e279d58dffa29942552dcb2fad9d14dc39790785fa158b6adf012102c0c89554ca035a1d840eb87ae679146d5cca0fac2a57d078811894ed9e3a4b5ab4da2400\",\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": \"010000000001013545008007e0d26a52ab3b64217909a77ab1dd06ada0520b66b6710c04162d970100000000fdffffff0240420f00000000001600142e61106d61d1ad4f1e44e79c8d5bc5907d48d3552da5640000000000160014783997038ebfd28edef06a1e088e9fd905c48d610247304402207878752ca1cc79758db2959062b2fd0ae0682e83fb258385a340d636c9b85810022049a0275f3e8dde8ddaffaab5298c44195c1066ebb414fc3d942b57728058ff6a012102be2e344c2f49afa8d734a2f79a09827b038d2f8f6bfa77d5a2e077c22a3fd80aa6941500\",\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": \"01000000000101d3b8ef348489a948e31003be75291a6bfe0cf7288a695a9f9931f2ab3249eb030100000000fdffffff0240420f0000000000160014eb23be36ca7b46f3508a042a121b17f36384c4222f2f830000000000160014e5ec5a8eff827f8ab906f94026e83a7404b8bf8602483045022100c61749c026980911afde50838cb72d3484cdd9b9f419d8e6c7647574a52f0a070220471133c1fb6d21a139f91f01e6fd927463d1f5017dd248f2ca6f97363de396d8012103a23dcf3edc18180b0d82b0be8eb1c30ae61597e99e298246e05109b7ba897637a5941500\",\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": \"02000000000101384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bc90100000000fdffffff02ec130000000000001976a9147f20306730683c828f9a3a990193007106aaca9788ac2471010000000000160014edc62a3c999159ea7aa02f4798f06e2020dc35ad0247304402205f8aa5c02cc6d949f11dc6f61b10ffb37a9fbbd5a4dcc840e1048c257a55ae080220153a6e02eb8b12de1f943d0071cef4f710c3406c8a67174ddf491ea94f4104b0012103555bc1ad91fea0ace08ef29d9e1eeaaa6b9fc45d225c60e30d7b8a6ee37ed03e7f5c2700\",\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": \"02000000000101ce08f7d7b4b76a57ba5693a31219e4068e02c5fa8e7e57bc1cf50c47625cf56f0000000000fdffffff02a0860100000000002200204915b90ffa7ef166e0818b75a206d7d179efa14af39c23781a083c749aa651a46c9e040000000000160014905190f141d2089497a554d5b74175929d2b2c7d0247304402203c9cbdd54e067f9a45c7795ae570fc49c706bc4174255f934afbaa6bf8e5411f02203b019772e113869efc8573fed181b14e33ea9bfdeb3f495d3ae1e4f8844e3290012103a85bd8a32a699000359752570dd3bc027d1ca9865fe5fa39c7d4aaab785b50cb48e82400\",\n        \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": \"020000000001013cf21d22012066ba1c566dad36c1e5d76e4c95acc95b9e357e4d58e0862acaef0100000000feffffff0200710200000000001600146f9726216be985680857a2c95200be19dd0c217960130f0000000000160014a96ad868b42a6944f6adc75b68a4b30a158ad175024730440220235d1cddad375f7d7612c50fa08b2ec1d4772b333795b6cb98754225626757b902200f4a784e44a2a23fd811f9f28e82ee0333068156ba969556aed1f6337337823f0121030ee45e829f5f95c3a0a184897c7fdb687307301164ed55d98411368d962510bd40d92300\",\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": \"020000000001037fbdcc64290f40401515f0cf75d761e9d2b80acf70118d6fa923976b0a49b5230100000000feffffff8bf2c1baabcc788c47c5d2da265a823ddecf97250ba0ddebca1f234e98f8098c0000000000feffffff3cf21d22012066ba1c566dad36c1e5d76e4c95acc95b9e357e4d58e0862acaef0000000000feffffff020000000000000000166a14b837667ddbc6acf0464c1eb35b8a2ba485364cd7e72c0600000000002200206a21894e5bd3a85c5c0f3211772c1553137bcdfbe0c06d5c691289d4a8862b0b0247304402206bfc98da12ba0095c12a9929e7b39132ed9f37f8ba3beaf32a479a738f12a66f0220678fa3ac65fbe79801a39931a4d83255bd75c62a76045ddd7f611ad109538b8a012102bb8e9193021c1f704d7ce8577c5fdc7ba22dd05a03b5b5ecd6ef6307dd8a60bc024730440220730470f1b831e706a633091f2bdaa0944e8b80a61db84dfc41aae5e9265809dd022055362b2618d4dee152819fd259db76aca5c7e7cb7f56d7cd6da8d006b44ca3bf01210319d5b935f6f1cd4a1b84645c3f88db404a93baf78451da144a0dc00aaa1d978c024730440220205c9e119f1b2a6951129f8f4a917572ee4c5c7ed55042b1a6e4d787c8d65bdd02204ced0cbfb7bbdb9e89938ebaca90a9a15727c21a03d5d758617aa25077eba4ed01210319d5b935f6f1cd4a1b84645c3f88db404a93baf78451da144a0dc00aaa1d978c98b72400\",\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": \"01000000000101ba709d2dafe9a11fdc2c1b235587455baf0415a2cf302f9cb9c046129a9326770100000000fdffffff0240420f0000000000160014e278e5edb47b274f91d137e767190b4d02b09a172eea730000000000160014692a676d16f83641a6518e9154d2b43a8465ed4402473044022074b4e39c36785a2751fa3b944ed1430bab6461714549d32a195132aace1ca507022043c545d8a1d349ecb6cd3d87fd6c73eb5b68e0d2ba99891d72fce278dce090d0012103e0e2b67a3e8244900fa916decb6a9d45787ebe15b882c7a065cd0151d298fb4ea6941500\",\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": \"020000000001016136fe76bf2a9528c3eeff3200dd1c84dbbde4d3df9c5571e0f189a0a7f5db870100000000feffffff020000000000000000166a1492e1315ad9862cb68d93417085728e974f2f8bbed39d0400000000002200204c0dffbbeacb7180f46597605740cdab2f8cbbe603b737b95eb7076de632659a0247304402204b95fa4019d2d5427646c04a8b740111da39b91ab8cd8e7976aa99912d1cf7d60220458db68cf3d977cfc5ecb38c193b890a3beae714d3485233f28b52b4db5f7563012102576b988dc3260d1dc5e579d604f6d3663287c933cde4ccd60d6875e9ced8fc5607012500\",\n        \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\": \"020000000001026962bc75f03d744a58dc43f17c61ea1f5ebf2aa9e80ede831eb2e097e1e142200000000000fdffffff1f657881066bcd13a189eed6825e30061c1062c473124fcbeaa4e8cde5bc48ba0000000000fdffffff013dd13000000000001976a914fb08dfdd325a10708693d8565b93d2eb38373c0188ac0247304402205039ecdcbbd57fb7093b38bfbd599aeed5c085a0812d7d977f1325d63733ea710220281af83a941ca7e1dd99381904905d095413a654f1d9e9997060bd5f61adf8be01210379da8156b704d5dfe4432bc829588c643ec65c5a409763dccc3da211479f4bb702483045022100e0c07bb37d8da22ea0036b835d86512ecaf584f00534b2548855b02c1d49f59802200cd94644caa3e90349a0e22de080037290b75e2a5906333cb46ef5fb8f36ba7e0121027ba4d3ee6471a985307a37d09eb9a0a73c1a31b57616fe3e53a3d6d454002519c3271800\",\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": \"02000000000101e00607297fdfc9808759f2042adfe27912e79031bbff6deb627583a65538bc300000000000fdffffff02e803000000000000536a4c504a61637175656c696e652c2049206c6f766520796f75206d6f7265207468616e20616e797468696e672e20596f75206172652065766572797468696e6720746f206d652e204c6f76652c2044796c616e5e22000000000000160014c388b18dd1771046fecb85f44642ffb7a4efb62702483045022100fa3523ed4f12e036b952c8c7aa88dfc663086bd6a951d14a7faaa3fd28587294022017c7984bdcfeed7e25de571f020f65d30ff03e0dce4c6c2514c2ffc208280c80012103c3172a9f8820681c62b8bf28961988a4642b33f4920b9da14b06965c7fff83fded3b1600\",\n        \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": \"0200000002ba7361bf5ff80a3f30b89d7b26c5a1a60fbea526bed89d2e78dc358611be874a010000006b483045022100e2886246bf9d98bae1ed94dddb3da345456d4848c02dcd90e0e9357ed609836f0220344890230f0748c7389645e2a71cf693aeb84617822114bc01e15e17960b36cc01210348d667e58824a00bfc719645c0ff685513243f3476b6b05e4f46cf9b8c3146b0fdffffffd945b6b076ad7858f7a2227374a42ada71582b1d558fde9ece4bf6c9395c67ca010000006a473044022047dcdee44123175b7301882c628da72810ff585e30b94c8403aa641ffd6d8825022046ff6818ed60b5dffbe19534ea4b7a275e48aaee183a14eeb66e36d136880c6b012103ef48fb7c170e02b9f6f481e4eb541947ce89709163c4cdef0af65bf8b64b2d8dfdffffff010f4c2f00000000001600147714df0e53290ddfa3d893b41b8056f7be98a770c3271800\",\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": \"020000000001017d6f488442fa311ff6e3a1124ff3cf6a974aa13867306f0456c8e73ca5c7be030100000000feffffff030000000000000000166a14cbfdb55e747e0cbdc2328518ae6363cc5f5c3fc0e2850100000000001600143ece1158a80cbbb40dd12688732d3b77a959e5bf801a06000000000022002084081ee1045d5e33acb4d90081dc6db80b14d09596d1b066917557f23437c4fa0247304402205113e656225c09f8b05fb5bef8c48196e2520a988bc89832f8cc806c64dc3dce02204fbb91805640038fb7e73eb2bf5a28a3a015982a1d9dcd591e6bb9c8c103bab9012102fe3de4d6b6bf207134f40ada206ff6e2ed088169d2413db656c8de963a15c6e17e5c2700\",\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": \"0200000000010169fb54298cd7f5490eb80c10e80bb8720390f7d96a9382c91c74827edee9fc940100000000ffffffff015d2c060000000000160014edb4b55f1f8c33ac8597fa53d08d8997131f1b93040047304402202033a46f390d2ed27d1e7aaca2c29cc6af20b92a7535ce7bfa9355480a1a83d202204a94016d878e7e25647738ff28884046d27e4ca83287a0fd4e51ad7d899e47ea0147304402201bddd59748ba0a79d73fda2747b9688b86eef9cad625201d26d6591ceebe18ae022014ea8ecb3722b3ab4873476ca77b2d6ae366d2e61af42ead40419221d6192ba301475221031c9cd5530ea64a7e411c21701d5bcac74e55420924110c93706c12ef7184f0a42103672361cd8f92dfac7fb1d1bb8974bf826cf8cc0eaa6fcb23175be9312d5aced652ae00000000\",\n        \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": \"020000000001018cc9bd58583a2aba20e8747b6945a157cdcbb75c2c6cea5a1e036ea7bff30ab30100000000feffffff02b0ad0100000000001600146f9726216be985680857a2c95200be19dd0c2179288511000000000016001418d1896a5e9360279ab7d07a65ecc992d9b514c50247304402206941c4f8cdd0f4a907931f4400ed37a310b00425a2cf218f11affd53d70268e50220412b7e34545d3aabe2b9f00b9fa18c84a2d030b2f4244127dca69738abec6fe10121033399307690d676c7faf57ac454ca6399017f45ef5aa3bebac07b8691bb572a073dd92300\"\n    },\n    \"tx_fees\": {\n        \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": [\n            null,\n            false,\n            1\n        ],\n        \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": [\n            null,\n            false,\n            1\n        ],\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": [\n            1000,\n            true,\n            1\n        ],\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": [\n            1000,\n            true,\n            1\n        ],\n        \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\": [\n            111,\n            true,\n            1\n        ],\n        \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": [\n            null,\n            false,\n            1\n        ],\n        \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": [\n            null,\n            false,\n            1\n        ],\n        \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": [\n            null,\n            false,\n            1\n        ],\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": [\n            141,\n            true,\n            1\n        ],\n        \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\": [\n            246,\n            true,\n            3\n        ],\n        \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": [\n            null,\n            false,\n            3\n        ],\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": [\n            1000,\n            true,\n            1\n        ],\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": [\n            141,\n            true,\n            1\n        ],\n        \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": [\n            null,\n            false,\n            1\n        ],\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": [\n            183,\n            true,\n            1\n        ],\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": [\n            1097,\n            true,\n            1\n        ],\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": [\n            705,\n            true,\n            1\n        ],\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": [\n            705,\n            true,\n            1\n        ],\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": [\n            210,\n            true,\n            1\n        ],\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": [\n            776,\n            true,\n            1\n        ],\n        \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": [\n            null,\n            false,\n            1\n        ],\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": [\n            289,\n            true,\n            3\n        ],\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": [\n            705,\n            true,\n            1\n        ],\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": [\n            153,\n            true,\n            1\n        ],\n        \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\": [\n            181,\n            true,\n            2\n        ],\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": [\n            202,\n            true,\n            1\n        ],\n        \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": [\n            null,\n            false,\n            2\n        ],\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": [\n            190,\n            true,\n            1\n        ],\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": [\n            138,\n            true,\n            1\n        ],\n        \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": [\n            null,\n            false,\n            1\n        ]\n    },\n    \"txi\": {\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": {\n            \"tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5\": {\n                \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39:1\": 9799000\n            }\n        },\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": {\n            \"tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu\": {\n                \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c:0\": 10000000\n            }\n        },\n        \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\": {\n            \"tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk\": {\n                \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea:1\": 100000\n            }\n        },\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": {\n            \"tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl\": {\n                \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e:1\": 3204744\n            }\n        },\n        \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\": {\n            \"tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq\": {\n                \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:1\": 3194603\n            },\n            \"tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3\": {\n                \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c:1\": 8798\n            },\n            \"tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd\": {\n                \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c:0\": 14695224\n            }\n        },\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": {\n            \"tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4\": {\n                \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6:1\": 6595885\n            }\n        },\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": {\n            \"tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct\": {\n                \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c:1\": 6394885\n            }\n        },\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": {\n            \"tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp\": {\n                \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe:1\": 302547\n            }\n        },\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": {\n            \"tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9\": {\n                \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1:0\": 404573\n            }\n        },\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": {\n            \"tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l\": {\n                \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535:1\": 7596590\n            }\n        },\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": {\n            \"tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu\": {\n                \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3:1\": 9598000\n            }\n        },\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": {\n            \"tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5\": {\n                \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:1\": 99810\n            }\n        },\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": {\n            \"tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e\": {\n                \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce:0\": 403476\n            }\n        },\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": {\n            \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\": {\n                \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b:0\": 160000,\n                \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c:0\": 110000\n            },\n            \"tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8\": {\n                \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f:1\": 135000\n            }\n        },\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": {\n            \"tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj\": {\n                \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba:1\": 8597295\n            }\n        },\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": {\n            \"tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg\": {\n                \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661:1\": 302700\n            }\n        },\n        \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\": {\n            \"tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z\": {\n                \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269:0\": 99811\n            },\n            \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\": {\n                \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f:0\": 3099663\n            }\n        },\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": {\n            \"tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c\": {\n                \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0:0\": 10000\n            }\n        },\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": {\n            \"tb1qplsf242vay6vavy4eguef855fx3klmp9505g88\": {\n                \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d:1\": 500000\n            }\n        },\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": {\n            \"tb1qdgscjnjm6w59chq0xgghwtq42vfhhn0murqx6hrfz2yaf2yx9v9skh03as\": {\n                \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69:1\": 404711\n            }\n        }\n    },\n    \"txo\": {\n        \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": {\n            \"tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu\": {\n                \"0\": [\n                    10000000,\n                    false\n                ]\n            }\n        },\n        \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": {\n            \"tb1qplsf242vay6vavy4eguef855fx3klmp9505g88\": {\n                \"1\": [\n                    500000,\n                    false\n                ]\n            }\n        },\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": {\n            \"tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu\": {\n                \"1\": [\n                    9598000,\n                    false\n                ]\n            }\n        },\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": {\n            \"tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5\": {\n                \"1\": [\n                    9799000,\n                    false\n                ]\n            }\n        },\n        \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": {\n            \"tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z\": {\n                \"0\": [\n                    99811,\n                    false\n                ]\n            }\n        },\n        \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": {\n            \"tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8\": {\n                \"1\": [\n                    135000,\n                    false\n                ]\n            }\n        },\n        \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": {\n            \"tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk\": {\n                \"1\": [\n                    100000,\n                    false\n                ]\n            }\n        },\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": {\n            \"tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq\": {\n                \"1\": [\n                    3194603,\n                    false\n                ]\n            },\n            \"tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c\": {\n                \"0\": [\n                    10000,\n                    false\n                ]\n            }\n        },\n        \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": {\n            \"tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd\": {\n                \"0\": [\n                    14695224,\n                    false\n                ]\n            }\n        },\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": {\n            \"tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct\": {\n                \"1\": [\n                    6394885,\n                    false\n                ]\n            }\n        },\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": {\n            \"tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl\": {\n                \"1\": [\n                    3204744,\n                    false\n                ]\n            }\n        },\n        \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": {\n            \"tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3\": {\n                \"1\": [\n                    654165,\n                    false\n                ]\n            }\n        },\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": {\n            \"tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r\": {\n                \"0\": [\n                    302364,\n                    false\n                ]\n            }\n        },\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": {\n            \"tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e\": {\n                \"0\": [\n                    403476,\n                    false\n                ]\n            }\n        },\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": {\n            \"tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4\": {\n                \"1\": [\n                    6595885,\n                    false\n                ]\n            }\n        },\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": {\n            \"tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj\": {\n                \"1\": [\n                    8597295,\n                    false\n                ]\n            }\n        },\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": {\n            \"tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6\": {\n                \"1\": [\n                    94500,\n                    false\n                ]\n            }\n        },\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": {\n            \"tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg\": {\n                \"1\": [\n                    302700,\n                    false\n                ]\n            }\n        },\n        \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": {\n            \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\": {\n                \"0\": [\n                    160000,\n                    false\n                ]\n            }\n        },\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": {\n            \"tb1qdgscjnjm6w59chq0xgghwtq42vfhhn0murqx6hrfz2yaf2yx9v9skh03as\": {\n                \"1\": [\n                    404711,\n                    false\n                ]\n            }\n        },\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": {\n            \"tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l\": {\n                \"1\": [\n                    7596590,\n                    false\n                ]\n            }\n        },\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": {\n            \"tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp\": {\n                \"1\": [\n                    302547,\n                    false\n                ]\n            }\n        },\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": {\n            \"tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3\": {\n                \"1\": [\n                    8798,\n                    false\n                ]\n            }\n        },\n        \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": {\n            \"tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu\": {\n                \"0\": [\n                    3099663,\n                    false\n                ]\n            }\n        },\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": {\n            \"tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5\": {\n                \"1\": [\n                    99810,\n                    false\n                ]\n            },\n            \"tb1qssypacgyt40r8t95myqgrhrdhq93f5y4jmgmqe53w4tlydphcnaqmpm8kr\": {\n                \"2\": [\n                    400000,\n                    false\n                ]\n            }\n        },\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": {\n            \"tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9\": {\n                \"0\": [\n                    404573,\n                    false\n                ]\n            }\n        },\n        \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": {\n            \"tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc\": {\n                \"0\": [\n                    110000,\n                    false\n                ]\n            }\n        }\n    },\n    \"use_change\": true,\n    \"use_encryption\": false,\n    \"verified_tx3\": {\n        \"024464eae9858f079d2f49bc3db28f92237c63fe950c8ddc6c9b18d1855fa32c\": [\n            1346870,\n            1530271747,\n            11,\n            \"0000000000000be4817e499bebdcebd9c792f68551bfedbf14fa058fd8db4e2d\"\n        ],\n        \"03bec7a53ce7c856046f306738a14a976acff34f12a1e3f61f31fa4284486f7d\": [\n            2579582,\n            1708966959,\n            675,\n            \"0000000000002b0be5f214bcdcd765f6e4cefa1226a6d3a4e1b482da9707603a\"\n        ],\n        \"03eb4932abf231999f5a698a28f70cfe6b1a2975be0310e348a9898434efb8d3\": [\n            1346978,\n            1530282040,\n            3,\n            \"00000000000004f73116c1b9d17dafc409710362110c7cd9e52f1e8703147238\"\n        ],\n        \"0ef1c91636d20331acc2759d0f0335925cd899e270efdd2598e9449c66649d39\": [\n            1346957,\n            1530280346,\n            10,\n            \"00000000000004e05e5f82a8f3db5f37cf8b3c8ad4d44a80556c63173f2fdb8d\"\n        ],\n        \"10a2a7695a5b6ba857b10d04ad047b968656c6d84b58ea226dc92c150cb73b2f\": [\n            1892447,\n            1605658960,\n            42,\n            \"000000000000009e8f7f68acdbf0d5d20d920794916249d7300d3d4ddea01a7a\"\n        ],\n        \"2042e1e197e0b21e83de0ee8a92abf5e1fea617cf143dc584a743df075bc6269\": [\n            1583044,\n            1571148168,\n            273,\n            \"00000000691cd68ef23f24f9bd245aaf85f8dec4abe26d1f9ff97681af9eb222\"\n        ],\n        \"23b5490a6b9723a96f8d1170cf0ab8d2e961d775cff0151540400f2964ccbd7f\": [\n            2404107,\n            1667575669,\n            18,\n            \"00000000000000dd5c68b0c41dcd77382c18fada64632c0475b906f0f6a936d7\"\n        ],\n        \"25ddebe4b7e7caf7b175762ddc4cbb5d704f27520a997b7672057f2455b2fcea\": [\n            1775825,\n            1594136749,\n            81,\n            \"000000007583aa1310c8368737317e3443990a03c2bdcc95513f82c21e7e8609\"\n        ],\n        \"30bc3855a6837562eb6dffbb3190e71279e2df2a04f2598780c9df7f290706e0\": [\n            1455208,\n            1549129020,\n            23,\n            \"000000000000002bbb81515fa47a4aaa4af7b324b606df83b90be6018172ba99\"\n        ],\n        \"3407144b10e0c7f60a88e9f3adcc7e732c83e92fa17a923f523b8acc50718c2f\": [\n            1568264,\n            1562691216,\n            85,\n            \"000000000000027001cae7dc4b5b14a66f2219d13a620e1ed12bf5a7a9f444fd\"\n        ],\n        \"4029e471c030066ecca20621cc2917cade0fd44c5cbcb6f5299c086f25ec4b6c\": [\n            1455781,\n            1549379178,\n            61,\n            \"000000000000006f773a59309e17ce987f302945141d657b6e310bf04a5a11d2\"\n        ],\n        \"41284afa51eccc94d10fac25eaa59d1fa1afdc3e27bae2b16a64399fe0f1be3c\": [\n            1414562,\n            1538150440,\n            81,\n            \"0000000085ee7b5accad0063fc1a368d905cbf704223bccac1b9d2e351a4578d\"\n        ],\n        \"4dbdf2810f130653d1eb17ce3645d0362e4ef41ffbd521aad1f9294c95baf76e\": [\n            1454548,\n            1548893536,\n            44,\n            \"000000000000004bfd6914c088814a418c4308cc0f6b87b993455b1a2eca7531\"\n        ],\n        \"59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\": [\n            2579584,\n            1708968409,\n            1814,\n            \"00000000000000120fe076771544efb54ec2ed49c0ad1278bb49f9c9efa51944\"\n        ],\n        \"66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\": [\n            2528392,\n            1696346138,\n            155,\n            \"00000000f7d4a4dcb86d377009f654832d52f9f94299c51434eab00038e4ab6a\"\n        ],\n        \"6ff55c62470cf51cbc577e8efac5028e06e41912a39356ba576ab7b4d7f708ce\": [\n            2415285,\n            1673173967,\n            17,\n            \"00000000000000105c5d1016f327809add5b052aa5032e34cab26050535143ff\"\n        ],\n        \"737857196825fa2e600bac822c866f10e5f484b0f193cff1fc3d38b91884c7a6\": [\n            1414311,\n            1537898497,\n            45,\n            \"0000000000000046df1be1854387a550e40df1f487218b6b977b7323db986c45\"\n        ],\n        \"7726939a1246c0b99c2f30cfa21504af5b458755231b2cdc1fa1e9af2d9d70ba\": [\n            1414310,\n            1537897872,\n            48,\n            \"0000000050c14b6289103b0a666e76bb178e757f0411947e9461ae82483df756\"\n        ],\n        \"778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\": [\n            2579584,\n            1708968409,\n            284,\n            \"00000000000000120fe076771544efb54ec2ed49c0ad1278bb49f9c9efa51944\"\n        ],\n        \"87dbf5a7a089f1e071559cdfd3e4bddb841cdd0032ffeec328952abf76fe3661\": [\n            2418761,\n            1675433925,\n            56,\n            \"00000000000037c0576afb47f7f757544ad44710a08c7829dcfc56710d2216d8\"\n        ],\n        \"8c09f8984e231fcaebdda00b2597cfde3d825a26dad2c5478c78ccabbac1f28b\": [\n            2349377,\n            1664914506,\n            58,\n            \"000000003309f969ede680d3aea4ca8643912b4b4b793172de15a127a790f778\"\n        ],\n        \"94fce9de7e82741cc982936ad9f7900372b80be8100cb80e49f5d78c2954fb69\": [\n            2406297,\n            1668461654,\n            24,\n            \"000000000000001c4dd10ac70eb8f0af901aec27be9469354f98f15beeebdc03\"\n        ],\n        \"972d16040c71b6660b52a0ad06ddb17aa7097921643bab526ad2e00780004535\": [\n            1414311,\n            1537898497,\n            44,\n            \"0000000000000046df1be1854387a550e40df1f487218b6b977b7323db986c45\"\n        ],\n        \"97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe\": [\n            2425096,\n            1679252503,\n            21,\n            \"000000000000001221ae1721de7f87c9313912720a43b867546e1c93f5d94945\"\n        ],\n        \"b234c4f252cd193377bc6cf931ff1ef6a398e3a434e43b22cdb457a8213d749b\": [\n            1583044,\n            1571148168,\n            286,\n            \"00000000691cd68ef23f24f9bd245aaf85f8dec4abe26d1f9ff97681af9eb222\"\n        ],\n        \"b424362ec4eb39eb84f0bea8e00ffd8382d5dde29092bd1e268b9a027dc5225c\": [\n            1457134,\n            1550165277,\n            52,\n            \"0000000000000057c116d32f1edf128ec4f81a5e734a4095cda3c9ef1d6cb7f7\"\n        ],\n        \"ba48bce5cde8a4eacb4f1273c462101c06305e82d6ee89a113cd6b068178651f\": [\n            1583044,\n            1571148168,\n            191,\n            \"00000000691cd68ef23f24f9bd245aaf85f8dec4abe26d1f9ff97681af9eb222\"\n        ],\n        \"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\": [\n            2579583,\n            1708967515,\n            479,\n            \"000000000000001f81c50f287422a15099c2bc5cd4464d9962f84e8c1a7b94bb\"\n        ],\n        \"d57bb7a379c7eb8c86e34d595722ca4f389394b4497c1f0028971e0a8e75c8e1\": [\n            2415285,\n            1673173967,\n            16,\n            \"00000000000000105c5d1016f327809add5b052aa5032e34cab26050535143ff\"\n        ],\n        \"efca2a86e0584d7e359e5bc9ac954c6ed7e5c136ad6d561cba662001221df23c\": [\n            2349374,\n            1664911683,\n            13,\n            \"0000000000000012fd19ff17ac6890f0911be5791042eb2a6243e3a6b4817e94\"\n        ]\n    },\n    \"wallet_nonce\": 129,\n    \"wallet_type\": \"standard\",\n    \"winpos-qt\": [\n        344,\n        374,\n        1336,\n        899\n    ]\n},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_commitment_signature\", \"value\": \"3fa7982d5a533e8e83121fd801215bf1818472e47476028e2aee4d604d308e714a9a1d5e572aa25b72371003bd8d9bec131b8871255680a29e793f1c9ec12fbf\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_htlc_signatures\", \"value\": \"de434ac331615e64f872394efec24f18e60d0d3b41a387317a2c40527c81da2107c8cb28c55565b41760b4bfc348e0630664500a6601dde519a0f6c4c0fc2849\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/ctn\", \"value\": 18},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": false},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/was_revoke_last\", \"value\": true},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/fails/8\", \"value\": {\"1\": 19, \"-1\": null}},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_commitment_signature\", \"value\": \"ebdaf05cae4ea1d1748cc54d1bf9e70d5595f719a3612d0ea1f4f8ebc5ddd2074e2ce1e8f0b2c56fa2b998a9be54db2e38515fd3cbdc387687502ee281c130dc\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_htlc_signatures\", \"value\": \"\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/ctn\", \"value\": 19},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": false},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/fails/8/-1\", \"value\": 19},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/was_revoke_last\", \"value\": false},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/19\", \"value\": [\"0084384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb7dde84e4a82afa2ff4ffdeeb0cd13b204c86a7044c6621870c9bb6593a23bd6d19f573b997b24d1a13899a67c37229170cf44c97f8df9c9b811fbc43e0ca5ce10000\"]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/buckets/0\", \"value\": [\"85885a59b8e0e62230a8f3fa61271fecf6ab19a3064d6e2464980df4e7bd900b\", 281474976710637]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/index\", \"value\": 281474976710636},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/ctn\", \"value\": 19},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": false},\n{\"op\": \"remove\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/19\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/current_per_commitment_point\", \"value\": \"03338204605ae6b11537000011434fbef308a8940ab743b11ab8939b221726689b\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/next_per_commitment_point\", \"value\": \"03a6385b6bc75ddce49e40a2abe6938a7eca11e79b496186fa3785806297a3e035\"},\n{\"op\": \"remove\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/onion_keys/8\"},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/adds/9\", \"value\": [10010000, \"720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\", 2580311, 9, 1708972473]},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/locked_in/9\", \"value\": {\"1\": null, \"-1\": 20}},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/next_htlc_id\", \"value\": 10},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/onion_keys/9\", \"value\": \"ec1ecda6ce0fcad4bb77426e885bf9db45126b38aaf140361d6d84b3277f7b32\"},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/20\", \"value\": [\"0080384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb0000000000000009000000000098bd90720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b00275f570003707884d225af4e1532d5f01d51c1addc2be5e047c281f755d3195e912c89c67cc2e91008d77dc5e5dfb5413cf59fcdde7158f4519c6ba18ccbb8c5bf4f4bbd4eb55490972a5a1a621be4139a57dabcb458ac06dbf1bdbd88c7d6ba6ec19c66ccb41144540bb61e03bc4b1bee93b20389fc015cb29dcb36cd52abcb4d08b2c26c4ed387edce1d4d6987ffa83d5d19912d86a181f5cab13bd6520a3010a045b387034a282297f589c644fdfe495606ff5a05086b0c68ea27850ff3bb44f92189c6a795fda09ebb5255b756ab682512763f95eae3b048f49cca21837b6c428b7d6b5407fdadca2ee03d026c39142b35cdcb6475f2fdfddbc68ee4a8a2d3601f27f7a596649d937901a348ef05bd70ac20a47a2618dee01f46d1e9268fdbcfcae016c95c4300e4bca381cbae52f54d40060f91807ff763299f45ef4b686cdc25573fcea671a773199dfb3b09a46f6657990d7936c65f037fa89442074384ff6a42bb460af3f5124adc115f41ee558a49c558296a0894bef09541a4d1f73895b7f1e1bca870f61f94f747414dec1ace49610b07fc0a8f77140446e974cf6ea5a52bccbeb3eede17956f83b24b37d1f7d9eb61dc6ce0a5d2e56945ff9f5b464047a8daa1438e1d21591ce2a7dba8da105b3036958e4ace4f80d5a6b65e3d8c835521113e89083500176ddb2fd09d1c3e768c2530c19c8d766143bc18fb2e57605f0944a4e5a201a10109a579bdbdb54b0bc5ffb4f4d97b7cdaaaeacf3529f9d2a4fa079c41c9d07cfe6ec7bbae7e340bca8ff7e4e47d580a445dc9f550813fa904f3016aadbb43211e5d4c8af8a4e8df3ab94f180b85fecce44a12674d6b56ccd60f09ce69d8f2fea614cf3101c17281307cc7e9fd14a2094f92b1457b9fea45db37781a37fafe1b2ed55e3c36bda3c004f597cdbb16c63e31dc56242af7a9a257f3cc32a85625ae6d3bb978fbb475637bb9ad7731ca3b11205810fd8d9d31116b2d143706900662d26dd5a91d122cf47dd62ad76287c4633f0b74f30849424451f0ee974ee3692f95b05878a0307aed7ae9d557bb55aafc54de72e1fc68ec82ff81c340d4a05e878e219cf5623b72a995e4bb6097ef510fdcc26744c5129162a5e1be9029f4c1b2b0687a866233a8ae780a881f43774e998a5869f3a037d9be7c88b90986659a553fde4bb943e649b8c47ebcb5581e13abeb130ee2a5a44ae74cc927d7d79316b25983408c0c266d0ed0adc9e44cccef48e400184ba05bde90fadfdb73172475d54a8ec76126cd8b28b076a95b93c4eee50af3254d921b8070c594ebbe723809587bfb592739615d9a8c84cdfb6374ec993667b5419f97e643c938210b8a06cd6e8d19cc0aae57737a6f9201e2d3804cf022bf9466753e7bd742a694fc52bda6b9f08011514f460788080b93e9d9ab6f0155a58bd2c378862f9c13b812c6b46595e0107a27b2404eb4c308fe9b85fa8acb5787ad4bf9f71c2af04ee98c91fce9f3187f9f4c3e7494ba8f9705484980071a3007084e392ae1e4f30e904b94752d9740ab2c327f69bb7b639ccfacceeb40d281ccf0a3eddcd189b3bcc2aebd8f7b46e30f557140091476f40e419dfa144cc2237036a4a3f5e865196e69375f0785a36dccbebb73f33f097c3233a9503ffed7dc52dddbfba97943636989860a443123fec99ec7ba279fec4ea625893f01b88ac27cc82d6afc0858ccd6c1ffd2b5ac064bedc76e17cde2c68b1d0514f6181f7794fa4cfbc48678c957572adb8b7adce366c05b0b5f5858c5f9faf3204b2a3489eb68106642f0de4a21a9eb9f7571a020022abe1ef0fc97c909878d63d6d1ff41c8c9db56137ad0bdfe55ebd5288b0dadeee9c32e04b6098953c122a9abd9f189bd6c5cf88e2ab530285a3e5e6a87c390ca595858e2b85696d235da4e62926f\"]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": true},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/20/1\", \"value\": \"0084384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb0871a887a59d19dfda382be69c1dc38a247e463603e4dd2913e861a035d0ff4b69a0a30efba5fadd5a35476e60060ba943e58fd89222af6efbf4b539491bf1ac00014d007beb2da74beb1a681465e9df4df6dcd3df00eb32b6b810a7328d2c16462932a0a34595b4a8f1f3528cd39630fa4be034061133a64689cf5952638197bd13\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/buckets/2\", \"value\": [\"eec4d6f305b149ac61b7a8658ba86f32eb5db80d45d8a55c0dc143a060a92615\", 281474976710636]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/index\", \"value\": 281474976710635},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/ctn\", \"value\": 20},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": false},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/locked_in/9/1\", \"value\": 20},\n{\"op\": \"remove\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/20\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/current_per_commitment_point\", \"value\": \"03a6385b6bc75ddce49e40a2abe6938a7eca11e79b496186fa3785806297a3e035\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/next_per_commitment_point\", \"value\": \"0389be855c10ed5cdf9745fbdbe6c6392c34ed2e247dfd4684abdd3fdac58128ca\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_commitment_signature\", \"value\": \"a5db2258c9ad30ae7a796b62f05830bf98b2fec7b87117b791667e32922e60bc25f6f68b8ffba1d2e07db31dd2501e5022facc7b71fbacd94252bc4cddeaa354\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_htlc_signatures\", \"value\": \"e9edbe89f7ba854b1bb9ec55c45f01a9566ddcb632b91d40b9f3e78db00721661ec85689a9d48e768d545e04efa1900f301b7159eafc6bd580f1d2ceae9d01c6\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/ctn\", \"value\": 20},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": false},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/was_revoke_last\", \"value\": true},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/settles/9\", \"value\": {\"1\": 21, \"-1\": null}},\n{\"op\": \"add\", \"path\": \"/lightning_preimages/720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\", \"value\": \"31a84f775c07316ad0cf3b894cf37bb2420b3f4d69a41794df2a4cf29c91e0a9\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_commitment_signature\", \"value\": \"9291815383df48f15ef0f953e9272061824304973e313338c6811184eb9fd2ca4e6d37ff7ad7c289d434afdebdca8f5a32c4944d1b4ffec848777677305cd940\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_htlc_signatures\", \"value\": \"\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/ctn\", \"value\": 21},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": false},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/settles/9/-1\", \"value\": 21},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/was_revoke_last\", \"value\": false},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/21\", \"value\": [\"0084384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb7a59628a0033cad138f2e71b43b8fba19d4fd2e012d77a8af8d11b8dea08cc77322b6b947c9f865cf25c9630fe06bae1942930c66f1c22435cc21ac8a03d39e50000\"]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/buckets/0\", \"value\": [\"491f96e1b35e4987d2dc70b9a2ebdae9998c4be6b7e27dc49e9d9349757174c8\", 281474976710635]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/index\", \"value\": 281474976710634},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/ctn\", \"value\": 21},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": false},\n{\"op\": \"remove\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/21\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/current_per_commitment_point\", \"value\": \"0389be855c10ed5cdf9745fbdbe6c6392c34ed2e247dfd4684abdd3fdac58128ca\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/next_per_commitment_point\", \"value\": \"02183d975765551eb16b07876b594a367261dc330ee5a17bd909a4a3be2dd795fc\"},\n{\"op\": \"remove\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/onion_keys/9\"},\n{\"op\": \"replace\", \"path\": \"/lightning_payments/720848b95f41736e6bdc838f4587e6205c417702353f3fd7d9b6b1ceb632e65b\", \"value\": [10000000, -1, 3]},\n{\"op\": \"add\", \"path\": \"/lightning_preimages/cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3\", \"value\": \"ee4f549b3844282ae596dfef14fc75e64067517fa92e9735fe792415bced3db9\"},\n{\"op\": \"add\", \"path\": \"/lightning_payments/cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3\", \"value\": [100000, 1, 0]},\n{\"op\": \"add\", \"path\": \"/payment_requests/cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3\", \"value\": {\"amount_msat\": 100000, \"message\": \"general kenobi\", \"time\": 1708972498, \"exp\": 0, \"outputs\": [], \"height\": 2579588, \"bip70\": null, \"payment_hash\": \"cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3\"}},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/adds/0\", \"value\": [100000, \"cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3\", 2579735, 0, 1708972515]},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/locked_in/0\", \"value\": {\"1\": 22, \"-1\": null}},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/next_htlc_id\", \"value\": 1},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/unfulfilled_htlcs/0\", \"value\": [21, 21, \"0002820bf19eee399d66837dbd3316d21be683fb337881e77cee1588016d3561d08fbf4cafd0d35f5e558e1e8b59e990f17df4c811f59a304fa78f9da117824d93c38a81f2696c633903a75c6d027d64134e5bcfec279d3bf53419fc6512e393a8b264bd86fcc85ed4aa2e11b1578bed29af9957bdd752212eae2b233fe00957a9188a9904325a47d0fb74862430e9e4c8b4ef72d1ae7a4f19ba8172b94f2426e078da91c500481f833d309f4068cff01aadf5f8c0de90cd7d4f125cfef68e43c44e5682df3894b90ca35b495a6a39a1c80d7ac12202826cc8d053e4162ddeb40cb20aece1ec7871a1b3afb25518014e1d4b57e2a75ae5359d8f4a042090d25fa998c05feaa07a5d0d5617185b14d4072fd8b10a1d1e013186212ef7ff8761cff53615058958132b23246bdc2af2503514547f032f4a0a7039dcfd86ea3915ee66dee5d5b300f99cee6a86b4823606fccf2dd044e8a7bc00a68dc258c9c2e140df44da5af84b1f89cc3272dcd9a5e598bd5b2a70769df7879ec5aa67cb5cbdf1f96d02d1cf8abb06077a024c6c73d81eaed7f6ada61fdb53de4ea080bacb5d8f5904d2c5ec349ac2da3f40295d7a83df252ebc60556fdf0a234b5951a7ed0ed8ad15218af9db2c9fb4b2f9b21b6e9cac1db6a42818effe26fe21c9bb29840f14404cce454c5ccfc2c1ae5881edab7584092e26a4bfc08d22cc17e9670100b0594229dd9dfdb4c1ff3dbc2665327a2db129321c1e047213a88d789203709a2b8cadb98b4398a8d45d0f8f7f86f39852ca95de224945df522a35b18ae31b949d08722bd56c6cdc1414f062b768558998522131040a1ffc6757284b7c4ff22edbc8d6ed5427d3857d2d64b615149911cc737ab8387a2245967ab996cb20d7da127be74d1faead44645b9525b04ceea5ac41633ec7cd526a4d82bdbb2abe1e8123a7b5c64059e138859c36f98a563b8dd36808dff42a0be40cd617ff705ac5c983a30277b014da93fba2d5dc3d163923cfc03ac5c8839f2cad333b9c574ad273c7a3d4b2d4f86cc4d77000dc062711114bd787b1389193f94c850a033125156b443bb4b0a3ed331ca17d95880ee4582e861220e83fddc91279f4d07c30f72dc3c838455d37eaa71ecd0929e3a0d305413746f1416b4e746f552a1544263c51a0ecad33406fb1da4909b3242c9ce4e93078b7f20228b2a33010bbac16a02ce5c6bd03b2674f9898c3b1bb33f629ec0e2fdd25c55c6c3792a1285c330e38b94e778c94831b15856a2f7dd014146e9e8aae9fd931540a7ca87b2fd8bb7e23e21555e9d6e32fc04d4af462d395091a6e05d22941e479312f9fd734852a9dc607a17a332de4e5a8188e6d387ce99d25a7fbe8ad0042e436db672264d3c73a0d574e8133171523c5144656246e8239eeb9a08b8df09c91bdc4241409fa9f362a4b3a915bd2322638a23b5d90e25d88d1283a0fce3c52d8bb3dc328057ae8dc8ce80970273c083406ce3c140728c6be226f74b0c9fa1734006675a60627564aa3511568a8cd4d02eb89efa0c88f9acfde8d5175542adf2130a176abaf0dd4c2eb4b29fed2ec3102d29677e8abaaeff394b85b931d81c929a7a46d4e8b58bb479218ca615b7b469f29fd96525c699fcae432e0785a31598adb88baffa07714cf838966317f0575bf86c165bffaff7104a1058bf4e1f41d79d10dbb4e92296d5e08652336671bb4755f8babb3f6dbc5a7c4f40e3f85e9b211567f28d5997529a0b8859954afcc59e630902bee006ad86dfbd80bb45b3588334fcd7252ba2c73b3393a38295025df906e1c883562811a01c41b622e75cc2121cd8fc3c79e3465e98fdf455ddca17873b995004eddeeaa270fe6081cdadfd80420a8ffd2e0aec3d378d9b1f94bd17e7292224fc12ed0e7645d077461e9197120e1a41e42\", false]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_commitment_signature\", \"value\": \"eac74b6467ce315595cbf8a8cd7d28aa35d3c6fbf6e9f1e4a7ebcca5ce4164c57db71c3d9d47158b0499866918a077c99d6b0908dfbc11b1093620c4d66f97bd\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/ctn\", \"value\": 22},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": false},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/was_revoke_last\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/locked_in/0/-1\", \"value\": 22},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/was_revoke_last\", \"value\": false},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/22\", \"value\": [\"0084384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb312e9fb1a6ee227d069a8751e2751623fa2c8192e591791931a5d0e1534d71ae5cc1f3b89692a0c9702b5eae4058748bfd9779061c9242e24c39683713a614b60000\"]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/buckets/1\", \"value\": [\"d251b82c23183575e91754d4f078bf7c33585e868d20036ed67c04784c4dd387\", 281474976710634]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/index\", \"value\": 281474976710633},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/ctn\", \"value\": 22},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": false},\n{\"op\": \"remove\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/22\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/current_per_commitment_point\", \"value\": \"02183d975765551eb16b07876b594a367261dc330ee5a17bd909a4a3be2dd795fc\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/next_per_commitment_point\", \"value\": \"03190c65d2caa10c864d45df1fbbc9ea3efcc60aa0b0016c240840135b52134a4e\"},\n{\"op\": \"replace\", \"path\": \"/lightning_payments/cb869e44bcfe4f770fcba707788bd626956669989e7ea31981aef4674f78bff3\", \"value\": [100000, 1, 3]},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/settles/0\", \"value\": {\"1\": null, \"-1\": 23}},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/23\", \"value\": [\"0082384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb0000000000000000ee4f549b3844282ae596dfef14fc75e64067517fa92e9735fe792415bced3db9\"]},\n{\"op\": \"remove\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/unfulfilled_htlcs/0\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": true},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/23/1\", \"value\": \"0084384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb528c912b152b905628908387245f308f4344d5bd871009480337292163166a9b53ca1974ca90dc624ca9324aa6759947821267113284a3289eeacde4e2123bbe0000\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/buckets/0\", \"value\": [\"aca2acb2997f8acbe9a2a6c3dc7ffc8f36d2b0fe8b8b5b3b8bbad8eef70702c3\", 281474976710633]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/index\", \"value\": 281474976710632},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/ctn\", \"value\": 23},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": false},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/settles/0/1\", \"value\": 23},\n{\"op\": \"remove\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/23\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/current_per_commitment_point\", \"value\": \"03190c65d2caa10c864d45df1fbbc9ea3efcc60aa0b0016c240840135b52134a4e\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/next_per_commitment_point\", \"value\": \"03098c5da4cf38bc4c75499f874a3415a3c7bdf45b2dc11625d3f4fd110ab8c028\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_commitment_signature\", \"value\": \"c73bd8e408b446378c1ea26ff6c689535c8f99bec4bd6f55ace0adef9a082f49605b312ceadfca76e1ceb1abe19caff164c358ef3225e6d08299f850e1b6a73c\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/ctn\", \"value\": 23},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": false},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/was_revoke_last\", \"value\": true},\n{\"op\": \"add\", \"path\": \"/lightning_preimages/45ea9cc85808195b3f9b5972f62989495e34c4691b0ad6bc2b81a4406bd5c7bc\", \"value\": \"7ba5e266765623597e68670a434e6754be249cc8575f1bf4935422cd232deed6\"},\n{\"op\": \"add\", \"path\": \"/lightning_payments/45ea9cc85808195b3f9b5972f62989495e34c4691b0ad6bc2b81a4406bd5c7bc\", \"value\": [111000, 1, 0]},\n{\"op\": \"add\", \"path\": \"/payment_requests/45ea9cc85808195b3f9b5972f62989495e34c4691b0ad6bc2b81a4406bd5c7bc\", \"value\": {\"amount_msat\": 111000, \"message\": \"general kenobi 2\", \"time\": 1708972527, \"exp\": 0, \"outputs\": [], \"height\": 2579588, \"bip70\": null, \"payment_hash\": \"45ea9cc85808195b3f9b5972f62989495e34c4691b0ad6bc2b81a4406bd5c7bc\"}},\n{\"op\": \"add\", \"path\": \"/lightning_preimages/6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34\", \"value\": \"d6e4067da77dd810c6b0465a54e0557238b1b317c7b2a6ac679429780ad56f43\"},\n{\"op\": \"add\", \"path\": \"/lightning_payments/6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34\", \"value\": [125000, 1, 0]},\n{\"op\": \"add\", \"path\": \"/payment_requests/6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34\", \"value\": {\"amount_msat\": 125000, \"message\": \"general kenobi 3\", \"time\": 1708972537, \"exp\": 0, \"outputs\": [], \"height\": 2579588, \"bip70\": null, \"payment_hash\": \"6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34\"}},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/adds/1\", \"value\": [125000, \"6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34\", 2579735, 1, 1708972548]},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/locked_in/1\", \"value\": {\"1\": 24, \"-1\": null}},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/next_htlc_id\", \"value\": 2},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/unfulfilled_htlcs/1\", \"value\": [23, 23, \"00038f00b809562c57f23cbac3fe52ca3ba6bbd29ecbd955d188a64a064bcb8a5d1e8b8e45d13b0fb754be1c286dbf520e2774f2db838f0d13715ade0ab6ac9549a61c66444b0eeb5a142a22b5b0a8b796679100e24d1695a76f1ede57f5c57ee06a788cf5fe998f3f688b570fa6326b4f5d4330e40f43ea727e247e8f09e48ebebd169b4b931bc7db560b7904168af27405e0b2987cb5599f5d643c83f617e0ea861928f1be539272bed2261250faf06f8751dc3ba43888e278f3722e2e32a61b3e76b58134418152a87084138a136fd7ce9244902536d31b33564d44d164db3edc2ddef8a46438315bcc1ff114280f692f4c16ca9f4b4c536bd3db3f40667be3cc309132f50cdeb4f685b86bf168ec3239f7dbe76f15d94f798d50edf3f19bbdf82b3481981298c2a4982c9cdfdc1b198ceb6628d8b50e35b5ca88ae6d1b78ee3da4a6707b5b372fd5d064f5a79e2abf94bc5783fadffaeba74a8cfdbdfd4724138ecdb1ce0eb5020e3b56afa03d6f4147eb33b1bdf30555c4a1a94221bcbb40cfdec5d3afa10302cb35b2e2a75b4d8ce9cf4cd5a1530e96275b1cec11e8f7dcc3b7ca4b3d0efcc89e6bad994cf270ce4a1709d2acd6a62432e875c5819f9ff016519b3c3b80337a5714707932dc860bdec68aabe7d030a0ec2e96d4da0e7057888c6b4795017db1718002472a25bebdbfad8ae1b69fc316150405e3217fd2cb100959cd2f695d8f179669b3dc70f8572226be62555b2a3c487848df518f6d677efe7b371b1fdc24736172e869688463942d2f0a4c70d7fb07bc373a584082d677993ca6566028231cf4a8e4f4deecadf1d77113bdfd1b8e37786bcbe69dcea17e85533f34e5b98028444328010f365cf8b631c66c75eb0880ef566cdc1147f35b4fa9ad5c012f9c95e8a13736174dfcdc2275b54042f69051b556be30870dc5552a3fa9526e8c89c2e01c3dc5264692d9283ed3ebc8fff2e55f42f9ba775069860148b6cfd546938ba88be51debb170fa5cf099197680e5711f56181df28f51d0a17a26c95f3bfb57646dcb230a4a93c661059923647f584f87b7bb91c02b2fd53a5cb5b4db2f6e93827e21dabf4ab17db5723c66b8e0ad559d9c35b5d82f544a9b8b187d6398b8e5a4b76e3d50eb1ccdd5fec1427e5e8fb5b5be9a844d12620ebefecfa265d9c5079c580368d206c78562b5e9280ba14c328bde0f1acc33087f5c9d4d7b8a9a7e981b9992942a9941d88f24e38b686a959e9985b4d27703afaecd5e45387688d5632bd6534db5d54faddd8bd1d7844523942fb499b46ce0cd41363f0e216475ee9cc5581c056e4bb39daa29ad5fa36c99ba2bf52c38520281fc3bab4f95d0f297a8de705c9cc2fa703f51e6843d92535c66ba8d34b62f1e2a0984ea2e5dd04661354b4684e0d7e0667edbfb265e228a28965d62d199a1360f65d3cda5686e44e5c3ad97ba4b3819e6e143a5eafd3b1a81bcb0b28a251d9cf3837cc8dea00704d6a74dbeb0f974818af1c93c5b4999ff2ce603a9a1c256f6770fad9322d407391e492fe7e79f3631b659e1cab7141735557c3f7a04784d89fe84b97f3c71216371128fb4429522d9f66149e4ba9c917bb70c9ce641f3d1ffdaabaf6e2279b7966882b43ef07791613a72fe8f92da1c56806aa988dc37f1aa5a75426dc130ffcc2c92a8f88619ad12e4a48556a3cfa09cbdb2bc761ce90749f32ab5e4a9efae3ce37c2dfac2a52868d22288b223b573de9de02ad05342455322fea6b6b157ac27eddbdebc744ffcfba127dcb90b5c1565eb82f9a8d2c3a3d9708b030b4a5cfd0e3b38c851960a6ed6cc2cb270c74a2ba16566f74bc86ab0150cb4e473571bc4827ccd83b0d15122703845f0f6c977566ca710aeb590edc8fdfdb0e5de271792a1d9058ca9095f1a18299ba95a5857\", false]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_commitment_signature\", \"value\": \"2afcbd10ebafd5156bccc79ce7fa4bce4302196f136849994e571ee21cab700b4d3b6d29e21ba3dcd9f8fad81ef3f1df796359077e77b2eab5bd44174d04a482\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/ctn\", \"value\": 24},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": false},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/locked_in/1/-1\", \"value\": 24},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/was_revoke_last\", \"value\": false},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/24\", \"value\": [\"0084384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb61c549da86d2a6f1406538b9efce32ad9df657d08ca750482ef3c6eafbd5b1163a1caa038e6a2d08aab5b6751d1cb7f6a9dfb2300208feeba9a65d74295a22350000\"]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/buckets/3\", \"value\": [\"c48b955c8381cb5053dcdb8b95fca0d98f49a076e9878c89cb323324358d0cd5\", 281474976710632]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/index\", \"value\": 281474976710631},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/ctn\", \"value\": 24},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": false},\n{\"op\": \"remove\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/24\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/current_per_commitment_point\", \"value\": \"03098c5da4cf38bc4c75499f874a3415a3c7bdf45b2dc11625d3f4fd110ab8c028\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/next_per_commitment_point\", \"value\": \"02e2c6d277d4b3f7106064ce4eb22a25ac2542bb2bb227ab1f75851a3e337425b9\"},\n{\"op\": \"replace\", \"path\": \"/lightning_payments/6fe5489ea2303c6b5164590266df359130c4390e9bbeba73854a6a57e52f8b34\", \"value\": [125000, 1, 3]},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/settles/1\", \"value\": {\"1\": null, \"-1\": 25}},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/25\", \"value\": [\"0082384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb0000000000000001d6e4067da77dd810c6b0465a54e0557238b1b317c7b2a6ac679429780ad56f43\"]},\n{\"op\": \"remove\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/unfulfilled_htlcs/1\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": true},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/25/1\", \"value\": \"0084384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb3040e01f58ddc129dcddaf50597152a53c431762b6591b05c661a7091a88dc7a7ee7f6ce80a6e7ae5b06309b4efe135ba2b1ba4a9073aa304c1965afd955ae250000\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/buckets/0\", \"value\": [\"a61b44ff967f2cf23ce00be76f6f9c97b81a02849f57fdddb8d7adf517360338\", 281474976710631]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/revocation_store/index\", \"value\": 281474976710630},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/ctn\", \"value\": 25},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/revack_pending\", \"value\": false},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/-1/settles/1/1\", \"value\": 25},\n{\"op\": \"remove\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/unacked_updates/25\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/current_per_commitment_point\", \"value\": \"02e2c6d277d4b3f7106064ce4eb22a25ac2542bb2bb227ab1f75851a3e337425b9\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/remote_config/next_per_commitment_point\", \"value\": \"03c865a71e115ec94688b2c01b1bd2cbb723fe2b4cd450d9649d67d8bcf4269a4e\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/local_config/current_commitment_signature\", \"value\": \"35ca3cfac4df21b4d71f5dc6ca7d42988c811843d7a9b3ac823ceb74ee2663294ce524c475edf6a4ced20453050edfba84132f03e92e3bab5da9d1d93c08eaeb\"},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/ctn\", \"value\": 25},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/revack_pending\", \"value\": false},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/log/1/was_revoke_last\", \"value\": true},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/state\", \"value\": \"SHUTDOWN\"},\n{\"op\": \"add\", \"path\": \"/spent_outpoints/c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38/2\", \"value\": \"f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\"},\n{\"op\": \"add\", \"path\": \"/txi/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txi/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106/tb1qssypacgyt40r8t95myqgrhrdhq93f5y4jmgmqe53w4tlydphcnaqmpm8kr\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txi/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106/tb1qssypacgyt40r8t95myqgrhrdhq93f5y4jmgmqe53w4tlydphcnaqmpm8kr/c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38:2\", \"value\": 400000},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/38b9788da930ea4e90e24adfc953093ae28f589a6c097a048506a32283287def\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/38b9788da930ea4e90e24adfc953093ae28f589a6c097a048506a32283287def/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106:0\", \"value\": 61821},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/5b6f7752864ff821241b3ae4ac9e94ce83e03a936e20a8d85ca040071cef964e\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/5b6f7752864ff821241b3ae4ac9e94ce83e03a936e20a8d85ca040071cef964e/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106:1\", \"value\": 338007},\n{\"op\": \"add\", \"path\": \"/txo/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txo/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106/tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txo/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106/tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5/1\", \"value\": [338007, false]},\n{\"op\": \"add\", \"path\": \"/transactions/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", \"value\": \"02000000000101384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bc90200000000ffffffff027df10000000000001600140f3f5dfc300c3c3aaed1ec09b79ef2f01a7db78057280500000000001600144012c452a8ef5b02abba6ddd843189932666ac110400473044022006390d0caf77d8da3255d1a2d7e8ff11f6a9b0760545c7e2677b7014a1522b82022052fa18d2fc4fe4347e1c45192885eac2271cd7a37310eb00ad19f6c1401ddbbe0147304402203757ca109e09a7b682a504daf532e4476d493083a04c544f26128c4303a3ca6f02207470bb9d1becebb386e6e0a8b33231c085e9bc6b1822e77ea92cd24b6dd3ae110147522102ebef86631b36d05117734751f7e9b96c5cb78211647854abc9001559b4b3611b210396ecb0125961b5bbb506b6e0c9565178b70e35ab0ec13690715be04a4655e0f552ae00000000\"},\n{\"op\": \"add\", \"path\": \"/tx_fees/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", \"value\": [null, false, null]},\n{\"op\": \"replace\", \"path\": \"/tx_fees/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", \"value\": [null, false, 1]},\n{\"op\": \"replace\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/state\", \"value\": \"CLOSING\"},\n{\"op\": \"add\", \"path\": \"/addr_history/tb1qpul4mlpsps7r4tk3asym08hj7qd8mduqv6xmjs\", \"value\": []},\n{\"op\": \"replace\", \"path\": \"/tx_fees/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", \"value\": [172, true, 1]},\n{\"op\": \"add\", \"path\": \"/num_parents/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", \"value\": 3},\n{\"op\": \"add\", \"path\": \"/num_parents/778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\", \"value\": 3},\n{\"op\": \"add\", \"path\": \"/num_parents/59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\", \"value\": 1},\n{\"op\": \"add\", \"path\": \"/num_parents/66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\", \"value\": 9},\n{\"op\": \"replace\", \"path\": \"/addr_history/tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5\", \"value\": [[\"f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", 0]]},\n{\"op\": \"add\", \"path\": \"/txo/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106/tb1qpul4mlpsps7r4tk3asym08hj7qd8mduqv6xmjs\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txo/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106/tb1qpul4mlpsps7r4tk3asym08hj7qd8mduqv6xmjs/0\", \"value\": [61821, false]},\n{\"op\": \"replace\", \"path\": \"/addr_history/tb1qpul4mlpsps7r4tk3asym08hj7qd8mduqv6xmjs\", \"value\": [[\"f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", 0]]},\n{\"op\": \"replace\", \"path\": \"/addr_history/tb1qssypacgyt40r8t95myqgrhrdhq93f5y4jmgmqe53w4tlydphcnaqmpm8kr\", \"value\": [[\"c96b3ed60c8531e1370051662390defa07152cbc028855ea591fc444d8db4e38\", 2579583], [\"f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", 0]]},\n{\"op\": \"add\", \"path\": \"/channels/384edbd844c41f59ea558802bc2c1507fade902366510037e131850cd63e6bcb/closing_height\", \"value\": [\"f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", 0, null]},\n{\"op\": \"add\", \"path\": \"/spent_outpoints/778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/spent_outpoints/778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4/1\", \"value\": \"b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd\"},\n{\"op\": \"add\", \"path\": \"/txi/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txi/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd/tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txi/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd/tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6/778b01899d5ed48df03e406bc5babd1fdc8f1be4b7e5b9d20dd8caf24dd66ff4:1\", \"value\": 94500},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/53ff5555b1cecb5db763dd39daabbea38e4516af74e3eba6b60fd3975ace23ae\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/53ff5555b1cecb5db763dd39daabbea38e4516af74e3eba6b60fd3975ace23ae/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd:0\", \"value\": 20000},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/89f0b6e9045dbfde39e0b45e4bb38f8dcd7a7ecb794da24a8cbadbe1a966e640\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/89f0b6e9045dbfde39e0b45e4bb38f8dcd7a7ecb794da24a8cbadbe1a966e640/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd:1\", \"value\": 74300},\n{\"op\": \"add\", \"path\": \"/txo/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txo/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd/tb1q02g5nde0heaed0y24rztkedh9nvswknw50h7fx\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txo/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd/tb1q02g5nde0heaed0y24rztkedh9nvswknw50h7fx/1\", \"value\": [74300, false]},\n{\"op\": \"add\", \"path\": \"/transactions/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd\", \"value\": \"cHNidP8BAHECAAAAAfRv1k3yytgN0rnlt+Qbj9wfvbrFa0A+8I3UXp2JAYt3AQAAAAD9////AiBOAAAAAAAAFgAUN7XNOBKS9zcP+t3lFm4UxEK2aZc8IgEAAAAAABYAFHqRSbcvvnuWvIqoxLtltyzZB1puhFwnAAABAR8kcQEAAAAAABYAFO3GKjyZkVnqeqAvR5jwbiAg3DWtAQDhAgAAAAABAThO29hExB9Z6lWIArwsFQf63pAjZlEAN+ExhQzWPmvJAQAAAAD9////AuwTAAAAAAAAGXapFH8gMGcwaDyCj5o6mQGTAHEGqsqXiKwkcQEAAAAAABYAFO3GKjyZkVnqeqAvR5jwbiAg3DWtAkcwRAIgX4qlwCzG2UnxHcb2GxD/s3qfu9Wk3MhA4QSMJXpVrggCIBU6bgLrixLeH5Q9AHHO9PcQw0BsimcXTd9JHqlPQQSwASEDVVvBrZH+oKzgjvKdnh7qqmufxF0iXGDjDXuKbuN+0D5/XCcAIgYDlqn+nWN7iCwCS0ySNHKnz42tu8a4l0SkApDyuINsXxkQU15HPwAAAIABAAAADwAAAAAAIgIDDesIX5ZN2AWvqyKhrjy7c2dOJaVsFa4z7Q2Gc3tL5hAQU15HPwAAAIABAAAAEAAAAAA=\"},\n{\"op\": \"add\", \"path\": \"/tx_fees/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd\", \"value\": [null, false, null]},\n{\"op\": \"replace\", \"path\": \"/tx_fees/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd\", \"value\": [null, false, 1]},\n{\"op\": \"replace\", \"path\": \"/tx_fees/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd\", \"value\": [200, true, 1]},\n{\"op\": \"add\", \"path\": \"/num_parents/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", \"value\": 3},\n{\"op\": \"add\", \"path\": \"/num_parents/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd\", \"value\": 4},\n{\"op\": \"add\", \"path\": \"/num_parents/59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\", \"value\": 1},\n{\"op\": \"add\", \"path\": \"/num_parents/66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\", \"value\": 9},\n{\"op\": \"add\", \"path\": \"/labels/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd\", \"value\": \"psbt. sign me\"},\n{\"op\": \"replace\", \"path\": \"/wallet_nonce\", \"value\": 130},\n{\"op\": \"add\", \"path\": \"/spent_outpoints/59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/spent_outpoints/59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2/1\", \"value\": \"f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c\"},\n{\"op\": \"add\", \"path\": \"/txi/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txi/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c/tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txi/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c/tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3/59a9ff5fa62586f102b92504584f52e47f4ca0d5af061e99a0a3023fa70a70e2:1\", \"value\": 654165},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/3932c5e05e3702163eeb006f082ef46fd159bf4ee9a5bc1c4768b03e4447370e\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/3932c5e05e3702163eeb006f082ef46fd159bf4ee9a5bc1c4768b03e4447370e/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c:0\", \"value\": 10000},\n{\"op\": \"add\", \"path\": \"/txo/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txo/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c/tb1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sak7ncg\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txo/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c/tb1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sak7ncg/0\", \"value\": [10000, false]},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/07f16db9ecc6bb95c9bd12acc647d4215e5f6cd455d7f91a3a7a471553cfb38a\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/07f16db9ecc6bb95c9bd12acc647d4215e5f6cd455d7f91a3a7a471553cfb38a/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c:1\", \"value\": 644000},\n{\"op\": \"add\", \"path\": \"/txo/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c/tb1qzyaz308030saay93zqma0at032vfqa9y0gfge3\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txo/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c/tb1qzyaz308030saay93zqma0at032vfqa9y0gfge3/1\", \"value\": [644000, false]},\n{\"op\": \"add\", \"path\": \"/transactions/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c\", \"value\": \"02000000000101e2700aa73f02a3a0991e06afd5a04c7fe4524f580425b902f18625a65fffa9590100000000fdffffff0210270000000000001600145a4bb72587e19f22bf0e598b3608bfaea5d655f0a0d3090000000000160014113a28bcef8be1de90b11037d7f56f8a989074a40247304402205a5149c97de0ba1991e6ecc35ff9bd3c2b9ebb05938e4c83685d85099da1049902207cd14cb2a69d291bc83e3baa04a0469d1a1b7ad2cf6dc6ae82e7946829da2c58012102495c2a9cd87fac7325cbebd75aa7b4c4ccf687d222210bbeb402b704de150ed5845c2700\"},\n{\"op\": \"add\", \"path\": \"/tx_fees/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c\", \"value\": [null, false, null]},\n{\"op\": \"replace\", \"path\": \"/tx_fees/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c\", \"value\": [null, false, 1]},\n{\"op\": \"replace\", \"path\": \"/tx_fees/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c\", \"value\": [165, true, 1]},\n{\"op\": \"add\", \"path\": \"/num_parents/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", \"value\": 3},\n{\"op\": \"add\", \"path\": \"/num_parents/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd\", \"value\": 4},\n{\"op\": \"add\", \"path\": \"/num_parents/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c\", \"value\": 2},\n{\"op\": \"add\", \"path\": \"/num_parents/66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\", \"value\": 9},\n{\"op\": \"add\", \"path\": \"/invoices/552819c6e6\", \"value\": {\"amount_msat\": 10000000, \"message\": \"orange22\", \"time\": 1708972670, \"exp\": 0, \"outputs\": [[0, \"tb1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sak7ncg\", 10000]], \"height\": 2579588, \"bip70\": null, \"lightning_invoice\": null}},\n{\"op\": \"add\", \"path\": \"/labels/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c\", \"value\": \"orange22\"},\n{\"op\": \"replace\", \"path\": \"/wallet_nonce\", \"value\": 131},\n{\"op\": \"add\", \"path\": \"/lightning_preimages/e92b054dcbcf43cc62fe06f9f3cdc6dd35784da2fed96a521a7a34ff7980a7cf\", \"value\": \"9af5a4ba8e2b92a7fb32b51f393a6383b00cd6210fdb9f9f7974a7e64e253b3e\"},\n{\"op\": \"add\", \"path\": \"/lightning_payments/e92b054dcbcf43cc62fe06f9f3cdc6dd35784da2fed96a521a7a34ff7980a7cf\", \"value\": [100000000, 1, 0]},\n{\"op\": \"add\", \"path\": \"/payment_requests/e92b054dcbcf43cc62fe06f9f3cdc6dd35784da2fed96a521a7a34ff7980a7cf\", \"value\": {\"amount_msat\": 100000000, \"message\": \"qweqwe9\", \"time\": 1708972721, \"exp\": 0, \"outputs\": [[0, \"tb1qt339ksrha0n5a6lwpql778erkm272hxgamdc0u\", 100000]], \"height\": 2579588, \"bip70\": null, \"payment_hash\": \"e92b054dcbcf43cc62fe06f9f3cdc6dd35784da2fed96a521a7a34ff7980a7cf\"}},\n{\"op\": \"add\", \"path\": \"/spent_outpoints/05dfaba3754c71f4090dff3a913145fff89c967f74d7828a149bfe55d81228d8\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/spent_outpoints/05dfaba3754c71f4090dff3a913145fff89c967f74d7828a149bfe55d81228d8/1\", \"value\": \"30df8468214e5d8f017a6bbde6502d134fc7048050f7d22a8ff59917c82d8013\"},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/a46ea62258cc33b9380d89a93303c803e921986b3234f069df991fb312ee237f\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/a46ea62258cc33b9380d89a93303c803e921986b3234f069df991fb312ee237f/30df8468214e5d8f017a6bbde6502d134fc7048050f7d22a8ff59917c82d8013:0\", \"value\": 100000},\n{\"op\": \"add\", \"path\": \"/txo/30df8468214e5d8f017a6bbde6502d134fc7048050f7d22a8ff59917c82d8013\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txo/30df8468214e5d8f017a6bbde6502d134fc7048050f7d22a8ff59917c82d8013/tb1qt339ksrha0n5a6lwpql778erkm272hxgamdc0u\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/txo/30df8468214e5d8f017a6bbde6502d134fc7048050f7d22a8ff59917c82d8013/tb1qt339ksrha0n5a6lwpql778erkm272hxgamdc0u/0\", \"value\": [100000, false]},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/1befb3eb3af9b8522a209f4c2ec72bd674f16ca2efbe0a9dcc361eb7cd075914\", \"value\": {}},\n{\"op\": \"add\", \"path\": \"/prevouts_by_scripthash/1befb3eb3af9b8522a209f4c2ec72bd674f16ca2efbe0a9dcc361eb7cd075914/30df8468214e5d8f017a6bbde6502d134fc7048050f7d22a8ff59917c82d8013:1\", \"value\": 110700},\n{\"op\": \"add\", \"path\": \"/transactions/30df8468214e5d8f017a6bbde6502d134fc7048050f7d22a8ff59917c82d8013\", \"value\": \"cHNidP8BAHECAAAAAdgoEthV/psUioLXdH+WnPj/RTGROv8NCfRxTHWjq98FAQAAAAD9////AqCGAQAAAAAAFgAUXGJbQHfr507r7gg/7x8jttXlXMhssAEAAAAAABYAFOECTpY3FEj0ei/w/z+OdEINks9whFwnAAABAR/UNwMAAAAAABYAFBvBOhMtuZvJEufzeG1vAzBF9vjgAQDeAgAAAAABAemVezbVRgmjlRxhrB5rTtHC3FNe0zm3IMXQbzAk9x6qAQAAAAD9////AkANAwAAAAAAFgAUpbMmOz0LLwEWZgc4r+DueDpch/PUNwMAAAAAABYAFBvBOhMtuZvJEufzeG1vAzBF9vjgAkcwRAIgFYzX0p24a/PtByBt3dCuR2VBVPJsewvudXIRBO0c5jwCIGIBwFW0q1QZWrUk95BCG8R2/Pz60WyoL6vXbkdf0ZAKASECsFi45Z15t/SWK9JAn8KzSVwH1WElmtfq7s3kbd+fVf2AWicAIgYDl3x3TMTvw2XTKZkop4xk7dYkHFrIhrLiVhJNsd4d8UkQ0uN52gAAAIABAAAA7gAAAAAiAgPrDNYLjiLqthkvbuWrEVXICjnR4dBBYQsnihULNBOkdBBTXkc/AAAAgAAAAAAMAAAAACICAkY8+sZ83YCKqkHuFRC2mvrnDTQDZtwdPvD2F7cCQ7cCENLjedoAAACAAQAAAPEAAAAA\"},\n{\"op\": \"add\", \"path\": \"/tx_fees/30df8468214e5d8f017a6bbde6502d134fc7048050f7d22a8ff59917c82d8013\", \"value\": [null, false, null]},\n{\"op\": \"replace\", \"path\": \"/tx_fees/30df8468214e5d8f017a6bbde6502d134fc7048050f7d22a8ff59917c82d8013\", \"value\": [null, false, 1]},\n{\"op\": \"add\", \"path\": \"/num_parents/f2f97b3df3d464eed09611b48b7701aacd3cf789b1cafcb66888cc74eb784106\", \"value\": 3},\n{\"op\": \"add\", \"path\": \"/num_parents/b9f339901048c7cefd7943cf09211a2c63ff8577f6e4c5081d516aaffff91ebd\", \"value\": 4},\n{\"op\": \"add\", \"path\": \"/num_parents/f864cce1ecf74a49770907db7085de3ceed68062a07710f06f4cd066b728422c\", \"value\": 2},\n{\"op\": \"add\", \"path\": \"/num_parents/66f846809c2d45842f17e40fb6a696288c3274a66b0bf71107940f250978832e\", \"value\": 9},\n{\"op\": \"add\", \"path\": \"/num_parents/30df8468214e5d8f017a6bbde6502d134fc7048050f7d22a8ff59917c82d8013\", \"value\": 1},\n{\"op\": \"replace\", \"path\": \"/winpos-qt\", \"value\": [424, 651, 1336, 629]}"
  },
  {
    "path": "tests/test_storage_upgrade.py",
    "content": "import shutil\nimport tempfile\nimport os\nimport json\nfrom typing import Optional\nimport asyncio\nimport inspect\n\nimport electrum\nfrom electrum.wallet_db import WalletDBUpgrader, WalletDB, WalletRequiresUpgrade, WalletRequiresSplit\nfrom electrum.wallet import Wallet\nfrom electrum import constants\nfrom electrum import util\nfrom electrum.plugin import Plugins\nfrom electrum.simple_config import SimpleConfig\n\nfrom . import as_testnet\nfrom .test_wallet import WalletTestCase\n\n\n\n\n# TODO add other wallet types: 2fa, xpub-only\n# TODO hw wallet with client version 2.6.x (single-, and multiacc)\nclass TestStorageUpgrade(WalletTestCase):\n\n    def _get_wallet_str(self):\n        test_method_name = inspect.stack()[1][3]\n        assert isinstance(test_method_name, str)\n        assert test_method_name.startswith(\"test_upgrade_from_\")\n        fname = test_method_name[len(\"test_upgrade_from_\"):]\n        test_vector_file = self.get_wallet_file_path(fname)\n        with open(test_vector_file, \"r\") as f:\n            wallet_str = f.read()\n        return wallet_str\n\n\n##########\n\n    async def test_upgrade_from_client_1_9_8_seeded(self):\n        \"\"\"note: this wallet file is not valid json: it tests the ast.literal_eval()\n        fallback in wallet_db.load_data()\n        \"\"\"\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    # TODO pre-2.0 mixed wallets are not split currently\n    #async def test_upgrade_from_client_1_9_8_mixed(self):\n    #    wallet_str = \"{'addr_history':{'15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc':[],'177hEYTccmuYH8u68pYfaLteTxwJrVgvJj':[],'1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC':[],'1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm':[],'1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj':[],'1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf':[],'1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs':[],'1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa':[]},'accounts_expanded':{},'master_public_key':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb','use_encryption':False,'seed':'2605aafe50a45bdf2eb155302437e678','accounts':{0:{0:['1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC','1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj','177hEYTccmuYH8u68pYfaLteTxwJrVgvJj','1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm','15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc'],1:['1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs','1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa','1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf'],'mpk':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb'}},'imported_keys':{'15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA':'5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq','1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6':'L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U','1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr':'L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM'},'seed_version':4}\"\n    #    await self._upgrade_storage(wallet_str, accounts=2)\n\n    async def test_upgrade_from_client_2_0_4_seeded(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_0_4_importedkeys(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_0_4_watchaddresses(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_0_4_trezor_singleacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_0_4_trezor_multiacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str, accounts=2)\n\n    async def test_upgrade_from_client_2_0_4_multisig(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_1_1_seeded(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_1_1_importedkeys(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_1_1_watchaddresses(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_1_1_trezor_singleacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_1_1_trezor_multiacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str, accounts=2)\n\n    async def test_upgrade_from_client_2_1_1_multisig(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_2_0_seeded(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_2_0_importedkeys(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_2_0_watchaddresses(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_2_0_trezor_singleacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_2_0_trezor_multiacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str, accounts=2)\n\n    async def test_upgrade_from_client_2_2_0_multisig(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_3_2_seeded(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_3_2_importedkeys(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_3_2_watchaddresses(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_3_2_trezor_singleacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_3_2_trezor_multiacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str, accounts=2)\n\n    async def test_upgrade_from_client_2_3_2_multisig(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_4_3_seeded(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_4_3_importedkeys(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_4_3_watchaddresses(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_4_3_trezor_singleacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_4_3_trezor_multiacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str, accounts=2)\n\n    async def test_upgrade_from_client_2_4_3_multisig(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_5_4_seeded(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_5_4_importedkeys(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_5_4_watchaddresses(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_5_4_trezor_singleacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_5_4_trezor_multiacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str, accounts=2)\n\n    async def test_upgrade_from_client_2_5_4_multisig(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_6_4_seeded(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_6_4_importedkeys(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_6_4_watchaddresses(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_6_4_multisig(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_7_18_seeded(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_7_18_importedkeys(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_7_18_watchaddresses(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_7_18_trezor_singleacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_7_18_multisig(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    # seed_version 13 is ambiguous\n    # client 2.7.18 created wallets with an earlier \"v13\" structure\n    # client 2.8.3 created wallets with a later \"v13\" structure\n    # client 2.8.3 did not do a proper clean-slate upgrade\n    # the wallet here was created in 2.7.18 with a couple privkeys imported\n    # then opened in 2.8.3, after which a few other new privkeys were imported\n    # it's in some sense in an \"inconsistent\" state\n    async def test_upgrade_from_client_2_8_3_importedkeys_flawed_previous_upgrade_from_2_7_18(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_8_3_seeded(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_8_3_importedkeys(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_8_3_watchaddresses(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_8_3_trezor_singleacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_8_3_multisig(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_9_3_seeded(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    @as_testnet\n    async def test_upgrade_from_client_2_9_3_old_seeded_with_realistic_history(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_9_3_importedkeys(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_9_3_watchaddresses(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_9_3_trezor_singleacc(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_2_9_3_multisig(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    async def test_upgrade_from_client_3_2_3_ledger_standard_keystore_changes(self):\n        # see #6066\n        wallet_str = self._get_wallet_str()\n        db = await self._upgrade_storage(wallet_str)\n        wallet = Wallet(db, config=self.config)\n        ks = wallet.keystore\n        # to simulate ks.opportunistically_fill_in_missing_info_from_device():\n        ks._root_fingerprint = \"deadbeef\"\n        ks.is_requesting_to_be_rewritten_to_wallet_file = True\n        await wallet.stop()\n\n    async def test_upgrade_from_client_2_9_3_importedkeys_keystore_changes(self):\n        # see #6401\n        wallet_str = self._get_wallet_str()\n        db = await self._upgrade_storage(wallet_str)\n        wallet = Wallet(db, config=self.config)\n        wallet.import_private_keys(\n            [\"p2wpkh:L1cgMEnShp73r9iCukoPE3MogLeueNYRD9JVsfT1zVHyPBR3KqBY\"],\n            password=None\n        )\n        await wallet.stop()\n\n    @as_testnet\n    async def test_upgrade_from_client_3_3_8_xpub_with_realistic_history(self):\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n    @as_testnet\n    async def test_upgrade_from_client_4_5_2_9dk_with_ln(self):\n        # This is a realistic testnet wallet, from the \"9dk\" seed, including some lightning sends/receives,\n        # some labels, frozen addresses, saved local txs, invoices/requests, etc. The file also has partial writes.\n        # Also, regression test for #8913\n        wallet_str = self._get_wallet_str()\n        await self._upgrade_storage(wallet_str)\n\n##########\n\n    plugins: 'electrum.plugin.Plugins'\n\n    def setUp(self):\n        super().setUp()\n        gui_name = 'cmdline'\n        # TODO it's probably wasteful to load all plugins... only need Trezor\n        self.plugins = Plugins(self.config, gui_name)\n\n    def tearDown(self):\n        self.plugins.stop()\n        self.plugins.stopped_event.wait()\n        super().tearDown()\n\n    async def _upgrade_storage(self, wallet_json, accounts=1) -> Optional[WalletDB]:\n        if accounts == 1:\n            # test manual upgrades\n            try:\n                db = self._load_db_from_json_string(\n                    wallet_json=wallet_json,\n                    upgrade=False)\n            except WalletRequiresUpgrade:\n                db = self._load_db_from_json_string(\n                    wallet_json=wallet_json,\n                    upgrade=True)\n                await self._sanity_check_upgraded_db(db)\n            return db\n        else:\n            try:\n                db = self._load_db_from_json_string(\n                    wallet_json=wallet_json,\n                    upgrade=False)\n            except WalletRequiresSplit as e:\n                split_data = e._split_data\n                self.assertEqual(accounts, len(split_data))\n                for item in split_data:\n                    data = json.dumps(item)\n                    new_db = WalletDB(data, storage=None, upgrade=True)\n                    await self._sanity_check_upgraded_db(new_db)\n\n    async def _sanity_check_upgraded_db(self, db):\n        wallet = Wallet(db, config=self.config)\n        await wallet.stop()\n\n    @staticmethod\n    def _load_db_from_json_string(*, wallet_json, upgrade):\n        db = WalletDB(wallet_json, storage=None, upgrade=upgrade)\n        return db\n"
  },
  {
    "path": "tests/test_transaction.py",
    "content": "import json\nimport os\nfrom typing import NamedTuple, Union\n\nfrom electrum_ecc import ECPrivkey\n\nfrom electrum import transaction, bitcoin\nfrom electrum.transaction import (convert_raw_tx_to_hex, tx_from_any, Transaction,\n                                  PartialTransaction, TxOutpoint, PartialTxInput,\n                                  PartialTxOutput, Sighash, match_script_against_template,\n                                  SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT, TxOutput, script_GetOp,\n                                  MalformedBitcoinScript)\nfrom electrum.util import bfh\nfrom electrum.bitcoin import (deserialize_privkey, opcodes,\n                              construct_script, construct_witness)\nfrom electrum import descriptor\n\nfrom .test_bitcoin import disable_ecdsa_r_value_grinding\nfrom . import ElectrumTestCase\n\nsigned_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'\nv2_blob = \"0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700\"\nsigned_segwit_blob = \"01000000000101b66d722484f2db63e827ebf41d02684fed0c6550e85015a6c9d41ef216a8a6f00000000000fdffffff0280c3c90100000000160014b65ce60857f7e7892b983851c2a8e3526d09e4ab64bac30400000000160014c478ebbc0ab2097706a98e10db7cf101839931c4024730440220789c7d47f876638c58d98733c30ae9821c8fa82b470285dcdf6db5994210bf9f02204163418bbc44af701212ad42d884cc613f3d3d831d2d0cc886f767cca6e0235e012103083a6dc250816d771faa60737bfe78b23ad619f6b458e0a1f1688e3a0605e79c00000000\"\n\nsigned_blob_signatures = [bfh('3046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d98501'),]\n\nclass TestBCDataStream(ElectrumTestCase):\n\n    def test_compact_size(self):\n        s = transaction.BCDataStream()\n        values = [0, 1, 252, 253, 2**16-1, 2**16, 2**32-1, 2**32, 2**64-1]\n        for v in values:\n            s.write_compact_size(v)\n\n        with self.assertRaises(transaction.SerializationError):\n            s.write_compact_size(-1)\n\n        self.assertEqual(s.input.hex(),\n                          '0001fcfdfd00fdfffffe00000100feffffffffff0000000001000000ffffffffffffffffff')\n        for v in values:\n            self.assertEqual(s.read_compact_size(), v)\n\n        with self.assertRaises(transaction.SerializationError):\n            s.read_compact_size()\n\n    def test_string(self):\n        s = transaction.BCDataStream()\n        with self.assertRaises(transaction.SerializationError):\n            s.read_string()\n\n        msgs = ['Hello', ' ', 'World', '', '!']\n        for msg in msgs:\n            s.write_string(msg)\n        for msg in msgs:\n            self.assertEqual(s.read_string(), msg)\n\n        with self.assertRaises(transaction.SerializationError):\n            s.read_string()\n\n    def test_bytes(self):\n        s = transaction.BCDataStream()\n        with self.assertRaises(transaction.SerializationError):\n            s.read_bytes(1)\n        s.write(b'foobar')\n        self.assertEqual(s.read_bytes(3), b'foo')\n        self.assertEqual(s.read_bytes(2), b'ba')\n        with self.assertRaises(transaction.SerializationError):\n            s.read_bytes(4)\n        self.assertEqual(s.read_bytes(0), b'')\n        self.assertEqual(s.read_bytes(1), b'r')\n        self.assertEqual(s.read_bytes(0), b'')\n\n    def test_bool(self):\n        s = transaction.BCDataStream()\n        s.write(b'f\\x00\\x00b')\n        self.assertTrue(s.read_boolean())\n        self.assertFalse(s.read_boolean())\n        self.assertFalse(s.read_boolean())\n        self.assertTrue(s.read_boolean())\n        s.write_boolean(True)\n        s.write_boolean(False)\n        self.assertEqual(b'\\x01\\x00', s.read_bytes(2))\n        self.assertFalse(s.can_read_more())\n\n\nclass TestTransaction(ElectrumTestCase):\n    def test_match_against_script_template(self):\n        script = construct_script([opcodes.OP_5, bytes(29)])\n        self.assertTrue(match_script_against_template(script, SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT))\n        script = construct_script([opcodes.OP_NOP, bytes(30)])\n        self.assertFalse(match_script_against_template(script, SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT))\n        script = construct_script([opcodes.OP_0, bytes(50)])\n        self.assertFalse(match_script_against_template(script, SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT))\n\n    def test_script_GetOp(self):\n        # TODO add more test cases for script_GetOp\n        # cases from https://github.com/bitcoinj/bitcoinj/blob/09defa626648687f8bd6ea7d197818249eebd3c8/core/src/test/resources/org/bitcoinj/script/script_tests.json#L721-L723\n        with self.assertRaises(MalformedBitcoinScript):\n            [x for x in script_GetOp(bfh(\"4c01\"))]            # PUSHDATA1 with not enough bytes\n        with self.assertRaises(MalformedBitcoinScript):\n            [x for x in script_GetOp(bfh(\"4d0200ff\"))]        # PUSHDATA2 with not enough bytes\n        with self.assertRaises(MalformedBitcoinScript):\n            [x for x in script_GetOp(bfh(\"4e03000000ffff\"))]  # PUSHDATA4 with not enough bytes\n\n    def test_tx_update_signatures(self):\n        tx = tx_from_any(\"cHNidP8BAFUBAAAAASpcmpT83pj1WBzQAWLGChOTbOt1OJ6mW/OGM7Qk60AxAAAAAAD/////AUBCDwAAAAAAGXapFCMKw3g0BzpCFG8R74QUrpKf6q/DiKwAAAAAAAAA\")\n        pubkey = bfh('02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6')\n        script_type = 'p2pkh'\n        desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=script_type)\n        tx.inputs()[0].script_descriptor = desc\n        tx.update_signatures(signed_blob_signatures)\n        self.assertEqual(tx.serialize(), signed_blob)\n\n    def test_tx_setting_locktime_invalidates_ser_cache(self):\n        tx = tx_from_any(\"cHNidP8BAJICAAAAAdAEtnw/IOVkr4oexG2xYnm+Vevsn3J7nbZsGpiBWS8MAQAAAAD9////A2Q5AwAAAAAAF6kUF6jKG6BuNVhq1RilflIDCitepw6H/NEEAAAAAAAXqRQx9SsFxDAaaOWbLB2ely1ZoZ61DYeIbQoAAAAAABYAFItCjFDsC28Z1R3tFaoi//pcInvnI3AZAAABAR+weRIAAAAAABYAFEK0I6qyqoA/lXCEgysQNZvqokaQIgYC9tgRn6/8hlDLEvEg3lKD1HmNim0gGRYwt4x3aJURIq4MqAq7DwEAAAAUAAAAAAAAIgICXYdVjyDIufLQ3yeDA4M8016luFER2SWaGPk6UF8CbuQMqAq7DwEAAAAXAAAAAA==\")\n        self.assertEqual(\"2774c819a05e44861a0555401d2741e6c03079cc4d892c69b910c0f52f407859\", tx.txid())\n        tx.locktime = 111222333\n        self.assertEqual(\"3d33a69c3f7717840b266c24ae1a6d29486820249b47261232e93ee118a6565b\", tx.txid())\n\n    def test_tx_setting_version_invalidates_ser_cache(self):\n        tx = tx_from_any(\"cHNidP8BAJICAAAAAdAEtnw/IOVkr4oexG2xYnm+Vevsn3J7nbZsGpiBWS8MAQAAAAD9////A2Q5AwAAAAAAF6kUF6jKG6BuNVhq1RilflIDCitepw6H/NEEAAAAAAAXqRQx9SsFxDAaaOWbLB2ely1ZoZ61DYeIbQoAAAAAABYAFItCjFDsC28Z1R3tFaoi//pcInvnI3AZAAABAR+weRIAAAAAABYAFEK0I6qyqoA/lXCEgysQNZvqokaQIgYC9tgRn6/8hlDLEvEg3lKD1HmNim0gGRYwt4x3aJURIq4MqAq7DwEAAAAUAAAAAAAAIgICXYdVjyDIufLQ3yeDA4M8016luFER2SWaGPk6UF8CbuQMqAq7DwEAAAAXAAAAAA==\")\n        self.assertEqual(\"2774c819a05e44861a0555401d2741e6c03079cc4d892c69b910c0f52f407859\", tx.txid())\n        tx.version = 555\n        self.assertEqual(\"8a9b89a1a7aac1995dd013069d9866197d77c14c22315958d612fc02fd4b596a\", tx.txid())\n\n    def test_tx_deserialize_for_signed_network_tx(self):\n        tx = transaction.Transaction(signed_blob)\n        tx.deserialize()\n        self.assertEqual(1, tx.version)\n        self.assertEqual(0, tx.locktime)\n        self.assertEqual(1, len(tx.inputs()))\n        self.assertEqual(4294967295, tx.inputs()[0].nsequence)\n        self.assertEqual(bfh('493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6'),\n                         tx.inputs()[0].script_sig)\n        self.assertEqual(None, tx.inputs()[0].witness)\n        self.assertEqual('3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a:0', tx.inputs()[0].prevout.to_str())\n        self.assertEqual(1, len(tx.outputs()))\n        self.assertEqual(bfh('76a914230ac37834073a42146f11ef8414ae929feaafc388ac'), tx.outputs()[0].scriptpubkey)\n        self.assertEqual('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', tx.outputs()[0].address)\n        self.assertEqual(1000000, tx.outputs()[0].value)\n\n        self.assertEqual(tx.serialize(), signed_blob)\n\n    def test_estimated_tx_size(self):\n        tx = transaction.Transaction(signed_blob)\n\n        self.assertEqual(tx.estimated_total_size(), 193)\n        self.assertEqual(tx.estimated_base_size(), 193)\n        self.assertEqual(tx.estimated_witness_size(), 0)\n        self.assertEqual(tx.estimated_weight(), 772)\n        self.assertEqual(tx.estimated_size(), 193)\n\n    def test_estimated_output_size(self):\n        estimated_output_size = transaction.Transaction.estimated_output_size_for_address\n        self.assertEqual(estimated_output_size('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), 34)\n        self.assertEqual(estimated_output_size('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), 32)\n        self.assertEqual(estimated_output_size('bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af'), 31)\n        self.assertEqual(estimated_output_size('bc1qnvks7gfdu72de8qv6q6rhkkzu70fqz4wpjzuxjf6aydsx7wxfwcqnlxuv3'), 43)\n\n    # TODO other tests for segwit tx\n    def test_tx_signed_segwit(self):\n        tx = transaction.Transaction(signed_segwit_blob)\n\n        self.assertEqual(tx.estimated_total_size(), 222)\n        self.assertEqual(tx.estimated_base_size(), 113)\n        self.assertEqual(tx.estimated_witness_size(), 109)\n        self.assertEqual(tx.estimated_weight(), 561)\n        self.assertEqual(tx.estimated_size(), 141)\n\n    def test_version_field(self):\n        tx = transaction.Transaction(v2_blob)\n        self.assertEqual(tx.txid(), \"b97f9180173ab141b61b9f944d841e60feec691d6daab4d4d932b24dd36606fe\")\n\n    def test_convert_raw_tx_to_hex(self):\n        # raw hex\n        self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600',\n                         convert_raw_tx_to_hex('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600'))\n        # base43\n        self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600',\n                         convert_raw_tx_to_hex('64XF-8+PM6*4IYN-QWW$B2QLNW+:C8-$I$-+T:L.6DKXTSWSFFONDP1J/MOS3SPK0-SYVW38U9.3+A1/*2HTHQTJGP79LVEK-IITQJ1H.C/X$NSOV$8DWR6JAFWXD*LX4-EN0.BDOF+PPYPH16$NM1H.-MAA$V1SCP0Q.6Y5FR822S6K-.5K5F.Z4Q:0SDRG-4GEBLAO4W9Z*H-$1-KDYAFOGF675W0:CK5M1LT92IG:3X60P3GKPM:X2$SP5A7*LT9$-TTEG0/DRZYV$7B4ADL9CVS5O7YG.J64HLZ24MVKO/-GV:V.T/L$D3VQ:MR8--44HK8W'))\n\n    def test_get_address_from_output_script(self):\n        # the inverse of this test is in test_bitcoin: test_address_to_script\n        addr_from_script = lambda script: transaction.get_address_from_output_script(bfh(script))\n\n        # bech32/bech32m native segwit\n        # test vectors from BIP-0173/BIP-0350\n        self.assertEqual('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', addr_from_script('0014751e76e8199196d454941c45d1b3a323f1433bd6'))\n        self.assertEqual('bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y', addr_from_script('5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6'))\n        self.assertEqual('bc1sw50qgdz25j', addr_from_script('6002751e'))\n        self.assertEqual('bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs', addr_from_script('5210751e76e8199196d454941c45d1b3a323'))\n        self.assertEqual('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0', addr_from_script('512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))\n        # almost but not quite\n        self.assertEqual(None, addr_from_script('0013751e76e8199196d454941c45d1b3a323f1433b'))\n\n        # base58 p2pkh\n        self.assertEqual('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG', addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac'))\n        self.assertEqual('1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv', addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac'))\n        # almost but not quite\n        self.assertEqual(None, addr_from_script('76a9130000000000000000000000000000000000000088ac'))\n\n        # base58 p2sh\n        self.assertEqual('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT', addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487'))\n        self.assertEqual('3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji', addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387'))\n        # almost but not quite\n        self.assertEqual(None, addr_from_script('a912f47c8954e421031ad04ecd8e7752c947920687'))\n\n        # p2pk\n        self.assertEqual(None, addr_from_script('210289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'))\n        self.assertEqual(None, addr_from_script('41045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120ac'))\n        # almost but not quite\n        self.assertEqual(None, addr_from_script('200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac'))\n        self.assertEqual(None, addr_from_script('210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'))\n\n    def test_tx_serialize_methods_for_psbt(self):\n        raw_hex = \"70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000\"\n        raw_base64 = \"cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA\"\n        partial_tx = tx_from_any(raw_hex)\n        self.assertEqual(PartialTransaction, type(partial_tx))\n        self.assertEqual(raw_base64,\n                         partial_tx.serialize())\n        self.assertEqual(raw_hex,\n                         partial_tx.serialize_as_bytes().hex())\n        self.assertEqual(raw_base64,\n                         partial_tx._serialize_as_base64())\n\n    def test_tx_serialize_methods_for_network_tx(self):\n        raw_hex = \"0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000\"\n        tx = tx_from_any(raw_hex)\n        self.assertEqual(Transaction, type(tx))\n        self.assertEqual(raw_hex,\n                         tx.serialize())\n        self.assertEqual(raw_hex,\n                         tx.serialize_as_bytes().hex())\n\n    def test_tx_serialize_methods_for_psbt_that_is_ready_to_be_finalized(self):\n        raw_hex_psbt = \"70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000\"\n        raw_hex_network_tx = \"0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000\"\n        partial_tx = tx_from_any(raw_hex_psbt)\n        self.assertEqual(PartialTransaction, type(partial_tx))\n        self.assertEqual(raw_hex_network_tx,\n                         partial_tx.serialize())\n        self.assertEqual(raw_hex_network_tx,\n                         partial_tx.serialize_as_bytes().hex())\n        # note: the diff between the following, and raw_hex_psbt, is that we added\n        #       an extra FINAL_SCRIPTWITNESS field in finalize_psbt()\n        self.assertEqual(\"70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae010801000001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000\",\n                         partial_tx.serialize_as_bytes(force_psbt=True).hex())\n\n    def test_tx_from_any(self):\n        class RawTx(NamedTuple):\n            data: Union[str, bytes]\n        raw_tx_map = {\n            \"network_tx_hex_str\": RawTx(\"0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000\"),\n            \"network_tx_hex_bytes\": RawTx(b\"0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000\"),\n            \"network_tx_base43_str\": RawTx(\"3E2DH7.J3PKVZJ3RCOXQVS3Y./6-WE.75DDU0K58-0N1FRL565N8ZH-DG1Z.1IGWTE5HK8F7PWH5P8+V3XGZZ6GQBPHNDE+RD8CAQVV1/6PQEMJIZTGPMIJ93B8P$QX+Y2R:TGT9QW8S89U4N2.+FUT8VG+34USI/N/JJ3CE*KLSW:REE8T5Y*9:U6515JIUR$6TODLYHSDE3B5DAF:5TF7V*VAL3G40WBOM0DO2+CFKTTM$G-SO:8U0EW:M8V:4*R9ZDX$B1IRBP9PLMDK8H801PNTFB4$HL1+/U3F61P$4N:UAO88:N5D+J:HI4YR8IM:3A7K1YZ9VMRC/47$6GGW5JEL1N690TDQ4XW+TWHD:V.1.630QK*JN/.EITVU80YS3.8LWKO:2STLWZAVHUXFHQ..NZ0:.J/FTZM.KYDXIE1VBY7/:PHZMQ$.JZQ2.XT32440X/HM+UY/7QP4I+HTD9.DUSY-8R6HDR-B8/PF2NP7I2-MRW9VPW3U9.S0LQ.*221F8KVMD5ANJXZJ8WV4UFZ4R.$-NXVE+-FAL:WFERGU+WHJTHAP\"),\n            \"network_tx_base43_bytes\": RawTx(b\"3E2DH7.J3PKVZJ3RCOXQVS3Y./6-WE.75DDU0K58-0N1FRL565N8ZH-DG1Z.1IGWTE5HK8F7PWH5P8+V3XGZZ6GQBPHNDE+RD8CAQVV1/6PQEMJIZTGPMIJ93B8P$QX+Y2R:TGT9QW8S89U4N2.+FUT8VG+34USI/N/JJ3CE*KLSW:REE8T5Y*9:U6515JIUR$6TODLYHSDE3B5DAF:5TF7V*VAL3G40WBOM0DO2+CFKTTM$G-SO:8U0EW:M8V:4*R9ZDX$B1IRBP9PLMDK8H801PNTFB4$HL1+/U3F61P$4N:UAO88:N5D+J:HI4YR8IM:3A7K1YZ9VMRC/47$6GGW5JEL1N690TDQ4XW+TWHD:V.1.630QK*JN/.EITVU80YS3.8LWKO:2STLWZAVHUXFHQ..NZ0:.J/FTZM.KYDXIE1VBY7/:PHZMQ$.JZQ2.XT32440X/HM+UY/7QP4I+HTD9.DUSY-8R6HDR-B8/PF2NP7I2-MRW9VPW3U9.S0LQ.*221F8KVMD5ANJXZJ8WV4UFZ4R.$-NXVE+-FAL:WFERGU+WHJTHAP\"),\n            \"network_tx_raw_bytes\": RawTx(bytes.fromhex(\"0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000\")),\n            \"psbt_hex_str\": RawTx(\"70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000\"),\n            \"psbt_hex_bytes\": RawTx(b\"70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000\"),\n            \"psbt_base64_str\": RawTx(\"cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA\"),\n            \"psbt_base64_bytes\": RawTx(b\"cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA\"),\n            \"psbt_base43_str\": RawTx(\"EFS+4WQ2R.5QWTVLQS.BWHG21Q-M0HVA+KKLG+9RYJ2B/GHJ$3OUOKH32U0NISGM$CI*KI24$PKQI0F*GA..*DW:UJ4FM-6S3B*VIJ0.M8M*DVCR9+TNG2R/IMZY0A72MS8+QMTO*:25V577L*DRC78WBK0BNAK7/8JQR-FA/L-KNBBKTQRZEU93T/Z/.7OBC6A7WWZ49DNHLG:X37ISM2+.ZT4ZC00.G8K8O6NM48GRR+N/-W/ASXT5VLPPHPRWR2PHMLZWDYAN8DMW$1EKD/EU4LHQWIKM0N42B*/32MQ1C:I17FMQVVQF6OA*X1RW8X.H17040FPH8/L5HBPFTTT0PMJGR9I0/J0+4PS5WJE/:W-Y.YA:SMFZMO36IMK0P/KUHVR.44OPT$TD*ZHSPZVULFWH75T+APO636*NYL19ZJIU3N37$KN7OY7*WOA498BWCPM$G6::C:YPDZ:PUISTZB-RL**B/QAX+HA*55O+:R/L8*J9B*G:QU6FS+HS+:OXPKOP-GDJK$NU72E4Y11BZENGIJWLWZ+C:01G$H-*LY5.LGC5I1A4G7$KT9BZ81LEPV5D09RAKI5BW1*O8QWMIIG17/8$J0X:W7F9FC7EMQRLESY2NNNVLZTHWB/FJVB-*.5JU0GQ*3JIBTQ$OR3AHTJ-U-C-8MWV$R::*VTRY-2+*H7U594$0JCN27R9*TY0+8:3Q7T/HES5OF*GLL::5Z8M$JNCLLKH6:J:GCT$E:27AXUA1L4ROJO3*-86C2QH.9TBNP0SS51XRL/J7:-+QES+37SOZIJCKG2XOUVPJR:F+MGS1L6-8/YA.6BKZ+FMF8F:OT2P4X:9M4P8IGM-/UUSS8S5*8YMZ$S+JKY.DHDCY$$+F53TGYK1.RHYA68-KO3O:$$IO/R9995TPAE6FF6$UOAUD5A2YLY.Q$VE$Q*0437K-DHKRFM$Q$3G:58+GER9D.6S$EW++HBBE0T62MAA/$-A2OV22QHY.JHK:6GNT.QQ*5YWZKCZI$+IQPU0UT/0H0PR9BYB1BB-Y+A.Q2HD$JP+:AT0CQR$R3PVHC9K/IPTVWJK0J4$J-B-CL6713L5PM-33-.CHXGJ$*ME.U-71V2JNX*W7JYC$V2VEEL:3GU3HGV84O.5PT+K*NB/2.-0.GCQNJ*ZOC$M2Y86V:URJ4/.ZVK6X1I--.B9NAWE54IDPLRN0FSWYGA/INDLPUBW7SZ/YADHWFLU*MWI121O2Y56QWG.EFNLSIVAVXYF$.:N/OU-PS7U/*Z94CR7T0+.F+RL9.-U5FQ1QL/A2$O2E0$TP-AX4*55QET9BPH6:K+CD$.+$*F0BUWSW.*$IF*WIC+UMJRZP5.50V1DMTIZ.D/2+$0T-GDBE7LHPGY0X0:G/*ZPTAMQABEC4HPML4UCLSAR-.5UT-X1.PMM60HUFAF\"),\n            \"psbt_base43_bytes\": RawTx(b'EFS+4WQ2R.5QWTVLQS.BWHG21Q-M0HVA+KKLG+9RYJ2B/GHJ$3OUOKH32U0NISGM$CI*KI24$PKQI0F*GA..*DW:UJ4FM-6S3B*VIJ0.M8M*DVCR9+TNG2R/IMZY0A72MS8+QMTO*:25V577L*DRC78WBK0BNAK7/8JQR-FA/L-KNBBKTQRZEU93T/Z/.7OBC6A7WWZ49DNHLG:X37ISM2+.ZT4ZC00.G8K8O6NM48GRR+N/-W/ASXT5VLPPHPRWR2PHMLZWDYAN8DMW$1EKD/EU4LHQWIKM0N42B*/32MQ1C:I17FMQVVQF6OA*X1RW8X.H17040FPH8/L5HBPFTTT0PMJGR9I0/J0+4PS5WJE/:W-Y.YA:SMFZMO36IMK0P/KUHVR.44OPT$TD*ZHSPZVULFWH75T+APO636*NYL19ZJIU3N37$KN7OY7*WOA498BWCPM$G6::C:YPDZ:PUISTZB-RL**B/QAX+HA*55O+:R/L8*J9B*G:QU6FS+HS+:OXPKOP-GDJK$NU72E4Y11BZENGIJWLWZ+C:01G$H-*LY5.LGC5I1A4G7$KT9BZ81LEPV5D09RAKI5BW1*O8QWMIIG17/8$J0X:W7F9FC7EMQRLESY2NNNVLZTHWB/FJVB-*.5JU0GQ*3JIBTQ$OR3AHTJ-U-C-8MWV$R::*VTRY-2+*H7U594$0JCN27R9*TY0+8:3Q7T/HES5OF*GLL::5Z8M$JNCLLKH6:J:GCT$E:27AXUA1L4ROJO3*-86C2QH.9TBNP0SS51XRL/J7:-+QES+37SOZIJCKG2XOUVPJR:F+MGS1L6-8/YA.6BKZ+FMF8F:OT2P4X:9M4P8IGM-/UUSS8S5*8YMZ$S+JKY.DHDCY$$+F53TGYK1.RHYA68-KO3O:$$IO/R9995TPAE6FF6$UOAUD5A2YLY.Q$VE$Q*0437K-DHKRFM$Q$3G:58+GER9D.6S$EW++HBBE0T62MAA/$-A2OV22QHY.JHK:6GNT.QQ*5YWZKCZI$+IQPU0UT/0H0PR9BYB1BB-Y+A.Q2HD$JP+:AT0CQR$R3PVHC9K/IPTVWJK0J4$J-B-CL6713L5PM-33-.CHXGJ$*ME.U-71V2JNX*W7JYC$V2VEEL:3GU3HGV84O.5PT+K*NB/2.-0.GCQNJ*ZOC$M2Y86V:URJ4/.ZVK6X1I--.B9NAWE54IDPLRN0FSWYGA/INDLPUBW7SZ/YADHWFLU*MWI121O2Y56QWG.EFNLSIVAVXYF$.:N/OU-PS7U/*Z94CR7T0+.F+RL9.-U5FQ1QL/A2$O2E0$TP-AX4*55QET9BPH6:K+CD$.+$*F0BUWSW.*$IF*WIC+UMJRZP5.50V1DMTIZ.D/2+$0T-GDBE7LHPGY0X0:G/*ZPTAMQABEC4HPML4UCLSAR-.5UT-X1.PMM60HUFAF'),\n            \"psbt_raw_bytes\": RawTx(bytes.fromhex(\"70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000\")),\n        }\n        whitespace_str = \" \\r\\n  \\n  \"\n        whitespace_bytes = b\" \\r\\n  \\n  \"\n        for case_name, raw_tx in raw_tx_map.items():\n            with self.subTest(msg=case_name, has_whitespaces=False):\n                data = raw_tx.data\n                tx_from_any(data)  # test if raises (should not)\n            with self.subTest(msg=case_name, has_whitespaces=True):\n                mid = len(raw_tx.data) // 2\n                if isinstance(raw_tx.data, str):\n                    # for str, sprinkle whitespaces all over\n                    data = whitespace_str + raw_tx.data[:mid] + whitespace_str + raw_tx.data[mid:] + whitespace_str\n                    tx_from_any(data)  # test if raises (should not)\n                else:\n                    assert isinstance(raw_tx.data, bytes)\n                    # whitespace leading/trailing\n                    data = whitespace_bytes + raw_tx.data + whitespace_bytes\n                    with self.assertRaises(transaction.SerializationError):\n                        tx_from_any(data)  # should raise\n                    # whitespace in middle\n                    data = raw_tx.data[:mid] + whitespace_bytes + raw_tx.data[mid:]\n                    with self.assertRaises(transaction.SerializationError):\n                        tx_from_any(data)  # should raise\n\n#####\n\n    def _run_naive_tests_on_tx(self, raw_tx, txid):\n        tx = transaction.Transaction(raw_tx)\n        self.assertEqual(txid, tx.txid())\n        self.assertEqual(raw_tx, tx.serialize())\n        self.assertTrue(tx.estimated_size() >= 0)\n\n    def test_txid_coinbase_to_p2pk(self):\n        raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4103400d0302ef02062f503253482f522cfabe6d6dd90d39663d10f8fd25ec88338295d4c6ce1c90d4aeb368d8bdbadcc1da3b635801000000000000000474073e03ffffffff013c25cf2d01000000434104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac00000000'\n        txid = 'dbaf14e1c476e76ea05a8b71921a46d6b06f0a950f17c5f9f1a03b8fae467f10'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_coinbase_to_p2pkh(self):\n        raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff25033ca0030400001256124d696e656420627920425443204775696c640800000d41000007daffffffff01c00d1298000000001976a91427a1f12771de5cc3b73941664b2537c15316be4388ac00000000'\n        txid = '4328f9311c6defd9ae1bd7f4516b62acf64b361eb39dfcf09d9925c5fd5c61e8'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_segwit_coinbase_to_p2pk(self):\n        raw_tx = '020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0502cd010101ffffffff0240be402500000000232103f4e686cdfc96f375e7c338c40c9b85f4011bb843a3e62e46a1de424ef87e9385ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000'\n        txid = 'fb5a57c24e640a6d8d831eb6e41505f3d54363c507da3733b098d820e3803301'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_segwit_coinbase_to_p2pkh(self):\n        raw_tx = '020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0502c3010101ffffffff0240be4025000000001976a9141ea896d897483e0eb33dd6423f4a07970d0a0a2788ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000'\n        txid = 'ed3d100577477d799107eba97e76770b3efa253c7200e9abfb43da5d2b33513e'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_segwit_coinbase_to_p2sh(self):\n        raw_tx = '020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff050214030101ffffffff02902f50090000000017a914ba582096f8647ca4195f55c8ef7e7e6e120e88b1870000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000'\n        txid = 'e28ee5866ec0535fe5efac5ad350cbf4960ed981b471a0c4a6baad1d8168d3d7'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2pk_to_p2pkh(self):\n        raw_tx = '010000000118231a31d2df84f884ced6af11dc24306319577d4d7c340124a7e2dd9c314077000000004847304402200b6c45891aed48937241907bc3e3868ee4c792819821fcde33311e5a3da4789a02205021b59692b652a01f5f009bd481acac2f647a7d9c076d71d85869763337882e01fdffffff016c95052a010000001976a9149c4891e7791da9e622532c97f43863768264faaf88ac00000000'\n        txid = '90ba90a5b115106d26663fce6c6215b8699c5d4b2672dd30756115f3337dddf9'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2pk_to_p2sh(self):\n        raw_tx = '0100000001e4643183d6497823576d17ac2439fb97eba24be8137f312e10fcc16483bb2d070000000048473044022032bbf0394dfe3b004075e3cbb3ea7071b9184547e27f8f73f967c4b3f6a21fa4022073edd5ae8b7b638f25872a7a308bb53a848baa9b9cc70af45fcf3c683d36a55301fdffffff011821814a0000000017a9143c640bc28a346749c09615b50211cb051faff00f8700000000'\n        txid = '172bdf5a690b874385b98d7ab6f6af807356f03a26033c6a65ab79b4ac2085b5'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2pk_to_p2wpkh(self):\n        raw_tx = '01000000015e5e2bf15f5793fdfd01e0ccd380033797ed2d4dba9498426ca84904176c26610000000049483045022100c77aff69f7ab4bb148f9bccffc5a87ee893c4f7f7f96c97ba98d2887a0f632b9022046367bdb683d58fa5b2e43cfc8a9c6d57724a27e03583942d8e7b9afbfeea5ab01fdffffff017289824a00000000160014460fc70f208bffa9abf3ae4abbd2f629d9cdcf5900000000'\n        txid = 'ca554b1014952f900aa8cf6e7ab02137a6fdcf933ad6a218de3891a2ef0c350d'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2pkh_to_p2pkh(self):\n        raw_tx = '0100000001f9dd7d33f315617530dd72264b5d9c69b815626cce3f66266d1015b1a590ba90000000006a4730440220699bfee3d280a499daf4af5593e8750b54fef0557f3c9f717bfa909493a84f60022057718eec7985b7796bb8630bf6ea2e9bf2892ac21bd6ab8f741a008537139ffe012103b4289890b40590447b57f773b5843bf0400e9cead08be225fac587b3c2a8e973fdffffff01ec24052a010000001976a914ce9ff3d15ed5f3a3d94b583b12796d063879b11588ac00000000'\n        txid = '24737c68f53d4b519939119ed83b2a8d44d716d7f3ca98bcecc0fbb92c2085ce'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2pkh_to_p2sh(self):\n        raw_tx = '010000000195232c30f6611b9f2f82ec63f5b443b132219c425e1824584411f3d16a7a54bc000000006b4830450221009f39ac457dc8ff316e5cc03161c9eff6212d8694ccb88d801dbb32e85d8ed100022074230bb05e99b85a6a50d2b71e7bf04d80be3f1d014ea038f93943abd79421d101210317be0f7e5478e087453b9b5111bdad586038720f16ac9658fd16217ffd7e5785fdffffff0200e40b540200000017a914d81df3751b9e7dca920678cc19cac8d7ec9010b08718dfd63c2c0000001976a914303c42b63569ff5b390a2016ff44651cd84c7c8988acc7010000'\n        txid = '155e4740fa59f374abb4e133b87247dccc3afc233cb97c2bf2b46bba3094aedc'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2pkh_to_p2wpkh(self):\n        raw_tx = '0100000001ce85202cb9fbc0ecbc98caf3d716d7448d2a3bd89e113999514b3df5687c7324000000006b483045022100adab7b6cb1179079c9dfc0021f4db0346730b7c16555fcc4363059dcdd95f653022028bcb816f4fb98615fb8f4b18af3ad3708e2d72f94a6466cc2736055860422cf012102a16a25148dd692462a691796db0a4a5531bcca970a04107bf184a2c9f7fd8b12fdffffff012eb6042a010000001600147d0170de18eecbe84648979d52b666dddee0b47400000000'\n        txid = 'ed29e100499e2a3a64a2b0cb3a68655b9acd690d29690fa541be530462bf3d3c'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2sh_to_p2pkh(self):\n        raw_tx = '01000000000101f9823f87af35d158e7dc81a67011f4e511e3f6cab07ac108e524b0ff8b950b39000000002322002041f0237866eb72e4a75cd6faf5ccd738703193907d883aa7b3a8169c636706a9fdffffff020065cd1d000000001976a9148150cd6cf729e7e262699875fec1f760b0aab3cc88acc46f9a3b0000000017a91433ccd0f95a7b9d8eef68be40bb59c64d6e14d87287040047304402205ca97126a5956c2deaa956a2006d79a348775d727074a04b71d9c18eb5e5525402207b9353497af15881100a2786adab56c8930c02d46cc1a8b55496c06e22d3459b01483045022100b4fa898057927c2d920ae79bca752dda58202ea8617d3e6ed96cbd5d1c0eb2fc02200824c0e742d1b4d643cec439444f5d8779c18d4f42c2c87cce24044a3babf2df0147522102db78786b3c214826bd27010e3c663b02d67144499611ee3f2461c633eb8f1247210377082028c124098b59a5a1e0ea7fd3ebca72d59c793aecfeedd004304bac15cd52aec9010000'\n        txid = '17e1d498ba82503e3bfa81ac4897a57e33f3d36b41bcf4765ba604466c478986'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2sh_to_p2sh(self):\n        raw_tx = '01000000000101b58520acb479ab656a3c03263af0567380aff6b67a8db98543870b695adf2b170000000017160014cfd2b9f7ed9d4d4429ed6946dbb3315f75e85f14fdffffff020065cd1d0000000017a91485f5681bec38f9f07ae9790d7f27c2bb90b5b63c87106ab32c0000000017a914ff402e164dfce874435641ae9ac41fc6fb14c4e18702483045022100b3d1c89c7c92151ed1df78815924569446782776b6a2c170ca5d74c5dd1ad9b102201d7bab1974fd2aa66546dd15c1f1e276d787453cec31b55a2bd97b050abf20140121024a1742ece86df3dbce4717c228cf51e625030cef7f5e6dde33a4fffdd17569eac7010000'\n        txid = 'ead0e7abfb24ddbcd6b89d704d7a6091e43804a458baa930adf6f1cb5b6b42f7'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2sh_to_p2wpkh(self):\n        raw_tx = '010000000001018689476c4604a65b76f4bc416bd3f3337ea59748ac81fa3b3e5082ba98d4e1170100000023220020ae40340707f9726c0f453c3d47c96e7f3b7b4b85608eb3668b69bbef9c7ab374fdffffff0218b2cc1d0000000017a914f2fdd81e606ff2ab804d7bb46bf8838a711c277b870065cd1d0000000016001496ad8959c1f0382984ecc4da61c118b4c8751e5104004730440220387b9e7d402fbcada9ba55a27a8d0563eafa9904ebd2f8f7e3d86e4b45bc0ec202205f37fa0e2bf8cbd384f804562651d7c6f69adce5db4c1a5b9103250a47f73e6b01473044022074903f4dd4fd6b32289be909eb5109924740daa55e79be6dbd728687683f9afa02205d934d981ca12cbec450611ca81dc4127f8da5e07dd63d41049380502de3f15401475221025c3810b37147105106cef970f9b91d3735819dee4882d515c1187dbd0b8f0c792103e007c492323084f1c103beff255836408af89bb9ae7f2fcf60502c28ff4b0c9152aeca010000'\n        txid = '6f294c84cbd0241650931b4c1be3dfb2f175d682c7a9538b30b173e1083deed3'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2wpkh_to_p2pkh(self):\n        raw_tx = '0100000000010197e6bf4a70bc118e3a8d9842ed80422e335679dfc29b5ba0f9123f6a5863b8470000000000fdffffff02402bca7f130000001600146f579c953d9e7e7719f2baa20bde22eb5f24119200e87648170000001976a9140cd8fa5fd81c3acf33f93efd179b388de8dd693388ac0247304402204ff33b3ea8fb270f62409bfc257457ca5eb1fec5e4d3a7c11aa487207e131d4d022032726b998e338e5245746716e5cd0b40d32b69d1535c3d841f049d98a5d819b1012102dc3ce3220363aff579eb2c45c973e8b186a829c987c3caea77c61975666e7d1bc8010000'\n        txid = 'c721ed35767a3a209b688e68e3bb136a72d2b631fe81c56be8bdbb948c343dbc'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2wpkh_to_p2sh(self):\n        raw_tx = '010000000001013c3dbf620453be41a50f69290d69cd9a5b65683acbb0a2643a2a9e4900e129ed0000000000fdffffff02002f68590000000017a914c7c4dcd0ddf70f15c6df13b4a4d56e9f13c49b2787a0429cd000000000160014e514e3ecf89731e7853e4f3a20983484c569d3910247304402205368cc548209303db5a8f2ebc282bd0f7af0d080ce0f7637758587f94d3971fb0220098cec5752554758bc5fa4de332b980d5e0054a807541581dc5e4de3ed29647501210233717cd73d95acfdf6bd72c4fb5df27cd6bd69ce947daa3f4a442183a97877efc8010000'\n        txid = '390b958bffb024e508c17ab0caf6e311e5f41170a681dce758d135af873f82f9'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_p2wpkh_to_p2wpkh(self):\n        raw_tx = '010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000'\n        txid = '51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_input_p2wsh_p2sh_not_multisig(self):\n        raw_tx = '0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000'\n        txid = 'e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    # input: p2sh, not multisig\n    def test_txid_regression_issue_3899(self):\n        raw_tx = '0100000004328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c010000000b0009630330472d5fae685bffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c020000000b0009630359646d5fae6858ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c030000000b000963034bd4715fae6854ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c040000000b000963036de8705fae6860ffffffff0130750000000000001976a914b5abca61d20f9062fb1fdbb880d9d93bac36675188ac00000000'\n        txid = 'f570d5d1e965ee61bcc7005f8fefb1d3abbed9d7ddbe035e2a68fa07e5fc4a0d'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_negative_version_num(self):\n        raw_tx = 'f0b47b9a01ecf5e5c3bbf2cf1f71ecdc7f708b0b222432e914b394e24aad1494a42990ddfc000000008b483045022100852744642305a99ad74354e9495bf43a1f96ded470c256cd32e129290f1fa191022030c11d294af6a61b3da6ed2c0c296251d21d113cfd71ec11126517034b0dcb70014104a0fe6e4a600f859a0932f701d3af8e0ecd4be886d91045f06a5a6b931b95873aea1df61da281ba29cadb560dad4fc047cf47b4f7f2570da4c0b810b3dfa7e500ffffffff0240420f00000000001976a9147eeacb8a9265cd68c92806611f704fc55a21e1f588ac05f00d00000000001976a914eb3bd8ccd3ba6f1570f844b59ba3e0a667024a6a88acff7f0000'\n        txid = 'c659729a7fea5071361c2c1a68551ca2bf77679b27086cc415adeeb03852e369'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_regression_issue_4333(self):\n        raw_tx = '0100000001a300499298b3f03200c05d1a15aa111a33c769aff6fb355c6bf52ebdb58ca37100000000171600756161616161616161616161616161616161616151fdffffff01c40900000000000017a914001975d5f07f3391674416c1fcd67fd511d257ff871bc71300'\n        txid = '9b9f39e314662a7433aadaa5c94a2f1e24c7e7bf55fc9e1f83abd72be933eb95'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    # see https://bitcoin.stackexchange.com/questions/38006/txout-script-criteria-scriptpubkey-critieria\n    def test_txid_invalid_op_return(self):\n        raw_tx = '01000000019ac03d5ae6a875d970128ef9086cef276a1919684a6988023cc7254691d97e6d010000006b4830450221009d41dc793ba24e65f571473d40b299b6459087cea1509f0d381740b1ac863cb6022039c425906fcaf51b2b84d8092569fb3213de43abaff2180e2a799d4fcb4dd0aa012102d5ede09a8ae667d0f855ef90325e27f6ce35bbe60a1e6e87af7f5b3c652140fdffffffff080100000000000000010101000000000000000202010100000000000000014c0100000000000000034c02010100000000000000014d0100000000000000044dffff010100000000000000014e0100000000000000064effffffff0100000000'\n        txid = 'ebc9fa1196a59e192352d76c0f6e73167046b9d37b8302b6bb6968dfd279b767'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n\n# these transactions are from Bitcoin Core unit tests --->\n# https://github.com/bitcoin/bitcoin/blob/11376b5583a283772c82f6d32d0007cdbf5b8ef0/src/test/data/tx_valid.json\n\n    def test_txid_bitcoin_core_0001(self):\n        raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000'\n        txid = '23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0002(self):\n        raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a0048304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000'\n        txid = 'fcabc409d8e685da28536e1e5ccc91264d755cd4c57ed4cae3dbaa4d3b93e8ed'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0003(self):\n        raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a01ff47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000'\n        txid = 'c9aa95f2c48175fdb70b34c23f1c3fc44f869b073a6f79b1343fbce30c3cb575'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0004(self):\n        raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000495147304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000'\n        txid = 'da94fda32b55deb40c3ed92e135d69df7efc4ee6665e0beb07ef500f407c9fd2'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0005(self):\n        raw_tx = '0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000494f47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000'\n        txid = 'f76f897b206e4f78d60fe40f2ccb542184cfadc34354d3bb9bdc30cc2f432b86'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0006(self):\n        raw_tx = '01000000010276b76b07f4935c70acf54fbf1f438a4c397a9fb7e633873c4dd3bc062b6b40000000008c493046022100d23459d03ed7e9511a47d13292d3430a04627de6235b6e51a40f9cd386f2abe3022100e7d25b080f0bb8d8d5f878bba7d54ad2fda650ea8d158a33ee3cbd11768191fd004104b0e2c879e4daf7b9ab68350228c159766676a14f5815084ba166432aab46198d4cca98fa3e9981d0a90b2effc514b76279476550ba3663fdcaff94c38420e9d5000000000100093d00000000001976a9149a7b0f3b80c6baaeedce0a0842553800f832ba1f88ac00000000'\n        txid = 'c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0007(self):\n        raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000'\n        txid = 'e41ffe19dff3cbedb413a2ca3fbbcd05cb7fd7397ffa65052f8928aa9c700092'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0008(self):\n        raw_tx = '01000000023d6cf972d4dff9c519eff407ea800361dd0a121de1da8b6f4138a2f25de864b4000000008a4730440220ffda47bfc776bcd269da4832626ac332adfca6dd835e8ecd83cd1ebe7d709b0e022049cffa1cdc102a0b56e0e04913606c70af702a1149dc3b305ab9439288fee090014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff21ebc9ba20594737864352e95b727f1a565756f9d365083eb1a8596ec98c97b7010000008a4730440220503ff10e9f1e0de731407a4a245531c9ff17676eda461f8ceeb8c06049fa2c810220c008ac34694510298fa60b3f000df01caa244f165b727d4896eb84f81e46bcc4014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff01f0da5200000000001976a914857ccd42dded6df32949d4646dfa10a92458cfaa88ac00000000'\n        txid = 'f7fdd091fa6d8f5e7a8c2458f5c38faffff2d3f1406b6e4fe2c99dcc0d2d1cbb'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0009(self):\n        raw_tx = '01000000020002000000000000000000000000000000000000000000000000000000000000000000000151ffffffff0001000000000000000000000000000000000000000000000000000000000000000000006b483045022100c9cdd08798a28af9d1baf44a6c77bcc7e279f47dc487c8c899911bc48feaffcc0220503c5c50ae3998a733263c5c0f7061b483e2b56c4c41b456e7d2f5a78a74c077032102d5c25adb51b61339d2b05315791e21bbe80ea470a49db0135720983c905aace0ffffffff010000000000000000015100000000'\n        txid = 'b56471690c3ff4f7946174e51df68b47455a0d29344c351377d712e6d00eabe5'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0010(self):\n        raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000009085768617420697320ffffffff010000000000000000015100000000'\n        txid = '99517e5b47533453cc7daa332180f578be68b80370ecfe84dbfff7f19d791da4'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0011(self):\n        raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100c66c9cdf4c43609586d15424c54707156e316d88b0a1534c9e6b0d4f311406310221009c0fe51dbc9c4ab7cc25d3fdbeccf6679fe6827f08edf2b4a9f16ee3eb0e438a0123210338e8034509af564c62644c07691942e0c056752008a173c89f60ab2a88ac2ebfacffffffff010000000000000000015100000000'\n        txid = 'ab097537b528871b9b64cb79a769ae13c3c3cd477cc9dddeebe657eabd7fdcea'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0012(self):\n        raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100e1eadba00d9296c743cb6ecc703fd9ddc9b3cd12906176a226ae4c18d6b00796022100a71aef7d2874deff681ba6080f1b278bac7bb99c61b08a85f4311970ffe7f63f012321030c0588dc44d92bdcbf8e72093466766fdc265ead8db64517b0c542275b70fffbacffffffff010040075af0750700015100000000'\n        txid = '4d163e00f1966e9a1eab8f9374c3e37f4deb4857c247270e25f7d79a999d2dc9'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0013(self):\n        raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000006d483045022027deccc14aa6668e78a8c9da3484fbcd4f9dcc9bb7d1b85146314b21b9ae4d86022100d0b43dece8cfb07348de0ca8bc5b86276fa88f7f2138381128b7c36ab2e42264012321029bb13463ddd5d2cc05da6e84e37536cb9525703cfd8f43afdb414988987a92f6acffffffff020040075af075070001510000000000000000015100000000'\n        txid = '9fe2ef9dde70e15d78894a4800b7df3bbfb1addb9a6f7d7c204492fdb6ee6cc4'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0014(self):\n        raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025151ffffffff010000000000000000015100000000'\n        txid = '99d3825137602e577aeaf6a2e3c9620fd0e605323dc5265da4a570593be791d4'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0015(self):\n        raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff6451515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151ffffffff010000000000000000015100000000'\n        txid = 'c0d67409923040cc766bbea12e4c9154393abef706db065ac2e07d91a9ba4f84'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0016(self):\n        raw_tx = '010000000200010000000000000000000000000000000000000000000000000000000000000000000049483045022100d180fd2eb9140aeb4210c9204d3f358766eb53842b2a9473db687fa24b12a3cc022079781799cd4f038b85135bbe49ec2b57f306b2bb17101b17f71f000fcab2b6fb01ffffffff0002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000'\n        txid = 'c610d85d3d5fdf5046be7f123db8a0890cee846ee58de8a44667cfd1ab6b8666'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0017(self):\n        raw_tx = '01000000020001000000000000000000000000000000000000000000000000000000000000000000004948304502203a0f5f0e1f2bdbcd04db3061d18f3af70e07f4f467cbc1b8116f267025f5360b022100c792b6e215afc5afc721a351ec413e714305cb749aae3d7fee76621313418df101010000000002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000'\n        txid = 'a647a7b3328d2c698bfa1ee2dd4e5e05a6cea972e764ccb9bd29ea43817ca64f'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0018(self):\n        raw_tx = '010000000370ac0a1ae588aaf284c308d67ca92c69a39e2db81337e563bf40c59da0a5cf63000000006a4730440220360d20baff382059040ba9be98947fd678fb08aab2bb0c172efa996fd8ece9b702201b4fb0de67f015c90e7ac8a193aeab486a1f587e0f54d0fb9552ef7f5ce6caec032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff7d815b6447e35fbea097e00e028fb7dfbad4f3f0987b4734676c84f3fcd0e804010000006b483045022100c714310be1e3a9ff1c5f7cacc65c2d8e781fc3a88ceb063c6153bf950650802102200b2d0979c76e12bb480da635f192cc8dc6f905380dd4ac1ff35a4f68f462fffd032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff3f1f097333e4d46d51f5e77b53264db8f7f5d2e18217e1099957d0f5af7713ee010000006c493046022100b663499ef73273a3788dea342717c2640ac43c5a1cf862c9e09b206fcb3f6bb8022100b09972e75972d9148f2bdd462e5cb69b57c1214b88fc55ca638676c07cfc10d8032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff0380841e00000000001976a914bfb282c70c4191f45b5a6665cad1682f2c9cfdfb88ac80841e00000000001976a9149857cc07bed33a5cf12b9c5e0500b675d500c81188ace0fd1c00000000001976a91443c52850606c872403c0601e69fa34b26f62db4a88ac00000000'\n        txid = 'afd9c17f8913577ec3509520bd6e5d63e9c0fd2a5f70c787993b097ba6ca9fae'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0019(self):\n        raw_tx = '01000000012312503f2491a2a97fcd775f11e108a540a5528b5d4dee7a3c68ae4add01dab300000000fdfe0000483045022100f6649b0eddfdfd4ad55426663385090d51ee86c3481bdc6b0c18ea6c0ece2c0b0220561c315b07cffa6f7dd9df96dbae9200c2dee09bf93cc35ca05e6cdf613340aa0148304502207aacee820e08b0b174e248abd8d7a34ed63b5da3abedb99934df9fddd65c05c4022100dfe87896ab5ee3df476c2655f9fbe5bd089dccbef3e4ea05b5d121169fe7f5f4014c695221031d11db38972b712a9fe1fc023577c7ae3ddb4a3004187d41c45121eecfdbb5b7210207ec36911b6ad2382860d32989c7b8728e9489d7bbc94a6b5509ef0029be128821024ea9fac06f666a4adc3fc1357b7bec1fd0bdece2b9d08579226a8ebde53058e453aeffffffff0180380100000000001976a914c9b99cddf847d10685a4fabaa0baf505f7c3dfab88ac00000000'\n        txid = 'f4b05f978689c89000f729cae187dcfbe64c9819af67a4f05c0b4d59e717d64d'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0020(self):\n        raw_tx = '0100000001f709fa82596e4f908ee331cb5e0ed46ab331d7dcfaf697fe95891e73dac4ebcb000000008c20ca42095840735e89283fec298e62ac2ddea9b5f34a8cbb7097ad965b87568100201b1b01dc829177da4a14551d2fc96a9db00c6501edfa12f22cd9cefd335c227f483045022100a9df60536df5733dd0de6bc921fab0b3eee6426501b43a228afa2c90072eb5ca02201c78b74266fac7d1db5deff080d8a403743203f109fbcabf6d5a760bf87386d20100ffffffff01c075790000000000232103611f9a45c18f28f06f19076ad571c344c82ce8fcfe34464cf8085217a2d294a6ac00000000'\n        txid = 'cc60b1f899ec0a69b7c3f25ddf32c4524096a9c5b01cbd84c6d0312a0c478984'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0021(self):\n        raw_tx = '01000000012c651178faca83be0b81c8c1375c4b0ad38d53c8fe1b1c4255f5e795c25792220000000049483045022100d6044562284ac76c985018fc4a90127847708c9edb280996c507b28babdc4b2a02203d74eca3f1a4d1eea7ff77b528fde6d5dc324ec2dbfdb964ba885f643b9704cd01ffffffff010100000000000000232102c2410f8891ae918cab4ffc4bb4a3b0881be67c7a1e7faa8b5acf9ab8932ec30cac00000000'\n        txid = '1edc7f214659d52c731e2016d258701911bd62a0422f72f6c87a1bc8dd3f8667'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0022(self):\n        raw_tx = '0100000001f725ea148d92096a79b1709611e06e94c63c4ef61cbae2d9b906388efd3ca99c000000000100ffffffff0101000000000000002321028a1d66975dbdf97897e3a4aef450ebeb5b5293e4a0b4a6d3a2daaa0b2b110e02ac00000000'\n        txid = '018adb7133fde63add9149a2161802a1bcf4bdf12c39334e880c073480eda2ff'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0023(self):\n        raw_tx = '0100000001be599efaa4148474053c2fa031c7262398913f1dc1d9ec201fd44078ed004e44000000004900473044022022b29706cb2ed9ef0cb3c97b72677ca2dfd7b4160f7b4beb3ba806aa856c401502202d1e52582412eba2ed474f1f437a427640306fd3838725fab173ade7fe4eae4a01ffffffff010100000000000000232103ac4bba7e7ca3e873eea49e08132ad30c7f03640b6539e9b59903cf14fd016bbbac00000000'\n        txid = '1464caf48c708a6cc19a296944ded9bb7f719c9858986d2501cf35068b9ce5a2'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0024(self):\n        raw_tx = '010000000112b66d5e8c7d224059e946749508efea9d66bf8d0c83630f080cf30be8bb6ae100000000490047304402206ffe3f14caf38ad5c1544428e99da76ffa5455675ec8d9780fac215ca17953520220779502985e194d84baa36b9bd40a0dbd981163fa191eb884ae83fc5bd1c86b1101ffffffff010100000000000000232103905380c7013e36e6e19d305311c1b81fce6581f5ee1c86ef0627c68c9362fc9fac00000000'\n        txid = '1fb73fbfc947d52f5d80ba23b67c06a232ad83fdd49d1c0a657602f03fbe8f7a'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0025(self):\n        raw_tx = '0100000001b0ef70cc644e0d37407e387e73bfad598d852a5aa6d691d72b2913cebff4bceb000000004a00473044022068cd4851fc7f9a892ab910df7a24e616f293bcb5c5fbdfbc304a194b26b60fba022078e6da13d8cb881a22939b952c24f88b97afd06b4c47a47d7f804c9a352a6d6d0100ffffffff0101000000000000002321033bcaa0a602f0d44cc9d5637c6e515b0471db514c020883830b7cefd73af04194ac00000000'\n        txid = '24cecfce0fa880b09c9b4a66c5134499d1b09c01cc5728cd182638bea070e6ab'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0026(self):\n        raw_tx = '0100000001c188aa82f268fcf08ba18950f263654a3ea6931dabc8bf3ed1d4d42aaed74cba000000004b0000483045022100940378576e069aca261a6b26fb38344e4497ca6751bb10905c76bb689f4222b002204833806b014c26fd801727b792b1260003c55710f87c5adbd7a9cb57446dbc9801ffffffff0101000000000000002321037c615d761e71d38903609bf4f46847266edc2fb37532047d747ba47eaae5ffe1ac00000000'\n        txid = '9eaa819e386d6a54256c9283da50c230f3d8cd5376d75c4dcc945afdeb157dd7'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0027(self):\n        raw_tx = '01000000012432b60dc72cebc1a27ce0969c0989c895bdd9e62e8234839117f8fc32d17fbc000000004a493046022100a576b52051962c25e642c0fd3d77ee6c92487048e5d90818bcf5b51abaccd7900221008204f8fb121be4ec3b24483b1f92d89b1b0548513a134e345c5442e86e8617a501ffffffff010000000000000000016a00000000'\n        txid = '46224764c7870f95b58f155bce1e38d4da8e99d42dbb632d0dd7c07e092ee5aa'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0028(self):\n        raw_tx = '01000000014710b0e7cf9f8930de259bdc4b84aa5dfb9437b665a3e3a21ff26e0bf994e183000000004a493046022100a166121a61b4eeb19d8f922b978ff6ab58ead8a5a5552bf9be73dc9c156873ea02210092ad9bc43ee647da4f6652c320800debcf08ec20a094a0aaf085f63ecb37a17201ffffffff010000000000000000016a00000000'\n        txid = '8d66836045db9f2d7b3a75212c5e6325f70603ee27c8333a3bce5bf670d9582e'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0029(self):\n        raw_tx = '01000000015ebaa001d8e4ec7a88703a3bcf69d98c874bca6299cca0f191512bf2a7826832000000004948304502203bf754d1c6732fbf87c5dcd81258aefd30f2060d7bd8ac4a5696f7927091dad1022100f5bcb726c4cf5ed0ed34cc13dadeedf628ae1045b7cb34421bc60b89f4cecae701ffffffff010000000000000000016a00000000'\n        txid = 'aab7ef280abbb9cc6fbaf524d2645c3daf4fcca2b3f53370e618d9cedf65f1f8'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0030(self):\n        raw_tx = '010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a900000000924830450221009c0a27f886a1d8cb87f6f595fbc3163d28f7a81ec3c4b252ee7f3ac77fd13ffa02203caa8dfa09713c8c4d7ef575c75ed97812072405d932bd11e6a1593a98b679370148304502201e3861ef39a526406bad1e20ecad06be7375ad40ddb582c9be42d26c3a0d7b240221009d0a3985e96522e59635d19cc4448547477396ce0ef17a58e7d74c3ef464292301ffffffff010000000000000000016a00000000'\n        txid = '6327783a064d4e350c454ad5cd90201aedf65b1fc524e73709c52f0163739190'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0031(self):\n        raw_tx = '010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a48304502207a6974a77c591fa13dff60cabbb85a0de9e025c09c65a4b2285e47ce8e22f761022100f0efaac9ff8ac36b10721e0aae1fb975c90500b50c56e8a0cc52b0403f0425dd0100ffffffff010000000000000000016a00000000'\n        txid = '892464645599cc3c2d165adcc612e5f982a200dfaa3e11e9ce1d228027f46880'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0032(self):\n        raw_tx = '010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a483045022100fa4a74ba9fd59c59f46c3960cf90cbe0d2b743c471d24a3d5d6db6002af5eebb02204d70ec490fd0f7055a7c45f86514336e3a7f03503dacecabb247fc23f15c83510151ffffffff010000000000000000016a00000000'\n        txid = '578db8c6c404fec22c4a8afeaf32df0e7b767c4dda3478e0471575846419e8fc'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0033(self):\n        raw_tx = '0100000001e0be9e32f1f89c3d916c4f21e55cdcd096741b895cc76ac353e6023a05f4f7cc00000000d86149304602210086e5f736a2c3622ebb62bd9d93d8e5d76508b98be922b97160edc3dcca6d8c47022100b23c312ac232a4473f19d2aeb95ab7bdf2b65518911a0d72d50e38b5dd31dc820121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac4730440220508fa761865c8abd81244a168392876ee1d94e8ed83897066b5e2df2400dad24022043f5ee7538e87e9c6aef7ef55133d3e51da7cc522830a9c4d736977a76ef755c0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000'\n        txid = '974f5148a0946f9985e75a240bb24c573adbbdc25d61e7b016cdbb0a5355049f'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0034(self):\n        raw_tx = '01000000013c6f30f99a5161e75a2ce4bca488300ca0c6112bde67f0807fe983feeff0c91001000000e608646561646265656675ab61493046022100ce18d384221a731c993939015e3d1bcebafb16e8c0b5b5d14097ec8177ae6f28022100bcab227af90bab33c3fe0a9abfee03ba976ee25dc6ce542526e9b2e56e14b7f10121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac493046022100c3b93edcc0fd6250eb32f2dd8a0bba1754b0f6c3be8ed4100ed582f3db73eba2022100bf75b5bd2eff4d6bf2bda2e34a40fcc07d4aa3cf862ceaa77b47b81eff829f9a01ab21038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000'\n        txid = 'b0097ec81df231893a212657bf5fe5a13b2bff8b28c0042aca6fc4159f79661b'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0035(self):\n        raw_tx = '01000000016f3dbe2ca96fa217e94b1017860be49f20820dea5c91bdcb103b0049d5eb566000000000fd1d0147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac47304402203757e937ba807e4a5da8534c17f9d121176056406a6465054bdd260457515c1a02200f02eccf1bec0f3a0d65df37889143c2e88ab7acec61a7b6f5aa264139141a2b0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000'\n        txid = 'feeba255656c80c14db595736c1c7955c8c0a497622ec96e3f2238fbdd43a7c9'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0036(self):\n        raw_tx = '01000000012139c555ccb81ee5b1e87477840991ef7b386bc3ab946b6b682a04a621006b5a01000000fdb40148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f2204148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390175ac4830450220646b72c35beeec51f4d5bc1cbae01863825750d7f490864af354e6ea4f625e9c022100f04b98432df3a9641719dbced53393022e7249fb59db993af1118539830aab870148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a580039017521038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000'\n        txid = 'a0c984fc820e57ddba97f8098fa640c8a7eb3fe2f583923da886b7660f505e1e'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0037(self):\n        raw_tx = '0100000002f9cbafc519425637ba4227f8d0a0b7160b4e65168193d5af39747891de98b5b5000000006b4830450221008dd619c563e527c47d9bd53534a770b102e40faa87f61433580e04e271ef2f960220029886434e18122b53d5decd25f1f4acb2480659fea20aabd856987ba3c3907e0121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffff42e7988254800876b69f24676b3e0205b77be476512ca4d970707dd5c60598ab00000000fd260100483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a53034930460221008431bdfa72bc67f9d41fe72e94c88fb8f359ffa30b33c72c121c5a877d922e1002210089ef5fc22dd8bfc6bf9ffdb01a9862d27687d424d1fefbab9e9c7176844a187a014c9052483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c7153aeffffffff01a08601000000000017a914d8dacdadb7462ae15cd906f1878706d0da8660e68700000000'\n        txid = '5df1375ffe61ac35ca178ebb0cab9ea26dedbd0e96005dfcee7e379fa513232f'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0038(self):\n        raw_tx = '0100000002dbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce000000006b4830450221009627444320dc5ef8d7f68f35010b4c050a6ed0d96b67a84db99fda9c9de58b1e02203e4b4aaa019e012e65d69b487fdf8719df72f488fa91506a80c49a33929f1fd50121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffffdbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce010000009300483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303ffffffff01a0860100000000001976a9149bc0bbdd3024da4d0c38ed1aecf5c68dd1d3fa1288ac00000000'\n        txid = 'ded7ff51d89a4e1ec48162aee5a96447214d93dfb3837946af2301a28f65dbea'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0039(self):\n        raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000'\n        txid = '3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0040(self):\n        raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d'\n        txid = 'abd62b4627d8d9b2d95fcfd8c87e37d2790637ce47d28018e3aece63c1d62649'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0041(self):\n        raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d'\n        txid = '58b6de8413603b7f556270bf48caedcf17772e7105f5419f6a80be0df0b470da'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0042(self):\n        raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff'\n        txid = '5f99c0abf511294d76cbe144d86b77238a03e086974bc7a8ea0bdb2c681a0324'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0043(self):\n        raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000'\n        txid = '25d35877eaba19497710666473c50d5527d38503e3521107a3fc532b74cd7453'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0044(self):\n        raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000feffffff'\n        txid = '1b9aef851895b93c62c29fbd6ca4d45803f4007eff266e2f96ff11e9b6ef197b'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0045(self):\n        raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000'\n        txid = '3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0046(self):\n        raw_tx = '01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1000000000100000000000000000001000000'\n        txid = 'f53761038a728b1f17272539380d96e93f999218f8dcb04a8469b523445cd0fd'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0047(self):\n        raw_tx = '0100000001000100000000000000000000000000000000000000000000000000000000000000000000030251b1000000000100000000000000000001000000'\n        txid = 'd193f0f32fceaf07bb25c897c8f99ca6f69a52f6274ca64efc2a2e180cb97fc1'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0048(self):\n        raw_tx = '010000000132211bdd0d568506804eef0d8cc3db68c3d766ab9306cdfcc0a9c89616c8dbb1000000006c493045022100c7bb0faea0522e74ff220c20c022d2cb6033f8d167fb89e75a50e237a35fd6d202203064713491b1f8ad5f79e623d0219ad32510bfaa1009ab30cbee77b59317d6e30001210237af13eb2d84e4545af287b919c2282019c9691cc509e78e196a9d8274ed1be0ffffffff0100000000000000001976a914f1b3ed2eda9a2ebe5a9374f692877cdf87c0f95b88ac00000000'\n        txid = '50a1e0e6a134a564efa078e3bd088e7e8777c2c0aec10a752fd8706470103b89'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0049(self):\n        raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000'\n        txid = 'e2207d1aaf6b74e5d98c2fa326d2dc803b56b30a3f90ce779fa5edb762f38755'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0050(self):\n        raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000'\n        txid = 'f335864f7c12ec7946d2c123deb91eb978574b647af125a414262380c7fbd55c'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0051(self):\n        raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000'\n        txid = 'd1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0052(self):\n        raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000'\n        txid = '3a13e1b6371c545147173cc4055f0ed73686a9f73f092352fb4b39ca27d360e6'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0053(self):\n        raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff40000100000000000000000000000000'\n        txid = 'bffda23e40766d292b0510a1b556453c558980c70c94ab158d8286b3413e220d'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0054(self):\n        raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000'\n        txid = '01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0055(self):\n        raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000800100000000000000000000000000'\n        txid = 'f6d2359c5de2d904e10517d23e7c8210cca71076071bbf46de9fbd5f6233dbf1'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0056(self):\n        raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000'\n        txid = '19c2b7377229dae7aa3e50142a32fd37cef7171a01682f536e9ffa80c186f6c9'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0057(self):\n        raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000'\n        txid = 'c9dda3a24cc8a5acb153d1085ecd2fecf6f87083122f8cdecc515b1148d4c40d'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0058(self):\n        raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000'\n        txid = 'd1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0059(self):\n        raw_tx = '020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000'\n        txid = '01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0060(self):\n        raw_tx = '02000000010001000000000000000000000000000000000000000000000000000000000000000000000251b2010000000100000000000000000000000000'\n        txid = '4b5e0aae1251a9dc66b4d5f483f1879bf518ea5e1765abc5a9f2084b43ed1ea7'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0061(self):\n        raw_tx = '0200000001000100000000000000000000000000000000000000000000000000000000000000000000030251b2010000000100000000000000000000000000'\n        txid = '5f16eb3ca4581e2dfb46a28140a4ee15f85e4e1c032947da8b93549b53c105f5'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0062(self):\n        raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000'\n        txid = 'b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0063(self):\n        raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000'\n        txid = 'b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0064(self):\n        raw_tx = '01000000000101000100000000000000000000000000000000000000000000000000000000000000000000171600144c9c3dfac4207d5d8cb89df5722cb3d712385e3fffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000'\n        txid = 'fee125c6cd142083fabd0187b1dd1f94c66c89ec6e6ef6da1374881c0c19aece'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0065(self):\n        raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000023220020ff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3dbffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000'\n        txid = '5f32557914351fee5f89ddee6c8983d476491d29e601d854e3927299e50450da'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0066(self):\n        raw_tx = '0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff05540b0000000000000151d0070000000000000151840300000000000001513c0f00000000000001512c010000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71000000000000'\n        txid = '07dfa2da3d67c8a2b9f7bd31862161f7b497829d5da90a88ba0f1a905e7a43f7'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0067(self):\n        raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0068(self):\n        raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff0484030000000000000151d0070000000000000151540b0000000000000151c800000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = 'f92bb6e4f3ff89172f23ef647f74c13951b665848009abb5862cdf7a0412415a'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0069(self):\n        raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0070(self):\n        raw_tx = '0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff04b60300000000000001519e070000000000000151860b00000000000001009600000000000000015100000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = 'e657e25fc9f2b33842681613402759222a58cf7dd504d6cdc0b69a0b8c2e7dcb'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0071(self):\n        raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0072(self):\n        raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff04b60300000000000001519e070000000000000151860b0000000000000100960000000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = '4ede5e22992d43d42ccdf6553fb46e448aa1065ba36423f979605c1e5ab496b8'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0073(self):\n        raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0074(self):\n        raw_tx = '01000000000103000100000000000000000000000000000000000000000000000000000000000000000000000200000000010000000000000000000000000000000000000000000000000000000000000100000000ffffffff000100000000000000000000000000000000000000000000000000000000000002000000000200000003e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = 'cfe9f4b19f52b8366860aec0d2b5815e329299b2e9890d477edd7f1182be7ac8'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0075(self):\n        raw_tx = '0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = 'aee8f4865ca40fa77ff2040c0d7de683bea048b103d42ca406dc07dd29d539cb'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0076(self):\n        raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0077(self):\n        raw_tx = '0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623ffffffffff1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = '8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0078(self):\n        raw_tx = '0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff010000000000000000015102fd08020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002755100000000'\n        txid = 'd93ab9e12d7c29d2adc13d5cdf619d53eec1f36eb6612f55af52be7ba0448e97'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0079(self):\n        raw_tx = '0100000000010c00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff0001000000000000000000000000000000000000000000000000000000000000020000006a473044022026c2e65b33fcd03b2a3b0f25030f0244bd23cc45ae4dec0f48ae62255b1998a00220463aa3982b718d593a6b9e0044513fd67a5009c2fdccc59992cffc2b167889f4012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000030000006a4730440220008bd8382911218dcb4c9f2e75bf5c5c3635f2f2df49b36994fde85b0be21a1a02205a539ef10fb4c778b522c1be852352ea06c67ab74200977c722b0bc68972575a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000040000006b483045022100d9436c32ff065127d71e1a20e319e4fe0a103ba0272743dbd8580be4659ab5d302203fd62571ee1fe790b182d078ecfd092a509eac112bea558d122974ef9cc012c7012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000050000006a47304402200e2c149b114ec546015c13b2b464bbcb0cdc5872e6775787527af6cbc4830b6c02207e9396c6979fb15a9a2b96ca08a633866eaf20dc0ff3c03e512c1d5a1654f148012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000060000006b483045022100b20e70d897dc15420bccb5e0d3e208d27bdd676af109abbd3f88dbdb7721e6d6022005836e663173fbdfe069f54cde3c2decd3d0ea84378092a5d9d85ec8642e8a41012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff00010000000000000000000000000000000000000000000000000000000000000700000000ffffffff00010000000000000000000000000000000000000000000000000000000000000800000000ffffffff00010000000000000000000000000000000000000000000000000000000000000900000000ffffffff00010000000000000000000000000000000000000000000000000000000000000a00000000ffffffff00010000000000000000000000000000000000000000000000000000000000000b0000006a47304402206639c6e05e3b9d2675a7f3876286bdf7584fe2bbd15e0ce52dd4e02c0092cdc60220757d60b0a61fc95ada79d23746744c72bac1545a75ff6c2c7cdb6ae04e7e9592012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0ce8030000000000000151e9030000000000000151ea030000000000000151eb030000000000000151ec030000000000000151ed030000000000000151ee030000000000000151ef030000000000000151f0030000000000000151f1030000000000000151f2030000000000000151f30300000000000001510248304502210082219a54f61bf126bfc3fa068c6e33831222d1d7138c6faa9d33ca87fd4202d6022063f9902519624254d7c2c8ea7ba2d66ae975e4e229ae38043973ec707d5d4a83012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022017fb58502475848c1b09f162cb1688d0920ff7f142bed0ef904da2ccc88b168f02201798afa61850c65e77889cbcd648a5703b487895517c88f85cdd18b021ee246a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000000247304402202830b7926e488da75782c81a54cd281720890d1af064629ebf2e31bf9f5435f30220089afaa8b455bbeb7d9b9c3fe1ed37d07685ade8455c76472cda424d93e4074a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022026326fcdae9207b596c2b05921dbac11d81040c4d40378513670f19d9f4af893022034ecd7a282c0163b89aaa62c22ec202cef4736c58cd251649bad0d8139bcbf55012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71024730440220214978daeb2f38cd426ee6e2f44131a33d6b191af1c216247f1dd7d74c16d84a02205fdc05529b0bc0c430b4d5987264d9d075351c4f4484c16e91662e90a72aab24012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402204a6e9f199dc9672cf2ff8094aaa784363be1eb62b679f7ff2df361124f1dca3302205eeb11f70fab5355c9c8ad1a0700ea355d315e334822fa182227e9815308ee8f012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000'\n        txid = 'b83579db5246aa34255642768167132a0c3d2932b186cd8fb9f5490460a0bf91'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0080(self):\n        raw_tx = '010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000'\n        txid = '2b1e44fff489d09091e5e20f9a01bbc0e8d80f0662e629fd10709cdb4922a874'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0081(self):\n        raw_tx = '0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff01d00700000000000001510003483045022100e078de4e96a0e05dcdc0a414124dd8475782b5f3f0ed3f607919e9a5eeeb22bf02201de309b3a3109adb3de8074b3610d4cf454c49b61247a2779a0bcbf31c889333032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc711976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac00000000'\n        txid = '60ebb1dd0b598e20dd0dd462ef6723dd49f8f803b6a2492926012360119cfdd7'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0082(self):\n        raw_tx = '0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff02e8030000000000000151e90300000000000001510247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000'\n        txid = 'ed0c7f4163e275f3f77064f471eac861d01fdf55d03aa6858ebd3781f70bf003'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0083(self):\n        raw_tx = '0100000000010200010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff02e9030000000000000151e80300000000000001510248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000'\n        txid = 'f531ddf5ce141e1c8a7fdfc85cc634e5ff686f446a5cf7483e9dbe076b844862'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0084(self):\n        raw_tx = '01000000020001000000000000000000000000000000000000000000000000000000000000000000004847304402202a0b4b1294d70540235ae033d78e64b4897ec859c7b6f1b2b1d8a02e1d46006702201445e756d2254b0f1dfda9ab8e1e1bc26df9668077403204f32d16a49a36eb6983ffffffff00010000000000000000000000000000000000000000000000000000000000000100000049483045022100acb96cfdbda6dc94b489fd06f2d720983b5f350e31ba906cdbd800773e80b21c02200d74ea5bdf114212b4bbe9ed82c36d2e369e302dff57cb60d01c428f0bd3daab83ffffffff02e8030000000000000151e903000000000000015100000000'\n        txid = '98229b70948f1c17851a541f1fe532bf02c408267fecf6d7e174c359ae870654'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0085(self):\n        raw_tx = '01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000'\n        txid = '570e3730deeea7bd8bc92c836ccdeb4dd4556f2c33f2a1f7b889a4cb4e48d3ab'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0086(self):\n        raw_tx = '01000000000102e9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff80e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffff0280969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac80969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000'\n        txid = 'e0b8142f587aaa322ca32abce469e90eda187f3851043cc4f2a0fff8c13fc84e'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0087(self):\n        raw_tx = '0100000000010280e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffffe9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff0280969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac80969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000'\n        txid = 'b9ecf72df06b8f98f8b63748d1aded5ffc1a1186f8a302e63cf94f6250e29f4d'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0088(self):\n        raw_tx = '0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000'\n        txid = '27eae69aff1dd4388c0fa05cbbfe9a3983d1b0b5811ebcd4199b86f299370aac'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0089(self):\n        raw_tx = '010000000169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f1581b0000b64830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0121037a3fb04bcdb09eba90f69961ba1692a3528e45e67c85b200df820212d7594d334aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01ffffffff0101000000000000000000000000'\n        txid = '22d020638e3b7e1f2f9a63124ac76f5e333c74387862e3675f64b25e960d3641'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0090(self):\n        raw_tx = '0100000000010169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f14c1d000000ffffffff01010000000000000000034830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e012102a9781d66b61fb5a7ef00ac5ad5bc6ffc78be7b44a566e3c87870e1079368df4c4aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0100000000'\n        txid = '2862bc0c69d2af55da7284d1b16a7cddc03971b77e5a97939cca7631add83bf5'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0091(self):\n        raw_tx = '01000000019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a662896581b0000fd6f01004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c03959601522102cd74a2809ffeeed0092bc124fd79836706e41f048db3f6ae9df8708cefb83a1c2102e615999372426e46fd107b76eaf007156a507584aa2cc21de9eee3bdbd26d36c4c9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960175ffffffff0101000000000000000000000000'\n        txid = '1aebf0c98f01381765a8c33d688f8903e4d01120589ac92b78f1185dc1f4119c'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n    def test_txid_bitcoin_core_0092(self):\n        raw_tx = '010000000001019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a6628964c1d000000ffffffff0101000000000000000007004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960101022102966f109c54e85d3aee8321301136cedeb9fc710fdef58a9de8a73942f8e567c021034ffc99dd9a79dd3cb31e2ab3e0b09e0e67db41ac068c625cd1f491576016c84e9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c039596017500000000'\n        txid = '45d17fb7db86162b2b6ca29fa4e163acf0ef0b54110e49b819bda1f948d423a3'\n        self._run_naive_tests_on_tx(raw_tx, txid)\n\n# txns from Bitcoin Core ends <---\n\n\nclass TestTransactionTestnet(ElectrumTestCase):\n    TESTNET = True\n\n    def test_spending_op_cltv_p2sh(self):\n        # from https://github.com/brianddk/reddit/blob/8ca383c9e00cb5a4c1201d1bab534d5886d3cb8f/python/elec-p2sh-hodl.py\n        wif = 'cQNjiPwYKMBr2oB3bWzf3rgBsu198xb8Nxxe51k6D3zVTA98L25N'\n        sats = 9999\n        sats_less_fees = sats - 200\n        locktime = 1602565200\n\n        # Build the Transaction Input\n        _, privkey, compressed = deserialize_privkey(wif)\n        pubkey = ECPrivkey(privkey).get_public_key_hex(compressed=compressed)\n        prevout = TxOutpoint(txid=bfh('6d500966f9e494b38a04545f0cea35fc7b3944e341a64b804fed71cdee11d434'), out_idx=1)\n        txin = PartialTxInput(prevout=prevout)\n        txin.nsequence = 2 ** 32 - 3\n        redeem_script = construct_script([\n            locktime, opcodes.OP_CHECKLOCKTIMEVERIFY, opcodes.OP_DROP, pubkey, opcodes.OP_CHECKSIG,\n        ])\n        txin.redeem_script = redeem_script\n\n        # Build the Transaction Output\n        txout = PartialTxOutput.from_address_and_value(\n            'tb1qv9hg20f0g08d460l67ph6p4ukwt7m0ttqzj7mk', sats_less_fees)\n\n        # Build and sign the transaction\n        tx = PartialTransaction.from_io([txin], [txout], locktime=locktime, version=1)\n        sig = tx.sign_txin(0, privkey)\n        txin.script_sig = construct_script([sig, redeem_script])\n\n        # note: in testnet3 chain, signature differs (no low-R grinding),\n        # so txid there is: a8110bbdd40d65351f615897d98c33cbe33e4ebedb4ba2fc9e8c644423dadc93\n        self.assertEqual('3266138b0b79007f35ac9a1824e294763708bd4a6440b5c227f4e1251b66e92b',\n                         tx.txid())\n\n    def test_spending_op_cltv_p2wsh(self):\n        wif = 'cSw3py1CQa2tmzzDm3ghQVrgqqNuFhUyBXjABge5j8KRxzd6kaFj'\n        sats = 99_878\n        sats_less_fees = sats - 300\n        locktime = 1602572140\n\n        # Build the Transaction Input\n        _, privkey, compressed = deserialize_privkey(wif)\n        pubkey = ECPrivkey(privkey).get_public_key_hex(compressed=compressed)\n        witness_script = construct_script([\n            locktime, opcodes.OP_CHECKLOCKTIMEVERIFY, opcodes.OP_DROP, pubkey, opcodes.OP_CHECKSIG,\n        ])\n        from_addr = bitcoin.script_to_p2wsh(witness_script)\n        self.assertEqual(\"tb1q9dn6qke9924xe3zmptmhrdge0s043pjxpjndypgnu2t9fvsd4crs2qjuer\", from_addr)\n        prevout = TxOutpoint(txid=bfh('8680971efd5203025cffe746f8598d0a704fae81f236ffe009c2609ec673d59a'), out_idx=0)\n        txin = PartialTxInput(prevout=prevout)\n        txin._trusted_value_sats = sats\n        txin.nsequence = 0\n        txin.script_sig = b''\n        txin.witness_script = witness_script\n\n        # Build the Transaction Output\n        txout = PartialTxOutput.from_address_and_value(\n            'tb1qtgsfkgptcxdn6dz6wh8c4dguk3cezwne5j5c47', sats_less_fees)\n\n        # Build and sign the transaction\n        tx = PartialTransaction.from_io([txin], [txout], locktime=locktime, version=2)\n        sig = tx.sign_txin(0, privkey)\n        txin.witness = construct_witness([sig, witness_script])\n\n        self.assertEqual('1cdb274755b144090c7134b6459e8d4cb6b4552fe620102836d751e8389b2694',\n                         tx.txid())\n        self.assertEqual('020000000001019ad573c69e60c209e0ff36f281ae4f700a8d59f846e7ff5c020352fd1e97808600000000000000000001fa840100000000001600145a209b202bc19b3d345a75cf8ab51cb471913a790247304402207b191c1e3ff1a2d3541770b496c9f871406114746b3aa7347ec4ef0423d3a975022043d3a746fa7a794d97e95d74b6d17d618dfc4cd7644476813e08006f271e51bd012a046c4f855fb1752102aec53aa5f347219a7378b13006eb16ce48125f9cf14f04a5509a565ad5e51507ac6c4f855f',\n                         tx.serialize())\n\n\nclass TestTransactionVerifySig(ElectrumTestCase):\n\n    def test_verifysig_spending_uncompressed_p2pk(self):\n        funding_tx_raw = \"010000000001021b41471d6af3aa80ebe536dbf4f505a6d46af456131a8e12e1950171959b690e0f00000000fdffffff2ef29833a69863b31e884fc5e6f7b99a23b5601e14f0eb65905faa42fec0776d0000000000fdffffff02f96a070000000000160014e61b989a740056254b5f8061281ac96ca15d35e140420f00000000004341049afa8fb50f52104b381a673c6e4fb7fb54987271d0e948dd9a568bb2af6f9310a7a809ce06e09d1510e5836f20414596232e2c0be63715459fa3cf8e7092af05ac0247304402201fe20012c1c732a6a8f942c4e0feed5ed0bddfb94db736ec3d0c0d38f0f7f46a022021d690e6d2688b90b76002f4c3134981502d666211e85e8a6ca91e78405dfa3801210346fb31136ab48e6c648865264d32004b43643d01f0ba485cffac4bb0b3f739470247304402204a2473ab4b3bfc8e6b1a6b8675dc2c3d115d8c04f5df37f29779dca6d300d9db02205e72ebbccd018c67b86ae4da6b0e6222902a8de85915ed6115330b9328764b370121027a93ffc9444a12d99307318e2e538949072cb35b2aca344b8163795a022414c7d73a1400\"\n        funding_tx = Transaction(funding_tx_raw)\n        spending_tx_raw = \"010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400\"\n        spending_tx = Transaction(spending_tx_raw)\n        spending_tx.inputs()[0].utxo = funding_tx\n        sig = list(script_GetOp(spending_tx.inputs()[0].script_sig))[0][1]\n        pubkey = list(script_GetOp(funding_tx.outputs()[1].scriptpubkey))[0][1]\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))\n\n    def test_verifysig_spending_compressed_p2pk(self):\n        funding_tx_raw = \"02000000000102b7bfcd442c91134743c6e4100bb9f79456a6015de3c3920166bb0c3b7a8f7c070100000000fdffffff5ab39480d4b35ffa843691d944a8479dfe825d38b03fcb1804197482bfad80fb0100000000fdffffff02d4ec000000000000160014769114e56e0913de3719a3b00a446b78e61751f007b201000000000023210332e147520e4743299d95196afaf9db7c86fe02507d9ca89acd7a4e96a63653d5ac0247304402200387fe79ffe10cec73d9b131058d7128665f729d14597828b483842889c4f5ea02201197b2f1295e4011e2d174d53c240fd13c6351451ab961ccb3678fc21fa5323b0121023c221dfbf7c3f61b9e5f66343c1a302d6beca2a8883504b0f484faec9919636b024730440220687d387af37df458efc104ee0065262cb5ea195e526ed7a480fd16e6cf708c3a022019bd3fd9c3ca3f1a1fbeabe20547876eb4572a7339de37b706fbd55031e60428012102c9c459e58b01a864d7bb80f6d577326465a04219c48541b5f3ea556a06ca61a425ed2400\"\n        funding_tx = Transaction(funding_tx_raw)\n        spending_tx_raw = \"02000000015eb359ccfcd67c3e6b10bb937a796807007708c1f413840d0e627a3f94a1a48401000000484730440220043fc85a43e918ac41e494e309fdf204ca245d260cb5ea09108b196ca65d8a09022056f852f0f521e79ab2124d7e9f779c7290329ce5628ef8e92601980b065d3eb501fdffffff017f9e010000000000160014a709434b13a510e6db68bdd672062c70a2f39d3a26ed2400\"\n        spending_tx = Transaction(spending_tx_raw)\n        spending_tx.inputs()[0].utxo = funding_tx\n        sig = list(script_GetOp(spending_tx.inputs()[0].script_sig))[0][1]\n        pubkey = list(script_GetOp(funding_tx.outputs()[1].scriptpubkey))[0][1]\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))\n\n    def test_verifysig_spending_uncompressed_p2pkh(self):\n        funding_tx_raw = \"02000000000101c6a49fbd701f1526c8e43025a6dda8dd235b3593cfd38af040cba3e37b474fdb0e00000000fdffffff020e640300000000001976a914f1b02b7028fb81aefbb25809a2baf8d94d0c2ba288acb9e3080000000000160014c2eee75efe6621be177f7edd8198f671d1640c2602473044022072b8a6154590704063c377af451b4d69f76cc9064085d4a0c80f08625c57628802207844164839d93ce54ce7db092bbd809d5270142b5dedc823e95400e8bdae88c6012102b6ad13f48fd679a209b7d822376550e5e694a3a2862546ceb72c4012977eac4829ed2400\"\n        funding_tx = Transaction(funding_tx_raw)\n        spending_tx_raw = \"02000000010615142d6c296f276c7da9fb2b09f655d594f73b76740404f1424c66c78ca715000000008a47304402206d2dae571ca2f51e0d4a8ce6a6335fa25ac09f4bbed26439124d93f035bdbb130220249dc2039f1da338a40679f0e79c25a2dc2983688e6c04753348f2aa8435e375014104b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b2987b4c862d5b687bb5328adccc69e67a17b109b6328228695a1c384573acd6199fdffffff0186500300000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071f2aed2400\"\n        spending_tx = Transaction(spending_tx_raw)\n        spending_tx.inputs()[0].utxo = funding_tx\n        sig = list(script_GetOp(spending_tx.inputs()[0].script_sig))[0][1]\n        pubkey = list(script_GetOp(spending_tx.inputs()[0].script_sig))[1][1]\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))\n\n    def test_verifysig_spending_compressed_p2pkh(self):\n        funding_tx_raw = \"020000000001010615142d6c296f276c7da9fb2b09f655d594f73b76740404f1424c66c78ca7150100000000fdffffff0240e20100000000001976a914f1d49f51f9b58c4805431c303d12d3dcf51ae54188ace9000700000000001600145bdb04f2d096ee48b8b350c85481392ab47c01e70247304402200a72a4599cb27f16011cd67e2951733d6775cbd008506eacb2c20d69db3f531702204c944ec09224a347481c9eea78cac79b77b194b19dfef01b1e3b428010a82570012102fc38612ca7cc42d05a7089f1a6ec3900535604bd779f83c7817aae7bfd907dbd2aed2400\"\n        funding_tx = Transaction(funding_tx_raw)\n        spending_tx_raw = \"02000000016717835a2e1e152a69e7528a0f1346c1d37ee6e76c5e23b5d1c5a5b40241768a000000006a473044022038ad38003943bfd3ed39ba4340d545753fcad632a8fe882d01e4f0140ddb3cfb022019498260e29f5fbbcde9176bfb3553b7acec5fe284a9a3a33547a2d082b60355012103b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b29fdffffff0158de010000000000160014f1d49f51f9b58c4805431c303d12d3dcf51ae5412aed2400\"\n        spending_tx = Transaction(spending_tx_raw)\n        spending_tx.inputs()[0].utxo = funding_tx\n        sig = list(script_GetOp(spending_tx.inputs()[0].script_sig))[0][1]\n        pubkey = list(script_GetOp(spending_tx.inputs()[0].script_sig))[1][1]\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))\n\n    def test_verifysig_spending_p2wpkh_p2sh(self):\n        funding_tx_raw = \"020000000001038fc862be3bc8022866cc83b4f2feeaa914b015a3c6644251960baaccc4a5740b0000000000fdffffff7bfd61e391034e28848fae269183f1c5929e26befd5b2d798cf12c91d4d00dbf0100000000fdffffff014764d324e70e7e3e4fa27077bda2d880b3d1545588b75f79deb2855d9f31cb0000000000fdffffff01f04902000000000017a9147d0530db22c8124ff1558269f543dfeedd37131b87024730440220568ae75314f6414ccf2b0bbed522e1b4b1086ed6eb185ba4bc044ba2723c1f3402206c82253797d0f180db38986b46d8ad952829cf25bc31e3ca6ee54665f5a44b3c0121038a466bdcb979b96d70fde84b9ded4aba0c3cd9c0d2d59121fc3555428fd1a4890247304402203ba1b482b0b6ce5c3d29ef21ee8afad641af8381d3b131103c384757922f0c04022072320e260b60fc862669b2ea3dfb663f7f3a0b6babe8d265ac9ebf268e7225c2012103ff0877f34157a3444afbfdd7432032a93187bc1932e1c155d56dd66ef527906c02473044022058b1c1a2a8c1a256d4870b550ba93777a2cce36b89abe3515f024fd4eec48ce4022023e0002193a26064275433e8ade98642d74d58ee4f8e9717a8acca737856a6c401210364e8f5d9c30986931bca1197138d7250a17a0711a223f113b3ccc11ef09efccb2aed2400\"\n        funding_tx = Transaction(funding_tx_raw)\n        spending_tx_raw = \"020000000001011d1725072a6e60687a59b878ecaf940ea0385880613d9d5502102bd78ef97b9a0000000017160014e7a6a58b657f629516cc37ee2863cbabdadb3fd4fdffffff01fc47020000000000160014e7a6a58b657f629516cc37ee2863cbabdadb3fd402473044022048ea4c558fd374f5d5066440a7f4933393cb377802cb949e3039fedf0378a29402204b4a58c591117cc1e37f07b03cc03cc6198dbf547e2bff813e2e2102bd2057e00121029f46ba81b3c6ad84e52841364dc54ca1097d0c30a68fb529766504c4b1c599352aed2400\"\n        spending_tx = Transaction(spending_tx_raw)\n        spending_tx.inputs()[0].utxo = funding_tx\n        sig = spending_tx.inputs()[0].witness_elements()[0]\n        pubkey = spending_tx.inputs()[0].witness_elements()[1]\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))\n\n    def test_verifysig_spending_p2wpkh(self):\n        funding_tx_raw = \"02000000000101208840a3310ae4b88181374b5812f56f5dd56f12574f3bcd8041b48bfadc92cf0000000000fdffffff02fc7f010000000000160014d339efed7cd5d28d31995caf10b8973a9a13c656a08601000000000043410403886197eb13c59721b94a29f9a68a841caedb7782b35121cd81d50d0cc70db3f8955c7a07b08dd6470141b66eedd324406e29d6b6799033314512334461e3f9ac0247304402203328153753e934d7a13215bf58f093f84281d57f8c7d42f3b7704cd714c7b32c02205a502f3f3e4302561ccc93df413be3c78a439ff35b60cea03d19f8804a9a1239012103f41052be701441d1bc8f7cc6a6053d7e7f5e63be212fe5e3687344ddd52e3af525ed2400\"\n        funding_tx = Transaction(funding_tx_raw)\n        spending_tx_raw = \"02000000000101e328aeb4f9dc1b85a2709ce59b0478a15ed9fb5e7f84fb62422f99b8cd6ad7010000000000fdffffff01087e010000000000160014bf08acd69f1b7012c2b91642b352ce3627db89010247304402204993099c4663d92ef4c9a28b3f45a40a6585754fe22ecfdc0a76c43fda7c9d04022006a75e0fd3ad1862d8e81015a71d2a1489ec7a9264e6e63b8fe6bb90c27e799b0121038ca94e7c715152fd89803c2a40a934c7c4035fb87b3cba981cd1e407369cfe312aed2400\"\n        spending_tx = Transaction(spending_tx_raw)\n        spending_tx.inputs()[0].utxo = funding_tx\n        sig = spending_tx.inputs()[0].witness_elements()[0]\n        pubkey = spending_tx.inputs()[0].witness_elements()[1]\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))\n\n    def test_verifysig_spending_p2sh_multisig_2of3(self):\n        funding_tx_raw = \"010000000001014121f99dc02f0364d2dab3d08905ff4c36fc76c55437fd90b769c35cc18618280100000000fdffffff02d4c22d00000000001600143fd1bc5d32245850c8cb5be5b09c73ccbb9a0f75001bb7000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887024830450221008781c78df0c9d4b5ea057333195d5d76bc29494d773f14fa80e27d2f288b2c360220762531614799b6f0fb8d539b18cb5232ab4253dd4385435157b28a44ff63810d0121033de77d21926e09efd04047ae2d39dbd3fb9db446e8b7ed53e0f70f9c9478f735dac11300\"\n        funding_tx = Transaction(funding_tx_raw)\n        spending_tx_raw = \"01000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb201000000fc004730440220751ee3599e59debb8b2aeef61bb5f574f26379cd961caf382d711a507bc632390220598d53e62557c4a5ab8cfb2f8948f37cca06a861714b55c781baf2c3d7a580b501473044022023b55c679397bdf3a04d545adc6193eabc11b3a28850d3d46049a51a30c6732402205dbfdade5620e9072ae4aa7577c5f0fd294f59a6b0064cc7105093c0fe7a6d24014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefeffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000\"\n        spending_tx = Transaction(spending_tx_raw)\n        spending_tx.inputs()[0].utxo = funding_tx\n        sig1 = list(script_GetOp(spending_tx.inputs()[0].script_sig))[1][1]\n        sig2 = list(script_GetOp(spending_tx.inputs()[0].script_sig))[2][1]\n        redeem_script = list(script_GetOp(spending_tx.inputs()[0].script_sig))[-1][1]\n        pubkey1 = list(script_GetOp(redeem_script))[1][1]\n        pubkey2 = list(script_GetOp(redeem_script))[2][1]\n        pubkey3 = list(script_GetOp(redeem_script))[3][1]\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey1, sig=sig1))\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey2, sig=sig2))\n\n        self.assertFalse(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey3, sig=sig2))\n\n    def test_verifysig_spending_p2wsh_p2sh_multisig_2of2(self):\n        funding_tx_raw = \"01000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000\"\n        funding_tx = Transaction(funding_tx_raw)\n        spending_tx_raw = \"0100000000010149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e01000000232200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163cfeffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a086010000000000220020f7b6b30c3073ae2680a7e90c589bbfec5303331be68bbab843eed5d51ba0123904004730440220091ea67af7c1131f51f62fe9596dff0a60c8b45bfc5be675389e193912e8a71802201bf813bbf83933a35ecc46e2d5b0442bd8758fa82e0f8ed16392c10d51f7f7660147304402203ecf75b0316a449dd31bc549251b687dc904194aa551941bd5e8c67603661bdb02204ed58b3a6b070ec138d2127093bebcc6581495818fa611583e1c81cd9b2cf5ee0147522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae00000000\"\n        spending_tx = Transaction(spending_tx_raw)\n        spending_tx.inputs()[0].utxo = funding_tx\n        sig1 = spending_tx.inputs()[0].witness_elements()[1]\n        sig2 = spending_tx.inputs()[0].witness_elements()[2]\n        witness_script = spending_tx.inputs()[0].witness_elements()[-1]\n        pubkey1 = list(script_GetOp(witness_script))[1][1]\n        pubkey2 = list(script_GetOp(witness_script))[2][1]\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey1, sig=sig1))\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey2, sig=sig2))\n\n        self.assertFalse(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey1, sig=sig2))\n\n    def test_verifysig_spending_p2wsh_multisig_2of3(self):\n        funding_tx_raw = \"01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c11300\"\n        funding_tx = Transaction(funding_tx_raw)\n        spending_tx_raw = \"01000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000\"\n        spending_tx = Transaction(spending_tx_raw)\n        spending_tx.inputs()[0].utxo = funding_tx\n        sig1 = spending_tx.inputs()[0].witness_elements()[1]\n        sig2 = spending_tx.inputs()[0].witness_elements()[2]\n        witness_script = spending_tx.inputs()[0].witness_elements()[-1]\n        pubkey1 = list(script_GetOp(witness_script))[1][1]\n        pubkey2 = list(script_GetOp(witness_script))[2][1]\n        pubkey3 = list(script_GetOp(witness_script))[3][1]\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey1, sig=sig1))\n        self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey3, sig=sig2))\n\n        self.assertFalse(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey2, sig=sig2))\n\n\nclass TestSighashBIP143(ElectrumTestCase):\n    #These tests are taken from bip143, https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki\n    #Input of transaction\n    locktime=0\n    prevout = TxOutpoint(txid=bfh('6eb98797a21c6c10aa74edf29d618be109f48a8e94c694f3701e08ca69186436'), out_idx=1)\n    txin = PartialTxInput(prevout=prevout)\n    txin.nsequence=0xffffffff\n    txin.witness_script = bfh('56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae')\n    txin.redeem_script = bfh('0020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54')\n    txin._trusted_value_sats = 987654321\n    #Output of Transaction\n    txout1 = PartialTxOutput(scriptpubkey=bfh('76a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688ac'), value=900000000)\n    txout2 = PartialTxOutput(scriptpubkey=bfh('76a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac'), value=87000000)\n\n    def test_check_sighash_types_sighash_all(self):\n        self.txin.sighash=Sighash.ALL\n        privkey = bfh('730fff80e1413068a05b57d6a58261f07551163369787f349438ea38ca80fac6')\n        tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)\n        sig = tx.sign_txin(0,privkey)\n        self.assertEqual('304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01',\n                         sig.hex())\n\n    def test_check_sighash_types_sighash_none(self):\n        self.txin.sighash=Sighash.NONE\n        privkey = bfh('11fa3d25a17cbc22b29c44a484ba552b5a53149d106d3d853e22fdd05a2d8bb3')\n        tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)\n        sig = tx.sign_txin(0,privkey)\n        self.assertEqual('3044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502',\n                         sig.hex())\n\n    def test_check_sighash_types_sighash_single(self):\n        self.txin.sighash=Sighash.SINGLE\n        privkey = bfh('77bf4141a87d55bdd7f3cd0bdccf6e9e642935fec45f2f30047be7b799120661')\n        tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)\n        sig = tx.sign_txin(0,privkey)\n        self.assertEqual('3044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403',\n                         sig.hex())\n\n    @disable_ecdsa_r_value_grinding\n    def test_check_sighash_types_sighash_all_anyonecanpay(self):\n        self.txin.sighash=Sighash.ALL|Sighash.ANYONECANPAY\n        privkey = bfh('14af36970f5025ea3e8b5542c0f8ebe7763e674838d08808896b63c3351ffe49')\n        tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)\n        sig = tx.sign_txin(0,privkey)\n        self.assertEqual('3045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381',\n                         sig.hex())\n\n    @disable_ecdsa_r_value_grinding\n    def test_check_sighash_types_sighash_none_anyonecanpay(self):\n        self.txin.sighash=Sighash.NONE|Sighash.ANYONECANPAY\n        privkey = bfh('fe9a95c19eef81dde2b95c1284ef39be497d128e2aa46916fb02d552485e0323')\n        tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)\n        sig = tx.sign_txin(0,privkey)\n        self.assertEqual('3045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a0882',\n                         sig.hex())\n\n    def test_check_sighash_types_sighash_single_anyonecanpay(self):\n        self.txin.sighash=Sighash.SINGLE|Sighash.ANYONECANPAY\n        privkey = bfh('428a7aee9f0c2af0cd19af3cf1c78149951ea528726989b2e83e4778d2c3f890')\n        tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)\n        sig = tx.sign_txin(0,privkey)\n        self.assertEqual('30440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783',\n                         sig.hex())\n\n\nclass TestSighashBIP341(ElectrumTestCase):\n\n    def test_taproot_keypath_spending(self):\n        test_vector_file = os.path.join(os.path.dirname(__file__), \"bip-0341\", \"wallet-test-vectors.json\")\n        with open(test_vector_file, \"r\") as f:\n            vectors = json.load(f)\n        assert len(vectors[\"keyPathSpending\"]) > 0, \"test vectors missing\"\n        for tcase in vectors[\"keyPathSpending\"]:\n            unsigned_tx = Transaction(tcase[\"given\"][\"rawUnsignedTx\"])\n            self.assertEqual(len(tcase[\"given\"][\"utxosSpent\"]), len(unsigned_tx.inputs()))\n            tx = PartialTransaction.from_tx(unsigned_tx)\n            # add utxo data\n            for txin, json_utxo in zip(tx.inputs(), tcase[\"given\"][\"utxosSpent\"]):\n                txin.witness_utxo = TxOutput(scriptpubkey=bfh(json_utxo[\"scriptPubKey\"]), value=int(json_utxo[\"amountSats\"]))\n            for txin_test in tcase[\"inputSpending\"]:\n                txin_idx = txin_test[\"given\"][\"txinIndex\"]\n                txin = tx.inputs()[txin_idx]\n                txin.sighash = int(txin_test[\"given\"][\"hashType\"])\n                txin.tap_merkle_root = bfh(txin_test[\"given\"][\"merkleRoot\"]) if txin_test[\"given\"][\"merkleRoot\"] else None\n                pre_hash = tx.serialize_preimage(txin_idx)\n                self.assertEqual(txin_test[\"intermediary\"][\"sigMsg\"], pre_hash.hex())\n                privkey = bfh(txin_test[\"given\"][\"internalPrivkey\"])\n                sig = tx.sign_txin(txin_idx, privkey)\n                assert len(txin_test[\"expected\"][\"witness\"]) == 1\n                self.assertEqual(txin_test[\"expected\"][\"witness\"][0], sig.hex())\n                txin.witness = construct_witness([sig])\n                txin.script_sig = b\"\"\n                self.assertTrue(txin.is_complete())\n            # note: some input utxos are not taproot, and there is no key data for them\n            #       - txin_idx=2, addr 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH\n            #       - txin_idx=5, addr bc1q0ht9tyks4vh7p5p904t340cr9nvahy7u3re7zg\n"
  },
  {
    "path": "tests/test_txbatcher.py",
    "content": "import logging\nfrom unittest import mock\nimport asyncio\nimport dataclasses\n\nfrom aiorpcx import timeout_after\n\nimport electrum.fee_policy\nfrom electrum import keystore, wallet, lnutil\nfrom electrum import SimpleConfig\nfrom electrum import util\nfrom electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED\nfrom electrum.transaction import Transaction, PartialTxInput, PartialTxOutput, TxOutpoint\nfrom electrum.logging import console_stderr_handler, Logger\nfrom electrum.submarine_swaps import SwapManager, SwapData\nfrom electrum.lnsweep import SweepInfo, sweep_ctx_anchor\nfrom electrum.fee_policy import FeeTimeEstimates\n\nfrom . import ElectrumTestCase\nfrom .test_wallet_vertical import WalletIntegrityHelper, read_test_vector\n\nWALLET_DATA = read_test_vector('cause_carbon_wallet.json')\n\nclass MockNetwork(Logger):\n\n    def __init__(self, config):\n        Logger.__init__(self)\n        self.config = config\n        self.fee_estimates = FeeTimeEstimates()\n        self.asyncio_loop = util.get_asyncio_loop()\n        self.interface = None\n        self.relay_fee = 1000\n        self.wallets = []\n        self._tx_queue = asyncio.Queue()\n\n    def get_local_height(self):\n        return 42\n\n    def blockchain(self):\n        class BlockchainMock:\n            def is_tip_stale(self):\n                return True\n        return BlockchainMock()\n\n    async def try_broadcasting(self, tx, name):\n        for w in self.wallets:\n            w.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self._tx_queue.put_nowait(tx)\n        return tx.txid()\n\n    async def next_tx(self):\n        tx = await util.wait_for2(self._tx_queue.get(), timeout=10)\n        return tx\n\n    def is_connected(self):\n        return True\n\n\nSWAP_FUNDING_TX = \"01000000000101500e9d67647481864edfb020b5c45e1c40d90f06b0130f9faed1a5149c6d26450000000000ffffffff0226080300000000002200205059c44bf57534303ab8f090f06b7bde58f5d2522440247a1ff6b41bdca9348df312c20100000000160014021d4f3b17921d790e1c022367a5bb078ce4deb402483045022100d41331089a2031396a1db8e4dec6dda9cacefe1288644b92f8e08a23325aa19b02204159230691601f7d726e4e6e0b7124d3377620f400d699a01095f0b0a09ee26a012102d60315c72c0cefd41c6d07883c20b88be3fc37aac7912f0052722a95de0de71600000000\"\nSWAP_CLAIM_TX = \"02000000000101f9db8580febd5c0f85b6f1576c83f7739109e3a2d772743e3217e9537fea7e89000000000001000000017005030000000000160014b113a47f3718da3fd161339a6681c150fef2cfe30347304402204c6d40103589b1a8177a37a824f0c66a3a7b22bc570b14c9e07965b56f6ace8f02203a35cffe0ab10de00f3e15ecf5aafdd2c7f6c62da11edd9054a1bce7a9e1455c0120f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a366a8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac00000000\"\n\nSWAPDATA = SwapData(\n    is_reverse=True,\n    locktime=2420532,\n    onchain_amount=198694,\n    lightning_amount=200000,\n    redeem_script=bytes.fromhex('8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac'),\n    preimage=bytes.fromhex('f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a36'),\n    prepay_hash=None,\n    privkey=bytes.fromhex('58fd0018a9a2737d1d6b81d380df96bf0c858473a9592015508a270a7c9b1d8d'),\n    lockup_address='tb1q2pvugjl4w56rqw4c7zg0q6mmmev0t5jjy3qzg7sl766phh9fxjxsrtl77t',\n    claim_to_output=None,\n    funding_txid='897eea7f53e917323e7472d7a2e3099173f7836c57f1b6850f5cbdfe8085dbf9',\n    spending_txid=None,\n    is_redeemed=False,\n)\n\ntxin = PartialTxInput(\n    prevout=TxOutpoint(txid=bytes.fromhex(SWAPDATA.funding_txid), out_idx=0),\n)\ntxin._trusted_value_sats = SWAPDATA.onchain_amount\ntxin, locktime = SwapManager.create_claim_txin(txin=txin, swap=SWAPDATA)\nSWAP_SWEEP_INFO = SweepInfo(\n    txin=txin,\n    cltv_abs=locktime,\n    txout=None,\n    name='swap claim',\n    can_be_batched=True,\n    dust_override=False,\n)\nanchor_chan_ctx = Transaction(\n    \"02000000000101d24af3b7adefff5a068f736d64842c18da7087b41ba43ab5b999c545c5f1606501000000008d0d5d8\"\n    \"0024a010000000000002200207b95cb2555b3f8fc246d26ac38023ec8edf423d70b41dfe17efc89baa6e0cc72740003\"\n    \"000000000022002075f8af76a5b5c4b25e4aee3a4f96a190a168bde5d9761de8d45d3e49cd6f1d82040047304402200\"\n    \"6524eb2f467bf1eacd2116ea79a80c182eb95e18d0fa24fc5c600581ec4aa5f02206f50bdfc3577ca3bc9892eade7d8\"\n    \"1a9b06b9a8803296fad689af9fee9375191d0147304402202112d08ffa79010b1d698ca9fb0790119a42f7b70e183a2\"\n    \"a05dbf79c32806d3b022001cd3e3aec8c1142c8689e9e679c684ea82dcde8c86cf8e55466c743b3647b1f0147522103\"\n    \"0551e6017a0e9dbffd468c2a08ecf8446b532f0a6d5db291eb77026f2ef3deb421034f986fd43561d52a96b19fbdd0c\"\n    \"296f0442034d3f3f63fc394320a95750d42a852ae08572d20\"\n)\nchan_multisig_key = lnutil.Keypair(\n    privkey=\"4c8b2c19d6528f54a4c900d87b3d8dbf746314925d126e0abf8e2ed965a9302f\",\n    pubkey=\"034f986fd43561d52a96b19fbdd0c296f0442034d3f3f63fc394320a95750d42a8\",\n)\nanchor_txin = sweep_ctx_anchor(ctx=anchor_chan_ctx, multisig_key=chan_multisig_key)\nANCHOR_SWEEP_INFO = SweepInfo(\n    name='local_anchor',\n    cltv_abs=None,\n    txin=anchor_txin,\n    txout=None,\n    can_be_batched=True,\n    dust_override=True,\n)\n\n\nclass TestTxBatcher(ElectrumTestCase):\n\n    TESTNET = True\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        console_stderr_handler.setLevel(logging.DEBUG)\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n        self.config.FEE_POLICY = 'feerate:5000'\n\n    async def asyncSetUp(self):\n        await super().asyncSetUp()\n        self.network = MockNetwork(self.config)\n\n    def create_standard_wallet_from_seed(self, seed_words, *, config=None, gap_limit=2):\n        if config is None:\n            config = self.config\n        ks = keystore.from_seed(seed_words, passphrase='', for_multisig=False)\n        return WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=gap_limit, config=config)\n\n    def _create_wallet(self):\n        wallet = self.create_standard_wallet_from_seed(WALLET_DATA[\"seed\"])\n        wallet.start_network(self.network)\n        wallet.txbatcher.SLEEP_INTERVAL = 0.01\n        self.network.wallets.append(wallet)\n        return wallet\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_batch_payments(self, mock_save_db):\n        # output 1:     tx1(o1) ---------------\n        #                                      \\\n        # output 2:     tx1'(o1,o2)             ----> tx2(tx1|o2)\n        #\n        # tx1 is broadcast, and replaced by tx1'\n        # tx1 gets mined\n        # txbatcher creates a new transaction tx2, child of tx1\n        #\n        OUTGOING_ADDRESS = 'tb1q7rl9cxr85962ztnsze089zs8ycv52hk43f3m9n'\n        wallet = self._create_wallet()\n        # fund wallet\n        funding_tx = Transaction(WALLET_DATA[\"funding_tx\"])\n        await self.network.try_broadcasting(funding_tx, 'funding')\n        await self.network.next_tx()\n        assert wallet.adb.get_transaction(funding_tx.txid()) is not None\n        self.logger.info(f'wallet balance {wallet.get_balance()}')\n        # payment 1 -> tx1(output1)\n        output1 = PartialTxOutput.from_address_and_value(OUTGOING_ADDRESS, 10_000)\n        wallet.txbatcher.add_payment_output('default', output1)\n        tx1 = await self.network.next_tx()\n        assert output1 in tx1.outputs()\n        # payment 2 -> tx2(output1, output2)\n        output2 = PartialTxOutput.from_address_and_value(OUTGOING_ADDRESS, 20_000)\n        wallet.txbatcher.add_payment_output('default', output2)\n        tx1_prime = await self.network.next_tx()\n        assert wallet.adb.get_transaction(tx1_prime.txid()) is not None\n        assert len(tx1_prime.outputs()) == 3\n        assert output1 in tx1_prime.outputs()\n        assert output2 in tx1_prime.outputs()\n        # tx1 gets confirmed, tx2 gets removed\n        wallet.adb.receive_tx_callback(tx1, tx_height=1)\n        tx_mined_status = wallet.adb.get_tx_height(tx1.txid())\n        wallet.adb.add_verified_tx(tx1.txid(), dataclasses.replace(tx_mined_status, conf=1))\n        assert wallet.adb.get_transaction(tx1.txid()) is not None\n        assert wallet.adb.get_transaction(tx1_prime.txid()) is None\n        # txbatcher creates tx2\n        tx2 = await self.network.next_tx()\n        assert output1 in tx1.outputs()\n        assert output2 in tx2.outputs()\n        # check that tx2 is child of tx1\n        assert len(tx2.inputs()) == 1\n        assert tx2.inputs()[0].prevout.txid.hex() == tx1.txid()\n\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_rbf_batching__cannot_batch_as_would_need_to_use_ismine_outputs_of_basetx(self, mock_save_db):\n        \"\"\"Wallet history contains unconf tx1 that spends all its coins to two ismine outputs,\n        one 'recv' address (20k sats) and one 'change' (80k sats).\n        The user tries to create tx2, that pays an invoice for 90k sats.\n        The tx batcher fails  to batch, and should create a child transaction\n        \"\"\"\n        wallet = self._create_wallet()\n        # fund wallet\n        funding_tx = Transaction(WALLET_DATA['funding_tx'])\n        await self.network.try_broadcasting(funding_tx, 'funding')\n        await self.network.next_tx()\n        assert wallet.adb.get_transaction(funding_tx.txid()) is not None\n        self.logger.info(f'wallet balance1 {wallet.get_balance()}')\n\n        # to_self_payment tx1\n        output1 = PartialTxOutput.from_address_and_value(\"tb1qyfnv3y866ufedugxxxfksyratv4pz3h78g9dad\", 20_000)\n        wallet.txbatcher.add_payment_output('default', output1)\n        tx1 = await self.network.next_tx()\n        assert len(tx1.outputs()) == 2\n        assert output1 in tx1.outputs()\n\n        # outgoing payment tx2\n        output2 = PartialTxOutput.from_address_and_value(\"tb1qkfn0fude7z789uys2u7sf80kd4805zpvs3na0h\", 90_000)\n        wallet.txbatcher.add_payment_output('default', output2)\n        # before tx1 gets confirmed, txbatch.create_transaction will raise notenoughfunds\n        await asyncio.sleep(wallet.txbatcher.SLEEP_INTERVAL)\n\n        # tx1 gets confirmed\n        wallet.adb.receive_tx_callback(tx1, tx_height=1)\n        tx_mined_status = wallet.adb.get_tx_height(tx1.txid())\n        wallet.adb.add_verified_tx(tx1.txid(), dataclasses.replace(tx_mined_status, conf=1))\n\n        tx2 = await self.network.next_tx()\n        assert len(tx2.outputs()) == 2\n        assert output2 in tx2.outputs()\n\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sweep_from_submarine_swap(self, mock_save_db):\n        self.maxDiff = None\n        # create wallet\n        wallet = self._create_wallet()\n        wallet.adb.db.transactions[SWAPDATA.funding_txid] = tx = Transaction(SWAP_FUNDING_TX)\n        wallet.adb.receive_tx_callback(tx, tx_height=1)\n        tx_mined_status = wallet.adb.get_tx_height(tx.txid())\n        wallet.adb.add_verified_tx(tx.txid(), dataclasses.replace(tx_mined_status, conf=1))\n        wallet.txbatcher.add_sweep_input('default', SWAP_SWEEP_INFO)\n        tx = await self.network.next_tx()\n        txid = tx.txid()\n        self.assertEqual(SWAP_CLAIM_TX, str(tx))\n        # add a new payment, reusing the same input\n        # this tests that txin.make_witness() can be called more than once\n        output1 = PartialTxOutput.from_address_and_value(\"tb1qyfnv3y866ufedugxxxfksyratv4pz3h78g9dad\", 20_000)\n        wallet.txbatcher.add_payment_output('default', output1)\n        new_tx = await self.network.next_tx()\n        # check that we batched with previous tx\n        assert new_tx.inputs()[0].prevout == tx.inputs()[0].prevout == txin.prevout\n        assert output1 in new_tx.outputs()\n\n    async def test_to_sweep_after_anchor_sweep_conditions(self):\n        # create wallet\n        wallet = self._create_wallet()\n        wallet.txbatcher.add_sweep_input('lnwatcher', ANCHOR_SWEEP_INFO)\n        anchor_batch = wallet.txbatcher.tx_batches['lnwatcher']\n\n        # does not return sweep info if prev_tx is not in db\n        to_sweep_no_tx = anchor_batch._to_sweep_after(tx=None)\n        self.assertFalse(to_sweep_no_tx)\n\n        # returns sweep input if anchor_chan_ctx conf < 1\n        wallet.adb.db.transactions[anchor_chan_ctx.txid()] = anchor_chan_ctx\n        to_sweep_no_conf = anchor_batch._to_sweep_after(tx=None)\n        self.assertEqual(to_sweep_no_conf[anchor_txin.prevout], ANCHOR_SWEEP_INFO)\n        self.assertEqual(len(to_sweep_no_conf), 1)\n\n        # does not return sweep input if ctx fee is already higher than target fee\n        with mock.patch.object(wallet.adb, 'get_tx_fee', return_value=2000), \\\n                mock.patch.object(electrum.fee_policy.FeePolicy, 'estimate_fee', return_value=1000):\n            to_sweep_high_fee = anchor_batch._to_sweep_after(tx=None)\n        self.assertFalse(to_sweep_high_fee)\n\n        # after the ctx is confirmed the anchor claim shouldn't be broadcast anymore\n        wallet.adb.receive_tx_callback(anchor_chan_ctx, tx_height=1)\n        tx_mined_status = wallet.adb.get_tx_height(anchor_chan_ctx.txid())\n        wallet.adb.add_verified_tx(anchor_chan_ctx.txid(), dataclasses.replace(tx_mined_status, conf=1))\n        self.assertIn(anchor_txin.prevout, anchor_batch.batch_inputs)\n        to_sweep_ctx_conf = anchor_batch._to_sweep_after(tx=None)\n        self.assertFalse(to_sweep_ctx_conf)\n        self.assertFalse(anchor_batch.batch_inputs)\n        self.assertEqual(wallet.txbatcher.tx_batches['lnwatcher'], anchor_batch)\n\n    async def _wait_for_base_tx(self, txbatch, should_be_none=False):\n        async with timeout_after(10):\n            while True:\n                base_tx = txbatch._base_tx\n                if (base_tx is not None) ^ should_be_none:\n                    return base_tx\n                await asyncio.sleep(0.1)\n"
  },
  {
    "path": "tests/test_util.py",
    "content": "import asyncio\nfrom datetime import datetime\nfrom decimal import Decimal\n\nfrom electrum import util\nfrom electrum.util import (format_satoshis, format_fee_satoshis, is_hash256_str, chunks, is_ip_address,\n                           list_enabled_bits, format_satoshis_plain, is_private_netaddress, is_hex_str,\n                           is_integer, is_non_negative_integer, is_int_or_float, is_non_negative_int_or_float,\n                           ShortID)\nfrom electrum.bip21 import parse_bip21_URI, InvalidBitcoinURI\nfrom . import ElectrumTestCase, as_testnet\n\n\nclass TestUtil(ElectrumTestCase):\n\n    def test_format_satoshis(self):\n        self.assertEqual(\"0.00001234\", format_satoshis(1234))\n\n    def test_format_satoshis_negative(self):\n        self.assertEqual(\"-0.00001234\", format_satoshis(-1234))\n\n    def test_format_satoshis_to_mbtc(self):\n        self.assertEqual(\"0.01234\", format_satoshis(1234, decimal_point=5))\n\n    def test_format_satoshis_decimal(self):\n        self.assertEqual(\"0.00001234\", format_satoshis(Decimal(1234)))\n\n    def test_format_satoshis_msat_resolution(self):\n        self.assertEqual(\"45831276.\",    format_satoshis(Decimal(\"45831276\"), decimal_point=0))\n        self.assertEqual(\"45831276.\",    format_satoshis(Decimal(\"45831275.748\"), decimal_point=0))\n        self.assertEqual(\"45831275.75\", format_satoshis(Decimal(\"45831275.748\"), decimal_point=0, precision=2))\n        self.assertEqual(\"45831275.748\", format_satoshis(Decimal(\"45831275.748\"), decimal_point=0, precision=3))\n\n        self.assertEqual(\"458312.76\",    format_satoshis(Decimal(\"45831276\"), decimal_point=2))\n        self.assertEqual(\"458312.76\",    format_satoshis(Decimal(\"45831275.748\"), decimal_point=2))\n        self.assertEqual(\"458312.7575\", format_satoshis(Decimal(\"45831275.748\"), decimal_point=2, precision=2))\n        self.assertEqual(\"458312.75748\", format_satoshis(Decimal(\"45831275.748\"), decimal_point=2, precision=3))\n\n        self.assertEqual(\"458.31276\", format_satoshis(Decimal(\"45831276\"), decimal_point=5))\n        self.assertEqual(\"458.31276\", format_satoshis(Decimal(\"45831275.748\"), decimal_point=5))\n        self.assertEqual(\"458.3127575\", format_satoshis(Decimal(\"45831275.748\"), decimal_point=5, precision=2))\n        self.assertEqual(\"458.31275748\", format_satoshis(Decimal(\"45831275.748\"), decimal_point=5, precision=3))\n\n    def test_format_fee_float(self):\n        self.assertEqual(\"1.7\", format_fee_satoshis(1700/1000))\n\n    def test_format_fee_decimal(self):\n        self.assertEqual(\"1.7\", format_fee_satoshis(Decimal(\"1.7\")))\n\n    def test_format_fee_precision(self):\n        self.assertEqual(\"1.666\",\n                         format_fee_satoshis(1666/1000, precision=6))\n        self.assertEqual(\"1.7\",\n                         format_fee_satoshis(1666/1000, precision=1))\n\n    def test_format_satoshis_whitespaces(self):\n        self.assertEqual(\"     0.0001234 \", format_satoshis(12340, whitespaces=True))\n        self.assertEqual(\"     0.00001234\", format_satoshis(1234, whitespaces=True))\n        self.assertEqual(\"     0.45831275\", format_satoshis(Decimal(\"45831275.\"), whitespaces=True))\n        self.assertEqual(\"     0.45831275   \", format_satoshis(Decimal(\"45831275.\"), whitespaces=True, precision=3))\n        self.assertEqual(\"     0.458312757  \", format_satoshis(Decimal(\"45831275.7\"), whitespaces=True, precision=3))\n        self.assertEqual(\"     0.45831275748\", format_satoshis(Decimal(\"45831275.748\"), whitespaces=True, precision=3))\n\n    def test_format_satoshis_whitespaces_negative(self):\n        self.assertEqual(\"    -0.0001234 \", format_satoshis(-12340, whitespaces=True))\n        self.assertEqual(\"    -0.00001234\", format_satoshis(-1234, whitespaces=True))\n\n    def test_format_satoshis_diff_positive(self):\n        self.assertEqual(\"+0.00001234\", format_satoshis(1234, is_diff=True))\n        self.assertEqual(\"+456789.00001234\", format_satoshis(45678900001234, is_diff=True))\n\n    def test_format_satoshis_diff_negative(self):\n        self.assertEqual(\"-0.00001234\", format_satoshis(-1234, is_diff=True))\n        self.assertEqual(\"-456789.00001234\", format_satoshis(-45678900001234, is_diff=True))\n\n    def test_format_satoshis_add_thousands_sep(self):\n        self.assertEqual(\"178 890 000.\", format_satoshis(Decimal(178890000), decimal_point=0, add_thousands_sep=True))\n        self.assertEqual(\"458 312.757 48\", format_satoshis(Decimal(\"45831275.748\"), decimal_point=2, add_thousands_sep=True, precision=5))\n        # is_diff\n        self.assertEqual(\"+4 583 127.574 8\", format_satoshis(Decimal(\"45831275.748\"), decimal_point=1, is_diff=True, add_thousands_sep=True, precision=4))\n        self.assertEqual(\"+456 789 112.004 56\", format_satoshis(Decimal(\"456789112.00456\"), decimal_point=0, is_diff=True, add_thousands_sep=True, precision=5))\n        self.assertEqual(\"-0.000 012 34\", format_satoshis(-1234, is_diff=True, add_thousands_sep=True))\n        self.assertEqual(\"-456 789.000 012 34\", format_satoshis(-45678900001234, is_diff=True, add_thousands_sep=True))\n        # num_zeros\n        self.assertEqual(\"-456 789.123 400\", format_satoshis(-45678912340000, num_zeros=6, add_thousands_sep=True))\n        self.assertEqual(\"-456 789.123 4\", format_satoshis(-45678912340000, num_zeros=2, add_thousands_sep=True))\n        # whitespaces\n        self.assertEqual(\"      1 432.731 11\", format_satoshis(143273111, decimal_point=5, add_thousands_sep=True, whitespaces=True))\n        self.assertEqual(\"      1 432.731   \", format_satoshis(143273100, decimal_point=5, add_thousands_sep=True, whitespaces=True))\n        self.assertEqual(\" 67 891 432.731   \", format_satoshis(6789143273100, decimal_point=5, add_thousands_sep=True, whitespaces=True))\n        self.assertEqual(\"       143 273 100.\", format_satoshis(143273100, decimal_point=0, add_thousands_sep=True, whitespaces=True))\n        self.assertEqual(\" 6 789 143 273 100.\", format_satoshis(6789143273100, decimal_point=0, add_thousands_sep=True, whitespaces=True))\n        self.assertEqual(\"56 789 143 273 100.\", format_satoshis(56789143273100, decimal_point=0, add_thousands_sep=True, whitespaces=True))\n\n    def test_format_satoshis_plain(self):\n        self.assertEqual(\"0.00001234\", format_satoshis_plain(1234))\n\n    def test_format_satoshis_plain_decimal(self):\n        self.assertEqual(\"0.00001234\", format_satoshis_plain(Decimal(1234)))\n\n    def test_format_satoshis_plain_to_mbtc(self):\n        self.assertEqual(\"0.01234\", format_satoshis_plain(1234, decimal_point=5))\n\n    def _do_test_parse_URI(self, uri, expected):\n        result = parse_bip21_URI(uri)\n        self.assertEqual(expected, result)\n\n    def test_parse_URI_address(self):\n        self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma',\n                                {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma'})\n\n    def test_parse_URI_only_address(self):\n        self._do_test_parse_URI('15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma',\n                                {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma'})\n\n\n    def test_parse_URI_address_label(self):\n        self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?label=electrum%20test',\n                                {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'label': 'electrum test'})\n\n    def test_parse_URI_address_message(self):\n        self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?message=electrum%20test',\n                                {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'message': 'electrum test', 'memo': 'electrum test'})\n\n    def test_parse_URI_address_amount(self):\n        self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.0003',\n                                {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'amount': 30000})\n\n    def test_parse_URI_address_request_url(self):\n        self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?r=http://domain.tld/page?h%3D2a8628fc2fbe',\n                                {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'r': 'http://domain.tld/page?h=2a8628fc2fbe'})\n\n    def test_parse_URI_ignore_args(self):\n        self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?test=test',\n                                {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'test': 'test'})\n\n    def test_parse_URI_multiple_args(self):\n        self._do_test_parse_URI('bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.00004&label=electrum-test&message=electrum%20test&test=none&r=http://domain.tld/page',\n                                {'address': '15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma', 'amount': 4000, 'label': 'electrum-test', 'message': u'electrum test', 'memo': u'electrum test', 'r': 'http://domain.tld/page', 'test': 'none'})\n\n    def test_parse_URI_no_address_request_url(self):\n        self._do_test_parse_URI('bitcoin:?r=http://domain.tld/page?h%3D2a8628fc2fbe',\n                                {'r': 'http://domain.tld/page?h=2a8628fc2fbe'})\n\n    def test_parse_URI_invalid_address(self):\n        self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:invalidaddress')\n\n    def test_parse_URI_invalid(self):\n        self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'notbitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma')\n\n    def test_parse_URI_parameter_pollution(self):\n        self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.0003&label=test&amount=30.0')\n\n    @as_testnet\n    def test_parse_URI_unsupported_req_key(self):\n        self._do_test_parse_URI('bitcoin:TB1QXJ6KVTE6URY2MX695METFTFT7LR5HYK4M3VT5F?amount=0.00100000&label=test&somethingyoudontunderstand=50',\n                                {'address': 'TB1QXJ6KVTE6URY2MX695METFTFT7LR5HYK4M3VT5F', 'amount': 100000, 'label': 'test', 'somethingyoudontunderstand': '50'})\n        # now test same URI but with \"req-test=1\" added\n        self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:TB1QXJ6KVTE6URY2MX695METFTFT7LR5HYK4M3VT5F?amount=0.00100000&label=test&req-test=1&somethingyoudontunderstand=50')\n\n    @as_testnet\n    def test_parse_URI_lightning_consistency(self):\n        # bip21 uri that *only* includes a \"lightning\" key. LN part does not have fallback address\n        self._do_test_parse_URI('bitcoin:?lightning=lntb700u1p3kqy0cpp5azvqy3wez7hcz3ka7tpqqvw5mpsa7fknxl4ca7a7669kswhf0hgqsp5qxhxul9k88w2nsk643elzuu4nepwkq052ek79esmz47yj6lfrhuqdqvw3jhxapjxcmscqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqyznyzw55q63yytup920n9qcsnh6qqht48maapzgadll2qy5vheeq26crapt0rcv9aqmpm93ljkapgtc05keud9jhlasns795fylfdjsphud9uh',\n                                {'lightning': 'lntb700u1p3kqy0cpp5azvqy3wez7hcz3ka7tpqqvw5mpsa7fknxl4ca7a7669kswhf0hgqsp5qxhxul9k88w2nsk643elzuu4nepwkq052ek79esmz47yj6lfrhuqdqvw3jhxapjxcmscqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqyznyzw55q63yytup920n9qcsnh6qqht48maapzgadll2qy5vheeq26crapt0rcv9aqmpm93ljkapgtc05keud9jhlasns795fylfdjsphud9uh'})\n        # bip21 uri that *only* includes a \"lightning\" key. LN part has fallback address\n        self._do_test_parse_URI('bitcoin:?lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql',\n                                {'lightning': 'lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql'})\n        # bip21 uri that includes \"lightning\" key. LN part does not have fallback address\n        self._do_test_parse_URI('bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0007&message=test266&lightning=lntb700u1p3kqy0cpp5azvqy3wez7hcz3ka7tpqqvw5mpsa7fknxl4ca7a7669kswhf0hgqsp5qxhxul9k88w2nsk643elzuu4nepwkq052ek79esmz47yj6lfrhuqdqvw3jhxapjxcmscqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqyznyzw55q63yytup920n9qcsnh6qqht48maapzgadll2qy5vheeq26crapt0rcv9aqmpm93ljkapgtc05keud9jhlasns795fylfdjsphud9uh',\n                                {'address': 'tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl',\n                                 'amount': 70000,\n                                 'lightning': 'lntb700u1p3kqy0cpp5azvqy3wez7hcz3ka7tpqqvw5mpsa7fknxl4ca7a7669kswhf0hgqsp5qxhxul9k88w2nsk643elzuu4nepwkq052ek79esmz47yj6lfrhuqdqvw3jhxapjxcmscqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqyznyzw55q63yytup920n9qcsnh6qqht48maapzgadll2qy5vheeq26crapt0rcv9aqmpm93ljkapgtc05keud9jhlasns795fylfdjsphud9uh',\n                                 'memo': 'test266',\n                                 'message': 'test266'})\n        # bip21 uri that includes \"lightning\" key. LN part has fallback address\n        self._do_test_parse_URI('bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0007&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql',\n                                {'address': 'tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl',\n                                 'amount': 70000,\n                                 'lightning': 'lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql',\n                                 'memo': 'test266',\n                                 'message': 'test266'})\n        # bip21 uri that includes \"lightning\" key. LN part has fallback address BUT it mismatches the top-level address\n        self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:tb1qvu0c9xme0ul3gzx4nzqdgxsu25acuk9wvsj2j2?amount=0.0007&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql')\n        # bip21 uri that includes \"lightning\" key. top-level amount mismatches LN amount\n        self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0008&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql')\n        # bip21 uri that includes \"lightning\" key with garbage unparsable value\n        self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0008&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdasdasdasdasd')\n\n    def test_is_hash256_str(self):\n        self.assertTrue(is_hash256_str('09a4c03e3bdf83bbe3955f907ee52da4fc12f4813d459bc75228b64ad08617c7'))\n        self.assertTrue(is_hash256_str('2A5C3F4062E4F2FCCE7A1C7B4310CB647B327409F580F4ED72CB8FC0B1804DFA'))\n        self.assertTrue(is_hash256_str('00' * 32))\n\n        self.assertFalse(is_hash256_str('00' * 33))\n        self.assertFalse(is_hash256_str('qweqwe'))\n        self.assertFalse(is_hash256_str(None))\n        self.assertFalse(is_hash256_str(7))\n\n    def test_is_hex_str(self):\n        self.assertTrue(is_hex_str('09a4'))\n        self.assertTrue(is_hex_str('abCD'))\n        self.assertTrue(is_hex_str('2A5C3F4062E4F2FCCE7A1C7B4310CB647B327409F580F4ED72CB8FC0B1804DFA'))\n        self.assertTrue(is_hex_str('00' * 33))\n\n        self.assertFalse(is_hex_str('0x09a4'))\n        self.assertFalse(is_hex_str('2A 5C3F'))\n        self.assertFalse(is_hex_str(' 2A5C3F'))\n        self.assertFalse(is_hex_str('2A5C3F '))\n        self.assertFalse(is_hex_str('000'))\n        self.assertFalse(is_hex_str('123'))\n        self.assertFalse(is_hex_str('0x123'))\n        self.assertFalse(is_hex_str('qweqwe'))\n        self.assertFalse(is_hex_str(b'09a4'))\n        self.assertFalse(is_hex_str(b'\\x09\\xa4'))\n        self.assertFalse(is_hex_str(None))\n        self.assertFalse(is_hex_str(7))\n        self.assertFalse(is_hex_str(7.2))\n\n    def test_is_integer(self):\n        self.assertTrue(is_integer(7))\n        self.assertTrue(is_integer(0))\n        self.assertTrue(is_integer(-1))\n        self.assertTrue(is_integer(-7))\n\n        self.assertFalse(is_integer(Decimal(\"2.0\")))\n        self.assertFalse(is_integer(Decimal(2.0)))\n        self.assertFalse(is_integer(Decimal(2)))\n        self.assertFalse(is_integer(0.72))\n        self.assertFalse(is_integer(2.0))\n        self.assertFalse(is_integer(-2.0))\n        self.assertFalse(is_integer('09a4'))\n        self.assertFalse(is_integer('2A5C3F4062E4F2FCCE7A1C7B4310CB647B327409F580F4ED72CB8FC0B1804DFA'))\n        self.assertFalse(is_integer('000'))\n        self.assertFalse(is_integer('qweqwe'))\n        self.assertFalse(is_integer(None))\n\n    def test_is_non_negative_integer(self):\n        self.assertTrue(is_non_negative_integer(7))\n        self.assertTrue(is_non_negative_integer(0))\n\n        self.assertFalse(is_non_negative_integer(Decimal(\"2.0\")))\n        self.assertFalse(is_non_negative_integer(Decimal(2.0)))\n        self.assertFalse(is_non_negative_integer(Decimal(2)))\n        self.assertFalse(is_non_negative_integer(0.72))\n        self.assertFalse(is_non_negative_integer(2.0))\n        self.assertFalse(is_non_negative_integer(-2.0))\n        self.assertFalse(is_non_negative_integer(-1))\n        self.assertFalse(is_non_negative_integer(-7))\n        self.assertFalse(is_non_negative_integer('09a4'))\n        self.assertFalse(is_non_negative_integer('2A5C3F4062E4F2FCCE7A1C7B4310CB647B327409F580F4ED72CB8FC0B1804DFA'))\n        self.assertFalse(is_non_negative_integer('000'))\n        self.assertFalse(is_non_negative_integer('qweqwe'))\n        self.assertFalse(is_non_negative_integer(None))\n\n    def test_is_int_or_float(self):\n        self.assertTrue(is_int_or_float(7))\n        self.assertTrue(is_int_or_float(0))\n        self.assertTrue(is_int_or_float(-1))\n        self.assertTrue(is_int_or_float(-7))\n        self.assertTrue(is_int_or_float(0.72))\n        self.assertTrue(is_int_or_float(2.0))\n        self.assertTrue(is_int_or_float(-2.0))\n\n        self.assertFalse(is_int_or_float(Decimal(\"2.0\")))\n        self.assertFalse(is_int_or_float(Decimal(2.0)))\n        self.assertFalse(is_int_or_float(Decimal(2)))\n        self.assertFalse(is_int_or_float('09a4'))\n        self.assertFalse(is_int_or_float('2A5C3F4062E4F2FCCE7A1C7B4310CB647B327409F580F4ED72CB8FC0B1804DFA'))\n        self.assertFalse(is_int_or_float('000'))\n        self.assertFalse(is_int_or_float('qweqwe'))\n        self.assertFalse(is_int_or_float(None))\n\n    def test_is_non_negative_int_or_float(self):\n        self.assertTrue(is_non_negative_int_or_float(7))\n        self.assertTrue(is_non_negative_int_or_float(0))\n        self.assertTrue(is_non_negative_int_or_float(0.0))\n        self.assertTrue(is_non_negative_int_or_float(0.72))\n        self.assertTrue(is_non_negative_int_or_float(2.0))\n\n        self.assertFalse(is_non_negative_int_or_float(-1))\n        self.assertFalse(is_non_negative_int_or_float(-7))\n        self.assertFalse(is_non_negative_int_or_float(-2.0))\n        self.assertFalse(is_non_negative_int_or_float(Decimal(\"2.0\")))\n        self.assertFalse(is_non_negative_int_or_float(Decimal(2.0)))\n        self.assertFalse(is_non_negative_int_or_float(Decimal(2)))\n        self.assertFalse(is_non_negative_int_or_float('09a4'))\n        self.assertFalse(is_non_negative_int_or_float('2A5C3F4062E4F2FCCE7A1C7B4310CB647B327409F580F4ED72CB8FC0B1804DFA'))\n        self.assertFalse(is_non_negative_int_or_float('000'))\n        self.assertFalse(is_non_negative_int_or_float('qweqwe'))\n        self.assertFalse(is_non_negative_int_or_float(None))\n\n    def test_chunks(self):\n        self.assertEqual([[1, 2], [3, 4], [5]],\n                         list(chunks([1, 2, 3, 4, 5], 2)))\n        self.assertEqual([], list(chunks(b'', 64)))\n        self.assertEqual([b'12', b'34', b'56'],\n                         list(chunks(b'123456', 2)))\n        with self.assertRaises(ValueError):\n            list(chunks([1, 2, 3], 0))\n\n    def test_list_enabled_bits(self):\n        self.assertEqual((0, 2, 3, 6), list_enabled_bits(77))\n        self.assertEqual((), list_enabled_bits(0))\n\n    def test_is_ip_address(self):\n        self.assertTrue(is_ip_address(\"127.0.0.1\"))\n        #self.assertTrue(is_ip_address(\"127.000.000.1\"))  # disabled as result differs based on python version\n        self.assertTrue(is_ip_address(\"255.255.255.255\"))\n        self.assertFalse(is_ip_address(\"255.255.256.255\"))\n        self.assertFalse(is_ip_address(\"123.456.789.000\"))\n        self.assertTrue(is_ip_address(\"2001:0db8:0000:0000:0000:ff00:0042:8329\"))\n        self.assertTrue(is_ip_address(\"2001:db8:0:0:0:ff00:42:8329\"))\n        self.assertTrue(is_ip_address(\"2001:db8::ff00:42:8329\"))\n        self.assertFalse(is_ip_address(\"2001:::db8::ff00:42:8329\"))\n        self.assertTrue(is_ip_address(\"::1\"))\n        self.assertFalse(is_ip_address(\"2001:db8:0:0:g:ff00:42:8329\"))\n        self.assertFalse(is_ip_address(\"lol\"))\n        self.assertFalse(is_ip_address(\":@ASD:@AS\\x77\\x22\\xff¬!\"))\n\n    def test_is_private_netaddress(self):\n        self.assertTrue(is_private_netaddress(\"127.0.0.1\"))\n        self.assertTrue(is_private_netaddress(\"127.5.6.7\"))\n        self.assertTrue(is_private_netaddress(\"::1\"))\n        self.assertTrue(is_private_netaddress(\"[::1]\"))\n        self.assertTrue(is_private_netaddress(\"localhost\"))\n        self.assertTrue(is_private_netaddress(\"localhost.\"))\n        self.assertTrue(is_private_netaddress(\"192.168.1.1\"))  # RFC1918\n        self.assertTrue(is_private_netaddress(\"10.10.10.10\"))  # RFC1918\n        self.assertTrue(is_private_netaddress(\"172.16.0.1\"))   # RFC1918\n        self.assertTrue(is_private_netaddress(\"172.31.255.254\"))  # RFC1918\n        self.assertTrue(is_private_netaddress(\"::ffff:ac10:0001\"))  # RFC1918 IPv4 in IPv6\n        self.assertTrue(is_private_netaddress(\"[::ffff:c0a8:0001]\"))  # RFC1918 IPv4 in IPv6\n        self.assertTrue(is_private_netaddress(\"fe80::0001\"))  # IPv6 link-local\n        self.assertFalse(is_private_netaddress(\"[::2]\"))\n        self.assertFalse(is_private_netaddress(\"2a00:1450:400e:80d::200e\"))\n        self.assertFalse(is_private_netaddress(\"[2a00:1450:400e:80d::200e]\"))\n        self.assertFalse(is_private_netaddress(\"8.8.8.8\"))\n        self.assertFalse(is_private_netaddress(\"example.com\"))\n\n    def test_is_subpath(self):\n        self.assertTrue(util.is_subpath(\"/a/b/c/d/e\", \"/\"))\n        self.assertTrue(util.is_subpath(\"/a/b/c/d/e\", \"/a\"))\n        self.assertTrue(util.is_subpath(\"/a/b/c/d/e\", \"/a/\"))\n        self.assertTrue(util.is_subpath(\"/a/b/c/d/e\", \"/a/b/c/\"))\n        self.assertTrue(util.is_subpath(\"/a/b/c/d/e/\", \"/a/b/c/\"))\n        self.assertTrue(util.is_subpath(\"/a/b/c/d/e/\", \"/a/b/c\"))\n        self.assertTrue(util.is_subpath(\"/a/b/c/d/e/\", \"/a/b/c/d/e/\"))\n        self.assertTrue(util.is_subpath(\"/\", \"/\"))\n        self.assertTrue(util.is_subpath(\"a/b/c\", \"a\"))\n        self.assertTrue(util.is_subpath(\"a/b/c\", \"a/\"))\n        self.assertTrue(util.is_subpath(\"a/b/c\", \"a/b\"))\n        self.assertTrue(util.is_subpath(\"a/b/c\", \"a/b/c\"))\n\n        self.assertFalse(util.is_subpath(\"/a/b/c/d/e/\", \"/b\"))\n        self.assertFalse(util.is_subpath(\"/a/b/c/d/e/\", \"/b/c/\"))\n        self.assertFalse(util.is_subpath(\"/a/b/c\", \"/a/b/c/d/e/\"))\n        self.assertFalse(util.is_subpath(\"/a/b/c\", \"a\"))\n        self.assertFalse(util.is_subpath(\"/a/b/c\", \"c\"))\n        self.assertFalse(util.is_subpath(\"a\", \"/a/b/c\"))\n        self.assertFalse(util.is_subpath(\"c\", \"/a/b/c\"))\n\n    def test_error_text_bytes_to_safe_str(self):\n        # ascii\n        self.assertEqual(\"'test'\", util.error_text_bytes_to_safe_str(b\"test\"))\n        self.assertEqual('\"test123 \\'QWE\"', util.error_text_bytes_to_safe_str(b\"test123 'QWE\"))\n        self.assertEqual(\"'prefix: \\\\x08\\\\x08\\\\x08\\\\x08\\\\x08\\\\x08\\\\x08\\\\x08malicious_stuff'\",\n                         util.error_text_bytes_to_safe_str(b\"prefix: \" + 8 * b\"\\x08\" + b\"malicious_stuff\"))\n        # unicode\n        self.assertEqual(\"'here is some unicode: \\\\\\\\xe2\\\\\\\\x82\\\\\\\\xbf \\\\\\\\xf0\\\\\\\\x9f\\\\\\\\x98\\\\\\\\x80 \\\\\\\\xf0\\\\\\\\x9f\\\\\\\\x98\\\\\\\\x88'\",\n                         util.error_text_bytes_to_safe_str(b'here is some unicode: \\xe2\\x82\\xbf \\xf0\\x9f\\x98\\x80 \\xf0\\x9f\\x98\\x88'))\n        # not even unicode\n        self.assertEqual(\"\"\"\\'\\\\x00\\\\x01\\\\x02\\\\x03\\\\x04\\\\x05\\\\x06\\\\x07\\\\x08\\\\t\\\\n\\\\x0b\\\\x0c\\\\r\\\\x0e\\\\x0f\\\\x10\\\\x11\\\\x12\\\\x13\\\\x14\\\\x15\\\\x16\\\\x17\\\\x18\\\\x19\\\\x1a\\\\x1b\\\\x1c\\\\x1d\\\\x1e\\\\x1f !\"#$%&\\\\\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\\\\x7f\\\\\\\\x80\\\\\\\\x81\\\\\\\\x82\\\\\\\\x83\\\\\\\\x84\\\\\\\\x85\\\\\\\\x86\\\\\\\\x87\\\\\\\\x88\\\\\\\\x89\\\\\\\\x8a\\\\\\\\x8b\\\\\\\\x8c\\\\\\\\x8d\\\\\\\\x8e\\\\\\\\x8f\\\\\\\\x90\\\\\\\\x91\\\\\\\\x92\\\\\\\\x93\\\\\\\\x94\\\\\\\\x95\\\\\\\\x96\\\\\\\\x97\\\\\\\\x98\\\\\\\\x99\\\\\\\\x9a\\\\\\\\x9b\\\\\\\\x9c\\\\\\\\x9d\\\\\\\\x9e\\\\\\\\x9f\\\\\\\\xa0\\\\\\\\xa1\\\\\\\\xa2\\\\\\\\xa3\\\\\\\\xa4\\\\\\\\xa5\\\\\\\\xa6\\\\\\\\xa7\\\\\\\\xa8\\\\\\\\xa9\\\\\\\\xaa\\\\\\\\xab\\\\\\\\xac\\\\\\\\xad\\\\\\\\xae\\\\\\\\xaf\\\\\\\\xb0\\\\\\\\xb1\\\\\\\\xb2\\\\\\\\xb3\\\\\\\\xb4\\\\\\\\xb5\\\\\\\\xb6\\\\\\\\xb7\\\\\\\\xb8\\\\\\\\xb9\\\\\\\\xba\\\\\\\\xbb\\\\\\\\xbc\\\\\\\\xbd\\\\\\\\xbe\\\\\\\\xbf\\\\\\\\xc0\\\\\\\\xc1\\\\\\\\xc2\\\\\\\\xc3\\\\\\\\xc4\\\\\\\\xc5\\\\\\\\xc6\\\\\\\\xc7\\\\\\\\xc8\\\\\\\\xc9\\\\\\\\xca\\\\\\\\xcb\\\\\\\\xcc\\\\\\\\xcd\\\\\\\\xce\\\\\\\\xcf\\\\\\\\xd0\\\\\\\\xd1\\\\\\\\xd2\\\\\\\\xd3\\\\\\\\xd4\\\\\\\\xd5\\\\\\\\xd6\\\\\\\\xd7\\\\\\\\xd8\\\\\\\\xd9\\\\\\\\xda\\\\\\\\xdb\\\\\\\\xdc\\\\\\\\xdd\\\\\\\\xde\\\\\\\\xdf\\\\\\\\xe0\\\\\\\\xe1\\\\\\\\xe2\\\\\\\\xe3\\\\\\\\xe4\\\\\\\\xe5\\\\\\\\xe6\\\\\\\\xe7\\\\\\\\xe8\\\\\\\\xe9\\\\\\\\xea\\\\\\\\xeb\\\\\\\\xec\\\\\\\\xed\\\\\\\\xee\\\\\\\\xef\\\\\\\\xf0\\\\\\\\xf1\\\\\\\\xf2\\\\\\\\xf3\\\\\\\\xf4\\\\\\\\xf5\\\\\\\\xf6\\\\\\\\xf7\\\\\\\\xf8\\\\\\\\xf9\\\\\\\\xfa\\\\\\\\xfb\\\\\\\\xfc\\\\\\\\xfd\\\\\\\\xfe\\\\\\\\xff\\'\"\"\",\n                         util.error_text_bytes_to_safe_str(bytes(range(256)), max_len=1000))\n        # long text\n        t1 = util.error_text_bytes_to_safe_str(b\"test\" * 10000)\n        self.assertTrue(t1.endswith(\"... (truncated. orig_len=40002)\"))\n        self.assertTrue(len(t1) < 550)\n\n    def test_error_text_str_to_safe_str(self):\n        # ascii\n        self.assertEqual(\"'test'\", util.error_text_str_to_safe_str(\"test\"))\n        self.assertEqual('\"test123 \\'QWE\"', util.error_text_str_to_safe_str(\"test123 'QWE\"))\n        self.assertEqual(\"'prefix: \\\\x08\\\\x08\\\\x08\\\\x08\\\\x08\\\\x08\\\\x08\\\\x08malicious_stuff'\",\n                         util.error_text_str_to_safe_str(\"prefix: \" + 8 * \"\\x08\" + \"malicious_stuff\"))\n        # unicode\n        self.assertEqual(\"'here is some unicode: \\\\\\\\u20bf \\\\\\\\U0001f600 \\\\\\\\U0001f608'\",\n                         util.error_text_str_to_safe_str(\"here is some unicode: ₿ 😀 😈\"))\n        # long text\n        t1 = util.error_text_str_to_safe_str(\"test\"*10000)\n        self.assertTrue(t1.endswith(\"... (truncated. orig_len=40002)\"))\n        self.assertTrue(len(t1) < 550)\n\n    def test_age(self):\n        now = datetime(2023, 4, 16, 22, 30, 00)\n        self.assertEqual(\"Unknown\",\n                         util.age(from_date=None, since_date=now))\n        # past\n        self.assertEqual(\"less than a minute ago\",\n                         util.age(from_date=now.timestamp()-1, since_date=now))\n        self.assertEqual(\"1 seconds ago\",\n                         util.age(from_date=now.timestamp()-1, since_date=now, include_seconds=True))\n        self.assertEqual(\"25 seconds ago\",\n                         util.age(from_date=now.timestamp()-25, since_date=now, include_seconds=True))\n        self.assertEqual(\"about 30 minutes ago\",\n                         util.age(from_date=now.timestamp()-1800, since_date=now))\n        self.assertEqual(\"about 30 minutes ago\",\n                         util.age(from_date=now.timestamp()-1800, since_date=now, include_seconds=True))\n        self.assertEqual(\"about 1 hour ago\",\n                         util.age(from_date=now.timestamp()-3300, since_date=now))\n        self.assertEqual(\"about 2 hours ago\",\n                         util.age(from_date=now.timestamp()-8700, since_date=now))\n        self.assertEqual(\"about 7 hours ago\",\n                         util.age(from_date=now.timestamp()-26700, since_date=now))\n        self.assertEqual(\"about 1 day ago\",\n                         util.age(from_date=now.timestamp()-109800, since_date=now))\n        self.assertEqual(\"about 3 days ago\",\n                         util.age(from_date=now.timestamp()-282600, since_date=now))\n        self.assertEqual(\"about 15 days ago\",\n                         util.age(from_date=now.timestamp()-1319400, since_date=now))\n        self.assertEqual(\"about 1 month ago\",\n                         util.age(from_date=now.timestamp()-3220200, since_date=now))\n        self.assertEqual(\"about 3 months ago\",\n                         util.age(from_date=now.timestamp()-8317800, since_date=now))\n        self.assertEqual(\"about 1 year ago\",\n                         util.age(from_date=now.timestamp()-39853800, since_date=now))\n        self.assertEqual(\"over 3 years ago\",\n                         util.age(from_date=now.timestamp()-103012200, since_date=now))\n        # future\n        self.assertEqual(\"in less than a minute\",\n                         util.age(from_date=now.timestamp()+1, since_date=now))\n        self.assertEqual(\"in 1 seconds\",\n                         util.age(from_date=now.timestamp()+1, since_date=now, include_seconds=True))\n        self.assertEqual(\"in 25 seconds\",\n                         util.age(from_date=now.timestamp()+25, since_date=now, include_seconds=True))\n        self.assertEqual(\"in about 30 minutes\",\n                         util.age(from_date=now.timestamp()+1800, since_date=now))\n        self.assertEqual(\"in about 30 minutes\",\n                         util.age(from_date=now.timestamp()+1800, since_date=now, include_seconds=True))\n        self.assertEqual(\"in about 1 hour\",\n                         util.age(from_date=now.timestamp()+3300, since_date=now))\n        self.assertEqual(\"in about 2 hours\",\n                         util.age(from_date=now.timestamp()+8700, since_date=now))\n        self.assertEqual(\"in about 7 hours\",\n                         util.age(from_date=now.timestamp()+26700, since_date=now))\n        self.assertEqual(\"in about 1 day\",\n                         util.age(from_date=now.timestamp()+109800, since_date=now))\n        self.assertEqual(\"in about 3 days\",\n                         util.age(from_date=now.timestamp()+282600, since_date=now))\n        self.assertEqual(\"in about 15 days\",\n                         util.age(from_date=now.timestamp()+1319400, since_date=now))\n        self.assertEqual(\"in about 1 month\",\n                         util.age(from_date=now.timestamp()+3220200, since_date=now))\n        self.assertEqual(\"in about 3 months\",\n                         util.age(from_date=now.timestamp()+8317800, since_date=now))\n        self.assertEqual(\"in about 1 year\",\n                         util.age(from_date=now.timestamp()+39853800, since_date=now))\n        self.assertEqual(\"in over 3 years\",\n                         util.age(from_date=now.timestamp()+103012200, since_date=now))\n\n    def test_shortchannelid(self):\n        scid1 = ShortID.from_components(2, 45, 789)\n        self.assertEqual(\"2x45x789\", str(scid1))\n        self.assertEqual(2, scid1.block_height)\n        self.assertEqual(45, scid1.txpos)\n        self.assertEqual(789, scid1.output_index)\n\n        scid2_raw = bytes([0, 0, 2, 0, 0, 45, 789 // 256, 789 % 256])\n        scid2 = ShortID(scid2_raw)\n        self.assertEqual(scid1, scid2_raw)\n        self.assertEqual(scid1, scid2)\n        self.assertEqual(scid1, ShortID.from_str(str(scid1)))\n        self.assertEqual(scid1, ShortID.normalize(scid1.hex()))\n        self.assertEqual(scid1, ShortID.normalize(bytes(scid1)))\n\n        self.assertTrue(ShortID.from_components(3, 30, 300) == ShortID.from_components(3, 30, 300))\n        self.assertTrue(ShortID.from_components(3, 30, 300) > ShortID.from_components(2, 999, 999))\n        self.assertTrue(ShortID.from_components(3, 30, 300) < ShortID.from_components(3, 999, 999))\n        self.assertTrue(ShortID.from_components(3, 30, 300) > ShortID.from_components(3, 1, 1))\n        self.assertTrue(ShortID.from_components(3, 30, 300) > ShortID.from_components(3, 1, 999))\n        self.assertTrue(ShortID.from_components(3, 30, 300) < ShortID.from_components(3, 999, 1))\n\n    async def test_custom_task_factory(self):\n        loop = util.get_running_loop()\n        # set our factory.  note: this does not leak into other unit tests\n        util._set_custom_task_factory(loop)\n\n        evt = asyncio.Event()\n        async def foo():\n            await evt.wait()\n\n        # spawn tasks\n        fut = asyncio.ensure_future(foo())\n        self.assertTrue(fut in util._running_asyncio_tasks)\n        fut = asyncio.create_task(foo())\n        self.assertTrue(fut in util._running_asyncio_tasks)\n        fut = loop.create_task(foo())\n        self.assertTrue(fut in util._running_asyncio_tasks)\n        fut = asyncio.run_coroutine_threadsafe(foo(), loop=loop)\n        # run_coroutine_threadsafe will create a different (chained) future in _running_asyncio_tasks\n        # (which btw will only happen a few event loop iterations later)\n        #self.assertTrue(fut in util._running_asyncio_tasks)\n\n        # wait a few event loop iterations\n        for _ in range(10):\n            await asyncio.sleep(0)\n        # we should have stored one ref for each above.\n        # (though what if test framework is doing stuff ~concurrently?)\n        self.assertEqual(4, len(util._running_asyncio_tasks))\n        for task in util._running_asyncio_tasks:\n            self.assertEqual(foo.__qualname__, task.get_coro().__qualname__)\n        # let tasks finish\n        evt.set()\n        # wait a few event loop iterations\n        for _ in range(10):\n            await asyncio.sleep(0)\n        # refs should be cleaned up by now:\n        self.assertEqual(0, len(util._running_asyncio_tasks))\n\n"
  },
  {
    "path": "tests/test_verifier.py",
    "content": "# -*- coding: utf-8 -*-\n\nfrom electrum.bitcoin import hash_encode\nfrom electrum.transaction import Transaction\nfrom electrum.util import bfh\nfrom electrum.verifier import SPV, InnerNodeOfSpvProofIsValidTx\n\nfrom . import ElectrumTestCase\n\n\nMERKLE_BRANCH = [\n    'f2994fd4546086b21b4916b76cf901afb5c4db1c3ecbfc91d6f4cae1186dfe12',\n    '6b65935528311901c7acda7db817bd6e3ce2f05d1c62c385b7caadb65fac7520']\n\nMERKLE_ROOT = '11dbac015b6969ea75509dd1250f33c04ec4d562c2d895de139a65f62f808254'\n\nVALID_64_BYTE_TX = ('0200000001cb659c5528311901a7aada7db817bd6e3ce2f05d1c62c385b7caad'\n                    'b65fac75201234000000fabcdefa01abcd1234010000000405060708fabcdefa')\nassert len(VALID_64_BYTE_TX) == 128\n\n\nclass VerifierTestCase(ElectrumTestCase):\n    # these tests are regarding the attack described in\n    # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-June/016105.html\n    TESTNET = True\n\n    def test_verify_ok_t_tx(self):\n        \"\"\"Actually mined 64 byte tx should not raise.\"\"\"\n        t_tx = Transaction(VALID_64_BYTE_TX)\n        t_tx_hash = t_tx.txid()\n        self.assertEqual(MERKLE_ROOT, SPV.hash_merkle_root(MERKLE_BRANCH, t_tx_hash, 3))\n\n    def test_verify_fail_f_tx_odd(self):\n        \"\"\"Raise if inner node of merkle branch is valid tx. ('odd' fake leaf position)\"\"\"\n        # first 32 bytes of T encoded as hash\n        fake_branch_node = hash_encode(bfh(VALID_64_BYTE_TX[:64]))\n        fake_mbranch = [fake_branch_node] + MERKLE_BRANCH\n        # last 32 bytes of T encoded as hash\n        f_tx_hash = hash_encode(bfh(VALID_64_BYTE_TX[64:]))\n        with self.assertRaises(InnerNodeOfSpvProofIsValidTx):\n            SPV.hash_merkle_root(fake_mbranch, f_tx_hash, 7)\n\n    def test_verify_fail_f_tx_even(self):\n        \"\"\"Raise if inner node of merkle branch is valid tx. ('even' fake leaf position)\"\"\"\n        # last 32 bytes of T encoded as hash\n        fake_branch_node = hash_encode(bfh(VALID_64_BYTE_TX[64:]))\n        fake_mbranch = [fake_branch_node] + MERKLE_BRANCH\n        # first 32 bytes of T encoded as hash\n        f_tx_hash = hash_encode(bfh(VALID_64_BYTE_TX[:64]))\n        with self.assertRaises(InnerNodeOfSpvProofIsValidTx):\n            SPV.hash_merkle_root(fake_mbranch, f_tx_hash, 6)\n"
  },
  {
    "path": "tests/test_wallet.py",
    "content": "import shutil\nimport tempfile\nimport sys\nimport os\nimport json\nfrom decimal import Decimal\nimport time\nfrom io import StringIO\nimport asyncio\nfrom unittest import mock\nfrom pathlib import Path\n\nfrom electrum.storage import WalletStorage\nfrom electrum.wallet_db import FINAL_SEED_VERSION\nfrom electrum.wallet import (Abstract_Wallet, Standard_Wallet, create_new_wallet,\n                             Imported_Wallet, Wallet)\nfrom electrum.exchange_rate import ExchangeBase, FxThread\nfrom electrum.util import TxMinedInfo, InvalidPassword\nfrom electrum.bitcoin import COIN\nfrom electrum.wallet_db import WalletDB, JsonDB\nfrom electrum.simple_config import SimpleConfig\nfrom electrum import util, storage\nfrom electrum.daemon import Daemon\nfrom electrum.invoices import PR_UNPAID, PR_PAID, PR_UNCONFIRMED\nfrom electrum.transaction import tx_from_any\nfrom electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED\n\nfrom . import ElectrumTestCase\nfrom . import restore_wallet_from_text__for_unittest\n\n\nclass FakeSynchronizer(object):\n\n    def __init__(self, db):\n        self.db = db\n        self.store = []\n\n    def add(self, address):\n        self.store.append(address)\n\n\nclass WalletTestCase(ElectrumTestCase):\n\n    def setUp(self):\n        super(WalletTestCase, self).setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n        self.wallet_path = os.path.join(self.electrum_path, \"somewallet\")\n\n        self._saved_stdout = sys.stdout\n        self._stdout_buffer = StringIO()\n        sys.stdout = self._stdout_buffer\n\n    def tearDown(self):\n        super(WalletTestCase, self).tearDown()\n        # Restore the \"real\" stdout\n        sys.stdout = self._saved_stdout\n\n\nclass TestWalletStorage(WalletTestCase):\n\n    def test_read_dictionary_from_file(self):\n\n        some_dict = {\"a\":\"b\", \"c\":\"d\"}\n        contents = json.dumps(some_dict)\n        with open(self.wallet_path, \"w\") as f:\n            contents = f.write(contents)\n\n        storage = WalletStorage(self.wallet_path)\n        db = JsonDB(storage.read(), storage=storage)\n        self.assertEqual(\"b\", db.get(\"a\"))\n        self.assertEqual(\"d\", db.get(\"c\"))\n\n    def test_write_dictionary_to_file(self):\n\n        storage = WalletStorage(self.wallet_path)\n        db = JsonDB('', storage=storage)\n\n        some_dict = {\n            u\"a\": u\"b\",\n            u\"c\": u\"d\",\n            u\"seed_version\": FINAL_SEED_VERSION}\n\n        for key, value in some_dict.items():\n            db.put(key, value)\n        db.write()\n\n        with open(self.wallet_path, \"r\") as f:\n            contents = f.read()\n        d = json.loads(contents)\n        for key, value in some_dict.items():\n            self.assertEqual(d[key], value)\n\n    async def test_storage_imported_add_privkeys_persistence_test(self):\n        text = ' '.join([\n            'p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL',\n            'p2wpkh:L24GxnN7NNUAfCXA6hFzB1jt59fYAAiFZMcLaJ2ZSawGpM3uqhb1'\n        ])\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet_path, config=self.config)\n        wallet = d['wallet']  # type: Imported_Wallet\n        self.assertEqual(2, len(wallet.get_receiving_addresses()))\n        await wallet.stop()\n\n        # open the wallet anew again, and add a privkey. This should add the new data as a json_patch\n        del wallet\n        wallet = Daemon._load_wallet(self.wallet_path, password=None, config=self.config)\n\n        wallet.import_private_keys(['p2wpkh:KzuqaaLp9zYjVuj8vQtCwFdiZFreW3NJNBachgVS8S9XMgj5y78b'], password=None)\n        self.assertEqual(3, len(wallet.get_receiving_addresses()))\n        self.assertEqual(3, len(wallet.keystore.keypairs))\n        await wallet.stop()\n\n        # open the wallet anew again, and verify if the privkey was stored\n        del wallet\n        wallet = Daemon._load_wallet(self.wallet_path, password=None, config=self.config)\n        self.assertEqual(3, len(wallet.get_receiving_addresses()))\n        self.assertEqual(3, len(wallet.keystore.keypairs))\n        self.assertTrue('03bf450797034dc95693096e575e3b3db14e5f074679b349b727f90fc7804ce7ab' in wallet.keystore.keypairs)\n        self.assertTrue('030dac677b9484e23db6f9255eddf433f4f12c02f9b35e0100f2f103ffbccf540f' in wallet.keystore.keypairs)\n        self.assertTrue('02f11d5f222a728fd08226cb5a1e85a74d58fc257bd3764bf1234346f91defed72' in wallet.keystore.keypairs)\n\n    async def test_storage_prevouts_by_scripthash_persistence(self):\n        text = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet_path, config=self.config)\n        wallet1 = d['wallet']  # type: Standard_Wallet\n        # create payreq\n        addr = wallet1.get_unused_address()\n        self.assertEqual(\"1NNkttn1YvVGdqBW4PR6zvc3Zx3H5owKRf\", addr)\n        pr_key = wallet1.create_request(amount_sat=10000, message=\"msg\", address=addr, exp_delay=86400)\n        pr = wallet1.get_request(pr_key)\n        self.assertIsNotNone(pr)\n        self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr))\n        await wallet1.stop()\n\n        # open the wallet anew again, and get paid onchain\n        del wallet1\n        wallet1 = Daemon._load_wallet(self.wallet_path, password=None, config=self.config)\n        tx = tx_from_any(\"02000000000101a97a9ae7fb1a9220fdd170a974987ac24631dcff89b60fa4907c78c3639994db0000000000fdffffff0210270000000000001976a914ea7804a2c266063572cc009a63dc25dcc0e9d9b588ac20491e0000000000160014b8e4fdc91593b67de2bf214694ef47e38dc2ee8e02473044022005326882904906cfa9c1de75333ace1019596f2ab25d21118220d037dfc0e48b02207d0b3f075cfe5e1e0247ff3cdd7155dc05e7459daf1bfa0ea02e9112b9151ec90121026cc6a74c2b0e38661d341ffae48fe7dde5196ca4afe95d28b496673fa4cf646700000000\")\n        wallet1.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))\n        await wallet1.stop()\n\n        # open the wallet anew again, and verify payreq is still paid\n        del wallet1\n        wallet1 = Daemon._load_wallet(self.wallet_path, password=None, config=self.config)\n        self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))\n\n\nclass FakeExchange(ExchangeBase):\n    def __init__(self, rate):\n        super().__init__(lambda self: None, lambda self: None)\n        self._quotes = {'TEST': rate}\n        self._quotes_timestamp = float(\"inf\")  # spot price from the far future never becomes stale :P\n\nclass FakeFxThread:\n    def __init__(self, exchange):\n        self.exchange = exchange\n        self.ccy = 'TEST'\n\n    remove_thousands_separator = staticmethod(FxThread.remove_thousands_separator)\n    timestamp_rate = FxThread.timestamp_rate\n    ccy_amount_str = FxThread.ccy_amount_str\n    history_rate = FxThread.history_rate\n\nclass FakeADB:\n    def get_tx_height(self, txid):\n        # because we use a current timestamp, and history is empty,\n        # FxThread.history_rate will use spot prices\n        return TxMinedInfo(_height=10, conf=10, timestamp=int(time.time()), header_hash='def')\n\nclass FakeWallet:\n    def __init__(self, fiat_value):\n        super().__init__()\n        self.fiat_value = fiat_value\n        self.db = WalletDB('', storage=None, upgrade=False)\n        self.adb = FakeADB()\n        self.db.transactions = self.db.verified_tx = {'abc':'Tx'}\n\n    default_fiat_value = Abstract_Wallet.default_fiat_value\n    price_at_timestamp = Abstract_Wallet.price_at_timestamp\n    class storage:\n        put = lambda self, x: None\n\ntxid = 'abc'\nccy = 'TEST'\n\nclass TestFiat(ElectrumTestCase):\n    def setUp(self):\n        super().setUp()\n        self.value_sat = COIN\n        self.fiat_value = {}\n        self.wallet = FakeWallet(fiat_value=self.fiat_value)\n        self.fx = FakeFxThread(FakeExchange(Decimal('1000.001')))\n        default_fiat = Abstract_Wallet.default_fiat_value(self.wallet, txid, self.fx, self.value_sat)\n        self.assertEqual(Decimal('1000.001'), default_fiat)\n        self.assertEqual('1 000.00', self.fx.ccy_amount_str(default_fiat, add_thousands_sep=True))\n\n    def test_save_fiat_and_reset(self):\n        self.assertEqual(False, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1000.01', self.fx, self.value_sat))\n        saved = self.fiat_value[ccy][txid]\n        self.assertEqual('1 000.01', self.fx.ccy_amount_str(Decimal(saved), add_thousands_sep=True))\n        self.assertEqual(True,       Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '', self.fx, self.value_sat))\n        self.assertNotIn(txid, self.fiat_value[ccy])\n        # even though we are not setting it to the exact fiat value according to the exchange rate, precision is truncated away\n        self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1 000.002', self.fx, self.value_sat))\n\n    def test_too_high_precision_value_resets_with_no_saved_value(self):\n        self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1 000.001', self.fx, self.value_sat))\n\n    def test_empty_resets(self):\n        self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '', self.fx, self.value_sat))\n        self.assertNotIn(ccy, self.fiat_value)\n\n    def test_save_garbage(self):\n        self.assertEqual(False, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, 'garbage', self.fx, self.value_sat))\n        self.assertNotIn(ccy, self.fiat_value)\n\n\nclass TestHistoryExport(ElectrumTestCase):\n    TESTNET = True\n\n    def setUp(self):\n        # mock timezone, explicitly define timezone as the CI seems to miss a timezone db and cant resolve just 'CET'\n        self.patch_timezone = mock.patch.dict(os.environ, {'TZ': 'CET-1CEST,M3.5.0,M10.5.0/3'})\n        self.patch_timezone.start()\n        time.tzset()\n        super(TestHistoryExport, self).setUp()\n        shutil.copytree(Path(__file__).parent / \"fiat_fx_data\", Path(self.electrum_path) / \"cache\")\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n    def tearDown(self):\n        super(TestHistoryExport, self).tearDown()\n        self.patch_timezone.stop()\n        time.tzset()\n\n    @mock.patch('electrum.wallet.run_hook')\n    @mock.patch.object(storage.WalletStorage, 'write')\n    @mock.patch.object(storage.WalletStorage, 'append')\n    async def test_export_history_to_file(self, _mock_append, _mock_write, mock_run_hook):\n        # prepare wallet with realistic history\n        c = self.config\n        c.NETWORK_OFFLINE = True\n        c.FX_EXCHANGE, c.FX_CURRENCY, c.FX_USE_EXCHANGE_RATE, c.FX_HISTORY_RATES = \"BitFinex\", \"EUR\", True, True\n        daemon = Daemon(config=c, listen_jsonrpc=False)\n        test_wallet_name = \"client_4_5_2_9dk_with_ln\"  # has labels, local tx, ln tx\n        wallet_path = self.get_wallet_file_path(test_wallet_name)\n        test_wallet = daemon.load_wallet(wallet_path, None, upgrade=True)\n        self.assertTrue(daemon.fx.has_history())\n        mock_run_hook.return_value = False\n\n        testcases = (\n            f'history_no_fx_{test_wallet_name}.csv',\n            f'history_with_fx_{test_wallet_name}.csv',\n            f'history_no_fx_{test_wallet_name}.json',\n            f'history_with_fx_{test_wallet_name}.json',\n        )\n        for filename in testcases:\n            test_export_path = (Path(self.electrum_path) / filename).as_posix()\n            is_csv = filename.endswith('.csv')\n            fx = daemon.fx if 'with_fx' in filename else None\n            test_wallet.export_history_to_file(\n                file_path=test_export_path,\n                is_csv=is_csv,\n                fx=fx,\n            )\n            mock_run_hook.assert_called_with('export_history_to_file', test_wallet, fx, test_export_path, is_csv)\n            reference_path = Path(__file__).parent / \"test_history_export\" / filename\n            with open(reference_path, 'r', encoding='utf-8') as reference:\n                reference_text = reference.readlines()\n            with open(test_export_path, 'r', encoding='utf-8') as test_export:\n                test_export_text = test_export.readlines()\n            # compare line by line for more readable traceback on difference\n            for reference, test in zip(reference_text, test_export_text):\n                self.assertEqual(reference, test)\n\n\nclass TestCreateRestoreWallet(WalletTestCase):\n\n    async def test_create_new_wallet(self):\n        passphrase = 'mypassphrase'\n        password = 'mypassword'\n        encrypt_file = True\n        d = create_new_wallet(path=self.wallet_path,\n                              passphrase=passphrase,\n                              password=password,\n                              encrypt_file=encrypt_file,\n                              gap_limit=1,\n                              gap_limit_for_change=1,\n                              config=self.config)\n        wallet = d['wallet']  # type: Standard_Wallet\n\n        # lightning initialization\n        self.assertTrue(wallet.db.get('lightning_xprv').startswith('zprv'))\n\n        wallet.check_password(password)\n        self.assertEqual(passphrase, wallet.keystore.get_passphrase(password))\n        self.assertEqual(d['seed'], wallet.keystore.get_seed(password))\n        self.assertEqual(encrypt_file, wallet.storage.is_encrypted())\n\n    async def test_restore_wallet_from_text_mnemonic(self):\n        text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'\n        passphrase = 'mypassphrase'\n        password = 'mypassword'\n        encrypt_file = True\n        d = restore_wallet_from_text__for_unittest(\n            text,\n            path=self.wallet_path,\n            passphrase=passphrase,\n            password=password,\n            encrypt_file=encrypt_file,\n            gap_limit=1,\n            config=self.config)\n        wallet = d['wallet']  # type: Standard_Wallet\n        self.assertEqual(passphrase, wallet.keystore.get_passphrase(password))\n        self.assertEqual(text, wallet.keystore.get_seed(password))\n        self.assertEqual(encrypt_file, wallet.storage.is_encrypted())\n        self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0])\n\n    async def test_restore_wallet_from_text_no_storage(self):\n        text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'\n        d = restore_wallet_from_text__for_unittest(\n            text,\n            path=None,\n            gap_limit=1,\n            config=self.config,\n        )\n        wallet = d['wallet']  # type: Standard_Wallet\n        self.assertEqual(None, wallet.storage)\n        self.assertEqual(text, wallet.keystore.get_seed(None))\n        self.assertEqual('bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', wallet.get_receiving_addresses()[0])\n\n    async def test_restore_wallet_from_text_xpub(self):\n        text = 'zpub6nydoME6CFdJtMpzHW5BNoPz6i6XbeT9qfz72wsRqGdgGEYeivso6xjfw8cGcCyHwF7BNW4LDuHF35XrZsovBLWMF4qXSjmhTXYiHbWqGLt'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet_path, gap_limit=1, config=self.config)\n        wallet = d['wallet']  # type: Standard_Wallet\n        self.assertEqual(text, wallet.keystore.get_master_public_key())\n        self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0])\n\n    async def test_restore_wallet_from_text_xkey_that_is_also_a_valid_electrum_seed_by_chance(self):\n        text = 'yprvAJBpuoF4FKpK92ofzQ7ge6VJMtorow3maAGPvPGj38ggr2xd1xCrC9ojUVEf9jhW5L9SPu6fU2U3o64cLrRQ83zaQGNa6YP3ajZS6hHNPXj'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet_path, gap_limit=1, config=self.config)\n        wallet = d['wallet']  # type: Standard_Wallet\n        self.assertEqual(text, wallet.keystore.get_master_private_key(password=None))\n        self.assertEqual('3Pa4hfP3LFWqa2nfphYaF7PZfdJYNusAnp', wallet.get_receiving_addresses()[0])\n\n    async def test_restore_wallet_from_text_xprv(self):\n        text = 'zprvAZzHPqhCMt51fskXBUYB1fTFYgG3CBjJUT4WEZTpGw6hPSDWBPZYZARC5sE9xAcX8NeWvvucFws8vZxEa65RosKAhy7r5MsmKTxr3hmNmea'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet_path, gap_limit=1, config=self.config)\n        wallet = d['wallet']  # type: Standard_Wallet\n        self.assertEqual(text, wallet.keystore.get_master_private_key(password=None))\n        self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0])\n\n    async def test_restore_wallet_from_text_addresses(self):\n        text = 'bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw bc1qnp78h78vp92pwdwq5xvh8eprlga5q8gu66960c'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet_path, config=self.config)\n        wallet = d['wallet']  # type: Imported_Wallet\n        self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0])\n        self.assertEqual(2, len(wallet.get_receiving_addresses()))\n        # also test addr deletion\n        wallet.delete_address('bc1qnp78h78vp92pwdwq5xvh8eprlga5q8gu66960c')\n        self.assertEqual(1, len(wallet.get_receiving_addresses()))\n\n    async def test_restore_wallet_from_text_privkeys(self):\n        text = 'p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL p2wpkh:L24GxnN7NNUAfCXA6hFzB1jt59fYAAiFZMcLaJ2ZSawGpM3uqhb1'\n        d = restore_wallet_from_text__for_unittest(text, path=self.wallet_path, config=self.config)\n        wallet = d['wallet']  # type: Imported_Wallet\n        addr0 = wallet.get_receiving_addresses()[0]\n        self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', addr0)\n        self.assertEqual('p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL',\n                         wallet.export_private_key(addr0, password=None))\n        self.assertEqual(2, len(wallet.get_receiving_addresses()))\n        # also test addr deletion\n        wallet.delete_address('bc1qnp78h78vp92pwdwq5xvh8eprlga5q8gu66960c')\n        self.assertEqual(1, len(wallet.get_receiving_addresses()))\n\n\nclass TestWalletPassword(WalletTestCase):\n\n    async def test_update_password_of_imported_wallet(self):\n        wallet_str = '{\"addr_history\":{\"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\":[],\"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\":[],\"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\":[]},\"addresses\":{\"change\":[],\"receiving\":[\"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\",\"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\",\"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\"]},\"keystore\":{\"keypairs\":{\"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\":\"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\",\"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\":\"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\",\"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\":\"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"},\"type\":\"imported\"},\"pruned_txo\":{},\"seed_version\":13,\"stored_height\":-1,\"transactions\":{},\"tx_fees\":{},\"txi\":{},\"txo\":{},\"use_encryption\":false,\"verified_tx3\":{},\"wallet_type\":\"standard\",\"winpos-qt\":[100,100,840,405]}'\n        storage = WalletStorage(self.wallet_path)\n        db = WalletDB(wallet_str, storage=storage, upgrade=True)\n        wallet = Wallet(db, config=self.config)\n\n        wallet.check_password(None)\n\n        wallet.update_password(None, \"1234\")\n\n        with self.assertRaises(InvalidPassword):\n            wallet.check_password(None)\n        with self.assertRaises(InvalidPassword):\n            wallet.check_password(\"wrong password\")\n        wallet.check_password(\"1234\")\n\n    async def test_update_password_of_standard_wallet(self):\n        wallet_str = '''{\"addr_history\":{\"12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes\":[],\"12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1\":[],\"13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB\":[],\"13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c\":[],\"14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz\":[],\"14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA\":[],\"15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV\":[],\"17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z\":[],\"18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv\":[],\"18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B\":[],\"19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz\":[],\"19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G\":[],\"1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq\":[],\"1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d\":[],\"1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs\":[],\"1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado\":[],\"1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z\":[],\"1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52\":[],\"1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP\":[],\"1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv\":[],\"1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb\":[],\"1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ\":[],\"1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G\":[],\"1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN\":[],\"1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J\":[],\"1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt\":[]},\"addresses\":{\"change\":[\"1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP\",\"1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z\",\"15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV\",\"1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq\",\"19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G\",\"1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb\"],\"receiving\":[\"14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA\",\"13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB\",\"19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz\",\"1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv\",\"1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt\",\"13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c\",\"1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ\",\"12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes\",\"12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1\",\"14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz\",\"1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN\",\"17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z\",\"1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado\",\"18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv\",\"1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G\",\"18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B\",\"1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d\",\"1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs\",\"1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52\",\"1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J\"]},\"keystore\":{\"seed\":\"cereal wise two govern top pet frog nut rule sketch bundle logic\",\"type\":\"bip32\",\"xprv\":\"xprv9s21ZrQH143K29XjRjUs6MnDB9wXjXbJP2kG1fnRk8zjdDYWqVkQYUqaDtgZp5zPSrH5PZQJs8sU25HrUgT1WdgsPU8GbifKurtMYg37d4v\",\"xpub\":\"xpub661MyMwAqRbcEdcCXm1sTViwjBn28zK9kFfrp4C3JUXiW1sfP34f6HA45B9yr7EH5XGzWuTfMTdqpt9XPrVQVUdgiYb5NW9m8ij1FSZgGBF\"},\"pruned_txo\":{},\"seed_type\":\"standard\",\"seed_version\":13,\"stored_height\":-1,\"transactions\":{},\"tx_fees\":{},\"txi\":{},\"txo\":{},\"use_encryption\":false,\"verified_tx3\":{},\"wallet_type\":\"standard\",\"winpos-qt\":[619,310,840,405]}'''\n        storage = WalletStorage(self.wallet_path)\n        db = WalletDB(wallet_str, storage=storage, upgrade=True)\n        wallet = Wallet(db, config=self.config)\n\n        wallet.check_password(None)\n\n        wallet.update_password(None, \"1234\")\n        with self.assertRaises(InvalidPassword):\n            wallet.check_password(None)\n        with self.assertRaises(InvalidPassword):\n            wallet.check_password(\"wrong password\")\n        wallet.check_password(\"1234\")\n\n    async def test_update_password_of_standard_wallet_oldseed(self):\n        d = restore_wallet_from_text__for_unittest(\n            \"powerful random nobody notice nothing important anyway look away hidden message over\", path=self.wallet_path, config=self.config)\n        wallet = d['wallet']  # type: Standard_Wallet\n\n        wallet.check_password(None)\n\n        wallet.update_password(None, \"1234\")\n        with self.assertRaises(InvalidPassword):\n            wallet.check_password(None)\n        with self.assertRaises(InvalidPassword):\n            wallet.check_password(\"wrong password\")\n        wallet.check_password(\"1234\")\n\n    async def test_update_password_with_app_restarts(self):\n        wallet_str = '{\"addr_history\":{\"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\":[],\"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\":[],\"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\":[]},\"addresses\":{\"change\":[],\"receiving\":[\"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr\",\"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6\",\"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA\"]},\"keystore\":{\"keypairs\":{\"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5\":\"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM\",\"0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f\":\"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U\",\"04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2\":\"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq\"},\"type\":\"imported\"},\"pruned_txo\":{},\"seed_version\":13,\"stored_height\":-1,\"transactions\":{},\"tx_fees\":{},\"txi\":{},\"txo\":{},\"use_encryption\":false,\"verified_tx3\":{},\"wallet_type\":\"standard\",\"winpos-qt\":[100,100,840,405]}'\n        storage = WalletStorage(self.wallet_path)\n        db = WalletDB(wallet_str, storage=storage, upgrade=True)\n        wallet = Wallet(db, config=self.config)\n        await wallet.stop()\n\n        storage = WalletStorage(self.wallet_path)\n        # if storage.is_encrypted():\n        #     storage.decrypt(password)\n        db = WalletDB(storage.read(), storage=storage, upgrade=True)\n        wallet = Wallet(db, config=self.config)\n\n        wallet.check_password(None)\n\n        wallet.update_password(None, \"1234\")\n        with self.assertRaises(InvalidPassword):\n            wallet.check_password(None)\n        with self.assertRaises(InvalidPassword):\n            wallet.check_password(\"wrong password\")\n        wallet.check_password(\"1234\")\n"
  },
  {
    "path": "tests/test_wallet_vertical.py",
    "content": "import unittest\nfrom unittest import mock\nimport shutil\nimport tempfile\nfrom typing import Sequence\nimport asyncio\nimport copy\n\nfrom electrum import bitcoin, keystore, bip32, slip39, wallet\nfrom electrum.wallet_db import WalletDB\nfrom electrum.storage import WalletStorage\nfrom electrum import SimpleConfig\nfrom electrum import util\nfrom electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE\nfrom electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet,\n                             Abstract_Wallet, CannotBumpFee, BumpFeeStrategy,\n                             TransactionPotentiallyDangerousException,\n                             TransactionDangerousException,\n                             TxSighashRiskLevel, CannotDoubleSpendTx)\nfrom electrum.util import bfh, NotEnoughFunds, UnrelatedTransactionException, UserFacingException, TxMinedInfo\nfrom electrum.fee_policy import FixedFeePolicy\nfrom electrum.transaction import Transaction, PartialTxOutput, tx_from_any, Sighash\nfrom electrum.mnemonic import calc_seed_type\nfrom electrum.network import Network\n\nfrom electrum.plugins.trustedcoin import trustedcoin\n\nfrom . import ElectrumTestCase\nfrom . import restore_wallet_from_text__for_unittest\n\n\nUNICODE_HORROR_HEX = 'e282bf20f09f988020f09f98882020202020e3818620e38191e3819fe381be20e3828fe3828b2077cda2cda2cd9d68cda16fcda2cda120ccb8cda26bccb5cd9f6eccb4cd98c7ab77ccb8cc9b73cd9820cc80cc8177cd98cda2e1b8a9ccb561d289cca1cda27420cca7cc9568cc816fccb572cd8fccb5726f7273cca120ccb6cda1cda06cc4afccb665cd9fcd9f20ccb6cd9d696ecda220cd8f74cc9568ccb7cca1cd9f6520cd9fcd9f64cc9b61cd9c72cc95cda16bcca2cca820cda168ccb465cd8f61ccb7cca2cca17274cc81cd8f20ccb4ccb7cda0c3b2ccb5ccb666ccb82075cca7cd986ec3adcc9bcd9c63cda2cd8f6fccb7cd8f64ccb8cda265cca1cd9d3fcd9e'\nUNICODE_HORROR = bfh(UNICODE_HORROR_HEX).decode('utf-8')\nassert UNICODE_HORROR == '₿ 😀 😈     う けたま わる w͢͢͝h͡o͢͡ ̸͢k̵͟n̴͘ǫw̸̛s͘ ̀́w͘͢ḩ̵a҉̡͢t ̧̕h́o̵r͏̵rors̡ ̶͡͠lį̶e͟͟ ̶͝in͢ ͏t̕h̷̡͟e ͟͟d̛a͜r̕͡k̢̨ ͡h̴e͏a̷̢̡rt́͏ ̴̷͠ò̵̶f̸ u̧͘ní̛͜c͢͏o̷͏d̸͢e̡͝?͞'\n\n\nclass WalletIntegrityHelper:\n\n    gap_limit = 1  # make tests run faster\n    gap_limit_for_change = 1  # make tests run faster\n\n    @classmethod\n    def check_seeded_keystore_sanity(cls, test_obj, ks):\n        test_obj.assertTrue(ks.is_deterministic())\n        test_obj.assertFalse(ks.is_watching_only())\n        test_obj.assertFalse(ks.can_import())\n        test_obj.assertTrue(ks.has_seed())\n\n    @classmethod\n    def check_xpub_keystore_sanity(cls, test_obj, ks):\n        test_obj.assertTrue(ks.is_deterministic())\n        test_obj.assertTrue(ks.is_watching_only())\n        test_obj.assertFalse(ks.can_import())\n        test_obj.assertFalse(ks.has_seed())\n\n    @classmethod\n    def create_standard_wallet(cls, ks, *, config: SimpleConfig, gap_limit=None, gap_limit_for_change=None):\n        db = WalletDB('', storage=None, upgrade=True)\n        db.put('keystore', ks.dump())\n        db.put('gap_limit', gap_limit or cls.gap_limit)\n        db.put('gap_limit_for_change', gap_limit_for_change or cls.gap_limit_for_change)\n        w = Standard_Wallet(db, config=config)\n        w.synchronize()\n        return w\n\n    @classmethod\n    def create_imported_wallet(cls, *, config: SimpleConfig, privkeys: bool):\n        db = WalletDB('', storage=None, upgrade=True)\n        if privkeys:\n            k = keystore.Imported_KeyStore({})\n            db.put('keystore', k.dump())\n        w = Imported_Wallet(db, config=config)\n        return w\n\n    @classmethod\n    def create_multisig_wallet(\n        cls,\n        keystores: Sequence,\n        multisig_type: str,\n        *,\n        config: SimpleConfig,\n        storage: WalletStorage | None = None,\n        gap_limit=None,\n        gap_limit_for_change=None,\n    ):\n        \"\"\"Creates a multisig wallet.\"\"\"\n        db = WalletDB('', storage=storage, upgrade=False)\n        for i, ks in enumerate(keystores):\n            cosigner_index = i + 1\n            db.put('x%d' % cosigner_index, ks.dump())\n        db.put('wallet_type', multisig_type)\n        db.put('gap_limit', gap_limit or cls.gap_limit)\n        db.put('gap_limit_for_change', gap_limit_for_change or cls.gap_limit_for_change)\n        w = Multisig_Wallet(db, config=config)\n        w.synchronize()\n        return w\n\n\ndef read_test_vector(filename: str):\n    import os\n    from electrum.util import read_json_file\n    path = os.path.join(os.path.dirname(__file__), filename)\n    data = read_json_file(path)\n    return data\n\n\nclass TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_electrum_seed_standard(self, mock_save_db):\n        seed_words = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song'\n        self.assertEqual(calc_seed_type(seed_words), 'standard')\n\n        ks = keystore.from_seed(seed_words, passphrase='', for_multisig=False)\n\n        WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks)\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'xprv9s21ZrQH143K32jECVM729vWgGq4mUDJCk1ozqAStTphzQtCTuoFmFafNoG1g55iCnBTXUzz3zWnDb5CVLGiFvmaZjuazHDL8a81cPQ8KL6')\n        self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2pkh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '1NNkttn1YvVGdqBW4PR6zvc3Zx3H5owKRf')\n        self.assertEqual(w.get_change_addresses()[0], '1KSezYMhAJMWqFbVFB2JshYg69UpmEXR4D')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_electrum_seed_segwit(self, mock_save_db):\n        seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'\n        self.assertEqual(calc_seed_type(seed_words), 'segwit')\n\n        ks = keystore.from_seed(seed_words, passphrase='', for_multisig=False)\n\n        WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks)\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'zprvAZswDvNeJeha8qZ8g7efN3FXYVJLaEUsE9TW6qXDEbVe74AZ75c2sZFZXPNFzxnhChDQ89oC8C5AjWwHmH1HeRKE1c4kKBQAmjUDdKDUZw2')\n        self.assertEqual(ks.xpub, 'zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2wpkh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], 'bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af')\n        self.assertEqual(w.get_change_addresses()[0], 'bc1qdy94n2q5qcp0kg7v9yzwe6wvfkhnvyzje7nx2p')\n\n        self.assertEqual('zprvAabC4ncjU4qVMNbpYZ5G4XqmKJoJN3EA4TVCodaPwyvEatrZpVYmWVHfKwS1fdq2uCdPyCmbjAjQ5FzeqHFSGv9KUmUFptTMAcyKzHiUM6Q',\n                         ks.get_lightning_xprv(None))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_electrum_seed_segwit_passphrase(self, mock_save_db):\n        seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'\n        self.assertEqual(calc_seed_type(seed_words), 'segwit')\n\n        ks = keystore.from_seed(seed_words, passphrase=UNICODE_HORROR, for_multisig=False)\n\n        WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks)\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'zprvAZDmEQiCLUcZXPfrBXoksCD2R6RMAzAre7SUyBotibisy9c7vGhLYvHaP3d9rYU12DKAWdZfscPNA7qEPgTkCDqX5sE93ryAJAQvkDbfLxU')\n        self.assertEqual(ks.xpub, 'zpub6nD7dvF6ArArjskKHZLmEL9ky8FqaSti1LN5maDWGwFrqwwGTp1b6ic4EHwciFNaYDmCXcQYxXSiF9BjcLCMPcaYkVN2nQD6QjYQ8vpSR3Z')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2wpkh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], 'bc1qx94dutas7ysn2my645cyttujrms5d9p57f6aam')\n        self.assertEqual(w.get_change_addresses()[0], 'bc1qcywwsy87sdp8vz5rfjh3sxdv6rt95kujdqq38g')\n\n        self.assertEqual('zprvAaoTFrze53KLvVYL8yL5H4sxoBFto98dgfTxFxcBepBPaEWStxpsdYqvNGxskGMTgX11bUtPiVj3aCe2jXFkAJQMi9RmksGBgFVwFM85Gir',\n                         ks.get_lightning_xprv(None))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_electrum_seed_old(self, mock_save_db):\n        seed_words = 'powerful random nobody notice nothing important anyway look away hidden message over'\n        seed_hex = 'acb740e454c3134901d7c8f16497cc1c'\n\n        for seed_to_restore_from in (seed_words, seed_hex):\n            with self.subTest(seed_to_restore_from=seed_to_restore_from):\n                self.assertEqual(calc_seed_type(seed_to_restore_from), 'old')\n\n                ks = keystore.from_seed(seed_to_restore_from, passphrase='', for_multisig=False)\n\n                WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks)\n                self.assertTrue(isinstance(ks, keystore.Old_KeyStore))\n\n                self.assertEqual(ks.mpk, 'e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3')\n                self.assertEqual(ks.seed, seed_hex)\n                self.assertEqual(ks._get_hex_seed(password=None), seed_hex)\n                self.assertEqual(ks.get_seed(password=None), seed_words)\n\n                w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n                self.assertEqual(w.txin_type, 'p2pkh')\n\n                self.assertEqual(w.get_receiving_addresses()[0], '1FJEEB8ihPMbzs2SkLmr37dHyRFzakqUmo')\n                self.assertEqual(w.get_change_addresses()[0], '1KRW8pH6HFHZh889VDq6fEKvmrsmApwNfe')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_electrum_seed_2fa_legacy_pre27_25words(self, mock_save_db):\n        # pre-version-2.7 2fa seed, containing 25 words\n        seed_words = 'bind clever room kidney crucial sausage spy edit canvas soul liquid ribbon slam open alpha suffer gate relax voice carpet law hill woman tonight abstract'\n        assert len(seed_words.split()) == 25\n        self.assertEqual(calc_seed_type(seed_words), '2fa')\n\n        xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '')\n\n        ks1 = keystore.from_xprv(xprv1)\n        self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))\n        self.assertEqual(ks1.xprv, 'xprv9s21ZrQH143K2TsDemiaPqaTuBkW3gns4sGi9f65pWtg27nmmmAut6fErgaHFxj3d4rHgyFKjhvtAUafqF3wwU8Bkou8LefQgBtRWjUKN3V')\n        self.assertEqual(ks1.xpub, 'xpub661MyMwAqRbcEwwgkoFakyXCTDazT9WiS6CJx3VhNrRetv7vKJVARtyihwCVatSsUtVsEYcvdxhvDtkSk8qKV3VVtcL3csz6sQTbGzmEckd')\n        self.assertEqual(ks1.xpub, xpub1)\n\n        ks2 = keystore.from_xprv(xprv2)\n        self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))\n        self.assertEqual(ks2.xprv, 'xprv9s21ZrQH143K3r6H1h91TqRECE7tmDB5PYGZDKPuSjefTzNbDMauUMUnjsUSv8X8nuzQsrtGmtCuA51CNz7XimRj2HPYxUxXGGf4KB7M74y')\n        self.assertEqual(ks2.xpub, 'xpub661MyMwAqRbcGLAk7ig1pyMxkFxPAftvkmCA1hoX15BeLnhjktuA29oGb7bh9opQgNERu6iWhwcY6b5bZX57dYsGo7zYjwXTNCryfKuPfek')\n        self.assertEqual(ks2.xpub, xpub2)\n\n        long_user_id, short_id = trustedcoin.get_user_id(\n            {'x1': {'xpub': xpub1},\n             'x2': {'xpub': xpub2}})\n        xtype = bip32.xpub_type(xpub1)\n        xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id)\n        ks3 = keystore.from_xpub(xpub3)\n        WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks3)\n        self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore))\n\n        w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config)\n        self.assertEqual(w.txin_type, 'p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '3Bw5jczNModhFAbvfwvUHbdGrC2Lh2qRQp')\n        self.assertEqual(w.get_change_addresses()[0], '3Ke6pKrmtSyyQaMob1ES4pk8siAAkRmst9')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_electrum_seed_2fa_legacy_pre27_24words(self, mock_save_db):\n        # pre-version-2.7 2fa seed, containing 24 words\n        seed_words = 'sibling leg cable timber patient foot occur plate travel finger chef scale radio citizen promote immune must chef fluid sea sphere common acid lab'\n        assert len(seed_words.split()) == 24\n        self.assertEqual(calc_seed_type(seed_words), '2fa')\n\n        xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '')\n\n        ks1 = keystore.from_xprv(xprv1)\n        self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))\n        self.assertEqual(ks1.xprv, 'xprv9s21ZrQH143K37iqjPnsBm27cRgrg6TiKNwhCYg7Uk46yLKB5s4N1Knzo7rTkYvjojh9Z6KkGTMi6CV5h4kEcWYLmHjcTW8kK5bnMVXvEvp')\n        self.assertEqual(ks1.xpub, 'xpub661MyMwAqRbcFboJqRKsYtxrATXM5ZBZgbsHzw5j35b5r8eKdQNcZ87UeR24LDSn2RxspwL9s7yM3KqtPFq5dwP5csmQ2Xb1dgaQztrNGyP')\n        self.assertEqual(ks1.xpub, xpub1)\n\n        ks2 = keystore.from_xprv(xprv2)\n        self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))\n        self.assertEqual(ks2.xprv, 'xprv9s21ZrQH143K2qJ6sVTs5bXnrw7CPEpYTkefvW6Xj9fMuskny5t3TaLMAvZtSkYwT68asJdrEaay8q4ntmXvYCuQL3ULdEziFCB9KyZhuDX')\n        self.assertEqual(ks2.xpub, 'xpub661MyMwAqRbcFKNZyWzsSjUXQxwgnhYPpyaGitW9HVCLng5wWdCJ1Neq2DLV3717ED1RG3aTGLJVVBt5CJEXmCzMLBjqXtK4MEvRXiYSvnJ')\n        self.assertEqual(ks2.xpub, xpub2)\n\n        long_user_id, short_id = trustedcoin.get_user_id(\n            {'x1': {'xpub': xpub1},\n             'x2': {'xpub': xpub2}})\n        xtype = bip32.xpub_type(xpub1)\n        xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id)\n        ks3 = keystore.from_xpub(xpub3)\n        WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks3)\n        self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore))\n\n        w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config)\n        self.assertEqual(w.txin_type, 'p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '39XK9VBGiK4bqNJYrajfKE8C1ky4gYA5Zy')\n        self.assertEqual(w.get_change_addresses()[0], '3PKtHrjiKdsZ73ULZ4Sf1vDBnrUoAEtLDe')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_electrum_seed_2fa_legacy_post27(self, mock_save_db):\n        # post-version-2.7 2fa seed\n        seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove'\n        self.assertEqual(calc_seed_type(seed_words), '2fa')\n\n        xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '')\n\n        ks1 = keystore.from_xprv(xprv1)\n        self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))\n        self.assertEqual(ks1.xprv, 'xprv9uraXy9F3HP7i8QDqwNTBiD8Jf4bPD4Epif8cS8qbUbgeidUesyZpKmzfcSeHutsGfFnjgih7kzwTB5UQVRNB5LoXaNc8pFusKYx3KVVvYR')\n        self.assertEqual(ks1.xpub, 'xpub68qvwUg8sewQvcUgwxuTYr9rrgu5nfn6BwajQpYT9p8fXWxdCRHpN86UWruWJAD1ede8Sv8ERrTa22Gyc4SBfm7zFpcyoVWVBKCVwnw6s1J')\n        self.assertEqual(ks1.xpub, xpub1)\n\n        ks2 = keystore.from_xprv(xprv2)\n        self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))\n        self.assertEqual(ks2.xprv, 'xprv9uraXy9F3HP7kKSiRAvLV7Nrjj7YzspDys7dvGLLu4tLZT49CEBxPWp88dHhVxvZ69SHrPQMUCWjj4Ka2z9kNvs1HAeEf3extGGeSWqEVqf')\n        self.assertEqual(ks2.xpub, 'xpub68qvwUg8sewQxoXBXCTLrFKbHkx3QLY5M63EiejxTQRKSFPHjmWCwK8byvZMM2wZNYA3SmxXoma3M1zxhGESHZwtB7SwrxRgKXAG8dCD2eS')\n        self.assertEqual(ks2.xpub, xpub2)\n\n        long_user_id, short_id = trustedcoin.get_user_id(\n            {'x1': {'xpub': xpub1},\n             'x2': {'xpub': xpub2}})\n        xtype = bip32.xpub_type(xpub1)\n        xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id)\n        ks3 = keystore.from_xpub(xpub3)\n        WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks3)\n        self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore))\n\n        w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config)\n        self.assertEqual(w.txin_type, 'p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '35L8XmCDoEBKeaWRjvmZvoZvhp8BXMMMPV')\n        self.assertEqual(w.get_change_addresses()[0], '3PeZEcumRqHSPNN43hd4yskGEBdzXgY8Cy')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_electrum_seed_2fa_segwit(self, mock_save_db):\n        seed_words = 'universe topic remind silver february ranch shine worth innocent cattle enhance wise'\n        self.assertEqual(calc_seed_type(seed_words), '2fa_segwit')\n\n        xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '')\n\n        ks1 = keystore.from_xprv(xprv1)\n        self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))\n        self.assertEqual(ks1.xprv, 'ZprvAm1R3RZMrkSLYKZer8QECGoc8oA1RQuKfsztHkBTmi2yF8RhmN1JRb7Ag69mMrL88sP67WiaegaSSDnKndorWEpFr7a5B2QgrD7TkERSYX6')\n        self.assertEqual(ks1.xpub, 'Zpub6yzmSw6Fh7zdkoe7x9wEZQkLgpzVpsdB36vV68b5L3Zx7vkrJuKYyPReXMSjBegmtUjFBxP2uZEdL87cYvtTtGaVuwtRRCTSFUsoAdKZMge')\n        self.assertEqual(ks1.xpub, xpub1)\n\n        ks2 = keystore.from_xprv(xprv2)\n        self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))\n        self.assertEqual(ks2.xprv, 'ZprvAm1R3RZMrkSLab4jVKTwuroBgKEfnsmK9CQa1ErkuRzpsPauYuv9z2UzhDNn9YgbLHcmXpmxbNq4MdDRAUM5B2N9Wr3Uq9yp2c4AtTJDFdi')\n        self.assertEqual(ks2.xpub, 'Zpub6yzmSw6Fh7zdo59CbLzxGzjvEM5ACLVAWRLAodGNTmXokBv46TEQXpoUYUaoxPCeynysxg7APfScikCQ2jhCfM3NcNEk46BCVfSSrdrSkbR')\n        self.assertEqual(ks2.xpub, xpub2)\n\n        long_user_id, short_id = trustedcoin.get_user_id(\n            {'x1': {'xpub': xpub1},\n             'x2': {'xpub': xpub2}})\n        xtype = bip32.xpub_type(xpub1)\n        xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id)\n        ks3 = keystore.from_xpub(xpub3)\n        WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks3)\n        self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore))\n\n        w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config)\n        self.assertEqual(w.txin_type, 'p2wsh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], 'bc1qpmufh0zjp5prfsrk2yskcy82sa26srqkd97j0457andc6m0gh5asw7kqd2')\n        self.assertEqual(w.get_change_addresses()[0], 'bc1qd4q50nft7kxm9yglfnpup9ed2ukj3tkxp793y0zya8dc9m39jcwq308dxz')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_bip39_seed_bip44_standard(self, mock_save_db):\n        seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial'\n        self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))\n\n        root_seed = keystore.bip39_to_seed(seed_words, passphrase='')\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/44'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'xprv9zGLcNEb3cHUKizLVBz6RYeE9bEZAVPjH2pD1DEzCnPcsemWc3d3xTao8sfhfUmDLMq6e3RcEMEvJG1Et8dvfL8DV4h7mwm9J6AJsW9WXQD')\n        self.assertEqual(ks.xpub, 'xpub6DFh1smUsyqmYD4obDX6ngaxhd53Zx7aeFjoobebm7vbkT6f9awJWFuGzBT9FQJEWFBL7UyhMXtYzRcwDuVbcxtv9Ce2W9eMm4KXLdvdbjv')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2pkh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '16j7Dqk3Z9DdTdBtHcCVLaNQy9MTgywUUo')\n        self.assertEqual(w.get_change_addresses()[0], '1GG5bVeWgAp5XW7JLCphse14QaC4qiHyWn')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_bip39_seed_bip44_standard_passphrase(self, mock_save_db):\n        seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial'\n        self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))\n\n        root_seed = keystore.bip39_to_seed(seed_words, passphrase=UNICODE_HORROR)\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/44'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'xprv9z8izheguGnLopSqkY7GcGFrP2Gu6rzBvvHo6uB9B8DWJhsows6WDZAsbBTaP3ncP2AVbTQphyEQkahrB9s1L7ihZtfz5WGQPMbXwsUtSik')\n        self.assertEqual(ks.xpub, 'xpub6D85QDBajeLe2JXJrZeGyQCaw47PWKi3J9DPuHakjTkVBWCxVQQkmMVMSSfnw39tj9FntbozpRtb1AJ8ubjeVSBhyK4M5mzdvsXZzKPwodT')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2pkh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '1F88g2naBMhDB7pYFttPWGQgryba3hPevM')\n        self.assertEqual(w.get_change_addresses()[0], '1H4QD1rg2zQJ4UjuAVJr5eW1fEM8WMqyxh')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_bip39_seed_bip49_p2sh_segwit(self, mock_save_db):\n        seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial'\n        self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))\n\n        root_seed = keystore.bip39_to_seed(seed_words, passphrase='')\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/49'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'yprvAJEYHeNEPcyBoQYM7sGCxDiNCTX65u4ANgZuSGTrKN5YCC9MP84SBayrgaMyZV7zvkHrr3HVPTK853s2SPk4EttPazBZBmz6QfDkXeE8Zr7')\n        self.assertEqual(ks.xpub, 'ypub6XDth9u8DzXV1tcpDtoDKMf6kVMaVMn1juVWEesTshcX4zUVvfNgjPJLXrD9N7AdTLnbHFL64KmBn3SNaTe69iZYbYCqLCCNPZKbLz9niQ4')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2wpkh-p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '35ohQTdNykjkF1Mn9nAVEFjupyAtsPAK1W')\n        self.assertEqual(w.get_change_addresses()[0], '3KaBTcviBLEJajTEMstsA2GWjYoPzPK7Y7')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_bip39_seed_bip84_native_segwit(self, mock_save_db):\n        # test case from bip84\n        seed_words = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'\n        self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))\n\n        root_seed = keystore.bip39_to_seed(seed_words, passphrase='')\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/84'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'zprvAdG4iTXWBoARxkkzNpNh8r6Qag3irQB8PzEMkAFeTRXxHpbF9z4QgEvBRmfvqWvGp42t42nvgGpNgYSJA9iefm1yYNZKEm7z6qUWCroSQnE')\n        self.assertEqual(ks.xpub, 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2wpkh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu')\n        self.assertEqual(w.get_change_addresses()[0], 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_electrum_multisig_seed_standard(self, mock_save_db):\n        seed_words = 'blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure'\n        self.assertEqual(calc_seed_type(seed_words), 'standard')\n\n        ks1 = keystore.from_seed(seed_words, passphrase='', for_multisig=True)\n        WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks1)\n        self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))\n        self.assertEqual(ks1.xprv, 'xprv9s21ZrQH143K3t9vo23J3hajRbzvkRLJ6Y1zFrUFAfU3t8oooMPfb7f87cn5KntgqZs5nipZkCiBFo5ZtaSD2eDo7j7CMuFV8Zu6GYLTpY6')\n        self.assertEqual(ks1.xpub, 'xpub661MyMwAqRbcGNEPu3aJQqXTydqR9t49Tkwb4Esrj112kw8xLthv8uybxvaki4Ygt9xiwZUQGeFTG7T2TUzR3eA4Zp3aq5RXsABHFBUrq4c')\n\n        # electrum seed: ghost into match ivory badge robot record tackle radar elbow traffic loud\n        ks2 = keystore.from_xpub('xpub661MyMwAqRbcGfCPEkkyo5WmcrhTq8mi3xuBS7VEZ3LYvsgY1cCFDbenT33bdD12axvrmXhuX3xkAbKci3yZY9ZEk8vhLic7KNhLjqdh5ec')\n        WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2)\n        self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))\n\n        w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2', config=self.config)\n        self.assertEqual(w.txin_type, 'p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '32ji3QkAgXNz6oFoRfakyD3ys1XXiERQYN')\n        self.assertEqual(w.get_change_addresses()[0], '36XWwEHrrVCLnhjK5MrVVGmUHghr9oWTN1')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_electrum_multisig_seed_segwit(self, mock_save_db):\n        seed_words = 'snow nest raise royal more walk demise rotate smooth spirit canyon gun'\n        self.assertEqual(calc_seed_type(seed_words), 'segwit')\n\n        ks1 = keystore.from_seed(seed_words, passphrase='', for_multisig=True)\n        WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks1)\n        self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))\n        self.assertEqual(ks1.xprv, 'ZprvAjxLRqPiDfPDxXrm8JvcoCGRAW6xUtktucG6AMtdzaEbTEJN8qcECvujfhtDU3jLJ9g3Dr3Gz5m1ypfMs8iSUh62gWyHZ73bYLRWyeHf6y4')\n        self.assertEqual(ks1.xpub, 'Zpub6xwgqLvc42wXB1wEELTdALD9iXwStMUkGqBgxkJFYumaL2dWgNvUkjEDWyDFZD3fZuDWDzd1KQJ4NwVHS7hs6H6QkpNYSShfNiUZsgMdtNg')\n\n        # electrum seed: hedgehog sunset update estate number jungle amount piano friend donate upper wool\n        ks2 = keystore.from_xpub('Zpub6y4oYeETXAbzLNg45wcFDGwEG3vpgsyMJybiAfi2pJtNF3i3fJVxK2BeZJaw7VeKZm192QHvXP3uHDNpNmNDbQft9FiMzkKUhNXQafUMYUY')\n        WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2)\n        self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))\n\n        w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2', config=self.config)\n        self.assertEqual(w.txin_type, 'p2wsh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], 'bc1qvzezdcv6vs5h45ugkavp896e0nde5c5lg5h0fwe2xyfhnpkxq6gq7pnwlc')\n        self.assertEqual(w.get_change_addresses()[0], 'bc1qxqf840dqswcmu7a8v82fj6ej0msx08flvuy6kngr7axstjcaq6us9hrehd')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_bip39_multisig_seed_bip45_standard(self, mock_save_db):\n        seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial'\n        self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))\n\n        root_seed = keystore.bip39_to_seed(seed_words, passphrase='')\n        ks1 = keystore.from_bip43_rootseed(root_seed, derivation=\"m/45'/0\")\n        self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))\n        self.assertEqual(ks1.xprv, 'xprv9vyEFyXf7pYVv4eDU3hhuCEAHPHNGuxX73nwtYdpbLcqwJCPwFKknAK8pHWuHHBirCzAPDZ7UJHrYdhLfn1NkGp9rk3rVz2aEqrT93qKRD9')\n        self.assertEqual(ks1.xpub, 'xpub69xafV4YxC6o8Yiga5EiGLAtqR7rgNgNUGiYgw3S9g9pp6XYUne1KxdcfYtxwmA3eBrzMFuYcNQKfqsXCygCo4GxQFHfywxpUbKNfYvGJka')\n\n        # bip39 seed: tray machine cook badge night page project uncover ritual toward person enact\n        # der: m/45'/0\n        ks2 = keystore.from_xpub('xpub6B26nSWddbWv7J3qQn9FbwPPQktSBdPQfLfHhRK4375QoZq8fvM8rQey1koGSTxC5xVoMzNMaBETMUmCqmXzjc8HyAbN7LqrvE4ovGRwNGg')\n        WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2)\n        self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))\n\n        w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2', config=self.config)\n        self.assertEqual(w.txin_type, 'p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '3JPTQ2nitVxXBJ1yhMeDwH6q417UifE3bN')\n        self.assertEqual(w.get_change_addresses()[0], '3FGyDuxgUDn2pSZe5xAJH1yUwSdhzDMyEE')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_bip39_multisig_seed_p2sh_segwit(self, mock_save_db):\n        # bip39 seed: pulse mixture jazz invite dune enrich minor weapon mosquito flight fly vapor\n        # der: m/49'/0'/0'\n        # NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh\n        ks1 = keystore.from_xprv('YprvAUXFReVvDjrPerocC3FxVH748sJUTvYjkAhtKop5VnnzVzMEHr1CHrYQKZwfJn1As3X4LYMav6upxd5nDiLb6SCjRZrBH76EFvyQAG4cn79')\n        self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))\n        self.assertEqual(ks1.xpub, 'Ypub6hWbqA2p47QgsLt5J4nxrR3ngu8xsPGb7PdV8CDh48KyNngNqPKSqertAqYhQ4umELu1UsZUCYfj9XPA6AdSMZWDZQobwF7EJ8uNrECaZg1')\n\n        # bip39 seed: slab mixture skin evoke harsh tattoo rare crew sphere extend balcony frost\n        # der: m/49'/0'/0'\n        ks2 = keystore.from_xpub('Ypub6iNDhL4WWq5kFZcdFqHHwX4YTH4rYGp8xbndpRrY7WNZFFRfogSrL7wRTajmVHgR46AT1cqUG1mrcRd7h1WXwBsgX2QvT3zFbBCDiSDLkau')\n        WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2)\n        self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))\n\n        w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2', config=self.config)\n        self.assertEqual(w.txin_type, 'p2wsh-p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '35LeC45QgCVeRor1tJD6LiDgPbybBXisns')\n        self.assertEqual(w.get_change_addresses()[0], '39RhtDchc6igmx5tyoimhojFL1ZbQBrXa6')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_bip32_extended_version_bytes(self, mock_save_db):\n        seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant'\n        self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))\n        bip32_seed = keystore.bip39_to_seed(seed_words, passphrase='')\n        self.assertEqual('0df68c16e522eea9c1d8e090cfb2139c3b3a2abed78cbcb3e20be2c29185d3b8df4e8ce4e52a1206a688aeb88bfee249585b41a7444673d1f16c0d45755fa8b9',\n                         bip32_seed.hex())\n\n        def create_keystore_from_bip32seed(xtype):\n            ks = keystore.BIP32_KeyStore({})\n            ks.add_xprv_from_seed(bip32_seed, xtype=xtype, derivation='m/')\n            return ks\n\n        ks = create_keystore_from_bip32seed(xtype='standard')\n        self.assertEqual('033a05ec7ae9a9833b0696eb285a762f17379fa208b3dc28df1c501cf84fe415d0', ks.derive_pubkey(0, 0).hex())\n        self.assertEqual('02bf27f41683d84183e4e930e66d64fc8af5508b4b5bf3c473c505e4dbddaeed80', ks.derive_pubkey(1, 0).hex())\n\n        ks = create_keystore_from_bip32seed(xtype='standard')  # p2pkh\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(ks.xprv, 'xprv9s21ZrQH143K3nyWMZVjzGL4KKAE1zahmhTHuV5pdw4eK3o3igC5QywgQG7UTRe6TGBniPDpPFWzXMeMUFbBj8uYsfXGjyMmF54wdNt8QBm')\n        self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcGH3yTb2kMQGnsLziRTJZ8vNthsVSCGbdBr8CGDWKxnGAFYgyKTzBtwvPPmfVAWJuFmxRXjSbUTg87wDkWQ5GmzpfUcN9t8Z')\n        self.assertEqual(w.get_receiving_addresses()[0], '19fWEVaXqgJFFn7JYNr6ouxyjZy3uK7CdK')\n        self.assertEqual(w.get_change_addresses()[0], '1EEX7da31qndYyeKdbM665w1ze5gbkkAZZ')\n\n        ks = create_keystore_from_bip32seed(xtype='p2wpkh-p2sh')\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(ks.xprv, 'yprvABrGsX5C9janu6AdBvHNCMRZVHJfxcaCgoyWgsyi1wSXN9cGyLMe33bpRU54TLJ1ruJbTrpNqusYQeFvBx1CXNb9k1DhKtBFWo8b1sLbXhN')\n        self.assertEqual(ks.xpub, 'ypub6QqdH2c5z7967aF6HwpNZVNJ3K9AN5J442u7VGPKaGyWEwwRWsftaqvJGkeZKNe7Jb3C9FG3dAfT94ZzFRrcGhMizGvB6Jtm3itJsEFhxMC')\n        self.assertEqual(w.get_receiving_addresses()[0], '34SAT5gGF5UaBhhSZ8qEuuxYvZ2cm7Zi23')\n        self.assertEqual(w.get_change_addresses()[0], '38unULZaetSGSKvDx7Krukh8zm8NQnxGiA')\n\n        ks = create_keystore_from_bip32seed(xtype='p2wpkh')\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(ks.xprv, 'zprvAWgYBBk7JR8GkPMk2H4zQSX4fFT7uEZhbvVjUGsbPwpQRFRWDzXCf7FxSg2eTEwwGYRQDLQwJaE6HvsUueRDKcGkcLv7unzjnXCEQVWhrF9')\n        self.assertEqual(ks.xpub, 'zpub6jftahH18ngZxsSD8JbzmaToDHHcJhHYy9RLGfHCxHMPJ3kemXqTCuaSHxc9KHJ2iE9ztirc5q212MBYy8Gd4w3KrccbgDiFKSwxFpYKEH6')\n        self.assertEqual(w.get_receiving_addresses()[0], 'bc1qtuynwzd0d6wptvyqmc6ehkm70zcamxpshyzu5e')\n        self.assertEqual(w.get_change_addresses()[0], 'bc1qjy5zunxh6hjysele86qqywfa437z4xwmleq8wk')\n\n        ks = create_keystore_from_bip32seed(xtype='standard')  # p2sh\n        w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1', config=self.config)\n        self.assertEqual(ks.xprv, 'xprv9s21ZrQH143K3nyWMZVjzGL4KKAE1zahmhTHuV5pdw4eK3o3igC5QywgQG7UTRe6TGBniPDpPFWzXMeMUFbBj8uYsfXGjyMmF54wdNt8QBm')\n        self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcGH3yTb2kMQGnsLziRTJZ8vNthsVSCGbdBr8CGDWKxnGAFYgyKTzBtwvPPmfVAWJuFmxRXjSbUTg87wDkWQ5GmzpfUcN9t8Z')\n        self.assertEqual(w.get_receiving_addresses()[0], '3F4nm8Vunb7mxVvqhUP238PYge2hpU5qYv')\n        self.assertEqual(w.get_change_addresses()[0], '3N8jvKGmxzVHENn6B4zTdZt3N9bmRKjj96')\n\n        ks = create_keystore_from_bip32seed(xtype='p2wsh-p2sh')\n        w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1', config=self.config)\n        self.assertEqual(ks.xprv, 'YprvANkMzkodih9AKfL18akM2RmND5LwAyFo15dBc9FFPiGvzLBBjjjv8ATkEB2Y1mWv6NNaLSpVj8G3XosgVBA9frhpaUL6jHeFQXQTbqVPcv2')\n        self.assertEqual(ks.xpub, 'Ypub6bjiQGLXZ4hTY9QUEcHMPZi6m7BRaRyeNJYnQXerx3ous8WLHH4AfxnE5Tc2sos1Y47B1qGAWP3xGEBkYf1ZRBUPpk2aViMkwTABT6qoiBb')\n        self.assertEqual(w.get_receiving_addresses()[0], '3L1BxLLASGKE3DR1ruraWm3hZshGCKqcJx')\n        self.assertEqual(w.get_change_addresses()[0], '3NDGcbZVXTpaQWRhiuVPpXsNt4g2JiCX4E')\n\n        ks = create_keystore_from_bip32seed(xtype='p2wsh')\n        w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1', config=self.config)\n        self.assertEqual(ks.xprv, 'ZprvAhadJRUYsNgeAxX7xwXyEWrsP3VP7bFHvC9QPY98miep3RzQzPuUkE7tFNz81gAqW1VP5vR4BncbR6VFCsaAU6PRSp2XKCTjgFU6zRpk6Xp')\n        self.assertEqual(ks.xpub, 'Zpub6vZyhw1ShkEwPSbb4y4ybeobw5KsX3y9HR51BvYkL4BnvEKZXwDjJ2SN6fZcsiWvwhDymJriy3QW9WoKGMRaDR9zh5j15dBFDBDpqjK1ekQ')\n        self.assertEqual(w.get_receiving_addresses()[0], 'bc1q84x0yrztvcjg88qef4d6978zccxulcmc9y88xcg4ghjdau999x7q7zv2qe')\n        self.assertEqual(w.get_change_addresses()[0], 'bc1q0fj5mra96hhnum80kllklc52zqn6kppt3hyzr49yhr3ecr42z3tsrkg3gs')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_slip39_non_extendable_basic_3of6_bip44_standard(self, mock_save_db):\n        \"\"\"\n        BIP32 Root Key for passphrase \"TREZOR\":\n        xprv9s21ZrQH143K2pMWi8jrTawHaj16uKk4CSbvo4Zt61tcrmuUDMx2o1Byzcr3saXNGNvHP8zZgXVdJHsXVdzYFPavxvCyaGyGr1WkAYG83ce\n        \"\"\"\n        mnemonics = [\n            \"extra extend academic bishop cricket bundle tofu goat apart victim enlarge program behavior permit course armed jerky faint language modern\",\n            \"extra extend academic acne away best indicate impact square oasis prospect painting voting guest either argue username racism enemy eclipse\",\n            \"extra extend academic arcade born dive legal hush gross briefing talent drug much home firefly toxic analysis idea umbrella slice\",\n        ]\n\n        encrypted_seed = slip39.recover_ems(mnemonics)\n        root_seed = encrypted_seed.decrypt('TREZOR')\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/44'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'xprv9yELEwkzJkSUHXz4hX6iv1SkhKeEhNtgoRDqm8whrymd3f3W2Abdpx6MjRmdEAERNeGauGx1u5djsExCT8qE6e4fGNeetfWtp45rSJu7kNW')\n        self.assertEqual(ks.xpub, 'xpub6CDgeTHt97zmW24XoYdjH9PVFMUj6qcYAe9SZXMKRKJbvTNeZhutNkQqajLyZrQ9DCqdnGenKhBD6UTrT1nHnoLCfFHkdeX8hDsZx1je6b2')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2pkh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '1NomKAUNnbASwbPuGHmkSVmnrJS5tZeVce')\n        self.assertEqual(w.get_change_addresses()[0], '1Aw4wpXsAyEHSgMZqPdyewoAtJqH9Jaso3')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_slip39_non_extendable_basic_2of5_bip49_p2sh_segwit(self, mock_save_db):\n        \"\"\"\n        BIP32 Root Key for passphrase \"TREZOR\":\n        xprv9s21ZrQH143K2o6EXEHpVy8TCYoMmkBnDCCESLdR2ieKwmcNG48ck2XJQY4waS7RUQcXqR9N7HnQbUVEDMWYyREdF1idQqxFHuCfK7fqFni\n        \"\"\"\n        mnemonics = [\n            \"hobo romp academic axis august founder knife legal recover alien expect emphasis loan kitchen involve teacher capture rebuild trial numb spider forward ladle lying voter typical security quantity hawk legs idle leaves gasoline\",\n            \"hobo romp academic agency ancestor industry argue sister scene midst graduate profile numb paid headset airport daisy flame express scene usual welcome quick silent downtown oral critical step remove says rhythm venture aunt\",\n        ]\n\n        encrypted_seed = slip39.recover_ems(mnemonics)\n        root_seed = encrypted_seed.decrypt('TREZOR')\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/49'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'yprvAK7DoEDitppjkdf6LrveZUBjB1SFQ54mTy8pqyb1wDyTjNkzNnFC1PEeGyBLfEAjxv3RmtusmBco7LF5DPxtV94mP7qa8t4dP4mmiDrnZF2')\n        self.assertEqual(ks.xpub, 'ypub6Y6aCjkcjCP2y7jZStTevc8Tj3GjoXncqC4ReMzdVZWScB68vKZSZBZ88ENvuPUXXBBR58JXkuz1UrwLnCFvnFTUEpzu5yQabeYBRyd7Edf')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2wpkh-p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '3GCgNoWWVqVdhBxWxrnWQHgwLtffGSYn7D')\n        self.assertEqual(w.get_change_addresses()[0], '3FVvdRhR7racZhmcvrGAqX9eJoP8Sw3ypp')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_slip39_non_extendable_groups_128bit_bip84_native_segwit(self, mock_save_db):\n        \"\"\"\n        BIP32 Root Key for passphrase \"TREZOR\":\n        xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV\n        \"\"\"\n\n        # SLIP39 shares (128 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6)\n        mnemonics = [\n            \"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice\",\n            \"eraser senior ceramic snake clay various huge numb argue hesitate auction category timber browser greatest hanger petition script leaf pickup\",\n            \"eraser senior ceramic shaft dynamic become junior wrist silver peasant force math alto coal amazing segment yelp velvet image paces\",\n            \"eraser senior ceramic round column hawk trust auction smug shame alive greatest sheriff living perfect corner chest sled fumes adequate\",\n        ]\n\n        encrypted_seed = slip39.recover_ems(mnemonics)\n        root_seed = encrypted_seed.decrypt('TREZOR')\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/84'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'zprvAdskBk5s8FxC4hq9PVU1nSRRzotSzUy9vTwv5hscqr3ANM52mtJJT5cdfHTJnfd2cPFKWXpm4WhB9ruQCEC8KWkSeziMEZjbheNp4xUUTTG')\n        self.assertEqual(ks.xpub, 'zpub6rs6bFckxdWVHBucVX129aNAYqiwPwh1HgsWt6HEQBa9F9QBKRcYzsw7WZR7rPSCWKmRVTUaEgrGrHStx2LSTpbgAEerbnrh4XxkRXbUUZF')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2wpkh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], 'bc1qaggygkqgqjjpt58zrmhvjz5m9dj8mjshw0lpgu')\n        self.assertEqual(w.get_change_addresses()[0], 'bc1q8l6hcvlczu4mtjcnlwhczw7vdxnvwccpjl3cwz')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_slip39_non_extendable_groups_256bit_bip49_p2sh_segwit(self, mock_save_db):\n        \"\"\"\n        BIP32 Root Key for passphrase \"TREZOR\":\n        xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c\n        \"\"\"\n\n        # SLIP39 shares (256 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6):\n        mnemonics = [\n            \"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium\",\n            \"wildlife deal acrobat romp anxiety axis starting require metric flexible geology game drove editor edge screw helpful have huge holy making pitch unknown carve holiday numb glasses survive already tenant adapt goat fangs\",\n        ]\n\n        encrypted_seed = slip39.recover_ems(mnemonics)\n        root_seed = encrypted_seed.decrypt('TREZOR')\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/49'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'yprvAHiJ72E8kJU1XQ2adZcUv8Buffr48bik1F3EHCSDDafScwLdfJ5oDgENm1cAAxNPeXMCBxmm7rmyoKua5LfjnrmgxqP5sYtAVDYngxF2zsB')\n        self.assertEqual(ks.xpub, 'ypub6WheWXm2ag2Jjt73jb9VHG8eDhgYY4SbNTxq5aqpmvCRVjfnCqQ3mUYrcGiBR5qvbhJap5hjSiN2eoXBFLGuipWLRAgf11bRThSJLoGrBag')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2wpkh-p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '3FoqkcrEHgkKQ3iXStantygCetRGSRMMNE')\n        self.assertEqual(w.get_change_addresses()[0], '32tvTmBLfLofu8ps4SWpUJC4fS699jiWvC')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_slip39_extendable_basic_3of6_bip44_standard(self, mock_save_db):\n        \"\"\"\n        BIP32 Root Key for passphrase \"TREZOR\":\n        xprv9yba7duYBT5g7SbaN1oCX43xeDtjKXNUZ2uSmJ3efHsWYaLkqzdjg2bjLYYzQ9rmXdNzDHYWXv5m9aBCqbFbZzAoGcAceH1K8cPYVDpsJLH\n        \"\"\"\n        mnemonics = [\n            \"judicial dramatic academic agree craft physics memory born prize academic black listen elder station premium dance sympathy flip always kitchen\",\n            \"judicial dramatic academic arcade clogs timber taught recover burning judicial desktop square ecology budget nervous overall tidy knife fused knit\",\n            \"judicial dramatic academic axle destroy justice username elegant filter seafood device ranked behavior pecan infant lunar answer identify hour enjoy\",\n        ]\n\n        encrypted_seed = slip39.recover_ems(mnemonics)\n        root_seed = encrypted_seed.decrypt('TREZOR')\n        self.assertEqual(\"255415e2b20ad13cef7adca1e336eaec\", root_seed.hex())\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/44'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'xprv9yba7duYBT5g7SbaN1oCX43xeDtjKXNUZ2uSmJ3efHsWYaLkqzdjg2bjLYYzQ9rmXdNzDHYWXv5m9aBCqbFbZzAoGcAceH1K8cPYVDpsJLH')\n        self.assertEqual(ks.xpub, 'xpub6CavX9SS1pdyKvg3U3LCtBzhCFjDiz6KvFq3ZgTGDdQVRNfuPXwzDpvDBqbg1kEsDgEeHo6uWeYsZWALRejoJMVCq4rprrHkbw8Jyu3uaMb')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2pkh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '1N4hqJRTVqUbwT5WCbbsQSwKRPPPzG1TSo')\n        self.assertEqual(w.get_change_addresses()[0], '1FW3QQzbYRSUoNDDYGWPvSCoom8fBhPC9k')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_slip39_extendable_basic_2of5_bip49_p2sh_segwit(self, mock_save_db):\n        \"\"\"\n        BIP32 Root Key for passphrase \"TREZOR\":\n        yprvAJP391MZiYGpkDnSkAfHBGrEKNxpkFVbx9hap59M2hxD1i7kmnaBUC2yo8tzz5AwxSv3ekJRrSGYWA8ec7XmQGLvX4xkWwCRqiadT5fuTfh\n        \"\"\"\n        mnemonics = [\n            \"station type academic acid away gather venture pupal speak treat ruler pecan soldier cowboy paces wavy review similar born moment\",\n            \"station type academic aquatic bundle mineral twice temple miracle ruin earth olympic system dining inform alive branch false easy manual\",\n        ]\n\n        encrypted_seed = slip39.recover_ems(mnemonics)\n        root_seed = encrypted_seed.decrypt('TREZOR')\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/49'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'yprvAJP391MZiYGpkDnSkAfHBGrEKNxpkFVbx9hap59M2hxD1i7kmnaBUC2yo8tzz5AwxSv3ekJRrSGYWA8ec7XmQGLvX4xkWwCRqiadT5fuTfh')\n        self.assertEqual(ks.xpub, 'ypub6XNPYWtTYuq7xhrurCCHYQnxsQoK9iDTKNdBcTYxb3VBtWSuKKtS1zMTeQTDeVe1Y8mzGue1oDYyvjczspnPznLmyruzxVTU785W2QpbTW9')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2wpkh-p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '38diDMcH7japAtpJjVKviBroQfTdvgpdqX')\n        self.assertEqual(w.get_change_addresses()[0], '36Hd2PnEvJpN9pUdhpZWh3aQccbRp46FVc')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_slip39_extendable_groups_128bit_bip84_native_segwit(self, mock_save_db):\n        \"\"\"\n        BIP32 Root Key for passphrase \"TREZOR\":\n        zprvAe6okUFoH5tieuTJJxN84xjPCvWkhFiiP87myHqTNmfux4wY8XnLG7DxezL5Dt2jXu5FrsMc4wEPhAJovAGhH1cAPjmkhh3KcSCMRyuQghd\n        \"\"\"\n\n        # SLIP39 shares (128 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6)\n        mnemonics = [\n            \"fact else acrobat romp analysis usher havoc vitamins analysis garden prevent romantic silent dramatic adjust priority mailman plains vintage else\",\n            \"fact else ceramic round craft lips snake faint adorn square bucket deadline violence guitar greatest academic stadium snake frequent memory\",\n            \"fact else ceramic scatter counter remove club forbid busy cause taxi forecast prayer uncover living type training forward software pumps\",\n            \"fact else ceramic shaft clock crowd detect cleanup wildlife depict include trip profile isolate express category wealthy advance garden mixture\",\n        ]\n\n        encrypted_seed = slip39.recover_ems(mnemonics)\n        root_seed = encrypted_seed.decrypt('TREZOR')\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/84'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'zprvAe6okUFoH5tieuTJJxN84xjPCvWkhFiiP87myHqTNmfux4wY8XnLG7DxezL5Dt2jXu5FrsMc4wEPhAJovAGhH1cAPjmkhh3KcSCMRyuQghd')\n        self.assertEqual(ks.xpub, 'zpub6s6A9ynh7TT1sPXmQyu8S6g7kxMF6iSZkM3NmgF4w7CtpsGgg56aouYSWHgAoMy186a8FRT8zkmhcwV5SWKFFQfMpvV8C9Ft4woWSzD5sXz')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2wpkh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], 'bc1qs2svwhfz47qv9qju2waa6prxzv5f522fc4p06t')\n        self.assertEqual(w.get_change_addresses()[0], 'bc1qmjq5nenac3vjwltldk5qsq4yd8mttw2dpkmx06')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_slip39_extendable_groups_256bit_bip49_p2sh_segwit(self, mock_save_db):\n        \"\"\"\n        BIP32 Root Key for passphrase \"TREZOR\":\n        yprvAJbhup8ey3hmPhgVsXKySTS54BfywUZR6SvQ2jrjdsUgNd4P8B5HR7ute93zXVTXKUvrmvnav1spLzEkDuT7Cy3bf3hWtYoH6A5p8vNzbEC\n        \"\"\"\n\n        # SLIP39 shares (256 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6):\n        mnemonics = [\n            \"smart surprise acrobat romp deal omit pupal capacity invasion should glen smear segment frost surprise ancestor plan frost cultural herd\",\n            \"smart surprise beard romp closet antenna pencil rapids goat artwork race industry segment parcel briefing glad voice camera priority satoshi\",\n        ]\n\n        encrypted_seed = slip39.recover_ems(mnemonics)\n        root_seed = encrypted_seed.decrypt('TREZOR')\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/49'/0'/0'\")\n\n        self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))\n\n        self.assertEqual(ks.xprv, 'yprvAJbhup8ey3hmPhgVsXKySTS54BfywUZR6SvQ2jrjdsUgNd4P8B5HR7ute93zXVTXKUvrmvnav1spLzEkDuT7Cy3bf3hWtYoH6A5p8vNzbEC')\n        self.assertEqual(ks.xpub, 'ypub6Xb4KKfYoRG4cBkxyYryobNocDWULwHGTfqzq8GMCD1fFRPXfiPXxvENVQYVbi64BJzdPnPUiJ4iY37X5BA594dqxyE4FwccHdhydU9RhPJ')\n\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(w.txin_type, 'p2wpkh-p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '3JDN4wF5BphZqcJFFYuDA7N1apzfPYyJLG')\n        self.assertEqual(w.get_change_addresses()[0], '3J8zNvhJndqzBcuPuarzUn1kWs9N4ZY7HS')\n\nclass TestWalletKeystoreAddressIntegrityForTestnet(ElectrumTestCase):\n    TESTNET = True\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_bip39_multisig_seed_p2sh_segwit_testnet(self, mock_save_db):\n        # bip39 seed: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose\n        # der: m/49'/1'/0'\n        # NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh\n        ks1 = keystore.from_xprv('Uprv9BEixD3As2LK5h6G2SNT3cTqbZpsWYPceKTSuVAm1yuSybxSvQz2MV1o8cHTtctQmj4HAenb3eh5YJv4YRZjv35i8fofVnNbs4Dd2B4i5je')\n        self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))\n        self.assertEqual(ks1.xpub, 'Upub5QE5Mia4hPtcJBAj8TuTQkQa9bfMv17U1YP3hsaNaKSRrQHbTxJGuHLGyv3MbKZixuPyjfXGUdbTjE4KwyFcX8YD7PX5ybTDbP11UT8UpZR')\n\n        # bip39 seed: square page wood spy oil story rebel give milk screen slide shuffle\n        # der: m/49'/1'/0'\n        ks2 = keystore.from_xpub('Upub5QRzUGRJuWJe5MxGzwgQAeyJjzcdGTXkkq77w6EfBkCyf5iWppSaZ4caY2MgWcU9LP4a4uE5apUFN4wLoENoe9tpu26mrUxeGsH84dN3JFh')\n        WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2)\n        self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))\n\n        w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2], '2of2', config=self.config)\n        self.assertEqual(w.txin_type, 'p2wsh-p2sh')\n\n        self.assertEqual(w.get_receiving_addresses()[0], '2MzsfTfTGomPRne6TkctMmoDj6LwmVkDrMt')\n        self.assertEqual(w.get_change_addresses()[0], '2NFp9w8tbYYP9Ze2xQpeYBJQjx3gbXymHX7')\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_bip32_extended_version_bytes(self, mock_save_db):\n        seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant'\n        self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))\n        bip32_seed = keystore.bip39_to_seed(seed_words, passphrase='')\n        self.assertEqual('0df68c16e522eea9c1d8e090cfb2139c3b3a2abed78cbcb3e20be2c29185d3b8df4e8ce4e52a1206a688aeb88bfee249585b41a7444673d1f16c0d45755fa8b9',\n                         bip32_seed.hex())\n\n        def create_keystore_from_bip32seed(xtype):\n            ks = keystore.BIP32_KeyStore({})\n            ks.add_xprv_from_seed(bip32_seed, xtype=xtype, derivation='m/')\n            return ks\n\n        ks = create_keystore_from_bip32seed(xtype='standard')\n        self.assertEqual('033a05ec7ae9a9833b0696eb285a762f17379fa208b3dc28df1c501cf84fe415d0', ks.derive_pubkey(0, 0).hex())\n        self.assertEqual('02bf27f41683d84183e4e930e66d64fc8af5508b4b5bf3c473c505e4dbddaeed80', ks.derive_pubkey(1, 0).hex())\n\n        ks = create_keystore_from_bip32seed(xtype='standard')  # p2pkh\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(ks.xprv, 'tprv8ZgxMBicQKsPecD328MF9ux3dSaSFWci7FNQmuWH7uZ86eY8i3XpvjK8KSH8To2QphiZiUqaYc6nzDC6bTw8YCB9QJjaQL5pAApN4z7vh2B')\n        self.assertEqual(ks.xpub, 'tpubD6NzVbkrYhZ4Y5Epun1qZKcACU6NQqocgYyC4RYaYBMWw8nuLSMR7DvzVamkqxwRgrTJ1MBMhc8wwxT2vbHqMu8RBXy4BvjWMxR5EdZroxE')\n        self.assertEqual(w.get_receiving_addresses()[0], 'mpBTXYfWehjW2tavFwpUdqBJbZZkup13k2')\n        self.assertEqual(w.get_change_addresses()[0], 'mtkUQgf1psDtL67wMAKTv19LrdgPWy6GDQ')\n\n        ks = create_keystore_from_bip32seed(xtype='p2wpkh-p2sh')\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(ks.xprv, 'uprv8tXDerPXZ1QsVuQ9rV8sN13YoQitC8cD2MtdZJQAVuw19kMMxhhPYnyGLeEiThgLELqNTxS91GTLsVofKAM9LRrkGeRzzEuJRtt1Tcostr7')\n        self.assertEqual(ks.xpub, 'upub57Wa4MvRPNyAiPUcxWfsj8zHMSZNbbL4PapEMgon4FTz2YgWWF1e6bHkBvpDKk2Rg2Zy9LsonXFFbv7jNeCZ5kdKWv8UkfcoxpdjJrZuBX6')\n        self.assertEqual(w.get_receiving_addresses()[0], '2MuzNWpcHrXyvPVKzEGT7Xrwp8uEnXXjWnK')\n        self.assertEqual(w.get_change_addresses()[0], '2MzTzY5VcGLwce7YmdEwjXhgQD7LYEKLJTm')\n\n        ks = create_keystore_from_bip32seed(xtype='p2wpkh')\n        w = WalletIntegrityHelper.create_standard_wallet(ks, config=self.config)\n        self.assertEqual(ks.xprv, 'vprv9DMUxX4ShgxMMCbGgqvVa693yNsL8kbhwUQrLhJ3svJtCrAbDMrxArdQMrCJTcLFdyxBDS2hTvotknRE2rmA8fYM8z8Ra9inhcwerEsG6Ev')\n        self.assertEqual(ks.xpub, 'vpub5SLqN2bLY4WeZgfjnsTVwE5nXQhpYDKZJhLT95hfSFqs5eVjkuBCiewtD8moKegM5fgmtpUNFBboVCjJ6LcZszJvPFpuLaSJEYhNhUAnrCS')\n        self.assertEqual(w.get_receiving_addresses()[0], 'tb1qtuynwzd0d6wptvyqmc6ehkm70zcamxpsaze002')\n        self.assertEqual(w.get_change_addresses()[0], 'tb1qjy5zunxh6hjysele86qqywfa437z4xwm4lm549')\n\n        ks = create_keystore_from_bip32seed(xtype='standard')  # p2sh\n        w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1', config=self.config)\n        self.assertEqual(ks.xprv, 'tprv8ZgxMBicQKsPecD328MF9ux3dSaSFWci7FNQmuWH7uZ86eY8i3XpvjK8KSH8To2QphiZiUqaYc6nzDC6bTw8YCB9QJjaQL5pAApN4z7vh2B')\n        self.assertEqual(ks.xpub, 'tpubD6NzVbkrYhZ4Y5Epun1qZKcACU6NQqocgYyC4RYaYBMWw8nuLSMR7DvzVamkqxwRgrTJ1MBMhc8wwxT2vbHqMu8RBXy4BvjWMxR5EdZroxE')\n        self.assertEqual(w.get_receiving_addresses()[0], '2N6czpsRwQ3d8AHZPNbztf5NotzEsaZmVQ8')\n        self.assertEqual(w.get_change_addresses()[0], '2NDgwz4CoaSzdSAQdrCcLFWsJaVowCNgiPA')\n\n        ks = create_keystore_from_bip32seed(xtype='p2wsh-p2sh')\n        w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1', config=self.config)\n        self.assertEqual(ks.xprv, 'Uprv95RJn67y7xyEvUZXo9brC5PMXCm9QVHoLdYJUZfhsgmQmvvGj75fduqC9MCC28uETouMLYSFtUqqzfRRcPW6UuyR77YQPeNJKd9t3XutF8b')\n        self.assertEqual(ks.xpub, 'Upub5JQfBberxLXY8xdzuB8rZDL65Ebdox1ehrTuGx5KS2JPejFRGePvBi9fzdmgtBFKuVdx1vsvfjdkj5jVfsMWEEjzMPEtA55orYubtrCZmRr')\n        self.assertEqual(w.get_receiving_addresses()[0], '2NBZQ25GC3ipaF13ZY3UT8i2xnDuS17pJqx')\n        self.assertEqual(w.get_change_addresses()[0], '2NDmUgLVX8vKvcJ4FQ37GSUre6QtBzKkb6k')\n\n        ks = create_keystore_from_bip32seed(xtype='p2wsh')\n        w = WalletIntegrityHelper.create_multisig_wallet([ks], '1of1', config=self.config)\n        self.assertEqual(ks.xprv, 'Vprv16YtLrHXxePM6noKqtFtMtmUgBE9bEpF3fPLmpvuPksssLostujtdHBwqhEeVuzESz22UY8hyPx9ed684SQpCmUKSVhpxPFbvVNY7qnviNR')\n        self.assertEqual(ks.xpub, 'Vpub5dEvVGKn7251zFq7jXvUmJRbFCk5ka19cxz84LyCp2gGhq4eXJZUomop1qjGt5uFK8kkmQUV8PzJcNM4PZmX2URbDiwJjyuJ8GyFHRrEmmG')\n        self.assertEqual(w.get_receiving_addresses()[0], 'tb1q84x0yrztvcjg88qef4d6978zccxulcmc9y88xcg4ghjdau999x7qf2696k')\n        self.assertEqual(w.get_change_addresses()[0], 'tb1q0fj5mra96hhnum80kllklc52zqn6kppt3hyzr49yhr3ecr42z3ts5777jl')\n\n\nclass TestWalletSending(ElectrumTestCase):\n    TESTNET = True\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n    def create_standard_wallet_from_seed(self, seed_words, *, config=None, gap_limit=2):\n        if config is None:\n            config = self.config\n        ks = keystore.from_seed(seed_words, passphrase='', for_multisig=False)\n        return WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=gap_limit, config=config)\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_save_db):\n        wallet1 = self.create_standard_wallet_from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver')\n        wallet2 = self.create_standard_wallet_from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song')\n\n        # bootstrap wallet1\n        funding_tx = Transaction('01000000014576dacce264c24d81887642b726f5d64aa7825b21b350c7b75a57f337da6845010000006b483045022100a3f8b6155c71a98ad9986edd6161b20d24fad99b6463c23b463856c0ee54826d02200f606017fd987696ebbe5200daedde922eee264325a184d5bbda965ba5160821012102e5c473c051dae31043c335266d0ef89c1daab2f34d885cc7706b267f3269c609ffffffff0240420f00000000001600148a28bddb7f61864bdcf58b2ad13d5aeb3abc3c42a2ddb90e000000001976a914c384950342cb6f8df55175b48586838b03130fad88ac00000000')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 1000000\n        self.assertEqual('add2535aedcbb5ba79cc2260868bb9e57f328738ca192937f2c92e0e94c19203', funding_txid)\n        wallet1.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet1 -> wallet2\n        outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 250000)]\n        tx = wallet1.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)\n        wallet1.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet1.is_mine(wallet1.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('010000000001010392c1940e2ec9f2372919ca3887327fe5b98b866022cc79bab5cbed5a53d2ad0000000000feffffff0290d00300000000001976a914ea7804a2c266063572cc009a63dc25dcc0e9d9b588ac285e0b0000000000160014690b59a8140602fb23cc2904ece9cc4daf361052024730440220608a5339ca894592da82119e1e4a1d09335d70a552c683687223b8ed724465e902201b3f0feccf391b1b6257e4b18970ae57d7ca060af2dae519b3690baad2b2a34e0121030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c00000000',\n                         str(tx_copy))\n        self.assertEqual('3c06ae4d9be8226a472b3e7f7c127c7e3016f525d658d26106b80b4c7e3228e2', tx_copy.txid())\n        self.assertEqual('d8d930ae91dce73118c3fffabbdfcfb87f5d91673fb4c7dfd0fbe7cf03bf426b', tx_copy.wtxid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n\n        wallet1.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)  # TX_HEIGHT_UNCONF_PARENT but nvm\n        wallet2.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet2 -> wallet1\n        outputs = [PartialTxOutput.from_address_and_value(wallet1.get_receiving_address(), 100000)]\n        tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)\n        wallet2.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('0100000001e228327e4c0bb80661d258d625f516307e7c127c7f3e2b476a22e89b4dae063c000000006a47304402200c7b06ff882db5ffe9d6e2a3cc2cabf5cd1b4224f1453d1e3dadd13b3d391e2c02201d23fde8482b05837f27d43021d17a1be2ee619dfc889ee80d4c2761e7c7ffb20121030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cffeffffff02a086010000000000160014284520c815980d426264766d8d930013dd20aa6068360200000000001976a914ca4c60999c46c2108326590b125aefd476dcb11888ac00000000',\n                         str(tx_copy))\n        self.assertEqual('4ff22c31dd884dedbb905fae275508d1f7bb4948c1c979d2567132848fdff24a', tx_copy.txid())\n        self.assertEqual('4ff22c31dd884dedbb905fae275508d1f7bb4948c1c979d2567132848fdff24a', tx_copy.wtxid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n\n        wallet1.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        wallet2.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet level checks\n        self.assertEqual((0, funding_output_value - 250000 - 5000 + 100000, 0), wallet1.get_balance())\n        self.assertEqual((0, 250000 - 5000 - 100000, 0), wallet2.get_balance())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_save_db):\n        wallet1a = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure', passphrase='', for_multisig=True),\n                keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'),\n                keystore.from_xpub('tpubD6NzVbkrYhZ4XJzYkhsCbDCcZRmDAKSD7bXi9mdCni7acVt45fxbTVZyU6jRGh29ULKTjoapkfFsSJvQHitcVKbQgzgkkYsAmaovcro7Mhf')\n            ],\n            '2of3', gap_limit=2,\n            config=self.config\n        )\n        wallet1b = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song', passphrase='', for_multisig=True),\n                keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'),\n                keystore.from_xpub('tpubD6NzVbkrYhZ4YARFMEZPckrqJkw59GZD1PXtQnw14ukvWDofR7Z1HMeSCxfYEZVvg4VdZ8zGok5VxHwdrLqew5cMdQntWc5mT7mh1CSgrnX')\n            ],\n            '2of3', gap_limit=2,\n            config=self.config\n        )\n        # ^ third seed: ghost into match ivory badge robot record tackle radar elbow traffic loud\n        wallet2 = self.create_standard_wallet_from_seed('powerful random nobody notice nothing important anyway look away hidden message over')\n\n        # bootstrap wallet1\n        funding_tx = Transaction('010000000001014121f99dc02f0364d2dab3d08905ff4c36fc76c55437fd90b769c35cc18618280100000000fdffffff02d4c22d00000000001600143fd1bc5d32245850c8cb5be5b09c73ccbb9a0f75001bb7000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887024830450221008781c78df0c9d4b5ea057333195d5d76bc29494d773f14fa80e27d2f288b2c360220762531614799b6f0fb8d539b18cb5232ab4253dd4385435157b28a44ff63810d0121033de77d21926e09efd04047ae2d39dbd3fb9db446e8b7ed53e0f70f9c9478f735dac11300')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 12000000\n        self.assertEqual('b25cd55687c9e528c2cfd546054f35fb6741f7cf32d600f07dfecdf2e1d42071', funding_txid)\n        wallet1a.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet1 -> wallet2\n        outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 370000)]\n        tx = wallet1a.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)\n        wallet1a.sign_transaction(tx, password=None)\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff01007501000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb20100000000feffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000000100e0010000000001014121f99dc02f0364d2dab3d08905ff4c36fc76c55437fd90b769c35cc18618280100000000fdffffff02d4c22d00000000001600143fd1bc5d32245850c8cb5be5b09c73ccbb9a0f75001bb7000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887024830450221008781c78df0c9d4b5ea057333195d5d76bc29494d773f14fa80e27d2f288b2c360220762531614799b6f0fb8d539b18cb5232ab4253dd4385435157b28a44ff63810d0121033de77d21926e09efd04047ae2d39dbd3fb9db446e8b7ed53e0f70f9c9478f735dac11300220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f284730440220751ee3599e59debb8b2aeef61bb5f574f26379cd961caf382d711a507bc632390220598d53e62557c4a5ab8cfb2f8948f37cca06a861714b55c781baf2c3d7a580b501010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000000100695221022ec6f62b0f3b7c2446f44346bff0a6f06b5fdbc27368be8a36478e0287fe47be21024238f21f90527dc87e945f389f3d1711943b06f0a738d5baab573fc0ab6c98582102b7139e93747d7c77f62af5a38b8a2b009f3456aa94dea9bf21f73a6298c867a253ae2202022ec6f62b0f3b7c2446f44346bff0a6f06b5fdbc27368be8a36478e0287fe47be0cdb69242701000000000000002202024238f21f90527dc87e945f389f3d1711943b06f0a738d5baab573fc0ab6c98580c0036e9ac0100000000000000220202b7139e93747d7c77f62af5a38b8a2b009f3456aa94dea9bf21f73a6298c867a20c48adc7a0010000000000000000\",\n                         partial_tx)\n        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n        wallet1b.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('01000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb201000000fc004730440220751ee3599e59debb8b2aeef61bb5f574f26379cd961caf382d711a507bc632390220598d53e62557c4a5ab8cfb2f8948f37cca06a861714b55c781baf2c3d7a580b501473044022023b55c679397bdf3a04d545adc6193eabc11b3a28850d3d46049a51a30c6732402205dbfdade5620e9072ae4aa7577c5f0fd294f59a6b0064cc7105093c0fe7a6d24014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefeffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000',\n                         str(tx_copy))\n        self.assertEqual('b508ee1908181e55d2a18a5b2a3904dffbc7cb6b6320bbfba4433578d0f7831e', tx_copy.txid())\n        self.assertEqual('b508ee1908181e55d2a18a5b2a3904dffbc7cb6b6320bbfba4433578d0f7831e', tx_copy.wtxid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n\n        wallet1a.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        wallet2.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet2 -> wallet1\n        outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)]\n        tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)\n        self.assertEqual(\n            \"pkh(045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25ed)\",\n            tx.inputs()[0].script_descriptor.to_string_no_checksum())\n        wallet2.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('01000000011e83f7d0783543a4fbbb20636bcbc7fbdf04392a5b8aa1d2551e180819ee08b5000000008a473044022007569f938b5d7a7f529ceccc413363d84325c11d589c1897660bebfd5fd1cc4302203ef71fa42f9b31bb1e816af13b0bf725c493a0405433390c783cd9374713c5880141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfeffffff02a08601000000000017a914efe136b8275f49bc0f9871eebb9a48d0516229fd87280b0400000000001976a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac00000000',\n                         str(tx_copy))\n        self.assertEqual('30f6eec4db5e6b1dfe572dfbc7077661df9a15a2a1b7701612b906d3e1bee3d8', tx_copy.txid())\n        self.assertEqual('30f6eec4db5e6b1dfe572dfbc7077661df9a15a2a1b7701612b906d3e1bee3d8', tx_copy.wtxid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n\n        wallet1a.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        wallet2.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet level checks\n        self.assertEqual((0, funding_output_value - 370000 - 5000 + 100000, 0), wallet1a.get_balance())\n        self.assertEqual((0, 370000 - 5000 - 100000, 0), wallet2.get_balance())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db):\n        wallet1a = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', passphrase='', for_multisig=True),\n                keystore.from_xpub('Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf'),\n                keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra')\n            ],\n            '2of3', gap_limit=2,\n            config=self.config\n        )\n        wallet1b = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('snow nest raise royal more walk demise rotate smooth spirit canyon gun', passphrase='', for_multisig=True),\n                keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra'),\n                keystore.from_xpub('Vpub5gSKXzxK7FeKQedu2q1z9oJWxqvX72AArW3HSWpEhc8othDH8xMDu28gr7gf17sp492BuJod8Tn7anjvJrKpETwqnQqX7CS8fcYyUtedEMk')\n            ],\n            '2of3', gap_limit=2,\n            config=self.config\n        )\n        # ^ third seed: hedgehog sunset update estate number jungle amount piano friend donate upper wool\n        wallet2a = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                # bip39: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose, der: m/1234'/1'/0', p2wsh-p2sh multisig\n                keystore.from_xprv('Uprv9CvELvByqm8k2dpecJVjgLMX1z5DufEjY4fBC5YvdGF5WjGCa7GVJJ2fYni1tyuF7Hw83E6W2ZBjAhaFLZv2ri3rEsubkCd5avg4EHKoDBN'),\n                keystore.from_xpub('Upub5Qb8ik4Cnu8g97KLXKgVXHqY6tH8emQvqtBncjSKsyfTZuorPtTZgX7ovKKZHuuVGBVd1MTTBkWez1XXt2weN1sWBz6SfgRPQYEkNgz81QF')\n            ],\n            '2of2', gap_limit=2,\n            config=self.config\n        )\n        wallet2b = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                # bip39: square page wood spy oil story rebel give milk screen slide shuffle, der: m/1234'/1'/0', p2wsh-p2sh multisig\n                keystore.from_xprv('Uprv9BbnKEXJxXaNvdEsRJ9VA9toYrSeFJh5UfGBpM2iKe8Uh7UhrM9K8ioL53s8gvCoGfirHHaqpABDAE7VUNw8LNU1DMJKVoWyeNKu9XcDC19'),\n                keystore.from_xpub('Upub5RuakRisg8h3F7u7iL2k3UJFa1uiK7xauHamzTxYBbn4PXbM7eajr6M9Q2VCr6cVGhfhqWQqxnABvtSATuVM1xzxk4nA189jJwzaMn1QX7V')\n            ],\n            '2of2', gap_limit=2,\n            config=self.config\n        )\n\n        # bootstrap wallet1\n        funding_tx = Transaction('01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c11300')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 200000\n        self.assertEqual('d2bd6c9d332db8e2c50aa521cd50f963fba214645aab2f7556e061a412103e21', funding_txid)\n        wallet1a.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet1 -> wallet2\n        outputs = [PartialTxOutput.from_address_and_value(wallet2a.get_receiving_address(), 165000)]\n        tx = wallet1a.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)\n        self.assertEqual((0, 2), tx.signature_count())\n        self.assertEqual(\n            \"wsh(sortedmulti(2,[b2e35a7d/1h]tpubD9aPYLPPYw8MxU3cD57LwpV5v7GomHxdv62MSbPcRkp47zwXx69ACUFsKrj8xzuzRrij9FWVhfvkvNqtqsr8ZtefkDsGZ9GLuHzoS6bXyk1/0/0,[53b77ddb/1h]tpubD8spLJysN7v7V1KHvkZ7AwjnXShKafopi7Vu3Ahs2S46FxBPTode8DgGxDo55k4pJvETGScZFwnM5f2Y31EUjteJdhxR73sjr9ieydgah2U/0/0,[43067d63/1h]tpubD8khd1g1tzFeKeaU59QV811hyvhwn9KDfy5sqFJ5m2wJLw6rUt4AZviqutRPXTUAK4SpU2we3y2WBP916Ma8Em4qFGcbYkFvXVfpGYV3oZR/0/0))\",\n            tx.inputs()[0].script_descriptor.to_string_no_checksum())\n        wallet1a.sign_transaction(tx, password=None)\n        self.assertEqual((1, 2), tx.signature_count())\n        txid = tx.txid()\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff01007e0100000001213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf874387000000000001012b400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0100eb01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c1130022020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf0101056952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22060223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa10b2e35a7d01000080000000000000000022060273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e1053b77ddb010000800000000000000000220602aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9411043067d6301000080000000000000000000010169522102174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a2102c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd52102eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98053ae220202174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a1053b77ddb010000800100000000000000220202c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd51043067d63010000800100000000000000220202eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98010b2e35a7d0100008001000000000000000000\",\n                         partial_tx)\n        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertEqual(txid, tx.txid())\n        self.assertFalse(tx.is_complete())\n        wallet1b.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertEqual((2, 2), tx.signature_count())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('01000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000',\n                         str(tx_copy))\n        self.assertEqual('6e9c3cd8788bdb970a124ea06136d52bc01cec4f9b1e217627d5e90ebe77d049', tx_copy.txid())\n        self.assertEqual('dfd568f4fe0d41f8679b665d2d65e514315bcd5ac3ff63ef1b1596e5313740a3', tx_copy.wtxid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual(txid, tx_copy.txid())\n\n        wallet1a.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        wallet2a.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet2 -> wallet1\n        outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)]\n        tx = wallet2a.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)\n        wallet2a.sign_transaction(tx, password=None)\n        self.assertEqual((1, 2), tx.signature_count())\n        self.assertEqual(\n            \"sh(wsh(sortedmulti(2,[d1dbcc21]tpubDDsv4RpsGViZeEVwivuj3aaKhFQSv1kYsz64mwRoHkqBfw8qBSYEmc8TtyVGotJb44V3pviGzefP9m9hidRg9dPPaDWL2yoRpMW3hdje3Rk/0/0,[17cea914]tpubDCZU2kACPGACYDvAXvZUXQ7cE7msFfCtpah5QCuaz8iarKMLTgR4c2u8RGKdFhbb3YJxzmktDd1rCtF58ksyVgFw28pchY55uwkDiXjY9hU/0/0)))\",\n            tx.inputs()[0].script_descriptor.to_string_no_checksum())\n        txid = tx.txid()\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff01007e010000000149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e0100000000feffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a086010000000000220020f7b6b30c3073ae2680a7e90c589bbfec5303331be68bbab843eed5d51ba012390000000000010120888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870100fd7c0101000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000220202119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb14730440220091ea67af7c1131f51f62fe9596dff0a60c8b45bfc5be675389e193912e8a71802201bf813bbf83933a35ecc46e2d5b0442bd8758fa82e0f8ed16392c10d51f7f7660101042200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163c010547522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae220602119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb10cd1dbcc210000000000000000220602fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab81260c17cea9140000000000000000000100220020717ab7037b81797cb3e192a8a1b4d88083444bbfcd26934cadf3bcf890f14e05010147522102987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde21034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f9952ae220202987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde0c17cea91401000000000000002202034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f990cd1dbcc2101000000000000000000\",\n                         partial_tx)\n        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertEqual(txid, tx.txid())\n        self.assertFalse(tx.is_complete())\n        wallet2b.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertEqual((2, 2), tx.signature_count())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet2a.is_mine(wallet2a.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('0100000000010149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e01000000232200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163cfeffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a086010000000000220020f7b6b30c3073ae2680a7e90c589bbfec5303331be68bbab843eed5d51ba0123904004730440220091ea67af7c1131f51f62fe9596dff0a60c8b45bfc5be675389e193912e8a71802201bf813bbf83933a35ecc46e2d5b0442bd8758fa82e0f8ed16392c10d51f7f7660147304402203ecf75b0316a449dd31bc549251b687dc904194aa551941bd5e8c67603661bdb02204ed58b3a6b070ec138d2127093bebcc6581495818fa611583e1c81cd9b2cf5ee0147522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae00000000',\n                         str(tx_copy))\n        self.assertEqual('df92f0179b2bd4d0845472a8492edcaa3c24883ec4c7816dcd634183e0f89f29', tx_copy.txid())\n        self.assertEqual('614a3c2d908229e5421364b5ac9802eb4636ead08c080cae3c7ca6ba4ad5f3cf', tx_copy.wtxid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual(txid, tx_copy.txid())\n\n        wallet1a.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        wallet2a.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet level checks\n        self.assertEqual((0, funding_output_value - 165000 - 5000 + 100000, 0), wallet1a.get_balance())\n        self.assertEqual((0, 165000 - 5000 - 100000, 0), wallet2a.get_balance())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_save_db):\n        wallet1a = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('phone guilt ancient scan defy gasp off rotate approve ill word exchange', passphrase='', for_multisig=True),\n                keystore.from_xpub('tpubD6NzVbkrYhZ4YPZ3ntVjqSCxiUUv2jikrUBU73Q3iJ7Y8iR41oYf991L5fanv7ciHjbjokdK2bjYqg1BzEUDxucU9qM5WRdBiY738wmgLP4')\n            ],\n            '1of2', gap_limit=2,\n            config=self.config\n        )\n        # ^ second seed: kingdom now gift initial age right velvet exotic harbor enforce kingdom kick\n        wallet2 = WalletIntegrityHelper.create_standard_wallet(\n            # bip39: uniform tank success logic lesson awesome stove elegant regular desert drip device, der: m/49'/1'/0'\n            keystore.from_xprv('uprv91HGbrNZTK4x8u22nbdYGzEuWPxjaHMREUi7CNhY64KsG5ZGnVM99uCa16EMSfrnaPTFxjbRdBZ2WiBkokoM8anzAy3Vpc52o88WPkitnxi'),\n            gap_limit=2,\n            config=self.config\n        )\n\n        # bootstrap wallet1\n        funding_tx = Transaction('010000000001027e20990282eb29588375ad04936e1e991af3bc5b9c6f1ab62eca8c25becaef6a01000000171600140e6a17fadc8bafba830f3467a889f6b211d69a00fdffffff51847fd6bcbdfd1d1ea2c2d95c2d8de1e34c5f2bd9493e88a96a4e229f564e800100000017160014ecdf9fa06856f9643b1a73144bc76c24c67774a6fdffffff021e8501000000000017a91451991bfa68fbcb1e28aa0b1e060b7d24003352e38700093d000000000017a914b0b9f31bace76cdfae2c14abc03e223403d7dc4b870247304402205e19721b92c6afd70cd932acb50815a36ee32ab46a934147d62f02c13aeacf4702207289c4a4131ef86e27058ff70b6cb6bf0e8e81c6cbab6dddd7b0a9bc732960e4012103fe504411c21f7663caa0bbf28931f03fae7e0def7bc54851e0194dfb1e2c85ef02483045022100e969b65096fba4f8b24eb5bc622d2282076241621f3efe922cc2067f7a8a6be702203ec4047dd2a71b9c83eb6a0875a6d66b4d65864637576c06ed029d3d1a8654b0012102bbc8100dca67ba0297aba51296a4184d714204a5fc2eda34708360f37019a3dccfcc1300')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 4000000\n        self.assertEqual('1137c12de4ce0f5b08de8846ba14c0814351a7f0f31457c8ea51a5d4b3c891a3', funding_txid)\n        wallet1a.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet1 -> wallet2\n        outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 1000000)]\n        tx = wallet1a.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)\n        wallet1a.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('0100000001a391c8b3d4a551eac85714f3f0a7514381c014ba4688de085b0fcee42dc1371101000000910047304402204f1e1821b93b80a2033d3045325fe5c123d7ef54c2050aa356712eb32111ee670220039825c63cfe5879e808bf95aa365967d06a5f4072154955448becb65b8c5926014751210245c90e040d4f9d1fc136b3d4d6b7535bbb5df2bd27666c21977042cc1e05b5b02103c9a6bebfce6294488315e58137a279b2efe09f1f528ecf93b40675ded3cf0e5f52aefeffffff0240420f000000000017a9149573eb50f3136dff141ac304190f41c8becc92ce8738b32d000000000017a914b815d1b430ae9b632e3834ed537f7956325ee2a98700000000',\n                         str(tx_copy))\n        self.assertEqual('4649d6b6f8f967a84309de15c6d7403e628aa92ecb4f4d6d21299156fddff9e6', tx_copy.txid())\n        self.assertEqual('4649d6b6f8f967a84309de15c6d7403e628aa92ecb4f4d6d21299156fddff9e6', tx_copy.wtxid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n\n        wallet1a.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        wallet2.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet2 -> wallet1\n        outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 300000)]\n        tx = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)\n        wallet2.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('01000000000101e6f9dffd569129216d4d4fcb2ea98a623e40d7c615de0943a867f9f8b6d6494600000000171600149fad840ed174584ee054bd26f3e411817338c5edfeffffff02e09304000000000017a9145ae3933a6e13100f301f23227b98b0bdb5d16b8487d89a0a000000000017a9148ccd0efb2be5b412c4033715f560ed8f446c8ceb8702473044022020a3c46886b72f4ec561c5983a789098202307eae9679ff74fcb0879f65fff1d0220242ec3bfa747c513ef31874670d9c68ad235892588be55564696dd6690952e5a0121038362bbf0b4918b37e9d7c75930ed3a78e3d445724cb5c37ade4a59b6e411fe4e00000000',\n                         str(tx_copy))\n        self.assertEqual('ae5dcacdf9e3067e18fcfd33582c24f60f844730e7872049bb627796929879ee', tx_copy.txid())\n        self.assertEqual('f70bce6418fc44dcab41cbd466086aea54283821487189e4d15c4d1e2d1e267d', tx_copy.wtxid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n\n        wallet1a.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        wallet2.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet level checks\n        self.assertEqual((0, funding_output_value - 1000000 - 5000 + 300000, 0), wallet1a.get_balance())\n        self.assertEqual((0, 1000000 - 5000 - 300000, 0), wallet2.get_balance())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_rbf(self, mock_save_db):\n        self.maxDiff = None\n\n        class TmpConfig(tempfile.TemporaryDirectory):  # to avoid sub-tests side-effecting each other\n            def __init__(self, *args, **kwargs):\n                super().__init__(*args, **kwargs)\n                self.config = SimpleConfig({'electrum_path': self.name})\n                self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = False\n            def __enter__(self):\n                return self.config\n\n        for simulate_moving_txs in (False, True):\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_p2pkh_when_there_is_a_change_address\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_p2pkh_when_there_is_a_change_address(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_p2wpkh_when_there_is_a_change_address\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_p2wpkh_when_there_is_a_change_address(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_when_user_sends_max\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_when_user_sends_max(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_when_new_inputs_need_to_be_added\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_when_new_inputs_need_to_be_added(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_rbf_batching\", simulate_moving_txs=simulate_moving_txs):\n                    await self._rbf_batching(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_rbf_sufficient_fee_increase_adding_outputs_to_base_tx\", simulate_moving_txs=simulate_moving_txs):\n                    await self._rbf_sufficient_fee_increase_adding_outputs_to_base_tx(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_p2wpkh_decrease_payment\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_p2wpkh_decrease_payment(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_p2wpkh_decrease_payment_batch\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_p2wpkh_decrease_payment_batch(\n                        simulate_moving_txs=simulate_moving_txs,\n                        config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_p2wpkh_insane_high_target_fee\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_p2wpkh_insane_high_target_fee(config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_p2wpkh_csv\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_p2wpkh_csv(config=config)\n            with TmpConfig() as config:\n                with self.subTest(msg=\"_bump_fee_sufficient_fee_increase\", simulate_moving_txs=simulate_moving_txs):\n                    await self._bump_fee_sufficient_fee_increase(config=config, simulate_moving_txs=simulate_moving_txs)\n\n    async def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config):\n        wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('010000000001011f4db0ecd81f4388db316bc16efb4e9daf874cf4950d54ecb4c0fb372433d68500000000171600143d57fd9e88ef0e70cddb0d8b75ef86698cab0d44fdffffff0280969800000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88ac86a0ae020000000017a9149188bc82bdcae077060ebb4f02201b73c806edc887024830450221008e0725d531bd7dee4d8d38a0f921d7b1213e5b16c05312a80464ecc2b649598d0220596d309cf66d5f47cb3df558dbb43c5023a7796a80f5a88b023287e45a4db6b9012102c34d61ceafa8c216f01e05707672354f8119334610f7933a3f80dd7fb6290296bd391400')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 10000000\n        self.assertEqual('03052739fcfa2ead5f8e57e26021b0c2c546bcd3d74c6e708d5046dc58d90762', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create tx\n        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 1325501\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff01007501000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc392705030000000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d7200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400000100fa010000000001011f4db0ecd81f4388db316bc16efb4e9daf874cf4950d54ecb4c0fb372433d68500000000171600143d57fd9e88ef0e70cddb0d8b75ef86698cab0d44fdffffff0280969800000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88ac86a0ae020000000017a9149188bc82bdcae077060ebb4f02201b73c806edc887024830450221008e0725d531bd7dee4d8d38a0f921d7b1213e5b16c05312a80464ecc2b649598d0220596d309cf66d5f47cb3df558dbb43c5023a7796a80f5a88b023287e45a4db6b9012102c34d61ceafa8c216f01e05707672354f8119334610f7933a3f80dd7fb6290296bd391400220602a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587a0c8296e57100000000000000000000220203aa6a5d43c6de66d60f50942cf34f20e02c2c6f55349548fbf2cde5dd5d69b9180c8296e571010000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet.is_mine(wallet.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006a473044022003660461e018c78c2cc73e12c367062a51f71c79b5123b1508765980cbe131bd02205c09bf00e629ea166e2b810a220a20bf4327b4479fb8d841e0c9bca0f843a009012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d7200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400',\n                         str(tx_copy))\n        self.assertEqual('212cd9aca604cfb4f2c43161b94e32c1a6bc9773fced360e5d4dda98e84b168d', tx_copy.txid())\n        self.assertEqual('212cd9aca604cfb4f2c43161b94e32c1a6bc9773fced360e5d4dda98e84b168d', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance())\n\n        # bump tx\n        tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0)\n        tx.locktime = 1325501\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff01007501000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc392705030000000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987a0337200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400000100fa010000000001011f4db0ecd81f4388db316bc16efb4e9daf874cf4950d54ecb4c0fb372433d68500000000171600143d57fd9e88ef0e70cddb0d8b75ef86698cab0d44fdffffff0280969800000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88ac86a0ae020000000017a9149188bc82bdcae077060ebb4f02201b73c806edc887024830450221008e0725d531bd7dee4d8d38a0f921d7b1213e5b16c05312a80464ecc2b649598d0220596d309cf66d5f47cb3df558dbb43c5023a7796a80f5a88b023287e45a4db6b9012102c34d61ceafa8c216f01e05707672354f8119334610f7933a3f80dd7fb6290296bd391400220602a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587a0c8296e57100000000000000000000220203aa6a5d43c6de66d60f50942cf34f20e02c2c6f55349548fbf2cde5dd5d69b9180c8296e571010000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006a4730440220228deafd10b344371cb828eda507707f0b01f8b421feae5b079396aef72fa08f02205c63a540ac54b483cb59275ff191c89997be02fcf548a216ed1b1045c5d21041012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987a0337200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400',\n                         str(tx_copy))\n        self.assertEqual('fa1eba447d88bd84c6ceca16f2767232c488c73a25b51989b2fc6aacaa05d16f', tx_copy.txid())\n        self.assertEqual('fa1eba447d88bd84c6ceca16f2767232c488c73a25b51989b2fc6aacaa05d16f', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 7484320, 0), wallet.get_balance())\n\n    async def _bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv(self, *, simulate_moving_txs, config):\n        \"\"\"This tests a regression where sometimes we created a replacement tx\n        that spent from the original (which is clearly invalid).\n        \"\"\"\n        wallet = self.create_standard_wallet_from_seed('amazing vapor slab rib chat cousin east float plug baby session weird',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('02000000000101a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080100000000fdffffff0220a10700000000001976a9143decc30f4f7eec45c5775347050b85a43ac7ee0b88ac203c3500000000001600149d91f0053172fab394d277ae27e9fa5c5a4921090247304402207a2b4abe2c4128fe80db297d636b81487feda2ee3c51a95bc670b7b377b09ca402205147bc550dfdff72e9159554c19045111daf6d95f556a4f4dc370c90aa37a3e0012102cccad56b36e7bd1ae44c37d69019d006d8911b43071725d6dcbbdfcade05650313f71c00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('0d98d8615f7b711beff2efcd4cf6b9f7ecd3b16a53fb9374e6a81d852492674e', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        orig_rbf_tx = Transaction('02000000014e679224851da8e67493fb536ab1d3ecf7b9f64ccdeff2ef1b717b5f61d8980d000000006a4730440220361b332f0488501e0605b9a5385edda762e761c00f95195f308e2baea5e12f9d0220051be1c834f0de69ecf084b0311abf541687436cb34311a002efa4f104a722a3012103d4ce4ba5be0b861d2ee7c715b84ab0e791ccd36530bd8652babae37eda693c39fdffffff02bc020000000000001976a914093107975170d4416bd2dad961414ac0a5c9b3de88ac389d0700000000001976a914ac55156f62fa9085c114fc6496aee5ab153cb22888ac13f71c00')\n        orig_rbf_txid = orig_rbf_tx.txid()\n        self.assertEqual('2bce74c17a2b4c1f57b454604c87006173716e92028de60463182c344f3e2180', orig_rbf_txid)\n        wallet.adb.receive_tx_callback(orig_rbf_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # bump tx\n        tx = wallet.bump_fee(tx=tx_from_any(orig_rbf_tx.serialize()), new_fee_rate=200)\n        self.assertTrue(not any([txin for txin in tx.inputs() if txin.prevout.txid.hex() == orig_rbf_txid]))\n        tx.locktime = 1898260\n        tx.version = 2\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff01005502000000014e679224851da8e67493fb536ab1d3ecf7b9f64ccdeff2ef1b717b5f61d8980d0000000000fdffffff01200b0700000000001976a914ac55156f62fa9085c114fc6496aee5ab153cb22888ac14f71c00000100e102000000000101a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080100000000fdffffff0220a10700000000001976a9143decc30f4f7eec45c5775347050b85a43ac7ee0b88ac203c3500000000001600149d91f0053172fab394d277ae27e9fa5c5a4921090247304402207a2b4abe2c4128fe80db297d636b81487feda2ee3c51a95bc670b7b377b09ca402205147bc550dfdff72e9159554c19045111daf6d95f556a4f4dc370c90aa37a3e0012102cccad56b36e7bd1ae44c37d69019d006d8911b43071725d6dcbbdfcade05650313f71c00220603d4ce4ba5be0b861d2ee7c715b84ab0e791ccd36530bd8652babae37eda693c390c11aad9ae000000000000000000220203feceda5212994b3552847c93288c47490404784d90f1966b7d02e009ba40680e0c11aad9ae000000000100000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000014e679224851da8e67493fb536ab1d3ecf7b9f64ccdeff2ef1b717b5f61d8980d000000006a473044022043b34ed26822f120a2454aa9dd271400883e5c7133d3cd58ac018ddfa8ba4648022010394ca68edaf75df31217d3097f1171a87c846facfd963e49618fb1af89b66d012103d4ce4ba5be0b861d2ee7c715b84ab0e791ccd36530bd8652babae37eda693c39fdffffff01200b0700000000001976a914ac55156f62fa9085c114fc6496aee5ab153cb22888ac14f71c00',\n                         str(tx_copy))\n        self.assertEqual('9599a45a566251a5949b4f4b4a5f8d9a34c9e38e1ead9337c8338e34ea5bcd6e', tx_copy.txid())\n        self.assertEqual('9599a45a566251a5949b4f4b4a5f8d9a34c9e38e1ead9337c8338e34ea5bcd6e', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 461600, 0), wallet.get_balance())\n\n    async def _bump_fee_p2wpkh_decrease_payment(self, *, simulate_moving_txs, config):\n        wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('020000000001022ea8f7940c2e4bca2f34f21ba15a5c8d5e3c93d9c6deb17983412feefa0f1f6d0100000000fdffffff9d4ba5ab41951d506a7fa8272ef999ce3df166fe28f6f885aa791f012a0924cf0000000000fdffffff027485010000000000160014f80e86af4246960a24cd21c275a8e8842973fbcaa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400247304402203bf6dd875a775f356d4bb8c4e295a2cd506338c100767518f2b31fb85db71c1302204dc4ebca5584fc1cc08bd7f7171135d1b67ca6c8812c3723cd332eccaa7b848101210360bdbd16d9ef390fd3e804c421e6f30e6b065ac314f4d2b9a80d2f0682ad1431024730440220126b442d7988c5883ca17c2429f51ce770e3a57895524c8dfe07b539e483019e02200b50feed4f42f0035c9a9ddd044820607281e45e29e41a29233c2b8be6080bac01210245d47d08915816a5ecc934cff1b17e00071ca06172f51d632ba95392e8aad4fdd38a1d00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('dd0bf0d1563cd588b4c93cc1a9623c051ddb1c4f4581cf8ef43cfd27f031f246', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        orig_rbf_tx = Transaction('0200000000010146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff02c8af000000000000160014999a95482213a896c72a251b6cc9f3d137b0a45850c3000000000000160014ea76d391236726af7d7a9c10abe600129154eb5a02473044022076d298537b524a926a8fadad0e9ded5868c8f4cf29246048f76f00eb4afa56310220739ad9e0417e97ce03fad98a454b4977972c2805cef37bfa822c6d6c56737c870121024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af4d48a1d00')\n        orig_rbf_txid = orig_rbf_tx.txid()\n        self.assertEqual('db2f77709a4a04417b3a45838c21470877fe7c182a4f81005a21ce1315c6a5e6', orig_rbf_txid)\n        wallet.adb.receive_tx_callback(orig_rbf_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # bump tx\n        tx = wallet.bump_fee(\n            tx=tx_from_any(orig_rbf_tx.serialize()),\n            new_fee_rate=60,\n            strategy=BumpFeeStrategy.DECREASE_PAYMENT,\n        )\n        tx.locktime = 1936085\n        tx.version = 2\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff010071020000000146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff02c8af000000000000160014999a95482213a896c72a251b6cc9f3d137b0a458ccb5000000000000160014ea76d391236726af7d7a9c10abe600129154eb5ad58a1d000001011fa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400100fd7201020000000001022ea8f7940c2e4bca2f34f21ba15a5c8d5e3c93d9c6deb17983412feefa0f1f6d0100000000fdffffff9d4ba5ab41951d506a7fa8272ef999ce3df166fe28f6f885aa791f012a0924cf0000000000fdffffff027485010000000000160014f80e86af4246960a24cd21c275a8e8842973fbcaa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400247304402203bf6dd875a775f356d4bb8c4e295a2cd506338c100767518f2b31fb85db71c1302204dc4ebca5584fc1cc08bd7f7171135d1b67ca6c8812c3723cd332eccaa7b848101210360bdbd16d9ef390fd3e804c421e6f30e6b065ac314f4d2b9a80d2f0682ad1431024730440220126b442d7988c5883ca17c2429f51ce770e3a57895524c8dfe07b539e483019e02200b50feed4f42f0035c9a9ddd044820607281e45e29e41a29233c2b8be6080bac01210245d47d08915816a5ecc934cff1b17e00071ca06172f51d632ba95392e8aad4fdd38a1d002206024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af410ce2dd7cb00000080000000000000000000220203ecb63cc22d200c96225671b88a51a71deb053c6445dbd4694f61166e3e5bd05910ce2dd7cb0000008001000000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('0200000000010146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff02c8af000000000000160014999a95482213a896c72a251b6cc9f3d137b0a458ccb5000000000000160014ea76d391236726af7d7a9c10abe600129154eb5a024730440220063a2d330f0d659b3f686cc291722a87cc37371d3520c946e74da8dbbd4c57e00220604b0f387754988f71af47db78263698a513173e8ce3b27a696b9e3954ba757b0121024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af4d58a1d00',\n                         str(tx_copy))\n        self.assertEqual('6b03c00f47cb145ffb632c3ce54dece29b9a980949ef5c574321f7fc83fa2238', tx_copy.txid())\n        self.assertEqual('cb1f123231a3de5b02babddb43208f0273cb0df8addd4275583234eb50c7a87d', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 45000, 0), wallet.get_balance())\n\n    async def _bump_fee_p2wpkh_decrease_payment_batch(self, *, simulate_moving_txs, config):\n        wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('020000000001022ea8f7940c2e4bca2f34f21ba15a5c8d5e3c93d9c6deb17983412feefa0f1f6d0100000000fdffffff9d4ba5ab41951d506a7fa8272ef999ce3df166fe28f6f885aa791f012a0924cf0000000000fdffffff027485010000000000160014f80e86af4246960a24cd21c275a8e8842973fbcaa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400247304402203bf6dd875a775f356d4bb8c4e295a2cd506338c100767518f2b31fb85db71c1302204dc4ebca5584fc1cc08bd7f7171135d1b67ca6c8812c3723cd332eccaa7b848101210360bdbd16d9ef390fd3e804c421e6f30e6b065ac314f4d2b9a80d2f0682ad1431024730440220126b442d7988c5883ca17c2429f51ce770e3a57895524c8dfe07b539e483019e02200b50feed4f42f0035c9a9ddd044820607281e45e29e41a29233c2b8be6080bac01210245d47d08915816a5ecc934cff1b17e00071ca06172f51d632ba95392e8aad4fdd38a1d00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('dd0bf0d1563cd588b4c93cc1a9623c051ddb1c4f4581cf8ef43cfd27f031f246', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        orig_rbf_tx = Transaction('0200000000010146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff05e803000000000000160014a01f6b2a4bdaf3fb61f2a45e5eac92fcc58daee3881300000000000016001470fcde1ed0159ba5af97baec085ceb857098cedb0c49000000000000160014999a95482213a896c72a251b6cc9f3d137b0a458a86100000000000016001440c234c451fbd9ddf7824d6b8f0dc968a220946450c3000000000000160014ea76d391236726af7d7a9c10abe600129154eb5a024730440220782fb75f2398997ac77cd1b5c0d78f30a66b83df1d2d21c7a06cb03eb592d91702200540cf329c4b21e26aaba79a0c0ebdf465c4befb76a61e4eec924bc482cbf2930121024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af4a58a1d00')\n        orig_rbf_txid = orig_rbf_tx.txid()\n        self.assertEqual('9e0c7d890053c47c7cd653be984bc4b9a5dab8acf9a6ae075a00113d3077ad74', orig_rbf_txid)\n        wallet.adb.receive_tx_callback(orig_rbf_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # bump tx\n        tx = wallet.bump_fee(\n            tx=tx_from_any(orig_rbf_tx.serialize()),\n            new_fee_rate=60,\n            strategy=BumpFeeStrategy.DECREASE_PAYMENT,\n        )\n        tx.locktime = 1936095\n        tx.version = 2\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100af020000000146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff045d0500000000000016001470fcde1ed0159ba5af97baec085ceb857098cedb0c49000000000000160014999a95482213a896c72a251b6cc9f3d137b0a4587d5300000000000016001440c234c451fbd9ddf7824d6b8f0dc968a220946425b5000000000000160014ea76d391236726af7d7a9c10abe600129154eb5adf8a1d000001011fa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400100fd7201020000000001022ea8f7940c2e4bca2f34f21ba15a5c8d5e3c93d9c6deb17983412feefa0f1f6d0100000000fdffffff9d4ba5ab41951d506a7fa8272ef999ce3df166fe28f6f885aa791f012a0924cf0000000000fdffffff027485010000000000160014f80e86af4246960a24cd21c275a8e8842973fbcaa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400247304402203bf6dd875a775f356d4bb8c4e295a2cd506338c100767518f2b31fb85db71c1302204dc4ebca5584fc1cc08bd7f7171135d1b67ca6c8812c3723cd332eccaa7b848101210360bdbd16d9ef390fd3e804c421e6f30e6b065ac314f4d2b9a80d2f0682ad1431024730440220126b442d7988c5883ca17c2429f51ce770e3a57895524c8dfe07b539e483019e02200b50feed4f42f0035c9a9ddd044820607281e45e29e41a29233c2b8be6080bac01210245d47d08915816a5ecc934cff1b17e00071ca06172f51d632ba95392e8aad4fdd38a1d002206024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af410ce2dd7cb0000008000000000000000000000220203ecb63cc22d200c96225671b88a51a71deb053c6445dbd4694f61166e3e5bd05910ce2dd7cb000000800100000000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('0200000000010146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff045d0500000000000016001470fcde1ed0159ba5af97baec085ceb857098cedb0c49000000000000160014999a95482213a896c72a251b6cc9f3d137b0a4587d5300000000000016001440c234c451fbd9ddf7824d6b8f0dc968a220946425b5000000000000160014ea76d391236726af7d7a9c10abe600129154eb5a024730440220477ff315d3ac58de3bc1ec0b44b90a90da9bc09c440982fd9a1563eae98df0dc0220574033b0e306d388edcc77e4c2b39338fc8f182c747014aef3ce2c99cf9e5e960121024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af4df8a1d00',\n                         str(tx_copy))\n        self.assertEqual('bc86f4f14fea5305b197c02ae7b0d6b04c5f49144d9ad37c9f64ec0ec6d34594', tx_copy.txid())\n        self.assertEqual('368e4c0429b38e66ac64ac9dbb66145c9f28dfaf2fad60f6424db32c379a12da', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 18700, 0), wallet.get_balance())\n\n    async def _bump_fee_p2wpkh_insane_high_target_fee(self, *, config):\n        wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('020000000001022ea8f7940c2e4bca2f34f21ba15a5c8d5e3c93d9c6deb17983412feefa0f1f6d0100000000fdffffff9d4ba5ab41951d506a7fa8272ef999ce3df166fe28f6f885aa791f012a0924cf0000000000fdffffff027485010000000000160014f80e86af4246960a24cd21c275a8e8842973fbcaa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400247304402203bf6dd875a775f356d4bb8c4e295a2cd506338c100767518f2b31fb85db71c1302204dc4ebca5584fc1cc08bd7f7171135d1b67ca6c8812c3723cd332eccaa7b848101210360bdbd16d9ef390fd3e804c421e6f30e6b065ac314f4d2b9a80d2f0682ad1431024730440220126b442d7988c5883ca17c2429f51ce770e3a57895524c8dfe07b539e483019e02200b50feed4f42f0035c9a9ddd044820607281e45e29e41a29233c2b8be6080bac01210245d47d08915816a5ecc934cff1b17e00071ca06172f51d632ba95392e8aad4fdd38a1d00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('dd0bf0d1563cd588b4c93cc1a9623c051ddb1c4f4581cf8ef43cfd27f031f246', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        orig_rbf_tx = Transaction('0200000000010146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff02c8af000000000000160014999a95482213a896c72a251b6cc9f3d137b0a45850c3000000000000160014ea76d391236726af7d7a9c10abe600129154eb5a02473044022076d298537b524a926a8fadad0e9ded5868c8f4cf29246048f76f00eb4afa56310220739ad9e0417e97ce03fad98a454b4977972c2805cef37bfa822c6d6c56737c870121024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af4d48a1d00')\n        orig_rbf_txid = orig_rbf_tx.txid()\n        self.assertEqual('db2f77709a4a04417b3a45838c21470877fe7c182a4f81005a21ce1315c6a5e6', orig_rbf_txid)\n        wallet.adb.receive_tx_callback(orig_rbf_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        with self.assertRaises(CannotBumpFee):\n            tx = wallet.bump_fee(\n                tx=tx_from_any(orig_rbf_tx.serialize()),\n                new_fee_rate=99999,\n                strategy=BumpFeeStrategy.DECREASE_PAYMENT,\n            )\n        with self.assertRaises(CannotBumpFee):\n            tx = wallet.bump_fee(\n                tx=tx_from_any(orig_rbf_tx.serialize()),\n                new_fee_rate=99999,\n                strategy=BumpFeeStrategy.PRESERVE_PAYMENT,\n            )\n\n        tx = wallet.bump_fee(\n            tx=tx_from_any(orig_rbf_tx.serialize()),\n            new_fee_rate=60,\n            strategy=BumpFeeStrategy.DECREASE_PAYMENT,\n        )\n        tx.locktime = 1936085\n        tx.version = 2\n        self.assertEqual('6b03c00f47cb145ffb632c3ce54dece29b9a980949ef5c574321f7fc83fa2238', tx.txid())\n\n    async def _bump_fee_p2wpkh_csv(self, *, config):\n        wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('020000000001022ea8f7940c2e4bca2f34f21ba15a5c8d5e3c93d9c6deb17983412feefa0f1f6d0100000000fdffffff9d4ba5ab41951d506a7fa8272ef999ce3df166fe28f6f885aa791f012a0924cf0000000000fdffffff027485010000000000160014f80e86af4246960a24cd21c275a8e8842973fbcaa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400247304402203bf6dd875a775f356d4bb8c4e295a2cd506338c100767518f2b31fb85db71c1302204dc4ebca5584fc1cc08bd7f7171135d1b67ca6c8812c3723cd332eccaa7b848101210360bdbd16d9ef390fd3e804c421e6f30e6b065ac314f4d2b9a80d2f0682ad1431024730440220126b442d7988c5883ca17c2429f51ce770e3a57895524c8dfe07b539e483019e02200b50feed4f42f0035c9a9ddd044820607281e45e29e41a29233c2b8be6080bac01210245d47d08915816a5ecc934cff1b17e00071ca06172f51d632ba95392e8aad4fdd38a1d00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('dd0bf0d1563cd588b4c93cc1a9623c051ddb1c4f4581cf8ef43cfd27f031f246', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        orig_rbf_tx = Transaction(\"0200000000010146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd01000000000100000002c8af000000000000160014999a95482213a896c72a251b6cc9f3d137b0a45850c3000000000000160014ea76d391236726af7d7a9c10abe600129154eb5a02473044022076d298537b524a926a8fadad0e9ded5868c8f4cf29246048f76f00eb4afa56310220739ad9e0417e97ce03fad98a454b4977972c2805cef37bfa822c6d6c56737c870121024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af4d48a1d00\")\n        orig_rbf_txid = orig_rbf_tx.txid()\n        self.assertEqual('726f97590389fbef8570609eb2f8640464edb8ef44c30e48f4082b0191fa699c', orig_rbf_txid)\n        self.assertEqual(orig_rbf_tx.get_block_based_relative_locktime(), 1)\n\n        wallet.adb.receive_tx_callback(orig_rbf_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        tx = wallet.bump_fee(\n            tx=tx_from_any(orig_rbf_tx.serialize()),\n            new_fee_rate=60,\n            strategy=BumpFeeStrategy.DECREASE_PAYMENT,\n        )\n        tx.locktime = 1936085\n        tx.version = 2\n        self.assertEqual(tx.get_block_based_relative_locktime(), 1)\n        self.assertEqual('9f1842ea9c4d7cf88ac58d55d1b73e6ad7d34693a046d428887ead2c22865483', tx.txid())\n\n    async def _bump_fee_sufficient_fee_increase(self, *, config, simulate_moving_txs):\n        \"\"\"\n        Test that wallet.bump_fee raises if the replacement transaction has an equal feerate than the\n        transaction it tries to replace.\n        \"\"\"\n        wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',\n                                                       config=config)\n        # create tx\n        tx_to_bump_raw = '70736274ff0100d10200000002032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610000000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610100000000fdffffff0378d4030000000000220020f381d0d2c633cdd890015bb438c8e73e9960b21defa126c594e5cf67bd06419f78d40300000000002200204a1c25db1aa7165cb655638fb32319a945262fee7c8ce6f2f17f1223679bb0b84072070000000000160014f0fe5c1867a174a12e70165e728a072619455ed5000000000001011f20a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f80100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220603565a3904c7d2d6a6c2cf3fcdf89d9e5c60b509483104992cfdf85b196665170c10e8a903980000008000000000020000000001011f20a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a90398000000800000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000'\n        tx_to_bump = tx_from_any(tx_to_bump_raw)\n        if simulate_moving_txs:\n            partial_tx = tx_to_bump.serialize_as_bytes().hex()\n            self.assertEqual(tx_to_bump_raw,\n                             partial_tx)\n            tx_to_bump = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        tx_to_bump = wallet.sign_transaction(tx_to_bump, password=None)\n        wallet.adb.receive_tx_callback(tx_to_bump, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        self.assertTrue(tx_to_bump.is_complete())\n        self.assertTrue(tx_to_bump.is_segwit())\n        self.assertEqual(2, len(tx_to_bump.inputs()))\n        self.assertEqual(3, len(tx_to_bump.outputs()))\n        self.assertTrue(wallet.can_rbf_tx(tx_to_bump))\n\n        # cancel tx\n        tx_to_bump_feerate = tx_to_bump.get_fee() / tx_to_bump.estimated_size()\n        with self.assertRaises(CannotBumpFee):\n            wallet.bump_fee(tx=tx_to_bump, new_fee_rate=tx_to_bump_feerate)\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_cpfp_p2pkh(self, mock_save_db):\n        wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean')\n\n        # bootstrap wallet\n        funding_tx = Transaction('010000000001010f40064d66d766144e17bb3276d96042fd5aee2196bcce7e415f839e55a83de800000000171600147b6d7c7763b9185b95f367cf28e4dc6d09441e73fdffffff02404b4c00000000001976a9141df43441a3a3ee563e560d3ddc7e07cc9f9c3cdb88ac009871000000000017a9143873281796131b1996d2f94ab265327ee5e9d6e28702473044022029c124e5a1e2c6fa12e45ccdbdddb45fec53f33b982389455b110fdb3fe4173102203b3b7656bca07e4eae3554900aa66200f46fec0af10e83daaa51d9e4e62a26f4012103c8f0460c245c954ef563df3b1743ea23b965f98b120497ac53bd6b8e8e9e0f9bbe391400')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 5000000\n        self.assertEqual('9973bf8918afa349b63934432386f585613b51034db6c8628b61ba2feb8a3668', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # cpfp tx\n        tx = wallet.cpfp(funding_tx, fee=50000)\n        tx.set_rbf(True)\n        tx.locktime = 1325502\n        tx.version = 1\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('010000000168368aeb2fba618b62c8b64d03513b6185f58623433439b649a3af1889bf7399000000006a473044022014139c4c8dd4148851c1306c4901b759799e87a22885a3c23f6a6472a3c580dd02205df8037a19261a80157143ee61d24b64b8f60c3cb196e36e758920669f88eb56012102a7536f0bfbc60c5a8e86e2b9df26431fc062f9f454016dbc26f2467e0bc98b3ffdffffff01f0874b00000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbe391400',\n                         str(tx_copy))\n        self.assertEqual('c064c0dd89077de615f0ff8a626d4a62092c02649ed8266ed4c54302918e87d5', tx_copy.txid())\n        self.assertEqual('c064c0dd89077de615f0ff8a626d4a62092c02649ed8266ed4c54302918e87d5', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance())\n\n    async def _bump_fee_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config):\n        wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 10000000\n        self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create tx\n        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 1325499\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100720100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a903980000008000000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet.is_mine(wallet.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402205442705e988abe74bf391b293bb1b886674284a92ed0788c33024f9336d60aef022013a93049d3bed693254cd31a704d70bb988a36750f0b74d0a5b4d9e29c54ca9d0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',\n                         str(tx_copy))\n        self.assertEqual('b019bbad45a46ed25365e46e4cae6428fb12ae425977eb93011ffb294cb4977e', tx_copy.txid())\n        self.assertEqual('ba87313e2b3b42f1cc478843d4d53c72d6e06f6c66ac8cfbe2a59cdac2fd532d', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance())\n\n        # bump tx\n        tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0)\n        tx.locktime = 1325500\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100720100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f9870c4a720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a903980000008000000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f9870c4a720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402202a7e412d37f7a54f7ede0f85e58c7f9dc0f7244d222a4f50a90f87b05badeed40220788d4a4a13f660de7d5464dce5e79419361fdd5d1853c7da65469cd32f7981a90121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400',\n                         str(tx_copy))\n        self.assertEqual('dad75ab7078b9ce9698a83e7a954c1c38b235d3a4ab79bcb340245e3d9b62b93', tx_copy.txid())\n        self.assertEqual('05a484c64a094724b1c58a15463c8c772a98f084cc23ee636204ad9c4d9e5b51', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 7490060, 0), wallet.get_balance())\n\n    async def _bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all(self, *, simulate_moving_txs, config):\n        class NetworkMock:\n            relay_fee = 1000\n            async def get_transaction(self, txid, timeout=None):\n                if txid == \"597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd\":\n                    return \"02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00\"\n                else:\n                    raise Exception(\"unexpected txid\")\n            def has_internet_connection(self):\n                return True\n            run_from_another_thread = Network.run_from_another_thread\n            def get_local_height(self):\n                return 0\n            def blockchain(self):\n                class BlockchainMock:\n                    def is_tip_stale(self):\n                        return True\n                return BlockchainMock()\n\n        wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve',\n                                                       config=config)\n        wallet.network = NetworkMock()\n\n        # bootstrap wallet\n        funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('08557327673db61cc921e1a30826608599b86457836be3021105c13940d9a9a3', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        orig_rbf_tx = Transaction('02000000000102a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdfffffffd57af9ecf29b1cb42cb91087cf0d1d9fce59a3ca0b25bbfa7d27c07f99870590200000000fdffffff03b2a00700000000001600145dc80fd43eb70fd21a6c4446e3ce043df94f100cb2a00700000000001600147db4ab480b7d2218fba561ff304178f4afcbc972be358900000000001600149d91f0053172fab394d277ae27e9fa5c5a49210902473044022003999f03be8b9e299b2cd3bc7bce05e273d5d9ce24fc47af8754f26a7a13e13f022004e668499a67061789f6ebd2932c969ece74417ae3f2307bf696428bbed4fe36012102a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb0247304402207121358a66c0e716e2ba2be928076736261c691b4fbf89ea8d255449a4f5837b022042cadf9fe1b4f3c03ede3cef6783b42f0ba319f2e0273b624009cd023488c4c1012103a5ba95fb1e0043428ed70680fc17db254b3f701dfccf91e48090aa17c1b7ea40fef61c00')\n        orig_rbf_txid = orig_rbf_tx.txid()\n        self.assertEqual('6057690010ddac93a371629e1f41866400623e13a9cd336d280fc3239086a983', orig_rbf_txid)\n        wallet.adb.receive_tx_callback(orig_rbf_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # bump tx\n        orig_rbf_tx = tx_from_any(orig_rbf_tx.serialize())\n        orig_rbf_tx.add_info_from_wallet(wallet=wallet)\n        await orig_rbf_tx.add_info_from_network(network=wallet.network)\n        tx = wallet.bump_fee(tx=orig_rbf_tx, new_fee_rate=70)\n        tx.locktime = 1898268\n        tx.version = 2\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100b90200000002a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdfffffffd57af9ecf29b1cb42cb91087cf0d1d9fce59a3ca0b25bbfa7d27c07f99870590200000000fdffffff031660070000000000160014a36590fb127d05cf17a07a84a17f2f2d6cc90a7bb2a00700000000001600147db4ab480b7d2218fba561ff304178f4afcbc972be358900000000001600149d91f0053172fab394d277ae27e9fa5c5a4921091cf71c000001011f20a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec7270100de02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00220602a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb109c9fff98000000800000000000000000000100fd910102000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c0000220203b1b437d6d3366441e63e387594ffacb80676d7d518971d1d284b775cd7d8c38b109c9fff98000000800100000000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertFalse(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('70736274ff0100b90200000002a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdfffffffd57af9ecf29b1cb42cb91087cf0d1d9fce59a3ca0b25bbfa7d27c07f99870590200000000fdffffff031660070000000000160014a36590fb127d05cf17a07a84a17f2f2d6cc90a7bb2a00700000000001600147db4ab480b7d2218fba561ff304178f4afcbc972be358900000000001600149d91f0053172fab394d277ae27e9fa5c5a4921091cf71c000001011f20a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec7270100de02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c0001070001086b0247304402201f5ea643f6bc59c96ab8f1a3935b455e8f9395a67b74d618d121d16ae76f7b440220574d05df88740f915798e7993158c08e544801a044d19ef140574da19c1937d7012102a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb000100fd910102000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c0000220203b1b437d6d3366441e63e387594ffacb80676d7d518971d1d284b775cd7d8c38b109c9fff98000000800100000000000000000000',\n                         tx_copy.serialize_as_bytes().hex())\n        self.assertEqual('6a8ed07cd97a10ace851b67a65035f04ff477d67cde62bb8679007e87b214e79', tx_copy.txid())\n\n    async def _bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine(self, *, simulate_moving_txs, config):\n        class NetworkMock:\n            relay_fee = 1000\n            async def get_transaction(self, txid, timeout=None):\n                if txid == \"08557327673db61cc921e1a30826608599b86457836be3021105c13940d9a9a3\":\n                    return \"02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00\"\n                else:\n                    raise Exception(\"unexpected txid\")\n            def has_internet_connection(self):\n                return True\n            run_from_another_thread = Network.run_from_another_thread\n            def get_local_height(self):\n                return 0\n            def blockchain(self):\n                class BlockchainMock:\n                    def is_tip_stale(self):\n                        return True\n                return BlockchainMock()\n\n        wallet = self.create_standard_wallet_from_seed(\n            'faint orbit extend hope moon head mercy still debate sick cotton path',\n            config=config,\n            gap_limit=4,\n        )\n        wallet.network = NetworkMock()\n\n        # bootstrap wallet\n        funding_tx = Transaction('02000000000102c247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0000000000fdffffffc247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0100000000fdffffff01d63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060247304402201dc5be86749d8ce33571a6f1a2f8bbfceba89b9dbf2b4683e66c8c17cf7df6090220729199516cb894569ebbe3e998d47fc74030231ed30f110c9babd8a9dc361115012102728251a5f5f55375eef3c14fe59ab0755ba4d5f388619895238033ac9b51aad20247304402202e5d416489c20810e96e931b98a84b0c0c4fc32d2d34d3470b7ee16810246a4c022040f86cf8030d2117d6487bbe6e23d68d6d70408b002d8055de1f33d038d3a0550121039c009e7e7dad07e74ec5a8ac9f9e3499420dd9fe9709995525c714170152512620f71c00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('59ff0dd3962db651444d9fa6a61311302e47158533714d006e7e024ce45777da', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        orig_rbf_tx = Transaction('02000000000102a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdffffffda7757e44c027e6e004d71338515472e301113a6a69f4d4451b62d96d30dff590000000000fdffffff02b2a00700000000001600144710cfecc31828d31e68ad101dd022fe091a02b1683f0f00000000001600145fd89e3ff2f32c48d85ac65edb4fdf40112ffdfb02473044022032a64a01b0975b65b0adfee53baa6dfb2ca9917714ae3f3acbe609397cc4912d02207da348511a156f6b6eab9d4c762a421e629784108c61d128ad9409483c1e4819012102a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb024730440220620795910e9d96680a2d869024fc5048cb80d038e60a5b92850de65eb938a49c02201a550737b18eda5f93ce3ce0c5907d7b0a9856bbc3bb81cec14349c5b6c97c08012102999b1062a5acf7071a43fd6f2bd37a4e0f7162182490661949dbeeb7d1b03401eef61c00')\n        orig_rbf_txid = orig_rbf_tx.txid()\n        self.assertEqual('2dcc543035c90c25734c9381096cc2f211ac1c2467e072170bc9e51e4580029b', orig_rbf_txid)\n        wallet.adb.receive_tx_callback(orig_rbf_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # bump tx\n        orig_rbf_tx = tx_from_any(orig_rbf_tx.serialize())\n        orig_rbf_tx.add_info_from_wallet(wallet=wallet)\n        await orig_rbf_tx.add_info_from_network(network=wallet.network)\n        tx = wallet.bump_fee(tx=orig_rbf_tx, new_fee_rate=50)\n        tx.locktime = 1898273\n        tx.version = 2\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff01009a0200000002a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdffffffda7757e44c027e6e004d71338515472e301113a6a69f4d4451b62d96d30dff590000000000fdffffff02bc780700000000001600144710cfecc31828d31e68ad101dd022fe091a02b1683f0f00000000001600145fd89e3ff2f32c48d85ac65edb4fdf40112ffdfb21f71c00000100de02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c000001011fd63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060100fd530102000000000102c247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0000000000fdffffffc247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0100000000fdffffff01d63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060247304402201dc5be86749d8ce33571a6f1a2f8bbfceba89b9dbf2b4683e66c8c17cf7df6090220729199516cb894569ebbe3e998d47fc74030231ed30f110c9babd8a9dc361115012102728251a5f5f55375eef3c14fe59ab0755ba4d5f388619895238033ac9b51aad20247304402202e5d416489c20810e96e931b98a84b0c0c4fc32d2d34d3470b7ee16810246a4c022040f86cf8030d2117d6487bbe6e23d68d6d70408b002d8055de1f33d038d3a0550121039c009e7e7dad07e74ec5a8ac9f9e3499420dd9fe9709995525c714170152512620f71c00220602999b1062a5acf7071a43fd6f2bd37a4e0f7162182490661949dbeeb7d1b0340110277f031200000080000000000000000000220202519a4072fd8c29362693439f441bd7a45c0d8dea26ce88872a4bca7e5d07cb4510277f03120000008000000000020000000022020314c9b46fce4c6111e4bbe89bb06b3dd29c6cbac586a4914bb18fe8bb7e0a463c10277f031200000080000000000100000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertFalse(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('70736274ff01009a0200000002a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdffffffda7757e44c027e6e004d71338515472e301113a6a69f4d4451b62d96d30dff590000000000fdffffff02bc780700000000001600144710cfecc31828d31e68ad101dd022fe091a02b1683f0f00000000001600145fd89e3ff2f32c48d85ac65edb4fdf40112ffdfb21f71c00000100de02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c000001011fd63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060100fd530102000000000102c247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0000000000fdffffffc247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0100000000fdffffff01d63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060247304402201dc5be86749d8ce33571a6f1a2f8bbfceba89b9dbf2b4683e66c8c17cf7df6090220729199516cb894569ebbe3e998d47fc74030231ed30f110c9babd8a9dc361115012102728251a5f5f55375eef3c14fe59ab0755ba4d5f388619895238033ac9b51aad20247304402202e5d416489c20810e96e931b98a84b0c0c4fc32d2d34d3470b7ee16810246a4c022040f86cf8030d2117d6487bbe6e23d68d6d70408b002d8055de1f33d038d3a0550121039c009e7e7dad07e74ec5a8ac9f9e3499420dd9fe9709995525c714170152512620f71c0001070001086b0247304402206842258bbe37829facadef81fa17eb1c97e6f9a4c66717c0cea37b61c9be804902203d291a2c9e3df57e3422f9b90589c2350f0168867c3320e994258169b8da402b012102999b1062a5acf7071a43fd6f2bd37a4e0f7162182490661949dbeeb7d1b0340100220202519a4072fd8c29362693439f441bd7a45c0d8dea26ce88872a4bca7e5d07cb4510277f03120000008000000000020000000022020314c9b46fce4c6111e4bbe89bb06b3dd29c6cbac586a4914bb18fe8bb7e0a463c10277f031200000080000000000100000000',\n                         tx_copy.serialize_as_bytes().hex())\n        self.assertEqual('b46cdce7e7564dfd09618ab9008ec3a921c6372f3dcdab2f6094735b024485f0', tx_copy.txid())\n\n\n    async def _bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address(self, *, simulate_moving_txs, config):\n        wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 10000000\n        self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1q7rl9cxr85962ztnsze089zs8ycv52hk43f3m9n', '!')]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 1325499\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100520200000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f882980000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a9039800000080000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet.is_mine(wallet.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('02000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f882980000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402201050a398878098e695e2fcef181383d529d0bd0c959554bc01c35cc1791dd83b02202a193fbc77ab47879093d01c131fd4f2c80dd76750b7f0be027751ca970b84a50121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',\n                         str(tx_copy))\n        self.assertEqual('839b4d7ec2480975126ffa0c2a4552a85dd43435b23b375536391943e1f27074', tx_copy.txid())\n        self.assertEqual('b6fc78267494951771d935ef0338f50b13e62258e54265ad4989fe9ffe98b018', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, funding_output_value - 5000, 0), wallet.get_balance())\n\n        # bump tx\n        tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=75)\n        tx.locktime = 1325500\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100520200000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff014676980000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a9039800000080000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff014676980000000000160014f0fe5c1867a174a12e70165e728a072619455ed502473044022008bcb6fab261e9f4d5ccdd11c389b0620de1a1f493e97df6ec83f0c1a261e96c02205e352d3096cc68d4b1279f05dd4a2b1f9d1134dd01f761d01e21f4a88e608cca0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400',\n                         str(tx_copy))\n        self.assertEqual('0787da6829907ede8a322273d19ba47943ac234ad7fd1cb1821f6a0e78fcc003', tx_copy.txid())\n        self.assertEqual('65760ae60ed5feedfd10a9198b44e483ea64dcfa116d32cf247f45d474ee5ce0', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 9991750, 0), wallet.get_balance())\n\n    async def _bump_fee_when_user_sends_max(self, *, simulate_moving_txs, config):\n        wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create tx\n        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 1325499\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100530100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a903980000008000000000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet.is_mine(wallet.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987024730440220520ab41536d5d0fac8ad44e6aa4a8258a266121bab1eb6599f1ee86bbc65719d02205944c2fb765fca4753a850beadac49f5305c6722410c347c08cec4d90e3eb4430121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',\n                         str(tx_copy))\n        self.assertEqual('dc4b622f3225f00edb886011fa02b74630cdbc24cebdd3210d5ea3b68bef5cc9', tx_copy.txid())\n        self.assertEqual('a00340ee8c90673e05f2cf368601b6bba6a7f0513bd974feb218a326e39b1874', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 0, 0), wallet.get_balance())\n\n        # bump tx\n        tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0, strategy=BumpFeeStrategy.DECREASE_PAYMENT)\n        tx.locktime = 1325500\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100530100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01267898000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a903980000008000000000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01267898000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98702473044022069412007c3a6509fdfcfbe90679395c202c973740b0530b8ff366bc86ebff99d02206a02e3c0beb0921fa7d30379db4999d685d4b97239a2b8c7dd839531c72863110121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400',\n                         str(tx_copy))\n        self.assertEqual('53824cc67e8fe973b0dfa1b8cc10f4e2441b9b4b2b1eb92576fbba7000c2908a', tx_copy.txid())\n        self.assertEqual('bb137a5a810bb44d3b1cc77fb4f840e7c8c0f84771f7ce4671c3b1a9f5f93724', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 0, 0), wallet.get_balance())\n\n    async def _bump_fee_when_new_inputs_need_to_be_added(self, *, simulate_moving_txs, config):\n        wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',\n                                                       config=config)\n\n        # bootstrap wallet (incoming funding_tx1)\n        funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')\n        funding_txid1 = funding_tx1.txid()\n        #funding_output_value = 10_000_000\n        self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid1)\n        wallet.adb.receive_tx_callback(funding_tx1, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create tx\n        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 1325499\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100530100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a903980000008000000000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet.is_mine(wallet.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987024730440220520ab41536d5d0fac8ad44e6aa4a8258a266121bab1eb6599f1ee86bbc65719d02205944c2fb765fca4753a850beadac49f5305c6722410c347c08cec4d90e3eb4430121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',\n                         str(tx_copy))\n        self.assertEqual('dc4b622f3225f00edb886011fa02b74630cdbc24cebdd3210d5ea3b68bef5cc9', tx_copy.txid())\n        self.assertEqual('a00340ee8c90673e05f2cf368601b6bba6a7f0513bd974feb218a326e39b1874', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 0, 0), wallet.get_balance())\n\n        # another incoming transaction (funding_tx2)\n        funding_tx2 = Transaction('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400')\n        funding_txid2 = funding_tx2.txid()\n        #funding_output_value = 5_000_000\n        self.assertEqual('c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a', funding_txid2)\n        wallet.adb.receive_tx_callback(funding_tx2, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 5_000_000, 0), wallet.get_balance())\n\n        # bump tx\n        tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0)\n        tx.locktime = 1325500\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff01009b0100000002c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff025c254c0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a903980000008000000000000000000001011f404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100f601000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a9039800000080000000000100000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a903980000008001000000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('01000000000102c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff025c254c0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f9870247304402200d295ba3935c797c8eec441f1525f43697ddb07b2d5950a1474054d594bc2e4e0220549e9f07c01d35c19737d7e651c8a0a87c28b33b489ac2be2cc5f1cebbab3fc80121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50247304402206ac987d1ac834bc29c8b763da115942da6b070988eed1c33a3a53571f9d7c18e02204cb082efb881b1852abafdc28693ca45864b0130e252d97f58e790618010a629012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbc391400',\n                         str(tx_copy))\n        self.assertEqual('cdcf070cb8ddd9fbdd6b5cd29f2da395aa1e00640c3123a1a60941f49baddb6c', tx_copy.txid())\n        self.assertEqual('dceb4ffe55261c861f6f0841ba603fdd18f187df13d2b67c86bfbcb57e6a1870', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 4_990_300, 0), wallet.get_balance())\n\n    async def _rbf_batching(self, *, simulate_moving_txs, config):\n        wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',\n                                                       config=config)\n        # bootstrap wallet (incoming funding_tx1)\n        funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')\n        funding_txid1 = funding_tx1.txid()\n        #funding_output_value = 10_000_000\n        self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid1)\n        wallet.adb.receive_tx_callback(funding_tx1, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create tx\n        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2_500_000)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 1325499\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100720100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a903980000008000000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet.is_mine(wallet.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402205442705e988abe74bf391b293bb1b886674284a92ed0788c33024f9336d60aef022013a93049d3bed693254cd31a704d70bb988a36750f0b74d0a5b4d9e29c54ca9d0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',\n                         str(tx_copy))\n        self.assertEqual('b019bbad45a46ed25365e46e4cae6428fb12ae425977eb93011ffb294cb4977e', tx_copy.txid())\n        self.assertEqual('ba87313e2b3b42f1cc478843d4d53c72d6e06f6c66ac8cfbe2a59cdac2fd532d', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 7_495_000, 0), wallet.get_balance())\n\n        # another incoming transaction (funding_tx2)\n        funding_tx2 = Transaction('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400')\n        funding_txid2 = funding_tx2.txid()\n        #funding_output_value = 5_000_000\n        self.assertEqual('c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a', funding_txid2)\n        wallet.adb.receive_tx_callback(funding_tx2, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 12_495_000, 0), wallet.get_balance())\n\n        # create new tx (output should be batched with existing!)\n        # no new input will be needed. just a new output, and change decreased.\n        outputs = [PartialTxOutput.from_address_and_value('tb1qy6xmdj96v5dzt3j08hgc05yk3kltqsnmw4r6ry', 2_500_000)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(20000), base_tx=tx)\n        tx.set_rbf(True)\n        tx.locktime = 1325499\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100910100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff03a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98720fd4b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a90398000000800000000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet.is_mine(wallet.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff03a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98720fd4b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402206add1d6fc8b5fc6fd1bbf50d06fe432e65b16a9d715dbfe7f2d26473f48a128302207983d8db3508e3b953e6e26581d2bbba5a7ca0ff0dd07361de60977dc61ed1580121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',\n                         str(tx_copy))\n        self.assertEqual('21112d35fa08b9577bfe46405ad17720d0fa85bcefab0b0a1cffe79b9d6167c4', tx_copy.txid())\n        self.assertEqual('d49ffdaa832a35d88f3f43bcfb08306347c2342200098f450e41ccb289b26db3', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 9_980_000, 0), wallet.get_balance())\n\n        # create new tx (output should be batched with existing!)\n        # new input will be needed!\n        outputs = [PartialTxOutput.from_address_and_value('2NCVwbmEpvaXKHpXUGJfJr9iB5vtRN3vcut', 6_000_000)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(100000), base_tx=tx)\n        tx.set_rbf(True)\n        tx.locktime = 1325499\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100da0100000002c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff04a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98760823b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5808d5b000000000017a914d332f2f63019da6f2d23ee77bbe30eed7739790587bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a903980000008000000000000000000001011f404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100f601000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a90398000000800000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a903980000008001000000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(2, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet.is_mine(wallet.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('01000000000102c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff04a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98760823b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5808d5b000000000017a914d332f2f63019da6f2d23ee77bbe30eed7739790587024730440220730ac17af4ac14f008ee5d0a7be524d8ca344afc19b548faa9ac8c21a216df81022010d9cc878402103c1dd6b06e97e7910a23b7ec88251627f47ed1d5a8d741beba0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50247304402201005fc1e9091ac36d98b60c1c8b65aada0d4fe4da438d69b3262028644005cfc02207353c987be9e33d1e8702689960df76ac28adacc2f9093d731bc56c9578c5458012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbb391400',\n                         str(tx_copy))\n        self.assertEqual('88791bcd352b50592a5521c15595972b14b5d6be165be2df0e57ea19e588c025', tx_copy.txid())\n        self.assertEqual('7c5e5bff601e5467036b574b41090681a86de403867dd2b14097920b95e392ed', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 3_900_000, 0), wallet.get_balance())\n\n    async def _rbf_sufficient_fee_increase_adding_outputs_to_base_tx(self, *, simulate_moving_txs, config):\n        \"\"\"\n        Initial tx1: 2 inputs of our wallet -> 2 external p2wsh outputs + 1 change output\n        -> let make_unsigned_tx add one external output and another input -> tx2\n        Compare fee of tx1 and tx2, is the fee increase sufficient to satisfy relay policy?\n        \"\"\"\n        wallet: Abstract_Wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',\n                                                       config=config, gap_limit=3)\n\n        # bootstrap wallet, creates three utxos for wallet\n        funding_tx = Transaction('020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('615080b19e3bd1da18bcdbf74a9bcc94fe4f2348e1115a9028e5667d2c1c2b03', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        tx1 = tx_from_any('70736274ff0100d10200000002032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610000000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610100000000fdffffff0378d4030000000000220020f381d0d2c633cdd890015bb438c8e73e9960b21defa126c594e5cf67bd06419f78d40300000000002200204a1c25db1aa7165cb655638fb32319a945262fee7c8ce6f2f17f1223679bb0b84072070000000000160014f0fe5c1867a174a12e70165e728a072619455ed5000000000001011f20a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f80100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220603565a3904c7d2d6a6c2cf3fcdf89d9e5c60b509483104992cfdf85b196665170c10e8a903980000008000000000020000000001011f20a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a90398000000800000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000')\n        if simulate_moving_txs:\n            partial_tx = tx1.serialize_as_bytes().hex()\n            self.assertEqual('70736274ff0100d10200000002032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610000000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610100000000fdffffff0378d4030000000000220020f381d0d2c633cdd890015bb438c8e73e9960b21defa126c594e5cf67bd06419f78d40300000000002200204a1c25db1aa7165cb655638fb32319a945262fee7c8ce6f2f17f1223679bb0b84072070000000000160014f0fe5c1867a174a12e70165e728a072619455ed5000000000001011f20a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f80100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220603565a3904c7d2d6a6c2cf3fcdf89d9e5c60b509483104992cfdf85b196665170c10e8a903980000008000000000020000000001011f20a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a90398000000800000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000',\n                             partial_tx)\n            tx1 = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx1, password=None)\n        wallet.adb.receive_tx_callback(tx1, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        self.assertTrue(tx1.is_complete())\n        self.assertTrue(tx1.is_segwit())\n        self.assertEqual(2, len(tx1.inputs()))\n        self.assertEqual(3, len(tx1.outputs()))\n\n        tx2_additional_output = [\n            PartialTxOutput.from_address_and_value(\"tb1qnp9z33k8dz22dfklvyusv95hp7n3gc0yamzp6mj9y9es636ekrxs5mjmzp\", 510_000)\n        ]\n        tx2 = wallet.make_unsigned_transaction(\n            base_tx=tx1,\n            outputs=tx2_additional_output,\n            fee_policy=FixedFeePolicy(1_000),  # lower fee\n            BIP69_sort=False,\n        )\n        if simulate_moving_txs:\n            partial_tx = tx2.serialize_as_bytes().hex()\n            self.assertEqual('70736274ff0100fd25010200000003032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610000000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610100000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610200000000fdffffff0478d4030000000000220020f381d0d2c633cdd890015bb438c8e73e9960b21defa126c594e5cf67bd06419f78d40300000000002200204a1c25db1aa7165cb655638fb32319a945262fee7c8ce6f2f17f1223679bb0b830c8070000000000220020984a28c6c76894a6a6df61390616970fa71461e4eec41d6e4521730d4759b0cd8d3a070000000000160014f0fe5c1867a174a12e70165e728a072619455ed5000000000001011f20a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f80100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220603565a3904c7d2d6a6c2cf3fcdf89d9e5c60b509483104992cfdf85b196665170c10e8a903980000008000000000020000000001011f20a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a903980000008000000000010000000001011f20a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d249002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a9039800000080000000000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000',\n                             partial_tx)\n            tx2 = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx2, password=None)\n\n        self.assertTrue(tx2.is_complete())\n        self.assertTrue(tx2.is_segwit())\n        self.assertEqual(3, len(tx2.inputs()))\n        self.assertEqual(4, len(tx2.outputs()))\n\n        relayfee_sat_vb = bitcoin.relayfee() / 1000  # assume relayfee is equal to incrementalrelayfee\n        minimal_accepted_new_fee = int(tx1.get_fee() + tx2.estimated_size() * relayfee_sat_vb)\n        self.assertGreaterEqual(tx2.get_fee(), minimal_accepted_new_fee)\n\n        tx1_feerate_sat_vb = tx1.get_fee() / tx1.estimated_size()\n        tx2_feerate_sat_vb = tx2.get_fee() / tx2.estimated_size()\n        self.assertGreater(tx2_feerate_sat_vb, tx1_feerate_sat_vb, msg=\"tx2 feerate lower than tx1\")\n\n    def _create_cause_carbon_wallet(self):\n        data = read_test_vector('cause_carbon_wallet.json')\n        ks = keystore.from_seed(data['seed'], passphrase='', for_multisig=False)\n        wallet = WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=2, gap_limit_for_change=2, config=self.config)\n        # bootstrap wallet (incoming funding_tx0)\n        funding_tx = Transaction(data['funding_tx'])\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        return wallet, data['outgoing_address'], data['to_self_address']\n\n    async def test_ln_reserve_send_everything(self):\n        \"\"\" send all the coins, not using 'max' \"\"\"\n        wallet, outgoing_address, to_self_address = self._create_cause_carbon_wallet()\n        balance = sum(wallet.get_balance())\n        assert balance == 100_000\n        fee_sats = 1000\n        outputs = [PartialTxOutput.from_address_and_value(outgoing_address, balance - fee_sats)]\n        def make_tx(b):\n            wallet.lnworker = mock.Mock()\n            wallet.lnworker.has_anchor_channels.return_value = b\n            return wallet.make_unsigned_transaction(\n                outputs = outputs,\n                fee_policy = FixedFeePolicy(fee_sats),\n            )\n        tx = make_tx(False)\n        self.assertEqual(1, len(tx.outputs()))\n        with self.assertRaises(NotEnoughFunds):\n            make_tx(True)\n\n    async def test_ln_reserve_spend_max(self):\n        \"\"\" send all the coins using 'max'. test with outgoing and to self address \"\"\"\n        wallet, outgoing_address, to_self_address = self._create_cause_carbon_wallet()\n        def make_tx(address):\n            outputs = [PartialTxOutput.from_address_and_value(address, '!')]\n            wallet.lnworker = mock.Mock()\n            wallet.lnworker.has_anchor_channels.return_value = True\n            return wallet.make_unsigned_transaction(\n                outputs = outputs,\n                fee_policy = FixedFeePolicy(100),\n            )\n        tx = make_tx(outgoing_address)\n        self.assertEqual(2, len(tx.outputs()))\n        tx = make_tx(to_self_address)\n        self.assertEqual(1, len(tx.outputs()))\n\n    async def test_ln_reserve__usechange_off(self):\n        \"\"\"Send all the coins using 'max', with wallet.use_change being off.\n        This will create a reserve UTXO, reusing an input address.\n        \"\"\"\n        wallet, outgoing_address, to_self_address = self._create_cause_carbon_wallet()\n        wallet.use_change = False\n        def make_tx():\n            outputs = [PartialTxOutput.from_address_and_value(outgoing_address, '!')]\n            wallet.lnworker = mock.Mock()\n            wallet.lnworker.has_anchor_channels.return_value = True\n            return wallet.make_unsigned_transaction(\n                outputs = outputs,\n                fee_policy = FixedFeePolicy(100),\n            )\n        tx = make_tx()\n        self.assertEqual(1, len(tx.inputs()))\n        self.assertEqual(2, len(tx.outputs()))\n        outputs = {txout.address: txout.value for txout in tx.outputs()}\n        assert outgoing_address in outputs\n        assert outputs[tx.inputs()[0].address] == self.config.LN_UTXO_RESERVE  # address-reuse\n\n    async def test_ln_reserve_keep_existing_reserve(self):\n        \"\"\"\n        tests if make_unsigned_transaction keeps the existing reserve utxo\n        instead of creating a new one\n        \"\"\"\n        wallet1, outgoing_address1, to_self_address1 = self._create_cause_carbon_wallet()\n        wallet2 = self.create_standard_wallet_from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song')\n        wallet2_addr = wallet2.get_receiving_addresses()[0]\n        def make_tx(address, wallet):\n            outputs = [PartialTxOutput.from_address_and_value(address, '!')]\n            wallet.lnworker = mock.Mock()\n            wallet.lnworker.has_anchor_channels.return_value = True\n            return wallet.make_unsigned_transaction(\n                outputs = outputs,\n                fee_policy = FixedFeePolicy(100),\n            )\n        # send ! from wallet1 to outgoing address so wallet1 has exactly one reserve utxo\n        tx = make_tx(wallet2_addr, wallet1)\n        self.assertEqual(2, len(tx.outputs()))\n        wallet1.sign_transaction(tx, password=None)\n        wallet1.adb.receive_tx_callback(tx, tx_height=1000000)\n        wallet2.adb.receive_tx_callback(tx, tx_height=1000000)\n        assert sum(utxo.value_sats() for utxo in wallet1.get_spendable_coins()) == self.config.LN_UTXO_RESERVE\n\n        # send funds back to wallet1, so wallet1 is able to do a max spend again\n        tx = make_tx(to_self_address1, wallet2)\n        wallet2.sign_transaction(tx, password=None)\n        wallet1.adb.receive_tx_callback(tx, tx_height=1000100)\n\n        # now there is a reserve UTXO of config.LN_UTXO_RESERVE sat in wallet1, so wallet1 should\n        # not add it as input to the tx\n        assert len(wallet1.get_spendable_coins()) > 1, f\"{len(wallet1.get_spendable_coins())=}\"\n        tx = make_tx(outgoing_address1, wallet1)\n        self.assertEqual(1, len(tx.outputs()))\n        wallet1.adb.receive_tx_callback(tx, tx_height=1000200)\n        assert len(wallet1.get_spendable_coins()) == 1, f\"{len(wallet1.get_spendable_coins())=}\"\n        assert wallet1.get_spendable_coins()[0].value_sats() == self.config.LN_UTXO_RESERVE\n\n    async def test_rbf_batching__cannot_batch_as_would_need_to_use_ismine_outputs_of_basetx(self):\n        \"\"\"Wallet history contains unconf tx1 that spends all its coins to two ismine outputs,\n        one 'recv' address (20k sats) and one 'change' (80k sats).\n        The user tries to create tx2, that pays an invoice for 90k sats.\n        Even if batch_rbf==True, no batching should be done. Instead, the outputs of tx1 should be used.\n        \"\"\"\n        wallet, outgoing_address, to_self_address = self._create_cause_carbon_wallet()\n        # to_self_payment tx1\n        outputs = [PartialTxOutput.from_address_and_value(to_self_address, 20_000)]\n        toself_tx = wallet.make_unsigned_transaction(\n            outputs = outputs,\n            fee_policy = FixedFeePolicy(200),\n            locktime = 2423281,\n            rbf = True,\n        )\n        wallet.sign_transaction(toself_tx, password=None)\n        toself_txid = toself_tx.txid()\n        wallet.adb.receive_tx_callback(toself_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create outgoing tx2\n        outputs = [PartialTxOutput.from_address_and_value(outgoing_address, 90_000)]\n        coins = wallet.get_spendable_coins(domain=None)\n        self.assertEqual(2, len(coins))\n\n        candidates = wallet.get_candidates_for_batching(outputs, coins=coins)\n        self.assertEqual(candidates, [])\n        with self.assertRaises(NotEnoughFunds):\n            wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000), base_tx=toself_tx)\n\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000))\n        tx.set_rbf(True)\n        tx.locktime = 2423302\n        tx.version = 2\n        wallet.sign_transaction(tx, password=None)\n        self.assertEqual('02000000000102bbef0182c2c746bd28517b6fd27ba9eef9c7fb5982efd27bd612cc5a28615a3a0000000000fdffffffbbef0182c2c746bd28517b6fd27ba9eef9c7fb5982efd27bd612cc5a28615a3a0100000000fdffffff02602200000000000016001413fabce9be995554a722fc4e1c5ae53ebfd58164905f010000000000160014b266f4f1b9f0bc72f090573d049df66d4efa082c0247304402205c50b9ddb1b3ead6214d7d9707c74ba29ff547880d017aae2459db156bf85b9b022041134562fffa3dccf1ac05d9b07da62a8d57dd158d25d22d1965a011325e64aa012102c72b815ba00ccb0b469cc61a0ceb843d974e630cf34abcfac178838f1974f68f02473044022049774c32b0ad046b7acdb4acc38107b6b1be57c0d167643a48cbc045850c86c202205189ed61342fc52a377c2865a879c4c2606de98eebd6bf4d73874d62329668c70121033484c8ed83c359d1c3e569accb04b77988daab9408fc82869051c10d0749ac2006fa2400', str(tx))\n\n\n    async def test_rbf_batching__merge_duplicate_outputs(self):\n        \"\"\"txos paying to the same address might be merged into a single output with a larger value\"\"\"\n        wallet = self.create_standard_wallet_from_seed('response era cable net spike again observe dumb wage wonder sail tortoise',\n                                                       config=self.config)\n\n        # bootstrap wallet (incoming funding_tx0): for 500k sat\n        funding_tx = Transaction('02000000000101013548c9019890e27ce9e58766de05f18ea40ede70751fb6cd7a3a1715ece0a30100000000fdffffff0220a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa05485a080000000000160014bc69f7d82c403a9f35dfb6d1a4531d6b19cab0e3024730440220346b200f21c3024e1d51fb4ecddbdbd68bd24ae7b9dfd501519f6dcbeb7c052402200617e3ce7b0eb308e30caf23894fb0388b68fb1c15dd0681dd13ae5e735f148101210360d0c9ef15b8b6a16912d341ad218a4e4e4e07e9347f4a2dbc7ca8d974f8bc9ec1ad2600')\n        funding_txid = funding_tx.txid()\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        dest_addr = \"tb1qtzhwpufqr5dwztdaysfqnwlf9m29uwdkq8zm9w\"\n        # first payment to dest_addr\n        outputs1 = [PartialTxOutput.from_address_and_value(dest_addr, 200_000)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx1 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs1, fee_policy=FixedFeePolicy(2000))\n        tx1.set_rbf(True)\n        tx1.locktime = 2534850\n        tx1.version = 2\n        wallet.sign_transaction(tx1, password=None)\n        self.assertEqual(2, len(tx1.outputs()))\n        self.assertEqual('020000000001019264597cffcce8f0c17b16a02adca7a95ae90f2ea51bd4b4df60c76dfe86686e0000000000fdffffff02400d03000000000016001458aee0f1201d1ae12dbd241209bbe92ed45e39b6108c0400000000001600144e1b662f616fe134430054e29295ea6e5c18f1730247304402205ea932303bb89bfe07c1e4c28117cb84f613e09dd51464aa2ed2b184c2f2b76902202968280003b0e7d4098bf9adc47246db7b84c83f718e70a609de05f3b2ae64e80121029b1a61d66896486ab893741b38dbafb9673b91a82237d6e4ca0da3cda7cbeb7cc2ad2600',\n                         str(tx1))\n        wallet.adb.receive_tx_callback(tx1, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 298_000, 0), wallet.get_balance())\n\n        # second payment to dest_addr  (merged)\n        outputs2 = [PartialTxOutput.from_address_and_value(dest_addr, 100_000)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx2 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs2, fee_policy=FixedFeePolicy(3000), base_tx=tx1, merge_duplicate_outputs=True)\n        tx2.set_rbf(True)\n        tx2.locktime = 2534850\n        tx2.version = 2\n        wallet.sign_transaction(tx2, password=None)\n        self.assertEqual(2, len(tx2.outputs()))\n        self.assertEqual('020000000001019264597cffcce8f0c17b16a02adca7a95ae90f2ea51bd4b4df60c76dfe86686e0000000000fdffffff0288010300000000001600144e1b662f616fe134430054e29295ea6e5c18f173e09304000000000016001458aee0f1201d1ae12dbd241209bbe92ed45e39b60247304402201b5856f572a70f667392f000780044a6c6677eadadd5b56d2b15d1f90a8bf4b7022046566836d7e1e1a099ff72b4ecb09d6b24e701e12c0fb4c5667172d47d9b54520121029b1a61d66896486ab893741b38dbafb9673b91a82237d6e4ca0da3cda7cbeb7cc2ad2600',\n                         str(tx2))\n        wallet.adb.receive_tx_callback(tx2, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 197_000, 0), wallet.get_balance())\n\n        # remove tx2 from wallet, by replacing it with tx1\n        wallet.adb.receive_tx_callback(tx1, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 298_000, 0), wallet.get_balance())\n\n        # second payment to dest_addr  (not merged, just duplicate outputs)\n        outputs2 = [PartialTxOutput.from_address_and_value(dest_addr, 100_000)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx3 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs2, fee_policy=FixedFeePolicy(3000), base_tx=tx1)\n        tx3.set_rbf(True)\n        tx3.locktime = 2534850\n        tx3.version = 2\n        wallet.sign_transaction(tx3, password=None)\n        self.assertEqual(3, len(tx3.outputs()))\n        self.assertEqual('020000000001019264597cffcce8f0c17b16a02adca7a95ae90f2ea51bd4b4df60c76dfe86686e0000000000fdffffff03a08601000000000016001458aee0f1201d1ae12dbd241209bbe92ed45e39b688010300000000001600144e1b662f616fe134430054e29295ea6e5c18f173400d03000000000016001458aee0f1201d1ae12dbd241209bbe92ed45e39b602473044022061386129ebefda19e22ab9e2c06642a2a5eb7637e1b492d5c164591ff0fb27c9022006129d5d0c780d6830fb6cf924e3eeef03b8a349a9ebb36969cae410d9ff0fa50121029b1a61d66896486ab893741b38dbafb9673b91a82237d6e4ca0da3cda7cbeb7cc2ad2600',\n                         str(tx3))\n        wallet.adb.receive_tx_callback(tx3, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 197_000, 0), wallet.get_balance())\n\n    async def test_join_psbts__merge_duplicate_outputs(self):\n        \"\"\"txos paying to the same address might be merged into a single output with a larger value\"\"\"\n        rawtx1 = \"70736274ff01007102000000019264597cffcce8f0c17b16a02adca7a95ae90f2ea51bd4b4df60c76dfe86686e0000000000fdffffff02400d03000000000016001458aee0f1201d1ae12dbd241209bbe92ed45e39b6108c0400000000001600144e1b662f616fe134430054e29295ea6e5c18f173c2ad26000001011f20a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa050100de02000000000101013548c9019890e27ce9e58766de05f18ea40ede70751fb6cd7a3a1715ece0a30100000000fdffffff0220a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa05485a080000000000160014bc69f7d82c403a9f35dfb6d1a4531d6b19cab0e3024730440220346b200f21c3024e1d51fb4ecddbdbd68bd24ae7b9dfd501519f6dcbeb7c052402200617e3ce7b0eb308e30caf23894fb0388b68fb1c15dd0681dd13ae5e735f148101210360d0c9ef15b8b6a16912d341ad218a4e4e4e07e9347f4a2dbc7ca8d974f8bc9ec1ad26002206029b1a61d66896486ab893741b38dbafb9673b91a82237d6e4ca0da3cda7cbeb7c101f1b48320000008000000000000000000000220203db4846ec1841f48484590e67fcd7d1039f124a04410c5794f38ec8625329ea23101f1b483200000080010000000000000000\"\n        rawtx2 = \"70736274ff0100710200000001a4c6da70097e1bfbbcba0edad4ba1143295300b60851aa6c4916a0b32381bf7f0000000000fdffffff02a08601000000000016001458aee0f1201d1ae12dbd241209bbe92ed45e39b6108c040000000000160014fac4435311276a6cfda5681cfb02252acdd14c3fc2ad26000001011f801a06000000000016001452af44a1e32754fd8d2e7c1c3cc1b305379f0b660100de020000000001018eeaf0cd7de0e0e117af1a7f2bab59b4ddfbd416ef7460b3fd42a1f7bc039cfd0000000000fdffffff02801a06000000000016001452af44a1e32754fd8d2e7c1c3cc1b305379f0b66909f0700000000001600140847a3685a3ce9911cdce3fbf33cb42edc8f6dd902473044022044d3485c09784f03cd648117ef2d4d0dabeeb2929b30f2e52c3bbd5efd1c0f820220346655235eb9fcb54b23bbf194217092cc8aa6dd33ecf018907626b90289be6801210304e06afd290a4e7a9eb008cf408a4f9b0640fd2688258b523aa3dbb236bb3f7eccad2600220602c1ed648e71f15643950b444b864ab784b9d0e31e6ca6ec7d849d3dda4d98da05101f1b48320000008000000000010000000000220203aba60233db3aab45d0196cb70a22d667faa92124760700d20c953b0222ced96d101f1b483200000080010000000100000000\"\n\n        self.config.WALLET_MERGE_DUPLICATE_OUTPUTS = False\n        joined_tx = tx_from_any(rawtx1)\n        joined_tx.join_with_other_psbt(tx_from_any(rawtx2), config=self.config)\n        self.assertEqual(4, len(joined_tx.outputs()))\n        self.assertEqual(\"70736274ff0100d802000000029264597cffcce8f0c17b16a02adca7a95ae90f2ea51bd4b4df60c76dfe86686e0000000000fdffffffa4c6da70097e1bfbbcba0edad4ba1143295300b60851aa6c4916a0b32381bf7f0000000000fdffffff04a08601000000000016001458aee0f1201d1ae12dbd241209bbe92ed45e39b6400d03000000000016001458aee0f1201d1ae12dbd241209bbe92ed45e39b6108c0400000000001600144e1b662f616fe134430054e29295ea6e5c18f173108c040000000000160014fac4435311276a6cfda5681cfb02252acdd14c3fc2ad26000001011f20a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa050100de02000000000101013548c9019890e27ce9e58766de05f18ea40ede70751fb6cd7a3a1715ece0a30100000000fdffffff0220a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa05485a080000000000160014bc69f7d82c403a9f35dfb6d1a4531d6b19cab0e3024730440220346b200f21c3024e1d51fb4ecddbdbd68bd24ae7b9dfd501519f6dcbeb7c052402200617e3ce7b0eb308e30caf23894fb0388b68fb1c15dd0681dd13ae5e735f148101210360d0c9ef15b8b6a16912d341ad218a4e4e4e07e9347f4a2dbc7ca8d974f8bc9ec1ad26002206029b1a61d66896486ab893741b38dbafb9673b91a82237d6e4ca0da3cda7cbeb7c101f1b48320000008000000000000000000001011f801a06000000000016001452af44a1e32754fd8d2e7c1c3cc1b305379f0b660100de020000000001018eeaf0cd7de0e0e117af1a7f2bab59b4ddfbd416ef7460b3fd42a1f7bc039cfd0000000000fdffffff02801a06000000000016001452af44a1e32754fd8d2e7c1c3cc1b305379f0b66909f0700000000001600140847a3685a3ce9911cdce3fbf33cb42edc8f6dd902473044022044d3485c09784f03cd648117ef2d4d0dabeeb2929b30f2e52c3bbd5efd1c0f820220346655235eb9fcb54b23bbf194217092cc8aa6dd33ecf018907626b90289be6801210304e06afd290a4e7a9eb008cf408a4f9b0640fd2688258b523aa3dbb236bb3f7eccad2600220602c1ed648e71f15643950b444b864ab784b9d0e31e6ca6ec7d849d3dda4d98da05101f1b4832000000800000000001000000000000220203db4846ec1841f48484590e67fcd7d1039f124a04410c5794f38ec8625329ea23101f1b483200000080010000000000000000220203aba60233db3aab45d0196cb70a22d667faa92124760700d20c953b0222ced96d101f1b483200000080010000000100000000\",\n                         joined_tx.serialize_as_bytes().hex())\n\n        self.config.WALLET_MERGE_DUPLICATE_OUTPUTS = True\n        joined_tx = tx_from_any(rawtx1)\n        joined_tx.join_with_other_psbt(tx_from_any(rawtx2), config=self.config)\n        self.assertEqual(3, len(joined_tx.outputs()))\n        self.assertEqual(\"70736274ff0100b902000000029264597cffcce8f0c17b16a02adca7a95ae90f2ea51bd4b4df60c76dfe86686e0000000000fdffffffa4c6da70097e1bfbbcba0edad4ba1143295300b60851aa6c4916a0b32381bf7f0000000000fdffffff03108c0400000000001600144e1b662f616fe134430054e29295ea6e5c18f173108c040000000000160014fac4435311276a6cfda5681cfb02252acdd14c3fe09304000000000016001458aee0f1201d1ae12dbd241209bbe92ed45e39b6c2ad26000001011f20a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa050100de02000000000101013548c9019890e27ce9e58766de05f18ea40ede70751fb6cd7a3a1715ece0a30100000000fdffffff0220a1070000000000160014542266519a44eb9b903761d40c6fe1055d33fa05485a080000000000160014bc69f7d82c403a9f35dfb6d1a4531d6b19cab0e3024730440220346b200f21c3024e1d51fb4ecddbdbd68bd24ae7b9dfd501519f6dcbeb7c052402200617e3ce7b0eb308e30caf23894fb0388b68fb1c15dd0681dd13ae5e735f148101210360d0c9ef15b8b6a16912d341ad218a4e4e4e07e9347f4a2dbc7ca8d974f8bc9ec1ad26002206029b1a61d66896486ab893741b38dbafb9673b91a82237d6e4ca0da3cda7cbeb7c101f1b48320000008000000000000000000001011f801a06000000000016001452af44a1e32754fd8d2e7c1c3cc1b305379f0b660100de020000000001018eeaf0cd7de0e0e117af1a7f2bab59b4ddfbd416ef7460b3fd42a1f7bc039cfd0000000000fdffffff02801a06000000000016001452af44a1e32754fd8d2e7c1c3cc1b305379f0b66909f0700000000001600140847a3685a3ce9911cdce3fbf33cb42edc8f6dd902473044022044d3485c09784f03cd648117ef2d4d0dabeeb2929b30f2e52c3bbd5efd1c0f820220346655235eb9fcb54b23bbf194217092cc8aa6dd33ecf018907626b90289be6801210304e06afd290a4e7a9eb008cf408a4f9b0640fd2688258b523aa3dbb236bb3f7eccad2600220602c1ed648e71f15643950b444b864ab784b9d0e31e6ca6ec7d849d3dda4d98da05101f1b483200000080000000000100000000220203db4846ec1841f48484590e67fcd7d1039f124a04410c5794f38ec8625329ea23101f1b483200000080010000000000000000220203aba60233db3aab45d0196cb70a22d667faa92124760700d20c953b0222ced96d101f1b48320000008001000000010000000000\",\n                         joined_tx.serialize_as_bytes().hex())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_cpfp_p2wpkh(self, mock_save_db):\n        wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage')\n\n        # bootstrap wallet\n        funding_tx = Transaction('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 5000000\n        self.assertEqual('c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # cpfp tx\n        tx = wallet.cpfp(funding_tx, fee=50000)\n        tx.set_rbf(True)\n        tx.locktime = 1325501\n        tx.version = 1\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('010000000001014a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff01f0874b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed502473044022029314c8fb5e05dcd6e94d26f7d96bd9824290977bdc0602b2ef1faf8aa7da53c022003c0477a2b45f05ec4e06e4669a9c3a9e8d9ad0ab78ed85a37b93064c5358e9a012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbd391400',\n                         str(tx_copy))\n        self.assertEqual('6bb0490b29b65c7292f6bb1715982fe4474417b4fbdcf8a4675a0994ce12d156', tx_copy.txid())\n        self.assertEqual('ce94905afcb396d7bc6de28e4d102dcefc85224abae7df16399b2789f5596db8', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance())\n\n    async def test_sweep_uncompressed_p2pk(self):\n        class NetworkMock:\n            relay_fee = 1000\n            async def listunspent_for_scripthash(self, scripthash):\n                if scripthash == '460e4fb540b657d775d84ff4955c9b13bd954c2adc26a6b998331343f85b6a45':\n                    return [{'tx_hash': 'ac24de8b58e826f60bd7b9ba31670bdfc3e8aedb2f28d0e91599d741569e3429', 'tx_pos': 1, 'height': 1325785, 'value': 1000000}]\n                else:\n                    return []\n            async def get_transaction(self, txid):\n                if txid == \"ac24de8b58e826f60bd7b9ba31670bdfc3e8aedb2f28d0e91599d741569e3429\":\n                    return \"010000000001021b41471d6af3aa80ebe536dbf4f505a6d46af456131a8e12e1950171959b690e0f00000000fdffffff2ef29833a69863b31e884fc5e6f7b99a23b5601e14f0eb65905faa42fec0776d0000000000fdffffff02f96a070000000000160014e61b989a740056254b5f8061281ac96ca15d35e140420f00000000004341049afa8fb50f52104b381a673c6e4fb7fb54987271d0e948dd9a568bb2af6f9310a7a809ce06e09d1510e5836f20414596232e2c0be63715459fa3cf8e7092af05ac0247304402201fe20012c1c732a6a8f942c4e0feed5ed0bddfb94db736ec3d0c0d38f0f7f46a022021d690e6d2688b90b76002f4c3134981502d666211e85e8a6ca91e78405dfa3801210346fb31136ab48e6c648865264d32004b43643d01f0ba485cffac4bb0b3f739470247304402204a2473ab4b3bfc8e6b1a6b8675dc2c3d115d8c04f5df37f29779dca6d300d9db02205e72ebbccd018c67b86ae4da6b0e6222902a8de85915ed6115330b9328764b370121027a93ffc9444a12d99307318e2e538949072cb35b2aca344b8163795a022414c7d73a1400\"\n                else:\n                    raise Exception(\"unexpected txid\")\n\n        privkeys = ['93NQ7CFbwTPyKDJLXe97jczw33fiLijam2SCZL3Uinz1NSbHrTu',]\n        network = NetworkMock()\n        dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2'\n        tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(5000), locktime=1325785, tx_version=1)\n\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400',\n                         str(tx_copy))\n        self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.txid())\n        self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.wtxid())\n\n    async def test_sweep_compressed_p2pk(self):\n        class NetworkMock:\n            relay_fee = 1000\n            async def listunspent_for_scripthash(self, scripthash):\n                if scripthash == 'cc911adb9fb939d0003a138ebdaa5195bf1d6f9172e438309ab4c00a5ebc255b':\n                    return [{'tx_hash': '84a4a1943f7a620e0d8413f4c10877000768797a93bb106b3e7cd6fccc59b35e', 'tx_pos': 1, 'height': 2420005, 'value': 111111}]\n                else:\n                    return []\n            async def get_transaction(self, txid):\n                if txid == \"84a4a1943f7a620e0d8413f4c10877000768797a93bb106b3e7cd6fccc59b35e\":\n                    return \"02000000000102b7bfcd442c91134743c6e4100bb9f79456a6015de3c3920166bb0c3b7a8f7c070100000000fdffffff5ab39480d4b35ffa843691d944a8479dfe825d38b03fcb1804197482bfad80fb0100000000fdffffff02d4ec000000000000160014769114e56e0913de3719a3b00a446b78e61751f007b201000000000023210332e147520e4743299d95196afaf9db7c86fe02507d9ca89acd7a4e96a63653d5ac0247304402200387fe79ffe10cec73d9b131058d7128665f729d14597828b483842889c4f5ea02201197b2f1295e4011e2d174d53c240fd13c6351451ab961ccb3678fc21fa5323b0121023c221dfbf7c3f61b9e5f66343c1a302d6beca2a8883504b0f484faec9919636b024730440220687d387af37df458efc104ee0065262cb5ea195e526ed7a480fd16e6cf708c3a022019bd3fd9c3ca3f1a1fbeabe20547876eb4572a7339de37b706fbd55031e60428012102c9c459e58b01a864d7bb80f6d577326465a04219c48541b5f3ea556a06ca61a425ed2400\"\n                else:\n                    raise Exception(\"unexpected txid\")\n\n        privkeys = ['cUygTZe4jZLVwE4G44NznCPTeGvgsgassqucUHkAJxGC71Rst2kH',]\n        network = NetworkMock()\n        dest_addr = 'tb1q5uy5xjcn55gwdkmghht8yp3vwz3088f6e3e0em'\n        tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(5000), locktime=2420006, tx_version=2)\n\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000015eb359ccfcd67c3e6b10bb937a796807007708c1f413840d0e627a3f94a1a48401000000484730440220043fc85a43e918ac41e494e309fdf204ca245d260cb5ea09108b196ca65d8a09022056f852f0f521e79ab2124d7e9f779c7290329ce5628ef8e92601980b065d3eb501fdffffff017f9e010000000000160014a709434b13a510e6db68bdd672062c70a2f39d3a26ed2400',\n                         str(tx_copy))\n        self.assertEqual('968a501350b954ecb51948202b8d0613aa84123ca9b745c14e208cb14feeff59', tx_copy.txid())\n        self.assertEqual('968a501350b954ecb51948202b8d0613aa84123ca9b745c14e208cb14feeff59', tx_copy.wtxid())\n\n    async def test_sweep_uncompressed_p2pkh(self):\n        class NetworkMock:\n            relay_fee = 1000\n            async def listunspent_for_scripthash(self, scripthash):\n                if scripthash == '71e8c6a9fd8ab498290d5ccbfe1cfe2c5dc2a389b4c036dd84e305a59c4a4d53':\n                    return [{'tx_hash': '15a78cc7664c42f1040474763bf794d555f6092bfba97d6c276f296c2d141506', 'tx_pos': 0, 'height': -1, 'value': 222222}]\n                else:\n                    return []\n            async def get_transaction(self, txid):\n                if txid == \"15a78cc7664c42f1040474763bf794d555f6092bfba97d6c276f296c2d141506\":\n                    return \"02000000000101c6a49fbd701f1526c8e43025a6dda8dd235b3593cfd38af040cba3e37b474fdb0e00000000fdffffff020e640300000000001976a914f1b02b7028fb81aefbb25809a2baf8d94d0c2ba288acb9e3080000000000160014c2eee75efe6621be177f7edd8198f671d1640c2602473044022072b8a6154590704063c377af451b4d69f76cc9064085d4a0c80f08625c57628802207844164839d93ce54ce7db092bbd809d5270142b5dedc823e95400e8bdae88c6012102b6ad13f48fd679a209b7d822376550e5e694a3a2862546ceb72c4012977eac4829ed2400\"\n                else:\n                    raise Exception(\"unexpected txid\")\n\n        privkeys = ['p2pkh:91gxDahzHiJ63HXmLP7pvZrkF8i5gKBXk4VqWfhbhJjtf6Ni5NU',]\n        network = NetworkMock()\n        dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2'\n        tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(5000), locktime=2420010, tx_version=2)\n\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000010615142d6c296f276c7da9fb2b09f655d594f73b76740404f1424c66c78ca715000000008a47304402206d2dae571ca2f51e0d4a8ce6a6335fa25ac09f4bbed26439124d93f035bdbb130220249dc2039f1da338a40679f0e79c25a2dc2983688e6c04753348f2aa8435e375014104b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b2987b4c862d5b687bb5328adccc69e67a17b109b6328228695a1c384573acd6199fdffffff0186500300000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071f2aed2400',\n                         str(tx_copy))\n        self.assertEqual('d62048493bf8459be5e1e3cab6caabc8f15661d02c364d8dc008297e573772bf', tx_copy.txid())\n        self.assertEqual('d62048493bf8459be5e1e3cab6caabc8f15661d02c364d8dc008297e573772bf', tx_copy.wtxid())\n\n    async def test_sweep_compressed_p2pkh(self):\n        class NetworkMock:\n            relay_fee = 1000\n            async def listunspent_for_scripthash(self, scripthash):\n                if scripthash == '941b2ca8bd850e391abc5e024c83b773842c40268a8fa8a5ef7aeca19fb395c5':\n                    return [{'tx_hash': '8a764102b4a5c5d1b5235e6ce7e67ed3c146130f8a52e7692a151e2e5a831767', 'tx_pos': 0, 'height': -1, 'value': 123456}]\n                else:\n                    return []\n            async def get_transaction(self, txid):\n                if txid == \"8a764102b4a5c5d1b5235e6ce7e67ed3c146130f8a52e7692a151e2e5a831767\":\n                    return \"020000000001010615142d6c296f276c7da9fb2b09f655d594f73b76740404f1424c66c78ca7150100000000fdffffff0240e20100000000001976a914f1d49f51f9b58c4805431c303d12d3dcf51ae54188ace9000700000000001600145bdb04f2d096ee48b8b350c85481392ab47c01e70247304402200a72a4599cb27f16011cd67e2951733d6775cbd008506eacb2c20d69db3f531702204c944ec09224a347481c9eea78cac79b77b194b19dfef01b1e3b428010a82570012102fc38612ca7cc42d05a7089f1a6ec3900535604bd779f83c7817aae7bfd907dbd2aed2400\"\n                else:\n                    raise Exception(\"unexpected txid\")\n\n        privkeys = ['p2pkh:cN3LiXmurmGRF5xngYd8XS2ZsP2KeXFUh4SH7wpC8uJJzw52JPq1',]\n        network = NetworkMock()\n        dest_addr = 'tb1q782f750ekkxysp2rrscr6yknmn634e2pv8lktu'\n        tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(1000), locktime=2420010, tx_version=2)\n\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000016717835a2e1e152a69e7528a0f1346c1d37ee6e76c5e23b5d1c5a5b40241768a000000006a473044022038ad38003943bfd3ed39ba4340d545753fcad632a8fe882d01e4f0140ddb3cfb022019498260e29f5fbbcde9176bfb3553b7acec5fe284a9a3a33547a2d082b60355012103b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b29fdffffff0158de010000000000160014f1d49f51f9b58c4805431c303d12d3dcf51ae5412aed2400',\n                         str(tx_copy))\n        self.assertEqual('432c108626581fc6a7d3efc9dac5f3dec8286cec47dfaab86b4267d10381586c', tx_copy.txid())\n        self.assertEqual('432c108626581fc6a7d3efc9dac5f3dec8286cec47dfaab86b4267d10381586c', tx_copy.wtxid())\n\n    async def test_sweep_p2wpkh_p2sh(self):\n        class NetworkMock:\n            relay_fee = 1000\n            async def listunspent_for_scripthash(self, scripthash):\n                if scripthash == '9ee9bddbe9dc47f7f6c5a652a09012f49dfc54d5b997f58d7ccc49040871e61b':\n                    return [{'tx_hash': '9a7bf98ed72b1002559d3d61805838a00e94afec78b8597a68606e2a0725171d', 'tx_pos': 0, 'height': -1, 'value': 150000}]\n                else:\n                    return []\n            async def get_transaction(self, txid):\n                if txid == \"9a7bf98ed72b1002559d3d61805838a00e94afec78b8597a68606e2a0725171d\":\n                    return \"020000000001038fc862be3bc8022866cc83b4f2feeaa914b015a3c6644251960baaccc4a5740b0000000000fdffffff7bfd61e391034e28848fae269183f1c5929e26befd5b2d798cf12c91d4d00dbf0100000000fdffffff014764d324e70e7e3e4fa27077bda2d880b3d1545588b75f79deb2855d9f31cb0000000000fdffffff01f04902000000000017a9147d0530db22c8124ff1558269f543dfeedd37131b87024730440220568ae75314f6414ccf2b0bbed522e1b4b1086ed6eb185ba4bc044ba2723c1f3402206c82253797d0f180db38986b46d8ad952829cf25bc31e3ca6ee54665f5a44b3c0121038a466bdcb979b96d70fde84b9ded4aba0c3cd9c0d2d59121fc3555428fd1a4890247304402203ba1b482b0b6ce5c3d29ef21ee8afad641af8381d3b131103c384757922f0c04022072320e260b60fc862669b2ea3dfb663f7f3a0b6babe8d265ac9ebf268e7225c2012103ff0877f34157a3444afbfdd7432032a93187bc1932e1c155d56dd66ef527906c02473044022058b1c1a2a8c1a256d4870b550ba93777a2cce36b89abe3515f024fd4eec48ce4022023e0002193a26064275433e8ade98642d74d58ee4f8e9717a8acca737856a6c401210364e8f5d9c30986931bca1197138d7250a17a0711a223f113b3ccc11ef09efccb2aed2400\"\n                else:\n                    raise Exception(\"unexpected txid\")\n\n        privkeys = ['p2wpkh-p2sh:cQMRGsiEsFX5YoxVZaMEzBruAkCWnoFf1SG7SRm2tLHDEN165TrA',]\n        network = NetworkMock()\n        dest_addr = 'tb1qu7n2tzm90a3f29kvxlhzsc7t40ddk075ut5w44'\n        tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(500), locktime=2420010, tx_version=2)\n\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('020000000001011d1725072a6e60687a59b878ecaf940ea0385880613d9d5502102bd78ef97b9a0000000017160014e7a6a58b657f629516cc37ee2863cbabdadb3fd4fdffffff01fc47020000000000160014e7a6a58b657f629516cc37ee2863cbabdadb3fd402473044022048ea4c558fd374f5d5066440a7f4933393cb377802cb949e3039fedf0378a29402204b4a58c591117cc1e37f07b03cc03cc6198dbf547e2bff813e2e2102bd2057e00121029f46ba81b3c6ad84e52841364dc54ca1097d0c30a68fb529766504c4b1c599352aed2400',\n                         str(tx_copy))\n        self.assertEqual('0680124954ccc158cbf24d289c93579f68fd75916509214066f69e09adda1861', tx_copy.txid())\n        self.assertEqual('da8567d9b28e9e0ed8b3dcef6e619eba330cec6cb0c55d57f658f5ca06e02eb0', tx_copy.wtxid())\n\n    async def test_sweep_p2wpkh(self):\n        class NetworkMock:\n            relay_fee = 1000\n            async def listunspent_for_scripthash(self, scripthash):\n                if scripthash == '7630f6b2121336279b55e5b71d4a59be5ffa782e86bae249ba0b5ad6a791933f':\n                    return [{'tx_hash': '01d76acdb8992f4262fb847f5efbd95ea178049be59c70a2851bdcf9b4ae28e3', 'tx_pos': 0, 'height': 2420006, 'value': 98300}]\n                else:\n                    return []\n            async def get_transaction(self, txid):\n                if txid == \"01d76acdb8992f4262fb847f5efbd95ea178049be59c70a2851bdcf9b4ae28e3\":\n                    return \"02000000000101208840a3310ae4b88181374b5812f56f5dd56f12574f3bcd8041b48bfadc92cf0000000000fdffffff02fc7f010000000000160014d339efed7cd5d28d31995caf10b8973a9a13c656a08601000000000043410403886197eb13c59721b94a29f9a68a841caedb7782b35121cd81d50d0cc70db3f8955c7a07b08dd6470141b66eedd324406e29d6b6799033314512334461e3f9ac0247304402203328153753e934d7a13215bf58f093f84281d57f8c7d42f3b7704cd714c7b32c02205a502f3f3e4302561ccc93df413be3c78a439ff35b60cea03d19f8804a9a1239012103f41052be701441d1bc8f7cc6a6053d7e7f5e63be212fe5e3687344ddd52e3af525ed2400\"\n                else:\n                    raise Exception(\"unexpected txid\")\n\n        privkeys = ['p2wpkh:cV2BvgtpLNX328m4QrhqycBGA6EkZUFfHM9kKjVXjfyD53uNfC4q',]\n        network = NetworkMock()\n        dest_addr = 'tb1qhuy2e45lrdcp9s4ezeptx5kwxcnahzgpar9scc'\n        tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(500), locktime=2420010, tx_version=2)\n\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000000101e328aeb4f9dc1b85a2709ce59b0478a15ed9fb5e7f84fb62422f99b8cd6ad7010000000000fdffffff01087e010000000000160014bf08acd69f1b7012c2b91642b352ce3627db89010247304402204993099c4663d92ef4c9a28b3f45a40a6585754fe22ecfdc0a76c43fda7c9d04022006a75e0fd3ad1862d8e81015a71d2a1489ec7a9264e6e63b8fe6bb90c27e799b0121038ca94e7c715152fd89803c2a40a934c7c4035fb87b3cba981cd1e407369cfe312aed2400',\n                         str(tx_copy))\n        self.assertEqual('e02641928e5394332eec0a36c196f1e30e2b8645ebbeef89d6cc27bf237ae548', tx_copy.txid())\n        self.assertEqual('b062d2e19880c66b36e80b823c2d00a2769658d1e574ff854dab15efd8fd7da8', tx_copy.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_coinjoin_between_two_p2wpkh_electrum_seeds(self, mock_save_db):\n        wallet1 = WalletIntegrityHelper.create_standard_wallet(\n            keystore.from_seed('humor argue expand gain goat shiver remove morning security casual leopard degree', passphrase=''),\n            gap_limit=2,\n            config=self.config\n        )\n        wallet2 = WalletIntegrityHelper.create_standard_wallet(\n            keystore.from_seed('couple fade lift useless text thank badge act august roof drastic violin', passphrase=''),\n            gap_limit=2,\n            config=self.config\n        )\n\n        # bootstrap wallet1\n        funding_tx = Transaction('0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca010851800')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5', funding_txid)\n        wallet1.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # bootstrap wallet2\n        funding_tx = Transaction('02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f784169851800')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5', funding_txid)\n        wallet2.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet1 creates tx1, with output back to himself\n        outputs = [PartialTxOutput.from_address_and_value(\"tb1qhye4wfp26kn0l7ynpn5a4hvt539xc3zf0n76t3\", 10_000_000)]\n        tx1 = wallet1.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=2, rbf=True)\n        tx1.locktime = 1607022\n        partial_tx1 = tx1.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100710200000001d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff02b82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44496e8518000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0100df0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca01085180022060205e8db1b1906219782fadb18e763c0874a3118a17ce931e01707cbde194e041510775087560000008000000000000000000022020240ef5d2efee3b04b313a254df1b13a0b155451581e73943b21f3346bf6e1ba351077508756000000800100000000000000002202024a410b1212e88573561887b2bc38c90c074e4be425b9f3d971a9207825d9d3c8107750875600000080000000000100000000\",\n                         partial_tx1)\n        tx1.prepare_for_export_for_coinjoin()\n        partial_tx1 = tx1.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100710200000001d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff02b82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44496e8518000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0100df0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca010851800000000\",\n                         partial_tx1)\n\n        # wallet2 creates tx2, with output back to himself\n        outputs = [PartialTxOutput.from_address_and_value(\"tb1qufnj5k2rrsnpjq7fg6d2pq3q9um6skdyyehw5m\", 10_000_000)]\n        tx2 = wallet2.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=2, rbf=True)\n        tx2.locktime = 1607023\n        partial_tx2 = tx2.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100710200000001e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffff02988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4c8096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c30100df02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f784169851800220602275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f41067f36697000000800000000001000000002202036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f1067f366970000008001000000000000000022020200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc1067f3669700000080000000000000000000\",\n                         partial_tx2)\n\n        # wallet2 gets raw partial tx1, merges it into his own tx2\n        tx2.join_with_other_psbt(tx_from_any(partial_tx1), config=self.config)\n        partial_tx2 = tx2.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c30100df02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f784169851800220602275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f41067f366970000008000000000010000000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0100df0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca010851800002202036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f1067f3669700000080010000000000000000000022020200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc1067f3669700000080000000000000000000\",\n                         partial_tx2)\n        tx2.prepare_for_export_for_coinjoin()\n        partial_tx2 = tx2.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c30100df02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f7841698518000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0100df0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca0108518000000000000\",\n                         partial_tx2)\n\n        # wallet2 signs\n        wallet2.sign_transaction(tx2, password=None)\n        partial_tx2 = tx2.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c30100df02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f78416985180001070001086b0247304402205106349e1644223b5128009376fc497477227172ac28a54942da58014869d4f502205aa60ba466f53b52c5933c39cfa1ab735c1722029039d7a5a7577789ae891389012102275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f40001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0100df0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca010851800002202036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f1067f3669700000080010000000000000000000022020200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc1067f3669700000080000000000000000000\",\n                         partial_tx2)\n        tx2.prepare_for_export_for_coinjoin()\n        partial_tx2 = tx2.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c30100df02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f78416985180001070001086b0247304402205106349e1644223b5128009376fc497477227172ac28a54942da58014869d4f502205aa60ba466f53b52c5933c39cfa1ab735c1722029039d7a5a7577789ae891389012102275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f40001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0100df0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca0108518000000000000\",\n                         partial_tx2)\n\n        # wallet1 gets raw partial tx2, and signs\n        tx2 = tx_from_any(partial_tx2)\n        wallet1.sign_transaction(tx2, password=None)\n        tx = tx_from_any(tx2.serialize_as_bytes().hex())  # simulates moving partial txn between cosigners\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(\"02000000000102e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a40247304402205106349e1644223b5128009376fc497477227172ac28a54942da58014869d4f502205aa60ba466f53b52c5933c39cfa1ab735c1722029039d7a5a7577789ae891389012102275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f402473044022003010ece3471f7a23f31b2a0fd157f88f7d436c0c73ec408043c7f5dd2b7ccbb02204bd21f5829555c3f94fbd0b5295d1071f739c6b8f2682f8a688e34d0ad26c90101210205e8db1b1906219782fadb18e763c0874a3118a17ce931e01707cbde194e04156f851800\",\n                         str(tx))\n        self.assertEqual('4a33546eeaed0e25f9e6a58968be92a804a7e70a5332360dabc79f93cd059752', tx.txid())\n        self.assertEqual('32584f78479a1b6f7aeff4f4d0e0323b67c36ce155d010f9b324b6189b91a540', tx.wtxid())\n\n        wallet1.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        wallet2.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet level checks\n        self.assertEqual((0, 10995000, 0), wallet1.get_balance())\n        self.assertEqual((0, 10495000, 0), wallet2.get_balance())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_standard_wallet_cannot_sign_multisig_input_even_if_cosigner(self, mock_save_db):\n        \"\"\"Just because our keystore recognizes the pubkeys in a txin, if the prevout does not belong to the wallet,\n        then wallet.is_mine and wallet.can_sign should return False (e.g. multisig input for single-sig wallet).\n        (see issue #5948)\n        \"\"\"\n        wallet_2of2 = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                # seed: frost repair depend effort salon ring foam oak cancel receive save usage\n                # convert_xkey(wallet.get_master_public_key(), \"p2wsh\")\n                keystore.from_xpub('Vpub5gqF73Wpbp9ThwEgZKHLjBDthsatXjajYvrN8CVnkdBYeTR1M1sfZFQqQ5wpKHGhnwKhzgMhaWrtgKG2LthCzxjd653KqKVUAw7UrwYnbKQ'),\n                # seed: bitter grass shiver impose acquire brush forget axis eager alone wine silver\n                # convert_xkey(wallet.get_master_public_key(), \"p2wsh\")\n                keystore.from_xpub('Vpub5gSKXzxK7FeKNi2WPNW9iuA48SbJRZvKFBwtgucpegMWPdohQPeK2DoR6XFtC7BBLsHhfWDAPKaiecqJ7jTzYSfeg5YATowmPcgCWxARabT')\n            ],\n            '2of2', gap_limit=2,\n            config=self.config\n        )\n        wallet_frost = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage')\n\n        # bootstrap wallet_2of2\n        funding_tx = Transaction('020000000001018ed0132bb5f35d097572081524cd5e847c895e765b93d5af46b8a8bef621244a0100000000fdffffff0220a1070000000000220020302981db44eb5dad0dab3987134a985b360ae2227a7e7a10cfe8cffd23bacdc9b07912000000000016001442b423aab2aa803f957084832b10359beaa2469002473044022065c5e28900b4706487223357e8539e176552e3560e2081ac18de7c26e8e420ba02202755c7fc8177ff502634104c090e3fd4c4252bfa8566d4eb6605bb9e236e7839012103b63bbf85ec9e5e312e4d7a2b45e690f48b916a442e787a47a6092d6c052394c5966a1900')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0', funding_txid)\n        wallet_2of2.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1qfrlx5pza9vmez6vpx7swt8yp0nmgz3qa7jjkuf', 100_000)]\n        coins = wallet_2of2.get_spendable_coins(domain=None)\n        tx = wallet_2of2.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 1665628\n\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff01007d0200000001d004b67c3f20e564af8a1ec46db16279be55ebec9f727b9db66c1a9881592f0c0000000000fdffffff02a08601000000000016001448fe6a045d2b3791698137a0e59c817cf681441df806060000000000220020eb428a0bdeca2c1b3731aedb81c0518456875a99755d177d204d6516d8f6b3075c6a19000001012b20a1070000000000220020302981db44eb5dad0dab3987134a985b360ae2227a7e7a10cfe8cffd23bacdc90100ea020000000001018ed0132bb5f35d097572081524cd5e847c895e765b93d5af46b8a8bef621244a0100000000fdffffff0220a1070000000000220020302981db44eb5dad0dab3987134a985b360ae2227a7e7a10cfe8cffd23bacdc9b07912000000000016001442b423aab2aa803f957084832b10359beaa2469002473044022065c5e28900b4706487223357e8539e176552e3560e2081ac18de7c26e8e420ba02202755c7fc8177ff502634104c090e3fd4c4252bfa8566d4eb6605bb9e236e7839012103b63bbf85ec9e5e312e4d7a2b45e690f48b916a442e787a47a6092d6c052394c5966a19000105475221028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c521030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c52ae2206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a903980000008000000000000000002206030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c10b2e35a7d0000008000000000000000000000010147522102105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea21028584e789e39f41391b2f27852ca18abec06a5411c21be350fed61eec7120de5352ae220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a903980000008001000000000000002202028584e789e39f41391b2f27852ca18abec06a5411c21be350fed61eec7120de5310b2e35a7d00000080010000000000000000\",\n                         partial_tx)\n        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n\n        self.assertFalse(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual('652c1a903a659c9fabb9caf4a2281a9fbcc59cd598bf6edc88cd60f940c2352c', tx.txid())\n\n        self.assertEqual('tb1qxq5crk6yadw66rdt8xr3xj5ctvmq4c3z0fl85yx0ar8l6ga6ehysk0rjrk', tx.inputs()[0].address)\n        self.assertEqual('tb1qfrlx5pza9vmez6vpx7swt8yp0nmgz3qa7jjkuf',                     tx.outputs()[0].address)\n        self.assertEqual('tb1qadpg5z77egkpkde34mdcrsz3s3tgwk5ew4w3wlfqf4j3dk8kkvrs3t3mn0', tx.outputs()[1].address)\n\n        # check that wallet_frost does not mistakenly think tx is related to it in any way\n        tx.add_info_from_wallet(wallet_frost)\n        self.assertFalse(wallet_frost.can_sign(tx))\n        self.assertFalse(any([wallet_frost.is_mine(txin.address) for txin in tx.inputs()]))\n        self.assertFalse(any([wallet_frost.is_mine(txout.address) for txout in tx.outputs()]))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_dscancel(self, mock_save_db):\n        self.maxDiff = None\n        config = SimpleConfig({'electrum_path': self.electrum_path})\n        config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = False\n\n        for simulate_moving_txs in (False, True):\n            with self.subTest(msg=\"_dscancel_when_all_outputs_are_ismine\", simulate_moving_txs=simulate_moving_txs):\n                await self._dscancel_when_all_outputs_are_ismine(\n                    simulate_moving_txs=simulate_moving_txs,\n                    config=config)\n            with self.subTest(msg=\"_dscancel_p2wpkh_when_there_is_a_change_address\", simulate_moving_txs=simulate_moving_txs):\n                await self._dscancel_p2wpkh_when_there_is_a_change_address(\n                    simulate_moving_txs=simulate_moving_txs,\n                    config=config)\n            with self.subTest(msg=\"_dscancel_when_user_sends_max\", simulate_moving_txs=simulate_moving_txs):\n                await self._dscancel_when_user_sends_max(\n                    simulate_moving_txs=simulate_moving_txs,\n                    config=config)\n            with self.subTest(msg=\"_dscancel_when_not_all_inputs_are_ismine\", simulate_moving_txs=simulate_moving_txs):\n                await self._dscancel_when_not_all_inputs_are_ismine(\n                    simulate_moving_txs=simulate_moving_txs,\n                    config=config)\n            with self.subTest(msg=\"_dscancel_sufficient_fee_increase\", simulate_moving_txs=simulate_moving_txs):\n                await self._dscancel_sufficient_fee_increase(\n                    simulate_moving_txs=simulate_moving_txs,\n                    config=config)\n\n    async def _dscancel_when_all_outputs_are_ismine(self, *, simulate_moving_txs, config):\n        wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('010000000001011f4db0ecd81f4388db316bc16efb4e9daf874cf4950d54ecb4c0fb372433d68500000000171600143d57fd9e88ef0e70cddb0d8b75ef86698cab0d44fdffffff0280969800000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88ac86a0ae020000000017a9149188bc82bdcae077060ebb4f02201b73c806edc887024830450221008e0725d531bd7dee4d8d38a0f921d7b1213e5b16c05312a80464ecc2b649598d0220596d309cf66d5f47cb3df558dbb43c5023a7796a80f5a88b023287e45a4db6b9012102c34d61ceafa8c216f01e05707672354f8119334610f7933a3f80dd7fb6290296bd391400')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 10000000\n        self.assertEqual('03052739fcfa2ead5f8e57e26021b0c2c546bcd3d74c6e708d5046dc58d90762', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create tx\n        outputs = [PartialTxOutput.from_address_and_value('miFLSDZBXUo4on8PGhTRTAufUn4mP61uoH', '!')]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 1859362\n        tx.version = 2\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff01005502000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc392705030000000000fdffffff01f8829800000000001976a9141df43441a3a3ee563e560d3ddc7e07cc9f9c3cdb88ac225f1c00000100fa010000000001011f4db0ecd81f4388db316bc16efb4e9daf874cf4950d54ecb4c0fb372433d68500000000171600143d57fd9e88ef0e70cddb0d8b75ef86698cab0d44fdffffff0280969800000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88ac86a0ae020000000017a9149188bc82bdcae077060ebb4f02201b73c806edc887024830450221008e0725d531bd7dee4d8d38a0f921d7b1213e5b16c05312a80464ecc2b649598d0220596d309cf66d5f47cb3df558dbb43c5023a7796a80f5a88b023287e45a4db6b9012102c34d61ceafa8c216f01e05707672354f8119334610f7933a3f80dd7fb6290296bd391400220602a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587a0c8296e571000000000000000000220202a7536f0bfbc60c5a8e86e2b9df26431fc062f9f454016dbc26f2467e0bc98b3f0c8296e571000000000100000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet.is_mine(wallet.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('02000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006a47304402200c1ad6499cfd7a808c2463e211e0aaf503a571c85b679e69af215b76f05ad74d022066fccfec30164ad62686734ec3eca024e33e935b1bf30a98df85d87f01ba1b5f012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff01f8829800000000001976a9141df43441a3a3ee563e560d3ddc7e07cc9f9c3cdb88ac225f1c00',\n                         str(tx_copy))\n        self.assertEqual('200d5173d3113e9cec7a63e885b64836245572d93b6dda4035f3ed44341b6277', tx_copy.txid())\n        self.assertEqual('200d5173d3113e9cec7a63e885b64836245572d93b6dda4035f3ed44341b6277', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, funding_output_value - 5000, 0), wallet.get_balance())\n\n        # cancel tx\n        tx_details = wallet.get_tx_info(tx_from_any(tx.serialize()))\n        self.assertFalse(tx_details.can_dscancel)\n\n    async def _dscancel_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config):\n        wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')\n        funding_txid = funding_tx.txid()\n        funding_output_value = 10000000\n        self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create tx\n        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 1325499\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100720100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a903980000008000000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet.is_mine(wallet.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402205442705e988abe74bf391b293bb1b886674284a92ed0788c33024f9336d60aef022013a93049d3bed693254cd31a704d70bb988a36750f0b74d0a5b4d9e29c54ca9d0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',\n                         str(tx_copy))\n        self.assertEqual('b019bbad45a46ed25365e46e4cae6428fb12ae425977eb93011ffb294cb4977e', tx_copy.txid())\n        self.assertEqual('ba87313e2b3b42f1cc478843d4d53c72d6e06f6c66ac8cfbe2a59cdac2fd532d', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance())\n\n        # cancel tx\n        tx_details = wallet.get_tx_info(tx_from_any(tx.serialize()))\n        self.assertTrue(tx_details.can_dscancel)\n        tx = wallet.dscancel(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0)\n        tx.locktime = 1859397\n        tx.version = 2\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100520200000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff016c78980000000000160014f0fe5c1867a174a12e70165e728a072619455ed5455f1c000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a9039800000080000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff016c78980000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402201e706f7ab50e4212a98782e483476102cd6579dad91196002b13dedec79a9a6302205ae30e6c3cf6dd8c566ddae090eeedaac09ba0adc4c0205dfa77bc627621a6b70121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5455f1c00',\n                         str(tx_copy))\n        self.assertEqual('165f82b1440cd3a31c005cec660cf834917a1e0a89011805a620c702840fc46a', tx_copy.txid())\n        self.assertEqual('a164fff4f4231a09e8745eb27d0fe636c5c291400b8506d932b0bde6ff8cf9ee', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 9992300, 0), wallet.get_balance())\n\n    async def _dscancel_when_user_sends_max(self, *, simulate_moving_txs, config):\n        wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',\n                                                       config=config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create tx\n        outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 1325499\n        tx.version = 1\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100530100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a903980000008000000000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        wallet.sign_transaction(tx, password=None)\n\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertTrue(wallet.is_mine(wallet.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n        self.assertEqual(tx.wtxid(), tx_copy.wtxid())\n        self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987024730440220520ab41536d5d0fac8ad44e6aa4a8258a266121bab1eb6599f1ee86bbc65719d02205944c2fb765fca4753a850beadac49f5305c6722410c347c08cec4d90e3eb4430121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400',\n                         str(tx_copy))\n        self.assertEqual('dc4b622f3225f00edb886011fa02b74630cdbc24cebdd3210d5ea3b68bef5cc9', tx_copy.txid())\n        self.assertEqual('a00340ee8c90673e05f2cf368601b6bba6a7f0513bd974feb218a326e39b1874', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 0, 0), wallet.get_balance())\n\n        # cancel tx\n        tx_details = wallet.get_tx_info(tx_from_any(tx.serialize()))\n        self.assertTrue(tx_details.can_dscancel)\n        tx = wallet.dscancel(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0)\n        tx.locktime = 1859455\n        tx.version = 2\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100520200000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff016c78980000000000160014f0fe5c1867a174a12e70165e728a072619455ed57f5f1c000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fda20101000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb3914002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a9039800000080000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff016c78980000000000160014f0fe5c1867a174a12e70165e728a072619455ed502473044022013892ba1580bd8b35fe74cb7a0dceb6914b01ed5cfef6435b94ac0256866971c02200290d08d5f199fcdbba1a2dc4884f5cdea0177cb88e423d8588480d6a5fd62740121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c57f5f1c00',\n                         str(tx_copy))\n        self.assertEqual('42e222b8faff6cb7fcb82697e04f7bc88a5ed57293773a57a5e400ce0450203e', tx_copy.txid())\n        self.assertEqual('0c6511d0c008604948ea68b0f8cb3da00966c5a97a08a220716ff47eecd4922d', tx_copy.wtxid())\n\n        wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual((0, 9992300, 0), wallet.get_balance())\n\n    async def _dscancel_when_not_all_inputs_are_ismine(self, *, simulate_moving_txs, config):\n        class NetworkMock:\n            relay_fee = 1000\n            async def get_transaction(self, txid, timeout=None):\n                if txid == \"597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd\":\n                    return \"02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00\"\n                else:\n                    raise Exception(\"unexpected txid\")\n            def has_internet_connection(self):\n                return True\n            run_from_another_thread = Network.run_from_another_thread\n            def get_local_height(self):\n                return 0\n            def blockchain(self):\n                class BlockchainMock:\n                    def is_tip_stale(self):\n                        return True\n                return BlockchainMock()\n\n        wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve',\n                                                       config=config)\n        wallet.network = NetworkMock()\n\n        # bootstrap wallet\n        funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('08557327673db61cc921e1a30826608599b86457836be3021105c13940d9a9a3', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        orig_rbf_tx = Transaction('02000000000102a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdfffffffd57af9ecf29b1cb42cb91087cf0d1d9fce59a3ca0b25bbfa7d27c07f99870590200000000fdffffff03b2a00700000000001600145dc80fd43eb70fd21a6c4446e3ce043df94f100cb2a00700000000001600147db4ab480b7d2218fba561ff304178f4afcbc972be358900000000001600149d91f0053172fab394d277ae27e9fa5c5a49210902473044022003999f03be8b9e299b2cd3bc7bce05e273d5d9ce24fc47af8754f26a7a13e13f022004e668499a67061789f6ebd2932c969ece74417ae3f2307bf696428bbed4fe36012102a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb0247304402207121358a66c0e716e2ba2be928076736261c691b4fbf89ea8d255449a4f5837b022042cadf9fe1b4f3c03ede3cef6783b42f0ba319f2e0273b624009cd023488c4c1012103a5ba95fb1e0043428ed70680fc17db254b3f701dfccf91e48090aa17c1b7ea40fef61c00')\n        orig_rbf_txid = orig_rbf_tx.txid()\n        self.assertEqual('6057690010ddac93a371629e1f41866400623e13a9cd336d280fc3239086a983', orig_rbf_txid)\n        wallet.adb.receive_tx_callback(orig_rbf_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # bump tx\n        orig_rbf_tx = tx_from_any(orig_rbf_tx.serialize())\n        orig_rbf_tx.add_info_from_wallet(wallet=wallet)\n        await orig_rbf_tx.add_info_from_network(network=wallet.network)\n        tx = wallet.dscancel(tx=orig_rbf_tx, new_fee_rate=70)\n        tx.locktime = 1898278\n        tx.version = 2\n        if simulate_moving_txs:\n            partial_tx = tx.serialize_as_bytes().hex()\n            self.assertEqual(\"70736274ff0100520200000001a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdffffff010c830700000000001600145dc80fd43eb70fd21a6c4446e3ce043df94f100c26f71c000001011f20a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec7270100de02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00220602a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb109c9fff980000008000000000000000000022020353becea8bbfe746452e5d2fa2e0688013e43ca6409c8e30b6cc99e7625ff2265109c9fff9800000080000000000100000000\",\n                             partial_tx)\n            tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000000101a3a9d94039c1051102e36b835764b89985602608a3e121c91cb63d67277355080000000000fdffffff010c830700000000001600145dc80fd43eb70fd21a6c4446e3ce043df94f100c0247304402202e75e1edceb8ce27d75814bc7895bc48a0d5c423b492b980b655908612485cc8022072a947c4516ab220d0825634efd8b1ad3a5503e63ed8fbb97700b5d73786c63f012102a1c9b25b37aa31ccbb2d72caaffce81ec8253020a74017d92bbfc14a832fc9cb26f71c00',\n                         str(tx_copy))\n        self.assertEqual('3021a4fe24e33af9d0ccdf25c478387c97df671fe1fd8b4db0de4255b3a348c5', tx_copy.txid())\n\n    async def _dscancel_sufficient_fee_increase(self, *, simulate_moving_txs, config):\n        \"\"\"\n        Tries to cancel a tx with a replacement tx of the same feerate as the original tx. This shouldn't\n        work as the feerate needs to be higher.\n        \"\"\"\n        wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',\n                                                       config=config)\n        # create tx\n        tx_to_cancel_raw = '70736274ff0100d10200000002032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610000000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610100000000fdffffff0378d4030000000000220020f381d0d2c633cdd890015bb438c8e73e9960b21defa126c594e5cf67bd06419f78d40300000000002200204a1c25db1aa7165cb655638fb32319a945262fee7c8ce6f2f17f1223679bb0b84072070000000000160014f0fe5c1867a174a12e70165e728a072619455ed5000000000001011f20a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f80100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220603565a3904c7d2d6a6c2cf3fcdf89d9e5c60b509483104992cfdf85b196665170c10e8a903980000008000000000020000000001011f20a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a90398000000800000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000'\n        tx_to_cancel = tx_from_any(tx_to_cancel_raw)\n        if simulate_moving_txs:\n            partial_tx = tx_to_cancel.serialize_as_bytes().hex()\n            self.assertEqual(tx_to_cancel_raw,\n                             partial_tx)\n            tx_to_cancel = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        tx_to_cancel = wallet.sign_transaction(tx_to_cancel, password=None)\n        wallet.adb.receive_tx_callback(tx_to_cancel, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        self.assertTrue(tx_to_cancel.is_complete())\n        self.assertTrue(tx_to_cancel.is_segwit())\n        self.assertEqual(2, len(tx_to_cancel.inputs()))\n        self.assertEqual(3, len(tx_to_cancel.outputs()))\n\n        # cancel tx\n        tx_details = wallet.get_tx_info(tx_to_cancel)\n        self.assertTrue(tx_details.can_dscancel)\n        tx_to_cancel_feerate = tx_to_cancel.get_fee() / tx_to_cancel.estimated_size()\n        with self.assertRaises(CannotDoubleSpendTx):\n            wallet.dscancel(tx=tx_to_cancel, new_fee_rate=tx_to_cancel_feerate)\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_wallet_history_chain_of_unsigned_transactions(self, mock_save_db):\n        wallet = self.create_standard_wallet_from_seed('cross end slow expose giraffe fuel track awake turtle capital ranch pulp',\n                                                       config=self.config, gap_limit=3)\n\n        # bootstrap wallet\n        funding_tx = Transaction('0200000000010132515e6aade1b79ec7dd3bac0896d8b32c56195d23d07d48e21659cef24301560100000000fdffffff0112841e000000000016001477fe6d2a27e8860c278d4d2cd90bad716bb9521a02473044022041ed68ef7ef122813ac6a5e996b8284f645c53fbe6823b8e430604a8915a867802203233f5f4d347a687eb19b2aa570829ab12aeeb29a24cc6d6d20b8b3d79e971ae012102bee0ee043817e50ac1bb31132770f7c41e35946ccdcb771750fb9696bdd1b307ad951d00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('db949963c3787c90a40fb689ffdc3146c27a9874a970d1fd20921afbe79a7aa9', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create tx1\n        outputs = [PartialTxOutput.from_address_and_value('tb1qsfcddwf7yytl62e3catwv8hpl2hs9e36g2cqxl', 100000)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(190))\n        tx.set_rbf(True)\n        tx.locktime = 1938861\n        tx.version = 2\n        self.assertEqual(\"70736274ff0100710200000001a97a9ae7fb1a9220fdd170a974987ac24631dcff89b60fa4907c78c3639994db0000000000fdffffff02a0860100000000001600148270d6b93e2117fd2b31c756e61ee1faaf02e63ab4fc1c0000000000160014b8e4fdc91593b67de2bf214694ef47e38dc2ee8ead951d000001011f12841e000000000016001477fe6d2a27e8860c278d4d2cd90bad716bb9521a0100bf0200000000010132515e6aade1b79ec7dd3bac0896d8b32c56195d23d07d48e21659cef24301560100000000fdffffff0112841e000000000016001477fe6d2a27e8860c278d4d2cd90bad716bb9521a02473044022041ed68ef7ef122813ac6a5e996b8284f645c53fbe6823b8e430604a8915a867802203233f5f4d347a687eb19b2aa570829ab12aeeb29a24cc6d6d20b8b3d79e971ae012102bee0ee043817e50ac1bb31132770f7c41e35946ccdcb771750fb9696bdd1b307ad951d002206026cc6a74c2b0e38661d341ffae48fe7dde5196ca4afe95d28b496673fa4cf6467105f83afb40000008000000000000000000022020312ea49b9b1eea28e3330316a5b7e6673b43e01da38f802c99a777d30b903fa5e105f83afb40000008000000000010000000022020349321bee98c012887997f26c6400018b0711dd254b702c038b96a30ebe2af1d2105f83afb400000080010000000000000000\",\n                         tx.serialize_as_bytes().hex())\n        self.assertFalse(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        wallet.adb.add_transaction(tx)\n\n        # create tx2, which spends from unsigned tx1\n        outputs = [PartialTxOutput.from_address_and_value('tb1qq0lm9esmq6pfjc3jls7v6twy93lnqcs85wlth3', '!')]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 1938863\n        tx.version = 2\n        self.assertEqual(\"70736274ff01007b020000000288234495e0ff1d8ac06038f6cc5d5a92738d719f4c15afd581366da94754478f0000000000fdffffff88234495e0ff1d8ac06038f6cc5d5a92738d719f4c15afd581366da94754478f0100000000fdffffff01cc6f1e000000000016001403ffb2e61b0682996232fc3ccd2dc42c7f306207af951d000001011fa0860100000000001600148270d6b93e2117fd2b31c756e61ee1faaf02e63a22060312ea49b9b1eea28e3330316a5b7e6673b43e01da38f802c99a777d30b903fa5e105f83afb40000008000000000010000000001011fb4fc1c0000000000160014b8e4fdc91593b67de2bf214694ef47e38dc2ee8e22060349321bee98c012887997f26c6400018b0711dd254b702c038b96a30ebe2af1d2105f83afb4000000800100000000000000002202036f9a5913f1c22742dbc9e7f3ac3064be8b125a23563fcc8a519f387e16c7244c105f83afb400000080000000000200000000\",\n                         tx.serialize_as_bytes().hex())\n        self.assertFalse(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        wallet.adb.add_transaction(tx)\n\n        coins = wallet.get_spendable_coins(domain=None)\n        self.assertEqual(1, len(coins))\n        self.assertEqual(\"bf08206effded4126a95fbed375cedc0452b5e16a5d2025ac645dfae81addbe4:0\",\n                         coins[0].prevout.to_str())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_wallet_does_not_create_zero_input_tx(self, mock_save_db):\n        wallet = self.create_standard_wallet_from_seed('cross end slow expose giraffe fuel track awake turtle capital ranch pulp',\n                                                       config=self.config, gap_limit=3)\n\n        with self.subTest(msg=\"no coins to use as inputs, max output value, zero fee\"):\n            outputs = [PartialTxOutput.from_address_and_value('tb1qsfcddwf7yytl62e3catwv8hpl2hs9e36g2cqxl', '!')]\n            coins = wallet.get_spendable_coins(domain=None)\n            with self.assertRaises(NotEnoughFunds):\n                tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(0))\n\n        # bootstrap wallet\n        funding_tx = Transaction('0200000000010132515e6aade1b79ec7dd3bac0896d8b32c56195d23d07d48e21659cef24301560100000000fdffffff0112841e000000000016001477fe6d2a27e8860c278d4d2cd90bad716bb9521a02473044022041ed68ef7ef122813ac6a5e996b8284f645c53fbe6823b8e430604a8915a867802203233f5f4d347a687eb19b2aa570829ab12aeeb29a24cc6d6d20b8b3d79e971ae012102bee0ee043817e50ac1bb31132770f7c41e35946ccdcb771750fb9696bdd1b307ad951d00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('db949963c3787c90a40fb689ffdc3146c27a9874a970d1fd20921afbe79a7aa9', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        with self.subTest(msg=\"funded wallet, zero output value, zero fee\"):\n            outputs = [PartialTxOutput.from_address_and_value('tb1qsfcddwf7yytl62e3catwv8hpl2hs9e36g2cqxl', 0)]\n            coins = wallet.get_spendable_coins(domain=None)\n            tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(0))\n            self.assertEqual(1, len(tx.inputs()))\n            self.assertEqual(2, len(tx.outputs()))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_wallet_adb_gettxheight_treats_mempool_txid_as_local_if_missing_fulltx(self, mock_save_db):\n        wallet = self.create_standard_wallet_from_seed('dismiss smile transfer input market ten damage city duck dolphin entire because',\n                                                       config=self.config, gap_limit=2)\n        self.assertEqual(0, len(wallet.get_spendable_coins()))\n\n        # fund wallet with two utxos\n        funding_tx1 = Transaction('02000000000101bf03f2d37ae084d729e5685d64988c92e8a98cb73062802646dfbb10d77e88410000000000fdffffff02a03007000000000016001443a24a730a7ddd2ce4da777a949a9e87c6ad870920a107000000000016001447597395323a834378d7577d848187684d0d70fe0247304402200e6f1898a0681c4ff1f5995b357c3388ca53fcf56760e0d14d4ea72c48d1134b0220683b8e5045743c087d488dfc5f8c5b7369ff92f611595eaba0dbb0c0009c816e0121021bd313412fad3802801f6c45321a10c7bf35603bf8571aa263ece764d1ab7ef1a2434300')\n        wallet.adb.receive_tx_callback(funding_tx1, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(1, len(wallet.get_spendable_coins(nonlocal_only=True)))\n        self.assertEqual(1, len(wallet.get_spendable_coins(nonlocal_only=False)))\n\n        funding_tx2 = Transaction('02000000000101d8a9691c534e90655623cd1a642c3b3f31db09548a5922e0218289a34daf27fc0000000000fdffffff021061070000000000160014910f3a772d33c615abe4f1c346476cae1414f6d7c027090000000000160014071955c9141dfaa8df1abbfe04527ff061b652450247304402203e45c9d4191239273af9fa97eb986f66afe66345a2f2b6284e214ab91fce072802205a50e8b74f191202442876d6a0cd7e95262e6c125a21eebaebe0bd93aa15107f0121022e8590152fad3aa6a8730648dfcb84ebe432c9190987d498b81707588e40626da2434300')\n        wallet.adb.receive_tx_callback(funding_tx2, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=True)))\n        self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False)))\n\n        # create payment_tx that spends utxo1 and creates a change txo\n        outputs = [PartialTxOutput.from_address_and_value('tb1qrxrp08s5d4cgudlmyfasyme9rgxc7n6z29g2m9', 200_000)]\n        coins = wallet.get_spendable_coins()\n        payment_tx = wallet.make_unsigned_transaction(coins=[coins[0]], outputs=outputs, fee_policy=FixedFeePolicy(0))\n        payment_txid = payment_tx.txid()\n        assert payment_txid\n        # save payment_tx as LOCAL and UNSIGNED\n        wallet.adb.add_transaction(payment_tx)\n        self.assertEqual(TX_HEIGHT_LOCAL, wallet.adb.get_tx_height(payment_txid).height())\n        self.assertEqual(1, len(wallet.get_spendable_coins(nonlocal_only=True)))\n        self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False)))\n        # transition payment_tx to mempool (but it is still unsigned!)\n        #   This can happen organically in a workflow if\n        #     1. we save as local an unsigned tx,\n        #     2. sign+broadcast it, but we don't save the signed tx as local,\n        #     3. then some RTTs later the server will tell us that the txid is now in the mempool (or mined),\n        #     4. then yet more RTTs later we request and receive the full tx from the server\n        #   between (3) and (4), the wallet could consider txid to be mempool/mined,\n        #   but the wallet db does not yet have the corresponding full tx.\n        #   In such cases, we instead want the txid to be considered LOCAL.\n        wallet.adb.receive_tx_callback(payment_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(TX_HEIGHT_LOCAL, wallet.adb.get_tx_height(payment_txid).height())\n        self.assertEqual(1, len(wallet.get_spendable_coins(nonlocal_only=True)))\n        self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False)))\n        # wallet gets signed tx (e.g. from network).  payment_tx is now considered to be in mempool\n        wallet.sign_transaction(payment_tx, password=None)\n        wallet.adb.receive_tx_callback(payment_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(TX_HEIGHT_UNCONFIRMED, wallet.adb.get_tx_height(payment_txid).height())\n        self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=True)))\n        self.assertEqual(2, len(wallet.get_spendable_coins(nonlocal_only=False)))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_wallet_adb_gettxheight_treats_future_txid_as_future_even_if_missing_fulltx(self, mock_save_db):\n        wallet = self.create_standard_wallet_from_seed('dismiss smile transfer input market ten damage city duck dolphin entire because',\n                                                       config=self.config, gap_limit=2)\n        # fund wallet\n        funding_tx1 = Transaction('02000000000101bf03f2d37ae084d729e5685d64988c92e8a98cb73062802646dfbb10d77e88410000000000fdffffff02a03007000000000016001443a24a730a7ddd2ce4da777a949a9e87c6ad870920a107000000000016001447597395323a834378d7577d848187684d0d70fe0247304402200e6f1898a0681c4ff1f5995b357c3388ca53fcf56760e0d14d4ea72c48d1134b0220683b8e5045743c087d488dfc5f8c5b7369ff92f611595eaba0dbb0c0009c816e0121021bd313412fad3802801f6c45321a10c7bf35603bf8571aa263ece764d1ab7ef1a2434300')\n        wallet.adb.receive_tx_callback(funding_tx1, tx_height=TX_HEIGHT_UNCONFIRMED)\n        # create payment_tx that spends utxo1 and creates a change txo\n        outputs = [PartialTxOutput.from_address_and_value('tb1qrxrp08s5d4cgudlmyfasyme9rgxc7n6z29g2m9', 200_000)]\n        coins = wallet.get_spendable_coins()\n        payment_tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(0))\n        payment_txid = payment_tx.txid()\n        assert payment_txid\n        # save payment_tx as LOCAL and UNSIGNED\n        wallet.adb.add_transaction(payment_tx)\n        self.assertEqual(TX_HEIGHT_LOCAL, wallet.adb.get_tx_height(payment_txid).height())\n        # mark payment_tx as future\n        wallet.adb.set_future_tx(payment_txid, wanted_height=300)\n        self.assertEqual(TX_HEIGHT_FUTURE, wallet.adb.get_tx_height(payment_txid).height())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_imported_wallet_usechange_off(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            \"p2wpkh:cVcwSp488C8Riguq55Tuktgi6TpzuyLdDwUxkBDBz3yzV7FW4af2 p2wpkh:cPWyoPvnv2hiyyxbhMkhX3gPEENzB6DqoP9bbR8SDTg5njK5SL9n\",\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']  # type: Abstract_Wallet\n\n        # bootstrap wallet\n        funding_tx = Transaction('02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('9bed2a210b4154183295bc7b78c8841a3a6116197713f744e5cd95ab0c0c01ce', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # imported wallets do not send change to change addresses by default\n        # (they send it back to the \"from address\")\n        self.assertFalse(wallet.use_change)\n\n        outputs = [PartialTxOutput.from_address_and_value('tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v', 49646)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000))\n        tx.set_rbf(True)\n        tx.locktime = 2004420\n        tx.version = 2\n\n        # check that change is sent back to the \"from address\"\n        self.assertEqual(2, len(tx.outputs()))\n        self.assertTrue(tx.output_value_for_address(\"tb1q0fj7pxa3m2q2hlr964zn3z3wvx4t03ep5fgnhy\") > 0)\n        self.assertEqual(49646, tx.output_value_for_address(\"tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v\"))\n\n        self.assertEqual(\"70736274ff0100710200000001ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac00000000000001600147a65e09bb1da80abfc65d545388a2e61aab7c721eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b240c4951e000001011fa0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210100de02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00000000\",\n                         tx.serialize_as_bytes().hex())\n        wallet.sign_transaction(tx, password=None)\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000000101ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac00000000000001600147a65e09bb1da80abfc65d545388a2e61aab7c721eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b240024730440220526eac6c56cba19842b67f6c9e45af113b1a2d44fb229335bdeaf08cb2cc164e0220087fba65619016fd3f62f6c8717070e48f94b45743b86d8e0517698d2b9c3afc012102d67eaa10463f5c786271feb9ae3456c27d35c3cf6c7d881617e915d1f32cb875c4951e00',\n                         str(tx_copy))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_imported_wallet_usechange_on(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            \"p2wpkh:cVcwSp488C8Riguq55Tuktgi6TpzuyLdDwUxkBDBz3yzV7FW4af2 p2wpkh:cPWyoPvnv2hiyyxbhMkhX3gPEENzB6DqoP9bbR8SDTg5njK5SL9n\",\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']  # type: Abstract_Wallet\n\n        # bootstrap wallet\n        funding_tx = Transaction('02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('9bed2a210b4154183295bc7b78c8841a3a6116197713f744e5cd95ab0c0c01ce', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # instead of sending the change back to the \"from address\", we want it sent to another unused address\n        wallet.use_change = True\n\n        outputs = [PartialTxOutput.from_address_and_value('tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v', 49646)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000))\n        tx.set_rbf(True)\n        tx.locktime = 2004420\n        tx.version = 2\n\n        # check that change is sent to another unused imported address\n        self.assertEqual(2, len(tx.outputs()))\n        self.assertTrue(tx.output_value_for_address(\"tb1qetcgdwuzlpdnt5fmzxxdpczjhadz06cynpttpv\") > 0)\n        self.assertEqual(49646, tx.output_value_for_address(\"tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v\"))\n\n        self.assertEqual(\"70736274ff0100710200000001ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac0000000000000160014caf086bb82f85b35d13b118cd0e052bf5a27eb04eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b240c4951e000001011fa0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210100de02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00000000\",\n                         tx.serialize_as_bytes().hex())\n        wallet.sign_transaction(tx, password=None)\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000000101ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac0000000000000160014caf086bb82f85b35d13b118cd0e052bf5a27eb04eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b24002473044022006dfe30f851b0174e5c920fd5b2e294a25fe5d449b17b422f3fda485d514c39b022047a6760f9d6ddfac5273094bed1f640fc1622a42938ebfb0b5f61cce7b161a00012102d67eaa10463f5c786271feb9ae3456c27d35c3cf6c7d881617e915d1f32cb875c4951e00',\n                         str(tx_copy))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_imported_wallet_usechange_on__no_more_unused_addresses(self, mock_save_db):\n        wallet = restore_wallet_from_text__for_unittest(\n            \"p2wpkh:cVcwSp488C8Riguq55Tuktgi6TpzuyLdDwUxkBDBz3yzV7FW4af2 p2wpkh:cPWyoPvnv2hiyyxbhMkhX3gPEENzB6DqoP9bbR8SDTg5njK5SL9n\",\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']  # type: Abstract_Wallet\n\n        # bootstrap wallet\n        funding_tx = Transaction('02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('9bed2a210b4154183295bc7b78c8841a3a6116197713f744e5cd95ab0c0c01ce', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        # add more txs so that all addresses become used\n        _txs = [\n            (\"077c8f7a3b0cbb660192c3e35d01a65694f7b90b10e4c6434713912c44cdbfb7\", \"02000000000101bc125beec2014e3b89679207116e28bcf5bf85cab63ac2903119c8c21ab84cac0100000000fdffffff02daff000000000000160014caf086bb82f85b35d13b118cd0e052bf5a27eb04814201000000000016001491145275b4c4a4814b733fbd28f2a519a5874bad02473044022008ae14e4f7802639a34e92348db7eef95c9fb5d480d7a110d4b11e7d0c45a0cc02205d29414eebcdc76a07f5e2422ed3e560cd663de4b733a0f9c7b3ad7102a733510121030438b8bdbe8121b6a6508e54247b9d1b0547d9ac94c4d3154afd7d7376fe7ae6b6951e00\"),\n            (\"5f8e17612ad4e04819f1b1cf9039509518e230db07140b2eec81582a8647f8d6\", \"02000000000101b7bfcd442c91134743c6e4100bb9f79456a6015de3c3920166bb0c3b7a8f7c070000000000fdffffff016cff0000000000001600146a84f3681e545d13fa41de090b6e404401198e7d0247304402204e16704d836cb6e1fffa34244c42578267853e8c3933a3d367bd6a236c24596a0220025a7be9483eeba06a433b96b5cb35a6a4b117ffa884569b09cedc4a5f3d6381012103c19caa2ced1b74bf31ba7885d83eeda35c0011e740273ebdf6750e0298588cc5c7951e00\"),\n        ]\n        for txid, rawtx in _txs:\n            tx = Transaction(rawtx)\n            self.assertEqual(txid, tx.txid())\n            wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # instead of sending the change back to the \"from address\", we want it sent to another unused address.\n        # (except all our addresses are used! so we expect change sent back to \"from address\")\n        wallet.use_change = True\n\n        outputs = [PartialTxOutput.from_address_and_value('tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v', 49646)]\n        coins = wallet.get_spendable_coins(domain=None)\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000))\n        tx.set_rbf(True)\n        tx.locktime = 2004420\n        tx.version = 2\n\n        # check that change is sent back to the \"from address\"\n        self.assertEqual(2, len(tx.outputs()))\n        self.assertTrue(tx.output_value_for_address(\"tb1q0fj7pxa3m2q2hlr964zn3z3wvx4t03ep5fgnhy\") > 0)\n        self.assertEqual(49646, tx.output_value_for_address(\"tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v\"))\n\n        self.assertEqual(\"70736274ff0100710200000001ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac00000000000001600147a65e09bb1da80abfc65d545388a2e61aab7c721eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b240c4951e000001011fa0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210100de02000000000101c6edaaf0157020a38de8b07810b22ffe331d5b79c83b680dad24da15c572ae7d0000000000fdffffff026080010000000000160014eabbd791df76eeeaa3ed273cac4e1dde3be295cca0860100000000001600147a65e09bb1da80abfc65d545388a2e61aab7c7210247304402203cb8b2f84ed4fb8de5f51a07b2159bc0d8d474e5dba0f77cc66ab641cf48621b022076fb3c6b4bc76aa06dd29ebe1dd081c063cdbd2949ffcf4ab4bd8bddae6c948b0121029f16b602a6b3c738b66a03dd5133abe810169a377bbc2fdf5c5363f59b8d9bdec3951e00000000\",\n                         tx.serialize_as_bytes().hex())\n        wallet.sign_transaction(tx, password=None)\n        tx_copy = tx_from_any(tx.serialize())\n        self.assertEqual('02000000000101ce010c0cab95cde544f713771916613a1a84c8787bbc95321854410b212aed9b0100000000fdffffff02cac00000000000001600147a65e09bb1da80abfc65d545388a2e61aab7c721eec100000000000016001405424089c64d39d9a498b6e1c8e646327431b240024730440220526eac6c56cba19842b67f6c9e45af113b1a2d44fb229335bdeaf08cb2cc164e0220087fba65619016fd3f62f6c8717070e48f94b45743b86d8e0517698d2b9c3afc012102d67eaa10463f5c786271feb9ae3456c27d35c3cf6c7d881617e915d1f32cb875c4951e00',\n                         str(tx_copy))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_get_spendable_coins(self, mock_save_db):\n        wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',\n                                                       config=self.config)\n\n        # bootstrap wallet (incoming funding_tx1)\n        funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400')\n        funding_txid1 = funding_tx1.txid()\n        self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid1)\n        wallet.adb.receive_tx_callback(funding_tx1, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # another incoming transaction (funding_tx2)\n        funding_tx2 = Transaction('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400')\n        funding_txid2 = funding_tx2.txid()\n        self.assertEqual('c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a', funding_txid2)\n        wallet.adb.receive_tx_callback(funding_tx2, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        self.assertEqual((0, 15_000_000, 0), wallet.get_balance())\n        self.assertEqual(\n            {'c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a:1',\n             '52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0:1'},\n            {txi.prevout.to_str() for txi in wallet.get_spendable_coins()})\n        self.assertEqual(\n            {'52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0:1'},\n            {txi.prevout.to_str() for txi in wallet.get_spendable_coins([\"tb1q6n99dl96mx8mfh90m3tn5awk5mllkzdh25dw7z\"])})\n\n        utxo1 = \"c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a:1\"\n        utxo2 = \"52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0:1\"\n\n        # test freezing an address\n        with self.subTest(msg=\"freeze_address\"):\n            wallet.set_frozen_state_of_addresses([\"tb1q6n99dl96mx8mfh90m3tn5awk5mllkzdh25dw7z\"], freeze=True)\n            self.assertEqual(\n                {utxo1},\n                {txi.prevout.to_str() for txi in wallet.get_spendable_coins()})\n            wallet.set_frozen_state_of_addresses([\"tb1q6n99dl96mx8mfh90m3tn5awk5mllkzdh25dw7z\"], freeze=False)\n            self.assertEqual(\n                {utxo1, utxo2},\n                {txi.prevout.to_str() for txi in wallet.get_spendable_coins()})\n\n        # test freezing a utxo\n        with self.subTest(msg=\"freeze_coin\"):\n            self.assertTrue(utxo1 not in wallet._frozen_coins)\n\n            wallet.set_frozen_state_of_coins([utxo1], freeze=True)\n            self.assertEqual(wallet._frozen_coins.get(utxo1), True)\n            self.assertEqual(\n                {utxo2},\n                {txi.prevout.to_str() for txi in wallet.get_spendable_coins()})\n\n            wallet.set_frozen_state_of_coins([utxo1], freeze=False)\n            self.assertEqual(wallet._frozen_coins.get(utxo1), False)\n            self.assertEqual(\n                {utxo1, utxo2},\n                {txi.prevout.to_str() for txi in wallet.get_spendable_coins()})\n\n            wallet.set_frozen_state_of_coins([utxo1], freeze=None)\n            self.assertTrue(utxo1 not in wallet._frozen_coins)\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_export_psbt_with_xpubs__multisig(self, mock_save_db):\n        \"\"\"When exporting a PSBT to be signed by a hw device, test that we populate\n        the PSBT_GLOBAL_XPUB field with wallet xpubs.\n        \"\"\"\n        wallet = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                # bip39 seed: \"pulse mixture jazz invite dune enrich minor weapon mosquito flight fly vapor\"\n                # der path: m/48'/1'/0'/2'\n                keystore.from_xpub('Vpub5n73Y3mMpc5vXFt3EUzvWjLdTrsDw3X4ksZ7GxZHi8yrGc4zBEyd77VzKaC21A4FmGqDMKwcVKFpmLUSzFM6LG84HjMfcLcbvyM1oGj5LGd'),\n                # bip39 seed: \"treat dwarf wealth gasp brass outside high rent blood crowd make initial\"\n                # der path: m/9999'\n                keystore.from_xpub('Vpub5gDjDJrhnjJRXQwyhugFGx8u9B88wQ2ZkDNPoTVtYzPvu2ykP75yVhVzqnJYukhiqZ8X5FpULWYEXTs3Ve3A1Zo2hgson1Q9qPzz8uxL63m')\n            ],\n            '2of2', gap_limit=2,\n            config=self.config\n        )\n\n        # bootstrap wallet\n        funding_tx = Transaction('02000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a2400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('98c039c9b528a8edf2c64e295bb50cf773ddbf418c98119ef54c31b60e73c322', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        outputs = [PartialTxOutput.from_address_and_value(\"tb1q0ezagv55krljkz9973fryeyczhj3dnlsgr02g7\", 123456)]\n        coins = wallet.get_spendable_coins(domain=None)\n\n        # create spending tx\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.version = 2\n        tx.locktime = 2378363\n        self.assertEqual(\"04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035\", tx.txid())\n        self.assertEqual(\n            \"wsh(sortedmulti(2,[9559fbd1/9999h]tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK/0/0,[015148ee]tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg/0/0))\",\n            tx.inputs()[0].script_descriptor.to_string_no_checksum())\n        self.assertEqual({}, tx.to_json()['xpubs'])\n        self.assertEqual(\n            {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', \"m/9999h/0/0\"),\n             '03cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca': ('015148ee', \"m/0/0\")},\n            tx.inputs()[0].to_json()['bip32_paths'])\n        self.assertEqual(\"70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca0c015148ee000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd0c015148ee010000000000000000\",\n                         tx.serialize_as_bytes().hex())\n        await tx.prepare_for_export_for_hardware_device(wallet)\n        # As the keystores were created from just xpubs, they are missing key origin information\n        # (derivation prefix and root fingerprint).\n        # Note that info for ks1 contains the expected bip32 path (m/9999') and fingerprint, but not ks0.\n        # It just so happens that as the der prefix is shallow (<=1 deep) for ks1, we can read it from the xpub itself.\n        # For ks0, as the der prefix is missing, we treat the given xpub as the root.\n        # Note that xpub0 itself has to be changed as its serialisation includes depth/fp/child_num.\n        self.assertEqual(\n            {'tpubD6NzVbkrYhZ4WW1saJM1hDjGz1rm5swdKwbhcsx9hW5VVXDdbnt6GbXEQVXQq97dYsvGVeMEw5Ge2Zx4QGBy6W5KXahih4aTRs5hLqgy9c9': ('015148ee', 'm'),\n             'tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK': ('9559fbd1', \"m/9999h\")},\n            tx.to_json()['xpubs'])\n        self.assertEqual(\"70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24004f01043587cf0000000000000000001044dcc4a72f0084f25ca3b7927abd5596715a515e2a59004ce10a51a17cf4b403a5b8b89c28c5a51832be51bb184749ac2ea6c561259bfc5bf58b852ad60f6fe404015148ee4f01043587cf019559fbd18000270f1b7a7db8a20f23be687941c8bcc8b330fd8823f19eea6ad5cb4af09b00cf6fd802db662ac8cf00e16cebe67e4d9f88b266eddbe0dfbb24b884bf3002b68ade721b089559fbd10f2700800001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca0c015148ee000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd0c015148ee010000000000000000\",\n                         tx.serialize_as_bytes().hex())\n\n        # create spending tx again, but now we have full key origin info\n        wallet.get_keystores()[0].add_key_origin(derivation_prefix=\"m/48'/1'/0'/2'\", root_fingerprint=\"30cf1be5\")\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.version = 2\n        tx.locktime = 2378363\n        self.assertEqual(\"04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035\", tx.txid())\n        self.assertEqual(\n            \"wsh(sortedmulti(2,[9559fbd1/9999h]tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK/0/0,[30cf1be5/48h/1h/0h/2h]tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg/0/0))\",\n            tx.inputs()[0].script_descriptor.to_string_no_checksum())\n        self.assertEqual({}, tx.to_json()['xpubs'])\n        self.assertEqual(\n            {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', \"m/9999h/0/0\"),\n             '03cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca': ('30cf1be5', \"m/48h/1h/0h/2h/0/0\")},\n            tx.inputs()[0].to_json()['bip32_paths'])\n        self.assertEqual(\"70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca1c30cf1be530000080010000800000008002000080000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd1c30cf1be530000080010000800000008002000080010000000000000000\",\n                         tx.serialize_as_bytes().hex())\n        await tx.prepare_for_export_for_hardware_device(wallet)\n        self.assertEqual(\n            {'tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg': ('30cf1be5', \"m/48h/1h/0h/2h\"),\n             'tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK': ('9559fbd1', \"m/9999h\")},\n            tx.to_json()['xpubs'])\n        self.assertEqual(\"70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24004f01043587cf04b5faa014800000021044dcc4a72f0084f25ca3b7927abd5596715a515e2a59004ce10a51a17cf4b403a5b8b89c28c5a51832be51bb184749ac2ea6c561259bfc5bf58b852ad60f6fe41430cf1be5300000800100008000000080020000804f01043587cf019559fbd18000270f1b7a7db8a20f23be687941c8bcc8b330fd8823f19eea6ad5cb4af09b00cf6fd802db662ac8cf00e16cebe67e4d9f88b266eddbe0dfbb24b884bf3002b68ade721b089559fbd10f2700800001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca1c30cf1be530000080010000800000008002000080000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd1c30cf1be530000080010000800000008002000080010000000000000000\",\n                         tx.serialize_as_bytes().hex())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_export_psbt_with_xpubs__singlesig(self, mock_save_db):\n        \"\"\"When exporting a PSBT to be signed by a hw device, test that we populate\n        the PSBT_GLOBAL_XPUB field with wallet xpubs.\n        \"\"\"\n        root_seed = keystore.bip39_to_seed(\"pulse mixture jazz invite dune enrich minor weapon mosquito flight fly vapor\", passphrase='')\n        ks = keystore.from_bip43_rootseed(root_seed, derivation=\"m/84'/1'/0'\")\n        wallet = WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=2, config=self.config)\n\n        # bootstrap wallet\n        funding_tx = Transaction('0200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a2400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('c70d83827d09b334bb373738be25c93dbe7dd37186d09bb10cae80704da06f91', funding_txid)\n        wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        outputs = [PartialTxOutput.from_address_and_value(\"tb1q0ezagv55krljkz9973fryeyczhj3dnlsgr02g7\", 123456)]\n        coins = wallet.get_spendable_coins(domain=None)\n\n        # create spending tx\n        tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.version = 2\n        tx.locktime = 2378367\n        self.assertEqual(\"5c0d5eea8c2c12a383406bb37e6158167e44bfe6cd1ad590b7d97002cdfc9fff\", tx.txid())\n        self.assertEqual({}, tx.to_json()['xpubs'])\n        self.assertEqual(\n            {'029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb8': ('30cf1be5', \"m/84h/1h/0h/0/0\")},\n            tx.inputs()[0].to_json()['bip32_paths'])\n        self.assertEqual(\"70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000\",\n                         tx.serialize_as_bytes().hex())\n        # if there are no multisig inputs, we never include xpubs in the psbt:\n        await tx.prepare_for_export_for_hardware_device(wallet)\n        self.assertEqual({}, tx.to_json()['xpubs'])\n        self.assertEqual(\"70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000\",\n                         tx.serialize_as_bytes().hex())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_export_psbt__rm_witness_utxo_from_non_segwit_input(self, mock_save_db):\n        \"\"\"We sometimes convert full utxo to witness_utxo in psbt inputs when using QR codes, to save space,\n        even for non-segwit inputs (which goes against the spec).\n        This tests that upon scanning the QR code, if we can add the full utxo to the input (e.g. via network),\n        we remove the witness_utxo before e.g. re-exporting it. (see #8305)\n        \"\"\"\n        wallet1a = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_bip43_rootseed(\n                    keystore.bip39_to_seed(\"income sample useless art skate lucky fold field bargain course hope chest\", passphrase=''),\n                    derivation=\"m/45h/0\", xtype=\"standard\"),\n                keystore.from_xpub('tpubDC1y33c2iTcxCBFva3zxbQxUnbzBT1TPVrwLgwVHtqSnVRx2pbJsrHzNYmXnKEnrNqyKk9BERrpSatqVu4JHV4K4hepFQdqnMojA5NVKxcF'),\n            ],\n            '2of2', gap_limit=2,\n            config=self.config,\n        )\n        wallet1a.get_keystores()[1].add_key_origin(derivation_prefix=\"m/45h/0\", root_fingerprint=\"25750cf7\")\n        wallet1b = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_xpub('tpubDAKtPDG6fezcwhB7rNJ9NVEWwGokNzowW3AaMVYFTS4WKoBTNESS1NpntWYDq2uABVYM1xa5cVmu8LD2xKYipMRVLy1VjBQeVe6pixJeBgr'),\n                keystore.from_xpub('tpubDC1y33c2iTcxCBFva3zxbQxUnbzBT1TPVrwLgwVHtqSnVRx2pbJsrHzNYmXnKEnrNqyKk9BERrpSatqVu4JHV4K4hepFQdqnMojA5NVKxcF'),\n            ],\n            '2of2', gap_limit=2,\n            config=self.config,\n        )\n        wallet1b.get_keystores()[0].add_key_origin(derivation_prefix=\"m/45h/0\", root_fingerprint=\"18c2928f\")\n        wallet1b.get_keystores()[1].add_key_origin(derivation_prefix=\"m/45h/0\", root_fingerprint=\"25750cf7\")\n        wallet1b_offline = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_bip43_rootseed(\n                    keystore.bip39_to_seed(\"wear wasp subject october amount essay maximum monkey excuse plastic ginger donor\", passphrase=''),\n                    derivation=\"m/45h/0\", xtype=\"standard\"),\n                keystore.from_xpub('tpubDAKtPDG6fezcwhB7rNJ9NVEWwGokNzowW3AaMVYFTS4WKoBTNESS1NpntWYDq2uABVYM1xa5cVmu8LD2xKYipMRVLy1VjBQeVe6pixJeBgr'),\n            ],\n            '2of2', gap_limit=2,\n            config=self.config,\n        )\n        wallet1b_offline.get_keystores()[1].add_key_origin(derivation_prefix=\"m/45h/0\", root_fingerprint=\"18c2928f\")\n\n        # bootstrap wallet\n        funding_tx = Transaction('0200000000010199b6eb9629c9763e9e95c49f2e81d7a9bda0c8e96165897ce42df0c7a4757aa60100000000fdffffff0220a107000000000017a91482e2921d413a7cad08f76d1d35565dbcc85088db8750560e000000000016001481e6fc4a427d0176373bdd7482b8c1d08f3563300247304402202cf7be624cc30640e2b928adeb25b21ed581f32149f78bc1b0fa9c01da785486022066fadccb1aef8d46841388e83386f85ca5776f50890b9921f165f093fabfd2800121022e43546769a51181fad61474a773b0813106895971b6e3f1d43278beb7154d0a1a112500')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('e1a5465e813b51047e1ee95a2c635416f0105b52361084c7e005325f685f374e', funding_txid)\n        wallet1a.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        wallet1b.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # cosignerA creates and signs the tx\n        outputs = [PartialTxOutput.from_address_and_value(\"tb1qgacvp0zvgtk3etggjayuezrc2mkql8veshv4xw\", 200_000)]\n        coins = wallet1a.get_spendable_coins(domain=None)\n        tx = wallet1a.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        tx.set_rbf(True)\n        tx.locktime = 2429212\n        tx.version = 2\n        wallet1a.sign_transaction(tx, password=None)\n\n        # cosignerA shares psbt with cosignerB\n        orig_tx1 = tx\n        for uses_qr_code1 in (False, True, ):\n            with self.subTest(uses_qr_code1=uses_qr_code1):\n                tx = copy.deepcopy(orig_tx1)\n                if uses_qr_code1:\n                    partial_tx, is_complete = tx.to_qr_data()\n                    self.assertEqual(\"3PMZFRKS5WP6JMMK.-I6Z5JFJ+3ABTDQ.SEM2ATLOB0EF-5I3VH0+Z:P$3SWOO75P/P41QSRJ+4-P*V6MJLC0H.XH1CJ+066VC6IV/5+H1S0R*1NNW.EBSHKZ7IA3T$-$OTUQMP22B+ZVM4QSL/K/BIT8WOM1712MQWDH1DQA/0DEUH$YKYDYDC+/MO-$ZXBM:L+/8F83FD5*:N8HU45:9YULHULQ/P.HLIHVHFQR+WRVT7P.DTUE0BE91DK56:S$Y8+ZBJ0ZSSRRUPNE$I18Y.TXFRM.CTZSGVTSQWNX8Z+YLWR5F8.RVZ1039*U.H7BN6ZMHSBWS*PLY3SK+9LV/FBGJK4+YU3IGI3S4Z9RXS8$JVP+VZUZ:PDJI$KI-6DG2A//O5PRDLP3RUSX.KBFP.IY2JZV+B:DF3.C+R9LU0JUXF26W3SME9A*/WWNNH0-59RCI-YKG:SOO:U0F*SV5R5VERVP2J57EJMO*9.GH++/7P55YE/QTLU$MB8.KT*HD4S2ISP35+*R14HXP:SDUGWGGH$Y8O/NZSH0*CXQZ+H3G7E5:5HFFB8C-BA/O*04I/GF6.X0DKYETTJ:NO27RKHTL:/44U.PK/F/9+9V4D:N3*YS5OTA7+/:P70+L/JMB0OD7ZMO/HFJXRFCK7GS1-K464$96KODYGML8IJLR31-2W1EI0HXOWG:3N9M7QRTU83-NK*G:6SI.JU*71UW85MZ./Y:03L6KZTG7SJ.VKO3WFZU.XV+745QZ.OWET:VNV/.QNR-ETA2S/LTV-U-M2OC2LV7.*1AIN4XW3LR$*75/BVIV.KG1ZGMBJ7L0IE9F-7O4+1QSZ8JR$GECW6RZFKPZ516O+2GV9FTA:3L1C1QL/6YVSF*L8-38/7L1$**Y7K5FLOP-4T20.*1*8JK-M$C+:5U+S*KLZW3E3U0N$ODSMT\",\n                                     partial_tx)\n                    self.assertFalse(is_complete)\n                else:\n                    partial_tx = tx.serialize_as_bytes().hex()\n                    self.assertEqual(\"70736274ff01007202000000014e375f685f3205e0c7841036525b10f01654632c5ae91e7e04513b815e46a5e10000000000fdffffff02400d0300000000001600144770c0bc4c42ed1cad089749cc887856ec0f9d99588004000000000017a914493900cdec652a41c633436b53d574647e329b18871c112500000100df0200000000010199b6eb9629c9763e9e95c49f2e81d7a9bda0c8e96165897ce42df0c7a4757aa60100000000fdffffff0220a107000000000017a91482e2921d413a7cad08f76d1d35565dbcc85088db8750560e000000000016001481e6fc4a427d0176373bdd7482b8c1d08f3563300247304402202cf7be624cc30640e2b928adeb25b21ed581f32149f78bc1b0fa9c01da785486022066fadccb1aef8d46841388e83386f85ca5776f50890b9921f165f093fabfd2800121022e43546769a51181fad61474a773b0813106895971b6e3f1d43278beb7154d0a1a1125002202026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd4730440220414287f36a02b004d2e9a3892e1862edaf49c35d50b65ae10b601879b8c793ef0220073234c56d5a8ae9f4fcfeaecaa757e2724bf830d45aabfab8ffe37329ebf459010104475221026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd2103a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb98152ae2206026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd1418c2928f2d000080000000000000000000000000220603a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb9811425750cf72d000080000000000000000000000000000001004752210212de0581d6570d3cc432cdad2b07514807007dc80b792fafeb47bed69fe6276821028748a66f10b13944ccb14640ba36f65dc7a1f3462e9aca65ba8b05013842270b52ae22020212de0581d6570d3cc432cdad2b07514807007dc80b792fafeb47bed69fe627681425750cf72d0000800000000001000000000000002202028748a66f10b13944ccb14640ba36f65dc7a1f3462e9aca65ba8b05013842270b1418c2928f2d00008000000000010000000000000000\",\n                                     partial_tx)\n                # load tx into cosignerB's online wallet\n                tx = tx_from_any(partial_tx)\n                self.assertFalse(tx.is_segwit())\n                self.assertFalse(tx.is_complete())\n                tx.add_info_from_wallet(wallet1b)\n\n                # cosignerB moves psbt from his online wallet to offline wallet\n                orig_tx2 = tx\n                for uses_qr_code2 in (False, True, ):\n                    with self.subTest(uses_qr_code2=uses_qr_code2):\n                        tx = copy.deepcopy(orig_tx2)\n                        if uses_qr_code2:\n                            partial_tx, is_complete = tx.to_qr_data()\n                            self.assertEqual(\"3PMZFRKS5WP6JMMK.-I6Z5JFJ+3ABTDQ.SEM2ATLOB0EF-5I3VH0+Z:P$3SWOO75P/P41QSRJ+4-P*V6MJLC0H.XH1CJ+066VC6IV/5+H1S0R*1NNW.EBSHKZ7IA3T$-$OTUQMP22B+ZVM4QSL/K/BIT8WOM1712MQWDH1DQA/0DEUH$YKYDYDC+/MO-$ZXBM:L+/8F83FD5*:N8HU45:9YULHULQ/P.HLIHVHFQR+WRVT7P.DTUE0BE91DK56:S$Y8+ZBJ0ZSSRRUPNE$I18Y.TXFRM.CTZSGVTSQWNX8Z+YLWR5F8.RVZ1039*U.H7BN6ZMHSBWS*PLY3SK+9LV/FBGJK4+YU3IGI3S4Z9RXS8$JVP+VZUZ:PDJI$KI-6DG2A//O5PRDLP3RUSX.KBFP.IY2JZV+B:DF3.C+R9LU0JUXF26W3SME9A*/WWNNH0-59RCI-YKG:SOO:U0F*SV5R5VERVP2J57EJMO*9.GH++/7P55YE/QTLU$MB8.KT*HD4S2ISP35+*R14HXP:SDUGWGGH$Y8O/NZSH0*CXQZ+H3G7E5:5HFFB8C-BA/O*04I/GF6.X0DKYETTJ:NO27RKHTL:/44U.PK/F/9+9V4D:N3*YS5OTA7+/:P70+L/JMB0OD7ZMO/HFJXRFCK7GS1-K464$96KODYGML8IJLR31-2W1EI0HXOWG:3N9M7QRTU83-NK*G:6SI.JU*71UW85MZ./Y:03L6KZTG7SJ.VKO3WFZU.XV+745QZ.OWET:VNV/.QNR-ETA2S/LTV-U-M2OC2LV7.*1AIN4XW3LR$*75/BVIV.KG1ZGMBJ7L0IE9F-7O4+1QSZ8JR$GECW6RZFKPZ516O+2GV9FTA:3L1C1QL/6YVSF*L8-38/7L1$**Y7K5FLOP-4T20.*1*8JK-M$C+:5U+S*KLZW3E3U0N$ODSMT\",\n                                             partial_tx)\n                            self.assertFalse(is_complete)\n                        else:\n                            partial_tx = tx.serialize_as_bytes().hex()\n                            self.assertEqual(\"70736274ff01007202000000014e375f685f3205e0c7841036525b10f01654632c5ae91e7e04513b815e46a5e10000000000fdffffff02400d0300000000001600144770c0bc4c42ed1cad089749cc887856ec0f9d99588004000000000017a914493900cdec652a41c633436b53d574647e329b18871c112500000100df0200000000010199b6eb9629c9763e9e95c49f2e81d7a9bda0c8e96165897ce42df0c7a4757aa60100000000fdffffff0220a107000000000017a91482e2921d413a7cad08f76d1d35565dbcc85088db8750560e000000000016001481e6fc4a427d0176373bdd7482b8c1d08f3563300247304402202cf7be624cc30640e2b928adeb25b21ed581f32149f78bc1b0fa9c01da785486022066fadccb1aef8d46841388e83386f85ca5776f50890b9921f165f093fabfd2800121022e43546769a51181fad61474a773b0813106895971b6e3f1d43278beb7154d0a1a1125002202026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd4730440220414287f36a02b004d2e9a3892e1862edaf49c35d50b65ae10b601879b8c793ef0220073234c56d5a8ae9f4fcfeaecaa757e2724bf830d45aabfab8ffe37329ebf459010104475221026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd2103a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb98152ae2206026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd1418c2928f2d000080000000000000000000000000220603a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb9811425750cf72d000080000000000000000000000000000001004752210212de0581d6570d3cc432cdad2b07514807007dc80b792fafeb47bed69fe6276821028748a66f10b13944ccb14640ba36f65dc7a1f3462e9aca65ba8b05013842270b52ae22020212de0581d6570d3cc432cdad2b07514807007dc80b792fafeb47bed69fe627681425750cf72d0000800000000001000000000000002202028748a66f10b13944ccb14640ba36f65dc7a1f3462e9aca65ba8b05013842270b1418c2928f2d00008000000000010000000000000000\",\n                                             partial_tx)\n                        # load tx into cosignerB's offline wallet\n                        tx = tx_from_any(partial_tx)\n                        wallet1b_offline.sign_transaction(tx, password=None, ignore_warnings=True)\n\n                        self.assertEqual('02000000014e375f685f3205e0c7841036525b10f01654632c5ae91e7e04513b815e46a5e100000000d9004730440220414287f36a02b004d2e9a3892e1862edaf49c35d50b65ae10b601879b8c793ef0220073234c56d5a8ae9f4fcfeaecaa757e2724bf830d45aabfab8ffe37329ebf4590147304402203ba7cc21e407ce31c1eecd11c367df716a5d47f06e0bf7109f08063ede25a364022039f6bef0dd401aa2c3103b8cbab57cc4fed3905ccb0a726dc6594bf5930ae0b401475221026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd2103a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb98152aefdffffff02400d0300000000001600144770c0bc4c42ed1cad089749cc887856ec0f9d99588004000000000017a914493900cdec652a41c633436b53d574647e329b18871c112500',\n                                         str(tx))\n                        self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.txid())\n                        self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.wtxid())\n\n                        # again, but raise on warnings (here: signing non-segwit inputs is risky)\n                        tx = tx_from_any(partial_tx)\n                        try:\n                            wallet1b_offline.sign_transaction(tx, password=None)\n                            self.assertFalse(uses_qr_code2)\n                        except TransactionDangerousException:\n                            raise\n                        except TransactionPotentiallyDangerousException:\n                            self.assertTrue(uses_qr_code2)\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_we_dont_sign_tx_including_dummy_address(self, mock_save_db):\n        wallet1 = self.create_standard_wallet_from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver')\n\n        # bootstrap wallet1\n        funding_tx = Transaction('01000000014576dacce264c24d81887642b726f5d64aa7825b21b350c7b75a57f337da6845010000006b483045022100a3f8b6155c71a98ad9986edd6161b20d24fad99b6463c23b463856c0ee54826d02200f606017fd987696ebbe5200daedde922eee264325a184d5bbda965ba5160821012102e5c473c051dae31043c335266d0ef89c1daab2f34d885cc7706b267f3269c609ffffffff0240420f00000000001600148a28bddb7f61864bdcf58b2ad13d5aeb3abc3c42a2ddb90e000000001976a914c384950342cb6f8df55175b48586838b03130fad88ac00000000')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('add2535aedcbb5ba79cc2260868bb9e57f328738ca192937f2c92e0e94c19203', funding_txid)\n        wallet1.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet1 -> dummy address\n        outputs = [PartialTxOutput.from_address_and_value(bitcoin.DummyAddress.CHANNEL, 250000)]\n\n        tx = wallet1.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)\n        with self.assertRaises(bitcoin.DummyAddressUsedInTxException):\n            wallet1.sign_transaction(tx, password=None)\n\n        coins = wallet1.get_spendable_coins(domain=None)\n        tx = wallet1.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))\n        with self.assertRaises(bitcoin.DummyAddressUsedInTxException):\n            wallet1.sign_transaction(tx, password=None)\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sighash_warnings(self, mock_save_db):\n        wallet1 = self.create_standard_wallet_from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver')\n\n        # bootstrap wallet1\n        funding_tx = Transaction('01000000014576dacce264c24d81887642b726f5d64aa7825b21b350c7b75a57f337da6845010000006b483045022100a3f8b6155c71a98ad9986edd6161b20d24fad99b6463c23b463856c0ee54826d02200f606017fd987696ebbe5200daedde922eee264325a184d5bbda965ba5160821012102e5c473c051dae31043c335266d0ef89c1daab2f34d885cc7706b267f3269c609ffffffff0240420f00000000001600148a28bddb7f61864bdcf58b2ad13d5aeb3abc3c42a2ddb90e000000001976a914c384950342cb6f8df55175b48586838b03130fad88ac00000000')\n        self.assertEqual('add2535aedcbb5ba79cc2260868bb9e57f328738ca192937f2c92e0e94c19203', funding_tx.txid())\n        wallet1.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        funding_tx = Transaction('0200000000010141f2de02db45f99c3618e4bfb51cd3e5ec64db096886cfd8253bdbaf0bba58c72c01000000fdffffff0220e00900000000001600144d46b4729c7bf894fa5c510d6e72bec1d02b1aa640420f0000000000160014284520c815980d426264766d8d930013dd20aa6002473044022078a86cd15acb981a5aa4948176cb66583a4a4f4b728962f1497fbdd5f323ae3e02205301e5e3b34232bc139ca311a795377a3416b109b7bb8c70f3f6bb3fcc40e589012103cf9ad82ebea31e5c1bf08219c38302cc0ce5eba2ff5eecd90b9d3a951eebfb1cca2c1800')\n        self.assertEqual('9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539', funding_tx.txid())\n        wallet1.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        outputs = [PartialTxOutput.from_address_and_value('tb1qgacvp0zvgtk3etggjayuezrc2mkql8veshv4xw', '!')]\n        coins = wallet1.get_spendable_coins(domain=None)\n        tx = wallet1.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000))\n        self.assertEqual(2, len(tx.inputs()))\n\n        tx.inputs()[0].sighash = Sighash.NONE\n        tx.inputs()[1].sighash = Sighash.ALL\n        self.assertEqual(TxSighashRiskLevel.INSANE_SIGHASH, wallet1.check_sighash(tx).risk_level)\n        with self.assertRaises(TransactionDangerousException):\n            wallet1.sign_transaction(tx, password=None)\n        with self.assertRaises(TransactionDangerousException):\n            wallet1.sign_transaction(tx, password=None, ignore_warnings=True)\n\n        tx.inputs()[0].sighash = Sighash.ALL\n        tx.inputs()[1].sighash = Sighash.SINGLE\n        self.assertEqual(TxSighashRiskLevel.WEIRD_SIGHASH, wallet1.check_sighash(tx).risk_level)\n        with self.assertRaises(TransactionPotentiallyDangerousException):\n            wallet1.sign_transaction(tx, password=None)\n\n        tx.inputs()[0].sighash = Sighash.ALL | Sighash.ANYONECANPAY\n        tx.inputs()[1].sighash = Sighash.ALL\n        self.assertEqual(TxSighashRiskLevel.WEIRD_SIGHASH, wallet1.check_sighash(tx).risk_level)\n        with self.assertRaises(TransactionPotentiallyDangerousException):\n            wallet1.sign_transaction(tx, password=None)\n\n        tx.inputs()[0].sighash = Sighash.ALL\n        tx.inputs()[1].sighash = Sighash.ALL\n        self.assertEqual(TxSighashRiskLevel.SAFE, wallet1.check_sighash(tx).risk_level)\n        self.assertFalse(tx.is_complete())\n        wallet1.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n\n\nclass TestWalletOfflineSigning(ElectrumTestCase):\n    TESTNET = True\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_old_electrum_seed_online_mpk(self, mock_save_db):\n        wallet_offline = WalletIntegrityHelper.create_standard_wallet(\n            keystore.from_seed('alone body father children lead goodbye phone twist exist grass kick join', passphrase='', for_multisig=False),\n            gap_limit=4,\n            config=self.config\n        )\n        wallet_online = WalletIntegrityHelper.create_standard_wallet(\n            keystore.from_master_key('cd805ed20aec61c7a8b409c121c6ba60a9221f46d20edbc2be83ebd91460e97937cd7d782e77c1cb08364c6bc1c98bc040fdad53f22f29f7d3a85c8e51f9c875'),\n            gap_limit=4,\n            config=self.config\n        )\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('01000000000101161115f8d8110001aa0883989487f9c7a2faf4451038e4305c7594c5236cbb490100000000fdffffff0338117a0000000000160014c1d7b2ded7017cbde837aab36c1e7b2a3952a57800127a00000000001600143e2ab71fc9738ce16fbe6b3b1c210a68c12db84180969800000000001976a91424b64d981d621c227716b51479faf33019371f4688ac0247304402207a5efc6d970f6a5fdcd1933f68b353b4bf2904743f9f1dc3e9177d8754074baf02202eed707e661493bc450357f12cd7a8b8c610c7cb32ded10516c2933a2ba4346a01210287dce03f594fd889726b13a12970237992a0094a5c9f4eebcca6d50d454b39e9ff121600')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('3b9e0581602f4656cb04633dac13662bc62d9f5191caa15cc901dcc76e430856', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1qyw3c0rvn6kk2c688y3dygvckn57525y8qnxt3a', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1446655\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual((0, 1), tx.signature_count())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff01007401000000015608436ec7dc01c95ca1ca91519f2dc62b6613ac3d6304cb56462f6081059e3b0200000000fdffffff02a02526000000000016001423a3878d93d5acac68e7245a4433169d3d455087585d7200000000001976a914b6a6bbbc4cf9da58786a8acc58291e218d52130688acff121600000100fd000101000000000101161115f8d8110001aa0883989487f9c7a2faf4451038e4305c7594c5236cbb490100000000fdffffff0338117a0000000000160014c1d7b2ded7017cbde837aab36c1e7b2a3952a57800127a00000000001600143e2ab71fc9738ce16fbe6b3b1c210a68c12db84180969800000000001976a91424b64d981d621c227716b51479faf33019371f4688ac0247304402207a5efc6d970f6a5fdcd1933f68b353b4bf2904743f9f1dc3e9177d8754074baf02202eed707e661493bc450357f12cd7a8b8c610c7cb32ded10516c2933a2ba4346a01210287dce03f594fd889726b13a12970237992a0094a5c9f4eebcca6d50d454b39e9ff121600420604e79eb77f2f3f989f5e9d090bc0af50afeb0d5bd6ec916f2022c5629ed022e84a87584ef647d69f073ea314a0f0c110ebe24ad64bc1922a10819ea264fc3f35f50c343ddcab000000000100000000004202048e2004ca581afcc54a5d9b3b47affdf48b3f89e16d5bd96774fc0f167f2d7873bac6264e3d1f1bb96f64d1530a54e026e0bd7d674151d146fba582e79f4ef5e80c343ddcab010000000000000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx\n        tx = wallet_offline.sign_transaction(tx_copy, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertEqual((1, 1), tx.signature_count())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual('01000000015608436ec7dc01c95ca1ca91519f2dc62b6613ac3d6304cb56462f6081059e3b020000008a47304402206bed3e02af8a38f6ba2fa3bf5908cb8c643aa62e78e8de6d9af2e19dec55fafc0220039cc1d81d4e5e0292bbc54ea92b8ec4ec016d4828eedc8975a66952cedf13a1014104e79eb77f2f3f989f5e9d090bc0af50afeb0d5bd6ec916f2022c5629ed022e84a87584ef647d69f073ea314a0f0c110ebe24ad64bc1922a10819ea264fc3f35f5fdffffff02a02526000000000016001423a3878d93d5acac68e7245a4433169d3d455087585d7200000000001976a914b6a6bbbc4cf9da58786a8acc58291e218d52130688acff121600',\n                         str(tx))\n        self.assertEqual('06032230d0bf6a277bc4f8c39e3311a712e0e614626d0dea7cc9f592abfae5d8', tx.txid())\n        self.assertEqual('06032230d0bf6a277bc4f8c39e3311a712e0e614626d0dea7cc9f592abfae5d8', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_save_db):\n        wallet_offline = WalletIntegrityHelper.create_standard_wallet(\n            # bip39: \"qwe\", der: m/44'/1'/0'\n            keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'),\n            gap_limit=4,\n            config=self.config\n        )\n        wallet_online = WalletIntegrityHelper.create_standard_wallet(\n            keystore.from_xpub('tpubDDMN69wQjDZxaJz9afZQGa48hZS7X5oSegF2hg67yddNvqfpuTN9DqvDEp7YyVf7AzXnqBqHdLhzTAStHvsoMDDb8WoJQzNrcHgDJHVYgQF'),\n            gap_limit=4,\n            config=self.config\n        )\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325340\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n\n        orig_tx = tx\n        for uses_qr_code in (False, True):\n            with self.subTest(msg=\"uses_qr_code\", uses_qr_code=uses_qr_code):\n                tx = copy.deepcopy(orig_tx)\n                if uses_qr_code:\n                    partial_tx, is_complete = tx.to_qr_data()\n                    self.assertEqual(\"8VXO.MYW+UE2.+5LGGVQP.$087REZNQ8:6*U1CLU+NW7:.T7K04HTV.JW78BXOF$IM*4YYL6LWVSZ4QA0Q-1*8W38XJH833$K3EUK:87-TGQ86XAQ3/RD*PZKM1RLVRAVCFG/8.UHCF8IX*ED1HXNGI*WQ37K*HWJ:XXNKMU.M2A$IYUM-AR:*P34/.EGOQF-YUJ.F0UF$LMW-YXWQU$$CMXD4-L21B7X5/OL7MKXCAD5-9IL/TDP5J2$13KFIH2K5B0/2F*/-XCY:/G-+8K*+1U$56WUE3:J/8KOGSRAN66CNZLG7Y4IB$Y*.S64CC2A9Q/-P5TQFZCF7F+CYG+V363/ME.W0WTPXJM3BC.YPH+Y3K7VIF2+0D.O.JS4LYMZ\",\n                                     partial_tx)\n                    self.assertFalse(is_complete)\n                else:\n                    partial_tx = tx.serialize_as_bytes().hex()\n                    self.assertEqual(\"70736274ff010074010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980200000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d7200000000001976a9149b308d0b3efd4e3469441bc83c3521afde4072b988ac1c391400000100fd4c0d01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400220602ab053d10eda769fab03ab52ee4f1692730288751369643290a8506e31d1e80f00c233d2ae40000000002000000000022020327295144ffff9943356c2d6625f5e2d6411bab77fd56dce571fda6234324e3d90c233d2ae4010000000000000000\",\n                                     partial_tx)\n                tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n                self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n                self.assertEqual(tx.txid(), tx_copy.txid())\n\n                # sign tx\n                tx = wallet_offline.sign_transaction(tx_copy, password=None, ignore_warnings=True)\n                self.assertTrue(tx.is_complete())\n                self.assertFalse(tx.is_segwit())\n                self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.txid())\n                self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_xprv_online_xpub_p2wpkh_p2sh(self, mock_save_db):\n        wallet_offline = WalletIntegrityHelper.create_standard_wallet(\n            # bip39: \"qwe\", der: m/49'/1'/0'\n            keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'),\n            gap_limit=4,\n            config=self.config\n        )\n        wallet_online = WalletIntegrityHelper.create_standard_wallet(\n            keystore.from_xpub('upub5DGeFrwFEPfD711qQ6tKPaUYjBY6BRqfxcWPT77hiHz7VMo7oNGeom5EdXoKXEazePyoN3ueJMqHBfp3MwmsaD8k9dFHoa8KGeVXev7Pbg2'),\n            gap_limit=4,\n            config=self.config\n        )\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325341\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff010072010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980300000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d72000000000017a914191e7373ae7b4829532220e8f281f4581ed52638871d39140000010120809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f870100fd4c0d01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a193914000104160014105db4dae7e5b8dd4dda7b7d3b1e588c9bf26f192206030dddd5d3c31738ca2d8b25391f648af6a8b08e6961e8f56d4173d03e9db82d3e0c105d19280000000002000000000001001600144f485261505d5cbd33dce02a723776c99240c28722020211ab9359cc49c95b3b9a87ee95fd4edf0cecce862f9e9f86ff63e10880baaba80c105d1928010000000000000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('3f0d188519237478258ad2bf881643618635d11c2bb95512e830fcf2eda3c522', tx_copy.txid())\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx\n        tx = wallet_offline.sign_transaction(tx_copy, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual('3f0d188519237478258ad2bf881643618635d11c2bb95512e830fcf2eda3c522', tx.txid())\n        self.assertEqual('27b78ec072a403b0545258e7a1a8d494e4b6fd48bf77f4251a12160c92207cbc', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_save_db):\n        wallet_offline = WalletIntegrityHelper.create_standard_wallet(\n            # bip39: \"qwe\", der: m/84'/1'/0'\n            keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'),\n            gap_limit=4,\n            config=self.config\n        )\n        wallet_online = WalletIntegrityHelper.create_standard_wallet(\n            keystore.from_xpub('vpub5Y941QgusZGvuD5nXTpUvVWohm8q41uftcRNronjRWs9jB2iVr4BbxqbRfAoQjWHgJtDCQEXChgfsPbEuBnidtkFztZSD3zDKTrtwXa2LCa'),\n            gap_limit=4,\n            config=self.config\n        )\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325341\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual((0, 1), tx.signature_count())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n\n        orig_tx = tx\n        for uses_qr_code in (False, True):\n            with self.subTest(msg=\"uses_qr_code\", uses_qr_code=uses_qr_code):\n                tx = copy.deepcopy(orig_tx)\n                if uses_qr_code:\n                    partial_tx, is_complete = tx.to_qr_data()\n                    self.assertEqual(\"FP:A9SADM6+OGU/3KZ/RCI$7/Y2R7OZYNZXB1.$0Y9K69-BXZZ1EAWLM0/*SYX7G:1/0N9+E5YWF0KRPK/Y-GJSJ7TM/A0N0RO.H*S**8E*$W1P7-3RA-+I.1BA77$P8CSX55OHNIIG735$UEH5XTW5DDVD/HK*EQNTI:E3PO:K3$MSN4C3+LIR/-U91-Z9NS/AF*9BZ53VN.XPKD0$.GN*9HOFL3L7MA7ECA86IPZ1J-HJY:$EPZC*3D:+T-L195ULV7:DJ$$Q$H9:+UR:8:5X*S:YC9/HV-$+XQY8/*S1UN9UCE8R786.RW8V$TGQPUCP$KHFM-18I0Q7*RIHI-U0ULUSCG6L3YAS*O4:AEBQLHB37RHRI1E91\",\n                                     partial_tx)\n                    self.assertFalse(is_complete)\n                else:\n                    partial_tx = tx.serialize_as_bytes().hex()\n                    self.assertEqual(\"70736274ff010071010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980100000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d7200000000001600145543fe1a1364b806b27a5c9dc92ac9bbf0d42aa31d3914000001011f80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef0100fd4c0d01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400220603fd88f32a81e812af0187677fc0e7ac9b7fb63ca68c2d98c2afbcf99aa311ac060cdf758ae500000000020000000000220202ac05f54ef082ac98302d57d532e728653565bd55f46fcf03cacbddb168fd6c760cdf758ae5010000000000000000\",\n                                     partial_tx)\n                tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n                self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n                self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx_copy.txid())\n                self.assertEqual(tx.txid(), tx_copy.txid())\n\n                # sign tx\n                tx = wallet_offline.sign_transaction(tx_copy, password=None)\n                self.assertTrue(tx.is_complete())\n                self.assertEqual((1, 1), tx.signature_count())\n                self.assertTrue(tx.is_segwit())\n                self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx.txid())\n                self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_offline_signing_beyond_gap_limit(self, mock_save_db):\n        wallet_offline = WalletIntegrityHelper.create_standard_wallet(\n            # bip39: \"qwe\", der: m/84'/1'/0'\n            keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'),\n            gap_limit=1,  # gap limit of offline wallet intentionally set too low\n            config=self.config\n        )\n        wallet_online = WalletIntegrityHelper.create_standard_wallet(\n            keystore.from_xpub('vpub5Y941QgusZGvuD5nXTpUvVWohm8q41uftcRNronjRWs9jB2iVr4BbxqbRfAoQjWHgJtDCQEXChgfsPbEuBnidtkFztZSD3zDKTrtwXa2LCa'),\n            gap_limit=4,\n            config=self.config\n        )\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325341\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff010071010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980100000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d7200000000001600145543fe1a1364b806b27a5c9dc92ac9bbf0d42aa31d3914000001011f80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef0100fd4c0d01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400220603fd88f32a81e812af0187677fc0e7ac9b7fb63ca68c2d98c2afbcf99aa311ac060cdf758ae500000000020000000000220202ac05f54ef082ac98302d57d532e728653565bd55f46fcf03cacbddb168fd6c760cdf758ae5010000000000000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx_copy.txid())\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx\n        tx = wallet_offline.sign_transaction(tx_copy, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx.txid())\n        self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_signing_where_offline_ks_does_not_have_keyorigin_but_psbt_contains_it(self, mock_save_db):\n        # keystore has intermediate xprv without root fp; tx contains root fp and full path.\n        # tx has input with key beyond gap limit\n        wallet_offline = WalletIntegrityHelper.create_standard_wallet(\n            # bip39 seed: \"brave scare company drastic consider confirm grow differ alter wide olympic utility\"\n            # der: m/84'/1'/0'\n            keystore.from_xprv('vprv9KXDgRXYp3WCozCS3bMehASe2cJhY28DihCZ3KuyiTTjngopkfRC9QkH1SUREyCvnV7TSD6EgEHTTYa5yod7ZveBhVReEU1uDgfVASFqLNw'),\n            gap_limit=4,\n            config=self.config\n        )\n\n        tx = tx_from_any('70736274ff01005202000000017b748828553b1127b86674e71ad0cd4a2e5e8baeab8792a3c3263f7ea0ba86500000000000fdffffff01ad16010000000000160014d74b54300bc0d4b6e8f506fe540b47ce0da38b4a08f21c00000100bf0200000000010163a419b779be17167c54ff3acb1205e5347fbd72963f89fb1d66b5cf09f329c90000000000fdffffff011b17010000000000160014ed420532f0c33477b9b3fbb57431b4a1adce99c90247304402204e4ad4992fa8798e3b595d17c59961b905ca71c32dc3ba910ae14f139259ffbe02206ee2281f21499e46aa77f4bec2edce3674fea529d9dd340439365c2232bad35701210334080358ffdac08f83d6800a8e477e3512ad5c39ede553089db8c4bbe16f59aad7f11c00220602d137f257a96cbc58c7e60f2085cd65a311e242459e23d1efbed77dd8f372513818cc2bdaaa540000800100008000000080000000001e000000002202030671d324eeba0f85499a8749f783a4883103d23f5dedbe048391ff18c3da067818cc2bdaaa540000800100008000000080000000000100000000')\n        self.assertEqual('065b6e0a5731107641828337f5e000c9ddd94a12d074708643b0bca517374c6a', tx.txid())\n\n        # sign tx\n        tx = wallet_offline.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertEqual('020000000001017b748828553b1127b86674e71ad0cd4a2e5e8baeab8792a3c3263f7ea0ba86500000000000fdffffff01ad16010000000000160014d74b54300bc0d4b6e8f506fe540b47ce0da38b4a0247304402203098741bf4d4f956e96f2706a517a1c0a63f67a242a50d155fbc56ad0bbac8b102207e535391c03bdab641f3205762311c1e6648b3459681e53d68fa44e63604a7f6012102d137f257a96cbc58c7e60f2085cd65a311e242459e23d1efbed77dd8f372513808f21c00',\n                         str(tx))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_wif_online_addr_p2pkh(self, mock_save_db):  # compressed pubkey\n        wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config)\n        wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', password=None)\n        wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)\n        wallet_online.import_address('mg2jk6S5WGDhUPA8mLSxDLWpUoQnX1zzoG')\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325340\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100740100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0100000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d7200000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac1c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(None, tx_copy.txid())  # not segwit\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx\n        tx = wallet_offline.sign_transaction(tx_copy, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.txid())\n        self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_save_db):\n        wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config)\n        wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', password=None)\n        wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)\n        wallet_online.import_address('2NA2JbUVK7HGWUCK5RXSVNHrkgUYF8d9zV8')\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325340\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100720100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0200000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d72000000000017a914b808938a8007bc54509cd946944c479c0fa6554f871c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(None, tx_copy.txid())  # redeem script not available\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx\n        tx = wallet_offline.sign_transaction(tx_copy, password=None)\n        self.assertEqual(\n            \"sh(wpkh(03845818239fe468a9e7c7ae1a3d3653a8333f89ff316a771a3acf6854b4d8c6db))\",\n            tx.inputs()[0].script_descriptor.to_string_no_checksum())\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual('7642816d051aa3b333b6564bb6e44fe3a5885bfe7db9860dfbc9973a5c9a6562', tx.txid())\n        self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_wif_online_addr_p2wpkh(self, mock_save_db):\n        wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config)\n        wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', password=None)\n        wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)\n        wallet_online.import_address('tb1qm2eh4787lwanrzr6pf0ekf5c7jnmghm2y9k529')\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325340\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100710100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0000000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d720000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a1c3914000001011f8096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a0100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx_copy.txid())\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx\n        tx = wallet_offline.sign_transaction(tx_copy, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx.txid())\n        self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_signing_mixed_input_script_types(self, mock_save_db):\n        \"\"\"Create a tx that spends mixed non-segwit and segwit UTXOs, and try to offline-sign that.\"\"\"\n        wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config)\n        wallet_offline.import_private_key('p2pkh:cRd5PRVPgArr1eyrcGLKUAULM3EY3Zhseo5Xs8afkeA9UrtMdEFk', password=None)\n        wallet_offline.import_private_key('p2wpkh-p2sh:cRoYFk7m2nKFxkhQNd81HTUdpK9qBvRHcVmMSzeiFyzyNB712srM', password=None)\n        wallet_offline.import_private_key('p2wpkh:cTKEUyG8Q8t1GmBzy2jc9b9C6XPM5x2kE2xwapAAtkvKjdUXGgXA', password=None)\n        wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)\n        wallet_online.import_address('myfTqNq3cyxECtTR5uQukdZos7UfXa3vFU')\n        wallet_online.import_address('2N9BwLxhmiWuRHyTtZk6L52jJEtkukfGTo2')\n        wallet_online.import_address('tb1qkyrls8xvh8ynyrwly89kqu5y8yhf3znnx920t9')\n\n        # bootstrap wallet_online (funding each address separately)\n        funding_tx1 = Transaction('02000000000102a96b792a0872e5d669d503607beb823c99add690bb7c3df794d4b9539228fd8f0000000000fdffffff1306475c0380fe15237a5e800ff8adb415e32526cf284569619e43435e528bfd0000000000fdffffff02e878010000000000160014b32ed4fc9f845698d440cc2bb84a4c4443877309a0860100000000001976a914c70e40272d54659ce757b1a8b20091a26c2d404588ac02473044022048c4436152bf294fea37c89b2d9fca334ca56eb33147acd79a0f712e742edccc022058397bd3c91c8c82318c2dde32dce0c028e1f8d7dc77e394b867498b0195025c0121025635408bbafc2e28981744b28d96beda9582cac0dc49262c7fb2d6d8259c60a10247304402205f50a5cfee40eae71b1a8ceaf7d27347155a50fd0173662a37f86a0884894cbd022040c2ae5eb75a9c23b28200f8be4de576d1ae71b0c1ed3a5f5691b460fb96b05f0121031a95e3afc00c3be5f9c170b1bd0192f5c673741fd641c12490d8b646dd85cb036dfa4800')\n        funding_tx2 = Transaction('02000000000102afdbaf30fe788b337330761c7b92bbc455a86a6d9b81a5aa434afb53a9db7c2b0100000000fdffffff2e4d65ac3b41d6b4cce15ed5ab71e20f69a9581a30af625b8ba67dc6918a1ffa0000000000fdffffff02f62b010000000000160014d217382b1e148cbe850edf1a0e7121a8991f0feaa08601000000000017a914aee2df11c1692811b7f726bda3adff84a52e080f87024730440220160a0a8ae6687132b16a45dd0821d9f30ef4fb40f326ed7a6aa400d01fe5595a02204fcf94e91206d81d676caaa6abe8fa2aaa4ff35e9dc627c8010bd83e1b816209012103e7ed87fa568c645d2904208fd24a385ec54ae6dde1cce91eaac317fa800e0e7d0247304402204900eb3ce18bb315f20e90e10ee0a381684d8a8a94dcbe149658eda884b2ef8f02202d76b249879ab416eae0f672ea09dff9d5eb28356eb3f3a51e7f1e69538a931a012102a2e58987e0b6b1bf2c633cb994fce0380924b62efdb9ecae67ecd5d487eb68766dfa4800')\n        funding_tx3 = Transaction('0200000000010149c2b18f76a921e2fd93c3e59cfaeb6648d4846d85f8c98ef1e7675fd55aae980000000000fdffffff02df8301000000000016001480042981a1249dc8353b6045d0db948401f82842a086010000000000160014b107f81cccb9c9320ddf21cb607284392e988a730247304402201e4df29b132ee58fcd623e6fbc680cb66697e0a6d10dc48bf6dcec92fd593e3e0220288456826fd4702dc8738a1283e593302c1faf0661c88b5b09ae6855d2771530012102eb7f680725df776cd9d8444fca219347c96b9ce0b95c4ae2855b2a638e5513e06dfa4800')\n        wallet_online.adb.receive_tx_callback(funding_tx1, tx_height=TX_HEIGHT_UNCONFIRMED)\n        wallet_online.adb.receive_tx_callback(funding_tx2, tx_height=TX_HEIGHT_UNCONFIRMED)\n        wallet_online.adb.receive_tx_callback(funding_tx3, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1qjy38fmma9vj0tl4y9u3hj0lhj03p860c70ss06', \"!\")]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 4782701\n        tx.version = 2\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual(3, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100a402000000032079f96374ee9641d9a64f0d4bfc13c0f896e876eac6f7ffe1e6d01723b05a190100000000fdffffff94697ad8f540a9a93e2b7b11753289ed8a66c0a5bd5e09082c8d228b2e109b720100000000fdffffff58b03491083d8ac03ee29b73622094ef38108bc4a52595a26b433064aadbce9e0100000000fdffffff015880040000000000160014912274ef7d2b24f5fea42f23793ff793e213e9f86dfa48000001011fa086010000000000160014b107f81cccb9c9320ddf21cb607284392e988a730100de0200000000010149c2b18f76a921e2fd93c3e59cfaeb6648d4846d85f8c98ef1e7675fd55aae980000000000fdffffff02df8301000000000016001480042981a1249dc8353b6045d0db948401f82842a086010000000000160014b107f81cccb9c9320ddf21cb607284392e988a730247304402201e4df29b132ee58fcd623e6fbc680cb66697e0a6d10dc48bf6dcec92fd593e3e0220288456826fd4702dc8738a1283e593302c1faf0661c88b5b09ae6855d2771530012102eb7f680725df776cd9d8444fca219347c96b9ce0b95c4ae2855b2a638e5513e06dfa4800000100fd730102000000000102afdbaf30fe788b337330761c7b92bbc455a86a6d9b81a5aa434afb53a9db7c2b0100000000fdffffff2e4d65ac3b41d6b4cce15ed5ab71e20f69a9581a30af625b8ba67dc6918a1ffa0000000000fdffffff02f62b010000000000160014d217382b1e148cbe850edf1a0e7121a8991f0feaa08601000000000017a914aee2df11c1692811b7f726bda3adff84a52e080f87024730440220160a0a8ae6687132b16a45dd0821d9f30ef4fb40f326ed7a6aa400d01fe5595a02204fcf94e91206d81d676caaa6abe8fa2aaa4ff35e9dc627c8010bd83e1b816209012103e7ed87fa568c645d2904208fd24a385ec54ae6dde1cce91eaac317fa800e0e7d0247304402204900eb3ce18bb315f20e90e10ee0a381684d8a8a94dcbe149658eda884b2ef8f02202d76b249879ab416eae0f672ea09dff9d5eb28356eb3f3a51e7f1e69538a931a012102a2e58987e0b6b1bf2c633cb994fce0380924b62efdb9ecae67ecd5d487eb68766dfa4800000100fd750102000000000102a96b792a0872e5d669d503607beb823c99add690bb7c3df794d4b9539228fd8f0000000000fdffffff1306475c0380fe15237a5e800ff8adb415e32526cf284569619e43435e528bfd0000000000fdffffff02e878010000000000160014b32ed4fc9f845698d440cc2bb84a4c4443877309a0860100000000001976a914c70e40272d54659ce757b1a8b20091a26c2d404588ac02473044022048c4436152bf294fea37c89b2d9fca334ca56eb33147acd79a0f712e742edccc022058397bd3c91c8c82318c2dde32dce0c028e1f8d7dc77e394b867498b0195025c0121025635408bbafc2e28981744b28d96beda9582cac0dc49262c7fb2d6d8259c60a10247304402205f50a5cfee40eae71b1a8ceaf7d27347155a50fd0173662a37f86a0884894cbd022040c2ae5eb75a9c23b28200f8be4de576d1ae71b0c1ed3a5f5691b460fb96b05f0121031a95e3afc00c3be5f9c170b1bd0192f5c673741fd641c12490d8b646dd85cb036dfa48000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(None, tx_copy.txid())  # not all inputs are segwit\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx\n        tx = wallet_offline.sign_transaction(tx_copy, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual('020000000001032079f96374ee9641d9a64f0d4bfc13c0f896e876eac6f7ffe1e6d01723b05a190100000000fdffffff94697ad8f540a9a93e2b7b11753289ed8a66c0a5bd5e09082c8d228b2e109b7201000000171600146bcf730f3a82c8a047b567ed2fff9beb945090c3fdffffff58b03491083d8ac03ee29b73622094ef38108bc4a52595a26b433064aadbce9e010000006a4730440220534c7119d920f9589d47ecd2b92d9fb7d23308e4a8f65540fad4e3b73970bb2b02201aeedb63c27844666539e72ec7afa587310027ac5c4f1cc17737195258ebe9730121038daf92580f95544335297532e782f2493c6b852d2e1382aa8816945d819c08acfdffffff015880040000000000160014912274ef7d2b24f5fea42f23793ff793e213e9f8024730440220248bb782cf19430981bf346dc397316ad28d693a55c3c5de0a1b9f5be958fe1802204a8d8b056c76e0e77db083b50db36e618857692407027f4e299baf5cc433aa410121034e22a0f8b13e2f5e91355b74bdc8e07b7ec6c5e2c31ff7daabdc167ad2d175390247304402206c7a8c33e13ae3dd84c0605a8380d98bf7f543f5ab6061678ddd851dda2ca3d302201868479f8b5d9b2045f039dfd9624432c4b40e62273d8920aede6a99bd5622dd012103e46ebd17af4cb7746dd6e190f85f34a855467346e11f34a2cc864fcf9c7d33c9006dfa4800',\n                         str(tx))\n        self.assertEqual('08b4283f230ffbb72b001eef01e267b310fa6f9d3800d2000474787e13c98ae7', tx.txid())\n        self.assertEqual('24c32d0a7370ca664023a9a1305ae1554731f400723f119e6f11b54332e950c9', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_xprv_online_addr_p2pkh(self, mock_save_db):  # compressed pubkey\n        wallet_offline = WalletIntegrityHelper.create_standard_wallet(\n            # bip39: \"qwe\", der: m/44'/1'/0'\n            keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'),\n            gap_limit=4,\n            config=self.config\n        )\n        wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)\n        wallet_online.import_address('mg2jk6S5WGDhUPA8mLSxDLWpUoQnX1zzoG')\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325340\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100740100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0100000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d7200000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac1c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(None, tx_copy.txid())  # not segwit\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx\n        tx = wallet_offline.sign_transaction(tx_copy, password=None)\n        self.assertEqual(\n            \"pkh([233d2ae4]tpubDDMN69wQjDZxaJz9afZQGa48hZS7X5oSegF2hg67yddNvqfpuTN9DqvDEp7YyVf7AzXnqBqHdLhzTAStHvsoMDDb8WoJQzNrcHgDJHVYgQF/0/1)\",\n            tx.inputs()[0].script_descriptor.to_string_no_checksum())\n        self.assertTrue(tx.is_complete())\n        self.assertFalse(tx.is_segwit())\n        self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.txid())\n        self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_xprv_online_addr_p2wpkh_p2sh(self, mock_save_db):\n        wallet_offline = WalletIntegrityHelper.create_standard_wallet(\n            # bip39: \"qwe\", der: m/49'/1'/0'\n            keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'),\n            gap_limit=4,\n            config=self.config\n        )\n        wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)\n        wallet_online.import_address('2NA2JbUVK7HGWUCK5RXSVNHrkgUYF8d9zV8')\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325340\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100720100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0200000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d72000000000017a914b808938a8007bc54509cd946944c479c0fa6554f871c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(None, tx_copy.txid())  # redeem script not available\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx\n        tx = wallet_offline.sign_transaction(tx_copy, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual('7642816d051aa3b333b6564bb6e44fe3a5885bfe7db9860dfbc9973a5c9a6562', tx.txid())\n        self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_xprv_online_addr_p2wpkh(self, mock_save_db):\n        wallet_offline = WalletIntegrityHelper.create_standard_wallet(\n            # bip39: \"qwe\", der: m/84'/1'/0'\n            keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'),\n            gap_limit=4,\n            config=self.config\n        )\n        wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)\n        wallet_online.import_address('tb1qm2eh4787lwanrzr6pf0ekf5c7jnmghm2y9k529')\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('01000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('0a08ea26a49e2b80f253796d605b69e2d0403fac64bdf6f7db82ada4b7bb6b62', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325340\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff0100710100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0000000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d720000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a1c3914000001011f8096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a0100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx_copy.txid())\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx\n        tx = wallet_offline.sign_transaction(tx_copy, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertTrue(tx.is_segwit())\n        self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx.txid())\n        self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_save_db):\n        # 2-of-3 legacy p2sh multisig\n        wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure', passphrase='', for_multisig=True),\n                keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'),\n                keystore.from_xpub('tpubD6NzVbkrYhZ4XJzYkhsCbDCcZRmDAKSD7bXi9mdCni7acVt45fxbTVZyU6jRGh29ULKTjoapkfFsSJvQHitcVKbQgzgkkYsAmaovcro7Mhf')\n            ],\n            '2of3', gap_limit=2,\n            config=self.config\n        )\n        wallet_offline2 = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song', passphrase='', for_multisig=True),\n                keystore.from_xpub('tpubD6NzVbkrYhZ4YTPEgwk4zzr8wyo7pXGmbbVUnfYNtx6SgAMF5q3LN3Kch58P9hxGNsTmP7Dn49nnrmpE6upoRb1Xojg12FGLuLHkVpVtS44'),\n                keystore.from_xpub('tpubD6NzVbkrYhZ4YARFMEZPckrqJkw59GZD1PXtQnw14ukvWDofR7Z1HMeSCxfYEZVvg4VdZ8zGok5VxHwdrLqew5cMdQntWc5mT7mh1CSgrnX')\n            ],\n            '2of3', gap_limit=2,\n            config=self.config\n        )\n        wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)\n        wallet_online.import_address('2N4z38eTKcWTZnfugCCfRyXtXWMLnn8HDfw')\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('c59913a1fa9b1ef1f6928f0db490be67eeb9d7cb05aa565ee647e859642f3532', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325503\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff010073010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c50000000000fdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400000100f7010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(None, tx_copy.txid())  # not segwit\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx - first\n        tx = wallet_offline1.sign_transaction(tx_copy, password=None)\n        self.assertFalse(tx.is_complete())\n        self.assertEqual((1, 2), tx.signature_count())\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff010073010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c50000000000fdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400000100f7010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f284730440220451f77cb18224adcb4981492d9be2c3fa7537f94f4b29eb405992dbdd5df04aa022071e6759d40dde810caa01ca7f16bad3cb742d64428c419c8fb4bad6f1c3f718101010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb69242700000000000000000000010069522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002202030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220203e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000\",\n                         partial_tx)\n        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n\n        # sign tx - second\n        tx = wallet_offline2.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        self.assertEqual((2, 2), tx.signature_count())\n        tx = tx_from_any(tx.serialize())\n\n        self.assertEqual('010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c500000000fc004730440220451f77cb18224adcb4981492d9be2c3fa7537f94f4b29eb405992dbdd5df04aa022071e6759d40dde810caa01ca7f16bad3cb742d64428c419c8fb4bad6f1c3f718101473044022052980154bdf2e43d6bd8775316cc220ef5ae13b4b9574a7a904a691ee3c5efd3022069b3eddf904cc645bd8fc8b2aaa7aaf7eb5bbfb7bbbd3b6e6cd89b37dfb2856c014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400',\n                         str(tx))\n        self.assertEqual('0e8fdc8257a85ebe7eeab14a53c2c258c61a511f64176b7f8fc016bc2263d307', tx.txid())\n        self.assertEqual('0e8fdc8257a85ebe7eeab14a53c2c258c61a511f64176b7f8fc016bc2263d307', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_hd_multisig_online_addr_p2wsh_p2sh(self, mock_save_db):\n        # 2-of-2 p2sh-embedded segwit multisig\n        wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                # bip39: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose, der: m/1234'/1'/0', p2wsh-p2sh multisig\n                keystore.from_xprv('Uprv9CvELvByqm8k2dpecJVjgLMX1z5DufEjY4fBC5YvdGF5WjGCa7GVJJ2fYni1tyuF7Hw83E6W2ZBjAhaFLZv2ri3rEsubkCd5avg4EHKoDBN'),\n                keystore.from_xpub('Upub5Qb8ik4Cnu8g97KLXKgVXHqY6tH8emQvqtBncjSKsyfTZuorPtTZgX7ovKKZHuuVGBVd1MTTBkWez1XXt2weN1sWBz6SfgRPQYEkNgz81QF')\n            ],\n            '2of2', gap_limit=2,\n            config=self.config\n        )\n        wallet_offline2 = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                # bip39: square page wood spy oil story rebel give milk screen slide shuffle, der: m/1234'/1'/0', p2wsh-p2sh multisig\n                keystore.from_xprv('Uprv9BbnKEXJxXaNvdEsRJ9VA9toYrSeFJh5UfGBpM2iKe8Uh7UhrM9K8ioL53s8gvCoGfirHHaqpABDAE7VUNw8LNU1DMJKVoWyeNKu9XcDC19'),\n                keystore.from_xpub('Upub5RuakRisg8h3F7u7iL2k3UJFa1uiK7xauHamzTxYBbn4PXbM7eajr6M9Q2VCr6cVGhfhqWQqxnABvtSATuVM1xzxk4nA189jJwzaMn1QX7V')\n            ],\n            '2of2', gap_limit=2,\n            config=self.config\n        )\n        wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)\n        wallet_online.import_address('2MsHQRm1pNi6VsmXYRxYMcCTdPu7Xa1RyFe')\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('0100000000010118d494d28e5c3bf61566ca0313e22c3b561b888a317d689cc8b47b947adebd440000000017160014aec84704ea8508ddb94a3c6e53f0992d33a2a529fdffffff020f0925000000000017a91409f7aae0265787a02de22839d41e9c927768230287809698000000000017a91400698bd11c38f887f17c99846d9be96321fbf989870247304402206b906369f4075ebcfc149f7429dcfc34e11e1b7bbfc85d1185d5e9c324be0d3702203ce7fc12fd3131920fbcbb733250f05dbf7d03e18a4656232ee69d5c54dd46bd0121028a4b697a37f3f57f6e53f90db077fa9696095b277454fda839c211d640d48649c0391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('54356de9e156b85c8516fd4d51bdb68b5513f58b4a6147483978ae254627ee3e', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325504\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff01007301000000013eee274625ae78394847614a8bf513558bb6bd514dfd16855cb856e1e96d35540100000000fdffffff02a02526000000000017a914a4189ef02c95cfe36f8e880c6cb54dff0837b22687585d72000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987c0391400000100f70100000000010118d494d28e5c3bf61566ca0313e22c3b561b888a317d689cc8b47b947adebd440000000017160014aec84704ea8508ddb94a3c6e53f0992d33a2a529fdffffff020f0925000000000017a91409f7aae0265787a02de22839d41e9c927768230287809698000000000017a91400698bd11c38f887f17c99846d9be96321fbf989870247304402206b906369f4075ebcfc149f7429dcfc34e11e1b7bbfc85d1185d5e9c324be0d3702203ce7fc12fd3131920fbcbb733250f05dbf7d03e18a4656232ee69d5c54dd46bd0121028a4b697a37f3f57f6e53f90db077fa9696095b277454fda839c211d640d48649c0391400000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual(None, tx_copy.txid())  # redeem script not available\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx - first\n        tx = wallet_offline1.sign_transaction(tx_copy, password=None)\n        self.assertFalse(tx.is_complete())\n        self.assertEqual('6a58a51591142429203b62b6ddf6b799a6926882efac229998c51bee6c3573eb', tx.txid())\n        partial_tx = tx.serialize_as_bytes().hex()\n        # note re PSBT: online wallet had put a NON-WITNESS UTXO for input0, as they did not know if it was segwit.\n        #               offline wallet now replaced this with a WITNESS-UTXO.\n        #               this switch is needed to interop with bitcoin core... https://github.com/bitcoin/bitcoin/blob/fba574c908bb61eff1a0e83c935f3526ba9035f2/src/psbt.cpp#L163\n        self.assertEqual(\"70736274ff01007301000000013eee274625ae78394847614a8bf513558bb6bd514dfd16855cb856e1e96d35540100000000fdffffff02a02526000000000017a914a4189ef02c95cfe36f8e880c6cb54dff0837b22687585d72000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987c0391400000100f70100000000010118d494d28e5c3bf61566ca0313e22c3b561b888a317d689cc8b47b947adebd440000000017160014aec84704ea8508ddb94a3c6e53f0992d33a2a529fdffffff020f0925000000000017a91409f7aae0265787a02de22839d41e9c927768230287809698000000000017a91400698bd11c38f887f17c99846d9be96321fbf989870247304402206b906369f4075ebcfc149f7429dcfc34e11e1b7bbfc85d1185d5e9c324be0d3702203ce7fc12fd3131920fbcbb733250f05dbf7d03e18a4656232ee69d5c54dd46bd0121028a4b697a37f3f57f6e53f90db077fa9696095b277454fda839c211d640d48649c0391400220202d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c4730440220234f6648c5741eb195f0f4cd645298a10ce02f6ef557d05df93331e21c4f58cb022058ce2af0de1c238c4a8dd3b3c7a9a0da6e381ddad7593cddfc0480f9fe5baadf0101042200206ee8d4bb1277b7dbe1d4e49b880993aa993f417a9101cb23865c7c7258732704010547522102975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777462102d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c52ae220602975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777460c17cea9140000000001000000220602d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c0cd1dbcc210000000001000000000001002200206ee8d4bb1277b7dbe1d4e49b880993aa993f417a9101cb23865c7c7258732704010147522102975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777462102d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c52ae220202975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777460c17cea9140000000001000000220202d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c0cd1dbcc21000000000100000000\",\n                         partial_tx)\n        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n\n        # sign tx - second\n        tx = wallet_offline2.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        tx = tx_from_any(tx.serialize())\n\n        self.assertEqual('010000000001013eee274625ae78394847614a8bf513558bb6bd514dfd16855cb856e1e96d355401000000232200206ee8d4bb1277b7dbe1d4e49b880993aa993f417a9101cb23865c7c7258732704fdffffff02a02526000000000017a914a4189ef02c95cfe36f8e880c6cb54dff0837b22687585d72000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987040047304402205a9dd9eb5676196893fb08f60079a2e9f567ee39614075d8c5d9fab0f11cbbc7022039640855188ebb7bccd9e3f00b397a888766d42d00d006f1ca7457c15449285f014730440220234f6648c5741eb195f0f4cd645298a10ce02f6ef557d05df93331e21c4f58cb022058ce2af0de1c238c4a8dd3b3c7a9a0da6e381ddad7593cddfc0480f9fe5baadf0147522102975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777462102d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c52aec0391400',\n                         str(tx))\n        self.assertEqual('6a58a51591142429203b62b6ddf6b799a6926882efac229998c51bee6c3573eb', tx.txid())\n        self.assertEqual('96d0bca1001778c54e4c3a07929fab5562c5b5a23fd1ca3aa3870cc5df2bf97d', tx.wtxid())\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_sending_offline_hd_multisig_online_addr_p2wsh(self, mock_save_db):\n        # 2-of-3 p2wsh multisig\n        wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', passphrase='', for_multisig=True),\n                keystore.from_xpub('Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf'),\n                keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra')\n            ],\n            '2of3', gap_limit=2,\n            config=self.config\n        )\n        wallet_offline2 = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('snow nest raise royal more walk demise rotate smooth spirit canyon gun', passphrase='', for_multisig=True),\n                keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra'),\n                keystore.from_xpub('Vpub5gSKXzxK7FeKQedu2q1z9oJWxqvX72AArW3HSWpEhc8othDH8xMDu28gr7gf17sp492BuJod8Tn7anjvJrKpETwqnQqX7CS8fcYyUtedEMk')\n            ],\n            '2of3', gap_limit=2,\n            config=self.config\n        )\n        # ^ third seed: hedgehog sunset update estate number jungle amount piano friend donate upper wool\n        wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)\n        wallet_online.import_address('tb1q83p6eqxkuvq4eumcha46crpzg4nj84s9p0hnynkxg8nhvfzqcc7q4erju6')\n\n        # bootstrap wallet_online\n        funding_tx = Transaction('0100000000010132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c501000000171600142e5d579693b2a7679622935df94d9f3c84909b24fdffffff0280969800000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c83717d010000000017a91441b772909ad301b41b76f4a3c5058888a7fe6f9a8702483045022100de54689f74b8efcce7fdc91e40761084686003bcd56c886ee97e75a7e803526102204dea51ae5e7d01bd56a8c336c64841f7fe02a8b101fa892e13f2d079bb14e6bf012102024e2f73d632c49f4b821ccd3b6da66b155427b1e5b1c4688cefd5a4b4bfa404c1391400')\n        funding_txid = funding_tx.txid()\n        self.assertEqual('643a7ab9083d0227dd9df314ce56b18d279e6018ff975079dfaab82cd7a66fa3', funding_txid)\n        wallet_online.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # create unsigned tx\n        outputs = [PartialTxOutput.from_address_and_value('2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)]\n        tx = wallet_online.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)\n        tx.locktime = 1325505\n        tx.version = 1\n\n        self.assertFalse(tx.is_complete())\n        self.assertEqual(1, len(tx.inputs()))\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff01007e0100000001a36fa6d72cb8aadf795097ff18609e278db156ce14f39ddd27023d08b97a3a640000000000fdffffff02a02526000000000017a91447ee5a659f6ffb53f7e3afc1681b6415f3c00fa187585d7200000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63cc13914000001012b80969800000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0100fd03010100000000010132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c501000000171600142e5d579693b2a7679622935df94d9f3c84909b24fdffffff0280969800000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c83717d010000000017a91441b772909ad301b41b76f4a3c5058888a7fe6f9a8702483045022100de54689f74b8efcce7fdc91e40761084686003bcd56c886ee97e75a7e803526102204dea51ae5e7d01bd56a8c336c64841f7fe02a8b101fa892e13f2d079bb14e6bf012102024e2f73d632c49f4b821ccd3b6da66b155427b1e5b1c4688cefd5a4b4bfa404c1391400000000\",\n                         partial_tx)\n        tx_copy = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertTrue(wallet_online.is_mine(wallet_online.adb.get_txin_address(tx_copy.inputs()[0])))\n\n        self.assertEqual('32e946761b4e718c1fa8d044db9e72d5831f6395eb284faf2fb5c4af0743e501', tx_copy.txid())\n        self.assertEqual(tx.txid(), tx_copy.txid())\n\n        # sign tx - first\n        tx = wallet_offline1.sign_transaction(tx_copy, password=None)\n        self.assertFalse(tx.is_complete())\n        self.assertEqual('32e946761b4e718c1fa8d044db9e72d5831f6395eb284faf2fb5c4af0743e501', tx.txid())\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff01007e0100000001a36fa6d72cb8aadf795097ff18609e278db156ce14f39ddd27023d08b97a3a640000000000fdffffff02a02526000000000017a91447ee5a659f6ffb53f7e3afc1681b6415f3c00fa187585d7200000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63cc13914000001012b80969800000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0100fd03010100000000010132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c501000000171600142e5d579693b2a7679622935df94d9f3c84909b24fdffffff0280969800000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c83717d010000000017a91441b772909ad301b41b76f4a3c5058888a7fe6f9a8702483045022100de54689f74b8efcce7fdc91e40761084686003bcd56c886ee97e75a7e803526102204dea51ae5e7d01bd56a8c336c64841f7fe02a8b101fa892e13f2d079bb14e6bf012102024e2f73d632c49f4b821ccd3b6da66b155427b1e5b1c4688cefd5a4b4bfa404c139140022020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa4730440220629d89626585f563202e6b38ceddc26ccd00737e0b7ee4239b9266ef9174ea2f02200b74828399a2e35ed46c9b484af4817438d5fea890606ebb201b821944db1fdc0101056952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22060223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa10b2e35a7d01000080000000000000000022060273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e1053b77ddb010000800000000000000000220602aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9411043067d63010000800000000000000000000001016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa10b2e35a7d01000080000000000000000022020273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e1053b77ddb010000800000000000000000220202aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9411043067d6301000080000000000000000000\",\n                         partial_tx)\n        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n\n        # sign tx - second\n        tx = wallet_offline2.sign_transaction(tx, password=None)\n        self.assertTrue(tx.is_complete())\n        tx = tx_from_any(tx.serialize())\n\n        self.assertEqual('01000000000101a36fa6d72cb8aadf795097ff18609e278db156ce14f39ddd27023d08b97a3a640000000000fdffffff02a02526000000000017a91447ee5a659f6ffb53f7e3afc1681b6415f3c00fa187585d7200000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c04004730440220629d89626585f563202e6b38ceddc26ccd00737e0b7ee4239b9266ef9174ea2f02200b74828399a2e35ed46c9b484af4817438d5fea890606ebb201b821944db1fdc0147304402205d1a59c84c419992069e9764a7992abca6a812cc5dfd4f0d6515d4283e660ce802202597a38899f31545aaf305629bd488f36bf54e4a05fe983932cafbb3906efb8f016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153aec1391400',\n                         str(tx))\n        self.assertEqual('32e946761b4e718c1fa8d044db9e72d5831f6395eb284faf2fb5c4af0743e501', tx.txid())\n        self.assertEqual('4376fa5f1f6cb37b1f3956175d3bd4ef6882169294802b250a3c672f3ff431c1', tx.wtxid())\n\n\nclass TestWalletCreationChecks(ElectrumTestCase):\n    TESTNET = True\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_duplicate_masterkeys_in_multisig(self, mock_save_db):\n        # ks1 (seed) and ks2 have same xpub\n        with self.assertRaises(Exception) as ctx1:\n            w1 = WalletIntegrityHelper.create_multisig_wallet(\n                [\n                    keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', passphrase='', for_multisig=True),\n                    keystore.from_xpub('Vpub5gSKXzxK7FeKQedu2q1z9oJWxqvX72AArW3HSWpEhc8othDH8xMDu28gr7gf17sp492BuJod8Tn7anjvJrKpETwqnQqX7CS8fcYyUtedEMk'),  # collides with seed\n                    keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra'),\n                ],\n                '2of3', gap_limit=2,\n                config=self.config\n            )\n        self.assertIn('duplicate xpubs in multisig', ctx1.exception.args[0])\n        # ks2 and ks3 have same xpub\n        with self.assertRaises(Exception) as ctx2:\n            w2 = WalletIntegrityHelper.create_multisig_wallet(\n                [\n                    keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', passphrase='', for_multisig=True),\n                    keystore.from_xpub('Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf'),\n                    keystore.from_xpub('Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf'),\n                ],\n                '2of3', gap_limit=2,\n                config=self.config\n            )\n        self.assertIn('duplicate xpubs in multisig', ctx2.exception.args[0])\n        # all xpubs different. should not raise.\n        w3 = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', passphrase='', for_multisig=True),\n                keystore.from_xpub('Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf'),\n                keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra'),\n            ],\n            '2of3', gap_limit=2,\n            config=self.config\n        )\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_heterogeneous_xpub_types_in_multisig(self, mock_save_db):\n        # tpub + vpub\n        with self.assertRaises(Exception) as ctx1:\n            w1 = WalletIntegrityHelper.create_multisig_wallet(\n                [\n                    keystore.from_xpub('tpubD6NzVbkrYhZ4XYdbWCGSusTDQRAX4UnuqcikJAkqMYxBkvnGfUBvXBE84eyQS6e4To3Pz1xwLrEuxGgQayn4dqVXwNM7dWh4U4DgHai2scz'),\n                    keystore.from_xpub('vpub5VmsevU91fpRaJkfa8b6c9MK53gKY8rSzZjrZdp6dkHZjnFhM1HN74ezHY96JCgFnbQJhRbeUyr5S1vzdcTB6qUKrrG7GBuwPYDTzBjLQmv'),\n                ],\n                '2of2', gap_limit=2,\n                config=self.config\n            )\n        self.assertIn('multisig wallet needs to have homogeneous xpub types', ctx1.exception.args[0])\n        # tpub + \"segwit\" seed\n        with self.assertRaises(Exception) as ctx2:\n            w1 = WalletIntegrityHelper.create_multisig_wallet(\n                [\n                    keystore.from_xpub('tpubD6NzVbkrYhZ4XYdbWCGSusTDQRAX4UnuqcikJAkqMYxBkvnGfUBvXBE84eyQS6e4To3Pz1xwLrEuxGgQayn4dqVXwNM7dWh4U4DgHai2scz'),\n                    keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', passphrase='', for_multisig=True),\n                ],\n                '2of2', gap_limit=2,\n                config=self.config\n            )\n        self.assertIn('multisig wallet needs to have homogeneous xpub types', ctx2.exception.args[0])\n        # \"standard\" seed + \"segwit\" seed\n        with self.assertRaises(Exception) as ctx3:\n            w1 = WalletIntegrityHelper.create_multisig_wallet(\n                [\n                    keystore.from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song', passphrase='', for_multisig=True),\n                    keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', passphrase='', for_multisig=True),\n                ],\n                '2of2', gap_limit=2,\n                config=self.config\n            )\n        self.assertIn('multisig wallet needs to have homogeneous xpub types', ctx3.exception.args[0])\n        # \"old\" seed + \"standard\" seed\n        with self.assertRaises(Exception) as ctx4:\n            w1 = WalletIntegrityHelper.create_multisig_wallet(\n                [\n                    keystore.from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song', passphrase='', for_multisig=True),\n                    keystore.from_seed('powerful random nobody notice nothing important anyway look away hidden message over', passphrase='', for_multisig=True),\n                ],\n                '2of2', gap_limit=2,\n                config=self.config\n            )\n        self.assertIn('unexpected keystore type', ctx4.exception.args[0])\n\n\nclass TestWalletHistory_SimpleRandomOrder(ElectrumTestCase):\n    TESTNET = True\n    transactions = {\n        \"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67\": \"01000000029d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2000000008b483045022100a146a2078a318c1266e42265a369a8eef8993750cb3faa8dd80754d8d541d5d202207a6ab8864986919fd1a7fd5854f1e18a8a0431df924d7a878ec3dc283e3d75340141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfeffffff9d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2010000008a47304402201c7fa37b74a915668b0244c01f14a9756bbbec1031fb69390bcba236148ab37e02206151581f9aa0e6758b503064c1e661a726d75c6be3364a5a121a8c12cf618f64014104dc28da82e141416aaf771eb78128d00a55fdcbd13622afcbb7a3b911e58baa6a99841bfb7b99bcb7e1d47904fda5d13fdf9675cdbbe73e44efcc08165f49bac6feffffff02b0183101000000001976a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac005a6202000000001976a9145eb4eeaefcf9a709f8671444933243fbd05366a388ac54c51200\",\n        \"2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d\": \"010000000132201ff125888a326635a2fc6e971cd774c4d0c1a757d742d0f6b5b020f7203a050000006a47304402201d20bb5629a35b84ff9dd54788b98e265623022894f12152ac0e6158042550fe02204e98969e1f7043261912dd0660d3da64e15acf5435577fc02a00eccfe76b323f012103a336ad86546ab66b6184238fe63bb2955314be118b32fa45dd6bd9c4c5875167fdffffff0254959800000000001976a9148d2db0eb25b691829a47503006370070bc67400588ac80969800000000001976a914f96669095e6df76cfdf5c7e49a1909f002e123d088ace8ca1200\",\n        \"2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd\": \"010000000001036cdf8d2226c57d7cc8485636d8e823c14790d5f24e6cf38ba9323babc7f6db2901000000171600143fc0dbdc2f939c322aed5a9c3544468ec17f5c3efdffffff507dce91b2a8731636e058ccf252f02b5599489b624e003435a29b9862ccc38c0200000017160014c50ff91aa2a790b99aa98af039ae1b156e053375fdffffff6254162cf8ace3ddfb3ec242b8eade155fa91412c5bde7f55decfac5793743c1010000008b483045022100de9599dcd7764ca8d4fcbe39230602e130db296c310d4abb7f7ae4d139c4d46402200fbfd8e6dc94d90afa05b0c0eab3b84feb465754db3f984fbf059447282771c30141045eecefd39fabba7b0098c3d9e85794e652bdbf094f3f85a3de97a249b98b9948857ea1e8209ee4f196a6bbcfbad103a38698ee58766321ba1cdee0cbfb60e7b2fdffffff01e85af70100000000160014e8d29f07cd5f813317bec4defbef337942d85d74024730440220218049aee7bbd34a7fa17f972a8d24a0469b0131d943ef3e30860401eaa2247402203495973f006e6ee6ae74a83228623029f238f37390ee4b587d95cdb1d1aaee9901210392ba263f3a2b260826943ff0df25e9ca4ef603b98b0a916242c947ae0626575f02473044022002603e5ceabb4406d11aedc0cccbf654dd391ce68b6b2228a40e51cf8129310d0220533743120d93be8b6c1453973935b911b0a2322e74708d23e8b5f90e74b0f192012103221b4ee0f508ba595fc1b9c2252ed9d03e99c73b97344dae93263c68834f034800ed161300\",\n        \"31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f\": \"0100000000010454022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a000000008b483045022100ea8fe74db2aba23ad36ac66aaa481bad2b4d1b3c331869c1d60a28ce8cfad43c02206fa817281b33fbf74a6dd7352bdc5aa1d6d7966118a4ad5b7e153f37205f1ae80141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a01000000171600146dfe07e12af3db7c715bf1c455f8517e19c361e7fdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a020000006a47304402200b1fb89e9a772a8519294acd61a53a29473ce76077165447f49a686f1718db5902207466e2e8290f84114dc9d6c56419cb79a138f03d7af8756de02c810f19e4e03301210222bfebe09c2638cfa5aa8223fb422fe636ba9675c5e2f53c27a5d10514f49051fdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a0300000000fdffffff018793140d000000001600144b3e27ddf4fc5f367421ee193da5332ef351b700000247304402207ba52959938a3853bcfd942d8a7e6a181349069cde3ea73dbde43fa9669b8d5302207a686b92073863203305cb5d5550d88bdab0d21b9e9761ba4a106ea3970e08d901210265c1e014112ed19c9f754143fb6a2ff89f8630d62b33eb5ae708c9ea576e61b50002473044022029e868a905aa3ecae6eafcbd5959aefff0e5f39c1fc7a131a174828806e74e5202202f0aaa7c3cb3d9a9d526e5428ce37c0f0af0d774aa30b09ded8bc2230e7ffaf2012102fe0104455dc52b1689bba130664e452642180eb865217acfc6997260b7d946ae22c71200\",\n        \"336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa\": \"0100000000010232201ff125888a326635a2fc6e971cd774c4d0c1a757d742d0f6b5b020f7203a020000006a4730440220198c0ba2b2aefa78d8cca01401d408ecdebea5ac05affce36f079f6e5c8405ca02200eabb1b9a01ff62180cf061dfacedba6b2e07355841b9308de2d37d83489c7b80121031c663e5534fe2a6de816aded6bb9afca09b9e540695c23301f772acb29c64a05fdfffffffb28ff16811d3027a2405be68154be8fdaff77284dbce7a2314c4107c2c941600000000000fdffffff015e104f01000000001976a9146dfd56a0b5d0c9450d590ad21598ecfeaa438bd788ac000247304402207d6dc521e3a4577685535f098e5bac4601aa03658b924f30bf7afef1850e437e022045b76771d8b6ca1939352d6b759fca31029e5b2edffa44dc747fe49770e746cd012102c7f36d4ceed353b90594ebaf3907972b6d73289bdf4707e120de31ec4e1eb11679f31200\",\n        \"3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308\": \"010000000168091e76227e99b098ef8d6d5f7c1bb2a154dd49103b93d7b8d7408d49f07be0000000008a47304402202f683a63af571f405825066bd971945a35e7142a75c9a5255d364b25b7115d5602206c59a7214ae729a519757e45fdc87061d357813217848cf94df74125221267ac014104aecb9d427e10f0c370c32210fe75b6e72ccc4f415076cf1a6318fbed5537388862c914b29269751ab3a04962df06d96f5f4f54e393a0afcbfa44b590385ae61afdffffff0240420f00000000001976a9145f917fd451ca6448978ebb2734d2798274daf00b88aca8063d00000000001976a914e1232622a96a04f5e5a24ca0792bb9c28b089d6e88ace9ca1200\",\n        \"475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d\": \"01000000013a7e6f19a963adc7437d2f3eb0936f1fc9ef4ba7e083e19802eb1111525a59c2000000008b483045022100958d3931051306489d48fe69b32561e0a16e82a2447c07be9d1069317084b5e502202f70c2d9be8248276d334d07f08f934ffeea83977ad241f9c2de954a2d577f94014104d950039cec15ad10ad4fb658873bc746148bc861323959e0c84bf10f8633104aa90b64ce9f80916ab0a4238e025dcddf885b9a2dd6e901fe043a433731db8ab4fdffffff02a086010000000000160014bbfab2cc3267cea2df1b68c392cb3f0294978ca922940d00000000001976a914760f657c67273a06cad5b1d757a95f4ed79f5a4b88ac4c8d1300\",\n        \"56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216\": \"01000000000101614b142aeeb827d35d2b77a5b11f16655b6776110ddd9f34424ff49d85706cf90200000000fdffffff02784a4c00000000001600148464f47f35cbcda2e4e5968c5a3a862c43df65a1404b4c00000000001976a914c9efecf0ecba8b42dce0ae2b28e3ea0573d351c988ac0247304402207d8e559ed1f56cb2d02c4cb6c95b95c470f4b3cb3ce97696c3a58e39e55cd9b2022005c9c6f66a7154032a0bb2edc1af1f6c8f488bec52b6581a3a780312fb55681b0121024f83b87ac3440e9b30cec707b7e1461ecc411c2f45520b45a644655528b0a68ae9ca1200\",\n        \"6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254\": \"0100000000010496941b9f18710b39bacde890e39a7fa401e6bf49985857cb7adfb8a45147ef1e000000001716001441aec99157d762708339d7faf7a63a8c479ed84cfdffffff96941b9f18710b39bacde890e39a7fa401e6bf49985857cb7adfb8a45147ef1e0100000000fdffffff1a5d1e4ca513983635b0df49fd4f515c66dd26d7bff045cfbd4773aa5d93197f000000006a4730440220652145460092ef42452437b942cb3f563bf15ad90d572d0b31d9f28449b7a8dd022052aae24f58b8f76bd2c9cf165cc98623f22870ccdbef1661b6dbe01c0ef9010f01210375b63dd8e93634bbf162d88b25d6110b5f5a9638f6fe080c85f8b21c2199a1fdfdffffff1a5d1e4ca513983635b0df49fd4f515c66dd26d7bff045cfbd4773aa5d93197f010000008a47304402207517c52b241e6638a84b05385e0b3df806478c2e444f671ca34921f6232ee2e70220624af63d357b83e3abe7cdf03d680705df0049ec02f02918ee371170e3b4a73d014104de408e142c00615294813233cdfe9e7774615ae25d18ba4a1e3b70420bb6666d711464518457f8b947034076038c6f0cfc8940d85d3de0386e0ad88614885c7cfdffffff0480969800000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac809698000000000017a914f2a76207d7b54bd34282281205923841341d9e1f87002d3101000000001976a914b8d4651937cd7db5bcf5fc98e6d2d8cfa131e85088ac743db20a00000000160014c7d0df09e03173170aed0247243874c6872748ed02483045022100b932cda0aeb029922e126568a48c05d79317747dcd77e61dce44e190e140822002202d13f84338bb272c531c4086277ac11e166c59612f4aefa6e20f78455bdc09970121028e6808a8ac1e9ede621aaabfcad6f86662dbe0ace0236f078eb23c24bc88bd5e02483045022100d74a253262e3898626c12361ba9bb5866f9303b42eec0a55ced0578829e2e61e022059c08e61d90cd63c84de61c796c9d1bc1e2f8217892a7c07b383af357ddd7a730121028641e89822127336fc12ff99b1089eb1a124847639a0e98d17ff03a135ad578b000020c71200\",\n        \"72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112\": \"0100000002677b2113f26697718c8991823ec0e637f08cb61426da8da508b97449c872490f000000008b4830450221009c50c0f56f34781dfa7b3d540ac724436c67ffdc2e5b2d5a395c9ebf72116ef802205a94a490ea14e4824f36f1658a384aeaecadd54839600141eb20375a49d476d1014104c291245c2ee3babb2a35c39389df56540867f93794215f743b9aa97f5ba114c4cdee8d49d877966728b76bc649bb349efd73adef1d77452a9aac26f8c51ae1ddfdffffff677b2113f26697718c8991823ec0e637f08cb61426da8da508b97449c872490f010000008b483045022100ae0b286493491732e7d3f91ab4ac4cebf8fe8a3397e979cb689e62d350fdcf2802206cf7adf8b29159dd797905351da23a5f6dab9b9dbf5028611e86ccef9ff9012e014104c62c4c4201d5c6597e5999f297427139003fdb82e97c2112e84452d1cfdef31f92dd95e00e4d31a6f5f9af0dadede7f6f4284b84144e912ff15531f36358bda7fdffffff019f7093030000000022002027ce908c4ee5f5b76b4722775f23e20c5474f459619b94040258290395b88afb6ec51200\",\n        \"76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4\": \"0100000001f4ba9948cdc4face8315c7f0819c76643e813093ffe9fbcf83d798523c7965db000000006a473044022061df431a168483d144d4cffe1c5e860c0a431c19fc56f313a899feb5296a677c02200208474cc1d11ad89b9bebec5ec00b1e0af0adaba0e8b7f28eed4aaf8d409afb0121039742bf6ab70f12f6353e9455da6ed88f028257950450139209b6030e89927997fdffffff01d4f84b00000000001976a9140b93db89b6bf67b5c2db3370b73d806f458b3d0488ac0a171300\",\n        \"7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a\": \"01000000000102681b6a8dd3a406ee10e4e4aece3c2e69f6680c02f53157be6374c5c98322823a00000000232200209adfa712053a06cc944237148bcefbc48b16eb1dbdc43d1377809bcef1bea9affdffffff681b6a8dd3a406ee10e4e4aece3c2e69f6680c02f53157be6374c5c98322823a0100000023220020f40ed2e3fbffd150e5b74f162c3ce5dae0dfeba008a7f0f8271cf1cf58bfb442fdffffff02801d2c04000000001976a9140cc01e19090785d629cdcc98316f328df554de4f88ac6d455d05000000001976a914b9e828990a8731af4527bcb6d0cddf8d5ffe90ce88ac040047304402206eb65bd302eefae24eea05781e8317503e68584067d35af028a377f0751bb55b0220226453d00db341a4373f1bcac2391f886d3a6e4c30dd15133d1438018d2aad24014730440220343e578591fab0236d28fb361582002180d82cb1ba79eec9139a7a9519fca4260220723784bd708b4a8ed17bb4b83a5fd2e667895078e80eec55119015beb3592fd2016952210222eca5665ed166d090a5241d9a1eb27a92f85f125aaf8df510b2b5f701f3f534210227bca514c22353a7ae15c61506522872afecf10df75e599aabe4d562d0834fce2103601d7d49bada5a57a4832eafe4d1f1096d7b0b051de4a29cd5fc8ad62865e0a553ae0400483045022100b15ea9daacd809eb4d783a1449b7eb33e2965d4229e1a698db10869299dddc670220128871ffd27037a3e9dac6748ce30c14b145dd7f9d56cc9dcde482461fb6882601483045022100cb659e1de65f8b87f64d1b9e62929a5d565bbd13f73a1e6e9dd5f4efa024b6560220667b13ce2e1a3af2afdcedbe83e2120a6e8341198a79efb855b8bc5f93b4729f0169522102d038600af253cf5019f9d5637ca86763eca6827ed7b2b7f8cc6326dffab5eb68210315cdb32b7267e9b366fb93efe29d29705da3db966e8c8feae0c8eb51a7cf48e82103f0335f730b9414acddad5b3ee405da53961796efd8c003e76e5cd306fcc8600c53ae1fc71200\",\n        \"9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb\": \"010000000001013409c10fd732d9e4b3a9a1c4beb511fa5eb32bc51fd169102a21aa8519618f800000000000fdffffff0640420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac40420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac40420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac80841e00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac64064a000000000016001469825d422ca80f2a5438add92d741c7df45211f280969800000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac02483045022100b4369b18bccb74d72b6a38bd6db59122a9e8af3356890a5ecd84bdb8c7ffe317022076a5aa2b817be7b3637d179106fccebb91acbc34011343c8e8177acc2da4882e0121033c8112bbf60855f4c3ae489954500c4b8f3408665d8e1f63cf3216a76125c69865281300\",\n        \"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d\": \"010000000400899af3606e93106a5d0f470e4e2e480dfc2fd56a7257a1f0f4d16fd5961a0f000000006a47304402205b32a834956da303f6d124e1626c7c48a30b8624e33f87a2ae04503c87946691022068aa7f936591fb4b3272046634cf526e4f8a018771c38aff2432a021eea243b70121034bb61618c932b948b9593d1b506092286d9eb70ea7814becef06c3dfcc277d67fdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753000000006b483045022100de775a580c6cb47061d5a00c6739033f468420c5719f9851f32c6992610abd3902204e6b296e812bb84a60c18c966f6166718922780e6344f243917d7840398eb3db0121025d7317c6910ad2ad3d29a748c7796ddf01e4a8bc5e3bf2a98032f0a20223e4aafdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753010000006a4730440220615a26f38bf6eb7043794c08fb81f273896b25783346332bec4de8dfaf7ed4d202201c2bc4515fc9b07ded5479d5be452c61ce785099f5e33715e9abd4dbec410e11012103caa46fcb1a6f2505bf66c17901320cc2378057c99e35f0630c41693e97ebb7cffdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753030000006b483045022100c8fba762dc50041ee3d5c7259c01763ed913063019eefec66678fb8603624faa02200727783ccbdbda8537a6201c63e30c0b2eb9afd0e26cb568d885e6151ef2a8540121027254a862a288cfd98853161f575c49ec0b38f79c3ef0bf1fb89986a3c36a8906fdffffff0240787d01000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac3bfc1502000000001976a914c30f2af6a79296b6531bf34dba14c8419be8fb7d88ac52c51200\",\n        \"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462\": \"0100000003aabec9cb99096073ae47cfb84bfd5b0063ae7f157956fd37c5d1a79d74ee6e33000000008b4830450221008136fc880d5e24fdd9d2a43f5085f374fef013b814f625d44a8075104981d92a0220744526ec8fc7887c586968f22403f0180d54c9b7ff8db9b553a3c4497982e8250141047b8b4c91c5a93a1f2f171c619ca41770427aa07d6de5130c3ba23204b05510b3bd58b7a1b35b9c4409104cfe05e1677fc8b51c03eac98b206e5d6851b31d2368fdffffff16d23bdc750c7023c085a6fc76e3e468944919783535ea2c13826f181058a656010000008a47304402204148410f2d796b1bb976b83904167d28b65dcd7c21b3876022b4fa70abc86280022039ea474245c3dc8cd7e5a572a155df7a6a54496e50c73d9fed28e76a1cf998c00141044702781daed201e35aa07e74d7bda7069e487757a71e3334dc238144ad78819de4120d262e8488068e16c13eea6092e3ab2f729c13ef9a8c42136d6365820f7dfdffffff68091e76227e99b098ef8d6d5f7c1bb2a154dd49103b93d7b8d7408d49f07be0010000008b4830450221008228af51b61a4ee09f58b4a97f204a639c9c9d9787f79b2fc64ea54402c8547902201ed81fca828391d83df5fbd01a3fa5dd87168c455ed7451ba8ccb5bf06942c3b0141046fcdfab26ac08c827e68328dbbf417bbe7577a2baaa5acc29d3e33b3cc0c6366df34455a9f1754cb0952c48461f71ca296b379a574e33bcdbb5ed26bad31220bfdffffff0210791c00000000001976a914a4b991e7c72996c424fe0215f70be6aa7fcae22c88ac80c3c901000000001976a914b0f6e64ea993466f84050becc101062bb502b4e488ac7af31200\",\n        \"c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a\": \"01000000018557003cb450f53922f63740f0f77db892ef27e15b2614b56309bfcee96a0ad3010000006a473044022041923c905ae4b5ed9a21aa94c60b7dbcb8176d58d1eb1506d9fb1e293b65ce01022015d6e9d2e696925c6ad46ce97cc23dec455defa6309b839abf979effc83b8b160121029332bf6bed07dcca4be8a5a9d60648526e205d60c75a21291bffcdefccafdac3fdffffff01c01c0f00000000001976a914a2185918aa1006f96ed47897b8fb620f28a1b09988ac01171300\",\n        \"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968\": \"01000000016d445091b7b4fa19cbbee30141071b2202d0c27d195b9d6d2bcc7085c9cd9127010000008b483045022100daf671b52393af79487667eddc92ebcc657e8ae743c387b25d1c1a2e19c7a4e7022015ef2a52ea7e94695de8898821f9da539815775516f18329896e5fc52a3563b30141041704a3daafaace77c8e6e54cf35ed27d0bf9bb8bcd54d1b955735ff63ec54fe82a80862d455c12e739108b345d585014bf6aa0cbd403817c89efa18b3c06d6b5fdffffff02144a4c00000000001976a9148942ac692ace81019176c4fb0ac408b18b49237f88ac404b4c00000000001976a914dd36d773acb68ac1041bc31b8a40ee504b164b2e88ace9ca1200\",\n        \"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306\": \"010000000125af87b0c2ebb9539d644e97e6159ccb8e1aa80fe986d01f60d2f3f37f207ae8010000008b483045022100baed0747099f7b28a5624005d50adf1069120356ac68c471a56c511a5bf6972b022046fbf8ec6950a307c3c18ca32ad2955c559b0d9bbd9ec25b64f4806f78cadf770141041ea9afa5231dc4d65a2667789ebf6806829b6cf88bfe443228f95263730b7b70fb8b00b2b33777e168bcc7ad8e0afa5c7828842794ce3814c901e24193700f6cfdffffff02a0860100000000001976a914ade907333744c953140355ff60d341cedf7609fd88ac68830a00000000001976a9145d48feae4c97677e4ca7dcd73b0d9fd1399c962b88acc9cc1300\",\n        \"e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25\": \"01000000010db780fff7dfcef6dba9268ecf4f6df45a1a86b86cad6f59738a0ce29b145c47010000008a47304402202887ec6ec200e4e2b4178112633011cbdbc999e66d398b1ff3998e23f7c5541802204964bd07c0f18c48b7b9c00fbe34c7bc035efc479e21a4fa196027743f06095f0141044f1714ed25332bb2f74be169784577d0838aa66f2374f5d8cbbf216063626822d536411d13cbfcef1ff3cc1d58499578bc4a3c4a0be2e5184b2dd7963ef67713fdffffff02a0860100000000001600145bbdf3ba178f517d4812d286a40c436a9088076e6a0b0c00000000001976a9143fc16bef782f6856ff6638b1b99e4d3f863581d388acfbcb1300\"\n    }\n    txid_list = sorted(list(transactions))\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n    def create_old_wallet(self):\n        ks = keystore.from_old_mpk('e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3')\n        # seed words: powerful random nobody notice nothing important anyway look away hidden message over\n        w = WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=20, gap_limit_for_change=6, config=self.config)\n        # some txns are beyond gap limit:\n        w.create_new_address(for_change=True)\n        return w\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_restoring_old_wallet_txorder1(self, mock_save_db):\n        w = self.create_old_wallet()\n        for i in [2, 12, 7, 9, 11, 10, 16, 6, 17, 1, 13, 15, 5, 8, 4, 0, 14, 18, 3]:\n            tx = Transaction(self.transactions[self.txid_list[i]])\n            w.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(27633300, sum(w.get_balance()))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_restoring_old_wallet_txorder2(self, mock_save_db):\n        w = self.create_old_wallet()\n        for i in [9, 18, 2, 0, 13, 3, 1, 11, 4, 17, 7, 14, 12, 15, 10, 8, 5, 6, 16]:\n            tx = Transaction(self.transactions[self.txid_list[i]])\n            w.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(27633300, sum(w.get_balance()))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_restoring_old_wallet_txorder3(self, mock_save_db):\n        w = self.create_old_wallet()\n        for i in [5, 8, 17, 0, 9, 10, 12, 3, 15, 18, 2, 11, 14, 7, 16, 1, 4, 6, 13]:\n            tx = Transaction(self.transactions[self.txid_list[i]])\n            w.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n        self.assertEqual(27633300, sum(w.get_balance()))\n\n\nclass TestWalletHistory_EvilGapLimit(ElectrumTestCase):\n    TESTNET = True\n    transactions = {\n        # txn A:\n        \"511a35e240f4c8855de4c548dad932d03611a37e94e9203fdb6fc79911fe1dd4\": \"010000000001018aacc3c8f98964232ebb74e379d8ff4e800991eecfcf64bd1793954f5e50a8790100000000fdffffff0340420f0000000000160014dbf321e905d544b54b86a2f3ed95b0ac66a3ddb0ff0514000000000016001474f1c130d3db22894efb3b7612b2c924628d0d7e80841e000000000016001488492707677190c073b6555fb08d37e91bbb75d802483045022100cf2904e09ea9d2670367eccc184d92fcb8a9b9c79a12e4efe81df161077945db02203530276a3401d944cf7a292e0660f36ee1df4a1c92c131d2c0d31d267d52524901210215f523a412a5262612e1a5ef9842dc864b0d73dc61fb4c6bfd480a867bebb1632e181400\",\n        # txn B:\n        \"fde0b68938709c4979827caa576e9455ded148537fdb798fd05680da64dc1b4f\": \"01000000000101a317998ac6cc717de17213804e1459900fe257b9f4a3b9b9edd29806728277530100000000fdffffff03c0c62d00000000001600149543301687b1ca2c67718d55fbe10413c73ddec200093d00000000001600141bc12094a4475dcfbf24f9920dafddf9104ca95b3e4a4c0000000000160014b226a59f2609aa7da4026fe2c231b5ae7be12ac302483045022100f1082386d2ce81612a3957e2801803938f6c0066d76cfbd853918d4119f396df022077d05a2b482b89707a8a600013cb08448cf211218a462f2a23c2c0d80a8a0ca7012103f4aac7e189de53d95e0cb2e45d3c0b2be18e93420734934c61a6a5ad88dd541033181400\",\n        # txn C:\n        \"268fce617aaaa4847835c2212b984d7b7741fdab65de22813288341819bc5656\": \"010000000001014f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0100000000fdffffff0260e316000000000016001445e9879cf7cd5b4a15df7ddcaf5c6dca0e1508bacc242600000000001600141bc12094a4475dcfbf24f9920dafddf9104ca95b02483045022100ae3618912f341fefee11b67e0047c47c88c4fa031561c3fafe993259dd14d846022056fa0a5b5d8a65942fa68bcc2f848fd71fa455ba42bc2d421b67eb49ba62aa4e01210394d8f4f06c2ea9c569eb050c897737a7315e7f2104d9b536b49968cc89a1f11033181400\",\n    }\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({\n            'electrum_path': self.electrum_path,\n        })\n        self.config.NETWORK_SKIPMERKLECHECK = True  # needed for Synchronizer to generate new addresses without SPV\n\n    def create_wallet(self):\n        ks = keystore.from_xpub('vpub5Vhmk4dEJKanDTTw6immKXa3thw45u3gbd1rPYjREB6viP13sVTWcH6kvbR2YeLtGjradr6SFLVt9PxWDBSrvw1Dc1nmd3oko3m24CQbfaJ')\n        # seed words: nephew work weather maze pyramid employ check permit garment scene kiwi smooth\n        w = WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=20, config=self.config)\n        return w\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_restoring_wallet_txorder1(self, mock_save_db):\n        w = self.create_wallet()\n        w.db.put('stored_height', 1316917 + 100)\n        for txid in self.transactions:\n            tx = Transaction(self.transactions[txid])\n            w.adb.add_transaction(tx)\n        # txn A is an external incoming txn paying to addr (3) and (15)\n        # txn B is an external incoming txn paying to addr (4) and (25)\n        # txn C is an internal transfer txn from addr (25) -- to -- (1) and (25)\n        w.adb.receive_history_callback('tb1qgh5c088he4d559wl0hw27hrdeg8p2z96pefn4q',  # HD index 1\n                                   [('268fce617aaaa4847835c2212b984d7b7741fdab65de22813288341819bc5656', 1316917)],\n                                   {})\n        w.synchronize()\n        w.adb.receive_history_callback('tb1qm0ejr6g964zt2jux5te7m9ds43n28hdsdz9ull',  # HD index 3\n                                   [('511a35e240f4c8855de4c548dad932d03611a37e94e9203fdb6fc79911fe1dd4', 1316912)],\n                                   {})\n        w.synchronize()\n        w.adb.receive_history_callback('tb1qj4pnq958k89zcem3342lhcgyz0rnmhkzl6x0cl',  # HD index 4\n                                   [('fde0b68938709c4979827caa576e9455ded148537fdb798fd05680da64dc1b4f', 1316917)],\n                                   {})\n        w.synchronize()\n        w.adb.receive_history_callback('tb1q3pyjwpm8wxgvquak240mprfhaydmkawcsl25je',  # HD index 15\n                                   [('511a35e240f4c8855de4c548dad932d03611a37e94e9203fdb6fc79911fe1dd4', 1316912)],\n                                   {})\n        w.synchronize()\n        w.adb.receive_history_callback('tb1qr0qjp99ygawul0eylxfqmt7alygye22mj33vej',  # HD index 25\n                                   [('fde0b68938709c4979827caa576e9455ded148537fdb798fd05680da64dc1b4f', 1316917),\n                                    ('268fce617aaaa4847835c2212b984d7b7741fdab65de22813288341819bc5656', 1316917)],\n                                   {})\n        w.synchronize()\n        self.assertEqual(9999788, sum(w.get_balance()))\n\n\nclass TestWalletHistory_DoubleSpend(ElectrumTestCase):\n    TESTNET = True\n    transactions = {\n        # txn A:\n        \"a3849040f82705151ba12a4389310b58a17b78025d81116a3338595bdefa1625\": \"020000000001011b7eb29921187b40209c234344f57a3365669c8883a3d511fbde5155f11f64d10000000000fdffffff024c400f0000000000160014b50d21483fb5e088db90bf766ea79219fb377fef40420f0000000000160014aaf5fc4a6297375c32403a9c2768e7029c8dbd750247304402206efd510954b289829f8f778163b98a2a4039deb93c3b0beb834b00cd0add14fd02201c848315ddc52ced0350a981fe1a7f3cbba145c7a43805db2f126ed549eaa500012103083a50d63264743456a3e812bfc91c11bd2a673ba4628c09f02d78f62157e56d788d1700\",\n        # txn B:\n        \"0e2182ead6660790290371516cb0b80afa8baebd30dad42b5e58a24ceea17f1c\": \"020000000001012516fade5b5938336a11815d02787ba1580b3189432aa11b150527f8409084a30100000000fdffffff02a086010000000000160014cb893c9fbb565363556fb18a3bcdda6f20af0bf8d8ba0d0000000000160014478902f02c2b6cd405bb6bd1f90e9860bec173e20247304402206940671b5bdb230a9721aa57396af73d399fb210d795e7dbb8ec1977e101a5470220625505de035d4006b72bd6dfcf09468d1e8da53071080b37b16b0dbbf776db78012102254b5b20ed21c3bba75ec2a9ff230257d13a2493f6b7da066d8195dcdd484310788d1700\",\n        # txn C:\n        \"2c9aa33d9c8ec649f9bfb84af027a5414b760be5231fe9eca4a95b9eb3f8a017\": \"020000000001012516fade5b5938336a11815d02787ba1580b3189432aa11b150527f8409084a30100000000fdffffff01d2410f00000000001600147880a7c79744b908a5f6d6235f2eb46c174c84f002483045022100974d27c872f09115e57c6acb674cd4da6d0b26656ad967ddb2678ff409714b9502206d91b49cf778ced6ca9e40b4094fb57b86c86fac09ce46ce53aea4afa68ff311012102254b5b20ed21c3bba75ec2a9ff230257d13a2493f6b7da066d8195dcdd484310788d1700\",\n    }\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_restoring_wallet_without_manual_delete(self, mock_save_db):\n        w = restore_wallet_from_text__for_unittest(\n            \"small rapid pattern language comic denial donate extend tide fever burden barrel\",\n            path='if_this_exists_mocking_failed_648151893',\n            gap_limit=5,\n            config=self.config)['wallet']  # type: Abstract_Wallet\n        for txid in self.transactions:\n            tx = Transaction(self.transactions[txid])\n            w.adb.add_transaction(tx)\n        # txn A is an external incoming txn funding the wallet\n        # txn B is an outgoing payment to an external address\n        # txn C is double-spending txn B, to a wallet address\n        self.assertEqual(999890, sum(w.get_balance()))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_restoring_wallet_with_manual_delete(self, mock_save_db):\n        w = restore_wallet_from_text__for_unittest(\n            \"small rapid pattern language comic denial donate extend tide fever burden barrel\",\n            path='if_this_exists_mocking_failed_648151893',\n            gap_limit=5,\n            config=self.config)['wallet']  # type: Abstract_Wallet\n        # txn A is an external incoming txn funding the wallet\n        txA = Transaction(self.transactions[\"a3849040f82705151ba12a4389310b58a17b78025d81116a3338595bdefa1625\"])\n        w.adb.add_transaction(txA)\n        # txn B is an outgoing payment to an external address\n        txB = Transaction(self.transactions[\"0e2182ead6660790290371516cb0b80afa8baebd30dad42b5e58a24ceea17f1c\"])\n        w.adb.add_transaction(txB)\n        # now the user manually deletes txn B to attempt the double spend\n        # txn C is double-spending txn B, to a wallet address\n        # rationale1: user might do this with opt-in RBF transactions\n        # rationale2: this might be a local transaction, in which case the GUI even allows it\n        w.adb.remove_transaction(txB.txid())\n        txC = Transaction(self.transactions[\"2c9aa33d9c8ec649f9bfb84af027a5414b760be5231fe9eca4a95b9eb3f8a017\"])\n        w.adb.add_transaction(txC)\n        self.assertEqual(999890, sum(w.get_balance()))\n\n\nclass TestWalletHistory_HelperFns(ElectrumTestCase):\n    TESTNET = True\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_get_tx_status_feerate_for_local_2of3_multisig_partial_tx(self, mock_save_db):\n        wallet1 = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', passphrase='', for_multisig=True),\n                keystore.from_xpub('Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf'),\n                keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra')\n            ],\n            '2of3', gap_limit=2,\n            config=self.config,\n        )\n\n        # bootstrap wallet1\n        funding_tx = Transaction('01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c11300')\n        wallet1.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet1 -> wallet2\n        outputs = [PartialTxOutput.from_address_and_value(\"2MuUcGmQ2mLN3vjTuqDSgZpk4LPKDsuPmhN\", 165000)]\n        tx = wallet1.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)\n        self.assertEqual(\n            \"wsh(sortedmulti(2,[b2e35a7d/1h]tpubD9aPYLPPYw8MxU3cD57LwpV5v7GomHxdv62MSbPcRkp47zwXx69ACUFsKrj8xzuzRrij9FWVhfvkvNqtqsr8ZtefkDsGZ9GLuHzoS6bXyk1/0/0,[53b77ddb/1h]tpubD8spLJysN7v7V1KHvkZ7AwjnXShKafopi7Vu3Ahs2S46FxBPTode8DgGxDo55k4pJvETGScZFwnM5f2Y31EUjteJdhxR73sjr9ieydgah2U/0/0,[43067d63/1h]tpubD8khd1g1tzFeKeaU59QV811hyvhwn9KDfy5sqFJ5m2wJLw6rUt4AZviqutRPXTUAK4SpU2we3y2WBP916Ma8Em4qFGcbYkFvXVfpGYV3oZR/0/0))\",\n            tx.inputs()[0].script_descriptor.to_string_no_checksum())\n        partial_tx = tx.serialize_as_bytes().hex()\n        self.assertEqual(\"70736274ff01007e0100000001213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf874387000000000001012b400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0100eb01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c1130001056952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22060223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa10b2e35a7d01000080000000000000000022060273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e1053b77ddb010000800000000000000000220602aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9411043067d6301000080000000000000000000010169522102174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a2102c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd52102eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98053ae220202174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a1053b77ddb010000800100000000000000220202c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd51043067d63010000800100000000000000220202eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98010b2e35a7d0100008001000000000000000000\",\n                         partial_tx)\n        tx = tx_from_any(partial_tx)  # simulates moving partial txn between cosigners\n        self.assertFalse(tx.is_complete())\n\n        wallet1.adb.add_transaction(tx)\n        # let's see if the calculated feerate correct:\n        self.assertEqual((3, 'Local [26.3 sat/vB]'),\n                         wallet1.get_tx_status(tx.txid(), TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)))\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_get_tx_status_feerate_for_local_2of3_multisig_signed_tx(self, mock_save_db):\n        wallet1 = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', passphrase='', for_multisig=True),\n                keystore.from_xpub('Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf'),\n                keystore.from_xpub('Vpub5fjkKyYnvSS4wBuakWTkNvZDaBM2vQ1MeXWq368VJHNr2eT8efqhpmZ6UUkb7s2dwCXv2Vuggjdhk4vZVyiAQTwUftvff73XcUGq2NQmWra')\n            ],\n            '2of3', gap_limit=2,\n            config=self.config,\n        )\n\n        # bootstrap wallet1\n        funding_tx = Transaction('01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c11300')\n        wallet1.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)\n\n        # wallet1 -> wallet2\n        tx = tx_from_any(\"01000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000\")\n        wallet1.adb.add_transaction(tx)\n        # let's see if the calculated feerate correct:\n        self.assertEqual((3, 'Local [26.3 sat/vB]'),\n                         wallet1.get_tx_status(tx.txid(), TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0)))\n\n\nclass TestImportedWallet(ElectrumTestCase):\n    TESTNET = True\n    transactions = {\n        # txn A funds addr1:\n        \"0e350564ee7ed4ffce24a998b538f7f3ebbab6fcb4bb331f8bb6b9d86d86fcd8\": \"02000000000101470cfc737af6bf917ce35bf7224b1021ef87349cd7f150464e6a0e3ee0cf6f1a0400000000fdffffff0261de0c0000000000160014f6aa7ea83b54335553ece4de88b3e9af6fb4ff0b92b78b00000000001600141dfacc496a9c98227631e3df4796baf3ba8254120247304402201a1b70f27ffcaeecaebad147117e9f4f541e3c630112c395e8237b5f1404f9170220600c96b92a55f8ee99da3fcaf9ca5595468742107651c5cea5798b0e672c7a5b012103ccaf45a46ead9648fc60ba0476f3f820d73fbf75f7d9af626d0512a042c1fc9a41091e00\",\n        # txn B funds addr2:\n        \"314385a9f24457098de9fe5cb3893cc408b9f66085268457b82050c988c97908\": \"0200000000010165806607dd458280cb57bf64a16cf4be85d053145227b98c28932e953076b8e20000000000fdffffff01fa3e0f0000000000160014810480bbaf62145abf945ebe5f657c665a3a37320247304402206df590e0ebae186cd7078e2e9841ec8e2c4c1efff4ee3ac2029fe0a5f1a752c002204cd33bafe4145b66a28dff453d7cb440a7ec6ae53df786e0438bcd6aae50fc8e0121026269e54d06f7070c1f967eb2874ba60de550dfc327a945c98eb773672d9411fd7b181e00\",\n        # txn C spends both UTXOs:\n        \"54de13f7ee4853dc1a281c0e7132efb95330f7ceebc1dbce76fdf34c28028f14\": \"02000000000102d8fc866dd8b9b68b1f33bbb4fcb6baebf3f738b598a924ceffd47eee6405350e0000000000feffffff0879c988c95020b85784268560f6b908c43c89b35cfee98d095744f2a98543310000000000feffffff023cda0c000000000016001451c27c0521388d430ee91137a76d67a368e998c140420f0000000000220020210be57842d95c8cae3c9a2e0250407f9599c75c77eb435d5942fc5cf41505a40247304402201ec23b32a21c1efe186c6ffb0d0f0ed40f1819b4200a844f1a71463873a9e4240220613fca783787449d779cb3e2052682349cbf5f99316641e3eddc36cb510a4ac70121038e1724d08580eec8f7f7a52829a2f09473961df96010f55d913556dee69cc9a10247304402203441cd69d916fdd9fe1864713abad383972e51588fb161174d88471c907c803d022078ca2056407dca3b07f0b109d0f6f55aa5a15e2d385f58a928cac8a589afc026012103bf013054c5b2b4845a5f4b227bd6264dbbfe70936e2675b9ffe004226771e6c1e7692100\",\n    }\n\n    def setUp(self):\n        super().setUp()\n        self.config = SimpleConfig({'electrum_path': self.electrum_path})\n\n    @mock.patch.object(wallet.Abstract_Wallet, 'save_db')\n    async def test_importing_and_deleting_addresses(self, mock_save_db):\n        w = restore_wallet_from_text__for_unittest(\n            \"tb1q7648a2pm2se425lvun0g3vlf4ahmflcthegz63\",\n            path='if_this_exists_mocking_failed_648151893',\n            config=self.config)['wallet']  # type: Abstract_Wallet\n        self.assertEqual(1, len(w.get_addresses()))\n        w.adb.add_transaction(Transaction(self.transactions[\"0e350564ee7ed4ffce24a998b538f7f3ebbab6fcb4bb331f8bb6b9d86d86fcd8\"]))\n        w.adb.add_transaction(Transaction(self.transactions[\"54de13f7ee4853dc1a281c0e7132efb95330f7ceebc1dbce76fdf34c28028f14\"]))\n        self.assertEqual(0, sum(w.get_balance()))\n\n        with self.assertRaises(UnrelatedTransactionException):\n            w.adb.add_transaction(Transaction(self.transactions[\"314385a9f24457098de9fe5cb3893cc408b9f66085268457b82050c988c97908\"]))\n        w.import_address(\"tb1qsyzgpwa0vg2940u5t6l97etuvedr5dejpf9tdy\")\n        self.assertEqual(2, len(w.get_addresses()))\n        self.assertEqual(2, len(w.db.transactions))\n        self.assertEqual(0, sum(w.get_balance()))\n\n        w.adb.add_transaction(Transaction(self.transactions[\"314385a9f24457098de9fe5cb3893cc408b9f66085268457b82050c988c97908\"]))\n        self.assertEqual(3, len(w.db.transactions))\n        self.assertEqual(0, sum(w.get_balance()))\n\n        w.delete_address(\"tb1q7648a2pm2se425lvun0g3vlf4ahmflcthegz63\")\n        self.assertEqual(2, len(w.db.transactions))\n        self.assertEqual(\n            {\"54de13f7ee4853dc1a281c0e7132efb95330f7ceebc1dbce76fdf34c28028f14\", \"314385a9f24457098de9fe5cb3893cc408b9f66085268457b82050c988c97908\"},\n            set(w.db.transactions))\n        self.assertEqual(0, sum(w.get_balance()))\n\n        with self.assertRaises(UserFacingException) as ctx:\n            w.delete_address(\"tb1qsyzgpwa0vg2940u5t6l97etuvedr5dejpf9tdy\")\n        self.assertTrue(\"Cannot delete last remaining address\" in ctx.exception.args[0])\n"
  },
  {
    "path": "tests/test_wizard.py",
    "content": "import os\n\nfrom electrum import SimpleConfig\nfrom electrum.interface import ServerAddr\nfrom electrum.keystore import bip44_derivation, Hardware_KeyStore, KeyStore, BIP32_KeyStore\nfrom electrum.network import NetworkParameters, ProxySettings\nfrom electrum.plugin import Plugins, DeviceInfo, Device\nfrom electrum.wizard import ServerConnectWizard, NewWalletWizard, WizardViewState, KeystoreWizard\nfrom electrum.daemon import Daemon\nfrom electrum.wallet import Abstract_Wallet, Deterministic_Wallet\nfrom electrum import util\nfrom electrum import slip39\nfrom electrum.bip32 import KeyOriginInfo\nfrom electrum import keystore\nfrom electrum.storage import WalletStorage\n\nfrom . import ElectrumTestCase\nfrom .test_wallet_vertical import UNICODE_HORROR, WalletIntegrityHelper\n\n\nclass NetworkMock:\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self.run_called = False\n        self.parameters = NetworkParameters(server=None, proxy=None, auto_connect=None, oneserver=None)\n\n    def run_from_another_thread(self, *args, **kwargs):\n        self.run_called = True\n\n    def set_parameters(self, parameters):\n        self.parameters = parameters\n\n    def get_parameters(self):\n        return self.parameters\n\n\nclass DaemonMock:\n    def __init__(self, config: SimpleConfig):\n        self.config = config\n        self.network = NetworkMock()\n\n\nclass WizardTestCase(ElectrumTestCase):\n\n    def setUp(self):\n        super().setUp()\n\n        self.config = SimpleConfig({\n            'electrum_path': self.electrum_path,\n            'enable_plugin_trustedcoin': True,\n        })\n        self.wallet_path = os.path.join(self.electrum_path, \"somewallet\")\n        self.plugins = Plugins(self.config, gui_name='cmdline')\n        self.plugins.load_plugin_by_name('trustedcoin')\n        # note: hw plugins are loaded on-demand\n\n    def tearDown(self):\n        self.plugins.stop()\n        self.plugins.stopped_event.wait()\n        super().tearDown()\n\n\nclass ServerConnectWizardTestCase(WizardTestCase):\n\n    async def test_no_advanced(self):\n        w = ServerConnectWizard(DaemonMock(self.config))\n        v_init = w.start()\n\n        d = {'autoconnect': True, 'want_proxy': False}\n        self.assertTrue(w.is_last_view(v_init.view, d))\n        w.resolve_next(v_init.view, d)\n        self.assertEqual(True, self.config.NETWORK_AUTO_CONNECT)\n\n    async def test_server(self):\n        w = ServerConnectWizard(DaemonMock(self.config))\n        v_init = w.start()\n\n        d = {'autoconnect': False, 'want_proxy': False}\n        self.assertFalse(w.is_last_view(v_init.view, d))\n        v = w.resolve_next(v_init.view, d)\n        self.assertEqual('server_config', v.view)\n        self.assertFalse(self.config.cv.NETWORK_AUTO_CONNECT.is_set())\n\n    async def test_proxy(self):\n        w = ServerConnectWizard(DaemonMock(self.config))\n        v_init = w.start()\n        w._daemon.network.reset()\n\n        d = {'autoconnect': True, 'want_proxy': True}\n        self.assertFalse(w.is_last_view(v_init.view, d))\n        v = w.resolve_next(v_init.view, d)\n        self.assertEqual('proxy_config', v.view)\n        self.assertEqual(True, self.config.NETWORK_AUTO_CONNECT)\n        d_proxy = {'enabled': True, 'mode': 'socks5', 'host': 'localhost', 'port': '1'}\n        d.update({'proxy': d_proxy})\n        v = w.resolve_next(v.view, d)\n        self.assertTrue(w.is_last_view(v.view, d))\n\n        self.assertTrue(w._daemon.network.run_called)\n        self.assertEqual(NetworkParameters(server=None, proxy=ProxySettings.from_dict(d_proxy), auto_connect=True, oneserver=None), w._daemon.network.parameters)\n\n    async def test_proxy_and_server(self):\n        w = ServerConnectWizard(DaemonMock(self.config))\n        v_init = w.start()\n        w._daemon.network.reset()\n\n        d = {'autoconnect': False, 'want_proxy': True}\n        self.assertFalse(w.is_last_view(v_init.view, d))\n        v = w.resolve_next(v_init.view, d)\n        self.assertEqual('proxy_config', v.view)\n        self.assertFalse(self.config.cv.NETWORK_AUTO_CONNECT.is_set())\n        d_proxy = {'enabled': False}\n        d.update({'proxy': d_proxy})\n        v = w.resolve_next(v.view, d)\n\n        w._daemon.network.reset()\n        self.assertEqual('server_config', v.view)\n        d.update({'server': 'localhost:1:t'})\n        self.assertTrue(w.is_last_view(v.view, d))\n        v = w.resolve_next(v.view, d)\n\n        serverobj = ServerAddr.from_str_with_inference('localhost:1:t')\n        self.assertTrue(w._daemon.network.run_called)\n        self.assertEqual(NetworkParameters(server=serverobj, proxy=None, auto_connect=False, oneserver=False), w._daemon.network.parameters)\n\n\nclass KeystoreWizardTestCase(WizardTestCase):\n\n    class TKeystoreWizard(KeystoreWizard):\n        def is_single_password(self):\n            \"\"\"impl abstract reqd\"\"\"\n            return True\n\n    class TNewWalletWizard(NewWalletWizard):\n        def is_single_password(self):\n            \"\"\"impl abstract reqd\"\"\"\n            return True\n\n    def _wizard_for(self, *, wallet_type: str = 'standard', hww: bool = False) -> tuple[KeystoreWizard, WizardViewState]:\n        w = KeystoreWizardTestCase.TKeystoreWizard(self.plugins)\n        start_viewstate = WizardViewState('keystore_type', {'wallet_type': wallet_type}, {})\n        v = w.start(start_viewstate=start_viewstate)\n        self.assertEqual('keystore_type', v.view)\n        d = v.wizard_data\n        if hww:\n            d.update({'keystore_type': 'hardware'})\n            v = w.resolve_next(v.view, d)\n            self.assertEqual('choose_hardware_device', v.view)\n        else:\n            d.update({'keystore_type': 'haveseed'})\n            v = w.resolve_next(v.view, d)\n            self.assertEqual('enter_seed', v.view)\n\n        return w, v\n\n    def _create_xpub_keystore_wallet(self, *, wallet_type: str = 'standard', xpub):\n        w = KeystoreWizardTestCase.TNewWalletWizard(DaemonMock(self.config), self.plugins)\n        wallet_path = self.wallet_path\n        d = {\n            'wallet_type': wallet_type,\n            'keystore_type': 'masterkey',\n            'master_key': xpub,\n            'password': None,\n            'encrypt': False,\n        }\n        w.create_storage(wallet_path, d)\n        self.assertTrue(os.path.exists(wallet_path))\n        wallet = Daemon._load_wallet(wallet_path, password=None, config=self.config)\n        return wallet\n\n    def _sanity_checks_after_disabling_keystore(\n        self,\n        *,\n        ks: 'KeyStore',\n        xpub: str,\n        key_origin_info: KeyOriginInfo,\n    ) -> None:\n        self.assertTrue(ks.is_watching_only())\n        self.assertTrue(ks.type in ('bip32', 'old'))\n        self.assertFalse(ks.has_seed())\n        self.assertEqual(ks.get_master_public_key(), xpub)\n        if isinstance(ks, BIP32_KeyStore):\n            self.assertEqual(ks.xprv, None)\n        self.assertEqual(ks.get_key_origin_info(), key_origin_info)\n\n    async def test_haveseed_electrum(self):\n        w, v = self._wizard_for()\n        d = v.wizard_data\n        myseed = '9dk'\n        mypassphrase = ''\n        myxpub = 'zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr'\n        d.update({\n            'seed': myseed, 'seed_type': 'segwit', 'seed_extend': False, 'seed_variant': 'electrum',\n        })\n        self.assertTrue(w.is_last_view(v.view, d))\n        w.resolve_next(v.view, d)\n        ks, ishww = w._result\n        self.assertFalse(ishww)\n        self.assertEqual(ks.xpub, myxpub)\n\n        wallet = self._create_xpub_keystore_wallet(xpub=myxpub)\n        self.assertTrue(wallet.get_keystore().is_watching_only())\n        self.assertTrue(wallet.can_enable_disable_keystore(ks))\n        wallet.enable_keystore(ks, ishww, None)\n        self.assertFalse(wallet.get_keystore().is_watching_only())\n        self.assertEqual(myseed, wallet.get_keystore().get_seed(None))\n        self.assertEqual(mypassphrase, wallet.get_keystore().get_passphrase(None))\n\n        my_keyorigininfo = wallet.get_keystore().get_key_origin_info()\n        wallet.disable_keystore(wallet.get_keystore())\n        self._sanity_checks_after_disabling_keystore(ks=wallet.get_keystore(), xpub=myxpub, key_origin_info=my_keyorigininfo)\n\n    async def test_haveseed_ext_electrum(self):\n        w, v = self._wizard_for()\n        d = v.wizard_data\n        myseed = '9dk'\n        mypassphrase = 'abc'\n        myxpub = 'zpub6oLFCUpqxT8BUzy8g5miUuRofPZ46ZjjvZfcfH7qJanRM7aRYGpNX4uBGtcJRbgcKbi7dYkiiPw1GB2sc3SufyDcZskuQEWp5jBwbNcj1VL'\n        d.update({\n            'seed': myseed, 'seed_type': 'segwit', 'seed_extend': True, 'seed_variant': 'electrum',\n        })\n        self.assertFalse(w.is_last_view(v.view, d))\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('enter_ext', v.view)\n        d.update({'seed_extra_words': mypassphrase})\n        self.assertTrue(w.is_last_view(v.view, d))\n        w.resolve_next(v.view, d)\n        ks, ishww = w._result\n        self.assertFalse(ishww)\n        self.assertEqual(ks.xpub, myxpub)\n\n        wallet = self._create_xpub_keystore_wallet(xpub=myxpub)\n        self.assertTrue(wallet.get_keystore().is_watching_only())\n        self.assertTrue(wallet.can_enable_disable_keystore(ks))\n        wallet.enable_keystore(ks, ishww, None)\n        self.assertFalse(wallet.get_keystore().is_watching_only())\n        self.assertEqual(myseed, wallet.get_keystore().get_seed(None))\n        self.assertEqual(mypassphrase, wallet.get_keystore().get_passphrase(None))\n\n        my_keyorigininfo = wallet.get_keystore().get_key_origin_info()\n        wallet.disable_keystore(wallet.get_keystore())\n        self._sanity_checks_after_disabling_keystore(ks=wallet.get_keystore(), xpub=myxpub, key_origin_info=my_keyorigininfo)\n\n    async def test_haveseed_electrum__mismatching_seed(self):\n        \"\"\"adding an unrelated seed to an xpub-only keystore should raise\"\"\"\n        w, v = self._wizard_for()\n        d = v.wizard_data\n        d.update({\n            'seed': 'abandon bike', 'seed_type': 'segwit', 'seed_extend': False, 'seed_variant': 'electrum',\n        })\n        self.assertTrue(w.is_last_view(v.view, d))\n        w.resolve_next(v.view, d)\n        ks, ishww = w._result\n        self.assertFalse(ishww)\n\n        wallet = self._create_xpub_keystore_wallet(xpub='zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr')\n        self.assertTrue(wallet.get_keystore().is_watching_only())\n        self.assertTrue(wallet.can_enable_disable_keystore(ks))\n        with self.assertRaises(Exception) as ctx:\n            wallet.enable_keystore(ks, ishww, None)\n        self.assertTrue(\"mismatching xpubs\" in ctx.exception.args[0])\n\n    async def test_haveseed_electrum_oldseed(self):\n        w, v = self._wizard_for()\n        d = v.wizard_data\n        myseed = 'powerful random nobody notice nothing important anyway look away hidden message over'\n        mypassphrase = ''\n        myxpub = 'e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3'\n        d.update({\n            'seed': myseed,\n            'seed_type': 'old', 'seed_extend': False, 'seed_variant': 'electrum',\n        })\n        self.assertTrue(w.is_last_view(v.view, d))\n        w.resolve_next(v.view, d)\n        ks, ishww = w._result\n        self.assertFalse(ishww)\n        self.assertEqual(ks.get_master_public_key(), myxpub)\n\n        wallet = self._create_xpub_keystore_wallet(xpub=myxpub)\n        self.assertTrue(wallet.get_keystore().is_watching_only())\n        self.assertTrue(wallet.can_enable_disable_keystore(ks))\n        wallet.enable_keystore(ks, ishww, None)\n        self.assertFalse(wallet.get_keystore().is_watching_only())\n        self.assertEqual(myseed, wallet.get_keystore().get_seed(None))\n        self.assertEqual(mypassphrase, wallet.get_keystore().get_passphrase(None))\n\n        my_keyorigininfo = wallet.get_keystore().get_key_origin_info()\n        wallet.disable_keystore(wallet.get_keystore())\n        self._sanity_checks_after_disabling_keystore(ks=wallet.get_keystore(), xpub=myxpub, key_origin_info=my_keyorigininfo)\n\n    async def test_haveseed_bip39(self):\n        w, v = self._wizard_for()\n        d = v.wizard_data\n        myxpub = 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'\n        d.update({\n            'seed': 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',\n            'seed_type': 'bip39', 'seed_extend': False, 'seed_variant': 'bip39',\n        })\n        self.assertFalse(w.is_last_view(v.view, d))\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('script_and_derivation', v.view)\n        d.update({'script_type': 'p2wpkh', 'derivation_path': 'm/84h/0h/0h'})\n        v = w.resolve_next(v.view, d)\n        ks, ishww = w._result\n        self.assertFalse(ishww)\n        self.assertEqual(ks.xpub, myxpub)\n\n        wallet = self._create_xpub_keystore_wallet(xpub=myxpub)\n        self.assertTrue(wallet.get_keystore().is_watching_only())\n        self.assertTrue(wallet.can_enable_disable_keystore(ks))\n        wallet.enable_keystore(ks, ishww, None)\n        self.assertFalse(wallet.get_keystore().is_watching_only())\n\n        my_keyorigininfo = wallet.get_keystore().get_key_origin_info()\n        wallet.disable_keystore(wallet.get_keystore())\n        self._sanity_checks_after_disabling_keystore(ks=wallet.get_keystore(), xpub=myxpub, key_origin_info=my_keyorigininfo)\n\n    async def test_haveseed_ext_bip39(self):\n        w, v = self._wizard_for()\n        d = v.wizard_data\n        myxpub = 'zpub6qaQ1V7UyjNRXR5u8QzTi1ibaWQkskUsfpi7na4oqwkXrZWzVqqohSKG8g2sL5m8CJju2E8GFRkZBxKKq5iEqS167CLLDK2jNz4vpNAea7X'\n        d.update({\n            'seed': 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',\n            'seed_type': 'bip39', 'seed_extend': True, 'seed_variant': 'bip39',\n        })\n        self.assertFalse(w.is_last_view(v.view, d))\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('enter_ext', v.view)\n        d.update({'seed_extra_words': 'abc'})\n        v = w.resolve_next(v.view, d)\n\n        self.assertEqual('script_and_derivation', v.view)\n        d.update({'script_type': 'p2wpkh', 'derivation_path': 'm/84h/0h/0h'})\n        v = w.resolve_next(v.view, d)\n        ks, ishww = w._result\n        self.assertFalse(ishww)\n        self.assertEqual(ks.xpub, myxpub)\n\n        wallet = self._create_xpub_keystore_wallet(xpub=myxpub)\n        self.assertTrue(wallet.get_keystore().is_watching_only())\n        self.assertTrue(wallet.can_enable_disable_keystore(ks))\n        wallet.enable_keystore(ks, ishww, None)\n        self.assertFalse(wallet.get_keystore().is_watching_only())\n\n        my_keyorigininfo = wallet.get_keystore().get_key_origin_info()\n        wallet.disable_keystore(wallet.get_keystore())\n        self._sanity_checks_after_disabling_keystore(ks=wallet.get_keystore(), xpub=myxpub, key_origin_info=my_keyorigininfo)\n\n    async def test_hww(self):\n        w, v = self._wizard_for(hww=True)\n        d = v.wizard_data\n        myxpub = 'zpub6rakEaM5ps5UiQ2yhbWiEkd6ceJfmuzegwc62G4itMz8L7rRFRqh6y8bTCScXV6NfTMUhANYQnfqfBd9dYfBRKf4LD1Yyfc8UvwY1MtNKWs'\n        d.update({\n            'hardware_device': (\n                'trezor',\n                DeviceInfo(\n                    device=Device(path='webusb:002:1', interface_number=-1, id_='webusb:002:1', product_key='Trezor', usage_page=0, transport_ui_string='webusb:002:1'),\n                    label='trezor_unittests', initialized=True, exception=None, plugin_name='trezor', soft_device_id='088C3F260B66F60E15DE0FA5', model_name='Trezor T'))})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trezor_start', v.view)\n        d.update({\n            'script_type': 'p2wpkh',\n            'derivation_path': bip44_derivation(0, bip43_purpose=84)\n        })\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trezor_xpub', v.view)\n        d.update({\n            'hw_type': 'trezor',\n            'master_key': myxpub,\n            'root_fingerprint': 'b3569ff0',\n            'label': 'trezor_unittests',\n            'soft_device_id': '088C3F260B66F60E15DE0FA5',\n        })\n        self.assertTrue(w.is_last_view(v.view, d))\n        v = w.resolve_next(v.view, d)\n\n        ks, ishww = w._result\n        self.assertTrue(ishww)\n\n        wallet = self._create_xpub_keystore_wallet(xpub=myxpub)\n        self.assertTrue(wallet.get_keystore().is_watching_only())\n        self.assertTrue(wallet.can_enable_disable_keystore(ks))\n        wallet.enable_keystore(ks, ishww, None)\n        self.assertFalse(wallet.get_keystore().is_watching_only())\n        self.assertTrue(isinstance(wallet.get_keystore(), Hardware_KeyStore))\n\n        my_keyorigininfo = wallet.get_keystore().get_key_origin_info()\n        wallet.disable_keystore(wallet.get_keystore())\n        self._sanity_checks_after_disabling_keystore(ks=wallet.get_keystore(), xpub=myxpub, key_origin_info=my_keyorigininfo)\n\n    async def test_multisig(self):\n        seed1 = \"bitter grass shiver impose acquire brush forget axis eager alone wine silver\"\n        xpub1 = \"Zpub6ymNkfdyhypEoqQNNGAUz9gXeiWJsW8AWx8Aa6PnDdeL76UC9b1UPGmEvwWzzkVVghVQuDBry7CK7wCBBdysRQgFFmdDSqi5kWoZ3A4cBuA\"\n        seed2 = \"snow nest raise royal more walk demise rotate smooth spirit canyon gun\"\n        xpub2 = \"Zpub6xwgqLvc42wXB1wEELTdALD9iXwStMUkGqBgxkJFYumaL2dWgNvUkjEDWyDFZD3fZuDWDzd1KQJ4NwVHS7hs6H6QkpNYSShfNiUZsgMdtNg\"\n\n        wallet = WalletIntegrityHelper.create_multisig_wallet(\n            [\n                keystore.from_seed(seed1, passphrase='', for_multisig=True),\n                keystore.from_xpub(xpub2),\n            ],\n            '2of2',\n            config=self.config,\n            storage=WalletStorage(self.wallet_path),\n        )\n\n        w, v = self._wizard_for(wallet_type=wallet.wallet_type)\n        d = v.wizard_data\n        d.update({\n            'seed': seed2, 'seed_type': 'segwit', 'seed_extend': False, 'seed_variant': 'electrum',\n        })\n        self.assertTrue(w.is_last_view(v.view, d))\n        w.resolve_next(v.view, d)\n        ks, ishww = w._result\n        self.assertFalse(ishww)\n        self.assertEqual(ks.xpub, xpub2)\n\n        self.assertFalse(wallet.get_keystores()[0].is_watching_only())\n        self.assertTrue(wallet.get_keystores()[1].is_watching_only())\n        self.assertTrue(wallet.can_enable_disable_keystore(ks))\n        wallet.enable_keystore(ks, ishww, None)\n        self.assertFalse(wallet.get_keystores()[0].is_watching_only())\n        self.assertFalse(wallet.get_keystores()[1].is_watching_only())\n        self.assertEqual(seed1, wallet.get_keystores()[0].get_seed(None))\n        self.assertEqual(seed2, wallet.get_keystores()[1].get_seed(None))\n\n        keyorigininfo1 = wallet.get_keystores()[0].get_key_origin_info()\n        wallet.disable_keystore(wallet.get_keystores()[0])\n        self._sanity_checks_after_disabling_keystore(ks=wallet.get_keystores()[0], xpub=xpub1, key_origin_info=keyorigininfo1)\n\n\nclass WalletWizardTestCase(WizardTestCase):\n\n    def _wizard_for(\n        self,\n        *,\n        name: str = \"mywallet\",\n        wallet_type: str,\n    ) -> NewWalletWizard:\n        w = NewWalletWizard(DaemonMock(self.config), self.plugins)\n        if wallet_type == '2fa':\n            w.plugins.get_plugin('trustedcoin').extend_wizard(w)\n        v_init = w.start()\n        self.assertEqual('wallet_name', v_init.view)\n        d = {'wallet_name': name}\n        self.assertFalse(w.is_last_view(v_init.view, d))\n        v = w.resolve_next(v_init.view, d)\n        self.assertEqual('wallet_type', v.view)\n\n        d.update({'wallet_type': wallet_type})\n        w.resolve_next(v.view, d)\n\n        return w\n\n    def _set_password_and_check_address(\n        self,\n        *,\n        v: WizardViewState,\n        w: NewWalletWizard,\n        recv_addr: str | None,  # \"first addr\" only makes sense for HD wallets\n        password: str | None = None,\n        encrypt_file: bool = False,\n    ) -> Abstract_Wallet:\n        d = v.wizard_data\n        self.assertEqual('wallet_password', v.view)\n\n        d.update({'password': password, 'encrypt': encrypt_file})\n        self.assertTrue(w.is_last_view(v.view, d))\n        v = w.resolve_next(v.view, d)\n\n        wallet_path = os.path.join(w._daemon.config.get_datadir_wallet_path(), d['wallet_name'])\n        w.create_storage(wallet_path, d)\n\n        self.assertTrue(os.path.exists(wallet_path))\n        wallet = Daemon._load_wallet(wallet_path, password=password, config=self.config)\n        if recv_addr is not None:\n            self.assertEqual(recv_addr, wallet.get_receiving_addresses()[0])\n        self.assertEqual(bool(password), wallet.has_password())\n        self.assertEqual(encrypt_file, wallet.has_storage_encryption())\n        return wallet\n\n    async def test_set_password_and_encrypt_file(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n        d.update({'seed': '9dk', 'seed_type': 'segwit', 'seed_extend': False, 'seed_variant': 'electrum'})\n        v = w.resolve_next(v.view, d)\n\n        wallet = self._set_password_and_check_address(\n            v=v, w=w, recv_addr=\"bc1qq2tmmcngng78nllq2pvrkchcdukemtj56uyue0\",\n            password=\"1234\", encrypt_file=True,\n        )\n        self.assertTrue(wallet.has_password())\n        with self.assertRaises(util.InvalidPassword):\n            wallet.check_password(\"0000\")\n        wallet.check_password(\"1234\")\n        self.assertTrue(wallet.has_keystore_encryption())\n        self.assertTrue(wallet.has_storage_encryption())\n\n    async def test_set_password_but_dont_encrypt_file(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n        d.update({'seed': '9dk', 'seed_type': 'segwit', 'seed_extend': False, 'seed_variant': 'electrum'})\n        v = w.resolve_next(v.view, d)\n\n        wallet = self._set_password_and_check_address(\n            v=v, w=w, recv_addr=\"bc1qq2tmmcngng78nllq2pvrkchcdukemtj56uyue0\",\n            password=\"1234\", encrypt_file=False,\n        )\n        self.assertTrue(wallet.has_password())\n        with self.assertRaises(util.InvalidPassword):\n            wallet.check_password(\"0000\")\n        wallet.check_password(\"1234\")\n        self.assertTrue(wallet.has_keystore_encryption())\n        self.assertFalse(wallet.has_storage_encryption())\n\n    async def test_create_standard_wallet_createseed(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'createseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('create_seed', v.view)\n\n        d.update({\n            'seed': '9dk', 'seed_type': 'segwit', 'seed_extend': False, 'seed_variant': 'electrum',\n        })\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('confirm_seed', v.view)\n\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qq2tmmcngng78nllq2pvrkchcdukemtj56uyue0\")\n\n    async def test_create_standard_wallet_createseed_passphrase(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'createseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('create_seed', v.view)\n\n        d.update({\n            'seed': '9dk', 'seed_type': 'segwit', 'seed_extend': True, 'seed_variant': 'electrum',\n        })\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('create_ext', v.view)\n\n        d.update({'seed_extra_words': UNICODE_HORROR})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('confirm_seed', v.view)\n\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('confirm_ext', v.view)\n\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qgvx24uzdv4mapfmtlu8azty5fxdcw9ghxu4pr4\")\n\n    async def test_create_standard_wallet_haveseed_electrum_oldseed(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n\n        d.update({\n            'seed': 'powerful random nobody notice nothing important anyway look away hidden message over',\n            'seed_type': 'old', 'seed_extend': False, 'seed_variant': 'electrum'})\n        v = w.resolve_next(v.view, d)\n        wallet = self._set_password_and_check_address(v=v, w=w, recv_addr=\"1FJEEB8ihPMbzs2SkLmr37dHyRFzakqUmo\")\n\n        self.assertIsInstance(wallet, Deterministic_Wallet)\n        self.assertEqual(wallet.get_seed(password=None), 'powerful random nobody notice nothing important anyway look away hidden message over')\n\n    async def test_create_standard_wallet_haveseed_electrum_oldseed_in_hex_format(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n\n        d.update({\n            'seed': 'acb740e454c3134901d7c8f16497cc1c',\n            'seed_type': 'old', 'seed_extend': False, 'seed_variant': 'electrum'})\n        v = w.resolve_next(v.view, d)\n        wallet = self._set_password_and_check_address(v=v, w=w, recv_addr=\"1FJEEB8ihPMbzs2SkLmr37dHyRFzakqUmo\")\n\n        self.assertIsInstance(wallet, Deterministic_Wallet)\n        self.assertEqual(wallet.get_seed(password=None), 'powerful random nobody notice nothing important anyway look away hidden message over')\n\n    async def test_create_standard_wallet_haveseed_electrum_oldseed_passphrase(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n\n        d.update({\n            'seed': 'powerful random nobody notice nothing important anyway look away hidden message over',\n            'seed_type': 'old', 'seed_extend': True, 'seed_variant': 'electrum'})\n        v = w.resolve_next(v.view, d)\n        # FIXME this diverges from the actual GUIs :(\n        #  the GUIs do validation using wizard.validate_seed() and don't go to 'have_ext' for next view.\n        #  the validation should be moved to the base impl!\n        self.assertEqual('have_ext', v.view)\n\n        d.update({'seed_extra_words': UNICODE_HORROR})\n        with self.assertRaises(Exception) as ctx:\n            v = w.resolve_next(v.view, d)\n        self.assertTrue(\"cannot have passphrase\" in ctx.exception.args[0])\n\n    async def test_create_standard_wallet_haveseed_electrum(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n\n        d.update({'seed': '9dk', 'seed_type': 'segwit', 'seed_extend': False, 'seed_variant': 'electrum'})\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qq2tmmcngng78nllq2pvrkchcdukemtj56uyue0\")\n\n    async def test_create_standard_wallet_haveseed_electrum_passphrase(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n\n        d.update({'seed': '9dk', 'seed_type': 'segwit', 'seed_extend': True, 'seed_variant': 'electrum'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_ext', v.view)\n\n        d.update({'seed_extra_words': UNICODE_HORROR})\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qgvx24uzdv4mapfmtlu8azty5fxdcw9ghxu4pr4\")\n\n    async def test_create_standard_wallet_have_master_key(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'masterkey'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_master_key', v.view)\n\n        d.update({\n            'master_key': 'zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr',\n            'multisig_master_pubkey': 'zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr'})\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qq2tmmcngng78nllq2pvrkchcdukemtj56uyue0\")\n\n    async def test_create_standard_wallet_haveseed_bip39(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n\n        d.update({'seed': 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',\n                  'seed_type': 'bip39', 'seed_extend': False, 'seed_variant': 'bip39'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('script_and_derivation', v.view)\n\n        d.update({'script_type': 'p2wpkh', 'derivation_path': 'm/84h/0h/0h'})\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu\")\n\n    async def test_create_standard_wallet_haveseed_bip39_passphrase(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n\n        d.update({'seed': 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',\n                  'seed_type': 'bip39', 'seed_extend': True, 'seed_variant': 'bip39'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_ext', v.view)\n\n        d.update({'seed_extra_words': UNICODE_HORROR})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('script_and_derivation', v.view)\n\n        d.update({'script_type': 'p2wpkh', 'derivation_path': 'm/84h/0h/0h'})\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qjc3dsy5wxaksae6zqmr3nwjsmuckwqca8flql3\")\n\n    async def test_create_standard_wallet_haveseed_slip39(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n\n        # SLIP39 shares (128 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6)\n        mnemonics = [\n            \"fact else acrobat romp analysis usher havoc vitamins analysis garden prevent romantic silent dramatic adjust priority mailman plains vintage else\",\n            \"fact else ceramic round craft lips snake faint adorn square bucket deadline violence guitar greatest academic stadium snake frequent memory\",\n            \"fact else ceramic scatter counter remove club forbid busy cause taxi forecast prayer uncover living type training forward software pumps\",\n            \"fact else ceramic shaft clock crowd detect cleanup wildlife depict include trip profile isolate express category wealthy advance garden mixture\",\n        ]\n        encrypted_seed = slip39.recover_ems(mnemonics)\n\n        d.update({'seed': encrypted_seed, 'seed_variant': 'slip39', 'seed_type': 'slip39', 'seed_extend': False})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('script_and_derivation', v.view)\n\n        d.update({\n            'script_type': 'p2wpkh', 'derivation_path': 'm/84h/0h/0h',\n            'multisig_master_pubkey': 'zpub6riQosasrLdM1rmmohyUHtseLYeCBKP55Xe1LTT7jyKFM6dMMZPYVx5ug6zH2gZ6XFGcUYubjbm43vXHecTzNmoMS3yfp6oeZT3GetsGFt4'})\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1q40ksvkl7wvc2l999ppl48swgt3rsl45ykyyrjn\")\n\n    async def test_create_standard_wallet_haveseed_slip39_passphrase(self):\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n\n        # SLIP39 shares (128 bits, 2 groups from 1 of 1, 1 of 1, 3 of 5, 2 of 6)\n        mnemonics = [\n            \"fact else acrobat romp analysis usher havoc vitamins analysis garden prevent romantic silent dramatic adjust priority mailman plains vintage else\",\n            \"fact else ceramic round craft lips snake faint adorn square bucket deadline violence guitar greatest academic stadium snake frequent memory\",\n            \"fact else ceramic scatter counter remove club forbid busy cause taxi forecast prayer uncover living type training forward software pumps\",\n            \"fact else ceramic shaft clock crowd detect cleanup wildlife depict include trip profile isolate express category wealthy advance garden mixture\",\n        ]\n        encrypted_seed = slip39.recover_ems(mnemonics)\n\n        d.update({'seed': encrypted_seed, 'seed_variant': 'slip39', 'seed_type': 'slip39', 'seed_extend': True})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_ext', v.view)\n\n        d.update({'seed_extra_words': 'TREZOR'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('script_and_derivation', v.view)\n\n        d.update({\n            'script_type': 'p2wpkh', 'derivation_path': 'm/84h/0h/0h',\n            'multisig_master_pubkey': 'zpub6s6A9ynh7TT1sPXmQyu8S6g7kxMF6iSZkM3NmgF4w7CtpsGgg56aouYSWHgAoMy186a8FRT8zkmhcwV5SWKFFQfMpvV8C9Ft4woWSzD5sXz'})\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qs2svwhfz47qv9qju2waa6prxzv5f522fc4p06t\")\n\n    async def test_2fa_createseed(self):\n        self.assertTrue(self.config.get('enable_plugin_trustedcoin'))\n        w = self._wizard_for(wallet_type='2fa')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('trustedcoin_start', v.view)\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_choose_seed', v.view)\n        d.update({'keystore_type': 'createseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_create_seed', v.view)\n        d.update({\n            'seed': 'oblige basket safe educate whale bacon celery demand novel slice various awkward',\n            'seed_type': '2fa', 'seed_extend': False, 'seed_variant': 'electrum',\n        })\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_confirm_seed', v.view)\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_tos', v.view)\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_show_confirm_otp', v.view)\n        v = w.resolve_next(v.view, d)\n        wallet = self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qnf5qafvpx0afk47433j3tt30pqkxp5wa263m77wt0pvyqq67rmfs522m94\")\n\n        with self.subTest(msg=\"2fa wallet cannot enable/disable keystore\"):\n            for ks in wallet.get_keystores():\n                self.assertFalse(wallet.can_enable_disable_keystore(ks))\n                with self.assertRaises(Exception) as ctx:\n                    wallet.enable_keystore(ks, False, None)\n                self.assertTrue(\"2fa wallet cannot\" in ctx.exception.args[0])\n                with self.assertRaises(Exception) as ctx:\n                    wallet.enable_keystore(ks, False, None)\n                self.assertTrue(\"2fa wallet cannot\" in ctx.exception.args[0])\n\n    async def test_2fa_haveseed_keep2FAenabled(self):\n        self.assertTrue(self.config.get('enable_plugin_trustedcoin'))\n        w = self._wizard_for(wallet_type='2fa')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('trustedcoin_start', v.view)\n\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_choose_seed', v.view)\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_have_seed', v.view)\n        d.update({\n            'seed': 'oblige basket safe educate whale bacon celery demand novel slice various awkward',\n            'seed_type': '2fa', 'seed_extend': False, 'seed_variant': 'electrum',\n        })\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_keep_disable', v.view)\n        d.update({'trustedcoin_keepordisable': 'keep'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_tos', v.view)\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_show_confirm_otp', v.view)\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qnf5qafvpx0afk47433j3tt30pqkxp5wa263m77wt0pvyqq67rmfs522m94\")\n\n    async def test_2fa_haveseed_disable2FA(self):\n        self.assertTrue(self.config.get('enable_plugin_trustedcoin'))\n        w = self._wizard_for(wallet_type='2fa')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('trustedcoin_start', v.view)\n\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_choose_seed', v.view)\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_have_seed', v.view)\n        d.update({\n            'seed': 'oblige basket safe educate whale bacon celery demand novel slice various awkward',\n            'seed_type': '2fa', 'seed_extend': False, 'seed_variant': 'electrum',\n        })\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_keep_disable', v.view)\n        d.update({'trustedcoin_keepordisable': 'disable'})\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qnf5qafvpx0afk47433j3tt30pqkxp5wa263m77wt0pvyqq67rmfs522m94\")\n\n    async def test_2fa_haveseed_passphrase(self):\n        self.assertTrue(self.config.get('enable_plugin_trustedcoin'))\n        w = self._wizard_for(wallet_type='2fa')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('trustedcoin_start', v.view)\n\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_choose_seed', v.view)\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_have_seed', v.view)\n        d.update({\n            'seed': 'oblige basket safe educate whale bacon celery demand novel slice various awkward',\n            'seed_type': '2fa', 'seed_extend': True, 'seed_variant': 'electrum',\n        })\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_have_ext', v.view)\n        d.update({'seed_extra_words': UNICODE_HORROR})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_keep_disable', v.view)\n        d.update({'trustedcoin_keepordisable': 'keep'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_tos', v.view)\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trustedcoin_show_confirm_otp', v.view)\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qcnu9ay4v3w0tawuxe6wlh6mh33rrpauqnufdgkxx7we8vpx3e6wqa25qud\")\n\n    async def test_create_standard_wallet_trezor(self):\n        # bip39 seed for trezor: \"history six okay anchor sheriff flock atom tomorrow foster aerobic eternal foam\"\n        w = self._wizard_for(wallet_type='standard')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'hardware'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('choose_hardware_device', v.view)\n\n        d.update({\n            'hardware_device': (\n                'trezor',\n                DeviceInfo(\n                    device=Device(path='webusb:002:1', interface_number=-1, id_='webusb:002:1', product_key='Trezor', usage_page=0, transport_ui_string='webusb:002:1'),\n                    label='trezor_unittests', initialized=True, exception=None, plugin_name='trezor', soft_device_id='088C3F260B66F60E15DE0FA5', model_name='Trezor T'))})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trezor_start', v.view)\n\n        d.update({'script_type': 'p2wpkh', 'derivation_path': 'm/84h/0h/0h'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trezor_xpub', v.view)\n\n        d.update({\n            'hw_type': 'trezor', 'master_key': 'zpub6qqp9XwsVMsovwzayXhFDJTpoc8VFoNy6mjkJHygou9NPRPDNR7MXVp9DM7qpacWwoePFWg7Gt5L5xnKNLmZYH8AFoTm2AAZA7LasycHu3n',\n            'root_fingerprint': '6306ee35', 'label': 'trezor_unittests', 'soft_device_id': '088C3F260B66F60E15DE0FA5',\n            'multisig_master_pubkey': 'zpub6qqp9XwsVMsovwzayXhFDJTpoc8VFoNy6mjkJHygou9NPRPDNR7MXVp9DM7qpacWwoePFWg7Gt5L5xnKNLmZYH8AFoTm2AAZA7LasycHu3n'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('wallet_password_hardware', v.view)\n\n        d.update({'password': '03a580deb85ef85654ed177fc049867ce915a8b392a34a524123870925e48a5b9e', 'encrypt': True, 'xpub_encrypt': True})\n        self.assertTrue(w.is_last_view(v.view, d))\n        v = w.resolve_next(v.view, d)\n\n        wallet_path = os.path.join(w._daemon.config.get_datadir_wallet_path(), d['wallet_name'])\n        w.create_storage(wallet_path, d)\n\n        self.assertTrue(os.path.exists(wallet_path))\n        wallet = Daemon._load_wallet(wallet_path, password=d['password'], config=self.config)\n        self.assertEqual(\"bc1q7ltf4aq95rj695fu5aaa5mx5m9p55xyr2fy6y0\", wallet.get_receiving_addresses()[0])\n        self.assertTrue(wallet.has_password())\n        self.assertTrue(wallet.has_storage_encryption())\n\n    async def test_unlock_hw_trezor(self):\n        # bip39 seed for trezor: \"history six okay anchor sheriff flock atom tomorrow foster aerobic eternal foam\"\n        w = NewWalletWizard(DaemonMock(self.config), self.plugins)\n        v = w.start()\n        self.assertEqual('wallet_name', v.view)\n        d = {\n            'wallet_name': 'mywallet',\n            'wallet_exists': True, 'wallet_is_open': False, 'wallet_needs_hw_unlock': True,}\n        self.assertFalse(w.is_last_view(v.view, d))\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('hw_unlock', v.view)\n\n        d.update({\n            'hardware_device': (\n                'trezor',\n                DeviceInfo(\n                    device=Device(path='webusb:002:1', interface_number=-1, id_='webusb:002:1', product_key='Trezor', usage_page=0, transport_ui_string='webusb:002:1'),\n                    label='trezor_unittests', initialized=True, exception=None, plugin_name='trezor', soft_device_id='088C3F260B66F60E15DE0FA5', model_name='Trezor T'))})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trezor_unlock', v.view)\n\n        d.update({'password': '03a580deb85ef85654ed177fc049867ce915a8b392a34a524123870925e48a5b9e'})\n        self.assertTrue(w.is_last_view(v.view, d))\n        v = w.resolve_next(v.view, d)\n\n    async def test_create_multisig_wallet_2of2_createseed_cosigner2hasmasterkey(self):\n        w = self._wizard_for(wallet_type='multisig')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('multisig', v.view)\n\n        d.update({'multisig_participants': 2, 'multisig_signatures': 2, 'multisig_cosigner_data': {}})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'createseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('create_seed', v.view)\n\n        d.update({\n            'seed': 'eager divert pigeon dentist punch festival manage smart globe regular adult cash',\n            'seed_type': 'segwit', 'seed_extend': False, 'seed_variant': 'electrum'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('confirm_seed', v.view)\n\n        d.update({'multisig_master_pubkey': 'Zpub6y7YR1dmZZV4f5rRm6dJCKSqqxZhKUxc8PkssXm84k2bzbGYkL22ugC4aZxVxC1qz4yo53Zwz1c1kiSHmybB4JjCsjCPjzygSsN1UcdCcvB'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_keystore', v.view)\n\n        # 2nd cosigner uses Zpub from \"9dk\" seed\n        d['multisig_cosigner_data']['2'] = {'keystore_type': 'masterkey'}\n        d.update({\n            'multisig_current_cosigner': 2, 'cosigner_keystore_type': 'masterkey'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_key', v.view)\n\n        d['multisig_cosigner_data']['2'].update({'master_key': 'Zpub6y4evsU8HJw2d7ZH8QNyC6UKWHyxinAuQKkD6btsEZMbamy96UnefnM4sZp2K38rdiUssEhNq9TBpJ8Bh1GZCGTFpnYz8jM9pAdS6vk5VQs'})\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qg39tkymxwq4tn2ly6c3lmnyvsy94jyw52rdvfqkzdv2slvlj9xcsfy63vc\")\n\n    async def test_create_multisig_wallet_3of6_haveseed_passphrase__cs2hasbip39__cs3zpub__cs4trezor__cs5seedandpassphrase__cs6zprv(self):\n        w = self._wizard_for(wallet_type='multisig')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('multisig', v.view)\n\n        d.update({'multisig_participants': 6, 'multisig_signatures': 3, 'multisig_cosigner_data': {}})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('keystore_type', v.view)\n\n        d.update({'keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_seed', v.view)\n\n        d.update({\n            'seed': '9dk',\n            'seed_variant': 'electrum', 'seed_type': 'segwit', 'seed_extend': True})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('have_ext', v.view)\n\n        d.update({\n            'seed_extra_words': UNICODE_HORROR,\n            'multisig_master_pubkey': 'Zpub6zAYrXzLbLwWFCkahiB3fQz4KMUm68RsoGVHkM5aBjzHBGnQ9orvy7PKuFvMj4gyJXhFW5uFzHBgDDYFEPS75b3ADq3yvtuEJF86ZgLLyeL'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_keystore', v.view)\n\n        # 2nd cosigner\n        d['multisig_cosigner_data']['2'] = {'keystore_type': 'haveseed'}\n        d.update({\n            'multisig_current_cosigner': 2, 'cosigner_keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_seed', v.view)\n\n        d['multisig_cosigner_data']['2'].update({\n            'seed': 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',\n            'seed_variant': 'bip39', 'seed_type': 'bip39', 'seed_extend': False})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_script_and_derivation', v.view)\n\n        d['multisig_cosigner_data']['2'].update({\n            'script_type': 'p2wsh', 'derivation_path': 'm/48h/0h/0h/2h'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_keystore', v.view)\n\n        # 3rd cosigner uses Zpub from \"9dk\" seed\n        d['multisig_cosigner_data']['3'] = {'keystore_type': 'masterkey'}\n        d.update({\n            'multisig_current_cosigner': 3, 'cosigner_keystore_type': 'masterkey'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_key', v.view)\n\n        d['multisig_cosigner_data']['3'].update({\n            'master_key': 'Zpub6y4evsU8HJw2d7ZH8QNyC6UKWHyxinAuQKkD6btsEZMbamy96UnefnM4sZp2K38rdiUssEhNq9TBpJ8Bh1GZCGTFpnYz8jM9pAdS6vk5VQs'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_keystore', v.view)\n\n        # 4th cosigner\n        d['multisig_cosigner_data']['4'] = {'keystore_type': 'hardware'}\n        d.update({\n            'multisig_current_cosigner': 4, 'cosigner_keystore_type': 'hardware'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_hardware', v.view)\n\n        d['multisig_cosigner_data']['4'].update({\n            'hardware_device': (\n                'trezor',\n                DeviceInfo(\n                    device=Device(path='webusb:002:1', interface_number=-1, id_='webusb:002:1', product_key='Trezor', usage_page=0, transport_ui_string='webusb:002:1'),\n                    label='trezor_unittests', initialized=True, exception=None, plugin_name='trezor', soft_device_id='088C3F260B66F60E15DE0FA5', model_name='Trezor T'))})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trezor_start', v.view)\n\n        d['multisig_cosigner_data']['4'].update({\n            'script_type': 'p2wsh', 'derivation_path': 'm/48h/0h/0h/2h'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('trezor_xpub', v.view)\n\n        d['multisig_cosigner_data']['4'].update({\n            'hw_type': 'trezor', 'master_key': 'Zpub75t8XsK4GVa2EyQtjvT9auayKwonGaQJ149qB9r11o5iikugxJ99hYgbcaTdCGjd4DUdz4z2bqAtmDv2s8UihG1AnbzBufSG82GxjMDfVUn',\n            'root_fingerprint': '6306ee35', 'label': 'trezor_unittests', 'soft_device_id': '088C3F260B66F60E15DE0FA5'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_keystore', v.view)\n\n        # 5th cosigner\n        d['multisig_cosigner_data']['5'] = {'keystore_type': 'haveseed'}\n        d.update({\n            'multisig_current_cosigner': 5, 'cosigner_keystore_type': 'haveseed'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_seed', v.view)\n\n        d['multisig_cosigner_data']['5'].update({\n            'seed': 'abandon bike',\n            'seed_variant': 'electrum', 'seed_type': 'segwit', 'seed_extend': True})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_have_ext', v.view)\n\n        d['multisig_cosigner_data']['5'].update({\n            'seed_extra_words': UNICODE_HORROR})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_keystore', v.view)\n\n        # 6th cosigner uses Zprv from \"abandon bike\" seed\n        d['multisig_cosigner_data']['6'] = {'keystore_type': 'masterkey'}\n        d.update({\n            'multisig_current_cosigner': 6, 'cosigner_keystore_type': 'masterkey'})\n        v = w.resolve_next(v.view, d)\n        self.assertEqual('multisig_cosigner_key', v.view)\n\n        d['multisig_cosigner_data']['6'].update({\n            'master_key': 'ZprvAjWENdvYc1Ctvppxm4Z67U4EoiDy5VXKNvWmVAZshy7UjgKggu1UcAH7MqRqTaHVunuEPZ7o51wCrsZnJXPJtzHnAoxNmMLWFMHC7uvUN5P'})\n        v = w.resolve_next(v.view, d)\n        self._set_password_and_check_address(v=v, w=w, recv_addr=\"bc1qtuzp7rectyjquax5c3p80eletswhp6cxslye749l47h4m9x92hzs6cmymy\")\n\n    async def test_create_imported_wallet_from_addresses(self):\n        w = self._wizard_for(wallet_type='imported')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('imported', v.view)\n\n        d.update({\n            'address_list':\n                '14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG\\n'\n                '35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT\\n'\n                'BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4\\n'\n                'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y\\n'})\n        v = w.resolve_next(v.view, d)\n        wallet = self._set_password_and_check_address(v=v, w=w, recv_addr=None)\n        self.assertEqual(\n            set(wallet.get_receiving_addresses()),\n            {\n                \"14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG\",\n                \"35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT\",\n                \"BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4\",  # TODO normalize to lowercase?\n                \"bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y\",\n            },\n        )\n\n    async def test_create_imported_wallet_from_addresses__invalid_input(self):\n        w = self._wizard_for(wallet_type='imported')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('imported', v.view)\n\n        d.update({\n            'address_list':\n                'garbagegarbage\\n'\n                '35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT\\n'\n        })\n        v = w.resolve_next(v.view, d)\n        with self.assertRaises(AssertionError) as ctx:\n            wallet = self._set_password_and_check_address(v=v, w=w, recv_addr=None)\n        self.assertTrue(\"expected bitcoin addr\" in ctx.exception.args[0])\n\n    async def test_create_imported_wallet_from_wif_keys(self):\n        w = self._wizard_for(wallet_type='imported')\n        v = w._current\n        d = v.wizard_data\n        self.assertEqual('imported', v.view)\n\n        d.update({\n            'private_key_list':\n                'p2wpkh:L1cgMEnShp73r9iCukoPE3MogLeueNYRD9JVsfT1zVHyPBR3KqBY\\n'\n                'p2pkh:KyQ2voUQj71P6E9KyDFqQoYMMm3yKKAPMKbfqZccib6xWxbWHCex\\n'\n                'p2pkh:5JuecQZ1nH4VCQRQJTQjB4yu93BU6NmnAkDoGRdHX2PyH2E8QVX\\n'})\n        v = w.resolve_next(v.view, d)\n        wallet = self._set_password_and_check_address(v=v, w=w, recv_addr=None)\n        self.assertEqual(\n            set(wallet.get_receiving_addresses()),\n            {\"bc1qq2tmmcngng78nllq2pvrkchcdukemtj56uyue0\", \"1LNvv5h6QHoYv1nJcqrp13T2TBkD2sUGn1\", \"1FJEEB8ihPMbzs2SkLmr37dHyRFzakqUmo\"},\n        )\n        self.assertFalse(wallet.can_enable_disable_keystore(wallet.keystore))\n"
  },
  {
    "path": "tests/test_x509.py",
    "content": "import unittest\n\nfrom electrum.x509 import X509\n\nfrom . import ElectrumTestCase\n\n\nclass TestX509(ElectrumTestCase):\n    def test_generalizedtime(self):\n        full = X509(b'0\\x82\\x05F0\\x82\\x03.\\x02\\t\\x00\\xfeV\\xd6\\xb5?\\xb1j\\xe40\\r\\x06\\t*\\x86H\\x86\\xf7\\r\\x01\\x01\\x0b\\x05\\x000d1\\x0b0\\t\\x06\\x03U\\x04\\x06\\x13\\x02US1\\x130\\x11\\x06\\x03U\\x04\\x08\\x0c\\nCalifornia1!0\\x1f\\x06\\x03U\\x04\\n\\x0c\\x18Internet Widgits Pty Ltd1\\x1d0\\x1b\\x06\\x03U\\x04\\x03\\x0c\\x14testnet.qtornado.com0 \\x17\\r180206010225Z\\x18\\x0f21180113010225Z0d1\\x0b0\\t\\x06\\x03U\\x04\\x06\\x13\\x02US1\\x130\\x11\\x06\\x03U\\x04\\x08\\x0c\\nCalifornia1!0\\x1f\\x06\\x03U\\x04\\n\\x0c\\x18Internet Widgits Pty Ltd1\\x1d0\\x1b\\x06\\x03U\\x04\\x03\\x0c\\x14testnet.qtornado.com0\\x82\\x02\"0\\r\\x06\\t*\\x86H\\x86\\xf7\\r\\x01\\x01\\x01\\x05\\x00\\x03\\x82\\x02\\x0f\\x000\\x82\\x02\\n\\x02\\x82\\x02\\x01\\x00\\xc2B\\xe0\\xa8\\xd9$M\\xbc)Wx\\x0cv\\x00\\xc0\\xfa2Ew:\\xce\\xa7\\xcb\\xc8\\r?\\xea\\xc5R(\\xc7\\xc3Y\\xe7zq=\\xcd\\x8d\\xe3\\x86\\x9ecSI\\xc7\\x84\\xf2~\\x91\\xd4\\x19\\xc2;\\x97\\xe81e\\xf2\\xeb\\xf1\\xadw\\xa3p\\x88A*-\\r\\xb6Yt\\x98R\\xe8\\x8a\\xf9\\xb5>\"F\\xac\\x19%\\xc8~\\x1d\\xac\\x93A\\xffk\\xce\\xdb\\xfc9\\x05\\xa0\\xad\\xf9V\\x0f0\\xa2b\\xd0@\\xe4\\xf1\\xb1\\xe8\\xb1\\x10[&\\xa1\\xff\\x13\\xcfQ\\xb7\\x805\\xef\\xe7tL\\xe5|\\x08W\\x8c\\xd72\\x9d\\'\\xeb\\x92)3N\\x01M\\x06\\xa9\\xdc\\xe4\\'\\x13\\x90x\\xd8\\x830\\x97\\xa8\\xcc2d \\xfa\\x91\\x04\\xd0\\x1b\\xe7\\xaa t\\x87\\xba]\\xb5w\\x05(\\xba\\x07\\xc2X$~?L\\xc5\\x03\\xb2\\xdeQ\\xf3\\xf3\\xdab\\xd9\\x92\\xd9\\x86^:\\x93\\xc9\\x86~\\xd1\\x94\\xd4\\x80\\x9c\\xff0\\xc6m\\xf4\\xf0\\xd6\\x18\\x96l\\x1d\\x0c\\xe8\\x15 \\x8c\\x89\\xcb\\xa4*\\xd9\\xefg\\x844\\x81\\xb3\\xce\\xa1\\x8a|\\xf9h\\xc3\\xe1!\\xfeZ`\\xb71\\x97Kj\\x0b\"\\xd3\\x98T\\r\\xd9\\xbb<r\\x0c\\xd5Q\\xd0L\\x02\\xcb\\x19\\x19\\xd6\\xdf$\\xcej\\xa8l\\xbd\\x81\\x803\\x95\\x0e\\x907&\\x81J\\x88\\xaf\\xa23\\xb4q\\x96\\x08\\xa9]}\\xb8Rs\\x89{\\x04\\x88/\\xc1m\\x8c\\xe8\\\\X\\x95 \\x1cj\\xf2(t\\xd7\\xef\\x10-r\\xb6\\x17L\\xce_\\x1bf\\xc0c\\x18\\x83\\x99\\xdf\\xd5\\xad\\x88\\xcd \\xae\\x07 \\xed\\xb6\\xfc[\\x9a/f\\x92\\xce^\\x9c\\xd9\\x064\\xb4\\xcc\\x1d,d\\x99\\xee\\x9a4\\xbe\\xde0\\x92\\x8f/keq\\x94\\x9frf1\\xda\\xadM_\\x11C\\x19\\x01\\xf0\\xe0I\\x84W\\xf9\\xaa\\xd3\\x12ex\\x89\"\\xbfQ\\x1f\\xbdU\\xa0\\x92\\xa3\\x9d\\xdb?\\x86\\x82\\x0b\\x1e\\xe0\\x8aSq\\xce%\\xea4\\xfb\\x82\\x92\\x0f\\xcf\\xaa\\xe2\\r\\xedd\\xba\\xff\\x85\\xa2+\\xb0x9\\xba\\'\\xd3\\xf5\\xd6\\xfa\\xb43\\x0b\\xd4\\xf4\\xca\\xa5\\xb1\\xe4[\\xe7\\xf7\\xc3\\xd3\\xdd\\x85)\\xac5E\\x17\\xae\\x03fCC(\\x06\\x1cU\\xedM\\x90r\\xe87\\x8d}\\xf1i\\xfdO\\x83\\x05\\x83\\x83y\\xd9f,\\xe1\\xba\\xf0\\\\y\\x8d\\x08`\\xb1\\x02\\x03\\x01\\x00\\x010\\r\\x06\\t*\\x86H\\x86\\xf7\\r\\x01\\x01\\x0b\\x05\\x00\\x03\\x82\\x02\\x01\\x00,.\\x12jC3\\x9fdF\\x15\\x16\\xea*1\\x0b[\\xfa-\\xcf\\x80\\x17\\xf0\\xfa\\xf4\\x96C\\xff\\xf9\\xe9\\xa2N\\xda\\xf1&6\\x9ecV~\\xea[\\x07\\xc1R\\x03\\x95\\xd4\\x84B\\xe2r\\x92\\xad<mp\\xf1\\xcb\\xb3\\x8b\\xbf \\x08\\x12\\x1e6\\xe3\\xad\\xbd1\\x81\\xbe\\xaex\\x002\\xb6\\xf9\\xa0\\xf6\\xb7E^\"\\r\\xa0w\\x08\\x14\\xe7\\x84\\x03q2\\x9c\\xac\\xce>\\xc6\\x0b\\x81\\x81k\\x0e\\xd01\\x16\\x91\\xe4A\\x8c\\x1a\\xe9W\\xd4=<\\xd4m_\\xd4m\\xa4H\\x14\\xc0\\xae\\x12\\xab\\x808\\xf1\\xf9_\\xbb\\xfb\\xd0U\\x0e\\\\\\xd3.?\\xa36\\xe1hstU\"\\x17P\\xcb>\\x83\\x9c\\xaa\\x9b\\xb7\\xe5\\xb4\\xb5W\\xdc\\xc1\\xee\\x91K\\x12\\xc2\\xe1U\\xaf\\xf7I`\\x83\\x91\\x0c\\xc0\\xcb\\x15\\x13!V\\xa9\\xc1\\xca\\x1b\\x80\\xff\\xd8\\x1f\\xd8_+\\x83\\xcd\\xcb%\\xd6\\xb7\\xdc\\x8a2\\xa8Q\\x1f\\xbb.\\xdf\\x05\\xb7hD\\xab\\xea\\xe9\\xfb.\\xdd\\x93\\xd1\\xf0\\xb8r\\xb9t.\\xab\\xf6]\\xac\\xc9U9\\x87\\x9e\\xe36 \\x87\\xe7eo\\x98\\xac\\xf4\\x87\\x8e\\xf4\\xa86\\xd3\\xcapy\\xee\\xa0]\\xdbA\\xb9\\x00\\xe9_R\\xc8\\xf7\\xca\\x13\\xc6\\xb1Z|c\\xe8v\\xa24\\xac?k\\xf1\\xc4\\x97\\x18\\x07\\xbaU\\xc9\\xf5? \\x95\\x8f\\x11\\xa7\\xc9\\x8eY\\x9c\\xdfnx?\\x88\\xba\\x90\\xef\\x94WU\\xb5\\xcf\\x0b\"\\xe8\\xfe\\xa6.\\x0cr-\\xaf3\\x8a\\xe6v\\xf9\\xb91\\x87\\x91\\xc6\\xb1\\xe9\\xb9UP\\xf5\\x14\\xb7\\x99\\x80\\xc0\\xc5}\\x9a~\\x7f\\x06\\x1e\\xb8\\x05\\xd5\\xa2LXO\\\\73i\\x82\\xcd\\xc6#\\xb7\\xa4q\\xd7\\xd4y\\xb1d\\xaf\\xa8\\t\\x9e1K\\xd94\\xaf7\\x08\\x8c);\\xd2\\xed\\x91\\xc6\\xed\\x83\\x90\\r\\xef\\x85\\xf0\\xfeJi\\x02;\\xf0\\x0b\\x03\\xe7\\xc1\\x84\\xd45\\xaeP\\xc2Lp\\x1akb\\xcaP\\xe9\\xfc\\xc1\\xc8VPQu\\x85\\x92l\\x12\\xb99{\\x91\\xd0\\xa6d\\n\\xde\\xf85\\x93e\\xfa\\\\\\xf9cKx8\\x84\"s\\xb8\\xe52~\\x97\\x05\\xc3\\xf6\\x1c\\xca\\x0b\\xda\\x8b\\x90\\xfeu5,\\x94,\\x99\\xf9\\x9a\\xf3T\\x8dAZ\\xc7\\xe9\\x95-\\x98\\xf2\\xbaL\\x89\\xc0?\\xba1\\xb5\\\\t|RY_\\xc6\\xabr\\xe8')\n        full.check_date()\n"
  }
]